|  | <!DOCTYPE html> | 
        
          |  | <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: 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] | 
        
          |  | ]; | 
        
          |  | } | 
        
          |  |  | 
        
          |  | // https://github.com/substack/point-in-polygon | 
        
          |  | // based on http://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> |