diff --git a/superset/assets/package.json b/superset/assets/package.json
index afc983b322..bded7ece77 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -61,7 +61,7 @@
"d3-tip": "^0.9.1",
"datamaps": "^0.5.8",
"datatables.net-bs": "^1.10.15",
- "deck.gl": "^5.1.4",
+ "deck.gl": "^5.3.4",
"distributions": "^1.0.0",
"dnd-core": "^2.6.0",
"dompurify": "^1.0.3",
@@ -72,7 +72,6 @@
"jed": "^1.1.1",
"jquery": "3.1.1",
"lodash.throttle": "^4.1.1",
- "luma.gl": "^5.1.4",
"mapbox-gl": "^0.45.0",
"mathjs": "^3.20.2",
"moment": "^2.20.1",
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index ed49eea684..c593cee3b2 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -189,7 +189,7 @@ class Chart extends React.PureComponent {
className="chart-tooltip"
id="chart-tooltip"
placement="right"
- positionTop={this.state.tooltip.y - 10}
+ positionTop={this.state.tooltip.y + 30}
positionLeft={this.state.tooltip.x + 30}
arrowOffsetTop={10}
>
diff --git a/superset/assets/src/components/BootstrapSliderWrapper.css b/superset/assets/src/components/BootstrapSliderWrapper.css
new file mode 100644
index 0000000000..f2fef455e7
--- /dev/null
+++ b/superset/assets/src/components/BootstrapSliderWrapper.css
@@ -0,0 +1,8 @@
+.BootstrapSliderWrapper .slider-selection {
+ background: #efefef;
+}
+
+.BootstrapSliderWrapper .slider-handle {
+ background: #b3b3b3;
+}
+
diff --git a/superset/assets/src/components/BootstrapSliderWrapper.jsx b/superset/assets/src/components/BootstrapSliderWrapper.jsx
new file mode 100644
index 0000000000..6bcda11f65
--- /dev/null
+++ b/superset/assets/src/components/BootstrapSliderWrapper.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import ReactBootstrapSlider from 'react-bootstrap-slider';
+import 'bootstrap-slider/dist/css/bootstrap-slider.min.css';
+import './BootstrapSliderWrapper.css';
+
+export default function BootstrapSliderWrapper(props) {
+ return (
+
+
+
+ );
+}
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index 1479f88311..c703af8f6f 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -95,46 +95,34 @@ export default function dashboardStateReducer(state = {}, action) {
let filters = state.filters;
const { chart, col, vals: nextVals, merge, refresh } = action;
const sliceId = chart.id;
- const filterKeys = [
- '__time_range',
- '__time_col',
- '__time_grain',
- '__time_origin',
- '__granularity',
- ];
- if (
- filterKeys.indexOf(col) >= 0 ||
- action.chart.formData.groupby.indexOf(col) !== -1
- ) {
- let newFilter = {};
- if (!(sliceId in filters)) {
- // if no filters existed for the slice, set them
- newFilter = { [col]: nextVals };
- } else if ((filters[sliceId] && !(col in filters[sliceId])) || !merge) {
- // If no filters exist for this column, or we are overwriting them
- newFilter = { ...filters[sliceId], [col]: nextVals };
- } else if (filters[sliceId][col] instanceof Array) {
- newFilter[col] = [...filters[sliceId][col], ...nextVals];
- } else {
- newFilter[col] = [filters[sliceId][col], ...nextVals];
- }
- filters = { ...filters, [sliceId]: newFilter };
+ let newFilter = {};
+ if (!(sliceId in filters)) {
+ // if no filters existed for the slice, set them
+ newFilter = { [col]: nextVals };
+ } else if ((filters[sliceId] && !(col in filters[sliceId])) || !merge) {
+ // If no filters exist for this column, or we are overwriting them
+ newFilter = { ...filters[sliceId], [col]: nextVals };
+ } else if (filters[sliceId][col] instanceof Array) {
+ newFilter[col] = [...filters[sliceId][col], ...nextVals];
+ } else {
+ newFilter[col] = [filters[sliceId][col], ...nextVals];
+ }
+ filters = { ...filters, [sliceId]: newFilter };
- // remove any empty filters so they don't pollute the logs
- Object.keys(filters).forEach(chartId => {
- Object.keys(filters[chartId]).forEach(column => {
- if (
- !filters[chartId][column] ||
- filters[chartId][column].length === 0
- ) {
- delete filters[chartId][column];
- }
- });
- if (Object.keys(filters[chartId]).length === 0) {
- delete filters[chartId];
+ // remove any empty filters so they don't pollute the logs
+ Object.keys(filters).forEach(chartId => {
+ Object.keys(filters[chartId]).forEach(column => {
+ if (
+ !filters[chartId][column] ||
+ filters[chartId][column].length === 0
+ ) {
+ delete filters[chartId][column];
}
});
- }
+ if (Object.keys(filters[chartId]).length === 0) {
+ delete filters[chartId];
+ }
+ });
return { ...state, filters, refresh };
},
[SET_UNSAVED_CHANGES]() {
diff --git a/superset/assets/src/explore/components/controls/SliderControl.jsx b/superset/assets/src/explore/components/controls/SliderControl.jsx
new file mode 100644
index 0000000000..eee6fa524c
--- /dev/null
+++ b/superset/assets/src/explore/components/controls/SliderControl.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import BootstrapSliderWrapper from '../../../components/BootstrapSliderWrapper';
+import ControlHeader from '../ControlHeader';
+
+const propTypes = {
+ onChange: PropTypes.func,
+ value: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+};
+
+const defaultProps = {
+ onChange: () => {},
+};
+
+export default function SliderControl(props) {
+ // This wouldn't be necessary but might as well
+ return (
+
+
+ {
+ props.onChange(obj.target.value);
+ }}
+ />
+
+ );
+}
+
+SliderControl.propTypes = propTypes;
+SliderControl.defaultProps = defaultProps;
diff --git a/superset/assets/src/explore/components/controls/index.js b/superset/assets/src/explore/components/controls/index.js
index 4a2df4237b..cc9651faf6 100644
--- a/superset/assets/src/explore/components/controls/index.js
+++ b/superset/assets/src/explore/components/controls/index.js
@@ -10,6 +10,7 @@ import FixedOrMetricControl from './FixedOrMetricControl';
import HiddenControl from './HiddenControl';
import SelectAsyncControl from './SelectAsyncControl';
import SelectControl from './SelectControl';
+import SliderControl from './SliderControl';
import SpatialControl from './SpatialControl';
import TextAreaControl from './TextAreaControl';
import TextControl from './TextControl';
@@ -32,6 +33,7 @@ const controlMap = {
HiddenControl,
SelectAsyncControl,
SelectControl,
+ SliderControl,
SpatialControl,
TextAreaControl,
TextControl,
diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx
index bee2da08fe..cb46684eee 100644
--- a/superset/assets/src/explore/controls.jsx
+++ b/superset/assets/src/explore/controls.jsx
@@ -1470,10 +1470,10 @@ export const controls = {
table_filter: {
type: 'CheckboxControl',
- label: t('Table Filter'),
+ label: t('Emit Filter Events'),
renderTrigger: true,
default: false,
- description: t('Whether to apply filter when table cell is clicked'),
+ description: t('Whether to apply filter when items are clicked'),
},
align_pn: {
@@ -1777,6 +1777,17 @@ export const controls = {
'Between 0 and 1.'),
},
+ opacity: {
+ type: 'SliderControl',
+ label: t('Opacity'),
+ default: 80,
+ step: 1,
+ min: 0,
+ max: 100,
+ renderTrigger: true,
+ description: t('Opacity, expects values between 0 and 100'),
+ },
+
viewport: {
type: 'ViewportControl',
label: t('Viewport'),
@@ -2121,6 +2132,7 @@ export const controls = {
choices: [
['polyline', 'Polyline'],
['json', 'JSON'],
+ ['geohash', 'geohash (square)'],
],
},
@@ -2242,7 +2254,7 @@ export const controls = {
label: t('Filled'),
renderTrigger: true,
description: t('Whether to fill the objects'),
- default: false,
+ default: true,
},
normalized: {
diff --git a/superset/assets/src/explore/visTypes.jsx b/superset/assets/src/explore/visTypes.jsx
index d4578e0cee..d0c8c6054e 100644
--- a/superset/assets/src/explore/visTypes.jsx
+++ b/superset/assets/src/explore/visTypes.jsx
@@ -605,6 +605,14 @@ export const visTypes = {
],
},
],
+ controlOverrides: {
+ line_type: {
+ choices: [
+ ['polyline', 'Polyline'],
+ ['json', 'JSON'],
+ ],
+ },
+ },
},
deck_screengrid: {
@@ -703,25 +711,31 @@ export const visTypes = {
label: t('Query'),
expanded: true,
controlSetRows: [
- ['line_column', 'line_type'],
- ['row_limit', 'filter_nulls'],
['adhoc_filters'],
+ ['metric'],
+ ['row_limit', null],
+ ['line_column', 'line_type'],
+ ['reverse_long_lat', 'filter_nulls'],
],
},
{
label: t('Map'),
+ expanded: true,
controlSetRows: [
['mapbox_style', 'viewport'],
- ['reverse_long_lat', null],
+ ['autozoom', null],
],
},
{
label: t('Polygon Settings'),
+ expanded: true,
controlSetRows: [
['fill_color_picker', 'stroke_color_picker'],
['filled', 'stroked'],
['extruded', null],
- ['point_radius_scale', null],
+ ['line_width', null],
+ ['linear_color_scheme', 'opacity'],
+ ['table_filter', null],
],
},
{
@@ -734,6 +748,17 @@ export const visTypes = {
],
},
],
+ controlOverrides: {
+ metric: {
+ validators: [],
+ },
+ line_column: {
+ label: t('Polygon Column'),
+ },
+ line_type: {
+ label: t('Polygon Encoding'),
+ },
+ },
},
deck_arc: {
diff --git a/superset/assets/src/modules/colors.js b/superset/assets/src/modules/colors.js
index 0bb3221080..1cb9eed4e8 100644
--- a/superset/assets/src/modules/colors.js
+++ b/superset/assets/src/modules/colors.js
@@ -525,6 +525,16 @@ export const spectrums = {
],
};
+export function hexToRGB(hex, alpha = 255) {
+ if (!hex) {
+ return [0, 0, 0, alpha];
+ }
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+ return [r, g, b, alpha];
+}
+
/**
* Get a color from a scheme specific palette (scheme)
* The function cycles through the palette while memoizing labels
@@ -566,7 +576,7 @@ export const getColorFromScheme = (function () {
};
}());
-export const colorScalerFactory = function (colors, data, accessor, extents) {
+export const colorScalerFactory = function (colors, data, accessor, extents, outputRGBA = false) {
// Returns a linear scaler our of an array of color
if (!Array.isArray(colors)) {
/* eslint no-param-reassign: 0 */
@@ -581,15 +591,9 @@ export const colorScalerFactory = function (colors, data, accessor, extents) {
}
const chunkSize = (ext[1] - ext[0]) / (colors.length - 1);
const points = colors.map((col, i) => ext[0] + (i * chunkSize));
- return d3.scale.linear().domain(points).range(colors).clamp(true);
-};
-
-export function hexToRGB(hex, alpha = 255) {
- if (!hex) {
- return [0, 0, 0, alpha];
+ const scaler = d3.scale.linear().domain(points).range(colors).clamp(true);
+ if (outputRGBA) {
+ return v => hexToRGB(scaler(v));
}
- const r = parseInt(hex.slice(1, 3), 16);
- const g = parseInt(hex.slice(3, 5), 16);
- const b = parseInt(hex.slice(5, 7), 16);
- return [r, g, b, alpha];
-}
+ return scaler;
+};
diff --git a/superset/assets/src/visualizations/PlaySlider.css b/superset/assets/src/visualizations/PlaySlider.css
index df0fe77dba..e4919db38f 100644
--- a/superset/assets/src/visualizations/PlaySlider.css
+++ b/superset/assets/src/visualizations/PlaySlider.css
@@ -5,14 +5,6 @@
margin: 0;
}
-.slider-selection {
- background: #efefef;
-}
-
-.slider-handle {
- background: #b3b3b3;
-}
-
.slider.slider-horizontal {
width: 100% !important;
}
diff --git a/superset/assets/src/visualizations/PlaySlider.jsx b/superset/assets/src/visualizations/PlaySlider.jsx
index 107aa55a4a..fbe635ed7a 100644
--- a/superset/assets/src/visualizations/PlaySlider.jsx
+++ b/superset/assets/src/visualizations/PlaySlider.jsx
@@ -4,8 +4,7 @@ import { Row, Col } from 'react-bootstrap';
import Mousetrap from 'mousetrap';
-import 'bootstrap-slider/dist/css/bootstrap-slider.min.css';
-import ReactBootstrapSlider from 'react-bootstrap-slider';
+import BootrapSliderWrapper from '../components/BootstrapSliderWrapper';
import './PlaySlider.css';
import { t } from '../locales';
@@ -120,7 +119,7 @@ export default class PlaySlider extends React.PureComponent {
- {},
+ setControlValue: () => {},
};
export default class DeckGLContainer extends React.Component {
diff --git a/superset/assets/src/visualizations/deckgl/layers/common.js b/superset/assets/src/visualizations/deckgl/layers/common.js
index 6fb15dfd38..7eed061592 100644
--- a/superset/assets/src/visualizations/deckgl/layers/common.js
+++ b/superset/assets/src/visualizations/deckgl/layers/common.js
@@ -34,12 +34,18 @@ export function fitViewport(viewport, points, padding = 10) {
export function commonLayerProps(formData, slice) {
const fd = formData;
let onHover;
+ let tooltipContentGenerator;
if (fd.js_tooltip) {
- const jsTooltip = sandboxedEval(fd.js_tooltip);
+ const unsanitizedTooltipGenerator = sandboxedEval(fd.js_tooltip);
+ tooltipContentGenerator = o => dompurify.sanitize(unsanitizedTooltipGenerator(o));
+ } else if (fd.line_column && fd.line_type === 'geohash') {
+ tooltipContentGenerator = o => `${fd.line_column}: ${o.object[fd.line_column]}`;
+ }
+ if (tooltipContentGenerator) {
onHover = (o) => {
if (o.picked) {
slice.setTooltip({
- content: dompurify.sanitize(jsTooltip(o)),
+ content: tooltipContentGenerator(o),
x: o.x,
y: o.y,
});
@@ -54,6 +60,8 @@ export function commonLayerProps(formData, slice) {
const href = sandboxedEval(fd.js_onclick_href)(o);
window.open(href);
};
+ } else if (fd.table_filter && fd.line_type === 'geohash') {
+ onClick = o => slice.addFilter(fd.line_column, [o.object[fd.line_column]], false);
}
return {
onClick,
diff --git a/superset/assets/src/visualizations/deckgl/layers/polygon.jsx b/superset/assets/src/visualizations/deckgl/layers/polygon.jsx
index ae8a34c7bc..c0ac6d0c35 100644
--- a/superset/assets/src/visualizations/deckgl/layers/polygon.jsx
+++ b/superset/assets/src/visualizations/deckgl/layers/polygon.jsx
@@ -2,19 +2,36 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { PolygonLayer } from 'deck.gl';
+import _ from 'underscore';
+import d3 from 'd3';
import DeckGLContainer from './../DeckGLContainer';
import * as common from './common';
+import { colorScalerFactory } from '../../../modules/colors';
import sandboxedEval from '../../../modules/sandbox';
+function getPoints(features) {
+ return _.flatten(features.map(d => d.polygon), true);
+}
+
function getLayer(formData, payload, slice) {
const fd = formData;
const fc = fd.fill_color_picker;
- let data = payload.data.features.map(d => ({
- ...d,
- fillColor: [fc.r, fc.g, fc.b, 255 * fc.a],
- }));
+ const sc = fd.stroke_color_picker;
+ let data = [...payload.data.features];
+ const mainMetric = payload.data.metricLabels.length ? payload.data.metricLabels[0] : null;
+
+ let colorScaler;
+ if (mainMetric) {
+ const ext = d3.extent(data, d => d[mainMetric]);
+ const scaler = colorScalerFactory(fd.linear_color_scheme, null, null, ext, true);
+ colorScaler = (d) => {
+ const c = scaler(d[mainMetric]);
+ c[3] = (fd.opacity / 100.0) * 255;
+ return c;
+ };
+ }
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
@@ -26,19 +43,29 @@ function getLayer(formData, payload, slice) {
id: `path-layer-${fd.slice_id}`,
data,
filled: fd.filled,
- stroked: fd.stoked,
+ stroked: fd.stroked,
+ getFillColor: colorScaler || [fc.r, fc.g, fc.b, 255 * fc.a],
+ getLineColor: [sc.r, sc.g, sc.b, 255 * sc.a],
+ getLineWidth: fd.line_width,
extruded: fd.extruded,
+ fp64: true,
...common.commonLayerProps(fd, slice),
});
}
function deckPolygon(slice, payload, setControlValue) {
const layer = getLayer(slice.formData, payload, slice);
- const viewport = {
+ const fd = slice.formData;
+ let viewport = {
...slice.formData.viewport,
width: slice.width(),
height: slice.height(),
};
+
+ if (fd.autozoom) {
+ viewport = common.fitViewport(viewport, getPoints(payload.data.features));
+ }
+
ReactDOM.render(