feat: add deckgl files

This commit is contained in:
Krist Wongsuphasawat 2019-09-18 16:20:24 -07:00 committed by Yongjie Zhao
parent 3b7cec5e06
commit ad74e60e99
53 changed files with 2665 additions and 144 deletions

View File

@ -1,7 +1,7 @@
{
"name": "@superset-ui/plugins-monorepo",
"name": "@superset-ui/plugins-deckgl-monorepo",
"version": "0.0.0-master",
"description": "Superset UI Plugins",
"description": "Superset UI Plugins - deck.gl",
"private": true,
"scripts": {
"build": "yarn build:cjs && yarn build:esm && yarn run type:dts && yarn build:assets",
@ -24,7 +24,7 @@
"test": "yarn run type && yarn run jest",
"test:watch": "yarn run lint:fix && beemo create-config jest --react && jest --watch"
},
"repository": "https://github.com/apache-superset/superset-ui-plugins.git",
"repository": "https://github.com/apache-superset/superset-ui-plugins-deckgl.git",
"keywords": [
"apache",
"superset",

View File

@ -1,33 +0,0 @@
/* eslint-disable sort-keys, no-magic-numbers */
import React from 'react';
import { SuperChart } from '@superset-ui/chart';
import data from './data';
import dummyDatasource from '../../shared/dummyDatasource';
export default [
{
renderStory: () => (
<SuperChart
chartType="calendar"
width={400}
height={400}
datasource={dummyDatasource}
queryData={{ data }}
formData={{
cellSize: 10,
cellPadding: 2,
cellRadius: 0,
linearColorScheme: 'schemeRdYlBu',
steps: 10,
yAxisFormat: '.3s',
xAxisTimeFormat: 'smart_date',
showLegend: true,
showValues: false,
showMetricName: true,
}}
/>
),
storyName: 'Basic',
storyPath: 'legacy-|plugin-chart-calendar|CalendarChartPlugin',
},
];

View File

@ -1,100 +0,0 @@
/* eslint-disable sort-keys */
export default {
data: {
count: {
'1518652800.0': 3,
'1518048000.0': 2,
'1518220800.0': 2,
'1523145600.0': 2,
'1529798400.0': 2,
'1534204800.0': 2,
'1541289600.0': 2,
'1542672000.0': 2,
'1543881600.0': 2,
'1517616000.0': 1,
'1517875200.0': 1,
'1517961600.0': 1,
'1518307200.0': 1,
'1518393600.0': 1,
'1519257600.0': 1,
'1519516800.0': 1,
'1519776000.0': 1,
'1520208000.0': 1,
'1520294400.0': 1,
'1520985600.0': 1,
'1521072000.0': 1,
'1521244800.0': 1,
'1521331200.0': 1,
'1521676800.0': 1,
'1522108800.0': 1,
'1522627200.0': 1,
'1522800000.0': 1,
'1522972800.0': 1,
'1523491200.0': 1,
'1524096000.0': 1,
'1524268800.0': 1,
'1524614400.0': 1,
'1524960000.0': 1,
'1525305600.0': 1,
'1525564800.0': 1,
'1525737600.0': 1,
'1525824000.0': 1,
'1525910400.0': 1,
'1526083200.0': 1,
'1526256000.0': 1,
'1526688000.0': 1,
'1527033600.0': 1,
'1527292800.0': 1,
'1527465600.0': 1,
'1527638400.0': 1,
'1528070400.0': 1,
'1528329600.0': 1,
'1529539200.0': 1,
'1529625600.0': 1,
'1529712000.0': 1,
'1529971200.0': 1,
'1530144000.0': 1,
'1530576000.0': 1,
'1531267200.0': 1,
'1531353600.0': 1,
'1531440000.0': 1,
'1532736000.0': 1,
'1533081600.0': 1,
'1533168000.0': 1,
'1533945600.0': 1,
'1534377600.0': 1,
'1534809600.0': 1,
'1535155200.0': 1,
'1535328000.0': 1,
'1535932800.0': 1,
'1536710400.0': 1,
'1537056000.0': 1,
'1537142400.0': 1,
'1537488000.0': 1,
'1537660800.0': 1,
'1538611200.0': 1,
'1538697600.0': 1,
'1539475200.0': 1,
'1540771200.0': 1,
'1541116800.0': 1,
'1541376000.0': 1,
'1541635200.0': 1,
'1542153600.0': 1,
'1542931200.0': 1,
'1543190400.0': 1,
'1545177600.0': 1,
'1545436800.0': 1,
'1545782400.0': 1,
'1545868800.0': 1,
'1546300800.0': 1,
'1546732800.0': 1,
'1547769600.0': 1,
'1547942400.0': 1,
'1548633600.0': 1,
},
},
start: 1517270400000.0,
domain: 'month',
range: 13,
subdomain: 'day',
};

View File

@ -1,8 +0,0 @@
import CalendarChartPlugin from '../../../../superset-ui-legacy-plugin-chart-calendar';
import Stories from './Stories';
new CalendarChartPlugin().configure({ key: 'calendar' }).register();
export default {
examples: [...Stories],
};

View File

@ -0,0 +1,43 @@
## @superset-ui/legacy-preset-chart-nvd3
[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-preset-chart-nvd3.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/legacy-preset-chart-nvd3.svg?style=flat-square)
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui-plugins.svg?path=packages%2Fsuperset-ui-legacy-preset-chart-nvd3&style=flat-square)](https://david-dm.org/apache-superset/superset-ui-plugins?path=packages/superset-ui-legacy-preset-chart-nvd3)
This plugin provides Big Number for Superset.
### Usage
Import the preset and register. This will register all the chart plugins under nvd3.
```js
import { NVD3ChartPreset } from '@superset-ui/legacy-preset-chart-nvd3';
new NVD3ChartPreset().register();
```
or register charts one by one. Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to lookup this chart throughout the app.
```js
import { AreaChartPlugin, LineChartPlugin } from '@superset-ui/legacy-preset-chart-nvd3';
new AreaChartPlugin()
.configure({ key: 'area' })
.register();
new LineChartPlugin()
.configure({ key: 'line' })
.register();
```
Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui-plugins/?selectedKind=plugin-chart-nvd3) for more details.
```js
<SuperChart
chartType="line"
width={600}
height={600}
formData={...}
queryData={{
data: {...},
}}
/>
```

View File

