diff --git a/superset/assets/src/explore/visTypes.jsx b/superset/assets/src/explore/visTypes.jsx
index 2686b2d345347..5df65dfd9e1e9 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 7de070eab0069..57bd430dc9d53 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 0000000000000..39a202519b2ca
--- /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 (
+
+ );
+ }
+}
+
+CategoricalDeckGLContainer.propTypes = propTypes;
diff --git a/superset/assets/src/visualizations/deckgl/layers/arc.jsx b/superset/assets/src/visualizations/deckgl/layers/arc.jsx
index d34e7a13f60d3..b17e357326c80 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(
- ,
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 768978718e2e6..07590551ac860 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 (
-
- );
- }
-}
-
-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(
- ,
document.getElementById(slice.containerId),
);
diff --git a/superset/viz.py b/superset/viz.py
index 9113b038d04cb..14627488ff5fc 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):