You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ma...@apache.org on 2018/05/22 00:47:25 UTC

[incubator-superset] branch master updated: Visualization for multiple line charts (#4819)

This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 459cb70  Visualization for multiple line charts (#4819)
459cb70 is described below

commit 459cb701fb2140fcce8b97a1839a9511574375c7
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Mon May 21 17:47:21 2018 -0700

    Visualization for multiple line charts (#4819)
    
    * Initial test
    
    * Save
    
    * Working version
    
    * Use since/until from payload
    
    * Option to prefix metric name
    
    * Rename LineMultiLayer to MultiLineViz
    
    * Add more styles
    
    * Explicit nulls
    
    * Add more x controls
    
    * Refactor to reuse nvd3_vis
    
    * Fix x ticks
    
    * Fix spacing
    
    * Fix for druid datasource
    
    * Rename file
    
    * Small fixes and cleanup
    
    * Fix margins
    
    * Add proper thumbnails
    
    * Align yaxis1 and yaxis2 ticks
    
    * Improve code
    
    * Trigger tests
    
    * Move file
    
    * Small fixes plus example
    
    * Fix unit test
    
    * Remove SQL and Filter sections
---
 .../assets/images/viz_thumbnails/line_multi.png    | Bin 0 -> 54363 bytes
 .../images/viz_thumbnails_large/line_multi.png     | Bin 0 -> 116138 bytes
 .../explore/components/ExploreViewContainer.jsx    |   7 +-
 .../components/controls/DatasourceControl.jsx      |   2 +-
 superset/assets/src/explore/controls.jsx           |  43 +++++++++
 superset/assets/src/explore/visTypes.js            |  99 ++++++++++++++++++++-
 superset/assets/src/visualizations/index.js        |   3 +
 superset/assets/src/visualizations/line_multi.js   |  74 +++++++++++++++
 superset/assets/src/visualizations/nvd3_vis.js     |  70 ++++++++++++---
 superset/cli.py                                    |   3 +
 superset/data/__init__.py                          |  28 ++++++
 superset/viz.py                                    |  29 ++++++
 tests/core_tests.py                                |   2 +-
 13 files changed, 342 insertions(+), 18 deletions(-)

diff --git a/superset/assets/images/viz_thumbnails/line_multi.png b/superset/assets/images/viz_thumbnails/line_multi.png
new file mode 100644
index 0000000..f776bb8
Binary files /dev/null and b/superset/assets/images/viz_thumbnails/line_multi.png differ
diff --git a/superset/assets/images/viz_thumbnails_large/line_multi.png b/superset/assets/images/viz_thumbnails_large/line_multi.png
new file mode 100644
index 0000000..473be99
Binary files /dev/null and b/superset/assets/images/viz_thumbnails_large/line_multi.png differ
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index c8d7acd..bc875d4 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -74,7 +74,12 @@ class ExploreViewContainer extends React.Component {
       this.props.actions.resetControls();
       this.props.actions.triggerQuery(true, this.props.chart.chartKey);
     }
-    if (np.controls.datasource.value !== this.props.controls.datasource.value) {
+    if (
+      np.controls.datasource && (
+        this.props.controls.datasource == null ||
+        np.controls.datasource.value !== this.props.controls.datasource.value
+      )
+    ) {
       this.props.actions.fetchDatasourceMetadata(np.form_data.datasource, true);
     }
 
diff --git a/superset/assets/src/explore/components/controls/DatasourceControl.jsx b/superset/assets/src/explore/components/controls/DatasourceControl.jsx
index 0256aac..404ba5e 100644
--- a/superset/assets/src/explore/components/controls/DatasourceControl.jsx
+++ b/superset/assets/src/explore/components/controls/DatasourceControl.jsx
@@ -18,7 +18,7 @@ const propTypes = {
   name: PropTypes.string.isRequired,
   onChange: PropTypes.func,
   value: PropTypes.string.isRequired,
-  datasource: PropTypes.object.isRequired,
+  datasource: PropTypes.object,
 };
 
 const defaultProps = {
diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx
index 66d563b..710d6b4 100644
--- a/superset/assets/src/explore/controls.jsx
+++ b/superset/assets/src/explore/controls.jsx
@@ -2068,6 +2068,49 @@ export const controls = {
     description: t('The width of the lines'),
   },
 
+  line_charts: {
+    type: 'SelectAsyncControl',
+    multi: true,
+    label: t('Line charts'),
+    validators: [v.nonEmpty],
+    default: [],
+    description: t('Pick a set of line charts to layer on top of one another'),
+    dataEndpoint: '/sliceasync/api/read?_flt_0_viz_type=line&_flt_7_viz_type=line_multi',
+    placeholder: t('Select charts'),
+    onAsyncErrorMessage: t('Error while fetching charts'),
+    mutator: (data) => {
+      if (!data || !data.result) {
+        return [];
+      }
+      return data.result.map(o => ({ value: o.id, label: o.slice_name }));
+    },
+  },
+
+  line_charts_2: {
+    type: 'SelectAsyncControl',
+    multi: true,
+    label: t('Right Axis chart(s)'),
+    validators: [],
+    default: [],
+    description: t('Choose one or more charts for right axis'),
+    dataEndpoint: '/sliceasync/api/read?_flt_0_viz_type=line&_flt_7_viz_type=line_multi',
+    placeholder: t('Select charts'),
+    onAsyncErrorMessage: t('Error while fetching charts'),
+    mutator: (data) => {
+      if (!data || !data.result) {
+        return [];
+      }
+      return data.result.map(o => ({ value: o.id, label: o.slice_name }));
+    },
+  },
+
+  prefix_metric_with_slice_name: {
+    type: 'CheckboxControl',
+    label: t('Prefix metric name with slice name'),
+    default: false,
+    renderTrigger: true,
+  },
+
   reverse_long_lat: {
     type: 'CheckboxControl',
     label: t('Reverse Lat & Long'),
diff --git a/superset/assets/src/explore/visTypes.js b/superset/assets/src/explore/visTypes.js
index 35e49d4..1ad5c89 100644
--- a/superset/assets/src/explore/visTypes.js
+++ b/superset/assets/src/explore/visTypes.js
@@ -225,6 +225,82 @@ export const visTypes = {
     },
   },
 
+  line_multi: {
+    label: t('Time Series - Multiple Line Charts'),
+    showOnExplore: true,
+    requiresTime: true,
+    controlPanelSections: [
+      {
+        label: t('Chart Options'),
+        expanded: true,
+        controlSetRows: [
+          ['color_scheme'],
+          ['prefix_metric_with_slice_name', null],
+          ['show_legend', 'show_markers'],
+          ['line_interpolation', null],
+        ],
+      },
+      {
+        label: t('X Axis'),
+        expanded: true,
+        controlSetRows: [
+          ['x_axis_label', 'bottom_margin'],
+          ['x_ticks_layout', 'x_axis_format'],
+          ['x_axis_showminmax', null],
+        ],
+      },
+      {
+        label: t('Y Axis 1'),
+        expanded: true,
+        controlSetRows: [
+          ['line_charts', 'y_axis_format'],
+        ],
+      },
+      {
+        label: t('Y Axis 2'),
+        expanded: false,
+        controlSetRows: [
+          ['line_charts_2', 'y_axis_2_format'],
+        ],
+      },
+      sections.annotations,
+    ],
+    controlOverrides: {
+      line_charts: {
+        label: t('Left Axis chart(s)'),
+        description: t('Choose one or more charts for left axis'),
+      },
+      y_axis_format: {
+        label: t('Left Axis Format'),
+      },
+      x_axis_format: {
+        choices: D3_TIME_FORMAT_OPTIONS,
+        default: 'smart_date',
+      },
+    },
+    sectionOverrides: {
+      sqlClause: [],
+      filters: [[]],
+      datasourceAndVizType: {
+        label: t('Chart Type'),
+        controlSetRows: [
+          ['viz_type'],
+          ['slice_id', 'cache_timeout'],
+        ],
+      },
+      sqlaTimeSeries: {
+        controlSetRows: [
+          ['since', 'until'],
+        ],
+      },
+      druidTimeSeries: {
+        controlSetRows: [
+          ['since', 'until'],
+        ],
+      },
+    },
+  },
+
   time_pivot: {
     label: t('Time Series - Periodicity Pivot'),
     showOnExplore: true,
@@ -1731,11 +1807,26 @@ function adhocFilterEnabled(viz) {
 
 export function sectionsToRender(vizType, datasourceType) {
   const viz = visTypes[vizType];
+
+  const sectionsCopy = { ...sections };
+  if (viz.sectionOverrides) {
+    Object.entries(viz.sectionOverrides).forEach(([section, overrides]) => {
+      if (typeof overrides === 'object' && overrides.constructor === Object) {
+        sectionsCopy[section] = {
+          ...sectionsCopy[section],
+          ...overrides,
+        };
+      } else {
+        sectionsCopy[section] = overrides;
+      }
+    });
+  }
+
   return [].concat(
-    sections.datasourceAndVizType,
-    datasourceType === 'table' ? sections.sqlaTimeSeries : sections.druidTimeSeries,
+    sectionsCopy.datasourceAndVizType,
+    datasourceType === 'table' ? sectionsCopy.sqlaTimeSeries : sectionsCopy.druidTimeSeries,
     viz.controlPanelSections,
-    !adhocFilterEnabled(viz) && (datasourceType === 'table' ? sections.sqlClause : []),
-    !adhocFilterEnabled(viz) && (datasourceType === 'table' ? sections.filters[0] : sections.filters),
+    !adhocFilterEnabled(viz) && (datasourceType === 'table' ? sectionsCopy.sqlClause : []),
+    !adhocFilterEnabled(viz) && (datasourceType === 'table' ? sectionsCopy.filters[0] : sectionsCopy.filters),
   ).filter(section => section);
 }
diff --git a/superset/assets/src/visualizations/index.js b/superset/assets/src/visualizations/index.js
index 5477c33..77cc0ea 100644
--- a/superset/assets/src/visualizations/index.js
+++ b/superset/assets/src/visualizations/index.js
@@ -1,5 +1,6 @@
 /* eslint-disable global-require */
 import nvd3Vis from './nvd3_vis';
+import lineMulti from './line_multi';
 
 // You ***should*** use these to reference viz_types in code
 export const VIZ_TYPES = {
@@ -21,6 +22,7 @@ export const VIZ_TYPES = {
   horizon: 'horizon',
   iframe: 'iframe',
   line: 'line',
+  line_multi: 'line_multi',
   mapbox: 'mapbox',
   markup: 'markup',
   para: 'para',
@@ -71,6 +73,7 @@ const vizMap = {
   [VIZ_TYPES.horizon]: require('./horizon.js'),
   [VIZ_TYPES.iframe]: require('./iframe.js'),
   [VIZ_TYPES.line]: nvd3Vis,
+  [VIZ_TYPES.line_multi]: lineMulti,
   [VIZ_TYPES.time_pivot]: nvd3Vis,
   [VIZ_TYPES.mapbox]: require('./mapbox.jsx'),
   [VIZ_TYPES.markup]: require('./markup.js'),
diff --git a/superset/assets/src/visualizations/line_multi.js b/superset/assets/src/visualizations/line_multi.js
new file mode 100644
index 0000000..c164686
--- /dev/null
+++ b/superset/assets/src/visualizations/line_multi.js
@@ -0,0 +1,74 @@
+import nvd3Vis from './nvd3_vis';
+import { getExploreLongUrl } from '../explore/exploreUtils';
+
+
+export default function lineMulti(slice, payload) {
+  /*
+   * Show multiple line charts
+   *
+   * This visualization works by fetching the data from each of the saved
+   * charts, building the payload data and passing it along to nvd3Vis.
+   */
+  const fd = slice.formData;
+
+  // fetch data from all the charts
+  const promises = [];
+  const subslices = [
+    ...payload.data.slices.axis1.map(subslice => [1, subslice]),
+    ...payload.data.slices.axis2.map(subslice => [2, subslice]),
+  ];
+  subslices.forEach(([yAxis, subslice]) => {
+    let filters = subslice.form_data.filters || [];
+    filters.concat(fd.filters);
+    if (fd.extra_filters) {
+      filters = filters.concat(fd.extra_filters);
+    }
+    const fdCopy = {
+      ...subslice.form_data,
+      filters,
+      since: fd.since,
+      until: fd.until,
+    };
+    const url = getExploreLongUrl(fdCopy, 'json');
+    promises.push(new Promise((resolve, reject) => {
+      d3.json(url, (error, response) => {
+        if (error) {
+          reject(error);
+        } else {
+          const data = [];
+          response.data.forEach((datum) => {
+            let key = datum.key;
+            if (fd.prefix_metric_with_slice_name) {
+              key = subslice.slice_name + ': ' + key;
+            }
+            data.push({ key, values: datum.values, type: fdCopy.viz_type, yAxis });
+          });
+          resolve(data);
+        }
+      });
+    }));
+  });
+
+  Promise.all(promises).then((data) => {
+    const payloadCopy = { ...payload };
+    payloadCopy.data = [].concat(...data);
+
+    // add null values at the edges to fix multiChart bug when series with
+    // different x values use different y axes
+    if (fd.line_charts.length && fd.line_charts_2.length) {
+      let minx = Infinity;
+      let maxx = -Infinity;
+      payloadCopy.data.forEach((datum) => {
+        minx = Math.min(minx, ...datum.values.map(v => v.x));
+        maxx = Math.max(maxx, ...datum.values.map(v => v.x));
+      });
+      // add null values at the edges
+      payloadCopy.data.forEach((datum) => {
+        datum.values.push({ x: minx, y: null });
+        datum.values.push({ x: maxx, y: null });
+      });
+    }
+
+    nvd3Vis(slice, payloadCopy);
+  });
+}
diff --git a/superset/assets/src/visualizations/nvd3_vis.js b/superset/assets/src/visualizations/nvd3_vis.js
index 068b8a0..c2c74a0 100644
--- a/superset/assets/src/visualizations/nvd3_vis.js
+++ b/superset/assets/src/visualizations/nvd3_vis.js
@@ -32,6 +32,16 @@ const BREAKPOINTS = {
   small: 340,
 };
 
+const TIMESERIES_VIZ_TYPES = [
+  'line',
+  'dual_line',
+  'line_multi',
+  'area',
+  'compare',
+  'bar',
+  'time_pivot',
+];
+
 const addTotalBarValues = function (svg, chart, data, stacked, axisFormat) {
   const format = d3.format(axisFormat || '.3s');
   const countSeriesDisplayed = data.length;
@@ -149,8 +159,7 @@ export default function nvd3Vis(slice, payload) {
       svg = d3.select(slice.selector).append('svg');
     }
     let height = slice.height();
-    const isTimeSeries = [
-      'line', 'dual_line', 'area', 'compare', 'bar', 'time_pivot'].indexOf(vizType) >= 0;
+    const isTimeSeries = TIMESERIES_VIZ_TYPES.indexOf(vizType) >= 0;
 
     // Handling xAxis ticks settings
     let xLabelRotation = 0;
@@ -202,6 +211,11 @@ export default function nvd3Vis(slice, payload) {
         chart.interpolate('linear');
         break;
 
+      case 'line_multi':
+        chart = nv.models.multiChart();
+        chart.interpolate(fd.line_interpolation);
+        break;
+
       case 'bar':
         chart = nv.models.multiBarChart()
         .showControls(fd.show_controls)
@@ -461,13 +475,19 @@ export default function nvd3Vis(slice, payload) {
       }
     }
 
-    if (vizType === 'dual_line') {
+    if (['dual_line', 'line_multi'].indexOf(vizType) >= 0) {
       const yAxisFormatter1 = d3.format(fd.y_axis_format);
       const yAxisFormatter2 = d3.format(fd.y_axis_2_format);
       chart.yAxis1.tickFormat(yAxisFormatter1);
       chart.yAxis2.tickFormat(yAxisFormatter2);
-      customizeToolTip(chart, xAxisFormatter, [yAxisFormatter1, yAxisFormatter2]);
-      chart.showLegend(width > BREAKPOINTS.small);
+      const yAxisFormatters = data.map(datum => (
+        datum.yAxis === 1 ? yAxisFormatter1 : yAxisFormatter2));
+      customizeToolTip(chart, xAxisFormatter, yAxisFormatters);
+      if (vizType === 'dual_line') {
+        chart.showLegend(width > BREAKPOINTS.small);
+      } else {
+        chart.showLegend(fd.show_legend);
+      }
     }
     chart.height(height);
     slice.container.css('height', height + 'px');
@@ -479,6 +499,31 @@ export default function nvd3Vis(slice, payload) {
     .attr('width', width)
     .call(chart);
 
+    // align yAxis1 and yAxis2 ticks
+    if (['dual_line', 'line_multi'].indexOf(vizType) >= 0) {
+      const count = chart.yAxis1.ticks();
+      const ticks1 = chart.yAxis1.scale().domain(chart.yAxis1.domain()).nice(count).ticks(count);
+      const ticks2 = chart.yAxis2.scale().domain(chart.yAxis2.domain()).nice(count).ticks(count);
+
+      // match number of ticks in both axes
+      const difference = ticks1.length - ticks2.length;
+      if (ticks1.length && ticks2.length && difference !== 0) {
+        const smallest = difference < 0 ? ticks1 : ticks2;
+        const delta = smallest[1] - smallest[0];
+        for (let i = 0; i < Math.abs(difference); i++) {
+          if (i % 2 === 0) {
+            smallest.unshift(smallest[0] - delta);
+          } else {
+            smallest.push(smallest[smallest.length - 1] + delta);
+          }
+        }
+        chart.yDomain1([ticks1[0], ticks1[ticks1.length - 1]]);
+        chart.yDomain2([ticks2[0], ticks2[ticks2.length - 1]]);
+        chart.yAxis1.tickValues(ticks1);
+        chart.yAxis2.tickValues(ticks2);
+      }
+    }
+
     if (fd.show_markers) {
       svg.selectAll('.nv-point')
       .style('stroke-opacity', 1)
@@ -516,12 +561,9 @@ export default function nvd3Vis(slice, payload) {
         margins.bottom = 40;
       }
 
-      if (vizType === 'dual_line') {
+      if (['dual_line', 'line_multi'].indexOf(vizType) >= 0) {
         const maxYAxis2LabelWidth = getMaxLabelSize(slice.container, 'nv-y2');
-        // use y axis width if it's wider than axis width/height
-        if (maxYAxis2LabelWidth > maxXAxisLabelHeight) {
-          margins.right = maxYAxis2LabelWidth + marginPad;
-        }
+        margins.right = maxYAxis2LabelWidth + marginPad;
       }
       if (fd.bottom_margin && fd.bottom_margin !== 'auto') {
         margins.bottom = parseInt(fd.bottom_margin, 10);
@@ -600,7 +642,13 @@ export default function nvd3Vis(slice, payload) {
         } else {
           xMin = chart.xAxis.scale().domain()[0].valueOf();
           xMax = chart.xAxis.scale().domain()[1].valueOf();
-          xScale = chart.xScale ? chart.xScale() : d3.scale.linear();
+          if (chart.xScale) {
+            xScale = chart.xScale();
+          } else if (chart.xAxis.scale) {
+            xScale = chart.xAxis.scale();
+          } else {
+            xScale = d3.scale.linear();
+          }
         }
         if (xScale && xScale.clamp) {
           xScale.clamp(true);
diff --git a/superset/cli.py b/superset/cli.py
index d31e003..a304664 100755
--- a/superset/cli.py
+++ b/superset/cli.py
@@ -186,6 +186,9 @@ def load_examples(load_test_data):
     print('Loading [BART lines]')
     data.load_bart_lines()
 
+    print('Loading [Multi Line]')
+    data.load_multi_line()
+
     if load_test_data:
         print('Loading [Unicode test data]')
         data.load_unicode_test_data()
diff --git a/superset/data/__init__.py b/superset/data/__init__.py
index d3d7da8..8451b95 100644
--- a/superset/data/__init__.py
+++ b/superset/data/__init__.py
@@ -1864,3 +1864,31 @@ def load_bart_lines():
     db.session.merge(tbl)
     db.session.commit()
     tbl.fetch_metadata()
+
+
+def load_multi_line():
+    load_world_bank_health_n_pop()
+    load_birth_names()
+    ids = [
+        row.id for row in
+        db.session.query(Slice).filter(
+            Slice.slice_name.in_(['Growth Rate', 'Trends']))
+    ]
+
+    slc = Slice(
+        datasource_type='table',  # not true, but needed
+        datasource_id=1,          # cannot be empty
+        slice_name="Multi Line",
+        viz_type='line_multi',
+        params=json.dumps({
+            "slice_name": "Multi Line",
+            "viz_type": "line_multi",
+            "line_charts": [ids[0]],
+            "line_charts_2": [ids[1]],
+            "since": "1960-01-01",
+            "prefix_metric_with_slice_name": True,
+        }),
+    )
+
+    misc_dash_slices.append(slc.slice_name)
+    merge_slice(slc)
diff --git a/superset/viz.py b/superset/viz.py
index 39d3411..1236dd8 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -1269,6 +1269,35 @@ class NVD3TimeSeriesViz(NVD3Viz):
         return chart_data
 
 
+class MultiLineViz(NVD3Viz):
+
+    """Pile on multiple line charts"""
+
+    viz_type = 'line_multi'
+    verbose_name = _('Time Series - Multiple Line Charts')
+
+    is_timeseries = True
+
+    def query_obj(self):
+        return None
+
+    def get_data(self, df):
+        fd = self.form_data
+        # Late imports to avoid circular import issues
+        from superset.models.core import Slice
+        from superset import db
+        slice_ids1 = fd.get('line_charts')
+        slices1 = db.session.query(Slice).filter(Slice.id.in_(slice_ids1)).all()
+        slice_ids2 = fd.get('line_charts_2')
+        slices2 = db.session.query(Slice).filter(Slice.id.in_(slice_ids2)).all()
+        return {
+            'slices': {
+                'axis1': [slc.data for slc in slices1],
+                'axis2': [slc.data for slc in slices2],
+            },
+        }
+
+
 class NVD3DualLineViz(NVD3Viz):
 
     """A rich line chart with dual axis"""
diff --git a/tests/core_tests.py b/tests/core_tests.py
index 4b0fc46..c454c00 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -370,7 +370,7 @@ class CoreTests(SupersetTestCase):
 
         data = self.get_json_resp(
             '/superset/warm_up_cache?table_name=energy_usage&db_name=main')
-        assert len(data) == 3
+        assert len(data) == 4
 
     def test_shortner(self):
         self.login(username='admin')

-- 
To stop receiving notification emails like this one, please contact
maximebeauchemin@apache.org.