You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by gr...@apache.org on 2017/11/16 08:30:06 UTC
[incubator-superset] branch master updated: DECKGL integration -
Phase 1 (#3771)
This is an automated email from the ASF dual-hosted git repository.
graceguo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new 3a8af5d DECKGL integration - Phase 1 (#3771)
3a8af5d is described below
commit 3a8af5d0b049e1f5e7d70a8642984ddba4d70ba5
Author: Maxime Beauchemin <ma...@gmail.com>
AuthorDate: Thu Nov 16 00:30:02 2017 -0800
DECKGL integration - Phase 1 (#3771)
* DECKGL integration
Adding a new set of geospatial visualizations building on top of the
awesome deck.gl library. https://github.com/uber/deck.gl
While the end goal it to expose all types of layers and let users bind
their data to control most props exposed by the deck.gl API, this
PR focusses on a first set of visualizations and props:
* ScatterLayer
* HexagonLayer
* GridLayer
* ScreenGridLayer
* Addressing comments
* lint
* Linting
* Addressing chri's comments
---
.pylintrc | 2 +-
.../assets/images/viz_thumbnails/deck_grid.png | Bin 0 -> 2125810 bytes
superset/assets/images/viz_thumbnails/deck_hex.png | Bin 0 -> 1090997 bytes
.../assets/images/viz_thumbnails/deck_scatter.png | Bin 0 -> 795739 bytes
.../images/viz_thumbnails/deck_screengrid.png | Bin 0 -> 591701 bytes
.../javascripts/explore/components/Control.jsx | 1 +
.../components/controls/FixedOrMetricControl.jsx | 130 ++++++++++
.../explore/components/controls/SelectControl.jsx | 3 +
.../explore/components/controls/TextControl.jsx | 8 +-
.../components/controls/ViewportControl.jsx | 99 ++++++++
.../explore/components/controls/index.js | 4 +
.../assets/javascripts/explore/stores/controls.jsx | 101 +++++++-
.../assets/javascripts/explore/stores/visTypes.js | 152 +++++++++++-
superset/assets/javascripts/modules/colors.js | 10 +
superset/assets/javascripts/modules/geo.js | 25 ++
superset/assets/package.json | 3 +
.../components/FixedOrMetricControl_spec.jsx | 39 +++
.../explore/components/ViewportControl_spec.jsx | 46 ++++
.../spec/javascripts/modules/colors_spec.jsx | 11 +-
.../assets/spec/javascripts/modules/geo_spec.jsx | 27 ++
.../visualizations/deckgl/DeckGLContainer.jsx | 87 +++++++
superset/assets/visualizations/deckgl/grid.jsx | 43 ++++
superset/assets/visualizations/deckgl/hex.jsx | 43 ++++
superset/assets/visualizations/deckgl/scatter.jsx | 55 +++++
.../assets/visualizations/deckgl/screengrid.jsx | 43 ++++
superset/assets/visualizations/main.js | 4 +
superset/cli.py | 6 +
superset/data/__init__.py | 273 +++++++++++++++++++++
superset/data/airports.csv.gz | Bin 0 -> 9836 bytes
superset/data/fligth_data.csv.gz | Bin 0 -> 1897423 bytes
superset/viz.py | 104 ++++++++
31 files changed, 1308 insertions(+), 11 deletions(-)
diff --git a/.pylintrc b/.pylintrc
index 27b41d7..8a0e0ed 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -102,7 +102,7 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme
good-names=i,j,k,ex,Run,_,d,e,v,o,l,x,ts
# Bad variable names which should always be refused, separated by a comma
-bad-names=foo,bar,baz,toto,tutu,tata
+bad-names=foo,bar,baz,toto,tutu,tata,d,fd
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
diff --git a/superset/assets/images/viz_thumbnails/deck_grid.png b/superset/assets/images/viz_thumbnails/deck_grid.png
new file mode 100644
index 0000000..cd93965
Binary files /dev/null and b/superset/assets/images/viz_thumbnails/deck_grid.png differ
diff --git a/superset/assets/images/viz_thumbnails/deck_hex.png b/superset/assets/images/viz_thumbnails/deck_hex.png
new file mode 100644
index 0000000..31feff5
Binary files /dev/null and b/superset/assets/images/viz_thumbnails/deck_hex.png differ
diff --git a/superset/assets/images/viz_thumbnails/deck_scatter.png b/superset/assets/images/viz_thumbnails/deck_scatter.png
new file mode 100644
index 0000000..11f38cc
Binary files /dev/null and b/superset/assets/images/viz_thumbnails/deck_scatter.png differ
diff --git a/superset/assets/images/viz_thumbnails/deck_screengrid.png b/superset/assets/images/viz_thumbnails/deck_screengrid.png
new file mode 100644
index 0000000..d5da29c
Binary files /dev/null and b/superset/assets/images/viz_thumbnails/deck_screengrid.png differ
diff --git a/superset/assets/javascripts/explore/components/Control.jsx b/superset/assets/javascripts/explore/components/Control.jsx
index 7d8fcc3..e458807 100644
--- a/superset/assets/javascripts/explore/components/Control.jsx
+++ b/superset/assets/javascripts/explore/components/Control.jsx
@@ -21,6 +21,7 @@ const propTypes = {
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
+ PropTypes.object,
PropTypes.bool,
PropTypes.array,
PropTypes.func]),
diff --git a/superset/assets/javascripts/explore/components/controls/FixedOrMetricControl.jsx b/superset/assets/javascripts/explore/components/controls/FixedOrMetricControl.jsx
new file mode 100644
index 0000000..bf9e7f1
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/controls/FixedOrMetricControl.jsx
@@ -0,0 +1,130 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Label, Popover, OverlayTrigger } from 'react-bootstrap';
+
+import controls from '../../stores/controls';
+import TextControl from './TextControl';
+import SelectControl from './SelectControl';
+import ControlHeader from '../ControlHeader';
+import PopoverSection from '../../../components/PopoverSection';
+
+const controlTypes = {
+ fixed: 'fix',
+ metric: 'metric',
+};
+
+const propTypes = {
+ onChange: PropTypes.func,
+ value: PropTypes.object,
+ isFloat: PropTypes.bool,
+ datasource: PropTypes.object,
+ default: PropTypes.shape({
+ type: PropTypes.oneOf(['fix', 'metric']),
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ }),
+};
+
+const defaultProps = {
+ onChange: () => {},
+ default: { type: controlTypes.fixed, value: 5 },
+};
+
+export default class FixedOrMetricControl extends React.Component {
+ constructor(props) {
+ super(props);
+ this.onChange = this.onChange.bind(this);
+ this.setType = this.setType.bind(this);
+ this.setFixedValue = this.setFixedValue.bind(this);
+ this.setMetric = this.setMetric.bind(this);
+ const type = (props.value ? props.value.type : props.default.type) || controlTypes.fixed;
+ const value = (props.value ? props.value.value : props.default.value) || '100';
+ this.state = {
+ type,
+ fixedValue: type === controlTypes.fixed ? value : '',
+ metricValue: type === controlTypes.metric ? value : null,
+ };
+ }
+ onChange() {
+ this.props.onChange({
+ type: this.state.type,
+ value: this.state.type === controlTypes.fixed ?
+ this.state.fixedValue : this.state.metricValue,
+ });
+ }
+ setType(type) {
+ this.setState({ type }, this.onChange);
+ }
+ setFixedValue(fixedValue) {
+ this.setState({ fixedValue }, this.onChange);
+ }
+ setMetric(metricValue) {
+ this.setState({ metricValue }, this.onChange);
+ }
+ renderPopover() {
+ const value = this.props.value || this.props.default;
+ const type = value.type || controlTypes.fixed;
+ const metrics = this.props.datasource ? this.props.datasource.metrics : null;
+ return (
+ <Popover id="filter-popover">
+ <div style={{ width: '240px' }}>
+ <PopoverSection
+ title="Fixed"
+ isSelected={type === controlTypes.fixed}
+ onSelect={() => { this.onChange(controlTypes.fixed); }}
+ >
+ <TextControl
+ isFloat
+ onChange={this.setFixedValue}
+ onFocus={() => { this.setType(controlTypes.fixed); }}
+ value={this.state.fixedValue}
+ />
+ </PopoverSection>
+ <PopoverSection
+ title="Based on a metric"
+ isSelected={type === controlTypes.metric}
+ onSelect={() => { this.onChange(controlTypes.metric); }}
+ >
+ <SelectControl
+ {...controls.metric}
+ name="metric"
+ options={metrics}
+ onFocus={() => { this.setType(controlTypes.metric); }}
+ onChange={this.setMetric}
+ value={this.state.metricValue}
+ />
+ </PopoverSection>
+ </div>
+ </Popover>
+ );
+ }
+ render() {
+ return (
+ <div>
+ <ControlHeader {...this.props} />
+ <OverlayTrigger
+ container={document.body}
+ trigger="click"
+ rootClose
+ ref="trigger"
+ placement="right"
+ overlay={this.renderPopover()}
+ >
+ <Label style={{ cursor: 'pointer' }}>
+ {this.state.type === controlTypes.fixed &&
+ <span>{this.state.fixedValue}</span>
+ }
+ {this.state.type === controlTypes.metric &&
+ <span>
+ <span style={{ fontWeight: 'normal' }}>metric: </span>
+ <strong>{this.state.metricValue}</strong>
+ </span>
+ }
+ </Label>
+ </OverlayTrigger>
+ </div>
+ );
+ }
+}
+
+FixedOrMetricControl.propTypes = propTypes;
+FixedOrMetricControl.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explore/components/controls/SelectControl.jsx b/superset/assets/javascripts/explore/components/controls/SelectControl.jsx
index a82995d..6441b71 100644
--- a/superset/assets/javascripts/explore/components/controls/SelectControl.jsx
+++ b/superset/assets/javascripts/explore/components/controls/SelectControl.jsx
@@ -17,6 +17,7 @@ const propTypes = {
multi: PropTypes.bool,
name: PropTypes.string.isRequired,
onChange: PropTypes.func,
+ onFocus: PropTypes.func,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
showHeader: PropTypes.bool,
optionRenderer: PropTypes.func,
@@ -34,6 +35,7 @@ const defaultProps = {
label: null,
multi: false,
onChange: () => {},
+ onFocus: () => {},
showHeader: true,
optionRenderer: opt => opt.label,
valueRenderer: opt => opt.label,
@@ -115,6 +117,7 @@ export default class SelectControl extends React.PureComponent {
clearable: this.props.clearable,
isLoading: this.props.isLoading,
onChange: this.onChange,
+ onFocus: this.props.onFocus,
optionRenderer: VirtualizedRendererWrap(this.props.optionRenderer),
valueRenderer: this.props.valueRenderer,
selectComponent: this.props.freeForm ? Creatable : Select,
diff --git a/superset/assets/javascripts/explore/components/controls/TextControl.jsx b/superset/assets/javascripts/explore/components/controls/TextControl.jsx
index 4fe558e..bfe3f99 100644
--- a/superset/assets/javascripts/explore/components/controls/TextControl.jsx
+++ b/superset/assets/javascripts/explore/components/controls/TextControl.jsx
@@ -5,10 +5,8 @@ import * as v from '../../validators';
import ControlHeader from '../ControlHeader';
const propTypes = {
- name: PropTypes.string.isRequired,
- label: PropTypes.string,
- description: PropTypes.string,
onChange: PropTypes.func,
+ onFocus: PropTypes.func,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
@@ -18,9 +16,8 @@ const propTypes = {
};
const defaultProps = {
- label: null,
- description: null,
onChange: () => {},
+ onFocus: () => {},
value: '',
isInt: false,
isFloat: false,
@@ -64,6 +61,7 @@ export default class TextControl extends React.Component {
type="text"
placeholder=""
onChange={this.onChange}
+ onFocus={this.props.onFocus}
value={value}
/>
</FormGroup>
diff --git a/superset/assets/javascripts/explore/components/controls/ViewportControl.jsx b/superset/assets/javascripts/explore/components/controls/ViewportControl.jsx
new file mode 100644
index 0000000..382b7f7
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/controls/ViewportControl.jsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Label, Popover, OverlayTrigger } from 'react-bootstrap';
+import { decimal2sexagesimal } from 'geolib';
+
+import TextControl from './TextControl';
+import ControlHeader from '../ControlHeader';
+import { defaultViewport } from '../../../modules/geo';
+
+const PARAMS = [
+ 'longitude',
+ 'latitude',
+ 'zoom',
+ 'bearing',
+ 'pitch',
+];
+
+const propTypes = {
+ onChange: PropTypes.func.isRequired,
+ value: PropTypes.shape({
+ longitude: PropTypes.number,
+ latitude: PropTypes.number,
+ zoom: PropTypes.number,
+ bearing: PropTypes.number,
+ pitch: PropTypes.number,
+ }),
+ default: PropTypes.object,
+ name: PropTypes.string.isRequired,
+};
+
+const defaultProps = {
+ onChange: () => {},
+ default: { type: 'fix', value: 5 },
+ value: defaultViewport,
+};
+
+export default class ViewportControl extends React.Component {
+ constructor(props) {
+ super(props);
+ this.onChange = this.onChange.bind(this);
+ }
+ onChange(ctrl, value) {
+ this.props.onChange({
+ ...this.props.value,
+ [ctrl]: value,
+ });
+ }
+ renderTextControl(ctrl) {
+ return (
+ <div key={ctrl}>
+ {ctrl}
+ <TextControl
+ value={this.props.value[ctrl]}
+ onChange={this.onChange.bind(this, ctrl)}
+ isFloat
+ />
+ </div>
+ );
+ }
+ renderPopover() {
+ return (
+ <Popover id={`filter-popover-${this.props.name}`} title="Viewport">
+ {PARAMS.map(ctrl => this.renderTextControl(ctrl))}
+ </Popover>
+ );
+ }
+ renderLabel() {
+ if (this.props.value.longitude && this.props.value.latitude) {
+ return (
+ decimal2sexagesimal(this.props.value.longitude) +
+ ' | ' +
+ decimal2sexagesimal(this.props.value.latitude)
+ );
+ }
+ return 'N/A';
+ }
+ render() {
+ return (
+ <div>
+ <ControlHeader {...this.props} />
+ <OverlayTrigger
+ container={document.body}
+ trigger="click"
+ rootClose
+ ref="trigger"
+ placement="right"
+ overlay={this.renderPopover()}
+ >
+ <Label style={{ cursor: 'pointer' }}>
+ {this.renderLabel()}
+ </Label>
+ </OverlayTrigger>
+ </div>
+ );
+ }
+}
+
+ViewportControl.propTypes = propTypes;
+ViewportControl.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explore/components/controls/index.js b/superset/assets/javascripts/explore/components/controls/index.js
index 876bc4a..094a26b 100644
--- a/superset/assets/javascripts/explore/components/controls/index.js
+++ b/superset/assets/javascripts/explore/components/controls/index.js
@@ -6,12 +6,14 @@ import ColorSchemeControl from './ColorSchemeControl';
import DatasourceControl from './DatasourceControl';
import DateFilterControl from './DateFilterControl';
import FilterControl from './FilterControl';
+import FixedOrMetricControl from './FixedOrMetricControl';
import HiddenControl from './HiddenControl';
import SelectAsyncControl from './SelectAsyncControl';
import SelectControl from './SelectControl';
import TextAreaControl from './TextAreaControl';
import TextControl from './TextControl';
import TimeSeriesColumnControl from './TimeSeriesColumnControl';
+import ViewportControl from './ViewportControl';
import VizTypeControl from './VizTypeControl';
const controlMap = {
@@ -23,12 +25,14 @@ const controlMap = {
DatasourceControl,
DateFilterControl,
FilterControl,
+ FixedOrMetricControl,
HiddenControl,
SelectAsyncControl,
SelectControl,
TextAreaControl,
TextControl,
TimeSeriesColumnControl,
+ ViewportControl,
VizTypeControl,
};
export default controlMap;
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index e3da06b..eabd29f 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -1,7 +1,8 @@
import React from 'react';
import { formatSelectOptionsForRange, formatSelectOptions } from '../../modules/utils';
import * as v from '../validators';
-import { ALL_COLOR_SCHEMES, spectrums } from '../../modules/colors';
+import { colorPrimary, ALL_COLOR_SCHEMES, spectrums } from '../../modules/colors';
+import { defaultViewport } from '../../modules/geo';
import MetricOption from '../../components/MetricOption';
import ColumnOption from '../../components/ColumnOption';
import OptionDescription from '../../components/OptionDescription';
@@ -135,6 +136,14 @@ export const controls = {
}),
},
+ color_picker: {
+ label: t('Fixed Color'),
+ description: t('Use this to define a static color for all circles'),
+ type: 'ColorPickerControl',
+ default: colorPrimary,
+ renderTrigger: true,
+ },
+
annotation_layers: {
type: 'SelectAsyncControl',
multi: true,
@@ -424,6 +433,13 @@ export const controls = {
},
groupby: groupByControl,
+ dimension: {
+ ...groupByControl,
+ label: t('Dimension'),
+ description: t('Select a dimension'),
+ multi: false,
+ default: null,
+ },
columns: Object.assign({}, groupByControl, {
label: t('Columns'),
@@ -441,6 +457,28 @@ export const controls = {
}),
},
+ longitude: {
+ type: 'SelectControl',
+ label: t('Longitude'),
+ default: 1,
+ validators: [v.nonEmpty],
+ description: t('Select the longitude column'),
+ mapStateToProps: state => ({
+ choices: (state.datasource) ? state.datasource.all_cols : [],
+ }),
+ },
+
+ latitude: {
+ type: 'SelectControl',
+ label: t('Latitude'),
+ default: 1,
+ validators: [v.nonEmpty],
+ description: t('Select the latitude column'),
+ mapStateToProps: state => ({
+ choices: (state.datasource) ? state.datasource.all_cols : [],
+ }),
+ },
+
all_columns_x: {
type: 'SelectControl',
label: 'X',
@@ -730,6 +768,14 @@ export const controls = {
'with the [Periods] text box'),
},
+ multiplier: {
+ type: 'TextControl',
+ label: t('Multiplier'),
+ isFloat: true,
+ default: 1,
+ description: t('Factor to multiply the metric by'),
+ },
+
rolling_periods: {
type: 'TextControl',
label: t('Periods'),
@@ -738,6 +784,15 @@ export const controls = {
'relative to the time granularity selected'),
},
+ grid_size: {
+ type: 'TextControl',
+ label: t('Grid Size'),
+ renderTrigger: true,
+ default: 20,
+ isInt: true,
+ description: t('Defines the grid size in pixels'),
+ },
+
min_periods: {
type: 'TextControl',
label: t('Min Periods'),
@@ -1043,6 +1098,14 @@ export const controls = {
),
},
+ extruded: {
+ type: 'CheckboxControl',
+ label: t('Extruded'),
+ renderTrigger: true,
+ default: true,
+ description: ('Whether to make the grid 3D'),
+ },
+
show_brush: {
type: 'CheckboxControl',
label: t('Range Filter'),
@@ -1255,6 +1318,7 @@ export const controls = {
mapbox_style: {
type: 'SelectControl',
label: t('Map Style'),
+ renderTrigger: true,
choices: [
['mapbox://styles/mapbox/streets-v9', 'Streets'],
['mapbox://styles/mapbox/dark-v9', 'Dark'],
@@ -1288,6 +1352,15 @@ export const controls = {
'number of points (>1000) will cause lag.'),
},
+ point_radius_fixed: {
+ type: 'FixedOrMetricControl',
+ label: t('Point Size'),
+ description: t('Fixed point radius'),
+ mapStateToProps: state => ({
+ datasource: state.datasource,
+ }),
+ },
+
point_radius: {
type: 'SelectControl',
label: t('Point Radius'),
@@ -1308,6 +1381,22 @@ export const controls = {
description: t('The unit of measure for the specified point radius'),
},
+ point_unit: {
+ type: 'SelectControl',
+ label: t('Point Unit'),
+ default: 'square_m',
+ clearable: false,
+ choices: [
+ ['square_m', 'Square meters'],
+ ['square_km', 'Square kilometers'],
+ ['square_miles', 'Square miles'],
+ ['radius_m', 'Radius in meters'],
+ ['radius_km', 'Radius in kilometers'],
+ ['radius_miles', 'Radius in miles'],
+ ],
+ description: t('The unit of measure for the specified point radius'),
+ },
+
global_opacity: {
type: 'TextControl',
label: t('Opacity'),
@@ -1317,6 +1406,15 @@ export const controls = {
'Between 0 and 1.'),
},
+ viewport: {
+ type: 'ViewportControl',
+ label: t('Viewport'),
+ renderTrigger: true,
+ description: t('Parameters related to the view and perspective on the map'),
+ // default is whole world mostly centered
+ default: defaultViewport,
+ },
+
viewport_zoom: {
type: 'TextControl',
label: t('Zoom'),
@@ -1370,6 +1468,7 @@ export const controls = {
color: {
type: 'ColorPickerControl',
label: t('Color'),
+ default: colorPrimary,
description: t('Pick a color'),
},
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index 94115bb..b13d4a7 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -294,6 +294,153 @@ export const visTypes = {
},
},
+ deck_hex: {
+ label: t('Deck.gl - Hexagons'),
+ requiresTime: true,
+ controlPanelSections: [
+ {
+ label: t('Query'),
+ expanded: true,
+ controlSetRows: [
+ ['longitude', 'latitude'],
+ ['groupby', 'size'],
+ ['row_limit'],
+ ],
+ },
+ {
+ label: t('Map'),
+ controlSetRows: [
+ ['mapbox_style', 'viewport'],
+ ['color_picker', null],
+ ['grid_size', 'extruded'],
+ ],
+ },
+ ],
+ controlOverrides: {
+ size: {
+ label: t('Height'),
+ description: t('Metric used to control height'),
+ validators: [v.nonEmpty],
+ },
+ },
+ },
+
+ deck_grid: {
+ label: t('Deck.gl - Grid'),
+ requiresTime: true,
+ controlPanelSections: [
+ {
+ label: t('Query'),
+ expanded: true,
+ controlSetRows: [
+ ['longitude', 'latitude'],
+ ['groupby', 'size'],
+ ['row_limit'],
+ ],
+ },
+ {
+ label: t('Map'),
+ controlSetRows: [
+ ['mapbox_style', 'viewport'],
+ ['color_picker', null],
+ ['grid_size', 'extruded'],
+ ],
+ },
+ ],
+ controlOverrides: {
+ size: {
+ label: t('Height'),
+ description: t('Metric used to control height'),
+ validators: [v.nonEmpty],
+ },
+ },
+ },
+
+ deck_screengrid: {
+ label: t('Deck.gl - Screen grid'),
+ requiresTime: true,
+ controlPanelSections: [
+ {
+ label: t('Query'),
+ expanded: true,
+ controlSetRows: [
+ ['longitude', 'latitude'],
+ ['groupby', 'size'],
+ ['row_limit'],
+ ],
+ },
+ {
+ label: t('Map'),
+ controlSetRows: [
+ ['mapbox_style', 'viewport'],
+ ],
+ },
+ {
+ label: t('Grid'),
+ controlSetRows: [
+ ['grid_size', 'color_picker'],
+ ],
+ },
+ ],
+ controlOverrides: {
+ size: {
+ label: t('Weight'),
+ description: t("Metric used as a weight for the grid's coloring"),
+ validators: [v.nonEmpty],
+ },
+ },
+ },
+
+ deck_scatter: {
+ label: t('Deck.gl - Scatter plot'),
+ requiresTime: true,
+ controlPanelSections: [
+ {
+ label: t('Query'),
+ expanded: true,
+ controlSetRows: [
+ ['longitude', 'latitude'],
+ ['groupby'],
+ ['row_limit'],
+ ],
+ },
+ {
+ label: t('Map'),
+ controlSetRows: [
+ ['mapbox_style', 'viewport'],
+ ],
+ },
+ {
+ label: t('Point Size'),
+ controlSetRows: [
+ ['point_radius_fixed', 'point_unit'],
+ ['multiplier', null],
+ ],
+ },
+ {
+ label: t('Point Color'),
+ controlSetRows: [
+ ['color_picker', null],
+ ['dimension', 'color_scheme'],
+ ],
+ },
+ ],
+ controlOverrides: {
+ all_columns_x: {
+ label: t('Longitude Column'),
+ validators: [v.nonEmpty],
+ },
+ all_columns_y: {
+ label: t('Latitude Column'),
+ validators: [v.nonEmpty],
+ },
+ dimension: {
+ label: t('Categorical Color'),
+ description: t('Pick a dimension from which categorical colors are defined'),
+ },
+ },
+ },
+
area: {
label: t('Time Series - Stacked'),
requiresTime: true,
@@ -1062,9 +1209,8 @@ export const visTypes = {
{
label: t('Viewport'),
controlSetRows: [
- ['viewport_longitude'],
- ['viewport_latitude'],
- ['viewport_zoom'],
+ ['viewport_longitude', 'viewport_latitude'],
+ ['viewport_zoom', null],
],
},
],
diff --git a/superset/assets/javascripts/modules/colors.js b/superset/assets/javascripts/modules/colors.js
index 663fd6e..0c3d06a 100644
--- a/superset/assets/javascripts/modules/colors.js
+++ b/superset/assets/javascripts/modules/colors.js
@@ -142,3 +142,13 @@ export const colorScalerFactory = function (colors, data, accessor, extents) {
const points = colors.map((col, i) => ext[0] + (i * chunkSize));
return d3.scale.linear().domain(points).range(colors).clamp(true);
};
+
+export function hexToRGB(hex, alpha = 255) {
+ if (!hex) {
+ return [0, 0, 0, alpha];
+ }
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+ return [r, g, b, alpha];
+}
diff --git a/superset/assets/javascripts/modules/geo.js b/superset/assets/javascripts/modules/geo.js
new file mode 100644
index 0000000..e689a41
--- /dev/null
+++ b/superset/assets/javascripts/modules/geo.js
@@ -0,0 +1,25 @@
+export const defaultViewport = {
+ longitude: 6.85236157047845,
+ latitude: 31.222656842808707,
+ zoom: 1,
+ bearing: 0,
+ pitch: 0,
+};
+
+const METER_TO_MILE = 1609.34;
+export function unitToRadius(unit, num) {
+ if (unit === 'square_m') {
+ return Math.sqrt(num / Math.PI);
+ } else if (unit === 'radius_m') {
+ return num;
+ } else if (unit === 'radius_km') {
+ return num * 1000;
+ } else if (unit === 'radius_miles') {
+ return num * METER_TO_MILE;
+ } else if (unit === 'square_km') {
+ return Math.sqrt(num / Math.PI) * 1000;
+ } else if (unit === 'square_miles') {
+ return Math.sqrt(num / Math.PI) * METER_TO_MILE;
+ }
+ return null;
+}
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 99f8afc..51bee87 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -55,11 +55,14 @@
"d3-tip": "^0.6.7",
"datamaps": "^0.5.8",
"datatables.net-bs": "^1.10.15",
+ "deck.gl": "^4.1.5",
"distributions": "^1.0.0",
+ "geolib": "^2.0.24",
"immutable": "^3.8.2",
"jed": "^1.1.1",
"jquery": "3.1.1",
"lodash.throttle": "^4.1.1",
+ "luma.gl": "^4.0.5",
"moment": "2.18.1",
"mustache": "^2.2.1",
"nvd3": "1.8.6",
diff --git a/superset/assets/spec/javascripts/explore/components/FixedOrMetricControl_spec.jsx b/superset/assets/spec/javascripts/explore/components/FixedOrMetricControl_spec.jsx
new file mode 100644
index 0000000..6f5a1aa
--- /dev/null
+++ b/superset/assets/spec/javascripts/explore/components/FixedOrMetricControl_spec.jsx
@@ -0,0 +1,39 @@
+/* eslint-disable no-unused-expressions */
+import React from 'react';
+import { expect } from 'chai';
+import { describe, it, beforeEach } from 'mocha';
+import { shallow } from 'enzyme';
+import { OverlayTrigger } from 'react-bootstrap';
+
+import FixedOrMetricControl from
+ '../../../../javascripts/explore/components/controls/FixedOrMetricControl';
+import SelectControl from
+ '../../../../javascripts/explore/components/controls/SelectControl';
+import TextControl from
+ '../../../../javascripts/explore/components/controls/TextControl';
+import ControlHeader from '../../../../javascripts/explore/components/ControlHeader';
+
+const defaultProps = {
+ value: { },
+};
+
+describe('FixedOrMetricControl', () => {
+ let wrapper;
+ let inst;
+ beforeEach(() => {
+ wrapper = shallow(<FixedOrMetricControl {...defaultProps} />);
+ inst = wrapper.instance();
+ });
+
+ it('renders a OverlayTrigger', () => {
+ const controlHeader = wrapper.find(ControlHeader);
+ expect(controlHeader).to.have.lengthOf(1);
+ expect(wrapper.find(OverlayTrigger)).to.have.length(1);
+ });
+
+ it('renders a TextControl and a SelectControl', () => {
+ const popOver = shallow(inst.renderPopover());
+ expect(popOver.find(TextControl)).to.have.lengthOf(1);
+ expect(popOver.find(SelectControl)).to.have.lengthOf(1);
+ });
+});
diff --git a/superset/assets/spec/javascripts/explore/components/ViewportControl_spec.jsx b/superset/assets/spec/javascripts/explore/components/ViewportControl_spec.jsx
new file mode 100644
index 0000000..9864d83
--- /dev/null
+++ b/superset/assets/spec/javascripts/explore/components/ViewportControl_spec.jsx
@@ -0,0 +1,46 @@
+/* eslint-disable no-unused-expressions */
+import React from 'react';
+import { expect } from 'chai';
+import { describe, it, beforeEach } from 'mocha';
+import { shallow } from 'enzyme';
+import { OverlayTrigger, Label } from 'react-bootstrap';
+
+import ViewportControl from
+ '../../../../javascripts/explore/components/controls/ViewportControl';
+import TextControl from
+ '../../../../javascripts/explore/components/controls/TextControl';
+import ControlHeader from '../../../../javascripts/explore/components/ControlHeader';
+
+const defaultProps = {
+ value: {
+ longitude: 6.85236157047845,
+ latitude: 31.222656842808707,
+ zoom: 1,
+ bearing: 0,
+ pitch: 0,
+ },
+};
+
+describe('ViewportControl', () => {
+ let wrapper;
+ let inst;
+ beforeEach(() => {
+ wrapper = shallow(<ViewportControl {...defaultProps} />);
+ inst = wrapper.instance();
+ });
+
+ it('renders a OverlayTrigger', () => {
+ const controlHeader = wrapper.find(ControlHeader);
+ expect(controlHeader).to.have.lengthOf(1);
+ expect(wrapper.find(OverlayTrigger)).to.have.length(1);
+ });
+
+ it('renders a Popover with 5 TextControl', () => {
+ const popOver = shallow(inst.renderPopover());
+ expect(popOver.find(TextControl)).to.have.lengthOf(5);
+ });
+
+ it('renders a summary in the label', () => {
+ expect(wrapper.find(Label).first().render().text()).to.equal('6° 51\' 8.50" | 31° 13\' 21.56"');
+ });
+});
diff --git a/superset/assets/spec/javascripts/modules/colors_spec.jsx b/superset/assets/spec/javascripts/modules/colors_spec.jsx
index 5585471..31ccea8 100644
--- a/superset/assets/spec/javascripts/modules/colors_spec.jsx
+++ b/superset/assets/spec/javascripts/modules/colors_spec.jsx
@@ -1,7 +1,7 @@
import { it, describe } from 'mocha';
import { expect } from 'chai';
-import { ALL_COLOR_SCHEMES, getColorFromScheme } from '../../../javascripts/modules/colors';
+import { ALL_COLOR_SCHEMES, getColorFromScheme, hexToRGB } from '../../../javascripts/modules/colors';
describe('colors', () => {
it('default to bnbColors', () => {
@@ -19,4 +19,13 @@ describe('colors', () => {
expect(color1).to.equal(color3);
expect(color4).to.equal(ALL_COLOR_SCHEMES.bnbColors[1]);
});
+
+ it('hexToRGB converts properly', () => {
+ expect(hexToRGB('#FFFFFF')).to.have.same.members([255, 255, 255, 255]);
+ expect(hexToRGB('#000000')).to.have.same.members([0, 0, 0, 255]);
+ expect(hexToRGB('#FF0000')).to.have.same.members([255, 0, 0, 255]);
+ expect(hexToRGB('#00FF00')).to.have.same.members([0, 255, 0, 255]);
+ expect(hexToRGB('#0000FF')).to.have.same.members([0, 0, 255, 255]);
+ expect(hexToRGB('#FF0000', 128)).to.have.same.members([255, 0, 0, 128]);
+ });
});
diff --git a/superset/assets/spec/javascripts/modules/geo_spec.jsx b/superset/assets/spec/javascripts/modules/geo_spec.jsx
new file mode 100644
index 0000000..758bf15
--- /dev/null
+++ b/superset/assets/spec/javascripts/modules/geo_spec.jsx
@@ -0,0 +1,27 @@
+import { it, describe } from 'mocha';
+import { expect } from 'chai';
+
+import { unitToRadius } from '../../../javascripts/modules/geo';
+
+const METER_TO_MILE = 1609.34;
+
+describe('unitToRadius', () => {
+ it('converts to square meters', () => {
+ expect(unitToRadius('square_m', 4 * Math.PI)).to.equal(2);
+ });
+ it('converts to square meters', () => {
+ expect(unitToRadius('square_km', 25 * Math.PI)).to.equal(5000);
+ });
+ it('converts to radius meters', () => {
+ expect(unitToRadius('radius_m', 1000)).to.equal(1000);
+ });
+ it('converts to radius km', () => {
+ expect(unitToRadius('radius_km', 1)).to.equal(1000);
+ });
+ it('converts to radius miles', () => {
+ expect(unitToRadius('radius_miles', 1)).to.equal(METER_TO_MILE);
+ });
+ it('converts to square miles', () => {
+ expect(unitToRadius('square_miles', 25 * Math.PI)).to.equal(5000 * (METER_TO_MILE / 1000));
+ });
+});
diff --git a/superset/assets/visualizations/deckgl/DeckGLContainer.jsx b/superset/assets/visualizations/deckgl/DeckGLContainer.jsx
new file mode 100644
index 0000000..64ee934
--- /dev/null
+++ b/superset/assets/visualizations/deckgl/DeckGLContainer.jsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import MapGL from 'react-map-gl';
+import DeckGL from 'deck.gl';
+
+const propTypes = {
+ viewport: PropTypes.object.isRequired,
+ layers: PropTypes.array.isRequired,
+ setControlValue: PropTypes.func.isRequired,
+ mapStyle: PropTypes.string,
+ mapboxApiAccessToken: PropTypes.string.isRequired,
+ onViewportChange: PropTypes.func,
+};
+const defaultProps = {
+ mapStyle: 'light',
+ onViewportChange: () => {},
+};
+
+export default class DeckGLContainer extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ viewport: props.viewport,
+ };
+ this.tick = this.tick.bind(this);
+ this.onViewportChange = this.onViewportChange.bind(this);
+ }
+ componentWillMount() {
+ const timer = setInterval(this.tick, 1000);
+ this.setState(() => ({ timer }));
+ }
+ componentWillReceiveProps(nextProps) {
+ this.setState(() => ({
+ viewport: { ...nextProps.viewport },
+ }));
+ }
+ componentWillUnmount() {
+ this.clearInterval(this.state.timer);
+ }
+ onViewportChange(viewport) {
+ const vp = Object.assign({}, viewport);
+ delete vp.width;
+ delete vp.height;
+ const newVp = { ...this.state.viewport, ...vp };
+
+ this.setState(() => ({ viewport: newVp }));
+ this.props.onViewportChange(newVp);
+ }
+ tick() {
+ // Limiting updating viewport controls through Redux at most 1*sec
+ if (this.state.previousViewport !== this.state.viewport) {
+ const setCV = this.props.setControlValue;
+ const vp = this.state.viewport;
+ if (setCV) {
+ setCV('viewport', vp);
+ }
+ this.setState(() => ({ previousViewport: this.state.viewport }));
+ }
+ }
+ layers() {
+ // Support for layer factory
+ if (this.props.layers.some(l => typeof l === 'function')) {
+ return this.props.layers.map(l => typeof l === 'function' ? l() : l);
+ }
+ return this.props.layers;
+ }
+ render() {
+ const { viewport } = this.state;
+ return (
+ <MapGL
+ {...viewport}
+ mapStyle={this.props.mapStyle}
+ onViewportChange={this.onViewportChange}
+ mapboxApiAccessToken={this.props.mapboxApiAccessToken}
+ >
+ <DeckGL
+ {...viewport}
+ layers={this.layers()}
+ initWebGLParameters
+ />
+ </MapGL>
+ );
+ }
+}
+
+DeckGLContainer.propTypes = propTypes;
+DeckGLContainer.defaultProps = defaultProps;
diff --git a/superset/assets/visualizations/deckgl/grid.jsx b/superset/assets/visualizations/deckgl/grid.jsx
new file mode 100644
index 0000000..1ef2394
--- /dev/null
+++ b/superset/assets/visualizations/deckgl/grid.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { GridLayer } from 'deck.gl';
+
+import DeckGLContainer from './DeckGLContainer';
+
+function deckScreenGridLayer(slice, payload, setControlValue) {
+ const fd = slice.formData;
+ const c = fd.color_picker;
+ const data = payload.data.features.map(d => ({
+ ...d,
+ color: [c.r, c.g, c.b, 255 * c.a],
+ }));
+
+ const layer = new GridLayer({
+ id: `grid-layer-${slice.containerId}`,
+ data,
+ pickable: true,
+ cellSize: fd.grid_size,
+ minColor: [0, 0, 0, 0],
+ extruded: fd.extruded,
+ maxColor: [c.r, c.g, c.b, 255 * c.a],
+ outline: false,
+ getElevationValue: points => points.reduce((sum, point) => sum + point.weight, 0),
+ getColorValue: points => points.reduce((sum, point) => sum + point.weight, 0),
+ });
+ 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 = deckScreenGridLayer;
diff --git a/superset/assets/visualizations/deckgl/hex.jsx b/superset/assets/visualizations/deckgl/hex.jsx
new file mode 100644
index 0000000..9526825
--- /dev/null
+++ b/superset/assets/visualizations/deckgl/hex.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { HexagonLayer } from 'deck.gl';
+
+import DeckGLContainer from './DeckGLContainer';
+
+function deckHex(slice, payload, setControlValue) {
+ const fd = slice.formData;
+ const c = fd.color_picker;
+ const data = payload.data.features.map(d => ({
+ ...d,
+ color: [c.r, c.g, c.b, 255 * c.a],
+ }));
+
+ const layer = new HexagonLayer({
+ id: `hex-layer-${slice.containerId}`,
+ data,
+ pickable: true,
+ radius: fd.grid_size,
+ minColor: [0, 0, 0, 0],
+ extruded: fd.extruded,
+ maxColor: [c.r, c.g, c.b, 255 * c.a],
+ outline: false,
+ getElevationValue: points => points.reduce((sum, point) => sum + point.weight, 0),
+ getColorValue: points => points.reduce((sum, point) => sum + point.weight, 0),
+ });
+ 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 = deckHex;
diff --git a/superset/assets/visualizations/deckgl/scatter.jsx b/superset/assets/visualizations/deckgl/scatter.jsx
new file mode 100644
index 0000000..18cec55
--- /dev/null
+++ b/superset/assets/visualizations/deckgl/scatter.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { ScatterplotLayer } from 'deck.gl';
+
+import DeckGLContainer from './DeckGLContainer';
+import { getColorFromScheme, hexToRGB } from '../../javascripts/modules/colors';
+import { unitToRadius } from '../../javascripts/modules/geo';
+
+function deckScatter(slice, payload, setControlValue) {
+ const fd = slice.formData;
+ const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
+ const fixedColor = [c.r, c.g, c.b, 255 * c.a];
+
+ const data = payload.data.features.map((d) => {
+ let radius = unitToRadius(fd.point_unit, d.radius) || 10;
+ if (fd.multiplier) {
+ radius *= fd.multiplier;
+ }
+ let color;
+ if (fd.dimension) {
+ color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
+ } else {
+ color = fixedColor;
+ }
+ return {
+ ...d,
+ radius,
+ color,
+ };
+ });
+
+ const layer = new ScatterplotLayer({
+ id: `scatter-layer-${slice.containerId}`,
+ data,
+ pickable: true,
+ fp64: true,
+ outline: false,
+ });
+ 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 = deckScatter;
diff --git a/superset/assets/visualizations/deckgl/screengrid.jsx b/superset/assets/visualizations/deckgl/screengrid.jsx
new file mode 100644
index 0000000..b8b58ec
--- /dev/null
+++ b/superset/assets/visualizations/deckgl/screengrid.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { ScreenGridLayer } from 'deck.gl';
+
+import DeckGLContainer from './DeckGLContainer';
+
+function deckScreenGridLayer(slice, payload, setControlValue) {
+ const fd = slice.formData;
+ const c = fd.color_picker;
+ const data = payload.data.features.map(d => ({
+ ...d,
+ color: [c.r, c.g, c.b, 255 * c.a],
+ }));
+
+ const viewport = {
+ ...fd.viewport,
+ width: slice.width(),
+ height: slice.height(),
+ };
+ // Passing a layer creator function instead of a layer since the
+ // layer needs to be regenerated at each render
+ const layer = () => new ScreenGridLayer({
+ id: `screengrid-layer-${slice.containerId}`,
+ data,
+ pickable: true,
+ cellSizePixels: fd.grid_size,
+ minColor: [c.r, c.g, c.b, 0],
+ maxColor: [c.r, c.g, c.b, 255 * c.a],
+ outline: false,
+ getWeight: d => d.weight || 0,
+ });
+ ReactDOM.render(
+ <DeckGLContainer
+ mapboxApiAccessToken={payload.data.mapboxApiKey}
+ viewport={viewport}
+ layers={[layer]}
+ mapStyle={fd.mapbox_style}
+ setControlValue={setControlValue}
+ />,
+ document.getElementById(slice.containerId),
+ );
+}
+module.exports = deckScreenGridLayer;
diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js
index 78e81ab..2afc57b 100644
--- a/superset/assets/visualizations/main.js
+++ b/superset/assets/visualizations/main.js
@@ -36,5 +36,9 @@ const vizMap = {
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'),
};
export default vizMap;
diff --git a/superset/cli.py b/superset/cli.py
index 540bea8..a9f89af 100755
--- a/superset/cli.py
+++ b/superset/cli.py
@@ -133,10 +133,16 @@ def load_examples(load_test_data):
print('Loading [Misc Charts] dashboard')
data.load_misc_dashboard()
+ print("Loading DECK.gl demo")
+ data.load_deck_dash()
+
if load_test_data:
print('Loading [Unicode test data]')
data.load_unicode_test_data()
+ print("Loading flights data")
+ data.load_flights()
+
@manager.option(
'-d', '--datasource',
diff --git a/superset/data/__init__.py b/superset/data/__init__.py
index a96749a..f534914 100644
--- a/superset/data/__init__.py
+++ b/superset/data/__init__.py
@@ -1226,3 +1226,276 @@ def load_misc_dashboard():
dash.slices = slices
db.session.merge(dash)
db.session.commit()
+
+
+def load_deck_dash():
+ print("Loading deck.gl dashboard")
+ slices = []
+ tbl = db.session.query(TBL).filter_by(table_name='long_lat').first()
+ slice_data = {
+ "longitude": "LON",
+ "latitude": "LAT",
+ "color_picker": {
+ "r": 205,
+ "g": 0,
+ "b": 3,
+ "a": 0.82,
+ },
+ "datasource": "5__table",
+ "filters": [],
+ "granularity_sqla": "date",
+ "groupby": [],
+ "having": "",
+ "mapbox_style": "mapbox://styles/mapbox/light-v9",
+ "multiplier": 10,
+ "point_radius_fixed": {"type": "metric", "value": "count"},
+ "point_unit": "square_m",
+ "row_limit": 5000,
+ "since": "2014-01-01",
+ "size": "count",
+ "time_grain_sqla": "Time Column",
+ "until": "now",
+ "viewport": {
+ "bearing": -4.952916738791771,
+ "latitude": 37.78926922909199,
+ "longitude": -122.42613341901688,
+ "pitch": 4.750411100577438,
+ "zoom": 12.729132798697304,
+ },
+ "viz_type": "deck_scatter",
+ "where": "",
+ }
+
+ print("Creating Scatterplot slice")
+ slc = Slice(
+ slice_name="Scatterplot",
+ viz_type='deck_scatter',
+ datasource_type='table',
+ datasource_id=tbl.id,
+ params=get_slice_json(slice_data),
+ )
+ merge_slice(slc)
+ slices.append(slc)
+
+ slice_data = {
+ "point_unit": "square_m",
+ "filters": [],
+ "row_limit": 5000,
+ "longitude": "LON",
+ "latitude": "LAT",
+ "mapbox_style": "mapbox://styles/mapbox/dark-v9",
+ "granularity_sqla": "date",
+ "size": "count",
+ "viz_type": "deck_screengrid",
+ "since": "2014-01-01",
+ "point_radius": "Auto",
+ "until": "now",
+ "color_picker": {"a": 1,
+ "r": 14,
+ "b": 0,
+ "g": 255},
+ "grid_size": 20,
+ "where": "",
+ "having": "",
+ "viewport": {
+ "zoom": 14.161641703941438,
+ "longitude": -122.41827069521386,
+ "bearing": -4.952916738791771,
+ "latitude": 37.76024135844065,
+ "pitch": 4.750411100577438,
+ },
+ "point_radius_fixed": {"type": "fix", "value": 2000},
+ "datasource": "5__table",
+ "time_grain_sqla": "Time Column",
+ "groupby": [],
+ }
+ print("Creating Screen Grid slice")
+ slc = Slice(
+ slice_name="Screen grid",
+ viz_type='deck_screengrid',
+ datasource_type='table',
+ datasource_id=tbl.id,
+ params=get_slice_json(slice_data),
+ )
+ merge_slice(slc)
+ slices.append(slc)
+
+ slice_data = {
+ "filters": [],
+ "row_limit": 5000,
+ "longitude": "LON",
+ "latitude": "LAT",
+ "mapbox_style": "mapbox://styles/mapbox/streets-v9",
+ "granularity_sqla": "date",
+ "size": "count",
+ "viz_type": "deck_hex",
+ "since": "2014-01-01",
+ "point_radius_unit": "Pixels",
+ "point_radius": "Auto",
+ "until": "now",
+ "color_picker": {
+ "a": 1,
+ "r": 14,
+ "b": 0,
+ "g": 255,
+ },
+ "grid_size": 40,
+ "extruded": True,
+ "having": "",
+ "viewport": {
+ "latitude": 37.789795085160335,
+ "pitch": 54.08961642447763,
+ "zoom": 13.835465702403654,
+ "longitude": -122.40632230075536,
+ "bearing": -2.3984797349335167,
+ },
+ "where": "",
+ "point_radius_fixed": {"type": "fix", "value": 2000},
+ "datasource": "5__table",
+ "time_grain_sqla": "Time Column",
+ "groupby": [],
+ }
+ print("Creating Hex slice")
+ slc = Slice(
+ slice_name="Hexagons",
+ viz_type='deck_hex',
+ datasource_type='table',
+ datasource_id=tbl.id,
+ params=get_slice_json(slice_data),
+ )
+ merge_slice(slc)
+ slices.append(slc)
+
+ slice_data = {
+ "filters": [],
+ "row_limit": 5000,
+ "longitude": "LON",
+ "latitude": "LAT",
+ "mapbox_style": "mapbox://styles/mapbox/satellite-streets-v9",
+ "granularity_sqla": "date",
+ "size": "count",
+ "viz_type": "deck_grid",
+ "since": "2014-01-01",
+ "point_radius_unit": "Pixels",
+ "point_radius": "Auto",
+ "until": "now",
+ "color_picker": {
+ "a": 1,
+ "r": 14,
+ "b": 0,
+ "g": 255,
+ },
+ "grid_size": 120,
+ "extruded": True,
+ "having": "",
+ "viewport": {
+ "longitude": -122.42066918995666,
+ "bearing": 155.80099696026355,
+ "zoom": 12.699690845482069,
+ "latitude": 37.7942314882596,
+ "pitch": 53.470800300695146,
+ },
+ "where": "",
+ "point_radius_fixed": {"type": "fix", "value": 2000},
+ "datasource": "5__table",
+ "time_grain_sqla": "Time Column",
+ "groupby": [],
+ }
+ print("Creating Grid slice")
+ slc = Slice(
+ slice_name="Grid",
+ viz_type='deck_grid',
+ datasource_type='table',
+ datasource_id=tbl.id,
+ params=get_slice_json(slice_data),
+ )
+ merge_slice(slc)
+ slices.append(slc)
+
+ print("Creating a dashboard")
+ title = "deck.gl Demo"
+ dash = db.session.query(Dash).filter_by(dashboard_title=title).first()
+
+ if not dash:
+ dash = Dash()
+ js = textwrap.dedent("""\
+ [
+ {
+ "col": 1,
+ "row": 0,
+ "size_x": 6,
+ "size_y": 4,
+ "slice_id": "37"
+ },
+ {
+ "col": 7,
+ "row": 0,
+ "size_x": 6,
+ "size_y": 4,
+ "slice_id": "38"
+ },
+ {
+ "col": 7,
+ "row": 4,
+ "size_x": 6,
+ "size_y": 4,
+ "slice_id": "39"
+ },
+ {
+ "col": 1,
+ "row": 4,
+ "size_x": 6,
+ "size_y": 4,
+ "slice_id": "40"
+ }
+ ]
+ """)
+ l = json.loads(js)
+ for i, pos in enumerate(l):
+ pos['slice_id'] = str(slices[i].id)
+ dash.dashboard_title = title
+ dash.position_json = json.dumps(l, indent=4)
+ dash.slug = "deck"
+ dash.slices = slices
+ db.session.merge(dash)
+ db.session.commit()
+
+
+def load_flights():
+ """Loading random time series data from a zip file in the repo"""
+ with gzip.open(os.path.join(DATA_FOLDER, 'fligth_data.csv.gz')) as f:
+ pdf = pd.read_csv(f, encoding='latin-1')
+
+ # Loading airports info to join and get lat/long
+ with gzip.open(os.path.join(DATA_FOLDER, 'airports.csv.gz')) as f:
+ airports = pd.read_csv(f, encoding='latin-1')
+ airports = airports.set_index('IATA_CODE')
+
+ pdf['ds'] = pdf.YEAR.map(str) + '-0' + pdf.MONTH.map(str) + '-0' + pdf.DAY.map(str)
+ pdf.ds = pd.to_datetime(pdf.ds)
+ del pdf['YEAR']
+ del pdf['MONTH']
+ del pdf['DAY']
+
+ pdf = pdf.join(airports, on='ORIGIN_AIRPORT', rsuffix='_ORIG')
+ pdf = pdf.join(airports, on='DESTINATION_AIRPORT', rsuffix='_DEST')
+ pdf.to_sql(
+ 'flights',
+ db.engine,
+ if_exists='replace',
+ chunksize=500,
+ dtype={
+ 'ds': DateTime,
+ },
+ index=False)
+ print("Done loading table!")
+
+ print("Creating table [random_time_series] reference")
+ obj = db.session.query(TBL).filter_by(table_name='random_time_series').first()
+ if not obj:
+ obj = TBL(table_name='flights')
+ obj.main_dttm_col = 'ds'
+ obj.database = get_or_create_main_db()
+ db.session.merge(obj)
+ db.session.commit()
+ obj.fetch_metadata()
diff --git a/superset/data/airports.csv.gz b/superset/data/airports.csv.gz
new file mode 100644
index 0000000..3043486
Binary files /dev/null and b/superset/data/airports.csv.gz differ
diff --git a/superset/data/fligth_data.csv.gz b/superset/data/fligth_data.csv.gz
new file mode 100644
index 0000000..bbdebdf
Binary files /dev/null and b/superset/data/fligth_data.csv.gz differ
diff --git a/superset/viz.py b/superset/viz.py
index 8a90c81..932f204 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -1730,7 +1730,111 @@ class MapboxViz(BaseViz):
}
+class BaseDeckGLViz(BaseViz):
+
+ """Base class for deck.gl visualizations"""
+
+ is_timeseries = False
+ credits = '<a href="https://uber.github.io/deck.gl/">deck.gl</a>'
+
+ def get_metrics(self):
+ self.metric = self.form_data.get('size')
+ return [self.metric]
+
+ def get_properties(self, d):
+ return {
+ 'weight': d.get(self.metric) or 1,
+ }
+
+ def get_position(self, d):
+ return [
+ d.get(self.form_data.get('longitude')),
+ d.get(self.form_data.get('latitude')),
+ ]
+
+ def query_obj(self):
+ d = super(BaseDeckGLViz, self).query_obj()
+ fd = self.form_data
+
+ d['groupby'] = [fd.get('longitude'), fd.get('latitude')]
+ if fd.get('dimension'):
+ d['groupby'] += [fd.get('dimension')]
+
+ d['metrics'] = self.get_metrics()
+ return d
+
+ def get_data(self, df):
+ features = []
+ for d in df.to_dict(orient='records'):
+ d = dict(position=self.get_position(d), **self.get_properties(d))
+ features.append(d)
+ return {
+ "features": features,
+ "mapboxApiKey": config.get('MAPBOX_API_KEY'),
+ }
+
+
+class DeckScatterViz(BaseDeckGLViz):
+
+ """deck.gl's ScatterLayer"""
+
+ viz_type = "deck_scatter"
+ verbose_name = _("Deck.gl - Scatter plot")
+
+ def query_obj(self):
+ self.point_radius_fixed = self.form_data.get('point_radius_fixed')
+ return super(DeckScatterViz, self).query_obj()
+
+ def get_metrics(self):
+ if self.point_radius_fixed.get('type') == 'metric':
+ self.metric = self.point_radius_fixed.get('value')
+ else:
+ self.metric = 'count'
+ return [self.metric]
+
+ def get_properties(self, d):
+ return {
+ "radius": self.fixed_value if self.fixed_value else d.get(self.metric),
+ "cat_color": d.get(self.dim) if self.dim else None,
+ }
+
+ def get_data(self, df):
+ fd = self.form_data
+ self.point_radius_fixed = fd.get('point_radius_fixed')
+ self.fixed_value = None
+ self.dim = self.form_data.get('dimension')
+ if self.point_radius_fixed.get('type') != 'metric':
+ self.fixed_value = self.point_radius_fixed.get('value')
+
+ return super(DeckScatterViz, self).get_data(df)
+
+
+class DeckScreengrid(BaseDeckGLViz):
+
+ """deck.gl's ScreenGridLayer"""
+
+ viz_type = "deck_screengrid"
+ verbose_name = _("Deck.gl - Screen Grid")
+
+
+class DeckGrid(BaseDeckGLViz):
+
+ """deck.gl's DeckLayer"""
+
+ viz_type = "deck_grid"
+ verbose_name = _("Deck.gl - 3D Grid")
+
+
+class DeckHex(BaseDeckGLViz):
+
+ """deck.gl's DeckLayer"""
+
+ viz_type = "deck_hex"
+ verbose_name = _("Deck.gl - 3D HEX")
+
+
class EventFlowViz(BaseViz):
+
"""A visualization to explore patterns in event sequences"""
viz_type = 'event_flow'
--
To stop receiving notification emails like this one, please contact
['"commits@superset.apache.org" <co...@superset.apache.org>'].