[deck polygon] add support for geohash (#5712)

* [deck polygon] add support for geohash

+ improvements:
* added autozoom support
* support for metric & aggregations (only aggregates if metric is picked)
* fixed stroke
* fixed opacity
* introduced a SliderControl

* addressing comments, fixing build

* Addressing comments
This commit is contained in:
Maxime Beauchemin 2018-08-28 11:09:10 -07:00 committed by Beto Dealmeida
parent f0beb3ab80
commit 60ecd72aac
18 changed files with 343 additions and 119 deletions

View File

@ -61,7 +61,7 @@
"d3-tip": "^0.9.1", "d3-tip": "^0.9.1",
"datamaps": "^0.5.8", "datamaps": "^0.5.8",
"datatables.net-bs": "^1.10.15", "datatables.net-bs": "^1.10.15",
"deck.gl": "^5.1.4", "deck.gl": "^5.3.4",
"distributions": "^1.0.0", "distributions": "^1.0.0",
"dnd-core": "^2.6.0", "dnd-core": "^2.6.0",
"dompurify": "^1.0.3", "dompurify": "^1.0.3",
@ -72,7 +72,6 @@
"jed": "^1.1.1", "jed": "^1.1.1",
"jquery": "3.1.1", "jquery": "3.1.1",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"luma.gl": "^5.1.4",
"mapbox-gl": "^0.45.0", "mapbox-gl": "^0.45.0",
"mathjs": "^3.20.2", "mathjs": "^3.20.2",
"moment": "^2.20.1", "moment": "^2.20.1",

View File

@ -189,7 +189,7 @@ class Chart extends React.PureComponent {
className="chart-tooltip" className="chart-tooltip"
id="chart-tooltip" id="chart-tooltip"
placement="right" placement="right"
positionTop={this.state.tooltip.y - 10} positionTop={this.state.tooltip.y + 30}
positionLeft={this.state.tooltip.x + 30} positionLeft={this.state.tooltip.x + 30}
arrowOffsetTop={10} arrowOffsetTop={10}
> >

View File

@ -0,0 +1,8 @@
.BootstrapSliderWrapper .slider-selection {
background: #efefef;
}
.BootstrapSliderWrapper .slider-handle {
background: #b3b3b3;
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import ReactBootstrapSlider from 'react-bootstrap-slider';
import 'bootstrap-slider/dist/css/bootstrap-slider.min.css';
import './BootstrapSliderWrapper.css';
export default function BootstrapSliderWrapper(props) {
return (
<span className="BootstrapSliderWrapper">
<ReactBootstrapSlider {...props} />
</span>
);
}

View File

@ -95,46 +95,34 @@ export default function dashboardStateReducer(state = {}, action) {
let filters = state.filters; let filters = state.filters;
const { chart, col, vals: nextVals, merge, refresh } = action; const { chart, col, vals: nextVals, merge, refresh } = action;
const sliceId = chart.id; const sliceId = chart.id;
const filterKeys = [ let newFilter = {};
'__time_range', if (!(sliceId in filters)) {
'__time_col', // if no filters existed for the slice, set them
'__time_grain', newFilter = { [col]: nextVals };
'__time_origin', } else if ((filters[sliceId] && !(col in filters[sliceId])) || !merge) {
'__granularity', // If no filters exist for this column, or we are overwriting them
]; newFilter = { ...filters[sliceId], [col]: nextVals };
if ( } else if (filters[sliceId][col] instanceof Array) {
filterKeys.indexOf(col) >= 0 || newFilter[col] = [...filters[sliceId][col], ...nextVals];
action.chart.formData.groupby.indexOf(col) !== -1 } else {
) { newFilter[col] = [filters[sliceId][col], ...nextVals];
let newFilter = {}; }
if (!(sliceId in filters)) { filters = { ...filters, [sliceId]: newFilter };
// if no filters existed for the slice, set them
newFilter = { [col]: nextVals };
} else if ((filters[sliceId] && !(col in filters[sliceId])) || !merge) {
// If no filters exist for this column, or we are overwriting them
newFilter = { ...filters[sliceId], [col]: nextVals };
} else if (filters[sliceId][col] instanceof Array) {
newFilter[col] = [...filters[sliceId][col], ...nextVals];
} else {
newFilter[col] = [filters[sliceId][col], ...nextVals];
}
filters = { ...filters, [sliceId]: newFilter };
// remove any empty filters so they don't pollute the logs // remove any empty filters so they don't pollute the logs
Object.keys(filters).forEach(chartId => { Object.keys(filters).forEach(chartId => {
Object.keys(filters[chartId]).forEach(column => { Object.keys(filters[chartId]).forEach(column => {
if ( if (
!filters[chartId][column] || !filters[chartId][column] ||
filters[chartId][column].length === 0 filters[chartId][column].length === 0
) { ) {
delete filters[chartId][column]; delete filters[chartId][column];
}
});
if (Object.keys(filters[chartId]).length === 0) {
delete filters[chartId];
} }
}); });
} if (Object.keys(filters[chartId]).length === 0) {
delete filters[chartId];
}
});
return { ...state, filters, refresh }; return { ...state, filters, refresh };
}, },
[SET_UNSAVED_CHANGES]() { [SET_UNSAVED_CHANGES]() {

View File

@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import BootstrapSliderWrapper from '../../../components/BootstrapSliderWrapper';
import ControlHeader from '../ControlHeader';
const propTypes = {
onChange: PropTypes.func,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
};
const defaultProps = {
onChange: () => {},
};
export default function SliderControl(props) {
// This wouldn't be necessary but might as well
return (
<div>
<ControlHeader {...props} />
<BootstrapSliderWrapper
{...props}
change={(obj) => {
props.onChange(obj.target.value);
}}
/>
</div>
);
}
SliderControl.propTypes = propTypes;
SliderControl.defaultProps = defaultProps;

View File

@ -10,6 +10,7 @@ import FixedOrMetricControl from './FixedOrMetricControl';
import HiddenControl from './HiddenControl'; import HiddenControl from './HiddenControl';
import SelectAsyncControl from './SelectAsyncControl'; import SelectAsyncControl from './SelectAsyncControl';
import SelectControl from './SelectControl'; import SelectControl from './SelectControl';
import SliderControl from './SliderControl';
import SpatialControl from './SpatialControl'; import SpatialControl from './SpatialControl';
import TextAreaControl from './TextAreaControl'; import TextAreaControl from './TextAreaControl';
import TextControl from './TextControl'; import TextControl from './TextControl';
@ -32,6 +33,7 @@ const controlMap = {
HiddenControl, HiddenControl,
SelectAsyncControl, SelectAsyncControl,
SelectControl, SelectControl,
SliderControl,
SpatialControl, SpatialControl,
TextAreaControl, TextAreaControl,
TextControl, TextControl,

View File

@ -1470,10 +1470,10 @@ export const controls = {
table_filter: { table_filter: {
type: 'CheckboxControl', type: 'CheckboxControl',
label: t('Table Filter'), label: t('Emit Filter Events'),
renderTrigger: true, renderTrigger: true,
default: false, default: false,
description: t('Whether to apply filter when table cell is clicked'), description: t('Whether to apply filter when items are clicked'),
}, },
align_pn: { align_pn: {
@ -1777,6 +1777,17 @@ export const controls = {
'Between 0 and 1.'), 'Between 0 and 1.'),
}, },
opacity: {
type: 'SliderControl',
label: t('Opacity'),
default: 80,
step: 1,
min: 0,
max: 100,
renderTrigger: true,
description: t('Opacity, expects values between 0 and 100'),
},
viewport: { viewport: {
type: 'ViewportControl', type: 'ViewportControl',
label: t('Viewport'), label: t('Viewport'),
@ -2121,6 +2132,7 @@ export const controls = {
choices: [ choices: [
['polyline', 'Polyline'], ['polyline', 'Polyline'],
['json', 'JSON'], ['json', 'JSON'],
['geohash', 'geohash (square)'],
], ],
}, },
@ -2242,7 +2254,7 @@ export const controls = {
label: t('Filled'), label: t('Filled'),
renderTrigger: true, renderTrigger: true,
description: t('Whether to fill the objects'), description: t('Whether to fill the objects'),
default: false, default: true,
}, },
normalized: { normalized: {

View File

@ -605,6 +605,14 @@ export const visTypes = {
], ],
}, },
], ],
controlOverrides: {
line_type: {
choices: [
['polyline', 'Polyline'],
['json', 'JSON'],
],
},
},
}, },
deck_screengrid: { deck_screengrid: {
@ -703,25 +711,31 @@ export const visTypes = {
label: t('Query'), label: t('Query'),
expanded: true, expanded: true,
controlSetRows: [ controlSetRows: [
['line_column', 'line_type'],
['row_limit', 'filter_nulls'],
['adhoc_filters'], ['adhoc_filters'],
['metric'],
['row_limit', null],
['line_column', 'line_type'],
['reverse_long_lat', 'filter_nulls'],
], ],
}, },
{ {
label: t('Map'), label: t('Map'),
expanded: true,
controlSetRows: [ controlSetRows: [
['mapbox_style', 'viewport'], ['mapbox_style', 'viewport'],
['reverse_long_lat', null], ['autozoom', null],
], ],
}, },
{ {
label: t('Polygon Settings'), label: t('Polygon Settings'),
expanded: true,
controlSetRows: [ controlSetRows: [
['fill_color_picker', 'stroke_color_picker'], ['fill_color_picker', 'stroke_color_picker'],
['filled', 'stroked'], ['filled', 'stroked'],
['extruded', null], ['extruded', null],
['point_radius_scale', null], ['line_width', null],
['linear_color_scheme', 'opacity'],
['table_filter', null],
], ],
}, },
{ {
@ -734,6 +748,17 @@ export const visTypes = {
], ],
}, },
], ],
controlOverrides: {
metric: {
validators: [],
},
line_column: {
label: t('Polygon Column'),
},
line_type: {
label: t('Polygon Encoding'),
},
},
}, },
deck_arc: { deck_arc: {

View File

@ -525,6 +525,16 @@ export const spectrums = {
], ],
}; };
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];
}
/** /**
* Get a color from a scheme specific palette (scheme) * Get a color from a scheme specific palette (scheme)
* The function cycles through the palette while memoizing labels * The function cycles through the palette while memoizing labels
@ -566,7 +576,7 @@ export const getColorFromScheme = (function () {
}; };
}()); }());
export const colorScalerFactory = function (colors, data, accessor, extents) { export const colorScalerFactory = function (colors, data, accessor, extents, outputRGBA = false) {
// Returns a linear scaler our of an array of color // Returns a linear scaler our of an array of color
if (!Array.isArray(colors)) { if (!Array.isArray(colors)) {
/* eslint no-param-reassign: 0 */ /* eslint no-param-reassign: 0 */
@ -581,15 +591,9 @@ export const colorScalerFactory = function (colors, data, accessor, extents) {
} }
const chunkSize = (ext[1] - ext[0]) / (colors.length - 1); const chunkSize = (ext[1] - ext[0]) / (colors.length - 1);
const points = colors.map((col, i) => ext[0] + (i * chunkSize)); const points = colors.map((col, i) => ext[0] + (i * chunkSize));
return d3.scale.linear().domain(points).range(colors).clamp(true); const scaler = d3.scale.linear().domain(points).range(colors).clamp(true);
}; if (outputRGBA) {
return v => hexToRGB(scaler(v));
export function hexToRGB(hex, alpha = 255) {
if (!hex) {
return [0, 0, 0, alpha];
} }
const r = parseInt(hex.slice(1, 3), 16); return scaler;
const g = parseInt(hex.slice(3, 5), 16); };
const b = parseInt(hex.slice(5, 7), 16);
return [r, g, b, alpha];
}

