diff --git a/superset/assets/package.json b/superset/assets/package.json index 55d855fb8baab..4d0982324fc0d 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -66,6 +66,7 @@ "jquery": "3.1.1", "lodash.throttle": "^4.1.1", "luma.gl": "^5.0.1", + "mapbox-gl": "^0.43.0", "mathjs": "^3.16.3", "moment": "2.18.1", "mustache": "^2.2.1", diff --git a/superset/assets/visualizations/deckgl/DeckGLContainer.jsx b/superset/assets/visualizations/deckgl/DeckGLContainer.jsx index 64ee934ddbf36..3166917744fc7 100644 --- a/superset/assets/visualizations/deckgl/DeckGLContainer.jsx +++ b/superset/assets/visualizations/deckgl/DeckGLContainer.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import MapGL from 'react-map-gl'; import DeckGL from 'deck.gl'; +import 'mapbox-gl/dist/mapbox-gl.css'; const propTypes = { viewport: PropTypes.object.isRequired, diff --git a/superset/assets/visualizations/deckgl/layers/geojson.jsx b/superset/assets/visualizations/deckgl/layers/geojson.jsx index 3ee1f62830a95..c21ce3bbd19b2 100644 --- a/superset/assets/visualizations/deckgl/layers/geojson.jsx +++ b/superset/assets/visualizations/deckgl/layers/geojson.jsx @@ -14,40 +14,74 @@ const propertyMap = { 'stroke-width': 'strokeWidth', }; -const convertGeoJsonColorProps = (p, colors) => { - const obj = Object.assign(...Object.keys(p).map(k => ({ - [(propertyMap[k]) ? propertyMap[k] : k]: p[k] }))); - +const alterProps = (props, propOverrides) => { + const newProps = {}; + Object.keys(props).forEach((k) => { + if (k in propertyMap) { + newProps[propertyMap[k]] = props[k]; + } else { + newProps[k] = props[k]; + } + }); + if (typeof props.fillColor === 'string') { + newProps.fillColor = hexToRGB(p.fillColor); + } + if (typeof props.strokeColor === 'string') { + newProps.strokeColor = hexToRGB(p.strokeColor); + } return { - ...obj, - fillColor: (colors.fillColor[3] !== 0) ? colors.fillColor : hexToRGB(obj.fillColor), - strokeColor: (colors.strokeColor[3] !== 0) ? colors.strokeColor : hexToRGB(obj.strokeColor), + ...newProps, + ...propOverrides, }; }; +let features; +const recurseGeoJson = (node, propOverrides, jsFnMutator, extraProps) => { + if (node && node.features) { + node.features.forEach((obj) => { + recurseGeoJson(obj, propOverrides, jsFnMutator, node.extraProps || extraProps); + }); + } + if (node && node.geometry) { + const newNode = { + ...node, + properties: alterProps(node.properties, propOverrides), + }; + if (jsFnMutator) { + jsFnMutator(newNode); + } + if (!newNode.extraProps) { + newNode.extraProps = extraProps; + } + features.push(newNode); + } +}; export default function geoJsonLayer(formData, payload, slice) { const fd = formData; const fc = fd.fill_color_picker; const sc = fd.stroke_color_picker; - let data = payload.data.geojson.features.map(d => ({ - ...d, - properties: convertGeoJsonColorProps( - d.properties, { - fillColor: [fc.r, fc.g, fc.b, 255 * fc.a], - strokeColor: [sc.r, sc.g, sc.b, 255 * sc.a], - }), - })); + const fillColor = [fc.r, fc.g, fc.b, 255 * fc.a]; + const strokeColor = [sc.r, sc.g, sc.b, 255 * sc.a]; + const propOverrides = {}; + if (fillColor[3] > 0) { + propOverrides.fillColor = fillColor; + } + if (strokeColor[3] > 0) { + propOverrides.strokeColor = strokeColor; + } + let jsFnMutator; if (fd.js_datapoint_mutator) { // Applying user defined data mutator if defined - const jsFnMutator = sandboxedEval(fd.js_datapoint_mutator); - data = data.map(jsFnMutator); + jsFnMutator = sandboxedEval(fd.js_datapoint_mutator); } + features = []; + recurseGeoJson(payload.data, propOverrides, jsFnMutator); return new GeoJsonLayer({ - id: `path-layer-${fd.slice_id}`, - data, + id: `geojson-layer-${fd.slice_id}`, filled: fd.filled, + data: features, stroked: fd.stroked, extruded: fd.extruded, pointRadiusScale: fd.point_radius_scale, diff --git a/superset/assets/visualizations/deckgl/layers/grid.jsx b/superset/assets/visualizations/deckgl/layers/grid.jsx index 51b1e03d9de84..a461eb98e53a1 100644 --- a/superset/assets/visualizations/deckgl/layers/grid.jsx +++ b/superset/assets/visualizations/deckgl/layers/grid.jsx @@ -7,7 +7,6 @@ export default function getLayer(formData, payload) { ...d, color: [c.r, c.g, c.b, 255 * c.a], })); - return new GridLayer({ id: `grid-layer-${fd.slice_id}`, data, diff --git a/superset/viz.py b/superset/viz.py index 9d6a57d8fa6eb..65cc9757684fb 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -1825,14 +1825,6 @@ def get_metrics(self): self.metric = self.form_data.get('size') return [self.metric] if self.metric else [] - def get_properties(self, d): - return { - 'weight': d.get(self.metric) or 1, - } - - def get_position(self, d): - raise Exception('Not implemented in child class!') - def process_spatial_query_obj(self, key, group_by): spatial = self.form_data.get(key) if spatial is None: @@ -1898,16 +1890,19 @@ def get_data(self, df): features = [] for d in df.to_dict(orient='records'): - feature = dict( - position=self.get_position(d), - props=self.get_js_columns(d), - **self.get_properties(d)) + feature = self.get_properties(d) + extra_props = self.get_js_columns(d) + if extra_props: + feature['extraProps'] = extra_props features.append(feature) return { 'features': features, 'mapboxApiKey': config.get('MAPBOX_API_KEY'), } + def get_properties(self, d): + raise NotImplementedError() + class DeckScatterViz(BaseDeckGLViz): @@ -1923,9 +1918,6 @@ def query_obj(self): fd.get('point_radius_fixed') or {'type': 'fix', 'value': 500}) return super(DeckScatterViz, self).query_obj() - def get_position(self, d): - return d['spatial'] - def get_metrics(self): self.metric = None if self.point_radius_fixed.get('type') == 'metric': @@ -1937,6 +1929,7 @@ def get_properties(self, d): return { 'radius': self.fixed_value if self.fixed_value else d.get(self.metric), 'cat_color': d.get(self.dim) if self.dim else None, + 'position': d.get('spatial'), } def get_data(self, df): @@ -1957,8 +1950,11 @@ class DeckScreengrid(BaseDeckGLViz): verbose_name = _('Deck.gl - Screen Grid') spatial_control_keys = ['spatial'] - def get_position(self, d): - return d['spatial'] + def get_properties(self, d): + return { + 'position': d.get('spatial'), + 'weight': d.get(self.metric) or 1, + } class DeckGrid(BaseDeckGLViz): @@ -1969,8 +1965,11 @@ class DeckGrid(BaseDeckGLViz): verbose_name = _('Deck.gl - 3D Grid') spatial_control_keys = ['spatial'] - def get_position(self, d): - return d['spatial'] + def get_properties(self, d): + return { + 'position': d.get('spatial'), + 'weight': d.get(self.metric) or 1, + } class DeckPathViz(BaseDeckGLViz): @@ -1985,9 +1984,6 @@ class DeckPathViz(BaseDeckGLViz): } spatial_control_keys = ['spatial'] - def get_position(self, d): - return d['spatial'] - def query_obj(self): d = super(DeckPathViz, self).query_obj() line_col = self.form_data.get('line_column') @@ -2016,8 +2012,11 @@ class DeckHex(BaseDeckGLViz): verbose_name = _('Deck.gl - 3D HEX') spatial_control_keys = ['spatial'] - def get_position(self, d): - return d['spatial'] + def get_properties(self, d): + return { + 'position': d.get('spatial'), + 'weight': d.get(self.metric) or 1, + } class DeckGeoJson(BaseDeckGLViz): @@ -2029,22 +2028,14 @@ class DeckGeoJson(BaseDeckGLViz): def query_obj(self): d = super(DeckGeoJson, self).query_obj() - d['columns'] = [self.form_data.get('geojson')] + d['columns'] += [self.form_data.get('geojson')] d['metrics'] = [] d['groupby'] = [] return d - def get_data(self, df): - fd = self.form_data - geojson = { - 'type': 'FeatureCollection', - 'features': [json.loads(item) for item in df[fd.get('geojson')]], - } - - return { - 'geojson': geojson, - 'mapboxApiKey': config.get('MAPBOX_API_KEY'), - } + def get_properties(self, d): + geojson = d.get(self.form_data.get('geojson')) + return json.loads(geojson) class DeckArc(BaseDeckGLViz): @@ -2055,14 +2046,12 @@ class DeckArc(BaseDeckGLViz): verbose_name = _('Deck.gl - Arc') spatial_control_keys = ['start_spatial', 'end_spatial'] - def get_position(self, d): - deck_map = { - 'start_spatial': 'sourcePosition', - 'end_spatial': 'targetPosition', + def get_properties(self, d): + return { + 'sourcePosition': d.get('start_spatial'), + 'targetPosition': d.get('end_spatial'), } - return {deck_map[key]: d[key] for key in self.spatial_control_keys} - def get_data(self, df): d = super(DeckArc, self).get_data(df) arcs = d['features']