@ -0,0 +1,53 @@
{
"name": "@superset-ui/legacy-preset-chart-deckgl",
"version": "0.11.0",
"description": "Superset Legacy Chart - deck.gl",
"sideEffects": [
"*.css"
],
"main": "lib/index.js",
"module": "esm/index.js",
"files": [
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/apache-superset/superset-ui-plugins-deckgl.git"
},
"keywords": [
"superset"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache-superset/superset-ui-plugins-deckgl/issues"
},
"homepage": "https://github.com/apache-superset/superset-ui-plugins-deckgl#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@data-ui/xy-chart": "^0.0.80",
"d3": "^3.5.17",
"d3-tip": "^0.9.1",
"dompurify": "^1.0.3",
"fast-safe-stringify": "^2.0.6",
"lodash": "^4.17.11",
"mathjs": "^3.20.2",
"moment": "^2.20.1",
"nvd3": "1.8.6",
"prop-types": "^15.6.2",
"urijs": "^1.18.10"
},
"peerDependencies": {
"@superset-ui/chart": "^0.12.0",
"@superset-ui/color": "^0.12.0",
"@superset-ui/core": "^0.12.0",
"@superset-ui/dimension": "^0.12.0",
"@superset-ui/number-format": "^0.12.0",
"@superset-ui/time-format": "^0.12.0",
"@superset-ui/translation": "^0.12.0",
"react": "^15 || ^16"
}
}

View File

@ -0,0 +1,115 @@
/**
* 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 DeckGLContainer from './DeckGLContainer';
import PlaySlider from '../PlaySlider';
const PLAYSLIDER_HEIGHT = 20; // px
const propTypes = {
getLayers: PropTypes.func.isRequired,
start: PropTypes.number.isRequired,
end: PropTypes.number.isRequired,
getStep: PropTypes.func,
values: PropTypes.array.isRequired,
aggregation: PropTypes.bool,
disabled: PropTypes.bool,
viewport: PropTypes.object.isRequired,
children: PropTypes.node,
mapStyle: PropTypes.string,
mapboxApiAccessToken: PropTypes.string.isRequired,
setControlValue: PropTypes.func,
onViewportChange: PropTypes.func,
onValuesChange: PropTypes.func,
};
const defaultProps = {
aggregation: false,
disabled: false,
mapStyle: 'light',
setControlValue: () => {},
onViewportChange: () => {},
onValuesChange: () => {},
};
export default class AnimatableDeckGLContainer extends React.Component {
constructor(props) {
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,
end,
getStep,
disabled,
aggregation,
children,
getLayers,
values,
onValuesChange,
viewport,
setControlValue,
mapStyle,
mapboxApiAccessToken,
} = this.props;
const layers = getLayers(values);
// leave space for the play slider
const modifiedViewport = {
...viewport,
height: disabled ? viewport.height : viewport.height - PLAYSLIDER_HEIGHT,
};
return (
<div>
<DeckGLContainer
viewport={modifiedViewport}
layers={layers}
setControlValue={setControlValue}
mapStyle={mapStyle}
mapboxApiAccessToken={mapboxApiAccessToken}
onViewportChange={this.onViewportChange}
/>
{!disabled &&
<PlaySlider
start={start}
end={end}
step={getStep(start)}
values={values}
range={!aggregation}
onChange={onValuesChange}
/>
}
{children}
</div>
);
}
}
AnimatableDeckGLContainer.propTypes = propTypes;
AnimatableDeckGLContainer.defaultProps = defaultProps;

View File

@ -0,0 +1,248 @@
/**
* 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.
*/
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
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 { fitViewport } from './layers/common';
const { getScale } = CategoricalColorNamespace;
function getCategories(fd, data) {
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 colorFn = getScale(fd.color_scheme);
const categories = {};
data.forEach((d) => {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
let color;
if (fd.dimension) {
color = hexToRGB(colorFn(d.cat_color), c.a * 255);
} else {
color = fixedColor;
}
categories[d.cat_color] = { color, enabled: true };
}
});
return categories;
}
const propTypes = {
formData: PropTypes.object.isRequired,
mapboxApiKey: PropTypes.string.isRequired,
setControlValue: PropTypes.func.isRequired,
viewport: PropTypes.object.isRequired,
getLayer: PropTypes.func.isRequired,
getPoints: PropTypes.func.isRequired,
payload: PropTypes.object.isRequired,
onAddFilter: PropTypes.func,
setTooltip: PropTypes.func,
};
export default class CategoricalDeckGLContainer extends React.PureComponent {
/*
* A Deck.gl container that handles categories.
*
* The container will have an interactive legend, populated from the
* categories present in the data.
*/
constructor(props) {
super(props);
this.state = this.getStateFromProps(props);
this.getLayers = this.getLayers.bind(this);
this.onValuesChange = this.onValuesChange.bind(this);
this.onViewportChange = this.onViewportChange.bind(this);
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)],
});
}
onViewportChange(viewport) {
this.setState({ viewport });
}
getStateFromProps(props, state) {
const features = props.payload.data.features || [];
const timestamps = features.map(f => f.__timestamp);
const categories = getCategories(props.formData, features);
// 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
// slider position due to changes in form controls
if (state && props.payload.form_data === state.formData) {
return { ...state, categories };
}
// 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 {
start,
end,
getStep,
values,
disabled,
} = getPlaySliderParams(timestamps, granularity);
const viewport = props.formData.autozoom
? fitViewport(props.viewport, props.getPoints(features))
: props.viewport;
return {
start,
end,
getStep,
values,
disabled,
viewport,
selected: [],
lastClick: 0,
formData: props.payload.form_data,
categories,
};
}
getLayers(values) {
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);
// Apply user defined data mutator if defined
if (fd.js_data_mutator) {
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
features = jsFnMutator(features);
}
// Filter by time
if (values[0] === values[1] || values[1] === this.end) {
features = features.filter(d => d.__timestamp >= values[0] && d.__timestamp <= values[1]);
} else {
features = features.filter(d => d.__timestamp >= values[0] && d.__timestamp < values[1]);
}
// Show only categories selected in the legend
const cats = this.state.categories;
if (fd.dimension) {
features = features.filter(d => cats[d.cat_color] && cats[d.cat_color].enabled);
}
const filteredPayload = {
...payload,
data: { ...payload.data, features },
};
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) => {
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 = {
...this.state.categories,
[category]: {
...categoryState,
enabled: !categoryState.enabled,
},
};
// 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; });
}
this.setState({ categories });
}
showSingleCategory(category) {
const categories = { ...this.state.categories };
/* eslint-disable no-param-reassign */
Object.values(categories).forEach((v) => { v.enabled = false; });
categories[category].enabled = true;
this.setState({ categories });
}
render() {
return (
<div style={{ position: 'relative' }}>
<AnimatableDeckGLContainer
getLayers={this.getLayers}
start={this.state.start}
end={this.state.end}
getStep={this.state.getStep}
values={this.state.values}
onValuesChange={this.onValuesChange}
disabled={this.state.disabled}
viewport={this.state.viewport}
onViewportChange={this.onViewportChange}
mapboxApiAccessToken={this.props.mapboxApiKey}
mapStyle={this.props.formData.mapbox_style}
setControlValue={this.props.setControlValue}
>
<Legend
categories={this.state.categories}
toggleCategory={this.toggleCategory}
showSingleCategory={this.showSingleCategory}
position={this.props.formData.legend_position}
format={this.props.formData.legend_format}
/>
</AnimatableDeckGLContainer>
</div>
);
}
}
CategoricalDeckGLContainer.propTypes = propTypes;

