graceguo-supercat closed pull request #3771: DECKGL integration - Phase 1
URL: https://github.com/apache/incubator-superset/pull/3771
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/.pylintrc b/.pylintrc
index 27b41d7b42..8a0e0ed78f 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 0000000000..cd93965106
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 0000000000..31feff5c8f
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 0000000000..11f38ccc8d
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 0000000000..d5da29c99b
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 7d8fcc3dcd..e458807633 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 0000000000..bf9e7f13e2
--- /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 a82995d70e..6441b71c8d 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 4fe558e052..bfe3f99177 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 0000000000..382b7f7173
--- /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 876bc4a1c6..094a26b323 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 e3da06b621..eabd29fe77 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 94115bb076..b13d4a787e 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 663fd6e494..0c3d06a09d 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 0000000000..e689a41682
--- /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 6f6068d6e5..21274cfdbb 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 0000000000..6f5a1aa5b5
--- /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 0000000000..9864d83890
--- /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 558547101a..31ccea8326 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 0000000000..758bf15a13
--- /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 0000000000..64ee934ddb
--- /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 0000000000..1ef2394873
--- /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 0000000000..9526825d25
--- /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 0000000000..18cec553fa
--- /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 0000000000..b8b58ec056
--- /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 78e81ab6d7..2afc57b617 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 0c666e1261..e6dde29d25 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 39151062f2..e0794472b5 100644
--- a/superset/data/__init__.py
+++ b/superset/data/__init__.py
@@ -1227,3 +1227,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 0000000000..3043486664
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 0000000000..bbdebdfafc
Binary files /dev/null and b/superset/data/fligth_data.csv.gz differ
diff --git a/superset/viz.py b/superset/viz.py
index 025e9c52b0..b3320d149b 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -1731,7 +1731,111 @@ def get_data(self, df):
         }
 
 
+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"


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


With regards,
Apache Git Services

Reply via email to