Circles on an exploded mesh
Made with blockup.
.buttons{margin:0 auto;text-align:center}.map svg{display:block;margin:0 auto}.map svg circle{fill:#8f092a}.map svg path{fill:none;stroke:#8f092a} |
var margin={top:10,right:10,bottom:10,left:10},outerWidth=360,outerHeight=outerWidth,width=outerWidth-margin.left-margin.right,height=outerHeight-margin.top-margin.bottom,g=d3.select(".map svg").attrs({width:outerWidth,height:outerHeight}).append("g").attr("transform","translate("+margin.left+", "+margin.top+")");d3.json("./MA.topojson",function(t){var e=topojson.mesh(t),r=d3.geoBounds(e),n=d3.geoCentroid(e),o=d3.geoConicConformal().parallels([r[0][1],r[1][1]]).rotate([-n[0],0]).center([0,-n[1]]),i=d3.geoPath().projection(o);i.bounds(e);o.fitSize([width,height],e);var a=_(e.coordinates).flatten().map(function(t){return{xy:o(t)}}).value(),c=function(){var t=g.selectAll("circle").data(a);t.enter().append("circle").attrs({cx:width/2,cy:height,r:0}).transition("enter").duration(1e3).delay(function(t,e){return 5*e}).attrs({cx:function(t){return t.xy[0]},cy:function(t){return t.xy[1]},r:1})},d=function(){var t=g.selectAll("circle").data(a);t.transition("exit").duration(250).delay(function(t,e){return 2*e}).attrs({cx:width/2,cy:height,r:0}).remove()};c(),document.querySelector("button.enter").addEventListener("click",c),document.querySelector("button.exit").addEventListener("click",d)}); | |
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNjcmlwdC5qcyJdLCJuYW1lcyI6WyJjb25zdCIsIm1hcmdpbiIsInRvcCIsInJpZ2h0IiwiYm90dG9tIiwibGVmdCIsIm91dGVyV2lkdGgiLCJvdXRlckhlaWdodCIsIndpZHRoIiwiaGVpZ2h0IiwiZyIsImQzIiwic2VsZWN0IiwiYXR0cnMiLCJhcHBlbmQiLCJhdHRyIiwianNvbiIsImZlYXR1cmUiLCJ0b3BvanNvbiIsIm1lc2giLCJib3VuZHMiLCJnZW9Cb3VuZHMiLCJjZW50cm9pZCIsImdlb0NlbnRyb2lkIiwicHJvamVjdGlvbiIsImdlb0NvbmljQ29uZm9ybWFsIiwicGFyYWxsZWxzIiwicm90YXRlIiwiY2VudGVyIiwicGF0aCIsImdlb1BhdGgiLCJmaXRTaXplIiwicG9pbnRzIiwiXyIsImNvb3JkaW5hdGVzIiwiZmxhdHRlbiIsIm1hcCIsInYiLCJ4eSIsInZhbHVlIiwiZW50ZXIiLCJjaXJjbGVzIiwic2VsZWN0QWxsIiwiZGF0YSIsImN4IiwiY3kiLCJyIiwidHJhbnNpdGlvbiIsImR1cmF0aW9uIiwiZGVsYXkiLCJkIiwiaSIsImV4aXQiLCJyZW1vdmUiLCJkb2N1bWVudCIsInF1ZXJ5U2VsZWN0b3IiLCJhZGRFdmVudExpc3RlbmVyIl0sIm1hcHBpbmdzIjoiQUFDQUEsR0FBTUMsU0FBV0MsSUFBTyxHQUFFQyxNQUFTLEdBQUVDLE9BQVUsR0FBRUMsS0FBUSxJQUVuREMsV0FBYSxJQUNiQyxZQUFjRCxXQUVkRSxNQUFRRixXQUFhTCxPQUFPSSxLQUFPSixPQUFPRSxNQUMxQ00sT0FBU0YsWUFBY04sT0FBT0MsSUFBTUQsT0FBT0csT0FHMUNNLEVBQUtDLEdBQUNDLE9BQU8sWUFDakJDLE9BQVFMLE1BQU9GLFdBQVlHLE9BQVFGLGNBQ3BDTyxPQUFPLEtBQ05DLEtBQUssWUFBYSxhQUFXZCxPQUFTLEtBQUEsS0FBSUEsT0FBRyxJQUFBLElBR2hEVSxJQUFHSyxLQUFLLGdCQUFpQixTQUFBQSxHQUV4QmhCLEdBQU1pQixHQUFVQyxTQUFTQyxLQUFLSCxHQUd4QkksRUFBV1QsR0FBQ1UsVUFBVUosR0FDdEJLLEVBQWFYLEdBQUNZLFlBQVlOLEdBRzFCTyxFQUFlYixHQUFDYyxvQkFDcEJDLFdBQVdOLEVBQU8sR0FBRyxHQUFJQSxFQUFPLEdBQUcsS0FDbkNPLFNBQVNMLEVBQVMsR0FBSSxJQUN0Qk0sUUFBUSxHQUFJTixFQUFTLEtBR2pCTyxFQUFTbEIsR0FBQ21CLFVBQVVOLFdBQVdBLEVBRzNCSyxHQUFLVCxPQUFPSCxFQUd0Qk8sR0FBV08sU0FBU3ZCLE1BQU9DLFFBQVNRLEVBRXBDakIsSUFBTWdDLEdBQVdDLEVBQUFoQixFQUFRaUIsYUFDdkJDLFVBQ0FDLElBQUksU0FBQUMsR0FBQSxPQUNKQyxHQUFJZCxFQUFXYSxNQUVmRSxRQVFJQyxFQUFRLFdBR2J4QyxHQUFNeUMsR0FBWS9CLEVBQUFnQyxVQUFVLFVBQ3pCQyxLQUFLWCxFQUdSUyxHQUFRRCxRQUFRMUIsT0FBTyxVQUNwQkQsT0FDQStCLEdBQUlwQyxNQUFNLEVBQ1ZxQyxHQUFJcEMsT0FDSnFDLEVBQUcsSUFFSkMsV0FBVyxTQUNWQyxTQUFTLEtBQ1RDLE1BQU0sU0FBQUMsRUFBQUMsR0FBQSxNQUFLLEdBQUpBLElBQ1B0QyxPQUNBK0IsR0FBSSxTQUFBTSxHQUFBLE1BQUFBLEdBQUFaLEdBQUEsSUFDSk8sR0FBSSxTQUFBSyxHQUFBLE1BQUFBLEdBQUFaLEdBQUEsSUFDSlEsRUFBRyxLQU1ETSxFQUFPLFdBR1pwRCxHQUFNeUMsR0FBWS9CLEVBQUFnQyxVQUFVLFVBQ3pCQyxLQUFLWCxFQUdSUyxHQUNFTSxXQUFXLFFBQ1ZDLFNBQVMsS0FDVEMsTUFBTSxTQUFBQyxFQUFBQyxHQUFBLE1BQUssR0FBSkEsSUFDUHRDLE9BQ0ErQixHQUFJcEMsTUFBTSxFQUNWcUMsR0FBSXBDLE9BQ0pxQyxFQUFHLElBRUpPLFNBS0hiLEtBR0FjLFNBQVNDLGNBQWMsZ0JBQWdCQyxpQkFBaUIsUUFBU2hCLEdBQ2pFYyxTQUFTQyxjQUFjLGVBQWVDLGlCQUFpQixRQUFTSiIsImZpbGUiOiJzY3JpcHQuanMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBTZXR1cCBjaGFydCBkaW1lbnNpb25zLlxuY29uc3QgbWFyZ2luID0geyB0b3A6IDEwLCByaWdodDogMTAsIGJvdHRvbTogMTAsIGxlZnQ6IDEwIH1cblxuY29uc3Qgb3V0ZXJXaWR0aCA9IDM2MFxuY29uc3Qgb3V0ZXJIZWlnaHQgPSBvdXRlcldpZHRoXG5cbmNvbnN0IHdpZHRoID0gb3V0ZXJXaWR0aCAtIG1hcmdpbi5sZWZ0IC0gbWFyZ2luLnJpZ2h0XG5jb25zdCBoZWlnaHQgPSBvdXRlckhlaWdodCAtIG1hcmdpbi50b3AgLSBtYXJnaW4uYm90dG9tXG5cbi8vIFByZXBhcmUgc3ZnLlxuY29uc3QgZyA9IGQzLnNlbGVjdCgnLm1hcCBzdmcnKVxuXHRcdC5hdHRycyh7IHdpZHRoOiBvdXRlcldpZHRoLCBoZWlnaHQ6IG91dGVySGVpZ2h0IH0pXG5cdC5hcHBlbmQoJ2cnKVxuXHRcdC5hdHRyKCd0cmFuc2Zvcm0nLCBgdHJhbnNsYXRlKCR7bWFyZ2luLmxlZnR9LCAke21hcmdpbi50b3B9KWApXG5cbi8vIEdldCBHZW9KU09OLlxuZDMuanNvbignLi9NQS50b3BvanNvbicsIGpzb24gPT4ge1xuXG5cdGNvbnN0IGZlYXR1cmUgPSB0b3BvanNvbi5tZXNoKGpzb24pXG5cblx0Ly8gR2V0IGZlYXR1cmUncyBib3VuZHMgYW5kIGNlbnRyb2lkLlxuXHRjb25zdCBib3VuZHMgPSBkMy5nZW9Cb3VuZHMoZmVhdHVyZSlcblx0Y29uc3QgY2VudHJvaWQgPSBkMy5nZW9DZW50cm9pZChmZWF0dXJlKVxuXG5cdC8vIFVzZSB0aGVtIHRvIGNyZWF0ZSB0aGUgcHJvamVjdGlvbi5cblx0Y29uc3QgcHJvamVjdGlvbiA9IGQzLmdlb0NvbmljQ29uZm9ybWFsKClcblx0XHQucGFyYWxsZWxzKFtib3VuZHNbMF1bMV0sIGJvdW5kc1sxXVsxXV0pXG5cdFx0LnJvdGF0ZShbLWNlbnRyb2lkWzBdLCAwXSlcblx0XHQuY2VudGVyKFswLCAtY2VudHJvaWRbMV1dKVxuXG5cdC8vIEdldCB0aGUgcGF0aC5cblx0Y29uc3QgcGF0aCA9IGQzLmdlb1BhdGgoKS5wcm9qZWN0aW9uKHByb2plY3Rpb24pXG5cblx0Ly8gR2V0IHRoZSBwYXRoJ3MgYm91bmRzIChpLmUuLCBpbiBwaXhlbHMpLlxuXHRjb25zdCBiID0gcGF0aC5ib3VuZHMoZmVhdHVyZSlcblxuXHQvLyBGaXQgdGhlIGZlYXR1cmUgdG8gdGhlIGNvbnRhaW5lcidzIHdpZHRoLlxuXHRwcm9qZWN0aW9uLmZpdFNpemUoW3dpZHRoLCBoZWlnaHRdLCBmZWF0dXJlKVxuXG5cdGNvbnN0IHBvaW50cyA9IF8oZmVhdHVyZS5jb29yZGluYXRlcylcblx0XHQuZmxhdHRlbigpXG5cdFx0Lm1hcCh2ID0+ICh7XG5cdFx0XHR4eTogcHJvamVjdGlvbih2KSxcblx0XHR9KSlcblx0XHQudmFsdWUoKVxuXG5cdC8vIGcuc2VsZWN0QWxsKCdwYXRoJylcblx0Ly8gXHRcdC5kYXRhKFtmZWF0dXJlXSlcblx0Ly8gXHQuZW50ZXIoKS5hcHBlbmQoJ3BhdGgnKVxuXHQvLyBcdFx0LmF0dHIoJ2QnLCBwYXRoKVxuXG5cdC8vIFRoaXMgZnVuY3Rpb24gYWRkcyB0aGUgY2lyY2xlcy5cblx0Y29uc3QgZW50ZXIgPSAoKSA9PiB7XG5cblx0XHQvLyBKT0lOIG5ldyBkYXRhIHdpdGggb2xkIGVsZW1lbnRzLlxuXHRcdGNvbnN0IGNpcmNsZXMgPSBnLnNlbGVjdEFsbCgnY2lyY2xlJylcblx0XHRcdFx0LmRhdGEocG9pbnRzKVxuXG5cdFx0Ly8gRU5URVIgbmV3IGVsZW1lbnRzIHByZXNlbnQgaW4gbmV3IGRhdGEuXG5cdFx0Y2lyY2xlcy5lbnRlcigpLmFwcGVuZCgnY2lyY2xlJylcblx0XHRcdFx0LmF0dHJzKHtcblx0XHRcdFx0XHRjeDogd2lkdGgvMixcblx0XHRcdFx0XHRjeTogaGVpZ2h0LFxuXHRcdFx0XHRcdHI6IDAsXG5cdFx0XHRcdH0pXG5cdFx0XHQudHJhbnNpdGlvbignZW50ZXInKVxuXHRcdFx0XHQuZHVyYXRpb24oMTAwMClcblx0XHRcdFx0LmRlbGF5KChkLCBpKSA9PiBpICogNSlcblx0XHRcdFx0LmF0dHJzKHtcblx0XHRcdFx0XHRjeDogZCA9PiBkLnh5WzBdLFxuXHRcdFx0XHRcdGN5OiBkID0+IGQueHlbMV0sXG5cdFx0XHRcdFx0cjogMSxcblx0XHRcdFx0fSlcblxuXHR9XG5cblx0Ly8gVGhpcyBmdW5jdGlvbiByZW1vdmVzIHRoZSBjaXJjbGVzLlxuXHRjb25zdCBleGl0ID0gKCkgPT4ge1xuXG5cdFx0Ly8gSk9JTiBuZXcgZGF0YSB3aXRoIG9sZCBlbGVtZW50cy5cblx0XHRjb25zdCBjaXJjbGVzID0gZy5zZWxlY3RBbGwoJ2NpcmNsZScpXG5cdFx0XHRcdC5kYXRhKHBvaW50cylcblxuXHRcdC8vIFVQREFURSBvbGQgZWxlbWVudHMgcHJlc2VudCBpbiBuZXcgZGF0YS5cblx0XHRjaXJjbGVzXG5cdFx0XHQudHJhbnNpdGlvbignZXhpdCcpXG5cdFx0XHRcdC5kdXJhdGlvbigyNTApXG5cdFx0XHRcdC5kZWxheSgoZCwgaSkgPT4gaSAqIDIpXG5cdFx0XHRcdC5hdHRycyh7XG5cdFx0XHRcdFx0Y3g6IHdpZHRoLzIsXG5cdFx0XHRcdFx0Y3k6IGhlaWdodCxcblx0XHRcdFx0XHRyOiAwLFxuXHRcdFx0XHR9KVxuXHRcdFx0LnJlbW92ZSgpXG5cblx0fVxuXG5cdC8vIEZpcmUgdGhlIGVudGVyIGZ1bmN0aW9uIG9uIHBhZ2UgbG9hZC5cblx0ZW50ZXIoKVxuXG5cdC8vIExpc3RlbiB0byBidXR0b24gY2xpY2tzLlxuXHRkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdidXR0b24uZW50ZXInKS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGVudGVyKVxuXHRkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdidXR0b24uZXhpdCcpLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgZXhpdClcblxufSlcbiJdfQ== |
<!DOCTYPE html> | |
<title>Circles on an exploded mesh</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/turf@3.10.2/turf.min.js'></script> | |
<script src='https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js'></script> | |
<script src='dist.js'></script> | |
</body> |
// Setup chart dimensions. | |
const margin = { top: 10, right: 10, bottom: 10, left: 10 } | |
const outerWidth = 360 | |
const outerHeight = outerWidth | |
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})`) | |
// Get GeoJSON. | |
d3.json('./MA.topojson', json => { | |
const feature = topojson.mesh(json) | |
// Get feature's bounds and centroid. | |
const bounds = d3.geoBounds(feature) | |
const centroid = d3.geoCentroid(feature) | |
// Use them to create the projection. | |
const projection = d3.geoConicConformal() | |
.parallels([bounds[0][1], bounds[1][1]]) | |
.rotate([-centroid[0], 0]) | |
.center([0, -centroid[1]]) | |
// Get the path. | |
const path = d3.geoPath().projection(projection) | |
// Get the path's bounds (i.e., in pixels). | |
const b = path.bounds(feature) | |
// Fit the feature to the container's width. | |
projection.fitSize([width, height], feature) | |
const points = _(feature.coordinates) | |
.flatten() | |
.map(v => ({ | |
xy: projection(v), | |
})) | |
.value() | |
// g.selectAll('path') | |
// .data([feature]) | |
// .enter().append('path') | |
// .attr('d', path) | |
// 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, | |
r: 0, | |
}) | |
.transition('enter') | |
.duration(1000) | |
.delay((d, i) => i * 5) | |
.attrs({ | |
cx: d => d.xy[0], | |
cy: d => d.xy[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, | |
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) | |
}) |
.buttons | |
margin 0 auto | |
text-align center | |
.map | |
svg | |
display block | |
margin 0 auto | |
circle | |
fill #8f092a | |
// fill-opacity 0.125 | |
// stroke #8f092a | |
path | |
fill none | |
stroke #8f092a |