View File

@ -0,0 +1,115 @@
/**
* 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 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';
const TICK = 2000; // milliseconds
const propTypes = {
viewport: PropTypes.object.isRequired,
layers: PropTypes.array.isRequired,
setControlValue: PropTypes.func,
mapStyle: PropTypes.string,
mapboxApiAccessToken: PropTypes.string.isRequired,
onViewportChange: PropTypes.func,
};
const defaultProps = {
mapStyle: 'light',
onViewportChange: () => {},
setControlValue: () => {},
};
export default class DeckGLContainer extends React.Component {
constructor(props) {
super(props);
this.tick = this.tick.bind(this);
this.onViewportChange = this.onViewportChange.bind(this);
// This has to be placed after this.tick is bound to this
this.state = {
previousViewport: props.viewport,
timer: setInterval(this.tick, TICK),
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.viewport !== prevState.viewport) {
return {
viewport: { ...nextProps.viewport },
previousViewport: prevState.viewport,
};
}
return null;
}
componentWillUnmount() {
clearInterval(this.state.timer);
}
onViewportChange(viewport) {
const vp = Object.assign({}, viewport);
// delete vp.width;
// delete vp.height;
const newVp = { ...this.state.previousViewport, ...vp };
// 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
// changes id at every change
if (this.state && !isEqual(this.state.previousViewport, this.props.viewport)) {
const setCV = this.props.setControlValue;
const vp = this.props.viewport;
if (setCV) {
setCV('viewport', vp);
}
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;
}
render() {
const { viewport } = this.props;
return (
<MapGL
{...viewport}
mapStyle={this.props.mapStyle}
onViewportChange={this.onViewportChange}
mapboxApiAccessToken={this.props.mapboxApiAccessToken}
>
<DeckGL
{...viewport}
layers={this.layers()}
initWebGLParameters
/>
</MapGL>
);
}
}
DeckGLContainer.propTypes = propTypes;
DeckGLContainer.defaultProps = defaultProps;

View File

@ -0,0 +1,131 @@
/**
* 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 _ from 'lodash';
import PropTypes from 'prop-types';
import { SupersetClient } from '@superset-ui/connection';
import DeckGLContainer from '../DeckGLContainer';
import { getExploreLongUrl } from '../../../explore/exploreUtils';
import layerGenerators from '../layers';
const propTypes = {
formData: PropTypes.object.isRequired,
payload: PropTypes.object.isRequired,
setControlValue: PropTypes.func.isRequired,
viewport: PropTypes.object.isRequired,
onAddFilter: PropTypes.func,
setTooltip: PropTypes.func,
onSelect: PropTypes.func,
};
const defaultProps = {
onAddFilter() {},
setTooltip() {},
onSelect() {},
};
class DeckMulti extends React.PureComponent {
constructor(props) {
super(props);
this.state = { subSlicesLayers: {} };
this.onViewportChange = this.onViewportChange.bind(this);
}
componentDidMount() {
const { formData, payload } = this.props;
this.loadLayers(formData, payload);
}
UNSAFE_componentWillReceiveProps(nextProps) {
const { formData, payload } = nextProps;
const hasChanges = !_.isEqual(this.props.formData.deck_slices, nextProps.formData.deck_slices);
if (hasChanges) {
this.loadLayers(formData, payload);
}
}
onViewportChange(viewport) {
this.setState({ viewport });
}
loadLayers(formData, payload, viewport) {
this.setState({ subSlicesLayers: {}, viewport });
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
const filters = [
...(subslice.form_data.filters || []),
...(formData.filters || []),
...(formData.extra_filters || []),
];
const subsliceCopy = {
...subslice,
form_data: {
...subslice.form_data,
filters,
},
};
SupersetClient.get({
endpoint: getExploreLongUrl(subsliceCopy.form_data, 'json'),
})
.then(({ json }) => {
const layer = layerGenerators[subsliceCopy.form_data.viz_type](
subsliceCopy.form_data,
json,
this.props.onAddFilter,
this.props.setTooltip,
[],
this.props.onSelect,
);
this.setState({
subSlicesLayers: {
...this.state.subSlicesLayers,
[subsliceCopy.slice_id]: layer,
},
});
})
.catch(() => {});
});
}
render() {
const { payload, formData, setControlValue } = this.props;
const { subSlicesLayers } = this.state;
const layers = Object.values(subSlicesLayers);
return (
<DeckGLContainer
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={this.state.viewport || this.props.viewport}
onViewportChange={this.onViewportChange}
layers={layers}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
/>
);
}
}
DeckMulti.propTypes = propTypes;
DeckMulti.defaultProps = defaultProps;
export default DeckMulti;

View File

@ -0,0 +1,39 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
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'],
thumbnail,
});
export default class MultiChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Multi.jsx'),
transformProps,
});
}
}

View File

@ -0,0 +1,36 @@
/**
* 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';
const propTypes = {
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
};
export default class TooltipRow extends React.PureComponent {
render() {
return (
<div>{this.props.label}<strong>{this.props.value}</strong></div>
);
}
}
TooltipRow.propTypes = propTypes;

View File

@ -0,0 +1,134 @@
/**
* 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 { isEqual } from 'lodash';
import DeckGLContainer from './DeckGLContainer';
import CategoricalDeckGLContainer from './CategoricalDeckGLContainer';
import { fitViewport } from './layers/common';
const propTypes = {
formData: PropTypes.object.isRequired,
payload: PropTypes.object.isRequired,
setControlValue: PropTypes.func.isRequired,
viewport: PropTypes.object.isRequired,
onAddFilter: PropTypes.func,
setTooltip: PropTypes.func,
};
const defaultProps = {
onAddFilter() {},
setTooltip() {},
};
export function createDeckGLComponent(getLayer, getPoints) {
// Higher order component
class Component extends React.PureComponent {
constructor(props) {
super(props);
const originalViewport = props.viewport;
const viewport = props.formData.autozoom
? fitViewport(originalViewport, getPoints(props.payload.data.features))
: originalViewport;
this.state = {
viewport,
layer: this.computeLayer(props),
};
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
) {
this.setState({ layer: this.computeLayer(nextProps) });
}
}
onViewportChange(viewport) {
this.setState({ viewport });
}
computeLayer(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;
return (
<DeckGLContainer
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={[layer]}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
onViewportChange={this.onViewportChange}
/>);
}
}
Component.propTypes = propTypes;
Component.defaultProps = defaultProps;
return Component;
}
export function createCategoricalDeckGLComponent(getLayer, getPoints) {
function Component(props) {
const {
formData,
payload,
setControlValue,
onAddFilter,
setTooltip,
viewport,
} = props;
return (
<CategoricalDeckGLContainer
formData={formData}
mapboxApiKey={payload.data.mapboxApiKey}
setControlValue={setControlValue}
viewport={viewport}
getLayer={getLayer}
payload={payload}
onAddFilter={onAddFilter}
setTooltip={setTooltip}
getPoints={getPoints}
/>
);
}
Component.propTypes = propTypes;
Component.defaultProps = defaultProps;
return Component;
}

View File

@ -0,0 +1,10 @@
export { default as DeckGLChartPreset } from './preset';
export { default as ArcChartPlugin } from './layers/Arc';
export { default as GeoJsonChartPlugin } from './layers/Geojson';
export { default as GridChartPlugin } from './layers/Grid';
export { default as HexChartPlugin } from './layers/Hex';
export { default as MultiChartPlugin } from './Multi';
export { default as PathChartPlugin } from './layers/Path';
export { default as PolygonChartPlugin } from './layers/Polygon';
export { default as ScatterChartPlugin } from './layers/Scatter';
export { default as ScreengridChartPlugin } from './layers/Screengrid';

View File

@ -0,0 +1,61 @@
/**
* 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 { ArcLayer } from 'deck.gl';
import React from 'react';
import { t } from '@superset-ui/translation';
import { commonLayerProps } from '../common';
import { createCategoricalDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
function getPoints(data) {
const points = [];
data.forEach((d) => {
points.push(d.sourcePosition);
points.push(d.targetPosition);
});
return points;
}
function setTooltipContent(formData) {
return o => (
<div className="deckgl-tooltip">
<TooltipRow label={`${t('Start (Longitude, Latitude)')}: `} value={`${o.object.sourcePosition[0]}, ${o.object.sourcePosition[1]}`} />
<TooltipRow label={`${t('End (Longitude, Latitude)')}: `} value={`${o.object.targetPosition[0]}, ${o.object.targetPosition[1]}`} />
{
formData.dimension && <TooltipRow label={`${formData.dimension}: `} value={`${o.object.cat_color}`} />
}
</div>
);
}
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,
...commonLayerProps(fd, setTooltip, setTooltipContent(fd)),
});
}
export default createCategoricalDeckGLComponent(getLayer, getPoints);

View File

@ -0,0 +1,39 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
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'],
thumbnail,
});
export default class ArcChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Arc.jsx'),
transformProps,
});
}
}

View File

@ -0,0 +1,171 @@
/**
* 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 { 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 { commonLayerProps } from '../common';
import TooltipRow from '../../TooltipRow';
const propertyMap = {
fillColor: 'fillColor',
color: 'fillColor',
fill: 'fillColor',
'fill-color': 'fillColor',
strokeColor: 'strokeColor',
'stroke-color': 'strokeColor',
'stroke-width': 'strokeWidth',
};
const alterProps = (props, propOverrides) => {
const newProps = {};
Object.keys(props).forEach((k) => {
if (k in propertyMap) {
newProps[propertyMap[k]] = props[k];
} else {
newProps[k] = props[k];
}
});
if (typeof props.fillColor === 'string') {
newProps.fillColor = hexToRGB(props.fillColor);
}
if (typeof props.strokeColor === 'string') {
newProps.strokeColor = hexToRGB(props.strokeColor);
}
return {
...newProps,
...propOverrides,
};
};
let features;
const recurseGeoJson = (node, propOverrides, extraProps) => {
if (node && node.features) {
node.features.forEach((obj) => {
recurseGeoJson(obj, propOverrides, node.extraProps || extraProps);
});
}
if (node && node.geometry) {
const newNode = {
...node,
properties: alterProps(node.properties, propOverrides),
};
if (!newNode.extraProps) {
newNode.extraProps = extraProps;
}
features.push(newNode);
}
};
function setTooltipContent(o) {
return (
o.object.extraProps &&
<div className="deckgl-tooltip">
{
Object.keys(o.object.extraProps).map((prop, index) =>
<TooltipRow key={`prop-${index}`} label={`${prop}: `} value={`${o.object.extraProps[prop]}`} />,
)
}
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip) {
const fd = formData;
const fc = fd.fill_color_picker;
const sc = fd.stroke_color_picker;
const fillColor = [fc.r, fc.g, fc.b, 255 * fc.a];
const strokeColor = [sc.r, sc.g, sc.b, 255 * sc.a];
const propOverrides = {};
if (fillColor[3] > 0) {
propOverrides.fillColor = fillColor;
}
if (strokeColor[3] > 0) {
propOverrides.strokeColor = strokeColor;
}
features = [];
recurseGeoJson(payload.data, propOverrides);
let jsFnMutator;
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
jsFnMutator = sandboxedEval(fd.js_data_mutator);
features = jsFnMutator(features);
}
return new GeoJsonLayer({
id: `geojson-layer-${fd.slice_id}`,
filled: fd.filled,
data: features,
stroked: fd.stroked,
extruded: fd.extruded,
pointRadiusScale: fd.point_radius_scale,
...commonLayerProps(fd, setTooltip, setTooltipContent),
});
}
const propTypes = {
formData: PropTypes.object.isRequired,
payload: PropTypes.object.isRequired,
setControlValue: PropTypes.func.isRequired,
viewport: PropTypes.object.isRequired,
onAddFilter: PropTypes.func,
setTooltip: PropTypes.func,
};
const defaultProps = {
onAddFilter() {},
setTooltip() {},
};
function deckGeoJson(props) {
const {
formData,
payload,
setControlValue,
onAddFilter,
setTooltip,
viewport,
} = props;
// TODO get this to work
// if (formData.autozoom) {
// viewport = common.fitViewport(viewport, geojsonExtent(payload.data.features));
// }
const layer = getLayer(formData, payload, onAddFilter, setTooltip);
return (
<DeckGLContainer
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={[layer]}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
/>
);
}
deckGeoJson.propTypes = propTypes;
deckGeoJson.defaultProps = defaultProps;
export default deckGeoJson;

View File

@ -0,0 +1,39 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
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'],
thumbnail,
});
export default class GeojsonChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Geojson.jsx'),
transformProps,
});
}
}

View File

@ -0,0 +1,71 @@
/**
* 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 { GridLayer } from 'deck.gl';
import React from 'react';
import { t } from '@superset-ui/translation';
import { commonLayerProps, getAggFunc } from '../common';
import sandboxedEval from '../../../../modules/sandbox';
import { createDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
function setTooltipContent(o) {
return (
<div className="deckgl-tooltip">
<TooltipRow label={`${t('Longitude and Latitude')}: `} value={`${o.object.position[0]}, ${o.object.position[1]}`} />
<TooltipRow label={`${t('Height')}: `} value={`${o.object.elevationValue}`} />
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip) {
const fd = formData;
const c = fd.color_picker;
let data = payload.data.features.map(d => ({
...d,
color: [c.r, c.g, c.b, 255 * c.a],
}));
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
return new GridLayer({
id: `grid-layer-${fd.slice_id}`,
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: aggFunc,
getColorValue: aggFunc,
...commonLayerProps(fd, setTooltip, setTooltipContent),
});
}
function getPoints(data) {
return data.map(d => d.position);
}
export default createDeckGLComponent(getLayer, getPoints);

View File

@ -0,0 +1,39 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
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'],
thumbnail,
});
export default class GridChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Grid.jsx'),
transformProps,
});
}
}

View File

@ -0,0 +1,70 @@
/**
* 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 { HexagonLayer } from 'deck.gl';
import React from 'react';
import { t } from '@superset-ui/translation';
import { commonLayerProps, getAggFunc } from '../common';
import sandboxedEval from '../../../../modules/sandbox';
import { createDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
function setTooltipContent(o) {
return (
<div className="deckgl-tooltip">
<TooltipRow label={`${t('Centroid (Longitude and Latitude)')}: `} value={`(${o.object.centroid[0]}, ${o.object.centroid[1]})`} />
<TooltipRow label={`${t('Height')}: `} value={`${o.object.elevationValue}`} />
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip) {
const fd = formData;
const c = fd.color_picker;
let data = payload.data.features.map(d => ({
...d,
color: [c.r, c.g, c.b, 255 * c.a],
}));
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}
const aggFunc = getAggFunc(fd.js_agg_function, p => p.weight);
return new HexagonLayer({
id: `hex-layer-${fd.slice_id}`,
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: aggFunc,
getColorValue: aggFunc,
...commonLayerProps(fd, setTooltip, setTooltipContent),
});
}
function getPoints(data) {
return data.map(d => d.position);
}
export default createDeckGLComponent(getLayer, getPoints);

View File

@ -0,0 +1,39 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
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'],
thumbnail,
});
export default class HexChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Hex.jsx'),
transformProps,
});
}
}

View File

@ -0,0 +1,72 @@
/**
* 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 { PathLayer } from 'deck.gl';
import React from 'react';
import { commonLayerProps } from '../common';
import sandboxedEval from '../../../../modules/sandbox';
import { createDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
function setTooltipContent(o) {
return (
o.object.extraProps &&
<div className="deckgl-tooltip">
{
Object.keys(o.object.extraProps).map((prop, index) =>
<TooltipRow key={`prop-${index}`} label={`${prop}: `} value={`${o.object.extraProps[prop]}`} />,
)
}
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip) {
const fd = formData;
const c = fd.color_picker;
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
let data = payload.data.features.map(feature => ({
...feature,
path: feature.path,
width: fd.line_width,
color: fixedColor,
}));
if (fd.js_data_mutator) {
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}
return new PathLayer({
id: `path-layer-${fd.slice_id}`,
data,
rounded: true,
widthScale: 1,
...commonLayerProps(fd, setTooltip, setTooltipContent),
});
}
function getPoints(data) {
let points = [];
data.forEach((d) => {
points = points.concat(d.path);
});
return points;
}
export default createDeckGLComponent(getLayer, getPoints);

View File

@ -0,0 +1,39 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
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'],
thumbnail,
});
export default class PathChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Path.jsx'),
transformProps,
});
}
}

View File

@ -0,0 +1,288 @@
/**
* 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.
*/
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
import React from 'react';
import PropTypes from 'prop-types';
import { PolygonLayer } from 'deck.gl';
import AnimatableDeckGLContainer from '../../AnimatableDeckGLContainer';
import Legend from '../../../Legend';
import TooltipRow from '../../TooltipRow';
import { getBuckets, getBreakPointColorScaler } from '../../utils';
import { commonLayerProps, fitViewport } from '../common';
import { getPlaySliderParams } from '../../../../modules/time';
import sandboxedEval from '../../../../modules/sandbox';
const DOUBLE_CLICK_TRESHOLD = 250; // milliseconds
function getPoints(features) {
return features.map(d => d.polygon).flat();
}
function getElevation(d, colorScaler) {
/* in deck.gl 5.3.4 (used in Superset as of 2018-10-24), if a polygon has
* opacity zero it will make everything behind it have opacity zero,
* effectively showing the map layer no matter what other polygons are
* behind it.
*/
return colorScaler(d)[3] === 0
? 0
: d.elevation;
}
function setTooltipContent(formData) {
return (o) => {
const metricLabel = formData.metric.label || formData.metric;
return (
<div className="deckgl-tooltip">
<TooltipRow label={`${formData.line_column}: `} value={`${o.object[formData.line_column]}`} />
{formData.metric && <TooltipRow label={`${metricLabel}: `} value={`${o.object[metricLabel]}`} />}
</div>
);
};
}
export function getLayer(formData, payload, onAddFilter, setTooltip, selected, onSelect, filters) {
const fd = formData;
const fc = fd.fill_color_picker;
const sc = fd.stroke_color_picker;
let data = [...payload.data.features];
if (filters != null) {
filters.forEach((f) => {
data = data.filter(f);
});
}
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}
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);
// when polygons are selected, reduce the opacity of non-selected polygons
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;
return new PolygonLayer({
id: `path-layer-${fd.slice_id}`,
data,
pickable: true,
filled: fd.filled,
stroked: fd.stroked,
getPolygon: d => d.polygon,
getFillColor: colorScaler,
getLineColor: [sc.r, sc.g, sc.b, 255 * sc.a],
getLineWidth: fd.line_width,
extruded: fd.extruded,
getElevation: d => getElevation(d, colorScaler),
elevationScale: fd.multiplier,
fp64: true,
...commonLayerProps(fd, setTooltip, tooltipContentGenerator, onSelect),
});
}
const propTypes = {
formData: PropTypes.object.isRequired,
payload: PropTypes.object.isRequired,
setControlValue: PropTypes.func.isRequired,
viewport: PropTypes.object.isRequired,
onAddFilter: PropTypes.func,
setTooltip: PropTypes.func,
};
const defaultProps = {
onAddFilter() {},
setTooltip() {},
};
class DeckGLPolygon extends React.Component {
constructor(props) {
super(props);
this.state = DeckGLPolygon.getDerivedStateFromProps(props);
this.getLayers = this.getLayers.bind(this);
this.onSelect = this.onSelect.bind(this);
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
// slider position due to changes in form controls
if (state && props.payload.form_data === state.formData) {
return null;
}
const features = props.payload.data.features || [];
const timestamps = features.map(f => f.__timestamp);
// 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 {
start,
end,
getStep,
values,
disabled,
} = getPlaySliderParams(timestamps, granularity);
const viewport = props.formData.autozoom
? fitViewport(props.viewport, getPoints(features))
: props.viewport;
return {
start,
end,
getStep,
values,
disabled,
viewport,
selected: [],
lastClick: 0,
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;
// toggle selected polygons
const selected = [...this.state.selected];
if (doubleClick) {
selected.splice(0, selected.length, polygon);
} else if (formData.toggle_polygons) {
const i = selected.indexOf(polygon);
if (i === -1) {
selected.push(polygon);
} else {
selected.splice(i, 1);
}
} else {
selected.splice(0, 1, polygon);
}
this.setState({ selected, lastClick: now });
if (formData.table_filter) {
onAddFilter(formData.line_column, selected, false, true);
}
}
onValuesChange(values) {
this.setState({
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 [];
}
const filters = [];
// time filter
if (values[0] === values[1] || values[1] === this.end) {
filters.push(d => d.__timestamp >= values[0] && d.__timestamp <= values[1]);
} else {
filters.push(d => d.__timestamp >= values[0] && d.__timestamp < values[1]);
}
const layer = getLayer(
this.props.formData,
this.props.payload,
this.props.onAddFilter,
this.props.setTooltip,
this.state.selected,
this.onSelect,
filters);
return [layer];
}
render() {
const { payload, formData, setControlValue } = this.props;
const { start, end, getStep, values, disabled, viewport } = this.state;
const fd = formData;
const metricLabel = fd.metric ? fd.metric.label || fd.metric : null;
const accessor = d => d[metricLabel];
const buckets = getBuckets(formData, payload.data.features, accessor);
return (
<div style={{ position: 'relative' }}>
<AnimatableDeckGLContainer
getLayers={this.getLayers}
start={start}
end={end}
getStep={getStep}
values={values}
onValuesChange={this.onValuesChange}
disabled={disabled}
viewport={viewport}
onViewportChange={this.onViewportChange}
mapboxApiAccessToken={payload.data.mapboxApiKey}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
aggregation
>
{formData.metric !== null &&
<Legend
categories={buckets}
position={formData.legend_position}
format={formData.legend_format}
/>}
</AnimatableDeckGLContainer>
</div>
);
}
}
DeckGLPolygon.propTypes = propTypes;
DeckGLPolygon.defaultProps = defaultProps;
export default DeckGLPolygon;

View File

@ -0,0 +1,39 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
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'],
thumbnail,
});
export default class PolygonChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Polygon.jsx'),
transformProps,
});
}
}

