Skip to content

Commit

Permalink
Add play slider to screengrid (#4647)
Browse files Browse the repository at this point in the history
* Improved granularity parsing

* Add unit tests

* Explicit cast to int

* Screengrid play slider

* Clean code

* Refactor common code
  • Loading branch information
betodealmeida authored and mistercrunch committed Apr 9, 2018
1 parent 41c158e commit 47c085f
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 31 deletions.
2 changes: 1 addition & 1 deletion superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
18 changes: 18 additions & 0 deletions superset/assets/javascripts/modules/time.js
Original file line number Diff line number Diff line change
@@ -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 };
};

23 changes: 5 additions & 18 deletions superset/assets/visualizations/deckgl/layers/scatter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ 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';
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';

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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) {
Expand Down
102 changes: 90 additions & 12 deletions superset/assets/visualizations/deckgl/layers/screengrid.jsx
Original file line number Diff line number Diff line change
@@ -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 => ({
Expand All @@ -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({
Expand All @@ -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),
);
Expand Down
7 changes: 7 additions & 0 deletions superset/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
}


Expand Down

0 comments on commit 47c085f

Please sign in to comment.