Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[geo] Add Deckgl GeoJson layer #4068

Closed
wants to merge 16 commits into from
Closed
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,16 @@ 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 : [],
}),
},

all_columns_x: {
type: 'SelectControl',
label: 'X',
Expand Down
21 changes: 21 additions & 0 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,27 @@ 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'],
],
},
],
},

deck_scatter: {
label: t('Deck.gl - Scatter plot'),
requiresTime: true,
Expand Down
43 changes: 43 additions & 0 deletions superset/assets/visualizations/deckgl/geojson.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { GeoJsonLayer } from 'deck.gl';

import DeckGLContainer from './DeckGLContainer';

function DeckGeoJsonLayer(slice, payload, setControlValue) {
const fd = slice.formData;
const c = fd.color_picker;
const data = payload.data.geojson.features.map(d => ({
...d,
properties: {
fillColor: [c.r, c.g, c.b, 255 * c.a],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still feel like there should be a way to not override the color defined in the GeoJSON (if any). We need better mapping and precedence rules here.

First, a mapping from the spec to deck.gl's supported props.

Then, clearable colorpickers (we could use opacity=0 as a hint to not override, opacity=0 could be the default), only if opacity!=0 do we override the GeoJSON provided colors.

As a last resort, we should have a color if opacity=0 and color isn't defined in the geojson.

All of this should be made clear in the controls tooltip.

This is hard to model, let's chat about it.

elevation: 2000,
},
}));

const layer = new GeoJsonLayer({
id: 'geojson-layer',
data,
filled: true,
stroked: false,
extruded: true,
pointRadiusScale: 100,
});

const viewport = {
...fd.viewport,
width: slice.width(),
height: slice.height(),
};
ReactDOM.render(
<DeckGLContainer
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={[layer]}
mapStyle={fd.mapbox_style}
setControlValue={setControlValue}
/>,
document.getElementById(slice.containerId),
);
}
module.exports = DeckGeoJsonLayer;
84 changes: 43 additions & 41 deletions superset/assets/visualizations/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,49 +42,51 @@ export const VIZ_TYPES = {
deck_screengrid: 'deck_screengrid',
deck_grid: 'deck_grid',
deck_hex: 'deck_hex',
deck_geojson: 'deck_geojson',
};

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'),
area: require('./nvd3_vis.js'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use VIZ_TYPES here, I think you picked the wrong side of the merge conflict.

bar: require('./nvd3_vis.js'),
big_number: require('./big_number.js'),
big_number_total: require('./big_number.js'),
box_plot: require('./nvd3_vis.js'),
bubble: require('./nvd3_vis.js'),
bullet: require('./nvd3_vis.js'),
cal_heatmap: require('./cal_heatmap.js'),
compare: require('./nvd3_vis.js'),
directed_force: require('./directed_force.js'),
chord: require('./chord.jsx'),
dist_bar: require('./nvd3_vis.js'),
filter_box: require('./filter_box.jsx'),
heatmap: require('./heatmap.js'),
histogram: require('./histogram.js'),
horizon: require('./horizon.js'),
iframe: require('./iframe.js'),
line: require('./nvd3_vis.js'),
time_pivot: require('./nvd3_vis.js'),
mapbox: require('./mapbox.jsx'),
markup: require('./markup.js'),
para: require('./parallel_coordinates.js'),
pie: require('./nvd3_vis.js'),
pivot_table: require('./pivot_table.js'),
sankey: require('./sankey.js'),
separator: require('./markup.js'),
sunburst: require('./sunburst.js'),
table: require('./table.js'),
time_table: require('./time_table.jsx'),
treemap: require('./treemap.js'),
country_map: require('./country_map.js'),
word_cloud: require('./word_cloud.js'),
world_map: require('./world_map.js'),
dual_line: require('./nvd3_vis.js'),
event_flow: require('./EventFlow.jsx'),
paired_ttest: require('./paired_ttest.jsx'),
partition: require('./partition.js'),
deck_scatter: require('./deckgl/scatter.jsx'),
deck_screengrid: require('./deckgl/screengrid.jsx'),
deck_grid: require('./deckgl/grid.jsx'),
deck_hex: require('./deckgl/hex.jsx'),
deck_geojson: require('./deckgl/geojson.jsx'),
};
export default vizMap;
3 changes: 3 additions & 0 deletions superset/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
32 changes: 31 additions & 1 deletion superset/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import textwrap

import pandas as pd
from sqlalchemy import BigInteger, Date, DateTime, Float, String
from sqlalchemy import BigInteger, Date, DateTime, Float, String, Text
import geohash

from superset import app, db, utils
Expand Down Expand Up @@ -1519,3 +1519,33 @@ def load_flights():
db.session.merge(obj)
db.session.commit()
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()
Binary file added superset/data/paris_iris.json.gz
Binary file not shown.
44 changes: 36 additions & 8 deletions superset/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -1795,14 +1795,15 @@ def query_obj(self):

gb = []

spatial = fd.get('spatial')
if spatial.get('type') == 'latlong':
gb += [spatial.get('lonCol')]
gb += [spatial.get('latCol')]
elif spatial.get('type') == 'delimited':
gb += [spatial.get('lonlatCol')]
elif spatial.get('type') == 'geohash':
gb += [spatial.get('geohashCol')]
if fd.get('spatial'):
spatial = fd.get('spatial')
if spatial.get('type') == 'latlong':
gb += [spatial.get('lonCol')]
gb += [spatial.get('latCol')]
elif spatial.get('type') == 'delimited':
gb += [spatial.get('lonlatCol')]
elif spatial.get('type') == 'geohash':
gb += [spatial.get('geohashCol')]

if fd.get('dimension'):
gb += [fd.get('dimension')]
Expand Down Expand Up @@ -1907,6 +1908,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"""
Expand Down