View File

@ -0,0 +1,71 @@
/**
* 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 { ScatterplotLayer } from 'deck.gl';
import React from 'react';
import { t } from '@superset-ui/translation';
import { commonLayerProps } from '../common';
import { createCategoricalDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
import { unitToRadius } from '../../../../modules/geo';
function getPoints(data) {
return data.map(d => d.position);
}
function setTooltipContent(formData) {
return o => (
<div className="deckgl-tooltip">
<TooltipRow label={`${t('Longitude and Latitude')}: `} value={`${o.object.position[0]}, ${o.object.position[1]}`} />
{
o.object.cat_color && <TooltipRow label={`${t('Category')}: `} value={`${o.object.cat_color}`} />
}
{
o.object.metric && <TooltipRow label={`${formData.point_radius_fixed.value.label}: `} value={`${o.object.metric}`} />
}
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip) {
const fd = formData;
const dataWithRadius = payload.data.features.map((d) => {
let radius = unitToRadius(fd.point_unit, d.radius) || 10;
if (fd.multiplier) {
radius *= fd.multiplier;
}
if (d.color) {
return { ...d, radius };
}
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 };
});
return new ScatterplotLayer({
id: `scatter-layer-${fd.slice_id}`,
data: dataWithRadius,
fp64: true,
radiusMinPixels: fd.min_radius || null,
radiusMaxPixels: fd.max_radius || null,
outline: false,
...commonLayerProps(fd, setTooltip, setTooltipContent(fd)),
});
}
export default createCategoricalDeckGLComponent(getLayer, getPoints);

View File

@ -0,0 +1,39 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
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'],
thumbnail,
});
export default class ScatterChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Scatter.jsx'),
transformProps,
});
}
}

View File

@ -0,0 +1,202 @@
/**
* 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.
*/
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
import React from 'react';
import PropTypes from 'prop-types';
import { ScreenGridLayer } from 'deck.gl';
import { t } from '@superset-ui/translation';
import AnimatableDeckGLContainer from '../../AnimatableDeckGLContainer';
import { getPlaySliderParams } from '../../../../modules/time';
import sandboxedEval from '../../../../modules/sandbox';
import { commonLayerProps, fitViewport } from '../common';
import TooltipRow from '../../TooltipRow';
function getPoints(data) {
return data.map(d => d.position);
}
function setTooltipContent(o) {
return (
<div className="deckgl-tooltip">
<TooltipRow label={`${t('Longitude and Latitude')}: `} value={`${o.object.position[0]}, ${o.object.position[1]}`} />
<TooltipRow label={`${t('Weight')}: `} value={`${o.object.weight}`} />
</div>
);
}
export function getLayer(formData, payload, onAddFilter, setTooltip, selected, onSelect, filters) {
const fd = formData;
const c = fd.color_picker;
let data = payload.data.features.map(d => ({
...d,
color: [c.r, c.g, c.b, 255 * c.a],
}));
if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}
if (filters != null) {
filters.forEach((f) => {
data = data.filter(f);
});
}
// Passing a layer creator function instead of a layer since the
// layer needs to be regenerated at each render
return new ScreenGridLayer({
id: `screengrid-layer-${fd.slice_id}`,
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,
...commonLayerProps(fd, setTooltip, setTooltipContent),
});
}
const propTypes = {
formData: PropTypes.object.isRequired,
payload: PropTypes.object.isRequired,
setControlValue: PropTypes.func.isRequired,
viewport: PropTypes.object.isRequired,
onAddFilter: PropTypes.func,
setTooltip: PropTypes.func,
};
const defaultProps = {
onAddFilter() {},
setTooltip() {},
};
class DeckGLScreenGrid extends React.PureComponent {
constructor(props) {
super(props);
this.state = DeckGLScreenGrid.getDerivedStateFromProps(props);
this.getLayers = this.getLayers.bind(this);
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
// slider position due to changes in form controls
if (state && props.payload.form_data === state.formData) {
return null;
}
const features = props.payload.data.features || [];
const timestamps = features.map(f => f.__timestamp);
// 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 {
start,
end,
getStep,
values,
disabled,
} = getPlaySliderParams(timestamps, granularity);
const viewport = props.formData.autozoom
? fitViewport(props.viewport, getPoints(features))
: props.viewport;
return {
start,
end,
getStep,
values,
disabled,
viewport,
selected: [],
lastClick: 0,
formData: props.payload.form_data,
};
}
onValuesChange(values) {
this.setState({
values: Array.isArray(values)
? values
: [values, values + this.state.getStep(values)],
});
}
onViewportChange(viewport) {
this.setState({ viewport });
}
getLayers(values) {
const filters = [];
// time filter
if (values[0] === values[1] || values[1] === this.end) {
filters.push(d => d.__timestamp >= values[0] && d.__timestamp <= values[1]);
} else {
filters.push(d => d.__timestamp >= values[0] && d.__timestamp < values[1]);
}
const layer = getLayer(
this.props.formData,
this.props.payload,
this.props.onAddFilter,
this.props.setTooltip,
filters);
return [layer];
}
render() {
const { formData, payload, setControlValue } = this.props;
return (
<div>
<AnimatableDeckGLContainer
getLayers={this.getLayers}
start={this.state.start}
end={this.state.end}
getStep={this.state.getStep}
values={this.state.values}
onValuesChange={this.onValuesChange}
disabled={this.state.disabled}
viewport={this.state.viewport}
onViewportChange={this.onViewportChange}
mapboxApiAccessToken={payload.data.mapboxApiKey}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
aggregation
/>
</div>
);
}
}
DeckGLScreenGrid.propTypes = propTypes;
DeckGLScreenGrid.defaultProps = defaultProps;
export default DeckGLScreenGrid;

