Published
Edited
Nov 15, 2020
2 stars
Insert cell
Insert cell
myCanvas = DOM.canvas(975, 610)
Insert cell
viewof incidence = radio({options: ["distortion", "spin"], value: "distortion"})
Insert cell
viewof scale_by_votes = radio({title: "Scale by number of votes?", options: ["yes", "no", "by margin"], value: "no", description: "This is gonna be wrong for Alaska and Hawaii."})
Insert cell
Math.cos(0)
Insert cell
Insert cell
viewof zoom = slider({title: "zoom (square root)", min: .5, max: 15, value: 1, step: .1})
Insert cell
Insert cell
viewof speed = slider({title: "Speed of spin adjustment", min: -5, max: 5})
Insert cell
rate_of_change = d3.scaleDivergingLog().domain([.2,1, 5]).interpolator(d3.interpolatePiYG).clamp(true)
Insert cell
rate_of_change(2).split(/\(|\)/)[1].split(",").map(d => +d)
Insert cell
using.features[3].properties
Insert cell
import { data as d } from '@wattenberger2/us-election-2020'
Insert cell
votes = new Map(Object.entries(d))
Insert cell
date = "2020-07-30"
Insert cell
long_term_corona_counts.get(`53061-${date}`)
Insert cell
long_term_corona_counts
Insert cell
using.features[0].properties.CEN
Insert cell
render_loop = {
// return false
const size_scale = d3.scaleSqrt().domain([0, 10000]).range([0, 10])
const times = []
const start_time = Date.now()
while (true) {
const overall_radius = jitter_radius / 100 * (Math.sin(Date.now()/1000) + 1)/2
const start = performance.now()
gl.clear({color: [0.1, 0.1, .1,.81 ]});
for (let feature of using.features.filter(d => d.projected)) {
//Dumb
const rand = feature.rand ? feature.rand : feature.rand = [Math.random() - .5, Math.random() - .5]
const flip = rand[0] + rand[1] > 0 ? 1 : -1 // Pointless
// Gotta have a color
const { STATE, COUNTY } = feature.properties
const c_stats = long_term_corona_counts.get(`${STATE}${COUNTY}-${date}`)
const pop = pops.get(`${STATE}${COUNTY}`)
if (c_stats == undefined) {continue}
const { delta, rolling } = c_stats
const rate = rolling/pop
const val = fips_data.get(STATE+COUNTY)*10
const magnitude = val/100000;
const watt_data = votes.get(STATE+COUNTY) || {diff: 0, t: .5, b: .5, total: 0}
watt_data.margin_per_area = watt_data.diff * watt_data.total/feature.properties.CENSUSAREA;
watt_data.votes_per_area = watt_data.total/(feature.properties.CENSUSAREA + 1e-10)
const election_result = watt_data.diff
const t = Math.sin(Date.now()/(600+rand[1]*10));
if (isNaN(delta)) {continue}
let color = d3.rgb(d3.interpolateRdBu((.5 - election_result/2)))
let {r, g, b} = color;
//let color = rate_of_change(delta)
//let [r, g, b] = color.split(/\(|\)/)[1].split(",").map(d => +d)
let x_shift = overall_radius * Math.sin(Date.now()/(600+rand[1]*10) + rand[0]) * flip;
let y_shift = overall_radius * Math.cos(Date.now()/(600+rand[1]*10) + rand[1]);
x_shift = feature.centroid[0] + x_shift * .1
y_shift = feature.centroid[1] + y_shift * .1
render({
color: [r/255, g/255, b/255],
zoom,
angle: (Date.now() - start_time)/100*magnitude * Math.exp(speed) * (election_result < 0 ? -1 : 1),
scale: scale_by_votes == "yes" ? size_scale(watt_data.votes_per_area) : scale_by_votes == "by margin" ? 2 * size_scale(Math.abs(watt_data.margin_per_area)) : 1,
position: feature.coord_buffer,
elements: feature.vertex_buffer,
translate: [x_shift, y_shift],
incidence: incidence

})
}
// Logging.
times.push(performance.now() - start)
const average = d3.mean(times.slice(times.length > 200 ? times.length - 200 : 0))
yield md`Average render time of ${d3.format(".2f")(average)} milliseconds for ${n_points} points`
}
}
Insert cell
Insert cell
class BufferHandler {
// simple data structure to post blocks of data to regl buffers.
// Rather than allocate a new buffer for each polygon, which is kind of wasteful,
// just set them up in 2 MB blocks and keep using until the next call will overflow.
// Something is wrong with the regl scoping here, so it breaks if you have more than one buffer.
// Currently, I just make sure that the buffer is crazy big--would be worth fixing, though.
constructor(regl, size = 2**26) {
this.regl = regl;
this.size = size;
this.buffers = {"1": regl.buffer({length: this.size, type: "float", usage: "static"})}
this.current_buffer = "1"
this.current_position = 0;
}
post_data(data, stride = 8) {
if (data.length*4 + this.current_position > this.size) {
this.current_buffer = (1+parseInt(this.current_buffer)) + ""
this.buffers[this.current_buffer] = this.regl.buffer(this.size);
this.current_position = 0;
}
const buffer = this.buffers[this.current_buffer]
buffer.subdata(data, this.current_position)
const description = {
key: this.current_buffer,
buffer: buffer,
stride: stride ? stride : 8,
offset: this.current_position
}
this.current_position += data.length * 4;
return description;
}
}
Insert cell
using = resolution == "1:500,000" ? null : resolution == "1:5,000,000" ? m5_features : m20_features
Insert cell
Insert cell
polygon_to_triangles = function(polygon) {
// Actually perform the earcut work
const el_pos = []
const coords = polygon.flat(2)
const vertices = earcut(...Object.values(earcut.flatten(polygon)))
return { coords, vertices }
}
Insert cell
fips_data = {
const map = new Map()
const d = await d3.csv("https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_daily_reports/11-10-2020.csv")
d.forEach(d => map.set(d.FIPS.padStart(5, "0"), +d.Incident_Rate))
return map
}
Insert cell
function shift_points(coords, [x, y]) {
if (coords.length) {
if (typeof(coords[0]) == "number") {
return [coords[0] - x, coords[1] - y]
} else {
return coords.map(d => shift_points(d, [x, y]))
}
}
}
Insert cell
add_triangles_to_feature = function(feature, projection, buffers) {
/*if (feature.projected) {
return
}*/
feature.projected = d3.geoProject(feature.geometry, projection)
if (!feature.projected) {
//console.log(feature.geometry)
return
}
feature.centroid = d3.geoPath().centroid(feature.projected)
feature.shifted = shift_points(feature.projected.coordinates, feature.centroid)
let coordinates;

if (feature.projected.type == "Polygon") {
coordinates = [feature.shifted]
} else if (feature.projected.type == "MultiPolygon") {
coordinates = feature.shifted
} else {throw "All elements must be polygons or multipolgyons."}
let all_coords = []
let all_vertices = []
for (let polygon of 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))
}
const coords = buffers.post_data(all_coords.flat(10), 8)
feature.coord_buffer = coords
feature.vertex_buffer = buffers.regl.elements({
primitive: "triangles",
count: all_vertices.length,
data: all_vertices.flat(10),
// Use the smallest possible int type.
type: all_coords.length < 2**8 ? 'uint8' : all_coords.length < 2**16 ? 'uint16' : 'uint32'
})
feature.vertex_buffer.data = all_vertices.flat(10)
}
Insert cell
election_results = new Map(Object.entries(data))
Insert cell
import { data } from '@kushleshkumar/a-better-way-to-visualize-us-elections-2020-results'
Insert cell
projection = d3.geoAlbersUsa().scale(2).translate([-0.2, 0])
Insert cell
buffer_handler = new BufferHandler(gl, 2**26) // I would prefer multiple at 2**20 to one giant one like this.
Insert cell
import { radio, slider } from '@jashkenas/inputs'
Insert cell
render = gl(renderer);
Insert cell
colorscheme = d3.scaleLinear().domain([0, 100]).range(d3.schemeBuGn)
Insert cell
m5_features = FileAttachment("gz_2010_us_050_00_5m.json").text().then(d => {
const geojson = JSON.parse(d)
for (let feature of geojson.features) {
add_triangles_to_feature(feature, projection, buffer_handler)
}
return geojson
})
Insert cell
m20_features = {
return FileAttachment("gz_2010_us_050_00_20m.json").text().then(d => {
const geojson = JSON.parse(d)
for (let feature of geojson.features) {
add_triangles_to_feature(feature, projection, buffer_handler)
}
return geojson
})
}
Insert cell
renderer = (
//Starting point: https://observablehq.com/@marcom13/mesh-rendering-using-webgl-regl
{
vert: `
precision mediump float;
attribute vec2 position;
uniform float aspect;
uniform float u_zoom;
uniform float u_scale;
uniform float u_incidence;
uniform float u_theta;
uniform vec3 color;
uniform vec2 translate;
varying vec3 fragColor;

mat2 rotate2d(float _angle){
return mat2(cos(_angle),-sin(_angle),
sin(_angle),cos(_angle));
}


void main () {
gl_PointSize = 2.;
vec2 rot;
fragColor = color;
if (u_incidence == 0.) {
rot=position*rotate2d(u_theta);
} else {
float angle = atan(position.x, position.y);
float magnitude = sqrt(dot(position, position));
float adjust = magnitude * (2. + sin(angle - u_theta)) / 2.;
rot = vec2(adjust * sin(angle), adjust * cos(angle));
}
gl_Position = vec4(
rot.x * u_scale * u_zoom + translate.x * u_zoom,
-rot.y * u_zoom * u_scale*aspect - translate.y*u_zoom*aspect, 0., 1.);
}
`,
frag: `
precision mediump float;
varying vec3 fragColor;
void main () {
gl_FragColor = vec4(fragColor, 1.);
}
`,
attributes: {
position: (state, props) => props.position
},
elements: function (state, props) {return props.elements},
uniforms: {
u_theta: (_, {angle}) => angle,
u_zoom: (_, {zoom}) => zoom * zoom,
u_scale: (_, {scale}) => scale,
u_incidence: (_, {incidence}) => incidence == "distortion" ? 1 : 0,
aspect: 975/610,
translate: (state, props) => props.translate,
color: (state, props) => props.color
},
primitive: "triangle",
}

)
Insert cell
topojson = require("topojson-client@3")
Insert cell
earcut = require("earcut")
Insert cell
arrow = require("apache-arrow")
Insert cell
raw_data = FileAttachment("full@2.feather").arrayBuffer()
Insert cell
feathered.get(1)
Insert cell
feathered = arrow.Table.from(raw_data)
Insert cell
table
Insert cell
table = aq.fromArrow(feathered, {unpack : true}).groupby("fips").orderby("date").derive({ rolling: aq.rolling(d => op.sum(d.cases), [-14, 0]) }).derive({"delta": d => d.rolling/op.lag(d.rolling, 14)})
Insert cell
table.groupby().sample(10).view()
Insert cell
long_term_corona_counts = {
return d3.rollup(table, r => {return {delta: r[0].delta, rolling: r[0].rolling}}, k => ("" + k.fips).padStart(5, "0") + "-" + k.date.toISOString().slice(0, 10))
}
Insert cell
import { pops } from '@codingwithfire/cmu-covidcast-api-bubbles-export'
Insert cell
i = ("" + table.column("fips").get(10))
Insert cell
table
Insert cell
import {aq, op} from '@uwdata/arquero'
Insert cell
regl = require("regl") // Use the latest Version
Insert cell
d3 = require("d3@v6", "d3-geo-projection")
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