Published
Edited
Nov 11, 2020
2 forks
5 stars
Insert cell
Insert cell
myCanvas = DOM.canvas(975, 610)
Insert cell
Insert cell
viewof zoom = slider({title: "zoom (square root)", min: .5, max: 15, value: 1, step: .1})
Insert cell
Insert cell
render_loop = {
const times = []
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, 0.5]});
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 val = fips_data.get(STATE+COUNTY)*10
let color = d3.rgb(d3.interpolateMagma(val/100000))
render({
color: [color.r/255, color.g/255, color.b/255],
zoom,
position: feature.coord_buffer,
elements: feature.vertex_buffer,
jitter: [overall_radius * Math.sin(Date.now()/(600+rand[1]*10) + rand[0]) * flip, overall_radius * Math.cos(Date.now()/(600+rand[1]*10) + rand[1])]// Stupid.

})
}
// 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
n_points = using.features.map(d => d.geometry.coordinates).flat(12).length/2
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" ? k500_counties : 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
add_triangles_to_feature = function(feature, projection, buffers) {
if (feature.projected) {
return
}
let projected = d3.geoProject(feature.geometry, projection)
feature.projected = projected;
let coordinates;
if (!projected) {
//console.log(feature.geometry)
return
}
if (projected.type == "Polygon") {
coordinates = [projected.coordinates]
} else if (projected.type == "MultiPolygon") {
coordinates = projected.coordinates
} 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
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 = {
return 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 vec3 color;
uniform vec2 jitter;
varying vec3 fragColor;

void main () {
gl_PointSize = 2.;
fragColor = color;
gl_Position = vec4(position.x * u_zoom + jitter.x, -position.y * u_zoom * aspect + jitter.y * 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_zoom: (_, {zoom}) => zoom * zoom,
aspect: 975/610,
jitter: (state, props) => props.jitter,
color: (state, props) => props.color
},
primitive: "triangle",
}

)
Insert cell
topojson = require("topojson-client@3")
Insert cell
k500_counties = {
const f = topojson.feature(hirestopo, hirestopo.objects.gz_2010_us_050_00_500k)
for (let feature of f.features) {
add_triangles_to_feature(feature, projection, buffer_handler)
}
return f
}
Insert cell
hirestopo = FileAttachment("oput.topojson").text().then(d => JSON.parse(d))
Insert cell
earcut = require("earcut")
Insert cell
regl = require("regl") // Use the latest Version
Insert cell
d3 = require("d3@v6", "d3-geo-projection")
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more