Skip to content

Changelog

Release notes for every published version, generated from changesets. Also available on GitHub Releases and the npm versions tab.

@mapequation/d3gl

0.9.0

Minor Changes

  • #167 7d6d271 Thanks @danieledler! - Align instanced-lane selection styling with retained layers, add shader-driven network highlight, and harden the marquee gesture (#162):
    • selection.others now dims non-selected glyphs on instanced lanes (network nodes, a plot layer’s decluttered points) — the same focus effect retained GeoMap/Plot layers had. Default behavior change: with a selection active, non-selected glyphs fade to others.opacity (default 0.3); opt out with selection: { others: { opacity: 1 } }.
    • A selected network node keeps its outgoing links at full strength while the rest dim (“this node and what it points to”; incident links for undirected graphs; the selected aggregate’s outgoing super-edges under LOD). Selection highlight is ancestor-aware under LOD: zooming into a selected module keeps its expanding children highlighted, while selection() / on("select") stay node-only.
    • Hovering a network node recolours its outgoing links toward the highlight colour (luminance-preserving, so weight-encoded links keep their cue), and highlight colours are now red (selection + hover rings and the link recolour; the subtract-marquee “will remove” ring is yellow; the marquee +/− badge is neutral gray).
    • The network highlight is applied in the GPU vertex shader (per-instance group/selected columns + uniforms), so a hover/selection restyle is a uniform change — no per-frame geometry rebuild or buffer re-upload, even on a full LOD-off draw of a million nodes.
    • hover now mirrors selection: hover: { hovered?: HighlightStyle | draw-fn, others?: StyleOverride }hovered styles the hovered item (the overlay/ring), others fades the rest on hover (opt-in, the hover analogue of selection.others). hover: true and a bare HighlightStyle/draw-fn still work (back-compat). Replaces the short-lived hoverDimOthers.
    • Marquee robustness: the shift+drag box + mode badge are one reused overlay pair, torn down on any interruption (context menu, pointer cancel, window blur, Esc) — fixing duplicate badges accumulating on a ctrl-click context menu mid-drag.

Patch Changes

  • #194 c22adf7 Thanks @danieledler! - network: GPU force-layout backend now reheats on node-drag, at parity with the CPU worker (#183, N8.5). Dragging a node on layout({ backend: "gpu" }) pins the held set (skipped by the integrate pass but still repelling/anchoring its neighbours) and reflows the rest on the GPU; the layout is kept alive after convergence instead of destroyed, and releases + re-cools on drop. Physical-view drags of a state network reheat too.

  • #176 5273265 Thanks @danieledler! - network: add a GPU force-layout backend (layout({ backend: "gpu" })) — a WebGL2 Barnes-Hut grid-pyramid many-body solve streamed back into the existing render path, with automatic fallback to the CPU-worker backend when WebGL2 float render targets are unavailable. Milestone A of #106 (GPU layout).

  • #170 ac1f526 Thanks @danieledler! - Document the engine data-entry methods: add JSDoc to GeoMap.layer(), Plot.layer(), and Plot.points() so they carry descriptions in the API reference and editor hovers (previously they rendered as bare, undescribed signatures).

  • #186 a065bb7 Thanks @danieledler! - Only update positions when animating a network layout (#179), instead of re-deriving and re-uploading the whole graph every frame. Two changes together eliminate the per-frame bottleneck (100k nodes + 600k edges, LOD off, was ~446ms/frame):

    • In-place GPU buffers. InstancedLines, InstancedArrows, and InstancedHalfArrows gain an in-place update() (mirroring InstancedCircles): layout frames bufferSubData the endpoint/geometry buffers instead of destroying+recreating the GPU objects. updateInstancedLayer takes the in-place path for all four primitives, recreating only when a structural property changes (vertex-template samples, arrow half flag), the primitive type changes, or a layer’s pickable state toggles.
    • Cached style attributes. The no-LOD full-graph path caches its style-derived attributes (link/arrow colours, widths, per-edge radii/sizes/bends) per resolved-style version. A position-only layout frame recomputes only the position-derived endpoints/node-centres and reuses the cache — so the colour/width scale accessors run O(edges) once per style version, not once per edge per frame. data()/style()/lod() bust the cache; a genuine data/style change fully rebuilds.
    • Upload only what changed. update() skips the bufferSubData of any per-instance buffer whose source array is the same object as last frame (the cached colour/width/radius/bend arrays are reference-stable across position frames), so a position frame uploads only the freshly-allocated endpoint buffers — not the unchanged style buffers.
  • #190 37b13f7 Thanks @danieledler! - network: state networks (stateNetwork()) can now run their physical layout on the worker or gpu backend, not just forcelayout({ backend: "worker" | "gpu" }) lays out the physical graph off-thread / on the GPU and re-derives the rosette state positions from it each streamed frame, so the state/both views converge live alongside the physical layout. Tier 1 of #182 (rosette + GPU/worker backend); force/two-phase module-aware modes are deferred to #189.

  • #173 03eb8df Thanks @danieledler! - Render state (higher-order / memory) networks with network() (#171). buildStateGraph({ stateCount, stateToPhysical, source, target }) assembles a state network and derives its physical network (physical nodes = distinct physical ids; links = state edges aggregated across the physical boundary, directed + flow-summed). net.stateNetwork(graph, { modules }) ingests it and net.view("state" | "physical") toggles two renderings of the same data:

    • physical — the aggregated physical network, where a physical node whose state nodes span ≥2 modules draws as a pie-chart glyph (a wedge per module, sized by that module’s flow, module-coloured) and a single-module node as a solid disc;
    • state — every state node on a golden-angle rosette around its physical node, coloured by module.

    The pie is a new instanced glyph (one GPU instance per wedge — an angular sector of a disc, no wedge texture or per-fragment loop; updates in place) that also traces as filled arc sectors for Canvas/SVG and toSVG(), rendering identically across all three backends. New helpers: rosettePositions (deterministic state-node placement) and physicalPieWedges (overlapping-module → wedge derivation, colours matching moduleColors). Positions in this release come from the in-library force layout of the physical graph plus the rosette (a CPU path); the module-aware GPU stateLayout is a separate change.

