[geo] add support for deck.gl's path layer
Works with json and polyline data.
(cherry picked from commit 63e1cf77f99aba5845e593b0a1c4512085d16c64)
diff --git a/setup.py b/setup.py
index 8d0dba3..0ecd1ef 100644
--- a/setup.py
+++ b/setup.py
@@ -66,6 +66,7 @@
'pandas==0.20.3',
'parsedatetime==2.0.0',
'pathlib2==2.3.0',
+ 'polyline==1.3.2',
'pydruid==0.3.1',
'PyHive>=0.4.0',
'python-dateutil==2.6.0',
diff --git a/superset/assets/images/viz_thumbnails/deck_path.png b/superset/assets/images/viz_thumbnails/deck_path.png
new file mode 100644
index 0000000..eede9da
--- /dev/null
+++ b/superset/assets/images/viz_thumbnails/deck_path.png
Binary files differ
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index b18039f..dfb45b2 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -1723,5 +1723,37 @@
t('Partitions whose height to parent height proportions are ' +
'below this value are pruned'),
},
+
+ line_column: {
+ type: 'SelectControl',
+ label: t('Lines column'),
+ default: null,
+ description: t('The database columns that contains lines information'),
+ mapStateToProps: state => ({
+ choices: (state.datasource) ? state.datasource.all_cols : [],
+ }),
+ validators: [v.nonEmpty],
+ },
+ line_type: {
+ type: 'SelectControl',
+ label: t('Lines encoding'),
+ clearable: false,
+ default: 'json',
+ description: t('The encoding format of the lines'),
+ choices: [
+ ['polyline', 'Polyline'],
+ ['json', 'JSON'],
+ ],
+ },
+
+ line_width: {
+ type: 'TextControl',
+ label: t('Line width'),
+ renderTrigger: true,
+ isInt: true,
+ default: 10,
+ description: t('The width of the lines'),
+ },
+
};
export default controls;
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index a243cbf..416e5a6 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -397,6 +397,28 @@
},
},
+ deck_path: {
+ label: t('Deck.gl - Grid'),
+ requiresTime: true,
+ controlPanelSections: [
+ {
+ label: t('Query'),
+ expanded: true,
+ controlSetRows: [
+ ['line_column', 'line_type'],
+ ],
+ },
+ {
+ label: t('Map'),
+ expanded: true,
+ controlSetRows: [
+ ['mapbox_style', 'viewport'],
+ ['color_picker', 'line_width'],
+ ],
+ },
+ ],
+ },
+
deck_screengrid: {
label: t('Deck.gl - Screen grid'),
requiresTime: true,
diff --git a/superset/assets/visualizations/deckgl/path.jsx b/superset/assets/visualizations/deckgl/path.jsx
new file mode 100644
index 0000000..8162f18
--- /dev/null
+++ b/superset/assets/visualizations/deckgl/path.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { PathLayer } from 'deck.gl';
+
+import DeckGLContainer from './DeckGLContainer';
+
+function deckPath(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) => {
+ return {
+ path,
+ width: fd.line_width,
+ color: fixedColor,
+ };
+ });;
+
+ const layer = new PathLayer({
+ id: `path-layer-${slice.containerId}`,
+ data,
+ rounded: true,
+ widthScale: 1,
+ });
+ 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 = deckPath;
diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js
index 9976614..ac1ac75 100644
--- a/superset/assets/visualizations/main.js
+++ b/superset/assets/visualizations/main.js
@@ -41,5 +41,6 @@
deck_screengrid: require('./deckgl/screengrid.jsx'),
deck_grid: require('./deckgl/grid.jsx'),
deck_hex: require('./deckgl/hex.jsx'),
+ deck_path: require('./deckgl/path.jsx'),
};
export default vizMap;
diff --git a/superset/cli.py b/superset/cli.py
index 16500ac..56ead72 100755
--- a/superset/cli.py
+++ b/superset/cli.py
@@ -146,6 +146,9 @@
print('Loading flights data')
data.load_flights()
+ print('Loading bart lines data')
+ data.load_bart_lines()
+
@manager.option(
'-d', '--datasource',
diff --git a/superset/data/__init__.py b/superset/data/__init__.py
index 742a32b..b3cb4a8 100644
--- a/superset/data/__init__.py
+++ b/superset/data/__init__.py
@@ -12,8 +12,9 @@
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
+import polyline
from superset import app, db, utils
from superset.connectors.connector_registry import ConnectorRegistry
@@ -1519,3 +1520,33 @@
db.session.merge(obj)
db.session.commit()
obj.fetch_metadata()
+
+
+def load_bart_lines():
+ tbl_name = 'bart_lines'
+ with gzip.open(os.path.join(DATA_FOLDER, 'bart-lines.json.gz')) as f:
+ df = pd.read_json(f, encoding='latin-1')
+ df['path_json'] = df.path.map(json.dumps)
+ df['polyline'] = df.path.map(polyline.encode)
+ del df['path']
+ df.to_sql(
+ tbl_name,
+ db.engine,
+ if_exists='replace',
+ chunksize=500,
+ dtype={
+ 'color': String(255),
+ 'name': String(255),
+ 'polyline': Text,
+ 'path_json': 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 = "BART lines"
+ tbl.database = get_or_create_main_db()
+ db.session.merge(tbl)
+ db.session.commit()
+ tbl.fetch_metadata()
diff --git a/superset/data/bart-lines.json.gz b/superset/data/bart-lines.json.gz
new file mode 100644
index 0000000..91f50fb
--- /dev/null
+++ b/superset/data/bart-lines.json.gz
Binary files differ
diff --git a/superset/viz.py b/superset/viz.py
index 6551577..a398666 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -30,6 +30,7 @@
import simplejson as json
from six import PY3, string_types, text_type
from six.moves import reduce
+import polyline
from superset import app, cache, get_manifest_file, utils
from superset.utils import DTTM_ALIAS, merge_extra_filters
@@ -1814,13 +1815,14 @@
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 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')]
@@ -1881,8 +1883,10 @@
return super(DeckScatterViz, self).query_obj()
def get_metrics(self):
+ self.metric = None
if self.point_radius_fixed.get('type') == 'metric':
- return [self.point_radius_fixed.get('value')]
+ self.metric = self.point_radius_fixed.get('value')
+ return [self.metric]
return None
def get_properties(self, d):
@@ -1917,6 +1921,36 @@
verbose_name = _('Deck.gl - 3D Grid')
+class DeckPathViz(BaseDeckGLViz):
+
+ """deck.gl's PathLayer"""
+
+ viz_type = 'deck_path'
+ verbose_name = _('Deck.gl - Paths')
+ deser_map = {
+ 'json': json.loads,
+ 'polyline': polyline.decode,
+ }
+
+ def query_obj(self):
+ d = super(DeckPathViz, self).query_obj()
+ d['groupby'] = []
+ d['metrics'] = []
+ d['columns'] = [self.form_data.get('line_column')]
+ return d
+
+ def get_data(self, df):
+ fd = self.form_data
+ deser = self.deser_map[fd.get('line_type')]
+ paths = [deser(s) for s in df[fd.get('line_column')]]
+
+ d = {
+ 'mapboxApiKey': config.get('MAPBOX_API_KEY'),
+ 'paths': paths,
+ }
+ return d
+
+
class DeckHex(BaseDeckGLViz):
"""deck.gl's DeckLayer"""