Public
Edited
Oct 8, 2023
Insert cell
Insert cell
{
const svg = d3.select(DOM.svg(w, h))
drawStates([WI], svg, path)
return svg.node()
}
Insert cell
{
const svg = d3.select(DOM.svg(w,h))
drawTriangles(triangleArray, svg, "test");
drawStates([WI], svg, path);
return svg.node()
}
Insert cell
{
const svg = d3.select(DOM.svg(w,h))
drawPoints(randomPoints, svg, w * 0.01)
drawStates([WI], svg, path)
return svg.node()
}
Insert cell
{
const svg = d3.select(DOM.svg(width, h))
drawPoints(clusteredPoints, svg, w * 0.01);
drawPoints(kMeansCentroids, svg, w * 0.02, "centroid");
drawVoronoi(clippedVoronoiPolygons, svg);
drawStates([WI], svg, path);
return svg.node()
}
Insert cell
{
const svg = d3.select(DOM.svg(w*2, h))
const left = svg.append('g')
drawVoronoi(clippedVoronoiPolygons.map((d, i) => ({...d, color: d3.schemeSet3[i]})), left, "white")
drawStates([WI], left, path)

const right = svg.append('g').attr('transform', `translate(${w}, 0)`)
drawDistricts(WIDistricts.map((d, i) => ({...d, color: d3.schemeSet3[mapping.find(m => m[1] === i)[0]]})), right, path)
drawStates([WI], right, path)
return svg.node()
}
Insert cell
Insert cell
WIDistricts[0]
Insert cell
clippedVoronoiPolygons[0]
Insert cell
Insert cell
flubber = require('https://unpkg.com/flubber')
Insert cell
distances = kMeansCentroids.map(origin =>
WIDistricts
.map(destitnation => {
const x1 = origin.x
const y1 = origin.y
const x2 = path.centroid(destitnation)[0]
const y2 = path.centroid(destitnation)[1]
const dist = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
return dist**2
})
)
Insert cell
mapping = munkres(distances)
Insert cell
munkres = require('https://bundle.run/munkres-js@1.2.2')
Insert cell
function drawDistricts(districts, selector, path) {
selector.selectAll('.district')
.data(districts)
.join(enter => enter
.append("path")
.attr("fill", d => d.color || "lightgrey")
.attr("stroke", "white")
.attr("stroke-width", width*0.0010)
.attr("d", path)
)
}
Insert cell
WIDistricts = usCongress118Cleaned.features.filter(f => f.properties.stab === "WI")
Insert cell
kMeans = simple.kMeansCluster(randomPoints.map(p => [p.x, p.y]), 8)
Insert cell
kMeansCentroids = kMeans.centroids.map((p, i) => ({x: p[0], y: p[1], color: d3.schemeSet3[i], opacity: 1}))
Insert cell
kMeansCentroids.map(p => turf.point([p.x, p.y]))
Insert cell
voronoiPolygons = {
const points = {
type: "FeatureCollection",
features: kMeansCentroids.map(p => turf.point([p.x, p.y]))
}
const options = {
bbox: [3, 3, w-3, h-3]
};
return turf.voronoi(points, options)
}
Insert cell
d3.geoProject(WI, projectionWI).geometry
Insert cell
WI.geometry
Insert cell
Insert cell
d3 = require("d3", "d3-geo-projection")
Insert cell
function drawVoronoi(voronoiPolygons, selector, color) {
selector
.selectAll('voronoi')
.data(voronoiPolygons)
.join(enter => enter
.append('path')
.attr("class", "voronoi")
.attr("fill", d => d.color || "none")
.attr("stroke", color || "dodgerblue")
.attr("stroke-width", width*0.0015)
.attr("d", d3.geoPath())
)
}
Insert cell
clusteredPoints = randomPoints.map((p, i) => ({x: p.x, y: p.y, color: d3.schemeSet3[kMeans.labels[i]]}))
Insert cell
simple = import("simple-statistics@7")
Insert cell
triangles = {
const {coords, vertices} = multipolygon_to_triangles(WI.geometry);

const triangles = [];
for (let i = 0; i < vertices.length; i += 3){
const a = coords.slice(vertices[i]*2, vertices[i]*2+2);
const b = coords.slice(vertices[i+1]*2, vertices[i+1]*2+2);
const c = coords.slice(vertices[i+2]*2, vertices[i+2]*2+2);
triangles.push({a,b,c})
};

return triangles
}
Insert cell
triangles
Insert cell
triangleArray = {
let triangleArray = triangles.map(function(d){
let triangleTriplet = [];
for (var key in d) {
var obj = projectionWI(d[key]);
triangleTriplet.push({
"x": obj[0],
"y": obj[1]
})
}
return triangleTriplet
})

return triangleArray
}

