<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <style> path { fill: none; stroke-width: 2px; stroke-linejoin: round; } .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 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("norway.geojson",function(err,ca){ // Preproject to screen coords ca.coordinates[0] = ca.coordinates[0].map(projection); // 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"); 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()); 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){ // TODO why does this not work? // return line(d.boundary); 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"; }); } }); // 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], 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: http://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] ]; } function id(d) { return d; } d3.select(self.frameElement).style("height", "720px"); </script> </body> </html>