Public
Edited
Apr 12, 2023
1 fork
Importers
8 stars
Also listed in…
hex maps
Maps
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// The numbers
[
all_indices.length,
yellow.length,
yellow.length / all_indices.length,
purple.length,
purple.length / all_indices.length,
minorities.length,
4 / 9
]
Insert cell
// The precincts argument should be a list of objects, each of which
// extends one of the objects in all_precincts by adding a district
// property. The district should be a number between 1 and 9 indicating
// which district you want the precinct to be in.
function setup_hexmap(precincts) {
let W = 1000;
let w = width <= W ? width : W;
let h = 0.65 * w;
let svg = d3.create("svg:svg").attr("width", w).attr("height", h);

let r = (36 * w) / W;
svg
.append("g")
.selectAll("path.outer")
.data(precincts)
.join("path")
.attr("class", "outer")
.attr("d", (o) => hex(o.i, o.j, r))
.attr("stroke", "white")
.attr("stroke-width", 2)
.attr("fill", "#ddd");
svg
.append("g")
.selectAll("path.inner")
.data(precincts)
.join("path")
.attr("class", "inner")
.attr("d", (o) => hex(o.i, o.j, r, 0.5))
.attr("stroke", "black")
.attr("stroke-width", (o) => (o.minority ? 5 : 1))
//.attr("fill", (o) => d3.schemeCategory10[o.district]);
.attr("fill", (o) => (o.color == "yellow" ? "#EBB35F" : "#B180B0"));

// If you successfully label each precinct with a number from 1-9 as the
// district property, then the following code should automatically group
// and draw them and draw the outlines of the districts.
// As originally set up, the district property of the precincts is
// undefined. Thus, we should get a single district so that the whole
// collection is outlined.
let groups = d3.group(precincts, (o) => o.district);
let good_partition = true;
d3.range(1, 10).forEach(function (i) {
if (groups.has(i)) {
if (groups.get(i).length != 15) {
good_partition = false;
}
} else {
good_partition = false;
}
});
if (good_partition) {
groups.forEach(function (g) {
let purples = g.filter((p) => p.color == "purple");
let yellows = g.filter((p) => p.color == "yellow");
let color = purples.length > yellows.length ? "purple" : "yellow";
let merged_precinct = polygonClipping.union(
// Always use a constant size to keep polygonClipping happy.
g.map((o) => [vertices(o.i, o.j, 1)])
);
svg
.append("path")
.attr(
"d",
d3.line().curve(d3.curveLinearClosed)(merged_precinct[0][0].map(([x, y]) => [r * x, r * y]))
)
.attr("stroke", "black")
.attr("stroke-width", 5)
.attr("fill", color)
.attr("fill-opacity", 0.1)
.on("pointerenter", function () {
d3.select(this).attr("fill-opacity", 0.2);
})
.on("pointerleave", function () {
d3.select(this).attr("fill-opacity", 0.1);
});
});
}

return svg.node();
}
Insert cell
// The precincts of Hexapolis are hexagonal. Each precinct is drawn as
// a large hexagon with a smaller hexagon on top. The radius of the larger
// hexagon is the parameter r. The smaller hexagon is a copy of the large
// scaled by the factor r0 about its center.
function hex(i, j, r, r0 = 1) {
return d3.line()(vertices(i, j, r, r0)) + "Z";
}
Insert cell
vertices(0, 0, 1)
Insert cell
// The vertices are generated separately so they can also be easily access
// by polygonClipping's union method.
function vertices(i, j, r, r0 = 1) {
let s = (Math.sqrt(3) * r) / 2;
let x = 2 * (i - 3) * s + (j + 1) * s;
let y = (j + 1) * Math.sqrt(3) * s;

return d3
.range(6)
.map((t) => [
parseFloat(
(x + r * r0 * Math.cos((2 * Math.PI * t) / 6 + Math.PI / 6)).toFixed(5)
),
parseFloat(
(y + r * r0 * Math.sin((2 * Math.PI * t) / 6 + Math.PI / 6)).toFixed(5)
)
]);
}
Insert cell
// Each precinct is an object with i and j properties indicating its
// position in a hexagonal lattice.
// There are also color and minority properties indicating to which party
// the precinct swings and whether it's a minority district.
// Finally, there's a list of each precinct's neighbors, as indicated by
// the id property. While not used here, that's probably pretty key
// information for any gerrymandering algorithm.
all_precincts = all_indices.map(([i, j]) => ({
id: `precinct_${i}_${j}`,
i: i,
j: j,
color: pair_in_array([i, j], purple) ? "purple" : "yellow",
minority: pair_in_array([i, j], minorities) ? true : false,
neighbors: [
[i - 1, j],
[i + 1, j],
[i, j - 1],
[i, j + 1],
[i - 1, j + 1],
[i + 1, j - 1]
]
.filter(([i0, j0]) => pair_in_array([i0, j0], all_indices))
.map(([i0, j0]) => `precinct_${i0}_${j0}`)
}))
Insert cell
function pair_in_array([i, j], A) {
return _.findIndex(A, (pair) => pair[0] == i && pair[1] == j) > -1;
}
Insert cell
// All the indices of the precints, defined in terms of the
// purple and yellow indices.
all_indices = purple.concat(yellow)
Insert cell
minorities = [
[7, 0],
[8, 0],
[9, 0],
[14, 0],
[12, 1],
[13, 1],
[14, 1],
[6, 2],
[11, 2],
[12, 2],
[5, 3],
[6, 3],
[7, 3],
[10, 3],
[11, 3],
[12, 3],
[6, 4],
[2, 5],
[3, 5],
[15, 5],
[1, 6],
[2, 6],
[8, 6],
[9, 6],
[10, 6],
[13, 6],
[14, 6],
[1, 7],
[7, 7],
[8, 7],
[12, 7],
[13, 7],
[14, 7],
[3, 8],
[4, 8],
[6, 8],
[7, 8],
[8, 8],
[11, 8],
[2, 9],
[3, 9]
]
Insert cell
purple = [
[7, 0],
[8, 0],
[9, 0],
[14, 0],
[5, 1],
[6, 1],
[7, 1],
[9, 1],
[10, 1],
[14, 1],
[15, 1],
[4, 2],
[5, 2],
[8, 2],
[9, 2],
[11, 2],
[13, 2],
[14, 2],
[15, 2],
[16, 2],
[3, 3],
[4, 3],
[7, 3],
[8, 3],
[11, 3],
[12, 3],
[13, 3],
[15, 3],
[16, 3],
[2, 4],
[7, 4],
[8, 4],
[10, 4],
[11, 4],
[14, 4],
[15, 4],
[1, 5],
[2, 5],
[6, 5],
[7, 5],
[8, 5],
[9, 5],
[10, 5],
[12, 5],
[1, 6],
[2, 6],
[4, 6],
[5, 6],
[6, 6],
[7, 6],
[8, 6],
[12, 6],
[1, 7],
[3, 7],
[4, 7],
[5, 7],
[10, 7],
[11, 7],
[12, 7],
[2, 8],
[3, 8],
[4, 8],
[5, 8],
[6, 8],
[9, 8],
[10, 8],
[1, 9],
[2, 9],
[5, 9],
[6, 9],
[7, 9],
[8, 9],
[9, 9],
[7, 10],
[8, 10]
]
Insert cell
yellow = [
[12, 0],
[13, 0],
[4, 1],
[8, 1],
[11, 1],
[12, 1],
[13, 1],
[3, 2],
[6, 2],
[7, 2],
[10, 2],
[12, 2],
[5, 3],
[6, 3],
[9, 3],
[10, 3],
[14, 3],
[3, 4],
[4, 4],
[5, 4],
[6, 4],
[9, 4],
[12, 4],
[13, 4],
[3, 5],
[4, 5],
[5, 5],
[11, 5],
[13, 5],
[14, 5],
[15, 5],
[3, 6],
[9, 6],
[10, 6],
[11, 6],
[13, 6],
[14, 6],
[2, 7],
[6, 7],
[7, 7],
[8, 7],
[9, 7],
[13, 7],
[14, 7],
[0, 8],
[1, 8],
[7, 8],
[8, 8],
[11, 8],
[12, 8],
[13, 8],
[0, 9],
[3, 9],
[4, 9],
[10, 9],
[11, 9],
[2, 10],
[3, 10],
[4, 10],
[9, 10]
]
Insert cell
polygonClipping = require("polygon-clipping")
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