View File

@ -5,14 +5,6 @@
margin: 0; margin: 0;
} }
.slider-selection {
background: #efefef;
}
.slider-handle {
background: #b3b3b3;
}
.slider.slider-horizontal { .slider.slider-horizontal {
width: 100% !important; width: 100% !important;
} }

View File

@ -4,8 +4,7 @@ import { Row, Col } from 'react-bootstrap';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import 'bootstrap-slider/dist/css/bootstrap-slider.min.css'; import BootrapSliderWrapper from '../components/BootstrapSliderWrapper';
import ReactBootstrapSlider from 'react-bootstrap-slider';
import './PlaySlider.css'; import './PlaySlider.css';
import { t } from '../locales'; import { t } from '../locales';
@ -120,7 +119,7 @@ export default class PlaySlider extends React.PureComponent {
<i className="fa fa-step-forward fa-lg slider-button " onClick={this.step} /> <i className="fa fa-step-forward fa-lg slider-button " onClick={this.step} />
</Col> </Col>
<Col md={11} className="padded"> <Col md={11} className="padded">
<ReactBootstrapSlider <BootrapSliderWrapper
value={range ? values : values[0]} value={range ? values : values[0]}
range={range} range={range}
formatter={this.formatter} formatter={this.formatter}

View File

@ -7,7 +7,7 @@ import 'mapbox-gl/dist/mapbox-gl.css';
const propTypes = { const propTypes = {
viewport: PropTypes.object.isRequired, viewport: PropTypes.object.isRequired,
layers: PropTypes.array.isRequired, layers: PropTypes.array.isRequired,
setControlValue: PropTypes.func.isRequired, setControlValue: PropTypes.func,
mapStyle: PropTypes.string, mapStyle: PropTypes.string,
mapboxApiAccessToken: PropTypes.string.isRequired, mapboxApiAccessToken: PropTypes.string.isRequired,
onViewportChange: PropTypes.func, onViewportChange: PropTypes.func,
@ -15,6 +15,7 @@ const propTypes = {
const defaultProps = { const defaultProps = {
mapStyle: 'light', mapStyle: 'light',
onViewportChange: () => {}, onViewportChange: () => {},
setControlValue: () => {},
}; };
export default class DeckGLContainer extends React.Component { export default class DeckGLContainer extends React.Component {

View File

@ -34,12 +34,18 @@ export function fitViewport(viewport, points, padding = 10) {
export function commonLayerProps(formData, slice) { export function commonLayerProps(formData, slice) {
const fd = formData; const fd = formData;
let onHover; let onHover;
let tooltipContentGenerator;
if (fd.js_tooltip) { if (fd.js_tooltip) {
const jsTooltip = sandboxedEval(fd.js_tooltip); const unsanitizedTooltipGenerator = sandboxedEval(fd.js_tooltip);
tooltipContentGenerator = o => dompurify.sanitize(unsanitizedTooltipGenerator(o));
} else if (fd.line_column && fd.line_type === 'geohash') {
tooltipContentGenerator = o => `${fd.line_column}: ${o.object[fd.line_column]}`;
}
if (tooltipContentGenerator) {
onHover = (o) => { onHover = (o) => {
if (o.picked) { if (o.picked) {
slice.setTooltip({ slice.setTooltip({
content: dompurify.sanitize(jsTooltip(o)), content: tooltipContentGenerator(o),
x: o.x, x: o.x,
y: o.y, y: o.y,
}); });
@ -54,6 +60,8 @@ export function commonLayerProps(formData, slice) {
const href = sandboxedEval(fd.js_onclick_href)(o); const href = sandboxedEval(fd.js_onclick_href)(o);
window.open(href); window.open(href);
}; };
} else if (fd.table_filter && fd.line_type === 'geohash') {
onClick = o => slice.addFilter(fd.line_column, [o.object[fd.line_column]], false);
} }
return { return {
onClick, onClick,

View File

@ -2,19 +2,36 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { PolygonLayer } from 'deck.gl'; import { PolygonLayer } from 'deck.gl';
import _ from 'underscore';
import d3 from 'd3';
import DeckGLContainer from './../DeckGLContainer'; import DeckGLContainer from './../DeckGLContainer';
import * as common from './common'; import * as common from './common';
import { colorScalerFactory } from '../../../modules/colors';
import sandboxedEval from '../../../modules/sandbox'; import sandboxedEval from '../../../modules/sandbox';
function getPoints(features) {
return _.flatten(features.map(d => d.polygon), true);
}
function getLayer(formData, payload, slice) { function getLayer(formData, payload, slice) {
const fd = formData; const fd = formData;
const fc = fd.fill_color_picker; const fc = fd.fill_color_picker;
let data = payload.data.features.map(d => ({ const sc = fd.stroke_color_picker;
...d, let data = [...payload.data.features];
fillColor: [fc.r, fc.g, fc.b, 255 * fc.a], const mainMetric = payload.data.metricLabels.length ? payload.data.metricLabels[0] : null;
}));
let colorScaler;
if (mainMetric) {
const ext = d3.extent(data, d => d[mainMetric]);
const scaler = colorScalerFactory(fd.linear_color_scheme, null, null, ext, true);
colorScaler = (d) => {
const c = scaler(d[mainMetric]);
c[3] = (fd.opacity / 100.0) * 255;
return c;
};
}
if (fd.js_data_mutator) { if (fd.js_data_mutator) {
// Applying user defined data mutator if defined // Applying user defined data mutator if defined
@ -26,19 +43,29 @@ function getLayer(formData, payload, slice) {
id: `path-layer-${fd.slice_id}`, id: `path-layer-${fd.slice_id}`,
data, data,
filled: fd.filled, filled: fd.filled,
stroked: fd.stoked, stroked: fd.stroked,
getFillColor: colorScaler || [fc.r, fc.g, fc.b, 255 * fc.a],
getLineColor: [sc.r, sc.g, sc.b, 255 * sc.a],
getLineWidth: fd.line_width,
extruded: fd.extruded, extruded: fd.extruded,
fp64: true,
...common.commonLayerProps(fd, slice), ...common.commonLayerProps(fd, slice),
}); });
} }
function deckPolygon(slice, payload, setControlValue) { function deckPolygon(slice, payload, setControlValue) {
const layer = getLayer(slice.formData, payload, slice); const layer = getLayer(slice.formData, payload, slice);
const viewport = { const fd = slice.formData;
let viewport = {
...slice.formData.viewport, ...slice.formData.viewport,
width: slice.width(), width: slice.width(),
height: slice.height(), height: slice.height(),
}; };
if (fd.autozoom) {
viewport = common.fitViewport(viewport, getPoints(payload.data.features));
}
ReactDOM.render( ReactDOM.render(
<DeckGLContainer <DeckGLContainer
mapboxApiAccessToken={payload.data.mapboxApiKey} mapboxApiAccessToken={payload.data.mapboxApiKey}

View File

@ -230,9 +230,9 @@
d3-array "^1.2.0" d3-array "^1.2.0"
prop-types "^15.5.10" prop-types "^15.5.10"
"@deck.gl/core@^5.3.1": "@deck.gl/core@^5.3.3":
version "5.3.1" version "5.3.3"
resolved "https://registry.yarnpkg.com/@deck.gl/core/-/core-5.3.1.tgz#acfc1e5fefd3b12e9142419b0aeb77c07885626c" resolved "https://registry.yarnpkg.com/@deck.gl/core/-/core-5.3.3.tgz#a13c07e5fa3e22297fd450d6da8ab9aac334b1f0"
dependencies: dependencies:
luma.gl "^5.3.0" luma.gl "^5.3.0"
math.gl "^1.2.1" math.gl "^1.2.1"
@ -241,19 +241,19 @@
seer "^0.2.4" seer "^0.2.4"
viewport-mercator-project "^5.1.0" viewport-mercator-project "^5.1.0"
"@deck.gl/layers@^5.3.2": "@deck.gl/layers@^5.3.4":
version "5.3.2" version "5.3.4"
resolved "https://registry.yarnpkg.com/@deck.gl/layers/-/layers-5.3.2.tgz#c76b9a7890305a5d6a0fdd56bd0d0d68d4046f6f" resolved "https://registry.yarnpkg.com/@deck.gl/layers/-/layers-5.3.4.tgz#ab3de1bf8bb68d67772642acbb4e0f87f4f11300"
dependencies: dependencies:
"@deck.gl/core" "^5.3.1" "@deck.gl/core" "^5.3.3"
d3-hexbin "^0.2.1" d3-hexbin "^0.2.1"
earcut "^2.0.6" earcut "^2.0.6"
"@deck.gl/react@^5.3.1": "@deck.gl/react@^5.3.3":
version "5.3.1" version "5.3.3"
resolved "https://registry.yarnpkg.com/@deck.gl/react/-/react-5.3.1.tgz#0c16fac59061924eb3509dea06c837bcef8044f2" resolved "https://registry.yarnpkg.com/@deck.gl/react/-/react-5.3.3.tgz#e7352934f6742d3ce672a394cbff312aab5ccaa0"
dependencies: dependencies:
"@deck.gl/core" "^5.3.1" "@deck.gl/core" "^5.3.3"
prop-types "^15.6.0" prop-types "^15.6.0"
"@mapbox/geojson-area@0.2.2": "@mapbox/geojson-area@0.2.2":
@ -830,6 +830,12 @@ acorn@^5.0.0, acorn@^5.5.0, acorn@^5.6.2:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
add-dom-event-listener@1.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.0.2.tgz#8faed2c41008721cf111da1d30d995b85be42bed"
dependencies:
object-assign "4.x"
ajv-keywords@^2.1.0: ajv-keywords@^2.1.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
@ -1951,19 +1957,19 @@ babel-register@^6.24.1, babel-register@^6.26.0, babel-register@^6.9.0:
mkdirp "^0.5.1" mkdirp "^0.5.1"
source-map-support "^0.4.15" source-map-support "^0.4.15"
babel-runtime@^5.6.18: babel-runtime@6.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0:
version "5.8.38"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-5.8.38.tgz#1c0b02eb63312f5f087ff20450827b425c9d4c19"
dependencies:
core-js "^1.0.0"
babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
dependencies: dependencies:
core-js "^2.4.0" core-js "^2.4.0"
regenerator-runtime "^0.11.0" regenerator-runtime "^0.11.0"
babel-runtime@^5.6.18:
version "5.8.38"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-5.8.38.tgz#1c0b02eb63312f5f087ff20450827b425c9d4c19"
dependencies:
core-js "^1.0.0"
babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
@ -2578,7 +2584,7 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
classnames@^2.1.2, classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5: classnames@^2.1.2, classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5, classnames@^2.2.6:
version "2.2.6" version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
@ -2818,10 +2824,20 @@ complex.js@2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.4.tgz#d8e7cfb9652d1e853e723386421c1a0ca7a48373" resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.4.tgz#d8e7cfb9652d1e853e723386421c1a0ca7a48373"
component-classes@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/component-classes/-/component-classes-1.2.6.tgz#c642394c3618a4d8b0b8919efccbbd930e5cd691"
dependencies:
component-indexof "0.0.3"
component-emitter@^1.2.1: component-emitter@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
component-indexof@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24"
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -3012,6 +3028,13 @@ crypto-random-string@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
css-animation@^1.3.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/css-animation/-/css-animation-1.4.1.tgz#5b8813125de0fbbbb0bbe1b472ae84221469b7a8"
dependencies:
babel-runtime "6.x"
component-classes "^1.2.5"
css-color-names@0.0.4: css-color-names@0.0.4:
version "0.0.4" version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@ -3397,13 +3420,13 @@ decimal.js@9.0.1:
version "9.0.1" version "9.0.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-9.0.1.tgz#1cc8b228177da7ab6498c1cc06eb130a290e6e1e" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-9.0.1.tgz#1cc8b228177da7ab6498c1cc06eb130a290e6e1e"
deck.gl@^5.1.4: deck.gl@^5.3.4:
version "5.3.2" version "5.3.4"
resolved "https://registry.yarnpkg.com/deck.gl/-/deck.gl-5.3.2.tgz#2297d820fb8fb02eab95ac6a03261a689500bc11" resolved "https://registry.yarnpkg.com/deck.gl/-/deck.gl-5.3.4.tgz#35e5a7087ef0d8ca7811d06a721ea289edbe7c24"
dependencies: dependencies:
"@deck.gl/core" "^5.3.1" "@deck.gl/core" "^5.3.3"
"@deck.gl/layers" "^5.3.2" "@deck.gl/layers" "^5.3.4"
"@deck.gl/react" "^5.3.1" "@deck.gl/react" "^5.3.3"
decode-uri-component@^0.2.0: decode-uri-component@^0.2.0:
version "0.2.0" version "0.2.0"
@ -3586,6 +3609,10 @@ doctrine@^2.1.0:
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
dom-align@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.8.0.tgz#c0e89b5b674c6e836cd248c52c2992135f093654"
"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.2.0, dom-helpers@^3.2.1: "dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.2.0, dom-helpers@^3.2.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
@ -6384,7 +6411,7 @@ lodash.isplainobject@^4.0.6:
version "4.0.6" version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
lodash.keys@^3.0.0: lodash.keys@^3.0.0, lodash.keys@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
dependencies: dependencies:
@ -6518,7 +6545,7 @@ lru-cache@^4.0.1, lru-cache@^4.1.1:
pseudomap "^1.0.2" pseudomap "^1.0.2"
yallist "^2.1.2" yallist "^2.1.2"
luma.gl@^5.1.4, luma.gl@^5.3.0: luma.gl@^5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/luma.gl/-/luma.gl-5.3.0.tgz#a93b2f34489d8230eb6d8c871335800d9b83ee67" resolved "https://registry.yarnpkg.com/luma.gl/-/luma.gl-5.3.0.tgz#a93b2f34489d8230eb6d8c871335800d9b83ee67"
dependencies: dependencies:
@ -7378,14 +7405,14 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2:
version "0.8.2" version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
object-assign@^3.0.0: object-assign@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
object-copy@^0.1.0: object-copy@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
@ -8329,6 +8356,23 @@ randomfill@^1.0.3:
randombytes "^2.0.5" randombytes "^2.0.5"
safe-buffer "^5.1.0" safe-buffer "^5.1.0"
rc-align@^2.4.0:
version "2.4.3"
resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.3.tgz#b9b3c2a6d68adae71a8e1d041cd5e3b2a655f99a"
dependencies:
babel-runtime "^6.26.0"
dom-align "^1.7.0"
prop-types "^15.5.8"
rc-util "^4.0.4"
rc-animate@2.x:
version "2.4.4"
resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-2.4.4.tgz#a05a784c747beef140d99ff52b6117711bef4b1e"
dependencies:
babel-runtime "6.x"
css-animation "^1.3.2"
prop-types "15.x"
rc-config-loader@^2.0.1: rc-config-loader@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/rc-config-loader/-/rc-config-loader-2.0.1.tgz#8c8452f59bdd10d448a67762dccf7c1b247db860" resolved "https://registry.yarnpkg.com/rc-config-loader/-/rc-config-loader-2.0.1.tgz#8c8452f59bdd10d448a67762dccf7c1b247db860"
@ -8341,6 +8385,46 @@ rc-config-loader@^2.0.1:
path-exists "^2.1.0" path-exists "^2.1.0"
require-from-string "^2.0.1" require-from-string "^2.0.1"
rc-slider@^8.6.1:
version "8.6.1"
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-8.6.1.tgz#ee5e0380dbdf4b5de6955a265b0d4ff6196405d1"
dependencies:
babel-runtime "6.x"
classnames "^2.2.5"
prop-types "^15.5.4"
rc-tooltip "^3.7.0"
rc-util "^4.0.4"
shallowequal "^1.0.1"
warning "^3.0.0"
rc-tooltip@^3.7.0:
version "3.7.2"
resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.7.2.tgz#3698656d4bacd51b72d9e327bed15d1d5a9f1b27"
dependencies:
babel-runtime "6.x"
prop-types "^15.5.8"
rc-trigger "^2.2.2"
rc-trigger@^2.2.2:
version "2.5.4"
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.5.4.tgz#9088a24ba5a811b254f742f004e38a9e2f8843fb"
dependencies:
babel-runtime "6.x"
classnames "^2.2.6"
prop-types "15.x"
rc-align "^2.4.0"
rc-animate "2.x"
rc-util "^4.4.0"
rc-util@^4.0.4, rc-util@^4.4.0:
version "4.5.1"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.5.1.tgz#0e435057174c024901c7600ba8903dd03da3ab39"
dependencies:
add-dom-event-listener "1.x"
babel-runtime "6.x"
prop-types "^15.5.10"
shallowequal "^0.2.2"
rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@ -9469,6 +9553,16 @@ shallow-copy@~0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170" resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170"
shallowequal@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e"
dependencies:
lodash.keys "^3.1.2"
shallowequal@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
shapefile@0.3: shapefile@0.3:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/shapefile/-/shapefile-0.3.1.tgz#9bb9a429bd6086a0cfb03962d14cfdf420ffba12" resolved "https://registry.yarnpkg.com/shapefile/-/shapefile-0.3.1.tgz#9bb9a429bd6086a0cfb03962d14cfdf420ffba12"

View File

@ -11,7 +11,6 @@ import os
import re import re
import time import time
import traceback import traceback
from urllib import parse
from flask import ( from flask import (
flash, g, Markup, redirect, render_template, request, Response, url_for, flash, g, Markup, redirect, render_template, request, Response, url_for,
@ -25,6 +24,7 @@ from flask_babel import lazy_gettext as _
import pandas as pd import pandas as pd
import simplejson as json import simplejson as json
from six import text_type from six import text_type
from six.moves.urllib import parse
import sqlalchemy as sqla import sqlalchemy as sqla
from sqlalchemy import and_, create_engine, or_, update from sqlalchemy import and_, create_engine, or_, update
from sqlalchemy.engine.url import make_url from sqlalchemy.engine.url import make_url

View File

@ -10,7 +10,7 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import defaultdict from collections import defaultdict, OrderedDict
import copy import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
import hashlib import hashlib
@ -100,7 +100,7 @@ class BaseViz(object):
self.process_metrics() self.process_metrics()
def process_metrics(self): def process_metrics(self):
self.metric_dict = {} self.metric_dict = OrderedDict()
fd = self.form_data fd = self.form_data
for mkey in METRIC_KEYS: for mkey in METRIC_KEYS:
val = fd.get(mkey) val = fd.get(mkey)
@ -2208,6 +2208,7 @@ class BaseDeckGLViz(BaseViz):
return { return {
'features': features, 'features': features,
'mapboxApiKey': config.get('MAPBOX_API_KEY'), 'mapboxApiKey': config.get('MAPBOX_API_KEY'),
'metricLabels': self.metric_labels,
} }
def get_properties(self, d): def get_properties(self, d):
@ -2304,6 +2305,17 @@ class DeckGrid(BaseDeckGLViz):
return super(DeckGrid, self).get_data(df) return super(DeckGrid, self).get_data(df)
def geohash_to_json(geohash_code):
p = geohash.bbox(geohash_code)
return [
[p.get('w'), p.get('n')],
[p.get('e'), p.get('n')],
[p.get('e'), p.get('s')],
[p.get('w'), p.get('s')],
[p.get('w'), p.get('n')],
]
class DeckPathViz(BaseDeckGLViz): class DeckPathViz(BaseDeckGLViz):
"""deck.gl's PathLayer""" """deck.gl's PathLayer"""
@ -2314,26 +2326,32 @@ class DeckPathViz(BaseDeckGLViz):
deser_map = { deser_map = {
'json': json.loads, 'json': json.loads,
'polyline': polyline.decode, 'polyline': polyline.decode,
'geohash': geohash_to_json,
} }
def query_obj(self): def query_obj(self):
d = super(DeckPathViz, self).query_obj() d = super(DeckPathViz, self).query_obj()
line_col = self.form_data.get('line_column') line_col = self.form_data.get('line_column')
if d['metrics']: if d['metrics']:
self.has_metrics = True
d['groupby'].append(line_col) d['groupby'].append(line_col)
else: else:
self.has_metrics = False
d['columns'].append(line_col) d['columns'].append(line_col)
return d return d
def get_properties(self, d): def get_properties(self, d):
fd = self.form_data fd = self.form_data
deser = self.deser_map[fd.get('line_type')] line_type = fd.get('line_type')
path = deser(d[fd.get('line_column')]) deser = self.deser_map[line_type]
line_column = fd.get('line_column')
path = deser(d[line_column])
if fd.get('reverse_long_lat'): if fd.get('reverse_long_lat'):
path = (path[1], path[0]) path = [(o[1], o[0]) for o in path]
return { d[self.deck_viz_key] = path
self.deck_viz_key: path, if line_type != 'geohash':
} del d[line_column]
return d
class DeckPolygon(DeckPathViz): class DeckPolygon(DeckPathViz):