Skip to content

Commit abc6541

Browse files
committed
Add d3.geo.clip for radial clipping.
This can be used with d3.geo.path.clip to clip the input coordinates of geographical shapes using a given origin and angular radius, e.g. for hemispherical or near-hemispherical views. Geodesics are inserted as necessary. This also includes a minor bugfix and test for d3.geo.path: the last coordinate of Polygon features was being included unnecessarily (MultiPolygon already handled this correctly).
1 parent afe60a1 commit abc6541

9 files changed

Lines changed: 264 additions & 26 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ d3.geo.js: \
186186
src/geo/path.js \
187187
src/geo/bounds.js \
188188
src/geo/greatCircle.js \
189+
src/geo/clip.js \
189190
src/end.js
190191

191192
d3.csv.js: \

d3.geo.js

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ d3.geo.mercator = function() {
253253
d3.geo.path = function() {
254254
var pointRadius = 4.5,
255255
pointCircle = d3_path_circle(pointRadius),
256-
projection = d3.geo.albersUsa();
256+
projection = d3.geo.albersUsa(),
257+
clip = Object;
257258

258259
function path(d, i) {
259260
if (typeof pointRadius === "function") {
@@ -282,12 +283,14 @@ d3.geo.path = function() {
282283
},
283284

284285
Point: function(o) {
285-
return "M" + project(o.coordinates) + pointCircle;
286+
var coordinates = clip.call(this, [o.coordinates]);
287+
return coordinates.length
288+
? "M" + project(coordinates[0]) + pointCircle : "";
286289
},
287290

288291
MultiPoint: function(o) {
289292
var path = [],
290-
coordinates = o.coordinates,
293+
coordinates = clip.call(this, o.coordinates),
291294
i = -1, // coordinates.index
292295
n = coordinates.length;
293296
while (++i < n) path.push("M", project(coordinates[i]), pointCircle);
@@ -296,7 +299,7 @@ d3.geo.path = function() {
296299

297300
LineString: function(o) {
298301
var path = ["M"],
299-
coordinates = o.coordinates,
302+
coordinates = clip.call(this, o.coordinates),
300303
i = -1, // coordinates.index
301304
n = coordinates.length;
302305
while (++i < n) path.push(project(coordinates[i]), "L");
@@ -313,7 +316,7 @@ d3.geo.path = function() {
313316
j, // subcoordinates.index
314317
m; // subcoordinates.length
315318
while (++i < n) {
316-
subcoordinates = coordinates[i];
319+
subcoordinates = clip.call(this, coordinates[i]);
317320
j = -1;
318321
m = subcoordinates.length;
319322
path.push("M");
@@ -332,9 +335,10 @@ d3.geo.path = function() {
332335
j, // subcoordinates.index
333336
m; // subcoordinates.length
334337
while (++i < n) {
335-
subcoordinates = coordinates[i];
338+
subcoordinates = clip.call(this, coordinates[i]);
336339
j = -1;
337-
m = subcoordinates.length;
340+
m = subcoordinates.length - 1;
341+
if (m < 1) continue;
338342
path.push("M");
339343
while (++j < m) path.push(project(subcoordinates[j]), "L");
340344
path[path.length - 1] = "Z";
@@ -358,9 +362,10 @@ d3.geo.path = function() {
358362
j = -1;
359363
m = subcoordinates.length;
360364
while (++j < m) {
361-
subsubcoordinates = subcoordinates[j];
365+
subsubcoordinates = clip.call(this, subcoordinates[j]);
362366
k = -1;
363367
p = subsubcoordinates.length - 1;
368+
if (p < 1) continue;
364369
path.push("M");
365370
while (++k < p) path.push(project(subsubcoordinates[k]), "L");
366371
path[path.length - 1] = "Z";
@@ -498,6 +503,12 @@ d3.geo.path = function() {
498503
return path;
499504
};
500505

506+
path.clip = function(x) {
507+
if (!arguments.length) return clip;
508+
clip = x;
509+
return path;
510+
}
511+
501512
path.area = function(d) {
502513
return d3_geo_pathType(areaTypes, d);
503514
};
@@ -616,7 +627,7 @@ d3.geo.greatCircle = function() {
616627
radius = 6371; // Mean radius of Earth, in km.
617628
// TODO: breakAtDateLine?
618629

619-
function greatCircle(d, i) {
630+
function greatCircle(d, i, dist) {
620631
var from = source.call(this, d, i),
621632
to = target.call(this, d, i),
622633
x0 = from[0] * d3_radians,
@@ -627,13 +638,15 @@ d3.geo.greatCircle = function() {
627638
cy0 = Math.cos(y0), sy0 = Math.sin(y0),
628639
cx1 = Math.cos(x1), sx1 = Math.sin(x1),
629640
cy1 = Math.cos(y1), sy1 = Math.sin(y1),
630-
d = Math.acos(sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)),
641+
d = dist || Math.acos(sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)),
631642
sd = Math.sin(d),
632643
f = d / (n - 1),
633644
e = -f,
634645
path = [],
635646
i = -1;
636647

648+
if (d === 0) return [from];
649+
637650
while (++i < n) {
638651
e += f;
639652
var A = Math.sin(d - e) / sd,
@@ -699,4 +712,89 @@ function d3_geo_greatCircleSource(d) {
699712
function d3_geo_greatCircleTarget(d) {
700713
return d.target;
701714
}
715+
d3.geo.clip = function() {
716+
var origin = [0, 0],
717+
angle = 90,
718+
r = 6371 * angle / 180 * Math.PI;
719+
720+
function clip(d) {
721+
var o = {source: origin, target: null},
722+
n = d.length,
723+
i = -1,
724+
j,
725+
path,
726+
clipped = [],
727+
p = null,
728+
q = null;
729+
while (++i < n) {
730+
o.target = d[i];
731+
distance = d3_geo_clipGreatCircle.distance(o);
732+
if (distance < r) {
733+
if (q) {
734+
path = d3_geo_clipGreatCircle({source: q, target: o.target});
735+
j = d3_geo_clipClosest(path, o, r);
736+
if (p) {
737+
clipped.push.apply(clipped, d3_geo_clipGreatCircle({source: p, target: o.target}));
738+
}
739+
clipped.push.apply(clipped, path.slice(j));
740+
p = q = null;
741+
} else {
742+
clipped.push(o.target);
743+
}
744+
} else {
745+
q = o.target;
746+
if (!p && clipped.length) {
747+
path = d3_geo_clipGreatCircle({source: clipped[clipped.length - 1], target: o.target});
748+
j = d3_geo_clipClosest(path, o, r);
749+
clipped.push.apply(clipped, path.slice(0, j));
750+
p = o.target;
751+
}
752+
}
753+
}
754+
if (q && clipped.length) {
755+
o.target = clipped[0];
756+
path = d3_geo_clipGreatCircle({source: q, target: o.target});
757+
j = d3_geo_clipClosest(path, o, r);
758+
if (p) {
759+
clipped.push.apply(clipped, d3_geo_clipGreatCircle({source: p, target: o.target}));
760+
}
761+
clipped.push.apply(clipped, path.slice(j));
762+
}
763+
return clipped;
764+
}
765+
766+
clip.origin = function(x) {
767+
if (!arguments.length) return origin;
768+
origin = x;
769+
return clip;
770+
};
771+
772+
clip.angle = function(x) {
773+
if (!arguments.length) return angle;
774+
angle = +x;
775+
r = 6371 * angle / 180 * Math.PI;
776+
return clip;
777+
};
778+
779+
return clip;
780+
}
781+
782+
var d3_geo_clipGreatCircle = d3.geo.greatCircle().n(100);
783+
784+
function d3_geo_clipClosest(path, o, r) {
785+
var i = -1,
786+
n = path.length,
787+
index = 0,
788+
best = Infinity;
789+
while (++i < n) {
790+
o.target = path[i];
791+
var d = Math.abs(d3_geo_clipGreatCircle.distance(o) - r);
792+
if (d < best) {
793+
best = d;
794+
index = i;
795+
}
796+
}
797+
o.target = path[index];
798+
return index;
799+
}
702800
})();

d3.geo.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/azimuthal/azimuthal.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
<body>
1313
<h3>Azimuthal Projection</h3>
1414
<script type="text/javascript" src="azimuthal.js"></script><p>
15+
<select id="mode">
16+
<option value="stereographic">stereographic</option>
17+
<option value="orthographic">orthographic</option>
18+
<option value="equidistant">equidistant</option>
19+
<option value="gnomonic">gnomonic</option>
20+
<option value="equalarea">equalarea</option>
21+
</select>
1522
<div id="lon">origin.longitude: <span>0</span></div>
1623
<div id="lat">origin.latitude: <span>0</span></div><p>
1724
<div id="scale">scale: <span>240</span></div><p>
@@ -28,6 +35,7 @@ <h3>Azimuthal Projection</h3>
2835
var origin = xy.origin();
2936
origin[0] = ui.value;
3037
xy.origin(origin);
38+
clip.origin(origin);
3139
refresh();
3240
}
3341
});
@@ -41,6 +49,7 @@ <h3>Azimuthal Projection</h3>
4149
var origin = xy.origin();
4250
origin[1] = ui.value;
4351
xy.origin(origin);
52+
clip.origin(origin);
4453
refresh();
4554
}
4655
});
@@ -79,6 +88,11 @@ <h3>Azimuthal Projection</h3>
7988
}
8089
});
8190

91+
$("#mode").change(function() {
92+
xy.mode($(this).val());
93+
refresh(500);
94+
});
95+
8296
</script>
8397
</body>
8498
</html>

examples/azimuthal/azimuthal.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
var xy = d3.geo.azimuthal().scale(240).mode("equalarea"),
2-
path = d3.geo.path().projection(xy),
1+
var xy = d3.geo.azimuthal().scale(240).mode("stereographic"),
2+
clip = d3.geo.clip().angle(89),
3+
path = d3.geo.path().projection(xy).clip(clip),
34
svg = d3.select("body").append("svg:svg");
45

56
d3.json("../data/world-countries.json", function(collection) {
@@ -11,9 +12,10 @@ d3.json("../data/world-countries.json", function(collection) {
1112
.text(function(d) { return d.properties.name; });
1213
});
1314

14-
function refresh() {
15-
svg.selectAll("path")
16-
.attr("d", path);
15+
function refresh(duration) {
16+
var p = svg.selectAll("path");
17+
if (duration) p = p.transition().duration(duration);
18+
p.attr("d", path);
1719
d3.select("#lon span")
1820
.text(xy.origin()[0]);
1921
d3.select("#lat span")

src/geo/clip.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
d3.geo.clip = function() {
2+
var origin = [0, 0],
3+
angle = 90,
4+
r = 6371 * angle / 180 * Math.PI;
5+
6+
function clip(d) {
7+
var o = {source: origin, target: null},
8+
n = d.length,
9+
i = -1,
10+
j,
11+
path,
12+
clipped = [],
13+
p = null,
14+
q = null;
15+
while (++i < n) {
16+
o.target = d[i];
17+
distance = d3_geo_clipGreatCircle.distance(o);
18+
if (distance < r) {
19+
if (q) {
20+
path = d3_geo_clipGreatCircle({source: q, target: o.target});
21+
j = d3_geo_clipClosest(path, o, r);
22+
if (p) {
23+
clipped.push.apply(clipped, d3_geo_clipGreatCircle({source: p, target: o.target}));
24+
}
25+
clipped.push.apply(clipped, path.slice(j));
26+
p = q = null;
27+
} else {
28+
clipped.push(o.target);
29+
}
30+
} else {
31+
q = o.target;
32+
if (!p && clipped.length) {
33+
path = d3_geo_clipGreatCircle({source: clipped[clipped.length - 1], target: o.target});
34+
j = d3_geo_clipClosest(path, o, r);
35+
clipped.push.apply(clipped, path.slice(0, j));
36+
p = o.target;
37+
}
38+
}
39+
}
40+
if (q && clipped.length) {
41+
o.target = clipped[0];
42+
path = d3_geo_clipGreatCircle({source: q, target: o.target});
43+
j = d3_geo_clipClosest(path, o, r);
44+
if (p) {
45+
clipped.push.apply(clipped, d3_geo_clipGreatCircle({source: p, target: o.target}));
46+
}
47+
clipped.push.apply(clipped, path.slice(j));
48+
}
49+
return clipped;
50+
}
51+
52+
clip.origin = function(x) {
53+
if (!arguments.length) return origin;
54+
origin = x;
55+
return clip;
56+
};
57+
58+
clip.angle = function(x) {
59+
if (!arguments.length) return angle;
60+
angle = +x;
61+
r = 6371 * angle / 180 * Math.PI;
62+
return clip;
63+
};
64+
65+
return clip;
66+
}
67+
68+
var d3_geo_clipGreatCircle = d3.geo.greatCircle().n(100);
69+
70+
function d3_geo_clipClosest(path, o, r) {
71+
var i = -1,
72+
n = path.length,
73+
index = 0,
74+
best = Infinity;
75+
while (++i < n) {
76+
o.target = path[i];
77+
var d = Math.abs(d3_geo_clipGreatCircle.distance(o) - r);
78+
if (d < best) {
79+
best = d;
80+
index = i;
81+
}
82+
}
83+
o.target = path[index];
84+
return index;
85+
}

src/geo/greatCircle.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ d3.geo.greatCircle = function() {
66
radius = 6371; // Mean radius of Earth, in km.
77
// TODO: breakAtDateLine?
88

9-
function greatCircle(d, i) {
9+
function greatCircle(d, i, dist) {
1010
var from = source.call(this, d, i),
1111
to = target.call(this, d, i),
1212
x0 = from[0] * d3_radians,
@@ -17,13 +17,15 @@ d3.geo.greatCircle = function() {
1717
cy0 = Math.cos(y0), sy0 = Math.sin(y0),
1818
cx1 = Math.cos(x1), sx1 = Math.sin(x1),
1919
cy1 = Math.cos(y1), sy1 = Math.sin(y1),
20-
d = Math.acos(sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)),
20+
d = dist || Math.acos(sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)),
2121
sd = Math.sin(d),
2222
f = d / (n - 1),
2323
e = -f,
2424
path = [],
2525
i = -1;
2626

27+
if (d === 0) return [from];
28+
2729
while (++i < n) {
2830
e += f;
2931
var A = Math.sin(d - e) / sd,

0 commit comments

Comments
 (0)