mirror of https://github.com/apache/superset.git
Add categories and time slider to arc deck.gl viz (#5638)
* Fix legend position * Add categories and play slider to arc viz * New functionality to arc viz
This commit is contained in:
parent
bcc0954bb9
commit
6959b70c1c
|
@ -759,7 +759,8 @@ export const visTypes = {
|
||||||
{
|
{
|
||||||
label: t('Arc'),
|
label: t('Arc'),
|
||||||
controlSetRows: [
|
controlSetRows: [
|
||||||
['color_picker', null],
|
['color_picker', 'legend_position'],
|
||||||
|
['dimension', 'color_scheme'],
|
||||||
['stroke_width', null],
|
['stroke_width', null],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -773,6 +774,16 @@ export const visTypes = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
controlOverrides: {
|
||||||
|
dimension: {
|
||||||
|
label: t('Categorical Color'),
|
||||||
|
description: t('Pick a dimension from which categorical colors are defined'),
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
validators: [],
|
||||||
|
},
|
||||||
|
time_grain_sqla: timeGrainSqlaAnimationOverrides,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
deck_scatter: {
|
deck_scatter: {
|
||||||
|
|
|
@ -42,6 +42,7 @@ export default class Legend extends React.PureComponent {
|
||||||
const vertical = this.props.position.charAt(0) === 't' ? 'top' : 'bottom';
|
const vertical = this.props.position.charAt(0) === 't' ? 'top' : 'bottom';
|
||||||
const horizontal = this.props.position.charAt(1) === 'r' ? 'right' : 'left';
|
const horizontal = this.props.position.charAt(1) === 'r' ? 'right' : 'left';
|
||||||
const style = {
|
const style = {
|
||||||
|
position: 'absolute',
|
||||||
[vertical]: '0px',
|
[vertical]: '0px',
|
||||||
[horizontal]: '10px',
|
[horizontal]: '10px',
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import AnimatableDeckGLContainer from './AnimatableDeckGLContainer';
|
||||||
|
import Legend from '../Legend';
|
||||||
|
|
||||||
|
import { getColorFromScheme, hexToRGB } from '../../modules/colors';
|
||||||
|
import { getPlaySliderParams } from '../../modules/time';
|
||||||
|
import sandboxedEval from '../../modules/sandbox';
|
||||||
|
|
||||||
|
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 categories = {};
|
||||||
|
data.forEach((d) => {
|
||||||
|
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
|
||||||
|
let color;
|
||||||
|
if (fd.dimension) {
|
||||||
|
color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
|
||||||
|
} else {
|
||||||
|
color = fixedColor;
|
||||||
|
}
|
||||||
|
categories[d.cat_color] = { color, enabled: true };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
slice: PropTypes.object.isRequired,
|
||||||
|
data: PropTypes.array.isRequired,
|
||||||
|
mapboxApiKey: PropTypes.string.isRequired,
|
||||||
|
setControlValue: PropTypes.func.isRequired,
|
||||||
|
viewport: PropTypes.object.isRequired,
|
||||||
|
getLayer: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable-next-line react/sort-comp */
|
||||||
|
static getDerivedStateFromProps(nextProps) {
|
||||||
|
const fd = nextProps.slice.formData;
|
||||||
|
|
||||||
|
const timeGrain = fd.time_grain_sqla || fd.granularity || 'PT1M';
|
||||||
|
const timestamps = nextProps.data.map(f => f.__timestamp);
|
||||||
|
const { start, end, step, values, disabled } = getPlaySliderParams(timestamps, timeGrain);
|
||||||
|
const categories = getCategories(fd, nextProps.data);
|
||||||
|
|
||||||
|
return { start, end, step, values, disabled, categories };
|
||||||
|
}
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = CategoricalDeckGLContainer.getDerivedStateFromProps(props);
|
||||||
|
|
||||||
|
this.getLayers = this.getLayers.bind(this);
|
||||||
|
this.toggleCategory = this.toggleCategory.bind(this);
|
||||||
|
this.showSingleCategory = this.showSingleCategory.bind(this);
|
||||||
|
}
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.setState(CategoricalDeckGLContainer.getDerivedStateFromProps(nextProps, this.state));
|
||||||
|
}
|
||||||
|
addColor(data, fd) {
|
||||||
|
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||||
|
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||||
|
|
||||||
|
return data.map((d) => {
|
||||||
|
let color;
|
||||||
|
if (fd.dimension) {
|
||||||
|
color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
|
||||||
|
} else {
|
||||||
|
color = fixedColor;
|
||||||
|
}
|
||||||
|
return { ...d, color };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getLayers(values) {
|
||||||
|
const fd = this.props.slice.formData;
|
||||||
|
let data = [...this.props.data];
|
||||||
|
|
||||||
|
// Add colors from categories or fixed color
|
||||||
|
data = this.addColor(data, fd);
|
||||||
|
|
||||||
|
// Apply user defined data mutator if defined
|
||||||
|
if (fd.js_data_mutator) {
|
||||||
|
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||||
|
data = jsFnMutator(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by time
|
||||||
|
if (values[0] === values[1] || values[1] === this.end) {
|
||||||
|
data = data.filter(d => d.__timestamp >= values[0] && d.__timestamp <= values[1]);
|
||||||
|
} else {
|
||||||
|
data = data.filter(d => d.__timestamp >= values[0] && d.__timestamp < values[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show only categories selected in the legend
|
||||||
|
if (fd.dimension) {
|
||||||
|
data = data.filter(d => this.state.categories[d.cat_color].enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [this.props.getLayer(fd, data, this.props.slice)];
|
||||||
|
}
|
||||||
|
toggleCategory(category) {
|
||||||
|
const categoryState = this.state.categories[category];
|
||||||
|
categoryState.enabled = !categoryState.enabled;
|
||||||
|
const categories = { ...this.state.categories, [category]: categoryState };
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
step={this.state.step}
|
||||||
|
values={this.state.values}
|
||||||
|
disabled={this.state.disabled}
|
||||||
|
viewport={this.props.viewport}
|
||||||
|
mapboxApiAccessToken={this.props.mapboxApiKey}
|
||||||
|
mapStyle={this.props.slice.formData.mapbox_style}
|
||||||
|
setControlValue={this.props.setControlValue}
|
||||||
|
>
|
||||||
|
<Legend
|
||||||
|
categories={this.state.categories}
|
||||||
|
toggleCategory={this.toggleCategory}
|
||||||
|
showSingleCategory={this.showSingleCategory}
|
||||||
|
position={this.props.slice.formData.legend_position}
|
||||||
|
/>
|
||||||
|
</AnimatableDeckGLContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoricalDeckGLContainer.propTypes = propTypes;
|
|
@ -1,12 +1,13 @@
|
||||||
|
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import { ArcLayer } from 'deck.gl';
|
import { ArcLayer } from 'deck.gl';
|
||||||
|
|
||||||
import DeckGLContainer from './../DeckGLContainer';
|
import CategoricalDeckGLContainer from '../CategoricalDeckGLContainer';
|
||||||
|
|
||||||
import * as common from './common';
|
import * as common from './common';
|
||||||
import sandboxedEval from '../../../modules/sandbox';
|
|
||||||
|
|
||||||
function getPoints(data) {
|
function getPoints(data) {
|
||||||
const points = [];
|
const points = [];
|
||||||
|
@ -17,20 +18,7 @@ function getPoints(data) {
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLayer(formData, payload, slice) {
|
function getLayer(fd, data, slice) {
|
||||||
const fd = formData;
|
|
||||||
const fc = fd.color_picker;
|
|
||||||
let data = payload.data.arcs.map(d => ({
|
|
||||||
...d,
|
|
||||||
color: [fc.r, fc.g, fc.b, 255 * fc.a],
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (fd.js_data_mutator) {
|
|
||||||
// Applying user defined data mutator if defined
|
|
||||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
|
||||||
data = jsFnMutator(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ArcLayer({
|
return new ArcLayer({
|
||||||
id: `path-layer-${fd.slice_id}`,
|
id: `path-layer-${fd.slice_id}`,
|
||||||
data,
|
data,
|
||||||
|
@ -40,23 +28,25 @@ function getLayer(formData, payload, slice) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function deckArc(slice, payload, setControlValue) {
|
function deckArc(slice, payload, setControlValue) {
|
||||||
const layer = getLayer(slice.formData, payload, slice);
|
const fd = slice.formData;
|
||||||
let viewport = {
|
let viewport = {
|
||||||
...slice.formData.viewport,
|
...fd.viewport,
|
||||||
width: slice.width(),
|
width: slice.width(),
|
||||||
height: slice.height(),
|
height: slice.height(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (slice.formData.autozoom) {
|
if (fd.autozoom) {
|
||||||
viewport = common.fitViewport(viewport, getPoints(payload.data.arcs));
|
viewport = common.fitViewport(viewport, getPoints(payload.data.arcs));
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<DeckGLContainer
|
<CategoricalDeckGLContainer
|
||||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
slice={slice}
|
||||||
viewport={viewport}
|
data={payload.data.arcs}
|
||||||
layers={[layer]}
|
mapboxApiKey={payload.data.mapboxApiKey}
|
||||||
mapStyle={slice.formData.mapbox_style}
|
|
||||||
setControlValue={setControlValue}
|
setControlValue={setControlValue}
|
||||||
|
viewport={viewport}
|
||||||
|
getLayer={getLayer}
|
||||||
/>,
|
/>,
|
||||||
document.getElementById(slice.containerId),
|
document.getElementById(slice.containerId),
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,82 +2,30 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { ScatterplotLayer } from 'deck.gl';
|
import { ScatterplotLayer } from 'deck.gl';
|
||||||
|
|
||||||
import AnimatableDeckGLContainer from '../AnimatableDeckGLContainer';
|
import CategoricalDeckGLContainer from '../CategoricalDeckGLContainer';
|
||||||
import Legend from '../../Legend';
|
|
||||||
|
|
||||||
import * as common from './common';
|
import * as common from './common';
|
||||||
import { getColorFromScheme, hexToRGB } from '../../../modules/colors';
|
|
||||||
import { getPlaySliderParams } from '../../../modules/time';
|
|
||||||
import { unitToRadius } from '../../../modules/geo';
|
import { unitToRadius } from '../../../modules/geo';
|
||||||
import sandboxedEval from '../../../modules/sandbox';
|
|
||||||
|
|
||||||
|
|
||||||
function getPoints(data) {
|
function getPoints(data) {
|
||||||
return data.map(d => d.position);
|
return data.map(d => d.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCategories(formData, payload) {
|
function getLayer(fd, data, slice) {
|
||||||
const fd = formData;
|
const dataWithRadius = data.map((d) => {
|
||||||
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 categories = {};
|
|
||||||
|
|
||||||
payload.data.features.forEach((d) => {
|
|
||||||
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
|
|
||||||
let color;
|
|
||||||
if (fd.dimension) {
|
|
||||||
color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
|
|
||||||
} else {
|
|
||||||
color = fixedColor;
|
|
||||||
}
|
|
||||||
categories[d.cat_color] = { color, enabled: true };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLayer(formData, payload, slice, filters) {
|
|
||||||
const fd = formData;
|
|
||||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
|
||||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
|
||||||
|
|
||||||
let data = payload.data.features.map((d) => {
|
|
||||||
let radius = unitToRadius(fd.point_unit, d.radius) || 10;
|
let radius = unitToRadius(fd.point_unit, d.radius) || 10;
|
||||||
if (fd.multiplier) {
|
if (fd.multiplier) {
|
||||||
radius *= fd.multiplier;
|
radius *= fd.multiplier;
|
||||||
}
|
}
|
||||||
let color;
|
return { ...d, radius };
|
||||||
if (fd.dimension) {
|
|
||||||
color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
|
|
||||||
} else {
|
|
||||||
color = fixedColor;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...d,
|
|
||||||
radius,
|
|
||||||
color,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ScatterplotLayer({
|
return new ScatterplotLayer({
|
||||||
id: `scatter-layer-${fd.slice_id}`,
|
id: `scatter-layer-${fd.slice_id}`,
|
||||||
data,
|
data: dataWithRadius,
|
||||||
fp64: true,
|
fp64: true,
|
||||||
radiusMinPixels: fd.min_radius || null,
|
radiusMinPixels: fd.min_radius || null,
|
||||||
radiusMaxPixels: fd.max_radius || null,
|
radiusMaxPixels: fd.max_radius || null,
|
||||||
|
@ -86,109 +34,6 @@ function getLayer(formData, payload, slice, filters) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
slice: PropTypes.object.isRequired,
|
|
||||||
payload: PropTypes.object.isRequired,
|
|
||||||
setControlValue: PropTypes.func.isRequired,
|
|
||||||
viewport: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
class DeckGLScatter extends React.PureComponent {
|
|
||||||
/* eslint-disable-next-line react/sort-comp */
|
|
||||||
static getDerivedStateFromProps(nextProps) {
|
|
||||||
const fd = nextProps.slice.formData;
|
|
||||||
|
|
||||||
const timeGrain = fd.time_grain_sqla || fd.granularity || 'PT1M';
|
|
||||||
const timestamps = nextProps.payload.data.features.map(f => f.__timestamp);
|
|
||||||
const { start, end, step, values, disabled } = getPlaySliderParams(timestamps, timeGrain);
|
|
||||||
|
|
||||||
const categories = getCategories(fd, nextProps.payload);
|
|
||||||
|
|
||||||
return { start, end, step, values, disabled, categories };
|
|
||||||
}
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = DeckGLScatter.getDerivedStateFromProps(props);
|
|
||||||
|
|
||||||
this.getLayers = this.getLayers.bind(this);
|
|
||||||
this.toggleCategory = this.toggleCategory.bind(this);
|
|
||||||
this.showSingleCategory = this.showSingleCategory.bind(this);
|
|
||||||
}
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
this.setState(DeckGLScatter.getDerivedStateFromProps(nextProps, this.state));
|
|
||||||
}
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// legend filter
|
|
||||||
if (this.props.slice.formData.dimension) {
|
|
||||||
filters.push(d => this.state.categories[d.cat_color].enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
const layer = getLayer(
|
|
||||||
this.props.slice.formData,
|
|
||||||
this.props.payload,
|
|
||||||
this.props.slice,
|
|
||||||
filters);
|
|
||||||
|
|
||||||
return [layer];
|
|
||||||
}
|
|
||||||
toggleCategory(category) {
|
|
||||||
const categoryState = this.state.categories[category];
|
|
||||||
categoryState.enabled = !categoryState.enabled;
|
|
||||||
const categories = { ...this.state.categories, [category]: categoryState };
|
|
||||||
|
|
||||||
// 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>
|
|
||||||
<AnimatableDeckGLContainer
|
|
||||||
getLayers={this.getLayers}
|
|
||||||
start={this.state.start}
|
|
||||||
end={this.state.end}
|
|
||||||
step={this.state.step}
|
|
||||||
values={this.state.values}
|
|
||||||
disabled={this.state.disabled}
|
|
||||||
viewport={this.props.viewport}
|
|
||||||
mapboxApiAccessToken={this.props.payload.data.mapboxApiKey}
|
|
||||||
mapStyle={this.props.slice.formData.mapbox_style}
|
|
||||||
setControlValue={this.props.setControlValue}
|
|
||||||
>
|
|
||||||
<Legend
|
|
||||||
categories={this.state.categories}
|
|
||||||
toggleCategory={this.toggleCategory}
|
|
||||||
showSingleCategory={this.showSingleCategory}
|
|
||||||
position={this.props.slice.formData.legend_position}
|
|
||||||
/>
|
|
||||||
</AnimatableDeckGLContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DeckGLScatter.propTypes = propTypes;
|
|
||||||
|
|
||||||
function deckScatter(slice, payload, setControlValue) {
|
function deckScatter(slice, payload, setControlValue) {
|
||||||
const fd = slice.formData;
|
const fd = slice.formData;
|
||||||
let viewport = {
|
let viewport = {
|
||||||
|
@ -202,11 +47,13 @@ function deckScatter(slice, payload, setControlValue) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<DeckGLScatter
|
<CategoricalDeckGLContainer
|
||||||
slice={slice}
|
slice={slice}
|
||||||
payload={payload}
|
data={payload.data.features}
|
||||||
|
mapboxApiKey={payload.data.mapboxApiKey}
|
||||||
setControlValue={setControlValue}
|
setControlValue={setControlValue}
|
||||||
viewport={viewport}
|
viewport={viewport}
|
||||||
|
getLayer={getLayer}
|
||||||
/>,
|
/>,
|
||||||
document.getElementById(slice.containerId),
|
document.getElementById(slice.containerId),
|
||||||
);
|
);
|
||||||
|
|
|
@ -2381,11 +2381,21 @@ class DeckArc(BaseDeckGLViz):
|
||||||
viz_type = 'deck_arc'
|
viz_type = 'deck_arc'
|
||||||
verbose_name = _('Deck.gl - Arc')
|
verbose_name = _('Deck.gl - Arc')
|
||||||
spatial_control_keys = ['start_spatial', 'end_spatial']
|
spatial_control_keys = ['start_spatial', 'end_spatial']
|
||||||
|
is_timeseries = True
|
||||||
|
|
||||||
|
def query_obj(self):
|
||||||
|
fd = self.form_data
|
||||||
|
self.is_timeseries = bool(
|
||||||
|
fd.get('time_grain_sqla') or fd.get('granularity'))
|
||||||
|
return super(DeckArc, self).query_obj()
|
||||||
|
|
||||||
def get_properties(self, d):
|
def get_properties(self, d):
|
||||||
|
dim = self.form_data.get('dimension')
|
||||||
return {
|
return {
|
||||||
'sourcePosition': d.get('start_spatial'),
|
'sourcePosition': d.get('start_spatial'),
|
||||||
'targetPosition': d.get('end_spatial'),
|
'targetPosition': d.get('end_spatial'),
|
||||||
|
'cat_color': d.get(dim) if dim else None,
|
||||||
|
DTTM_ALIAS: d.get(DTTM_ALIAS),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_data(self, df):
|
def get_data(self, df):
|
||||||
|
|
Loading…
Reference in New Issue