Skip to content

Commit 190c733

Browse files
committed
fix choropleth hover over Antarctica
- ++ cleaner hover polygon logic - no mutation when offsetting RUS and FJI longitudes - do not offset RUS and FJI polygons that do not need modification for hover!
1 parent 0089488 commit 190c733

File tree

2 files changed

+106
-28
lines changed

2 files changed

+106
-28
lines changed

src/traces/choropleth/plot.js

+93-26
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,8 @@ function calcGeoJSON(calcTrace, topojson) {
5555
var trace = calcTrace[0].trace;
5656
var len = calcTrace.length;
5757
var features = getTopojsonFeatures(trace, topojson);
58-
var i, j, k, m;
5958

60-
for(i = 0; i < len; i++) {
59+
for(var i = 0; i < len; i++) {
6160
var calcPt = calcTrace[i];
6261
var feature = locationToFeature(trace.locationmode, calcPt.loc, features);
6362

@@ -66,48 +65,116 @@ function calcGeoJSON(calcTrace, topojson) {
6665
continue;
6766
}
6867

68+
6969
calcPt.geojson = feature;
7070
calcPt.ct = feature.properties.ct;
7171
calcPt.index = i;
72+
calcPt._polygons = feature2polygons(feature);
73+
}
74+
}
7275

73-
var geometry = feature.geometry;
74-
var coords = geometry.coordinates;
75-
calcPt._polygons = [];
76+
function feature2polygons(feature) {
77+
var geometry = feature.geometry;
78+
var coords = geometry.coordinates;
79+
var loc = feature.id;
80+
81+
var polygons = [];
82+
var appendPolygon, j, k, m;
83+
84+
function doesCrossAntiMerdian(pts) {
85+
for(var l = 0; l < pts.length - 1; l++) {
86+
if(pts[l][0] > 0 && pts[l + 1][0] < 0) return l;
87+
}
88+
return null;
89+
}
7690

91+
if(loc === 'RUS' || loc === 'FJI') {
7792
// Russia and Fiji have landmasses that cross the antimeridian,
7893
// we need to add +360 to their longitude coordinates, so that
7994
// polygon 'contains' doesn't get confused when crossing the antimeridian.
8095
//
8196
// Note that other countries have polygons on either side of the antimeridian
8297
// (e.g. some Aleutian island for the USA), but those don't confuse
8398
// the 'contains' method; these are skipped here.
84-
if(calcPt.loc === 'RUS' || calcPt.loc === 'FJI') {
85-
for(j = 0; j < coords.length; j++) {
86-
for(k = 0; k < coords[j].length; k++) {
87-
for(m = 0; m < coords[j][k].length; m++) {
88-
if(coords[j][k][m][0] < 0) {
89-
coords[j][k][m][0] += 360;
90-
}
91-
}
99+
appendPolygon = function(_pts) {
100+
var pts;
101+
102+
if(doesCrossAntiMerdian(_pts) === null) {
103+
pts = _pts;
104+
} else {
105+
pts = new Array(_pts.length);
106+
for(m = 0; m < _pts.length; m++) {
107+
// do nut mutate calcdata[i][j].geojson !!
108+
pts[m] = [
109+
_pts[m][0] < 0 ? _pts[m][0] + 360 : _pts[m][0],
110+
_pts[m][1]
111+
];
92112
}
93113
}
94-
}
95114

96-
switch(geometry.type) {
97-
case 'MultiPolygon':
98-
for(j = 0; j < coords.length; j++) {
99-
for(k = 0; k < coords[j].length; k++) {
100-
calcPt._polygons.push(polygon.tester(coords[j][k]));
101-
}
115+
polygons.push(polygon.tester(pts));
116+
};
117+
} else if(loc === 'ATA') {
118+
// Antarctica has a landmass that wraps around every longitudes which
119+
// confuses the 'contains' methods.
120+
appendPolygon = function(pts) {
121+
var crossAntiMeridianIndex = doesCrossAntiMerdian(pts);
122+
123+
// polygon that do not cross anti-meridian need no special handling
124+
if(crossAntiMeridianIndex === null) {
125+
return polygons.push(polygon.tester(pts));
126+
}
127+
128+
// stitch polygon by adding pt over South Pole,
129+
// so that it covers the projected region covers all latitudes
130+
//
131+
// Note that the algorithm below only works for polygons that
132+
// start and end on longitude -180 (like the ones built by
133+
// https://github.com/etpinard/sane-topojson).
134+
var stitch = new Array(pts.length + 1);
135+
var si = 0;
136+
137+
for(m = 0; m < pts.length; m++) {
138+
if(m > crossAntiMeridianIndex) {
139+
stitch[si++] = [pts[m][0] + 360, pts[m][1]];
140+
} else if(m === crossAntiMeridianIndex) {
141+
stitch[si++] = pts[m];
142+
stitch[si++] = [pts[m][0], -90];
143+
} else {
144+
stitch[si++] = pts[m];
102145
}
103-
break;
104-
case 'Polygon':
105-
for(j = 0; j < coords.length; j++) {
106-
calcPt._polygons.push(polygon.tester(coords[j]));
146+
}
147+
148+
// polygon.tester by default appends pt[0] to the points list,
149+
// we must remove it here, to avoid a jump in longitude from 180 to -180,
150+
// that would confuse the 'contains' method
151+
var tester = polygon.tester(stitch);
152+
tester.pts.pop();
153+
polygons.push(tester);
154+
};
155+
} else {
156+
// otherwise using same array ref is fine
157+
appendPolygon = function(pts) {
158+
polygons.push(polygon.tester(pts));
159+
};
160+
}
161+
162+
switch(geometry.type) {
163+
case 'MultiPolygon':
164+
for(j = 0; j < coords.length; j++) {
165+
for(k = 0; k < coords[j].length; k++) {
166+
appendPolygon(coords[j][k]);
107167
}
108-
break;
109-
}
168+
}
169+
break;
170+
case 'Polygon':
171+
for(j = 0; j < coords.length; j++) {
172+
appendPolygon(coords[j]);
173+
}
174+
break;
110175
}
176+
177+
return polygons;
111178
}
112179

113180
function style(geo) {

test/jasmine/tests/geo_test.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -1306,8 +1306,8 @@ describe('Test geo interactions', function() {
13061306

13071307
Plotly.newPlot(gd, [{
13081308
type: 'choropleth',
1309-
locations: ['RUS', 'FJI'],
1310-
z: [0, 1]
1309+
locations: ['RUS', 'FJI', 'ATA'],
1310+
z: [0, 1, 2]
13111311
}])
13121312
.then(function() {
13131313
check([81, 66], 1, 'spot in north-central Russia that polygon.contains gets wrong before +360 shift');
@@ -1326,6 +1326,17 @@ describe('Test geo interactions', function() {
13261326
.then(function() {
13271327
check([179, -16.6], 1, 'spot on Fiji island that cross antimeridian west of antimeridian');
13281328
check([-179.9, -16.2], 1, 'spot on Fiji island that cross antimeridian east of antimeridian');
1329+
1330+
return Plotly.relayout(gd, {
1331+
'geo.center.lat': null,
1332+
'geo.projection': {
1333+
type: 'orthographic',
1334+
rotation: {lat: -90}
1335+
}
1336+
});
1337+
})
1338+
.then(function() {
1339+
check([-150, -89], 1, 'spot in Antarctica that requires *stitching*');
13291340
})
13301341
.catch(fail)
13311342
.then(done);

0 commit comments

Comments
 (0)