Skip to content

Commit 5709d1b

Browse files
committed
add attribute featureidkey
... to choroplethmapbox, scattergeo and choropleth traces. - Coerced only when `locationmode: 'geojson-id'`. To determine which key (or nested key) to use to identify eahc geojson feature. - Improve error messages in extractTraceFeature
1 parent 6397ab3 commit 5709d1b

14 files changed

+192
-24
lines changed

src/lib/geo_location_utils.js

+16-7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var turfCentroid = require('@turf/centroid');
1616
var identity = require('./identity');
1717
var loggers = require('./loggers');
1818
var isPlainObject = require('./is_plain_object');
19+
var nestedProperty = require('./nested_property');
1920
var polygon = require('./polygon');
2021

2122
// make list of all country iso3 ids from at runtime
@@ -208,14 +209,16 @@ function extractTraceFeature(calcTrace) {
208209
}
209210

210211
function appendFeature(fIn) {
211-
var cdi = lookup[fIn.id];
212+
var id = nestedProperty(fIn, trace.featureidkey || 'id').get();
213+
var cdi = lookup[id];
212214

213215
if(cdi) {
214216
var geometry = fIn.geometry;
215217

216218
if(geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
217219
var fOut = {
218220
type: 'Feature',
221+
id: id,
219222
geometry: geometry,
220223
properties: {}
221224
};
@@ -230,15 +233,16 @@ function extractTraceFeature(calcTrace) {
230233
featuresOut.push(fOut);
231234
} else {
232235
loggers.log([
233-
'Location with id', cdi.loc, 'does not have a valid GeoJSON geometry,',
234-
'choroplethmapbox traces only support *Polygon* and *MultiPolygon* geometries.'
236+
'Location', cdi.loc, 'does not have a valid GeoJSON geometry.',
237+
'Traces with locationmode *geojson-id* only support',
238+
'*Polygon* and *MultiPolygon* geometries.'
235239
].join(' '));
236240
}
237241
}
238242

239243
// remove key from lookup, so that we can track (if any)
240244
// the locations that did not have a corresponding GeoJSON feature
241-
delete lookup[fIn.id];
245+
delete lookup[id];
242246
}
243247

244248
switch(geojsonIn.type) {
@@ -253,14 +257,19 @@ function extractTraceFeature(calcTrace) {
253257
break;
254258
default:
255259
loggers.warn([
256-
'Invalid GeoJSON type', (geojsonIn.type || 'none') + ',',
257-
'choroplethmapbox traces only support *FeatureCollection* and *Feature* types.'
260+
'Invalid GeoJSON type', (geojsonIn.type || 'none') + '.',
261+
'Traces with locationmode *geojson-id* only support',
262+
'*FeatureCollection* and *Feature* types.'
258263
].join(' '));
259264
return false;
260265
}
261266

262267
for(var loc in lookup) {
263-
loggers.log('Location with id ' + loc + ' does not have a matching feature');
268+
loggers.log([
269+
'Location *' + loc + '*',
270+
'does not have a matching feature with id-key',
271+
'*' + trace.featureidkey + '*.'
272+
].join(' '));
264273
}
265274

266275
return featuresOut;

src/traces/choropleth/attributes.js

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ module.exports = extendFlat({
4646
// https://github.com/topojson/topojson-specification/blob/master/README.md
4747
].join(' ')
4848
}),
49+
featureidkey: scatterGeoAttrs.featureidkey,
50+
4951
text: extendFlat({}, scatterGeoAttrs.text, {
5052
description: 'Sets the text elements associated with each location.'
5153
}),

src/traces/choropleth/defaults.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
2828
traceOut._length = Math.min(locations.length, z.length);
2929

3030
var geojson = coerce('geojson');
31+
3132
var locationmodeDflt;
3233
if((typeof geojson === 'string' && geojson !== '') || Lib.isPlainObject(geojson)) {
3334
locationmodeDflt = 'geojson-id';
3435
}
35-
coerce('locationmode', locationmodeDflt);
36+
37+
var locationMode = coerce('locationmode', locationmodeDflt);
38+
39+
if(locationMode === 'geojson-id') {
40+
coerce('featureidkey');
41+
}
3642

3743
coerce('text');
3844
coerce('hovertext');

src/traces/choroplethmapbox/attributes.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ module.exports = extendFlat({
2828
// Maybe start with only one value (that we could name e.g. 'geojson-id'),
2929
// but eventually:
3030
// - we could also support for our own dist/topojson/*
31-
// - some people might want `geojson-properties-name` to map data arrays to
32-
// GeoJSON features
33-
// locationmode: choroplethAttrs.locationmode,
31+
// .. and locationmode: choroplethAttrs.locationmode,
3432

3533
z: {
3634
valType: 'data_array',
@@ -52,6 +50,13 @@ module.exports = extendFlat({
5250
'with geometries of type *Polygon* and *MultiPolygon*.'
5351
].join(' ')
5452
},
53+
featureidkey: extendFlat({}, choroplethAttrs.featureidkey, {
54+
description: [
55+
'Sets the key in GeoJSON features which is used as id to match the items',
56+
'included in the `locations` array.',
57+
'Support nested property, for example *properties.name*.'
58+
].join(' ')
59+
}),
5560

5661
// TODO agree on name / behaviour
5762
//

src/traces/choroplethmapbox/defaults.js

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
2929
return;
3030
}
3131

32+
coerce('featureidkey');
33+
3234
traceOut._length = Math.min(locations.length, z.length);
3335

3436
coerce('below');

src/traces/scattergeo/attributes.js

+12
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ module.exports = overrideAll({
7070
// https://github.com/topojson/topojson-specification/blob/master/README.md
7171
].join(' ')
7272
},
73+
featureidkey: {
74+
valType: 'string',
75+
role: 'info',
76+
editType: 'calc',
77+
dflt: 'id',
78+
description: [
79+
'Sets the key in GeoJSON features which is used as id to match the items',
80+
'included in the `locations` array.',
81+
'Only has an effect when `geojson` is set.',
82+
'Support nested property, for example *properties.name*.'
83+
].join(' ')
84+
},
7385

7486
mode: extendFlat({}, scatterAttrs.mode, {dflt: 'markers'}),
7587

src/traces/scattergeo/defaults.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
3232
if((typeof geojson === 'string' && geojson !== '') || Lib.isPlainObject(geojson)) {
3333
locationmodeDflt = 'geojson-id';
3434
}
35-
coerce('locationmode', locationmodeDflt);
35+
36+
var locationMode = coerce('locationmode', locationmodeDflt);
37+
38+
if(locationMode === 'geojson-id') {
39+
coerce('featureidkey');
40+
}
41+
3642
len = locations.length;
3743
} else {
3844
var lon = coerce('lon') || [];
19.8 KB
Loading
Loading
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"data": [
3+
{
4+
"type": "scattergeo",
5+
"locations": ["AL"],
6+
"featureidkey": "properties.name",
7+
"geojson": {
8+
"type": "Feature",
9+
"properties": {
10+
"name": "AL"
11+
},
12+
"geometry": {
13+
"type": "Polygon",
14+
"coordinates": [[
15+
[-87.359296, 35.00118], [-85.606675, 34.984749], [-85.431413, 34.124869], [-85.184951, 32.859696],
16+
[-85.069935, 32.580372], [-84.960397, 32.421541], [-85.004212, 32.322956], [-84.889196, 32.262709],
17+
[-85.058981, 32.13674], [-85.053504, 32.01077], [-85.141136, 31.840985], [-85.042551, 31.539753],
18+
[-85.113751, 31.27686], [-85.004212, 31.003013], [-85.497137, 30.997536], [-87.600282, 30.997536],
19+
[-87.633143, 30.86609], [-87.408589, 30.674397], [-87.446927, 30.510088], [-87.37025, 30.427934],
20+
[-87.518128, 30.280057], [-87.655051, 30.247195], [-87.90699, 30.411504], [-87.934375, 30.657966],
21+
[-88.011052, 30.685351], [-88.10416, 30.499135], [-88.137022, 30.318396], [-88.394438, 30.367688],
22+
[-88.471115, 31.895754], [-88.241084, 33.796253], [-88.098683, 34.891641], [-88.202745, 34.995703],
23+
[-87.359296, 35.00118]
24+
]]
25+
}
26+
}
27+
},
28+
{
29+
"type": "choropleth",
30+
"name": "choropleth + RAW",
31+
"locations": ["AL"],
32+
"featureidkey": "properties.id",
33+
"z": [10],
34+
"showscale": false,
35+
"geojson": {
36+
"type": "Feature",
37+
"properties": {
38+
"id": "AL"
39+
},
40+
"geometry": {
41+
"type": "Polygon",
42+
"coordinates": [[
43+
[-87.359296, 35.00118], [-85.606675, 34.984749], [-85.431413, 34.124869], [-85.184951, 32.859696],
44+
[-85.069935, 32.580372], [-84.960397, 32.421541], [-85.004212, 32.322956], [-84.889196, 32.262709],
45+
[-85.058981, 32.13674], [-85.053504, 32.01077], [-85.141136, 31.840985], [-85.042551, 31.539753],
46+
[-85.113751, 31.27686], [-85.004212, 31.003013], [-85.497137, 30.997536], [-87.600282, 30.997536],
47+
[-87.633143, 30.86609], [-87.408589, 30.674397], [-87.446927, 30.510088], [-87.37025, 30.427934],
48+
[-87.518128, 30.280057], [-87.655051, 30.247195], [-87.90699, 30.411504], [-87.934375, 30.657966],
49+
[-88.011052, 30.685351], [-88.10416, 30.499135], [-88.137022, 30.318396], [-88.394438, 30.367688],
50+
[-88.471115, 31.895754], [-88.241084, 33.796253], [-88.098683, 34.891641], [-88.202745, 34.995703],
51+
[-87.359296, 35.00118]
52+
]]
53+
}
54+
}
55+
}
56+
],
57+
"layout": {
58+
"geo": {
59+
"center": { "lon": -86, "lat": 33 },
60+
"projection": {"scale": 30}
61+
},
62+
"width": 600,
63+
"height": 400,
64+
"showlegend": false,
65+
"title": {
66+
"text": "Geo traces with set <b>featureidkey</b>",
67+
"x": 0.1,
68+
"xref": "container",
69+
"xanchor": "left"
70+
}
71+
}
72+
}

test/image/mocks/mapbox_choropleth-raw-geojson.json

+14
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@
2828
]]
2929
}
3030
}
31+
}, {
32+
"type": "choroplethmapbox",
33+
"locations": ["Georgia"],
34+
"z": [5],
35+
"featureidkey": "properties.name",
36+
"coloraxis": "coloraxis",
37+
"geojson": {
38+
"type": "Feature",
39+
"properties": {"name": "Georgia"},
40+
"geometry": {
41+
"type": "Polygon",
42+
"coordinates": [[[-83.109191,35.00118],[-83.322791,34.787579],[-83.339222,34.683517],[-83.005129,34.469916],[-82.901067,34.486347],[-82.747713,34.26727],[-82.714851,34.152254],[-82.55602,33.94413],[-82.325988,33.81816],[-82.194542,33.631944],[-81.926172,33.462159],[-81.937125,33.347144],[-81.761863,33.160928],[-81.493493,33.007573],[-81.42777,32.843265],[-81.416816,32.629664],[-81.279893,32.558464],[-81.121061,32.290094],[-81.115584,32.120309],[-80.885553,32.032678],[-81.132015,31.693108],[-81.175831,31.517845],[-81.279893,31.364491],[-81.290846,31.20566],[-81.400385,31.13446],[-81.444201,30.707258],[-81.718048,30.745597],[-81.948079,30.827751],[-82.041187,30.751074],[-82.002849,30.564858],[-82.046664,30.362211],[-82.167157,30.356734],[-82.216449,30.570335],[-83.498053,30.647012],[-84.867289,30.712735],[-85.004212,31.003013],[-85.113751,31.27686],[-85.042551,31.539753],[-85.141136,31.840985],[-85.053504,32.01077],[-85.058981,32.13674],[-84.889196,32.262709],[-85.004212,32.322956],[-84.960397,32.421541],[-85.069935,32.580372],[-85.184951,32.859696],[-85.431413,34.124869],[-85.606675,34.984749],[-84.319594,34.990226],[-83.618546,34.984749],[-83.109191,35.00118]]]
43+
}
44+
}
3145
}],
3246
"layout": {
3347
"width": 600,

test/jasmine/tests/choropleth_test.js

+21
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,27 @@ describe('Test choropleth', function() {
127127
Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout);
128128
expect(traceOut.locationmode).toBe('ISO-3', 'invalid object');
129129
});
130+
131+
it('should only coerce *featureidkey* when locationmode is *geojson-id', function() {
132+
traceIn = {
133+
locations: ['CAN', 'USA'],
134+
z: [1, 2],
135+
geojson: 'url',
136+
featureidkey: 'properties.name'
137+
};
138+
traceOut = {};
139+
Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout);
140+
expect(traceOut.featureidkey).toBe('properties.name', 'coerced');
141+
142+
traceIn = {
143+
locations: ['CAN', 'USA'],
144+
z: [1, 2],
145+
featureidkey: 'properties.name'
146+
};
147+
traceOut = {};
148+
Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout);
149+
expect(traceOut.featureidkey).toBe(undefined, 'NOT coerced');
150+
});
130151
});
131152
});
132153

