diff --git a/setup.py b/setup.py index 0ecd1ef1be6e3..f662e885c36ac 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ def get_git_sha(): 'pyyaml>=3.11', 'requests==2.17.3', 'simplejson==3.10.0', - 'six==1.10.0', + 'six==1.11.0', 'sqlalchemy==1.1.9', 'sqlalchemy-utils==0.32.16', 'sqlparse==0.2.3', diff --git a/superset/assets/images/viz_thumbnails/multi.png b/superset/assets/images/viz_thumbnails/multi.png new file mode 100644 index 0000000000000..be62cd40e9002 Binary files /dev/null and b/superset/assets/images/viz_thumbnails/multi.png differ diff --git a/superset/assets/javascripts/chart/chartAction.js b/superset/assets/javascripts/chart/chartAction.js index a6341ddb6a7ba..393400d6bcd6f 100644 --- a/superset/assets/javascripts/chart/chartAction.js +++ b/superset/assets/javascripts/chart/chartAction.js @@ -120,7 +120,20 @@ export function runQuery(formData, force = false, timeout = 60, key) { if (err.statusText === 'timeout') { dispatch(chartUpdateTimeout(err.statusText, timeout, key)); } else if (err.statusText !== 'abort') { - dispatch(chartUpdateFailed(err.responseJSON, key)); + let errObject; + if (err.responseJSON) { + errObject = err.responseJSON; + } else if (err.stack) { + errObject = { + error: 'Unexpected error: ' + err.description, + stacktrace: err.stack, + }; + } else { + errObject = { + error: 'Unexpected error.', + }; + } + dispatch(chartUpdateFailed(errObject, key)); } }); const annotationLayers = formData.annotation_layers || []; diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx index 7aee160d9c3ab..70cc2314a21e5 100644 --- a/superset/assets/javascripts/explore/stores/controls.jsx +++ b/superset/assets/javascripts/explore/stores/controls.jsx @@ -1389,6 +1389,7 @@ export const controls = { mapbox_style: { type: 'SelectControl', label: t('Map Style'), + clearable: false, renderTrigger: true, choices: [ ['mapbox://styles/mapbox/streets-v9', 'Streets'], @@ -1816,5 +1817,23 @@ export const controls = { and returns a similarly shaped object. {sandboxedEvalInfo}

), }, + + deck_slices: { + type: 'SelectAsyncControl', + multi: true, + label: t('deck.gl charts'), + validators: [v.nonEmpty], + default: [], + description: t('Pick a set of deck.gl charts to layer on top of one another'), + dataEndpoint: '/sliceasync/api/read?_flt_0_viz_type=deck_', + placeholder: t('Select charts'), + onAsyncErrorMessage: t('Error while fetching charts'), + mutator: (data) => { + if (!data || !data.result) { + return []; + } + return data.result.map(o => ({ value: o.id, label: o.slice_name })); + }, + }, }; export default controls; diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js index f4720f9932fc7..f2e668f8f1829 100644 --- a/superset/assets/javascripts/explore/stores/visTypes.js +++ b/superset/assets/javascripts/explore/stores/visTypes.js @@ -338,6 +338,21 @@ export const visTypes = { }, }, + deck_multi: { + label: t('Deck.gl - Multiple Layers'), + requiresTime: true, + controlPanelSections: [ + { + label: t('Map'), + expanded: true, + controlSetRows: [ + ['mapbox_style', 'viewport'], + ['deck_slices', null], + ], + }, + ], + }, + deck_hex: { label: t('Deck.gl - Hexagons'), requiresTime: true, @@ -398,7 +413,7 @@ export const visTypes = { }, deck_path: { - label: t('Deck.gl - Grid'), + label: t('Deck.gl - Paths'), requiresTime: true, controlPanelSections: [ { diff --git a/superset/assets/visualizations/deckgl/path.jsx b/superset/assets/visualizations/deckgl/factory.jsx similarity index 54% rename from superset/assets/visualizations/deckgl/path.jsx rename to superset/assets/visualizations/deckgl/factory.jsx index c814adc501ccb..d715bc1a9cbe0 100644 --- a/superset/assets/visualizations/deckgl/path.jsx +++ b/superset/assets/visualizations/deckgl/factory.jsx @@ -1,25 +1,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { PathLayer } from 'deck.gl'; import DeckGLContainer from './DeckGLContainer'; +import layerGenerators from './layers'; -function deckPath(slice, payload, setControlValue) { +export default function deckglFactory(slice, payload, setControlValue) { const fd = slice.formData; - const c = fd.color_picker; - const fixedColor = [c.r, c.g, c.b, 255 * c.a]; - const data = payload.data.paths.map(path => ({ - path, - width: fd.line_width, - color: fixedColor, - })); - - const layer = new PathLayer({ - id: `path-layer-${slice.containerId}`, - data, - rounded: true, - widthScale: 1, - }); + const layer = layerGenerators[fd.viz_type](fd, payload); const viewport = { ...fd.viewport, width: slice.width(), @@ -36,4 +23,3 @@ function deckPath(slice, payload, setControlValue) { document.getElementById(slice.containerId), ); } -module.exports = deckPath; diff --git a/superset/assets/visualizations/deckgl/grid.jsx b/superset/assets/visualizations/deckgl/grid.jsx deleted file mode 100644 index 1ef2394873bad..0000000000000 --- a/superset/assets/visualizations/deckgl/grid.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { GridLayer } from 'deck.gl'; - -import DeckGLContainer from './DeckGLContainer'; - -function deckScreenGridLayer(slice, payload, setControlValue) { - const fd = slice.formData; - const c = fd.color_picker; - const data = payload.data.features.map(d => ({ - ...d, - color: [c.r, c.g, c.b, 255 * c.a], - })); - - const layer = new GridLayer({ - id: `grid-layer-${slice.containerId}`, - data, - pickable: true, - cellSize: fd.grid_size, - minColor: [0, 0, 0, 0], - extruded: fd.extruded, - maxColor: [c.r, c.g, c.b, 255 * c.a], - outline: false, - getElevationValue: points => points.reduce((sum, point) => sum + point.weight, 0), - getColorValue: points => points.reduce((sum, point) => sum + point.weight, 0), - }); - const viewport = { - ...fd.viewport, - width: slice.width(), - height: slice.height(), - }; - ReactDOM.render( - , - document.getElementById(slice.containerId), - ); -} -module.exports = deckScreenGridLayer; diff --git a/superset/assets/visualizations/deckgl/hex.jsx b/superset/assets/visualizations/deckgl/hex.jsx deleted file mode 100644 index 9526825d250b6..0000000000000 --- a/superset/assets/visualizations/deckgl/hex.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { HexagonLayer } from 'deck.gl'; - -import DeckGLContainer from './DeckGLContainer'; - -function deckHex(slice, payload, setControlValue) { - const fd = slice.formData; - const c = fd.color_picker; - const data = payload.data.features.map(d => ({ - ...d, - color: [c.r, c.g, c.b, 255 * c.a], - })); - - const layer = new HexagonLayer({ - id: `hex-layer-${slice.containerId}`, - data, - pickable: true, - radius: fd.grid_size, - minColor: [0, 0, 0, 0], - extruded: fd.extruded, - maxColor: [c.r, c.g, c.b, 255 * c.a], - outline: false, - getElevationValue: points => points.reduce((sum, point) => sum + point.weight, 0), - getColorValue: points => points.reduce((sum, point) => sum + point.weight, 0), - }); - const viewport = { - ...fd.viewport, - width: slice.width(), - height: slice.height(), - }; - ReactDOM.render( - , - document.getElementById(slice.containerId), - ); -} -module.exports = deckHex; diff --git a/superset/assets/visualizations/deckgl/geojson.jsx b/superset/assets/visualizations/deckgl/layers/geojson.jsx similarity index 59% rename from superset/assets/visualizations/deckgl/geojson.jsx rename to superset/assets/visualizations/deckgl/layers/geojson.jsx index 080d7ee3f1891..11a7b8375f5b2 100644 --- a/superset/assets/visualizations/deckgl/geojson.jsx +++ b/superset/assets/visualizations/deckgl/layers/geojson.jsx @@ -1,9 +1,6 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; import { GeoJsonLayer } from 'deck.gl'; -import { hexToRGB } from '../../javascripts/modules/colors'; +import { hexToRGB } from '../../../javascripts/modules/colors'; -import DeckGLContainer from './DeckGLContainer'; const propertyMap = { fillColor: 'fillColor', @@ -26,8 +23,8 @@ const convertGeoJsonColorProps = (p, colors) => { }; }; -function DeckGeoJsonLayer(slice, payload, setControlValue) { - const fd = slice.formData; +export default function geoJsonLayer(formData, payload) { + const fd = formData; const fc = fd.fill_color_picker; const sc = fd.stroke_color_picker; const data = payload.data.geojson.features.map(d => ({ @@ -39,29 +36,12 @@ function DeckGeoJsonLayer(slice, payload, setControlValue) { }), })); - const layer = new GeoJsonLayer({ - id: 'geojson-layer', + return new GeoJsonLayer({ + id: `path-layer-${fd.slice_id}`, data, filled: true, stroked: false, extruded: true, pointRadiusScale: fd.point_radius_scale, }); - - const viewport = { - ...fd.viewport, - width: slice.width(), - height: slice.height(), - }; - ReactDOM.render( - , - document.getElementById(slice.containerId), - ); } -module.exports = DeckGeoJsonLayer; diff --git a/superset/assets/visualizations/deckgl/layers/grid.jsx b/superset/assets/visualizations/deckgl/layers/grid.jsx new file mode 100644 index 0000000000000..51b1e03d9de84 --- /dev/null +++ b/superset/assets/visualizations/deckgl/layers/grid.jsx @@ -0,0 +1,23 @@ +import { GridLayer } from 'deck.gl'; + +export default function getLayer(formData, payload) { + const fd = formData; + const c = fd.color_picker; + const data = payload.data.features.map(d => ({ + ...d, + color: [c.r, c.g, c.b, 255 * c.a], + })); + + return new GridLayer({ + id: `grid-layer-${fd.slice_id}`, + data, + pickable: true, + cellSize: fd.grid_size, + minColor: [0, 0, 0, 0], + extruded: fd.extruded, + maxColor: [c.r, c.g, c.b, 255 * c.a], + outline: false, + getElevationValue: points => points.reduce((sum, point) => sum + point.weight, 0), + getColorValue: points => points.reduce((sum, point) => sum + point.weight, 0), + }); +} diff --git a/superset/assets/visualizations/deckgl/layers/hex.jsx b/superset/assets/visualizations/deckgl/layers/hex.jsx new file mode 100644 index 0000000000000..0e33e9495305a --- /dev/null +++ b/superset/assets/visualizations/deckgl/layers/hex.jsx @@ -0,0 +1,23 @@ +import { HexagonLayer } from 'deck.gl'; + +export default function getLayer(formData, payload) { + const fd = formData; + const c = fd.color_picker; + const data = payload.data.features.map(d => ({ + ...d, + color: [c.r, c.g, c.b, 255 * c.a], + })); + + return new HexagonLayer({ + id: `hex-layer-${fd.slice_id}`, + data, + pickable: true, + radius: fd.grid_size, + minColor: [0, 0, 0, 0], + extruded: fd.extruded, + maxColor: [c.r, c.g, c.b, 255 * c.a], + outline: false, + getElevationValue: points => points.reduce((sum, point) => sum + point.weight, 0), + getColorValue: points => points.reduce((sum, point) => sum + point.weight, 0), + }); +} diff --git a/superset/assets/visualizations/deckgl/layers/index.js b/superset/assets/visualizations/deckgl/layers/index.js new file mode 100644 index 0000000000000..a382af55b8a66 --- /dev/null +++ b/superset/assets/visualizations/deckgl/layers/index.js @@ -0,0 +1,17 @@ +/* eslint camelcase: 0 */ +import deck_grid from './grid'; +import deck_screengrid from './screengrid'; +import deck_path from './path'; +import deck_hex from './hex'; +import deck_scatter from './scatter'; +import deck_geojson from './geojson'; + +const layerGenerators = { + deck_grid, + deck_screengrid, + deck_path, + deck_hex, + deck_scatter, + deck_geojson, +}; +export default layerGenerators; diff --git a/superset/assets/visualizations/deckgl/layers/path.jsx b/superset/assets/visualizations/deckgl/layers/path.jsx new file mode 100644 index 0000000000000..c288ff0576a8e --- /dev/null +++ b/superset/assets/visualizations/deckgl/layers/path.jsx @@ -0,0 +1,19 @@ +import { PathLayer } from 'deck.gl'; + +export default function getLayer(formData, payload) { + const fd = formData; + const c = fd.color_picker; + const fixedColor = [c.r, c.g, c.b, 255 * c.a]; + const data = payload.data.paths.map(path => ({ + path, + width: fd.line_width, + color: fixedColor, + })); + + return new PathLayer({ + id: `path-layer-${fd.slice_id}`, + data, + rounded: true, + widthScale: 1, + }); +} diff --git a/superset/assets/visualizations/deckgl/layers/scatter.jsx b/superset/assets/visualizations/deckgl/layers/scatter.jsx new file mode 100644 index 0000000000000..d44e7272adb97 --- /dev/null +++ b/superset/assets/visualizations/deckgl/layers/scatter.jsx @@ -0,0 +1,35 @@ +import { ScatterplotLayer } from 'deck.gl'; + +import { getColorFromScheme, hexToRGB } from '../../../javascripts/modules/colors'; +import { unitToRadius } from '../../../javascripts/modules/geo'; + +export default function getLayer(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 data = payload.data.features.map((d) => { + let radius = unitToRadius(fd.point_unit, d.radius) || 10; + if (fd.multiplier) { + radius *= fd.multiplier; + } + let color; + if (fd.dimension) { + color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255); + } else { + color = fixedColor; + } + return { + ...d, + radius, + color, + }; + }); + return new ScatterplotLayer({ + id: `scatter-layer-${fd.slice_id}`, + data, + pickable: true, + fp64: true, + outline: false, + }); +} diff --git a/superset/assets/visualizations/deckgl/layers/screengrid.jsx b/superset/assets/visualizations/deckgl/layers/screengrid.jsx new file mode 100644 index 0000000000000..54edd9eaba0f1 --- /dev/null +++ b/superset/assets/visualizations/deckgl/layers/screengrid.jsx @@ -0,0 +1,23 @@ +import { ScreenGridLayer } from 'deck.gl'; + +export default function getLayer(formData, payload) { + const fd = formData; + const c = fd.color_picker; + const data = payload.data.features.map(d => ({ + ...d, + color: [c.r, c.g, c.b, 255 * c.a], + })); + + // Passing a layer creator function instead of a layer since the + // layer needs to be regenerated at each render + return new ScreenGridLayer({ + id: `screengrid-layer-${fd.slice_id}`, + data, + pickable: true, + cellSizePixels: fd.grid_size, + minColor: [c.r, c.g, c.b, 0], + maxColor: [c.r, c.g, c.b, 255 * c.a], + outline: false, + getWeight: d => d.weight || 0, + }); +} diff --git a/superset/assets/visualizations/deckgl/multi.jsx b/superset/assets/visualizations/deckgl/multi.jsx new file mode 100644 index 0000000000000..63f1a8801174b --- /dev/null +++ b/superset/assets/visualizations/deckgl/multi.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import $ from 'jquery'; + +import DeckGLContainer from './DeckGLContainer'; +import { getExploreUrl } from '../../javascripts/explore/exploreUtils'; +import layerGenerators from './layers'; + + +function deckMulti(slice, payload, setControlValue) { + if (!slice.subSlicesLayers) { + slice.subSlicesLayers = {}; // eslint-disable-line no-param-reassign + } + const fd = slice.formData; + const render = () => { + const viewport = { + ...fd.viewport, + width: slice.width(), + height: slice.height(), + }; + const layers = Object.keys(slice.subSlicesLayers).map(k => slice.subSlicesLayers[k]); + ReactDOM.render( + , + document.getElementById(slice.containerId), + ); + }; + render(); + payload.data.slices.forEach((subslice) => { + const url = getExploreUrl(subslice.form_data, 'json'); + $.get(url, (data) => { + // Late import to avoid circular deps + const layer = layerGenerators[subslice.form_data.viz_type](subslice.form_data, data); + slice.subSlicesLayers[subslice.slice_id] = layer; // eslint-disable-line no-param-reassign + render(); + }); + }); +} +module.exports = deckMulti; diff --git a/superset/assets/visualizations/deckgl/scatter.jsx b/superset/assets/visualizations/deckgl/scatter.jsx deleted file mode 100644 index 18cec553fa1c9..0000000000000 --- a/superset/assets/visualizations/deckgl/scatter.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { ScatterplotLayer } from 'deck.gl'; - -import DeckGLContainer from './DeckGLContainer'; -import { getColorFromScheme, hexToRGB } from '../../javascripts/modules/colors'; -import { unitToRadius } from '../../javascripts/modules/geo'; - -function deckScatter(slice, payload, setControlValue) { - const fd = slice.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 data = payload.data.features.map((d) => { - let radius = unitToRadius(fd.point_unit, d.radius) || 10; - if (fd.multiplier) { - radius *= fd.multiplier; - } - let color; - if (fd.dimension) { - color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255); - } else { - color = fixedColor; - } - return { - ...d, - radius, - color, - }; - }); - - const layer = new ScatterplotLayer({ - id: `scatter-layer-${slice.containerId}`, - data, - pickable: true, - fp64: true, - outline: false, - }); - const viewport = { - ...fd.viewport, - width: slice.width(), - height: slice.height(), - }; - ReactDOM.render( - , - document.getElementById(slice.containerId), - ); -} -module.exports = deckScatter; diff --git a/superset/assets/visualizations/deckgl/screengrid.jsx b/superset/assets/visualizations/deckgl/screengrid.jsx deleted file mode 100644 index b8b58ec056d59..0000000000000 --- a/superset/assets/visualizations/deckgl/screengrid.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { ScreenGridLayer } from 'deck.gl'; - -import DeckGLContainer from './DeckGLContainer'; - -function deckScreenGridLayer(slice, payload, setControlValue) { - const fd = slice.formData; - const c = fd.color_picker; - const data = payload.data.features.map(d => ({ - ...d, - color: [c.r, c.g, c.b, 255 * c.a], - })); - - const viewport = { - ...fd.viewport, - width: slice.width(), - height: slice.height(), - }; - // Passing a layer creator function instead of a layer since the - // layer needs to be regenerated at each render - const layer = () => new ScreenGridLayer({ - id: `screengrid-layer-${slice.containerId}`, - data, - pickable: true, - cellSizePixels: fd.grid_size, - minColor: [c.r, c.g, c.b, 0], - maxColor: [c.r, c.g, c.b, 255 * c.a], - outline: false, - getWeight: d => d.weight || 0, - }); - ReactDOM.render( - , - document.getElementById(slice.containerId), - ); -} -module.exports = deckScreenGridLayer; diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js index e692c107d8f2f..af7b0401017f4 100644 --- a/superset/assets/visualizations/main.js +++ b/superset/assets/visualizations/main.js @@ -1,4 +1,5 @@ /* eslint-disable global-require */ +import deckglFactory from './deckgl/factory'; // You ***should*** use these to reference viz_types in code export const VIZ_TYPES = { @@ -44,6 +45,7 @@ export const VIZ_TYPES = { deck_hex: 'deck_hex', deck_path: 'deck_path', deck_geojson: 'deck_geojson', + deck_multi: 'deck_multi', }; const vizMap = { @@ -84,11 +86,12 @@ const vizMap = { [VIZ_TYPES.event_flow]: require('./EventFlow.jsx'), [VIZ_TYPES.paired_ttest]: require('./paired_ttest.jsx'), [VIZ_TYPES.partition]: require('./partition.js'), - [VIZ_TYPES.deck_scatter]: require('./deckgl/scatter.jsx'), - [VIZ_TYPES.deck_screengrid]: require('./deckgl/screengrid.jsx'), - [VIZ_TYPES.deck_grid]: require('./deckgl/grid.jsx'), - [VIZ_TYPES.deck_hex]: require('./deckgl/hex.jsx'), - [VIZ_TYPES.deck_path]: require('./deckgl/path.jsx'), - [VIZ_TYPES.deck_geojson]: require('./deckgl/geojson.jsx'), + [VIZ_TYPES.deck_scatter]: deckglFactory, + [VIZ_TYPES.deck_screengrid]: deckglFactory, + [VIZ_TYPES.deck_grid]: deckglFactory, + [VIZ_TYPES.deck_hex]: deckglFactory, + [VIZ_TYPES.deck_path]: deckglFactory, + [VIZ_TYPES.deck_geojson]: deckglFactory, + [VIZ_TYPES.deck_multi]: require('./deckgl/multi.jsx'), }; export default vizMap; diff --git a/superset/views/core.py b/superset/views/core.py index 02e6f1d8a5b02..802fda9ef3705 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -493,7 +493,7 @@ def add(self): class SliceAsync(SliceModelView): # noqa list_columns = [ - 'slice_link', 'viz_type', + 'id', 'slice_link', 'viz_type', 'slice_name', 'creator', 'modified', 'icons'] label_columns = { 'icons': ' ', diff --git a/superset/viz.py b/superset/viz.py index 2a6b4940c93db..bb052d17aba00 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -86,6 +86,8 @@ def get_df(self, query_obj=None): """Returns a pandas dataframe based on the query object""" if not query_obj: query_obj = self.query_obj() + if not query_obj: + return None self.error_msg = '' self.results = None @@ -1768,6 +1770,32 @@ def get_data(self, df): } +class DeckGLMultiLayer(BaseViz): + + """Pile on multiple DeckGL layers""" + + viz_type = 'deck_multi' + verbose_name = _('Deck.gl - Multiple Layers') + + is_timeseries = False + credits = 'deck.gl' + + def query_obj(self): + return None + + def get_data(self, df): + fd = self.form_data + # Late imports to avoid circular import issues + from superset.models.core import Slice + from superset import db + slice_ids = fd.get('deck_slices') + slices = db.session.query(Slice).filter(Slice.id.in_(slice_ids)).all() + return { + 'mapboxApiKey': config.get('MAPBOX_API_KEY'), + 'slices': [slc.data for slc in slices], + } + + class BaseDeckGLViz(BaseViz): """Base class for deck.gl visualizations"""