A version of this strip map with some labeled features.
Labels are a bit trickier than background features because you don't want to clip them to the zones, but you can't just create one copy per zone of each, or you'll get a bunch of identical labels approximately on top of each other and it will look weird. This uses a point-in-polygon test to assign each labeled point to its proper zone at the start. Once that assignment is done, you could put the labels in their own top layer if you had to worry about other features overlapping (not really an issue in this case).
forked from veltman's block: Strip map with labels
xxxxxxxxxx
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
path {
fill: none;
stroke-width: 2px;
stroke-linejoin: round;
}
text {
font: 14px Helvetica, Arial, sans-serif;
text-anchor: end;
}
.state {
stroke: #999;
stroke-width: 1px;
fill: papayawhip;
}
.simplified {
stroke: #de1e3d;
stroke-width: 2px;
stroke-dasharray: 8,8;
}
.zone {
stroke: #0eb8ba;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="warper.js"></script>
<script src="simplify.js"></script>
<script>
var stripWidth = 80;
var points = [
{ name: "Eureka", coordinates: [-124.16748, 40.78886] },
{ name: "Mendocino", coordinates: [-123.77197, 39.29605] },
{ name: "San Francisco", coordinates: [-122.46872, 37.76094] },
{ name: "Monterey", coordinates: [-121.90842, 36.59238] },
{ name: "Santa Barbara", coordinates: [-119.69604, 34.41541] },
{ name: "Los Angeles", coordinates: [-118.42575, 33.97668] },
{ name: "San Diego", coordinates: [-117.23785, 32.73184] }
];
var projection = d3.geo.conicConformal()
.parallels([36, 37 + 15 / 60])
.rotate([119, -35 - 20 / 60])
.scale(3433)
.translate([355, 498]);
var line = d3.svg.line();
// Top point
var origin = [50, 100];
d3.json("ca.geojson",function(err,ca){
// Preproject to screen coords
ca.coordinates[0] = ca.coordinates[0].map(projection);
points.forEach(function(point){
point.coordinates = projection(point.coordinates);
});
// Get coastline
var ls = ca.coordinates[0].slice(0, 155);
// Get simplified vertices
var simplified = simplify(ls, 1000);
var zones = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 720)
.selectAll("g")
.data(getZones(simplified))
.enter()
.append("g");
zones.append("defs")
.append("clipPath")
.attr("id",function(d, i){
return "clip" + i;
})
.append("path");
var inner = zones.append("g")
.attr("class",function(d, i) {
return i ? "hidden" : null;
});
inner.append("path")
.attr("class", "state");
inner.append("line")
.attr("class", "simplified fade hidden");
// Put boundary outside so it isn't clipped
zones.append("path")
.attr("class", "zone fade hidden");
// Only put cities in zones they actually fall in
var cities = zones.selectAll(".city")
.data(function(d, i){
return points.filter(function(point){
if (pip(point.coordinates, d.boundary)) {
return point.zone = d;
}
});
})
.enter()
.append("g")
.attr("class", "city");
cities.append("circle")
.attr("r", 3);
cities.append("text")
.text(function(d){
return d.name;
})
.attr("dx", "-0.5em")
.attr("dy", "0.35em");
zones.call(update);
// Step-by-step for demo purposes
d3.select("body")
.transition()
.duration(1000)
.each("end", clipState)
.transition()
.each("end", showLine)
.transition()
.each("end", showZones)
.transition()
.each("end", move);
// 1. Clip out the rest of CA
function clipState() {
inner.classed("hidden", false)
.attr("clip-path",function(d, i){
return "url(#clip" + i + ")";
});
}
// 2. Show the simplified line
function showLine() {
inner.select(".simplified")
.classed("hidden", false);
}
// 3. Show the zone boundaries
function showZones() {
zones.select(".zone")
.classed("hidden", false);
}
// 4. Rotate/translate all the zones
function move() {
warpZones(zones.data());
// Flip text orientation
d3.selectAll("text").transition()
.duration(1000)
.each("end",function(){
d3.select(this).style("text-anchor", "middle")
.attr("dx", 0)
.attr("dy", "1.5em");
});
zones.transition()
.duration(2000)
.each("end",align)
.call(update);
}
// 5. Warp the zones to rectangles
function align(z) {
z.project = function(d){
return z.warp(z.translate(d));
};
z.boundary = z.corners;
d3.select(this)
.transition()
.duration(750)
.call(update)
.each("end",fade);
}
// 6. Fade out
function fade() {
d3.select(this).selectAll(".fade")
.transition()
.duration(500)
.style("opacity", 0);
}
// Redraw
function update(sel) {
sel.select(".zone")
.attr("d",function(d){
return line(d.boundary.slice(0,4)) + "Z";
});
sel.select(".state")
.attr("d",function(d){
return d.path(ca);
});
sel.select(".simplified")
.attr("x1",function(d){
return d.ends[0][0];
})
.attr("x2",function(d){
return d.ends[1][0];
})
.attr("y1",function(d){
return d.ends[0][1];
})
.attr("y2",function(d){
return d.ends[1][1];
});
sel.select("clipPath path")
.attr("d",function(d){
return line(d.boundary.slice(0,4)) + "Z";
});
sel.selectAll(".city")
.attr("transform",function(d){
return "translate(" + d.zone.project(d.coordinates) + ")";
});
}
});
// Turn a simplified LineString into one group per segment
function getZones(simp) {
return simp.slice(1).map(function(p, i){
return {
boundary: getBoundary(simp[i - 1], simp[i], p, simp[i + 2]),
ends: [simp[i], p],
project: id,
path: d3.geo.path().projection(null)
};
});
}
function warpZones(zones) {
zones.forEach(function(z,i){
var angle = getAngle(z.ends[0], z.ends[1]),
anchor = i ? zones[i - 1].ends[1] : origin;
// Anchor points to end of prev segment
var translate = [
anchor[0] - z.ends[0][0],
anchor[1] - z.ends[0][1]
];
// Get translation/rotation function
z.translate = translateAndRotate(translate, z.ends[0], angle);
// Warp the boundary line and the simplified segment
z.ends = z.ends.map(z.translate);
z.boundary = z.boundary.map(z.translate);
var top = bisect(null, z.ends[0], z.ends[1]),
bottom = bisect(z.ends[0], z.ends[1], null);
z.corners = [top[0], top[1], bottom[1], bottom[0], top[0]];
z.corners.push(z.corners[0]);
// See: https://bl.ocks.org/veltman/8f5a157276b1dc18ce2fba1bc06dfb48
z.warp = warper(z.boundary, z.corners);
z.project = function(d){
return z.translate(d);
};
z.path.projection(d3.geo.transform({
point: function(x, y) {
var p = z.project([x, y]);
this.stream.point(p[0], p[1]);
}
}));
});
}
function getBoundary(prev, first, second, next) {
// if prev is undefined, top is perpendicular through first
// otherwise top bisects the prev-first-second angle
// if next is undefined, bottom is perpendicular through second
// otherwise bottom bisects the first-second-next angle
var top = bisect(prev, first, second),
bottom = bisect(first, second, next);
return [top[0], top[1], bottom[1], bottom[0], top[0]];
}
function getAngle(a, b) {
return Math.atan2(b[1] - a[1], b[0] - a[0]);
}
// Given an anchor point, initial translate, and angle rotation
// Return a function to translate+rotate a point
function translateAndRotate(translate, anchor, angle) {
var cos = Math.cos(angle),
sin = Math.sin(angle);
return function(point) {
return [
translate[0] + anchor[0] + ( cos * (point[0] - anchor[0]) + sin * (point[1] - anchor[1])),
translate[1] + anchor[1] + ( -sin * (point[0] - anchor[0]) + cos * (point[1] - anchor[1]))
];
};
}
// Hacky angle bisector
function bisect(start, vertex, end) {
var at,
bt,
adjusted,
right,
left;
if (start) {
at = getAngle(start, vertex);
}
if (end) {
bt = getAngle(vertex, end);
}
if (!start) {
at = bt;
}
if (!end) {
bt = at;
}
adjusted = bt - at;
if (adjusted <= -Math.PI) {
adjusted = 2 * Math.PI + adjusted;
} else if (adjusted > Math.PI) {
adjusted = adjusted - 2 * Math.PI;
}
right = (adjusted - Math.PI) / 2;
left = Math.PI + right;
left += at;
right += at;
return [
[vertex[0] + stripWidth * Math.cos(left) / 2, vertex[1] + stripWidth * Math.sin(left) / 2],
[vertex[0] + stripWidth * Math.cos(right) / 2, vertex[1] + stripWidth * Math.sin(right) / 2]
];
}
// https://github.com/substack/point-in-polygon
// based on https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
function pip(point, vs) {
var x = point[0],
y = point[1],
inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i][0], yi = vs[i][1];
var xj = vs[j][0], yj = vs[j][1];
var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) {
inside = !inside;
}
}
return inside;
}
function id(d) {
return d;
}
d3.select(self.frameElement).style("height", "720px");
</script>
</body>
</html>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js