Skip to content

Commit 5d27983

Browse files
committed
introducing choroplethmapbox trace module
- reusing choropleth calc, hover, select, eventData and feature2polygon - use `PlotlyGeoAssets` to stash fetched geojson url data - use two layers, one fill (to filled choropleth polygon), one line to draw `marker.line` styles - add mockAxis on mapbox subplot instances, used to format choroplethmapbox 'z' on hover
1 parent 012a749 commit 5d27983

File tree

12 files changed

+501
-4
lines changed

12 files changed

+501
-4
lines changed

lib/choroplethmapbox.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = require('../src/traces/choroplethmapbox');

lib/index-mapbox.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
var Plotly = require('./core');
1212

1313
Plotly.register([
14-
require('./scattermapbox')
14+
require('./scattermapbox'),
15+
require('./choroplethmapbox')
1516
]);
1617

1718
module.exports = Plotly;

lib/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ Plotly.register([
4444

4545
require('./pointcloud'),
4646
require('./heatmapgl'),
47+
4748
require('./parcoords'),
49+
4850
require('./parcats'),
51+
4952
require('./scattermapbox'),
53+
require('./choroplethmapbox'),
5054

5155
require('./sankey'),
5256

package-lock.json

+31
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
},
5858
"dependencies": {
5959
"@plotly/d3-sankey": "0.7.2",
60+
"@turf/area": "^6.0.1",
61+
"@turf/centroid": "^6.0.2",
6062
"alpha-shape": "^1.0.0",
6163
"canvas-fit": "^1.5.0",
6264
"color-normalize": "^1.3.0",

src/plots/mapbox/mapbox.js

+9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var d3 = require('d3');
1616
var Fx = require('../../components/fx');
1717
var Lib = require('../../lib');
1818
var Registry = require('../../registry');
19+
var Axes = require('../cartesian/axes');
1920
var dragElement = require('../../components/dragelement');
2021
var prepSelect = require('../cartesian/select').prepSelect;
2122
var selectOnClick = require('../cartesian/select').selectOnClick;
@@ -408,6 +409,14 @@ proto.createFramework = function(fullLayout) {
408409
};
409410

410411
self.updateFramework(fullLayout);
412+
413+
// mock axis for hover formatting
414+
self.mockAxis = {
415+
type: 'linear',
416+
showexponent: 'all',
417+
exponentformat: 'B'
418+
};
419+
Axes.setConvert(self.mockAxis, fullLayout);
411420
};
412421

413422
proto.updateFx = function(fullLayout) {

src/traces/choropleth/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module.exports = {
1313
supplyDefaults: require('./defaults'),
1414
colorbar: require('../heatmap/colorbar'),
1515
calc: require('./calc'),
16-
plot: require('./plot'),
16+
plot: require('./plot').plot,
1717
style: require('./style').style,
1818
styleOnSelect: require('./style').styleOnSelect,
1919
hoverPoints: require('./hover'),

src/traces/choropleth/plot.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ var getTopojsonFeatures = require('../../lib/topojson_utils').getTopojsonFeature
1717
var locationToFeature = require('../../lib/geo_location_utils').locationToFeature;
1818
var style = require('./style').style;
1919

20-
module.exports = function plot(gd, geo, calcData) {
20+
function plot(gd, geo, calcData) {
2121
for(var i = 0; i < calcData.length; i++) {
2222
calcGeoJSON(calcData[i], geo.topojson);
2323
}
@@ -37,7 +37,7 @@ module.exports = function plot(gd, geo, calcData) {
3737
// call style here within topojson request callback
3838
style(gd, calcTrace);
3939
});
40-
};
40+
}
4141

4242
function calcGeoJSON(calcTrace, topojson) {
4343
var trace = calcTrace[0].trace;
@@ -162,3 +162,8 @@ function feature2polygons(feature) {
162162

163163
return polygons;
164164
}
165+
166+
module.exports = {
167+
plot: plot,
168+
feature2polygons: feature2polygons
169+
};
+243
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/**
2+
* Copyright 2012-2019, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var isNumeric = require('fast-isnumeric');
12+
var turfArea = require('@turf/area');
13+
var turfCentroid = require('@turf/centroid');
14+
15+
var Lib = require('../../lib');
16+
var Colorscale = require('../../components/colorscale');
17+
var Drawing = require('../../components/drawing');
18+
19+
var makeBlank = require('../../lib/geojson_utils').makeBlank;
20+
var feature2polygons = require('../choropleth/plot').feature2polygons;
21+
22+
/* N.B.
23+
*
24+
* We fetch the GeoJSON files "ourselves" (during
25+
* mapbox.prototype.fetchMapData) where they are stored in a global object
26+
* named `PlotlyGeoAssets` (same as for topojson files in `geo` subplots).
27+
*
28+
* Mapbox does allow using URLs as geojson sources, but does NOT allow filtering
29+
* features by feature `id` that are not numbers (more info in:
30+
* https://github.com/mapbox/mapbox-gl-js/issues/8088).
31+
*/
32+
33+
function convert(calcTrace) {
34+
var trace = calcTrace[0].trace;
35+
var isVisible = trace.visible === true && trace._length !== 0;
36+
37+
var fill = {
38+
layout: {visibility: 'none'},
39+
paint: {}
40+
};
41+
42+
var line = {
43+
layout: {visibility: 'none'},
44+
paint: {}
45+
};
46+
47+
var opts = trace._opts = {
48+
fill: fill,
49+
line: line,
50+
geojson: makeBlank()
51+
};
52+
53+
if(!isVisible) return opts;
54+
55+
var geojsonIn = typeof trace.geojson === 'string' ?
56+
(window.PlotlyGeoAssets || {})[trace.geojson] :
57+
trace.geojson;
58+
59+
// This should not happen, but just in case something goes
60+
// really wrong when fetching the GeoJSON
61+
if(!Lib.isPlainObject(geojsonIn)) {
62+
Lib.error('Oops ... something when wrong when fetching ' + trace.geojson);
63+
return opts;
64+
}
65+
66+
var lookup = {};
67+
var featuresOut = [];
68+
var i;
69+
70+
for(i = 0; i < calcTrace.length; i++) {
71+
var cdi = calcTrace[i];
72+
if(cdi.loc) lookup[cdi.loc] = cdi;
73+
}
74+
75+
var sclFunc = Colorscale.makeColorScaleFuncFromTrace(trace);
76+
var marker = trace.marker;
77+
var markerLine = marker.line || {};
78+
79+
var opacityFn;
80+
if(Lib.isArrayOrTypedArray(marker.opacity)) {
81+
opacityFn = function(d) {
82+
var mo = d.mo;
83+
return isNumeric(mo) ? +Lib.constrain(mo, 0, 1) : 0;
84+
};
85+
}
86+
87+
var lineColorFn;
88+
if(Lib.isArrayOrTypedArray(markerLine.color)) {
89+
lineColorFn = function(d) { return d.mlc; };
90+
}
91+
92+
var lineWidthFn;
93+
if(Lib.isArrayOrTypedArray(markerLine.width)) {
94+
lineWidthFn = function(d) { return d.mlw; };
95+
}
96+
97+
function appendFeature(fIn) {
98+
var cdi = lookup[fIn.id];
99+
100+
if(cdi) {
101+
var geometry = fIn.geometry;
102+
103+
if(geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
104+
var props = {fc: sclFunc(cdi.z)};
105+
106+
if(opacityFn) props.mo = opacityFn(cdi);
107+
if(lineColorFn) props.mlc = lineColorFn(cdi);
108+
if(lineWidthFn) props.mlw = lineWidthFn(cdi);
109+
110+
var fOut = {
111+
type: 'Feature',
112+
geometry: geometry,
113+
properties: props
114+
};
115+
116+
cdi._polygons = feature2polygons(fOut);
117+
cdi.ct = findCentroid(fOut);
118+
cdi.fIn = fIn;
119+
cdi.fOut = fOut;
120+
featuresOut.push(fOut);
121+
} else {
122+
Lib.log([
123+
'Location with id', cdi.loc, 'does not have a valid GeoJSON geometry,',
124+
'choroplethmapbox traces only support *Polygon* and *MultiPolygon* geometries.'
125+
].join(' '));
126+
}
127+
}
128+
129+
// remove key from lookup, so that we can track (if any)
130+
// the locations that did not have a corresponding GeoJSON feature
131+
delete lookup[fIn.id];
132+
}
133+
134+
switch(geojsonIn.type) {
135+
case 'FeatureCollection':
136+
var featuresIn = geojsonIn.features;
137+
for(i = 0; i < featuresIn.length; i++) {
138+
appendFeature(featuresIn[i]);
139+
}
140+
break;
141+
case 'Feature':
142+
appendFeature(geojsonIn);
143+
break;
144+
default:
145+
Lib.warn([
146+
'Invalid GeoJSON type', (geojsonIn.type || 'none') + ',',
147+
'choroplethmapbox traces only support *FeatureCollection* and *Feature* types.'
148+
].join(' '));
149+
return opts;
150+
}
151+
152+
for(var loc in lookup) {
153+
Lib.log('Location with id ' + loc + ' does not have a matching feature');
154+
}
155+
156+
var opacitySetting = opacityFn ?
157+
{type: 'identity', property: 'mo'} :
158+
marker.opacity;
159+
160+
Lib.extendFlat(fill.paint, {
161+
'fill-color': {type: 'identity', property: 'fc'},
162+
'fill-opacity': opacitySetting
163+
});
164+
165+
Lib.extendFlat(line.paint, {
166+
'line-color': lineColorFn ?
167+
{type: 'identity', property: 'mlc'} :
168+
markerLine.color,
169+
'line-width': lineWidthFn ?
170+
{type: 'identity', property: 'mlw'} :
171+
markerLine.width,
172+
'line-opacity': opacitySetting
173+
});
174+
175+
fill.layout.visibility = 'visible';
176+
line.layout.visibility = 'visible';
177+
178+
opts.geojson = {type: 'FeatureCollection', features: featuresOut};
179+
180+
convertOnSelect(calcTrace);
181+
182+
return opts;
183+
}
184+
185+
function convertOnSelect(calcTrace) {
186+
var trace = calcTrace[0].trace;
187+
var opts = trace._opts;
188+
var opacitySetting;
189+
190+
if(trace.selectedpoints) {
191+
var fns = Drawing.makeSelectedPointStyleFns(trace);
192+
193+
for(var i = 0; i < calcTrace.length; i++) {
194+
var cdi = calcTrace[i];
195+
if(cdi.fOut) {
196+
cdi.fOut.properties.mo2 = fns.selectedOpacityFn(cdi);
197+
}
198+
}
199+
200+
opacitySetting = {type: 'identity', property: 'mo2'};
201+
} else {
202+
opacitySetting = Lib.isArrayOrTypedArray(trace.marker.opacity) ?
203+
{type: 'identity', property: 'mo'} :
204+
trace.marker.opacity;
205+
}
206+
207+
Lib.extendFlat(opts.fill.paint, {'fill-opacity': opacitySetting});
208+
Lib.extendFlat(opts.line.paint, {'line-opacity': opacitySetting});
209+
210+
return opts;
211+
}
212+
213+
// TODO this find the centroid of the polygon of maxArea
214+
// (just like we currently do for geo choropleth polygons),
215+
// maybe instead it would make more sense to compute the centroid
216+
// of each polygon and consider those on hover/select
217+
function findCentroid(feature) {
218+
var geometry = feature.geometry;
219+
var poly;
220+
221+
if(geometry.type === 'MultiPolygon') {
222+
var coords = geometry.coordinates;
223+
var maxArea = 0;
224+
225+
for(var i = 0; i < coords.length; i++) {
226+
var polyi = {type: 'Polygon', coordinates: coords[i]};
227+
var area = turfArea.default(polyi);
228+
if(area > maxArea) {
229+
maxArea = area;
230+
poly = polyi;
231+
}
232+
}
233+
} else {
234+
poly = geometry;
235+
}
236+
237+
return turfCentroid.default(poly).geometry.coordinates;
238+
}
239+
240+
module.exports = {
241+
convert: convert,
242+
convertOnSelect: convertOnSelect
243+
};

0 commit comments

Comments
 (0)