superset-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From GitBox <...@apache.org>
Subject [GitHub] mistercrunch closed pull request #4032: [geospatial] provide more flexible Spatial controls
Date Thu, 01 Jan 1970 00:00:00 GMT
mistercrunch closed pull request #4032:  [geospatial] provide more flexible Spatial controls

URL: https://github.com/apache/incubator-superset/pull/4032
 
 
   

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/setup.py b/setup.py
index b50cf28cc9..8152c9c0d7 100644
--- a/setup.py
+++ b/setup.py
@@ -58,6 +58,7 @@ def get_git_sha():
         'flask-wtf==0.14.2',
         'flower==0.9.1',
         'future>=0.16.0, <0.17',
+        'python-geohash==0.8.5',
         'humanize==0.5.1',
         'gunicorn==19.7.1',
         'idna==2.5',
diff --git a/superset/__init__.py b/superset/__init__.py
index c96fea8d57..9ef665ecca 100644
--- a/superset/__init__.py
+++ b/superset/__init__.py
@@ -42,7 +42,7 @@ def parse_manifest_json():
         with open(MANIFEST_FILE, 'r') as f:
             manifest = json.load(f)
     except Exception:
-        print('no manifest file found at ' + MANIFEST_FILE)
+        pass
 
 
 def get_manifest_file(filename):
