Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,19 @@ export const controls = {
description: t('Defines how the color are attributed.'),
},

rgb_color_scheme: {
type: 'SelectControl',
freeForm: true,
label: 'RGB Color Scheme',
default: 'green_red',
choices: [
['green_red', 'Green/Red'],
['light_dark_blue', 'Light/Dark Blue'],
['white_yellow', 'White/Yellow'],
],
description: 'The color for polygons.',
},

canvas_image_rendering: {
type: 'SelectControl',
label: t('Rendering'),
Expand Down
44 changes: 44 additions & 0 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -1478,6 +1478,50 @@ export const visTypes = {
},
},

mapbox_with_polygon: {
label: t('Mapbox with Polygon'),
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['entity'],
['metric'],
],
},
{
label: t('Options'),
controlSetRows: [
['select_country'],
['rgb_color_scheme'],
['mapbox_style'],
],
},
{
label: t('Viewport'),
controlSetRows: [
['viewport_longitude'],
['viewport_latitude'],
['viewport_zoom'],
],
},
],
controlOverrides: {
entity: {
label: t('Codes of region/province/department'),
description: t("It's the code of your region/province/department in your table. (see documentation for list of ISO 3166-1)"),
},
metric: {
label: t('Metric'),
description: t('Metric to display bottom title'),
},
select_country: {
label: t('GeoJSON Layer'),
description: t('The name of GeoJSON Layer that Superset should display'),
},
},
},

