Skip to content

labels

HTML label overlay. LabelLayer keeps geometry on the GPU and renders only the visible labels into the DOM, positioning anchors (reference-space) under a core!ViewTransform; cullLabels does the viewport + collision culling.

import { LabelLayer } from "@mapequation/d3gl/labels";
const labels = new LabelLayer(containerEl, (a) => a.text);
labels.update(anchors, transform, { width, height });
ClassDescription
LabelLayerAn HTML overlay of absolutely-positioned label elements. On each update it maps reference anchors through the view transform to screen pixels, culls to the viewport with collision resolution, and reconciles the DOM (reusing nodes by id). Geometry stays on the GPU; only the surviving labels are in the DOM.
TextMeasurerText measurement for label sets whose text is derived per frame rather than registered once (#204): the network builds its label candidates from the current LOD frontier / viewport, so a width has to be available for every candidate on every placement pass. Measuring there directly would put a measureText on the per-frame path; this memoizes by text so each distinct string is measured exactly ONCE and every later frame is a Map lookup.
InterfaceDescription
CullOptions-
LabelAnchorA label anchored in REFERENCE (projected, pre-transform) pixel space.
LabelBoxA label positioned in SCREEN pixels (after the view transform is applied).
LabelCullScratchReusable buffers for cullLabels. Placement runs on the per-frame path (every setTransform re-places every label), so the caller holds ONE of these and passes it back in: the steady-state cull then allocates nothing but the survivor array. Same shape of contract as core/declutter’s core!DeclutterScratch.
LabelGeometryThe realised screen geometry of a label: its four corners, their AABB, whether it is axis-aligned (fast-path collision), and the CSS transform that reproduces the box.
LabelTextWhat a label set draws with on EVERY backend (#577): the font (a canvas font shorthand, no line-height), the fill colour and the halo (undefined for none). Resolved once per labels() call by resolveLabelText; the overlay (resolveLabelStyle), Canvas/SVG native text, every export and the label measurement all read it, so they agree.
Type AliasDescription
LabelBaselineWhere the anchor point sits vertically in a PLAIN label’s box. "top" (default) keeps the historical top-left box; "middle" centres the box on the anchor. Oriented labels (rotation) are always centred on their axis, so they ignore it.
LabelHaloA legibility halo (#571): a color stroke reaching width px beyond the glyph outlines, painted behind the fill. One shape for every backend — the HTML overlay (a CSS text stroke, haloStyle), Canvas strokeText and SVG <text>, exports included — so they draw the same halo. width 0 draws none.
LabelStyleInline CSS for overlay label elements: camelCased CSS property → value (the string-valued subset of CSSStyleDeclaration), e.g. { color: "#1f2937", textShadow: "0 0 2px #fff" }. A plain, engine-agnostic shape, so it can lift as-is into other engines’ label overlays (#223).
TextAnchorWhere the anchor point sits along the label’s own text axis — like SVG text-anchor.
VariableDescription
DEFAULT_LABEL_STYLEDefault overlay label look (#224): DEFAULT_LABEL_TEXT’s font and colour as overlay CSS. Its halo is added by resolveLabelStyle from the label set’s LabelHalo (#571). Applied by the engine label APIs (e.g. network.labels()); a raw LabelLayer stays unstyled by default (the low-level primitive keeps today’s inherit-from-container behaviour).
DEFAULT_LABEL_TEXTThe default LabelText — a compact dark sans-serif label with a 1px white halo, readable over busy geometry with zero user CSS.
ELLIPSISThe trim marker (#496): U+2026 HORIZONTAL ELLIPSIS, one glyph.
FunctionDescription
anchorBoxProject one anchor to its screen LabelBox (screen = k·ref + (x,y) + the constant offset).
canvasFontStrip a /line-height token from a CSS font shorthand so it is valid for the canvas font property (which rejects line-height), e.g. "600 11px/1 system-ui" → "600 11px system-ui", with or without spaces around the slash (browsers serialize it as "11px / 1").
cullLabelsReduce label candidates to a renderable subset: drop anchors outside the viewport (+padding), then place highest-priority first, skipping any whose box collides with an already-placed one. Collision uses each label’s true screen footprint (see labelGeometry) — the box it renders in, oriented labels included — so dense regions thin down to a readable set instead of overprinting (#204), and the survivor of each cluster is the most important label.
fontRowHeightThe label row-box height for a font (px size × 1.25, e.g. 11px → 14) — derived from the font string alone, so getting it never costs a measureText call.
haloStyleThe overlay CSS for a LabelHalo (#571): a text stroke twice the halo width, painted under the fill (paint-order), so it reaches width px beyond the glyphs as the Canvas/SVG stroke does, with the same miter joins. Chromium strokes HTML text that way whatever stroke-linejoin says; the join properties keep a browser that honours them on HTML text to the same stroke. Empty for no halo or width 0.
labelCullScratchA fresh, empty scratch (buffers grow lazily on first use). Hold one per label layer.
labelGeometryResolve a LabelBox to its on-screen geometry. Plain labels get an axis-aligned box placed by textAnchor/baseline (default: the historical top-left box); oriented labels (rotation) place the text along the rotated axis, vertically centred on the anchor, with the optional upright flip folded in. The collision corners and the CSS transform come from the same computation, so render and culling stay consistent.
labelTextAnchorThe side of the anchor the text runs from, as rendered: the box’s textAnchor (default "start"), swapped by the upright flip. What a native-text backend aligns with (#580).
labelTextRotationThe rotation the label is rendered at, in radians (CSS-clockwise, about its anchor): 0 for a plain label, else its rotation with the upright flip folded in. What a native-text backend rotates by (#580).
labelTextYThe y a backend drawing native text with a "middle" baseline must use — the vertical centre of the same box. Mirrors labelGeometry’s vertical placement (a unit test pins the two against each other), so overlay and native text sit on the same line. An oriented label is centred on its axis, so this is its anchor y, the point it rotates about.
labelTransformThe CSS transform that renders a label in the box labelGeometry culls against, applied to an element whose left/top is the anchor (with transform-origin at that point). Derived from the same extents as the box: the element’s left edge must land at x + lx0, i.e. a translate of lx0/width — 0% / −50% / −100% for start / middle / end, and likewise 0% / −50% vertically. Returns "" for the default top-left box, so a caller’s own transform still applies there (back-compatible).
measureTextMeasure a label’s screen box (CSS px) for collision. font is a canvas font shorthand (no /line-height — strip it first). Width is measured; height is the font size × 1.25 (a consistent row box; 11px → 14, matching the estimates this replaces).
overlayFontThe overlay form of a label font: line-height 1 (11px → 11px/1), so the element’s box is the measured label box and its text sits where Canvas/SVG centre theirs. A line-height in the font is replaced (native text has none); set style.lineHeight to change the overlay’s. Px font sizes only, as canvasFont.
placeLabelsProject label anchors to screen px (screen = k·ref + (x,y) + the constant offset) and resolve collisions, returning the survivors as LabelBoxes (carrying text/opacity). Shared by the HTML overlay (LabelLayer.update) and the backend-native text path (#105 N7b-2) so both place and cull labels identically — they differ only in how they render the survivors.
resolveLabelHaloThe halo a label set draws (#571): the explicit halo, else none when style.textShadow is "none" (the opt-out from when the overlay’s halo was a text-shadow), else DEFAULT_LABEL_TEXT’s.
resolveLabelStyleThe engine-label styling policy (#224): the overlay CSS for a label set’s LabelText — its font (overlayFont) and colour, the halo’s stroke (haloStyle) — with the caller’s inline LabelStyle for anything else. The resolved font and colour win over style’s: they already fold style.font/style.color in (#577), so the overlay draws what Canvas/SVG draw. When a className is given, return only the explicit style (possibly undefined): the class’s CSS keeps full control, since an inline default would beat it. text defaults to what style alone resolves to. Shared so every engine lifts the exact same policy (#223); pass the result as LabelLayer’s style.
resolveLabelTextResolve a label set’s LabelText from its options (#577), once per labels() call: the explicit font/color, else style’s, else DEFAULT_LABEL_TEXT’s; the halo per resolveLabelHalo. Shared by network.labels() and the plot/geoMap labels().
trimToWidthTrim text to at most maxWidth px with a trailing ELLIPSIS (#496). widthOf measures a string’s px width. A text that already fits comes back as the same string. Otherwise it is the longest prefix that fits together with the ellipsis (binary search: O(log length) measurements), cut on a whole character (never between the halves of a surrogate pair) and without whitespace before the ellipsis. When not even one character fits it is the ellipsis alone.