diff --git a/superset/assets/javascripts/components/PopoverSection.jsx b/superset/assets/javascripts/components/PopoverSection.jsx
index 149036681b..2142679ef5 100644
--- a/superset/assets/javascripts/components/PopoverSection.jsx
+++ b/superset/assets/javascripts/components/PopoverSection.jsx
@@ -23,7 +23,7 @@ export default function PopoverSection({ title, isSelected, children, onSelect,
         &nbsp;
         <i className={isSelected ? 'fa fa-check text-primary' : ''} />
       </div>
-      <div>
+      <div className="m-t-5 m-l-5">
         {children}
       </div>
     </div>);
diff --git a/superset/assets/javascripts/explore/components/controls/DateFilterControl.jsx
b/superset/assets/javascripts/explore/components/controls/DateFilterControl.jsx
index fc365bd0f7..98db93a7e7 100644
--- a/superset/assets/javascripts/explore/components/controls/DateFilterControl.jsx
+++ b/superset/assets/javascripts/explore/components/controls/DateFilterControl.jsx
@@ -91,7 +91,7 @@ export default class DateFilterControl extends React.Component {
   renderPopover() {
     return (
       <Popover id="filter-popover">
-        <div style={{ width: '240px' }}>
+        <div style={{ width: '250px' }}>
           <PopoverSection
             title="Fixed"
             isSelected={this.state.type === 'fix'}
diff --git a/superset/assets/javascripts/explore/components/controls/SelectControl.jsx b/superset/assets/javascripts/explore/components/controls/SelectControl.jsx
index 6441b71c8d..3e8c7be394 100644
--- a/superset/assets/javascripts/explore/components/controls/SelectControl.jsx
+++ b/superset/assets/javascripts/explore/components/controls/SelectControl.jsx
@@ -24,6 +24,7 @@ const propTypes = {
   valueRenderer: PropTypes.func,
   valueKey: PropTypes.string,
   options: PropTypes.array,
+  placeholder: PropTypes.string,
 };
 
 const defaultProps = {
@@ -105,10 +106,11 @@ export default class SelectControl extends React.PureComponent {
   }
   render() {
     //  Tab, comma or Enter will trigger a new option created for FreeFormSelect
+    const placeholder = this.props.placeholder || t('Select %s', this.state.options.length);
     const selectProps = {
       multi: this.props.multi,
       name: `select-${this.props.name}`,
-      placeholder: t('Select %s', this.state.options.length),
+      placeholder,
       options: this.state.options,
       value: this.props.value,
       labelKey: 'label',
diff --git a/superset/assets/javascripts/explore/components/controls/SpatialControl.jsx b/superset/assets/javascripts/explore/components/controls/SpatialControl.jsx
new file mode 100644
index 0000000000..f72306f552
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/controls/SpatialControl.jsx
@@ -0,0 +1,222 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+  Row, Col, Button, FormControl, Label, OverlayTrigger, Popover,
+} from 'react-bootstrap';
+import 'react-datetime/css/react-datetime.css';
+
+import ControlHeader from '../ControlHeader';
+import SelectControl from './SelectControl';
+import PopoverSection from '../../../components/PopoverSection';
+import Checkbox from '../../../components/Checkbox';
+import { t } from '../../../locales';
+
+const spatialTypes = {
+  latlong: 'latlong',
+  delimited: 'delimited',
+  geohash: 'geohash',
+};
+
+const propTypes = {
+  onChange: PropTypes.func,
+  value: PropTypes.object,
+  animation: PropTypes.bool,
+  choices: PropTypes.array,
+};
+
+const defaultProps = {
+  onChange: () => {},
+  animation: true,
+  choices: [],
+};
+
+export default class SpatialControl extends React.Component {
+  constructor(props) {
+    super(props);
+    const v = props.value || {};
+    let defaultCol;
+    if (props.choices.length > 0) {
+      defaultCol = props.choices[0][0];
+    }
+    this.state = {
+      type: v.type || spatialTypes.latlong,
+      delimiter: v.delimiter || ',',
+      latCol: v.latCol || defaultCol,
+      lonCol: v.lonCol || defaultCol,
+      lonlatCol: v.lonlatCol || defaultCol,
+      reverseCheckbox: v.reverseCheckbox || false,
+      geohashCol: v.geohashCol || defaultCol,
+      value: null,
+      errors: [],
+    };
+    this.onDelimiterChange = this.onDelimiterChange.bind(this);
+    this.toggleCheckbox = this.toggleCheckbox.bind(this);
+    this.onChange = this.onChange.bind(this);
+  }
+  componentDidMount() {
+    this.onChange();
+  }
+  onChange() {
+    const type = this.state.type;
+    const value = { type };
+    const errors = [];
+    const errMsg = t('Invalid lat/long configuration.');
+    if (type === spatialTypes.latlong) {
+      value.latCol = this.state.latCol;
+      value.lonCol = this.state.lonCol;
+      if (!value.lonCol || !value.latCol) {
+        errors.push(errMsg);
+      }
+    } else if (type === spatialTypes.delimited) {
+      value.lonlatCol = this.state.lonlatCol;
+      value.delimiter = this.state.delimiter;
+      value.reverseCheckbox = this.state.reverseCheckbox;
+      if (!value.lonlatCol || !value.delimiter) {
+        errors.push(errMsg);
+      }
+    } else if (type === spatialTypes.geohash) {
+      value.geohashCol = this.state.geohashCol;
+      if (!value.geohashCol) {
+        errors.push(errMsg);
+      }
+    }
+    this.setState({ value, errors });
+    this.props.onChange(value, errors);
+  }
+  onDelimiterChange(event) {
+    this.setState({ delimiter: event.target.value }, this.onChange);
+  }
+  setType(type) {
+    this.setState({ type }, this.onChange);
+  }
+  close() {
+    this.refs.trigger.hide();
+  }
+  toggleCheckbox() {
+    this.setState({ reverseCheckbox: !this.state.reverseCheckbox }, this.onChange);
+  }
+  renderLabelContent() {
+    if (this.state.errors.length > 0) {
+      return 'N/A';
+    }
+    if (this.state.type === spatialTypes.latlong) {
+      return `${this.state.lonCol} | ${this.state.latCol}`;
+    } else if (this.state.type === spatialTypes.delimited) {
+      return `${this.state.lonlatCol}`;
+    } else if (this.state.type === spatialTypes.geohash) {
+      return `${this.state.geohashCol}`;
+    }
+    return null;
+  }
+  renderSelect(name, type) {
+    return (
+      <SelectControl
+        name={name}
+        choices={this.props.choices}
+        value={this.state[name]}
+        clearable={false}
+        onFocus={() => {
+          this.setType(type);
+        }}
+        onChange={(value) => {
+          this.setState({ [name]: value }, this.onChange);
+        }}
+      />
+    );
+  }
+  renderPopover() {
+    return (
+      <Popover id="filter-popover">
+        <div style={{ width: '300px' }}>
+          <PopoverSection
+            title="Longitude & Latitude columns"
+            isSelected={this.state.type === spatialTypes.latlong}
+            onSelect={this.setType.bind(this, spatialTypes.latlong)}
+          >
+            <Row>
+              <Col md={6}>
+                Longitude
+                {this.renderSelect('lonCol', spatialTypes.latlong)}
+              </Col>
+              <Col md={6}>
+                Latitude
+                {this.renderSelect('latCol', spatialTypes.latlong)}
+              </Col>
+            </Row>
+          </PopoverSection>
+          <PopoverSection
+            title="Delimited long & lat single column"
+            isSelected={this.state.type === spatialTypes.delimited}
+            onSelect={this.setType.bind(this, spatialTypes.delimited)}
+          >
+            <Row>
+              <Col md={6}>
+                Column
+                {this.renderSelect('lonlatCol', spatialTypes.delimited)}
+              </Col>
+              <Col md={6}>
+                Delimiter
+                <FormControl
+                  onFocus={this.setType.bind(this, spatialTypes.delimited)}
+                  value={this.state.delimiter}
+                  onChange={this.onDelimiterChange}
+                  placeholder="delimiter"
+                  bsSize="small"
+                />
+              </Col>
+            </Row>
+            <div>
+              {t('Reverse lat/long ')}
+              <Checkbox checked={this.state.reverseCheckbox} onChange={this.toggleCheckbox}
/>
+            </div>
+          </PopoverSection>
+          <PopoverSection
+            title="Geohash"
+            isSelected={this.state.type === spatialTypes.geohash}
+            onSelect={this.setType.bind(this, spatialTypes.geohash)}
+          >
+            <Row>
+              <Col md={6}>
+                Column
+                {this.renderSelect('geohashCol', spatialTypes.geohash)}
+              </Col>
+            </Row>
+          </PopoverSection>
+          <div className="clearfix">
+            <Button
+              bsSize="small"
+              className="float-left ok"
+              bsStyle="primary"
+              onClick={this.close.bind(this)}
+            >
+              Ok
+            </Button>
+          </div>
+        </div>
+      </Popover>
+    );
+  }
+  render() {
+    return (
+      <div>
+        <ControlHeader {...this.props} />
+        <OverlayTrigger
+          animation={this.props.animation}
+          container={document.body}
+          trigger="click"
+          rootClose
+          ref="trigger"
+          placement="right"
+          overlay={this.renderPopover()}
+        >
+          <Label style={{ cursor: 'pointer' }}>
+            {this.renderLabelContent()}
+          </Label>
+        </OverlayTrigger>
+      </div>
+    );
+  }
+}
+
+SpatialControl.propTypes = propTypes;
+SpatialControl.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explore/components/controls/index.js b/superset/assets/javascripts/explore/components/controls/index.js
index 094a26b323..94b8c66ef6 100644
--- a/superset/assets/javascripts/explore/components/controls/index.js
+++ b/superset/assets/javascripts/explore/components/controls/index.js
@@ -10,6 +10,7 @@ import FixedOrMetricControl from './FixedOrMetricControl';
 import HiddenControl from './HiddenControl';
 import SelectAsyncControl from './SelectAsyncControl';
 import SelectControl from './SelectControl';
+import SpatialControl from './SpatialControl';
 import TextAreaControl from './TextAreaControl';
 import TextControl from './TextControl';
 import TimeSeriesColumnControl from './TimeSeriesColumnControl';
@@ -29,6 +30,7 @@ const controlMap = {
   HiddenControl,
   SelectAsyncControl,
   SelectControl,
+  SpatialControl,
   TextAreaControl,
   TextControl,
   TimeSeriesColumnControl,
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index 810c11d8c9..b18039f232 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -482,6 +482,16 @@ export const controls = {
     }),
   },
 
+  spatial: {
+    type: 'SpatialControl',
+    label: t('Longitude & Latitude'),
+    validators: [v.nonEmpty],
+    description: t('Point to your spatial columns'),
+    mapStateToProps: state => ({
+      choices: (state.datasource) ? state.datasource.all_cols : [],
+    }),
+  },
+
   longitude: {
     type: 'SelectControl',
     label: t('Longitude'),
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index 7bae7f2dbd..a243cbfd2d 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -346,9 +346,8 @@ export const visTypes = {
         label: t('Query'),
         expanded: true,
         controlSetRows: [
-          ['longitude', 'latitude'],
-          ['groupby', 'size'],
-          ['row_limit'],
+          ['spatial', 'size'],
+          ['groupby', 'row_limit'],
         ],
       },
       {
@@ -364,7 +363,6 @@ export const visTypes = {
       size: {
         label: t('Height'),
         description: t('Metric used to control height'),
-        validators: [v.nonEmpty],
       },
     },
   },
@@ -377,9 +375,8 @@ export const visTypes = {
         label: t('Query'),
         expanded: true,
         controlSetRows: [
-          ['longitude', 'latitude'],
-          ['groupby', 'size'],
-          ['row_limit'],
+          ['spatial', 'size'],
+          ['groupby', 'row_limit'],
         ],
       },
       {
@@ -408,9 +405,8 @@ export const visTypes = {
         label: t('Query'),
         expanded: true,
         controlSetRows: [
-          ['longitude', 'latitude'],
-          ['groupby', 'size'],
-          ['row_limit'],
+          ['spatial', 'size'],
+          ['groupby', 'row_limit'],
         ],
       },
       {
@@ -443,9 +439,8 @@ export const visTypes = {
         label: t('Query'),
         expanded: true,
         controlSetRows: [
-          ['longitude', 'latitude'],
-          ['groupby'],
-          ['row_limit'],
+          ['spatial', null],
+          ['groupby', 'row_limit'],
         ],
       },
       {
@@ -470,18 +465,13 @@ export const visTypes = {
       },
     ],
     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'),
       },
+      size: {
+        validators: [],
+      },
     },
   },
 
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 6f6b50282c..d86ad74b82 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -249,9 +249,15 @@ table.table-no-hover tr:hover {
 .m-t-5 {
     margin-top: 5px;
 }
+.m-t-10 {
+    margin-top: 10px;
+}
 .m-l-5 {
     margin-left: 5px;
 }
+.m-l-10 {
+    margin-left: 10px;
+}
 .m-l-25 {
     margin-left: 25px;
 }
diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py
index a666253f26..0b150686c6 100644
--- a/superset/connectors/druid/models.py
+++ b/superset/connectors/druid/models.py
@@ -988,6 +988,19 @@ def get_dimensions(self, groupby, columns_dict):
                 dimensions.append(column_name)
         return dimensions
 
+    def intervals_from_dttms(self, from_dttm, to_dttm):
+        # Couldn't find a way to just not filter on time...
+        from_dttm = from_dttm or datetime(1901, 1, 1)
+        to_dttm = to_dttm or datetime(2101, 1, 1)
+
+        # add tzinfo to native datetime with config
+        from_dttm = from_dttm.replace(tzinfo=DRUID_TZ)
+        to_dttm = to_dttm.replace(tzinfo=DRUID_TZ)
+        return '{}/{}'.format(
+            from_dttm.isoformat() if from_dttm else '',
+            to_dttm.isoformat() if to_dttm else '',
+        )
+
     def run_query(  # noqa / druid
             self,
             groupby, metrics,
@@ -1001,23 +1014,26 @@ def run_query(  # noqa / druid
             inner_from_dttm=None, inner_to_dttm=None,
             orderby=None,
             extras=None,  # noqa
-            select=None,  # noqa
             columns=None, phase=2, client=None, form_data=None,
             order_desc=True):
         """Runs a query against Druid and returns a dataframe.
         """
         # TODO refactor into using a TBD Query object
         client = client or self.cluster.get_pydruid_client()
+        row_limit = row_limit or conf.get('ROW_LIMIT')
 
         if not is_timeseries:
             granularity = 'all'
+
+        if (
+                granularity == 'all' or
+                timeseries_limit is None or
+                timeseries_limit == 0):
+            phase = 1
         inner_from_dttm = inner_from_dttm or from_dttm
         inner_to_dttm = inner_to_dttm or to_dttm
 
-        # add tzinfo to native datetime with config
-        from_dttm = from_dttm.replace(tzinfo=DRUID_TZ)
-        to_dttm = to_dttm.replace(tzinfo=DRUID_TZ)
-        timezone = from_dttm.tzname()
+        timezone = from_dttm.tzname() if from_dttm else None
 
         query_str = ''
         metrics_dict = {m.metric_name: m for m in self.metrics}
@@ -1043,7 +1059,7 @@ def run_query(  # noqa / druid
                 origin=extras.get('druid_time_origin'),
             ),
             post_aggregations=post_aggs,
-            intervals=from_dttm.isoformat() + '/' + to_dttm.isoformat(),
+            intervals=self.intervals_from_dttms(from_dttm, to_dttm),
         )
 
         filters = DruidDatasource.get_filters(filter, self.num_cols)
@@ -1056,14 +1072,22 @@ def run_query(  # noqa / druid
 
         order_direction = 'descending' if order_desc else 'ascending'
 
-        if len(groupby) == 0 and not having_filters:
+        if columns:
+            del qry['post_aggregations']
+            del qry['aggregations']
+            qry['dimensions'] = columns
+            qry['metrics'] = []
+            qry['granularity'] = 'all'
+            qry['limit'] = row_limit
+            client.scan(**qry)
+        elif len(groupby) == 0 and not having_filters:
             logging.info('Running timeseries query for no groupby values')
             del qry['dimensions']
             client.timeseries(**qry)
         elif (
-            not having_filters and
-            len(groupby) == 1 and
-            order_desc
+                not having_filters and
+                len(groupby) == 1 and
+                order_desc
         ):
             dim = list(qry.get('dimensions'))[0]
             logging.info('Running two-phase topn query for dimension [{}]'.format(dim))
@@ -1121,9 +1145,8 @@ def run_query(  # noqa / druid
                 pre_qry['limit_spec'] = {
                     'type': 'default',
                     'limit': min(timeseries_limit, row_limit),
-                    'intervals': (
-                        inner_from_dttm.isoformat() + '/' +
-                        inner_to_dttm.isoformat()),
+                    'intervals': self.intervals_from_dttms(
+                        inner_from_dttm, inner_to_dttm),
                     'columns': [{
                         'dimension': order_by,
                         'direction': order_direction,
@@ -1195,8 +1218,11 @@ def query(self, query_obj):
         cols = []
         if DTTM_ALIAS in df.columns:
             cols += [DTTM_ALIAS]
-        cols += [col for col in query_obj['groupby'] if col in df.columns]
-        cols += [col for col in query_obj['metrics'] if col in df.columns]
+        cols += query_obj.get('groupby') or []
+        cols += query_obj.get('columns') or []
+        cols += query_obj.get('metrics') or []
+
+        cols = [col for col in cols if col in df.columns]
         df = df[cols]
 
         time_offset = DruidDatasource.time_offset(query_obj['granularity'])
diff --git a/superset/data/__init__.py b/superset/data/__init__.py
index f534914038..742a32b4c5 100644
--- a/superset/data/__init__.py
+++ b/superset/data/__init__.py
@@ -13,6 +13,7 @@
 
 import pandas as pd
 from sqlalchemy import BigInteger, Date, DateTime, Float, String
+import geohash
 
 from superset import app, db, utils
 from superset.connectors.connector_registry import ConnectorRegistry
@@ -1017,6 +1018,9 @@ def load_long_lat_data():
     pdf['date'] = datetime.datetime.now().date()
     pdf['occupancy'] = [random.randint(1, 6) for _ in range(len(pdf))]
     pdf['radius_miles'] = [random.uniform(1, 3) for _ in range(len(pdf))]
+    pdf['geohash'] = pdf[['LAT', 'LON']].apply(
+        lambda x: geohash.encode(*x), axis=1)
+    pdf['delimited'] = pdf['LAT'].map(str).str.cat(pdf['LON'].map(str), sep=',')
     pdf.to_sql(  # pylint: disable=no-member
         'long_lat',
         db.engine,
@@ -1036,6 +1040,8 @@ def load_long_lat_data():
             'date': Date(),
             'occupancy': Float(),
             'radius_miles': Float(),
+            'geohash': String(12),
+            'delimited': String(60),
         },
         index=False)
     print("Done loading table!")
@@ -1233,8 +1239,11 @@ def load_deck_dash():
     slices = []
     tbl = db.session.query(TBL).filter_by(table_name='long_lat').first()
     slice_data = {
-        "longitude": "LON",
-        "latitude": "LAT",
+        "spatial": {
+            "type": "latlong",
+            "lonCol": "LON",
+            "latCol": "LAT",
+        },
         "color_picker": {
             "r": 205,
             "g": 0,
@@ -1281,8 +1290,11 @@ def load_deck_dash():
         "point_unit": "square_m",
         "filters": [],
         "row_limit": 5000,
-        "longitude": "LON",
-        "latitude": "LAT",
+        "spatial": {
+            "type": "latlong",
+            "lonCol": "LON",
+            "latCol": "LAT",
+        },
         "mapbox_style": "mapbox://styles/mapbox/dark-v9",
         "granularity_sqla": "date",
         "size": "count",
@@ -1290,10 +1302,12 @@ def load_deck_dash():
         "since": "2014-01-01",
         "point_radius": "Auto",
         "until": "now",
-        "color_picker": {"a": 1,
-        "r": 14,
-        "b": 0,
-        "g": 255},
+        "color_picker": {
+            "a": 1,
+            "r": 14,
+            "b": 0,
+            "g": 255,
+        },
         "grid_size": 20,
         "where": "",
         "having": "",
@@ -1321,10 +1335,13 @@ def load_deck_dash():
     slices.append(slc)
 
     slice_data = {
+        "spatial": {
+            "type": "latlong",
+            "lonCol": "LON",
+            "latCol": "LAT",
+        },
         "filters": [],
         "row_limit": 5000,
-        "longitude": "LON",
-        "latitude": "LAT",
         "mapbox_style": "mapbox://styles/mapbox/streets-v9",
         "granularity_sqla": "date",
         "size": "count",
@@ -1367,10 +1384,13 @@ def load_deck_dash():
     slices.append(slc)
 
     slice_data = {
+        "spatial": {
+            "type": "latlong",
+            "lonCol": "LON",
+            "latCol": "LAT",
+        },
         "filters": [],
         "row_limit": 5000,
-        "longitude": "LON",
-        "latitude": "LAT",
         "mapbox_style": "mapbox://styles/mapbox/satellite-streets-v9",
         "granularity_sqla": "date",
         "size": "count",
diff --git a/superset/migrations/versions/67a6ac9b727b_update_spatial_params.py b/superset/migrations/versions/67a6ac9b727b_update_spatial_params.py
new file mode 100644
index 0000000000..b3e81d6e96
--- /dev/null
+++ b/superset/migrations/versions/67a6ac9b727b_update_spatial_params.py
@@ -0,0 +1,51 @@
+"""update_spatial_params
+
+Revision ID: 67a6ac9b727b
+Revises: 4736ec66ce19
+Create Date: 2017-12-08 08:19:21.148775
+
+"""
+import json
+
+from alembic import op
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy import Column, Integer, String, Text
+
+from superset import db
+
+# revision identifiers, used by Alembic.
+revision = '67a6ac9b727b'
+down_revision = '4736ec66ce19'
+
+Base = declarative_base()
+
+
+class Slice(Base):
+    __tablename__ = 'slices'
+    id = Column(Integer, primary_key=True)
+    viz_type = Column(String(250))
+    params = Column(Text)
+
+
+def upgrade():
+    bind = op.get_bind()
+    session = db.Session(bind=bind)
+
+    for slc in session.query(Slice).filter(Slice.viz_type.like('deck_%')):
+        params = json.loads(slc.params)
+        if params.get('latitude'):
+            params['spatial'] = {
+                'lonCol': params.get('longitude'),
+                'latCol': params.get('latitude'),
+                'type': 'latlong',
+            }
+            del params['latitude']
+            del params['longitude']
+        slc.params = json.dumps(params)
+        session.merge(slc)
+        session.commit()
+    session.close()
+
+
+def downgrade():
+    pass
diff --git a/superset/viz.py b/superset/viz.py
index 0eeed238b6..2ea85d26a8 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -22,6 +22,7 @@
 from dateutil import relativedelta as rdelta
 from flask import request
 from flask_babel import lazy_gettext as _
+import geohash
 from markdown import markdown
 import numpy as np
 import pandas as pd
@@ -1790,7 +1791,7 @@ class BaseDeckGLViz(BaseViz):
 
     def get_metrics(self):
         self.metric = self.form_data.get('size')
-        return [self.metric]
+        return [self.metric] if self.metric else []
 
     def get_properties(self, d):
         return {
@@ -1799,22 +1800,60 @@ def get_properties(self, d):
 
     def get_position(self, d):
         return [
-            d.get(self.form_data.get('longitude')),
-            d.get(self.form_data.get('latitude')),
+            d.get('lon'),
+            d.get('lat'),
         ]
 
     def query_obj(self):
         d = super(BaseDeckGLViz, self).query_obj()
         fd = self.form_data
 
-        d['groupby'] = [fd.get('longitude'), fd.get('latitude')]
+        gb = []
+
+        spatial = fd.get('spatial')
+        if spatial.get('type') == 'latlong':
+            gb += [spatial.get('lonCol')]
+            gb += [spatial.get('latCol')]
+        elif spatial.get('type') == 'delimited':
+            gb += [spatial.get('lonlatCol')]
+        elif spatial.get('type') == 'geohash':
+            gb += [spatial.get('geohashCol')]
+
         if fd.get('dimension'):
-            d['groupby'] += [fd.get('dimension')]
+            gb += [fd.get('dimension')]
 
-        d['metrics'] = self.get_metrics()
+        metrics = self.get_metrics()
+        if metrics:
+            d['groupby'] = gb
+            d['metrics'] = self.get_metrics()
+        else:
+            d['columns'] = gb
         return d
 
     def get_data(self, df):
+        fd = self.form_data
+        spatial = fd.get('spatial')
+        if spatial.get('type') == 'latlong':
+            df = df.rename(columns={
+                spatial.get('lonCol'): 'lon',
+                spatial.get('latCol'): 'lat'})
+        elif spatial.get('type') == 'delimited':
+            cols = ['lon', 'lat']
+            if spatial.get('reverseCheckbox'):
+                cols.reverse()
+            df[cols] = (
+                df[spatial.get('lonlatCol')]
+                .str
+                .split(spatial.get('delimiter'), expand=True)
+                .astype(np.float64)
+            )
+            del df[spatial.get('lonlatCol')]
+        elif spatial.get('type') == 'geohash':
+            latlong = df[spatial.get('geohashCol')].map(geohash.decode)
+            df['lat'] = latlong.apply(lambda x: x[0])
+            df['lon'] = latlong.apply(lambda x: x[1])
+            del df['geohash']
+
         features = []
         for d in df.to_dict(orient='records'):
             d = dict(position=self.get_position(d), **self.get_properties(d))
@@ -1833,15 +1872,15 @@ class DeckScatterViz(BaseDeckGLViz):
     verbose_name = _('Deck.gl - Scatter plot')
 
     def query_obj(self):
-        self.point_radius_fixed = self.form_data.get('point_radius_fixed')
+        fd = self.form_data
+        self.point_radius_fixed = (
+            fd.get('point_radius_fixed') or {'type': 'fix', 'value': 500})
         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]
+            return [self.point_radius_fixed.get('value')]
+        return None
 
     def get_properties(self, d):
         return {
@@ -1856,7 +1895,6 @@ def get_data(self, df):
         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)
 
 
diff --git a/tests/druid_func_tests.py b/tests/druid_func_tests.py
index 74da48665f..3deb3e29af 100644
--- a/tests/druid_func_tests.py
+++ b/tests/druid_func_tests.py
@@ -200,7 +200,7 @@ def test_run_query_single_groupby(self):
         client.query_builder.last_query.query_dict = {'mock': 0}
         # client.topn is called twice
         ds.run_query(
-            groupby, metrics, None, from_dttm, to_dttm, row_limit=100,
+            groupby, metrics, None, from_dttm, to_dttm, timeseries_limit=100,
             client=client, order_desc=True, filter=[],
         )
         self.assertEqual(2, len(client.topn.call_args_list))


 

----------------------------------------------------------------
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:
users@infra.apache.org


With regards,
Apache Git Services

Mime
View raw message