diff --git a/setup.py b/setup.py index 8d0dba3a0f43d..0ecd1ef1be6e3 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ def get_git_sha(): '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 0000000000000..eede9da44ce79 Binary files /dev/null and b/superset/assets/images/viz_thumbnails/deck_path.png differ diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx index 006a1b49e3c93..ee6d63af334cd 100644 --- a/superset/assets/javascripts/explore/stores/controls.jsx +++ b/superset/assets/javascripts/explore/stores/controls.jsx @@ -1713,5 +1713,43 @@ export const controls = { 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'), + }, + + reverse_long_lat: { + type: 'CheckboxControl', + label: t('Reverse Lat & Long'), + default: false, + }, + }; export default controls; diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js index ef9dc4112cc51..2c5f2a61e1889 100644 --- a/superset/assets/javascripts/explore/stores/visTypes.js +++ b/superset/assets/javascripts/explore/stores/visTypes.js @@ -397,6 +397,30 @@ export const visTypes = { }, }, + deck_path: { + label: t('Deck.gl - Grid'), + requiresTime: true, + controlPanelSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: [ + ['line_column', 'line_type'], + ['row_limit', null], + ], + }, + { + label: t('Map'), + expanded: true, + controlSetRows: [ + ['mapbox_style', 'viewport'], + ['color_picker', 'line_width'], + ['reverse_long_lat', null], + ], + }, + ], + }, + 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 0000000000000..c814adc501ccb --- /dev/null +++ b/superset/assets/visualizations/deckgl/path.jsx @@ -0,0 +1,39 @@ +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 => ({ + 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( + , + document.getElementById(slice.containerId), + ); +} +module.exports = deckPath; diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js index fdcb7b1eab2e0..06d30a1bb143f 100644 --- a/superset/assets/visualizations/main.js +++ b/superset/assets/visualizations/main.js @@ -42,6 +42,7 @@ export const VIZ_TYPES = { deck_screengrid: 'deck_screengrid', deck_grid: 'deck_grid', deck_hex: 'deck_hex', + deck_path: 'deck_path', }; const vizMap = { @@ -86,5 +87,6 @@ const vizMap = { [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 16500ac031ed8..56ead72b41634 100755 --- a/superset/cli.py +++ b/superset/cli.py @@ -146,6 +146,9 @@ def load_examples(load_test_data): 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 742a32b4c5620..b3cb4a8ea9a49 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 @@ def load_flights(): 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 0000000000000..91f50fbe6accc Binary files /dev/null and b/superset/data/bart-lines.json.gz differ diff --git a/superset/viz.py b/superset/viz.py index 85bc854cf9de8..55f26034db30f 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -27,6 +27,7 @@ import numpy as np import pandas as pd from pandas.tseries.frequencies import to_offset +import polyline import simplejson as json from six import PY3, string_types, text_type from six.moves import reduce @@ -1796,13 +1797,14 @@ 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 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')] @@ -1863,8 +1865,10 @@ def query_obj(self): 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): @@ -1899,6 +1903,37 @@ class DeckGrid(BaseDeckGLViz): 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')]] + if fd.get('reverse_long_lat'): + paths = [[(point[1], point[0]) for point in path] for path in paths] + d = { + 'mapboxApiKey': config.get('MAPBOX_API_KEY'), + 'paths': paths, + } + return d + + class DeckHex(BaseDeckGLViz): """deck.gl's DeckLayer"""