Unlisted
Edited
Dec 13, 2024
3 stars
Insert cell
Insert cell
Insert cell
mutable debug = null
Insert cell
{
const updates = [];

const chart = Plot.plot({
width,
height,
projection,
marks: [
Plot.sphere({ fill: "white", render }),
Plot.geo([land50, land110], {
fill: "currentColor",
render: renderAlternative
}),
Plot.sphere({ render }),
// Plot.dot([[0, 0]], { fill: "red", render }),
Plot.line([[0, 0], [10, 10]], { stroke: "red", curve: "auto", render }),
Plot.graticule({render}),
Plot.frame({ stroke: "none", pointerEvents: "all" })
]
});

// https://observablehq.com/@d3/testing-projection-visibility
function tester(projection, X, Y) {
let visible;
const stream = projection.stream({point() { visible = true; }});
return (i) => ((visible = false), stream.point(X[i], Y[i]), visible);
}

// Replaces the output when zooming; this also covers marks such as Plot.dot that have x and y channels
function render(i, s, v, d, c, n) {
const index = d3.range((v.geometry??v.x).length);
let g = n(index, s, v, d, c);
if (v.x && v.y &&!this.curve) {
const X = v.channels.x.value;
const Y = v.channels.y.value;
updates.push(() => {
i = index.filter(tester(projection, X, Y));
for (const j of i) [v.x[j], v.y[j]] = projection([X[j], Y[j]]);
g.replaceWith((g = n(i, s, v, d, c)));
});
} else {
updates.push(() => g.replaceWith((g = n(i, s, v, d, c))));
}
return g;
}

// This alternative (for Plot.geo only) renders the second or first geometry,
// depending on whether the zoom is active
function renderAlternative(i, s, v, d, c, n) {
let g = n([0], s, v, d, c);
updates.push((active) => g.replaceWith((g = n([active], s, v, d, c))));
return g;
}

return d3
.select(chart)
.call(
zoom(projection)
.on("zoom.render", () => updates.forEach((update) => update(1)))
.on("end.render", () => updates.forEach((update) => update(0)))
)
.node();
}
Insert cell
function zoom(projection, {
// Capture the projection’s original scale, before any zooming.
scale = projection._scale === undefined
? (projection._scale = projection.scale())
: projection._scale,
scaleExtent = [0.8, 8]
} = {}) {
let v0, q0, r0, a0, tl;

const zoom = d3.zoom()
.scaleExtent(scaleExtent.map(x => x * scale))
.on("start", zoomstarted)
.on("zoom", zoomed);

function point(event, that) {
const t = d3.pointers(event, that);

if (t.length !== tl) {
tl = t.length;
if (tl > 1) a0 = Math.atan2(t[1][1] - t[0][1], t[1][0] - t[0][0]);
zoomstarted.call(that, event);
}

return tl > 1
? [
d3.mean(t, p => p[0]),
d3.mean(t, p => p[1]),
Math.atan2(t[1][1] - t[0][1], t[1][0] - t[0][0])
]
: t[0];
}

function zoomstarted(event) {
v0 = versor.cartesian(projection.invert(point(event, this)));
q0 = versor((r0 = projection.rotate()));
}

function zoomed(event) {
projection.scale(event.transform.k);
const pt = point(event, this);
const v1 = versor.cartesian(projection.rotate(r0).invert(pt));
const delta = versor.delta(v0, v1);
let q1 = versor.multiply(q0, delta);

// For multitouch, compose with a rotation around the axis.
if (pt[2]) {
const d = (pt[2] - a0) / 2;
const s = -Math.sin(d);
const c = Math.sign(Math.cos(d));
q1 = versor.multiply([Math.sqrt(1 - s * s), 0, 0, c * s], q1);
}

projection.rotate(versor.rotation(q1));
// In vicinity of the antipode (unstable) of q0, restart.
if (delta[0] < 0.7) zoomstarted.call(this, event);
}

return Object.assign(selection => selection
.property("__zoom", d3.zoomIdentity.scale(projection.scale()))
.call(zoom), {
on(type, ...options) {
return options.length
? (zoom.on(type, ...options), this)
: zoom.on(type);
}
});
}
Insert cell
projection = d3[projectionName]().precision(0.1)
Insert cell
height = {
const [[x0, y0], [x1, y1]] = d3.geoPath(projection.fitWidth(width, sphere)).bounds(sphere);
const dy = Math.ceil(y1 - y0), l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale(projection.scale() * (l - 1) / l).precision(0.2);
return dy;
}
Insert cell
sphere = ({type: "Sphere"})
Insert cell
land50 = FileAttachment("land-50m.json").json().then(world => topojson.feature(world, world.objects.land))
Insert cell
land110 = FileAttachment("land-110m.json").json().then(world => topojson.feature(world, world.objects.land))
Insert cell
versor = require("versor@0.0.4")
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