Skip to content

Commit

Permalink
[geo] provide more flexible Spatial controls (apache#4032)
Browse files Browse the repository at this point in the history
Before this PR the only way to query lat/long is in the shape of 2
columns that contains lat and long.

Now we're adding 2 more options:
* a single column that has lat and long with a delimiter in between
* support for geohashes - geohashes are cool
  • Loading branch information
mistercrunch authored and michellethomas committed May 23, 2018
1 parent ffcf4d7 commit 643091c
Show file tree
Hide file tree
Showing 15 changed files with 433 additions and 65 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion superset/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion superset/assets/javascripts/components/PopoverSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const propTypes = {
valueRenderer: PropTypes.func,
valueKey: PropTypes.string,
options: PropTypes.array,
placeholder: PropTypes.string,
};

const defaultProps = {
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,6 +30,7 @@ const controlMap = {
HiddenControl,
SelectAsyncControl,
SelectControl,
SpatialControl,
TextAreaControl,
TextControl,
TimeSeriesColumnControl,
Expand Down
10 changes: 10 additions & 0 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
32 changes: 11 additions & 21 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,8 @@ export const visTypes = {
label: t('Query'),
expanded: true,
controlSetRows: [
['longitude', 'latitude'],
['groupby', 'size'],
['row_limit'],
['spatial', 'size'],
['groupby', 'row_limit'],
],
},
{
Expand All @@ -364,7 +363,6 @@ export const visTypes = {
size: {
label: t('Height'),
description: t('Metric used to control height'),
validators: [v.nonEmpty],
},
},
},
Expand All @@ -377,9 +375,8 @@ export const visTypes = {
label: t('Query'),
expanded: true,
controlSetRows: [
['longitude', 'latitude'],
['groupby', 'size'],
['row_limit'],
['spatial', 'size'],
['groupby', 'row_limit'],
],
},
{
Expand Down Expand Up @@ -408,9 +405,8 @@ export const visTypes = {
label: t('Query'),
expanded: true,
controlSetRows: [
['longitude', 'latitude'],
['groupby', 'size'],
['row_limit'],
['spatial', 'size'],
['groupby', 'row_limit'],
],
},
{
Expand Down Expand Up @@ -443,9 +439,8 @@ export const visTypes = {
label: t('Query'),
expanded: true,
controlSetRows: [
['longitude', 'latitude'],
['groupby'],
['row_limit'],
['spatial', null],
['groupby', 'row_limit'],
],
},
{
Expand All @@ -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: [],
},
},
},

Expand Down
Loading

0 comments on commit 643091c

Please sign in to comment.