diff --git a/superset/assets/images/viz_thumbnails/mapbox_with_polygon.png b/superset/assets/images/viz_thumbnails/mapbox_with_polygon.png new file mode 100644 index 000000000000..e5dfb30f2b34 Binary files /dev/null and b/superset/assets/images/viz_thumbnails/mapbox_with_polygon.png differ diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx index 2c18f4b0fa47..4589922826b4 100644 --- a/superset/assets/javascripts/explore/stores/controls.jsx +++ b/superset/assets/javascripts/explore/stores/controls.jsx @@ -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'), diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js index f935a5b6c6a3..de6fc3234603 100644 --- a/superset/assets/javascripts/explore/stores/visTypes.js +++ b/superset/assets/javascripts/explore/stores/visTypes.js @@ -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, diff --git a/superset/assets/package.json b/superset/assets/package.json index c944ad2fa0bf..a87f082d3b6f 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -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", diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js index 40b6592d9013..e79f872e6920 100644 --- a/superset/assets/visualizations/main.js +++ b/superset/assets/visualizations/main.js @@ -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', @@ -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'), diff --git a/superset/assets/visualizations/mapbox_with_polygon.css b/superset/assets/visualizations/mapbox_with_polygon.css new file mode 100644 index 000000000000..b267de081498 --- /dev/null +++ b/superset/assets/visualizations/mapbox_with_polygon.css @@ -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; +} diff --git a/superset/assets/visualizations/mapbox_with_polygon.jsx b/superset/assets/visualizations/mapbox_with_polygon.jsx new file mode 100644 index 000000000000..e5825ed827d3 --- /dev/null +++ b/superset/assets/visualizations/mapbox_with_polygon.jsx @@ -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 && ( +
+
ID: {properties.ISO}
+
Region: {properties.NAME_2}
+
Count: {dmap[properties.ISO]}
+
+ ); + } + + 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 ( + + + {this.renderTooltip()} + + ); + } +} +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( + , + div.node(), + ); +} + +module.exports = mapboxWithPolygon; diff --git a/superset/viz.py b/superset/viz.py index e59835b60baf..4b22f220cfc3 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -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 = ( + 'Mapbox GL JS') + + 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"""