View File

@ -0,0 +1,39 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import thumbnail from './images/thumbnail.png';
import transformProps from '../../transformProps';
const metadata = new ChartMetadata({
name: t('deck.gl Screen Grid'),
description: '',
credits: ['https://uber.github.io/deck.gl'],
thumbnail,
});
export default class ScreengridChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Screengrid.jsx'),
transformProps,
});
}
}

View File

@ -0,0 +1,155 @@
/**
* 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 { fitBounds } from 'viewport-mercator-project';
import * as d3array from 'd3-array';
import sandboxedEval from '../../../modules/sandbox';
const PADDING = 0.25;
const GEO_BOUNDS = {
LAT_MIN: -90,
LAT_MAX: 90,
LNG_MIN: -180,
LNG_MAX: 180,
};
/**
* Get the latitude bounds if latitude is a single coordinate
* @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;
return [latMin, latMax];
}
/**
* Get the longitude bounds if longitude is a single coordinate
* @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;
return [lngMin, lngMax];
}
export function getBounds(points) {
const latExt = d3array.extent(points, d => d[1]);
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]],
];
}
export function fitViewport(viewport, points, padding = 10) {
try {
const bounds = getBounds(points);
return {
...viewport,
...fitBounds({
height: viewport.height,
width: viewport.width,
padding,
bounds,
}),
};
} catch (e) {
/* eslint no-console: 0 */
console.error('Could not auto zoom', e);
return viewport;
}
}
export function commonLayerProps(formData, setTooltip, setTooltipContent, onSelect) {
const fd = formData;
let onHover;
let tooltipContentGenerator = setTooltipContent;
if (fd.js_tooltip) {
tooltipContentGenerator = sandboxedEval(fd.js_tooltip);
}
if (tooltipContentGenerator) {
onHover = (o) => {
if (o.picked) {
setTooltip({
content: tooltipContentGenerator(o),
x: o.x,
y: o.y + 30,
});
} else {
setTooltip(null);
}
};
}
let onClick;
if (fd.js_onclick_href) {
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,
pickable: Boolean(onHover),
};
}
const percentiles = {
p1: 0.01,
p5: 0.05,
p95: 0.95,
p99: 0.99,
};
/* Get an a stat function that operates on arrays, aligns with control=js_agg_function */
export function getAggFunc(type = 'sum', accessor = null) {
if (type === 'count') {
return arr => arr.length;
}
let d3func;
if (type in percentiles) {
d3func = (arr, acc) => {
let sortedArr;
if (accessor) {
sortedArr = arr.sort((o1, o2) => d3array.ascending(accessor(o1), accessor(o2)));
} else {
sortedArr = arr.sort(d3array.ascending);
}
return d3array.quantile(sortedArr, percentiles[type], acc);
};
} else {
d3func = d3array[type];
}
if (!accessor) {
return arr => d3func(arr);
}
return arr => d3func(arr.map(accessor));
}

