Published
Edited
Dec 11, 2020
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
map.color_func = function(f) {
if (colorby == "State") {
let {r, g, b} = d3.rgb(d3.interpolateSinebow(+f.properties.STATEFP/80))
if (isNaN(r)) {
r = .5, g = .5 + Math.random() * .1, b = .4
}
return [r/255, g/255, b/255]
}
if (f._index_color) {return f._index_color}
f._index_color = random_pastel()
return f._index_color
}
Insert cell
{
const frame = map.regl.frame(() => {
if (automatic=="automatic") {

viewof pixels.setValue((Math.sin(performance.now()/5e2) + 1.01) * 6)
}
state.decay = Math.exp(Math.log(1/255)/pixels)
map.tick("triangles")
})
invalidation.then(() => frame.cancel())
}
Insert cell
stieler = new Image(await FileAttachment("stieler.png"))
Insert cell
import {fullscreen} from "@fil/fullscreen"
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
Insert cell
md`## Map projection

This is all in d3-geo-albersUsa.

`
Insert cell
state = ({})
Insert cell
Insert cell
Insert cell
congressional_districts = new TriFeather(await FileAttachment("congressional@5.trifeather").arrayBuffer())
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
Insert cell
scale_color = d3.scaleOrdinal(d3.schemeCategory10)
Insert cell
color_scale = (d) => {const {r, g, b} = d3.rgb(scale_color(d)); return [r/255, g/255, b/255]}
Insert cell
fullscreen(div, {center: true})

Insert cell
Insert cell
arrow = require('apache-arrow')
Insert cell
earcut = require('earcut')
Insert cell
clip = {}
Insert cell
function random_pastel() {
const scheme = d3.schemeDark2
const {r, g, b} = d3.rgb(scheme[Math.floor(Math.random() * scheme.length)])
return [r + Math.random() * 10 - 5, g + Math.random() * 10 - 5, b + Math.random() * 10 - 5].map(d => d/255)
}
Insert cell
colorscale = d3.scaleSequential(d3.interpolateRdBu).domain([0.05, .95])
Insert cell
map = new TriMap(div, [congressional_districts])