test/jasmine/tests/choroplethmapbox_test.js

+12-12
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ describe('Test choroplethmapbox convert:', function() {
174174
});
175175
expectBlank(opts);
176176
expect(loggers.warn).toHaveBeenCalledWith([
177-
'Invalid GeoJSON type none,',
178-
'choroplethmapbox traces only support *FeatureCollection* and *Feature* types.'
177+
'Invalid GeoJSON type none.',
178+
'Traces with locationmode *geojson-id* only support *FeatureCollection* and *Feature* types.'
179179
].join(' '));
180180
});
181181

@@ -188,8 +188,8 @@ describe('Test choroplethmapbox convert:', function() {
188188
});
189189
expectBlank(opts);
190190
expect(loggers.warn).toHaveBeenCalledWith([
191-
'Invalid GeoJSON type nop!,',
192-
'choroplethmapbox traces only support *FeatureCollection* and *Feature* types.'
191+
'Invalid GeoJSON type nop!.',
192+
'Traces with locationmode *geojson-id* only support *FeatureCollection* and *Feature* types.'
193193
].join(' '));
194194
});
195195
});
@@ -204,8 +204,8 @@ describe('Test choroplethmapbox convert:', function() {
204204
var opts = _convert(trace);
205205
expect(opts.geojson.features.length).toBe(2, '# of feature to be rendered');
206206
expect(loggers.log).toHaveBeenCalledWith([
207-
'Location with id b does not have a valid GeoJSON geometry,',
208-
'choroplethmapbox traces only support *Polygon* and *MultiPolygon* geometries.'
207+
'Location b does not have a valid GeoJSON geometry.',
208+
'Traces with locationmode *geojson-id* only support *Polygon* and *MultiPolygon* geometries.'
209209
].join(' '));
210210
});
211211