View File

@ -0,0 +1,40 @@
/**
* 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.
*/
/* eslint camelcase: 0 */
import { getLayer as deck_grid } from './Grid/Grid';
import { getLayer as deck_screengrid } from './Screengrid/Screengrid';
import { getLayer as deck_path } from './Path/Path';
import { getLayer as deck_hex } from './Hex/Hex';
import { getLayer as deck_scatter } from './Scatter/Scatter';
import { getLayer as deck_geojson } from './Geojson/Geojson';
import { getLayer as deck_arc } from './Arc/Arc';
import { getLayer as deck_polygon } from './Polygon/Polygon';
const layerGenerators = {
deck_grid,
deck_screengrid,
deck_path,
deck_hex,
deck_scatter,
deck_geojson,
deck_arc,
deck_polygon,
};
export default layerGenerators;

View File

@ -0,0 +1,47 @@
/**
* 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 { Preset } from '@superset-ui/core';
import ArcChartPlugin from './layers/Arc';
import GeoJsonChartPlugin from './layers/Geojson';
import GridChartPlugin from './layers/Grid';
import HexChartPlugin from './layers/Hex';
import MultiChartPlugin from './Multi';
import PathChartPlugin from './layers/Path';
import PolygonChartPlugin from './layers/Polygon';
import ScatterChartPlugin from './layers/Scatter';
import ScreengridChartPlugin from './layers/Screengrid';
export default class DeckGLChartPreset extends Preset {
constructor() {
super({
name: 'deck.gl charts',
plugins: [
new ArcChartPlugin().configure({ key: 'deck_arc' }),
new GeoJsonChartPlugin().configure({ key: 'deck_geojson' }),
new GridChartPlugin().configure({ key: 'deck_grid' }),
new HexChartPlugin().configure({ key: 'deck_hex' }),
new MultiChartPlugin().configure({ key: 'deck_multi' }),
new PathChartPlugin().configure({ key: 'deck_path' }),
new PolygonChartPlugin().configure({ key: 'deck_polygon' }),
new ScatterChartPlugin().configure({ key: 'deck_scatter' }),
new ScreengridChartPlugin().configure({ key: 'deck_screengrid' }),
],
});
}
}

View File

@ -0,0 +1,43 @@
/**
* 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.
*/
const NOOP = () => {};
export default function transformProps(chartProps) {
const {
width,
height,
rawFormData,
queryData,
hooks,
} = chartProps;
const { onAddFilter = NOOP, setControlValue = NOOP, setTooltip = NOOP } = hooks;
return {
formData: rawFormData,
payload: queryData,
setControlValue,
viewport: {
...rawFormData.viewport,
width,
height,
},
onAddFilter,
setTooltip,
};
}

