Add play slider to screengrid (#4647)

* Improved granularity parsing

* Add unit tests

* Explicit cast to int

* Screengrid play slider

* Clean code

* Refactor common code
This commit is contained in:
Beto Dealmeida 2018-04-09 14:02:20 -07:00 committed by Maxime Beauchemin
parent 41c158edac
commit 47c085fd00
5 changed files with 121 additions and 31 deletions

View File

@ -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'],

View File

@ -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 };
};

View File

@ -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 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) {

View File

@ -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 (
<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}
/>
</div>
);
}
}
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(
<DeckGLContainer
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={[layer]}
mapStyle={slice.formData.mapbox_style}
<DeckGLScreenGrid
slice={slice}
payload={payload}
setControlValue={setControlValue}
viewport={viewport}
/>,
document.getElementById(slice.containerId),
);

View File

@ -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'),
}