Insert cell
function drawTriangles(triangleCoords, selector, className) {
selector.selectAll(`.${className}`)
.data(triangleCoords)
.join(enter => enter
.append('path')
.attr("class", className)
.attr("d", function(d){
var coords = [d[0], d[1], d[2]];
console.log(coords)
return d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.curve(d3.curveLinear)(coords);
})
.attr("stroke", "black")
.attr("fill", function(d, i) {
return i % 2 === 0 ? "yellow" : "lightblue"; // alternate fill colors
})
)
}
Insert cell
function drawPoints(points, selector, r, className) {
selector.selectAll(`.${className}`)
.data(points)
.join(enter => enter
.append('circle')
.attr("class", className)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", r)
.attr("fill", d => d.color || "grey")
.attr("stroke", "white")
.attr("stroke-width", r * 0.3)
.style("opacity", d => d.opacity || 1)
)
}
Insert cell
function drawStates(states, selector, path) {
selector.selectAll('.state')
.data(states)
.join(enter => enter
.append('g')
.attr("class", "states")
.append("path")
// .call(enter => enter.append("path")
.attr("class", "white-border")
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", width * 0.002)
.attr("d", path)
)
}
Insert cell
w = Math.min(width, 300)
Insert cell
h = w
Insert cell
path = d3.geoPath(projectionWI)
Insert cell
WI = usStateFeatures.find(f => f.name === 'WI')
Insert cell
triangles
Insert cell
projectionWI([-91,42])
Insert cell
projectionWI = d3.geoAlbersUsa().fitSize([w, h], {type: "FeatureCollection", features: [WI]})
Insert cell
WI
Insert cell
usStateFeatures = topojson.feature(usTopo, usTopo.objects.states).features
.map(f => {
const congressCount = congressCountByState[f.id]?.count
const totalCongressCount = usCongress118Cleaned.features.length
const proportionOfCongresses = congressCount / totalCongressCount
const name = congressCountByState[f.id]?.name
const area = albersPath.area(f)
const totalArea = albersPath.area(usNationFeatures)
const proportionalArea = area/totalArea
const bbox = albersPath.bounds(f)
return {
...f,
name,
abrev: stateAbrev[f.id]?.abrev,
congressCount,
proportionOfCongresses,
bbox,
area,
totalArea,
proportionalArea,
equalAreaScale: Math.sqrt(1 / (50 * proportionalArea)), // 50 is the number of states
congressScale: Math.sqrt((totalArea * proportionOfCongresses)/ area / 2.5)
}
})
.filter(f => f.name)
Insert cell
randomPoints = _(dotDensity([WI], 1000))
.flatMap(c => c.points)
.map(p => projectionWI(p))
.map((p, i) => ({x: p[0], y: p[1]}))
.value()
Insert cell
dotDensity([WI], 5)
.flatMap(c => c.points)
Insert cell
WI
Insert cell
function dotDensity(features, pointCount) {
return features.map(feature => {
const {coords, vertices} = multipolygon_to_triangles(feature.geometry)
// unpack the triangles.
const triangles = [];
for (let i = 0; i < vertices.length; i += 3) { //3 vertices per triangle
const a = coords.slice(vertices[i]*2, vertices[i]*2+2);
const b = coords.slice(vertices[i+1]*2, vertices[i+1]*2+2);
const c = coords.slice(vertices[i+2]*2, vertices[i+2]*2+2);
// Double area saves an op.
const area = Math.abs(
a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1])
) / 2
triangles.push({a, b, c, area})
}
let totalArea = d3.sum(triangles.map(d => d.area))
// earcut seems to always return triangles in a form where the absolute
// value isn't necessary.

const randomPoints = _(triangles).flatMap(({a, b, c, area}) => {
const points = randround(pointCount * area/totalArea)
return d3.range(points).map(p => random_point(a, b, c))
})
.value()
return {
...feature,
points: randomPoints,
}
})
}
Insert cell
{
const {coords, vertices} = multipolygon_to_triangles(WI.geometry);
return {coords, vertices}
// return coords[201]
}
Insert cell
WI.geometry.coordinates
Insert cell
function multipolygon_to_triangles(projected) {
// When triangulated, everything is a multipolygon.
if (projected.type == "Polygon") {
return polygon_to_triangles(projected.coordinates)
}
let all_coords = []
let all_vertices = []
for (let polygon of projected.coordinates) {
const current_vertex = all_coords.length/2
const { coords, vertices } = polygon_to_triangles(polygon);
all_coords.push(...coords)
// If need to shift because we may be storing multiple triangle sets on a feature.
all_vertices.push(...vertices.map(d => d + current_vertex))
}
return {coords:all_coords, vertices:all_vertices}
}
Insert cell
6%1
Insert cell
function randround(how_many_points_do_i_get) {
const leftover = how_many_points_do_i_get % 1;
// Random round to decide if you get a fractional point.
if (Math.random() > leftover) {
how_many_points_do_i_get -= leftover
} else {
how_many_points_do_i_get += (1 - leftover)
}
return how_many_points_do_i_get
}
Insert cell
function random_point([ax, ay], [bx, by], [cx, cy]) {
const a = [bx - ax, by - ay]
const b = [cx - ax, cy - ay]
let [u1, u2] = [Math.random(), Math.random()]
if (u1 + u2 > 1) {u1 = 1 - u1; u2 = 1 - u2}
const w = [u1 * a[0] + u2 * b[0], u1 * a[1] + u2 * b[1]]
return [w[0] + ax, w[1] + ay]
}
Insert cell
6.8%1
Insert cell
multipolygon_to_triangles(NJ.geometry)
Insert cell
import { earcut, polygon_to_triangles } from '@bmschmidt/a-binary-file-format-for-projected-triangulated-shapefile'
Insert cell
projectionNJ = d3.geoAlbersUsa().fitSize([w, h], {type: "FeatureCollection", features: [NJ]})
Insert cell
usStateFeatures
Insert cell
Insert cell
_
Insert cell
stateAbrev = _([
{ name:"Alabama",abrev: "AL", id: "01"},
{ name:"Alaska",abrev: "AK", id: "02"},
{ name:"Arizona",abrev: "AZ", id: "04"},
{ name:"Arkansas",abrev: "AR", id: "05"},
{ name:"California",abrev: "CA", id: "06"},
{ name:"Colorado",abrev: "CO", id: "08"},
{ name:"Connecticut",abrev: "CT", id: "09"},
{ name:"Delaware",abrev: "DE", id: "10"},
{ name:"District of Columbia",abrev: "DC", id: "11"},
{ name:"Florida",abrev: "FL", id: "12"},
{ name:"Georgia",abrev: "GA", id: "13"},
{ name:"Hawaii",abrev: "HI", id: "15"},
{ name:"Idaho",abrev: "ID", id: "16"},
{ name:"Illinois",abrev: "IL", id: "17"},
{ name:"Indiana",abrev: "IN", id: "18"},
{ name:"Iowa",abrev: "IA", id: "19"},
{ name:"Kansas",abrev: "KS", id: "20"},
{ name:"Kentucky",abrev: "KY", id: "21"},
{ name:"Louisiana",abrev: "LA", id: "22"},
{ name:"Maine",abrev: "ME", id: "23"},
{ name:"Maryland",abrev: "MD", id: "24"},
{ name:"Massachusetts",abrev: "MA", id: "25"},
{ name:"Michigan",abrev: "MI", id: "26"},
{ name:"Minnesota",abrev: "MN", id: "27"},
{ name:"Mississippi",abrev: "MS", id: "28"},
{ name:"Missouri",abrev: "MO", id: "29"},
{ name:"Montana",abrev: "MT", id: "30"},
{ name:"Nebraska",abrev: "NE", id: "31"},
{ name:"Nevada",abrev: "NV", id: "32"},
{ name:"New Hampshire",abrev: "NH", id: "33"},
{ name:"New Jersey",abrev: "NJ", id: "34"},
{ name:"New Mexico",abrev: "NM", id: "35"},
{ name:"New York",abrev: "NY", id: "36"},
{ name:"North Carolina",abrev: "NC", id: "37"},
{ name:"North Dakota",abrev: "ND", id: "38"},
{ name:"Ohio",abrev: "OH", id: "39"},
{ name:"Oklahoma",abrev: "OK", id: "40"},
{ name:"Oregon",abrev: "OR", id: "41"},
{ name:"Pennsylvania",abrev: "PA", id: "42"},
{ name:"Puerto Rico",abrev: "PR", id: "72"},
{ name:"Rhode Island",abrev: "RI", id: "44"},
{ name:"South Carolina",abrev: "SC", id: "45"},
{ name:"South Dakota",abrev: "SD", id: "46"},
{ name:"Tennessee",abrev: "TN", id: "47"},
{ name:"Texas",abrev: "TX", id: "48"},
{ name:"Utah",abrev: "UT", id: "49"},
{ name:"Vermont",abrev: "VT", id: "50"},
{ name:"Virginia",abrev: "VA", id: "51"},
{ name:"Virgin Islands",abrev: "VI", id: "78"},
{ name:"Washington",abrev: "WA", id: "53"},
{ name:"West Virginia",abrev: "WV", id: "54"},
{ name:"Wisconsin",abrev: "WI", id: "55"},
{ name:"Wyoming",abrev: "WY", id: "56"}
])
.keyBy('id')
.value()
Insert cell
usNationFeatures = topojson.feature(usTopo, usTopo.objects.nation)
Insert cell
congressCountByState = _(usCongress118Cleaned.features)
.groupBy('properties.state')
.map(congresses => ({ id: congresses[0].properties.state, name: congresses[0].properties.stab, count: congresses.length}))
.keyBy('id')
.value()
Insert cell
albersPath = d3.geoPath(projection)
Insert cell
projection = d3.geoAlbersUsa().fitSize([width, height], topojson.feature(usTopo, usTopo.objects.nation))
Insert cell
height = width * 0.65
Insert cell
usTopo
Insert cell
usCongress118Cleaned = ({
...usCongress118Normalized,
features: usCongress118Normalized.features.map(f => {
const centroid = albersPath.centroid(f)
return {
...f,
centroid
}
})
})
Insert cell
ben = ({
...['a','b','c'],
features: ['a', 'b', 'c'].map(d => d)
})
Insert cell
usCongress118Normalized = normalizeWindingInPlace({
...usCongress118_large,
features: usCongress118_large.features
.map(f => ({
...f,
// geometry: f.geometry
geometry: turf.intersect(f.geometry, topojson.feature(usTopo, usTopo.objects.nation).features[0].geometry).geometry
}))
})
Insert cell
{
const container = yield htl.html`<div style="height: 500px;">`;
const map = L.map(container);
const layer = L.geoJSON(usCongress118Normalized).addTo(map);
map.fitBounds(layer.getBounds(), {maxZoom: 9});
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© <a href=https://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors"
}).addTo(map);
}
Insert cell
import {normalizeWindingInPlace} from "@fil/normalize-winding"
Insert cell
turf = require("@turf/turf")
Insert cell
import {usTopo} from "@karimdouieb/us-house-election-2022"
Insert cell
usCongress118_large
Insert cell
import {usCongress118_large} from "@karimdouieb/us-house-election-2022"
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