From 1dcde5317966020825e67d23c686e8ddbf00c398 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Mon, 9 Apr 2018 14:02:20 -0700 Subject: [PATCH] Add play slider to screengrid (#4647) * Improved granularity parsing * Add unit tests * Explicit cast to int * Screengrid play slider * Clean code * Refactor common code --- .../javascripts/explore/stores/controls.jsx | 2 +- superset/assets/javascripts/modules/time.js | 18 ++++ .../visualizations/deckgl/layers/scatter.jsx | 23 +--- .../deckgl/layers/screengrid.jsx | 102 +++++++++++++++--- superset/viz.py | 7 ++ 5 files changed, 121 insertions(+), 31 deletions(-) create mode 100644 superset/assets/javascripts/modules/time.js diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx index 5bd825a633a99..b675b03e50889 100644 --- a/superset/assets/javascripts/explore/stores/controls.jsx +++ b/superset/assets/javascripts/explore/stores/controls.jsx @@ -232,7 +232,7 @@ export const controls = { description: t('Choose the position of the legend'), type: 'SelectControl', clearable: false, - default: 'Top right', + default: 'tr', choices: [ ['tl', 'Top left'], ['tr', 'Top right'], diff --git a/superset/assets/javascripts/modules/time.js b/superset/assets/javascripts/modules/time.js new file mode 100644 index 0000000000000..0c13dae8591d7 --- /dev/null +++ b/superset/assets/javascripts/modules/time.js @@ -0,0 +1,18 @@ +import parseIsoDuration from 'parse-iso-duration'; + + +export const getPlaySliderParams = function (timestamps, timeGrain) { + let start = Math.min(...timestamps); + let end = Math.max(...timestamps); + + // lock start and end to the closest steps + const step = parseIsoDuration(timeGrain); + start -= start % step; + end += step - end % step; + + const values = timeGrain != null ? [start, start + step] : [start, end]; + const disabled = timestamps.every(timestamp => timestamp === null); + + return { start, end, step, values, disabled }; +}; + diff --git a/superset/assets/visualizations/deckgl/layers/scatter.jsx b/superset/assets/visualizations/deckgl/layers/scatter.jsx index 052a7ab7c1ce1..112f270fb059b 100644 --- a/superset/assets/visualizations/deckgl/layers/scatter.jsx +++ b/superset/assets/visualizations/deckgl/layers/scatter.jsx @@ -4,7 +4,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import parseIsoDuration from 'parse-iso-duration'; import { ScatterplotLayer } from 'deck.gl'; import AnimatableDeckGLContainer from '../AnimatableDeckGLContainer'; @@ -12,6 +11,7 @@ import Legend from '../../Legend'; import * as common from './common'; import { getColorFromScheme, hexToRGB } from '../../../javascripts/modules/colors'; +import { getPlaySliderParams } from '../../../javascripts/modules/time'; import { unitToRadius } from '../../../javascripts/modules/geo'; import sandboxedEval from '../../../javascripts/modules/sandbox'; @@ -97,20 +97,10 @@ class DeckGLScatter extends React.PureComponent { /* eslint-disable no-unused-vars */ static getDerivedStateFromProps(nextProps, prevState) { const fd = nextProps.slice.formData; - const timeGrain = fd.time_grain_sqla || fd.granularity || 'PT1M'; - // find start and end based on the data + const timeGrain = fd.time_grain_sqla || fd.granularity || 'PT1M'; const timestamps = nextProps.payload.data.features.map(f => f.__timestamp); - let start = Math.min(...timestamps); - let end = Math.max(...timestamps); - - // lock start and end to the closest steps - const step = parseIsoDuration(timeGrain); - start -= start % step; - end += step - end % step; - - const values = timeGrain != null ? [start, start + step] : [start, end]; - const disabled = timestamps.every(timestamp => timestamp === null); + const { start, end, step, values, disabled } = getPlaySliderParams(timestamps, timeGrain); const categories = getCategories(fd, nextProps.payload); @@ -200,14 +190,11 @@ class DeckGLScatter extends React.PureComponent { DeckGLScatter.propTypes = propTypes; function deckScatter(slice, payload, setControlValue) { - const layer = getLayer(slice.formData, payload, slice); const fd = slice.formData; - const width = slice.width(); - const height = slice.height(); let viewport = { ...fd.viewport, - width, - height, + width: slice.width(), + height: slice.height(), }; if (fd.autozoom) { diff --git a/superset/assets/visualizations/deckgl/layers/screengrid.jsx b/superset/assets/visualizations/deckgl/layers/screengrid.jsx index 7d6742e6e81d7..df11b5c5ee7ec 100644 --- a/superset/assets/visualizations/deckgl/layers/screengrid.jsx +++ b/superset/assets/visualizations/deckgl/layers/screengrid.jsx @@ -1,14 +1,22 @@ +/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */ + import React from 'react'; import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; import { ScreenGridLayer } from 'deck.gl'; -import DeckGLContainer from './../DeckGLContainer'; +import AnimatableDeckGLContainer from '../AnimatableDeckGLContainer'; import * as common from './common'; +import { getPlaySliderParams } from '../../../javascripts/modules/time'; import sandboxedEval from '../../../javascripts/modules/sandbox'; -function getLayer(formData, payload, slice) { +function getPoints(data) { + return data.map(d => d.position); +} + +function getLayer(formData, payload, slice, filters) { const fd = formData; const c = fd.color_picker; let data = payload.data.features.map(d => ({ @@ -22,6 +30,12 @@ function getLayer(formData, payload, slice) { data = jsFnMutator(data); } + if (filters != null) { + filters.forEach((f) => { + data = data.filter(f); + }); + } + // Passing a layer creator function instead of a layer since the // layer needs to be regenerated at each render return new ScreenGridLayer({ @@ -37,27 +51,91 @@ function getLayer(formData, payload, slice) { }); } -function getPoints(data) { - return data.map(d => d.position); +const propTypes = { + slice: PropTypes.object.isRequired, + payload: PropTypes.object.isRequired, + setControlValue: PropTypes.func.isRequired, + viewport: PropTypes.object.isRequired, +}; + +class DeckGLScreenGrid extends React.PureComponent { + /* eslint-disable no-unused-vars */ + static getDerivedStateFromProps(nextProps, prevState) { + 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); + + return { start, end, step, values, disabled }; + } + constructor(props) { + super(props); + this.state = DeckGLScreenGrid.getDerivedStateFromProps(props); + + this.getLayers = this.getLayers.bind(this); + } + componentWillReceiveProps(nextProps) { + this.setState(DeckGLScreenGrid.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]); + } + + const layer = getLayer( + this.props.slice.formData, + this.props.payload, + this.props.slice, + filters); + + return [layer]; + } + render() { + return ( +
+ +
+ ); + } } +DeckGLScreenGrid.propTypes = propTypes; + function deckScreenGrid(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.features)); } + ReactDOM.render( - , document.getElementById(slice.containerId), ); diff --git a/superset/viz.py b/superset/viz.py index 42e8e5a97a456..87dd1a9ebe01c 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -2103,11 +2103,18 @@ class DeckScreengrid(BaseDeckGLViz): viz_type = 'deck_screengrid' verbose_name = _('Deck.gl - Screen Grid') spatial_control_keys = ['spatial'] + is_timeseries = True + + def query_obj(self): + fd = self.form_data + self.is_timeseries = fd.get('time_grain_sqla') or fd.get('granularity') + return super(DeckScreengrid, self).query_obj() def get_properties(self, d): return { 'position': d.get('spatial'), 'weight': d.get(self.metric) or 1, + '__timestamp': d.get(DTTM_ALIAS) or d.get('__time'), }