event_flow: {
label: t('Event flow'),
requiresTime: true,
Expand Down
1 change: 1 addition & 0 deletions superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"d3": "^3.5.17",
"d3-cloud": "^1.2.1",
"d3-hierarchy": "^1.1.5",
"d3-request": "^1.0.6",
"d3-sankey": "^0.4.2",
"d3-svg-legend": "^1.x",
"d3-tip": "^0.6.7",
Expand Down
2 changes: 2 additions & 0 deletions superset/assets/visualizations/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const VIZ_TYPES = {
iframe: 'iframe',
line: 'line',
mapbox: 'mapbox',
mapbox_with_polygon: 'mapbox_with_polygon',
markup: 'markup',
para: 'para',
pie: 'pie',
Expand Down Expand Up @@ -71,6 +72,7 @@ const vizMap = {
[VIZ_TYPES.line]: require('./nvd3_vis.js'),
[VIZ_TYPES.time_pivot]: require('./nvd3_vis.js'),
[VIZ_TYPES.mapbox]: require('./mapbox.jsx'),
[VIZ_TYPES.mapbox_with_polygon]: require('./mapbox_with_polygon.jsx'),
[VIZ_TYPES.markup]: require('./markup.js'),
[VIZ_TYPES.para]: require('./parallel_coordinates.js'),
[VIZ_TYPES.pie]: require('./nvd3_vis.js'),
Expand Down
29 changes: 29 additions & 0 deletions superset/assets/visualizations/mapbox_with_polygon.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.mapbox_with_polygon div.widget .slice_container {
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
overflow: hidden;
}

.mapbox_with_polygon div.widget .slice_container:active {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}

.mapbox_with_polygon .slice_container div {
padding-top: 0px;
}

.mapbox_with_polygon .tooltip {
position: absolute;
margin: 8px;
padding: 4px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
max-width: 300px;
font-size: 14px;
z-index: 9;
pointer-events: none;
opacity: 1;
}
188 changes: 188 additions & 0 deletions superset/assets/visualizations/mapbox_with_polygon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/* eslint-disable no-param-reassign */
/* eslint-disable react/no-multi-comp */
import d3 from 'd3';
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import MapGL from 'react-map-gl';
import { json as requestJson } from 'd3-request';
import DeckGL, { GeoJsonLayer } from 'deck.gl';

import './mapbox_with_polygon.css';

const NOOP = () => {};

class MapboxViz extends React.Component {
constructor(props) {
super(props);
const longitude = this.props.viewportLongitude || DEFAULT_LONGITUDE;
const latitude = this.props.viewportLatitude || DEFAULT_LATITUDE;
this.state = {
viewport: {
longitude,
latitude,
zoom: this.props.viewportZoom || DEFAULT_ZOOM,
startDragLngLat: [longitude, latitude],
},
geojson: null,
dmap: null,
x_coord: 0,
y_coord: 0,
properties: null,
hoveredFeature: false,
minCount: 0,
maxCount: 0,
};
this.onViewportChange = this.onViewportChange.bind(this);
this.onHover = this.onHover.bind(this);
this.renderTooltip = this.renderTooltip.bind(this);
}

componentDidMount() {
const country = this.props.country;
requestJson('/static/assets/visualizations/countries/' + country + '.geojson', (error, response) => {
const resp = this.props.dataResponse;
const dataMap = [];
let i = 0;
let key = '';
if (!error) {
for (i = 0; i < resp.length; i++) {
key = resp[i].country_id;
dataMap[key] = resp[i].metric;
}
const max = d3.max(d3.values(dataMap));
const min = d3.min(d3.values(dataMap));
const center = d3.geo.centroid(response);
const longitude = center[0];
const latitude = center[1];
this.setState({
geojson: response,
dmap: dataMap,
maxCount: max,
minCount: min,
viewport: {
longitude,
latitude,
zoom: this.props.viewportZoom,
startDragLngLat: [longitude, latitude],
},
});
}
});
}

onViewportChange(viewport) {
this.setState({ viewport });
this.props.setControlValue('viewport_longitude', viewport.longitude);
this.props.setControlValue('viewport_latitude', viewport.latitude);
this.props.setControlValue('viewport_zoom', viewport.zoom);
}

onHover(event) {
let hoveredFeature = false;
if (event !== undefined) {
const properties = event.object.properties;
const xCoord = event.x;
const yCoord = event.y;
hoveredFeature = true;
this.setState({ xCoord, yCoord, properties, hoveredFeature });
} else {
hoveredFeature = false;
}
}

initialize(gl) {
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ONE_MINUS_DST_ALPHA, gl.ONE);
gl.blendEquation(gl.FUNC_ADD);
}

colorScale(r, rgbColorScheme) {
if (isNaN(r)) {
return [211, 211, 211];
} else if (rgbColorScheme === 'green_red') {
return [r * 255, 200 * (1 - r), 50];
} else if (rgbColorScheme === 'light_dark_blue') {
return [0, (1 - r) * 255, 255];
}
return [255, 255, (1 - r) * 200]; // white-yellow
}

renderTooltip() {
const { hoveredFeature, properties, x_coord, y_coord, dmap } = this.state;
return hoveredFeature && (
<div className="tooltip" style={{ left: x_coord, top: y_coord }}>
<div>ID: {properties.ISO}</div>
<div>Region: {properties.NAME_2}</div>
<div>Count: {dmap[properties.ISO]}</div>
</div>
);
}

render() {
const { geojson, dmap, minCount, maxCount } = this.state;
const rgbColorScheme = this.props.rgbColorScheme;
const geosjsonLayer = new GeoJsonLayer({
id: 'geojson-layer',
data: geojson,
opacity: 0.3,
filled: true,
stroked: true,
lineWidthMinPixels: 1,
lineWidthScale: 2,
getFillColor: f => this.colorScale(((dmap[f.properties.ISO] - minCount) /
(maxCount - minCount)), rgbColorScheme),
pickable: true,
});

return (
<MapGL
{...this.state.viewport}
mapboxApiAccessToken={this.props.mapboxApiKey}
mapStyle={this.props.mapStyle}
perspectiveEnabled
width={this.props.sliceWidth}
height={this.props.sliceHeight}
onChangeViewport={this.onViewportChange}
>
<DeckGL
{...this.state.viewport}
layers={[geosjsonLayer]}
onWebGLInitialized={this.initialize}
onLayerHover={this.onHover}
width={this.props.sliceWidth}
height={this.props.sliceHeight}
/>
{this.renderTooltip()}
</MapGL>
);
}
}
MapboxViz.propTypes = {
setControlValue: PropTypes.func,
mapStyle: PropTypes.string,
mapboxApiKey: PropTypes.string,
sliceHeight: PropTypes.number,
sliceWidth: PropTypes.number,
viewportLatitude: PropTypes.number,
viewportLongitude: PropTypes.number,
viewportZoom: PropTypes.number,
country: PropTypes.string,
rgbColorScheme: PropTypes.string,
dataResponse: PropTypes.array,
};

function mapboxWithPolygon(slice, json, setControlValue) {
const div = d3.select(slice.selector);
div.selectAll('*').remove();
ReactDOM.render(
<MapboxViz
{...json.data}
sliceHeight={slice.height()}
sliceWidth={slice.width()}
setControlValue={setControlValue || NOOP}
/>,
div.node(),
);
}

module.exports = mapboxWithPolygon;
38 changes: 38 additions & 0 deletions superset/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -1805,6 +1805,44 @@ def get_data(self, df):
}


class MapboxWithPloygonViz(BaseViz):

"""Rich maps made with Mapbox"""

viz_type = 'mapbox_with_polygon'
verbose_name = _('Mapbox With Ploygon')
is_timeseries = False
credits = (
'<a href=https://www.mapbox.com/mapbox-gl-js/api/>Mapbox GL JS</a>')

def query_obj(self):
qry = super(MapboxWithPloygonViz, self).query_obj()
qry['metrics'] = [
self.form_data['metric']]
qry['groupby'] = [self.form_data['entity']]
return qry

def get_data(self, df):
fd = self.form_data
cols = [fd.get('entity')]
metric = fd.get('metric')
cols += [metric]
ndf = df[cols]
df = ndf
df.columns = ['country_id', 'metric']
d = df.to_dict(orient='records')
return {
'dataResponse': d,
'mapboxApiKey': config.get('MAPBOX_API_KEY'),
'mapStyle': fd.get('mapbox_style'),
'viewportLongitude': fd.get('viewport_longitude'),
'viewportLatitude': fd.get('viewport_latitude'),
'viewportZoom': fd.get('viewport_zoom'),
'country': fd.get('select_country'),
'rgb_color_scheme': fd.get('rgb_color_scheme'),
}


class DeckGLMultiLayer(BaseViz):

"""Pile on multiple DeckGL layers"""
Expand Down