Skip to content

Commit

Permalink
explorerhq#480 Disable chart rendering unless `EXPLORER_CHARTS_ENABLE…
Browse files Browse the repository at this point in the history
…D` is set to `True`
  • Loading branch information
eeriksp committed Jul 27, 2022
1 parent be328a0 commit 8ab998b
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 96 deletions.
18 changes: 17 additions & 1 deletion docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@ Pivot Table
- Hit the link icon on the top right to get a URL to recreate the
exact pivot setup to share with colleagues.

Displaying query results as charts
----------------------------------

If the results table adheres to a certain format, the results can be displayed as a pie chart or a line chart.

To enable this feature, set `` `` setting to ``True`` and install the plotting libraries ``matplotlib`` and ``seaborn`` with

.. code-block:: console
pip install matplotlib
pip install seaborn
This will add the "Pie chart" and the "Line chart" tabs alongside the "Preview" and the "Pivot" tabs in the query results view.

The tabs show the respective charts if the query result table adheres to a format which the chart widget can read. Otherwise a message explaining the required format together with an example query is displayed.

Query Logs
----------
- Explorer will save a snapshot of every query you execute so you
Expand Down Expand Up @@ -196,4 +212,4 @@ You can also pass the token with a query parameter like this:

.. code-block:: console
curl https://www.your-site.com/explorer/<QUERY_ID>/stream?format=csv&token=<TOKEN>
curl https://www.your-site.com/explorer/<QUERY_ID>/stream?format=csv&token=<TOKEN>
2 changes: 2 additions & 0 deletions explorer/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,5 @@
)

UNSAFE_RENDERING = getattr(settings, "EXPLORER_UNSAFE_RENDERING", False)

EXPLORER_CHARTS_ENABLED = getattr(settings, "EXPLORER_CHARTS_ENABLED", False)
53 changes: 47 additions & 6 deletions explorer/chart.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
from typing import Optional, Iterable
from io import BytesIO

import matplotlib.pyplot as plt
from matplotlib.figure import Figure
import seaborn as sns
from django.core.exceptions import ImproperlyConfigured

try:
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
import seaborn as sns
except ImportError:
from . import app_settings

if app_settings.EXPLORER_CHARTS_ENABLED:
raise ImproperlyConfigured(
"If `EXPLORER_CHARTS_ENABLED` is enabled, `matplotlib` and `seaborn` must be installed.")

from .models import QueryResult


def get_pie_chart(result: QueryResult) -> Optional[str]:
"""
Return a pie chart in SVG format if the result table adheres to the expected format.
A pie chart is rendered if
* there is at least on row of in the result table
* the result table has at least two columns
* the second column is of a numeric type
The first column is used as labels for the pie sectors.
The second column is used to determine the size of the sectors
(hence the requirement of it being numeric).
All other columns are ignored.
"""
if len(result.data) < 1 or len(result.data[0]) < 2:
return None
not_none_rows = [row for row in result.data if row[0] is not None and row[1] is not None]
Expand All @@ -18,10 +40,22 @@ def get_pie_chart(result: QueryResult) -> Optional[str]:
return None
fig, ax = plt.subplots(figsize=(4.5, 4.5))
ax.pie(values, labels=labels)
return get_csv_as_hex(fig)
return get_svg(fig)


def get_line_chart(result: QueryResult) -> Optional[str]:
"""
Return a line chart in SVG format if the result table adheres to the expected format.
A line chart is rendered if
* there is at least on row of in the result table
* there is at least one numeric column (the first colum (with index 0) does not count)
The first column is used as x-axis labels.
All other numeric columns represent a line on the chart.
The name of the column is used as the name of the line in the legend.
Not numeric columns (except the first on) are ignored.
"""
if len(result.data) < 1:
return None
numeric_columns = [c for c in range(1, len(result.data[0]))
Expand All @@ -36,10 +70,14 @@ def get_line_chart(result: QueryResult) -> Optional[str]:
y=[row[col_num] for row in result.data],
label=result.headers[col_num])
ax.set_xlabel(result.headers[0])
return get_csv_as_hex(fig)
# Rotate x-axis labels by 20 degrees to reduce overlap
for label in ax.get_xticklabels():
label.set_rotation(20)
label.set_ha('right')
return get_svg(fig)


def get_csv_as_hex(fig: Figure) -> str:
def get_svg(fig: 'Figure') -> str:
buffer = BytesIO()
fig.savefig(buffer, format='svg')
buffer.seek(0)
Expand All @@ -49,4 +87,7 @@ def get_csv_as_hex(fig: Figure) -> str:


def is_numeric(column: Iterable) -> bool:
"""
Indicate if all the values in the given column are numeric or None.
"""
return all([isinstance(value, (int, float)) or value is None for value in column])
177 changes: 90 additions & 87 deletions explorer/templates/explorer/preview_pane.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@
{% trans "Pivot" %}
</a>
</li>
<li role="presentation">
<a id="pie-chart-tab-label" href="#pie-chart" aria-controls="pie-chart" role="tab" data-toggle="tab">
{% trans "Pie chart" %}
</a>
</li>
<li role="presentation">
<a id="line-chart-tab-label" href="#line-chart" aria-controls="line-chart" role="tab" data-toggle="tab">
{% trans "Line chart" %}
</a>
</li>

{% if charts_enabled %}
<li role="presentation">
<a id="pie-chart-tab-label" href="#pie-chart" aria-controls="pie-chart" role="tab" data-toggle="tab">
{% trans "Pie chart" %}
</a>
</li>
<li role="presentation">
<a id="line-chart-tab-label" href="#line-chart" aria-controls="line-chart" role="tab" data-toggle="tab">
{% trans "Line chart" %}
</a>
</li>
{% endif %}
{% endif %}
</ul>
<div class="tab-content">
Expand Down Expand Up @@ -163,90 +164,92 @@
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="pie-chart">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-6">
<span class="panel-title">{% trans "Pie chart" %}</span>
{% if charts_enabled %}
<div role="tabpanel" class="tab-pane" id="pie-chart">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-6">
<span class="panel-title">{% trans "Pie chart" %}</span>
</div>
</div>
</div>
</div>
<div class="overflow-wrapper">
{% if pie_chart_svg %}
<div style="margin: 2em;">
{{ pie_chart_svg | safe }}
</div>
{% else %}
<div style="margin: 6em;">
<p>{% blocktrans %}
This query result table is not formatted in a way
which could be displayed as a pie chart.
{% endblocktrans %}</p>
<p>{% blocktrans %}
Query results can be displayed as a pie chart as follows:
each row represents one sector of the pie;
the first column will be used as a label
while the second column is used to determine the size of the sector.
Thus the second column must be of a numeric type.
Rows which contain <code>NULL</code>s will be ignored.
{% endblocktrans %}</p>
<p>{% blocktrans %}
Use this sample query to see it in action:
{% endblocktrans %}</p>
<pre>
SELECT *
FROM (VALUES ('apple', 7),
('orange', 8),
('grape', 9),
('peppermint', 1))
AS fruit_salad_proportions;</pre>
</div>
{% endif %}
<div class="overflow-wrapper">
{% if pie_chart_svg %}
<div style="margin: 2em;">
{{ pie_chart_svg | safe }}
</div>
{% else %}
<div style="margin: 6em;">
<p>{% blocktrans %}
This query result table is not formatted in a way
which could be displayed as a pie chart.
{% endblocktrans %}</p>
<p>{% blocktrans %}
Query results can be displayed as a pie chart as follows:
each row represents one sector of the pie;
the first column will be used as a label
while the second column is used to determine the size of the sector.
Thus the second column must be of a numeric type.
Rows which contain <code>NULL</code>s will be ignored.
{% endblocktrans %}</p>
<p>{% blocktrans %}
Use this sample query to see it in action:
{% endblocktrans %}</p>
<pre>
SELECT *
FROM (VALUES ('apple', 7),
('orange', 8),
('grape', 9),
('peppermint', 1))
AS fruit_salad_proportions;</pre>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="line-chart">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-6">
<span class="panel-title">{% trans "Line chart" %}</span>
<div role="tabpanel" class="tab-pane" id="line-chart">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-6">
<span class="panel-title">{% trans "Line chart" %}</span>
</div>
</div>
</div>
</div>
<div class="overflow-wrapper">
{% if line_chart_svg %}
<div style="margin: 2em;">
{{ line_chart_svg | safe }}
</div>
{% else %}
<div style="margin: 6em;">
<p>{% blocktrans %}
This query result table is not formatted in a way
which could be displayed as a line chart.
{% endblocktrans %}</p>
<p>{% blocktrans %}
Query results can be displayed as a line chart as follows:
the first column represents the values on the x-axis (e.g. dates).
All other numeric columns represent one line on the chart.
Other columns will be ignored.
{% endblocktrans %}</p>
<p>{% blocktrans %}
Use this sample query to see it in action:
{% endblocktrans %}</p>
<pre>
SELECT *
FROM (VALUES ('2019-01-01'::date, 500,550,530),
('2020-01-01'::date, 530, 580, 570),
('2021-01-01'::date, 580, 590, 670),
('2022-01-01'::date, 700, 620, 780))
AS fruit_salad_proportions(date, generosity, joy, happiness);</pre>
</div>
{% endif %}
<div class="overflow-wrapper">
{% if line_chart_svg %}
<div style="margin: 2em;">
{{ line_chart_svg | safe }}
</div>
{% else %}
<div style="margin: 6em;">
<p>{% blocktrans %}
This query result table is not formatted in a way
which could be displayed as a line chart.
{% endblocktrans %}</p>
<p>{% blocktrans %}
Query results can be displayed as a line chart as follows:
the first column represents the values on the x-axis (e.g. dates).
All other numeric columns represent one line on the chart.
Other columns will be ignored.
{% endblocktrans %}</p>
<p>{% blocktrans %}
Use this sample query to see it in action:
{% endblocktrans %}</p>
<pre>
SELECT *
FROM (VALUES ('2019-01-01'::date, 500,550,530),
('2020-01-01'::date, 530, 580, 570),
('2021-01-01'::date, 580, 590, 670),
('2022-01-01'::date, 700, 620, 780))
AS fruit_salad_proportions(date, generosity, joy, happiness);</pre>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{% endif %}
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions explorer/views/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def query_viewmodel(request, query, title=None, form=None, message=None,
'ql_id': ql.id if ql else None,
'unsafe_rendering': app_settings.UNSAFE_RENDERING,
'fullscreen_params': fullscreen_params.urlencode(),
'pie_chart_svg': get_pie_chart(res) if has_valid_results else None,
'line_chart_svg': get_line_chart(res) if has_valid_results else None
'charts_enabled': app_settings.EXPLORER_CHARTS_ENABLED,
'pie_chart_svg': get_pie_chart(res) if app_settings.EXPLORER_CHARTS_ENABLED and has_valid_results else None,
'line_chart_svg': get_line_chart(res) if app_settings.EXPLORER_CHARTS_ENABLED and has_valid_results else None
}
return ret
2 changes: 2 additions & 0 deletions requirements/optional.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ boto>=2.49
django-celery>=3.3.1
xlsxwriter>=1.3.6
factory-boy>=3.1.0
matplotlib < 4
seaborn < 0.12

0 comments on commit 8ab998b

Please sign in to comment.