Merge pull request #15 from mistercrunch/scatter

Adding Bubble charts
This commit is contained in:
Maxime Beauchemin 2015-08-13 18:09:16 -07:00
commit 81dbd142e1
5 changed files with 175 additions and 25 deletions

View File

@ -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

View File

@ -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 %}

View File

@ -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({

View File

@ -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

View File

@ -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],
])