Published
Edited
Jun 11, 2020
21 stars
Insert cell
Insert cell
Insert cell
canvas = DOM.canvas(width, height)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
md`# The DepthTree class

All the extensible work is handled through a depthtree class.

It works like RBush or RBush3d, but the points must have the following elements defined:

* \`height\`
* \`aspect_ratio\`
* \`x\` and \`y\` (or another point accessor method, defined by calling DepthTree.accessor)
`
Insert cell
class DepthTree extends RBush3D.RBush3D {
constructor(scale_factor = 0.5, zoom = [.1, 1000]) {
// scale factor used to determine how quickly points scale.
// Not implemented.
// size = exp(log(k) * scale_factor);
super()
this.scale_factor = scale_factor
this.mindepth = zoom[0]
this.maxdepth = zoom[1]
this._accessor = p => [p.x, p.y]
}
max_collision_depth(p1, p2) {
const [x1, y1] = this._accessor(p1)
const [x2, y2] = this._accessor(p2)
// The zoom factor after which two points do not collide with each other.
// First x
let diff = Math.abs(x1 - x2)
let extension = p1.aspect_ratio*p1.height/2 + p2.aspect_ratio*p2.height/2
let max_overlap = extension/diff
// Then y
diff = Math.abs(y1-y2)
extension = p1.height/2 + p2.height/2
max_overlap = Math.min(extension/diff, max_overlap)
return max_overlap
}
accessor(f) {
this._accessor = f
return this
}
to3d(point, zoom = 1, maxZ = undefined) {
// Each point should have a center, an aspect ratio, and a height.
// The height is the pixel height at a zoom level of one.
const [x, y] = this._accessor(point)
const {
aspect_ratio,
height
} = point;

let p = {
minX: x - (aspect_ratio * height)/zoom,
maxX: x + (aspect_ratio * height)/zoom,
minY: y - (height)/zoom,
maxY: y + (height)/zoom,
minZ: zoom,
maxZ: maxZ || this.maxdepth,
data: point
}
if (isNaN(height)) throw "Missing Height: " + JSON.stringify(point)
if (isNaN(x) || isNaN(y)) throw "Missing position" + JSON.stringify(point)
if (isNaN(aspect_ratio)) throw "Missing Aspect Ratio" + JSON.stringify(point)

return p
}
insert(point, mindepth = 1) {
const p3d = this.to3d(point, mindepth, this.maxdepth)
if (!this.collides(p3d)) {
if (mindepth <= this.mindepth) {
// It's visible from the minimum depth.
point._visible_from = mindepth;
super.insert(p3d)
} else {
// If we can't find the colliders, try inserting it twice as high up.
this.insert(point, mindepth/2)
}
} else {
this.insert_after_collisions(p3d)
}
}
insert_after_collisions(p3d) {
// The depth until which we're hidden.
let hidden_until = 0
// The node hiding this one.
let hidden_by = undefined
for (const overlapper of this.search(p3d)) {
// Find the most closely overlapping 3d block.
// Although the other ones will retain 3d blocks that extend all the way down to the
// bottom of the depth tree and so collide with this, it's guaranteed that their *data*
// will not. And it means we can avoid unnecessary trees.

let blocked_until = this.max_collision_depth(p3d.data, overlapper.data)
if (blocked_until > hidden_until) {
hidden_until = blocked_until
hidden_by = overlapper
}
}
if (hidden_by && hidden_until < this.maxdepth) {
// Remove the blocker and replace it by two new 3d rectangles.
const hid_data = hidden_by.data
const hid_start = hidden_by.minZ
this.remove(hidden_by)
// Down from here.
super.insert(this.to3d(hid_data, hidden_until))
// Up until this point.
super.insert(this.to3d(hid_data, hid_start, hidden_until))
// Insert the new point
p3d.data.blocked_by = hid_data
const revised_3d = this.to3d(p3d.data, hidden_until)
revised_3d.data._visible_from = hidden_until
super.insert(revised_3d)
}
}
}
Insert cell
height = width * .5
Insert cell
tree = {
const tree = new DepthTree(.5, [.1, 1000])
data.slice(0, N).map((datum, i) => tree.insert(datum, 1))
return tree
}
Insert cell
Insert cell
md`## Canvas BBox code

Calculating bboxes for text is kind of a pain. This method isn't perfect.
`
Insert cell
scratch = DOM.canvas(300, 100)
Insert cell
function set_word_bbox(context, d) {
// Called for the side-effect of setting `d.aspect_ratio` on the passed item.

const ms = context.measureText(d.text)
//context.clearRect(0, 0, 300, 100)
//context.fillText(d.text, 50, 20)
if (isNaN(ms.actualBoundingBoxLeft) || ms.actualBoundingBoxLeft === undefined) {
// Some browsers don't support the full standard.
ms.actualBoundingBoxLeft = ms.width/2
ms.actualBoundingBoxRight = ms.width/2
// Hard coded at 6px
ms.actualBoundingBoxAscent = 4
ms.actualBoundingBoxDescent = 4
}
d.aspect_ratio = (ms.actualBoundingBoxLeft + ms.actualBoundingBoxRight)/
(ms.actualBoundingBoxAscent + ms.actualBoundingBoxDescent)
// For inspection only.
return [d.text, d.aspect_ratio]
}
Insert cell
d3 = require("d3@5")
Insert cell
RBush3D = require('https://bundle.run/rbush-3d@0.0.4')

Insert cell
import {radio} from "@jashkenas/inputs"

Insert cell
md`## Utility functions I may not be using`
Insert cell
pull = function(word) {
return data.filter(d => d.text == word)[0]
}
Insert cell
words = d3.text("https://gist.githubusercontent.com/bmschmidt/7a903432d42606ead4c22b677646df1a/raw/0feb5962658ac8b92c6a33971973357333b6fe53/wordlist.txt").then(d => d.split("\n"))
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