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

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