Published
Edited
Oct 20, 2020
2 forks
17 stars
Insert cell
Insert cell
Insert cell
Insert cell
force = {
let svg = d3
.create("svg")
.attr("width", width)
.attr("height", height);

svg
.append("rect")
.attr("width", width)
.attr("height", height)
.attr("x", 0)
.attr("y", 0)
.attr("fill", "#111");

let nodeG = svg
.selectAll("g")
.data(simulation.nodes())
.join("g")
.attr("transform", d => {
return `translate(${d.x}, ${d.y})`;
});

let node = nodeG
.append("circle")
.attr("r", radius)
// .attr("transform", `translate(${width / 2},${height / 2})`)
// .attr("cx", d => d.x)
// .attr("cy", d => d.y)
.attr("fill", d => color(d.state));
// .attr("stroke", d => color(d.state))
// .attr("stroke-width", 0.5);

// nodeG
// .append("text")
// .text(d => {
// let c = cartogramByState.get(getState(d.state));
// // return d.state;
// return c.state_postal;
// })
// .style("font-size", 8)
// .style("font-family", "'Seymour One', sans-serif")
// .style("text-transform", "uppercase")
// .style("text-anchor", "middle")
// .attr("fill", "white")
// .attr("dx", 100);

simulation.on("tick", tick => {
// node.attr("cx", d => d.x).attr("cy", d => d.y);
nodeG.attr("transform", d => {
return `translate(${d.x}, ${d.y})`;
});
});

invalidation.then(() => simulation.stop());

return svg.node();
}
Insert cell
forceX = d => {
if (target == "centroid") {
return getCenter(d.state)[0];
} else if (target == "lean") {
let lean = leaningsByState.get(getState(d.state));
if (!lean) return width / 2;
if (lean.party == "D") {
return d3
.scaleLinear()
.domain(demExtent)
.range([width / 2, margin.left])(lean.value);
} else if (lean.party == "R") {
return d3
.scaleLinear()
.domain(repExtent)
.range([width / 2, width - margin.right])(lean.value);
}
} else if (target == "cartogram") {
let c = cartogramByState.get(getState(d.state));
return margin.left + x_column(c) + state_size;
} else if (target == "results2016") {
let c = cartogramByState.get(getState(d.state));
let r = results2016ByState.get(c.state_postal);
let lean = leaningsByState.get(getState(d.state));
if (!lean) return width / 2;
if (!r) return width / 2;
if (r.electoral_R) {
// return width - margin.right - width / 4;
return d3
.scaleLinear()
.domain(repExtent)
.range([width / 2 + width / 6, width - margin.right - width / 10])(
lean.value
);
} else {
// return margin.left + width / 4;
return d3
.scaleLinear()
.domain(demExtent)
.range([width / 2 - width / 6, margin.left + width / 10])(lean.value);
}
}
// grid
return d.tx;
}
Insert cell
forceY = d => {
if (target == "centroid") {
return getCenter(d.state)[1];
} else if (target == "lean") {
return getCenter(d.state)[1];
} else if (target == "cartogram") {
let c = cartogramByState.get(getState(d.state));
return margin.top + y_column(c) + state_size;
// return height / 2;
} else if (target == "results2016") {
return getCenter(d.state)[1];
}

return d.ty;
}
Insert cell
import { electoral_votes } from "@eyeseast/fivethirtyeight-2020-forecast-data"
Insert cell
electoral_votes
Insert cell
d3.sum(Array.from(electoral_votes.values()))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
md`force layout`
Insert cell
results2016 = FileAttachment("Table 2. Electoral & Pop Vote-Table 1.csv").csv()
Insert cell
results2016ByState = new Map(
results2016.map(d => [
d.STATE,
{
state_postal: d.STATE,
// TODO: deal with third parties
electoral_R: +d["ELECTORAL VOTE Trump (R)"].replace("**", ""),
electoral_D: +d["ELECTORAL VOTE Clinton (D)"].replace("**", "")
}
])
)
Insert cell
simulation.nodes().map(d => x_column(cartogramByState.get(getState(d.state))))
Insert cell
state_size = width / 14
Insert cell
cartogram_data
Insert cell
cartogram_data_mapped = cartogram_data.map(d => [d.state_full, d])
Insert cell
cartogramByState = new Map(cartogram_data_mapped)
Insert cell
import { cartogram_data, x_column, y_column, axisPosition } with {
state_size,
margin
} from "@aboutaaron/small-multiple-chart-cartogram"
Insert cell
margin = ({
top: 40,
left: 40,
right: 40,
bottom: 0
})
Insert cell
repExtent = d3.extent(
Array.from(leaningsByState).filter(d => d[1].party == "R"),
d => d[1].value
)
Insert cell
demExtent = d3.extent(
Array.from(leaningsByState).filter(d => d[1].party == "D"),
d => d[1].value
)
Insert cell
color = state => {
let d = leaningsByState.get(getState(state));
if (!d) return "#999"//console.log(state, getState(state), d);
if (d.party == "R") {
return repScale(d.value);
} else {
return demScale(d.value);
}
}
Insert cell
repScale = d3.scaleSequential(d3.interpolateReds).domain(repExtent)
Insert cell
demScale = d3.scaleSequential(d3.interpolateBlues).domain(demExtent)
Insert cell
leanings = d3.csv(
"https://raw.githubusercontent.com/fivethirtyeight/data/master/partisan-lean/fivethirtyeight_partisan_lean_STATES.csv"
)
Insert cell
leaningsByState = {
let processed = leanings.map(d => {
// let s = d.pvi_538;
let s = d[2020];
return [
d.state,
{
state: d.state,
party: s.slice(0, 1),
value: +s.slice(1)
}
];
});
return new Map(processed);
}
Insert cell
{
simulation
.force("x", d3.forceX(forceX).strength(0.08))
.force("y", d3.forceY(forceY).strength(0.08));
simulation.alphaDecay(0.02);
simulation.alphaTarget(0.6);
simulation.restart();
}
Insert cell
getCenter("Florida")
Insert cell
function getCenter(state) {
return centroidsByName.get(getState(state)).center;
}
Insert cell
function getState(state) {
let s = state;
if (state.indexOf("NE-") == 0) {
s = "Nebraska";
} else if (state.indexOf("ME-") == 0) {
s = "Maine";
}
return s
}
Insert cell
simulation.nodes()
Insert cell
d3.group(simulation.nodes(), d => d.state)
Insert cell
simulation = {
let processed_electoral_votes = stateUnits({
r: radius,
spacing,
columns
}).sort((a, b) => {
getCenter(a.state)[0] - getCenter(b.state)[0];
});
return (
d3
.forceSimulation(processed_electoral_votes)
// .force("x", d3.forceX(width / 2).strength(0.001))
// .force("y", d3.forceY(height / 2).strength(0.001))
.force('charge', d3.forceManyBody().strength(0.1))
.force('center', d3.forceCenter(width / 2, height / 2).strength(0.000001))
.force('collision', d3.forceCollide().radius(d => d.r * 0.9))
);
}
Insert cell
gridUnits(538)
Insert cell
function gridUnits(number, options = {}) {
let cols = options.columns || 10;
let r = options.r * 2 || 10;
let spacing = options.spacing || 0;
let array = d3.range(number).map(i => {
return {
i: i,
x: margin.left + (i % cols) * (r + spacing),
y: margin.top + Math.floor(i / cols) * (r + spacing)
};
});

return array;
}
Insert cell
function stateUnits(options) {
let cols = options.columns || 10;
let r = options.r * 2 || 10;
let spacing = options.spacing || 0;

let array = [];
let i = 0;

Array.from(electoral_votes).forEach(d => {
let state = d[0];
let count = d[1];
d3.range(count).forEach(j => {
let tx = margin.left + (i % cols) * (r + spacing);
let ty = margin.top + Math.floor(i / cols) * (r + spacing);
array.push({
i: i,
x: tx,
y: ty,
tx: tx,
ty: ty,
r: r,
state
});
i += 1;
});
});

return array;
}
Insert cell
us = d3.json("https://unpkg.com/us-atlas@3/counties-10m.json")
Insert cell
stateShapes = topojson.feature(us, us.objects.states)//.features
Insert cell
centroids = stateShapes.features.map(d => {
let center = turf.centroid(d).geometry.coordinates;
return {
fips: d.id,
name: d.properties.name,
center: projection(center)
};
})
Insert cell
centroidsByName = new Map(centroids.map(d => [d.name, d]))
Insert cell
projection = d3.geoAlbersUsa().fitSize([width, height], stateShapes)
Insert cell
height = 620
Insert cell
import { slider, select } from "@jashkenas/inputs"
Insert cell
d3 = require("d3")
Insert cell
turf = require("@turf/turf@5")
Insert cell
topojson = require("topojson-client")
Insert cell
html`<link href="https://fonts.googleapis.com/css2?family=Seymour+One&display=swap" rel="stylesheet">
`
Insert cell
import { twitch } from "@codingwithfire/twitch"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more