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;