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>'].