Public
Edited
May 4
Insert cell
Insert cell
voteData2024 = await FileAttachment("plus fips_virginia_2024_presidential_results_by_county.csv").csv(d => ({
county: d["County/City"].trim().toLowerCase(),
fips_code: d["FIPS Code"],
harris: +d["Kamala Harris Votes"],
trump: +d["Donald Trump Votes"],
totalVotes: +d["Total Votes"]
}));

Insert cell
voteData2020 = await
FileAttachment("plus fips_virginia_2020_presidential_results_biden_trump.csv").csv(d => ({
county: d["County/City"].trim().toLowerCase(),
fips_code: d["FIPS Code"],
biden: +d["Joe Biden Votes"],
trump: +d["Donald Trump Votes"],
totalVotes: +d["Total Votes"]
}))
Insert cell
virginiaCounties2024 = topojson.feature(us, us.objects.counties).features.filter(d => d.id.startsWith("51"));
Insert cell
function checkCityVersusCounty(county) {
let name = county.properties.name.trim().toLowerCase()
if (name == "fairfax") {
if (county.id == "51600") {
name = "fairfax city"
} else {
name = "fairfax"
}
} else if (name == "roanoke") {
if (county.id == "51770") {
name = "roanoke city"
} else {
name = "roanoke county"
}
} else if (name == "richmond") {
if (county.id == "51760") {
name = "richmond city"
} else {
name = "richmond county"
}
}
return name
}
Insert cell
adjustedCountyNames = {
const map = new Map();
for (const county of virginiaCounties2024) {
const adjustedName = checkCityVersusCounty(county);
const rawName = county.properties.name.trim().toLowerCase();
map.set(rawName, adjustedName);
}
return map;
}
Insert cell
virginiaCounties2024.forEach(county => {

let name = checkCityVersusCounty(county)
const voteInfo = voteData2024.find(d => d["County/City"].trim().toLowerCase() === name);
if (voteInfo) {
county.properties.votes = {
harris: +voteInfo["Kamala Harris Votes"],
trump: +voteInfo["Donald Trump Votes"],
total: +voteInfo["Total Votes"]
};
} else {
county.properties.votes = null
}
})
Insert cell
virginiaCounties2024
Insert cell
Plot.plot({
width: 1200,
height: 500,
axis: null,
marks: [
Plot.geo(virginia),
Plot.geo(virginiaCounties2024),
Plot.geo(virginiaCounties2024, {
fill: d => {
const v = d.properties.votes;
if (!v) return "#ccc"; // fallback if no data
return v.harris > v.trump ? "CornflowerBlue" : "FireBrick";
},
stroke: 'white'
})
]
})
Insert cell
import {us} from "@observablehq/us-geographic-data"
Insert cell
// Filter for Virginia (FIPS code "51")
virginia = topojson.feature(us, us.objects.states).features.filter(d => d.id === "51");
Insert cell
data = new Map(
virginiaCounties2024.map(d => [
d.id, // The county ID
[Math.random() * 1000000, Math.random() * 1000000] // Random values for two "years"
])
);
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// chart.update(year)
Insert cell
countyVotingData2024 = new Map(voteData2024.map(d => {
const id = d["FIPS"];
const kamala = +d["Kamala Harris Votes"];
const trump = +d["Donald Trump Votes"];
const total = +d["Total Votes"];
return [d["County/City"], [kamala / total, trump / total, total]];
}));
Insert cell
Insert cell
Insert cell
chart2 = {

// === Configuration ===
const width = 1000;
const height = 700;
const color = d3.scaleDiverging(d3.interpolateRdBu)
.domain([0.5, 0, -0.5]); // Democratic -> Neutral -> Republican

const sizeScale = d3.scaleSqrt()
.domain([0, 1]) // 0% -> 100% vote share
.range([0.5, 2]); // Scale for county size (voting power)

let currentYear = 2024; // Default starting year

// === Setup projection and path ===
const projection = d3.geoAlbers()
.fitExtent([[100, 50], [width - 50, height - 50]], { type: "FeatureCollection", features: virginiaCounties2024 });

const path = d3.geoPath().projection(projection);

// === Create SVG ===
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("style", "max-width: 100%; height: auto;");

// === Draw State and County Borders ===
svg.append("path")
.datum(topojson.mesh(us, us.objects.states))
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr("d", path);

svg.append("path")
.datum(
topojson.mesh(us, us.objects.counties, (a, b) =>
a !== b && a.id.startsWith("51") && b.id.startsWith("51")
)
)
.attr("fill", "none")
.attr("stroke", "#aaa")
.attr("stroke-width", 0.5)
.attr("d", path);

// === Prepare Voting Data Maps ===
const votingDataMap2020 = new Map(
voteData2020.map(d => [
d["FIPS Code"],
{
fips: d["FIPS Code"],
locality: d.Locality,
biden: +d["Joe Biden Votes"],
trump: +d["Donald Trump Votes"],
total: +d["Total Votes"]
}
])
);

const votingDataMap2024 = new Map(
voteData2024.map(d => [
d["FIPS Code"],
{
fips: d["FIPS Code"],
locality: d.Locality,
harris: +d["Kamala Harris Votes"],
trump: +d["Donald Trump Votes"],
total: +d["Total Votes"]
}
])
);

const totalVotesByYear = {
2020: d3.sum(voteData2020, d => +d["Total Votes"]),
2024: d3.sum(voteData2024, d => +d["Total Votes"])
};


// === Counties group ===
const countiesGroup = svg.append("g")
.attr("stroke", "#444")
.attr("stroke-width", 0.5);

const counties = countiesGroup.selectAll("path")
.data(virginiaCounties2024) // Initially 2024
.join("path")
.attr("d", path)
.attr("fill", d => getCountyColor(d, votingDataMap2024, 2024))
.attr("transform", d => getCountyTransform(d, votingDataMap2024, 2024))
.append("title")
.text(d => getCountyTooltip(d, votingDataMap2024, 2024));

// === Helper Functions ===

function getCountyKey(d) {
return d.id;
}

function getVotingData(d, votingDataMap) {
return votingDataMap.get(getCountyKey(d));
}

function getCountyColor(d, votingDataMap, year) {
const county = getVotingData(d, votingDataMap);
if (!county) return "#ccc";

const [demVotes, repVotes] = (year === 2020)
? [county.biden, county.trump]
: [county.harris, county.trump];

const diff = (repVotes - demVotes) / (demVotes + repVotes);
return color(diff);
}

function getCountyTransform(d, votingDataMap, year) {
const [x, y] = path.centroid(d);
const county = getVotingData(d, votingDataMap);

if (!county) return `translate(${x},${y})`; // If no data, return without scaling

// Determine the leading candidate and their votes based on the year
let leadingCandidateVotes = 0;
if (year === 2020) {
leadingCandidateVotes = county.biden > county.trump ? county.biden : county.trump; // For 2020, choose the leading candidate
} else if (year === 2024) {
leadingCandidateVotes = county.harris > county.trump ? county.harris : county.trump; // For 2024, choose the leading candidate
}
// Calculate the vote percentage of the leading candidate in the county
const percentage = leadingCandidateVotes / county.total;
// Apply the scaling based on the percentage of votes the leading candidate received
const scale = percentage; // Adjust scale as necessary
// Return the translation and scaling
return `translate(${x},${y}) scale(${scale}) translate(${-x},${-y})`;
}

function getCountyTooltip(d, votingDataMap, year) {
const county = getVotingData(d, votingDataMap);
if (!county) return `${d.properties.name}\nNo data`;

const [demVotes, repVotes, demName] = (year === 2020)
? [county.biden, county.trump, "Biden"]
: [county.harris, county.trump, "Harris"];

const diff = Math.abs(demVotes - repVotes) / (demVotes + repVotes) * 100;

// Who’s in front?
const isDemLeading = demVotes > repVotes;
const leaderName = isDemLeading ? demName : "Trump";
const leaderVotes = isDemLeading ? demVotes : repVotes;

// Build tooltip text
return [
`${county.locality}`,
`${leaderName} Lead: ${diff.toFixed(1)}%`,
`${leaderName} Votes: ${leaderVotes.toLocaleString()}`
].join("\n");
}

// === Update Function ===
function update(year) {
currentYear = year;
const votingDataMap = year === 2020 ? votingDataMap2020 : votingDataMap2024;

countiesGroup.selectAll("path")
.data(virginiaCounties2024)
.join("path")
.attr("d", path)
.attr("fill", d => getCountyColor(d, votingDataMap, year))
.attr("transform", d => getCountyTransform(d, votingDataMap, year))
.select("title")
.text(d => getCountyTooltip(d, votingDataMap, year));
}

// === Return ===
return Object.assign(svg.node(), {
update,
scales: { color }
});

}
Insert cell
chart2.update(year2)
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