Public
Edited
May 14, 2023
1 fork
51 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// this is a very classic function to draw a globe, not much to see there except the call to "zoom"
globe = function(
projection,
title,
{ invalidation, visibility, width = 300, height = 300 } = {}
) {
const context = context2d(width, height),
path = d3
.geoPath()
.projection(
projection
.precision(0.1)
.fitSize([width - 4, height - 4], { type: "Sphere" })
)
.context(context);

projection.translate(projection.translate().map(d => d + 2));

function drawText(text) {
context.save();
context.translate(4, 14);
context.font = "bold 9pt sans-serif";
context.shadowBlur = 3;
context.strokeStyle = context.shadowColor = "white";
context.shadowOffs = 0;
context.strokeText(text, 0, 0);
context.fillText(text, 0, 0);
context.restore();
}

function draw() {
const r = projection.rotate();
context.clearRect(0, 0, width, height);
context.beginPath();
path(d3.geoGraticule10());
context.lineWidth = 0.25;
context.stroke();
context.beginPath();
path(land);
context.fill();
context.beginPath();
path({ type: "Sphere" });
context.lineWidth = 1.5;
context.stroke();

drawText(title);
}

return d3
.select(context.canvas)
.classed("globe", true)
.call(
zoom(projection, draw, {
target: context.canvas,
invalidation,
visibility
})
)
.on("pointerenter pointerdown pointerup", event => {
d3.select(event.currentTarget).style(
"cursor",
event.type === "pointerdown" ? "grabbing" : "grab"
);
})
.node();
}
Insert cell
// this is where the magic happens:
// we take the versor-zooming component (renamed vzoom),
// and wrap it with the synchronization logic
zoom = {
// a communication channel between the globes
const dispatch = d3.dispatch("zoom");

return function(projection, draw, { target, invalidation, visibility }) {
invalidation && invalidation.then(() => dispatch.on("zoom." + id, null));

const id = Math.random(),
s0 = projection.scale();
let willdraw = 0;
dispatch.on("zoom." + id, async function(r, z) {
projection.rotate(r).scale((d3.zoomTransform(target).k = z * s0));

// priority given to the map under the mouse
if (this == target) draw();
// debounce the other maps
else if (!willdraw++) {
if (visibility) await visibility();
await new Promise(r => setTimeout(r, 5)); // change from 5 to 200ms to see what happens
willdraw = 0;
draw();
}
});

const zoom = vzoom(projection).on("zoom.render", function() {
dispatch.call("zoom", this, projection.rotate(), projection.scale() / s0);
});

// do first draw *after* zoom is applied
Promise.resolve().then(() =>
dispatch.call("zoom", this, projection.rotate(), 1)
);

return Object.assign(zoom, { dispatch });
};
}
Insert cell
html`<style>canvas.globe { margin: 3px; }</style>`
Insert cell
d3 = require("d3@7", "d3-geo-polygon@1.8")
Insert cell
import { zoom as vzoom } from "@d3/versor-zooming"
Insert cell
land = {
const topo = await d3.json(
"https://unpkg.com/visionscarto-world-atlas@0.0.6/world/110m.json"
),
simpler = topojson.simplify(topojson.presimplify(topo), 0.5); // try 0 for slower maps
return topojson.feature(simpler, simpler.objects.land);
}
Insert cell
topojson = require("topojson@3")
Insert cell
Insert cell
// outside of Observable, replace by or import from
// https://github.com/observablehq/stdlib/blob/master/src/dom/context2d.js
context2d = DOM.context2d
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