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/08/22 04:31:06 UTC

[incubator-superset] branch master updated: Add categories and time slider to arc deck.gl viz (#5638)

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 6959b70  Add categories and time slider to arc deck.gl viz (#5638)
6959b70 is described below

commit 6959b70c1c2801b0c828b332fa79f562495fcd2f
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Tue Aug 21 21:31:03 2018 -0700

    Add categories and time slider to arc deck.gl viz (#5638)
    
    * Fix legend position
    
    * Add categories and play slider to arc viz
    
    * New functionality to arc viz
---
 superset/assets/src/explore/visTypes.jsx           |  13 +-
 superset/assets/src/visualizations/Legend.jsx      |   1 +
 .../deckgl/CategoricalDeckGLContainer.jsx          | 158 +++++++++++++++++++
 .../src/visualizations/deckgl/layers/arc.jsx       |  38 ++---
 .../src/visualizations/deckgl/layers/scatter.jsx   | 171 ++-------------------
 superset/viz.py                                    |  10 ++
 6 files changed, 204 insertions(+), 187 deletions(-)

diff --git a/superset/assets/src/explore/visTypes.jsx b/superset/assets/src/explore/visTypes.jsx
index 2686b2d..5df65df 100644
--- a/superset/assets/src/explore/visTypes.jsx
+++ b/superset/assets/src/explore/visTypes.jsx
@@ -759,7 +759,8 @@ export const visTypes = {
       {
         label: t('Arc'),
         controlSetRows: [
-          ['color_picker', null],
+          ['color_picker', 'legend_position'],
+          ['dimension', 'color_scheme'],
           ['stroke_width', null],
         ],
       },
@@ -773,6 +774,16 @@ export const visTypes = {
         ],
       },
     ],
+    controlOverrides: {
+      dimension: {
+        label: t('Categorical Color'),
+        description: t('Pick a dimension from which categorical colors are defined'),
+      },
+      size: {
+        validators: [],
+      },
+      time_grain_sqla: timeGrainSqlaAnimationOverrides,
+    },
   },
 
   deck_scatter: {
diff --git a/superset/assets/src/visualizations/Legend.jsx b/superset/assets/src/visualizations/Legend.jsx
index 7de070e..57bd430 100644
--- a/superset/assets/src/visualizations/Legend.jsx
+++ b/superset/assets/src/visualizations/Legend.jsx
@@ -42,6 +42,7 @@ export default class Legend extends React.PureComponent {
     const vertical = this.props.position.charAt(0) === 't' ? 'top' : 'bottom';
     const horizontal = this.props.position.charAt(1) === 'r' ? 'right' : 'left';
     const style = {
+      position: 'absolute',
       [vertical]: '0px',
       [horizontal]: '10px',
     };
diff --git a/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx b/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx
new file mode 100644
index 0000000..39a2025
--- /dev/null
+++ b/superset/assets/src/visualizations/deckgl/CategoricalDeckGLContainer.jsx
@@ -0,0 +1,158 @@
+/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import AnimatableDeckGLContainer from './AnimatableDeckGLContainer';
+import Legend from '../Legend';
+
+import { getColorFromScheme, hexToRGB } from '../../modules/colors';
+import { getPlaySliderParams } from '../../modules/time';
+import sandboxedEval from '../../modules/sandbox';
+
+function getCategories(fd, data) {
+  const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
+  const fixedColor = [c.r, c.g, c.b, 255 * c.a];
+  const categories = {};
+  data.forEach((d) => {
+    if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
+      let color;
+      if (fd.dimension) {
+        color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
+      } else {
+        color = fixedColor;
+      }
+      categories[d.cat_color] = { color, enabled: true };
+    }
+  });
+  return categories;
+}
+
+const propTypes = {
+  slice: PropTypes.object.isRequired,
+  data: PropTypes.array.isRequired,
+  mapboxApiKey: PropTypes.string.isRequired,
+  setControlValue: PropTypes.func.isRequired,
+  viewport: PropTypes.object.isRequired,
+  getLayer: PropTypes.func.isRequired,
+};
+
+export default class CategoricalDeckGLContainer extends React.PureComponent {
+  /*
+   * A Deck.gl container that handles categories.
+   *
+   * The container will have an interactive legend, populated from the
+   * categories present in the data.
+   */
+
+  /* eslint-disable-next-line react/sort-comp */
+  static getDerivedStateFromProps(nextProps) {
+    const fd = nextProps.slice.formData;
+
+    const timeGrain = fd.time_grain_sqla || fd.granularity || 'PT1M';
+    const timestamps = nextProps.data.map(f => f.__timestamp);
+    const { start, end, step, values, disabled } = getPlaySliderParams(timestamps, timeGrain);
+    const categories = getCategories(fd, nextProps.data);
+
+    return { start, end, step, values, disabled, categories };
+  }
+  constructor(props) {
+    super(props);
+    this.state = CategoricalDeckGLContainer.getDerivedStateFromProps(props);
+
+    this.getLayers = this.getLayers.bind(this);
+    this.toggleCategory = this.toggleCategory.bind(this);
+    this.showSingleCategory = this.showSingleCategory.bind(this);
+  }
+  componentWillReceiveProps(nextProps) {
+    this.setState(CategoricalDeckGLContainer.getDerivedStateFromProps(nextProps, this.state));
+  }
+  addColor(data, fd) {
+    const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
+    const fixedColor = [c.r, c.g, c.b, 255 * c.a];
+
+    return data.map((d) => {
+      let color;
+      if (fd.dimension) {
+        color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
+      } else {
+        color = fixedColor;
+      }
+      return { ...d, color };
+    });
+  }
+  getLayers(values) {
+    const fd = this.props.slice.formData;
+    let data = [...this.props.data];
+
+    // Add colors from categories or fixed color
+    data = this.addColor(data, fd);
+
+    // Apply user defined data mutator if defined
+    if (fd.js_data_mutator) {
+      const jsFnMutator = sandboxedEval(fd.js_data_mutator);
+      data = jsFnMutator(data);
+    }
+
+    // Filter by time
+    if (values[0] === values[1] || values[1] === this.end) {
+      data = data.filter(d => d.__timestamp >= values[0] && d.__timestamp <= values[1]);
+    } else {
+      data = data.filter(d => d.__timestamp >= values[0] && d.__timestamp < values[1]);
+    }
+
+    // Show only categories selected in the legend
+    if (fd.dimension) {
+      data = data.filter(d => this.state.categories[d.cat_color].enabled);
+    }
+
+    return [this.props.getLayer(fd, data, this.props.slice)];
+  }
+  toggleCategory(category) {
+    const categoryState = this.state.categories[category];
+    categoryState.enabled = !categoryState.enabled;
+    const categories = { ...this.state.categories, [category]: categoryState };
+
+    // if all categories are disabled, enable all -- similar to nvd3
+    if (Object.values(categories).every(v => !v.enabled)) {
+      /* eslint-disable no-param-reassign */
+      Object.values(categories).forEach((v) => { v.enabled = true; });
+    }
+
+    this.setState({ categories });
+  }
+  showSingleCategory(category) {
+    const categories = { ...this.state.categories };
+    /* eslint-disable no-param-reassign */
+    Object.values(categories).forEach((v) => { v.enabled = false; });
+    categories[category].enabled = true;
+    this.setState({ categories });
+  }
+  render() {
+    return (
+      <div style={{ position: 'relative' }}>
+        <AnimatableDeckGLContainer
+          getLayers={this.getLayers}
+          start={this.state.start}
+          end={this.state.end}
+          step={this.state.step}
+          values={this.state.values}
+          disabled={this.state.disabled}
+          viewport={this.props.viewport}
+          mapboxApiAccessToken={this.props.mapboxApiKey}
+          mapStyle={this.props.slice.formData.mapbox_style}
+          setControlValue={this.props.setControlValue}
+        >
+          <Legend
+            categories={this.state.categories}
+            toggleCategory={this.toggleCategory}
+            showSingleCategory={this.showSingleCategory}
+            position={this.props.slice.formData.legend_position}
+          />
+        </AnimatableDeckGLContainer>
+      </div>
+    );
+  }
+}
+
+CategoricalDeckGLContainer.propTypes = propTypes;
diff --git a/superset/assets/src/visualizations/deckgl/layers/arc.jsx b/superset/assets/src/visualizations/deckgl/layers/arc.jsx
index d34e7a1..b17e357 100644
--- a/superset/assets/src/visualizations/deckgl/layers/arc.jsx
+++ b/superset/assets/src/visualizations/deckgl/layers/arc.jsx
@@ -1,12 +1,13 @@
+/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
+
 import React from 'react';
 import ReactDOM from 'react-dom';
 
 import { ArcLayer } from 'deck.gl';
 
-import DeckGLContainer from './../DeckGLContainer';
+import CategoricalDeckGLContainer from '../CategoricalDeckGLContainer';
 
 import * as common from './common';
-import sandboxedEval from '../../../modules/sandbox';
 
 function getPoints(data) {
   const points = [];
@@ -17,20 +18,7 @@ function getPoints(data) {
   return points;
 }
 
-function getLayer(formData, payload, slice) {
-  const fd = formData;
-  const fc = fd.color_picker;
-  let data = payload.data.arcs.map(d => ({
-    ...d,
-    color: [fc.r, fc.g, fc.b, 255 * fc.a],
-  }));
-
-  if (fd.js_data_mutator) {
-    // Applying user defined data mutator if defined
-    const jsFnMutator = sandboxedEval(fd.js_data_mutator);
-    data = jsFnMutator(data);
-  }
-
+function getLayer(fd, data, slice) {
   return new ArcLayer({
     id: `path-layer-${fd.slice_id}`,
     data,
@@ -40,23 +28,25 @@ function getLayer(formData, payload, slice) {
 }
 
 function deckArc(slice, payload, setControlValue) {
-  const layer = getLayer(slice.formData, payload, slice);
+  const fd = slice.formData;
   let viewport = {
-    ...slice.formData.viewport,
+    ...fd.viewport,
     width: slice.width(),
     height: slice.height(),
   };
 
-  if (slice.formData.autozoom) {
+  if (fd.autozoom) {
     viewport = common.fitViewport(viewport, getPoints(payload.data.arcs));
   }
+
   ReactDOM.render(
-    <DeckGLContainer
-      mapboxApiAccessToken={payload.data.mapboxApiKey}
-      viewport={viewport}
-      layers={[layer]}
-      mapStyle={slice.formData.mapbox_style}
+    <CategoricalDeckGLContainer
+      slice={slice}
+      data={payload.data.arcs}
+      mapboxApiKey={payload.data.mapboxApiKey}
       setControlValue={setControlValue}
+      viewport={viewport}
+      getLayer={getLayer}
     />,
     document.getElementById(slice.containerId),
   );
diff --git a/superset/assets/src/visualizations/deckgl/layers/scatter.jsx b/superset/assets/src/visualizations/deckgl/layers/scatter.jsx
index 7689787..0759055 100644
--- a/superset/assets/src/visualizations/deckgl/layers/scatter.jsx
+++ b/superset/assets/src/visualizations/deckgl/layers/scatter.jsx
@@ -2,82 +2,30 @@
 
 import React from 'react';
 import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
 
 import { ScatterplotLayer } from 'deck.gl';
 
-import AnimatableDeckGLContainer from '../AnimatableDeckGLContainer';
-import Legend from '../../Legend';
-
+import CategoricalDeckGLContainer from '../CategoricalDeckGLContainer';
 import * as common from './common';
-import { getColorFromScheme, hexToRGB } from '../../../modules/colors';
-import { getPlaySliderParams } from '../../../modules/time';
 import { unitToRadius } from '../../../modules/geo';
-import sandboxedEval from '../../../modules/sandbox';
 
 
 function getPoints(data) {
   return data.map(d => d.position);
 }
 
-function getCategories(formData, payload) {
-  const fd = formData;
-  const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
-  const fixedColor = [c.r, c.g, c.b, 255 * c.a];
-  const categories = {};
-
-  payload.data.features.forEach((d) => {
-    if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
-      let color;
-      if (fd.dimension) {
-        color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
-      } else {
-        color = fixedColor;
-      }
-      categories[d.cat_color] = { color, enabled: true };
-    }
-  });
-  return categories;
-}
-
-function getLayer(formData, payload, slice, filters) {
-  const fd = formData;
-  const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
-  const fixedColor = [c.r, c.g, c.b, 255 * c.a];
-
-  let data = payload.data.features.map((d) => {
+function getLayer(fd, data, slice) {
+  const dataWithRadius = data.map((d) => {
     let radius = unitToRadius(fd.point_unit, d.radius) || 10;
     if (fd.multiplier) {
       radius *= fd.multiplier;
     }
-    let color;
-    if (fd.dimension) {
-      color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
-    } else {
-      color = fixedColor;
-    }
-    return {
-      ...d,
-      radius,
-      color,
-    };
+    return { ...d, radius };
   });
 
-  if (fd.js_data_mutator) {
-    // Applying user defined data mutator if defined
-    const jsFnMutator = sandboxedEval(fd.js_data_mutator);
-    data = jsFnMutator(data);
-  }
-
-  if (filters != null) {
-    filters.forEach((f) => {
-      data = data.filter(f);
-    });
-  }
-
   return new ScatterplotLayer({
     id: `scatter-layer-${fd.slice_id}`,
-    data,
+    data: dataWithRadius,
     fp64: true,
     radiusMinPixels: fd.min_radius || null,
     radiusMaxPixels: fd.max_radius || null,
@@ -86,109 +34,6 @@ function getLayer(formData, payload, slice, filters) {
   });
 }
 
-const propTypes = {
-  slice: PropTypes.object.isRequired,
-  payload: PropTypes.object.isRequired,
-  setControlValue: PropTypes.func.isRequired,
-  viewport: PropTypes.object.isRequired,
-};
-
-class DeckGLScatter extends React.PureComponent {
-  /* eslint-disable-next-line react/sort-comp */
-  static getDerivedStateFromProps(nextProps) {
-    const fd = nextProps.slice.formData;
-
-    const timeGrain = fd.time_grain_sqla || fd.granularity || 'PT1M';
-    const timestamps = nextProps.payload.data.features.map(f => f.__timestamp);
-    const { start, end, step, values, disabled } = getPlaySliderParams(timestamps, timeGrain);
-
-    const categories = getCategories(fd, nextProps.payload);
-
-    return { start, end, step, values, disabled, categories };
-  }
-  constructor(props) {
-    super(props);
-    this.state = DeckGLScatter.getDerivedStateFromProps(props);
-
-    this.getLayers = this.getLayers.bind(this);
-    this.toggleCategory = this.toggleCategory.bind(this);
-    this.showSingleCategory = this.showSingleCategory.bind(this);
-  }
-  componentWillReceiveProps(nextProps) {
-    this.setState(DeckGLScatter.getDerivedStateFromProps(nextProps, this.state));
-  }
-  getLayers(values) {
-    const filters = [];
-
-    // time filter
-    if (values[0] === values[1] || values[1] === this.end) {
-      filters.push(d => d.__timestamp >= values[0] && d.__timestamp <= values[1]);
-    } else {
-      filters.push(d => d.__timestamp >= values[0] && d.__timestamp < values[1]);
-    }
-
-    // legend filter
-    if (this.props.slice.formData.dimension) {
-      filters.push(d => this.state.categories[d.cat_color].enabled);
-    }
-
-    const layer = getLayer(
-      this.props.slice.formData,
-      this.props.payload,
-      this.props.slice,
-      filters);
-
-    return [layer];
-  }
-  toggleCategory(category) {
-    const categoryState = this.state.categories[category];
-    categoryState.enabled = !categoryState.enabled;
-    const categories = { ...this.state.categories, [category]: categoryState };
-
-    // if all categories are disabled, enable all -- similar to nvd3
-    if (Object.values(categories).every(v => !v.enabled)) {
-      /* eslint-disable no-param-reassign */
-      Object.values(categories).forEach((v) => { v.enabled = true; });
-    }
-
-    this.setState({ categories });
-  }
-  showSingleCategory(category) {
-    const categories = { ...this.state.categories };
-    /* eslint-disable no-param-reassign */
-    Object.values(categories).forEach((v) => { v.enabled = false; });
-    categories[category].enabled = true;
-    this.setState({ categories });
-  }
-  render() {
-    return (
-      <div>
-        <AnimatableDeckGLContainer
-          getLayers={this.getLayers}
-          start={this.state.start}
-          end={this.state.end}
-          step={this.state.step}
-          values={this.state.values}
-          disabled={this.state.disabled}
-          viewport={this.props.viewport}
-          mapboxApiAccessToken={this.props.payload.data.mapboxApiKey}
-          mapStyle={this.props.slice.formData.mapbox_style}
-          setControlValue={this.props.setControlValue}
-        >
-          <Legend
-            categories={this.state.categories}
-            toggleCategory={this.toggleCategory}
-            showSingleCategory={this.showSingleCategory}
-            position={this.props.slice.formData.legend_position}
-          />
-        </AnimatableDeckGLContainer>
-      </div>
-    );
-  }
-}
-
-DeckGLScatter.propTypes = propTypes;
-
 function deckScatter(slice, payload, setControlValue) {
   const fd = slice.formData;
   let viewport = {
@@ -202,11 +47,13 @@ function deckScatter(slice, payload, setControlValue) {
   }
 
   ReactDOM.render(
-    <DeckGLScatter
+    <CategoricalDeckGLContainer
       slice={slice}
-      payload={payload}
+      data={payload.data.features}
+      mapboxApiKey={payload.data.mapboxApiKey}
       setControlValue={setControlValue}
       viewport={viewport}
+      getLayer={getLayer}
     />,
     document.getElementById(slice.containerId),
   );
diff --git a/superset/viz.py b/superset/viz.py
index 9113b03..1462748 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -2381,11 +2381,21 @@ class DeckArc(BaseDeckGLViz):
     viz_type = 'deck_arc'
     verbose_name = _('Deck.gl - Arc')
     spatial_control_keys = ['start_spatial', 'end_spatial']
+    is_timeseries = True
+
+    def query_obj(self):
+        fd = self.form_data
+        self.is_timeseries = bool(
+            fd.get('time_grain_sqla') or fd.get('granularity'))
+        return super(DeckArc, self).query_obj()
 
     def get_properties(self, d):
+        dim = self.form_data.get('dimension')
         return {
             'sourcePosition': d.get('start_spatial'),
             'targetPosition': d.get('end_spatial'),
+            'cat_color': d.get(dim) if dim else None,
+            DTTM_ALIAS: d.get(DTTM_ALIAS),
         }
 
     def get_data(self, df):