mirror of https://github.com/apache/superset.git
commit
81dbd142e1
|
@ -1,9 +1,27 @@
|
|||
import pandas
|
||||
from collections import defaultdict
|
||||
import copy
|
||||
import json
|
||||
from pandas.io.json import dumps
|
||||
|
||||
|
||||
class Highchart(object):
|
||||
class BaseHighchart(object):
|
||||
stockchart = False
|
||||
tooltip_formatter = ""
|
||||
target_div = 'chart'
|
||||
@property
|
||||
def javascript_cmd(self):
|
||||
js = dumps(self.chart)
|
||||
js = (
|
||||
js.replace('"{{TOOLTIP_FORMATTER}}"', self.tooltip_formatter)
|
||||
.replace("\n", " ")
|
||||
)
|
||||
if self.stockchart:
|
||||
return "new Highcharts.StockChart(%s);" % js
|
||||
return "new Highcharts.Chart(%s);" %js
|
||||
|
||||
|
||||
class Highchart(BaseHighchart):
|
||||
def __init__(
|
||||
self, df,
|
||||
chart_type="spline",
|
||||
|
@ -144,7 +162,8 @@ class Highchart(object):
|
|||
if df.index.dtype.kind in "M":
|
||||
x_axis["type"] = "datetime"
|
||||
if df.index.dtype.kind == 'O':
|
||||
x_axis['categories'] = sorted(list(df.index)) if self.sort_columns else list(df.index)
|
||||
x_axis['categories'] = sorted(
|
||||
list(df.index)) if self.sort_columns else list(df.index)
|
||||
print list(df.index)
|
||||
if self.grid:
|
||||
x_axis["gridLineWidth"] = 1
|
||||
|
@ -174,10 +193,38 @@ class Highchart(object):
|
|||
chart["yAxis"].append(yAxis2)
|
||||
|
||||
|
||||
@property
|
||||
def javascript_cmd(self):
|
||||
js = dumps(self.chart)
|
||||
js = js.replace('"{{TOOLTIP_FORMATTER}}"', self.tooltip_formatter).replace("\n", " ")
|
||||
if self.stockchart:
|
||||
return "new Highcharts.StockChart(%s);" % js
|
||||
return "new Highcharts.Chart(%s);" %js
|
||||
class HighchartBubble(BaseHighchart):
|
||||
def __init__(self, df, target_div='chart', height=800):
|
||||
self.df = df
|
||||
self.chart = {
|
||||
'chart': {
|
||||
'type': 'bubble',
|
||||
'zoomType': 'xy'
|
||||
},
|
||||
'title': {'text': None},
|
||||
'plotOptions': {
|
||||
'bubble': {
|
||||
'tooltip': {
|
||||
'headerFormat': '<b>{series.name}</b><br>',
|
||||
'pointFormat': '<b>{point.name}</b>: {point.x}, {point.y}, {point.z}'
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
chart = self.chart
|
||||
chart['series'] = self.series()
|
||||
chart['chart']['renderTo'] = target_div
|
||||
if height:
|
||||
chart['chart']["height"] = height
|
||||
|
||||
def series(self):
|
||||
#df = self.df[['name', 'x', 'y', 'z']]
|
||||
df = self.df
|
||||
series = defaultdict(list)
|
||||
for row in df.to_dict(orient='records'):
|
||||
series[row['group']].append(row)
|
||||
l = []
|
||||
for k, v in series.items():
|
||||
l.append({'data': v, 'name': k})
|
||||
print(json.dumps(l, indent=2))
|
||||
return l
|
||||
|
|
|
@ -39,12 +39,16 @@ form input.form-control {
|
|||
<hr>
|
||||
<form id="query" method="GET" style="display: none;">
|
||||
<div>{{ form.viz_type.label }}: {{ form.viz_type(class_="form-control select2") }}</div>
|
||||
<div>{{ form.metrics.label }}: {{ form.metrics(class_="form-control select2") }}</div>
|
||||
{% if 'metrics' not in viz.hidden_fields %}
|
||||
<div>{{ form.metrics.label }}: {{ form.metrics(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if 'granularity' not in viz.hidden_fields %}
|
||||
<div>{{ form.granularity.label }}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
||||
title="Supports natural language time as in '10 seconds', '1 day' or '1 week'"
|
||||
id="blah"></i>
|
||||
{{ form.granularity(class_="form-control select2_free_granularity") }}</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="col-xs-6">{{ form.since.label }}
|
||||
|
@ -56,7 +60,9 @@ form input.form-control {
|
|||
{{ form.until(class_="form-control select2_free_until") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>{{ form.groupby.label }}: {{ form.groupby(class_="form-control select2") }}</div>
|
||||
{% if 'groupby' not in viz.hidden_fields %}
|
||||
<div>{{ form.groupby.label }}: {{ form.groupby(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% block extra_fields %}{% endblock %}
|
||||
<hr>
|
||||
<h4>Filters</h4>
|
||||
|
@ -85,11 +91,19 @@ form input.form-control {
|
|||
|
||||
<div class="col-md-9">
|
||||
<h3>{{ viz.verbose_name }}
|
||||
<span class="label label-success">{{ "{0:0.2f}".format(results.duration.total_seconds()) }} s</span>
|
||||
<span class="label label-info btn" data-toggle="modal" data-target="#query_modal">query</span>
|
||||
{% if results %}
|
||||
<span class="label label-success">
|
||||
{{ "{0:0.2f}".format(results.duration.total_seconds()) }} s
|
||||
</span>
|
||||
<span class="label label-info btn"
|
||||
data-toggle="modal" data-target="#query_modal">query</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
<hr/>
|
||||
{% block viz %}
|
||||
{% if error_msg %}
|
||||
<span class="alert alert-danger">{{ error_msg }}</span>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% if debug %}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% extends "panoramix/datasource.html" %}
|
||||
{% block viz %}
|
||||
{{ super() }}
|
||||
<div id="chart"></div>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -14,6 +15,21 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
<div>{{ form.limit.label }}: {{ form.limit(class_="form-control select2") }}</div>
|
||||
{% if form.series %}
|
||||
<div>{{ form.series.label }}: {{ form.series(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if form.entity %}
|
||||
<div>{{ form.entity.label }}: {{ form.entity(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if form.x %}
|
||||
<div>{{ form.x.label }}: {{ form.x(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if form.y %}
|
||||
<div>{{ form.y.label }}: {{ form.y(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% if form.size %}
|
||||
<div>{{ form.size.label }}: {{ form.size(class_="form-control select2") }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
|
@ -23,6 +39,7 @@
|
|||
{% else %}
|
||||
<script src="{{ url_for("static", filename="highcharts.js") }}"></script>
|
||||
{% endif %}
|
||||
<script src="{{ url_for("static", filename="highcharts-more.js") }}"></script>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
Highcharts.setOptions({
|
||||
|
|
|
@ -194,8 +194,9 @@ class Panoramix(BaseView):
|
|||
json.dumps(obj.get_query(), indent=4),
|
||||
status=200,
|
||||
mimetype="application/json")
|
||||
if obj.df is None or obj.df.empty:
|
||||
return obj.render_no_data()
|
||||
if not hasattr(obj, 'df') or obj.df is None or obj.df.empty:
|
||||
pass
|
||||
#return obj.render_no_data()
|
||||
return obj.render()
|
||||
|
||||
@has_access
|
||||
|
@ -221,8 +222,9 @@ class Panoramix(BaseView):
|
|||
json.dumps(obj.get_query(), indent=4),
|
||||
status=200,
|
||||
mimetype="application/json")
|
||||
if obj.df is None or obj.df.empty:
|
||||
if not hasattr(obj, 'df') or obj.df is None or obj.df.empty:
|
||||
return obj.render_no_data()
|
||||
|
||||
return obj.render()
|
||||
|
||||
@has_access
|
||||
|
|
88
app/viz.py
88
app/viz.py
|
@ -3,7 +3,7 @@ from flask import flash, request
|
|||
import pandas as pd
|
||||
from collections import OrderedDict
|
||||
from app import utils
|
||||
from app.highchart import Highchart
|
||||
from app.highchart import Highchart, HighchartBubble
|
||||
from wtforms import Form, SelectMultipleField, SelectField, TextField
|
||||
import config
|
||||
from pydruid.utils.filters import Dimension, Filter
|
||||
|
@ -67,21 +67,28 @@ def form_factory(datasource, form_args=None, extra_fields_dict=None):
|
|||
class BaseViz(object):
|
||||
verbose_name = "Base Viz"
|
||||
template = "panoramix/datasource.html"
|
||||
hidden_fields = []
|
||||
def __init__(self, datasource, form_data, view):
|
||||
self.datasource = datasource
|
||||
self.form_class = self.form_class()
|
||||
self.view = view
|
||||
self.form_data = form_data
|
||||
self.metrics = form_data.getlist('metrics') or ['count']
|
||||
self.groupby = form_data.getlist('groupby') or []
|
||||
|
||||
self.results = self.bake_query()
|
||||
self.df = self.results.df
|
||||
self.view = view
|
||||
if self.df is not None:
|
||||
if 'timestamp' in self.df.columns:
|
||||
self.df.timestamp = pd.to_datetime(self.df.timestamp)
|
||||
self.df_prep()
|
||||
self.form_prep()
|
||||
self.error_msg = ""
|
||||
self.results = None
|
||||
try:
|
||||
self.results = self.bake_query()
|
||||
self.df = self.results.df
|
||||
if self.df is not None:
|
||||
if 'timestamp' in self.df.columns:
|
||||
self.df.timestamp = pd.to_datetime(self.df.timestamp)
|
||||
self.df_prep()
|
||||
self.form_prep()
|
||||
except Exception as e:
|
||||
self.error_msg = str(e)
|
||||
|
||||
|
||||
def form_class(self):
|
||||
return form_factory(self.datasource, request.args)
|
||||
|
@ -190,6 +197,68 @@ class HighchartsViz(BaseViz):
|
|||
compare = False
|
||||
|
||||
|
||||
class BubbleViz(HighchartsViz):
|
||||
verbose_name = "Bubble Chart"
|
||||
chart_type = 'bubble'
|
||||
hidden_fields = ['granularity', 'metrics', 'groupby']
|
||||
|
||||
def form_class(self):
|
||||
datasource = self.datasource
|
||||
limits = [0, 5, 10, 25, 50, 100, 500]
|
||||
return form_factory(self.datasource, request.args,
|
||||
extra_fields_dict={
|
||||
#'compare': TextField('Period Compare',),
|
||||
'series': SelectField(
|
||||
'Series', choices=[
|
||||
(s, s) for s in datasource.groupby_column_names]),
|
||||
'entity': SelectField(
|
||||
'Entity', choices=[
|
||||
(s, s) for s in datasource.groupby_column_names]),
|
||||
'x': SelectField(
|
||||
'X Axis', choices=datasource.metrics_combo),
|
||||
'y': SelectField(
|
||||
'Y Axis', choices=datasource.metrics_combo),
|
||||
'size': SelectField(
|
||||
'Bubble Size', choices=datasource.metrics_combo),
|
||||
'limit': SelectField(
|
||||
'Limit', choices=[(s, s) for s in limits]),
|
||||
})
|
||||
|
||||
def query_obj(self):
|
||||
d = super(BubbleViz, self).query_obj()
|
||||
d['granularity'] = 'all'
|
||||
d['groupby'] = [request.args.get('series')]
|
||||
self.x_metric = request.args.get('x')
|
||||
self.y_metric = request.args.get('y')
|
||||
self.z_metric = request.args.get('size')
|
||||
self.entity = request.args.get('entity')
|
||||
self.series = request.args.get('series')
|
||||
d['metrics'] = [
|
||||
self.x_metric,
|
||||
self.y_metric,
|
||||
self.z_metric,
|
||||
]
|
||||
if not all(d['metrics'] + [self.entity, self.series]):
|
||||
raise Exception("Pick a metric for x, y and size")
|
||||
return d
|
||||
|
||||
def render(self):
|
||||
metrics = self.metrics
|
||||
|
||||
if not self.error_msg:
|
||||
df = self.df
|
||||
df['x'] = df[[self.x_metric]]
|
||||
df['y'] = df[[self.y_metric]]
|
||||
df['z'] = df[[self.z_metric]]
|
||||
df['name'] = df[[self.entity]]
|
||||
df['group'] = df[[self.series]]
|
||||
chart = HighchartBubble(df)
|
||||
return super(BubbleViz, self).render(chart_js=chart.javascript_cmd)
|
||||
else:
|
||||
return super(BubbleViz, self).render(error_msg=self.error_msg)
|
||||
|
||||
|
||||
|
||||
class TimeSeriesViz(HighchartsViz):
|
||||
verbose_name = "Time Series - Line Chart"
|
||||
chart_type = "spline"
|
||||
|
@ -320,4 +389,5 @@ viz_types = OrderedDict([
|
|||
['stacked_ts_bar', TimeSeriesStackedBarViz],
|
||||
['dist_bar', DistributionBarViz],
|
||||
['pie', DistributionPieViz],
|
||||
['bubble', BubbleViz],
|
||||
])
|
||||
|
|
Loading…
Reference in New Issue