Block-a-Day #9. A dynamic Demers cartogram using force simulation, including yesterday's rectangular collision detection. Note that I am continuing to use the geographic position of each state to gently prod the map into a recognizable shape, so this may not be a strict Demers.
Data Sources: Census.
What I Learned: West Virginia turns out to be the most difficult state to get right. I ended up severing its link with Maryland to avoid it frequently popping to the east of Virginia (note if you intend to reuse the adjacency data for other purposes).
What I'd Do With More Time: Keep tinkering with the balance of the forces. If used for more than demonstration purposes, I'd run the simulation in the background and only show the output.
Just what it sounds like. For fifteen days, I will make a D3.js v4 block every single day. Rules:
forked from cmgiven's block: Demers Cartogram
xxxxxxxxxx
<meta charset="utf-8">
<style>
rect {
fill: #f3d439;
stroke: #000000;
stroke-width: 0.5px;
}
text {
font-family: sans-serif;
font-size: 11px;
fill: #333;
}
text.year {
font-size: 24px;
font-weight: 700;
fill: #555;
}
.links {
opacity: 0;
stroke: red;
}
.state text {
fill: black;
pointer-events: none;
}
</style>
<body>
<div id="lista_cands">
<select class="lista_c">
<option value="0">Selecionar candidato:</option>
</select>
</div>
<script src="//d3js.org/d3.v5.min.js"></script>
<script>
var width = 600
var height = 700
var interval = 2000
var maxSize = 140
var years = d3.range(1900, 2010 + 1, 10)
var yearIndex = -1
var year = years[0]
var projection = d3.geoAlbers()
.scale(width / 0.9)
var size = d3.scaleSqrt().range([0, maxSize])
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
var yearLabel = svg.append('text')
.attr('class', 'year')
.attr('x', width / 2)
.attr('y', 30)
.attr('text-anchor', 'middle')
var linkForce = d3.forceLink()
.id(function (d) { return d.sigla })
.distance(function (d) {
return (
size(d.source.ciro) +
size(d.target.ciro)
) / 2
})
.strength(0.6)
var collisionForce = rectCollide()
.size(function (d) {
var l = size(d.ciro)
return [l, l]
})
.iterations(1) // NORMAL: 10
var simulation = d3.forceSimulation()
.force('center', d3.forceCenter(width / 2, (height - maxSize) / 2))
.force('link', linkForce)
.force('collision', collisionForce)
.force('x', d3.forceX(function (d) { return d.xi }).strength(0.0125))
.force('y', d3.forceY(function (d) { return d.yi }).strength(0.0125))
const promises =
[d3.json('brasil-rede-ufs.json'),
d3.json('geral-brasil.json')];
Promise.all(promises)
.then((data) => {
var nodes = data[0].nodes
var links = data[0].links
var dados = data[1]
var apu = [{"UF":"AP","Bolsonaro":166935,"Haddad":134287,"Ciro":50553},
{"UF":"DF","Bolsonaro":936494,"Haddad":190508,"Ciro":266272},
{"UF":"ES","Bolsonaro":1122131,"Haddad":495868,"Ciro":195553},
{"UF":"GO","Bolsonaro":1868578,"Haddad":713292,"Ciro":280851},
{"UF":"MG","Bolsonaro":5293716,"Haddad":3021367,"Ciro":1276189},
{"UF":"MS","Bolsonaro":769116,"Haddad":333407,"Ciro":112296},
{"UF":"MT","Bolsonaro":980232,"Haddad":404029,"Ciro":91244},
{"UF":"PR","Bolsonaro":3495902,"Haddad":1210647,"Ciro":510457},
{"UF":"RO","Bolsonaro":538311,"Haddad":176107,"Ciro":52118},
{"UF":"RS","Bolsonaro":3351492,"Haddad":1452117,"Ciro":724015},
{"UF":"SC","Bolsonaro":2603665,"Haddad":598578,"Ciro":264312},
{"UF":"SP","Bolsonaro":12360744,"Haddad":3827399,"Ciro":2645694},
{"UF":"TO","Bolsonaro":337782,"Haddad":311212,"Ciro":54262},
{"UF":"AC","Bolsonaro":226457,"Haddad":69780,"Ciro":19285},
{"UF":"AL","Bolsonaro":525774,"Haddad":685942,"Ciro":154884},
{"UF":"AM","Bolsonaro":804029,"Haddad":738042,"Ciro":138536},
{"UF":"BA","Bolsonaro":1597140,"Haddad":4071665,"Ciro":645712},
{"UF":"CE","Bolsonaro":1058126,"Haddad":1598510,"Ciro":1987232},
{"UF":"MA","Bolsonaro":784539,"Haddad":1885474,"Ciro":269526},
{"UF":"PA","Bolsonaro":1491355,"Haddad":1684804,"Ciro":411894},
{"UF":"PB","Bolsonaro":674830,"Haddad":976866,"Ciro":361253},
{"UF":"PE","Bolsonaro":1443425,"Haddad":2296527,"Ciro":639772},
{"UF":"PI","Bolsonaro":1148702,"Haddad":344173,"Ciro":208932},
{"UF":"RJ","Bolsonaro":5069053,"Haddad":1247058,"Ciro":1291794},
{"UF":"RN","Bolsonaro":540300,"Haddad":733551,"Ciro":398441},
{"UF":"RR","Bolsonaro":167164,"Haddad":47701,"Ciro":14273},
{"UF":"SE","Bolsonaro":309306,"Haddad":569547,"Ciro":148162}]
console.log(apu)
/* Merge das bases
for (let i = 0; i < dados.ufs.length; i++) {
let siglaBase = dados.ufs[i].codAbr;
let cands = dados.ufs[i].candidatos;
let eleitr = dados.ufs[i].comp;
for (let j = 0; j < nodes.length; j++) {
let siglaNodes = nodes[j].sigla;
if (siglaBase === siglaNodes) {
nodes[j].cands = cands;
nodes[j].eleitorado = eleitr;
} else if (!nodes[j].hasOwnProperty('eleitorado')) {
nodes[j].eleitorado = 1;
}
}
} */
//Merge das bases
for (let i = 0; i < apu.length; i++) {
let siglaBase = apu[i].UF;
let bolsonaro = apu[i].Bolsonaro;
let haddad = apu[i].Haddad;
let ciro = apu[i].Ciro;
for (let j = 0; j < nodes.length; j++) {
let siglaNodes = nodes[j].sigla;
if (siglaBase === siglaNodes) {
nodes[j].bolsonaro = bolsonaro;
nodes[j].haddad = haddad;
nodes[j].ciro = ciro;
}
}
}
console.log(nodes)
size.domain([0, 12360744])
//size.domain([0, d3.max(nodes, function (d) {
// return d.bolsonaro})])
nodes.forEach(function (d) {
var coords = projection([d.lon, d.lat])
d.x = d.xi = coords[0]
d.y = d.yi = coords[1]
})
var states = svg.selectAll('.state')
.data(nodes)
.enter().append('g')
.attr('class', 'state')
states.append('rect')
states.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '.3em')
.text(function (d) { return d.sigla })
var linhas = svg.selectAll('.links')
.data(links).enter()
.append('line').attr('class', 'links')
simulation.nodes(nodes)
simulation.force('link').links(links)
simulation.on('tick', ticked)
update()
//d3.interval(update, interval)
function update() {
simulation.nodes(nodes).alpha(1).restart()
}
function ticked() {
var sizes = d3.local()
states
.property(sizes, function (d) {
return size(d.ciro)
})
.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')' })
states.selectAll('rect')
.attr('x', function (d) { return sizes.get(this) / -2 })
.attr('y', function (d) { return sizes.get(this) / -2 })
.attr('width', function (d) { return sizes.get(this) })
.attr('height', function (d) { return sizes.get(this) })
linhas
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
}
}
)
function rectCollide() {
var nodes, sizes, masses
var size = constant([0, 0])
var strength = 1
var iterations = 1
function force() {
var node, size, mass, xi, yi
var i = -1
while (++i < iterations) { iterate() }
function iterate() {
var j = -1
var tree = d3.quadtree(nodes, xCenter, yCenter).visitAfter(prepare)
while (++j < nodes.length) {
node = nodes[j]
size = sizes[j]
mass = masses[j]
xi = xCenter(node)
yi = yCenter(node)
tree.visit(apply)
}
}
function apply(quad, x0, y0, x1, y1) {
var data = quad.data
var xSize = (size[0] + quad.size[0]) / 2
var ySize = (size[1] + quad.size[1]) / 2
if (data) {
if (data.index <= node.index) { return }
var x = xi - xCenter(data)
var y = yi - yCenter(data)
var xd = Math.abs(x) - xSize
var yd = Math.abs(y) - ySize
if (xd < 0 && yd < 0) {
var l = Math.sqrt(x * x + y * y)
var m = masses[data.index] / (mass + masses[data.index])
if (Math.abs(xd) < Math.abs(yd)) {
node.vx -= (x *= xd / l * strength) * m
data.vx += x * (1 - m)
} else {
node.vy -= (y *= yd / l * strength) * m
data.vy += y * (1 - m)
}
}
}
return x0 > xi + xSize || y0 > yi + ySize ||
x1 < xi - xSize || y1 < yi - ySize
}
function prepare(quad) {
if (quad.data) {
quad.size = sizes[quad.data.index]
} else {
quad.size = [0, 0]
var i = -1
while (++i < 4) {
if (quad[i] && quad[i].size) {
quad.size[0] = Math.max(quad.size[0], quad[i].size[0])
quad.size[1] = Math.max(quad.size[1], quad[i].size[1])
}
}
}
}
}
function xCenter(d) { return d.x + d.vx }
function yCenter(d) { return d.y + d.vy }
force.initialize = function (_) {
sizes = (nodes = _).map(size)
masses = sizes.map(function (d) { return d[0] * d[1] })
}
force.size = function (_) {
return (arguments.length
? (size = typeof _ === 'function' ? _ : constant(_), force)
: size)
}
force.strength = function (_) {
return (arguments.length ? (strength = +_, force) : strength)
}
force.iterations = function (_) {
return (arguments.length ? (iterations = +_, force) : iterations)
}
return force
}
function constant(_) {
return function () { return _ }
}
</script>
</body>
https://d3js.org/d3.v5.min.js