import type { Canvas2D } from "../charts/canvas-types"; import type { PlotLayout } from "../layout/plot-layout"; import type { Theme } from "../theme/theme"; /** * Minimal positioning input — PlotLayout satisfies this. */ export interface CssBounds { cssWidth: number; cssHeight: number; } export interface TooltipCallbacks { /** * RAF-throttled mouse position in CSS pixels, relative to the GL * canvas (host already subtracted `getBoundingClientRect`). */ onHover(mx: number, my: number): void; /** * Fires on mouseleave; skipped while a pinned tooltip is active. */ onLeave(): void; /** * Fires on click with mouse position. Return true to consume the click * (skipping the default pin/dismiss flow — used for legend clicks). */ onClickPre?(mx: number, my: number): boolean; /** * Fires when a click should pin the current hover target. */ onPin?(mx: number, my: number): void; /** * Fires on dblclick (treemap drill-up gesture). Optional — charts * that don't bind a handler simply ignore the event. */ onDblClick?(mx: number, my: number): void; /** * Fires when an active pin is dismissed by a click on the * already-pinned target (the "click again to unpin" gesture in * `dispatchClick`). Chart impls hook this to emit a * `perspective-global-filter` with `selected: false`. Does *not* * fire on the implicit dismiss inside `pin()` that replaces an * existing pin — that path is followed by a fresh `onPin` which * emits its own `selected: true`. */ onUnpin?(): void; } export interface RenderTooltipOptions { /** * Draw a dashed crosshair at `pos`. Used by scatter/line. */ crosshair?: boolean; /** * Draw a ring of `radius` CSS pixels at `pos`. Used to highlight a * hovered point. Omit for bars (where the bar itself highlights). */ highlightRadius?: number; } /** * Side-channel from the chart back to the host's DOM. The chart calls * into a sink rather than touching the DOM itself; the host * materializes the actual visual (`
` for pinned tooltip, cursor * mutation on the GL canvas). * * - `MessageHostSink` (in worker/) — forwards calls over a * `postMessage`-shaped channel back to * the host. * - `DomHostSink` (in host transport) — receives the matching * envelopes and applies them to the DOM. * * The controller's `_pinned` flag is the source of truth for whether * hover updates are gated; the sink only owns the visual artifact. */ export interface HostSink { pin(lines: string[], pos: { px: number; py: number; }, bounds: CssBounds): void; dismiss(): void; setCursor(cursor: string): void; /** * Forward a `perspective-click` to the host. Optional — only the * worker-bound `MessageHostSink` implements it; `DomHostSink` (the * host-side consumer of pin/dismiss) never sees user-event calls, * so omits the implementation. */ emitUserClick?(detail: UserClickPayload): void; /** * Forward a `perspective-global-filter` to the host with the * `selected: true` / `selected: false` semantics. The host owns the * `removeConfigs` history (mirrors datagrid's * `model._last_insert_configs`); the sink only ships the new state. */ emitUserSelect?(payload: UserSelectPayload): void; } /** * Plain-object payload for `HostSink.emitUserClick`. Matches * `PerspectiveClickDetail` byte-for-byte; defined locally to avoid a * cycle through `event-detail.ts`. */ export interface UserClickPayload { row: Record; column_names: string[]; config: { filter?: unknown[]; }; } /** * Plain-object payload for `HostSink.emitUserSelect`. The host * transport reconstructs a `PerspectiveSelectDetail` class instance * from this plus its cached `_lastInsertConfig`. */ export interface UserSelectPayload { selected: boolean; row: Record; column_names: string[]; insertConfig: { filter?: unknown[]; }; } /** * Owns the hover/click/dblclick state machine and the pinned-tooltip * lifecycle. The renderer drives this purely through * `dispatchHover` / `dispatchLeave` / `dispatchClick` / * `dispatchDblClick` — the host's `RawEventForwarder` captures DOM * events on the GL canvas and posts them as `InteractionEvent`s. * * Pinning + cursor changes go through a {@link HostSink} so the actual * DOM mutations happen host-side regardless of where the chart runs. */ export declare class TooltipController { private _callbacks; private _hoverRAFId; private _hoverTimeoutId; private _host; private _pinned; get isPinned(): boolean; /** * Replace the active host sink. Dismisses any existing pin via the * prior sink so we never leak a pinned artifact across resets — * though in practice each chart instance uses one sink for its * lifetime. */ setHost(sink: HostSink): void; /** * Forward a cursor change to the host. No-op when no host sink is * installed (chart constructed without a transport). */ setCursor(cursor: string): void; /** * Install the chart's tooltip callbacks. The renderer drives the * controller via `dispatchHover` / `dispatchLeave` / * `dispatchClick` / `dispatchDblClick`; this controller never * touches the DOM directly. */ attach(callbacks: TooltipCallbacks): void; detach(): void; /** * Schedule an `onHover` callback for the given canvas-relative * coords. Coalesces multiple calls within one animation frame so * pointer streams don't backlog the chart's hit-test path. * * Workers ship with `requestAnimationFrame` (DedicatedWorkerGlobalScope * exposes it for OffscreenCanvas painting), so the same coalescer * works in both modes. We fall back to setTimeout if RAF is missing * (e.g. node tests without a polyfill). */ dispatchHover(mx: number, my: number): void; dispatchLeave(): void; dispatchClick(mx: number, my: number): void; dispatchDblClick(mx: number, my: number): void; /** * Pin a tooltip (or replace an active one). Forwards through the * configured sink and flips the controller's pinned flag so hover * dispatch is suppressed until dismissal. */ pin(lines: string[], pos: { px: number; py: number; }, bounds: CssBounds): void; dismiss(): void; } /** * Paint a canvas tooltip (crosshair, highlight ring, box + text) onto * `canvas`. The helper normalizes the 2D context to a DPR-scaled * identity transform on entry and restores prior state on exit, so it * composes cleanly with other chrome painters that may have already * called `initCanvas` on the same canvas — re-applying `scale(dpr,dpr)` * blind would double-scale in that case, misplacing the tooltip * proportionally to its distance from the origin. */ export declare function renderCanvasTooltip(canvas: Canvas2D | null, pos: { px: number; py: number; }, lines: string[], layout: PlotLayout, theme: Theme, dpr: number, options?: RenderTooltipOptions): void;