Equidistant map perimeter circles circles
Made with blockup.
.buttons{margin:0 auto;text-align:center}.map svg{display:block;margin:0 auto}.map svg circle{fill:#8f092a;fill-opacity:.25;stroke:#8f092a} |
var margin={top:10,right:10,bottom:10,left:10};d3.json("./boundary.topojson",function(t){var e=topojson.feature(t,t.objects.boundary),n=(d3.geoBounds(e),d3.geoCentroid(e),d3.geoAlbers()),r=d3.geoPath().projection(n),a=r.bounds(e),o=(a[1][0]-a[0][0])/(a[1][1]-a[0][1]),i=460,c=i*o,u=c-margin.left-margin.right,l=i-margin.top-margin.bottom,d=d3.select(".map svg").attrs({width:c,height:i}).append("g").attr("transform","translate("+margin.left+", "+margin.top+")");n.fitSize([u,l],e);var s=_(e.features).map("geometry").flatten().map("coordinates").flatten().sortBy("length").map(function(t){return turf.lineString(t)}).value(),f=_(s).map(function(t){return turf.lineDistance(t)}).sum(),m=1e3,g=m,p=_(s).map(function(t){var e=turf.lineDistance(t),n=Math.ceil(e*m/f),r=Math.min(g,n);g-=r;var a=e/r,o=d3.range(r).map(function(e){return turf.along(t,e*a)});return o}).flatten().map(function(t){return n(t.geometry.coordinates)}).value(),v=function(){var t=d.selectAll("circle").data(p);t.enter().append("circle").attrs({cx:u/2,cy:l/2,r:0}).transition("enter").duration(1e3).delay(function(t,e){return 10*e}).attrs({cx:function(t){return t[0]},cy:function(t){return t[1]},r:1})},y=function(){var t=d.selectAll("circle").data(p);t.transition("exit").duration(250).delay(function(t,e){return 2*e}).attrs({cx:u/2,cy:l/2,r:0}).remove()};v(),document.querySelector("button.enter").addEventListener("click",v),document.querySelector("button.exit").addEventListener("click",y)}); | |
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNjcmlwdC5qcyJdLCJuYW1lcyI6WyJjb25zdCIsIm1hcmdpbiIsInRvcCIsInJpZ2h0IiwiYm90dG9tIiwibGVmdCIsImQzIiwianNvbiIsImZlYXR1cmUiLCJ0b3BvanNvbiIsIm9iamVjdHMiLCJib3VuZGFyeSIsInByb2plY3Rpb24iLCJnZW9Cb3VuZHMiLCJnZW9DZW50cm9pZCIsImdlb0FsYmVycyIsInBhdGgiLCJnZW9QYXRoIiwiYiIsImJvdW5kcyIsImFzcGVjdCIsIm91dGVySGVpZ2h0Iiwib3V0ZXJXaWR0aCIsIndpZHRoIiwiaGVpZ2h0IiwiZyIsInNlbGVjdCIsImF0dHJzIiwiYXBwZW5kIiwiYXR0ciIsImZpdFNpemUiLCJsaW5lU3RyaW5ncyIsIl8iLCJmZWF0dXJlcyIsIm1hcCIsImZsYXR0ZW4iLCJzb3J0QnkiLCJkIiwidHVyZiIsImxpbmVTdHJpbmciLCJ2YWx1ZSIsInRvdGFsTGVuZ3RoIiwibGluZURpc3RhbmNlIiwic3VtIiwicG9pbnRzQ291bnQiLCJwb2ludHNSZW1haW5pbmciLCJwb2ludHMiLCJsaW5lIiwibGluZUxlbmd0aCIsInVwcGVyQ291bnQiLCJNYXRoIiwiY2VpbCIsImxpbmVQb2ludHNDb3VudCIsIm1pbiIsInN0ZXAiLCJsaW5lUG9pbnRzIiwicmFuZ2UiLCJhbG9uZyIsImdlb21ldHJ5IiwiY29vcmRpbmF0ZXMiLCJlbnRlciIsImNpcmNsZXMiLCJzZWxlY3RBbGwiLCJkYXRhIiwiY3giLCJjeSIsInIiLCJ0cmFuc2l0aW9uIiwiZHVyYXRpb24iLCJkZWxheSIsImkiLCJleGl0IiwicmVtb3ZlIiwiZG9jdW1lbnQiLCJxdWVyeVNlbGVjdG9yIiwiYWRkRXZlbnRMaXN0ZW5lciJdLCJtYXBwaW5ncyI6IkFBQ0FBLEdBQU1DLFNBQVdDLElBQU8sR0FBRUMsTUFBUyxHQUFFQyxPQUFVLEdBQUVDLEtBQVEsR0FHekRDLElBQUdDLEtBQUssc0JBQXVCLFNBQUFBLEdBRTlCUCxHQUFNUSxHQUFVQyxTQUFTRCxRQUFRRCxFQUFNQSxFQUFLRyxRQUFRQyxVQVc5Q0MsR0FSV04sR0FBQ08sVUFBVUwsR0FDVEYsR0FBQ1EsWUFBWU4sR0FPWEYsR0FBQ1MsYUFHaEJDLEVBQVNWLEdBQUNXLFVBQVVMLFdBQVdBLEdBRzlCTSxFQUFHRixFQUFLRyxPQUFPWCxHQUdoQlksR0FBWUYsRUFBRSxHQUFHLEdBQUtBLEVBQUUsR0FBRyxLQUFPQSxFQUFFLEdBQUcsR0FBS0EsRUFBRSxHQUFHLElBRWpERyxFQUFjLElBQ2RDLEVBQWFELEVBQWNELEVBRTNCRyxFQUFRRCxFQUFhckIsT0FBT0ksS0FBT0osT0FBT0UsTUFDMUNxQixFQUFTSCxFQUFjcEIsT0FBT0MsSUFBTUQsT0FBT0csT0FHMUNxQixFQUFLbkIsR0FBQ29CLE9BQU8sWUFDakJDLE9BQVFKLE1BQU9ELEVBQVlFLE9BQVFILElBQ3BDTyxPQUFPLEtBQ05DLEtBQUssWUFBYSxhQUFXNUIsT0FBUyxLQUFBLEtBQUlBLE9BQUcsSUFBQSxJQUdoRFcsR0FBV2tCLFNBQVNQLEVBQU9DLEdBQVNoQixFQUdwQ1IsSUFBTStCLEdBQWdCQyxFQUFBeEIsRUFBUXlCLFVBQzVCQyxJQUFJLFlBQ0pDLFVBQ0FELElBQUksZUFDSkMsVUFDQUMsT0FBTyxVQUNQRixJQUFJLFNBQUFHLEdBQUEsTUFBQUMsTUFBQUMsV0FBRUYsS0FDTkcsUUFHSUMsRUFBZ0JULEVBQUFELEdBQ3BCRyxJQUFJLFNBQUFHLEdBQUEsTUFBQUMsTUFBQUksYUFBS0wsS0FDVE0sTUFHSUMsRUFBYyxJQUNoQkMsRUFBa0JELEVBRWhCRSxFQUFXZCxFQUFBRCxHQUNmRyxJQUFJLFNBQUFhLEdBS0ovQyxHQUFNZ0QsR0FBYVYsS0FBS0ksYUFBYUssR0FHL0JFLEVBQWFDLEtBQUtDLEtBQUtILEVBQWFKLEVBQWNILEdBR2xEVyxFQUFrQkYsS0FBS0csSUFBSVIsRUFBaUJJLEVBR2xESixJQUFtQk8sQ0FJbkJwRCxJQUFNc0QsR0FBT04sRUFBYUksRUFFcEJHLEVBQWVqRCxHQUFDa0QsTUFBTUosR0FDMUJsQixJQUFJLFNBQUFHLEdBQUEsTUFBQUMsTUFBQW1CLE1BQUVWLEVBQUdWLEVBQUlpQixJQUVmLE9BQU9DLEtBR1BwQixVQUNBRCxJQUFJLFNBQUFHLEdBQUEsTUFBQXpCLEdBQUV5QixFQUFBcUIsU0FBR0MsZUFDVG5CLFFBR0lvQixFQUFRLFdBR2I1RCxHQUFNNkQsR0FBWXBDLEVBQUFxQyxVQUFVLFVBQ3pCQyxLQUFLakIsRUFHUmUsR0FBUUQsUUFBUWhDLE9BQU8sVUFDcEJELE9BQ0FxQyxHQUFJekMsRUFBTSxFQUNWMEMsR0FBSXpDLEVBQU8sRUFDWDBDLEVBQUcsSUFFSkMsV0FBVyxTQUNWQyxTQUFTLEtBQ1RDLE1BQU0sU0FBQWhDLEVBQUFpQyxHQUFBLE1BQUssSUFBSkEsSUFDUDNDLE9BQ0FxQyxHQUFJLFNBQUEzQixHQUFBLE1BQUFBLEdBQUEsSUFDSjRCLEdBQUksU0FBQTVCLEdBQUEsTUFBQUEsR0FBQSxJQUNKNkIsRUFBRyxLQU1ESyxFQUFPLFdBR1p2RSxHQUFNNkQsR0FBWXBDLEVBQUFxQyxVQUFVLFVBQ3pCQyxLQUFLakIsRUFHUmUsR0FDRU0sV0FBVyxRQUNWQyxTQUFTLEtBQ1RDLE1BQU0sU0FBQWhDLEVBQUFpQyxHQUFBLE1BQUssR0FBSkEsSUFDUDNDLE9BQ0FxQyxHQUFJekMsRUFBTSxFQUNWMEMsR0FBSXpDLEVBQU8sRUFDWDBDLEVBQUcsSUFFSk0sU0FLSFosS0FHQWEsU0FBU0MsY0FBYyxnQkFBZ0JDLGlCQUFpQixRQUFTZixHQUNqRWEsU0FBU0MsY0FBYyxlQUFlQyxpQkFBaUIsUUFBU0oiLCJmaWxlIjoic2NyaXB0LmpzIiwic291cmNlc0NvbnRlbnQiOlsiLy8gU2V0dXAgY2hhcnQgZGltZW5zaW9ucy5cbmNvbnN0IG1hcmdpbiA9IHsgdG9wOiAxMCwgcmlnaHQ6IDEwLCBib3R0b206IDEwLCBsZWZ0OiAxMCB9XG5cbi8vIEdldCBHZW9KU09OLlxuZDMuanNvbignLi9ib3VuZGFyeS50b3BvanNvbicsIGpzb24gPT4ge1xuXG5cdGNvbnN0IGZlYXR1cmUgPSB0b3BvanNvbi5mZWF0dXJlKGpzb24sIGpzb24ub2JqZWN0cy5ib3VuZGFyeSlcblxuXHQvLyBHZXQgZmVhdHVyZSdzIGJvdW5kcyBhbmQgY2VudHJvaWQuXG5cdGNvbnN0IGJvdW5kcyA9IGQzLmdlb0JvdW5kcyhmZWF0dXJlKVxuXHRjb25zdCBjZW50cm9pZCA9IGQzLmdlb0NlbnRyb2lkKGZlYXR1cmUpXG5cblx0Ly8gY29uc3QgcHJvamVjdGlvbiA9IGQzLmdlb0NvbmljQ29uZm9ybWFsKClcblx0Ly8gXHQucGFyYWxsZWxzKFtib3VuZHNbMF1bMV0sIGJvdW5kc1sxXVsxXV0pXG5cdC8vIFx0LnJvdGF0ZShbLWNlbnRyb2lkWzBdLCAwXSlcblx0Ly8gXHQuY2VudGVyKFswLCAtY2VudHJvaWRbMV1dKVxuXG5cdGNvbnN0IHByb2plY3Rpb24gPSBkMy5nZW9BbGJlcnMoKVxuXG5cdC8vIEdldCB0aGUgcGF0aC5cblx0Y29uc3QgcGF0aCA9IGQzLmdlb1BhdGgoKS5wcm9qZWN0aW9uKHByb2plY3Rpb24pXG5cblx0Ly8gR2V0IHRoZSBwYXRoJ3MgYm91bmRzIChpLmUuLCBpbiBwaXhlbHMpLlxuXHRjb25zdCBiID0gcGF0aC5ib3VuZHMoZmVhdHVyZSlcblxuXHQvLyBHZXQgYXNwZWN0IHJhdGlvLlxuXHRjb25zdCBhc3BlY3QgPSAoYlsxXVswXSAtIGJbMF1bMF0pIC8gKGJbMV1bMV0gLSBiWzBdWzFdKVxuXG5cdGNvbnN0IG91dGVySGVpZ2h0ID0gNDYwXG5cdGNvbnN0IG91dGVyV2lkdGggPSBvdXRlckhlaWdodCAqIGFzcGVjdFxuXG5cdGNvbnN0IHdpZHRoID0gb3V0ZXJXaWR0aCAtIG1hcmdpbi5sZWZ0IC0gbWFyZ2luLnJpZ2h0XG5cdGNvbnN0IGhlaWdodCA9IG91dGVySGVpZ2h0IC0gbWFyZ2luLnRvcCAtIG1hcmdpbi5ib3R0b21cblxuXHQvLyBQcmVwYXJlIHN2Zy5cblx0Y29uc3QgZyA9IGQzLnNlbGVjdCgnLm1hcCBzdmcnKVxuXHRcdFx0LmF0dHJzKHsgd2lkdGg6IG91dGVyV2lkdGgsIGhlaWdodDogb3V0ZXJIZWlnaHQgfSlcblx0XHQuYXBwZW5kKCdnJylcblx0XHRcdC5hdHRyKCd0cmFuc2Zvcm0nLCBgdHJhbnNsYXRlKCR7bWFyZ2luLmxlZnR9LCAke21hcmdpbi50b3B9KWApXG5cblx0Ly8gRml0IHRoZSBmZWF0dXJlIHRvIHRoZSBjb250YWluZXIncyB3aWR0aC5cblx0cHJvamVjdGlvbi5maXRTaXplKFt3aWR0aCwgaGVpZ2h0XSwgZmVhdHVyZSlcblxuXHQvLyBHZXQgdGhlIGluZGl2aWR1YWwgbGluZSBzdHJpbmdzLlxuXHRjb25zdCBsaW5lU3RyaW5ncyA9IF8oZmVhdHVyZS5mZWF0dXJlcylcblx0XHQubWFwKCdnZW9tZXRyeScpXG5cdFx0LmZsYXR0ZW4oKVxuXHRcdC5tYXAoJ2Nvb3JkaW5hdGVzJylcblx0XHQuZmxhdHRlbigpXG5cdFx0LnNvcnRCeSgnbGVuZ3RoJylcblx0XHQubWFwKGQgPT4gdHVyZi5saW5lU3RyaW5nKGQpKVxuXHRcdC52YWx1ZSgpXG5cblx0Ly8gQ2FsY3VsYXRlIHRoZSBvdmVyYWxsIGxpbmUgc3RyaW5nIGxlbmd0aC5cblx0Y29uc3QgdG90YWxMZW5ndGggPSBfKGxpbmVTdHJpbmdzKVxuXHRcdC5tYXAoZCA9PiB0dXJmLmxpbmVEaXN0YW5jZShkKSlcblx0XHQuc3VtKClcblxuXHQvLyBEZXNpcmVkIG51bWJlciBvZiB0b3RhbCBwb2ludHMuXG5cdGNvbnN0IHBvaW50c0NvdW50ID0gMTAwMFxuXHRsZXQgcG9pbnRzUmVtYWluaW5nID0gcG9pbnRzQ291bnRcblxuXHRjb25zdCBwb2ludHMgPSBfKGxpbmVTdHJpbmdzKVxuXHRcdC5tYXAobGluZSA9PiB7XG5cblx0XHRcdC8vIEhvdyBtYW55IHBvaW50cyB3aWxsIHRoaXMgbGluZSBnZXQ/XG5cblx0XHRcdC8vIEZpcnN0LCBjYWxjdWxhdGUgdGhpcyBsaW5lJ3MgbGVuZ3RoLlxuXHRcdFx0Y29uc3QgbGluZUxlbmd0aCA9IHR1cmYubGluZURpc3RhbmNlKGxpbmUpXG5cblx0XHRcdC8vIE5leHQsIGdldCB0aGlzIGxpbmUncyBwb2ludHMgcHJvcG9ydGlvbiwgcm91bmRlZCB1cC5cblx0XHRcdGNvbnN0IHVwcGVyQ291bnQgPSBNYXRoLmNlaWwobGluZUxlbmd0aCAqIHBvaW50c0NvdW50IC8gdG90YWxMZW5ndGgpXG5cblx0XHRcdC8vIERvbid0IGdldCBtb3JlIHBvaW50cyB0aGF0IGFyZSBhdmFpbGFibGUuXG5cdFx0XHRjb25zdCBsaW5lUG9pbnRzQ291bnQgPSBNYXRoLm1pbihwb2ludHNSZW1haW5pbmcsIHVwcGVyQ291bnQpXG5cblx0XHRcdC8vIE1ha2Ugc3VyZSB0byB1cGRhdGUgcG9pbnRzIHJlbWFpbmluZy5cblx0XHRcdHBvaW50c1JlbWFpbmluZyAtPSBsaW5lUG9pbnRzQ291bnRcblxuXHRcdFx0Ly8gTm93IHRoYXQgd2Uga25vdyBob3cgbWFueSBwb2ludHMgdGhpcyBsaW5lIHdpbGwgZ2V0LFxuXHRcdFx0Ly8gY2FsY3VsYXRlIHRoZSBkaXN0YW5jZSBiZXR3ZWVuIHBvaW50cyAtIHRoZSBzdGVwOlxuXHRcdFx0Y29uc3Qgc3RlcCA9IGxpbmVMZW5ndGggLyBsaW5lUG9pbnRzQ291bnRcblxuXHRcdFx0Y29uc3QgbGluZVBvaW50cyA9IGQzLnJhbmdlKGxpbmVQb2ludHNDb3VudClcblx0XHRcdFx0Lm1hcChkID0+IHR1cmYuYWxvbmcobGluZSwgZCAqIHN0ZXApKVxuXG5cdFx0XHRyZXR1cm4gbGluZVBvaW50c1xuXG5cdFx0fSlcblx0XHQuZmxhdHRlbigpXG5cdFx0Lm1hcChkID0+IHByb2plY3Rpb24oZC5nZW9tZXRyeS5jb29yZGluYXRlcykpXG5cdFx0LnZhbHVlKClcblxuXHQvLyBUaGlzIGZ1bmN0aW9uIGFkZHMgdGhlIGNpcmNsZXMuXG5cdGNvbnN0IGVudGVyID0gKCkgPT4ge1xuXG5cdFx0Ly8gSk9JTiBuZXcgZGF0YSB3aXRoIG9sZCBlbGVtZW50cy5cblx0XHRjb25zdCBjaXJjbGVzID0gZy5zZWxlY3RBbGwoJ2NpcmNsZScpXG5cdFx0XHRcdC5kYXRhKHBvaW50cylcblxuXHRcdC8vIEVOVEVSIG5ldyBlbGVtZW50cyBwcmVzZW50IGluIG5ldyBkYXRhLlxuXHRcdGNpcmNsZXMuZW50ZXIoKS5hcHBlbmQoJ2NpcmNsZScpXG5cdFx0XHRcdC5hdHRycyh7XG5cdFx0XHRcdFx0Y3g6IHdpZHRoLzIsXG5cdFx0XHRcdFx0Y3k6IGhlaWdodC8yLFxuXHRcdFx0XHRcdHI6IDAsXG5cdFx0XHRcdH0pXG5cdFx0XHQudHJhbnNpdGlvbignZW50ZXInKVxuXHRcdFx0XHQuZHVyYXRpb24oMTAwMClcblx0XHRcdFx0LmRlbGF5KChkLCBpKSA9PiBpICogMTApXG5cdFx0XHRcdC5hdHRycyh7XG5cdFx0XHRcdFx0Y3g6IGQgPT4gZFswXSxcblx0XHRcdFx0XHRjeTogZCA9PiBkWzFdLFxuXHRcdFx0XHRcdHI6IDEsXG5cdFx0XHRcdH0pXG5cblx0fVxuXG5cdC8vIFRoaXMgZnVuY3Rpb24gcmVtb3ZlcyB0aGUgY2lyY2xlcy5cblx0Y29uc3QgZXhpdCA9ICgpID0+IHtcblxuXHRcdC8vIEpPSU4gbmV3IGRhdGEgd2l0aCBvbGQgZWxlbWVudHMuXG5cdFx0Y29uc3QgY2lyY2xlcyA9IGcuc2VsZWN0QWxsKCdjaXJjbGUnKVxuXHRcdFx0XHQuZGF0YShwb2ludHMpXG5cblx0XHQvLyBVUERBVEUgb2xkIGVsZW1lbnRzIHByZXNlbnQgaW4gbmV3IGRhdGEuXG5cdFx0Y2lyY2xlc1xuXHRcdFx0LnRyYW5zaXRpb24oJ2V4aXQnKVxuXHRcdFx0XHQuZHVyYXRpb24oMjUwKVxuXHRcdFx0XHQuZGVsYXkoKGQsIGkpID0+IGkgKiAyKVxuXHRcdFx0XHQuYXR0cnMoe1xuXHRcdFx0XHRcdGN4OiB3aWR0aC8yLFxuXHRcdFx0XHRcdGN5OiBoZWlnaHQvMixcblx0XHRcdFx0XHRyOiAwLFxuXHRcdFx0XHR9KVxuXHRcdFx0LnJlbW92ZSgpXG5cblx0fVxuXG5cdC8vIEZpcmUgdGhlIGVudGVyIGZ1bmN0aW9uIG9uIHBhZ2UgbG9hZC5cblx0ZW50ZXIoKVxuXG5cdC8vIExpc3RlbiB0byBidXR0b24gY2xpY2tzLlxuXHRkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdidXR0b24uZW50ZXInKS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGVudGVyKVxuXHRkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdidXR0b24uZXhpdCcpLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgZXhpdClcblxufSlcbiJdfQ== |
<!DOCTYPE html> | |
<title>Equidistant outline circles</title> | |
<link href='dist.css' rel='stylesheet' /> | |
<body> | |
<div class='map'> | |
<svg></svg> | |
</div> | |
<div class='buttons'> | |
<button class='enter'>Enter</button> | |
<button class='exit'>Exit</button> | |
</div> | |
<script src='https://d3js.org/d3.v4.min.js'></script> | |
<script src='https://d3js.org/d3-selection-multi.v1.min.js'></script> | |
<script src='https://d3js.org/topojson.v2.min.js'></script> | |
<script src='https://npmcdn.com/@turf/[email protected]/turf.min.js'></script> | |
<script src='https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js'></script> | |
<script src='dist.js'></script> | |
</body> |
all: | |
rm boundary.topojson; | |
mapshaper -i ~/Downloads/cb_2015_us_nation_5m/cb_2015_us_nation_5m.shp name=boundary -clip bbox=-126,23,-65,50 -filter-slivers min-area=700000000 -lines -simplify dp 5% -o format=topojson boundary.topojson; |
// Setup chart dimensions. | |
const margin = { top: 10, right: 10, bottom: 10, left: 10 } | |
// Get GeoJSON. | |
d3.json('./boundary.topojson', json => { | |
const feature = topojson.feature(json, json.objects.boundary) | |
// Get feature's bounds and centroid. | |
const bounds = d3.geoBounds(feature) | |
const centroid = d3.geoCentroid(feature) | |
// const projection = d3.geoConicConformal() | |
// .parallels([bounds[0][1], bounds[1][1]]) | |
// .rotate([-centroid[0], 0]) | |
// .center([0, -centroid[1]]) | |
const projection = d3.geoAlbers() | |
// Get the path. | |
const path = d3.geoPath().projection(projection) | |
// Get the path's bounds (i.e., in pixels). | |
const b = path.bounds(feature) | |
// Get aspect ratio. | |
const aspect = (b[1][0] - b[0][0]) / (b[1][1] - b[0][1]) | |
const outerHeight = 460 | |
const outerWidth = outerHeight * aspect | |
const width = outerWidth - margin.left - margin.right | |
const height = outerHeight - margin.top - margin.bottom | |
// Prepare svg. | |
const g = d3.select('.map svg') | |
.attrs({ width: outerWidth, height: outerHeight }) | |
.append('g') | |
.attr('transform', `translate(${margin.left}, ${margin.top})`) | |
// Fit the feature to the container's width. | |
projection.fitSize([width, height], feature) | |
// Get the individual line strings. | |
const lineStrings = _(feature.features) | |
.map('geometry') | |
.flatten() | |
.map('coordinates') | |
.flatten() | |
.sortBy('length') | |
.map(d => turf.lineString(d)) | |
.value() | |
// Calculate the overall line string length. | |
const totalLength = _(lineStrings) | |
.map(d => turf.lineDistance(d)) | |
.sum() | |
// Desired number of total points. | |
const pointsCount = 1000 | |
let pointsRemaining = pointsCount | |
const points = _(lineStrings) | |
.map(line => { | |
// How many points will this line get? | |
// First, calculate this line's length. | |
const lineLength = turf.lineDistance(line) | |
// Next, get this line's points proportion, rounded up. | |
const upperCount = Math.ceil(lineLength * pointsCount / totalLength) | |
// Don't get more points that are available. | |
const linePointsCount = Math.min(pointsRemaining, upperCount) | |
// Make sure to update points remaining. | |
pointsRemaining -= linePointsCount | |
// Now that we know how many points this line will get, | |
// calculate the distance between points - the step: | |
const step = lineLength / linePointsCount | |
const linePoints = d3.range(linePointsCount) | |
.map(d => turf.along(line, d * step)) | |
return linePoints | |
}) | |
.flatten() | |
.map(d => projection(d.geometry.coordinates)) | |
.value() | |
// This function adds the circles. | |
const enter = () => { | |
// JOIN new data with old elements. | |
const circles = g.selectAll('circle') | |
.data(points) | |
// ENTER new elements present in new data. | |
circles.enter().append('circle') | |
.attrs({ | |
cx: width/2, | |
cy: height/2, | |
r: 0, | |
}) | |
.transition('enter') | |
.duration(1000) | |
.delay((d, i) => i * 10) | |
.attrs({ | |
cx: d => d[0], | |
cy: d => d[1], | |
r: 1, | |
}) | |
} | |
// This function removes the circles. | |
const exit = () => { | |
// JOIN new data with old elements. | |
const circles = g.selectAll('circle') | |
.data(points) | |
// UPDATE old elements present in new data. | |
circles | |
.transition('exit') | |
.duration(250) | |
.delay((d, i) => i * 2) | |
.attrs({ | |
cx: width/2, | |
cy: height/2, | |
r: 0, | |
}) | |
.remove() | |
} | |
// Fire the enter function on page load. | |
enter() | |
// Listen to button clicks. | |
document.querySelector('button.enter').addEventListener('click', enter) | |
document.querySelector('button.exit').addEventListener('click', exit) | |
}) |
$red = #8f092a | |
.buttons | |
margin 0 auto | |
text-align center | |
.map | |
svg | |
display block | |
margin 0 auto | |
circle | |
fill $red | |
fill-opacity 0.25 | |
stroke $red |