diff --git a/TODO.md b/TODO.md index f6a28803fd..6bbbfc32a7 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,8 @@ # TODO +* in/notin filters autocomplete +* Highstock, sort legend based on y value: ![stackoverflow](http://stackoverflow.com/questions/6867607/want-to-sort-highcharts-tooltip-results) * compare time ranges -* Label +* Add verbose_name and label method to metrics and columns * CSV * Save / bookmark / url shortener +* on save, process metadata / generate metrics diff --git a/app.db b/app.db index f0bf7ecb2d..5c4db1afcf 100644 Binary files a/app.db and b/app.db differ diff --git a/app/highchart.py b/app/highchart.py new file mode 100644 index 0000000000..a872bb9274 --- /dev/null +++ b/app/highchart.py @@ -0,0 +1,156 @@ +import pandas +import copy +from pandas.io.json import dumps + + +class Highchart(object): + def __init__( + self, df, + chart_type="spline", + target_div="#chart", + polar=False, + width=None, + height=None, + show_legend=True, + stockchart=True, + title=None, + tooltip=None, + sort_columns=False, + secondary_y=None, + mark_right=False, + compare=False, + stacked=False, + logx=False, + logy=False, + xlim=None, + ylim=None, + grid=False, + zoom=None): + self.df = df + self.chart_type = chart_type + self.chart = chart = {} + self.stockchart = stockchart + self.sort_columns = sort_columns + self.secondary_y = secondary_y or [] + self.mark_right = mark_right + self.compare = compare + self.logx = logx + self.logy = logy + self.xlim = xlim + self.ylim = ylim + self.zoom = zoom + self.polar = polar + self.grid = grid + + chart['chart'] = {} + chart['chart']["type"] = chart_type + chart['chart']['renderTo'] = target_div + + if width: + chart['chart']["width"] = width + if height: + chart['chart']["height"] = height + + chart['chart']['polar'] = polar + + chart["legend"] = { + "enabled": show_legend + } + if title: + chart["title"] = {"text": title} + if tooltip: + chart['tooltip'] = tooltip + if self.zoom: + chart["zoomType"] = self.zoom + + self.serialize_series() + self.serialize_xaxis() + self.serialize_yaxis() + + self.chart = chart + + def serialize_series(self): + df = self.df + chart = self.chart + if self.sort_columns: + df = df.sort_index() + series = df.to_dict('series') + chart["series"] = [] + for name, data in series.items(): + if df[name].dtype.kind not in "biufc": + continue + sec = name in self.secondary_y + d = { + "name": name if not sec or self.mark_right else name + " (right)", + "yAxis": int(sec), + "data": zip(df.index, data.tolist()) + } + if self.polar: + d['data'] = [v for k, v in d['data']] + if self.compare: + d['compare'] = self.compare # either `value` or `percent` + if self.chart_type in ("area", "bar") and self.stacked: + d["stacking"] = 'normal' + #if kwargs.get("style"): + # d["dashStyle"] = pd2hc_linestyle(kwargs["style"].get(name, "-")) + chart["series"].append(d) + + def serialize_xaxis(self): + df = self.df + x_axis = {} + if df.index.name: + x_axis["title"] = {"text": df.index.name} + if df.index.dtype.kind in "M": + x_axis["type"] = "datetime" + if df.index.dtype.kind == 'O': + chart['xAxis']['categories'] = sorted(list(df.index)) if self.sort_columns else list(df.index) + if self.grid: + x_axis["gridLineWidth"] = 1 + x_axis["gridLineDashStyle"] = "Dot" + if self.logx: + x_axis["type"] = 'logarithmic' + if self.xlim: + x_axis["min"] = self.xlim[0] + x_axis["max"] = self.xlim[1] + ''' + if "rot" in kwargs: + x_axis["labels"] = {"rotation": kwargs["rot"]} + if "fontsize" in kwargs: + x_axis.setdefault("labels", {})["style"] = {"fontSize": kwargs["fontsize"]} + if "xticks" in kwargs: + x_axis["tickPositions"] = kwargs["xticks"] + ''' + self.x_axis = x_axis + + def serialize_yaxis(self): + yAxis = {} + chart = self.chart + if self.grid: + yAxis["gridLineWidth"] = 1 + yAxis["gridLineDashStyle"] = "Dot" + if self.logy: + yAxis["type"] = 'logarithmic' + if self.ylim: + yAxis["min"] = self.ylim[0] + yAxis["max"] = self.ylim[1] + ''' + if "rot" in kwargs: + yAxis["labels"] = {"rotation": kwargs["rot"]} + if "fontsize" in kwargs: + yAxis.setdefault("labels", {})["style"] = {"fontSize": kwargs["fontsize"]} + if "yticks" in kwargs: + yAxis["tickPositions"] = kwargs["yticks"] + ''' + chart["yAxis"] = [yAxis] + if self.secondary_y: + yAxis2 = copy.deepcopy(yAxis) + yAxis2["opposite"] = True + chart["yAxis"].append(yAxis2) + + + @property + def javascript_cmd(self): + js = dumps(self.chart) + if self.stockchart: + return "new Highcharts.StockChart({});".format(js) + return "new Highcharts.Chart({});".format(js) diff --git a/app/viz.py b/app/viz.py index f529c2a73f..433ec3c0c9 100644 --- a/app/viz.py +++ b/app/viz.py @@ -1,19 +1,18 @@ from pydruid.utils.filters import Dimension, Filter from datetime import datetime -from flask import render_template, flash, request +from flask import flash, request import pandas as pd -from pandas_highcharts.core import serialize -from pydruid.utils import aggregators as agg from collections import OrderedDict from app import utils +from app.highchart import Highchart from wtforms import Form, SelectMultipleField, SelectField, TextField import config CHART_ARGS = { - 'figsize': (None, 700), + 'height': 700, 'title': None, - 'render_to': 'chart', + 'target_div': 'chart', } class OmgWtForm(Form): @@ -220,8 +219,8 @@ class HighchartsViz(BaseViz): class TimeSeriesViz(HighchartsViz): verbose_name = "Time Series - Line Chart" - chart_kind = "spline" - chart_type = 'stock' + chart_type = "spline" + highstock = True def render(self): metrics = self.metrics @@ -237,12 +236,13 @@ class TimeSeriesViz(HighchartsViz): if rolling_type == 'mean': df = pd.rolling_mean(df, int(rolling_periods)) - chart_js = serialize( - df, kind=self.chart_kind, - viz=self, + chart = Highchart( + df, compare=self.compare, - chart_type=self.chart_type, stacked=self.stacked, **CHART_ARGS) - return super(TimeSeriesViz, self).render(chart_js=chart_js) + chart_type=self.chart_type, + stacked=self.stacked, + **CHART_ARGS) + return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd) def form_class(self): return form_factory(self.datasource, request.args, @@ -297,23 +297,23 @@ class TimeSeriesCompareViz(TimeSeriesViz): class TimeSeriesAreaViz(TimeSeriesViz): verbose_name = "Time Series - Stacked Area Chart" stacked=True - chart_kind = "area" + chart_type = "area" class TimeSeriesBarViz(TimeSeriesViz): verbose_name = "Time Series - Bar Chart" - chart_kind = "bar" + chart_type = "bar" class TimeSeriesStackedBarViz(TimeSeriesViz): verbose_name = "Time Series - Stacked Bar Chart" - chart_kind = "bar" + chart_type = "bar" stacked = True class DistributionBarViz(HighchartsViz): verbose_name = "Distribution - Bar Chart" - chart_kind = "bar" + chart_type = "bar" def query_obj(self): d = super(DistributionBarViz, self).query_obj() diff --git a/requirements.txt b/requirements.txt index 40c6da8e83..e629f4fbbc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ flask flask-alembic flask-appbuilder pandas -pandas-highcharts parsedatetime pydruid python-dateutil