Insert cell
md`# TriMap

A handler class to deal with interactions between regl contexts and feather feature sets. I'll use this more later.`
Insert cell
gaussian_blur = `precision mediump float;

vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
vec4 color = vec4(0.0);
vec2 off1 = vec2(1.411764705882353) * direction;
vec2 off2 = vec2(3.2941176470588234) * direction;
vec2 off3 = vec2(5.176470588235294) * direction;
color += texture2D(image, uv) * 0.1964825501511404;
color += texture2D(image, uv + (off1 / resolution)) * 0.2969069646728344;
color += texture2D(image, uv - (off1 / resolution)) * 0.2969069646728344;
color += texture2D(image, uv + (off2 / resolution)) * 0.09447039785044732;
color += texture2D(image, uv - (off2 / resolution)) * 0.09447039785044732;
color += texture2D(image, uv + (off3 / resolution)) * 0.010381362401148057;
color += texture2D(image, uv - (off3 / resolution)) * 0.010381362401148057;
return color;
}

uniform vec2 iResolution;
uniform sampler2D iChannel0;
uniform vec2 direction;

void main() {
vec2 uv = vec2(gl_FragCoord.xy / iResolution.xy);
gl_FragColor = blur13(iChannel0, uv, iResolution.xy, direction);
}`
Insert cell
Insert cell
alpha_color_merge = `
precision mediump float;
varying vec2 uv;
uniform sampler2D color;
uniform sampler2D alpha;
uniform float wRcp, hRcp;
void main() {
vec4 col = texture2D(color, uv);
vec4 alph = texture2D(alpha, uv);
float a = alph.a;
if (a < 1./255.) {
discard;
} else if (col.a == 0.) {
discard;
if (sin(a*3.14*8.) > 0.9) {
a = .9;
} else {
discard;
}
} else if (a < .99) {
a = .25;
} else {
a = 1.;
}
gl_FragColor = vec4(col.rgb * a, a);
}
`
Insert cell
edge_detection = `
precision mediump float;
varying vec2 uv;
uniform sampler2D tex;
uniform float wRcp, hRcp;
void main() {
// 4 adjacent pixels; left, right, up down.
vec4 l = texture2D(tex, uv + vec2(-wRcp, 0.));
vec4 r = texture2D(tex, uv + vec2(wRcp, 0.));
vec4 u = texture2D(tex, uv + vec2(0., hRcp));
vec4 d = texture2D(tex, uv + vec2(0., -hRcp));
vec4 around = (l + r + u + d) / 4.;
vec4 current = texture2D(tex, uv);
if (distance(around, current) < 0.001) {
gl_FragColor = vec4(0., 0., 0., 0.);
} else {
gl_FragColor = vec4(0., 0., 0., 1.);
}
}
`
Insert cell
simple_shader = function(regl, frag_shader = edge_detection, blend = false) {
return regl({
blend: {
enable: blend,
func: {
srcRGB: 'one',
srcAlpha: 'one',
dstRGB: 'one minus src alpha',
dstAlpha: 'one minus src alpha',
}
},
frag: frag_shader,
vert: `
precision mediump float;
attribute vec2 position;
varying vec2 uv;
void main() {
uv = 0.5 * (position + 1.0);
gl_Position = vec4(position, 0, 1);
}
`,
attributes: {
position: [ -4, -4, 4, -4, 0, 4 ]
},
depth: { enable: false },
count: 3,
uniforms: {
u_decay: (_, {decay}) => decay,
tex: (_, {layer}) => layer,
color: (_, {color}) => color,
alpha: (_, {alpha}) => alpha,
wRcp: ({viewportWidth}) => {return 1.0 / viewportWidth},
hRcp: ({viewportHeight}) => 1.0 / viewportHeight
},
})
}
Insert cell
Insert cell
map.layers[1]
Insert cell
class TriMap {

constructor(div, layers) {

this.div = div
this.regl = wrapRegl({canvas: div, extensions: ["OES_element_index_uint"]})
for (let layer of layers) {
layer.bind_to_regl(this.regl)
}
this.layers = layers;

const {width, height} = div
this.width = width
this.height = height
this.set_magic_numbers()
this.prepare_div(width, height)
this.set_renderer()
//this.regl.frame(() => this.tick())
}
add_layer(layer) {
layer.bind_to_regl(this.regl)
this.layers.push(layer)
}

fbo(name) {
this.buffers = this.buffers || new Map()
if (this.buffers.get(name)) {
return this.buffers.get(name)
}
const fbo = this.regl.framebuffer({
width: this.width,
height: this.height,
stencil: false
})
this.buffers.set(name, fbo)
return this.buffers.get(name)
}
set_magic_numbers() {
const { layers, width, height } = this;

const extent = layers[0].bbox;

const scales = {};

const scale_dat = {'x': {}, 'y': {}}

for (let [name, dim] of [['x', width], ['y', height]]) {
const limits = extent[name]
scale_dat[name].limits = limits;
scale_dat[name].size_range = limits[1] - limits[0]
scale_dat[name].pixels_per_unit = dim / scale_dat[name].size_range
}

const data_aspect_ratio =
scale_dat.x.pixels_per_unit / scale_dat.y.pixels_per_unit

let x_buffer_size = 0, y_buffer_size = 0,
x_target_size = width, y_target_size = height;
if (data_aspect_ratio > 1) {
// There are more pixels in the x dimension, so we need a buffer
// around it.
x_target_size = width / data_aspect_ratio;
x_buffer_size = (width - x_target_size)/2
} else {
y_target_size = height * data_aspect_ratio;
y_buffer_size = (height - y_target_size)/2
}


scales.x =
d3.scaleLinear()
.domain(scale_dat.x.limits)
.range([x_buffer_size, width-x_buffer_size])

scales.y =
d3.scaleLinear()
.domain(scale_dat.y.limits)
.range([y_buffer_size, height-y_buffer_size])
this.magic_numbers = window_transform(
scales.x,
scales.y, width, height)
.map(d => d.flat())

}

prepare_div(width, height) {
this.zoom = {transform: {k: 1, x: 0, y:0}}
d3.select(this.div)
.call(d3.zoom().extent([[0, 0], [width, height]]).on("zoom", (event, g) => {
this.zoom.transform = event.transform
}));

return div;
}

get size_func() {
return this._size_function ? this._size_function : () => 1
}

set size_func(f) {
this._size_function = f
}

set color_func(f) {
this._color_function = f
}

get index_color() {
return function(f) {
if (f._index_color) {return f._index_color}
f._index_color = [0, 1, 2].map(d => 1 / 255 * Math.floor(Math.random() * 255))
return f._index_color
}
}
get color_func() {
return this._color_function ? this._color_function : () => [.8, .8, .8]

}
single_blur_pass(fbo1, fbo2, direction) {
const { regl } = this;
fbo2.use( () => {
regl.clear({color: [0, 0, 0, 0]});
regl(
{
frag: gaussian_blur,
uniforms: {
iResolution: ({viewportWidth, viewportHeight}) => [viewportWidth, viewportHeight],
iChannel0: fbo1,
direction
},
/* blend: {
enable: true,
func: {
srcRGB: 'one',
srcAlpha: 'one',
dstRGB: 'one minus src alpha',
dstAlpha: 'one minus src alpha',
},
}, */
vert: `
precision mediump float;
attribute vec2 position;
varying vec2 uv;
void main() {
uv = 0.5 * (position + 1.0);
gl_Position = vec4(position, 0, 1);
}`,
attributes: {
position: [ -4, -4, 4, -4, 0, 4 ]
},
depth: { enable: false },
count: 3,
})()
})
}

blur(fbo1, fbo2, passes = 5
) {
let remaining = passes - 1
while (remaining > -1) {
this.single_blur_pass(fbo1, fbo2, [2 ** remaining, 0])
this.single_blur_pass(fbo2, fbo1, [0, 2 ** remaining])
remaining -= 1
}
}
draw_edges(layer) {
const {regl} = this;
const colors = this.fbo("colorpicker")
const edges = this.fbo("edges")
colors.use(d => {
this.regl.clear({color: [0, 0, 0, 0]})
this.poly_tick(layer)
})
edges.use(() => {
this.regl.clear({color: [1, 1, 1, 1]})

const shader = simple_shader(this.regl, edge_detection)
shader({layer: colors})
})
// Copy the edges to a ping-pong shader to be blurred.
const pingpong = [this.fbo("ping"), this.fbo("pong")]
const copier = simple_shader(this.regl, copy_shader)
let alpha = state.decay;
const decay = state.decay;
pingpong[0].use(() => {
regl.clear({color: [0, 0, 0, 0]})
copier({layer: edges})
})
const edge_propagator = simple_shader(this.regl, edge_propagation)
while (alpha > 1/255) {
pingpong[1].use(() => {
regl.clear({color: [0, 0, 0, 0]})
edge_propagator({layer: pingpong[0], decay: decay})
})
// swap the buffers.
alpha *= decay
pingpong.reverse()
}
const final_shade = simple_shader(this.regl, alpha_color_merge, true)
// First copy the blur
final_shade({alpha: pingpong[0], color: colors})
}


point_tick() {
const { regl } = this;
const calls = []
// multiple interleaved tranches prevent Trump or Biden from always being on top. This is
// an issue with Williamson's maps, which over-represent the Hispanic population of DC because it
// gets plotted last.

const alpha_scale = d3.scaleSqrt().domain([0, 500]).range([0, 1])
const n_tranches = 1
for (let offset of d3.range(n_tranches)) {
for (let pointset of this.random_points) {
calls.push({
position: pointset.buffer,
offset: offset * 8,
stride: 8 * n_tranches,
transform: this.zoom.transform,
// Drops the last point in each tranch--needs a modulo operation to know how
// many to expect.
count: Math.floor(pointset.count/n_tranches),
color: color_scale(pointset.label),
centroid: [0, 0],
size: this.point_size ? this.point_size : 1,
alpha: this.point_opacity > 1/255 ? this.point_opacity : 1/255
})
}
}
const random = d3.randomLcg(0.9051667019185816);
const shuffle = d3.shuffler(random);
shuffle(calls)
this.render_points(calls)
}

tick(wut) {
const { regl } = this
regl.clear({
color: [1, 1, 1, 1],
})
const alpha = 1
if (wut === "points") {
this.point_tick()
} else {
this.draw_edges(this.layers[0])
/* this.fbo("points").use(d => {
regl.clear({
color: [0, 0, 0, 0]
})
// this.point_tick()
})
*/
//const copier = simple_shader(this.regl, copy_shader, true)
//copier({layer: this.fbo("points")})
}

}


poly_tick(layer) {
const { regl } = this;
const calls = []
for (let feature of layer) {
//if (feature.properties['2020_tot'] === null) {continue}
const {vertices, coords} = feature;
calls.push({
transform: this.zoom.transform,
color: this.color_func(feature),
centroid: [feature.properties.centroid_x, feature.properties.centroid_y],
size: this.size_func(feature),
alpha: 1,
vertices: vertices,
coords: coords
})
}
this.render_polygons(calls)
}

get vertex_shader() {return `
precision mediump float;
attribute vec2 position;
uniform float u_size;
uniform vec2 u_centroid;
varying vec4 fragColor;
uniform float u_k;
uniform float u_time;
uniform vec3 u_color;
varying vec4 fill;

// Transform from data space to the open window.
uniform mat3 u_window_scale;
// Transform from the open window to the d3-zoom.
uniform mat3 u_zoom;
uniform mat3 u_untransform;
uniform float u_scale_factor;

// We can bundle the three matrices together here for all shaders.
mat3 from_coord_to_gl = u_window_scale * u_zoom * u_untransform;



void main () {

// scale to normalized device coordinates
// gl_Position is a special variable that holds the position
// of a vertex

vec2 from_center = position-u_centroid;


vec3 p = vec3(from_center * u_size + u_centroid, 1.) * from_coord_to_gl;
gl_Position = vec4(p, 1.0);

gl_PointSize = u_size * (exp(log(u_k)*u_scale_factor));

fragColor = vec4(u_color.rgb, 1.);
//gl_Position = vec4(position / vec2(1., u_aspect), 1., 1.);
}
`}

set_renderer() {
this.render_polygons = this.regl(this.renderer())
this.render_points = this.regl(this.renderer("points"))
}

get point_frag() { return `
precision highp float;
uniform float u_alpha;
varying vec4 fragColor;

void main() {
vec2 coord = gl_PointCoord;
vec2 cxy = 2.0 * coord - 1.0;
float r_sq = dot(cxy, cxy);
if (r_sq > 1.0) {discard;}

gl_FragColor = fragColor * u_alpha;
}`}

get triangle_frag() { return `
precision highp float;
uniform float u_alpha;
varying vec4 fragColor;

void main() {
gl_FragColor = fragColor * u_alpha;
}`}
renderer(wut = "polygons") {
const { regl, magic_numbers } = this;
const definition = {
depth: {
enable: false
},
blend: {enable: true, func: {
srcRGB: 'one',
srcAlpha: 'one',
dstRGB: 'one minus src alpha',
dstAlpha: 'one minus src alpha',
}
},
vert: this.vertex_shader,
frag: wut == 'polygons' ? this.triangle_frag : this.point_frag,
attributes: {
position: wut == "polygons" ?
(_, {coords}) => coords:
(_, {position, stride, offset}) => {return {buffer: position, offset , stride}}
},
count: regl.prop("count"),
elements: wut == "polygons" ? (_, {vertices}) => vertices : undefined,
uniforms: {
u_time: (context, _) => performance.now()/500,
u_scale_factor: () => this.scale_factor ? this.scale_factor : .5,

u_k: function(context, props) {

return props.transform.k
},
u_centroid: propd("centroid", [0, 0]),
u_color: (_, {color}) => color ? color : [.8, .9, .2],
u_window_scale: magic_numbers[0].flat(),
u_untransform: magic_numbers[1].flat(),
u_zoom: function(context, props) {
const g = [
// This is how you build a transform matrix from d3 zoom.
[props.transform.k, 0, props.transform.x],
[0, props.transform.k, props.transform.y],
[0, 0, 1],
].flat()
return g
},
u_alpha: (_, {alpha}) => alpha ? alpha : 1,
u_size: (_, {size}) => size || 1,

},
primitive: wut == "polygons" ? "triangles" : "points"
}
if (wut === "polygons") {
delete definition['count']
}
return definition
}


}
Insert cell
m = new Uint16Array([1, 2, 3, 4, 5, 6])
Insert cell
function propd(string, def) {
return (_, props) => {
if (props[string] !== undefined) {return props[string]}
return def
}
}
Insert cell
class TriFeather {

constructor(bytes) {
this.bytes = bytes
this.t = arrow.Table.from(bytes)
}

get n_coords() {
this.coord_buffer;
return this._n_coords;
}

get coord_buffer() {
if (this._coord_buffer) {
return this._coord_buffer
}
const d = this.t.get(0).vertices;
this._coord_bytes = d.byteOffset
this._n_coords = (d.byteLength/4/2)
this._coord_buffer = new DataView(d.buffer, d.byteOffset, d.byteLength)
return this._coord_buffer
}
static polygon_to_triangles(polygon) {
// Actually perform the earcut work on a polygon.
const el_pos = []
const coords = polygon.flat(2)
const vertices = earcut(...Object.values(earcut.flatten(polygon)))
return { coords, vertices }
}

static from_feature_collection(feature_collection,
projection,
options = {dictionary_threshold: .75, clip_to_sphere: false}) {

if (projection === undefined) {throw "Must define a projection"}
// feature_collections: a (parsed) geoJSON object.
// projection: a d3.geoProjection instance;
// eg, d3.geoMollweide().transform([10, 20])
// options:

const properties = new Map()
// Stores the number of bytes used for the coordinates.
const coord_resolutions = [null]
const coord_buffer_offset = [null]
// centroids let you have fun with shapes. Store x and y separately.
const centroids = [[null], [null]]
const bounds = [null]
// Storing areas makes it possible to weight centroids.
const areas = [null]
let i = -1;

const path = d3.geoPath()
let clip_shape;

let projected = d3.geoProject(feature_collection, projection)
if (options.clip_to_sphere) {
clip_shape = d3.geoProject({"type": "Sphere"}, projection)
for (let feature of projected.features) {
const new_coords = clip.intersection(feature.coordinates, clip_shape.coordinates)
if (projected.type == "Polygon" && typeof(new_coords[0][0][0] != "numeric")) {
projected.type = "MultiPolygon"
}
feature.coordinates = new_coords
}
}
const {indices, points} = this.lookup_map_and_coord_buffer(projected)
const coord_indices = indices;
const coord_codes = points;

// Stash the vertices in the first item of the array.
const vertices = [new Uint8Array(coord_codes.buffer)]
properties.set("id", ["Dummy feather row"])

i = 0;
for (let feature of projected.features) {
// start at one; the first slot is reserved for caching the full
// feature list
i++;
properties.get("id")[i] = feature.id || `Feature_no_${i}`

for (let [k, v] of Object.entries(feature.properties)) {
if (!properties.get(k)) {properties.set(k, [])}
properties.get(k)[i] = v
}

const projected = feature.geometry
const [x, y] = path.centroid(projected)
const bbox = new Float32Array(path.bounds(projected).flat())

centroids[0][i] = x; centroids[1][i] = y
areas[i] = path.area(projected)
bounds[i] = bbox
let loc_coordinates;
if (projected === null) {
console.warn("Error on", projected)
coord_resolutions[i] = null
vertices[i] = null
continue
} else if (projected.type == "Polygon") {
loc_coordinates = [projected.coordinates]
} else if (projected.type == "MultiPolygon") {
loc_coordinates = projected.coordinates
} else {
throw "All elements must be polygons or multipolgyons."
}
let all_coords = []
let all_vertices = []
for (let polygon of loc_coordinates) {
const { coords, vertices } = TriFeather.polygon_to_triangles(polygon);
// Allow coordinate lookups by treating them as a single 64-bit int.
const bigint_coords = new BigInt64Array(new Float32Array(coords.flat(3)).buffer);
// Reduce to the indices of the master lookup table.
const lookup_points = vertices.map(vx => coord_indices.get(bigint_coords[vx]))
all_vertices.push(...lookup_points)
}
const [start, end] = d3.extent(all_vertices)
const diff = end - start

coord_buffer_offset[i] = (start)

// Normalize the vertices around the lowest element.
// Allows some vertices to be stored at a lower resolution.
for (let j=0; j<all_vertices.length; j++) {
all_vertices[j] = all_vertices[j]-start
}

// Determine the type based on the offset.
let MyArray
if (diff < 2**8) {
coord_resolutions[i] = 8
MyArray = Uint8Array
} else if (diff < 2**16) {
coord_resolutions[i] = 16
MyArray = Uint16Array
} else {
// Will not allow more than 4 billion points on a single feature,
// should be fine.
coord_resolutions[i] = 32
MyArray = Uint32Array
}
vertices[i] = MyArray.from(all_vertices)
}

const cols = {
"vertices": this.pack_binary(vertices),
"bounds": this.pack_binary(bounds),
"coord_resolution": arrow.Uint8Vector.from(coord_resolutions),
"coord_buffer_offset": arrow.Uint32Vector.from(coord_buffer_offset),
"pixel_area": arrow.Float64Vector.from(areas),
"centroid_x": arrow.Float32Vector.from(centroids[0]),
"centroid_y": arrow.Float32Vector.from(centroids[1])
}

for (const [k, v] of properties.entries()) {
if (k in cols) {
// silently ignore.
//throw `Duplicate column names--rename ${k} `;
}
cols[k] = arrow.Vector.from({nullable: true, values: v, type: this.infer_type(v, options.dictionary_threshold)})
}

const named_columns = []
for (const [k, v] of Object.entries(cols)) {
// console.log(k, v)
named_columns.push(arrow.Column.new(k, v))
}
const tab = arrow.Table.new(...named_columns)

const afresh = tab.serialize()
return new TriFeather(afresh)

}


static infer_type(array, dictionary_threshold = .75) {
// Certainly reinventing the wheel here--
// determine the most likely type of something based on a number of examples.

// Dictionary threshold: a number between 0 and one. Character strings will be cast
// as a dictionary if the unique values of the array are less than dictionary_threshold
// times as long as the length of all (not null) values.
const seen = new Set()
let strings = 0
let floats = 0
let max_int = 0

for (let el of array) {

if (Math.random() > 200/array.length) {continue} // Only check a subsample for speed. Try
// to get about 200 instances for each row.
if (el === undefined || el === null) {
continue
}
if (typeof(el) === "string") {
strings += 1
seen.add(el)
} else if (typeof(el) === "number") {
if (el % 1 > 0) {
floats += 1
} else if (isFinite(el)) {
max_int = Math.max(Math.abs(el), max_int)
} else {

}
} else {
throw `No behavior defined for type ${typeof(el)}`
}
}
if ( strings > 0 ) {
// moderate overlap
if (seen.length < strings.length * .75) {
return new arrow.Dictionary(new arrow.Utf8(), new arrow.Int32())
} else {
return new arrow.Utf8()
}
}
if (floats > 0) {
return new arrow.Float32()
}
if (Math.abs(max_int) < 2**8) {
return new arrow.Int8()
}
if (Math.abs(max_int) < 2**16) {
return new arrow.Int16()
}
if (Math.abs(max_int) < 2**32) {
return new arrow.Int32()
} else {
return new arrow.Int64()
}

}


coord(ix) {
// NB this manually specifies little-endian, although
// Arrow can potentially support big-endian frames under
// certain (future?) circumstances.
return [
this.coord_buffer.getFloat32(ix*4*2, true),
this.coord_buffer.getFloat32(ix*2*4 + 4, true)
]
}
static pack_binary(els) {
const { Builder, Binary } = arrow;
const binaryBuilder = Builder.new({
type: new Binary(),
nullValues: [null, undefined],
highWaterMark: 2**16
});
for (let el of els) { binaryBuilder.append(el) }
return binaryBuilder.finish().toVector()
}


bind_to_regl(regl) {
this.regl = regl
this.element_handler = new Map();
// Elements can't share buffers (?) so just use a map.
this.regl_coord_buffer = regl.buffer(
{data: this.t.get(0).vertices, type: "float", usage: "static"})
this.prepare_features_for_regl()
}
prepare_features_for_regl() {
this.features = []
const {t, features, regl, element_handler, regl_coord_buffer} = this;
// Start at 1, not zero, to avoid the dummy.
for (let ix = 1; ix<this.t.length; ix++) {
const feature = this.t.get(ix)
if (feature.vertices === null) {
continue
}
element_handler.set(ix, this.regl.elements({
primitive: 'points',
usage: 'static',
data: feature.vertices,
type: "uint" + feature.coord_resolution,
length: feature.vertices.length, // in bytes
count: feature.vertices.length / feature.coord_resolution * 8
}))
const f = {
ix,
vertices: element_handler.get(ix),
coords: {buffer: this.regl_coord_buffer, stride: 8, offset: feature.coord_buffer_offset * 8},
properties: feature
}; // Other data can be bound to this object if desired, which makes programming easier than
// working off the static feather frame.
features.push(f)
}
}
get bbox() {
if (this._bbox) {return this._bbox}
this._bbox = {
x: d3.extent(d3.range(this.n_coords).map(i => this.coord(i)[0])),
y: d3.extent(d3.range(this.n_coords).map(i => this.coord(i)[1])),
}
return this._bbox
}
*[Symbol.iterator]() {
for (let feature of this.features) {
yield feature
}
}
static lookup_map_and_coord_buffer (geojson) {
const all_coordinates = new Float64Array(geojson.features.filter(d => d.geometry).map(d => d.geometry.coordinates).flat(4))
const feature_collection = geojson
const codes = new Float64Array(all_coordinates.buffer)
const indices = new Map()
for (let code of codes) {
if (!indices.has(code)) {
indices.set(code, indices.size)
}
}
const points = new Float64Array(indices.size)
for (let [k, v] of indices.entries()) {
points[v] = k
}
return {indices, points}
}
}
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
wrapRegl = require("regl")
Insert cell
Insert cell
d3 = require("d3@v6")
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