You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@superset.apache.org by GitBox <gi...@apache.org> on 2018/02/04 04:16:09 UTC

[GitHub] mistercrunch closed pull request #4330: Adding support for Mapbox with Polygon

mistercrunch closed pull request #4330: Adding support for Mapbox with Polygon
URL: https://github.com/apache/incubator-superset/pull/4330
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

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 0000000000..e5dfb30f2b
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 2c18f4b0fa..4589922826 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 f935a5b6c6..de6fc32346 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 c944ad2fa0..a87f082d3b 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 40b6592d90..e79f872e69 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 0000000000..b267de0814
--- /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 0000000000..e5825ed827
--- /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 && (
+      <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;
diff --git a/superset/viz.py b/superset/viz.py
index e59835b60b..4b22f220cf 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 = (
+        '<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"""


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services