View File

@ -0,0 +1,135 @@
/**
* 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 { extent } from 'd3-array';
import { scaleThreshold } from 'd3-scale';
import { getSequentialSchemeRegistry, SequentialScheme } from '@superset-ui/color';
import { hexToRGB } from '../../modules/colors';
const DEFAULT_NUM_BUCKETS = 10;
export function getBreakPoints(
{ break_points: formDataBreakPoints, num_buckets: formDataNumBuckets },
features,
accessor,
) {
if (!features) {
return [];
}
if (formDataBreakPoints === undefined || formDataBreakPoints.length === 0) {
// compute evenly distributed break points based on number of buckets
const numBuckets = formDataNumBuckets ? parseInt(formDataNumBuckets, 10) : DEFAULT_NUM_BUCKETS;
const [minValue, maxValue] = extent(features, accessor);
if (minValue === undefined) {
return [];
}
const delta = (maxValue - minValue) / numBuckets;
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)
.fill()
.map((_, i) => (minValue + i * delta).toFixed(precision));
}
return formDataBreakPoints.sort((a, b) => parseFloat(a) - parseFloat(b));
}
export function getBreakPointColorScaler(
{
break_points: formDataBreakPoints,
num_buckets: formDataNumBuckets,
linear_color_scheme: linearColorScheme,
opacity,
},
features,
accessor,
) {
const breakPoints =
formDataBreakPoints || formDataNumBuckets
? getBreakPoints(
{
break_points: formDataBreakPoints,
num_buckets: formDataNumBuckets,
},
features,
accessor,
)
: null;
const colorScheme = Array.isArray(linearColorScheme)
? new SequentialScheme({
id: 'custom',
colors: linearColorScheme,
})
: getSequentialSchemeRegistry().get(linearColorScheme);
let scaler;
let maskPoint;
if (breakPoints !== null) {
// bucket colors into discrete colors
const n = breakPoints.length - 1;
const bucketedColors =
n > 1 ? colorScheme.getColors(n) : [colorScheme.colors[colorScheme.colors.length - 1]];
// repeat ends
const first = bucketedColors[0];
const last = bucketedColors[bucketedColors.length - 1];
bucketedColors.unshift(first);
bucketedColors.push(last);
const points = breakPoints.map(p => parseFloat(p));
scaler = scaleThreshold()
.domain(points)
.range(bucketedColors);
maskPoint = value => value > breakPoints[n] || value < breakPoints[0];
} else {
// interpolate colors linearly
scaler = colorScheme.createLinearScale(extent(features, accessor));
maskPoint = () => false;
}
return d => {
const v = accessor(d);
const c = hexToRGB(scaler(v));
if (maskPoint(v)) {
c[3] = 0;
} else {
c[3] = (opacity / 100.0) * 255;
}
return c;
};
}
export function getBuckets(fd, features, accessor) {
const breakPoints = getBreakPoints(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 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;
buckets[range] = {
color: colorScaler({ [metricLabel || fd.metric]: mid }),
enabled: true,
};
});
return buckets;
}