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( -
- - {t('You have unsaved changes.')} {t('Click the')}   -   - {t('button on the top right to save your changes.')} - -
, - document.getElementById('alert-container'), - ); -} - -function initDashboardView(dashboard) { - render( -
- -
-
, - document.getElementById('dashboard-header'), - ); - // eslint-disable-next-line no-param-reassign - dashboard.reactGridLayout = render( - , - document.getElementById('grid-container'), - ); - - // Displaying widget controls on hover - $('.react-grid-item').hover( - function () { - $(this).find('.chart-controls').fadeIn(300); - }, - function () { - $(this).find('.chart-controls').fadeOut(300); - }, - ); - $('div.grid-container').css('visibility', 'visible'); - - $('div.widget').click(function (e) { - const $this = $(this); - const $target = $(e.target); - - if ($target.hasClass('slice_info')) { - $this.find('.slice_description').slideToggle(0, function () { - $this.find('.refresh').click(); - }); - } else if ($target.hasClass('controls-toggle')) { - $this.find('.chart-controls').toggle(); - } - }); - px.initFavStars(); - $('[data-toggle="tooltip"]').tooltip({ container: 'body' }); -} - -export function dashboardContainer(dashboard, datasources, userid) { - return Object.assign({}, dashboard, { - type: 'dashboard', - filters: {}, - curUserId: userid, - init() { - this.sliceObjects = []; - dashboard.slices.forEach((data) => { - if (data.error) { - const html = `
${data.error}
`; - $(`#slice_${data.slice_id}`).find('.token').html(html); - } else { - const slice = px.Slice(data, datasources[data.form_data.datasource], this); - $(`#slice_${data.slice_id}`).find('a.refresh').click(() => { - slice.render(true); - }); - this.sliceObjects.push(slice); - } - }); - this.loadPreSelectFilters(); - this.renderSlices(this.sliceObjects); - this.firstLoad = false; - this.bindResizeToWindowResize(); - }, - onChange() { - onBeforeUnload(true); - renderAlert(); - }, - onSave() { - onBeforeUnload(false); - $('#alert-container').html(''); - }, - loadPreSelectFilters() { - try { - const filters = JSON.parse(px.getParam('preselect_filters') || '{}'); - for (const sliceId in filters) { - for (const col in filters[sliceId]) { - this.setFilter(sliceId, col, filters[sliceId][col], false, false); - } - } - } catch (e) { - // console.error(e); - } - }, - setFilter(sliceId, col, vals, refresh) { - this.addFilter(sliceId, col, vals, false, refresh); - }, - done(slice) { - const refresh = slice.getWidgetHeader().find('.refresh'); - const data = slice.data; - const cachedWhen = moment.utc(data.cached_dttm).fromNow(); - if (data !== undefined && data.is_cached) { - refresh - .addClass('danger') - .attr( - 'title', - t('Served from data cached %s . Click to force refresh.', cachedWhen)) - .tooltip('fixTitle'); - } else { - refresh - .removeClass('danger') - .attr('title', t('Click to force refresh')) - .tooltip('fixTitle'); - } - }, - effectiveExtraFilters(sliceId) { - const f = []; - const immuneSlices = this.metadata.filter_immune_slices || []; - if (sliceId && immuneSlices.includes(sliceId)) { - // The slice is immune to dashboard filters - return f; - } - - // Building a list of fields the slice is immune to filters on - let immuneToFields = []; - if ( - sliceId && - this.metadata.filter_immune_slice_fields && - this.metadata.filter_immune_slice_fields[sliceId]) { - immuneToFields = this.metadata.filter_immune_slice_fields[sliceId]; - } - for (const filteringSliceId in this.filters) { - if (filteringSliceId === sliceId.toString()) { - // Filters applied by the slice don't apply to itself - continue; - } - for (const field in this.filters[filteringSliceId]) { - if (!immuneToFields.includes(field)) { - f.push({ - col: field, - op: 'in', - val: this.filters[filteringSliceId][field], - }); - } - } - } - return f; - }, - addFilter(sliceId, col, vals, merge = true, refresh = true) { - if ( - this.getSlice(sliceId) && ( - ['__from', '__to', '__time_col', '__time_grain', '__time_origin', '__granularity'] - .indexOf(col) >= 0 || - this.getSlice(sliceId).formData.groupby.indexOf(col) !== -1 - ) - ) { - if (!(sliceId in this.filters)) { - this.filters[sliceId] = {}; - } - if (!(col in this.filters[sliceId]) || !merge) { - this.filters[sliceId][col] = vals; - - // d3.merge pass in array of arrays while some value form filter components - // from and to filter box require string to be process and return - } else if (this.filters[sliceId][col] instanceof Array) { - this.filters[sliceId][col] = d3.merge([this.filters[sliceId][col], vals]); - } else { - this.filters[sliceId][col] = d3.merge([[this.filters[sliceId][col]], vals])[0] || ''; - } - if (refresh) { - this.refreshExcept(sliceId); - } - } - this.updateFilterParamsInUrl(); - }, - readFilters() { - // Returns a list of human readable active filters - return JSON.stringify(this.filters, null, ' '); - }, - updateFilterParamsInUrl() { - const urlObj = urlLib.parse(location.href, true); - urlObj.query = urlObj.query || {}; - urlObj.query.preselect_filters = this.readFilters(); - urlObj.search = null; - history.pushState(urlObj.query, window.title, urlLib.format(urlObj)); - }, - bindResizeToWindowResize() { - let resizeTimer; - const dash = this; - $(window).on('resize', () => { - clearTimeout(resizeTimer); - resizeTimer = setTimeout(() => { - dash.sliceObjects.forEach((slice) => { - slice.resize(); - }); - }, 500); - }); - }, - stopPeriodicRender() { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - this.refreshTimer = null; - } - }, - renderSlices(slices, force = false, interval = 0) { - if (!interval) { - slices.forEach(slice => slice.render(force)); - return; - } - const meta = this.metadata; - const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds - if (typeof meta.stagger_refresh !== 'boolean') { - meta.stagger_refresh = meta.stagger_refresh === undefined ? - true : meta.stagger_refresh === 'true'; - } - const delay = meta.stagger_refresh ? refreshTime / (slices.length - 1) : 0; - slices.forEach((slice, i) => { - setTimeout(() => slice.render(force), delay * i); - }); - }, - startPeriodicRender(interval) { - this.stopPeriodicRender(); - const dash = this; - const immune = this.metadata.timed_refresh_immune_slices || []; - const refreshAll = () => { - const slices = dash.sliceObjects - .filter(slice => immune.indexOf(slice.data.slice_id) === -1); - dash.fetchSlices(slices, true, interval * 0.2); - }; - const fetchAndRender = function () { - refreshAll(); - if (interval > 0) { - dash.refreshTimer = setTimeout(function () { - fetchAndRender(); - }, interval); - } - }; - fetchAndRender(); - }, - refreshExcept(sliceId) { - const immune = this.metadata.filter_immune_slices || []; - const slices = this.sliceObjects.filter(slice => - slice.data.slice_id !== sliceId && immune.indexOf(slice.data.slice_id) === -1); - this.renderSlices(slices); - }, - clearFilters(sliceId) { - delete this.filters[sliceId]; - this.refreshExcept(sliceId); - this.updateFilterParamsInUrl(); - }, - removeFilter(sliceId, col, vals) { - if (sliceId in this.filters) { - if (col in this.filters[sliceId]) { - const a = []; - this.filters[sliceId][col].forEach(function (v) { - if (vals.indexOf(v) < 0) { - a.push(v); - } - }); - this.filters[sliceId][col] = a; - } - } - this.refreshExcept(sliceId); - this.updateFilterParamsInUrl(); - }, - getSlice(sliceId) { - const id = parseInt(sliceId, 10); - let i = 0; - let slice = null; - while (i < this.sliceObjects.length) { - // when the slice is found, assign to slice and break; - if (this.sliceObjects[i].data.slice_id === id) { - slice = this.sliceObjects[i]; - break; - } - i++; - } - return slice; - }, - getAjaxErrorMsg(error) { - const respJSON = error.responseJSON; - return (respJSON && respJSON.message) ? respJSON.message : - error.responseText; - }, - addSlicesToDashboard(sliceIds) { - const getAjaxErrorMsg = this.getAjaxErrorMsg; - $.ajax({ - type: 'POST', - url: `/superset/add_slices/${dashboard.id}/`, - data: { - data: JSON.stringify({ slice_ids: sliceIds }), - }, - success() { - // Refresh page to allow for slices to re-render - window.location.reload(); - }, - error(error) { - const errorMsg = getAjaxErrorMsg(error); - utils.showModal({ - title: t('Error'), - body: t('Sorry, there was an error adding slices to this dashboard: %s', errorMsg), - }); - }, - }); - }, - updateDashboardTitle(title) { - this.dashboard_title = title; - this.onChange(); - }, - }); -} - -$(document).ready(() => { - // Getting bootstrapped data from the DOM - utils.initJQueryAjax(); - const dashboardData = $('.dashboard').data('bootstrap'); - - const state = getInitialState(dashboardData); - px = superset(state); - const dashboard = dashboardContainer(state.dashboard, state.datasources, state.userId); - initDashboardView(dashboard); - dashboard.init(); -}); diff --git a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx index 03e0cb80c75e3..6c2ea0e4b73df 100644 --- a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx +++ b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx @@ -54,7 +54,8 @@ class SliceAdder extends React.Component { const slices = response.result.map(slice => ({ id: slice.id, sliceName: slice.slice_name, - vizType: slice.vizType, + vizType: slice.viz_type, + datasourceLink: slice.datasource_link, modified: slice.modified, })); @@ -165,6 +166,14 @@ class SliceAdder extends React.Component { > {t('Viz')} + datasourceLink} + > + {t('Datasource')} + ); - } - renderChartTitle() { let title; if (this.props.slice) { @@ -113,9 +106,12 @@ class ExploreChartHeader extends React.PureComponent { } - - {this.renderAlteredTag()} - + {this.props.chart.sliceFormData && + + }
{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