Skip to content

Instantly share code, notes, and snippets.

@anbnyc
Last active July 26, 2017 17:37
Show Gist options
  • Save anbnyc/ee86d4397446672f61396e00759fe107 to your computer and use it in GitHub Desktop.
Save anbnyc/ee86d4397446672f61396e00759fe107 to your computer and use it in GitHub Desktop.
Neighboring states

Neighboring States

Produced at TWO-N. How many neighbors does each state have? This map reimagines the states as regular polygons with one side for each neighbor. Click a state to see its neighbors up close!

Methodology

When you click on a state, the position of its neighbors approximate their true relative geographic position. The relative positions are calculated using the geographic centers of each state.

Sources

With inspiration and code from:

<html>
<head>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.0/topojson.min.js"></script>
<style>
polygon {
fill: #ccc;
stroke: #00f;
stroke-width: 1px;
}
text{
font-family: Helvetica;
font-size: 14px;
}
.country-state:hover polygon, .neighbor-state:hover polygon {
stroke: #f00;
}
.neighbor-state polygon {
fill-opacity: .7;
fill: #eee;
}
.country-state text, .neighbor-state text{
fill-opacity: 0;
}
.country-state:hover text, .neighbor-state:hover text{
fill-opacity: 1;
}
</style>
</head>
<body>
<svg width="100%" height="100%"></svg>
<script type="text/javascript" src="./index.js"></script>
</body>
</html
const pi = Math.PI;
var svg = d3.select("svg");
var path = d3.geoPath();
const bigStateRadius = 50;
const littleStateRadius = 20;
d3.queue()
.defer(d3.json, "https://d3js.org/us-10m.v1.json")
.defer(d3.json, './states.json')
.awaitAll(function(error, results){
run(results[0], results[1])
})
function run(us, states){
var current;
var features = topojson.feature(us, us.objects.states).features;
features.forEach(e => {
states[e.id] = Object.assign({}, states[e.id], e, { n_neighbors: states[e.id].neighbors.length, center: path.centroid(e) })
})
for(var k in states){
const s = states[k];
s.dir_neighbors = s.neighbors.length
? s.neighbors
.map(o => Math.atan2(states[o].center[1] - s.center[1], states[o].center[0] - s.center[0]) * 180 / Math.PI)
.map(o => o < 0 ? 360 + o : o)
: []
}
var countryMap = svg.append("g")
.attr("class","country-map")
.selectAll(".country-state")
.data(Object.values(states));
var countryGroups = countryMap.enter().append("g")
.attr("class","country-state")
.attr("transform", d => "translate("+d.center[0]+","+d.center[1]+")" )
.on('click', function(d){
current = d.id;
drawDetailMap();
});
countryGroups
.append("polygon")
.attr("points",d => createPointsPoly(d.n_neighbors, 20).map(o => o.join(",")).join(" "));
countryGroups
.append("text")
.attr("x", -10)
.attr("y", 5)
.text(d => d.abbr)
function drawDetailMap(){
const t = d3.transition()
.duration(5000);
var currentState = states[current]
var neighbors = [...currentState.neighbors]
.sort((a,b) =>
currentState.dir_neighbors[currentState.neighbors.indexOf(a)]
- currentState.dir_neighbors[currentState.neighbors.indexOf(b)]
);
const bigStateSideLen = getSideGivenRadius(neighbors.length, bigStateRadius);
var detailMapData = svg.selectAll(".detail-map")
.data([currentState], d => d.id);
detailMapData.exit().transition(t).style("opacity",0).remove();
var detailMap = detailMapData
.enter().append("g")
.attr("class","detail-map");
var bigState = detailMap.append("g")
.attr("class","detail-state big-state")
.attr("transform", d => "translate("+d.center[0]+","+d.center[1]+")");
bigState.append("polygon")
.attr("points",d => createPointsPoly(d.n_neighbors, littleStateRadius).map(o => o.join(",")).join(" "));
bigState.append("text")
.attr("x", -10)
.attr("y", 5)
.text(d => d.abbr)
bigState.transition(t)
.attr("transform","translate(0,0)")
bigState.select("polygon")
.transition(t)
.attr("points",d => createPointsPoly(d.n_neighbors, bigStateRadius).map(o => o.join(",")).join(" "));
var neighborPolyData = detailMap
.selectAll(".neighbor-state")
.data(neighbors, d => d.id);
var neighborPoly = neighborPolyData
.enter().append("g")
.attr("class","detail-state neighbor-state")
.on('click', function(d){
current = d;
drawDetailMap();
});
neighborPoly
.merge(neighborPolyData)
.attr("transform", d => "translate("+states[d].center[0]+","+states[d].center[1]+")")
.transition(t)
.attr("transform", (d,i) =>
getPositionBySide(i, currentState.n_neighbors, bigStateRadius, states[d].n_neighbors, bigStateSideLen))
neighborPoly.append("polygon")
.attr("points", d => createPointsPoly(states[d].n_neighbors, littleStateRadius))
.transition(t)
.attr("points", d => createPointsPoly(
states[d].n_neighbors,
getRadiusGivenSide(states[d].n_neighbors, bigStateSideLen)
));
detailMap.selectAll(".neighbor-state")
.append("text")
.attr("x", -10)
.attr("y", 5)
.attr("transform", (d,i) =>
"rotate("+ -180/pi * getRotateAngle(getAngleBySide(i, neighbors.length), states[d].n_neighbors)+")")
.text(d => states[d].abbr)
detailMap
.transition(t)
.attr("transform","translate(1150, 200)")
}
}
function createPointsPoly(n, radius) {
var base = 2 * pi / n;
var poly = new Array();
for(var i = 0; i < n; i++) {
var fi_i = base * i + 0;
poly.push([
radius * Math.cos(fi_i),
radius * Math.sin(fi_i)
]);
}
return poly;
}
function getPositionBySide(index, totalNeighbors, bigStateRadius, nSides, bigStateSideLen){
var angle = getAngleBySide(index, totalNeighbors);
var rotateAngle = getRotateAngle(angle, nSides);
var littleStateRadius = getRadiusGivenSide(nSides, bigStateSideLen);
var littleStateApothem = getApothemGivenRadius(nSides, littleStateRadius)
var bigStateApothem = getApothemGivenRadius(totalNeighbors, bigStateRadius);
var x = Math.cos(angle)*(bigStateApothem + littleStateApothem);
var y = Math.sin(angle)*(bigStateApothem + littleStateApothem);
return "translate("+x+","+y+")" + " rotate("+(180/pi * rotateAngle)+")";
}
function getAngleBySide(index, totalNeighbors){
return index*(2*pi/totalNeighbors) + (2*pi / totalNeighbors / 2)
}
function getRotateAngle(theta, nSides){
return nSides % 2 === 0 ? (theta + (2*pi / nSides / 2)) : theta
}
function getApothemGivenRadius(nSides, radius){
return radius * Math.cos(pi/nSides);
}
function getRadiusGivenSide(nSides, sideLen){
return sideLen / (2*Math.sin(pi/nSides));
}
function getSideGivenRadius(nSides, radius){
return radius * 2*Math.sin(pi/nSides);
}
{
"30":{
"neighbors":[
"16",
"38",
"46",
"56"
],
"abbr":"MT",
"name":"Montana"
},
"54":{
"neighbors":[
"21",
"24",
"39",
"42",
"51"
],
"abbr":"WV",
"name":"West Virginia"
},
"42":{
"neighbors":[
"10",
"24",
"34",
"36",
"39",
"54"
],
"abbr":"PA",
"name":"Pennsylvania"
},
"48":{
"neighbors":[
"05",
"22",
"35",
"40"
],
"abbr":"TX",
"name":"Texas"
},
"45":{
"neighbors":[
"13",
"37"
],
"abbr":"SC",
"name":"South Carolina"
},
"50":{
"neighbors":[
"25",
"33",
"36"
],
"abbr":"VT",
"name":"Vermont"
},
"49":{
"neighbors":[
"04",
"08",
"16",
"32",
"35",
"56"
],
"abbr":"UT",
"name":"Utah"
},
"53":{
"neighbors":[
"16",
"41"
],
"abbr":"WA",
"name":"Washington"
},
"02":{
"neighbors":[
],
"abbr":"AK",
"name":"Alaska"
},
"25":{
"neighbors":[
"09",
"33",
"36",
"44",
"50"
],
"abbr":"MA",
"name":"Massachusetts"
},
"26":{
"neighbors":[
"17",
"18",
"39",
"55"
],
"abbr":"MI",
"name":"Michigan"
},
"01":{
"neighbors":[
"12",
"13",
"28",
"47"
],
"abbr":"AL",
"name":"Alabama"
},
"06":{
"neighbors":[
"04",
"32",
"41"
],
"abbr":"CA",
"name":"California"
},
"21":{
"neighbors":[
"17",
"18",
"29",
"39",
"47",
"51",
"54"
],
"abbr":"KY",
"name":"Kentucky"
},
"04":{
"neighbors":[
"06",
"08",
"32",
"35",
"49"
],
"abbr":"AZ",
"name":"Arizona"
},
"05":{
"neighbors":[
"22",
"28",
"29",
"40",
"47",
"48"
],
"abbr":"AR",
"name":"Arkansas"
},
"46":{
"neighbors":[
"19",
"27",
"30",
"31",
"38",
"56"
],
"abbr":"SD",
"name":"South Dakota"
},
"47":{
"neighbors":[
"01",
"05",
"13",
"21",
"28",
"29",
"37",
"51"
],
"abbr":"TN",
"name":"Tennessee"
},
"08":{
"neighbors":[
"04",
"20",
"31",
"35",
"40",
"49",
"56"
],
"abbr":"CO",
"name":"Colorado"
},
"09":{
"neighbors":[
"25",
"36",
"44"
],
"abbr":"CT",
"name":"Connecticut"
},
"28":{
"neighbors":[
"01",
"05",
"22",
"47"
],
"abbr":"MS",
"name":"Mississippi"
},
"29":{
"neighbors":[
"05",
"17",
"19",
"20",
"21",
"31",
"40",
"47"
],
"abbr":"MO",
"name":"Missouri"
},
"40":{
"neighbors":[
"05",
"08",
"20",
"29",
"35",
"48"
],
"abbr":"OK",
"name":"Oklahoma"
},
"41":{
"neighbors":[
"06",
"16",
"32",
"53"
],
"abbr":"OR",
"name":"Oregon"
},
"51":{
"neighbors":[
"11",
"21",
"24",
"37",
"47",
"54"
],
"abbr":"VA",
"name":"Virginia"
},
"24":{
"neighbors":[
"10",
"42",
"51",
"54"
],
"abbr":"MD",
"name":"Maryland"
},
"56":{
"neighbors":[
"08",
"16",
"30",
"31",
"46",
"49"
],
"abbr":"WY",
"name":"Wyoming"
},
"39":{
"neighbors":[
"18",
"21",
"26",
"42",
"54"
],
"abbr":"OH",
"name":"Ohio"
},
"27":{
"neighbors":[
"19",
"38",
"46",
"55"
],
"abbr":"MN",
"name":"Minnesota"
},
"20":{
"neighbors":[
"08",
"29",
"31",
"40"
],
"abbr":"KS",
"name":"Kansas"
},
"38":{
"neighbors":[
"27",
"30",
"46"
],
"abbr":"ND",
"name":"North Dakota"
},
"11":{
"neighbors":[
"24",
"51"
],
"abbr":"DC",
"name":"District of Columbia"
},
"10":{
"neighbors":[
"24",
"34",
"42"
],
"abbr":"DE",
"name":"Delaware"
},
"13":{
"neighbors":[
"01",
"12",
"37",
"45",
"47"
],
"abbr":"GA",
"name":"Georgia"
},
"12":{
"neighbors":[
"01",
"13"
],
"abbr":"FL",
"name":"Florida"
},
"15":{
"neighbors":[
],
"abbr":"HI",
"name":"Hawaii"
},
"22":{
"neighbors":[
"05",
"28",
"48"
],
"abbr":"LA",
"name":"Louisiana"
},
"17":{
"neighbors":[
"18",
"19",
"26",
"21",
"29",
"55"
],
"abbr":"IL",
"name":"Illinois"
},
"16":{
"neighbors":[
"30",
"32",
"41",
"49",
"53",
"56"
],
"abbr":"ID",
"name":"Idaho"
},
"19":{
"neighbors":[
"17",
"27",
"29",
"31",
"46",
"55"
],
"abbr":"IA",
"name":"Iowa"
},
"18":{
"neighbors":[
"17",
"21",
"26",
"39"
],
"abbr":"IN",
"name":"Indiana"
},
"31":{
"neighbors":[
"08",
"19",
"20",
"29",
"46",
"56"
],
"abbr":"NE",
"name":"Nebraska"
},
"23":{
"neighbors":[
"33"
],
"abbr":"ME",
"name":"Maine"
},
"37":{
"neighbors":[
"13",
"45",
"47",
"51"
],
"abbr":"NC",
"name":"North Carolina"
},
"36":{
"neighbors":[
"09",
"25",
"34",
"42",
"50"
],
"abbr":"NY",
"name":"New York"
},
"35":{
"neighbors":[
"04",
"08",
"40",
"48",
"49"
],
"abbr":"NM",
"name":"New Mexico"
},
"34":{
"neighbors":[
"10",
"36",
"42"
],
"abbr":"NJ",
"name":"New Jersey"
},
"33":{
"neighbors":[
"23",
"25",
"50"
],
"abbr":"NH",
"name":"New Hampshire"
},
"55":{
"neighbors":[
"17",
"19",
"26",
"27"
],
"abbr":"WI",
"name":"Wisconsin"
},
"32":{
"neighbors":[
"04",
"06",
"16",
"41",
"49"
],
"abbr":"NV",
"name":"Nevada"
},
"44":{
"neighbors":[
"09",
"25"
],
"abbr":"RI",
"name":"Rhode Island"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment