mirror of https://github.com/apache/superset.git
DECKGL integration - Phase 1 (#3771)
* DECKGL integration Adding a new set of geospatial visualizations building on top of the awesome deck.gl library. https://github.com/uber/deck.gl While the end goal it to expose all types of layers and let users bind their data to control most props exposed by the deck.gl API, this PR focusses on a first set of visualizations and props: * ScatterLayer * HexagonLayer * GridLayer * ScreenGridLayer * Addressing comments * lint * Linting * Addressing chri's comments
This commit is contained in:
parent
1c545d3a2d
commit
3a8af5d0b0
|
@ -102,7 +102,7 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme
|
|||
good-names=i,j,k,ex,Run,_,d,e,v,o,l,x,ts
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
bad-names=foo,bar,baz,toto,tutu,tata,d,fd
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
After Width: | Height: | Size: 777 KiB |
Binary file not shown.
After Width: | Height: | Size: 578 KiB |
|
@ -21,6 +21,7 @@ const propTypes = {
|
|||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.object,
|
||||
PropTypes.bool,
|
||||
PropTypes.array,
|
||||
PropTypes.func]),
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Label, Popover, OverlayTrigger } from 'react-bootstrap';
|
||||
|
||||
import controls from '../../stores/controls';
|
||||
import TextControl from './TextControl';
|
||||
import SelectControl from './SelectControl';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import PopoverSection from '../../../components/PopoverSection';
|
||||
|
||||
const controlTypes = {
|
||||
fixed: 'fix',
|
||||
metric: 'metric',
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.object,
|
||||
isFloat: PropTypes.bool,
|
||||
datasource: PropTypes.object,
|
||||
default: PropTypes.shape({
|
||||
type: PropTypes.oneOf(['fix', 'metric']),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
}),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
default: { type: controlTypes.fixed, value: 5 },
|
||||
};
|
||||
|
||||
export default class FixedOrMetricControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.setType = this.setType.bind(this);
|
||||
this.setFixedValue = this.setFixedValue.bind(this);
|
||||
this.setMetric = this.setMetric.bind(this);
|
||||
const type = (props.value ? props.value.type : props.default.type) || controlTypes.fixed;
|
||||
const value = (props.value ? props.value.value : props.default.value) || '100';
|
||||
this.state = {
|
||||
type,
|
||||
fixedValue: type === controlTypes.fixed ? value : '',
|
||||
metricValue: type === controlTypes.metric ? value : null,
|
||||
};
|
||||
}
|
||||
onChange() {
|
||||
this.props.onChange({
|
||||
type: this.state.type,
|
||||
value: this.state.type === controlTypes.fixed ?
|
||||
this.state.fixedValue : this.state.metricValue,
|
||||
});
|
||||
}
|
||||
setType(type) {
|
||||
this.setState({ type }, this.onChange);
|
||||
}
|
||||
setFixedValue(fixedValue) {
|
||||
this.setState({ fixedValue }, this.onChange);
|
||||
}
|
||||
setMetric(metricValue) {
|
||||
this.setState({ metricValue }, this.onChange);
|
||||
}
|
||||
renderPopover() {
|
||||
const value = this.props.value || this.props.default;
|
||||
const type = value.type || controlTypes.fixed;
|
||||
const metrics = this.props.datasource ? this.props.datasource.metrics : null;
|
||||
return (
|
||||
<Popover id="filter-popover">
|
||||
<div style={{ width: '240px' }}>
|
||||
<PopoverSection
|
||||
title="Fixed"
|
||||
isSelected={type === controlTypes.fixed}
|
||||
onSelect={() => { this.onChange(controlTypes.fixed); }}
|
||||
>
|
||||
<TextControl
|
||||
isFloat
|
||||
onChange={this.setFixedValue}
|
||||
onFocus={() => { this.setType(controlTypes.fixed); }}
|
||||
value={this.state.fixedValue}
|
||||
/>
|
||||
</PopoverSection>
|
||||
<PopoverSection
|
||||
title="Based on a metric"
|
||||
isSelected={type === controlTypes.metric}
|
||||
onSelect={() => { this.onChange(controlTypes.metric); }}
|
||||
>
|
||||
<SelectControl
|
||||
{...controls.metric}
|
||||
name="metric"
|
||||
options={metrics}
|
||||
onFocus={() => { this.setType(controlTypes.metric); }}
|
||||
onChange={this.setMetric}
|
||||
value={this.state.metricValue}
|
||||
/>
|
||||
</PopoverSection>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<OverlayTrigger
|
||||
container={document.body}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="trigger"
|
||||
placement="right"
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<Label style={{ cursor: 'pointer' }}>
|
||||
{this.state.type === controlTypes.fixed &&
|
||||
<span>{this.state.fixedValue}</span>
|
||||
}
|
||||
{this.state.type === controlTypes.metric &&
|
||||
<span>
|
||||
<span style={{ fontWeight: 'normal' }}>metric: </span>
|
||||
<strong>{this.state.metricValue}</strong>
|
||||
</span>
|
||||
}
|
||||
</Label>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FixedOrMetricControl.propTypes = propTypes;
|
||||
FixedOrMetricControl.defaultProps = defaultProps;
|
|
@ -17,6 +17,7 @@ const propTypes = {
|
|||
multi: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
|
||||
showHeader: PropTypes.bool,
|
||||
optionRenderer: PropTypes.func,
|
||||
|
@ -34,6 +35,7 @@ const defaultProps = {
|
|||
label: null,
|
||||
multi: false,
|
||||
onChange: () => {},
|
||||
onFocus: () => {},
|
||||
showHeader: true,
|
||||
optionRenderer: opt => opt.label,
|
||||
valueRenderer: opt => opt.label,
|
||||
|
@ -115,6 +117,7 @@ export default class SelectControl extends React.PureComponent {
|
|||
clearable: this.props.clearable,
|
||||
isLoading: this.props.isLoading,
|
||||
onChange: this.onChange,
|
||||
onFocus: this.props.onFocus,
|
||||
optionRenderer: VirtualizedRendererWrap(this.props.optionRenderer),
|
||||
valueRenderer: this.props.valueRenderer,
|
||||
selectComponent: this.props.freeForm ? Creatable : Select,
|
||||
|
|
|
@ -5,10 +5,8 @@ import * as v from '../../validators';
|
|||
import ControlHeader from '../ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
|
@ -18,9 +16,8 @@ const propTypes = {
|
|||
};
|
||||
|
||||
const defaultProps = {
|
||||
label: null,
|
||||
description: null,
|
||||
onChange: () => {},
|
||||
onFocus: () => {},
|
||||
value: '',
|
||||
isInt: false,
|
||||
isFloat: false,
|
||||
|
@ -64,6 +61,7 @@ export default class TextControl extends React.Component {
|
|||
type="text"
|
||||
placeholder=""
|
||||
onChange={this.onChange}
|
||||
onFocus={this.props.onFocus}
|
||||
value={value}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Label, Popover, OverlayTrigger } from 'react-bootstrap';
|
||||
import { decimal2sexagesimal } from 'geolib';
|
||||
|
||||
import TextControl from './TextControl';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import { defaultViewport } from '../../../modules/geo';
|
||||
|
||||
const PARAMS = [
|
||||
'longitude',
|
||||
'latitude',
|
||||
'zoom',
|
||||
'bearing',
|
||||
'pitch',
|
||||
];
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.shape({
|
||||
longitude: PropTypes.number,
|
||||
latitude: PropTypes.number,
|
||||
zoom: PropTypes.number,
|
||||
bearing: PropTypes.number,
|
||||
pitch: PropTypes.number,
|
||||
}),
|
||||
default: PropTypes.object,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
default: { type: 'fix', value: 5 },
|
||||
value: defaultViewport,
|
||||
};
|
||||
|
||||
export default class ViewportControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
onChange(ctrl, value) {
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
[ctrl]: value,
|
||||
});
|
||||
}
|
||||
renderTextControl(ctrl) {
|
||||
return (
|
||||
<div key={ctrl}>
|
||||
{ctrl}
|
||||
<TextControl
|
||||
value={this.props.value[ctrl]}
|
||||
onChange={this.onChange.bind(this, ctrl)}
|
||||
isFloat
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
renderPopover() {
|
||||
return (
|
||||
<Popover id={`filter-popover-${this.props.name}`} title="Viewport">
|
||||
{PARAMS.map(ctrl => this.renderTextControl(ctrl))}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
renderLabel() {
|
||||
if (this.props.value.longitude && this.props.value.latitude) {
|
||||
return (
|
||||
decimal2sexagesimal(this.props.value.longitude) +
|
||||
' | ' +
|
||||
decimal2sexagesimal(this.props.value.latitude)
|
||||
);
|
||||
}
|
||||
return 'N/A';
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<OverlayTrigger
|
||||
container={document.body}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="trigger"
|
||||
placement="right"
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<Label style={{ cursor: 'pointer' }}>
|
||||
{this.renderLabel()}
|
||||
</Label>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewportControl.propTypes = propTypes;
|
||||
ViewportControl.defaultProps = defaultProps;
|
|
@ -6,12 +6,14 @@ import ColorSchemeControl from './ColorSchemeControl';
|
|||
import DatasourceControl from './DatasourceControl';
|
||||
import DateFilterControl from './DateFilterControl';
|
||||
import FilterControl from './FilterControl';
|
||||
import FixedOrMetricControl from './FixedOrMetricControl';
|
||||
import HiddenControl from './HiddenControl';
|
||||
import SelectAsyncControl from './SelectAsyncControl';
|
||||
import SelectControl from './SelectControl';
|
||||
import TextAreaControl from './TextAreaControl';
|
||||
import TextControl from './TextControl';
|
||||
import TimeSeriesColumnControl from './TimeSeriesColumnControl';
|
||||
import ViewportControl from './ViewportControl';
|
||||
import VizTypeControl from './VizTypeControl';
|
||||
|
||||
const controlMap = {
|
||||
|
@ -23,12 +25,14 @@ const controlMap = {
|
|||
DatasourceControl,
|
||||
DateFilterControl,
|
||||
FilterControl,
|
||||
FixedOrMetricControl,
|
||||
HiddenControl,
|
||||
SelectAsyncControl,
|
||||
SelectControl,
|
||||
TextAreaControl,
|
||||
TextControl,
|
||||
TimeSeriesColumnControl,
|
||||
ViewportControl,
|
||||
VizTypeControl,
|
||||
};
|
||||
export default controlMap;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import { formatSelectOptionsForRange, formatSelectOptions } from '../../modules/utils';
|
||||
import * as v from '../validators';
|
||||
import { ALL_COLOR_SCHEMES, spectrums } from '../../modules/colors';
|
||||
import { colorPrimary, ALL_COLOR_SCHEMES, spectrums } from '../../modules/colors';
|
||||
import { defaultViewport } from '../../modules/geo';
|
||||
import MetricOption from '../../components/MetricOption';
|
||||
import ColumnOption from '../../components/ColumnOption';
|
||||
import OptionDescription from '../../components/OptionDescription';
|
||||
|
@ -135,6 +136,14 @@ export const controls = {
|
|||
}),
|
||||
},
|
||||
|
||||
color_picker: {
|
||||
label: t('Fixed Color'),
|
||||
description: t('Use this to define a static color for all circles'),
|
||||
type: 'ColorPickerControl',
|
||||
default: colorPrimary,
|
||||
renderTrigger: true,
|
||||
},
|
||||
|
||||
annotation_layers: {
|
||||
type: 'SelectAsyncControl',
|
||||
multi: true,
|
||||
|
@ -424,6 +433,13 @@ export const controls = {
|
|||
},
|
||||
|
||||
groupby: groupByControl,
|
||||
dimension: {
|
||||
...groupByControl,
|
||||
label: t('Dimension'),
|
||||
description: t('Select a dimension'),
|
||||
multi: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
columns: Object.assign({}, groupByControl, {
|
||||
label: t('Columns'),
|
||||
|
@ -441,6 +457,28 @@ export const controls = {
|
|||
}),
|
||||
},
|
||||
|
||||
longitude: {
|
||||
type: 'SelectControl',
|
||||
label: t('Longitude'),
|
||||
default: 1,
|
||||
validators: [v.nonEmpty],
|
||||
description: t('Select the longitude column'),
|
||||
mapStateToProps: state => ({
|
||||
choices: (state.datasource) ? state.datasource.all_cols : [],
|
||||
}),
|
||||
},
|
||||
|
||||
latitude: {
|
||||
type: 'SelectControl',
|
||||
label: t('Latitude'),
|
||||
default: 1,
|
||||
validators: [v.nonEmpty],
|
||||
description: t('Select the latitude column'),
|
||||
mapStateToProps: state => ({
|
||||
choices: (state.datasource) ? state.datasource.all_cols : [],
|
||||
}),
|
||||
},
|
||||
|
||||
all_columns_x: {
|
||||
type: 'SelectControl',
|
||||
label: 'X',
|
||||
|
@ -730,6 +768,14 @@ export const controls = {
|
|||
'with the [Periods] text box'),
|
||||
},
|
||||
|
||||
multiplier: {
|
||||
type: 'TextControl',
|
||||
label: t('Multiplier'),
|
||||
isFloat: true,
|
||||
default: 1,
|
||||
description: t('Factor to multiply the metric by'),
|
||||
},
|
||||
|
||||
rolling_periods: {
|
||||
type: 'TextControl',
|
||||
label: t('Periods'),
|
||||
|
@ -738,6 +784,15 @@ export const controls = {
|
|||
'relative to the time granularity selected'),
|
||||
},
|
||||
|
||||
grid_size: {
|
||||
type: 'TextControl',
|
||||
label: t('Grid Size'),
|
||||
renderTrigger: true,
|
||||
default: 20,
|
||||
isInt: true,
|
||||
description: t('Defines the grid size in pixels'),
|
||||
},
|
||||
|
||||
min_periods: {
|
||||
type: 'TextControl',
|
||||
label: t('Min Periods'),
|
||||
|
@ -1043,6 +1098,14 @@ export const controls = {
|
|||
),
|
||||
},
|
||||
|
||||
extruded: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Extruded'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: ('Whether to make the grid 3D'),
|
||||
},
|
||||
|
||||
show_brush: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Range Filter'),
|
||||
|
@ -1255,6 +1318,7 @@ export const controls = {
|
|||
mapbox_style: {
|
||||
type: 'SelectControl',
|
||||
label: t('Map Style'),
|
||||
renderTrigger: true,
|
||||
choices: [
|
||||
['mapbox://styles/mapbox/streets-v9', 'Streets'],
|
||||
['mapbox://styles/mapbox/dark-v9', 'Dark'],
|
||||
|
@ -1288,6 +1352,15 @@ export const controls = {
|
|||
'number of points (>1000) will cause lag.'),
|
||||
},
|
||||
|
||||
point_radius_fixed: {
|
||||
type: 'FixedOrMetricControl',
|
||||
label: t('Point Size'),
|
||||
description: t('Fixed point radius'),
|
||||
mapStateToProps: state => ({
|
||||
datasource: state.datasource,
|
||||
}),
|
||||
},
|
||||
|
||||
point_radius: {
|
||||
type: 'SelectControl',
|
||||
label: t('Point Radius'),
|
||||
|
@ -1308,6 +1381,22 @@ export const controls = {
|
|||
description: t('The unit of measure for the specified point radius'),
|
||||
},
|
||||
|
||||
point_unit: {
|
||||
type: 'SelectControl',
|
||||
label: t('Point Unit'),
|
||||
default: 'square_m',
|
||||
clearable: false,
|
||||
choices: [
|
||||
['square_m', 'Square meters'],
|
||||
['square_km', 'Square kilometers'],
|
||||
['square_miles', 'Square miles'],
|
||||
['radius_m', 'Radius in meters'],
|
||||
['radius_km', 'Radius in kilometers'],
|
||||
['radius_miles', 'Radius in miles'],
|
||||
],
|
||||
description: t('The unit of measure for the specified point radius'),
|
||||
},
|
||||
|
||||
global_opacity: {
|
||||
type: 'TextControl',
|
||||
label: t('Opacity'),
|
||||
|
@ -1317,6 +1406,15 @@ export const controls = {
|
|||
'Between 0 and 1.'),
|
||||
},
|
||||
|
||||
viewport: {
|
||||
type: 'ViewportControl',
|
||||
label: t('Viewport'),
|
||||
renderTrigger: true,
|
||||
description: t('Parameters related to the view and perspective on the map'),
|
||||
// default is whole world mostly centered
|
||||
default: defaultViewport,
|
||||
},
|
||||
|
||||
viewport_zoom: {
|
||||
type: 'TextControl',
|
||||
label: t('Zoom'),
|
||||
|
@ -1370,6 +1468,7 @@ export const controls = {
|
|||
color: {
|
||||
type: 'ColorPickerControl',
|
||||
label: t('Color'),
|
||||
default: colorPrimary,
|
||||
description: t('Pick a color'),
|
||||
},
|
||||
|
||||
|
|
|
@ -294,6 +294,153 @@ export const visTypes = {
|
|||
},
|
||||
},
|
||||
|
||||
deck_hex: {
|
||||
label: t('Deck.gl - Hexagons'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['longitude', 'latitude'],
|
||||
['groupby', 'size'],
|
||||
['row_limit'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
['mapbox_style', 'viewport'],
|
||||
['color_picker', null],
|
||||
['grid_size', 'extruded'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
label: t('Height'),
|
||||
description: t('Metric used to control height'),
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
deck_grid: {
|
||||
label: t('Deck.gl - Grid'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['longitude', 'latitude'],
|
||||
['groupby', 'size'],
|
||||
['row_limit'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
['mapbox_style', 'viewport'],
|
||||
['color_picker', null],
|
||||
['grid_size', 'extruded'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
label: t('Height'),
|
||||
description: t('Metric used to control height'),
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
deck_screengrid: {
|
||||
label: t('Deck.gl - Screen grid'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['longitude', 'latitude'],
|
||||
['groupby', 'size'],
|
||||
['row_limit'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
['mapbox_style', 'viewport'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Grid'),
|
||||
controlSetRows: [
|
||||
['grid_size', 'color_picker'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
label: t('Weight'),
|
||||
description: t("Metric used as a weight for the grid's coloring"),
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
deck_scatter: {
|
||||
label: t('Deck.gl - Scatter plot'),
|
||||
requiresTime: true,
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['longitude', 'latitude'],
|
||||
['groupby'],
|
||||
['row_limit'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
controlSetRows: [
|
||||
['mapbox_style', 'viewport'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Point Size'),
|
||||
controlSetRows: [
|
||||
['point_radius_fixed', 'point_unit'],
|
||||
['multiplier', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Point Color'),
|
||||
controlSetRows: [
|
||||
['color_picker', null],
|
||||
['dimension', 'color_scheme'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
all_columns_x: {
|
||||
label: t('Longitude Column'),
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
all_columns_y: {
|
||||
label: t('Latitude Column'),
|
||||
validators: [v.nonEmpty],
|
||||
},
|
||||
dimension: {
|
||||
label: t('Categorical Color'),
|
||||
description: t('Pick a dimension from which categorical colors are defined'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
area: {
|
||||
label: t('Time Series - Stacked'),
|
||||
requiresTime: true,
|
||||
|
@ -1062,9 +1209,8 @@ export const visTypes = {
|
|||
{
|
||||
label: t('Viewport'),
|
||||
controlSetRows: [
|
||||
['viewport_longitude'],
|
||||
['viewport_latitude'],
|
||||
['viewport_zoom'],
|
||||
['viewport_longitude', 'viewport_latitude'],
|
||||
['viewport_zoom', null],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -142,3 +142,13 @@ export const colorScalerFactory = function (colors, data, accessor, extents) {
|
|||
const points = colors.map((col, i) => ext[0] + (i * chunkSize));
|
||||
return d3.scale.linear().domain(points).range(colors).clamp(true);
|
||||
};
|
||||
|
||||
export function hexToRGB(hex, alpha = 255) {
|
||||
if (!hex) {
|
||||
return [0, 0, 0, alpha];
|
||||
}
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return [r, g, b, alpha];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
export const defaultViewport = {
|
||||
longitude: 6.85236157047845,
|
||||
latitude: 31.222656842808707,
|
||||
zoom: 1,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
};
|
||||
|
||||
const METER_TO_MILE = 1609.34;
|
||||
export function unitToRadius(unit, num) {
|
||||
if (unit === 'square_m') {
|
||||
return Math.sqrt(num / Math.PI);
|
||||
} else if (unit === 'radius_m') {
|
||||
return num;
|
||||
} else if (unit === 'radius_km') {
|
||||
return num * 1000;
|
||||
} else if (unit === 'radius_miles') {
|
||||
return num * METER_TO_MILE;
|
||||
} else if (unit === 'square_km') {
|
||||
return Math.sqrt(num / Math.PI) * 1000;
|
||||
} else if (unit === 'square_miles') {
|
||||
return Math.sqrt(num / Math.PI) * METER_TO_MILE;
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -55,11 +55,14 @@
|
|||
"d3-tip": "^0.6.7",
|
||||
"datamaps": "^0.5.8",
|
||||
"datatables.net-bs": "^1.10.15",
|
||||
"deck.gl": "^4.1.5",
|
||||
"distributions": "^1.0.0",
|
||||
"geolib": "^2.0.24",
|
||||
"immutable": "^3.8.2",
|
||||
"jed": "^1.1.1",
|
||||
"jquery": "3.1.1",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"luma.gl": "^4.0.5",
|
||||
"moment": "2.18.1",
|
||||
"mustache": "^2.2.1",
|
||||
"nvd3": "1.8.6",
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import { shallow } from 'enzyme';
|
||||
import { OverlayTrigger } from 'react-bootstrap';
|
||||
|
||||
import FixedOrMetricControl from
|
||||
'../../../../javascripts/explore/components/controls/FixedOrMetricControl';
|
||||
import SelectControl from
|
||||
'../../../../javascripts/explore/components/controls/SelectControl';
|
||||
import TextControl from
|
||||
'../../../../javascripts/explore/components/controls/TextControl';
|
||||
import ControlHeader from '../../../../javascripts/explore/components/ControlHeader';
|
||||
|
||||
const defaultProps = {
|
||||
value: { },
|
||||
};
|
||||
|
||||
describe('FixedOrMetricControl', () => {
|
||||
let wrapper;
|
||||
let inst;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<FixedOrMetricControl {...defaultProps} />);
|
||||
inst = wrapper.instance();
|
||||
});
|
||||
|
||||
it('renders a OverlayTrigger', () => {
|
||||
const controlHeader = wrapper.find(ControlHeader);
|
||||
expect(controlHeader).to.have.lengthOf(1);
|
||||
expect(wrapper.find(OverlayTrigger)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('renders a TextControl and a SelectControl', () => {
|
||||
const popOver = shallow(inst.renderPopover());
|
||||
expect(popOver.find(TextControl)).to.have.lengthOf(1);
|
||||
expect(popOver.find(SelectControl)).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint-disable no-unused-expressions */
|
||||
import React from 'react';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import { shallow } from 'enzyme';
|
||||
import { OverlayTrigger, Label } from 'react-bootstrap';
|
||||
|
||||
import ViewportControl from
|
||||
'../../../../javascripts/explore/components/controls/ViewportControl';
|
||||
import TextControl from
|
||||
'../../../../javascripts/explore/components/controls/TextControl';
|
||||
import ControlHeader from '../../../../javascripts/explore/components/ControlHeader';
|
||||
|
||||
const defaultProps = {
|
||||
value: {
|
||||
longitude: 6.85236157047845,
|
||||
latitude: 31.222656842808707,
|
||||
zoom: 1,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
},
|
||||
};
|
||||
|
||||
describe('ViewportControl', () => {
|
||||
let wrapper;
|
||||
let inst;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ViewportControl {...defaultProps} />);
|
||||
inst = wrapper.instance();
|
||||
});
|
||||
|
||||
it('renders a OverlayTrigger', () => {
|
||||
const controlHeader = wrapper.find(ControlHeader);
|
||||
expect(controlHeader).to.have.lengthOf(1);
|
||||
expect(wrapper.find(OverlayTrigger)).to.have.length(1);
|
||||
});
|
||||
|
||||
it('renders a Popover with 5 TextControl', () => {
|
||||
const popOver = shallow(inst.renderPopover());
|
||||
expect(popOver.find(TextControl)).to.have.lengthOf(5);
|
||||
});
|
||||
|
||||
it('renders a summary in the label', () => {
|
||||
expect(wrapper.find(Label).first().render().text()).to.equal('6° 51\' 8.50" | 31° 13\' 21.56"');
|
||||
});
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import { it, describe } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { ALL_COLOR_SCHEMES, getColorFromScheme } from '../../../javascripts/modules/colors';
|
||||
import { ALL_COLOR_SCHEMES, getColorFromScheme, hexToRGB } from '../../../javascripts/modules/colors';
|
||||
|
||||
describe('colors', () => {
|
||||
it('default to bnbColors', () => {
|
||||
|
@ -19,4 +19,13 @@ describe('colors', () => {
|
|||
expect(color1).to.equal(color3);
|
||||
expect(color4).to.equal(ALL_COLOR_SCHEMES.bnbColors[1]);
|
||||
});
|
||||
|
||||
it('hexToRGB converts properly', () => {
|
||||
expect(hexToRGB('#FFFFFF')).to.have.same.members([255, 255, 255, 255]);
|
||||
expect(hexToRGB('#000000')).to.have.same.members([0, 0, 0, 255]);
|
||||
expect(hexToRGB('#FF0000')).to.have.same.members([255, 0, 0, 255]);
|
||||
expect(hexToRGB('#00FF00')).to.have.same.members([0, 255, 0, 255]);
|
||||
expect(hexToRGB('#0000FF')).to.have.same.members([0, 0, 255, 255]);
|
||||
expect(hexToRGB('#FF0000', 128)).to.have.same.members([255, 0, 0, 128]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { it, describe } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { unitToRadius } from '../../../javascripts/modules/geo';
|
||||
|
||||
const METER_TO_MILE = 1609.34;
|
||||
|
||||
describe('unitToRadius', () => {
|
||||
it('converts to square meters', () => {
|
||||
expect(unitToRadius('square_m', 4 * Math.PI)).to.equal(2);
|
||||
});
|
||||
it('converts to square meters', () => {
|
||||
expect(unitToRadius('square_km', 25 * Math.PI)).to.equal(5000);
|
||||
});
|
||||
it('converts to radius meters', () => {
|
||||
expect(unitToRadius('radius_m', 1000)).to.equal(1000);
|
||||
});
|
||||
it('converts to radius km', () => {
|
||||
expect(unitToRadius('radius_km', 1)).to.equal(1000);
|
||||
});
|
||||
it('converts to radius miles', () => {
|
||||
expect(unitToRadius('radius_miles', 1)).to.equal(METER_TO_MILE);
|
||||
});
|
||||
it('converts to square miles', () => {
|
||||
expect(unitToRadius('square_miles', 25 * Math.PI)).to.equal(5000 * (METER_TO_MILE / 1000));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MapGL from 'react-map-gl';
|
||||
import DeckGL from 'deck.gl';
|
||||
|
||||
const propTypes = {
|
||||
viewport: PropTypes.object.isRequired,
|
||||
layers: PropTypes.array.isRequired,
|
||||
setControlValue: PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.string,
|
||||
mapboxApiAccessToken: PropTypes.string.isRequired,
|
||||
onViewportChange: PropTypes.func,
|
||||
};
|
||||
const defaultProps = {
|
||||
mapStyle: 'light',
|
||||
onViewportChange: () => {},
|
||||
};
|
||||
|
||||
export default class DeckGLContainer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
viewport: props.viewport,
|
||||
};
|
||||
this.tick = this.tick.bind(this);
|
||||
this.onViewportChange = this.onViewportChange.bind(this);
|
||||
}
|
||||
componentWillMount() {
|
||||
const timer = setInterval(this.tick, 1000);
|
||||
this.setState(() => ({ timer }));
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState(() => ({
|
||||
viewport: { ...nextProps.viewport },
|
||||
}));
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.clearInterval(this.state.timer);
|
||||
}
|
||||
onViewportChange(viewport) {
|
||||
const vp = Object.assign({}, viewport);
|
||||
delete vp.width;
|
||||
delete vp.height;
|
||||
const newVp = { ...this.state.viewport, ...vp };
|
||||
|
||||
this.setState(() => ({ viewport: newVp }));
|
||||
this.props.onViewportChange(newVp);
|
||||
}
|
||||
tick() {
|
||||
// Limiting updating viewport controls through Redux at most 1*sec
|
||||
if (this.state.previousViewport !== this.state.viewport) {
|
||||
const setCV = this.props.setControlValue;
|
||||
const vp = this.state.viewport;
|
||||
if (setCV) {
|
||||
setCV('viewport', vp);
|
||||
}
|
||||
this.setState(() => ({ previousViewport: this.state.viewport }));
|
||||
}
|
||||
}
|
||||
layers() {
|
||||
// Support for layer factory
|
||||
if (this.props.layers.some(l => typeof l === 'function')) {
|
||||
return this.props.layers.map(l => typeof l === 'function' ? l() : l);
|
||||
}
|
||||
return this.props.layers;
|
||||
}
|
||||
render() {
|
||||
const { viewport } = this.state;
|
||||
return (
|
||||
<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;
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { GridLayer } from 'deck.gl';
|
||||
|
||||
import DeckGLContainer from './DeckGLContainer';
|
||||
|
||||
function deckScreenGridLayer(slice, payload, setControlValue) {
|
||||
const fd = slice.formData;
|
||||
const c = fd.color_picker;
|
||||
const data = payload.data.features.map(d => ({
|
||||
...d,
|
||||
color: [c.r, c.g, c.b, 255 * c.a],
|
||||
}));
|
||||
|
||||
const layer = new GridLayer({
|
||||
id: `grid-layer-${slice.containerId}`,
|
||||
data,
|
||||
pickable: true,
|
||||
cellSize: fd.grid_size,
|
||||
minColor: [0, 0, 0, 0],
|
||||
extruded: fd.extruded,
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
outline: false,
|
||||
getElevationValue: points => points.reduce((sum, point) => sum + point.weight, 0),
|
||||
getColorValue: points => points.reduce((sum, point) => sum + point.weight, 0),
|
||||
});
|
||||
const viewport = {
|
||||
...fd.viewport,
|
||||
width: slice.width(),
|
||||
height: slice.height(),
|
||||
};
|
||||
ReactDOM.render(
|
||||
<DeckGLContainer
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={fd.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
/>,
|
||||
document.getElementById(slice.containerId),
|
||||
);
|
||||
}
|
||||
module.exports = deckScreenGridLayer;
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { HexagonLayer } from 'deck.gl';
|
||||
|
||||
import DeckGLContainer from './DeckGLContainer';
|
||||
|
||||
function deckHex(slice, payload, setControlValue) {
|
||||
const fd = slice.formData;
|
||||
const c = fd.color_picker;
|
||||
const data = payload.data.features.map(d => ({
|
||||
...d,
|
||||
color: [c.r, c.g, c.b, 255 * c.a],
|
||||
}));
|
||||
|
||||
const layer = new HexagonLayer({
|
||||
id: `hex-layer-${slice.containerId}`,
|
||||
data,
|
||||
pickable: true,
|
||||
radius: fd.grid_size,
|
||||
minColor: [0, 0, 0, 0],
|
||||
extruded: fd.extruded,
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
outline: false,
|
||||
getElevationValue: points => points.reduce((sum, point) => sum + point.weight, 0),
|
||||
getColorValue: points => points.reduce((sum, point) => sum + point.weight, 0),
|
||||
});
|
||||
const viewport = {
|
||||
...fd.viewport,
|
||||
width: slice.width(),
|
||||
height: slice.height(),
|
||||
};
|
||||
ReactDOM.render(
|
||||
<DeckGLContainer
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={fd.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
/>,
|
||||
document.getElementById(slice.containerId),
|
||||
);
|
||||
}
|
||||
module.exports = deckHex;
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { ScatterplotLayer } from 'deck.gl';
|
||||
|
||||
import DeckGLContainer from './DeckGLContainer';
|
||||
import { getColorFromScheme, hexToRGB } from '../../javascripts/modules/colors';
|
||||
import { unitToRadius } from '../../javascripts/modules/geo';
|
||||
|
||||
function deckScatter(slice, payload, setControlValue) {
|
||||
const fd = slice.formData;
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||
|
||||
const data = payload.data.features.map((d) => {
|
||||
let radius = unitToRadius(fd.point_unit, d.radius) || 10;
|
||||
if (fd.multiplier) {
|
||||
radius *= fd.multiplier;
|
||||
}
|
||||
let color;
|
||||
if (fd.dimension) {
|
||||
color = hexToRGB(getColorFromScheme(d.cat_color, fd.color_scheme), c.a * 255);
|
||||
} else {
|
||||
color = fixedColor;
|
||||
}
|
||||
return {
|
||||
...d,
|
||||
radius,
|
||||
color,
|
||||
};
|
||||
});
|
||||
|
||||
const layer = new ScatterplotLayer({
|
||||
id: `scatter-layer-${slice.containerId}`,
|
||||
data,
|
||||
pickable: true,
|
||||
fp64: true,
|
||||
outline: false,
|
||||
});
|
||||
const viewport = {
|
||||
...fd.viewport,
|
||||
width: slice.width(),
|
||||
height: slice.height(),
|
||||
};
|
||||
ReactDOM.render(
|
||||
<DeckGLContainer
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={fd.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
/>,
|
||||
document.getElementById(slice.containerId),
|
||||
);
|
||||
}
|
||||
module.exports = deckScatter;
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { ScreenGridLayer } from 'deck.gl';
|
||||
|
||||
import DeckGLContainer from './DeckGLContainer';
|
||||
|
||||
function deckScreenGridLayer(slice, payload, setControlValue) {
|
||||
const fd = slice.formData;
|
||||
const c = fd.color_picker;
|
||||
const data = payload.data.features.map(d => ({
|
||||
...d,
|
||||
color: [c.r, c.g, c.b, 255 * c.a],
|
||||
}));
|
||||
|
||||
const viewport = {
|
||||
...fd.viewport,
|
||||
width: slice.width(),
|
||||
height: slice.height(),
|
||||
};
|
||||
// Passing a layer creator function instead of a layer since the
|
||||
// layer needs to be regenerated at each render
|
||||
const layer = () => new ScreenGridLayer({
|
||||
id: `screengrid-layer-${slice.containerId}`,
|
||||
data,
|
||||
pickable: true,
|
||||
cellSizePixels: fd.grid_size,
|
||||
minColor: [c.r, c.g, c.b, 0],
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
outline: false,
|
||||
getWeight: d => d.weight || 0,
|
||||
});
|
||||
ReactDOM.render(
|
||||
<DeckGLContainer
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport}
|
||||
layers={[layer]}
|
||||
mapStyle={fd.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
/>,
|
||||
document.getElementById(slice.containerId),
|
||||
);
|
||||
}
|
||||
module.exports = deckScreenGridLayer;
|
|
@ -36,5 +36,9 @@ const vizMap = {
|
|||
event_flow: require('./EventFlow.jsx'),
|
||||
paired_ttest: require('./paired_ttest.jsx'),
|
||||
partition: require('./partition.js'),
|
||||
deck_scatter: require('./deckgl/scatter.jsx'),
|
||||
deck_screengrid: require('./deckgl/screengrid.jsx'),
|
||||
deck_grid: require('./deckgl/grid.jsx'),
|
||||
deck_hex: require('./deckgl/hex.jsx'),
|
||||
};
|
||||
export default vizMap;
|
||||
|
|
|
@ -133,10 +133,16 @@ def load_examples(load_test_data):
|
|||
print('Loading [Misc Charts] dashboard')
|
||||
data.load_misc_dashboard()
|
||||
|
||||
print("Loading DECK.gl demo")
|
||||
data.load_deck_dash()
|
||||
|
||||
if load_test_data:
|
||||
print('Loading [Unicode test data]')
|
||||
data.load_unicode_test_data()
|
||||
|
||||
print("Loading flights data")
|
||||
data.load_flights()
|
||||
|
||||
|
||||
@manager.option(
|
||||
'-d', '--datasource',
|
||||
|
|
|
@ -1226,3 +1226,276 @@ def load_misc_dashboard():
|
|||
dash.slices = slices
|
||||
db.session.merge(dash)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def load_deck_dash():
|
||||
print("Loading deck.gl dashboard")
|
||||
slices = []
|
||||
tbl = db.session.query(TBL).filter_by(table_name='long_lat').first()
|
||||
slice_data = {
|
||||
"longitude": "LON",
|
||||
"latitude": "LAT",
|
||||
"color_picker": {
|
||||
"r": 205,
|
||||
"g": 0,
|
||||
"b": 3,
|
||||
"a": 0.82,
|
||||
},
|
||||
"datasource": "5__table",
|
||||
"filters": [],
|
||||
"granularity_sqla": "date",
|
||||
"groupby": [],
|
||||
"having": "",
|
||||
"mapbox_style": "mapbox://styles/mapbox/light-v9",
|
||||
"multiplier": 10,
|
||||
"point_radius_fixed": {"type": "metric", "value": "count"},
|
||||
"point_unit": "square_m",
|
||||
"row_limit": 5000,
|
||||
"since": "2014-01-01",
|
||||
"size": "count",
|
||||
"time_grain_sqla": "Time Column",
|
||||
"until": "now",
|
||||
"viewport": {
|
||||
"bearing": -4.952916738791771,
|
||||
"latitude": 37.78926922909199,
|
||||
"longitude": -122.42613341901688,
|
||||
"pitch": 4.750411100577438,
|
||||
"zoom": 12.729132798697304,
|
||||
},
|
||||
"viz_type": "deck_scatter",
|
||||
"where": "",
|
||||
}
|
||||
|
||||
print("Creating Scatterplot slice")
|
||||
slc = Slice(
|
||||
slice_name="Scatterplot",
|
||||
viz_type='deck_scatter',
|
||||
datasource_type='table',
|
||||
datasource_id=tbl.id,
|
||||
params=get_slice_json(slice_data),
|
||||
)
|
||||
merge_slice(slc)
|
||||
slices.append(slc)
|
||||
|
||||
slice_data = {
|
||||
"point_unit": "square_m",
|
||||
"filters": [],
|
||||
"row_limit": 5000,
|
||||
"longitude": "LON",
|
||||
"latitude": "LAT",
|
||||
"mapbox_style": "mapbox://styles/mapbox/dark-v9",
|
||||
"granularity_sqla": "date",
|
||||
"size": "count",
|
||||
"viz_type": "deck_screengrid",
|
||||
"since": "2014-01-01",
|
||||
"point_radius": "Auto",
|
||||
"until": "now",
|
||||
"color_picker": {"a": 1,
|
||||
"r": 14,
|
||||
"b": 0,
|
||||
"g": 255},
|
||||
"grid_size": 20,
|
||||
"where": "",
|
||||
"having": "",
|
||||
"viewport": {
|
||||
"zoom": 14.161641703941438,
|
||||
"longitude": -122.41827069521386,
|
||||
"bearing": -4.952916738791771,
|
||||
"latitude": 37.76024135844065,
|
||||
"pitch": 4.750411100577438,
|
||||
},
|
||||
"point_radius_fixed": {"type": "fix", "value": 2000},
|
||||
"datasource": "5__table",
|
||||
"time_grain_sqla": "Time Column",
|
||||
"groupby": [],
|
||||
}
|
||||
print("Creating Screen Grid slice")
|
||||
slc = Slice(
|
||||
slice_name="Screen grid",
|
||||
viz_type='deck_screengrid',
|
||||
datasource_type='table',
|
||||
datasource_id=tbl.id,
|
||||
params=get_slice_json(slice_data),
|
||||
)
|
||||
merge_slice(slc)
|
||||
slices.append(slc)
|
||||
|
||||
slice_data = {
|
||||
"filters": [],
|
||||
"row_limit": 5000,
|
||||
"longitude": "LON",
|
||||
"latitude": "LAT",
|
||||
"mapbox_style": "mapbox://styles/mapbox/streets-v9",
|
||||
"granularity_sqla": "date",
|
||||
"size": "count",
|
||||
"viz_type": "deck_hex",
|
||||
"since": "2014-01-01",
|
||||
"point_radius_unit": "Pixels",
|
||||
"point_radius": "Auto",
|
||||
"until": "now",
|
||||
"color_picker": {
|
||||
"a": 1,
|
||||
"r": 14,
|
||||
"b": 0,
|
||||
"g": 255,
|
||||
},
|
||||
"grid_size": 40,
|
||||
"extruded": True,
|
||||
"having": "",
|
||||
"viewport": {
|
||||
"latitude": 37.789795085160335,
|
||||
"pitch": 54.08961642447763,
|
||||
"zoom": 13.835465702403654,
|
||||
"longitude": -122.40632230075536,
|
||||
"bearing": -2.3984797349335167,
|
||||
},
|
||||
"where": "",
|
||||
"point_radius_fixed": {"type": "fix", "value": 2000},
|
||||
"datasource": "5__table",
|
||||
"time_grain_sqla": "Time Column",
|
||||
"groupby": [],
|
||||
}
|
||||
print("Creating Hex slice")
|
||||
slc = Slice(
|
||||
slice_name="Hexagons",
|
||||
viz_type='deck_hex',
|
||||
datasource_type='table',
|
||||
datasource_id=tbl.id,
|
||||
params=get_slice_json(slice_data),
|
||||
)
|
||||
merge_slice(slc)
|
||||
slices.append(slc)
|
||||
|
||||
slice_data = {
|
||||
"filters": [],
|
||||
"row_limit": 5000,
|
||||
"longitude": "LON",
|
||||
"latitude": "LAT",
|
||||
"mapbox_style": "mapbox://styles/mapbox/satellite-streets-v9",
|
||||
"granularity_sqla": "date",
|
||||
"size": "count",
|
||||
"viz_type": "deck_grid",
|
||||
"since": "2014-01-01",
|
||||
"point_radius_unit": "Pixels",
|
||||
"point_radius": "Auto",
|
||||
"until": "now",
|
||||
"color_picker": {
|
||||
"a": 1,
|
||||
"r": 14,
|
||||
"b": 0,
|
||||
"g": 255,
|
||||
},
|
||||
"grid_size": 120,
|
||||
"extruded": True,
|
||||
"having": "",
|
||||
"viewport": {
|
||||
"longitude": -122.42066918995666,
|
||||
"bearing": 155.80099696026355,
|
||||
"zoom": 12.699690845482069,
|
||||
"latitude": 37.7942314882596,
|
||||
"pitch": 53.470800300695146,
|
||||
},
|
||||
"where": "",
|
||||
"point_radius_fixed": {"type": "fix", "value": 2000},
|
||||
"datasource": "5__table",
|
||||
"time_grain_sqla": "Time Column",
|
||||
"groupby": [],
|
||||
}
|
||||
print("Creating Grid slice")
|
||||
slc = Slice(
|
||||
slice_name="Grid",
|
||||
viz_type='deck_grid',
|
||||
datasource_type='table',
|
||||
datasource_id=tbl.id,
|
||||
params=get_slice_json(slice_data),
|
||||
)
|
||||
merge_slice(slc)
|
||||
slices.append(slc)
|
||||
|
||||
print("Creating a dashboard")
|
||||
title = "deck.gl Demo"
|
||||
dash = db.session.query(Dash).filter_by(dashboard_title=title).first()
|
||||
|
||||
if not dash:
|
||||
dash = Dash()
|
||||
js = textwrap.dedent("""\
|
||||
[
|
||||
{
|
||||
"col": 1,
|
||||
"row": 0,
|
||||
"size_x": 6,
|
||||
"size_y": 4,
|
||||
"slice_id": "37"
|
||||
},
|
||||
{
|
||||
"col": 7,
|
||||
"row": 0,
|
||||
"size_x": 6,
|
||||
"size_y": 4,
|
||||
"slice_id": "38"
|
||||
},
|
||||
{
|
||||
"col": 7,
|
||||
"row": 4,
|
||||
"size_x": 6,
|
||||
"size_y": 4,
|
||||
"slice_id": "39"
|
||||
},
|
||||
{
|
||||
"col": 1,
|
||||
"row": 4,
|
||||
"size_x": 6,
|
||||
"size_y": 4,
|
||||
"slice_id": "40"
|
||||
}
|
||||
]
|
||||
""")
|
||||
l = json.loads(js)
|
||||
for i, pos in enumerate(l):
|
||||
pos['slice_id'] = str(slices[i].id)
|
||||
dash.dashboard_title = title
|
||||
dash.position_json = json.dumps(l, indent=4)
|
||||
dash.slug = "deck"
|
||||
dash.slices = slices
|
||||
db.session.merge(dash)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def load_flights():
|
||||
"""Loading random time series data from a zip file in the repo"""
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'fligth_data.csv.gz')) as f:
|
||||
pdf = pd.read_csv(f, encoding='latin-1')
|
||||
|
||||
# Loading airports info to join and get lat/long
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'airports.csv.gz')) as f:
|
||||
airports = pd.read_csv(f, encoding='latin-1')
|
||||
airports = airports.set_index('IATA_CODE')
|
||||
|
||||
pdf['ds'] = pdf.YEAR.map(str) + '-0' + pdf.MONTH.map(str) + '-0' + pdf.DAY.map(str)
|
||||
pdf.ds = pd.to_datetime(pdf.ds)
|
||||
del pdf['YEAR']
|
||||
del pdf['MONTH']
|
||||
del pdf['DAY']
|
||||
|
||||
pdf = pdf.join(airports, on='ORIGIN_AIRPORT', rsuffix='_ORIG')
|
||||
pdf = pdf.join(airports, on='DESTINATION_AIRPORT', rsuffix='_DEST')
|
||||
pdf.to_sql(
|
||||
'flights',
|
||||
db.engine,
|
||||
if_exists='replace',
|
||||
chunksize=500,
|
||||
dtype={
|
||||
'ds': DateTime,
|
||||
},
|
||||
index=False)
|
||||
print("Done loading table!")
|
||||
|
||||
print("Creating table [random_time_series] reference")
|
||||
obj = db.session.query(TBL).filter_by(table_name='random_time_series').first()
|
||||
if not obj:
|
||||
obj = TBL(table_name='flights')
|
||||
obj.main_dttm_col = 'ds'
|
||||
obj.database = get_or_create_main_db()
|
||||
db.session.merge(obj)
|
||||
db.session.commit()
|
||||
obj.fetch_metadata()
|
||||
|
|
Binary file not shown.
Binary file not shown.
104
superset/viz.py
104
superset/viz.py
|
@ -1730,7 +1730,111 @@ class MapboxViz(BaseViz):
|
|||
}
|
||||
|
||||
|
||||
class BaseDeckGLViz(BaseViz):
|
||||
|
||||
"""Base class for deck.gl visualizations"""
|
||||
|
||||
is_timeseries = False
|
||||
credits = '<a href="https://uber.github.io/deck.gl/">deck.gl</a>'
|
||||
|
||||
def get_metrics(self):
|
||||
self.metric = self.form_data.get('size')
|
||||
return [self.metric]
|
||||
|
||||
def get_properties(self, d):
|
||||
return {
|
||||
'weight': d.get(self.metric) or 1,
|
||||
}
|
||||
|
||||
def get_position(self, d):
|
||||
return [
|
||||
d.get(self.form_data.get('longitude')),
|
||||
d.get(self.form_data.get('latitude')),
|
||||
]
|
||||
|
||||
def query_obj(self):
|
||||
d = super(BaseDeckGLViz, self).query_obj()
|
||||
fd = self.form_data
|
||||
|
||||
d['groupby'] = [fd.get('longitude'), fd.get('latitude')]
|
||||
if fd.get('dimension'):
|
||||
d['groupby'] += [fd.get('dimension')]
|
||||
|
||||
d['metrics'] = self.get_metrics()
|
||||
return d
|
||||
|
||||
def get_data(self, df):
|
||||
features = []
|
||||
for d in df.to_dict(orient='records'):
|
||||
d = dict(position=self.get_position(d), **self.get_properties(d))
|
||||
features.append(d)
|
||||
return {
|
||||
"features": features,
|
||||
"mapboxApiKey": config.get('MAPBOX_API_KEY'),
|
||||
}
|
||||
|
||||
|
||||
class DeckScatterViz(BaseDeckGLViz):
|
||||
|
||||
"""deck.gl's ScatterLayer"""
|
||||
|
||||
viz_type = "deck_scatter"
|
||||
verbose_name = _("Deck.gl - Scatter plot")
|
||||
|
||||
def query_obj(self):
|
||||
self.point_radius_fixed = self.form_data.get('point_radius_fixed')
|
||||
return super(DeckScatterViz, self).query_obj()
|
||||
|
||||
def get_metrics(self):
|
||||
if self.point_radius_fixed.get('type') == 'metric':
|
||||
self.metric = self.point_radius_fixed.get('value')
|
||||
else:
|
||||
self.metric = 'count'
|
||||
return [self.metric]
|
||||
|
||||
def get_properties(self, d):
|
||||
return {
|
||||
"radius": self.fixed_value if self.fixed_value else d.get(self.metric),
|
||||
"cat_color": d.get(self.dim) if self.dim else None,
|
||||
}
|
||||
|
||||
def get_data(self, df):
|
||||
fd = self.form_data
|
||||
self.point_radius_fixed = fd.get('point_radius_fixed')
|
||||
self.fixed_value = None
|
||||
self.dim = self.form_data.get('dimension')
|
||||
if self.point_radius_fixed.get('type') != 'metric':
|
||||
self.fixed_value = self.point_radius_fixed.get('value')
|
||||
|
||||
return super(DeckScatterViz, self).get_data(df)
|
||||
|
||||
|
||||
class DeckScreengrid(BaseDeckGLViz):
|
||||
|
||||
"""deck.gl's ScreenGridLayer"""
|
||||
|
||||
viz_type = "deck_screengrid"
|
||||
verbose_name = _("Deck.gl - Screen Grid")
|
||||
|
||||
|
||||
class DeckGrid(BaseDeckGLViz):
|
||||
|
||||
"""deck.gl's DeckLayer"""
|
||||
|
||||
viz_type = "deck_grid"
|
||||
verbose_name = _("Deck.gl - 3D Grid")
|
||||
|
||||
|
||||
class DeckHex(BaseDeckGLViz):
|
||||
|
||||
"""deck.gl's DeckLayer"""
|
||||
|
||||
viz_type = "deck_hex"
|
||||
verbose_name = _("Deck.gl - 3D HEX")
|
||||
|
||||
|
||||
class EventFlowViz(BaseViz):
|
||||
|
||||
"""A visualization to explore patterns in event sequences"""
|
||||
|
||||
viz_type = 'event_flow'
|
||||
|
|
Loading…
Reference in New Issue