@@ -216,8 +216,8 @@ describe('Test choroplethmapbox convert:', function() {
216216
var opts = _convert(trace);
217217
expect(opts.geojson.features.length).toBe(2, '# of feature to be rendered');
218218
expect(loggers.log).toHaveBeenCalledWith([
219-
'Location with id c does not have a valid GeoJSON geometry,',
220-
'choroplethmapbox traces only support *Polygon* and *MultiPolygon* geometries.'
219+
'Location c does not have a valid GeoJSON geometry.',
220+
'Traces with locationmode *geojson-id* only support *Polygon* and *MultiPolygon* geometries.'
221221
].join(' '));
222222
});
223223
});
@@ -231,7 +231,7 @@ describe('Test choroplethmapbox convert:', function() {
231231

232232
var opts = _convert(trace);
233233
expect(opts.geojson.features.length).toBe(3, '# of features to be rendered');
234-
expect(loggers.log).toHaveBeenCalledWith('Location with id d does not have a matching feature');
234+
expect(loggers.log).toHaveBeenCalledWith('Location *d* does not have a matching feature with id-key *id*.');
235235
});
236236

237237
describe('should accept numbers as *locations* items', function() {
@@ -594,7 +594,7 @@ describe('@noCI Test choroplethmapbox hover:', function() {
594594
desc: 'with "typeof number" locations[i] and feature id (in *name* label case)',
595595
patch: function() {
596596
var fig = Lib.extendDeep({}, require('@mocks/mapbox_choropleth-raw-geojson.json'));
597-
fig.data.shift();
597+
fig.data = [fig.data[1]];
598598
fig.data[0].locations = [100];
599599
fig.data[0].geojson.id = 100;
600600
return fig;
@@ -606,7 +606,7 @@ describe('@noCI Test choroplethmapbox hover:', function() {
606606
desc: 'with "typeof number" locations[i] and feature id (in *nums* label case)',
607607
patch: function() {
608608
var fig = Lib.extendDeep({}, require('@mocks/mapbox_choropleth-raw-geojson.json'));
609-
fig.data.shift();
609+
fig.data = [fig.data[1]];
610610
fig.data[0].locations = [100];
611611
fig.data[0].geojson.id = 100;
612612
fig.data[0].hoverinfo = 'location+name';
@@ -619,7 +619,7 @@ describe('@noCI Test choroplethmapbox hover:', function() {
619619
desc: 'with "typeof number" locations[i] and feature id (hovertemplate case)',
620620
patch: function() {
621621
var fig = Lib.extendDeep({}, require('@mocks/mapbox_choropleth-raw-geojson.json'));
622-
fig.data.shift();
622+
fig.data = [fig.data[1]];
623623
fig.data[0].locations = [100];
624624
fig.data[0].geojson.id = 100;
625625
fig.data[0].hovertemplate = '### %{location}<extra>%{location} ###</extra>';

test/jasmine/tests/scattergeo_test.js

+19
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,25 @@ describe('Test scattergeo defaults', function() {
129129
ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
130130
expect(traceOut.locationmode).toBe(undefined, 'lon/lat coordinates');
131131
});
132+
133+
it('should only coerce *featureidkey* when locationmode is *geojson-id', function() {
134+
traceIn = {
135+
locations: ['CAN', 'USA'],
136+
geojson: 'url',
137+
featureidkey: 'properties.name'
138+
};
139+
traceOut = {};
140+
ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
141+
expect(traceOut.featureidkey).toBe('properties.name', 'coerced');
142+
143+
traceIn = {
144+
locations: ['CAN', 'USA'],
145+
featureidkey: 'properties.name'
146+
};
147+
traceOut = {};
148+
ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
149+
expect(traceOut.featureidkey).toBe(undefined, 'NOT coerced');
150+
});
132151
});
133152

134153
describe('Test scattergeo calc', function() {

0 commit comments

Comments
 (0)