const statesJson = {"AL":{"name":"Alabama","pop_2014":4849377,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":2,"W41":2,"W42":2,"W43":3,"W44":4,"W45":4,"W46":5,"W47":5,"W48":5,"W49":7,"W50":10,"W51":10,"W52":10},"AK":{"name":"Alaska","pop_2014":736732,"W1":6,"W2":6,"W3":6,"W4":8,"W5":10,"W6":10,"W40":1,"W41":2,"W42":4,"W43":3,"W44":2,"W45":2,"W46":4,"W47":4,"W48":5,"W49":6,"W50":5,"W51":6,"W52":7},"AZ":{"name":"Arizona","pop_2014":6731484,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":2,"W41":2,"W42":1,"W43":2,"W44":3,"W45":3,"W46":3,"W47":3,"W48":3,"W49":6,"W50":10,"W51":10,"W52":10},"AR":{"name":"Arkansas","pop_2014":2966369,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":3,"W47":1,"W48":4,"W49":8,"W50":10,"W51":10,"W52":10},"CA":{"name":"California","pop_2014":38802500,"W1":10,"W2":10,"W3":8,"W4":8,"W5":9,"W6":8,"W40":2,"W41":1,"W42":1,"W43":1,"W44":2,"W45":2,"W46":2,"W47":3,"W48":2,"W49":4,"W50":7,"W51":10,"W52":10},"CO":{"name":"Colorado","pop_2014":5355866,"W1":8,"W2":7,"W3":7,"W4":8,"W5":10,"W6":9,"W40":1,"W41":1,"W42":1,"W43":1,"W44":2,"W45":2,"W46":2,"W47":3,"W48":3,"W49":4,"W50":5,"W51":6,"W52":7},"CT":{"name":"Connecticut","pop_2014":3596677,"W1":4,"W2":5,"W3":9,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":2,"W49":2,"W50":3,"W51":2,"W52":3},"DC":{"name":"District of Columbia","pop_2014":658893,"W1":5,"W2":5,"W3":7,"W4":10,"W5":10,"W6":10,"W40":2,"W41":2,"W42":2,"W43":2,"W44":2,"W45":3,"W46":3,"W47":3,"W48":4,"W49":4,"W50":4,"W51":3,"W52":5},"DE":{"name":"Delaware","pop_2014":935614,"W1":1,"W2":1,"W3":2,"W4":5,"W5":8,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":1,"W51":1,"W52":1},"FL":{"name":"Florida","pop_2014":19893297,"W1":5,"W2":9,"W3":10,"W4":10,"W5":10,"W6":9,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":2,"W46":2,"W47":1,"W48":1,"W49":1,"W50":2,"W51":4,"W52":5},"GA":{"name":"Georgia","pop_2014":10097343,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":2,"W41":3,"W42":3,"W43":4,"W44":4,"W45":5,"W46":6,"W47":7,"W48":6,"W49":7,"W50":7,"W51":10,"W52":10},"HI":{"name":"Hawaii","pop_2014":1419561,"W1":5,"W2":8,"W3":7,"W4":8,"W5":6,"W6":5,"W40":1,"W41":1,"W42":1,"W43":1,"W44":2,"W45":3,"W46":3,"W47":3,"W48":5,"W49":5,"W50":3,"W51":6,"W52":7},"ID":{"name":"Idaho","pop_2014":1634464,"W1":5,"W2":7,"W3":6,"W4":5,"W5":6,"W6":4,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":2,"W49":1,"W50":1,"W51":3,"W52":1},"IL":{"name":"Illinois","pop_2014":12880580,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":2,"W49":3,"W50":6,"W51":10,"W52":10},"IN":{"name":"Indiana","pop_2014":6596855,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":2,"W45":1,"W46":1,"W47":1,"W48":3,"W49":4,"W50":3,"W51":9,"W52":10},"IA":{"name":"Iowa","pop_2014":3107126,"W1":5,"W2":8,"W3":9,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":2,"W50":2,"W51":3,"W52":8},"KS":{"name":"Kansas","pop_2014":2904021,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":2,"W48":1,"W49":3,"W50":7,"W51":10,"W52":10},"KY":{"name":"Kentucky","pop_2014":4413457,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":3,"W47":2,"W48":3,"W49":6,"W50":10,"W51":10,"W52":10},"LA":{"name":"Louisiana","pop_2014":4649676,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":3,"W41":3,"W42":4,"W43":5,"W44":7,"W45":7,"W46":8,"W47":10,"W48":10,"W49":10,"W50":10,"W51":10,"W52":10},"ME":{"name":"Maine","pop_2014":1330089,"W1":1,"W2":1,"W3":1,"W4":1,"W5":1,"W6":3,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":1,"W51":1,"W52":1},"MD":{"name":"Maryland","pop_2014":5976407,"W1":4,"W2":6,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":1,"W51":2,"W52":7},"MA":{"name":"Massachusetts","pop_2014":6745408,"W1":7,"W2":5,"W3":8,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":2,"W45":2,"W46":2,"W47":3,"W48":3,"W49":3,"W50":4,"W51":5,"W52":8},"MI":{"name":"Michigan","pop_2014":9909877,"W1":5,"W2":8,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":2,"W46":1,"W47":2,"W48":2,"W49":3,"W50":3,"W51":5,"W52":6},"MN":{"name":"Minnesota","pop_2014":5457173,"W1":7,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":2,"W50":2,"W51":4,"W52":5},"MS":{"name":"Mississippi","pop_2014":2994079,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":2,"W41":1,"W42":1,"W43":3,"W44":4,"W45":8,"W46":10,"W47":10,"W48":10,"W49":10,"W50":10,"W51":10,"W52":10},"MO":{"name":"Missouri","pop_2014":6063589,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":3,"W50":5,"W51":10,"W52":10},"MT":{"name":"Montana","pop_2014":1023579,"W1":1,"W2":2,"W3":2,"W4":1,"W5":2,"W6":1,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":1,"W51":1,"W52":1},"NE":{"name":"Nebraska","pop_2014":1881503,"W1":10,"W2":6,"W3":9,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":2,"W43":2,"W44":2,"W45":3,"W46":3,"W47":5,"W48":3,"W49":3,"W50":6,"W51":8,"W52":10},"NV":{"name":"Nevada","pop_2014":2839099,"W1":9,"W2":7,"W3":6,"W4":6,"W5":8,"W6":8,"W40":1,"W41":1,"W42":1,"W43":1,"W44":2,"W45":2,"W46":2,"W47":2,"W48":2,"W49":3,"W50":5,"W51":6,"W52":9},"NH":{"name":"New Hampshire","pop_2014":1326813,"W1":4,"W2":4,"W3":8,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":1,"W51":1,"W52":1},"NJ":{"name":"New Jersey","pop_2014":8938175,"W1":9,"W2":8,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":2,"W45":2,"W46":2,"W47":3,"W48":3,"W49":3,"W50":4,"W51":5,"W52":8},"NM":{"name":"New Mexico","pop_2014":2085572,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":2,"W46":3,"W47":3,"W48":1,"W49":3,"W50":5,"W51":7,"W52":10},"NY":{"name":"New York","pop_2014":19746227,"W1":7,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":2,"W51":5,"W52":4},"NC":{"name":"North Carolina","pop_2014":9943964,"W1":5,"W2":9,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":2,"W47":3,"W48":1,"W49":2,"W50":1,"W51":1,"W52":6},"ND":{"name":"North Dakota","pop_2014":739482,"W1":2,"W2":6,"W3":4,"W4":4,"W5":5,"W6":7,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":1,"W51":5,"W52":2},"OH":{"name":"Ohio","pop_2014":11594163,"W1":5,"W2":6,"W3":8,"W4":8,"W5":9,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":1,"W51":2,"W52":6},"OK":{"name":"Oklahoma","pop_2014":3878051,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":2,"W45":3,"W46":3,"W47":4,"W48":4,"W49":4,"W50":10,"W51":10,"W52":10},"OR":{"name":"Oregon","pop_2014":3970239,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":9,"W40":1,"W41":1,"W42":1,"W43":1,"W44":2,"W45":2,"W46":2,"W47":3,"W48":3,"W49":4,"W50":5,"W51":9,"W52":10},"PA":{"name":"Pennsylvania","pop_2014":12787209,"W1":6,"W2":7,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":2,"W45":2,"W46":2,"W47":2,"W48":2,"W49":2,"W50":3,"W51":3,"W52":6},"RI":{"name":"Rhode Island","pop_2014":1055173,"W1":5,"W2":7,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":3,"W50":2,"W51":3,"W52":2},"SC":{"name":"South Carolina","pop_2014":4832482,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":2,"W41":1,"W42":2,"W43":5,"W44":6,"W45":6,"W46":6,"W47":10,"W48":10,"W49":10,"W50":10,"W51":10,"W52":10},"SD":{"name":"South Dakota","pop_2014":853175,"W1":6,"W2":7,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":2,"W42":2,"W43":3,"W44":3,"W45":4,"W46":6,"W47":4,"W48":4,"W49":4,"W50":4,"W51":5,"W52":8},"TN":{"name":"Tennessee","pop_2014":6549352,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":2,"W51":9,"W52":7},"TX":{"name":"Texas","pop_2014":26956958,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":3,"W45":3,"W46":5,"W47":5,"W48":7,"W49":9,"W50":10,"W51":10,"W52":10},"UT":{"name":"Utah","pop_2014":2942902,"W1":3,"W2":4,"W3":5,"W4":3,"W5":4,"W6":6,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":1,"W51":1,"W52":4},"VT":{"name":"Vermont","pop_2014":626562,"W1":4,"W2":5,"W3":7,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":1,"W51":1,"W52":3},"VA":{"name":"Virginia","pop_2014":8326289,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":2,"W43":2,"W44":2,"W45":3,"W46":3,"W47":4,"W48":4,"W49":4,"W50":5,"W51":6,"W52":9},"WA":{"name":"Washington","pop_2014":7061530,"W1":10,"W2":10,"W3":8,"W4":9,"W5":7,"W6":5,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":1,"W50":1,"W51":1,"W52":10},"WV":{"name":"West Virginia","pop_2014":1850326,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":3,"W48":1,"W49":1,"W50":5,"W51":9,"W52":10},"WI":{"name":"Wisconsin","pop_2014":5757564,"W1":7,"W2":8,"W3":7,"W4":9,"W5":10,"W6":9,"W40":1,"W41":1,"W42":1,"W43":1,"W44":1,"W45":1,"W46":1,"W47":1,"W48":1,"W49":2,"W50":1,"W51":4,"W52":5},"WY":{"name":"Wyoming","pop_2014":584153,"W1":10,"W2":10,"W3":10,"W4":10,"W5":10,"W6":10,"W40":1,"W41":3,"W42":1,"W43":3,"W44":3,"W45":2,"W46":1,"W47":1,"W48":2,"W49":3,"W50":3,"W51":5,"W52":10}} const width = 600 const height = 400 const scale = 0.64 const colorScale = d3.scaleLinear().domain([0, 10]).range(['#feebe2', '#c51b8a']) function interpolateEachNode (node) { const circle = pseudocircle(node) const closestPoints = node.rings.slice(1).map(function (ring) { const i = d3.scan(circle.map(point => distance(point, ring.centroid))) return ring.map(() => circle[i]) }) const interpolator = d3.interpolateArray(node.rings, [circle, ...closestPoints]) node.interpolator = function (t) { const str = pathString(interpolator(t)) // Prevent some fill-rule flickering for MultiPolygons if (t > 0.99) { return str.split('Z')[0] + 'Z' } return str } } function displayTextNodes (g, show) { return () => { const display = show ? 'block' : 'none' g.selectAll('text').style('display', display) } } function pseudocircle (node) { return node.rings[0].map(point => { var angle = node.startingAngle - 2 * Math.PI * (point.along / node.perimeter) return [ Math.cos(angle) * node.r + node.x, Math.sin(angle) * node.r + node.y ] }) } function cleanUpGeometry (node) { node.rings = (node.geometry.type === 'Polygon' ? [node.geometry.coordinates] : node.geometry.coordinates) node.rings = node.rings.map(polygon => { polygon[0].area = d3.polygonArea(polygon[0]) polygon[0].centroid = d3.polygonCentroid(polygon[0]) return polygon[0] }) node.rings.sort((a, b) => b.area - a.area) node.perimeter = d3.polygonLength(node.rings[0]) // Optional step, but makes for more circular circles bisect(node.rings[0], node.perimeter / 72) node.rings[0].reduce(function (prev, point) { point.along = prev ? prev.along + distance(point, prev) : 0 node.perimeter = point.along return point }, null) node.startingAngle = Math.atan2(node.rings[0][0][1] - node.y0, node.rings[0][0][0] - node.x0) } function bisect (ring, maxSegmentLength) { for (var i = 0; i < ring.length; i++) { const a = ring[i] let b = i === ring.length - 1 ? ring[0] : ring[i + 1] while (distance(a, b) > maxSegmentLength) { b = midpoint(a, b) ring.splice(i + 1, 0, b) } } } function distance (a, b) { return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1])) } function midpoint (a, b) { return [a[0] + (b[0] - a[0]) * 0.5, a[1] + (b[1] - a[1]) * 0.5] } function pathString (d) { return (d.rings || d).map(ring => 'M' + ring.join('L') + 'Z').join(' ') } function applyLabelToNodes (g, nodes) { // apply state labels g.selectAll('g') .data(nodes) .enter() .append('text') .text(d => { if (d.r > 14) { return d.id } }) .attr('text-anchor', 'middle') .attr('y', d => d.y + 5) .attr('x', d => d.x) .attr('class', 'state-label') .style('display', 'none') } function updateNodesForWeek (val) { return (d, i) => { const state = statesJson[d.id] const colorNum = state[`W${val}`] return colorScale(colorNum) } } function createSimulation (nodes, links, width, height) { return d3.forceSimulation(nodes) .force('cx', d3.forceX().x(d => width / 2).strength(0.02)) .force('cy', d3.forceY().y(d => height / 2).strength(0.02)) .force('link', d3.forceLink(links).distance(d => d.distance)) .force('x', d3.forceX().x(d => d.x).strength(0.1)) .force('y', d3.forceY().y(d => d.y).strength(0.1)) .force('collide', d3.forceCollide().strength(0.8).radius(d => d.r + 1)) .stop() } function setupNodeData (node, i) { const centroid = d3.geoPath().centroid(node) const radius = d3.scaleSqrt().range([0, 75]).clamp(true) node.x0 = centroid[0] node.y0 = centroid[1] const state = statesJson[node.id] || {} node.stateName = state.name node.r = radius(state.pop_2014 / 35000000) cleanUpGeometry(node) } function updateWeekLabel (text) { return () => { d3.select('.week-label').text(text) } } function initMap(selector = '#cartogram') { d3.select(selector) .append('p') .attr('class', 'week-label') const svg = d3.select(selector).append('svg') .attr('width', width) .attr('height', height) .attr('id', 'map-svg') .attr('viewBox', `0 0 ${width} ${height}`) d3.json('us-topo.json', (err, us) => { if (err) { throw Error(err) } const neighbors = topojson.neighbors(us.objects.states.geometries) const nodes = topojson.feature(us, us.objects.states).features nodes.forEach(setupNodeData) const g = svg.append('g') .attr('transform', `scale(${scale}) translate(0, 0)`) .attr('id', 'states') const states = g.selectAll('g') .data(nodes) .enter() .append('path') .attr('d', pathString) .attr('fill', '#eee') d3.select(selector) .append('p') .text('Source: CDC') .attr('class', 'credit') simulate() function simulate () { nodes.forEach(node => { node.x = node.x0 node.y = node.y0 }) const links = d3.merge(neighbors.map(function (neighborSet, i) { return neighborSet.filter(j => nodes[j]).map(function (j) { return {source: i, target: j, distance: nodes[i].r + nodes[j].r + 3} }) })) const simulation = createSimulation(nodes, links, width, height) while (simulation.alpha() > 0.1) { simulation.tick() } nodes.forEach(interpolateEachNode) applyLabelToNodes(g, nodes) const transDelay = 600 const transDuration = 900 states.sort((a, b) => b.r - a.r) .transition().on('start', updateWeekLabel('Oct 7th, 2017')) .attr('fill', '#eee') .delay(transDelay).duration(transDuration) // change to circles .attrTween('d', node => node.interpolator) .on('end', displayTextNodes(g, true)) .transition() .delay(transDelay) .duration(transDuration) .attr('fill', updateNodesForWeek(40)) .transition().on('start', updateWeekLabel('Nov 4th, 2017')) .delay(transDelay) .duration(transDuration) .attr('fill', updateNodesForWeek(44)) .transition().on('start', updateWeekLabel('Dec 2nd, 2017')) .delay(transDelay) .duration(transDuration) .attr('fill', updateNodesForWeek(48)) .transition().on('start', updateWeekLabel('Dec 30th 2017')) .delay(transDelay) .duration(transDuration) .attr('fill', updateNodesForWeek(52)) .transition().on('start', updateWeekLabel('Jan 13, 2018')) .delay(transDelay) .duration(transDuration) .attr('fill', updateNodesForWeek(2)) .transition().on('start', updateWeekLabel('Feb 10, 2018')) .delay(transDelay) .duration(transDuration) .attr('fill', updateNodesForWeek(6)) .transition() .on('start', displayTextNodes(g, false)) .delay(transDelay) // back to states .attrTween('d', node => t => node.interpolator(1 - t)) .on('end', (d, i) => i || simulate()) } }) } initMap()