Skip to content

Commit e8fcad0

Browse files
committed
Redesigned d3.geo.albersUsa.
This commit changes the albersUsa projection so that it uses a custom projection stream rather than a custom projection function. The projection stream passes geometry to all three constituent projections, which are then clipped to their respective viewports. The result is that geometry that appears in multiple projections (such as a graticule) is now rendered correctly in each, with clipping! In addition, the inverse projection is much easier to compute because the viewport regions of each projection are explicitly defined.
1 parent d1d71e1 commit e8fcad0

4 files changed

Lines changed: 108 additions & 120 deletions

File tree

d3.js

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3360,53 +3360,72 @@ d3 = function() {
33603360
return d3_geo_conic(d3_geo_conicEqualArea);
33613361
}).raw = d3_geo_conicEqualArea;
33623362
d3.geo.albersUsa = function() {
3363-
var lower48 = d3.geo.conicEqualArea().rotate([ 98, 0 ]).center([ 0, 38 ]).parallels([ 29.5, 45.5 ]);
3364-
var alaska = d3.geo.conicEqualArea().rotate([ 160, 0 ]).center([ 0, 60 ]).parallels([ 55, 65 ]);
3363+
var lower48 = d3.geo.conicEqualArea().rotate([ 96, 0 ]).center([ -.6, 38.7 ]).parallels([ 29.5, 45.5 ]);
3364+
var alaska = d3.geo.conicEqualArea().rotate([ 160, 0, -35 ]).center([ 45, 44 ]).parallels([ 55, 65 ]);
33653365
var hawaii = d3.geo.conicEqualArea().rotate([ 160, 0 ]).center([ 0, 20 ]).parallels([ 8, 18 ]);
3366-
var puertoRico = d3.geo.conicEqualArea().rotate([ 60, 0 ]).center([ 0, 10 ]).parallels([ 8, 18 ]);
3367-
var alaskaInvert, hawaiiInvert, puertoRicoInvert;
33683366
function albersUsa(coordinates) {
3369-
return projection(coordinates)(coordinates);
3370-
}
3371-
function projection(point) {
3372-
var lon = point[0], lat = point[1];
3373-
return lat > 50 ? alaska : lon < -140 ? hawaii : lat < 21 ? puertoRico : lower48;
3367+
var y = point[0], x = point[1];
3368+
return (x > 50 ? alaska : y < -140 ? hawaii : lower48)(coordinates);
33743369
}
33753370
albersUsa.invert = function(coordinates) {
3376-
return alaskaInvert(coordinates) || hawaiiInvert(coordinates) || puertoRicoInvert(coordinates) || lower48.invert(coordinates);
3371+
var k = lower48.scale(), t = lower48.translate(), x = (coordinates[0] - t[0]) / k, y = (coordinates[1] - t[1]) / k;
3372+
return (y >= .123 && y < .23 && x >= -.427 && x < -.215 ? alaska : y >= .16 && y < .23 && x >= -.215 && x < -.115 ? hawaii : lower48).invert(coordinates);
33773373
};
3378-
albersUsa.scale = function(x) {
3374+
albersUsa.stream = d3_geo_albersUsaStream([ lower48, alaska, hawaii ]);
3375+
albersUsa.scale = function(_) {
33793376
if (!arguments.length) return lower48.scale();
3380-
lower48.scale(x);
3381-
alaska.scale(x * .6);
3382-
hawaii.scale(x);
3383-
puertoRico.scale(x * 1.5);
3377+
lower48.scale(_);
3378+
alaska.scale(_ * .35);
3379+
hawaii.scale(_);
33843380
return albersUsa.translate(lower48.translate());
33853381
};
3386-
albersUsa.translate = function(x) {
3382+
albersUsa.translate = function(_) {
33873383
if (!arguments.length) return lower48.translate();
3388-
var dz = lower48.scale(), dx = x[0], dy = x[1];
3389-
lower48.translate(x);
3390-
alaska.translate([ dx - .4 * dz, dy + .17 * dz ]);
3391-
hawaii.translate([ dx - .19 * dz, dy + .2 * dz ]);
3392-
puertoRico.translate([ dx + .58 * dz, dy + .43 * dz ]);
3393-
alaskaInvert = d3_geo_albersUsaInvert(alaska, [ [ -180, 50 ], [ -130, 72 ] ]);
3394-
hawaiiInvert = d3_geo_albersUsaInvert(hawaii, [ [ -164, 18 ], [ -154, 24 ] ]);
3395-
puertoRicoInvert = d3_geo_albersUsaInvert(puertoRico, [ [ -67.5, 17.5 ], [ -65, 19 ] ]);
3384+
var k = lower48.scale(), x = +_[0], y = +_[1];
3385+
lower48.translate(_).clipExtent([ [ x - .455 * k, y - .238 * k ], [ x + .455 * k, y + .238 * k ] ]);
3386+
alaska.translate([ x - .307 * k, y + .197 * k ]).clipExtent([ [ x - .427 * k, y + .123 * k ], [ x - .215 * k, y + .23 * k ] ]);
3387+
hawaii.translate([ x - .205 * k, y + .205 * k ]).clipExtent([ [ x - .215 * k, y + .16 * k ], [ x - .115 * k, y + .23 * k ] ]);
33963388
return albersUsa;
33973389
};
3398-
return albersUsa.scale(1e3);
3399-
};
3400-
function d3_geo_albersUsaInvert(projection, extent) {
3401-
var a = projection(extent[0]), b = projection([ .5 * (extent[0][0] + extent[1][0]), extent[0][1] ]), c = projection([ extent[1][0], extent[0][1] ]), d = projection(extent[1]);
3402-
var dya = b[1] - a[1], dxa = b[0] - a[0], dyb = c[1] - b[1], dxb = c[0] - b[0];
3403-
var ma = dya / dxa, mb = dyb / dxb;
3404-
var cx = .5 * (ma * mb * (a[1] - c[1]) + mb * (a[0] + b[0]) - ma * (b[0] + c[0])) / (mb - ma), cy = (.5 * (a[0] + b[0]) - cx) / ma + .5 * (a[1] + b[1]);
3405-
var dx0 = d[0] - cx, dy0 = d[1] - cy, dx1 = a[0] - cx, dy1 = a[1] - cy, r0 = dx0 * dx0 + dy0 * dy0, r1 = dx1 * dx1 + dy1 * dy1;
3406-
var a0 = Math.atan2(dy0, dx0), a1 = Math.atan2(dy1, dx1);
3407-
return function(coordinates) {
3408-
var dx = coordinates[0] - cx, dy = coordinates[1] - cy, r = dx * dx + dy * dy, a = Math.atan2(dy, dx);
3409-
if (r0 < r && r < r1 && a0 < a && a < a1) return projection.invert(coordinates);
3390+
return albersUsa.scale(1056);
3391+
};
3392+
function d3_geo_albersUsaStream(projections) {
3393+
return function(stream) {
3394+
var streams = projections.map(function(p) {
3395+
return p.stream(stream);
3396+
});
3397+
return {
3398+
point: function(x, y) {
3399+
streams.forEach(function(s) {
3400+
s.point(x, y);
3401+
});
3402+
},
3403+
sphere: function(x, y) {
3404+
streams.forEach(function(s) {
3405+
s.sphere();
3406+
});
3407+
},
3408+
lineStart: function(x, y) {
3409+
streams.forEach(function(s) {
3410+
s.lineStart();
3411+
});
3412+
},
3413+
lineEnd: function(x, y) {
3414+
streams.forEach(function(s) {
3415+
s.lineEnd();
3416+
});
3417+
},
3418+
polygonStart: function(x, y) {
3419+
streams.forEach(function(s) {
3420+
s.polygonStart();
3421+
});
3422+
},
3423+
polygonEnd: function(x, y) {
3424+
streams.forEach(function(s) {
3425+
s.polygonEnd();
3426+
});
3427+
}
3428+
};
34103429
};
34113430
}
34123431
var d3_geo_pathAreaSum, d3_geo_pathAreaPolygon, d3_geo_pathArea = {

d3.min.js

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/geo/albers-usa.js

Lines changed: 46 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -6,110 +6,79 @@ import "geo";
66
// http://egsc.usgs.gov/isb/pubs/MapProjections/projections.html#albers
77
d3.geo.albersUsa = function() {
88
var lower48 = d3.geo.conicEqualArea()
9-
.rotate([98, 0])
10-
.center([0, 38])
9+
.rotate([96, 0])
10+
.center([-.6, 38.7])
1111
.parallels([29.5, 45.5]);
1212

1313
var alaska = d3.geo.conicEqualArea()
14-
.rotate([160, 0])
15-
.center([0, 60])
14+
.rotate([160, 0, -35])
15+
.center([45, 44])
1616
.parallels([55, 65]);
1717

1818
var hawaii = d3.geo.conicEqualArea()
1919
.rotate([160, 0])
2020
.center([0, 20])
2121
.parallels([8, 18]);
2222

23-
var puertoRico = d3.geo.conicEqualArea()
24-
.rotate([60, 0])
25-
.center([0, 10])
26-
.parallels([8, 18]);
27-
28-
var alaskaInvert,
29-
hawaiiInvert,
30-
puertoRicoInvert;
31-
3223
function albersUsa(coordinates) {
33-
return projection(coordinates)(coordinates);
34-
}
35-
36-
function projection(point) {
37-
var lon = point[0],
38-
lat = point[1];
39-
return lat > 50 ? alaska
40-
: lon < -140 ? hawaii
41-
: lat < 21 ? puertoRico
42-
: lower48;
24+
var y = coordinates[0], x = coordinates[1];
25+
return (x > 50 ? alaska : y < -140 ? hawaii : lower48)(coordinates);
4326
}
4427

4528
albersUsa.invert = function(coordinates) {
46-
return alaskaInvert(coordinates) || hawaiiInvert(coordinates) || puertoRicoInvert(coordinates) || lower48.invert(coordinates);
29+
var k = lower48.scale(),
30+
t = lower48.translate(),
31+
x = (coordinates[0] - t[0]) / k,
32+
y = (coordinates[1] - t[1]) / k;
33+
return (y >= .123 && y < .230 && x >= -.427 && x < -.215 ? alaska
34+
: y >= .160 && y < .230 && x >= -.215 && x < -.115 ? hawaii
35+
: lower48).invert(coordinates);
4736
};
4837

49-
albersUsa.scale = function(x) {
38+
albersUsa.stream = d3_geo_albersUsaStream([lower48, alaska, hawaii]);
39+
40+
albersUsa.scale = function(_) {
5041
if (!arguments.length) return lower48.scale();
51-
lower48.scale(x);
52-
alaska.scale(x * .6);
53-
hawaii.scale(x);
54-
puertoRico.scale(x * 1.5);
42+
lower48.scale(_);
43+
alaska.scale(_ * .35);
44+
hawaii.scale(_);
5545
return albersUsa.translate(lower48.translate());
5646
};
5747

58-
albersUsa.translate = function(x) {
48+
albersUsa.translate = function(_) {
5949
if (!arguments.length) return lower48.translate();
60-
var dz = lower48.scale(),
61-
dx = x[0],
62-
dy = x[1];
63-
lower48.translate(x);
64-
alaska.translate([dx - .40 * dz, dy + .17 * dz]);
65-
hawaii.translate([dx - .19 * dz, dy + .20 * dz]);
66-
puertoRico.translate([dx + .58 * dz, dy + .43 * dz]);
50+
var k = lower48.scale(), x = +_[0], y = +_[1];
51+
52+
lower48
53+
.translate(_)
54+
.clipExtent([[x - .455 * k, y - .238 * k], [x + .455 * k, y + .238 * k]]);
6755

68-
alaskaInvert = d3_geo_albersUsaInvert(alaska, [[-180, 50], [-130, 72]]);
69-
hawaiiInvert = d3_geo_albersUsaInvert(hawaii, [[-164, 18], [-154, 24]]);
70-
puertoRicoInvert = d3_geo_albersUsaInvert(puertoRico, [[-67.5, 17.5], [-65, 19]]);
56+
alaska
57+
.translate([x - .307 * k, y + .197 * k])
58+
.clipExtent([[x - .427 * k, y + .123 * k], [x - .215 * k, y + .230 * k]]);
59+
60+
hawaii
61+
.translate([x - .205 * k, y + .205 * k])
62+
.clipExtent([[x - .215 * k, y + .160 * k], [x - .115 * k, y + .230 * k]]);
7163

7264
return albersUsa;
7365
};
7466

75-
return albersUsa.scale(1000);
67+
return albersUsa.scale(1056);
7668
};
7769

78-
function d3_geo_albersUsaInvert(projection, extent) {
79-
var a = projection(extent[0]),
80-
b = projection([.5 * (extent[0][0] + extent[1][0]), extent[0][1]]),
81-
c = projection([extent[1][0], extent[0][1]]),
82-
d = projection(extent[1]);
83-
84-
var dya = b[1]- a[1],
85-
dxa = b[0]- a[0],
86-
dyb = c[1]- b[1],
87-
dxb = c[0]- b[0];
88-
89-
var ma = dya / dxa,
90-
mb = dyb / dxb;
91-
92-
// Find center of circle going through points [a, b, c].
93-
var cx = .5 * (ma * mb * (a[1] - c[1]) + mb * (a[0] + b[0]) - ma * (b[0] + c[0])) / (mb - ma),
94-
cy = (.5 * (a[0] + b[0]) - cx) / ma + .5 * (a[1] + b[1]);
95-
96-
// Radial distance² from center.
97-
var dx0 = d[0] - cx,
98-
dy0 = d[1] - cy,
99-
dx1 = a[0] - cx,
100-
dy1 = a[1] - cy,
101-
r0 = dx0 * dx0 + dy0 * dy0,
102-
r1 = dx1 * dx1 + dy1 * dy1;
103-
104-
// Angular extent.
105-
var a0 = Math.atan2(dy0, dx0),
106-
a1 = Math.atan2(dy1, dx1);
107-
108-
return function(coordinates) {
109-
var dx = coordinates[0] - cx,
110-
dy = coordinates[1] - cy,
111-
r = dx * dx + dy * dy,
112-
a = Math.atan2(dy, dx);
113-
if (r0 < r && r < r1 && a0 < a && a < a1) return projection.invert(coordinates);
70+
// A naïve multi-projection stream.
71+
// It probably only works because clipping buffers internally.
72+
function d3_geo_albersUsaStream(projections) {
73+
return function(stream) {
74+
var streams = projections.map(function(p) { return p.stream(stream); });
75+
return {
76+
point: function(x, y) { streams.forEach(function(s) { s.point(x, y); }); },
77+
sphere: function(x, y) { streams.forEach(function(s) { s.sphere(); }); },
78+
lineStart: function(x, y) { streams.forEach(function(s) { s.lineStart(); }); },
79+
lineEnd: function(x, y) { streams.forEach(function(s) { s.lineEnd(); }); },
80+
polygonStart: function(x, y) { streams.forEach(function(s) { s.polygonStart(); }); },
81+
polygonEnd: function(x, y) { streams.forEach(function(s) { s.polygonEnd(); }); }
82+
};
11483
};
11584
}

test/geo/path-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,13 +334,13 @@ suite.addBatch({
334334
},
335335
"coerces point radius to a number": {
336336
"when the radius is specified as a constant": function(path) {
337-
var p = path().context(testContext).pointRadius("6");
337+
var p = path().projection(null).context(testContext).pointRadius("6");
338338
assert.strictEqual(p.pointRadius(), 6);
339339
p({type: "Point", coordinates: [0, 0]});
340340
assert.strictEqual(testContext.buffer().filter(function(d) { return d.type === "arc"; })[0].r, 6);
341341
},
342342
"when the radius is specified as a function": function(path) {
343-
var p = path().context(testContext).pointRadius(function() { return "6"; });
343+
var p = path().projection(null).context(testContext).pointRadius(function() { return "6"; });
344344
p({type: "Point", coordinates: [0, 0]});
345345
assert.strictEqual(testContext.buffer().filter(function(d) { return d.type === "arc"; })[0].r, 6);
346346
}

0 commit comments

Comments
 (0)