Public
Edited
Jun 30, 2023
1 fork
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function make_boundaries(boundaries) {
let svg = d3.create("svg").attr("width", w).attr("height", h);
let map = svg.append("g");
map
.append("g")
.selectAll("path.boundary")
.data(boundaries.features)
.join("path")
.attr("class", "boundary")
.attr("data-district_votes", function (o) {
return `${o.properties.district}-${o.properties.dem_vote}-${o.properties.rep_vote}`;
})
.attr("fill", "white")
.attr("fill-opacity", 0)
.attr("stroke", "#333")
.attr("stroke-width", 2)
.attr("stroke-linejoin", "round")
.attr("d", path)
.on("pointerenter", function () {
d3.selectAll("path.boundary").attr("fill-opacity", 0.4);
d3.select(this).attr("fill-opacity", 0).attr("stroke-width", 5).raise();
})
.on("pointerleave", function () {
d3.selectAll("path.boundary")
.attr("fill-opacity", 0)
.attr("stroke-width", 2);
});
map
.append("g")
.selectAll("circle.city")
.data(cities)
.join("circle")
.attr("class", "city")
.attr("cx", (o) => o.x)
.attr("cy", (o) => o.y)
.attr("r", 4)
.attr("fill", "black")
.attr("stroke", "white")
.attr("stroke-width", 2)
.attr("pointer-events", "none");
let city_background_container = map
.append("g")
.attr("id", "city_background_container");
let city_name_display = map.append("g");
city_name_display
.selectAll("text.city")
.data(cities)
.join("text")
.attr("class", "city")
.attr("x", (o) => o.x)
.attr("y", (o) => o.y)
.attr("dx", (o) => (o.city == "Winston-Salem" ? (-90 * width) / 1100 : 3))
.attr("dy", (o) => (o.city == "Winston-Salem" ? (-15 * width) / 1100 : -3))
.attr("opacity", show_cities ? 1 : 0)
.text((o) => o.city)
.style("font-family", "sans-serif")
.attr("font-size", (14 * width) / 1100)
.attr("pointer-events", "none");
map
.selectAll("path.boundary")
.nodes()
.forEach(function (d, i) {
let [district, dem, rep] = d
.getAttribute("data-district_votes")
.split("-")
.map((x) => parseInt(x));
let total = dem + rep;
let dprop = dem / total;
let rprop = rep / total;
let dpct = d3.format("0.1%")(dem / total);
let rpct = d3.format("0.1%")(rep / total);
let lean;
if (dprop < rprop) {
lean = `R +${d3.format("0.1%")(rprop - state_dem_proportion)}`;
} else {
lean = `D +${d3.format("0.1%")(dprop - state_dem_proportion)}`;
}
let content = `
<div>
<div>
<div style="display: inline-block; margin-right: 5px">
District: ${district}
</div>
<div style="display: inline-block">
Lean: ${lean}
</div>
</div>
</div>`;
tippy(d, {
content: content,
allowHTML: true,
theme: "light-border",
followCursor: true
});
});

return svg.node();
}
Insert cell
// viewof show_cities = Inputs.toggle({ label: "Show city labels:" })
// This was a toggle but it's been demoted.
show_cities = true
Insert cell
// This cell places the rect backgrounds of the city lables. Since we call the
// svg:text.getBBox() method, this needs to run *after* the labels have been
// generated; thus, it's odd placement here.
outline_cities = {
let container = d3.select(map).select("#city_background_container");
if (show_cities) {
d3.select(map)
.selectAll("text")
.each(function (o) {
let bbox = this.getBBox();
container
.append("rect")
.attr("x", bbox.x - 2)
.attr("y", bbox.y - 2)
.attr("width", bbox.width + 4)
.attr("height", bbox.height + 4)
.attr("fill", "white")
.attr("fill-opacity", 0.4)
.attr("stroke", "black")
.attr("stroke-width", 0.4)
.style("pointer-events", "none");
});
} else {
container.selectAll("rect").remove();
}
}
Insert cell
Insert cell
state_dem_proportion = {
let dem_vote_total = d3.sum(
all_boundaries[0].boundaries.features.map((o) => o.properties.dem_vote)
);
let rep_vote_total = d3.sum(
all_boundaries[0].boundaries.features.map((o) => o.properties.rep_vote)
);
return dem_vote_total / (dem_vote_total + rep_vote_total);
}
Insert cell
function partisan_lean(district) {
let d = parseFloat(district.properties.dem_vote);
let r = parseFloat(district.properties.rep_vote);
let t = r + d;
return d / t;
}
Insert cell
function efficiency_gap(districts) {
let wasted_dem_votes = d3.sum(districts.map((d) => wasted_votes(d, "dem")));
let wasted_rep_votes = d3.sum(districts.map((d) => wasted_votes(d, "rep")));
let total_votes = d3.sum(
districts.map(
(d) =>
parseFloat(d.properties.dem_vote) + parseFloat(d.properties.rep_vote)
)
);
return (wasted_dem_votes - wasted_rep_votes) / total_votes;
}
Insert cell
function wasted_votes(district, p) {
let dvote = parseFloat(district.properties.dem_vote);
let rvote = parseFloat(district.properties.rep_vote);
let tvote = rvote + dvote;

if (p == "dem") {
if (dvote < tvote / 2) {
return dvote;
} else {
return dvote - tvote / 2;
}
} else {
if (rvote < tvote / 2) {
return rvote;
} else {
return rvote - tvote / 2;
}
}
}
Insert cell
Insert cell
path = d3.geoPath().projection(projection)
Insert cell
projection = d3.geoIdentity().reflectY(true).fitSize([w, h], congress_map_data)
Insert cell
h = 0.5 * w
Insert cell
w = width < 1200 ? width : 1200
Insert cell
Insert cell
congress_map_data = topojson.feature(map_data, map_data.objects.us_congress)
Insert cell
all_boundaries = Object.keys(map_data.objects).map((s) => ({
name: s,
boundaries: topojson.feature(map_data, map_data.objects[s])
}))
Insert cell
cities = all_boundaries[3].boundaries.features.map(function (o) {
let [x, y] = projection(o.geometry.coordinates);
let city = o.properties.city;
return { x, y, city };
})
Insert cell
map_data = FileAttachment("congressional_boundaries.json").json()
Insert cell
background = FileAttachment("im0.png").image()
Insert cell
Insert cell
import { Legend } from "@d3/color-legend"
Insert cell
tippy_style = html`<link style="display: none" rel="stylesheet" href="${await require.resolve(
`tippy.js/themes/light-border.css`
)}">`
Insert cell
tippy = require("tippy.js@6")
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