You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/09/05 19:12:32 UTC

[incubator-superset] branch master updated: [SIP-5] Refactor MapBox (#5783)

This is an automated email from the ASF dual-hosted git repository.

ccwilliams pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new bebbdb8  [SIP-5] Refactor MapBox (#5783)
bebbdb8 is described below

commit bebbdb85d2cc301d2cb86dc6a6da46a285cdd7da
Author: Krist Wongsuphasawat <kr...@gmail.com>
AuthorDate: Wed Sep 5 12:12:30 2018 -0700

    [SIP-5] Refactor MapBox (#5783)
    
    * Break MapBox into smaller pieces
    
    * Replace React.createElement with regular jsx
    
    * detach setControlValue
    
    * enable render trigger
    
    * Pass explicit props rather than pass all that exists in payload.data. Also use formData when possible.
    
    * Rename sliceWidth, sliceHeight to width, height. Use deconstructor. Extract function.
    
    * use arrow function
    
    * fix linting and remove css
---
 superset/assets/src/explore/controls.jsx           |   3 +
 .../assets/src/visualizations/MapBox/MapBox.css    |   3 +
 .../assets/src/visualizations/MapBox/MapBox.jsx    | 225 +++++++++++++++++++
 .../ScatterPlotGlowOverlay.jsx}                    | 246 ++++-----------------
 superset/assets/src/visualizations/index.js        |   2 +-
 superset/assets/src/visualizations/mapbox.css      |  16 --
 6 files changed, 281 insertions(+), 214 deletions(-)

diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx
index 152d4a7..5e422ea 100644
--- a/superset/assets/src/explore/controls.jsx
+++ b/superset/assets/src/explore/controls.jsx
@@ -1802,6 +1802,7 @@ export const controls = {
   viewport_zoom: {
     type: 'TextControl',
     label: t('Zoom'),
+    renderTrigger: true,
     isFloat: true,
     default: 11,
     description: t('Zoom level of the map'),
@@ -1813,6 +1814,7 @@ export const controls = {
   viewport_latitude: {
     type: 'TextControl',
     label: t('Default latitude'),
+    renderTrigger: true,
     default: 37.772123,
     isFloat: true,
     description: t('Latitude of default viewport'),
@@ -1824,6 +1826,7 @@ export const controls = {
   viewport_longitude: {
     type: 'TextControl',
     label: t('Default longitude'),
+    renderTrigger: true,
     default: -122.405293,
     isFloat: true,
     description: t('Longitude of default viewport'),
diff --git a/superset/assets/src/visualizations/MapBox/MapBox.css b/superset/assets/src/visualizations/MapBox/MapBox.css
new file mode 100644
index 0000000..3ec640d
--- /dev/null
+++ b/superset/assets/src/visualizations/MapBox/MapBox.css
@@ -0,0 +1,3 @@
+.mapbox .slice_container div {
+  padding-top: 0px;
+}
diff --git a/superset/assets/src/visualizations/MapBox/MapBox.jsx b/superset/assets/src/visualizations/MapBox/MapBox.jsx
new file mode 100644
index 0000000..81f41f0
--- /dev/null
+++ b/superset/assets/src/visualizations/MapBox/MapBox.jsx
@@ -0,0 +1,225 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactDOM from 'react-dom';
+import MapGL from 'react-map-gl';
+import Immutable from 'immutable';
+import supercluster from 'supercluster';
+import ViewportMercator from 'viewport-mercator-project';
+import ScatterPlotGlowOverlay from './ScatterPlotGlowOverlay';
+
+import {
+  DEFAULT_LONGITUDE,
+  DEFAULT_LATITUDE,
+  DEFAULT_ZOOM,
+} from '../../utils/common';
+import './MapBox.css';
+
+const NOOP = () => {};
+const DEFAULT_POINT_RADIUS = 60;
+const DEFAULT_MAX_ZOOM = 16;
+
+const propTypes = {
+  width: PropTypes.number,
+  height: PropTypes.number,
+  aggregatorName: PropTypes.string,
+  clusterer: PropTypes.object,
+  globalOpacity: PropTypes.number,
+  mapStyle: PropTypes.string,
+  mapboxApiKey: PropTypes.string,
+  onViewportChange: PropTypes.func,
+  pointRadius: PropTypes.number,
+  pointRadiusUnit: PropTypes.string,
+  renderWhileDragging: PropTypes.bool,
+  rgb: PropTypes.array,
+  viewportLatitude: PropTypes.number,
+  viewportLongitude: PropTypes.number,
+  viewportZoom: PropTypes.number,
+};
+
+const defaultProps = {
+  globalOpacity: 1,
+  onViewportChange: NOOP,
+  pointRadius: DEFAULT_POINT_RADIUS,
+  pointRadiusUnit: 'Pixels',
+  viewportLatitude: DEFAULT_LATITUDE,
+  viewportLongitude: DEFAULT_LONGITUDE,
+  viewportZoom: DEFAULT_ZOOM,
+};
+
+class MapBox extends React.Component {
+  constructor(props) {
+    super(props);
+
+    const {
+      viewportLatitude: latitude,
+      viewportLongitude: longitude,
+      viewportZoom: zoom,
+    } = this.props;
+
+    this.state = {
+      viewport: {
+        longitude,
+        latitude,
+        zoom,
+        startDragLngLat: [longitude, latitude],
+      },
+    };
+    this.onViewportChange = this.onViewportChange.bind(this);
+  }
+
+  onViewportChange(viewport) {
+    this.setState({ viewport });
+    this.props.onViewportChange(viewport);
+  }
+
+  render() {
+    const {
+      width,
+      height,
+      aggregatorName,
+      globalOpacity,
+      mapStyle,
+      mapboxApiKey,
+      pointRadius,
+      pointRadiusUnit,
+      renderWhileDragging,
+      rgb,
+    } = this.props;
+    const { viewport } = this.state;
+    const { latitude, longitude, zoom } = viewport;
+    const mercator = new ViewportMercator({
+      width,
+      height,
+      longitude,
+      latitude,
+      zoom,
+    });
+    const topLeft = mercator.unproject([0, 0]);
+    const bottomRight = mercator.unproject([width, height]);
+    const bbox = [topLeft[0], bottomRight[1], bottomRight[0], topLeft[1]];
+    const clusters = this.props.clusterer.getClusters(bbox, Math.round(zoom));
+    const isDragging = viewport.isDragging === undefined ? false :
+                       viewport.isDragging;
+    return (
+      <MapGL
+        {...this.state.viewport}
+        mapStyle={mapStyle}
+        width={width}
+        height={height}
+        mapboxApiAccessToken={mapboxApiKey}
+        onViewportChange={this.onViewportChange}
+      >
+        <ScatterPlotGlowOverlay
+          {...viewport}
+          isDragging={isDragging}
+          width={width}
+          height={height}
+          locations={Immutable.fromJS(clusters)}
+          dotRadius={pointRadius}
+          pointRadiusUnit={pointRadiusUnit}
+          rgb={rgb}
+          globalOpacity={globalOpacity}
+          compositeOperation={'screen'}
+          renderWhileDragging={renderWhileDragging}
+          aggregatorName={aggregatorName}
+          lngLatAccessor={(location) => {
+            const coordinates = location.get('geometry').get('coordinates');
+            return [coordinates.get(0), coordinates.get(1)];
+          }}
+        />
+      </MapGL>
+    );
+  }
+}
+
+MapBox.propTypes = propTypes;
+MapBox.defaultProps = defaultProps;
+
+function createReducer(aggregatorName, customMetric) {
+  if (aggregatorName === 'sum' || !customMetric) {
+    return (a, b) => a + b;
+  } else if (aggName === 'min') {
+    return Math.min;
+  } else if (aggName === 'max') {
+    return Math.max;
+  }
+  return function (a, b) {
+    if (a instanceof Array) {
+      if (b instanceof Array) {
+        return a.concat(b);
+      }
+      a.push(b);
+      return a;
+    }
+    if (b instanceof Array) {
+      b.push(a);
+      return b;
+    }
+    return [a, b];
+  };
+}
+
+function mapbox(slice, payload, setControlValue) {
+  const { formData, selector } = slice;
+  const {
+    customMetric,
+    geoJSON,
+    mapboxApiKey,
+  } = payload.data;
+  const {
+    clustering_radius: clusteringRadius,
+    global_opacity: globalOpacity,
+    mapbox_color: color,
+    mapbox_style: mapStyle,
+    pandas_aggfunc: aggregatorName,
+    point_radius: pointRadius,
+    point_radius_unit: pointRadiusUnit,
+    render_while_dragging: renderWhileDragging,
+    viewport_latitude: viewportLatitude,
+    viewport_longitude: viewportLongitude,
+    viewport_zoom: viewportZoom,
+  } = formData;
+
+  // Validate mapbox color
+  const rgb = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/
+    .exec(color);
+  if (rgb === null) {
+    slice.error('Color field must be of form \'rgb(%d, %d, %d)\'');
+    return;
+  }
+
+  const clusterer = supercluster({
+    radius: clusteringRadius,
+    maxZoom: DEFAULT_MAX_ZOOM,
+    metricKey: 'metric',
+    metricReducer: createReducer(aggregatorName, customMetric),
+  });
+  clusterer.load(geoJSON.features);
+
+  ReactDOM.render(
+    <MapBox
+      width={slice.width()}
+      height={slice.height()}
+      aggregatorName={aggregatorName}
+      clusterer={clusterer}
+      globalOpacity={globalOpacity}
+      mapStyle={mapStyle}
+      mapboxApiKey={mapboxApiKey}
+      onViewportChange={({ latitude, longitude, zoom }) => {
+        setControlValue('viewport_longitude', longitude);
+        setControlValue('viewport_latitude', latitude);
+        setControlValue('viewport_zoom', zoom);
+      }}
+      pointRadius={pointRadius === 'Auto' ? DEFAULT_POINT_RADIUS : pointRadius}
+      pointRadiusUnit={pointRadiusUnit}
+      renderWhileDragging={renderWhileDragging}
+      rgb={rgb}
+      viewportLatitude={viewportLatitude}
+      viewportLongitude={viewportLongitude}
+      viewportZoom={viewportZoom}
+    />,
+    document.querySelector(selector),
+  );
+}
+
+export default mapbox;
diff --git a/superset/assets/src/visualizations/mapbox.jsx b/superset/assets/src/visualizations/MapBox/ScatterPlotGlowOverlay.jsx
similarity index 59%
rename from superset/assets/src/visualizations/mapbox.jsx
rename to superset/assets/src/visualizations/MapBox/ScatterPlotGlowOverlay.jsx
index 1a156ae..ea4e115 100644
--- a/superset/assets/src/visualizations/mapbox.jsx
+++ b/superset/assets/src/visualizations/MapBox/ScatterPlotGlowOverlay.jsx
@@ -1,28 +1,46 @@
-/* eslint-disable no-param-reassign */
-/* eslint-disable react/no-multi-comp */
 import d3 from 'd3';
+import Immutable from 'immutable';
 import React from 'react';
 import PropTypes from 'prop-types';
-import ReactDOM from 'react-dom';
-import MapGL from 'react-map-gl';
-import Immutable from 'immutable';
-import supercluster from 'supercluster';
 import ViewportMercator from 'viewport-mercator-project';
-
 import {
   kmToPixels,
   rgbLuminance,
   isNumeric,
   MILES_PER_KM,
-  DEFAULT_LONGITUDE,
-  DEFAULT_LATITUDE,
-  DEFAULT_ZOOM,
-} from '../utils/common';
-import './mapbox.css';
+} from '../../utils/common';
+
+const propTypes = {
+  locations: PropTypes.instanceOf(Immutable.List).isRequired,
+  lngLatAccessor: PropTypes.func,
+  renderWhileDragging: PropTypes.bool,
+  globalOpacity: PropTypes.number,
+  dotRadius: PropTypes.number,
+  dotFill: PropTypes.string,
+  compositeOperation: PropTypes.string,
+};
+
+const defaultProps = {
+  lngLatAccessor: location => [location.get(0), location.get(1)],
+  renderWhileDragging: true,
+  dotRadius: 4,
+  dotFill: '#1FBAD6',
+  globalOpacity: 1,
+  // Same as browser default.
+  compositeOperation: 'source-over',
+};
 
-const NOOP = () => {};
+const contextTypes = {
+  viewport: PropTypes.object,
+  isDragging: PropTypes.bool,
+};
 
 class ScatterPlotGlowOverlay extends React.Component {
+  constructor(props) {
+    super(props);
+    this.setCanvasRef = this.setCanvasRef.bind(this);
+  }
+
   componentDidMount() {
     this.redraw();
   }
@@ -30,6 +48,11 @@ class ScatterPlotGlowOverlay extends React.Component {
   componentDidUpdate() {
     this.redraw();
   }
+
+  setCanvasRef(element) {
+    this.canvas = element;
+  }
+
   drawText(ctx, pixel, options = {}) {
     const IS_DARK_THRESHOLD = 110;
     const { fontHeight = 0, label = '', radius = 0, rgb = [0, 0, 0], shadow = false } = options;
@@ -62,8 +85,7 @@ class ScatterPlotGlowOverlay extends React.Component {
   redraw() {
     const props = this.props;
     const pixelRatio = window.devicePixelRatio || 1;
-    const canvas = this.refs.overlay;
-    const ctx = canvas.getContext('2d');
+    const ctx = this.canvas.getContext('2d');
     const radius = props.dotRadius;
     const mercator = new ViewportMercator(props);
     const rgb = props.rgb;
@@ -185,9 +207,9 @@ class ScatterPlotGlowOverlay extends React.Component {
         }
       }, this);
     }
-
     ctx.restore();
   }
+
   render() {
     let width = 0;
     let height = 0;
@@ -198,11 +220,11 @@ class ScatterPlotGlowOverlay extends React.Component {
     const { globalOpacity } = this.props;
     const pixelRatio = window.devicePixelRatio || 1;
     return (
-      React.createElement('canvas', {
-        ref: 'overlay',
-        width: width * pixelRatio,
-        height: height * pixelRatio,
-        style: {
+      <canvas
+        ref={this.setCanvasRef}
+        width={width * pixelRatio}
+        height={height * pixelRatio}
+        style={{
           width: `${width}px`,
           height: `${height}px`,
           position: 'absolute',
@@ -210,184 +232,14 @@ class ScatterPlotGlowOverlay extends React.Component {
           opacity: globalOpacity,
           left: 0,
           top: 0,
-        },
-      })
-    );
-  }
-}
-ScatterPlotGlowOverlay.propTypes = {
-  locations: PropTypes.instanceOf(Immutable.List).isRequired,
-  lngLatAccessor: PropTypes.func,
-  renderWhileDragging: PropTypes.bool,
-  globalOpacity: PropTypes.number,
-  dotRadius: PropTypes.number,
-  dotFill: PropTypes.string,
-  compositeOperation: PropTypes.string,
-};
-
-ScatterPlotGlowOverlay.defaultProps = {
-  lngLatAccessor: location => [location.get(0), location.get(1)],
-  renderWhileDragging: true,
-  dotRadius: 4,
-  dotFill: '#1FBAD6',
-  globalOpacity: 1,
-  // Same as browser default.
-  compositeOperation: 'source-over',
-};
-ScatterPlotGlowOverlay.contextTypes = {
-  viewport: PropTypes.object,
-  isDragging: PropTypes.bool,
-};
-
-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],
-      },
-    };
-    this.onViewportChange = this.onViewportChange.bind(this);
-  }
-
-  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);
-  }
-
-  render() {
-    const mercator = new ViewportMercator({
-      width: this.props.sliceWidth,
-      height: this.props.sliceHeight,
-      longitude: this.state.viewport.longitude,
-      latitude: this.state.viewport.latitude,
-      zoom: this.state.viewport.zoom,
-    });
-    const topLeft = mercator.unproject([0, 0]);
-    const bottomRight = mercator.unproject([this.props.sliceWidth, this.props.sliceHeight]);
-    const bbox = [topLeft[0], bottomRight[1], bottomRight[0], topLeft[1]];
-    const clusters = this.props.clusterer.getClusters(bbox, Math.round(this.state.viewport.zoom));
-    const isDragging = this.state.viewport.isDragging === undefined ? false :
-                       this.state.viewport.isDragging;
-    return (
-      <MapGL
-        {...this.state.viewport}
-        mapStyle={this.props.mapStyle}
-        width={this.props.sliceWidth}
-        height={this.props.sliceHeight}
-        mapboxApiAccessToken={this.props.mapboxApiKey}
-        onViewportChange={this.onViewportChange}
-      >
-        <ScatterPlotGlowOverlay
-          {...this.state.viewport}
-          isDragging={isDragging}
-          width={this.props.sliceWidth}
-          height={this.props.sliceHeight}
-          locations={Immutable.fromJS(clusters)}
-          dotRadius={this.props.pointRadius}
-          pointRadiusUnit={this.props.pointRadiusUnit}
-          rgb={this.props.rgb}
-          globalOpacity={this.props.globalOpacity}
-          compositeOperation={'screen'}
-          renderWhileDragging={this.props.renderWhileDragging}
-          aggregatorName={this.props.aggregatorName}
-          lngLatAccessor={function (location) {
-            const coordinates = location.get('geometry').get('coordinates');
-            return [coordinates.get(0), coordinates.get(1)];
-          }}
-        />
-      </MapGL>
+        }}
+      />
     );
   }
 }
-MapboxViz.propTypes = {
-  aggregatorName: PropTypes.string,
-  clusterer: PropTypes.object,
-  setControlValue: PropTypes.func,
-  globalOpacity: PropTypes.number,
-  mapStyle: PropTypes.string,
-  mapboxApiKey: PropTypes.string,
-  pointRadius: PropTypes.number,
-  pointRadiusUnit: PropTypes.string,
-  renderWhileDragging: PropTypes.bool,
-  rgb: PropTypes.array,
-  sliceHeight: PropTypes.number,
-  sliceWidth: PropTypes.number,
-  viewportLatitude: PropTypes.number,
-  viewportLongitude: PropTypes.number,
-  viewportZoom: PropTypes.number,
-};
-
-function mapbox(slice, json, setControlValue) {
-  const div = d3.select(slice.selector);
-  const DEFAULT_POINT_RADIUS = 60;
-  const DEFAULT_MAX_ZOOM = 16;
 
-  // Validate mapbox color
-  const rgb = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/.exec(json.data.color);
-  if (rgb === null) {
-    slice.error('Color field must be of form \'rgb(%d, %d, %d)\'');
-    return;
-  }
-
-  const aggName = json.data.aggregatorName;
-  let reducer;
-
-  if (aggName === 'sum' || !json.data.customMetric) {
-    reducer = function (a, b) {
-      return a + b;
-    };
-  } else if (aggName === 'min') {
-    reducer = Math.min;
-  } else if (aggName === 'max') {
-    reducer = Math.max;
-  } else {
-    reducer = function (a, b) {
-      if (a instanceof Array) {
-        if (b instanceof Array) {
-          return a.concat(b);
-        }
-        a.push(b);
-        return a;
-      }
-      if (b instanceof Array) {
-        b.push(a);
-        return b;
-      }
-      return [a, b];
-    };
-  }
-
-  const clusterer = supercluster({
-    radius: json.data.clusteringRadius,
-    maxZoom: DEFAULT_MAX_ZOOM,
-    metricKey: 'metric',
-    metricReducer: reducer,
-  });
-  clusterer.load(json.data.geoJSON.features);
-
-  div.selectAll('*').remove();
-  ReactDOM.render(
-    <MapboxViz
-      {...json.data}
-      rgb={rgb}
-      sliceHeight={slice.height()}
-      sliceWidth={slice.width()}
-      clusterer={clusterer}
-      pointRadius={DEFAULT_POINT_RADIUS}
-      aggregatorName={aggName}
-      setControlValue={setControlValue || NOOP}
-    />,
-    div.node(),
-  );
-}
+ScatterPlotGlowOverlay.propTypes = propTypes;
+ScatterPlotGlowOverlay.defaultProps = defaultProps;
+ScatterPlotGlowOverlay.contextTypes = contextTypes;
 
-module.exports = mapbox;
+export default ScatterPlotGlowOverlay;
diff --git a/superset/assets/src/visualizations/index.js b/superset/assets/src/visualizations/index.js
index 7cccf1f..66cff81 100644
--- a/superset/assets/src/visualizations/index.js
+++ b/superset/assets/src/visualizations/index.js
@@ -89,7 +89,7 @@ const vizMap = {
   [VIZ_TYPES.line_multi]: () =>
     loadVis(import(/* webpackChunkName: "line_multi" */ './line_multi.js')),
   [VIZ_TYPES.time_pivot]: loadNvd3,
-  [VIZ_TYPES.mapbox]: () => loadVis(import(/* webpackChunkName: "mapbox" */ './mapbox.jsx')),
+  [VIZ_TYPES.mapbox]: () => loadVis(import(/* webpackChunkName: "mapbox" */ './MapBox/MapBox.jsx')),
   [VIZ_TYPES.markup]: () => loadVis(import(/* webpackChunkName: "markup" */ './markup.js')),
   [VIZ_TYPES.para]: () =>
     loadVis(import(/* webpackChunkName: "parallel_coordinates" */ './parallel_coordinates.js')),
diff --git a/superset/assets/src/visualizations/mapbox.css b/superset/assets/src/visualizations/mapbox.css
deleted file mode 100644
index babb33b..0000000
--- a/superset/assets/src/visualizations/mapbox.css
+++ /dev/null
@@ -1,16 +0,0 @@
-.mapbox div.widget .slice_container {
-    cursor: grab;
-    cursor: -moz-grab;
-    cursor: -webkit-grab;
-    overflow: hidden;
-}
-
-.mapbox div.widget .slice_container:active {
-    cursor: grabbing;
-    cursor: -moz-grabbing;
-    cursor: -webkit-grabbing;
-}
-
-.mapbox .slice_container div {
-    padding-top: 0px;
-}