0.8.0

Minor Changes

  • #150 4696a20 Thanks @danieledler! - Selection API: selectable layer option; select() + gesture both fire on("select") (#79).

    selectable?: boolean | { multi?: boolean } — new per-layer option that opts a layer into click-driven selection. true = single-select (plain click replaces). { multi: true } = shift/cmd/ctrl-click toggles add/remove; plain click replaces. Omitting selectable leaves the layer un-selectable (no gesture, no click-styling) — opt-in is preserved.

    One managed selection path — the click gesture (on a selectable layer) and the programmatic select(name, set|null) both update the managed set, apply styling (selection.selected/others), and fire on("select").

    on("select", (selected, ev?) => void) — pure observer of selection changes. ev is present for a gesture, undefined for a programmatic select() call. Registering it no longer enables anything (the layer’s selectable does).

    on("click") — unchanged: fires first (before selection updates) on every pointer-up that passes the click-slop test, regardless of selectable.

    Migration: add selectable: { multi: true } (or selectable: true) to any layer that was previously activated by on("select"). The on("select", cb) call stays as the observer.

  • #113 71dca06 Thanks @danieledler! - Add the network() engine for large node–link diagrams, exported from the new @mapequation/d3gl/network subpath.

    • Instanced WebGL rendering: GPU-instanced nodes (points), links (lines), and triangle arrowheads for directed edges, via a shared instanced-primitive lane in the WebGL backend.
    • SVG/Canvas + export: the same glyphs emit through the PathContext seam, so small networks render on the SVG/Canvas backends and toSVG() produces publication output.
    • Data model: columnar SoA + CSR graph (buildGraph), with per-node degree, strength (weighted degree), and optional app-provided flow (buildGraph({ nodeFlow })); a label-interning edge-list parser (parseEdgeList), a Pajek .net parser (parsePajek, supporting *Vertices/*Arcs/*Edges/*Arcslist/*Edgeslist with optional labels and coordinates), and a parseNetwork(text, filename) dispatcher (.net → Pajek, else edge list).
    • Node sizing: nodeRadius takes a constant, a per-node Float32Array, a (degree, index, graph) => radius accessor (a d3 scale fits directly, fed the node’s degree), or { by, scale } to size by a metric — "degree", "strength", "flow", or a custom (index, graph) => value accessor — through any d3 scale. Resolved once per style() with no per-frame or rendering cost (radius is already a per-instance GPU attribute), so degree/flow-scaled sizing holds at millions of nodes.
    • In-library force layout (layout({ backend: "force" })): force-directed simulation with a Barnes-Hut quadtree (O(n log n)) and deterministic seeding, seeded by default via multilevel coarsening (heavy-edge matching) for faster convergence and fewer tangles on clustered graphs (opt out with multilevel: false).
    • Off-thread layout (layout({ backend: "worker" })): runs the whole solve in a Web Worker and streams positions back for progressive on-screen convergence while the main thread stays responsive — zero-copy via SharedArrayBuffer on cross-origin-isolated pages, postMessage snapshots otherwise, with a synchronous fallback where Workers are unavailable. stopLayout() cancels; whenSettled() awaits convergence.
    • Module-free level of detail (lod({ … })): an adaptive hierarchy cut over the retained multilevel-coarsening tree draws dense regions as aggregate glyphs that expand into their members as you zoom, with importance-ordered declutter of overlapping glyphs and super-edges summarising connectivity — so per-frame work tracks the visible frontier rather than the whole graph. The geometry tracks the layout as it converges; panning/zooming only re-runs the cheap cut. sizeMode: "screen" keeps glyphs a constant pixel size for navigating large layouts. Runs live every frame on WebGL and re-cuts on zoom-end on the Canvas/SVG backends (so toSVG() exports an LOD map — see the vector-backend LOD note).
    • Worker-built LOD (lod() before layout({ backend: "worker" })): the worker builds the LOD tree itself, reusing the coarsening it computes for the multilevel seed, and streams the tree once plus its aggregate geometry each frame (shared via SharedArrayBuffer, copied otherwise). The main thread then never coarsens or runs the per-frame O(N) geometry pass — only the on-screen-bounded cut — keeping it free as networks scale toward millions of nodes. The read-only lodSource getter reports which tree drives rendering ("worker" / "spatial" / "main" / "none").
    • Edge-less LOD (point clouds): a graph with no edges can’t be coarsened (heavy-edge matching needs edges), so LOD builds a spatial quadtree over the node positions instead (buildSpatialLODTree, exported and generic over any positions buffer). The cut then aggregates dense regions and prunes off-screen in O(visible) rather than degenerating to a flat O(N)-per-frame scan with no aggregation. Engaged automatically when nodeCount > 0 && edgeCount === 0; tune via lod({ spatial: { maxDepth } }).
  • #166 18ecd4f Thanks @danieledler! - network() frontier labels (#105 N7b). Backfilled changeset.

    • net.labels({ labelOf, max }) — HTML-overlay labels on the visible LOD frontier, importance-ranked (top-max by flow/size), re-placed on pan/zoom with overlap culling. Shipped in #153 (ef52473).
    • Backend-native label text + export — on the SVG/Canvas backends the labels render as real <text> / fillText rather than the HTML overlay, so toSVG() exports publication output with the labels baked in. Shipped in #154 (f33b985).
  • #166 18ecd4f Thanks @danieledler! - network() pixel-exact GPU-readback link/glyph picking (#141). Backfilled changeset; shipped in #158 (afdcf44).

    Opt in with net.pickLinks(): hover/click then resolve thin links / bent half-arrows / module super-edges that the CPU circle picker can’t hit, via a backend pick FBO (Backend.pickInstanced, clean-room). A link hit is a HoverHit with layer: "links" and a NetworkLinkHit datum ({ source, target, weight, aggregate }). Nodes are drawn on top, so they win where they overlap. Off by default — a non-interactive network pays nothing.

  • #166 18ecd4f Thanks @danieledler! - network() selection/hover ring + members() on the instanced lane (#105 N7c-2). Backfilled changeset; shipped in #152 (96b67b2).

    • interactive({ selectable, hover }) draws a companion ring overlay on selected/hovered nodes and aggregates (instanced glyphs have no Scene drawable to recolor, so styling is a ring rather than a fill change).
    • A hit’s members() enumerates the leaf node ids it covers — itself for a leaf, the whole subtree for a collapsed module — exposed on on("hover" | "click") hits and every selection() entry.
  • #144 d8e8f85 Thanks @danieledler! - Two opt-in LOD level-transition options on lod({ … }), both off by default with no added cost when unset:

    • crossLevelEdges (#139): also draw super-edges between mixed-level visible nodes — a visible leaf (or finer aggregate) and a visible coarser aggregate at a different cut level. The off-frontier on-screen endpoint is projected to its nearest present ancestor (an O(depth) walk), so an aggregate keeps its links when you expand a neighbouring region instead of losing them until both sides are at the same level. Applies wherever the directed super-edge CSR exists (module and coarsening LOD trees).
    • crossFade (#133): an opacity cross-fade across the expand threshold. Over a band whose half-width is crossFade × expandPx, an aggregate eases out (smoothstep) as its children ease in, so a split/merge reads smoothly instead of popping. The per-node alpha flows through the frontier glyphs’ fill and border, the aggregate halo rings, and the super-edges (faded by their least-visible endpoint), and blends on every backend. During the fade a child ignores its ancestor as a declutter occluder — so a fading parent doesn’t cull the children emerging behind it — while children still declutter normally against their siblings, keeping the split/merge smooth without a blank moment.
  • #144 d8e8f85 Thanks @danieledler! - Render the network LOD frontier on the Canvas and SVG (vector) backends, not just WebGL — so vector backends show the same aggregate map as the instanced lane, and toSVG() exports a level-of-detail network map (#138). The frontier (cut → declutter → super-edges / aggregate glyphs) is traced into retained Scene layers keyed by stable tree-node id, byte-identical to the WebGL lane. On the retained backends the cut can’t re-tessellate per frame, so the frontier is static during a gesture and re-cuts on release (the redraw-on-zoom-end model); call syncScreenGeometry() to re-cut at a chosen zoom before a programmatic export.

  • #166 18ecd4f Thanks @danieledler! - network() maps of networks (#104 N6) — render a network as a directed map of modules. Backfilled changeset; shipped in #127 (2a1ed81), #129 (3c60fae), #130 (c0d7346), #131 (82bc507), #132 (b9d301a), #134 (150284e), #136 (7e0afd1).

    • Provided module hierarchy as an LOD sourcelod({ modules }) takes an Infomap-style per-node path partition; modules collapse to one aggregate glyph (inheriting their module colour) and expand into sub-modules → leaves as you zoom.
    • Flow-border nodesflowBorder: { flow, scale } rings each node by its enter/exit flow (a darker shade of the node fill by default).
    • Bent half-arrow linkslinkStyle: "half-arrow" (directed): one filled shape per link that pinches toward the target, curved by linkBend — the map-of-networks link glyph.
    • Directed module super-edges — under module LOD, half-arrow super-edges between collapsed modules thicken/darken with their accumulated flow.
    • moduleColors() helper for hierarchical categorical palettes, plus the modular-lod and modular-map examples.
  • #166 18ecd4f Thanks @danieledler! - network() shift+drag marquee selection (#159). Backfilled changeset; shipped in #160 (36d71b4).

    On a multi-selectable lane, shift+drag draws a box that adds every node/aggregate whose centre falls inside it to the selection (additive, like shift+click), with a live hover-ring preview of what releasing will select. A CPU range query over the screen-bounded frontier (pickRegion), so it stays cheap at millions of nodes; plain drag still pans.

  • #166 18ecd4f Thanks @danieledler! - network() interactive node-drag (#140). Backfilled changeset; shipped in #161 (b2d31cd).

    • interactive({ draggable: true }) — a plain drag starting on a node moves it instead of panning; it tracks the cursor with no lag while the layout reheats around it and re-cools on release. Grab a selected node to drag the whole selection; grab a collapsed module to drag its whole subtree. Works on the force and worker layout backends (reheat) and positions (translate-only). ForceLayout.setPinned holds the dragged set; the worker is kept alive after convergence and reheats via a pin/unpin protocol.
    • Marquee subtract — hold option/alt while shift+dragging to remove the box’s glyphs from the selection (red “will-remove” preview ring + a +/− cursor badge); the additive marquee is unchanged.
    • Consistent selection-ring palette — defaults are now blue #2563eb (selected), green #16a34a (hover / will-add), red #dc2626 (will-remove), overridable via selection.selected.stroke and a hover HighlightStyle’s stroke. (Changes the previous orange/white defaults.)
  • #145 3311bb8 Thanks @danieledler! - Add picking to the network() engine (#105 N7a): on("hover" | "click") now resolve the node — or the aggregate (collapsed module) — under the cursor on the WebGL instanced lane, which the Scene hit index can’t see.

    • CPU hit-test over the LOD cut frontier: pick(x, y) tests the on-screen frontier glyphs as exact circles, or the full node set when LOD is off. Cost is proportional to the visible frontier (screen-bounded), never the graph size, so hover/click stay cheap at millions of nodes with no GPU readback.
    • Unified interaction API: uses the same on("hover" | "click") surface as the GeoMap/Plot engines — network() overrides only the resolver. On the SVG/Canvas backends, where the frontier is drawn as Scene drawables, picking already flows through the shared Scene hit index.
    • Hit shape: the HoverHit’s id is the tree node id (a leaf’s id is its original node index; aggregate ids are ≥ leafCount), and its datum is a NetworkHit{ aggregate, count } (leaf vs collapsed module, and the leaf count it covers).
  • #164 9226fe5 Thanks @danieledler! - Report which position transport the worker layout uses, so the SharedArrayBuffer zero-copy path is observable (#163):

    • sharedMemoryAvailable() — new export: whether this environment can use the SAB zero-copy transport (SharedArrayBuffer exists and the page is cross-origin isolated via Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp). The environment’s capability, independent of any run.
    • Network.layoutTransport ("shared" | "copy" | "none") — new getter: the transport the active worker layout actually selected. "shared" = positions stream zero-copy through a SharedArrayBuffer; "copy" = posted as per-frame snapshots (also when the worker fell back to a synchronous main-thread solve); "none" = no worker-backed layout running.
    • WorkerLayoutHandle.shared — new boolean on the handle returned by startWorkerLayout, backing the getter.

    Layout behaviour is unchanged — the SAB path already self-selected at runtime; this only makes the selection inspectable.

  • #148 dfc1c30 Thanks @danieledler! - Decluttered plot.points() scatters now render through the shared instanced lane on WebGL (#108-C): draw cost is proportional to the kept (post-declutter) set rather than total N — index compaction instead of draw-all-then-hide — so dense decluttered scatters scale much further. The lane is used for declutter-enabled point layers with no clipTo, hover, or selection (those keep the Scene path, so clipTo stencil, hover-highlight, and selection restyle are unaffected); plain points, vector (SVG/Canvas) backends, and passThrough are unchanged. Under backend:"auto", a declutter layer transparently upgrades from the Scene path to the lane once the WebGL backend is live (and downgrades back on a swap). tooltip works on lane layers; append() on a declutter layer now throws (rebuild with the full data) rather than silently mishandling the captured snapshot.

Patch Changes

  • #147 d3a4de5 Thanks @danieledler! - Internal: BaseEngine now owns the instanced-selection lane registry (#108-B). setTransform drives every registered dynamic lane’s re-select + re-emit (static lanes emit once and ride the matrix), and pick() resolves lanes (topmost-first) before Scene hit-indexes. network() registers its LOD (dynamic) and no-LOD (static) lanes via a single syncLane() and drops its setTransform/pick overrides + emitInstancedLayers. No behaviour change; this is the seam plot.points() will register onto (#108-C).

  • #146 129ca40 Thanks @danieledler! - Internal: introduce core/InstancedLane — the shared select(transform) → visibleIndices → emit → pick orchestration over an instanced layer — and adopt it in the network() LOD frontier. No behaviour change: the cut/declutter/pick math and the glyph emit are unchanged, just routed through the lane. Removes the now-redundant lodLayers method and write-only frontier field. Groundwork for unifying picking/declutter/plot.points() onto one shared instanced lane (#108).

  • #166 18ecd4f Thanks @danieledler! - Fix: correct aggregate leaf-count on worker-streamed LOD trees (#105). Backfilled changeset; shipped in #156 (01831b3).

    The per-aggregate leaf count (used for frontier label badges and members() sizing) was miscomputed on the worker-built/streamed LOD tree; it now matches the main-thread tree.

0.7.0

Minor Changes

  • #59 50a8506 Thanks @danieledler! - Collide rotated labels by their true oriented footprint. LabelBox / LabelAnchor gain rotation (radians), textAnchor (start | middle | end, like SVG), and keepUpright; the library now derives both the rendered CSS transform and the collision box from the same angle (an oriented-box / separating-axis test, with the fast axis-aligned path kept for plain labels). Previously rotated labels were culled by their un-rotated dimensions, so near-vertical labels — e.g. toward the top of a radial tree — over-excluded their angular neighbors and left gaps that grew with the rotation.

Patch Changes

  • #65 f170ba6 Thanks @danieledler! - Share engine-level options through one BaseEngineOptions type. tooltipClass, width/height/aspectRatio, and backend were re-declared per engine and consumed in each subclass — so plot(host, { tooltipClass }) was silently dropped (only geoMap wired it). These shared fields now live on a single BaseEngineOptions (exported) that both GeoMapOptions and PlotOptions extend, and the BaseEngine constructor consumes them once. plot() tooltips now honor tooltipClass, and base-level options can no longer drift between engines.

  • #64 3c55631 Thanks @danieledler! - Add h, a tiny framework-free hyperscript helper exported from @mapequation/d3gl/map, for building rich tooltip / HTML-overlay content declaratively. The layer tooltip option accepts the returned HTMLElement, so tooltip: (d) => h("div", null, [...]) replaces hand-rolled document.createElement ceremony. Children are always inserted as text nodes (never parsed as markup).

  • #94 350f1ba Thanks @danieledler! - Make screen-space glyph declutter scale to very large node counts. The per-zoom cull ran on every transform but rebuilt transform-independent work each frame and materialized the full vector view twice. It now:

    • caches the anchor grouping on the Scene (built once per layer, reused every frame);
    • bins with a reused flat typed-array grid + intrusive linked list (no per-frame Map or bucket allocation), bounded to the viewport plus a one-cell margin;
    • writes visibility flags in place; and
    • skips the export-only drawables() rebuild on WebGL while interacting (the new optional Backend.updateLayerStyles drawables arg + stylesNeedDrawables capability — Canvas/SVG render from the vector view and still receive it; the settle frame refreshes it for toSVG).

    At 131k screen-mode nodes a full zoom frame drops from ~33ms to ~8ms; cull output is unchanged (verified against a brute-force reference).

    Also fixes declutter not being applied on the first draw — it now runs before the initial upload, not only after the first zoom/pan.

  • #96 7968c2c Thanks @danieledler! - Let screen-space declutter act on analytic points (Plot.points). A lone point’s anchor now defaults to its center, and points() accepts a declutter option, so a decluttered scatter can use lightweight GPU points (~4 verts each) instead of tessellated ctx.arc paths (tens of verts). This lifts a decluttered cloud from ~256k (where the path geometry OOMs a tab) to ~1M. Rendering and screen-mode hit-testing are unchanged (the point shader already culls by the visibility flag, and hit-testing already used a lone point’s center as its anchor).

0.6.0

Minor Changes

  • #55 df49dd6 Thanks @danieledler! - Make the engines responsive to their parent and resize in place. width/height are now optional on plot() / geoMap() (and the React <Plot> / <GeoMap>), with a new aspectRatio option. Sizing is responsive by default:

    • aspectRatio set → width-driven: fills the parent’s width and keeps the ratio.
    • nothing set → fill-parent: tracks the parent box (the parent supplies the height).
    • both width & height → fixed: a static size (the previous behavior, unchanged).

    In responsive modes the engine observes its host (a ResizeObserver, coalesced per animation frame) and resizes in place via a new setSize(width, height) — no teardown, so the view transform, layers, hover, and selection are preserved. A resized geoMap also refits its projection to the new box (uniform resizes preserve the original framing exactly; an aspect-ratio change re-letterboxes via the engine’s own retained geometry). The React wrappers no longer recreate the engine on a size change — they call setSize instead.

0.5.1

Patch Changes

  • #52 a0294c8 Thanks @danieledler! - Make the declarative interaction options (hover, tooltip, selection) universal across both engines. They were only exposed on geoMap layers, even though the underlying machinery (hover overlay, tooltip, selection styling, hit-testing) already lived in the shared base — so plot layers could not declare hover/tooltip/selection. The options are now lifted into a shared InteractiveLayerOptions interface and forwarded by both Plot.layer()/Plot.points() and GeoMap.layer(), so plot.layer(..., { hover, tooltip, selection }) and plot.points(..., { hover, … }) work exactly like their geoMap counterparts. No change to existing geoMap behavior.

0.5.0

Minor Changes

  • #51 b459367 Thanks @danieledler! - Interactive styling for retained layers: on("click") (drag-suppressed), hover highlight via per-item overlay (hover layer option / highlight(), with custom draw through HighlightBuilder), core tooltips (tooltip option + tooltipClass), click selection with complement dimming (selection option + select()), per-drawable style overrides (setStyle/clearStyle) on a new styles-only backend path (updateLayerStyles), faster recolor(), and clip-aware picking (clipTo layers no longer hit where they are visibly clipped away).

Patch Changes

  • #49 9b7a40f Thanks @danieledler! - Backend swap now re-inserts the new rendering surface at the previous surface’s DOM position instead of appending it to the end of the host. This keeps the canvas a stable base layer, so HTML elements the caller appended to the host after it (e.g. an overlay) keep painting on top across a setBackend() switch or the "auto" canvas→WebGL upgrade, with no z-index needed.

0.4.1

Patch Changes

  • #39 672f1fa Thanks @danieledler! - Fix layout shift in "auto" backend mode. Backend <canvas> elements are now positioned absolutely within the (positioned) host instead of sitting in normal flow. During the canvas→WebGL upgrade — and the React StrictMode double-mount that compounds it — two or more backend canvases briefly coexist; as display:block elements in normal flow they stacked vertically, inflating the host’s height and rendering the live map below its reserved box until the stale canvases detached (a visible “jump up”). Absolute positioning overlaps coexisting canvases at the host’s origin so the swap never affects layout. The engine also promotes a static host to position:relative so the absolute canvas anchors correctly even for bare-engine consumers (the React <GeoMap>/<Plot> wrappers already set position:relative). Hit-testing is unaffected — pointers are measured from host.getBoundingClientRect().

  • #43 464fc3b Thanks @danieledler! - WebGL now composites overlapping fills and strokes in the same painter’s order as Canvas and SVG. Previously WebGL drew all fills then all strokes, so a shape’s border always landed on top of every fill — overlapping bordered shapes (e.g. node range pies) looked different on WebGL than on Canvas/SVG, where a later shape’s fill correctly occludes an earlier shape’s border. The three backends now match. (Internally this is one fewer draw call per layer, not a slowdown.)

    Stroke joins and caps now match across backends too: WebGL renders miter/round joins and square/round caps (previously only bevel joins + butt caps), and all three backends are pinned to the same join/cap/miter-limit (Canvas/SVG no longer use their differing defaults of 10 and 4). New layer options lineJoin ("bevel" default | "miter" | "round"), miterLimit (default 10), and lineCap ("butt" default | "square" | "round") on plot().layer() and geoMap().layer() control this consistently everywhere. The default join is "bevel" (matching the prior WebGL look); pass lineJoin: "miter" for sharp corners.

    Stroke joins now emit only the outer-side geometry (the inner side is already covered by the segment quads), and a miter replaces the bevel rather than stacking on top of it. This removes redundant overlapping triangles, so translucent strokes no longer double-blend (darken) at joins — keeping WebGL close to Canvas/SVG for semi-transparent borders too.

    Also renders the raster backends at devicePixelRatio, so WebGL and Canvas stay crisp on HiDPI/retina displays instead of upscaling a CSS-resolution buffer.

  • #37 776876c Thanks @danieledler! - Export version from the package root, inlined from package.json at build time. Downstream apps can surface the d3gl version (e.g. a “Powered by d3gl v0.4.0” badge) without importing @mapequation/d3gl/package.json:

    import { version } from "@mapequation/d3gl";
    console.log(`Powered by d3gl v${version}`);
  • #42 456b923 Thanks @danieledler! - Render the orthographic globe via the same per-frame CPU reprojection as Canvas/SVG instead of an equirectangular bake-to-texture. WebGL now matches Canvas/SVG output (crisp coastlines and lines, correct globe size, no “droplet” artifact when changing layers mid-globe), honors hideOnInteraction while rotating/zooming the globe, and shares one zoom/rotate state model across backends — fixing the inability to zoom back out after switching backends.

0.4.0

Minor Changes

  • #27 a03c1f8 Thanks @danieledler! - Add an opt-in backend: "auto" mode that paints with the Canvas backend synchronously for an instant first paint, then creates the WebGL device in the background and swaps to it transparently when ready. whenReady() (and the React onReady) resolve at the canvas first paint, so consumers see a working map immediately without paying the WebGL device-creation startup cost up front. If WebGL is unavailable the map stays on Canvas (with a console.warn). Existing "webgl" / "canvas" / "svg" behavior is unchanged.
  • #35 cc33ebb Thanks @danieledler! - Add a passThrough: true layer mode for huge / streaming datasets. A pass-through layer retains no per-feature geometry in d3gl (no Scene entry, no hit index): you own the data and d3gl projects, draws, and discards it on each repaint. This lifts the retained ceiling (~4–7M features, where Canvas runs out of memory and WebGL silently stops drawing) up to whatever your own array costs — 250M+ for a packed Float32Array.
    • Opt in via geoMap.layer(name, features, { passThrough: true }) or plot.points(name, data, { passThrough: true }). The data argument may be a callback (() => features) that d3gl re-invokes on each full repaint, so it always reflects your current array; handle.append(batch) draws new arrivals immediately (O(new)).
    • Works for all GeoJSON geometry — points/multipoints (analytic circles) and polygons/lines (projected paths) — on both Canvas and WebGL. WebGL accumulates into an offscreen FBO with per-vertex color (no per-drawable color texture) and re-tessellates path geometry per repaint.
    • Pan/zoom uses snapshot-pan (a slightly stale raster during the gesture, re-crisp on settle); full repaints are time-sliced so a multi-million-feature redraw never freezes the main thread. auto mode upgrades Canvas→WebGL with pass-through layers intact.
    • Limitations: pass-through layers are not pickable, clipTo is not applied to them yet, path geometry is world-mode only, and the svg backend rejects passThrough. Retained rendering is unchanged for all existing layers.

0.3.0

Minor Changes

  • #14 925b635 Thanks @danieledler! - GPU-accelerate orthographic-globe rotation on the WebGL backend: the map is baked into an equirectangular texture and drawn on a spinning 3D sphere, so rotation and zoom are uniform updates instead of per-frame re-projection. Activation is automatic (WebGL + orthographic); canvas/SVG and other projections are unchanged. GeoMap.enableZoom(extent) now auto-dispatches: versor rotation for spherical projections (azimuthal, clipAngle > 0), affine pan/zoom for flat ones.

  • #15 4397a4b Thanks @danieledler! - Add incremental layer append for live-streaming data:

    • GeoMap.layer(), Plot.layer(), and Plot.points() now return a LayerHandle (previously the engine instance). The handle exposes append(items), plus recolor() / setClip(clipTo?).
    • LayerHandle.append(features) builds and projects only the new items and re-pushes only that layer — existing features are not re-projected. This makes live streaming (e.g. species occurrences) cheap instead of quadratic in the total point count.
    • Appended features survive setProjection and globe rotation (re-projected from the layer’s accumulated data).
    • A duplicate drawable id within a layer now throws (previously it silently corrupted the layer’s id index).
  • #12 524132f Thanks @danieledler! - Add map projection switching and a rotatable globe:

    • GeoMap.setProjection(projection) re-projects existing layers against a new projection and resets the view.
    • GeoMap.enableRotation(opts?) drag-rotates a spherical projection (versor trackball) and wheel-scales it, re-projecting on the CPU per frame.
    • BaseEngine.disableInteraction() detaches the current pan/zoom or rotation.
    • LayerOptions.hideOnInteraction drops dense layers from the render while the user is interacting — a rotation drag or a zoom/pan gesture — so only cheap layers re-project per frame; they reappear when the gesture ends.
    • The WebGL backend now alpha-blends, so fills/strokes with alpha < 1 (e.g. "#9bd1a466") composite correctly instead of rendering opaque.
    • On azimuthal projections (e.g. orthographic), point geometries on the back hemisphere are culled instead of showing through the globe.
  • #16 c98087c Thanks @danieledler! - Make incremental layer append O(new) on the Canvas backend (and lay the groundwork for WebGL):

    • Scene.appendedBuffers(name, fromDrawable) returns GPU-ready buffers for only the appended tail (group-absolute indices), and Scene.drawables(name, from) reads only the new vector views — so an append serializes O(new), not O(total).
    • New Backend.appendToLayer(delta) contract carrying a RenderDelta (delta buffers + new drawables). The Canvas backend implements it as draw-on-top: new drawables are drawn over the current canvas with no clear; full redraws happen only on transform/recolor/resize. This restores cheap live streaming on canvas.
    • Fix: appending a large batch no longer throws RangeError — the engine and backends extend their arrays with loops instead of push(...spread) (which exceeded the argument-count limit for big batches).

    WebGL still rebuilds the layer’s renderer on a count change (correct, O(total) per batch); a true O(new) WebGL bufferSubData path is a follow-up.

  • #23 310db91 Thanks @danieledler! - Reduce memory for very large layers (live streaming):

    • New pickable: false option on GeoMap.layer / Plot.layer / Plot.points skips building the CPU hit index for that layer (no hover/pick on it) — saves one Entry object per drawable, which dominates memory for huge non-interactive layers.
    • Drawable ids are now keyed by their raw value (string or number) instead of String(id) in the scene’s id map and the engine’s per-layer id set, so numeric-id layers no longer allocate a string per drawable.
  • #24 be9c7bf Thanks @danieledler! - SVG pan/zoom is now O(1). The SVG backend keeps persistent <defs> / view-<g> / screen-<g> elements; setTransform updates only the view group’s transform attribute instead of re-serializing the whole document every frame. This applies whenever no layer uses sizeMode: "screen" (the common case — maps, polygons, world points). Screen-mode content (constant-pixel circles/glyphs) still bakes the transform into coordinates and is re-serialized on a move, as before. svgFromLayers output is unchanged.

  • #21 e111f6c Thanks @danieledler! - WebGL incremental append is now O(new) per batch. Backend.appendToLayer is implemented on the WebGL backend with capacity-doubling growable buffers (bufferSubData for the appended tail, reallocate + rebind the model only when a buffer overflows) and incremental color/flag texture growth, bumping the indexed draw count. Previously a LayerHandle.append on WebGL rebuilt the whole layer renderer each batch (O(total)), which made live streaming slow down as the layer grew; appends are now constant-time in the existing size.

0.2.0

Minor Changes

  • #8 f2bf4c5 Thanks @danieledler! - Declarative React API and rendering fixes.
    • react: new <Plot> / <Layer> / <Points> components for declarative, non-geo rendering — the imperative-engine sibling of <GeoMap>.
    • geo: GeoInput now accepts a GeoJSON Sphere ({ type: "Sphere" }) directly, with no casts.
    • svg: the SVG backend sets a viewBox so it maps identically to the Canvas2D / WebGL2 backends when the rendered element is resized.
    • map: enableZoom gains an optional onTransform callback and seeds d3-zoom from the engine’s current transform, so zoom centres correctly from a non-identity base view.
    • fix: destroying an engine mid backend-swap no longer leaves an orphaned canvas; re-applying a layer keeps the current view transform.