Skip to content

Commit

Permalink
Legend for deck.gl scatterplot (#4572)
Browse files Browse the repository at this point in the history
* Initial work

* Working version

* Specify legend position

* Max height with scroll

* Fix lint

* Better compatibility with nvd3

* Fix object.keys polyfill version

* Fix lint
  • Loading branch information
betodealmeida authored and mistercrunch committed Mar 14, 2018
1 parent 86a03d1 commit 7089344
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 23 deletions.
14 changes: 14 additions & 0 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ export const controls = {
default: colorPrimary,
renderTrigger: true,
},
legend_position: {
label: t('Legend Position'),
description: t('Choose the position of the legend'),
type: 'SelectControl',
clearable: false,
default: 'Top right',
choices: [
['tl', 'Top left'],
['tr', 'Top right'],
['bl', 'Bottom left'],
['br', 'Bottom right'],
],
renderTrigger: true,
},

fill_color_picker: {
label: t('Fill Color'),
Expand Down
2 changes: 1 addition & 1 deletion superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ export const visTypes = {
{
label: t('Point Color'),
controlSetRows: [
['color_picker', null],
['color_picker', 'legend_position'],
['dimension', 'color_scheme'],
],
},
Expand Down
3 changes: 3 additions & 0 deletions superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
"mousetrap": "^1.6.1",
"mustache": "^2.2.1",
"nvd3": "1.8.6",
"object.entries": "^1.0.4",
"object.keys": "^0.1.0",
"object.values": "^1.0.4",
"po2json": "^0.4.5",
"prop-types": "^15.6.0",
"react": "^15.6.2",
Expand Down
22 changes: 22 additions & 0 deletions superset/assets/visualizations/Legend.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
div.legend {
font-size: 90%;
position: absolute;
background: #fff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
margin: 24px;
padding: 12px 24px;
outline: none;
overflow-y: scroll;
max-height: 200px;
}

ul.categories {
list-style: none;
padding-left: 0;
margin: 0;
}

ul.categories li a {
color: rgb(51, 51, 51);
text-decoration: none;
}
58 changes: 58 additions & 0 deletions superset/assets/visualizations/Legend.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';

import './Legend.css';

const propTypes = {
categories: PropTypes.object,
toggleCategory: PropTypes.func,
showSingleCategory: PropTypes.func,
position: PropTypes.oneOf(['tl', 'tr', 'bl', 'br']),
};

const defaultProps = {
categories: {},
toggleCategory: () => {},
showSingleCategory: () => {},
position: 'tr',
};

export default class Legend extends React.PureComponent {
render() {
if (Object.keys(this.props.categories).length === 0) {
return null;
}

const categories = Object.entries(this.props.categories).map(([k, v]) => {
const style = { color: 'rgba(' + v.color.join(', ') + ')' };
const icon = v.enabled ? '\u25CF' : '\u25CB';
return (
<li>
<a
href="#"
onClick={() => this.props.toggleCategory(k)}
onDoubleClick={() => this.props.showSingleCategory(k)}
>
<span style={style}>{icon}</span> {k}
</a>
</li>
);
});

const vertical = this.props.position.charAt(0) === 't' ? 'top' : 'bottom';
const horizontal = this.props.position.charAt(1) === 'r' ? 'right' : 'left';
const style = {
[vertical]: '0px',
[horizontal]: '10px',
};

return (
<div className={'legend'} style={style}>
<ul className={'categories'}>{categories}</ul>
</div>
);
}
}

Legend.propTypes = propTypes;
Legend.defaultProps = defaultProps;
6 changes: 4 additions & 2 deletions superset/assets/visualizations/PlaySlider.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.play-slider {
height: 100px;
margin-top: -5px;
position: absolute;
bottom: -16px;
height: 20px;
width: 100%;
}

.slider-selection {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const propTypes = {
values: PropTypes.array.isRequired,
disabled: PropTypes.bool,
viewport: PropTypes.object.isRequired,
children: PropTypes.node,
};

const defaultProps = {
Expand Down Expand Up @@ -48,6 +49,7 @@ export default class AnimatableDeckGLContainer extends React.Component {
onChange={newValues => this.setState({ values: newValues })}
/>
}
{this.props.children}
</div>
);
}
Expand Down
104 changes: 84 additions & 20 deletions superset/assets/visualizations/deckgl/layers/scatter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
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';
Expand Down Expand Up @@ -39,7 +40,27 @@ function getPoints(data) {
return data.map(d => d.position);
}

function getLayer(formData, payload, slice, inFrame) {
function getCategories(formData, payload) {
const fd = formData;
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 = {};

payload.data.features.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;
}

function getLayer(formData, payload, slice, filters) {
const fd = formData;
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
Expand Down Expand Up @@ -68,8 +89,10 @@ function getLayer(formData, payload, slice, inFrame) {
data = jsFnMutator(data);
}

if (inFrame != null) {
data = data.filter(inFrame);
if (filters != null) {
filters.forEach((f) => {
data = data.filter(f);
});
}

return new ScatterplotLayer({
Expand Down Expand Up @@ -109,46 +132,87 @@ class DeckGLScatter extends React.PureComponent {
const values = timeGrain != null ? [start, start + step] : [start, end];
const disabled = timestamps.every(timestamp => timestamp === null);

return { start, end, step, values, disabled };
const categories = getCategories(fd, nextProps.payload);

return { start, end, step, values, disabled, categories };
}
constructor(props) {
super(props);
this.state = DeckGLScatter.getDerivedStateFromProps(props);

this.getLayers = this.getLayers.bind(this);
this.toggleCategory = this.toggleCategory.bind(this);
this.showSingleCategory = this.showSingleCategory.bind(this);
}
componentWillReceiveProps(nextProps) {
this.setState(DeckGLScatter.getDerivedStateFromProps(nextProps, this.state));
}
getLayers(values) {
let inFrame;
const filters = [];

// time filter
if (values[0] === values[1] || values[1] === this.end) {
inFrame = t => t.__timestamp >= values[0] && t.__timestamp <= values[1];
filters.push(d => d.__timestamp >= values[0] && d.__timestamp <= values[1]);
} else {
inFrame = t => t.__timestamp >= values[0] && t.__timestamp < values[1];
filters.push(d => d.__timestamp >= values[0] && d.__timestamp < values[1]);
}

// legend filter
if (this.props.slice.formData.dimension) {
filters.push(d => this.state.categories[d.cat_color].enabled);
}

const layer = getLayer(
this.props.slice.formData,
this.props.payload,
this.props.slice,
inFrame);
filters);

return [layer];
}
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 (
<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>
<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}
>
<Legend
categories={this.state.categories}
toggleCategory={this.toggleCategory}
showSingleCategory={this.showSingleCategory}
position={this.props.slice.formData.legend_position}
/>
</AnimatableDeckGLContainer>
</div>
);
}
}
Expand Down

0 comments on commit 7089344

Please sign in to comment.