/** * SVG renderer. * * The hybrid layering is set up here even * though only the SVG tier exists, because layer order is the part that is * expensive to change later: * * ``` * overlay (screen space) labels, annotations, focus <- crisp text, a11y * symbols (screen space) bubbles, endpoint dots <- constant size * regions (world space) annotation areas <- camera transform * marks (world space) feature fills, arcs <- camera transform * base (world space) sphere, graticule <- camera transform * ``` * * Annotation areas sit above the fills and below the symbols on purpose: a * translucent "this region" highlight belongs over the choropleth it qualifies, * but drawing it over the bubbles would veil the data it is pointing at. * * World-space content sits under a single `` carrying the camera transform, so * a pan costs one attribute write regardless of feature count, and strokes use * `vector-effect: non-scaling-stroke` so borders keep their weight when zoomed. * * Symbols are the exception: a bubble must **not** grow when the reader zooms in, * because its radius encodes a value. They therefore live in screen space and are * repositioned per camera frame, which is two attribute writes per symbol. That is * fine for the hundreds-to-low-thousands range this renderer is built for, and * clustering bounds the count above it. * * @module renderers/SvgRenderer */ import { PaintRegistry } from './Paint'; import type { FeaturePaint } from './Paint'; import type { Viewport } from '../geo/Viewport'; import type { NormalizedFeature, StrokeOptions, WorldPoint } from '../types'; export interface SymbolSpec { /** Stable identity within the series, used for the DOM key and hit-testing. */ key: string; /** Index within the series, carried on the DOM for event resolution. */ item: number; world: WorldPoint; radius: number; fill: string; stroke?: StrokeOptions; opacity?: number; } /** * A shaped, optionally labelled screen-space mark: a marker or a point cluster. * * Drawn as a `` wrapping a path and an optional label, so however * many parts a mark has, moving it stays one attribute write. */ export interface MarkSpec { key: string; /** Index into the series' items, or -1 for a cluster. */ item: number; /** Index into the series' clusters, or -1 for a single mark. */ cluster: number; world: WorldPoint; /** Path data centred on the origin, or with its tip there when point-anchored. */ d: string; pointAnchored?: boolean; fill: string; stroke?: StrokeOptions; opacity?: number; label?: string; labelSize?: number; /** * Ink for the label, which the mark decides rather than the stylesheet: a * cluster count sits on the cluster's own fill, and that fill is the caller's. * Left unset the text inherits, which is only safe when nothing is behind it. */ labelFill?: string; /** Radius of the invisible hit circle, so small shapes stay clickable. */ hitRadius?: number; } export interface PathSpec { key: string; item: number; /** Path data in world space. */ d: string; stroke: string; width: number; opacity?: number; dashArray?: string; /** Beads travelling along this path. See `series/flow`. */ flow?: FlowSpec; } /** * A resolved flow: everything the dashed companion path needs, in screen pixels * and seconds. Built by `series/flow`, which is where the reasoning lives. */ export interface FlowSpec { /** Dash length. Near zero with a round cap is a dot. */ dash: number; gap: number; /** Stroke width, which is the bead's diameter. */ width: number; color: string; opacity: number; /** Seconds to advance one full period. Zero paints the beads without motion. */ duration: number; /** Negative seconds, so a staggered route starts mid-pattern rather than pausing. */ delay: number; /** * `'zoom'` anchors the beads to the ground: every number above is multiplied by * how far the camera has zoomed from the view the map opened at, so a bead stays * over the same stretch of route. `'screen'` holds them at a fixed size and * spacing however far the reader zooms. */ scale: 'zoom' | 'screen'; } export declare class SvgRenderer { static readonly kind: "svg"; readonly viewport: Viewport; root: SVGSVGElement | null; world: SVGGElement | null; baseLayer: SVGGElement | null; marksLayer: SVGGElement | null; /** World space, above the marks: annotation area highlights. */ regionLayer: SVGGElement | null; symbolLayer: SVGGElement | null; overlayLayer: SVGGElement | null; defs: SVGDefsElement | null; /** Pattern and image fills, which are document resources rather than attributes. */ paints: PaintRegistry | null; private readonly pathsByKey; /** Calibrated geometry of live flow paths, so a camera change can rescale them. */ private readonly flowByKey; /** The scale factor last written, so an unchanged camera writes nothing. */ private flowFactor; private readonly symbolsByKey; private readonly marksByKey; /** World positions of live symbols, so a camera change can reposition them. */ private readonly symbolWorld; private readonly markWorld; constructor({ viewport }: { viewport: Viewport; }); mount(container: HTMLElement, { width, height, background, fontFamily, }: { width: number; height: number; background?: string; fontFamily?: string; }): SVGSVGElement; resize(width: number, height: number): void; /** * Push the current camera onto the world group, and reposition screen-space * symbols. The only work that runs on every pan and zoom frame. */ applyCamera(): void; /** * Re-place every screen-space mark from its cached world position. * * This is the per-frame cost of screen-space marks, and it is O(marks) rather * than O(features): the features themselves ride the world transform untouched. */ positionSymbols(): void; /** * Draw (or update) feature fills. * * Paths are keyed and reused across renders so a data update tweens fills * instead of tearing down the DOM, which is what makes `updateSeries` feel like * a chart update rather than a page reload. * * `paint` is the licensed path: a pattern or an image instead of the flat fill. * The flat colour is still written, to `data-fill`, because a paint is a * document reference and the places that need to *reason* about the colour * (darkening on hover, seeding a drilldown) cannot read one out of `url(#id)`. */ drawFeatures({ features, fill, paint, stroke, opacity, seriesId, }: { features: NormalizedFeature[]; fill: (feature: NormalizedFeature) => string; paint?: (feature: NormalizedFeature) => FeaturePaint | null; stroke?: StrokeOptions; opacity?: number; seriesId?: string; }): void; /** * Draw (or update) world-space line marks, such as arcs and routes. * * Thin lines get an invisible wider companion path so they can actually be * hovered: a 1px flight line is effectively unpointable otherwise, and widening * the visible stroke to compensate would misrepresent the value it encodes. */ drawPaths({ paths, seriesId, hitWidth, markClass, animateFlow, }: { paths: PathSpec[]; seriesId?: string; hitWidth?: number; /** CSS class per mark; the hit companion gets `${markClass}-hit`. */ markClass?: string; /** * Whether a flow's beads travel. False still paints them, spaced along the * route: a dotted route reads as a route, which is a better answer under * reduced motion than a route that lost its marks. */ animateFlow?: boolean; }): void; /** * The travelling beads, one dashed companion path per route. * * They go in a group of their own, after the routes, for two reasons. Every * bead then sits above every route, rather than being cut by whichever route * happened to be drawn later, and the only content on the map that repaints * every frame is isolated in its own paint chunk from the geometry that never * moves. */ private drawFlow; /** * Size and space the beads for where the camera is. * * Beads are anchored to the ground rather than to the screen, so a bead stays * over the same stretch of route as the reader zooms, and the number of them on * a route stays what it was set to be. Holding the spacing constant in screen * pixels instead means the count grows with the zoom, and a route that read as * five beads at the opening view is a dotted line at 3x, which is the whole * reason this exists. * * Everything is written in screen pixels, against `non-scaling-stroke`, rather * than left in world units for the camera transform to scale: expressing it in * world units would scale the bead's width by the same factor as its spacing, * and those two want different laws. See `MAX_BEAD_GROWTH`. * * The travel goes with them, so the beads cross the same ground per second at * any zoom and appear faster when the reader is closer, which is what anything * moving over a map does, up to `MAX_FLOW_SPEEDUP`. Past that the cycle is * stretched to hold the speed, because a deep zoom is where the honest version * turns into agitation. * * The factor is the camera's zoom, with nothing subtracted, because `view.fit` * fits the *projection* and never moves the camera: `k` is already the zoom * relative to the view the map opened at, whatever that view was framed on. A * flow's screen-pixel options therefore mean what they say in the opening view of * a map fitted to one country as much as one fitted to the world. * * @param force Write even when the camera has not moved, for newly drawn paths. */ private applyFlowScale; /** * Draw (or update) screen-space symbols. * * Order matters and is the caller's responsibility: the series sorts largest * first so small bubbles end up on top and stay clickable. */ drawSymbols({ symbols, seriesId }: { symbols: SymbolSpec[]; seriesId?: string; }): void; /** * Draw (or update) shaped screen-space marks: markers and point clusters. * * Each mark is a group holding its shape, an optional label, and an invisible * hit circle. The hit circle exists because a 10px star has perhaps 30 square * pixels of actual ink, and a reader should not have to hit the ink. */ drawMarks({ marks, seriesId }: { marks: MarkSpec[]; seriesId?: string; }): void; markGroupFor(seriesId: string, key: string | number): SVGGElement | undefined; /** * Draw (or clear) the selection box. * * Lives in the overlay layer, in screen space: a box drawn in world space would * follow the map if the camera moved under it, which is not what a reader * dragging a box on their screen means. `null` removes it. */ drawSelectBox(box: [[number, number], [number, number]] | null): void; drawBasePath(d: string, attrs: Record, className: string): void; clearBasePath(className: string): void; /** The rendered element for a mark, for hover and selection styling. */ pathFor(seriesId: string, key: string | number): SVGPathElement | undefined; symbolFor(seriesId: string, key: string | number): SVGCircleElement | undefined; markFor(seriesId: string, key: string | number): SVGElement | undefined; overlay(): SVGGElement | null; /** World space, above the marks: where annotation areas are drawn. */ regions(): SVGGElement | null; clearOverlay(): void; /** Remove every mark belonging to a series, e.g. when its type changes. */ clearSeries(seriesId: string): void; destroy(): void; private ensureGroup; /** * Drop marks whose data disappeared, e.g. after a filter or a reload. * * Companion marks (an arc's invisible hit path) are pruned by the same rule: * their keys enter `seen` alongside their primary mark, so a surviving arc * keeps its hit path and a removed arc loses both. Exempting them instead * leaves a transparent, still-hoverable path whose stale item index resolves * to a different arc's data. */ private prune; } //# sourceMappingURL=SvgRenderer.d.ts.map