Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve deck.gl GeoJSON visualization #4220

Merged
merged 3 commits into from
Jan 17, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"jquery": "3.1.1",
"lodash.throttle": "^4.1.1",
"luma.gl": "^5.0.1",
"mapbox-gl": "^0.43.0",
"mathjs": "^3.16.3",
"moment": "2.18.1",
"mustache": "^2.2.1",
Expand Down
1 change: 1 addition & 0 deletions superset/assets/visualizations/deckgl/DeckGLContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import MapGL from 'react-map-gl';
import DeckGL from 'deck.gl';
import 'mapbox-gl/dist/mapbox-gl.css';

const propTypes = {
viewport: PropTypes.object.isRequired,
Expand Down
72 changes: 53 additions & 19 deletions superset/assets/visualizations/deckgl/layers/geojson.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,74 @@ const propertyMap = {
'stroke-width': 'strokeWidth',
};

const convertGeoJsonColorProps = (p, colors) => {
const obj = Object.assign(...Object.keys(p).map(k => ({
[(propertyMap[k]) ? propertyMap[k] : k]: p[k] })));

const alterProps = (props, propOverrides) => {
const newProps = {};
Object.keys(props).forEach((k) => {
if (k in propertyMap) {
newProps[propertyMap[k]] = props[k];
} else {
newProps[k] = props[k];
}
});
if (typeof props.fillColor === 'string') {
newProps.fillColor = hexToRGB(p.fillColor);
}
if (typeof props.strokeColor === 'string') {
newProps.strokeColor = hexToRGB(p.strokeColor);
}
return {
...obj,
fillColor: (colors.fillColor[3] !== 0) ? colors.fillColor : hexToRGB(obj.fillColor),
strokeColor: (colors.strokeColor[3] !== 0) ? colors.strokeColor : hexToRGB(obj.strokeColor),
...newProps,
...propOverrides,
};
};
let features;
const recurseGeoJson = (node, propOverrides, jsFnMutator, extraProps) => {
if (node && node.features) {
node.features.forEach((obj) => {
recurseGeoJson(obj, propOverrides, jsFnMutator, node.extraProps || extraProps);
});
}
if (node && node.properties) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think checking for geometry instead of properties would be better here. Not all nodes need the property set, but the geometry is mandatory for defining the shape.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call

const newNode = {
...node,
properties: alterProps(node.properties, propOverrides),
Copy link
Member

@hughhhh hughhhh Jan 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: properties: (node.properties) ? alterProps(node.properties, propOverrides) : {}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overrides wouldn't apply in that case. It'd have to be (node.properties || Object.keys(propOverrides).length > 0) or something, but that would give the same result. I'll just leave it as is as it's more readable.

};
if (jsFnMutator) {
jsFnMutator(newNode);
}
if (!newNode.extraProps) {
newNode.extraProps = extraProps;
}
features.push(newNode);
}
};

export default function geoJsonLayer(formData, payload, slice) {
const fd = formData;
const fc = fd.fill_color_picker;
const sc = fd.stroke_color_picker;
let data = payload.data.geojson.features.map(d => ({
...d,
properties: convertGeoJsonColorProps(
d.properties, {
fillColor: [fc.r, fc.g, fc.b, 255 * fc.a],
strokeColor: [sc.r, sc.g, sc.b, 255 * sc.a],
}),
}));
const fillColor = [fc.r, fc.g, fc.b, 255 * fc.a];
const strokeColor = [sc.r, sc.g, sc.b, 255 * sc.a];
const propOverrides = {};
if (fillColor[3] > 0) {
propOverrides.fillColor = fillColor;
}
if (strokeColor[3] > 0) {
propOverrides.strokeColor = strokeColor;
}

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

features = [];
recurseGeoJson(payload.data, propOverrides, jsFnMutator);
return new GeoJsonLayer({
id: `path-layer-${fd.slice_id}`,
data,
id: `geojson-layer-${fd.slice_id}`,
filled: fd.filled,
data: features,
stroked: fd.stroked,
extruded: fd.extruded,
pointRadiusScale: fd.point_radius_scale,
Expand Down
1 change: 0 additions & 1 deletion superset/assets/visualizations/deckgl/layers/grid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export default function getLayer(formData, payload) {
...d,
color: [c.r, c.g, c.b, 255 * c.a],
}));

return new GridLayer({
id: `grid-layer-${fd.slice_id}`,
data,
Expand Down
70 changes: 28 additions & 42 deletions superset/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -1812,14 +1812,6 @@ def get_metrics(self):
self.metric = self.form_data.get('size')
return [self.metric] if self.metric else []

def get_properties(self, d):
return {
'weight': d.get(self.metric) or 1,
}

def get_position(self, d):
raise Exception('Not implemented in child class!')

def process_spatial_query_obj(self, key, group_by):
spatial = self.form_data.get(key)
if spatial is None:
Expand Down Expand Up @@ -1885,10 +1877,10 @@ def get_data(self, df):

features = []
for d in df.to_dict(orient='records'):
feature = dict(
position=self.get_position(d),
props=self.get_js_columns(d),
**self.get_properties(d))
feature = self.get_properties(d)
extra_props = self.get_js_columns(d)
if extra_props:
feature['extraProps'] = extra_props
features.append(feature)
return {
'features': features,
Expand All @@ -1910,9 +1902,6 @@ def query_obj(self):
fd.get('point_radius_fixed') or {'type': 'fix', 'value': 500})
return super(DeckScatterViz, self).query_obj()

def get_position(self, d):
return d['spatial']

def get_metrics(self):
self.metric = None
if self.point_radius_fixed.get('type') == 'metric':
Expand All @@ -1924,6 +1913,7 @@ 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,
'position': d.get('spatial'),
}

def get_data(self, df):
Expand All @@ -1944,8 +1934,11 @@ class DeckScreengrid(BaseDeckGLViz):
verbose_name = _('Deck.gl - Screen Grid')
spatial_control_keys = ['spatial']

def get_position(self, d):
return d['spatial']
def get_properties(self, d):
return {
'position': d.get('spatial'),
'weight': d.get(self.metric) or 1,
}


class DeckGrid(BaseDeckGLViz):
Expand All @@ -1956,8 +1949,11 @@ class DeckGrid(BaseDeckGLViz):
verbose_name = _('Deck.gl - 3D Grid')
spatial_control_keys = ['spatial']

def get_position(self, d):
return d['spatial']
def get_properties(self, d):
return {
'position': d.get('spatial'),
'weight': d.get(self.metric) or 1,
}


class DeckPathViz(BaseDeckGLViz):
Expand All @@ -1972,9 +1968,6 @@ class DeckPathViz(BaseDeckGLViz):
}
spatial_control_keys = ['spatial']

def get_position(self, d):
return d['spatial']

def query_obj(self):
d = super(DeckPathViz, self).query_obj()
line_col = self.form_data.get('line_column')
Expand Down Expand Up @@ -2003,8 +1996,11 @@ class DeckHex(BaseDeckGLViz):
verbose_name = _('Deck.gl - 3D HEX')
spatial_control_keys = ['spatial']

def get_position(self, d):
return d['spatial']
def get_properties(self, d):
return {
'position': d.get('spatial'),
'weight': d.get(self.metric) or 1,
}


class DeckGeoJson(BaseDeckGLViz):
Expand All @@ -2016,22 +2012,14 @@ class DeckGeoJson(BaseDeckGLViz):

def query_obj(self):
d = super(DeckGeoJson, self).query_obj()
d['columns'] = [self.form_data.get('geojson')]
d['columns'] += [self.form_data.get('geojson')]
d['metrics'] = []
d['groupby'] = []
return d

def get_data(self, df):
fd = self.form_data
geojson = {
'type': 'FeatureCollection',
'features': [json.loads(item) for item in df[fd.get('geojson')]],
}

return {
'geojson': geojson,
'mapboxApiKey': config.get('MAPBOX_API_KEY'),
}
def get_properties(self, d):
geojson = d.get(self.form_data.get('geojson'))
return json.loads(geojson)


class DeckArc(BaseDeckGLViz):
Expand All @@ -2042,14 +2030,12 @@ class DeckArc(BaseDeckGLViz):
verbose_name = _('Deck.gl - Arc')
spatial_control_keys = ['start_spatial', 'end_spatial']

def get_position(self, d):
deck_map = {
'start_spatial': 'sourcePosition',
'end_spatial': 'targetPosition',
def get_properties(self, d):
return {
'sourcePosition': d.get('start_spatial'),
'targetPosition': d.get('end_spatial'),
}

return {deck_map[key]: d[key] for key in self.spatial_control_keys}

def get_data(self, df):
d = super(DeckArc, self).get_data(df)
arcs = d['features']
Expand Down