You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@superset.apache.org by GitBox <gi...@apache.org> on 2018/08/22 04:31:05 UTC

[GitHub] mistercrunch closed pull request #5638: Add categories and time slider to arc deck.gl viz

mistercrunch closed pull request #5638: Add categories and time slider to arc deck.gl viz
URL: https://github.com/apache/incubator-superset/pull/5638
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/superset/assets/src/explore/visTypes.jsx b/superset/assets/src/explore/visTypes.jsx
index 4c5ae01f76..c9ffa4a175 100644
--- a/superset/assets/src/explore/visTypes.jsx
+++ b/superset/assets/src/explore/visTypes.jsx
@@ -758,7 +758,8 @@ export const visTypes = {
       {
         label: t('Arc'),
         controlSetRows: [
-          ['color_picker', null],
+          ['color_picker', 'legend_position'],
+          ['dimension', 'color_scheme'],
           ['stroke_width', null],
         ],
       },
@@ -772,6 +773,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 7de070eab0..57bd430dc9 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 0000000000..39a202519b
--- /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 d34e7a13f6..b17e357326 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 768978718e..07590551ac 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 e50ee1b77a..d9d73b0e33 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -2353,11 +2353,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):


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org