diff --git a/superset/assets/images/viz_thumbnails/dual_line.png b/superset/assets/images/viz_thumbnails/dual_line.png new file mode 100644 index 0000000000000..cf2f6ccc62e0c Binary files /dev/null and b/superset/assets/images/viz_thumbnails/dual_line.png differ diff --git a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js index 5982ce591a6e2..09a057e4a0adb 100644 --- a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js +++ b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js @@ -50,6 +50,9 @@ export const exploreReducer = function (state, action) { newState.filterColumnOpts = optionsByFieldName[fieldName]; } else { newState.fields[fieldName].choices = optionsByFieldName[fieldName]; + if (fieldName === 'metric' && state.viz.form_data.viz_type === 'dual_line') { + newState.fields.metric_2.choices = optionsByFieldName[fieldName]; + } } }); return Object.assign({}, state, newState); diff --git a/superset/assets/javascripts/explorev2/stores/fields.js b/superset/assets/javascripts/explorev2/stores/fields.js index ba47a1b1fa55b..cbe195482c189 100644 --- a/superset/assets/javascripts/explorev2/stores/fields.js +++ b/superset/assets/javascripts/explorev2/stores/fields.js @@ -72,6 +72,14 @@ export const fields = { description: 'Choose the metric', }, + metric_2: { + type: 'SelectField', + label: 'Right Axis Metric', + choices: [], + default: [], + description: 'Choose a metric for right axis', + }, + stacked_style: { type: 'SelectField', label: 'Stacked Style', @@ -692,6 +700,14 @@ export const fields = { description: D3_FORMAT_DOCS, }, + y_axis_2_format: { + type: 'FreeFormSelectField', + label: 'Right axis format', + default: '.3s', + choices: D3_TIME_FORMAT_OPTIONS, + description: D3_FORMAT_DOCS, + }, + markup_type: { type: 'SelectField', label: 'Markup Type', diff --git a/superset/assets/javascripts/explorev2/stores/visTypes.js b/superset/assets/javascripts/explorev2/stores/visTypes.js index 1d6c51bb8196c..876293c5dd1c6 100644 --- a/superset/assets/javascripts/explorev2/stores/visTypes.js +++ b/superset/assets/javascripts/explorev2/stores/visTypes.js @@ -138,6 +138,42 @@ const visTypes = { ], }, + dual_line: { + label: 'Time Series - Dual Axis Line Chart', + requiresTime: true, + controlPanelSections: [ + { + label: 'Chart Options', + fieldSetRows: [ + ['x_axis_format'], + ], + }, + { + label: 'Y Axis 1', + fieldSetRows: [ + ['metric'], + ['y_axis_format'], + ], + }, + { + label: 'Y Axis 2', + fieldSetRows: [ + ['metric_2'], + ['y_axis_2_format'], + ], + }, + ], + fieldOverrides: { + metric: { + label: 'Left Axis Metric', + description: 'Choose a metric for left axis', + }, + y_axis_format: { + label: 'Left Axis Format', + }, + }, + }, + bar: { label: 'Time Series - Bar Chart', requiresTime: true, diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js index ab2e265662300..36c9ac89a2393 100644 --- a/superset/assets/visualizations/main.js +++ b/superset/assets/visualizations/main.js @@ -29,5 +29,6 @@ const vizMap = { treemap: require('./treemap.js'), word_cloud: require('./word_cloud.js'), world_map: require('./world_map.js'), + dual_line: require('./nvd3_vis.js'), }; export default vizMap; diff --git a/superset/assets/visualizations/nvd3_vis.js b/superset/assets/visualizations/nvd3_vis.js index d4db0fb340b8e..df8a199c42ab3 100644 --- a/superset/assets/visualizations/nvd3_vis.js +++ b/superset/assets/visualizations/nvd3_vis.js @@ -126,6 +126,11 @@ function nvd3Vis(slice) { .staggerLabels(false); break; + case 'dual_line': + chart = nv.models.multiChart(); + chart.interpolate('linear'); + break; + case 'bar': chart = nv.models.multiBarChart() .showControls(fd.show_controls) @@ -309,7 +314,7 @@ function nvd3Vis(slice) { chart.yAxis.tickFormat(d3.format('.3s')); } - if (fd.y_axis_format) { + if (fd.y_axis_format && chart.yAxis) { chart.yAxis.tickFormat(d3.format(fd.y_axis_format)); if (chart.y2Axis !== undefined) { chart.y2Axis.tickFormat(d3.format(fd.y_axis_format)); @@ -347,7 +352,12 @@ function nvd3Vis(slice) { if (svg.empty()) { svg = d3.select(slice.selector).append('svg'); } - + if (vizType === 'dual_line') { + chart.yAxis1.tickFormat(d3.format(fd.y_axis_format)); + chart.yAxis2.tickFormat(d3.format(fd.y_axis_2_format)); + chart.showLegend(true); + chart.margin({ right: 50 }); + } svg .datum(payload.data) .transition().duration(500) diff --git a/superset/data/__init__.py b/superset/data/__init__.py index 518703621b662..e96ef36f35af9 100644 --- a/superset/data/__init__.py +++ b/superset/data/__init__.py @@ -672,6 +672,15 @@ def load_birth_names(): defaults, viz_type="line", groupby=['name'], granularity='ds', rich_tooltip='y', show_legend='y')), + Slice( + slice_name="Average and Sum Trends", + viz_type='dual_line', + datasource_type='table', + datasource_id=tbl.id, + params=get_slice_json( + defaults, + viz_type="dual_line", metric='avg__num', metric_2='sum__num', + granularity='ds')), Slice( slice_name="Title", viz_type='markup', diff --git a/superset/forms.py b/superset/forms.py index 805cb47b727da..0a1dcb45d0b87 100755 --- a/superset/forms.py +++ b/superset/forms.py @@ -27,6 +27,14 @@ '"%Y-%m-%d %H:%M:%S" | 2019-01-14 01:32:10'), ("%H:%M:%S", '"%H:%M:%S" | 01:32:10'), ] +AXIS_FORMAT_CHOICES = [ + ('.3s', '".3s" | 12.3k'), + ('.3%', '".3%" | 1234543.210%'), + ('.4r', '".4r" | 12350'), + ('.3f', '".3f" | 12345.432'), + ('+,', '"+," | +12,345.4321'), + ('$,.2f', '"$,.2f" | $12,345.43'), +] D3_FORMAT_DOCS = _( "D3 format syntax " "https://github.com/d3/d3-format") @@ -162,6 +170,12 @@ def __init__(self, viz): "default": default_metric, "description": _("Choose the metric") }), + 'metric_2': (SelectField, { + "label": _("Right Axis Metric"), + "choices": datasource.metrics_combo, + "default": default_metric, + "description": _("Choose the metric for second y axis") + }), 'stacked_style': (SelectField, { "label": _("Chart Style"), "choices": ( @@ -679,14 +693,13 @@ def __init__(self, viz): 'y_axis_format': (FreeFormSelectField, { "label": _("Y axis format"), "default": '.3s', - "choices": [ - ('.3s', '".3s" | 12.3k'), - ('.3%', '".3%" | 1234543.210%'), - ('.4r', '".4r" | 12350'), - ('.3f', '".3f" | 12345.432'), - ('+,', '"+," | +12,345.4321'), - ('$,.2f', '"$,.2f" | $12,345.43'), - ], + "choices": AXIS_FORMAT_CHOICES, + "description": D3_FORMAT_DOCS, + }), + 'y_axis_2_format': (FreeFormSelectField, { + "label": _("Right axis format"), + "default": '.3s', + "choices": AXIS_FORMAT_CHOICES, "description": D3_FORMAT_DOCS, }), 'markup_type': (SelectField, { diff --git a/superset/viz.py b/superset/viz.py index 01c3cf35fa36a..a0e11f4790590 100755 --- a/superset/viz.py +++ b/superset/viz.py @@ -1253,6 +1253,8 @@ def to_series(self, df, classed='', title_suffix=''): def get_data(self): df = self.get_df() + import ipdb + ipdb.set_trace() chart_data = self.to_series(df) time_compare = self.form_data.get('time_compare') @@ -1272,6 +1274,111 @@ def get_data(self): return chart_data +class NVD3DualLineViz(NVD3Viz): + + """A rich line chart with dual axis""" + + viz_type = "dual_line" + verbose_name = _("Time Series - Dual Axis Line Chart") + sort_series = False + is_timeseries = True + fieldsets = ({ + 'label': _('Chart Options'), + 'fields': ('x_axis_format',), + }, { + 'label': _('Y Axis 1'), + 'fields': ( + 'metric', + 'y_axis_format' + ), + }, { + 'label': _('Y Axis 2'), + 'fields': ( + 'metric_2', + 'y_axis_2_format' + ), + },) + form_overrides = { + 'y_axis_format': { + 'label': _('Left Axis Format'), + 'description': _("Select the numeric column to draw the histogram"), + }, + 'metric': { + 'label': _("Left Axis Metric"), + } + } + + def get_df(self, query_obj=None): + if not query_obj: + query_obj = super(NVD3DualLineViz, self).query_obj() + metrics = [ + self.form_data.get('metric'), + self.form_data.get('metric_2') + ] + query_obj['metrics'] = metrics + df = super(NVD3DualLineViz, self).get_df(query_obj) + df = df.fillna(0) + if self.form_data.get("granularity") == "all": + raise Exception("Pick a time granularity for your time series") + + df = df.pivot_table( + index=DTTM_ALIAS, + values=metrics) + + return df + + def to_series(self, df, classed=''): + cols = [] + for col in df.columns: + if col == '': + cols.append('N/A') + elif col is None: + cols.append('NULL') + else: + cols.append(col) + df.columns = cols + series = df.to_dict('series') + chart_data = [] + index_list = df.T.index.tolist() + for i in range(0, len(index_list)): + name = index_list[i] + ys = series[name] + if df[name].dtype.kind not in "biufc": + continue + df[DTTM_ALIAS] = pd.to_datetime(df.index, utc=False) + if isinstance(name, string_types): + series_title = name + else: + name = ["{}".format(s) for s in name] + series_title = ", ".join(name[1:]) + + d = { + "key": series_title, + "classed": classed, + "values": [ + {'x': ds, 'y': ys[ds] if ds in ys else None} + for ds in df[DTTM_ALIAS] + ], + "yAxis": i+1, + "type": "line" + } + chart_data.append(d) + return chart_data + + def get_data(self): + form_data = self.form_data + metric = form_data.get('metric') + metric_2 = form_data.get('metric_2') + if not metric: + raise Exception("Pick a metric for left axis!") + if not metric_2: + raise Exception("Pick a metric for right axis!") + + df = self.get_df() + chart_data = self.to_series(df) + return chart_data + + class NVD3TimeSeriesBarViz(NVD3TimeSeriesViz): """A bar chart where the x axis is time""" @@ -2102,6 +2209,7 @@ def get_data(self): TableViz, PivotTableViz, NVD3TimeSeriesViz, + NVD3DualLineViz, NVD3CompareTimeSeriesViz, NVD3TimeSeriesStackedViz, NVD3TimeSeriesBarViz,