diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index e9d73013d..800e1dfde 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -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'),
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index 836da7b9c..dd144c55c 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -679,7 +679,7 @@ export const visTypes = {
{
label: t('Point Color'),
controlSetRows: [
- ['color_picker', null],
+ ['color_picker', 'legend_position'],
['dimension', 'color_scheme'],
],
},
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 1c5089572..7d41be372 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -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",
diff --git a/superset/assets/visualizations/Legend.css b/superset/assets/visualizations/Legend.css
new file mode 100644
index 000000000..4c6222d79
--- /dev/null
+++ b/superset/assets/visualizations/Legend.css
@@ -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;
+}
diff --git a/superset/assets/visualizations/Legend.jsx b/superset/assets/visualizations/Legend.jsx
new file mode 100644
index 000000000..7de070eab
--- /dev/null
+++ b/superset/assets/visualizations/Legend.jsx
@@ -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 (
+
+ this.props.toggleCategory(k)}
+ onDoubleClick={() => this.props.showSingleCategory(k)}
+ >
+ {icon} {k}
+
+
+ );
+ });
+
+ 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 (
+
+ );
+ }
+}
+
+Legend.propTypes = propTypes;
+Legend.defaultProps = defaultProps;
diff --git a/superset/assets/visualizations/PlaySlider.css b/superset/assets/visualizations/PlaySlider.css
index e4338d963..7de07de54 100644
--- a/superset/assets/visualizations/PlaySlider.css
+++ b/superset/assets/visualizations/PlaySlider.css
@@ -1,6 +1,8 @@
.play-slider {
- height: 100px;
- margin-top: -5px;
+ position: absolute;
+ bottom: -16px;
+ height: 20px;
+ width: 100%;
}
.slider-selection {
diff --git a/superset/assets/visualizations/deckgl/AnimatableDeckGLContainer.jsx b/superset/assets/visualizations/deckgl/AnimatableDeckGLContainer.jsx
index 0343421bd..3bee4a4d4 100644
--- a/superset/assets/visualizations/deckgl/AnimatableDeckGLContainer.jsx
+++ b/superset/assets/visualizations/deckgl/AnimatableDeckGLContainer.jsx
@@ -12,6 +12,7 @@ const propTypes = {
values: PropTypes.array.isRequired,
disabled: PropTypes.bool,
viewport: PropTypes.object.isRequired,
+ children: PropTypes.node,
};
const defaultProps = {
@@ -48,6 +49,7 @@ export default class AnimatableDeckGLContainer extends React.Component {
onChange={newValues => this.setState({ values: newValues })}
/>
}
+ {this.props.children}
);
}
diff --git a/superset/assets/visualizations/deckgl/layers/scatter.jsx b/superset/assets/visualizations/deckgl/layers/scatter.jsx
index 26d320c50..3d8e99cb7 100644
--- a/superset/assets/visualizations/deckgl/layers/scatter.jsx
+++ b/superset/assets/visualizations/deckgl/layers/scatter.jsx
@@ -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';
@@ -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];
@@ -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({
@@ -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 (
-
+
);
}
}