/** * ApexMaps: interactive geographic data visualization for the ApexCharts * ecosystem. * * The public surface is a single declarative, JSON-serialisable options tree * (tier 1), a layer engine underneath (tier 2), and imperative controllers for * anything inherently temporal (tier 3, currently `map.camera`). * * @module ApexMaps */ import './ApexMaps.css'; import { BaseChart } from './core/BaseChart'; import { A11y } from './core/A11y'; import { type GeoFetcher, type GeoPack } from './core/GeoCatalogue'; import type { MapMeta } from './core/MapRegistry'; import { Viewport } from './geo/Viewport'; import { Camera } from './geo/Camera'; import type { ProjectionFactory } from './geo/Projections'; import { SvgRenderer } from './renderers/SvgRenderer'; import type { ExportOptions } from './export/Exporter'; import { ChoroplethSeries } from './series/Choropleth'; import { BubbleSeries } from './series/Bubble'; import { MarkerSeries } from './series/Marker'; import { ArcSeries } from './series/Arc'; import { LineSeries } from './series/Line'; import { BaseFeatures } from './series/BaseFeatures'; import { Legend } from './components/Legend'; import { Tooltip } from './components/Tooltip'; import { Labels } from './components/Labels'; import { Annotations } from './components/Annotations'; import { Breadcrumb } from './components/Breadcrumb'; import { ZoomControls } from './components/ZoomControls'; import { ZoomPan } from './interaction/ZoomPan'; import { GlobeRotation } from './interaction/GlobeRotation'; import type { Palette } from './scales/Palettes'; import type { JoinResult } from './data/Join'; import type { Anchor, ApexMapsEventMap, ApexMapsEventName, ApexMapsOptions, GeoInput, NormalizedGeo, Padding, ResolvedOptions, Series } from './types'; /** Anything the renderer can draw. */ type AnySeries = ChoroplethSeries | BubbleSeries | ArcSeries | LineSeries | MarkerSeries | BaseFeatures; declare class ApexMaps extends BaseChart { userOptions: ApexMapsOptions; config: ResolvedOptions; readonly viewport: Viewport; renderer: SvgRenderer | null; camera: Camera | null; geo: NormalizedGeo | null; /** Data series, excluding the basemap pseudo-series. */ series: (ChoroplethSeries | BubbleSeries | ArcSeries | LineSeries | MarkerSeries)[]; /** What actually gets drawn: the series, or the basemap when there are none. */ renderTargets: AnySeries[]; /** World-space label anchors per feature index. */ anchors: Map; selection: Set; hovered: { seriesId: string; markKey: string | number; } | null; warnings: string[]; rendered: boolean; mapId?: string; mapMeta?: MapMeta; /** * Set by `destroy()` and never cleared: an instance is not reusable. * * `render()` is async (geometry may be a URL or a lazy pack), so a caller that * renders and tears down without awaiting leaves the tail of a render running * against a destroyed map. `rendered` cannot stand in for this, because it is * only set at the end of that same tail. */ private _destroyed; /** Levels drilled into, outermost first. Empty at the top level. */ readonly drillPath: { key: string; name?: string; mapId?: string; }[]; plot: HTMLElement | null; legend: Legend | null; tooltip: Tooltip | null; labels: Labels | null; annotations: Annotations | null; a11y: A11y | null; zoomPan: ZoomPan | null; globe: GlobeRotation | null; breadcrumb: Breadcrumb | null; zoomControls: ZoomControls | null; private _listeners; /** * Premium features this map's current options put into use. Rebuilt from the * resolved config on every `_checkPremium`, so removing the option removes the * watermark: a mark that outlives what caused it describes the map's history * rather than the map, and reads as a bug. Responsive rules and the framework * wrappers both rewrite the config routinely, so this is a normal event, not * an edge case. */ private readonly _premiumUsed; /** * Premium features used imperatively, which nothing writes to yet. * * The set above cannot hold these: a morph transition, a story step or a * playback run leaves no option behind to recompute from, so recording it there * would clear the watermark the moment the animation ended. Anything triggered * by a call rather than by config belongs here, and here it stays for the life * of the instance. */ private readonly _premiumInvoked; private _resizeObserver; private _renderRaf; private _resizeRaf; private _rotateRaf; /** The rotation the map opened at, which `resetView` returns a globe to. */ private _initialRotation; private _enterTimer; private _attribution; private _a11yMounted; private _warnedLinkKeys; private readonly _drillStack; /** Guards against a second click landing while a level is still loading. */ private _drilling; /** The level being faded out, live only for the length of a level change. */ private _ghost; /** The level being developed, which outlives the level change by its own tail. */ private _reveal; private readonly _onMarkPointerOver; private readonly _onMarkPointerMove; private readonly _onMarkPointerOut; private readonly _onMarkClick; private readonly _onKeyDown; private readonly _onSurfacePointerMove; private readonly _onSurfacePointerLeave; private readonly _onSurfaceClick; /** * The point mark currently hovered by proximity rather than directly. Owning * this separately from `hovered` is what lets the direct handlers know when * to yield: a pointerout from the feature underneath must not clear a hover * that belongs to the bubble beside it. */ private _proximity; constructor(element: HTMLElement, options?: ApexMapsOptions); /** Build and draw. Async because geometry may be a URL or a lazy pack. */ render(): Promise; /** * @param meta Provenance for the geometry being ingested. Defaults to the * current map's, and is passed explicitly while drilling, where the child * pack's recommended key is not yet the instance's. */ private _ingest; private _mountShell; private _measure; /** Column width a left or right legend claims, and zero for top or bottom. */ private _reservedLegendWidth; /** * Tell the root which side the legend is on. * * The root is the layout container (see `ApexMaps.css`), so the position is a * class on it rather than a DOM move: the legend keeps its place in the tab * order and its element identity through a position change, and `updateOptions` * can flip a legend from bottom to left without a re-render. */ private _applyLegendLayout; /** * @param keepRotation Restore a spin the reader had applied. Passed by the * resize path only: a *projection* change is a new starting point and takes * the rotation the new spec asked for. */ private _buildViewport; private _buildSeries; private _draw; /** * Draw the world-space layers: features, symbols, marks and paths. * * Split out of `_draw` because it is also the whole of what a globe rotation * has to redo. A spin changes every projected coordinate on the map, but it * changes nothing about the legend, the attribution or the accessible * description, and rebuilding those sixty times a second is how a smooth * gesture turns into a janky one. */ private _drawGeometry; private _drawBaseLayers; /** * Lay out the screen-space overlay: annotations first, then labels. * * The order is the contract. Annotations publish the boxes they occupy and * labels treat those as already taken, so a generated label gives way to an * editorial one rather than winning by arriving first. */ private _drawOverlay; /** * Re-point every component at the live config. * * Components take their options at construction, and construction happens in * `_mountShell` (once) or `_buildViewport` (only for a map or projection * change). `buildConfig` returns a fresh tree each time, so without this an * `updateOptions` that changed nothing else left each component reading a * snapshot from first render. That had silently broken `dataLabels`, * `legend.position`/`align` and `tooltip.offset` for any caller who set them * after render, which is the same "set it and get silence" failure the * options audit exists to prevent; it also covers responsive rules, which * reach these components by exactly the same path. */ private _syncComponentOptions; private _drawAnnotations; private _drawLabels; private _drawLegend; private _drawAttribution; private _setupA11y; /** * Create (or recreate) the gesture handling from the current config. * * Idempotent on purpose: `updateOptions` calls it again when the interaction * tree changes, because ZoomPan decides its listener set at attach time and a * gesture handler reading an abandoned config is how "zoom.enabled: false set * later does nothing" happens. */ private _attachInteraction; /** * The controls' options in object form, `show: false` when they are off. * * `controls: false` is what a caller writes, and the resolved tree carries an * object; normalising here means the component never has to know about both. */ private _zoomControlsOptions; private _zoomControlsState; /** * Step the zoom about the centre of the plot, animated. * * Centre rather than the pointer, because a button press has no position on the * map: anchoring to where the button happens to sit would drag the geography * towards the corner. Animated for the same reason the double-click zoom is: a * scale change that teleports loses the reader's place. */ private _zoomStep; private _bindMarkEvents; /** Resolve a DOM event target to a mark, uniformly across series types. */ private _resolveMark; private _featureMark; private _itemMark; private _handleMarkPointerOver; private _handleMarkPointerMove; private _handleMarkPointerOut; private _handleMarkClick; /** Act on a resolved mark, exactly as a direct click on it does. */ private _activateMark; /** * The nearest point mark within the proximity radius of a screen position. * * Computed, not rendered: an actual DOM Voronoi layer would swallow pointer * events for the entire plot, taking features and arcs with it, and would * need rebuilding every time clustering or the camera changed. Resolving * nearest-within-a-threshold keeps the property that matters (near a small * mark, the nearest mark wins, exactly where its Voronoi cell would) and * leaves the rest of the map to its own handlers. * * Distances compare in world space, which is safe because the camera scale * is uniform: nearest in world is nearest on screen, and a screen radius * divides by `k` to become a world radius. */ private _resolveNearest; /** * The proximity pass, on every pointer move over the plot. * * Direct hits on point and path marks keep precedence, because z-order is * meaningful where marks overlap: the small bubble painted on top of a large * one must win when the pointer is actually on it. Features and empty * basemap yield to a nearby point mark, because on the most common combined * map (bubbles over a choropleth) everything near a bubble is over some * feature, and a proximity assist that only worked over blank ocean would * not be one. */ private _handleSurfacePointerMove; private _handleSurfacePointerLeave; /** * Capture-phase click companion to the proximity pass, so that what the * tooltip shows is what the click acts on: a click that hover attributed to * a nearby bubble must not select the feature underneath instead. */ private _handleSurfaceClick; private _handleKeyDown; /** * `+` and `-` zoom, and `0` returns the opening view. * * The keyboard path exists whether or not the controls are rendered, because a * host that hides them is styling the map, not opting its readers out of * navigating it. Only while the map surface itself holds focus: the container * listener is on the whole element, and a `-` typed into a host's own input * inside it is a character, not a gesture. * * @returns Whether the key was consumed. */ private _handleZoomKey; /** * Frame a cluster's members. * * Members that share a position give a zero-size box, which would ask the camera * for infinite zoom, so that case steps in by a fixed factor instead. */ private _zoomToCluster; /** Levels below the top level currently displayed. */ get drillDepth(): number; /** * Drill into a feature by key, exactly as a click on it would. * * @returns Whether a deeper level was entered. False means the drilldown * declined: no such feature, no `drilldown` configured, the child map is the * one already on screen, or no child feature belongs to this parent. Each * case explains itself in the dev-mode diagnostics. */ drillTo(key: string): Promise; /** * Climb back out. `levels` of `Infinity` returns to the top. * * Synchronous work, awaited only for the camera move: the geometry for each * level above is still held, so going back never refetches or re-ingests. */ drillUp(levels?: number): Promise; /** * Replace the map with a deeper level, scoped to one feature. * * The two halves are deliberately ordered: geometry starts loading immediately * but the camera move runs first, so a cold child pack downloads while the * reader watches the parent feature fill the frame, and the swap then happens * between two views of the same geography at the same size. Ordering it the * other way makes the click feel unresponsive for as long as the fetch takes. */ private _drill; /** Swap in a level's geometry, keeping `geo.map` honest for later updates. */ private _enterLevel; private _resetDrill; private _restoreLevel; private _isCurrentMap; private _drilldownOptions; private _drillAnimation; /** Whether a level change animates: the author's option and the reader's setting. */ private _animateLevels; /** * The screen rect a GeoJSON object occupies under the live projection and * camera. Null when the object projects to nothing, which is a clipped feature * on a globe or an empty collection. */ private _screenRectOf; /** * The camera that lands a world-space box on a given screen rect. * * Expressed as padding because that is exactly what it is: "fit this box into * that rect" is `cameraForBounds` with the rect's insets as the padding, which * keeps one implementation of the fit arithmetic rather than a second one that * can disagree with it. */ private _cameraForRect; /** * Copy the level about to be replaced, so it can fade out over the one * replacing it. See `renderers/LevelGhost` for why a camera move alone cannot * cover the swap. */ private _captureLevel; /** * Drop the copy. Called from the `finally` of both level changes, so the DOM is * back to one level per map by the time either promise resolves and a caller * counting marks never sees two. */ private _releaseLevel; /** * Develop the level just drawn out of the shape it replaced: see * `renderers/LevelReveal`. * * The ripple starts at the middle of the level, which is the middle of the * feature that was clicked, because the child covers that feature's geography * and nothing else. Ordering runs off the label anchors, which are already * projected for this level and are the one position per feature the map keeps. */ private _revealLevel; private _renderBreadcrumb; private _drillAnnouncement; private _eventPayload; private _setHover; private _clearHover; /** * Put the legend's arrow where the hovered feature falls on the scale. * * The whole point of a choropleth legend is the value-to-colour mapping, and * the reader normally has to run it backwards by eye. Hovering runs it for * them. A feature with no value parks the arrow rather than pointing at zero, * which would be a lie about missing data. */ private _moveLegendMarker; /** * Put a mark's outline back after a hover changed it. * * The right stroke is not the series default: a selected mark keeps its * selection outline, which is the same precedence `_applySelectionStyles` * applies to every mark at once. */ private _restoreStroke; private _focusFeature; private _describeFeature; /** * What Enter on a focused feature does: exactly what a click on it would. * * That means drilling when the series has a drilldown, and toggling selection * otherwise. Keyboard users had only the selection half, which left the way * into a drilldown mouse-only while the way out (Escape) worked, and a11y * parity is not a place this product accepts "mostly". */ private _activateFeature; private _onLegendToggle; private _redrawFills; private _onCameraChange; private _observeResize; private _relayout; /** * Recompute every feature's label anchor. * * Anchors live in a side map keyed by feature index rather than on the feature * objects, which hold the caller's properties and geometry. They are * world-space, so they survive camera changes and only need recomputing when * the projection changes, which includes the globe turning. */ private _rebuildAnchors; /** * Redraw after the sphere has turned. * * A rotation is a projection change, so unlike a pan or a zoom it invalidates * every projected coordinate: paths, arc geometry, bubble positions and label * anchors all have to be rebuilt. That is the price * of the gesture and there is no cheaper correct version, so the work is * coalesced to one pass per frame instead: a 120 Hz mouse delivers pointer * moves faster than the display can show them, and reprojecting twice for one * painted frame is pure waste. * * @param immediate Redraw now rather than on the next frame. For a camera * move, which is already once per frame and has to stay in step with the * affine transform applied straight after it. */ private _onRotate; private _redrawRotated; /** Rebuild projection-dependent geometry after a projection or size change. */ private _reprojectSeries; /** Replace series data, tweening fills rather than rebuilding the DOM. */ updateSeries(series: readonly Series[]): this; /** * Merge new options and redraw. * * @param flags.redrawGeometry Force reprojection. Inferred for map and * projection changes, so it is only needed for exotic cases. */ updateOptions(options: ApexMapsOptions, { redrawGeometry }?: { redrawGeometry?: boolean; }): Promise; toggleSelection(key: string): this; setSelection(keys: readonly string[]): this; clearSelection(): this; /** * Restyle, announce, and propagate to the link group. * * @param source Instance the change originated from. A selection arriving from a * peer is applied and re-emitted locally but never rebroadcast, which is what * keeps a bidirectional group from ringing. */ private _selectionChanged; private _applySelectionStyles; private _handleSelectBox; /** * Select everything whose anchor falls inside a screen-space box. * * **Anchors, not bounding boxes.** A feature's bbox is the wrong test: Alaska's * spans the Pacific, so any box touching the Aleutians would select it, and a box * over the Great Lakes would select half a dozen states it does not visibly * cover. Testing the label anchor (the point already computed for labelling, which * sits inside the shape) matches what the reader thinks they are enclosing. * * A box that catches nothing clears the selection, which is the only obvious way * a reader can undo one. */ private _selectInBox; /** * Push this map's selection to the others in its `link.group`. * * Peers are read from their live config rather than from what they registered * with, so a group changed through `updateOptions` takes effect. */ private _broadcastSelection; private _receiveSelection; private _matchesAnyKey; /** Frame a feature by key. */ frameFeature(key: string, options?: { padding?: Padding; duration?: number; transition?: 'fly' | 'ease' | 'jump'; }): Promise; /** * The projection's rotation, `[lambda, phi, gamma]` in degrees. `[0, 0, 0]` * on a projection that cannot rotate. */ get rotation(): [number, number, number]; /** * Turn the globe to an absolute rotation, as a drag would. * * Note that this is not the camera: it moves the sphere under the projection * rather than the viewer over the map, so it reprojects rather than * transforms. On a projection that cannot rotate it does nothing. */ rotateTo(angles: readonly [number, number, number?]): this; /** * Step the zoom in by `interaction.zoom.step` (1.6 by default), about the * centre of the plot. What the `+` control does, so a host that renders its own * chrome behaves identically to the built-in one. */ zoomIn(): this; /** Step the zoom out. See {@link zoomIn}. */ zoomOut(): this; /** The camera's current scale, 1 at the opening fit. */ get zoom(): number; /** * Reset the camera to the initial fit, and a spun globe to the rotation it * opened at: on an orthographic the spin *is* where the reader has navigated * to, so resetting the camera alone would leave the map where they left it. */ resetView(options?: { padding?: Padding; duration?: number; transition?: 'fly' | 'ease' | 'jump'; }): Promise; /** * The join diagnostic for a series, as data. The console version prints * automatically in dev mode; this is the programmatic form for tests and CI. */ diagnoseJoin(seriesIndex?: number): JoinResult | null; /** * Serialise the effective spec. The round-trip that later makes saved * dashboards, static export and agent authoring possible. */ toSpec(): ApexMapsOptions; /** * The current view as a standalone SVG document. * * Computed styles are inlined, so dark mode, custom properties and everything * else the stylesheet decides survive leaving the page. The legend and * tooltips are HTML outside the SVG, so the export is the map plot itself. */ getSvgString(options?: ExportOptions): string; /** Download the current view as an `.svg` file. */ exportSVG(options?: ExportOptions): void; /** * Download the current view as a `.png`. * * `scale` multiplies pixel density (default 2, which survives print and * retina). The background defaults to the container's own, falling back to * white, so a dark-mode map arrives dark rather than as pale strokes on * transparency. */ exportPNG(options?: ExportOptions): Promise; /** * The current view as a PNG data URI, for embedding rather than downloading. * Mirrors `chart.dataURI()` in core apexcharts. */ dataURI(options?: ExportOptions): Promise<{ imgURI: string; }>; private _exportRaster; private _exportFilename; on(event: K, handler: (payload: ApexMapsEventMap[K]) => void): this; off(event: K, handler?: (payload: ApexMapsEventMap[K]) => void): this; emit(event: K, payload?: ApexMapsEventMap[K]): void; private _isDebug; /** * Warn about options that are declared in the public tree but not implemented. * * These are worse than absent: a caller can set them and get silence. Until * each is built or withdrawn, setting one says so in the dev diagnostics. * Checked against `userOptions`, because the resolved config always carries * the defaults and cannot say what the caller asked for. */ private _warnUnimplemented; /** * Print join diagnostics and cartographic advice. * * Dev-mode only and grouped, so it is useful during development and invisible in * production. This is the cheapest high-goodwill feature in the product: it turns * "my map is grey" from an hour of string-diffing into a one-line answer. */ private _reportDiagnostics; /** * Declare which premium features this spec actually uses. * * Called on render and on every options change, so a map that gains a story * context or a link group later is evaluated then rather than staying on * whatever the first render decided. */ private _checkPremium; /** * Mark a premium feature as in use. Basic maps never call this, which is how the * free tier stays watermark-free. * * The parameter is the `PremiumFeature` union rather than a string, so a typo at * a call site is a compile error. It used to be a string checked against the set * at runtime, which silently made the feature free. */ private _requirePremium; private _evaluateLicense; /** * Publish state options that CSS applies, rather than writing them per mark. * Muting 3,000 features is then a class toggle each instead of 3,000 style writes. */ private _applyStateVars; /** * Publish `chart.animations` as the CSS variables the transitions read. * * The transitions themselves live in the stylesheet, permanently armed on the * value-carrying properties (fill, r, stroke-width) and never on the * camera-driven ones, so a data update tweens while a pan stays a single * transform write per frame. What the engine decides per draw is only the * duration: zero when animations are off, when the reader prefers reduced * motion, or (geometry first, then everything) when the mark count outgrows * the motion budget, because dropped frames read as a bug while a simpler * transition just reads as restraint. */ private _applyMotionVars; /** * The duration a mark's cheap properties (fill, stroke) will actually * transition for, in ms, which is what `--apexmaps-anim` is set to. * * Read as well as written, because an effect built out of those transitions has * to know whether they are going to run at all: at zero the level reveal would * be a single frame of flat colour rather than a ripple, so it declines instead. */ private _markAnimationMs; /** * Whether a flow's beads travel, or are painted spaced along the route and left * there. * * Counted per series rather than over the whole map, because the cost is the * routes being repainted and a choropleth underneath them contributes nothing to * it. `motionBudget` is reused for the decision so that `prefers-reduced-motion` * still answers first, at whatever the route count happens to be. */ private _flowTravels; /** Marks this draw will produce, for the motion budget. */ private _markCount; /** * Fade the mark layers in on first paint, when configured. * * Only ever called from `render()`: a drilldown or an options update is a * continuation of something already on screen, and replaying an entrance * there would present old acquaintances as arrivals. */ private _entrance; private _isDark; /** Tear down: listeners, observers, animation frames, DOM. */ destroy(): void; /** * Set the global licence key. Shared across the whole ApexCharts family, so one * customer key works everywhere. */ static setLicense(key: string): typeof ApexMaps; static registerMap(id: string, geometry: GeoInput | (() => Promise), meta?: MapMeta): typeof ApexMaps; /** * Register a projection factory under a name. * * Registering is free, and so is every built-in projection. Rendering a map * *with* a projection registered here is a licensed feature: works without a * key for evaluation, with a watermark. Re-registering a built-in name over the * built-in stays free, since the gate is by name. */ static registerProjection(name: string, factory: ProjectionFactory): typeof ApexMaps; static registerPalette(name: string, palette: Palette): typeof ApexMaps; /** * Point the geometry catalogue at a copy of the dataset: a base URL, or a * loader function for bundler and air-gapped use. */ static setGeoSource(source: string | GeoFetcher): typeof ApexMaps; static listMaps(): string[]; /** * The built-in catalogue with provenance, for a picker UI or a docs table. * Excludes anything registered by hand through `registerMap()`. */ static catalogue(): GeoPack[]; /** * Provenance for a registered map: source, licence, vintage, boundary policy, * recommended join key. */ static mapMeta(id: string): MapMeta | undefined; static listProjections(): string[]; /** * Registered palette names, including any added through `registerPalette()`. * * The counterpart of `listMaps()` and `listProjections()`: without it a palette * picker or a docs table has to hard-code the list and go stale. */ static listPalettes(): string[]; /** * A palette's anchor stops and family. The class colours a map actually draws * are these stops sampled in OkLab to the class count, so a swatch built from * `stops` shows the ramp, not the classes. */ static palette(name: string): Palette | undefined; static getInstance(id: string): ApexMaps | undefined; static get version(): string; } export default ApexMaps; //# sourceMappingURL=ApexMaps.d.ts.map