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

[GitHub] williaster closed pull request #5783: [SIP-5] Refactor MapBox

williaster closed pull request #5783: [SIP-5] Refactor MapBox
URL: https://github.com/apache/incubator-superset/pull/5783
 
 
   

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/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx
index 8c063a7fc7..6df8f49d19 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 0000000000..3ec640dac6
--- /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 0000000000..81f41f074b
--- /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 1a156ae520..ea4e115de3 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 c322bef528..fc4b2ec51a 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 babb33be0e..0000000000
--- 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;
-}


 

----------------------------------------------------------------
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

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org