Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add categories and time slider to arc deck.gl viz #5638

Merged
merged 3 commits into from
Aug 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion superset/assets/src/explore/visTypes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,8 @@ export const visTypes = {
{
label: t('Arc'),
controlSetRows: [
['color_picker', null],
['color_picker', 'legend_position'],
['dimension', 'color_scheme'],
['stroke_width', null],
],
},
Expand All @@ -772,6 +773,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