From b1074fc872a9072b285f32db0ca8c6d0d2493f81 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 18 Sep 2019 16:30:04 -0700 Subject: [PATCH] fix: lint --- .../src/AnimatableDeckGLContainer.jsx | 24 +-- .../src/CategoricalDeckGLContainer.jsx | 64 ++++--- .../src/DeckGLContainer.jsx | 21 ++- .../src/Legend.css | 44 +++++ .../src/Legend.jsx | 105 +++++++++++ .../src/Multi/Multi.jsx | 6 +- .../src/Multi/index.js | 6 +- .../src/PlaySlider.css | 46 +++++ .../src/PlaySlider.jsx | 170 ++++++++++++++++++ .../src/TooltipRow.jsx | 8 +- .../src/css/deckgl.css | 22 +++ .../src/factory.jsx | 42 ++--- .../src/layers/Arc/Arc.jsx | 24 ++- .../src/layers/Arc/index.js | 6 +- .../src/layers/Geojson/Geojson.jsx | 37 ++-- .../src/layers/Geojson/index.js | 6 +- .../src/layers/Grid/Grid.jsx | 8 +- .../src/layers/Grid/index.js | 6 +- .../src/layers/Hex/Hex.jsx | 8 +- .../src/layers/Hex/index.js | 6 +- .../src/layers/Path/Path.jsx | 21 ++- .../src/layers/Path/index.js | 6 +- .../src/layers/Polygon/Polygon.jsx | 82 +++++---- .../src/layers/Polygon/index.js | 6 +- .../src/layers/Scatter/Scatter.jsx | 23 ++- .../src/layers/Scatter/index.js | 6 +- .../src/layers/Screengrid/Screengrid.jsx | 34 ++-- .../src/layers/Screengrid/index.js | 6 +- .../src/layers/common.jsx | 41 ++--- .../src/transformProps.js | 8 +- .../src/utils.js | 12 +- .../src/utils/colors.js | 27 +++ .../src/utils/sandbox.js | 51 ++++++ .../src/utils/time.js | 124 +++++++++++++ 34 files changed, 857 insertions(+), 249 deletions(-) create mode 100644 superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Legend.css create mode 100644 superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Legend.jsx create mode 100644 superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/PlaySlider.css create mode 100644 superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/PlaySlider.jsx create mode 100644 superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/css/deckgl.css create mode 100644 superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/colors.js create mode 100644 superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/sandbox.js create mode 100644 superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/time.js diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/AnimatableDeckGLContainer.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/AnimatableDeckGLContainer.jsx index fe2c7165db7c..83574cd79f94 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/AnimatableDeckGLContainer.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/AnimatableDeckGLContainer.jsx @@ -22,7 +22,7 @@ import PropTypes from 'prop-types'; import DeckGLContainer from './DeckGLContainer'; import PlaySlider from '../PlaySlider'; -const PLAYSLIDER_HEIGHT = 20; // px +const PLAYSLIDER_HEIGHT = 20; // px const propTypes = { getLayers: PropTypes.func.isRequired, @@ -55,12 +55,14 @@ export default class AnimatableDeckGLContainer extends React.Component { super(props); this.onViewportChange = this.onViewportChange.bind(this); } + onViewportChange(viewport) { const originalViewport = this.props.disabled ? { ...viewport } : { ...viewport, height: viewport.height + PLAYSLIDER_HEIGHT }; this.props.onViewportChange(originalViewport); } + render() { const { start, @@ -95,16 +97,16 @@ export default class AnimatableDeckGLContainer extends React.Component { mapboxApiAccessToken={mapboxApiAccessToken} onViewportChange={this.onViewportChange} /> - {!disabled && - - } + {!disabled && ( + + )} {children} ); diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx index 2a3e7b3c717a..7ba534879fb5 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx @@ -22,10 +22,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { CategoricalColorNamespace } from '@superset-ui/color'; import AnimatableDeckGLContainer from './AnimatableDeckGLContainer'; -import Legend from '../Legend'; -import { hexToRGB } from '../../modules/colors'; -import { getPlaySliderParams } from '../../modules/time'; -import sandboxedEval from '../../modules/sandbox'; +import Legend from './Legend'; +import { hexToRGB } from './utils/colors'; +import { getPlaySliderParams } from './utils/time'; +import sandboxedEval from './utils/sandbox'; import { fitViewport } from './layers/common'; const { getScale } = CategoricalColorNamespace; @@ -35,7 +35,7 @@ function getCategories(fd, data) { const fixedColor = [c.r, c.g, c.b, 255 * c.a]; const colorFn = getScale(fd.color_scheme); const categories = {}; - data.forEach((d) => { + data.forEach(d => { if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) { let color; if (fd.dimension) { @@ -46,6 +46,7 @@ function getCategories(fd, data) { categories[d.cat_color] = { color, enabled: true }; } }); + return categories; } @@ -78,21 +79,23 @@ export default class CategoricalDeckGLContainer extends React.PureComponent { this.toggleCategory = this.toggleCategory.bind(this); this.showSingleCategory = this.showSingleCategory.bind(this); } + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.payload.form_data !== this.state.formData) { this.setState({ ...this.getStateFromProps(nextProps) }); } } + onValuesChange(values) { this.setState({ - values: Array.isArray(values) - ? values - : [values, values + this.state.getStep(values)], + values: Array.isArray(values) ? values : [values, values + this.state.getStep(values)], }); } + onViewportChange(viewport) { this.setState({ viewport }); } + getStateFromProps(props, state) { const features = props.payload.data.features || []; const timestamps = features.map(f => f.__timestamp); @@ -107,19 +110,10 @@ export default class CategoricalDeckGLContainer extends React.PureComponent { // the granularity has to be read from the payload form_data, not the // props formData which comes from the instantaneous controls state - const granularity = ( - props.payload.form_data.time_grain_sqla || - props.payload.form_data.granularity || - 'P1D' - ); + const granularity = + props.payload.form_data.time_grain_sqla || props.payload.form_data.granularity || 'P1D'; - const { - start, - end, - getStep, - values, - disabled, - } = getPlaySliderParams(timestamps, granularity); + const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, granularity); const viewport = props.formData.autozoom ? fitViewport(props.viewport, props.getPoints(features)) @@ -138,17 +132,10 @@ export default class CategoricalDeckGLContainer extends React.PureComponent { categories, }; } + getLayers(values) { - const { - getLayer, - payload, - formData: fd, - onAddFilter, - setTooltip, - } = this.props; - let features = payload.data.features - ? [...payload.data.features] - : []; + const { getLayer, payload, formData: fd, onAddFilter, setTooltip } = this.props; + let features = payload.data.features ? [...payload.data.features] : []; // Add colors from categories or fixed color features = this.addColor(features, fd); @@ -179,18 +166,23 @@ export default class CategoricalDeckGLContainer extends React.PureComponent { return [getLayer(fd, filteredPayload, onAddFilter, setTooltip)]; } + addColor(data, fd) { const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 }; const colorFn = getScale(fd.color_scheme); - return data.map((d) => { + + return data.map(d => { let color; if (fd.dimension) { color = hexToRGB(colorFn(d.cat_color), c.a * 255); + return { ...d, color }; } + return d; }); } + toggleCategory(category) { const categoryState = this.state.categories[category]; const categories = { @@ -204,17 +196,23 @@ export default class CategoricalDeckGLContainer extends React.PureComponent { // if all categories are disabled, enable all -- similar to nvd3 if (Object.values(categories).every(v => !v.enabled)) { /* eslint-disable no-param-reassign */ - Object.values(categories).forEach((v) => { v.enabled = true; }); + Object.values(categories).forEach(v => { + v.enabled = true; + }); } this.setState({ categories }); } + showSingleCategory(category) { const categories = { ...this.state.categories }; /* eslint-disable no-param-reassign */ - Object.values(categories).forEach((v) => { v.enabled = false; }); + Object.values(categories).forEach(v => { + v.enabled = false; + }); categories[category].enabled = true; this.setState({ categories }); } + render() { return (
diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/DeckGLContainer.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/DeckGLContainer.jsx index ff414bfc0ad9..702b5c7ac366 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/DeckGLContainer.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/DeckGLContainer.jsx @@ -22,9 +22,9 @@ import MapGL from 'react-map-gl'; import DeckGL from 'deck.gl'; import 'mapbox-gl/dist/mapbox-gl.css'; import { isEqual } from 'lodash'; -import '../stylesheets/deckgl.css'; +import './css/deckgl.css'; -const TICK = 2000; // milliseconds +const TICK = 2000; // milliseconds const propTypes = { viewport: PropTypes.object.isRequired, @@ -51,6 +51,7 @@ export default class DeckGLContainer extends React.Component { timer: setInterval(this.tick, TICK), }; } + static getDerivedStateFromProps(nextProps, prevState) { if (nextProps.viewport !== prevState.viewport) { return { @@ -58,11 +59,14 @@ export default class DeckGLContainer extends React.Component { previousViewport: prevState.viewport, }; } + return null; } + componentWillUnmount() { clearInterval(this.state.timer); } + onViewportChange(viewport) { const vp = Object.assign({}, viewport); // delete vp.width; @@ -72,6 +76,7 @@ export default class DeckGLContainer extends React.Component { // this.setState(() => ({ viewport: newVp })); this.props.onViewportChange(newVp); } + tick() { // Limiting updating viewport controls through Redux at most 1*sec // Deep compare is needed as shallow equality doesn't work here, viewport object @@ -85,15 +90,19 @@ export default class DeckGLContainer extends React.Component { this.setState(() => ({ previousViewport: this.props.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.map(l => (typeof l === 'function' ? l() : l)); } + return this.props.layers; } + render() { const { viewport } = this.props; + return ( - + ); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Legend.css b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Legend.css new file mode 100644 index 000000000000..6b6345c0a002 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Legend.css @@ -0,0 +1,44 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +div.legend { + font-size: 90%; + position: absolute; + background: #fff; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.15); + margin: 24px; + padding: 12px 20px; + outline: none; + overflow-y: scroll; + max-height: 200px; +} + +ul.categories { + list-style: none; + padding-left: 0; + margin: 0; +} + +ul.categories li a { + color: rgb(51, 51, 51); + text-decoration: none; +} + +ul.categories li a span { + margin-right: 10px; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Legend.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Legend.jsx new file mode 100644 index 000000000000..355f632d34b9 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Legend.jsx @@ -0,0 +1,105 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { formatNumber } from '@superset-ui/number-format'; + +import './Legend.css'; + +const categoryDelimiter = ' - '; + +const propTypes = { + categories: PropTypes.object, + toggleCategory: PropTypes.func, + showSingleCategory: PropTypes.func, + format: PropTypes.string, + position: PropTypes.oneOf([null, 'tl', 'tr', 'bl', 'br']), +}; + +const defaultProps = { + categories: {}, + toggleCategory: () => {}, + showSingleCategory: () => {}, + format: null, + position: 'tr', +}; + +export default class Legend extends React.PureComponent { + format(value) { + if (!this.props.format) { + return value; + } + + const numValue = parseFloat(value); + return formatNumber(this.props.format, numValue); + + } + + formatCategoryLabel(k) { + if (!this.props.format) { + return k; + } + + if (k.includes(categoryDelimiter)) { + const values = k.split(categoryDelimiter); + return this.format(values[0]) + categoryDelimiter + this.format(values[1]); + } + + return this.format(k); + } + + render() { + if (Object.keys(this.props.categories).length === 0 || this.props.position === null) { + return null; + } + + const categories = Object.entries(this.props.categories).map(([k, v]) => { + const style = { color: 'rgba(' + v.color.join(', ') + ')' }; + const icon = v.enabled ? '\u25FC' : '\u25FB'; + return ( +
  • + this.props.toggleCategory(k)} + onDoubleClick={() => this.props.showSingleCategory(k)} + > + {icon} {this.formatCategoryLabel(k)} + +
  • + ); + }); + + const vertical = this.props.position.charAt(0) === 't' ? 'top' : 'bottom'; + const horizontal = this.props.position.charAt(1) === 'r' ? 'right' : 'left'; + const style = { + position: 'absolute', + [vertical]: '0px', + [horizontal]: '10px', + }; + + return ( +
    +
      {categories}
    +
    + ); + } +} + +Legend.propTypes = propTypes; +Legend.defaultProps = defaultProps; diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Multi/Multi.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Multi/Multi.jsx index 335ba3a7e02b..1206b0df99c0 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Multi/Multi.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Multi/Multi.jsx @@ -66,7 +66,7 @@ class DeckMulti extends React.PureComponent { loadLayers(formData, payload, viewport) { this.setState({ subSlicesLayers: {}, viewport }); - payload.data.slices.forEach((subslice) => { + payload.data.slices.forEach(subslice => { // Filters applied to multi_deck are passed down to underlying charts // note that dashboard contextual information (filter_immune_slices and such) aren't // taken into consideration here @@ -84,8 +84,8 @@ class DeckMulti extends React.PureComponent { }; SupersetClient.get({ - endpoint: getExploreLongUrl(subsliceCopy.form_data, 'json'), - }) + endpoint: getExploreLongUrl(subsliceCopy.form_data, 'json'), + }) .then(({ json }) => { const layer = layerGenerators[subsliceCopy.form_data.viz_type]( subsliceCopy.form_data, diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Multi/index.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Multi/index.js index c3cae628ac6c..290df741fa3a 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Multi/index.js +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/Multi/index.js @@ -22,17 +22,17 @@ import thumbnail from './images/thumbnail.png'; import transformProps from '../transformProps'; const metadata = new ChartMetadata({ - name: t('deck.gl Multiple Layers'), - description: '', credits: ['https://uber.github.io/deck.gl'], + description: '', + name: t('deck.gl Multiple Layers'), thumbnail, }); export default class MultiChartPlugin extends ChartPlugin { constructor() { super({ + loadChart: () => import('./Multi'), metadata, - loadChart: () => import('./Multi.jsx'), transformProps, }); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/PlaySlider.css b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/PlaySlider.css new file mode 100644 index 000000000000..0c21b3e8fe0f --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/PlaySlider.css @@ -0,0 +1,46 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +.play-slider { + display: flex; + height: 40px; + width: 100%; + margin: 0; +} + +.play-slider-controls { + flex: 0 0 80px; + text-align: middle; +} + +.play-slider-scrobbler { + flex: 1; +} + +.slider.slider-horizontal { + width: 100% !important; +} + +.slider-button { + color: #b3b3b3; + margin-right: 5px; +} + +div.slider > div.tooltip.tooltip-main.top.in { + margin-left: 0 !important; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/PlaySlider.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/PlaySlider.jsx new file mode 100644 index 000000000000..f6600226f77c --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/PlaySlider.jsx @@ -0,0 +1,170 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import PropTypes from 'prop-types'; +import Mousetrap from 'mousetrap'; +import { t } from '@superset-ui/translation'; +import BootrapSliderWrapper from '../components/BootstrapSliderWrapper'; +import './PlaySlider.css'; + +const propTypes = { + start: PropTypes.number.isRequired, + step: PropTypes.number.isRequired, + end: PropTypes.number.isRequired, + values: PropTypes.array.isRequired, + onChange: PropTypes.func, + loopDuration: PropTypes.number, + maxFrames: PropTypes.number, + orientation: PropTypes.oneOf(['horizontal', 'vertical']), + reversed: PropTypes.bool, + disabled: PropTypes.bool, + range: PropTypes.bool, +}; + +const defaultProps = { + onChange: () => {}, + loopDuration: 15000, + maxFrames: 100, + orientation: 'horizontal', + reversed: false, + disabled: false, + range: true, +}; + +export default class PlaySlider extends React.PureComponent { + constructor(props) { + super(props); + this.state = { intervalId: null }; + + const range = props.end - props.start; + const frames = Math.min(props.maxFrames, range / props.step); + const width = range / frames; + this.intervalMilliseconds = props.loopDuration / frames; + this.increment = width < props.step ? props.step : width - (width % props.step); + + this.onChange = this.onChange.bind(this); + this.play = this.play.bind(this); + this.pause = this.pause.bind(this); + this.stepBackward = this.stepBackward.bind(this); + this.stepForward = this.stepForward.bind(this); + this.getPlayClass = this.getPlayClass.bind(this); + this.formatter = this.formatter.bind(this); + } + componentDidMount() { + Mousetrap.bind(['space'], this.play); + } + componentWillUnmount() { + Mousetrap.unbind(['space']); + } + onChange(event) { + this.props.onChange(event.target.value); + if (this.state.intervalId != null) { + this.pause(); + } + } + getPlayClass() { + if (this.state.intervalId == null) { + return 'fa fa-play fa-lg slider-button'; + } + return 'fa fa-pause fa-lg slider-button'; + } + play() { + if (this.props.disabled) { + return; + } + if (this.state.intervalId != null) { + this.pause(); + } else { + const id = setInterval(this.stepForward, this.intervalMilliseconds); + this.setState({ intervalId: id }); + } + } + pause() { + clearInterval(this.state.intervalId); + this.setState({ intervalId: null }); + } + stepForward() { + const { start, end, step, values, disabled } = this.props; + + if (disabled) { + return; + } + + const currentValues = Array.isArray(values) ? values : [values, values + step]; + const nextValues = currentValues.map(value => value + this.increment); + const carriageReturn = (nextValues[1] > end) ? (nextValues[0] - start) : 0; + + this.props.onChange(nextValues.map(value => value - carriageReturn)); + } + stepBackward() { + const { start, end, step, values, disabled } = this.props; + + if (disabled) { + return; + } + + const currentValues = Array.isArray(values) ? values : [values, values + step]; + const nextValues = currentValues.map(value => value - this.increment); + const carriageReturn = (nextValues[0] < start) ? (end - nextValues[1]) : 0; + + this.props.onChange(nextValues.map(value => value + carriageReturn)); + } + formatter(values) { + if (this.props.disabled) { + return t('Data has no time steps'); + } + + let parts = values; + if (!Array.isArray(values)) { + parts = [values]; + } else if (values[0] === values[1]) { + parts = [values[0]]; + } + return parts.map(value => (new Date(value)).toUTCString()).join(' : '); + } + render() { + const { start, end, step, orientation, reversed, disabled, range, values } = this.props; + return ( +
    +
    + + + +
    +
    + +
    +
    + ); + } +} + +PlaySlider.propTypes = propTypes; +PlaySlider.defaultProps = defaultProps; diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/TooltipRow.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/TooltipRow.jsx index cc85bfd984ed..0e1138d28002 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/TooltipRow.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/TooltipRow.jsx @@ -24,11 +24,15 @@ const propTypes = { value: PropTypes.string.isRequired, }; - export default class TooltipRow extends React.PureComponent { render() { + const { label, value } = this.props; + return ( -
    {this.props.label}{this.props.value}
    +
    + {label} + {value} +
    ); } } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/css/deckgl.css b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/css/deckgl.css new file mode 100644 index 000000000000..b085d1c6b2e3 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/css/deckgl.css @@ -0,0 +1,22 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + .deckgl-tooltip > div { + overflow: hidden; + text-overflow: ellipsis; + } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/factory.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/factory.jsx index abbdcca0db43..824eabd4454b 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/factory.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/factory.jsx @@ -52,39 +52,30 @@ export function createDeckGLComponent(getLayer, getPoints) { }; this.onViewportChange = this.onViewportChange.bind(this); } + UNSAFE_componentWillReceiveProps(nextProps) { // Only recompute the layer if anything BUT the viewport has changed const nextFdNoVP = { ...nextProps.formData, viewport: null }; const currFdNoVP = { ...this.props.formData, viewport: null }; - if ( - !isEqual(nextFdNoVP, currFdNoVP) || - nextProps.payload !== this.props.payload - ) { + if (!isEqual(nextFdNoVP, currFdNoVP) || nextProps.payload !== this.props.payload) { this.setState({ layer: this.computeLayer(nextProps) }); } } + onViewportChange(viewport) { this.setState({ viewport }); } + computeLayer(props) { - const { - formData, - payload, - onAddFilter, - setTooltip, - } = props; + const { formData, payload, onAddFilter, setTooltip } = props; + return getLayer(formData, payload, onAddFilter, setTooltip); } + render() { - const { - formData, - payload, - setControlValue, - } = this.props; - const { - layer, - viewport, - } = this.state; + const { formData, payload, setControlValue } = this.props; + const { layer, viewport } = this.state; + return ( ); + /> + ); } } Component.propTypes = propTypes; Component.defaultProps = defaultProps; + return Component; } export function createCategoricalDeckGLComponent(getLayer, getPoints) { function Component(props) { - const { - formData, - payload, - setControlValue, - onAddFilter, - setTooltip, - viewport, - } = props; + const { formData, payload, setControlValue, onAddFilter, setTooltip, viewport } = props; return ( { + data.forEach(d => { points.push(d.sourcePosition); points.push(d.targetPosition); }); + return points; } function setTooltipContent(formData) { return o => (
    - - - { - formData.dimension && - } + + + {formData.dimension && ( + + )}
    ); } @@ -48,12 +55,13 @@ export function getLayer(fd, payload, onAddFilter, setTooltip) { const data = payload.data.features; const sc = fd.color_picker; const tc = fd.target_color_picker; + return new ArcLayer({ - id: `path-layer-${fd.slice_id}`, data, getSourceColor: d => d.sourceColor || d.color || [sc.r, sc.g, sc.b, 255 * sc.a], getTargetColor: d => d.targetColor || d.color || [tc.r, tc.g, tc.b, 255 * tc.a], - strokeWidth: (fd.stroke_width) ? fd.stroke_width : 3, + id: `path-layer-${fd.slice_id}`, + strokeWidth: fd.stroke_width ? fd.stroke_width : 3, ...commonLayerProps(fd, setTooltip, setTooltipContent(fd)), }); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Arc/index.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Arc/index.js index 8f597631da48..2ef3543eb123 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Arc/index.js +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Arc/index.js @@ -22,17 +22,17 @@ import thumbnail from './images/thumbnail.png'; import transformProps from '../../transformProps'; const metadata = new ChartMetadata({ - name: t('deck.gl Arc'), - description: '', credits: ['https://uber.github.io/deck.gl'], + description: '', + name: t('deck.gl Arc'), thumbnail, }); export default class ArcChartPlugin extends ChartPlugin { constructor() { super({ + loadChart: () => import('./Arc'), metadata, - loadChart: () => import('./Arc.jsx'), transformProps, }); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Geojson/Geojson.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Geojson/Geojson.jsx index 7488a3ddf56a..83bb44211152 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Geojson/Geojson.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Geojson/Geojson.jsx @@ -22,8 +22,8 @@ import { GeoJsonLayer } from 'deck.gl'; // TODO import geojsonExtent from 'geojson-extent'; import DeckGLContainer from '../../DeckGLContainer'; -import { hexToRGB } from '../../../../modules/colors'; -import sandboxedEval from '../../../../modules/sandbox'; +import { hexToRGB } from '../../utils/colors'; +import sandboxedEval from '../../utils/sandbox'; import { commonLayerProps } from '../common'; import TooltipRow from '../../TooltipRow'; @@ -39,7 +39,7 @@ const propertyMap = { const alterProps = (props, propOverrides) => { const newProps = {}; - Object.keys(props).forEach((k) => { + Object.keys(props).forEach(k => { if (k in propertyMap) { newProps[propertyMap[k]] = props[k]; } else { @@ -52,6 +52,7 @@ const alterProps = (props, propOverrides) => { if (typeof props.strokeColor === 'string') { newProps.strokeColor = hexToRGB(props.strokeColor); } + return { ...newProps, ...propOverrides, @@ -60,7 +61,7 @@ const alterProps = (props, propOverrides) => { let features; const recurseGeoJson = (node, propOverrides, extraProps) => { if (node && node.features) { - node.features.forEach((obj) => { + node.features.forEach(obj => { recurseGeoJson(obj, propOverrides, node.extraProps || extraProps); }); } @@ -78,14 +79,17 @@ const recurseGeoJson = (node, propOverrides, extraProps) => { function setTooltipContent(o) { return ( - o.object.extraProps && -
    - { - Object.keys(o.object.extraProps).map((prop, index) => - , - ) - } -
    + o.object.extraProps && ( +
    + {Object.keys(o.object.extraProps).map((prop, index) => ( + + ))} +
    + ) ); } @@ -138,14 +142,7 @@ const defaultProps = { }; function deckGeoJson(props) { - const { - formData, - payload, - setControlValue, - onAddFilter, - setTooltip, - viewport, - } = props; + const { formData, payload, setControlValue, onAddFilter, setTooltip, viewport } = props; // TODO get this to work // if (formData.autozoom) { diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Geojson/index.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Geojson/index.js index 06fded5de738..dbedf53418c3 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Geojson/index.js +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Geojson/index.js @@ -22,17 +22,17 @@ import thumbnail from './images/thumbnail.png'; import transformProps from '../../transformProps'; const metadata = new ChartMetadata({ - name: t('deck.gl Geojson'), - description: '', credits: ['https://uber.github.io/deck.gl'], + description: '', + name: t('deck.gl Geojson'), thumbnail, }); export default class GeojsonChartPlugin extends ChartPlugin { constructor() { super({ + loadChart: () => import('./Geojson'), metadata, - loadChart: () => import('./Geojson.jsx'), transformProps, }); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Grid/Grid.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Grid/Grid.jsx index a0cc8613faff..370cb3dfdb1c 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Grid/Grid.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Grid/Grid.jsx @@ -20,7 +20,7 @@ import { GridLayer } from 'deck.gl'; import React from 'react'; import { t } from '@superset-ui/translation'; -import { commonLayerProps, getAggFunc } from '../common'; +import { commonLayerProps, getAggFunc } from '../common'; import sandboxedEval from '../../../../modules/sandbox'; import { createDeckGLComponent } from '../../factory'; import TooltipRow from '../../TooltipRow'; @@ -28,7 +28,10 @@ import TooltipRow from '../../TooltipRow'; function setTooltipContent(o) { return (
    - +
    ); @@ -49,6 +52,7 @@ export function getLayer(formData, payload, onAddFilter, setTooltip) { } const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight); + return new GridLayer({ id: `grid-layer-${fd.slice_id}`, data, diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Grid/index.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Grid/index.js index 291b967afcdb..33ca75260fe5 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Grid/index.js +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Grid/index.js @@ -22,17 +22,17 @@ import thumbnail from './images/thumbnail.png'; import transformProps from '../../transformProps'; const metadata = new ChartMetadata({ - name: t('deck.gl Grid'), - description: '', credits: ['https://uber.github.io/deck.gl'], + description: '', + name: t('deck.gl Grid'), thumbnail, }); export default class GridChartPlugin extends ChartPlugin { constructor() { super({ + loadChart: () => import('./Grid'), metadata, - loadChart: () => import('./Grid.jsx'), transformProps, }); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Hex/Hex.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Hex/Hex.jsx index 9901b22628bd..22dd39b6ff97 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Hex/Hex.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Hex/Hex.jsx @@ -20,7 +20,7 @@ import { HexagonLayer } from 'deck.gl'; import React from 'react'; import { t } from '@superset-ui/translation'; -import { commonLayerProps, getAggFunc } from '../common'; +import { commonLayerProps, getAggFunc } from '../common'; import sandboxedEval from '../../../../modules/sandbox'; import { createDeckGLComponent } from '../../factory'; import TooltipRow from '../../TooltipRow'; @@ -28,7 +28,10 @@ import TooltipRow from '../../TooltipRow'; function setTooltipContent(o) { return (
    - +
    ); @@ -48,6 +51,7 @@ export function getLayer(formData, payload, onAddFilter, setTooltip) { data = jsFnMutator(data); } const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight); + return new HexagonLayer({ id: `hex-layer-${fd.slice_id}`, data, diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Hex/index.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Hex/index.js index 940ae5bac0e7..03251a3f0bd6 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Hex/index.js +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Hex/index.js @@ -22,17 +22,17 @@ import thumbnail from './images/thumbnail.png'; import transformProps from '../../transformProps'; const metadata = new ChartMetadata({ - name: t('deck.gl 3D Hexagon'), - description: '', credits: ['https://uber.github.io/deck.gl'], + description: '', + name: t('deck.gl 3D Hexagon'), thumbnail, }); export default class HexChartPlugin extends ChartPlugin { constructor() { super({ + loadChart: () => import('./Hex'), metadata, - loadChart: () => import('./Hex.jsx'), transformProps, }); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Path/Path.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Path/Path.jsx index 7bf0982419e2..15794d4ed472 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Path/Path.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Path/Path.jsx @@ -25,14 +25,18 @@ import TooltipRow from '../../TooltipRow'; function setTooltipContent(o) { return ( - o.object.extraProps && -
    - { + o.object.extraProps && ( +
    + { Object.keys(o.object.extraProps).map((prop, index) => - , - ) - } -
    + + ))} +
    + ) ); } @@ -63,9 +67,10 @@ export function getLayer(formData, payload, onAddFilter, setTooltip) { function getPoints(data) { let points = []; - data.forEach((d) => { + data.forEach(d => { points = points.concat(d.path); }); + return points; } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Path/index.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Path/index.js index 5b584c530a7d..66e978e5d77d 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Path/index.js +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Path/index.js @@ -22,17 +22,17 @@ import thumbnail from './images/thumbnail.png'; import transformProps from '../../transformProps'; const metadata = new ChartMetadata({ - name: t('deck.gl Path'), - description: '', credits: ['https://uber.github.io/deck.gl'], + description: '', + name: t('deck.gl Path'), thumbnail, }); export default class PathChartPlugin extends ChartPlugin { constructor() { super({ + loadChart: () => import('./Path'), metadata, - loadChart: () => import('./Path.jsx'), transformProps, }); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Polygon/Polygon.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Polygon/Polygon.jsx index 891856d04302..dc62446efce6 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Polygon/Polygon.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Polygon/Polygon.jsx @@ -32,7 +32,7 @@ import { commonLayerProps, fitViewport } from '../common'; import { getPlaySliderParams } from '../../../../modules/time'; import sandboxedEval from '../../../../modules/sandbox'; -const DOUBLE_CLICK_TRESHOLD = 250; // milliseconds +const DOUBLE_CLICK_TRESHOLD = 250; // milliseconds function getPoints(features) { return features.map(d => d.polygon).flat(); @@ -44,18 +44,22 @@ function getElevation(d, colorScaler) { * effectively showing the map layer no matter what other polygons are * behind it. */ - return colorScaler(d)[3] === 0 - ? 0 - : d.elevation; + return colorScaler(d)[3] === 0 ? 0 : d.elevation; } function setTooltipContent(formData) { - return (o) => { + return o => { const metricLabel = formData.metric.label || formData.metric; + return (
    - - {formData.metric && } + + {formData.metric && ( + + )}
    ); }; @@ -68,7 +72,7 @@ export function getLayer(formData, payload, onAddFilter, setTooltip, selected, o let data = [...payload.data.features]; if (filters != null) { - filters.forEach((f) => { + filters.forEach(f => { data = data.filter(f); }); } @@ -82,21 +86,25 @@ export function getLayer(formData, payload, onAddFilter, setTooltip, selected, o const metricLabel = fd.metric ? fd.metric.label || fd.metric : null; const accessor = d => d[metricLabel]; // base color for the polygons - const baseColorScaler = fd.metric === null - ? () => [fc.r, fc.g, fc.b, 255 * fc.a] - : getBreakPointColorScaler(fd, data, accessor); + const baseColorScaler = + fd.metric === null + ? () => [fc.r, fc.g, fc.b, 255 * fc.a] + : getBreakPointColorScaler(fd, data, accessor); // when polygons are selected, reduce the opacity of non-selected polygons - const colorScaler = (d) => { + const colorScaler = d => { const baseColor = baseColorScaler(d); if (selected.length > 0 && selected.indexOf(d[fd.line_column]) === -1) { baseColor[3] /= 2; } + return baseColor; }; - const tooltipContentGenerator = (fd.line_column && fd.metric && ['geohash', 'zipcode'].indexOf(fd.line_type) >= 0) - ? setTooltipContent(fd) - : undefined; + const tooltipContentGenerator = + fd.line_column && fd.metric && ['geohash', 'zipcode'].indexOf(fd.line_type) >= 0 + ? setTooltipContent(fd) + : undefined; + return new PolygonLayer({ id: `path-layer-${fd.slice_id}`, data, @@ -140,6 +148,7 @@ class DeckGLPolygon extends React.Component { this.onValuesChange = this.onValuesChange.bind(this); this.onViewportChange = this.onViewportChange.bind(this); } + static getDerivedStateFromProps(props, state) { // the state is computed only from the payload; if it hasn't changed, do // not recompute state since this would reset selections and/or the play @@ -153,19 +162,10 @@ class DeckGLPolygon extends React.Component { // the granularity has to be read from the payload form_data, not the // props formData which comes from the instantaneous controls state - const granularity = ( - props.payload.form_data.time_grain_sqla || - props.payload.form_data.granularity || - 'P1D' - ); + const granularity = + props.payload.form_data.time_grain_sqla || props.payload.form_data.granularity || 'P1D'; - const { - start, - end, - getStep, - values, - disabled, - } = getPlaySliderParams(timestamps, granularity); + const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, granularity); const viewport = props.formData.autozoom ? fitViewport(props.viewport, getPoints(features)) @@ -183,11 +183,12 @@ class DeckGLPolygon extends React.Component { formData: props.payload.form_data, }; } + onSelect(polygon) { const { formData, onAddFilter } = this.props; const now = new Date(); - const doubleClick = (now - this.state.lastClick) <= DOUBLE_CLICK_TRESHOLD; + const doubleClick = now - this.state.lastClick <= DOUBLE_CLICK_TRESHOLD; // toggle selected polygons const selected = [...this.state.selected]; @@ -209,16 +210,17 @@ class DeckGLPolygon extends React.Component { onAddFilter(formData.line_column, selected, false, true); } } + onValuesChange(values) { this.setState({ - values: Array.isArray(values) - ? values - : [values, values + this.state.getStep(values)], + values: Array.isArray(values) ? values : [values, values + this.state.getStep(values)], }); } + onViewportChange(viewport) { this.setState({ viewport }); } + getLayers(values) { if (this.props.payload.data.features === undefined) { return []; @@ -240,10 +242,12 @@ class DeckGLPolygon extends React.Component { this.props.setTooltip, this.state.selected, this.onSelect, - filters); + filters, + ); return [layer]; } + render() { const { payload, formData, setControlValue } = this.props; const { start, end, getStep, values, disabled, viewport } = this.state; @@ -253,6 +257,7 @@ class DeckGLPolygon extends React.Component { const accessor = d => d[metricLabel]; const buckets = getBuckets(formData, payload.data.features, accessor); + return (
    - {formData.metric !== null && - } + {formData.metric !== null && ( + + )}
    ); diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Polygon/index.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Polygon/index.js index 4b5f7c051365..0106461c2570 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Polygon/index.js +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Polygon/index.js @@ -22,17 +22,17 @@ import thumbnail from './images/thumbnail.png'; import transformProps from '../../transformProps'; const metadata = new ChartMetadata({ - name: t('deck.gl Polygon'), - description: '', credits: ['https://uber.github.io/deck.gl'], + description: '', + name: t('deck.gl Polygon'), thumbnail, }); export default class PolygonChartPlugin extends ChartPlugin { constructor() { super({ + loadChart: () => import('./Polygon'), metadata, - loadChart: () => import('./Polygon.jsx'), transformProps, }); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Scatter/Scatter.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Scatter/Scatter.jsx index 42dd7c8df4a8..78e49f3fda14 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Scatter/Scatter.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Scatter/Scatter.jsx @@ -31,20 +31,26 @@ function getPoints(data) { function setTooltipContent(formData) { return o => (
    - - { - o.object.cat_color && - } - { - o.object.metric && - } + + {o.object.cat_color && ( + + )} + {o.object.metric && ( + + )}
    ); } export function getLayer(formData, payload, onAddFilter, setTooltip) { const fd = formData; - const dataWithRadius = payload.data.features.map((d) => { + const dataWithRadius = payload.data.features.map(d => { let radius = unitToRadius(fd.point_unit, d.radius) || 10; if (fd.multiplier) { radius *= fd.multiplier; @@ -54,6 +60,7 @@ export function getLayer(formData, payload, onAddFilter, setTooltip) { } const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 }; const color = [c.r, c.g, c.b, c.a * 255]; + return { ...d, radius, color }; }); diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Scatter/index.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Scatter/index.js index 093a751d5e9d..b46273f67ef1 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Scatter/index.js +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Scatter/index.js @@ -22,17 +22,17 @@ import thumbnail from './images/thumbnail.png'; import transformProps from '../../transformProps'; const metadata = new ChartMetadata({ - name: t('deck.gl Scatterplot'), - description: '', credits: ['https://uber.github.io/deck.gl'], + description: '', + name: t('deck.gl Scatterplot'), thumbnail, }); export default class ScatterChartPlugin extends ChartPlugin { constructor() { super({ + loadChart: () => import('./Scatter'), metadata, - loadChart: () => import('./Scatter.jsx'), transformProps, }); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Screengrid/Screengrid.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Screengrid/Screengrid.jsx index d9fba4c70439..8c738a30e33a 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Screengrid/Screengrid.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/Screengrid/Screengrid.jsx @@ -35,7 +35,10 @@ function getPoints(data) { function setTooltipContent(o) { return (
    - +
    ); @@ -56,7 +59,7 @@ export function getLayer(formData, payload, onAddFilter, setTooltip, selected, o } if (filters != null) { - filters.forEach((f) => { + filters.forEach(f => { data = data.filter(f); }); } @@ -99,6 +102,7 @@ class DeckGLScreenGrid extends React.PureComponent { this.onValuesChange = this.onValuesChange.bind(this); this.onViewportChange = this.onViewportChange.bind(this); } + static getDerivedStateFromProps(props, state) { // the state is computed only from the payload; if it hasn't changed, do // not recompute state since this would reset selections and/or the play @@ -112,19 +116,10 @@ class DeckGLScreenGrid extends React.PureComponent { // the granularity has to be read from the payload form_data, not the // props formData which comes from the instantaneous controls state - const granularity = ( - props.payload.form_data.time_grain_sqla || - props.payload.form_data.granularity || - 'P1D' - ); + const granularity = + props.payload.form_data.time_grain_sqla || props.payload.form_data.granularity || 'P1D'; - const { - start, - end, - getStep, - values, - disabled, - } = getPlaySliderParams(timestamps, granularity); + const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, granularity); const viewport = props.formData.autozoom ? fitViewport(props.viewport, getPoints(features)) @@ -142,16 +137,17 @@ class DeckGLScreenGrid extends React.PureComponent { formData: props.payload.form_data, }; } + onValuesChange(values) { this.setState({ - values: Array.isArray(values) - ? values - : [values, values + this.state.getStep(values)], + values: Array.isArray(values) ? values : [values, values + this.state.getStep(values)], }); } + onViewportChange(viewport) { this.setState({ viewport }); } + getLayers(values) { const filters = []; @@ -167,13 +163,15 @@ class DeckGLScreenGrid extends React.PureComponent { this.props.payload, this.props.onAddFilter, this.props.setTooltip, - filters); + filters, + ); return [layer]; } render() { const { formData, payload, setControlValue } = this.props; + return (
    import('./Screengrid'), metadata, - loadChart: () => import('./Screengrid.jsx'), transformProps, }); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/common.jsx b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/common.jsx index aaee55361500..b23be4ba12f9 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/common.jsx +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/layers/common.jsx @@ -22,10 +22,10 @@ import sandboxedEval from '../../../modules/sandbox'; const PADDING = 0.25; const GEO_BOUNDS = { - LAT_MIN: -90, LAT_MAX: 90, - LNG_MIN: -180, + LAT_MIN: -90, LNG_MAX: 180, + LNG_MIN: -180, }; /** @@ -33,12 +33,11 @@ const GEO_BOUNDS = { * @param latExt Latitude range */ function getLatBoundsForSingleCoordinate(latExt) { - const latMin = latExt[0] - PADDING < GEO_BOUNDS.LAT_MIN - ? GEO_BOUNDS.LAT_MIN - : latExt[0] - PADDING; - const latMax = latExt[1] + PADDING > GEO_BOUNDS.LAT_MAX - ? GEO_BOUNDS.LAT_MAX - : latExt[1] + PADDING; + const latMin = + latExt[0] - PADDING < GEO_BOUNDS.LAT_MIN ? GEO_BOUNDS.LAT_MIN : latExt[0] - PADDING; + const latMax = + latExt[1] + PADDING > GEO_BOUNDS.LAT_MAX ? GEO_BOUNDS.LAT_MAX : latExt[1] + PADDING; + return [latMin, latMax]; } @@ -47,12 +46,11 @@ function getLatBoundsForSingleCoordinate(latExt) { * @param lngExt Longitude range */ function getLngBoundsForSingleCoordinate(lngExt) { - const lngMin = lngExt[0] - PADDING < GEO_BOUNDS.LNG_MIN - ? GEO_BOUNDS.LNG_MIN - : lngExt[0] - PADDING; - const lngMax = lngExt[1] + PADDING > GEO_BOUNDS.LNG_MAX - ? GEO_BOUNDS.LNG_MAX - : lngExt[1] + PADDING; + const lngMin = + lngExt[0] - PADDING < GEO_BOUNDS.LNG_MIN ? GEO_BOUNDS.LNG_MIN : lngExt[0] - PADDING; + const lngMax = + lngExt[1] + PADDING > GEO_BOUNDS.LNG_MAX ? GEO_BOUNDS.LNG_MAX : lngExt[1] + PADDING; + return [lngMin, lngMax]; } @@ -61,15 +59,14 @@ export function getBounds(points) { const lngExt = d3array.extent(points, d => d[0]); const latBounds = latExt[0] === latExt[1] ? getLatBoundsForSingleCoordinate(latExt) : latExt; const lngBounds = lngExt[0] === lngExt[1] ? getLngBoundsForSingleCoordinate(lngExt) : lngExt; - return [ - [lngBounds[0], latBounds[0]], - [lngBounds[1], latBounds[1]], - ]; + + return [[lngBounds[0], latBounds[0]], [lngBounds[1], latBounds[1]]]; } export function fitViewport(viewport, points, padding = 10) { try { const bounds = getBounds(points); + return { ...viewport, ...fitBounds({ @@ -82,6 +79,7 @@ export function fitViewport(viewport, points, padding = 10) { } catch (e) { /* eslint no-console: 0 */ console.error('Could not auto zoom', e); + return viewport; } } @@ -94,7 +92,7 @@ export function commonLayerProps(formData, setTooltip, setTooltipContent, onSele tooltipContentGenerator = sandboxedEval(fd.js_tooltip); } if (tooltipContentGenerator) { - onHover = (o) => { + onHover = o => { if (o.picked) { setTooltip({ content: tooltipContentGenerator(o), @@ -108,13 +106,14 @@ export function commonLayerProps(formData, setTooltip, setTooltipContent, onSele } let onClick; if (fd.js_onclick_href) { - onClick = (o) => { + onClick = o => { const href = sandboxedEval(fd.js_onclick_href)(o); window.open(href); }; } else if (fd.table_filter && onSelect !== undefined) { onClick = o => onSelect(o.object[fd.line_column]); } + return { onClick, onHover, @@ -143,6 +142,7 @@ export function getAggFunc(type = 'sum', accessor = null) { } else { sortedArr = arr.sort(d3array.ascending); } + return d3array.quantile(sortedArr, percentiles[type], acc); }; } else { @@ -151,5 +151,6 @@ export function getAggFunc(type = 'sum', accessor = null) { if (!accessor) { return arr => d3func(arr); } + return arr => d3func(arr.map(accessor)); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/transformProps.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/transformProps.js index 9e7350baed17..02b821a0d72a 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/transformProps.js +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/transformProps.js @@ -19,13 +19,7 @@ const NOOP = () => {}; export default function transformProps(chartProps) { - const { - width, - height, - rawFormData, - queryData, - hooks, - } = chartProps; + const { width, height, rawFormData, queryData, hooks } = chartProps; const { onAddFilter = NOOP, setControlValue = NOOP, setTooltip = NOOP } = hooks; return { diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils.js index 246e31b46c71..cc5487c55523 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils.js +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils.js @@ -19,7 +19,7 @@ import { extent } from 'd3-array'; import { scaleThreshold } from 'd3-scale'; import { getSequentialSchemeRegistry, SequentialScheme } from '@superset-ui/color'; -import { hexToRGB } from '../../modules/colors'; +import { hexToRGB } from './utils/colors'; const DEFAULT_NUM_BUCKETS = 10; @@ -42,12 +42,12 @@ export function getBreakPoints( const precision = delta === 0 ? 0 : Math.max(0, Math.ceil(Math.log10(1 / delta))); const extraBucket = maxValue > maxValue.toFixed(precision) ? 1 : 0; -return Array(numBuckets + 1 + extraBucket) + return Array(numBuckets + 1 + extraBucket) .fill() .map((_, i) => (minValue + i * delta).toFixed(precision)); } -return formDataBreakPoints.sort((a, b) => parseFloat(a) - parseFloat(b)); + return formDataBreakPoints.sort((a, b) => parseFloat(a) - parseFloat(b)); } export function getBreakPointColorScaler( @@ -112,7 +112,7 @@ export function getBreakPointColorScaler( c[3] = (opacity / 100.0) * 255; } -return c; + return c; }; } @@ -121,7 +121,7 @@ export function getBuckets(fd, features, accessor) { const colorScaler = getBreakPointColorScaler(fd, features, accessor); const buckets = {}; breakPoints.slice(1).forEach((value, i) => { - const range = `${breakPoints[i] } - ${ breakPoints[i + 1]}`; + const range = `${breakPoints[i]} - ${breakPoints[i + 1]}`; const mid = 0.5 * (parseFloat(breakPoints[i]) + parseFloat(breakPoints[i + 1])); // fix polygon doesn't show const metricLabel = fd.metric ? fd.metric.label || fd.metric : null; @@ -131,5 +131,5 @@ export function getBuckets(fd, features, accessor) { }; }); -return buckets; + return buckets; } diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/colors.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/colors.js new file mode 100644 index 000000000000..6b0f72af70ea --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/colors.js @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { rgb } from 'd3-color'; + +export function hexToRGB(hex, alpha = 255) { + if (!hex) { + return [0, 0, 0, alpha]; + } + const { r, g, b } = rgb(hex); + return [r, g, b, alpha]; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/sandbox.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/sandbox.js new file mode 100644 index 000000000000..7c2e77b0536b --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/sandbox.js @@ -0,0 +1,51 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +// A safe alternative to JS's eval +import vm from 'vm'; +import _ from 'underscore'; +import * as d3array from 'd3-array'; +import * as colors from './colors'; + +// Objects exposed here should be treated like a public API +// if `underscore` had backwards incompatible changes in a future release, we'd +// have to be careful about bumping the library as those changes could break user charts +const GLOBAL_CONTEXT = { + console, + _, + colors, + d3array, +}; + +// Copied/modified from https://github.com/hacksparrow/safe-eval/blob/master/index.js +export default function sandboxedEval(code, context, opts) { + const sandbox = {}; + const resultKey = 'SAFE_EVAL_' + Math.floor(Math.random() * 1000000); + sandbox[resultKey] = {}; + const codeToEval = resultKey + '=' + code; + const sandboxContext = { ...GLOBAL_CONTEXT, ...context }; + Object.keys(sandboxContext).forEach(function (key) { + sandbox[key] = sandboxContext[key]; + }); + try { + vm.runInNewContext(codeToEval, sandbox, opts); + return sandbox[resultKey]; + } catch (error) { + return () => error; + } +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/time.js b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/time.js new file mode 100644 index 000000000000..3d4466f3cfe3 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui-plugins-deckgl/packages/superset-ui-preset-chart-deckgl/src/utils/time.js @@ -0,0 +1,124 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import moment from 'moment'; + + +// array with the minimum values of each part of a timestamp -- note that +// months are zero-indexed in Javascript +const truncatePartTo = [ + 1, // year + 0, // month + 1, // day + 0, // hour + 0, // minute + 0, // second + 0, // millisecond +]; + + +export function truncate(timestamp, step) { + /* + * Truncate timestamp down to duration resolution. + */ + const lowerBound = moment(timestamp).subtract(step); + const explodedTimestamp = timestamp.toArray(); + const explodedLowerBound = lowerBound.toArray(); + + const firstDiffIndex = explodedTimestamp + .map((part, i) => (explodedLowerBound[i] !== part)) + .indexOf(true); + const dateParts = explodedTimestamp.map((part, i) => { + if (i === firstDiffIndex) { + // truncate down to closest `truncatePartTo[i] + n * step` + const difference = part - explodedLowerBound[i]; + return part - ((part - truncatePartTo[i]) % difference); + } else if (i < firstDiffIndex || firstDiffIndex === -1) { + return part; + } + return truncatePartTo[i]; + }); + + return moment(dateParts); +} + +function getStepSeconds(step, start) { + /* Return number of seconds in a step. + * + * The step might be ambigous, eg, "1 month" has a variable number of + * seconds, which is why we need to know the start time. + */ + const startMillliseconds = parseInt(moment(start).format('x'), 10); + const endMilliseconds = parseInt(moment(start).add(step).format('x'), 10); + return endMilliseconds - startMillliseconds; +} + +export const getPlaySliderParams = function (timestamps, timeGrain) { + const minTimestamp = moment(Math.min(...timestamps)); + const maxTimestamp = moment(Math.max(...timestamps)); + let step; + let reference; + + if (timeGrain.indexOf('/') !== -1) { + // Here, time grain is a time interval instead of a simple duration, either + // `reference/duration` or `duration/reference`. We need to parse the + // duration and make sure that start and end are in the right places. For + // example, if `reference` is a Saturday and `duration` is 1 week (P1W) + // then both start and end should be Saturdays. + const parts = timeGrain.split('/', 2); + if (parts[0].endsWith('Z')) { // ISO string + reference = moment(parts[0]); + step = moment.duration(parts[1]); + } else { + reference = moment(parts[1]); + step = moment.duration(parts[0]); + } + } else { + step = moment.duration(timeGrain); + reference = truncate(minTimestamp, step); + } + + // find the largest `reference + n * step` smaller than the minimum timestamp + const start = moment(reference); + while (start < minTimestamp) { + start.add(step); + } + while (start > minTimestamp) { + start.subtract(step); + } + + // find the smallest `reference + n * step` larger than the maximum timestamp + const end = moment(reference); + while (end > maxTimestamp) { + end.subtract(step); + } + while (end < maxTimestamp) { + end.add(step); + } + + const values = timeGrain != null ? [start, moment(start).add(step)] : [start, end]; + const disabled = timestamps.every(timestamp => timestamp === null); + + return { + start: parseInt(start.format('x'), 10), + end: parseInt(end.format('x'), 10), + getStep: getStepSeconds.bind(this, step), + values: values.map(v => parseInt(v.format('x'), 10)), + disabled, + }; +};