diff --git a/.pylintrc b/.pylintrc
index 27b41d7b42e0c..8a0e0ed78f612 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/.travis.yml b/.travis.yml
index 4641daadd6eaf..31a177635fab9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -24,7 +24,7 @@ before_install:
before_script:
- mysql -e 'drop database if exists superset; create database superset DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci' -u root
- mysql -u root -e "CREATE USER 'mysqluser'@'localhost' IDENTIFIED BY 'mysqluserpassword';"
- - mysql -u root -e "GRANT ALL ON superset.* TO 'mysqluser'@'localhost';"
+ - mysql -u root -e "GRANT ALL ON *.* TO 'mysqluser'@'localhost';"
- psql -c 'create database superset;' -U postgres
- psql -c "CREATE USER postgresuser WITH PASSWORD 'pguserpassword';" -U postgres
- export PATH=${PATH}:/tmp/hive/bin
diff --git a/docs/faq.rst b/docs/faq.rst
index b1f43877a639a..b6520b9181fbf 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -228,3 +228,19 @@ When adding columns to a table, you can have Superset detect and merge the
new columns in by using the "Refresh Metadata" action in the
``Source -> Tables`` page. Simply check the box next to the tables
you want the schema refreshed, and click ``Actions -> Refresh Metadata``.
+
+Is there a way to force the use specific colors?
+------------------------------------------------
+
+It is possible on a per-dashboard basis by providing a mapping of
+labels to colors in the ``JSON Metadata`` attribute using the
+``label_colors`` key.
+
+..code::
+
+ {
+ "label_colors": {
+ "Girls": "#FF69B4",
+ "Boys": "#ADD8E6"
+ }
+ }
diff --git a/superset/assets/images/favicon.png b/superset/assets/images/favicon.png
index 804a1c96a8f35..55316fa7c5e58 100644
Binary files a/superset/assets/images/favicon.png and b/superset/assets/images/favicon.png differ
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 0000000000000..cd9396510633d
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 0000000000000..31feff5c8fb08
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 0000000000000..11f38ccc8dbf3
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 0000000000000..d5da29c99be9f
Binary files /dev/null and b/superset/assets/images/viz_thumbnails/deck_screengrid.png differ
diff --git a/superset/assets/javascripts/chart/ChartContainer.jsx b/superset/assets/javascripts/chart/ChartContainer.jsx
index 11c432221fcab..d517677ec4d41 100644
--- a/superset/assets/javascripts/chart/ChartContainer.jsx
+++ b/superset/assets/javascripts/chart/ChartContainer.jsx
@@ -12,10 +12,10 @@ function mapStateToProps({ charts }, ownProps) {
chartUpdateEndTime: chart.chartUpdateEndTime,
chartUpdateStartTime: chart.chartUpdateStartTime,
latestQueryFormData: chart.latestQueryFormData,
+ lastRendered: chart.lastRendered,
queryResponse: chart.queryResponse,
queryRequest: chart.queryRequest,
triggerQuery: chart.triggerQuery,
- triggerRender: chart.triggerRender,
};
}
diff --git a/superset/assets/javascripts/chart/chartReducer.js b/superset/assets/javascripts/chart/chartReducer.js
index 835ee94034b0b..ade8c5bf68f03 100644
--- a/superset/assets/javascripts/chart/chartReducer.js
+++ b/superset/assets/javascripts/chart/chartReducer.js
@@ -12,6 +12,7 @@ export const chartPropType = {
chartUpdateEndTime: PropTypes.number,
chartUpdateStartTime: PropTypes.number,
latestQueryFormData: PropTypes.object,
+ queryRequest: PropTypes.object,
queryResponse: PropTypes.object,
triggerQuery: PropTypes.bool,
lastRendered: PropTypes.number,
@@ -24,6 +25,7 @@ export const chart = {
chartUpdateEndTime: null,
chartUpdateStartTime: now(),
latestQueryFormData: null,
+ queryRequest: null,
queryResponse: null,
triggerQuery: true,
lastRendered: 0,
diff --git a/superset/assets/javascripts/dashboard/Dashboard.jsx b/superset/assets/javascripts/dashboard/Dashboard.jsx
deleted file mode 100644
index 9e67647258925..0000000000000
--- a/superset/assets/javascripts/dashboard/Dashboard.jsx
+++ /dev/null
@@ -1,381 +0,0 @@
-import React from 'react';
-import { render } from 'react-dom';
-import d3 from 'd3';
-import { Alert } from 'react-bootstrap';
-import moment from 'moment';
-
-import GridLayout from './components/GridLayout';
-import Header from './components/Header';
-import { appSetup } from '../common';
-import AlertsWrapper from '../components/AlertsWrapper';
-import { t } from '../locales';
-import '../../stylesheets/dashboard.css';
-
-const superset = require('../modules/superset');
-const urlLib = require('url');
-const utils = require('../modules/utils');
-
-let px;
-
-appSetup();
-
-export function getInitialState(boostrapData) {
- const dashboard = Object.assign(
- {},
- utils.controllerInterface,
- boostrapData.dashboard_data,
- { common: boostrapData.common });
- dashboard.firstLoad = true;
-
- dashboard.posDict = {};
- if (dashboard.position_json) {
- dashboard.position_json.forEach((position) => {
- dashboard.posDict[position.slice_id] = position;
- });
- }
- dashboard.refreshTimer = null;
- const state = Object.assign({}, boostrapData, { dashboard });
- return state;
-}
-
-function unload() {
- const message = t('You have unsaved changes.');
- window.event.returnValue = message; // Gecko + IE
- return message; // Gecko + Webkit, Safari, Chrome etc.
-}
-
-function onBeforeUnload(hasChanged) {
- if (hasChanged) {
- window.addEventListener('beforeunload', unload);
- } else {
- window.removeEventListener('beforeunload', unload);
- }
-}
-
-function renderAlert() {
- render(
-
{this.props.chart.chartStatus === 'success' &&
queryResponse &&
diff --git a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
index e3ea7f2a732a4..43f6c012e61f4 100644
--- a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
@@ -9,6 +9,7 @@ import ControlPanelsContainer from './ControlPanelsContainer';
import SaveModal from './SaveModal';
import QueryAndSaveBtns from './QueryAndSaveBtns';
import { getExploreUrl } from '../exploreUtils';
+import { areObjectsEqual } from '../../reduxUtils';
import { getFormDataFromControls } from '../stores/store';
import { chartPropType } from '../../chart/chartReducer';
import * as exploreActions from '../actions/exploreActions';
@@ -50,6 +51,11 @@ class ExploreViewContainer extends React.Component {
if (np.controls.datasource.value !== this.props.controls.datasource.value) {
this.props.actions.fetchDatasourceMetadata(np.form_data.datasource, true);
}
+ // if any control value changed and it's an instant control
+ if (Object.keys(np.controls).some(key => (np.controls[key].renderTrigger &&
+ !areObjectsEqual(np.controls[key].value, this.props.controls[key].value)))) {
+ this.props.actions.renderTriggered(new Date().getTime(), this.props.chart.chartKey);
+ }
}
componentDidUpdate() {
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 0000000000000..bf9e7f13e243c
--- /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 (
+
+
+
{ this.onChange(controlTypes.fixed); }}
+ >
+ { this.setType(controlTypes.fixed); }}
+ value={this.state.fixedValue}
+ />
+
+
{ this.onChange(controlTypes.metric); }}
+ >
+ { this.setType(controlTypes.metric); }}
+ onChange={this.setMetric}
+ value={this.state.metricValue}
+ />
+
+
+
+ );
+ }
+ render() {
+ return (
+
+
+
+
+
+
+ );
+ }
+}
+
+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 a82995d70e4fa..6441b71c8de1b 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 4fe558e0524ac..bfe3f99177cab 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}
/>
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 0000000000000..382b7f7173601
--- /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 (
+
+ {ctrl}
+
+
+ );
+ }
+ renderPopover() {
+ return (
+
+ {PARAMS.map(ctrl => this.renderTextControl(ctrl))}
+
+ );
+ }
+ 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 (
+
+
+
+
+
+
+ );
+ }
+}
+
+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 876bc4a1c631c..094a26b32318d 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/index.jsx b/superset/assets/javascripts/explore/index.jsx
index 2247019f082fb..7e2ed35926431 100644
--- a/superset/assets/javascripts/explore/index.jsx
+++ b/superset/assets/javascripts/explore/index.jsx
@@ -36,20 +36,26 @@ const bootstrappedState = Object.assign(
isStarred: false,
},
);
-
-const chartKey = bootstrappedState.slice ? ('slice_' + bootstrappedState.slice.slice_id) : 'slice';
+const slice = bootstrappedState.slice;
+const sliceFormData = slice ?
+ getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
+ :
+ null;
+const chartKey = slice ? ('slice_' + slice.slice_id) : 'slice';
const initState = {
charts: {
[chartKey]: {
chartKey,
chartAlert: null,
- chartStatus: null,
+ chartStatus: 'loading',
chartUpdateEndTime: null,
chartUpdateStartTime: now(),
latestQueryFormData: getFormDataFromControls(controls),
+ sliceFormData,
+ queryRequest: null,
queryResponse: null,
triggerQuery: true,
- triggerRender: false,
+ lastRendered: 0,
},
},
saveModal: {
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index e3da06b621bd7..325c8786924e6 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',
@@ -691,6 +729,7 @@ export const controls = {
type: 'SelectControl',
freeForm: true,
label: t('Row limit'),
+ validators: [v.integer],
default: null,
choices: formatSelectOptions(ROW_LIMIT_OPTIONS),
},
@@ -699,6 +738,7 @@ export const controls = {
type: 'SelectControl',
freeForm: true,
label: t('Series limit'),
+ validators: [v.integer],
choices: formatSelectOptions(SERIES_LIMITS),
default: 50,
description: t('Limits the number of time series that get displayed'),
@@ -730,6 +770,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 +786,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 +1100,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 +1320,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 +1354,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 +1383,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 +1408,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 +1470,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 94115bb07622c..ef3100375b89c 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,
@@ -463,7 +610,8 @@ export const visTypes = {
label: t('Query'),
expanded: true,
controlSetRows: [
- ['series', 'metric', 'limit'],
+ ['series', 'metric'],
+ ['row_limit', null],
],
},
{
@@ -1062,9 +1210,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 663fd6e4947c3..03c3bb2ed4412 100644
--- a/superset/assets/javascripts/modules/colors.js
+++ b/superset/assets/javascripts/modules/colors.js
@@ -103,17 +103,36 @@ export const spectrums = {
],
};
+/**
+ * Get a color from a scheme specific palette (scheme)
+ * The function cycles through the palette while memoizing labels
+ * association to colors. If the function is called twice with the
+ * same string, it will return the same color.
+ *
+ * @param {string} s - The label for which we want to get a color
+ * @param {string} scheme - The palette name, or "scheme"
+ * @param {string} forcedColor - A color that the caller wants to
+ forcibly associate to a label.
+ */
export const getColorFromScheme = (function () {
- // Color factory
const seen = {};
- return function (s, scheme) {
+ const forcedColors = {};
+ return function (s, scheme, forcedColor) {
if (!s) {
return;
}
const selectedScheme = scheme ? ALL_COLOR_SCHEMES[scheme] : ALL_COLOR_SCHEMES.bnbColors;
- let stringifyS = String(s);
+ let stringifyS = String(s).toLowerCase();
// next line is for superset series that should have the same color
stringifyS = stringifyS.replace('---', '');
+
+ if (forcedColor && !forcedColors[stringifyS]) {
+ forcedColors[stringifyS] = forcedColor;
+ }
+ if (forcedColors[stringifyS]) {
+ return forcedColors[stringifyS];
+ }
+
if (seen[selectedScheme] === undefined) {
seen[selectedScheme] = {};
}
@@ -142,3 +161,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 0000000000000..e689a4168235a
--- /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/javascripts/modules/superset.js b/superset/assets/javascripts/modules/superset.js
deleted file mode 100644
index 9d58f02e94959..0000000000000
--- a/superset/assets/javascripts/modules/superset.js
+++ /dev/null
@@ -1,262 +0,0 @@
-/* eslint camel-case: 0 */
-import $ from 'jquery';
-import Mustache from 'mustache';
-import vizMap from '../../visualizations/main';
-import { getExploreUrl } from '../explore/exploreUtils';
-import { applyDefaultFormData } from '../explore/stores/store';
-import { t } from '../locales';
-
-const utils = require('./utils');
-
-/* eslint wrap-iife: 0 */
-const px = function (state) {
- let slice;
- const timeout = state.common.conf.SUPERSET_WEBSERVER_TIMEOUT;
- function getParam(name) {
- /* eslint no-useless-escape: 0 */
- const formattedName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
- const regex = new RegExp('[\\?&]' + formattedName + '=([^]*)');
- const results = regex.exec(location.search);
- return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
- }
- function initFavStars() {
- const baseUrl = '/superset/favstar/';
- // Init star behavihor for favorite
- function show() {
- if ($(this).hasClass('selected')) {
- $(this).html('
');
- } else {
- $(this).html('
');
- }
- }
- $('.favstar')
- .attr('title', t('Click to favorite/unfavorite'))
- .css('cursor', 'pointer')
- .each(show)
- .each(function () {
- let url = baseUrl + $(this).attr('class_name');
- const star = this;
- url += '/' + $(this).attr('obj_id') + '/';
- $.getJSON(url + 'count/', function (data) {
- if (data.count > 0) {
- $(star).addClass('selected').each(show);
- }
- });
- })
- .click(function () {
- $(this).toggleClass('selected');
- let url = baseUrl + $(this).attr('class_name');
- url += '/' + $(this).attr('obj_id') + '/';
- if ($(this).hasClass('selected')) {
- url += 'select/';
- } else {
- url += 'unselect/';
- }
- $.get(url);
- $(this).each(show);
- })
- .tooltip();
- }
- const Slice = function (data, datasource, controller) {
- const token = $('#token_' + data.slice_id);
- const controls = $('#controls_' + data.slice_id);
- const containerId = 'con_' + data.slice_id;
- const selector = '#' + containerId;
- const container = $(selector);
- const sliceId = data.slice_id;
- const formData = applyDefaultFormData(data.form_data);
- const sliceCell = $(`#${data.slice_id}-cell`);
- slice = {
- data,
- formData,
- container,
- containerId,
- datasource,
- selector,
- getWidgetHeader() {
- return this.container.parents('div.widget').find('.chart-header');
- },
- render_template(s) {
- const context = {
- width: this.width,
- height: this.height,
- };
- return Mustache.render(s, context);
- },
- jsonEndpoint(data) {
- return this.endpoint(data, 'json');
- },
- endpoint(data, endpointType = 'json') {
- let endpoint = getExploreUrl(data, endpointType, this.force);
- if (endpoint.charAt(0) !== '/') {
- // Known issue for IE <= 11:
- // https://connect.microsoft.com/IE/feedbackdetail/view/1002846/pathname-incorrect-for-out-of-document-elements
- endpoint = '/' + endpoint;
- }
- return endpoint;
- },
- d3format(col, number) {
- // uses the utils memoized d3format function and formats based on
- // column level defined preferences
- let format = '.3s';
- if (this.datasource.column_formats[col]) {
- format = this.datasource.column_formats[col];
- }
- return utils.d3format(format, number);
- },
- /* eslint no-shadow: 0 */
- always(data) {
- if (data && data.query) {
- slice.viewSqlQuery = data.query;
- }
- },
- done(payload) {
- Object.assign(data, payload);
-
- token.find('img.loading').hide();
- container.fadeTo(0.5, 1);
- sliceCell.removeClass('slice-cell-highlight');
- container.show();
-
- $('.query-and-save button').removeAttr('disabled');
- this.always(data);
- controller.done(this);
- },
- getErrorMsg(xhr) {
- let msg = '';
- if (!xhr.responseText) {
- const status = xhr.status;
- if (status === 0) {
- // This may happen when the worker in gunicorn times out
- msg += (
- t('The server could not be reached. You may want to ' +
- 'verify your connection and try again.'));
- } else {
- msg += (t('An unknown error occurred. (Status: %s )', status));
- }
- }
- return msg;
- },
- error(msg, xhr) {
- let errorMsg = msg;
- token.find('img.loading').hide();
- container.fadeTo(0.5, 1);
- sliceCell.removeClass('slice-cell-highlight');
- let errHtml = '';
- let o;
- try {
- o = JSON.parse(msg);
- if (o.error) {
- errorMsg = o.error;
- }
- } catch (e) {
- // pass
- }
- if (errorMsg) {
- errHtml += `
${errorMsg}
`;
- }
- if (xhr) {
- if (xhr.statusText === 'timeout') {
- errHtml += (
- '
' +
- 'Query timeout - visualization query are set to time out ' +
- `at ${timeout} seconds.
`);
- } else {
- const extendedMsg = this.getErrorMsg(xhr);
- if (extendedMsg) {
- errHtml += `
${extendedMsg}
`;
- }
- }
- }
- container.html(errHtml);
- container.show();
- $('span.query').removeClass('disabled');
- $('.query-and-save button').removeAttr('disabled');
- this.always(o);
- controller.error(this);
- },
- clearError() {
- $(selector + ' div.alert').remove();
- },
- width() {
- return container.width();
- },
- height() {
- let others = 0;
- const widget = container.parents('.widget');
- const sliceDescription = widget.find('.slice_description');
- if (sliceDescription.is(':visible')) {
- others += widget.find('.slice_description').height() + 25;
- }
- others += widget.find('.chart-header').height();
- return widget.height() - others - 10;
- },
- bindResizeToWindowResize() {
- let resizeTimer;
- const slice = this;
- $(window).on('resize', function () {
- clearTimeout(resizeTimer);
- resizeTimer = setTimeout(function () {
- slice.resize();
- }, 500);
- });
- },
- render(force) {
- if (force === undefined) {
- this.force = false;
- } else {
- this.force = force;
- }
- const formDataExtra = Object.assign({}, formData);
- formDataExtra.extra_filters = controller.effectiveExtraFilters(sliceId);
- controls.find('a.exploreChart').attr('href', getExploreUrl(formDataExtra));
- controls.find('a.exportCSV').attr('href', getExploreUrl(formDataExtra, 'csv'));
- token.find('img.loading').show();
- container.fadeTo(0.5, 0.25);
- sliceCell.addClass('slice-cell-highlight');
- container.css('height', this.height());
- $.ajax({
- url: this.jsonEndpoint(formDataExtra),
- timeout: timeout * 1000,
- success: (queryResponse) => {
- try {
- vizMap[formData.viz_type](this, queryResponse);
- this.done(queryResponse);
- } catch (e) {
- this.error(t('An error occurred while rendering the visualization: %s', e));
- }
- },
- error: (err) => {
- this.error(err.responseText, err);
- },
- });
- },
- resize() {
- this.render();
- },
- addFilter(col, vals, merge = true, refresh = true) {
- controller.addFilter(sliceId, col, vals, merge, refresh);
- },
- setFilter(col, vals, refresh = true) {
- controller.setFilter(sliceId, col, vals, refresh);
- },
- getFilters() {
- return controller.filters[sliceId];
- },
- clearFilter() {
- controller.clearFilter(sliceId);
- },
- removeFilter(col, vals) {
- controller.removeFilter(sliceId, col, vals);
- },
- };
- return slice;
- };
- // Export public functions
- return {
- getParam,
- initFavStars,
- Slice,
- };
-};
-module.exports = px;
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 99f8afc16f66a..16a2757d2879c 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",
@@ -135,10 +138,10 @@
"sinon": "^4.0.0",
"style-loader": "^0.18.2",
"transform-loader": "^0.2.3",
- "uglifyjs-webpack-plugin": "^0.4.6",
- "url-loader": "^0.5.7",
- "webpack": "^3.4.1",
- "webpack-manifest-plugin": "1.3.1",
- "webworkify-webpack": "2.0.5"
+ "uglifyjs-webpack-plugin": "^1.1.0",
+ "url-loader": "^0.6.2",
+ "webpack": "^3.8.1",
+ "webpack-manifest-plugin": "1.3.2",
+ "webworkify-webpack": "2.1.0"
}
}
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 0000000000000..6f5a1aa5b5c60
--- /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(
);
+ 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 0000000000000..9864d83890ca6
--- /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(
);
+ 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 558547101a06e..2a24633fe7bb5 100644
--- a/superset/assets/spec/javascripts/modules/colors_spec.jsx
+++ b/superset/assets/spec/javascripts/modules/colors_spec.jsx
@@ -1,14 +1,14 @@
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', () => {
const color1 = getColorFromScheme('CA');
expect(color1).to.equal(ALL_COLOR_SCHEMES.bnbColors[0]);
});
- it('series with same scheme should have the same color', () => {
+ it('getColorFromScheme series with same scheme should have the same color', () => {
const color1 = getColorFromScheme('CA', 'bnbColors');
const color2 = getColorFromScheme('CA', 'googleCategory20c');
const color3 = getColorFromScheme('CA', 'bnbColors');
@@ -19,4 +19,28 @@ describe('colors', () => {
expect(color1).to.equal(color3);
expect(color4).to.equal(ALL_COLOR_SCHEMES.bnbColors[1]);
});
+ it('getColorFromScheme forcing colors persists through calls', () => {
+ expect(getColorFromScheme('boys', 'bnbColors', 'blue')).to.equal('blue');
+ expect(getColorFromScheme('boys', 'bnbColors')).to.equal('blue');
+ expect(getColorFromScheme('boys', 'googleCategory20c')).to.equal('blue');
+
+ expect(getColorFromScheme('girls', 'bnbColors', 'pink')).to.equal('pink');
+ expect(getColorFromScheme('girls', 'bnbColors')).to.equal('pink');
+ expect(getColorFromScheme('girls', 'googleCategory20c')).to.equal('pink');
+ });
+ it('getColorFromScheme is not case sensitive', () => {
+ const c1 = getColorFromScheme('girls', 'bnbColors');
+ const c2 = getColorFromScheme('Girls', 'bnbColors');
+ const c3 = getColorFromScheme('GIRLS', 'bnbColors');
+ expect(c1).to.equal(c2);
+ expect(c3).to.equal(c2);
+ });
+ 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 0000000000000..758bf15a135d2
--- /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/stylesheets/react-select/select.less b/superset/assets/stylesheets/react-select/select.less
index c76c1861bb215..474f9893d6440 100644
--- a/superset/assets/stylesheets/react-select/select.less
+++ b/superset/assets/stylesheets/react-select/select.less
@@ -1,5 +1,6 @@
@import "~react-select/less/select.less";
@select-primary-color: black;
+@select-input-height: 30px;
// imports
@import "~react-select/less/control.less";
@@ -8,6 +9,21 @@
@import "~react-select/less/multi.less";
@import "~react-select/less/spinner.less";
+.Select--multi {
+ .Select-multi-value-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .Select-value {
+ margin: 2px;
+ }
+
+ .Select-input > input {
+ width: 100px;
+ }
+}
+
.VirtualSelectGrid {
z-index: 1;
}
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 89ba020adff8a..49c65448a56e3 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -356,22 +356,6 @@ iframe {
color: transparent;
}
-// overwrite react-select css
-.Select--multi {
- .Select-multi-value-wrapper {
- display: flex;
- flex-wrap: wrap;
- }
-
- .Select-value {
- height: 23px;
- }
-
- .Select-input > input {
- width: 100px;
- }
-}
-
.dimmed {
opacity: 0.5;
}
diff --git a/superset/assets/visualizations/countries/uk.geojson b/superset/assets/visualizations/countries/uk.geojson
index 14fecaa71e0a3..fa66f817c6c2a 100644
--- a/superset/assets/visualizations/countries/uk.geojson
+++ b/superset/assets/visualizations/countries/uk.geojson
@@ -190,5 +190,30 @@
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.889312982559205,51.61791610717779],[-3.923054933547974,51.60902786254877],[-3.965831995010319,51.61180496215826],[-3.998054981231689,51.592361450195305],[-3.990278005599918,51.56319427490234],[-4.036943912506103,51.56791687011713],[-4.074166774749641,51.56180572509776],[-4.114721775054818,51.57125091552739],[-4.13972091674799,51.56930541992187],[-4.160276889800969,51.554862976074276],[-4.234723091125431,51.54097366333008],[-4.280834197998047,51.560695648193416],[-4.306387901306096,51.60902786254877],[-4.283055782318115,51.613471984863395],[-4.253056049346924,51.63152694702143],[-4.248611927032471,51.64402770996088],[-4.200833797454834,51.62625122070324],[-4.118611812591553,51.64402770996088],[-4.080276966094971,51.65902709960943],[-4.070831775665283,51.673873901367244],[-4.041944026947021,51.702220916748104],[-4.023056983947754,51.744159698486385],[-3.978055000305175,51.76665878295904],[-3.933089971542301,51.762283325195256],[-3.82989597320551,51.68771362304687],[-3.889312982559205,51.61791610717779]]]},"properties":{"ID_0":242,"ISO":"GB-SWA","NAME_0":"United Kingdom","ID_1":4,"NAME_1":"Wales","ID_2":189,"NAME_2":"Swansea","TYPE_2":"Unitary Authority (wales)","ENGTYPE_2":"Unitary Authority (wales","NL_NAME_2":null,"VARNAME_2":null}},
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.031853914260864,51.60578918457037],[-3.075934886932373,51.62974166870123],[-3.070498943328857,51.68046569824219],[-3.114710092544499,51.70082473754883],[-3.106729984283447,51.70401763916021],[-3.110353946685791,51.765609741210994],[-3.128468990325871,51.79459381103521],[-3.081367969512825,51.79459381103521],[-3.048760890960637,51.78372192382818],[-3.021589040756226,51.727565765380916],[-2.990792989730835,51.7221298217774],[-2.976300001144352,51.70944976806635],[-2.976300001144352,51.66778564453131],[-2.961265087127629,51.62749099731457],[-3.031853914260864,51.60578918457037]]]},"properties":{"ID_0":242,"ISO":"GB-TOF","NAME_0":"United Kingdom","ID_1":4,"NAME_1":"Wales","ID_2":190,"NAME_2":"Torfaen","TYPE_2":"Unitary Authority (wales)","ENGTYPE_2":"Unitary Authority (wales","NL_NAME_2":null,"VARNAME_2":null}},
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.179722070693913,51.458801269531364],[-3.164166927337532,51.440139770507805],[-3.169167041778508,51.405693054199276],[-3.189723014831543,51.399307250976506],[-3.22749996185297,51.402362823486385],[-3.290832042694035,51.38569259643555],[-3.310277938842717,51.39236068725585],[-3.340277910232487,51.38069534301769],[-3.40416693687439,51.38069534301769],[-3.520833015441781,51.399581909179744],[-3.560277938842773,51.40124893188482],[-3.578056097030583,51.41977310180675],[-3.525556087493896,51.43582916259771],[-3.517220973968392,51.45000076293945],[-3.523056030273438,51.49361038208019],[-3.475198030471745,51.510440826416016],[-3.442500114440918,51.51583099365246],[-3.392221927642822,51.49610900878912],[-3.372221946716308,51.506938934326286],[-3.291353940963688,51.50046157836925],[-3.260731935501099,51.46937179565441],[-3.200683116912841,51.46776962280279],[-3.179722070693913,51.458801269531364]]]},"properties":{"ID_0":242,"ISO":"GB-VGL","NAME_0":"United Kingdom","ID_1":4,"NAME_1":"Wales","ID_2":191,"NAME_2":"Vale of Glamorgan","TYPE_2":"Unitary Authority (wales)","ENGTYPE_2":"Unitary Authority (wales","NL_NAME_2":null,"VARNAME_2":null}},
-{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.140832901000977,52.796390533447266],[-3.190834999084416,52.793331146240234],[-3.242223024368172,52.779159545898494],[-3.299722909927311,52.82693862915045],[-3.371943950653076,52.84999847412121],[-3.390722036361581,52.86741638183594],[-3.373075008392334,52.88907241821289],[-3.311233043670654,52.93287658691406],[-3.200437068939209,52.938030242920036],[-3.094794988632202,52.96379470825207],[-3.141175031661987,52.99729156494135],[-3.132157087325936,53.05913162231457],[-3.122706890106144,53.0664329528808],[-3.10381293296814,53.08103561401373],[-2.991724967956543,53.1390113830567],[-2.956809043884277,53.14467239379883],[-2.93833398818964,53.124721527099666],[-2.895487070083504,53.100749969482536],[-2.864376068115177,53.05360031127941],[-2.860470056533813,53.021930694580185],[-2.827500104904174,53.00222015380871],[-2.812777042388916,52.984161376953125],[-2.7744460105896,52.9849891662597],[-2.727740049362126,52.96660995483404],[-2.716943979263249,52.946109771728516],[-2.71972203254694,52.91722106933593],[-2.75,52.91471862792969],[-2.783334970474129,52.898609161376946],[-2.853610038757324,52.939720153808594],[-2.879168033599854,52.9419403076173],[-2.92833304405201,52.93278121948242],[-2.962729930877686,52.95272827148443],[-2.993890047073364,52.95360946655279],[-3.089446067810059,52.913059234619254],[-3.096668004989624,52.89416885375988],[-3.131666898727417,52.883331298828175],[-3.11916804313654,52.85610961914074],[-3.155833959579468,52.82194137573242],[-3.140832901000977,52.796390533447266]]]},"properties":{"ID_0":242,"ISO":"GB-WRX","NAME_0":"United Kingdom","ID_1":4,"NAME_1":"Wales","ID_2":192,"NAME_2":"Wrexham","TYPE_2":"Unitary Authority (wales)","ENGTYPE_2":"Unitary Authority (wales","NL_NAME_2":null,"VARNAME_2":null}}
+{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.140832901000977,52.796390533447266],[-3.190834999084416,52.793331146240234],[-3.242223024368172,52.779159545898494],[-3.299722909927311,52.82693862915045],[-3.371943950653076,52.84999847412121],[-3.390722036361581,52.86741638183594],[-3.373075008392334,52.88907241821289],[-3.311233043670654,52.93287658691406],[-3.200437068939209,52.938030242920036],[-3.094794988632202,52.96379470825207],[-3.141175031661987,52.99729156494135],[-3.132157087325936,53.05913162231457],[-3.122706890106144,53.0664329528808],[-3.10381293296814,53.08103561401373],[-2.991724967956543,53.1390113830567],[-2.956809043884277,53.14467239379883],[-2.93833398818964,53.124721527099666],[-2.895487070083504,53.100749969482536],[-2.864376068115177,53.05360031127941],[-2.860470056533813,53.021930694580185],[-2.827500104904174,53.00222015380871],[-2.812777042388916,52.984161376953125],[-2.7744460105896,52.9849891662597],[-2.727740049362126,52.96660995483404],[-2.716943979263249,52.946109771728516],[-2.71972203254694,52.91722106933593],[-2.75,52.91471862792969],[-2.783334970474129,52.898609161376946],[-2.853610038757324,52.939720153808594],[-2.879168033599854,52.9419403076173],[-2.92833304405201,52.93278121948242],[-2.962729930877686,52.95272827148443],[-2.993890047073364,52.95360946655279],[-3.089446067810059,52.913059234619254],[-3.096668004989624,52.89416885375988],[-3.131666898727417,52.883331298828175],[-3.11916804313654,52.85610961914074],[-3.155833959579468,52.82194137573242],[-3.140832901000977,52.796390533447266]]]},"properties":{"ID_0":242,"ISO":"GB-WRX","NAME_0":"United Kingdom","ID_1":4,"NAME_1":"Wales","ID_2":192,"NAME_2":"Wrexham","TYPE_2":"Unitary Authority (wales)","ENGTYPE_2":"Unitary Authority (wales","NL_NAME_2":null,"VARNAME_2":null}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-NET","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":205,"NAME_2":"Newcastle upon Tyne","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.637893605385297,55.064768597812005],[-1.6385078008313936,55.041551349041356],[-1.5926377415084274,55.038940380402764],[-1.5873825018672498,55.025777798790564],[-1.6002664388791212,55.00983844727313],[-1.5585039788567177,55.00554365830061],[-1.562981060121042,54.992317493771715],[-1.5292129836146306,54.98334130741928],[-1.5355253430199736,54.96523484767873],[-1.5538817384420445,54.95908490640094],[-1.5943326191343197,54.97060544223477],[-1.640073958064805,54.95926565999575],[-1.7003925142994276,54.97078331957437],[-1.719662014574189,54.96748374975912],[-1.747127463447843,54.981765602633345],[-1.766667008399963,54.98443984985363],[-1.728332042694035,55.02804946899419],[-1.697221994400024,55.033050537109375],[-1.688889026641789,55.07221984863287],[-1.637893605385297,55.064768597812005]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-NTY","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":204,"NAME_2":"North Tyneside","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.562981060121042,54.992317493771715],[-1.5585039788567177,55.00554365830061],[-1.6002664388791212,55.00983844727313],[-1.5873825018672498,55.025777798790564],[-1.5926377415084274,55.038940380402764],[-1.6385078008313936,55.041551349041356],[-1.637893605385297,55.064768597812005],[-1.586109042167664,55.06055068969721],[-1.559999942779484,55.05110931396479],[-1.493610978126469,55.04916000366211],[-1.454121947288513,55.07013702392589],[-1.428056001663208,55.03819274902344],[-1.42972195148468,55.007362365722706],[-1.4533035788215456,54.98981534304894],[-1.5308458423819309,54.98401528309465],[-1.5292129836146306,54.98334130741928],[-1.562981060121042,54.992317493771715]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-STY","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":203,"NAME_2":"South Tyneside","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.42972195148468,55.007362365722706],[-1.379166007041931,54.98597335815424],[-1.353055953979379,54.95652770996093],[-1.367498993873483,54.93847274780285],[-1.3696831726000283,54.9424293829535],[-1.4193708269241863,54.92992401027664],[-1.5112175753936958,54.93166591553027],[-1.5153298812543685,54.957349818849906],[-1.5355253430199736,54.96523484767873],[-1.5292129836146306,54.98334130741928],[-1.5308458423819309,54.98401528309465],[-1.4495276527721541,54.9843355871573],[-1.42972195148468,55.007362365722706]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-SND","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":202,"NAME_2":"Sunderland","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.351943969726506,54.86963653564459],[-1.389835000038033,54.860801696777344],[-1.411224007606506,54.80133056640631],[-1.440425038337708,54.79700088500976],[-1.506346940994263,54.7999000549317],[-1.523211956024113,54.831798553466854],[-1.552342057228032,54.85789871215826],[-1.5594108423845006,54.88203709563008],[-1.5570802954893481,54.90921128231478],[-1.5688913696667814,54.92462482411063],[-1.5112175753936958,54.93166591553027],[-1.4193708269241863,54.92992401027664],[-1.3696831726000283,54.9424293829535],[-1.3696850794979791,54.94242762876271],[-1.3756848680534932,54.93690908273037],[-1.367498993873483,54.93847274780285],[-1.350834012031498,54.905418395996094],[-1.360277056693917,54.89014053344732],[-1.351943969726506,54.86963653564459]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-GAT","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":201,"NAME_2":"Gateshead","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.766667008399963,54.98443984985363],[-1.747127463447843,54.981765602633345],[-1.719662014574189,54.96748374975912],[-1.7003925142994276,54.97078331957437],[-1.640073958064805,54.95926565999575],[-1.5943326191343197,54.97060544223477],[-1.5538817384420445,54.95908490640094],[-1.5355253430199736,54.96523484767873],[-1.5153298812543685,54.957349818849906],[-1.5112175753936958,54.93166591553027],[-1.5688913696667814,54.92462482411063],[-1.5570802954893481,54.90921128231478],[-1.5594108423845006,54.88203709563008],[-1.560006022453308,54.879650115966854],[-1.595293998718205,54.89270019531256],[-1.70114004611969,54.894161224365284],[-1.820001006126347,54.90472030639643],[-1.850558042526132,54.91971969604498],[-1.821666955947819,54.92943954467779],[-1.799998998641968,54.97222137451172],[-1.766667008399963,54.98443984985363]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-BRD","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":206,"NAME_2":"Bradford","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.728610038757267,53.90277099609374],[-1.73204879266721,53.89594325075279],[-1.717057547419288,53.89226166106078],[-1.7254307560718554,53.88568922907194],[-1.7563547985969659,53.884706129499996],[-1.7874648701095268,53.896900483232514],[-1.800424229633669,53.88596457577919],[-1.7605110704384566,53.863609042741246],[-1.7152860252485,53.86624455080066],[-1.6950919029119742,53.857537979195925],[-1.7119957303650024,53.78306976757263],[-1.6822705352699179,53.786414904743225],[-1.6742333842008723,53.7800047384966],[-1.640405933268333,53.77968368790602],[-1.6495638555890175,53.76819793719327],[-1.6816208329636275,53.75646888034447],[-1.7144316115903575,53.76245816984497],[-1.7337654265688964,53.74691831169833],[-1.7472250975667662,53.7468142321564],[-1.7459009737585947,53.73448984312321],[-1.760908957841842,53.73464545418409],[-1.7700862781627797,53.72625231008317],[-1.8093678285622556,53.7643806918915],[-1.827836366896612,53.76373265358225],[-1.8554563588416322,53.748307995249604],[-1.8726533729287733,53.75494083924053],[-1.8734027438503054,53.77869041991246],[-1.9281716252138732,53.787577043719274],[-1.9808488762530807,53.786353020276685],[-1.9867652441712251,53.796150574521526],[-2.047224044799748,53.82444000244146],[-2.032510042190552,53.84766006469738],[-2.002779006957951,53.86999893188471],[-1.966109991073608,53.87638854980463],[-1.973610043525696,53.91722106933594],[-1.950834989547729,53.93193817138672],[-1.943611979484501,53.957771301269645],[-1.901944994926396,53.95499038696294],[-1.871762990951538,53.9404411315918],[-1.79778003692627,53.94305038452142],[-1.728610038757267,53.90277099609374]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-CLD","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":207,"NAME_2":"Calderdale","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.7700862781627797,53.72625231008317],[-1.7536367530860233,53.7258603031649],[-1.7358710915125493,53.71324284745348],[-1.7312828421501738,53.680371982778546],[-1.7473017947117206,53.69459457269439],[-1.8257242848934756,53.67025621583839],[-1.8536828281573843,53.672537894106384],[-1.8435663241589404,53.66612189117391],[-1.8945985643971932,53.64542178165289],[-1.9341522381980882,53.648339600553584],[-1.9727437163101147,53.62577347065747],[-2.009470755392109,53.61677953810537],[-1.988055944442749,53.6147193908692],[-2.01694393157959,53.61861038208002],[-2.035001039505005,53.646938323974666],[-2.031388998031616,53.663330078124936],[-2.046390056610051,53.68416976928722],[-2.098612070083618,53.67193984985357],[-2.15134596824646,53.69654846191406],[-2.159722089767456,53.725559234619254],[-2.121388912200871,53.755268096923885],[-2.120558023452759,53.7913818359375],[-2.095555067062378,53.810550689697266],[-2.047224044799748,53.82444000244146],[-2.047224044799748,53.82444000244146],[-1.9867652441712251,53.796150574521526],[-1.9808488762530807,53.786353020276685],[-1.9281716252138732,53.787577043719274],[-1.8734027438503054,53.77869041991246],[-1.8726533729287733,53.75494083924053],[-1.8554563588416322,53.748307995249604],[-1.827836366896612,53.76373265358225],[-1.8093678285622556,53.7643806918915],[-1.7700862781627797,53.72625231008317]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-KIR","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":208,"NAME_2":"Kirklees","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.6816208329636275,53.75646888034447],[-1.6584439961554542,53.74545777049434],[-1.6378819490635026,53.747728913926046],[-1.6233716039931367,53.71854696150013],[-1.5922042738516327,53.71853616246708],[-1.5711222061724999,53.7064046197108],[-1.5983760291819142,53.699780759264726],[-1.6024994122575773,53.692171616634305],[-1.591920090885816,53.689334699229015],[-1.615216853840093,53.67758113210644],[-1.590626057095915,53.66066361221685],[-1.6249014560431192,53.653642139813186],[-1.6136755976408235,53.62457316512705],[-1.5864532989071738,53.60717405390706],[-1.584167957305851,53.59777069091797],[-1.615278005599975,53.56333160400402],[-1.674445986747685,53.54999923706055],[-1.714722990989628,53.55472183227539],[-1.771667957305851,53.53527069091802],[-1.80666601657856,53.531940460205135],[-1.816110968589726,53.51610946655279],[-1.870833039283696,53.532218933105526],[-1.897222995758,53.53305053710949],[-1.937777996063176,53.5680503845216],[-1.97694194316864,53.593891143798885],[-1.988055944442749,53.6147193908692],[-2.01694393157959,53.61861038208002],[-2.009470755392109,53.61677953810537],[-1.9727437163101147,53.62577347065747],[-1.9341522381980882,53.648339600553584],[-1.8945985643971932,53.64542178165289],[-1.8435663241589404,53.66612189117391],[-1.8536828281573843,53.672537894106384],[-1.8257242848934756,53.67025621583839],[-1.7473017947117206,53.69459457269439],[-1.7312828421501738,53.680371982778546],[-1.7358710915125493,53.71324284745348],[-1.7536367530860233,53.7258603031649],[-1.7700862781627797,53.72625231008317],[-1.760908957841842,53.73464545418409],[-1.7459009737585947,53.73448984312321],[-1.7472250975667662,53.7468142321564],[-1.7337654265688964,53.74691831169833],[-1.7144316115903575,53.76245816984497],[-1.6816208329636275,53.75646888034447]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-LDS","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":209,"NAME_2":"Leeds","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.304167032241764,53.749721527099666],[-1.3997217052678315,53.71930941607449],[-1.4431494342181035,53.72822662954728],[-1.4882982958378521,53.72775701029367],[-1.4957279953127665,53.722308403027206],[-1.5103956816858568,53.72969011580311],[-1.5593228953993103,53.69898368127084],[-1.5711222061724999,53.7064046197108],[-1.5922042738516327,53.71853616246708],[-1.6233716039931367,53.71854696150013],[-1.6378819490635026,53.747728913926046],[-1.6584439961554542,53.74545777049434],[-1.6816208329636275,53.75646888034447],[-1.6495638555890175,53.76819793719327],[-1.640405933268333,53.77968368790602],[-1.6742333842008723,53.7800047384966],[-1.6822705352699179,53.786414904743225],[-1.7119957303650024,53.78306976757263],[-1.6950919029119742,53.857537979195925],[-1.7152860252485,53.86624455080066],[-1.7605110704384566,53.863609042741246],[-1.800424229633669,53.88596457577919],[-1.7874648701095268,53.896900483232514],[-1.7563547985969659,53.884706129499996],[-1.7254307560718554,53.88568922907194],[-1.717057547419288,53.89226166106078],[-1.73204879266721,53.89594325075279],[-1.728610038757267,53.90277099609374],[-1.641389966011047,53.907218933105526],[-1.583333015441838,53.900829315185604],[-1.549167037010193,53.90861129760747],[-1.534446001052856,53.93526840209961],[-1.507187962532043,53.91077041625988],[-1.454722046852055,53.90610885620117],[-1.386667013168221,53.9394416809082],[-1.333611011505127,53.94499969482433],[-1.294167995452824,53.9255485534668],[-1.307777047157288,53.89471817016607],[-1.305557012557869,53.860828399658146],[-1.326110005378666,53.846378326416016],[-1.305276989936772,53.819721221923885],[-1.307500958442574,53.79027938842773],[-1.288612008094788,53.77027130126959],[-1.304167032241764,53.749721527099666]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-WKF","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":210,"NAME_2":"Wakefield","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.584167957305851,53.59777069091797],[-1.5864532989071738,53.60717405390706],[-1.6136755976408235,53.62457316512705],[-1.6249014560431192,53.653642139813186],[-1.590626057095915,53.66066361221685],[-1.615216853840093,53.67758113210644],[-1.591920090885816,53.689334699229015],[-1.6024994122575773,53.692171616634305],[-1.5983760291819142,53.699780759264726],[-1.5711222061724999,53.7064046197108],[-1.5593228953993103,53.69898368127084],[-1.5103956816858568,53.72969011580311],[-1.4957279953127665,53.722308403027206],[-1.4882982958378521,53.72775701029367],[-1.4431494342181035,53.72822662954728],[-1.3997217052678315,53.71930941607449],[-1.304167032241764,53.749721527099666],[-1.259443044662419,53.71776962280285],[-1.219980001449585,53.714698791503906],[-1.245278000831547,53.67193984985357],[-1.246947050094548,53.63555145263671],[-1.229943990707341,53.620719909668026],[-1.24333202838892,53.59693908691417],[-1.305276989936772,53.57611083984386],[-1.344444036483708,53.58082962036144],[-1.367776989936829,53.59888076782232],[-1.423889994621277,53.59498977661133],[-1.448055028915405,53.600830078125],[-1.485000014305115,53.59222030639654],[-1.535277962684631,53.593608856201286],[-1.560556054115239,53.60667037963873],[-1.584167957305851,53.59777069091797]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"IM","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"Isle of Man","ID_2":2,"NAME_2":"Isle of Man","TYPE_2":"Crown Dependency","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-4.383544921875,54.34134830540375],[-4.37530517578125,54.36375806691923],[-4.362945556640625,54.39175308509071],[-4.365692138671874,54.41733182694633],[-4.482421875,54.38855462060335],[-4.53460693359375,54.37255855831926],[-4.54833984375,54.35815677227375],[-4.5600128173828125,54.334943271492],[-4.603958129882812,54.279257540609336],[-4.6451568603515625,54.250784674876684],[-4.6904754638671875,54.22470072101279],[-4.7021484375,54.225503550071174],[-4.720001220703125,54.20984556727275],[-4.7234344482421875,54.18373573436089],[-4.7412872314453125,54.17489482345622],[-4.7454071044921875,54.16444403731646],[-4.733734130859375,54.15559900222241],[-4.7385406494140625,54.131868892085414],[-4.750213623046875,54.1069175102521],[-4.7646331787109375,54.11174798232063],[-4.7824859619140625,54.090811871873825],[-4.7632598876953125,54.0900064257852],[-4.7975921630859375,54.063820915086225],[-4.8332977294921875,54.05495438499805],[-4.82025146484375,54.043666972870625],[-4.780426025390625,54.055357450153174],[-4.757080078125,54.0609999517185],[-4.7296142578125,54.08356229415844],[-4.692535400390625,54.081951104880396],[-4.6856689453125,54.067044638606795],[-4.6630096435546875,54.062611954247565],[-4.658203125,54.07187975467914],[-4.648590087890625,54.07590858798479],[-4.6307373046875,54.07631144981169],[-4.622497558593749,54.067044638606795],[-4.6300506591796875,54.05817879674286],[-4.623870849609375,54.05374516606874],[-4.603271484375,54.07550572224815],[-4.6149444580078125,54.07067102844564],[-4.618377685546875,54.0787285386706],[-4.5764923095703125,54.10047600536083],[-4.54833984375,54.10168386374337],[-4.5298004150390625,54.12382170046237],[-4.50439453125,54.12502887884479],[-4.4659423828125,54.142730121693035],[-4.4707489013671875,54.14514334149681],[-4.478302001953125,54.15077364078996],[-4.4769287109375,54.159217654166895],[-4.464569091796875,54.165650031996904],[-4.43572998046875,54.16725797022493],[-4.387664794921875,54.19538677476911],[-4.401397705078125,54.20342006190774],[-4.404144287109375,54.213861000644926],[-4.382171630859375,54.22751055441583],[-4.35333251953125,54.26361995010228],[-4.30389404296875,54.299697745943455],[-4.340972900390625,54.30690951430536],[-4.379425048828125,54.3197273165176],[-4.383544921875,54.34134830540375]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-CHE","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":211,"NAME_2":"Cheshire East","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.503124,53.332322],[-2.447022914886475,53.355045318603516],[-2.42877006530756,53.38039016723633],[-2.364721059799137,53.36249923706055],[-2.316668033599854,53.359989166259766],[-2.28333306312561,53.34222030639659],[-2.238333940505868,53.35499954223644],[-2.176111936569157,53.34555053710949],[-2.159446954727173,53.321388244628906],[-2.140279054641724,53.3277702331543],[-2.119998931884766,53.359439849853516],[-2.080832004547119,53.3611106872558],[-2.049443960189762,53.3522186279298],[-2.016736030578613,53.370479583740234],[-1.998056054115296,53.33583068847667],[-1.995555996894836,53.26416015624999],[-1.990556001663208,53.241661071777344],[-1.970834016799927,53.23027038574224],[-1.976848959922791,53.207370758056754],[-2.005557060241642,53.18610000610357],[-2.030555009841919,53.18471908569335],[-2.06833291053772,53.165828704833984],[-2.140001058578491,53.16999816894537],[-2.134721994399968,53.15027999877935],[-2.157778024673462,53.14749908447271],[-2.20491099357605,53.113666534423885],[-2.243597030639535,53.08589172363286],[-2.272222995757943,53.07749938964849],[-2.305557012557927,53.0791587829591],[-2.33555793762207,53.056110382080185],[-2.36583399772644,53.0522193908692],[-2.37388801574707,53.03722000122081],[-2.366389989852848,53.00027084350586],[-2.378781080245972,52.98960876464855],[-2.423609972000065,52.9786109924317],[-2.430835008621216,52.96500015258788],[-2.478888034820557,52.95415878295904],[-2.512778997421208,52.96165847778332],[-2.527224063873234,52.94721984863287],[-2.57778000831604,52.95360946655279],[-2.589446067810059,52.9769401550294],[-2.622776985168457,52.98582839965826],[-2.727740049362126,52.96660995483404],[-2.69929242541042,52.99543881492112],[-2.668817362661816,53.03865395690339],[-2.7024105948798103,53.0543212271167],[-2.718262793890581,53.04421408238489],[-2.7529286697448767,53.069226255530296],[-2.7317386071692047,53.091808954566744],[-2.71136363193619,53.09364241328605],[-2.706054071937529,53.118509149806194],[-2.671145068527531,53.11587002635502],[-2.6597303498762486,53.130716437310326],[-2.6412634457727053,53.12842106828762],[-2.6252837142956897,53.1508474454638],[-2.5920897823706253,53.14450477640153],[-2.5965047449945127,53.1588720534501],[-2.583580955796339,53.155497891245666],[-2.572402573358417,53.16347223219018],[-2.54292438715405,53.14977217705609],[-2.4997406601267818,53.164151979736],[-2.4688673347879777,53.152736732701385],[-2.443759598733158,53.15988323300474],[-2.4434774505382038,53.1708381853081],[-2.4573741833378864,53.176693897687585],[-2.4565568822113124,53.20261297077961],[-2.4306634912809755,53.197965287645374],[-2.4063665724789765,53.174087431160686],[-2.392109733478435,53.17991522992649],[-2.378319611154369,53.172012897685356],[-2.3687309526100115,53.18293310732565],[-2.372554311462734,53.19558288298865],[-2.4101609019432515,53.205696397148344],[-2.414181807392303,53.21929974563053],[-2.4011722965015694,53.22175880134511],[-2.3962893093078983,53.23436231637357],[-2.363799855536885,53.22357083157431],[-2.3490260583775426,53.249012074682156],[-2.3641523737211614,53.24850109129004],[-2.394115190421391,53.266754280242004],[-2.4144548386760607,53.268319390382516],[-2.4274608589892663,53.2611695571301],[-2.453170662658378,53.28455076075722],[-2.498006104194348,53.289908925446895],[-2.512281155915481,53.32134620700367],[-2.503124,53.332322]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-CHW","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":212,"NAME_2":"Cheshire West and Chester","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.69929242541042,52.99543881492112],[-2.727740049362126,52.96660995483404],[-2.7744460105896,52.9849891662597],[-2.812777042388916,52.984161376953125],[-2.827500104904174,53.00222015380871],[-2.860470056533813,53.021930694580185],[-2.864376068115177,53.05360031127941],[-2.895487070083504,53.100749969482536],[-2.93833398818964,53.124721527099666],[-2.956809043884277,53.14467239379883],[-2.909168004989624,53.171390533447266],[-2.912499904632568,53.18638992309582],[-2.955276966094914,53.215549468994254],[-3.001389026641846,53.23944091796881],[-3.037220954894906,53.25111007690429],[-3.092499971389771,53.25672149658203],[-3.099165916442757,53.285758972168026],[-3.104025388762669,53.28805576260414],[-3.103733121138837,53.29999435427673],[-3.0741654904919797,53.316380908019056],[-3.026139522596778,53.29774869209405],[-2.992737815610769,53.30710878331546],[-2.968033296688178,53.3012551093274],[-2.939551319692313,53.31041563488773],[-2.931602543078512,53.306068832815846],[-2.9287564668151167,53.30841299304941],[-2.9285733193370813,53.30824801712984],[-2.901730978034698,53.29923482940577],[-2.8761936549383713,53.29065064886871],[-2.84558026284004,53.29113403786215],[-2.8126861614917296,53.30481522264111],[-2.7894899824606516,53.296849726505705],[-2.752827376996626,53.31456936820613],[-2.7524892430550585,53.31473267321978],[-2.739292174240041,53.30687033883386],[-2.738836637359983,53.30705152380454],[-2.7382055794543425,53.30669637740584],[-2.7386470041916118,53.307126947182],[-2.7235253728668827,53.31313964458688],[-2.700801145463609,53.30580718868542],[-2.685151721447282,53.31545145878494],[-2.6415569202347107,53.30503479895434],[-2.6450614629819666,53.31013510072038],[-2.624122729072159,53.30939815904222],[-2.619934770655549,53.32032094142578],[-2.609086573898332,53.312071202293055],[-2.595223105393976,53.32245434572998],[-2.580697059631234,53.31558609008789],[-2.55492901802063,53.31236648559575],[-2.447022914886475,53.355045318603516],[-2.503124,53.332322],[-2.512281155915481,53.32134620700367],[-2.498006104194348,53.289908925446895],[-2.453170662658378,53.28455076075722],[-2.4274608589892663,53.2611695571301],[-2.4144548386760607,53.268319390382516],[-2.394115190421391,53.266754280242004],[-2.3641523737211614,53.24850109129004],[-2.3490260583775426,53.249012074682156],[-2.363799855536885,53.22357083157431],[-2.3962893093078983,53.23436231637357],[-2.4011722965015694,53.22175880134511],[-2.414181807392303,53.21929974563053],[-2.4101609019432515,53.205696397148344],[-2.372554311462734,53.19558288298865],[-2.3687309526100115,53.18293310732565],[-2.378319611154369,53.172012897685356],[-2.392109733478435,53.17991522992649],[-2.4063665724789765,53.174087431160686],[-2.4306634912809755,53.197965287645374],[-2.4565568822113124,53.20261297077961],[-2.4573741833378864,53.176693897687585],[-2.4434774505382038,53.1708381853081],[-2.443759598733158,53.15988323300474],[-2.4688673347879777,53.152736732701385],[-2.4997406601267818,53.164151979736],[-2.54292438715405,53.14977217705609],[-2.572402573358417,53.16347223219018],[-2.583580955796339,53.155497891245666],[-2.5965047449945127,53.1588720534501],[-2.5920897823706253,53.14450477640153],[-2.6252837142956897,53.1508474454638],[-2.6412634457727053,53.12842106828762],[-2.6597303498762486,53.130716437310326],[-2.671145068527531,53.11587002635502],[-2.706054071937529,53.118509149806194],[-2.71136363193619,53.09364241328605],[-2.7317386071692047,53.091808954566744],[-2.7529286697448767,53.069226255530296],[-2.718262793890581,53.04421408238489],[-2.7024105948798103,53.0543212271167],[-2.668817362661816,53.03865395690339],[-2.69929242541042,52.99543881492112]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-HAL","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":213,"NAME_2":"Halton","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"MultiPolygon","coordinates":[[[[-2.6906328033643474,53.38538819887531],[-2.680557012557927,53.37887954711914],[-2.662832021713257,53.357460021972656],[-2.605659961700383,53.338134765624936],[-2.580697059631234,53.31558609008789],[-2.595223105393976,53.32245434572998],[-2.609086573898332,53.312071202293055],[-2.619934770655549,53.32032094142578],[-2.624122729072159,53.30939815904222],[-2.6450614629819666,53.31013510072038],[-2.6415569202347107,53.30503479895434],[-2.685151721447282,53.31545145878494],[-2.700801145463609,53.30580718868542],[-2.7235253728668827,53.31313964458688],[-2.7386470041916118,53.307126947182],[-2.7463205093272602,53.31461009870791],[-2.7630580540853917,53.33092174120937],[-2.753249998793975,53.34359955705048],[-2.740202777458212,53.34506489881115],[-2.7381914457377103,53.34793646284791],[-2.7591335731230733,53.34960649072296],[-2.7755257843675882,53.33895375514488],[-2.785584172527674,53.32345942791167],[-2.8221495120534503,53.3334093671374],[-2.8275834556324004,53.33098145503876],[-2.832457303217845,53.33728947774635],[-2.8187798745443087,53.33977154129373],[-2.818806904374221,53.34800073421497],[-2.7873017229574066,53.35629039621995],[-2.7766710534347734,53.38105868293533],[-2.7576540799353046,53.38073789254475],[-2.745174605499206,53.402095648481286],[-2.715226797703995,53.399034871811146],[-2.712803447263301,53.39062599574497],[-2.6906328033643474,53.38538819887531]]],[[[-2.738836637359983,53.30705152380454],[-2.739292174240041,53.30687033883386],[-2.7524892430550585,53.31473267321978],[-2.752205913970762,53.314573312537085],[-2.738836637359983,53.30705152380454]]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-KWL","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":215,"NAME_2":"Knowsley","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.8249644186438823,53.48520925194317],[-2.818711728158975,53.46689107502111],[-2.804407775672048,53.467236409853285],[-2.7950694680621293,53.44323009479305],[-2.8050295780688237,53.43865997711093],[-2.776201389989665,53.42668703835805],[-2.7867759144555664,53.40118756707545],[-2.745174605499206,53.402095648481286],[-2.7576540799353046,53.38073789254475],[-2.7766710534347734,53.38105868293533],[-2.7873017229574066,53.35629039621995],[-2.818806904374221,53.34800073421497],[-2.8404055929822314,53.347331071139905],[-2.853495811111275,53.363514490179924],[-2.8561650149124373,53.378815010693316],[-2.821954837185798,53.38066707898254],[-2.8371937067863002,53.399741433945394],[-2.856219824988291,53.39496011378423],[-2.892411156705693,53.4107621106839],[-2.865927223044769,53.41830162941371],[-2.867855282181373,53.4492945946253],[-2.900315030312638,53.4691586759534],[-2.914377598458968,53.46500517358472],[-2.922615127321468,53.474983221867795],[-2.8879942087273798,53.5038287512821],[-2.863610982894841,53.51805114746094],[-2.805000066757145,53.493610382080135],[-2.8249644186438823,53.48520925194317]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-LIV","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":216,"NAME_2":"Liverpool","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.818806904374221,53.34800073421497],[-2.8187798745443087,53.33977154129373],[-2.832457303217845,53.33728947774635],[-2.8275834556324004,53.33098145503876],[-2.836474654189664,53.3270076975817],[-2.878158399223132,53.334198839003406],[-2.9027093942427484,53.345606530987155],[-2.9817232994180594,53.3822445228796],[-3.0033556769120726,53.41410662121449],[-3.00864346820523,53.43787912516354],[-2.974914555250172,53.443323961595766],[-2.9738232140443714,53.462756641485505],[-2.9563065635473573,53.472993440769805],[-2.922615127321468,53.474983221867795],[-2.914377598458968,53.46500517358472],[-2.900315030312638,53.4691586759534],[-2.867855282181373,53.4492945946253],[-2.865927223044769,53.41830162941371],[-2.892411156705693,53.4107621106839],[-2.856219824988291,53.39496011378423],[-2.8371937067863002,53.399741433945394],[-2.821954837185798,53.38066707898254],[-2.8561650149124373,53.378815010693316],[-2.853495811111275,53.363514490179924],[-2.8404055929822314,53.347331071139905],[-2.818806904374221,53.34800073421497]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-SHN","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":217,"NAME_2":"St. Helens","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.6906328033643474,53.38538819887531],[-2.712803447263301,53.39062599574497],[-2.715226797703995,53.399034871811146],[-2.745174605499206,53.402095648481286],[-2.7867759144555664,53.40118756707545],[-2.776201389989665,53.42668703835805],[-2.8050295780688237,53.43865997711093],[-2.7950694680621293,53.44323009479305],[-2.804407775672048,53.467236409853285],[-2.818711728158975,53.46689107502111],[-2.8249644186438823,53.48520925194317],[-2.805000066757145,53.493610382080135],[-2.801666021346989,53.51694107055658],[-2.776109933853149,53.5288810729981],[-2.751987934112549,53.51115036010747],[-2.690000057220345,53.47166061401373],[-2.603888988494873,53.47249984741205],[-2.59972095489502,53.458328247070305],[-2.566404104232788,53.43566894531255],[-2.616111993789559,53.43054962158208],[-2.639722108840942,53.44055175781244],[-2.670834064483643,53.431110382080014],[-2.648056983947754,53.391391754150504],[-2.680557012557927,53.37887954711914],[-2.6906328033643474,53.38538819887531]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-SFT","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":218,"NAME_2":"Sefton","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.863610982894841,53.51805114746094],[-2.8879942087273798,53.5038287512821],[-2.922615127321468,53.474983221867795],[-2.9563065635473573,53.472993440769805],[-2.9738232140443714,53.462756641485505],[-2.974914555250172,53.443323961595766],[-3.00864346820523,53.43787912516354],[-3.008755289200723,53.43838154498731],[-3.040833950042668,53.4651374816895],[-3.06138896942133,53.501251220703125],[-3.06361198425293,53.5223617553712],[-3.1002779006958,53.54291534423834],[-3.1002779006958,53.56652832031256],[-3.051388978958016,53.62236022949219],[-3.01916599273676,53.65124893188476],[-2.99083399772644,53.66736221313482],[-2.972501039504948,53.69402694702154],[-2.947770118713265,53.70819473266613],[-2.932777881622258,53.66054916381847],[-2.963609933853149,53.6261100769043],[-2.996387958526554,53.613609313964844],[-3.018887996673527,53.593608856201286],[-3.011667966842651,53.57749938964837],[-3.030555009841805,53.56026840209961],[-3.031666994094849,53.54193878173834],[-2.958611965179443,53.51583099365239],[-2.954444885253906,53.54582977294933],[-2.910278081893864,53.52444076538086],[-2.863610982894841,53.51805114746094]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-WRL","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":219,"NAME_2":"Wirral","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.9287564668151167,53.30841299304941],[-2.931602543078512,53.306068832815846],[-2.939551319692313,53.31041563488773],[-2.968033296688178,53.3012551093274],[-2.992737815610769,53.30710878331546],[-3.026139522596778,53.29774869209405],[-3.0741654904919797,53.316380908019056],[-3.103733121138837,53.29999435427673],[-3.1037322636345945,53.30002934804953],[-3.1090147166578257,53.29725750745639],[-3.1166343558439107,53.31171865570986],[-3.122336556752299,53.32253388267948],[-3.186277019464458,53.36444603100735],[-3.2003693343126156,53.38751432995951],[-3.1736326305871425,53.40143583086426],[-3.0403203214367336,53.44289654953395],[-3.01645759994499,53.410577110896554],[-3.002522132920189,53.37473245214664],[-2.934169251041866,53.313288041250125],[-2.9287564668151167,53.30841299304941]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-BIR","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":220,"NAME_2":"Birmingham","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.7535232040259174,52.51296690956616],[-1.7994465715786179,52.50429325232655],[-1.7557828743406476,52.49948012688693],[-1.759600499826434,52.45191093161349],[-1.779146248816952,52.450154615190755],[-1.8003174036920766,52.45829756060253],[-1.8346435093023545,52.4174332370946],[-1.8444961945912532,52.410252604968385],[-1.8666451481106627,52.41104531977225],[-1.8687468031867753,52.404737506339956],[-1.845498272804539,52.39978223961458],[-1.844444990158081,52.383609771728516],[-1.90055501461029,52.39278030395508],[-1.954723000526428,52.38694000244135],[-1.983054995536747,52.37749099731439],[-2.001945018768254,52.39278030395508],[-1.983054995536747,52.41777038574224],[-2.016990282419047,52.4326829049244],[-2.013243469381798,52.4621907598166],[-1.9778967430437608,52.46716543160672],[-1.964006976484736,52.481985666946336],[-1.9507770607038752,52.48324663801256],[-1.9381321414490529,52.49842476289323],[-1.9630249126644133,52.50489727915291],[-1.9619814538100087,52.5284156793635],[-1.9294410516361902,52.53129919343769],[-1.9331498676191794,52.54581281031442],[-1.9181574543575888,52.547306508689616],[-1.878715984113474,52.56943440492526],[-1.872564469611435,52.584944671524696],[-1.875277042388859,52.58639144897461],[-1.855556011199895,52.57555007934576],[-1.815001010894719,52.590831756591854],[-1.777673006057739,52.575420379638786],[-1.753056049346867,52.54666137695318],[-1.733332037925663,52.5116691589356],[-1.7535232040259174,52.51296690956616]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-COV","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":221,"NAME_2":"Coventry","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.6010673268718723,52.38929945930054],[-1.6021755434992606,52.41605700579916],[-1.6144499964861112,52.42796797184463],[-1.6082707976447488,52.438841008005525],[-1.5952286648660792,52.439926413404116],[-1.5952286648660792,52.442926413404116],[-1.455000042915287,52.43804931640619],[-1.438053965568486,52.429439544677734],[-1.431666016578617,52.383609771728516],[-1.46277904510498,52.36582946777343],[-1.510833978652954,52.36748886108404],[-1.536944031715336,52.35583114624029],[-1.562777996063176,52.37221908569335],[-1.5676068988662508,52.384688959934735],[-1.6010673268718723,52.38929945930054]]]}},{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-DUD","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":222,"NAME_2":"Dudley","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.073942090146734,52.5498796695657],[-2.0795904608909996,52.524084766328386],[-2.057257620053761,52.51227148967935],[-2.0643229399889718,52.48716900687397],[-2.097090363561211,52.46839477878771],[-2.0590167147981466,52.46197535251913],[-2.0227492132989906,52.48055141631847],[-2.013243469381798,52.4621907598166],[-2.016990282419047,52.4326829049244],[-2.030555009841919,52.43693923950201],[-2.058332920074463,52.42248916625988],[-2.082776069641056,52.436100006103516],[-2.118889093399048,52.42221832275385],[-2.153856039047241,52.423000335693416],[-2.158890008926335,52.47748947143555],[-2.177500009536743,52.5011100769043],[-2.134443998336735,52.5152702331543],[-2.12611198425293,52.544441223144645],[-2.1214536447560484,52.55693494609824],[-2.108244358337544,52.543944892365815],[-2.0797787067929865,52.55702729259615],[-2.073942090146734,52.5498796695657]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-SAW","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":223,"NAME_2":"Sandwell","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.9181574543575888,52.547306508689616],[-1.9331498676191794,52.54581281031442],[-1.9294410516361902,52.53129919343769],[-1.9619814538100087,52.5284156793635],[-1.9630249126644133,52.50489727915291],[-1.9381321414490529,52.49842476289323],[-1.9507770607038752,52.48324663801256],[-1.964006976484736,52.481985666946336],[-1.9778967430437608,52.46716543160672],[-2.013243469381798,52.4621907598166],[-2.0227492132989906,52.48055141631847],[-2.0590167147981466,52.46197535251913],[-2.097090363561211,52.46839477878771],[-2.0643229399889718,52.48716900687397],[-2.057257620053761,52.51227148967935],[-2.0795904608909996,52.524084766328386],[-2.073942090146734,52.5498796695657],[-2.050982445654555,52.55272911259622],[-2.010978452375163,52.56906533086296],[-1.975496610455888,52.55556040195912],[-1.9641434593340144,52.5632268063678],[-1.9510685945348045,52.55683306471927],[-1.9339272996596213,52.560034196862155],[-1.9181574543575888,52.547306508689616]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-SOL","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":224,"NAME_2":"Solihull","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.5952286648660792,52.442926413404116],[-1.5952286648660792,52.439926413404116],[-1.6082707976447488,52.438841008005525],[-1.6144499964861112,52.42796797184463],[-1.6021755434992606,52.41605700579916],[-1.6010673268718723,52.38929945930054],[-1.5676068988662508,52.384688959934735],[-1.562777996063176,52.37221908569335],[-1.682500958442688,52.34249877929682],[-1.714722990989628,52.35527038574213],[-1.751667976379281,52.34749984741222],[-1.79694497585291,52.35221862792963],[-1.82249903678894,52.3763885498048],[-1.844444990158081,52.383609771728516],[-1.845498272804539,52.39978223961458],[-1.8687468031867753,52.404737506339956],[-1.8666451481106627,52.41104531977225],[-1.8444961945912532,52.410252604968385],[-1.8346435093023545,52.4174332370946],[-1.8003174036920766,52.45829756060253],[-1.779146248816952,52.450154615190755],[-1.759600499826434,52.45191093161349],[-1.7557828743406476,52.49948012688693],[-1.7994465715786179,52.50429325232655],[-1.7535232040259174,52.51296690956616],[-1.733332037925663,52.5116691589356],[-1.741111040115356,52.49750137329101],[-1.682500958442688,52.43972015380871],[-1.661666989326477,52.42472076416015],[-1.619444012641793,52.444438934326115],[-1.5952286648660792,52.442926413404116]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-WLL","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":225,"NAME_2":"Walsall","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-1.872564469611435,52.584944671524696],[-1.878715984113474,52.56943440492526],[-1.9181574543575888,52.547306508689616],[-1.9339272996596213,52.560034196862155],[-1.9510685945348045,52.55683306471927],[-1.9641434593340144,52.5632268063678],[-1.975496610455888,52.55556040195912],[-2.010978452375163,52.56906533086296],[-2.050982445654555,52.55272911259622],[-2.061952548381922,52.55824902064915],[-2.0500221605804145,52.57214052345834],[-2.077823919994913,52.58606048217102],[-2.054599038491829,52.60084758246004],[-2.066389083862248,52.60805892944347],[-2.025834083557072,52.61610031127924],[-1.952499032020512,52.637500762939446],[-1.934445977210999,52.66527938842785],[-1.897222995758,52.649440765380966],[-1.896389007568303,52.617488861083984],[-1.875277042388859,52.58639144897461],[-1.855556011199895,52.57555007934576],[-1.872564469611435,52.584944671524696]]]}},
+{"type":"Feature","properties":{"ID_0":242,"ISO":"GB-WLV","NAME_0":"United Kingdom","ID_1":1,"NAME_1":"England","ID_2":226,"NAME_2":"Wolverhampton","TYPE_2":"Metropolitan District","ENGTYPE_2":"Metropolitan District","NL_NAME_2":null,"VARNAME_2":null},"geometry":{"type":"Polygon","coordinates":[[[-2.1214536447560484,52.55693494609824],[-2.12611198425293,52.544441223144645],[-2.157500028610229,52.54639053344732],[-2.174444913864136,52.567501068115234],[-2.200557947158757,52.579441070556754],[-2.180556058883667,52.599998474121094],[-2.144166946411132,52.61278152465826],[-2.131109952926579,52.629440307617244],[-2.097500085830575,52.63415908813482],[-2.066389083862248,52.60805892944347],[-2.054599038491829,52.60084758246004],[-2.077823919994913,52.58606048217102],[-2.0500221605804145,52.57214052345834],[-2.061952548381922,52.55824902064915],[-2.050982445654555,52.55272911259622],[-2.073942090146734,52.5498796695657],[-2.0797787067929865,52.55702729259615],[-2.108244358337544,52.543944892365815],[-2.1214536447560484,52.55693494609824]]]}}
]}
\ No newline at end of file
diff --git a/superset/assets/visualizations/deckgl/DeckGLContainer.jsx b/superset/assets/visualizations/deckgl/DeckGLContainer.jsx
new file mode 100644
index 0000000000000..64ee934ddbf36
--- /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 (
+
+
+
+ );
+ }
+}
+
+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 0000000000000..1ef2394873bad
--- /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(
+
,
+ 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 0000000000000..9526825d250b6
--- /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(
+
,
+ 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 0000000000000..18cec553fa1c9
--- /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(
+
,
+ 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 0000000000000..b8b58ec056d59
--- /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(
+
,
+ document.getElementById(slice.containerId),
+ );
+}
+module.exports = deckScreenGridLayer;
diff --git a/superset/assets/visualizations/filter_box.jsx b/superset/assets/visualizations/filter_box.jsx
index bdcf978d3df2a..7653ec2cc2a77 100644
--- a/superset/assets/visualizations/filter_box.jsx
+++ b/superset/assets/visualizations/filter_box.jsx
@@ -72,7 +72,14 @@ class FilterBox extends React.Component {
return control;
}
clickApply() {
- this.props.onChange(Object.keys(this.state.selectedValues)[0], [], true, true);
+ const { selectedValues } = this.state;
+ Object.keys(selectedValues).forEach((fltr, i, arr) => {
+ let refresh = false;
+ if (i === arr.length - 1) {
+ refresh = true;
+ }
+ this.props.onChange(fltr, selectedValues[fltr], false, refresh);
+ });
this.setState({ hasChanged: false });
}
changeFilter(filter, options) {
@@ -90,7 +97,9 @@ class FilterBox extends React.Component {
const selectedValues = Object.assign({}, this.state.selectedValues);
selectedValues[fltr] = vals;
this.setState({ selectedValues, hasChanged: true });
- this.props.onChange(fltr, vals, false, this.props.instantFiltering);
+ if (this.props.instantFiltering) {
+ this.props.onChange(fltr, vals, false, true);
+ }
}
render() {
let dateFilter;
diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js
index 78e81ab6d7340..2afc57b6175ea 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 540bea8ceed25..ddbdf4bc1926e 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/config.py b/superset/config.py
index cea4870a61c36..e66b41abe8c8f 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -56,9 +56,9 @@
SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h' # noqa
# The SQLAlchemy connection string.
-SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(DATA_DIR, 'superset.db')
+# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(DATA_DIR, 'superset.db')
# SQLALCHEMY_DATABASE_URI = 'mysql://myapp@localhost/myapp'
-# SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@localhost/myapp'
+SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@localhost/myapp'
# In order to hook up a custom password store for all SQLACHEMY connections
# implement a function that takes a single argument of type 'sqla.engine.url',
diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py
index 8042ac9c1e037..7ce19145439dd 100644
--- a/superset/connectors/base/models.py
+++ b/superset/connectors/base/models.py
@@ -145,10 +145,11 @@ def data(self):
order_by_choices.append((json.dumps([s, True]), s + ' [asc]'))
order_by_choices.append((json.dumps([s, False]), s + ' [desc]'))
- verbose_map = {
+ verbose_map = {'__timestamp': 'Time'}
+ verbose_map.update({
o.metric_name: o.verbose_name or o.metric_name
for o in self.metrics
- }
+ })
verbose_map.update({
o.column_name: o.verbose_name or o.column_name
for o in self.columns
diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py
index 4c8a016cb53a5..90b4dc063e46b 100644
--- a/superset/connectors/druid/models.py
+++ b/superset/connectors/druid/models.py
@@ -4,7 +4,7 @@
from datetime import datetime, timedelta
import json
import logging
-from multiprocessing import Pool
+from multiprocessing.pool import ThreadPool
from dateutil.parser import parse as dparse
from flask import escape, Markup
@@ -22,7 +22,7 @@
from six import string_types
import sqlalchemy as sa
from sqlalchemy import (
- Boolean, Column, DateTime, ForeignKey, Integer, or_, String, Text,
+ Boolean, Column, DateTime, ForeignKey, Integer, or_, String, Text, UniqueConstraint,
)
from sqlalchemy.orm import backref, relationship
@@ -122,9 +122,9 @@ def refresh_datasources(
ds_refresh.append(datasource_name)
else:
return
- self.refresh_async(ds_refresh, merge_flag, refreshAll)
+ self.refresh(ds_refresh, merge_flag, refreshAll)
- def refresh_async(self, datasource_names, merge_flag, refreshAll):
+ def refresh(self, datasource_names, merge_flag, refreshAll):
"""
Fetches metadata for the specified datasources andm
merges to the Superset database
@@ -157,7 +157,7 @@ def refresh_async(self, datasource_names, merge_flag, refreshAll):
session.flush()
# Prepare multithreaded executation
- pool = Pool()
+ pool = ThreadPool()
ds_refresh = list(ds_map.values())
metadata = pool.map(_fetch_metadata_for, ds_refresh)
pool.close()
@@ -166,36 +166,37 @@ def refresh_async(self, datasource_names, merge_flag, refreshAll):
for i in range(0, len(ds_refresh)):
datasource = ds_refresh[i]
cols = metadata[i]
- col_objs_list = (
- session.query(DruidColumn)
- .filter(DruidColumn.datasource_name == datasource.datasource_name)
- .filter(or_(DruidColumn.column_name == col for col in cols))
- )
- col_objs = {col.column_name: col for col in col_objs_list}
- for col in cols:
- if col == '__time': # skip the time column
- continue
- col_obj = col_objs.get(col, None)
- if not col_obj:
- col_obj = DruidColumn(
- datasource_name=datasource.datasource_name,
- column_name=col)
- with session.no_autoflush:
- session.add(col_obj)
- datatype = cols[col]['type']
- if datatype == 'STRING':
- col_obj.groupby = True
- col_obj.filterable = True
- if datatype == 'hyperUnique' or datatype == 'thetaSketch':
- col_obj.count_distinct = True
- # Allow sum/min/max for long or double
- if datatype == 'LONG' or datatype == 'DOUBLE':
- col_obj.sum = True
- col_obj.min = True
- col_obj.max = True
- col_obj.type = datatype
- col_obj.datasource = datasource
- datasource.generate_metrics_for(col_objs_list)
+ if cols:
+ col_objs_list = (
+ session.query(DruidColumn)
+ .filter(DruidColumn.datasource_id == datasource.id)
+ .filter(or_(DruidColumn.column_name == col for col in cols))
+ )
+ col_objs = {col.column_name: col for col in col_objs_list}
+ for col in cols:
+ if col == '__time': # skip the time column
+ continue
+ col_obj = col_objs.get(col, None)
+ if not col_obj:
+ col_obj = DruidColumn(
+ datasource_id=datasource.id,
+ column_name=col)
+ with session.no_autoflush:
+ session.add(col_obj)
+ datatype = cols[col]['type']
+ if datatype == 'STRING':
+ col_obj.groupby = True
+ col_obj.filterable = True
+ if datatype == 'hyperUnique' or datatype == 'thetaSketch':
+ col_obj.count_distinct = True
+ # Allow sum/min/max for long or double
+ if datatype == 'LONG' or datatype == 'DOUBLE':
+ col_obj.sum = True
+ col_obj.min = True
+ col_obj.max = True
+ col_obj.type = datatype
+ col_obj.datasource = datasource
+ datasource.generate_metrics_for(col_objs_list)
session.commit()
@property
@@ -219,9 +220,9 @@ class DruidColumn(Model, BaseColumn):
__tablename__ = 'columns'
- datasource_name = Column(
- String(255),
- ForeignKey('datasources.datasource_name'))
+ datasource_id = Column(
+ Integer,
+ ForeignKey('datasources.id'))
# Setting enable_typechecks=False disables polymorphic inheritance.
datasource = relationship(
'DruidDatasource',
@@ -230,7 +231,7 @@ class DruidColumn(Model, BaseColumn):
dimension_spec_json = Column(Text)
export_fields = (
- 'datasource_name', 'column_name', 'is_active', 'type', 'groupby',
+ 'datasource_id', 'column_name', 'is_active', 'type', 'groupby',
'count_distinct', 'sum', 'avg', 'max', 'min', 'filterable',
'description', 'dimension_spec_json',
)
@@ -333,15 +334,14 @@ def generate_metrics(self):
metrics = self.get_metrics()
dbmetrics = (
db.session.query(DruidMetric)
- .filter(DruidCluster.cluster_name == self.datasource.cluster_name)
- .filter(DruidMetric.datasource_name == self.datasource_name)
+ .filter(DruidMetric.datasource_id == self.datasource_id)
.filter(or_(
DruidMetric.metric_name == m for m in metrics
))
)
dbmetrics = {metric.metric_name: metric for metric in dbmetrics}
for metric in metrics.values():
- metric.datasource_name = self.datasource_name
+ metric.datasource_id = self.datasource_id
if not dbmetrics.get(metric.metric_name, None):
db.session.add(metric)
@@ -349,7 +349,7 @@ def generate_metrics(self):
def import_obj(cls, i_column):
def lookup_obj(lookup_column):
return db.session.query(DruidColumn).filter(
- DruidColumn.datasource_name == lookup_column.datasource_name,
+ DruidColumn.datasource_id == lookup_column.datasource_id,
DruidColumn.column_name == lookup_column.column_name).first()
return import_util.import_simple_obj(db.session, i_column, lookup_obj)
@@ -360,9 +360,9 @@ class DruidMetric(Model, BaseMetric):
"""ORM object referencing Druid metrics for a datasource"""
__tablename__ = 'metrics'
- datasource_name = Column(
- String(255),
- ForeignKey('datasources.datasource_name'))
+ datasource_id = Column(
+ Integer,
+ ForeignKey('datasources.id'))
# Setting enable_typechecks=False disables polymorphic inheritance.
datasource = relationship(
'DruidDatasource',
@@ -371,7 +371,7 @@ class DruidMetric(Model, BaseMetric):
json = Column(Text)
export_fields = (
- 'metric_name', 'verbose_name', 'metric_type', 'datasource_name',
+ 'metric_name', 'verbose_name', 'metric_type', 'datasource_id',
'json', 'description', 'is_restricted', 'd3format',
)
@@ -399,7 +399,7 @@ def perm(self):
def import_obj(cls, i_metric):
def lookup_obj(lookup_metric):
return db.session.query(DruidMetric).filter(
- DruidMetric.datasource_name == lookup_metric.datasource_name,
+ DruidMetric.datasource_id == lookup_metric.datasource_id,
DruidMetric.metric_name == lookup_metric.metric_name).first()
return import_util.import_simple_obj(db.session, i_metric, lookup_obj)
@@ -419,7 +419,7 @@ class DruidDatasource(Model, BaseDatasource):
baselink = 'druiddatasourcemodelview'
# Columns
- datasource_name = Column(String(255), unique=True)
+ datasource_name = Column(String(255))
is_hidden = Column(Boolean, default=False)
fetch_values_from = Column(String(100))
cluster_name = Column(
@@ -431,6 +431,7 @@ class DruidDatasource(Model, BaseDatasource):
sm.user_model,
backref=backref('datasources', cascade='all, delete-orphan'),
foreign_keys=[user_id])
+ UniqueConstraint('cluster_name', 'datasource_name')
export_fields = (
'datasource_name', 'is_hidden', 'description', 'default_endpoint',
@@ -518,7 +519,7 @@ def import_obj(cls, i_datasource, import_time=None):
superset instances. Audit metadata isn't copies over.
"""
def lookup_datasource(d):
- return db.session.query(DruidDatasource).join(DruidCluster).filter(
+ return db.session.query(DruidDatasource).filter(
DruidDatasource.datasource_name == d.datasource_name,
DruidCluster.cluster_name == d.cluster_name,
).first()
@@ -563,11 +564,15 @@ def latest_metadata(self):
"""Returns segment metadata from the latest segment"""
logging.info('Syncing datasource [{}]'.format(self.datasource_name))
client = self.cluster.get_pydruid_client()
- results = client.time_boundary(datasource=self.datasource_name)
- if not results:
- return
- max_time = results[0]['result']['maxTime']
- max_time = dparse(max_time)
+ try:
+ results = client.time_boundary(datasource=self.datasource_name)
+ except IOError:
+ results = None
+ if results:
+ max_time = results[0]['result']['maxTime']
+ max_time = dparse(max_time)
+ else:
+ max_time = datetime.now()
# Query segmentMetadata for 7 days back. However, due to a bug,
# we need to set this interval to more than 1 day ago to exclude
# realtime segments, which triggered a bug (fixed in druid 0.8.2).
@@ -615,13 +620,12 @@ def generate_metrics_for(self, columns):
metrics.update(col.get_metrics())
dbmetrics = (
db.session.query(DruidMetric)
- .filter(DruidCluster.cluster_name == self.cluster_name)
- .filter(DruidMetric.datasource_name == self.datasource_name)
+ .filter(DruidMetric.datasource_id == self.id)
.filter(or_(DruidMetric.metric_name == m for m in metrics))
)
dbmetrics = {metric.metric_name: metric for metric in dbmetrics}
for metric in metrics.values():
- metric.datasource_name = self.datasource_name
+ metric.datasource_id = self.id
if not dbmetrics.get(metric.metric_name, None):
with db.session.no_autoflush:
db.session.add(metric)
@@ -656,7 +660,7 @@ def sync_to_db_from_config(
dimensions = druid_config['dimensions']
col_objs = (
session.query(DruidColumn)
- .filter(DruidColumn.datasource_name == druid_config['name'])
+ .filter(DruidColumn.datasource_id == datasource.id)
.filter(or_(DruidColumn.column_name == dim for dim in dimensions))
)
col_objs = {col.column_name: col for col in col_objs}
@@ -664,7 +668,7 @@ def sync_to_db_from_config(
col_obj = col_objs.get(dim, None)
if not col_obj:
col_obj = DruidColumn(
- datasource_name=druid_config['name'],
+ datasource_id=datasource.id,
column_name=dim,
groupby=True,
filterable=True,
@@ -676,7 +680,7 @@ def sync_to_db_from_config(
# Import Druid metrics
metric_objs = (
session.query(DruidMetric)
- .filter(DruidMetric.datasource_name == druid_config['name'])
+ .filter(DruidMetric.datasource_id == datasource.id)
.filter(or_(DruidMetric.metric_name == spec['name']
for spec in druid_config['metrics_spec']))
)
diff --git a/superset/data/__init__.py b/superset/data/__init__.py
index a96749a7b5cbd..f534914038e08 100644
--- a/superset/data/__init__.py
+++ b/superset/data/__init__.py
@@ -1226,3 +1226,276 @@ def load_misc_dashboard():
dash.slices = slices
db.session.merge(dash)
db.session.commit()
+
+
+def load_deck_dash():
+ print("Loading deck.gl dashboard")
+ slices = []
+ tbl = db.session.query(TBL).filter_by(table_name='long_lat').first()
+ slice_data = {
+ "longitude": "LON",
+ "latitude": "LAT",
+ "color_picker": {
+ "r": 205,
+ "g": 0,
+ "b": 3,
+ "a": 0.82,
+ },
+ "datasource": "5__table",
+ "filters": [],
+ "granularity_sqla": "date",
+ "groupby": [],
+ "having": "",
+ "mapbox_style": "mapbox://styles/mapbox/light-v9",
+ "multiplier": 10,
+ "point_radius_fixed": {"type": "metric", "value": "count"},
+ "point_unit": "square_m",
+ "row_limit": 5000,
+ "since": "2014-01-01",
+ "size": "count",
+ "time_grain_sqla": "Time Column",
+ "until": "now",
+ "viewport": {
+ "bearing": -4.952916738791771,
+ "latitude": 37.78926922909199,
+ "longitude": -122.42613341901688,
+ "pitch": 4.750411100577438,
+ "zoom": 12.729132798697304,
+ },
+ "viz_type": "deck_scatter",
+ "where": "",
+ }
+
+ print("Creating Scatterplot slice")
+ slc = Slice(
+ slice_name="Scatterplot",
+ viz_type='deck_scatter',
+ datasource_type='table',
+ datasource_id=tbl.id,
+ params=get_slice_json(slice_data),
+ )
+ merge_slice(slc)
+ slices.append(slc)
+
+ slice_data = {
+ "point_unit": "square_m",
+ "filters": [],
+ "row_limit": 5000,
+ "longitude": "LON",
+ "latitude": "LAT",
+ "mapbox_style": "mapbox://styles/mapbox/dark-v9",
+ "granularity_sqla": "date",
+ "size": "count",
+ "viz_type": "deck_screengrid",
+ "since": "2014-01-01",
+ "point_radius": "Auto",
+ "until": "now",
+ "color_picker": {"a": 1,
+ "r": 14,
+ "b": 0,
+ "g": 255},
+ "grid_size": 20,
+ "where": "",
+ "having": "",
+ "viewport": {
+ "zoom": 14.161641703941438,
+ "longitude": -122.41827069521386,
+ "bearing": -4.952916738791771,
+ "latitude": 37.76024135844065,
+ "pitch": 4.750411100577438,
+ },
+ "point_radius_fixed": {"type": "fix", "value": 2000},
+ "datasource": "5__table",
+ "time_grain_sqla": "Time Column",
+ "groupby": [],
+ }
+ print("Creating Screen Grid slice")
+ slc = Slice(
+ slice_name="Screen grid",
+ viz_type='deck_screengrid',
+ datasource_type='table',
+ datasource_id=tbl.id,
+ params=get_slice_json(slice_data),
+ )
+ merge_slice(slc)
+ slices.append(slc)
+
+ slice_data = {
+ "filters": [],
+ "row_limit": 5000,
+ "longitude": "LON",
+ "latitude": "LAT",
+ "mapbox_style": "mapbox://styles/mapbox/streets-v9",
+ "granularity_sqla": "date",
+ "size": "count",
+ "viz_type": "deck_hex",
+ "since": "2014-01-01",
+ "point_radius_unit": "Pixels",
+ "point_radius": "Auto",
+ "until": "now",
+ "color_picker": {
+ "a": 1,
+ "r": 14,
+ "b": 0,
+ "g": 255,
+ },
+ "grid_size": 40,
+ "extruded": True,
+ "having": "",
+ "viewport": {
+ "latitude": 37.789795085160335,
+ "pitch": 54.08961642447763,
+ "zoom": 13.835465702403654,
+ "longitude": -122.40632230075536,
+ "bearing": -2.3984797349335167,
+ },
+ "where": "",
+ "point_radius_fixed": {"type": "fix", "value": 2000},
+ "datasource": "5__table",
+ "time_grain_sqla": "Time Column",
+ "groupby": [],
+ }
+ print("Creating Hex slice")
+ slc = Slice(
+ slice_name="Hexagons",
+ viz_type='deck_hex',
+ datasource_type='table',
+ datasource_id=tbl.id,
+ params=get_slice_json(slice_data),
+ )
+ merge_slice(slc)
+ slices.append(slc)
+
+ slice_data = {
+ "filters": [],
+ "row_limit": 5000,
+ "longitude": "LON",
+ "latitude": "LAT",
+ "mapbox_style": "mapbox://styles/mapbox/satellite-streets-v9",
+ "granularity_sqla": "date",
+ "size": "count",
+ "viz_type": "deck_grid",
+ "since": "2014-01-01",
+ "point_radius_unit": "Pixels",
+ "point_radius": "Auto",
+ "until": "now",
+ "color_picker": {
+ "a": 1,
+ "r": 14,
+ "b": 0,
+ "g": 255,
+ },
+ "grid_size": 120,
+ "extruded": True,
+ "having": "",
+ "viewport": {
+ "longitude": -122.42066918995666,
+ "bearing": 155.80099696026355,
+ "zoom": 12.699690845482069,
+ "latitude": 37.7942314882596,
+ "pitch": 53.470800300695146,
+ },
+ "where": "",
+ "point_radius_fixed": {"type": "fix", "value": 2000},
+ "datasource": "5__table",
+ "time_grain_sqla": "Time Column",
+ "groupby": [],
+ }
+ print("Creating Grid slice")
+ slc = Slice(
+ slice_name="Grid",
+ viz_type='deck_grid',
+ datasource_type='table',
+ datasource_id=tbl.id,
+ params=get_slice_json(slice_data),
+ )
+ merge_slice(slc)
+ slices.append(slc)
+
+ print("Creating a dashboard")
+ title = "deck.gl Demo"
+ dash = db.session.query(Dash).filter_by(dashboard_title=title).first()
+
+ if not dash:
+ dash = Dash()
+ js = textwrap.dedent("""\
+ [
+ {
+ "col": 1,
+ "row": 0,
+ "size_x": 6,
+ "size_y": 4,
+ "slice_id": "37"
+ },
+ {
+ "col": 7,
+ "row": 0,
+ "size_x": 6,
+ "size_y": 4,
+ "slice_id": "38"
+ },
+ {
+ "col": 7,
+ "row": 4,
+ "size_x": 6,
+ "size_y": 4,
+ "slice_id": "39"
+ },
+ {
+ "col": 1,
+ "row": 4,
+ "size_x": 6,
+ "size_y": 4,
+ "slice_id": "40"
+ }
+ ]
+ """)
+ l = json.loads(js)
+ for i, pos in enumerate(l):
+ pos['slice_id'] = str(slices[i].id)
+ dash.dashboard_title = title
+ dash.position_json = json.dumps(l, indent=4)
+ dash.slug = "deck"
+ dash.slices = slices
+ db.session.merge(dash)
+ db.session.commit()
+
+
+def load_flights():
+ """Loading random time series data from a zip file in the repo"""
+ with gzip.open(os.path.join(DATA_FOLDER, 'fligth_data.csv.gz')) as f:
+ pdf = pd.read_csv(f, encoding='latin-1')
+
+ # Loading airports info to join and get lat/long
+ with gzip.open(os.path.join(DATA_FOLDER, 'airports.csv.gz')) as f:
+ airports = pd.read_csv(f, encoding='latin-1')
+ airports = airports.set_index('IATA_CODE')
+
+ pdf['ds'] = pdf.YEAR.map(str) + '-0' + pdf.MONTH.map(str) + '-0' + pdf.DAY.map(str)
+ pdf.ds = pd.to_datetime(pdf.ds)
+ del pdf['YEAR']
+ del pdf['MONTH']
+ del pdf['DAY']
+
+ pdf = pdf.join(airports, on='ORIGIN_AIRPORT', rsuffix='_ORIG')
+ pdf = pdf.join(airports, on='DESTINATION_AIRPORT', rsuffix='_DEST')
+ pdf.to_sql(
+ 'flights',
+ db.engine,
+ if_exists='replace',
+ chunksize=500,
+ dtype={
+ 'ds': DateTime,
+ },
+ index=False)
+ print("Done loading table!")
+
+ print("Creating table [random_time_series] reference")
+ obj = db.session.query(TBL).filter_by(table_name='random_time_series').first()
+ if not obj:
+ obj = TBL(table_name='flights')
+ obj.main_dttm_col = 'ds'
+ obj.database = get_or_create_main_db()
+ db.session.merge(obj)
+ db.session.commit()
+ obj.fetch_metadata()
diff --git a/superset/data/airports.csv.gz b/superset/data/airports.csv.gz
new file mode 100644
index 0000000000000..3043486664476
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 0000000000000..bbdebdfafcba7
Binary files /dev/null and b/superset/data/fligth_data.csv.gz differ
diff --git a/superset/dataframe.py b/superset/dataframe.py
index 1d74fdde194d8..cd9f95fd2f323 100644
--- a/superset/dataframe.py
+++ b/superset/dataframe.py
@@ -14,6 +14,7 @@
import numpy as np
import pandas as pd
+from pandas.core.common import _maybe_box_datetimelike
from pandas.core.dtypes.dtypes import ExtensionDtype
from past.builtins import basestring
@@ -48,7 +49,10 @@ def size(self):
@property
def data(self):
- return self.__df.to_dict(orient='records')
+ # work around for https://github.com/pandas-dev/pandas/issues/18372
+ return [dict((k, _maybe_box_datetimelike(v))
+ for k, v in zip(self.__df.columns, np.atleast_1d(row)))
+ for row in self.__df.values]
@classmethod
def db_type(cls, dtype):
diff --git a/superset/db_engine_specs.py b/superset/db_engine_specs.py
index c1c826ee0c98d..22da3d8c62993 100644
--- a/superset/db_engine_specs.py
+++ b/superset/db_engine_specs.py
@@ -104,14 +104,14 @@ def df_to_db(df, table, **kwargs):
@staticmethod
def create_table_from_csv(form, table):
- def allowed_file(filename):
+ def _allowed_file(filename):
# Only allow specific file extensions as specified in the config
extension = os.path.splitext(filename)[1]
return extension and extension[1:] in app.config['ALLOWED_EXTENSIONS']
filename = secure_filename(form.csv_file.data.filename)
- if not allowed_file(filename):
- return (False, 'Invalid file type selected.')
+ if not _allowed_file(filename):
+ raise Exception('Invalid file type selected')
kwargs = {
'filepath_or_buffer': filename,
'sep': form.sep.data,
@@ -140,7 +140,6 @@ def allowed_file(filename):
'chunksize': 10000,
}
BaseEngineSpec.df_to_db(**df_to_db_kwargs)
- return (True, '')
@classmethod
def escape_sql(cls, sql):
@@ -800,8 +799,7 @@ def get_column_names(filepath):
if not bucket_path:
logging.info('No upload bucket specified')
- return (
- False,
+ raise Exception(
'No upload bucket specified. You can specify one in the config file.')
upload_prefix = app.config['CSV_TO_HIVE_UPLOAD_DIRECTORY']
@@ -821,15 +819,10 @@ def get_column_names(filepath):
sql = """CREATE EXTERNAL TABLE {table_name} ( {schema_definition} )
ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS
TEXTFILE LOCATION '{location}'""".format(**locals())
- try:
- logging.info(form.con.data)
- engine = create_engine(form.con.data)
- engine.execute(sql)
- return (True, '')
- except Exception as e:
- logging.exception(e)
- logging.info(sql)
- return (False, BaseEngineSpec.extract_error_message(e))
+
+ logging.info(form.con.data)
+ engine = create_engine(form.con.data)
+ engine.execute(sql)
@classmethod
def convert_dttm(cls, target_type, dttm):
diff --git a/superset/migrations/versions/4736ec66ce19_.py b/superset/migrations/versions/4736ec66ce19_.py
new file mode 100644
index 0000000000000..2d560d57dfd21
--- /dev/null
+++ b/superset/migrations/versions/4736ec66ce19_.py
@@ -0,0 +1,201 @@
+"""empty message
+
+Revision ID: 4736ec66ce19
+Revises: f959a6652acd
+Create Date: 2017-10-03 14:37:01.376578
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '4736ec66ce19'
+down_revision = 'f959a6652acd'
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.exc import OperationalError
+
+from superset.utils import (
+ generic_find_fk_constraint_name,
+ generic_find_fk_constraint_names,
+ generic_find_uq_constraint_name,
+)
+
+
+conv = {
+ 'fk': 'fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s',
+ 'uq': 'uq_%(table_name)s_%(column_0_name)s',
+}
+
+# Helper table for database migrations using minimal schema.
+datasources = sa.Table(
+ 'datasources',
+ sa.MetaData(),
+ sa.Column('id', sa.Integer, primary_key=True),
+ sa.Column('datasource_name', sa.String(255)),
+)
+
+bind = op.get_bind()
+insp = sa.engine.reflection.Inspector.from_engine(bind)
+
+
+def upgrade():
+
+ # Add the new less restrictive uniqueness constraint.
+ with op.batch_alter_table('datasources', naming_convention=conv) as batch_op:
+ batch_op.create_unique_constraint(
+ 'uq_datasources_cluster_name',
+ ['cluster_name', 'datasource_name'],
+ )
+
+ # Augment the tables which have a foreign key constraint related to the
+ # datasources.datasource_name column.
+ for foreign in ['columns', 'metrics']:
+ with op.batch_alter_table(foreign, naming_convention=conv) as batch_op:
+
+ # Add the datasource_id column with the relevant constraints.
+ batch_op.add_column(sa.Column('datasource_id', sa.Integer))
+
+ batch_op.create_foreign_key(
+ 'fk_{}_datasource_id_datasources'.format(foreign),
+ 'datasources',
+ ['datasource_id'],
+ ['id'],
+ )
+
+ # Helper table for database migration using minimal schema.
+ table = sa.Table(
+ foreign,
+ sa.MetaData(),
+ sa.Column('id', sa.Integer, primary_key=True),
+ sa.Column('datasource_name', sa.String(255)),
+ sa.Column('datasource_id', sa.Integer),
+ )
+
+ # Migrate the existing data.
+ for datasource in bind.execute(datasources.select()):
+ bind.execute(
+ table.update().where(
+ table.c.datasource_name == datasource.datasource_name,
+ ).values(
+ datasource_id=datasource.id,
+ ),
+ )
+
+ with op.batch_alter_table(foreign, naming_convention=conv) as batch_op:
+
+ # Drop the datasource_name column and associated constraints. Note
+ # due to prior revisions (1226819ee0e3, 3b626e2a6783) there may
+ # incorectly be multiple duplicate constraints.
+ names = generic_find_fk_constraint_names(
+ foreign,
+ {'datasource_name'},
+ 'datasources',
+ insp,
+ )
+
+ for name in names:
+ batch_op.drop_constraint(
+ name or 'fk_{}_datasource_name_datasources'.format(foreign),
+ type_='foreignkey',
+ )
+
+ batch_op.drop_column('datasource_name')
+
+ # Drop the old more restrictive uniqueness constraint.
+ with op.batch_alter_table('datasources', naming_convention=conv) as batch_op:
+ batch_op.drop_constraint(
+ generic_find_uq_constraint_name(
+ 'datasources',
+ {'datasource_name'},
+ insp,
+ ) or 'uq_datasources_datasource_name',
+ type_='unique',
+ )
+
+
+def downgrade():
+
+ # Add the new more restrictive uniqueness constraint which is required by
+ # the foreign key constraints. Note this operation will fail if the
+ # datasources.datasource_name column is no longer unique.
+ with op.batch_alter_table('datasources', naming_convention=conv) as batch_op:
+ batch_op.create_unique_constraint(
+ 'uq_datasources_datasource_name',
+ ['datasource_name'],
+ )
+
+ # Augment the tables which have a foreign key constraint related to the
+ # datasources.datasource_id column.
+ for foreign in ['columns', 'metrics']:
+ with op.batch_alter_table(foreign, naming_convention=conv) as batch_op:
+
+ # Add the datasource_name column with the relevant constraints.
+ batch_op.add_column(sa.Column('datasource_name', sa.String(255)))
+
+ batch_op.create_foreign_key(
+ 'fk_{}_datasource_name_datasources'.format(foreign),
+ 'datasources',
+ ['datasource_name'],
+ ['datasource_name'],
+ )
+
+ # Helper table for database migration using minimal schema.
+ table = sa.Table(
+ foreign,
+ sa.MetaData(),
+ sa.Column('id', sa.Integer, primary_key=True),
+ sa.Column('datasource_name', sa.String(255)),
+ sa.Column('datasource_id', sa.Integer),
+ )
+
+ # Migrate the existing data.
+ for datasource in bind.execute(datasources.select()):
+ bind.execute(
+ table.update().where(
+ table.c.datasource_id == datasource.id,
+ ).values(
+ datasource_name=datasource.datasource_name,
+ ),
+ )
+
+ with op.batch_alter_table(foreign, naming_convention=conv) as batch_op:
+
+ # Drop the datasource_id column and associated constraint.
+ batch_op.drop_constraint(
+ 'fk_{}_datasource_id_datasources'.format(foreign),
+ type_='foreignkey',
+ )
+
+ batch_op.drop_column('datasource_id')
+
+ with op.batch_alter_table('datasources', naming_convention=conv) as batch_op:
+
+ # Prior to dropping the uniqueness constraint, the foreign key
+ # associated with the cluster_name column needs to be dropped.
+ batch_op.drop_constraint(
+ generic_find_fk_constraint_name(
+ 'datasources',
+ {'cluster_name'},
+ 'clusters',
+ insp,
+ ) or 'fk_datasources_cluster_name_clusters',
+ type_='foreignkey',
+ )
+
+ # Drop the old less restrictive uniqueness constraint.
+ batch_op.drop_constraint(
+ generic_find_uq_constraint_name(
+ 'datasources',
+ {'cluster_name', 'datasource_name'},
+ insp,
+ ) or 'uq_datasources_cluster_name',
+ type_='unique',
+ )
+
+ # Re-create the foreign key associated with the cluster_name column.
+ batch_op.create_foreign_key(
+ 'fk_{}_datasource_id_datasources'.format(foreign),
+ 'clusters',
+ ['cluster_name'],
+ ['cluster_name'],
+ )
diff --git a/superset/utils.py b/superset/utils.py
index 469bbc26cfc7a..bae330b4af279 100644
--- a/superset/utils.py
+++ b/superset/utils.py
@@ -377,11 +377,36 @@ def generic_find_constraint_name(table, columns, referenced, db):
t = sa.Table(table, db.metadata, autoload=True, autoload_with=db.engine)
for fk in t.foreign_key_constraints:
- if (fk.referred_table.name == referenced and
- set(fk.column_keys) == columns):
+ if fk.referred_table.name == referenced and set(fk.column_keys) == columns:
return fk.name
+def generic_find_fk_constraint_name(table, columns, referenced, insp):
+ """Utility to find a foreign-key constraint name in alembic migrations"""
+ for fk in insp.get_foreign_keys(table):
+ if fk['referred_table'] == referenced and set(fk['referred_columns']) == columns:
+ return fk['name']
+
+
+def generic_find_fk_constraint_names(table, columns, referenced, insp):
+ """Utility to find foreign-key constraint names in alembic migrations"""
+ names = set()
+
+ for fk in insp.get_foreign_keys(table):
+ if fk['referred_table'] == referenced and set(fk['referred_columns']) == columns:
+ names.add(fk['name'])
+
+ return names
+
+
+def generic_find_uq_constraint_name(table, columns, insp):
+ """Utility to find a unique constraint name in alembic migrations"""
+
+ for uq in insp.get_unique_constraints(table):
+ if columns == set(uq['column_names']):
+ return uq['name']
+
+
def get_datasource_full_name(database_name, datasource_name, schema=None):
if not schema:
return '[{}].[{}]'.format(database_name, datasource_name)
diff --git a/superset/views/core.py b/superset/views/core.py
index 679803a7571c4..26f8b5f972a1a 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -333,21 +333,23 @@ def form_get(self, form):
form.infer_datetime_format.data = True
form.decimal.data = '.'
form.if_exists.data = 'append'
- all_datasources = db.session.query(
- models.Database.sqlalchemy_uri,
- models.Database.database_name)\
+ all_datasources = (
+ db.session.query(
+ models.Database.sqlalchemy_uri,
+ models.Database.database_name)
.all()
+ )
form.con.choices += all_datasources
def form_post(self, form):
- def upload_file(csv_file):
+ def _upload_file(csv_file):
if csv_file and csv_file.filename:
filename = secure_filename(csv_file.filename)
csv_file.save(os.path.join(config['UPLOAD_FOLDER'], filename))
return filename
csv_file = form.csv_file.data
- upload_file(csv_file)
+ _upload_file(csv_file)
table = SqlaTable(table_name=form.name.data)
database = (
db.session.query(models.Database)
@@ -356,22 +358,26 @@ def upload_file(csv_file):
)
table.database = database
table.database_id = database.id
- successful, message = database.db_engine_spec.create_table_from_csv(form, table)
- os.remove(os.path.join(config['UPLOAD_FOLDER'], csv_file.filename))
- if successful:
- # Go back to welcome page / splash screen
- db_name = db.session.query(models.Database.database_name)\
- .filter_by(sqlalchemy_uri=form.data.get('con')).one()
-
- message = _('CSV file "{0}" uploaded to table "{1}" in '
- 'database "{2}"'.format(form.csv_file.data.filename,
- form.name.data,
- db_name[0]))
- flash(message, 'info')
+ try:
+ database.db_engine_spec.create_table_from_csv(form, table)
+ except Exception as e:
+ os.remove(os.path.join(config['UPLOAD_FOLDER'], csv_file.filename))
+ flash(e, 'error')
return redirect('/tablemodelview/list/')
- else:
- flash(message, 'info')
+ os.remove(os.path.join(config['UPLOAD_FOLDER'], csv_file.filename))
+ # Go back to welcome page / splash screen
+ db_name = (
+ db.session.query(models.Database.database_name)
+ .filter_by(sqlalchemy_uri=form.data.get('con'))
+ .one()
+ )
+ message = _('CSV file "{0}" uploaded to table "{1}" in '
+ 'database "{2}"'.format(form.csv_file.data.filename,
+ form.name.data,
+ db_name[0]))
+ flash(message, 'info')
+ return redirect('/tablemodelview/list/')
appbuilder.add_view_no_menu(CsvToDatabaseView)
@@ -514,7 +520,7 @@ class SliceAsync(SliceModelView): # noqa
class SliceAddView(SliceModelView): # noqa
list_columns = [
'id', 'slice_name', 'slice_link', 'viz_type',
- 'owners', 'modified', 'changed_on']
+ 'datasource_link', 'owners', 'modified', 'changed_on']
appbuilder.add_view_no_menu(SliceAddView)
@@ -578,7 +584,7 @@ def pre_add(self, obj):
obj.slug = obj.slug.strip() or None
if obj.slug:
obj.slug = obj.slug.replace(' ', '-')
- obj.slug = re.sub(r'\W+', '', obj.slug)
+ obj.slug = re.sub(r'[^a-zA-Z0-9\-]+', '', obj.slug)
if g.user not in obj.owners:
obj.owners.append(g.user)
utils.validate_json(obj.json_metadata)
diff --git a/superset/viz.py b/superset/viz.py
index 8a90c81868b08..6b369bedb7451 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -1730,7 +1730,111 @@ def get_data(self, df):
}
+class BaseDeckGLViz(BaseViz):
+
+ """Base class for deck.gl visualizations"""
+
+ is_timeseries = False
+ credits = '
deck.gl'
+
+ 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'
diff --git a/tests/core_tests.py b/tests/core_tests.py
index 2edf8988a329e..e6381b779ea08 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -5,6 +5,7 @@
from __future__ import unicode_literals
import csv
+import datetime
import doctest
import io
import json
@@ -15,9 +16,11 @@
import unittest
from flask import escape
+import pandas as pd
+import psycopg2
import sqlalchemy as sqla
-from superset import appbuilder, db, jinja_context, sm, sql_lab, utils
+from superset import appbuilder, dataframe, db, jinja_context, sm, sql_lab, utils
from superset.connectors.sqla.models import SqlaTable
from superset.models import core as models
from superset.models.sql_lab import Query
@@ -828,6 +831,22 @@ def test_import_csv(self):
finally:
os.remove(filename)
+ def test_dataframe_timezone(self):
+ tz = psycopg2.tz.FixedOffsetTimezone(offset=60, name=None)
+ data = [(datetime.datetime(2017, 11, 18, 21, 53, 0, 219225, tzinfo=tz),),
+ (datetime.datetime(2017, 11, 18, 22, 6, 30, 61810, tzinfo=tz,),)]
+ df = dataframe.SupersetDataFrame(pd.DataFrame(data=list(data),
+ columns=['data', ]))
+ data = df.data
+ self.assertDictEqual(
+ data[0],
+ {'data': pd.Timestamp('2017-11-18 21:53:00.219225+0100', tz=tz), },
+ )
+ self.assertDictEqual(
+ data[1],
+ {'data': pd.Timestamp('2017-11-18 22:06:30.061810+0100', tz=tz), },
+ )
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/import_export_tests.py b/tests/import_export_tests.py
index e945630f36986..0710cacecec5d 100644
--- a/tests/import_export_tests.py
+++ b/tests/import_export_tests.py
@@ -485,13 +485,12 @@ def test_import_druid_2_col_2_met(self):
def test_import_druid_override(self):
datasource = self.create_druid_datasource(
- 'druid_override', id=10003, cols_names=['col1'],
+ 'druid_override', id=10004, cols_names=['col1'],
metric_names=['m1'])
imported_id = DruidDatasource.import_obj(
datasource, import_time=1991)
-
table_over = self.create_druid_datasource(
- 'druid_override', id=10003,
+ 'druid_override', id=10004,
cols_names=['new_col1', 'col2', 'col3'],
metric_names=['new_metric1'])
imported_over_id = DruidDatasource.import_obj(
@@ -500,19 +499,19 @@ def test_import_druid_override(self):
imported_over = self.get_datasource(imported_over_id)
self.assertEquals(imported_id, imported_over.id)
expected_datasource = self.create_druid_datasource(
- 'druid_override', id=10003, metric_names=['new_metric1', 'm1'],
+ 'druid_override', id=10004, metric_names=['new_metric1', 'm1'],
cols_names=['col1', 'new_col1', 'col2', 'col3'])
self.assert_datasource_equals(expected_datasource, imported_over)
def test_import_druid_override_idential(self):
datasource = self.create_druid_datasource(
- 'copy_cat', id=10004, cols_names=['new_col1', 'col2', 'col3'],
+ 'copy_cat', id=10005, cols_names=['new_col1', 'col2', 'col3'],
metric_names=['new_metric1'])
imported_id = DruidDatasource.import_obj(
datasource, import_time=1993)
copy_datasource = self.create_druid_datasource(
- 'copy_cat', id=10004, cols_names=['new_col1', 'col2', 'col3'],
+ 'copy_cat', id=10005, cols_names=['new_col1', 'col2', 'col3'],
metric_names=['new_metric1'])
imported_id_copy = DruidDatasource.import_obj(
copy_datasource, import_time=1994)
diff --git a/tox.ini b/tox.ini
index 1878fddc915a1..78198ea190894 100644
--- a/tox.ini
+++ b/tox.ini
@@ -68,12 +68,12 @@ commands =
[testenv:py27-mysql]
basepython = python2.7
setenv =
- SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://root@localhost/superset?charset=utf8
+ SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://mysqluser:mysqluserpassword@localhost/superset?charset=utf8
[testenv:py34-mysql]
basepython = python3.4
setenv =
- SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://root@localhost/superset
+ SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://mysqluser:mysqluserpassword@localhost/superset
[testenv:py35-mysql]
basepython = python3.5