Skip to content

Commit

Permalink
ZIP code viz
Browse files Browse the repository at this point in the history
(cherry picked from commit cafae6407f3b361138d1ed87b2a6c54988c0e6d5)
  • Loading branch information
betodealmeida authored and hughhhh committed Aug 22, 2018
1 parent 4bc5b83 commit 6f68520
Show file tree
Hide file tree
Showing 10 changed files with 355 additions and 6 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions superset/assets/src/explore/visTypes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,54 @@ export const visTypes = {
],
},

deck_zipcodes: {
label: t('Deck.gl - Zip codes'),
requiresTime: true,
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['geojson', 'autozoom'],
['row_limit', null],
['color_picker', 'size'],
['adhoc_filters'],
],
},
{
label: t('Map'),
controlSetRows: [
['mapbox_style', 'viewport'],
// TODO ['autozoom', null],
],
},
{
label: t('Advanced'),
controlSetRows: [
['js_columns'],
['js_data_mutator'],
['js_tooltip'],
['js_onclick_href'],
],
},
],
controlOverrides: {
adhoc_filters: {
validators: [v.nonEmpty],
},
geojson: {
label: t('ZIP code'),
description: t('Column with ZIP codes'),
},
size: {
label: t('Weight'),
description: t("Metric used as a weight for the grid's coloring"),
validators: [v.nonEmpty],
},
time_grain_sqla: timeGrainSqlaAnimationOverrides,
},
},

