plans = [
{ id: "2026", label: "2026 Enacted", category: "Enacted" },
{ id: "2024", label: "2024 Enacted", category: "Enacted" },
{ id: "2022", label: "2022 Enacted", category: "Enacted" },
{ id: "sim_most_democratic", label: "Most Democratic", category: "Simulated" },
{ id: "sim_q75", label: "75th Quantile", category: "Simulated" },
{ id: "sim_q50", label: "Median", category: "Simulated" },
{ id: "sim_q25", label: "25th Quantile", category: "Simulated" },
{ id: "sim_most_republican", label: "Most Republican", category: "Simulated" },
{ id: "optim_safe_max", label: "Democratic Safe Max", category: "Optimizations" },
{ id: "optim_safe_min", label: "Republican Safe Max", category: "Optimizations" },
{ id: "optim_max", label: "Democratic Max", category: "Optimizations" },
{ id: "optim_min", label: "Republican Max", category: "Optimizations" },
{ id: "county_splits", label: "County Splits", category: "Optimizations" },
]viewof selectedPlan = {
const el = html`<div class="plan-selector"></div>`;
let value = plans[0].id;
const categories = [...new Set(plans.map((p) => p.category))];
const wrapper = html`<div class="plan-groups"></div>`;
for (const cat of categories) {
const catPlans = plans.filter((p) => p.category === cat);
const group = html`<div class="plan-group"></div>`;
group.appendChild(html`<div class="plan-cat-label">${cat}</div>`);
const row = html`<div class="plan-btn-row"></div>`;
for (const plan of catPlans) {
const btn = html`<button class="plan-btn">${plan.label}</button>`;
if (plan.id === value) btn.classList.add("active");
btn.onclick = () => {
el.querySelectorAll(".plan-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
value = plan.id;
el.dispatchEvent(new Event("input"));
};
row.appendChild(btn);
}
group.appendChild(row);
wrapper.appendChild(group);
}
el.appendChild(wrapper);
Object.defineProperty(el, "value", { get: () => value });
return el;
}viewof selectedBaseline = {
const el = html`<div class="baseline-selector atlas-control-bar"></div>`;
let mode = "single";
let selected = new Set(["2024"]);
const row = html`<div class="baseline-btn-row"></div>`;
const modes = html`<div class="baseline-mode-row">
<button class="baseline-mode-toggle" aria-pressed="false">
<span class="active">Single</span>
<span>Multi</span>
</button>
</div>`;
const value = () => {
const ids = baselines.map((d) => d.id).filter((id) => selected.has(id));
return mode === "single" ? ids[0] : ids;
};
const dispatch = () => el.dispatchEvent(new Event("input"));
const renderButtons = () => {
row.replaceChildren(modes);
for (const baseline of baselines) {
const btn = html`<button class="baseline-btn">${baseline.label}</button>`;
if (selected.has(baseline.id)) btn.classList.add("active");
btn.onclick = () => {
if (mode === "single") {
selected = new Set([baseline.id]);
} else if (selected.has(baseline.id) && selected.size > 1) {
selected.delete(baseline.id);
} else {
selected.add(baseline.id);
}
renderButtons();
dispatch();
};
row.appendChild(btn);
}
};
const setMode = (nextMode) => {
mode = nextMode;
const btn = modes.querySelector(".baseline-mode-toggle");
btn.setAttribute("aria-pressed", mode === "multi" ? "true" : "false");
modes.querySelectorAll("span").forEach((span) => {
span.classList.toggle("active", span.textContent.toLowerCase() === mode);
});
if (mode === "single" && selected.size > 1) {
const selectedValues = Array.from(selected);
selected = new Set([selectedValues[selectedValues.length - 1]]);
}
renderButtons();
dispatch();
};
modes.querySelector(".baseline-mode-toggle").onclick = () => setMode(mode === "single" ? "multi" : "single");
renderButtons();
el.appendChild(html`<div class="baseline-label">Presidential baseline</div>`);
el.appendChild(row);
Object.defineProperty(el, "value", { get: value });
return el;
}scores = d3.csv(`data/scores/baselines/${selectedPlan}.csv`, d3.autoType)
baselineManifest = d3.csv("data/scores/baseline_manifest.csv", d3.autoType)
states = Array.from(new Set(scores.map((d) => d.state))).sort()
selectedBaselineIds = (Array.isArray(selectedBaseline) ? selectedBaseline : [selectedBaseline]).map(String)
selectedBaselineInfos = selectedBaselineIds.map((baseline) =>
baselineManifest.find((d) => String(d.baseline_year) === baseline)
)
baselineLabel = selectedBaselineIds.length === 1
? selectedBaselineIds[0]
: `${selectedBaselineIds.join(" + ")} average`
baselineNationalDemShare = d3.mean(selectedBaselineInfos, (d) => d.dem_share)viewof selectedEnvironment = {
let value = baselineMargin;
const valueLabel = html`<span class="swing-value">${formatSliderEnvironment(value)} national</span>`;
const input = Inputs.range(
[-10, 10],
{
step: 0.5,
value: baselineMargin,
format: formatSliderEnvironment,
width: 640
}
);
const view = html`<div class="swing-control atlas-control-bar">
<div class="swing-control-row">
<div class="swing-label">National environment</div>
<div class="swing-input-wrap">
<div class="swing-input">${input}</div>
<div class="swing-tick-marks" aria-hidden="true">
${Array.from({ length: 21 }, (_, i) => html`<span class="${i % 5 === 0 ? "major" : ""}"></span>`)}
</div>
<div class="swing-ticks" aria-hidden="true">
<span>D+10</span>
<span>D+5</span>
<span>Even</span>
<span>R+5</span>
<span>R+10</span>
</div>
<div class="swing-meta">
<span class="swing-baseline-note">${baselineLabel} baseline: ${formatSliderEnvironment(baselineMargin)}</span>
${valueLabel}
</div>
</div>
</div>
</div>`;
Object.defineProperty(view, "value", { get: () => value });
input.addEventListener("input", () => {
value = Number(input.value);
valueLabel.textContent = `${formatSliderEnvironment(value)} national`;
view.dispatchEvent(new Event("input", { bubbles: true }));
});
return view;
}viewof selectedState = {
const form = html`<label class="state-picker">
<span>View</span>
<select></select>
</label>`;
const sel = form.querySelector("select");
sel.appendChild(new Option("US", "US"));
Object.defineProperty(form, "value", { get: () => sel.value });
sel.addEventListener("change", () => form.dispatchEvent(new Event("input", { bubbles: true })));
return form;
}viewof selectedColorPalette = {
const el = html`<div class="color-palette-selector"></div>`;
let value = colorPalettes[0].id;
const btn = html`<button class="baseline-mode-toggle color-palette-toggle" aria-pressed="false">
${colorPalettes.map((palette) => html`<span class="${palette.id === value ? "active" : ""}" data-palette="${palette.id}">${palette.label}</span>`)}
</button>`;
btn.onclick = () => {
value = value === colorPalettes[0].id ? colorPalettes[1].id : colorPalettes[0].id;
btn.setAttribute("aria-pressed", value === colorPalettes[1].id ? "true" : "false");
btn.querySelectorAll("span").forEach((span) => {
span.classList.toggle("active", span.dataset.palette === value);
});
el.dispatchEvent(new Event("input"));
};
el.appendChild(btn);
Object.defineProperty(el, "value", { get: () => value });
return el;
}{
const form = viewof selectedState;
const sel = form.querySelector("select");
const prev = sel.value;
const opts = ["US", ...states];
while (sel.options.length) sel.remove(0);
for (const s of opts) sel.appendChild(new Option(s, s));
sel.value = opts.includes(prev) ? prev : "US";
if (sel.value !== prev) form.dispatchEvent(new Event("input", { bubbles: true }));
}clampShare = (value) => Math.max(0, Math.min(1, value))
meanBaselineField = (d, field) => d3.mean(selectedBaselineIds, (baseline) => d[`pres_${baseline}_${field}`])
shiftScore = (d) => {
const baseline_dem_share = meanBaselineField(d, "dem_share");
const baseline_total_votes = meanBaselineField(d, "total_votes");
const dem_share = clampShare(baseline_dem_share + demSwing);
return {
...d,
baseline_total_votes,
dem_share,
rep_share: 1 - dem_share
};
}
shiftedScores = scores.map(shiftScore)
selectedScores = (selectedState === "US" ? shiftedScores : shiftedScores.filter((d) => d.state === selectedState))
.slice()
.sort((a, b) => d3.ascending(a.state, b.state) || d3.ascending(a.district, b.district))
allPlanScores = Promise.all(
plans.map((plan) =>
d3.csv(`data/scores/baselines/${plan.id}.csv`, d3.autoType).then((rows) => ({
...plan,
scores: rows.map(shiftScore)
}))
)
)
selectedShapes = selectedState === "US"
? Promise.all(states.map((state) => d3.json(`data/shp/${state}/${selectedPlan}.geojson`)))
: d3.json(`data/shp/${selectedState}/${selectedPlan}.geojson`).then((shape) => [shape])
rewindGeometry = (geometry) => {
if (geometry.type === "Polygon") {
return {
...geometry,
coordinates: geometry.coordinates.map((ring) => ring.slice().reverse())
}
}
if (geometry.type === "MultiPolygon") {
return {
...geometry,
coordinates: geometry.coordinates.map((polygon) =>
polygon.map((ring) => ring.slice().reverse())
)
}
}
return geometry
}
rewindFeature = (feature) => ({
...feature,
geometry: rewindGeometry(feature.geometry)
})
districtShape = ({
type: "FeatureCollection",
features: selectedShapes.flatMap((shape) => shape.features).map(rewindFeature)
})
scoreByDistrict = new Map(selectedScores.map((d) => [`${d.state}-${d.district}`, d]))
districtFeatures = districtShape.features.map((feature) => ({
...feature,
properties: {
...feature.properties,
...scoreByDistrict.get(`${feature.properties.state}-${feature.properties.district}`)
}
}))
joinedDistrictShape = ({
type: "FeatureCollection",
features: districtFeatures
})
formatPercent = d3.format(".1%")
formatWholePercent = d3.format(".0%")
formatSignedPoints = (value) => `${value >= 0 ? "+" : "-"}${Math.abs(value * 100).toFixed(1)} pts`
formatNationalEnvironment = (demShare) => {
const margin = demShare * 2 - 1;
if (Math.abs(margin) < 0.0005) return "an even national environment";
return `a ${margin > 0 ? "D" : "R"}+${Math.abs(margin * 100).toFixed(1)} national environment`;
}
formatPartisanLean = (demShare) => {
const margin = demShare * 2 - 1;
if (Math.abs(margin) < 0.0005) return "even";
return `${margin > 0 ? "D" : "R"}+${Math.abs(margin * 100).toFixed(1)}`;
}
districtColorRanges = ({
neutral: ["#A0442C", "#B25D4C", "#C27568", "#D18E84", "#DFA8A0", "#EBC2BC", "#F6DCD9", "#F9F9F9", "#DAE2F4", "#BDCCEA", "#9FB6DE", "#82A0D2", "#638BC6", "#3D77BB", "#0063B1"],
purple: ["#A0442C", "#B04B39", "#BF584B", "#CD6A61", "#D77F7A", "#E19A97", "#DAA0CF", "#C03ABE", "#9B95E0", "#8DA0D7", "#778FCF", "#607FC6", "#476FBD", "#2E69B7", "#0063B1"]
})
districtColor = d3.scaleThreshold()
.domain([0.24, 0.28, 0.32, 0.36, 0.40, 0.44, 0.48, 0.52, 0.56, 0.60, 0.64, 0.68, 0.72, 0.76])
.range(districtColorRanges[selectedColorPalette])
contrastTextColor = (backgroundColor) => {
const color = d3.color(backgroundColor);
if (!color) return "#222";
const luminance = 0.2126 * (color.r / 255) + 0.7152 * (color.g / 255) + 0.0722 * (color.b / 255);
return luminance > 0.45 ? "#222" : "#fff";
}
ratingMargins = ({
tossUp: 0.025,
likely: 0.10
})
partyBucket = (demShare) => {
const margin = demShare * 2 - 1;
if (margin >= ratingMargins.likely) return "likely_dem";
if (margin >= ratingMargins.tossUp) return "lean_dem";
if (margin >= 0) return "toss_up_dem";
if (margin > -ratingMargins.tossUp) return "toss_up_rep";
if (margin > -ratingMargins.likely) return "lean_rep";
return "likely_rep";
}
overview = ({
districts: selectedScores.length,
demSeats: d3.sum(selectedScores, (d) => d.dem_share > 0.5),
repSeats: d3.sum(selectedScores, (d) => d.dem_share <= 0.5),
likelyDemSeats: d3.sum(selectedScores, (d) => partyBucket(d.dem_share) === "likely_dem"),
leanDemSeats: d3.sum(selectedScores, (d) => partyBucket(d.dem_share) === "lean_dem"),
tossUpDemSeats: d3.sum(selectedScores, (d) => partyBucket(d.dem_share) === "toss_up_dem"),
tossUpRepSeats: d3.sum(selectedScores, (d) => partyBucket(d.dem_share) === "toss_up_rep"),
leanRepSeats: d3.sum(selectedScores, (d) => partyBucket(d.dem_share) === "lean_rep"),
likelyRepSeats: d3.sum(selectedScores, (d) => partyBucket(d.dem_share) === "likely_rep"),
tossUpSeats: d3.sum(selectedScores, (d) => partyBucket(d.dem_share).startsWith("toss_up")),
meanDemShare: d3.mean(selectedScores, (d) => d.dem_share)
})
stateSummary = Array.from(
d3.rollup(
shiftedScores,
(rows) => ({
state: rows[0].state,
districts: rows.length,
demSeats: d3.sum(rows, (d) => d.dem_share > 0.5),
repSeats: d3.sum(rows, (d) => d.dem_share <= 0.5),
tossUpSeats: d3.sum(rows, (d) => partyBucket(d.dem_share).startsWith("toss_up")),
meanDemShare: d3.mean(rows, (d) => d.dem_share)
}),
(d) => d.state
).values()
).sort((a, b) =>
d3.descending(a.tossUpSeats, b.tossUpSeats) ||
d3.descending(a.districts, b.districts) ||
d3.ascending(a.state, b.state)
)
closeDistricts = selectedScores
.slice()
.sort((a, b) =>
d3.ascending(Math.abs(a.dem_share - 0.5), Math.abs(b.dem_share - 0.5)) ||
d3.ascending(a.state, b.state) ||
d3.ascending(a.district, b.district)
)
.slice(0, 10)
selectedPlanLabel = plans.find((plan) => plan.id === selectedPlan)?.label ?? selectedPlan
selectedPlanSourceNote = selectedPlan === "county_splits"
? "District shapes from Shahmizad and Buchanan 2025."
: selectedPlan.startsWith("optim_")
? "Prototype seat-maximizing optimization generated by Christopher T. Kenny using redist."
: null
baselineSourceText = selectedBaselineIds.length === 1
? `Source: ${selectedBaselineIds[0]} presidential baseline from ${selectedBaselineInfos[0].source_label}.`
: `Sources: ${selectedBaselineInfos.map((d) => `${d.baseline_year} presidential baseline from ${d.source_label}`).join("; ")}.`
baselineSourceNote = () => html`<div class="map-source-note">
${baselineSourceText}
</div>`
mapSourceNote = () => html`<div class="map-source-note">
${baselineSourceText}
${selectedPlanSourceNote ? html`<span>${selectedPlanSourceNote}</span>` : ""}
</div>`
selectedNationalDemShare = clampShare(baselineNationalDemShare + demSwing)
majorityDistrict = selectedScores.length >= 218
? selectedScores.slice().sort((a, b) =>
d3.descending(a.dem_share, b.dem_share) ||
d3.ascending(a.state, b.state) ||
d3.ascending(a.district, b.district)
)[217]
: null
districtVoteBins = {
const step = 0.05;
const thresholds = d3.range(0.05, 1.0001, step);
const bins = d3.bin()
.domain([0, 1])
.thresholds(thresholds)
.value((d) => d.rep_share)(selectedScores);
return bins.map((bin) => ({
x0: bin.x0,
x1: bin.x1,
xMid: (bin.x0 + bin.x1) / 2,
count: bin.length
}));
}(() => {
const mapWidth = Math.min(width, 900);
const mapHeight = selectedState === "US"
? Math.max(420, Math.min(620, mapWidth * 0.64))
: Math.max(300, Math.min(520, mapWidth * 0.64));
const projection = selectedState === "US"
? d3.geoAlbersUsa()
.scale(mapWidth / 960 * 1070)
.translate([mapWidth / 2, mapHeight / 2])
: d3.geoMercator().fitSize([mapWidth, mapHeight], joinedDistrictShape);
const ringPath = (ring) => {
const points = ring.map((point) => projection(point)).filter((point) => point);
const xExtent = d3.extent(points, (point) => point[0]);
const yExtent = d3.extent(points, (point) => point[1]);
const ringWidth = xExtent[1] - xExtent[0];
const ringHeight = yExtent[1] - yExtent[0];
if (selectedState === "US" && points.length <= 5 && ringWidth > 60 && ringHeight > 40) {
return "";
}
return points.length ? `M${points.map((point) => point.join(",")).join("L")}Z` : "";
};
const geometryPath = (geometry) => {
if (geometry.type === "Polygon") {
return geometry.coordinates.map(ringPath).join("");
}
if (geometry.type === "MultiPolygon") {
return geometry.coordinates
.flatMap((polygon) => polygon.map(ringPath))
.join("");
}
return "";
};
const stateStats = new Map(
Array.from(
d3.rollup(
districtFeatures,
(rows) => ({
districts: rows.length,
dem: rows.filter((d) => ["likely_dem", "lean_dem"].includes(partyBucket(d.properties.dem_share))).length,
competitive: rows.filter((d) => partyBucket(d.properties.dem_share).startsWith("toss_up")).length,
rep: rows.filter((d) => ["lean_rep", "likely_rep"].includes(partyBucket(d.properties.dem_share))).length,
}),
(d) => d.properties.state
)
)
);
const container = d3.create("div").style("position", "relative");
const tooltip = container.append("div").attr("class", "map-tooltip").style("display", "none");
const svg = d3.create("svg")
.attr("viewBox", [0, 0, mapWidth, mapHeight])
.attr("width", mapWidth)
.attr("height", mapHeight)
.attr("class", "atlas-map")
.attr("role", "img")
.attr("aria-label", `${selectedState === "US" ? "United States" : selectedState} congressional district map`);
svg.selectAll("path.district")
.data(districtFeatures)
.join("path")
.attr("class", "district")
.attr("d", (d) => geometryPath(d.geometry))
.attr("fill", (d) => districtColor(d.properties.dem_share))
.attr("fill-rule", "evenodd")
.attr("stroke", "white")
.attr("stroke-width", 0.4)
.on("mouseover", (event, d) => {
const state = d.properties.state;
const stats = stateStats.get(state);
const stateLabel = stats.districts === 1 ? "1 at-large" : `${stats.districts} districts`;
const districtId = d.properties.district_id || `${state}-${d.properties.district}`;
const demShare = d.properties.dem_share;
const pillColor = districtColor(demShare);
const pillTextColor = contrastTextColor(pillColor);
tooltip
.style("display", "block")
.html(`
<div class="tooltip-district-row">
<span class="tooltip-district-name">${districtId}</span>
<span class="tooltip-dem-pill" style="background:${pillColor};color:${pillTextColor}">${formatPercent(demShare)} Dem</span>
</div>
<div class="tooltip-state-row">
<span class="tooltip-state-label">${state} · ${stateLabel}</span>
<span class="tooltip-seat-split">${stats.dem}D-${stats.competitive}C-${stats.rep}R</span>
</div>
`);
})
.on("mousemove", (event) => {
const tipWidth = 200;
const x = event.clientX + 14 + tipWidth > window.innerWidth
? event.clientX - tipWidth - 14
: event.clientX + 14;
tooltip.style("left", x + "px").style("top", (event.clientY - 16) + "px");
})
.on("mouseout", () => tooltip.style("display", "none"));
container.node().appendChild(svg.node());
container.node().appendChild(mapSourceNote());
return container.node();
})()html`<section class="atlas-summary">
<div class="summary-lede">
<p>
With ${formatNationalEnvironment(selectedNationalDemShare)}, the ${selectedPlanLabel} U.S. House map has
<strong>${overview.demSeats}</strong> Democratic-majority districts,
<strong>${overview.repSeats}</strong> Republican-majority districts, and
<strong>${overview.tossUpSeats}</strong> toss-up districts within 2.5 points of an even presidential margin.
${majorityDistrict
? html`The 218th most-Democratic district is <strong>${majorityDistrict.district_id}</strong> at <strong>${formatPartisanLean(majorityDistrict.dem_share)}</strong>.`
: ""}
</p>
</div>
</section>`(() => {
const margin = { top: 14, right: 16, bottom: 56, left: 58 };
const chartWidth = Math.min(width, 860);
const chartHeight = selectedState === "US" ? 390 : 320;
const innerWidth = chartWidth - margin.left - margin.right;
const innerHeight = chartHeight - margin.top - margin.bottom;
const maxCount = d3.max(districtVoteBins, (d) => d.count) ?? 0;
const yCeiling = Math.max(1, Math.ceil(maxCount / 5) * 5);
const x = d3.scaleLinear()
.domain([0, 1])
.range([0, innerWidth]);
const y = d3.scaleLinear()
.domain([0, yCeiling])
.nice()
.range([innerHeight, 0]);
const svg = d3.create("svg")
.attr("viewBox", [0, 0, chartWidth, chartHeight])
.attr("width", chartWidth)
.attr("height", chartHeight)
.attr("class", "summary-histogram")
.attr("role", "img")
.attr("aria-label", `Histogram of average Democratic district vote share for ${selectedState === "US" ? "the United States" : selectedState}`);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
g.append("g")
.attr("class", "hist-grid")
.call(
d3.axisLeft(y)
.ticks(selectedState === "US" ? 5 : 4)
.tickSize(-innerWidth)
.tickFormat("")
)
.call((axis) => axis.select(".domain").remove());
g.append("g")
.attr("class", "hist-bars")
.selectAll("rect")
.data(districtVoteBins)
.join("rect")
.attr("x", (d) => x(d.x0) + 1)
.attr("y", (d) => y(d.count))
.attr("width", (d) => Math.max(0, x(d.x1) - x(d.x0) - 2))
.attr("height", (d) => innerHeight - y(d.count))
.attr("fill", (d) => districtColor(1 - d.xMid))
.attr("stroke", "#000000aa")
.attr("stroke-width", 0.6)
.append("title")
.text((d) => `${d.count} districts from ${formatWholePercent(d.x0)} to ${formatWholePercent(d.x1)} Republican vote share`);
g.append("g")
.attr("class", "hist-axis")
.attr("transform", `translate(0,${innerHeight})`)
.call(
d3.axisBottom(x)
.tickValues(d3.range(0, 1.01, 0.1))
.tickFormat(formatWholePercent)
.tickSizeOuter(0)
);
g.append("g")
.attr("class", "hist-axis")
.call(
d3.axisLeft(y)
.ticks(selectedState === "US" ? 5 : 4)
.tickSizeOuter(0)
);
svg.append("text")
.attr("class", "hist-axis-label")
.attr("x", margin.left + innerWidth / 2)
.attr("y", chartHeight - 12)
.attr("text-anchor", "middle")
.text("Average Republican district vote share");
svg.append("text")
.attr("class", "hist-axis-label")
.attr("transform", `translate(17,${margin.top + innerHeight / 2}) rotate(-90)`)
.attr("text-anchor", "middle")
.text("Number of seats");
const figure = html`<figure class="summary-figure">
${svg.node()}
<figcaption>
The histogram shows the distribution of expected district vote shares after the selected uniform swing.
Bars are colored by the Republican share of the district vote.
</figcaption>
${baselineSourceNote()}
</figure>`;
return figure;
})()html`<section class="atlas-summary">
<div class="summary-grid">
<div class="metric">
<div class="metric-value">${overview.likelyDemSeats}</div>
<div class="metric-label">Likely Dem.</div>
</div>
<div class="metric">
<div class="metric-value">${overview.leanDemSeats}</div>
<div class="metric-label">Lean Dem.</div>
</div>
<div class="metric">
<div class="metric-value">${overview.tossUpDemSeats}</div>
<div class="metric-label">Toss-up Dem.</div>
</div>
<div class="metric">
<div class="metric-value">${overview.tossUpRepSeats}</div>
<div class="metric-label">Toss-up Rep.</div>
</div>
<div class="metric">
<div class="metric-value">${overview.leanRepSeats}</div>
<div class="metric-label">Lean Rep.</div>
</div>
<div class="metric">
<div class="metric-value">${overview.likelyRepSeats}</div>
<div class="metric-label">Likely Rep.</div>
</div>
<div class="metric">
<div class="metric-value">${formatPercent(overview.meanDemShare)}</div>
<div class="metric-label">Mean Democratic share</div>
</div>
</div>
</section>`District partisanship by plan
Each square shows a district in the corresponding plan. A small dot is shown for the 218th seat.
(() => {
const sortedPlans = allPlanScores.map((plan) => ({
...plan,
scores: plan.scores
.slice()
.sort((a, b) =>
d3.descending(a.dem_share, b.dem_share) ||
d3.ascending(a.state, b.state) ||
d3.ascending(a.district, b.district)
)
}));
const columns = 87;
const rows = 5;
const gap = width < 640 ? 0.5 : 1;
const labelWidth = width < 700 ? 0 : 150;
const seatSplitWidth = width < 700 ? 0 : 90;
const rowGap = width < 700 ? 0.55 : 0.65;
const heatmapWidth = Math.max(260, Math.min(980, width - labelWidth - seatSplitWidth - 24));
const squareSize = (heatmapWidth - gap * (columns - 1)) / columns;
const heatmapHeight = squareSize * rows + gap * (rows - 1);
const headerHeight = squareSize < 5 ? 18 : 26;
const squarePosition = (i) => {
const column = Math.floor(i / rows);
const offset = i % rows;
const row = column % 2 === 0 ? offset : rows - 1 - offset;
return {
x: column * (squareSize + gap),
y: headerHeight + row * (squareSize + gap)
};
};
const planStats = (d) => {
const nDem = d.scores.filter(s => {
const b = partyBucket(s.dem_share);
return b === "likely_dem" || b === "lean_dem";
}).length;
const nComp = d.scores.filter(s => {
const b = partyBucket(s.dem_share);
return b === "toss_up_dem" || b === "toss_up_rep";
}).length;
const nRep = d.scores.filter(s => {
const b = partyBucket(s.dem_share);
return b === "lean_rep" || b === "likely_rep";
}).length;
const expDem = d3.sum(d.scores, s => pr_win_dem(s.dem_share));
return { nDem, nComp, nRep, expDem };
};
const container = d3.create("section").attr("class", "plan-heatmaps");
const tooltip = container.append("div")
.attr("class", "heatmap-tooltip")
.style("display", "none");
container.append("div")
.attr("class", "plan-heatmap-col-header")
.html(`
<div></div>
<div class="heatmap-zone-legend">
<span style="color:#3D77BB">Usually Dem.</span>
<span style="color:#9B59B6">Competitive</span>
<span style="color:#B25D4C">Usually Rep.</span>
</div>
<div class="heatmap-split-header">Expected seats</div>
`);
const planRows = container.selectAll("div.plan-heatmap-row")
.data(sortedPlans)
.join("div")
.attr("class", "plan-heatmap-row");
planRows.append("div")
.attr("class", "plan-heatmap-label")
.text((d) => d.label);
const svg = planRows.append("svg")
.attr("viewBox", [0, 0, heatmapWidth, heatmapHeight + headerHeight])
.attr("width", heatmapWidth)
.attr("height", heatmapHeight + headerHeight)
.attr("class", "plan-heatmap")
.attr("role", "img")
.attr("aria-label", (d) => `${d.label} district heatmap`);
svg.append("rect")
.attr("class", "plan-heatmap-background")
.attr("x", 0)
.attr("y", headerHeight)
.attr("width", heatmapWidth)
.attr("height", heatmapHeight)
.attr("rx", 2)
.attr("ry", 2);
// Zone count labels in the header area above each heatmap
const fontSize = Math.max(9, Math.min(13, squareSize * 1.3));
const zoneCountMidY = headerHeight / 2;
svg.each(function(d) {
const stats = planStats(d);
const g = d3.select(this).append("g").attr("class", "heatmap-zone-counts");
const zones = [
{ count: stats.nDem, start: 0, end: stats.nDem - 1, color: "#3D77BB" },
{ count: stats.nComp, start: stats.nDem, end: stats.nDem + stats.nComp - 1, color: "#9B59B6" },
{ count: stats.nRep, start: stats.nDem + stats.nComp, end: stats.nDem + stats.nComp + stats.nRep - 1, color: "#B25D4C" },
];
zones.forEach(z => {
if (z.count === 0) return;
const xLeft = Math.floor(z.start / rows) * (squareSize + gap);
const xRight = Math.floor(z.end / rows) * (squareSize + gap) + squareSize;
const xCenter = (xLeft + xRight) / 2;
g.append("text")
.attr("x", xCenter)
.attr("y", zoneCountMidY)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", z.color)
.attr("font-size", fontSize)
.attr("font-weight", "700")
.attr("font-family", "inherit")
.text(z.count);
});
});
svg.selectAll("rect.plan-heatmap-square")
.data((d) => d.scores.map((score, i) => ({ ...score, plan_label: d.label, index: i })))
.join("rect")
.attr("class", "plan-heatmap-square")
.attr("x", (d) => squarePosition(d.index).x)
.attr("y", (d) => squarePosition(d.index).y)
.attr("width", squareSize)
.attr("height", squareSize)
.attr("fill", (d) => districtColor(d.dem_share))
.attr("stroke", "none")
.on("mouseover", (event, d) => {
const pillColor = districtColor(d.dem_share);
const pillTextColor = contrastTextColor(pillColor);
tooltip
.style("display", "block")
.html(`
<div class="tooltip-district-row">
<span class="tooltip-district-name">${d.district_id}</span>
<span class="tooltip-dem-pill" style="background:${pillColor};color:${pillTextColor}">${formatPercent(d.dem_share)} Dem</span>
</div>
<div class="tooltip-state-row">
<span class="tooltip-state-label">${d.plan_label}</span>
<span class="tooltip-seat-split">${formatPartisanLean(d.dem_share)}</span>
</div>
`);
})
.on("mousemove", (event) => {
const tipWidth = 190;
const x = event.clientX + 14 + tipWidth > window.innerWidth
? event.clientX - tipWidth - 14
: event.clientX + 14;
tooltip.style("left", x + "px").style("top", (event.clientY - 16) + "px");
})
.on("mouseout", () => tooltip.style("display", "none"))
.append("title")
.text((d) => `${d.district_id}: ${formatPercent(d.dem_share)} Democratic share`);
svg.append("circle")
.attr("class", "plan-heatmap-majority-marker")
.attr("cx", () => squarePosition(217).x + squareSize / 2)
.attr("cy", () => squarePosition(217).y + squareSize / 2)
.attr("r", Math.max(1.5, squareSize * 0.22))
.append("title")
.text((d) => {
const seat = d.scores[217];
return `218th seat: ${seat.district_id}, ${formatPercent(seat.dem_share)} Democratic share`;
});
// Expected seat split column
planRows.append("div")
.attr("class", "heatmap-seat-split")
.html(d => {
const { expDem } = planStats(d);
const expRep = 435 - expDem;
return `
<div class="seat-split-row seat-split-dem">
<span class="seat-split-num">${expDem.toFixed(1)}</span>
<span class="seat-split-lbl">Dem.</span>
</div>
<div class="seat-split-row seat-split-rep">
<span class="seat-split-num">${expRep.toFixed(1)}</span>
<span class="seat-split-lbl">GOP</span>
</div>`;
});
container.selectAll(".plan-heatmap-row")
.style("margin-bottom", `${rowGap}rem`);
container.node().appendChild(baselineSourceNote());
return container.node();
})()planBiasMetrics = {
const allScores = await allPlanScores;
return allScores.map(plan => {
const scores = plan.scores;
const n = scores.length;
if (n === 0) return { id: plan.id, label: plan.label, category: plan.category, partisanBias: null, eg: null, dil: null, decl: null };
const demShares = scores.map(d => d.dem_share);
const totalVotes = scores.map(d => d.baseline_total_votes || 0);
const totalVotesSum = d3.sum(totalVotes);
const demVotes = demShares.map((s, i) => s * totalVotes[i]);
const repVotes = demShares.map((s, i) => (1 - s) * totalVotes[i]);
// redistmetrics::part_bias() evaluates biasatv(dvs, v), where colMeans(dvs)
// is the unweighted mean district share and v is the target vote share.
const meanDemShare = d3.mean(demShares);
const dShift = selectedNationalDemShare - meanDemShare;
const rShift = 1 - selectedNationalDemShare - meanDemShare;
const dSeatsAtV = demShares.filter(s => s + dShift > 0.5).length / n;
const rSeatsAtV = 1 - demShares.filter(s => s + rShift > 0.5).length / n;
const partisanBias = (rSeatsAtV - dSeatsAtV) / 2; // positive = R advantage
// Efficiency gap (minwin = floor(total/2)+1)
let wastedDeg = 0, wastedReg = 0;
for (let i = 0; i < n; i++) {
const dv = demVotes[i], rv = repVotes[i], tv = totalVotes[i];
const minwin = Math.floor(tv / 2) + 1;
if (dv > rv) {
wastedDeg += dv - minwin;
wastedReg += rv;
} else {
wastedDeg += dv;
wastedReg += rv - minwin;
}
}
const eg = totalVotesSum > 0 ? (wastedDeg - wastedReg) / totalVotesSum : null;
// Dilution asymmetry: match redistmetrics::part_dil_asym() tie handling.
let wastedDda = 0, wastedRda = 0;
for (let i = 0; i < n; i++) {
const dv = demVotes[i], rv = repVotes[i], tv = totalVotes[i];
const half = Math.floor(tv / 2) + 1;
if (rv > dv) {
wastedDda += dv;
} else {
wastedDda += dv - half;
}
if (dv > rv) {
wastedRda += rv;
} else {
wastedRda += rv - half;
}
}
const totalDem = d3.sum(demVotes);
const totalRep = d3.sum(repVotes);
const dil = (totalDem > 0 && totalRep > 0) ? wastedDda / totalDem - wastedRda / totalRep : null;
// Declination (normalize=TRUE, adjust=TRUE defaults from part_decl)
// D-won: dem_share >= 0.5 (matches dseats >= and calc_win_sums >= in C++)
const dWon = scores.filter(d => d.dem_share >= 0.5);
const rWon = scores.filter(d => d.dem_share < 0.5);
let decl = null;
if (dWon.length > 0 && rWon.length > 0) {
const meanD = d3.mean(dWon, d => d.dem_share);
const meanR = d3.mean(rWon, d => d.dem_share);
const sD = dWon.length / n;
const a = (meanD - 0.5) / sD;
const b = (0.5 - meanR) / (1 - sD);
const angle = Math.atan(a) - Math.atan(b);
decl = (2 * angle / Math.PI) * (Math.log(n) / 2);
}
return { id: plan.id, label: plan.label, category: plan.category, partisanBias, eg, dil, decl };
});
}National partisan fairness
(() => {
const metrics = planBiasMetrics;
const fmt3 = d3.format("+.3f");
const fmtOrDash = v => v === null || isNaN(v) ? "—" : fmt3(v);
// Positive = R advantage → red; negative = D advantage → blue.
// Each metric gets an independent symmetric scale so color intensity is
// comparable within a column, not across differently scaled measures.
const metricColor = (field) => {
const values = metrics
.map(d => d[field])
.filter(v => v !== null && !isNaN(v));
const maxAbs = d3.max(values, v => Math.abs(v)) || 1;
return d3.scaleDiverging(d3.interpolateRdBu).domain([maxAbs, 0, -maxAbs]);
};
const partisanBiasColor = metricColor("partisanBias");
const egColor = metricColor("eg");
const dilColor = metricColor("dil");
const declColor = metricColor("decl");
const cellBg = (v, scale) => {
if (v === null || isNaN(v)) return "";
return scale(v);
};
const textColor = (bg) => {
if (!bg) return "";
const c = d3.color(bg);
if (!c) return "";
const lum = 0.2126 * (c.r / 255) + 0.7152 * (c.g / 255) + 0.0722 * (c.b / 255);
return lum > 0.45 ? "#222" : "#fff";
};
const cellStyle = (v, scale) => {
const bg = cellBg(v, scale);
if (!bg) return "";
return `background:${bg};color:${textColor(bg)}`;
};
return html`<div class="partisan-table-wrap">
<table class="atlas-table partisan-table">
<thead>
<tr>
<th class="pt-plan-col">Plan</th>
<th title="Partisan bias at the selected national two-party vote share.">Partisan bias</th>
<th title="Efficiency gap: (wasted D votes − wasted R votes) / total votes.">Efficiency gap</th>
<th title="Dilution asymmetry: wasted D rate minus wasted R rate.">Dilution asymmetry</th>
<th title="Declination (normalized, log-adjusted). Undefined when one party wins all seats.">Declination</th>
</tr>
</thead>
<tbody>
${metrics.map(d => html`<tr>
<td class="pt-plan-name">${d.label}</td>
<td class="pt-metric" style="${cellStyle(d.partisanBias, partisanBiasColor)}">${fmtOrDash(d.partisanBias)}</td>
<td class="pt-metric" style="${cellStyle(d.eg, egColor)}">${fmtOrDash(d.eg)}</td>
<td class="pt-metric" style="${cellStyle(d.dil, dilColor)}">${fmtOrDash(d.dil)}</td>
<td class="pt-metric" style="${cellStyle(d.decl, declColor)}">${fmtOrDash(d.decl)}</td>
</tr>`)}
</tbody>
</table>
<p class="partisan-table-note">
Positive values indicate a Republican advantage. Negative values indicate a Democratic advantage.
All metrics are evaluated under ${formatNationalEnvironment(selectedNationalDemShare)}.
</p>
${baselineSourceNote()}
</div>`;
})()Where to look
html`<section class="detail-grid">
<div>
<h3>Closest districts</h3>
<table class="atlas-table compact">
<thead>
<tr>
<th>District</th>
<th>Dem share</th>
<th>Margin</th>
</tr>
</thead>
<tbody>
${closeDistricts.map((d) => html`<tr>
<td>${d.district_id}</td>
<td>${formatPercent(d.dem_share)}</td>
<td>${formatPercent(Math.abs(d.dem_share - 0.5))}</td>
</tr>`)}
</tbody>
</table>
</div>
<div>
<h3>${selectedState === "US" ? "Most toss-up states" : "Plan notes"}</h3>
${selectedState === "US"
? html`<table class="atlas-table compact">
<thead>
<tr>
<th>State</th>
<th>Seats</th>
<th>Toss Up</th>
<th>D-R split</th>
</tr>
</thead>
<tbody>
${stateSummary.slice(0, 12).map((d) => html`<tr>
<td>${d.state}</td>
<td>${d.districts}</td>
<td>${d.tossUpSeats}</td>
<td>${d.demSeats}-${d.repSeats}</td>
</tr>`)}
</tbody>
</table>`
: html`<p class="plan-note">
Districts are colored by the ${baselineLabel} presidential two-party vote under the selected congressional plan.
The closest-district table highlights seats where small shifts would change the district winner.
</p>`}
</div>
</section>`