This is not a true Demers cartogram; it lacks links between adjacent features. Instead of trying to preserve connectedness, this pseudo-cartogram tries to preserve locality, putting each square as close as possible to its origin without overlapping.
forked from mbostock's block: Pseudo-Demers Cartogram
forked from SpaceActuary's block: Pseudo-Demers Cartogram
xxxxxxxxxx
<meta charset="utf-8">
<title>Demers Cartogram</title>
<style>
body {
font-family: "Avenir", sans-serif;
}
.states {
fill: #b8b8b8;
stroke: #fff;
}
.states.hidden {
fill: none;
stroke: none;
}
rect {
/*fill: steelblue;*/
fill-opacity: .8;
stroke: #000;
stroke-width: 0px;
}
text.maptitle {
text-anchor: middle;
alignment-baseline: central;
font-size: 36px;
}
text.legendtitle {
font-weight: bold;
}
.node text {
text-anchor: middle;
alignment-baseline: central;
fill: #fff;
}
form {
position: absolute;
right: 10px;
top: 50px;
font-family: sans-serif;
}
</style>
<body>
<form>
<label><input type="radio" name="mode" value="mccann"> McCann</label>
<label><input type="radio" name="mode" value="demers" checked> Demers</label>
<label><input type="radio" name="mode" value="dorling"> Dorling</label>
<!--label><input type="radio" name="election" value="2008" > 2008</label>
<label><input type="radio" name="election" value="2012" checked> 2012</label-->
<label><input type="checkbox" name="showmap" value="dorling" checked> Show Map</label>
</form>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.10.0/d3-legend.min.js"></script>
<script id="grid" type="text/plain">
AK ME
VT NH
WA ID MT ND MN IL WI MI NY RI MA
OR NV WY SD IA IN OH PA NJ CT DE
CA UT CO NE MO KY WV VA MD
AZ NM KS AR TN NC SC DC
OK LA MS AL GA
HI TX FL PR
</script>
<script>
//console.clear()
var margin = {top: 50, right: 0, bottom: 20, left: 0},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
padding = 3;
var projection = d3.geo.albersUsa();
var radius = d3.scale.sqrt()
.range([0, 50]);
var force = d3.layout.force()
.charge(0)
.gravity(0)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")" +
"scale(" + Math.min(width / 960, height / 500) + ")");
var path = d3.geo.path();
var grid = {};
d3.select("#grid").text().split("\n").forEach(function(line, i) {
var re = /\w+/g, m;
while (m = re.exec(line)) {
grid[m[0]] = [m.index / 3, i]
}
});
var gridWidth = d3.max(d3.values(grid), function(d) { return d[0]; }) + 1,
gridHeight = d3.max(d3.values(grid), function(d) { return d[1]; }) + 1,
cellSize = 55,
cellPadding = 5;
var title = svg.append("text")
.attr("class", "maptitle")
.attr("x", width / 2)
.attr("y", -(margin.top / 2))
.text("2012 Presidental Election Results");
var colorScale = d3.scale.linear()
.domain([1, 0])
.range(["steelblue", "crimson"]);
svg.append("g")
.attr("class", "legendLinear")
.attr("transform", "translate(" + (width - 150) + "," + (height - 100) + ")");
queue()
.defer(d3.csv, "data.csv")
.defer(d3.json, "us-state-centroids.json")
.defer(d3.json, "us.json")
.await(ready);
function ready(error, data, centroids, us) {
if (error) throw error;
//console.table(data);
var dataById = d3.map(data, function(d) { return +d.id; });
var totalD = d3.sum(data, function(d) { return +d["2012D"]; }),
totalR = d3.sum(data, function(d) { return +d["2012R"]; })
var legendLinear = d3.legend.color()
.shapeWidth(30)
.cells(2)
.title("Candidate (electoral votes)")
.labels(["Obama (" + totalD + " votes)",
"Romney (" + totalR + " votes)"])
.scale(colorScale);
svg.select(".legendLinear")
.call(legendLinear);
svg.append("path")
.attr("class", "states visible")
.datum(topojson.feature(us, us.objects.states))
.attr("d", path);
radius.domain([0, d3.max(data, function(d){ return +d["2012Total"];} )]);
//console.log(dataById);
var nodes = centroids.features
.filter(function(d) { return dataById.has(+d.id); })
.map(function(d) {
var stateData = dataById.get(+d.id),
state = stateData.code,
cell = grid[state],
point = projection(d.geometry.coordinates),
votes = stateData["2012Total"],
marginD = +stateData["2012D"] / +stateData["2012Total"],
color = colorScale(marginD);
return {
id: +d.id,
x: point[0], y: point[1],
x0: point[0], y0: point[1],
xx: cell[0] * cellSize + 200, yy: cell[1] * cellSize - (cellSize / 2),
r: radius(votes),
r0: radius(votes),
value: votes,
state: state,
color: color
};
});
//console.log(nodes);
force
.nodes(nodes)
.on("tick", tickDemers)
.start();
var node = svg.selectAll("g.node")
.data(nodes)
var nodeEnter = node.enter().append("g")
.attr("class", "node");
nodeEnter.append("rect")
.attr("width", function(d) { return d.r * 2; })
.attr("height", function(d) { return d.r * 2; })
.style("fill", function(d) { return d.color; })
.attr("rx", function(d) { return 0; })
.attr("title", function(d) { return d.state + ": " + d.value + " electoral college votes"})
.on("click", function(d,i){
console.log("d[" + d.id + "]", d.state, d.value)
});
nodeEnter.append("text")
.attr("x", function (d) { return d.r; })
.attr("y", function (d) { return d.r; })
.style("font-size", function(d) { return (1.5 * d.r - 5); })
.text( function (d) { return d.state; });
function tickDemers(e) {
node.each(gravity(e.alpha * .1))
.each(collideDemers(.5))
.attr("transform", function(d) {
return "translate(" + (d.x - d.r) + "," + (d.y - d.r) + ")";
});
}
function tickDorling(e) {
node.each(gravity(e.alpha * .1))
.each(collideDorling(.5))
.attr("transform", function(d) {
return "translate(" + (d.x - d.r) + "," + (d.y - d.r) + ")";
});
}
function gravity(k) {
return function(d) {
d.x += (d.x0 - d.x) * k;
d.y += (d.y0 - d.y) * k;
};
}
function collideDemers(k) {
var q = d3.geom.quadtree(nodes);
return function(node) {
var nr = node.r + padding,
nx1 = node.x - nr,
nx2 = node.x + nr,
ny1 = node.y - nr,
ny2 = node.y + nr;
q.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
lx = Math.abs(x),
ly = Math.abs(y),
r = nr + quad.point.r;
if (lx < r && ly < r) {
if (lx > ly) {
lx = (lx - r) * (x < 0 ? -k : k);
node.x -= lx;
quad.point.x += lx;
} else {
ly = (ly - r) * (y < 0 ? -k : k);
node.y -= ly;
quad.point.y += ly;
}
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
function collideDorling(k) {
var q = d3.geom.quadtree(nodes);
return function(node) {
var nr = node.r + padding,
nx1 = node.x - nr,
nx2 = node.x + nr,
ny1 = node.y - nr,
ny2 = node.y + nr;
q.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = x * x + y * y,
r = nr + quad.point.r;
if (l < r * r) {
l = ((l = Math.sqrt(l)) - r) / l * k;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
d3.selectAll("input[type='radio']")
.on("change", function change() {
if(this.value === "mccann"){
force.stop();
node.transition().duration(1000)
.attr("transform", function(d) {
return "translate(" + d.xx + "," + d.yy + ")";
});
node.selectAll("rect").transition().duration(1000)
.attr("rx", function(d){ return 0; })
.attr("width", cellSize - cellPadding)
.attr("height", cellSize - cellPadding);
node.selectAll("text").transition().duration(1000)
.attr("x", (cellSize - cellPadding) / 2)
.attr("y", (cellSize - cellPadding) / 2)
.style("font-size", 20);
//force.on("tick", tickMcCann).resume()
} else if(this.value === "demers"){
node.transition().duration(1000)
.attr("transform", function(d) {
return "translate(" + (d.x - d.r) + "," + (d.y - d.r) + ")";
})
.each("end", function(){
force.on("tick", tickDemers).resume();
});
node.selectAll("rect").transition().duration(1000)
.attr("rx", function(d){ return 0; })
.attr("width", function(d){ return d.r * 2; })
.attr("height", function(d){ return d.r * 2; })
node.selectAll("text").transition().duration(1000)
.attr("x", function (d) { return d.r; })
.attr("y", function (d) { return d.r; })
.style("font-size", function(d) { return (1.5 * d.r - 5); });
} else {
node.transition().duration(1000)
.attr("transform", function(d) {
return "translate(" + (d.x - d.r) + "," + (d.y - d.r) + ")";
})
.each("end", function(){
force.on("tick", tickDorling).resume();
});
node.selectAll("rect").transition().duration(1000)
.attr("rx", function(d){ return d.r; })
.attr("width", function(d){ return d.r * 2; })
.attr("height", function(d){ return d.r * 2; })
node.selectAll("text").transition().duration(1000)
.attr("x", function (d) { return d.r; })
.attr("y", function (d) { return d.r; })
.style("font-size", function(d) { return (1.5 * d.r - 5); });
};
}
);
d3.selectAll("input[type='checkbox']")
.on("change", function change() {
if(d3.select(this).property("checked") === true){
console.log("Show Map")
d3.selectAll(".states")
.classed("hidden", false)
} else {
console.log("Hide Map")
d3.selectAll(".states")
.classed("hidden", true)
};
}
);
};
</script>
https://d3js.org/d3.v3.min.js
https://d3js.org/topojson.v1.min.js
https://d3js.org/queue.v1.min.js
https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.10.0/d3-legend.min.js