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 import pandas
from collections import defaultdict
import copy import copy
import json
from pandas.io.json import dumps 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__( def __init__(
self, df, self, df,
chart_type="spline", chart_type="spline",
@ -144,7 +162,8 @@ class Highchart(object):
if df.index.dtype.kind in "M": if df.index.dtype.kind in "M":
x_axis["type"] = "datetime" x_axis["type"] = "datetime"
if df.index.dtype.kind == 'O': 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) print list(df.index)
if self.grid: if self.grid:
x_axis["gridLineWidth"] = 1 x_axis["gridLineWidth"] = 1
@ -174,10 +193,38 @@ class Highchart(object):
chart["yAxis"].append(yAxis2) chart["yAxis"].append(yAxis2)
@property class HighchartBubble(BaseHighchart):
def javascript_cmd(self): def __init__(self, df, target_div='chart', height=800):
js = dumps(self.chart) self.df = df
js = js.replace('"{{TOOLTIP_FORMATTER}}"', self.tooltip_formatter).replace("\n", " ") self.chart = {
if self.stockchart: 'chart': {
return "new Highcharts.StockChart(%s);" % js 'type': 'bubble',
return "new Highcharts.Chart(%s);" %js '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> <hr>
<form id="query" method="GET" style="display: none;"> <form id="query" method="GET" style="display: none;">
<div>{{ form.viz_type.label }}: {{ form.viz_type(class_="form-control select2") }}</div> <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 }} <div>{{ form.granularity.label }}
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right" <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'" title="Supports natural language time as in '10 seconds', '1 day' or '1 week'"
id="blah"></i> id="blah"></i>
{{ form.granularity(class_="form-control select2_free_granularity") }}</div> {{ form.granularity(class_="form-control select2_free_granularity") }}</div>
{% endif %}
<div class="row"> <div class="row">
<div class="form-group"> <div class="form-group">
<div class="col-xs-6">{{ form.since.label }} <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> {{ form.until(class_="form-control select2_free_until") }}</div>
</div> </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 %} {% block extra_fields %}{% endblock %}
<hr> <hr>
<h4>Filters</h4> <h4>Filters</h4>
@ -85,11 +91,19 @@ form input.form-control {
<div class="col-md-9"> <div class="col-md-9">
<h3>{{ viz.verbose_name }} <h3>{{ viz.verbose_name }}
<span class="label label-success">{{ "{0:0.2f}".format(results.duration.total_seconds()) }} s</span> {% if results %}
<span class="label label-info btn" data-toggle="modal" data-target="#query_modal">query</span> <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> </h3>
<hr/> <hr/>
{% block viz %} {% block viz %}
{% if error_msg %}
<span class="alert alert-danger">{{ error_msg }}</span>
{% endif %}
{% endblock %} {% endblock %}
{% if debug %} {% if debug %}

View File

@ -1,5 +1,6 @@
{% extends "panoramix/datasource.html" %} {% extends "panoramix/datasource.html" %}
{% block viz %} {% block viz %}
{{ super() }}
<div id="chart"></div> <div id="chart"></div>
{% endblock %} {% endblock %}
@ -14,6 +15,21 @@
</div> </div>
{% endif %} {% endif %}
<div>{{ form.limit.label }}: {{ form.limit(class_="form-control select2") }}</div> <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 %} {% endblock %}
{% block tail %} {% block tail %}
@ -23,6 +39,7 @@
{% else %} {% else %}
<script src="{{ url_for("static", filename="highcharts.js") }}"></script> <script src="{{ url_for("static", filename="highcharts.js") }}"></script>
{% endif %} {% endif %}
<script src="{{ url_for("static", filename="highcharts-more.js") }}"></script>
<script> <script>
$( document ).ready(function() { $( document ).ready(function() {
Highcharts.setOptions({ Highcharts.setOptions({

View File

@ -194,8 +194,9 @@ class Panoramix(BaseView):
json.dumps(obj.get_query(), indent=4), json.dumps(obj.get_query(), indent=4),
status=200, status=200,
mimetype="application/json") 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() pass
#return obj.render_no_data()
return obj.render() return obj.render()
@has_access @has_access
@ -221,8 +222,9 @@ class Panoramix(BaseView):
json.dumps(obj.get_query(), indent=4), json.dumps(obj.get_query(), indent=4),
status=200, status=200,
mimetype="application/json") 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_no_data()
return obj.render() return obj.render()
@has_access @has_access

View File

@ -3,7 +3,7 @@ from flask import flash, request
import pandas as pd import pandas as pd
from collections import OrderedDict from collections import OrderedDict
from app import utils from app import utils
from app.highchart import Highchart from app.highchart import Highchart, HighchartBubble
from wtforms import Form, SelectMultipleField, SelectField, TextField from wtforms import Form, SelectMultipleField, SelectField, TextField
import config import config
from pydruid.utils.filters import Dimension, Filter 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): class BaseViz(object):
verbose_name = "Base Viz" verbose_name = "Base Viz"
template = "panoramix/datasource.html" template = "panoramix/datasource.html"
hidden_fields = []
def __init__(self, datasource, form_data, view): def __init__(self, datasource, form_data, view):
self.datasource = datasource self.datasource = datasource
self.form_class = self.form_class() self.form_class = self.form_class()
self.view = view
self.form_data = form_data self.form_data = form_data
self.metrics = form_data.getlist('metrics') or ['count'] self.metrics = form_data.getlist('metrics') or ['count']
self.groupby = form_data.getlist('groupby') or [] self.groupby = form_data.getlist('groupby') or []
self.results = self.bake_query() self.error_msg = ""
self.df = self.results.df self.results = None
self.view = view try:
if self.df is not None: self.results = self.bake_query()
if 'timestamp' in self.df.columns: self.df = self.results.df
self.df.timestamp = pd.to_datetime(self.df.timestamp) if self.df is not None:
self.df_prep() if 'timestamp' in self.df.columns:
self.form_prep() 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): def form_class(self):
return form_factory(self.datasource, request.args) return form_factory(self.datasource, request.args)
@ -190,6 +197,68 @@ class HighchartsViz(BaseViz):
compare = False 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): class TimeSeriesViz(HighchartsViz):
verbose_name = "Time Series - Line Chart" verbose_name = "Time Series - Line Chart"
chart_type = "spline" chart_type = "spline"
@ -320,4 +389,5 @@ viz_types = OrderedDict([
['stacked_ts_bar', TimeSeriesStackedBarViz], ['stacked_ts_bar', TimeSeriesStackedBarViz],
['dist_bar', DistributionBarViz], ['dist_bar', DistributionBarViz],
['pie', DistributionPieViz], ['pie', DistributionPieViz],
['bubble', BubbleViz],
]) ])