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..4519b76bb253 100644 --- a/superset/assets/javascripts/explore/stores/controls.jsx +++ b/superset/assets/javascripts/explore/stores/controls.jsx @@ -143,6 +143,22 @@ export const controls = { renderTrigger: true, }, + fill_color_picker: { + label: t('Fill Color'), + description: t('Use to set fill color of geojson'), + type: 'ColorPickerControl', + default: colorPrimary, + renderTrigger: true, + }, + + stroke_color_picker: { + label: t('Stroke Color'), + description: t('Use to set stroke color of 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..e3342331c20f 100644 --- a/superset/assets/javascripts/explore/stores/visTypes.js +++ b/superset/assets/javascripts/explore/stores/visTypes.js @@ -455,6 +455,35 @@ 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', null], + ['stroke_color_picker', null], + ['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..855d25a2ae6d --- /dev/null +++ b/superset/assets/visualizations/deckgl/geojson.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { GeoJsonLayer } from 'deck.gl'; + +import DeckGLContainer from './DeckGLContainer'; + +const propertyMap = { + color: 'fillColor', + fill: 'fillColor', + 'fill-color': 'fillColor', + 'stroke-color': 'strokeColor', + 'stroke-width': 'strokeWidth', +} + +const convertGeoJsonProps = (p) => { + const obj = {} + Object.entries(p).forEach( + ([key, value]) => { obj[(propertyMap[key]) ? propertyMap[key] : key] = value } + ); + return obj +} + +function DeckGeoJsonLayer(slice, payload, setControlValue) { + const fd = slice.formData; + const fillColor = fd.fill_color_picker; + const strokeColor = fd.stroke_color_picker; + const pointRadiusScale = fd.point_radius_scale; + const data = payload.data.geojson.features.map(d => ({ + ...d, + properties: convertGeoJsonProps(d.properties), + })); + + const layer = new GeoJsonLayer({ + id: 'geojson-layer', + data, + filled: true, + stroked: false, + extruded: true, + pointRadiusScale: pointRadiusScale, + }); + + 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..be65024c0a50 100644 --- a/superset/assets/visualizations/main.js +++ b/superset/assets/visualizations/main.js @@ -42,51 +42,7 @@ export const VIZ_TYPES = { deck_screengrid: 'deck_screengrid', deck_grid: 'deck_grid', deck_hex: 'deck_hex', + deck_geojson: 'deck_geojson', deck_path: 'deck_path', }; -const vizMap = { - [VIZ_TYPES.area]: require('./nvd3_vis.js'), - [VIZ_TYPES.bar]: require('./nvd3_vis.js'), - [VIZ_TYPES.big_number]: require('./big_number.js'), - [VIZ_TYPES.big_number_total]: require('./big_number.js'), - [VIZ_TYPES.box_plot]: require('./nvd3_vis.js'), - [VIZ_TYPES.bubble]: require('./nvd3_vis.js'), - [VIZ_TYPES.bullet]: require('./nvd3_vis.js'), - [VIZ_TYPES.cal_heatmap]: require('./cal_heatmap.js'), - [VIZ_TYPES.compare]: require('./nvd3_vis.js'), - [VIZ_TYPES.directed_force]: require('./directed_force.js'), - [VIZ_TYPES.chord]: require('./chord.jsx'), - [VIZ_TYPES.dist_bar]: require('./nvd3_vis.js'), - [VIZ_TYPES.filter_box]: require('./filter_box.jsx'), - [VIZ_TYPES.heatmap]: require('./heatmap.js'), - [VIZ_TYPES.histogram]: require('./histogram.js'), - [VIZ_TYPES.horizon]: require('./horizon.js'), - [VIZ_TYPES.iframe]: require('./iframe.js'), - [VIZ_TYPES.line]: require('./nvd3_vis.js'), - [VIZ_TYPES.time_pivot]: require('./nvd3_vis.js'), - [VIZ_TYPES.mapbox]: require('./mapbox.jsx'), - [VIZ_TYPES.markup]: require('./markup.js'), - [VIZ_TYPES.para]: require('./parallel_coordinates.js'), - [VIZ_TYPES.pie]: require('./nvd3_vis.js'), - [VIZ_TYPES.pivot_table]: require('./pivot_table.js'), - [VIZ_TYPES.sankey]: require('./sankey.js'), - [VIZ_TYPES.separator]: require('./markup.js'), - [VIZ_TYPES.sunburst]: require('./sunburst.js'), - [VIZ_TYPES.table]: require('./table.js'), - [VIZ_TYPES.time_table]: require('./time_table.jsx'), - [VIZ_TYPES.treemap]: require('./treemap.js'), - [VIZ_TYPES.country_map]: require('./country_map.js'), - [VIZ_TYPES.word_cloud]: require('./word_cloud.js'), - [VIZ_TYPES.world_map]: require('./world_map.js'), - [VIZ_TYPES.dual_line]: require('./nvd3_vis.js'), - [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'), -}; -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..b41eae2fe5b5 100644 --- a/superset/data/__init__.py +++ b/superset/data/__init__.py @@ -1522,6 +1522,37 @@ 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: @@ -1550,3 +1581,6 @@ def load_bart_lines(): db.session.merge(tbl) db.session.commit() tbl.fetch_metadata() + + + 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"""