Transform a collection of geojson features into squares and back again.
This block uses several functions from d3-geo and MapBox's polylabel algorithm to intelligently position the labels.
For more map tweening, see this block and this block.
license: gpl-3.0 |
Transform a collection of geojson features into squares and back again.
This block uses several functions from d3-geo and MapBox's polylabel algorithm to intelligently position the labels.
For more map tweening, see this block and this block.
<html> | |
<head> | |
<style> | |
body { | |
margin: 0; | |
font-family: "Helvetica Neue", sans-serif; | |
} | |
.state-path { | |
fill: #ccc; | |
stroke: #fff; | |
stroke-width: 1px; | |
} | |
.state-label { | |
font-size: .5em; | |
} | |
</style> | |
</head> | |
<body step="0"> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script src="polylabel.js"></script> | |
<script> | |
var width = window.innerWidth, | |
height = window.innerHeight, | |
duration = 800; | |
var svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height); | |
d3.json("us.json", function(map) { | |
var projection = centerZoom(map); | |
var polygons = []; | |
map.features.forEach(function(feature){ | |
polygons.push({id: feature.properties.state, geom: feature.geometry}) | |
}); | |
var init = parse(polygons, projection).sort(function(a, b){ | |
return b.area - a.area; | |
}); | |
init.forEach(function(d){ | |
var path = drawPath(d).attr("d", d.d0) | |
drawLabels(d, 0, 0); | |
}); | |
d3.interval(function(){ | |
var steps = stepUpdate(); | |
stepChange(steps[0], steps[1], init); | |
}, duration * 2); | |
}); | |
// This function "centers" and "zooms" a map by setting its projection's scale and translate according to its outer boundary | |
function centerZoom(data){ | |
// create a first guess for the projection | |
var scale = 1; | |
var offset = [width / 2, height / 2]; | |
var projection = d3.geoAlbersUsa().scale(scale).translate(offset); | |
// get bounds | |
var bounds = d3.geoPath().projection(projection).bounds(data); | |
// calculate the scale and offset | |
var hscale = scale * width / (bounds[1][0] - bounds[0][0]); | |
var vscale = scale * height / (bounds[1][1] - bounds[0][1]); | |
var scale = (hscale < vscale) ? hscale : vscale; | |
var offset = [width - (bounds[0][0] + bounds[1][0]) / 2, height - (bounds[0][1] + bounds[1][1]) / 2]; | |
// new projection | |
projection = d3.geoAlbersUsa() | |
.scale(scale) | |
.translate(offset); | |
return projection; | |
} | |
function drawLabels(obj,oldStep,newStep){ | |
var pOld = polylabel([obj["coordinates" + oldStep]], 1); | |
var pNew = polylabel([obj["coordinates" + newStep]], 1); | |
svg.append("text") | |
.attr("class", "state state-label") | |
.attr("x", pOld[0]) | |
.attr("y", pOld[1]) | |
.attr("dy", 5) | |
.attr("text-anchor", "middle") | |
.text(obj.id) | |
.transition().duration(duration) | |
.attr("x", pNew[0]) | |
.attr("y", pNew[1]) | |
} | |
function drawPath(obj){ | |
var path = svg.append("path") | |
.attr("class", "state state-path") | |
.attr("id", obj.id) | |
return path; | |
} | |
function parse(polygons, projection) { | |
var arr = []; | |
polygons.forEach(function(state){ | |
var obj = {}; | |
obj.id = state.id; | |
obj.coordinates0 = state.geom.coordinates[0].map(projection); | |
obj.coordinates1 = square(obj.coordinates0)[0]; | |
obj.d0 = "M" + obj.coordinates0.join("L") + "Z"; | |
obj.d1 = "M" + obj.coordinates1.join("L") + "Z"; | |
obj.area = square(obj.coordinates0)[1]; | |
arr.push(obj); | |
}); | |
return arr; | |
} | |
function square(coordinates){ | |
var area = d3.polygonArea(coordinates); | |
area < 0 ? area = area * -1 : area = area; | |
var r = Math.sqrt(area) / 2.5; | |
var centroid = d3.polygonCentroid(coordinates); | |
var x = centroid[0]; | |
var y = centroid[1]; | |
var len = coordinates.length; | |
var square = squareCoords(x, y, r, len); | |
return [square, area]; | |
} | |
function squareCoords(x, y, r, len){ | |
var square = []; | |
var topLf = [x - r, y - r]; | |
var topRt = [x + r, y - r]; | |
var botRt = [x + r, y + r]; | |
var botLf = [x - r, y + r]; | |
for (var i = 0; i < len / 4; i++){ | |
square.push(botRt); | |
} | |
for (var i = 0; i < len / 4; i++){ | |
square.push(botLf); | |
} | |
for (var i = 0; i < len / 4; i++){ | |
square.push(topLf); | |
} | |
for (var i = 0; i < len / 4; i++){ | |
square.push(topRt); | |
} | |
return square; | |
} | |
function stepChange(oldStep, newStep, obj){ | |
d3.selectAll(".state").remove(); | |
obj.forEach( function(d){ | |
transitionPath(drawPath(d), d["d" + oldStep], d["d" + newStep], duration); | |
drawLabels(d, oldStep, newStep); | |
} ); | |
} | |
function stepUpdate(){ | |
var currStep = +d3.select("body").attr("step"), newStep; | |
var newStep = currStep == 0 ? 1 : 0; | |
d3.select("body").attr("step", newStep); | |
return [currStep, newStep]; | |
} | |
function transitionPath(path, d0, d1, duration){ | |
path | |
.attr("d", d0) | |
.transition().duration(duration) | |
.attr("d", d1); | |
} | |
</script> | |
</body> | |
</html> |
// ISC License | |
// Copyright (c) 2016 Mapbox | |
// | |
// Permission to use, copy, modify, and/or distribute this software for any purpose | |
// with or without fee is hereby granted, provided that the above copyright notice | |
// and this permission notice appear in all copies. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO | |
// THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. | |
// IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR | |
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA | |
// OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS | |
// ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS | |
// SOFTWARE. | |
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.polylabel=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}({1:[function(require,module,exports){"use strict";var Queue=require("tinyqueue");module.exports=polylabel;module.exports.default=polylabel;function polylabel(polygon,precision,debug){precision=precision||1;var minX,minY,maxX,maxY;for(var i=0;i<polygon[0].length;i++){var p=polygon[0][i];if(!i||p[0]<minX)minX=p[0];if(!i||p[1]<minY)minY=p[1];if(!i||p[0]>maxX)maxX=p[0];if(!i||p[1]>maxY)maxY=p[1]}var width=maxX-minX;var height=maxY-minY;var cellSize=Math.min(width,height);var h=cellSize/2;var cellQueue=new Queue(null,compareMax);for(var x=minX;x<maxX;x+=cellSize){for(var y=minY;y<maxY;y+=cellSize){cellQueue.push(new Cell(x+h,y+h,h,polygon))}}var bestCell=getCentroidCell(polygon);var bboxCell=new Cell(minX+width/2,minY+height/2,0,polygon);if(bboxCell.d>bestCell.d)bestCell=bboxCell;var numProbes=cellQueue.length;while(cellQueue.length){var cell=cellQueue.pop();if(cell.d>bestCell.d){bestCell=cell;if(debug)console.log("found best %d after %d probes",Math.round(1e4*cell.d)/1e4,numProbes)}if(cell.max-bestCell.d<=precision)continue;h=cell.h/2;cellQueue.push(new Cell(cell.x-h,cell.y-h,h,polygon));cellQueue.push(new Cell(cell.x+h,cell.y-h,h,polygon));cellQueue.push(new Cell(cell.x-h,cell.y+h,h,polygon));cellQueue.push(new Cell(cell.x+h,cell.y+h,h,polygon));numProbes+=4}if(debug){console.log("num probes: "+numProbes);console.log("best distance: "+bestCell.d)}return[bestCell.x,bestCell.y]}function compareMax(a,b){return b.max-a.max}function Cell(x,y,h,polygon){this.x=x;this.y=y;this.h=h;this.d=pointToPolygonDist(x,y,polygon);this.max=this.d+this.h*Math.SQRT2}function pointToPolygonDist(x,y,polygon){var inside=false;var minDistSq=Infinity;for(var k=0;k<polygon.length;k++){var ring=polygon[k];for(var i=0,len=ring.length,j=len-1;i<len;j=i++){var a=ring[i];var b=ring[j];if(a[1]>y!==b[1]>y&&x<(b[0]-a[0])*(y-a[1])/(b[1]-a[1])+a[0])inside=!inside;minDistSq=Math.min(minDistSq,getSegDistSq(x,y,a,b))}}return(inside?1:-1)*Math.sqrt(minDistSq)}function getCentroidCell(polygon){var area=0;var x=0;var y=0;var points=polygon[0];for(var i=0,len=points.length,j=len-1;i<len;j=i++){var a=points[i];var b=points[j];var f=a[0]*b[1]-b[0]*a[1];x+=(a[0]+b[0])*f;y+=(a[1]+b[1])*f;area+=f*3}return new Cell(x/area,y/area,0,polygon)}function getSegDistSq(px,py,a,b){var x=a[0];var y=a[1];var dx=b[0]-x;var dy=b[1]-y;if(dx!==0||dy!==0){var t=((px-x)*dx+(py-y)*dy)/(dx*dx+dy*dy);if(t>1){x=b[0];y=b[1]}else if(t>0){x+=dx*t;y+=dy*t}}dx=px-x;dy=py-y;return dx*dx+dy*dy}},{tinyqueue:2}],2:[function(require,module,exports){"use strict";module.exports=TinyQueue;function TinyQueue(data,compare){if(!(this instanceof TinyQueue))return new TinyQueue(data,compare);this.data=data||[];this.length=this.data.length;this.compare=compare||defaultCompare;if(data)for(var i=Math.floor(this.length/2);i>=0;i--)this._down(i)}function defaultCompare(a,b){return a<b?-1:a>b?1:0}TinyQueue.prototype={push:function(item){this.data.push(item);this.length++;this._up(this.length-1)},pop:function(){var top=this.data[0];this.data[0]=this.data[this.length-1];this.length--;this.data.pop();this._down(0);return top},peek:function(){return this.data[0]},_up:function(pos){var data=this.data,compare=this.compare;while(pos>0){var parent=Math.floor((pos-1)/2);if(compare(data[pos],data[parent])<0){swap(data,parent,pos);pos=parent}else break}},_down:function(pos){var data=this.data,compare=this.compare,len=this.length;while(true){var left=2*pos+1,right=left+1,min=pos;if(left<len&&compare(data[left],data[min])<0)min=left;if(right<len&&compare(data[right],data[min])<0)min=right;if(min===pos)return;swap(data,min,pos);pos=min}}};function swap(data,i,j){var tmp=data[i];data[i]=data[j];data[j]=tmp}},{}]},{},[1])(1)}); |