diff --git a/superset/assets/images/viz_thumbnails/deck_geojson.png b/superset/assets/images/viz_thumbnails/deck_geojson.png new file mode 100644 index 000000000000..40d1b63f3bfa Binary files /dev/null and b/superset/assets/images/viz_thumbnails/deck_geojson.png differ diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx index 7117712d2100..47ceaa045805 100644 --- a/superset/assets/javascripts/explore/stores/controls.jsx +++ b/superset/assets/javascripts/explore/stores/controls.jsx @@ -36,7 +36,7 @@ const timeColumnOption = { verbose_name: 'Time', column_name: '__timestamp', description: t( - 'A reference to the [Time] configuration, taking granularity into ' + + 'A reference to the [Time] configuration, taking granularity into ' + 'account'), }; const sortAxisChoices = [ @@ -143,6 +143,22 @@ export const controls = { renderTrigger: true, }, + fill_color_picker: { + label: t('Fill Color'), + description: t(' Set the opacity to 0 if you do not want to override the color specified in the GeoJSON'), + type: 'ColorPickerControl', + default: colorPrimary, + renderTrigger: true, + }, + + stroke_color_picker: { + label: t('Stroke Color'), + description: t(' Set the opacity to 0 if you do not want to override the color specified in the GeoJSON'), + type: 'ColorPickerControl', + default: colorPrimary, + renderTrigger: true, + }, + metric: { type: 'SelectControl', label: t('Metric'), @@ -496,6 +512,25 @@ export const controls = { }), }, + geojson: { + type: 'SelectControl', + label: t('GeoJson Column'), + validators: [v.nonEmpty], + description: t('Select the geojson column'), + mapStateToProps: state => ({ + choices: (state.datasource) ? state.datasource.all_cols : [], + }), + }, + + point_radius_scale: { + type: 'SelectControl', + freeForm: true, + label: t('Point Radius Scale'), + validators: [v.integer], + default: null, + choices: formatSelectOptions([0, 100, 200, 300, 500]), + }, + all_columns_x: { type: 'SelectControl', label: 'X', diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js index 2c5f2a61e188..8e8a7f018eb5 100644 --- a/superset/assets/javascripts/explore/stores/visTypes.js +++ b/superset/assets/javascripts/explore/stores/visTypes.js @@ -455,6 +455,33 @@ export const visTypes = { }, }, + deck_geojson: { + label: t('Deck.gl - geoJson'), + requiresTime: true, + controlPanelSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: [ + ['geojson', 'row_limit'], + ], + }, + { + label: t('Map'), + controlSetRows: [ + ['mapbox_style', 'viewport'], + ], + }, + { + label: t('GeoJson Settings'), + controlSetRows: [ + ['fill_color_picker', 'stroke_color_picker'], + ['point_radius_scale', null], + ], + }, + ], + }, + deck_scatter: { label: t('Deck.gl - Scatter plot'), requiresTime: true, diff --git a/superset/assets/visualizations/deckgl/geojson.jsx b/superset/assets/visualizations/deckgl/geojson.jsx new file mode 100644 index 000000000000..080d7ee3f189 --- /dev/null +++ b/superset/assets/visualizations/deckgl/geojson.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { GeoJsonLayer } from 'deck.gl'; +import { hexToRGB } from '../../javascripts/modules/colors'; + +import DeckGLContainer from './DeckGLContainer'; + +const propertyMap = { + fillColor: 'fillColor', + color: 'fillColor', + fill: 'fillColor', + 'fill-color': 'fillColor', + strokeColor: 'strokeColor', + 'stroke-color': 'strokeColor', + 'stroke-width': 'strokeWidth', +}; + +const convertGeoJsonColorProps = (p, colors) => { + const obj = Object.assign(...Object.keys(p).map(k => ({ + [(propertyMap[k]) ? propertyMap[k] : k]: p[k] }))); + + return { + ...obj, + fillColor: (colors.fillColor[3] !== 0) ? colors.fillColor : hexToRGB(obj.fillColor), + strokeColor: (colors.strokeColor[3] !== 0) ? colors.strokeColor : hexToRGB(obj.strokeColor), + }; +}; + +function DeckGeoJsonLayer(slice, payload, setControlValue) { + const fd = slice.formData; + const fc = fd.fill_color_picker; + const sc = fd.stroke_color_picker; + const 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 layer = new GeoJsonLayer({ + id: 'geojson-layer', + 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/main.js b/superset/assets/visualizations/main.js index 06d30a1bb143..e692c107d8f2 100644 --- a/superset/assets/visualizations/main.js +++ b/superset/assets/visualizations/main.js @@ -43,6 +43,7 @@ export const VIZ_TYPES = { deck_grid: 'deck_grid', deck_hex: 'deck_hex', deck_path: 'deck_path', + deck_geojson: 'deck_geojson', }; const vizMap = { @@ -88,5 +89,6 @@ const vizMap = { [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'), }; export default vizMap; diff --git a/superset/cli.py b/superset/cli.py index 56ead72b4163..14a592bed12e 100755 --- a/superset/cli.py +++ b/superset/cli.py @@ -139,6 +139,9 @@ def load_examples(load_test_data): print('Loading DECK.gl demo') data.load_deck_dash() + print('Loading Paris geojson data') + data.load_paris_iris_geojson() + if load_test_data: print('Loading [Unicode test data]') data.load_unicode_test_data() diff --git a/superset/data/__init__.py b/superset/data/__init__.py index b3cb4a8ea9a4..bddc01432a46 100644 --- a/superset/data/__init__.py +++ b/superset/data/__init__.py @@ -1522,6 +1522,36 @@ def load_flights(): obj.fetch_metadata() +def load_paris_iris_geojson(): + tbl_name = 'paris_iris_mapping' + + with gzip.open(os.path.join(DATA_FOLDER, 'paris_iris.json.gz')) as f: + df = pd.read_json(f) + df['features'] = df.features.map(json.dumps) + + df.to_sql( + tbl_name, + db.engine, + if_exists='replace', + chunksize=500, + dtype={ + 'color': String(255), + 'name': String(255), + 'features': Text, + 'type': Text, + }, + index=False) + print("Creating table {} reference".format(tbl_name)) + tbl = db.session.query(TBL).filter_by(table_name=tbl_name).first() + if not tbl: + tbl = TBL(table_name=tbl_name) + tbl.description = "Map of Paris" + tbl.database = get_or_create_main_db() + db.session.merge(tbl) + db.session.commit() + tbl.fetch_metadata() + + def load_bart_lines(): tbl_name = 'bart_lines' with gzip.open(os.path.join(DATA_FOLDER, 'bart-lines.json.gz')) as f: diff --git a/superset/data/paris_iris.json.gz b/superset/data/paris_iris.json.gz new file mode 100644 index 000000000000..4a964c94f74d Binary files /dev/null and b/superset/data/paris_iris.json.gz differ diff --git a/superset/viz.py b/superset/viz.py index 55f26034db30..2a6b4940c93d 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -1942,6 +1942,33 @@ class DeckHex(BaseDeckGLViz): verbose_name = _('Deck.gl - 3D HEX') +class DeckGeoJson(BaseDeckGLViz): + + """deck.gl's GeoJSONLayer""" + + viz_type = 'deck_geojson' + verbose_name = _('Deck.gl - GeoJSON') + + def query_obj(self): + d = super(DeckGeoJson, self).query_obj() + 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'), + } + + class EventFlowViz(BaseViz): """A visualization to explore patterns in event sequences"""