Skip to content

Commit

Permalink
Add categories and time slider to arc deck.gl viz (apache#5638)
Browse files Browse the repository at this point in the history
* Fix legend position

* Add categories and play slider to arc viz

* New functionality to arc viz

(cherry picked from commit 6959b70)
  • Loading branch information
betodealmeida authored and kristw committed Sep 11, 2018
1 parent 4477f3c commit e874f88
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 187 deletions.
13 changes: 12 additions & 1 deletion superset/assets/src/explore/visTypes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,8 @@ export const visTypes = {
{
label: t('Arc'),
controlSetRows: [
['color_picker', null],
['color_picker', 'legend_position'],
['dimension', 'color_scheme'],
['stroke_width', null],
],
},
Expand All @@ -770,6 +771,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: {
Expand Down
1 change: 1 addition & 0 deletions superset/assets/src/visualizations/Legend.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
38 changes: 14 additions & 24 deletions superset/assets/src/visualizations/deckgl/layers/arc.jsx
Original file line number Diff line number Diff line change
@@ -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 = [];
Expand All @@ -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,
Expand All @@ -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),
);
Expand Down
Loading

0 comments on commit e874f88

Please sign in to comment.