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(