deck_polygon: {
label: t('Deck.gl - Polygon'),
requiresTime: true,
Expand Down
7 changes: 5 additions & 2 deletions superset/assets/src/visualizations/PlaySlider.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
.play-slider {
position: absolute;
bottom: -16px;
height: 20px;
width: 100%;
width: 90%;
}

.slider-selection {
Expand All @@ -21,3 +20,7 @@
color: #b3b3b3;
margin-right: 5px;
}

div.tooltip.tooltip-main.top.in {
margin-left: 0 !important;
}
11 changes: 9 additions & 2 deletions superset/assets/src/visualizations/PlaySlider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const propTypes = {
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
reversed: PropTypes.bool,
disabled: PropTypes.bool,
range: PropTypes.bool,
};

const defaultProps = {
Expand All @@ -30,6 +31,7 @@ const defaultProps = {
orientation: 'horizontal',
reversed: false,
disabled: false,
range: true,
};

export default class PlaySlider extends React.PureComponent {
Expand Down Expand Up @@ -87,7 +89,11 @@ export default class PlaySlider extends React.PureComponent {
if (this.props.disabled) {
return;
}
let values = this.props.values.map(value => value + this.increment);
let values = this.props.values;
if (!Array.isArray(values)) {
values = [values, values + this.props.step];
}
values = values.map(value => value + this.increment);
if (values[1] > this.props.end) {
const cr = values[0] - this.props.start;
values = values.map(value => value - cr);
Expand Down Expand Up @@ -116,7 +122,8 @@ export default class PlaySlider extends React.PureComponent {
</Col>
<Col md={11} className="padded">
<ReactBootstrapSlider
value={this.props.values}
value={this.props.range ? this.props.values : this.props.values[0]}
range={this.props.range}
formatter={this.formatter}
change={this.onChange}
min={this.props.start}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ const propTypes = {
end: PropTypes.number.isRequired,
step: PropTypes.number.isRequired,
values: PropTypes.array.isRequired,
aggregation: PropTypes.bool,
disabled: PropTypes.bool,
viewport: PropTypes.object.isRequired,
children: PropTypes.node,
};

const defaultProps = {
aggregation: false,
disabled: false,
step: 1,
};
Expand All @@ -26,10 +28,20 @@ export default class AnimatableDeckGLContainer extends React.Component {
const { getLayers, start, end, step, values, disabled, viewport, ...other } = props;
this.state = { values, viewport };
this.other = other;
this.onChange = this.onChange.bind(this);
}
componentWillReceiveProps(nextProps) {
this.setState({ values: nextProps.values, viewport: nextProps.viewport });
}
onChange(newValues) {
let values;
if (!Array.isArray(newValues)) {
values = [newValues, newValues + this.props.step];
} else {
values = newValues;
}
this.setState({ values });
}
render() {
const layers = this.props.getLayers(this.state.values);
return (
Expand All @@ -46,7 +58,8 @@ export default class AnimatableDeckGLContainer extends React.Component {
end={this.props.end}
step={this.props.step}
values={this.state.values}
onChange={newValues => this.setState({ values: newValues })}
range={!this.props.aggregation}
onChange={this.onChange}
/>
}
{this.props.children}
Expand Down
2 changes: 2 additions & 0 deletions superset/assets/src/visualizations/deckgl/layers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getLayer as deck_scatter } from './scatter';
import { getLayer as deck_geojson } from './geojson';
import { getLayer as deck_arc } from './arc';
import { getLayer as deck_polygon } from './polygon';
import { getLayer as deck_zipcodes } from './zipcodes';

const layerGenerators = {
deck_grid,
Expand All @@ -17,5 +18,6 @@ const layerGenerators = {
deck_geojson,
deck_arc,
deck_polygon,
deck_zipcodes,
};
export default layerGenerators;
199 changes: 199 additions & 0 deletions superset/assets/src/visualizations/deckgl/layers/zipcodes.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

import { GeoJsonLayer } from 'deck.gl';

import AnimatableDeckGLContainer from '../AnimatableDeckGLContainer';

import * as common from './common';
import { getPlaySliderParams } from '../../../modules/time';
import sandboxedEval from '../../../modules/sandbox';

function getPoints(geojson) {
const points = [];
Object.values(geojson).forEach((geometry) => {
geometry.coordinates.forEach((polygon) => {
polygon.forEach((coordinates) => {
coordinates.forEach((point) => {
points.push(point);
});
});
});
});
return points;
}

function getLayer(formData, payload, slice, filters) {
const fd = formData;
let data = payload.data.features.map(d => ({ ...d, geometry: payload.data.geojson[d.zipcode] }));
data = data.filter(d => d.geometry !== undefined);

if (filters != null) {
filters.forEach((f) => {
data = data.filter(f);
});
}

// find values range
let minValue = Infinity;
let maxValue = -Infinity;
data.forEach((d) => {
if (d.geometry !== null) {
minValue = Math.min(minValue, d.metric);
maxValue = Math.max(maxValue, d.metric);
}
});

const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
data = data.map(d => ({
...d,
properties: {
color: [c.r, c.g, c.b, 255 * c.a * (d.metric - minValue) / (maxValue - minValue)],
metric: d.metric,
zipcode: d.zipcode,
},
}));

if (fd.js_data_mutator) {
// Applying user defined data mutator if defined
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
data = jsFnMutator(data);
}

const layerProps = common.commonLayerProps(fd, slice);
if (layerProps.onHover === undefined) {
layerProps.pickable = true;
layerProps.onHover = (o) => {
if (o.picked) {
slice.setTooltip({
content: 'ZIP code: ' + o.object.zipcode + '<br />Metric: ' + o.object.metric,
x: o.x,
y: o.y + 75, // weird offset
});
} else {
slice.setTooltip(null);
}
};
}

return new GeoJsonLayer({
id: `zipcodes-layer-${fd.slice_id}`,
data,
pickable: true,
stroked: true,
filled: true,
extruded: false,
lineWidthScale: 20,
lineWidthMinPixels: 1,
getFillColor: d => d.properties.color,
getLineColor: [0, 0, 0, 100],
getRadius: 100,
getLineWidth: 1,
getElevation: 30,
...layerProps,
});
}

const propTypes = {
slice: PropTypes.object.isRequired,
payload: PropTypes.object.isRequired,
setControlValue: PropTypes.func.isRequired,
viewport: PropTypes.object.isRequired,
};

class DeckGLZipCodes extends React.PureComponent {
/* eslint-disable-next-line react/sort-comp */
static getDerivedStateFromProps(nextProps) {
const fd = nextProps.slice.formData;
const features = nextProps.payload.data.features || [];

const timeGrain = fd.time_grain_sqla || fd.granularity || 'PT1M';
const timestamps = features.map(f => f.__timestamp);
const { start, end, step, values, disabled } = getPlaySliderParams(timestamps, timeGrain);

return { start, end, step, values, disabled };
}
constructor(props) {
super(props);
this.state = DeckGLZipCodes.getDerivedStateFromProps(props);

this.getLayers = this.getLayers.bind(this);
}
componentWillReceiveProps(nextProps) {
this.setState(DeckGLZipCodes.getDerivedStateFromProps(nextProps, this.state));
}
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.slice.formData,
this.props.payload,
this.props.slice,
filters);

return [layer];
}
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}
aggregation
/>
</div>
);
}
}

DeckGLZipCodes.propTypes = propTypes;

function deckZipCodes(slice, payload, setControlValue) {
const fd = slice.formData;
let viewport = {
...fd.viewport,
width: slice.width(),
height: slice.height(),
};

if (fd.autozoom && payload.data.geojson) {
viewport = common.fitViewport(viewport, getPoints(payload.data.geojson));
}

ReactDOM.render(
<DeckGLZipCodes
slice={slice}
payload={payload}
setControlValue={setControlValue}
viewport={viewport}
/>,
document.getElementById(slice.containerId),
);
}

module.exports = {
default: deckZipCodes,
getLayer,
};
3 changes: 3 additions & 0 deletions superset/assets/src/visualizations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const VIZ_TYPES = {
deck_multi: 'deck_multi',
deck_arc: 'deck_arc',
deck_polygon: 'deck_polygon',
deck_zipcodes: 'deck_zipcodes',
rose: 'rose',
};

Expand Down Expand Up @@ -136,6 +137,8 @@ const vizMap = {
loadVis(import(/* webpackChunkName: "deckgl/layers/polygon" */ './deckgl/layers/polygon.jsx')),
[VIZ_TYPES.deck_multi]: () =>
loadVis(import(/* webpackChunkName: "deckgl/multi" */ './deckgl/multi.jsx')),
[VIZ_TYPES.deck_zipcodes]: () =>
loadVis(import(/* webpackChunkName: "deckgl/layers/zipcodes" */ './deckgl/layers/zipcodes.jsx')),
[VIZ_TYPES.rose]: () => loadVis(import(/* webpackChunkName: "rose" */ './rose.js')),
};

Expand Down
Loading

0 comments on commit 6f68520

Please sign in to comment.