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

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