@tanstack/charts
Version:
A chart grammar for TypeScript and JavaScript. Marks consume your data directly, channels describe visual encodings, and the engine compiles them into a renderer-neutral keyed scene. TanStack's compact scales cover common numeric and categorical mappings.
896 lines (895 loc) • 32.7 kB
JavaScript
import { createChartRuntime } from "./runtime.js";
import { resolveChartHostTabIndex } from "./adapter-shared.js";
import { createDomTextMeasurer } from "./dom-text.js";
import { findNearestPoint, viewportInteractionPoints } from "./scene.js";
import { focusDisabled } from "./focus-disabled.js";
import { nearestPoint } from "./nearest.js";
import { connectRendererTooltipMotion } from "./renderer-motion-internal.js";
import {
chartPointFromNavigationOrder,
chartPointFromSceneOrder,
resolveChartFocusStrategy,
resolveChartPointerFocus,
restoreChartFocusPoint,
sameChartPointIdentity
} from "./interaction.js";
import {
createChartCursorHostSession
} from "./cursor-host-contract.js";
function mountChartRenderer(container, initialOptions, runtime = createChartRuntime()) {
resolveTooltipInput(initialOptions.definition.tooltip);
let options = initialOptions;
let scene;
let interactionScene;
let focusedPoint = null;
let focusSource = "pointer";
let focusOwner = null;
let pointerPosition = null;
let pinnedKey = null;
let observer;
let renderFrame;
let forceScheduledRender = false;
let scheduledRenderReason;
let destroyed = false;
let hasRendered = false;
let surface;
let unsubscribePresentation;
let renderingSurface = false;
let paintingFocus = false;
let tooltipExtension;
let tooltipInstance;
let suppressNextSurfaceFocus = false;
let spatialIndex;
const controlInstances = /* @__PURE__ */ new Map();
let cursorSession;
let cursorMode;
let cursorMatch;
let cursorExtension;
let renderedCursorBinding;
let unsubscribeCursor;
let cursorPresentation = null;
let hasCursorPresentation = false;
const previousPosition = container.style.position;
const view = container.ownerDocument.defaultView;
const computedPosition = view?.getComputedStyle(container).position;
const ownsPosition = !computedPosition || computedPosition === "static";
const domText = createDomTextMeasurer(container);
const fontSet = container.ownerDocument.fonts;
if (ownsPosition) container.style.position = "relative";
const render = (refreshText = false, reason = "update") => {
if (destroyed) return;
if (refreshText && !options.measureText) domText.refresh();
const previousFocusedPoint = focusedPoint;
const previousCursorPresentation = cursorPresentation;
const previousCursorBinding = renderedCursorBinding;
scene = createHostedScene(createScene());
interactionScene = scene;
if (!surface) {
surface = options.renderer.mount(container, scheduleRender);
subscribeToPresentation();
} else if (surface.renderer !== options.renderer) {
unsubscribePresentation?.();
unsubscribePresentation = void 0;
destroyTooltip();
destroyHostControls();
surface.destroy();
container.replaceChildren();
surface = options.renderer.mount(container, scheduleRender);
subscribeToPresentation();
hasRendered = false;
}
renderingSurface = true;
try {
surface.render(scene, {
ariaLabel: options.ariaLabel,
ariaDescription: options.ariaDescription,
className: options.className,
tabIndex: resolveChartHostTabIndex(
options.definition,
options.tabIndex
),
idPrefix: options.idPrefix,
animation: hasRendered ? resolveAnimation(options.definition.svgAnimation, container, reason) : void 0
});
} finally {
renderingSurface = false;
}
syncHostControls();
hasRendered = true;
const presentedPoints = interactionPoints();
spatialIndex = options.definition.focus === false ? void 0 : options.definition.spatialIndex?.(
viewportInteractionPoints(scene, scene.points),
{ scene }
);
const nextCursorBinding = cursorBinding();
renderedCursorBinding = nextCursorBinding;
if (nextCursorBinding) {
applyCursorState(true);
} else if (previousCursorBinding) {
cursorPresentation = null;
focusedPoint = null;
pinnedKey = null;
pointerPosition = null;
focusOwner = null;
paintFocus(null, []);
if (previousFocusedPoint) {
options.onFocusChange?.(null);
options.onFocusGroupChange?.([]);
}
} else {
cursorPresentation = null;
const trackedPointer = (focusOwner === "pointer" || focusOwner === "controlled" && focusSource === "pointer") && pinnedKey === null ? pointerPosition : null;
const nextFocusedPoints = trackedPointer ? resolvePointerFocus(trackedPointer.x, trackedPointer.y, maxDistance()) : previousFocusedPoint ? (() => {
const restored = restoreChartFocusPoint(
presentedPoints,
previousFocusedPoint
);
return restored ? focusPointsForPoint(restored, presentedPoints) : [];
})() : [];
const nextFocusedPoint = nextFocusedPoints[0] ?? null;
focusedPoint = nextFocusedPoint;
if (!nextFocusedPoint) pinnedKey = null;
if (previousFocusedPoint || nextFocusedPoint || previousCursorPresentation) {
if (!trackedPointer) focusSource = "restored";
paintFocus(nextFocusedPoint, nextFocusedPoints);
options.onFocusChange?.(nextFocusedPoint);
options.onFocusGroupChange?.(nextFocusedPoints);
}
}
const onRender = options.onRender;
if (onRender) {
onRender({ container, scene, surface, interaction });
}
};
const currentWidth = () => {
const width = options.width ?? container.getBoundingClientRect().width;
return options.width !== void 0 || width > 0 ? width : void 0;
};
const configureObserver = () => {
observer?.disconnect();
observer = void 0;
if (options.width !== void 0) return;
const ResizeObserverConstructor = view?.ResizeObserver;
if (!ResizeObserverConstructor) return;
observer = new ResizeObserverConstructor(() => {
const width = currentWidth();
if (width === void 0 || width === scene.width) return;
scheduleRender(false, "resize");
});
observer.observe(container);
};
const scheduleRender = (force = false, reason = "layout") => {
forceScheduledRender ||= force;
scheduledRenderReason = scheduledRenderReason === "layout" || reason === "layout" ? "layout" : "resize";
if (renderFrame !== void 0) return;
if (!view?.requestAnimationFrame) {
const nextWidth = currentWidth();
const shouldRender = forceScheduledRender || nextWidth !== void 0 && nextWidth !== scene.width;
forceScheduledRender = false;
const nextReason = scheduledRenderReason ?? "layout";
scheduledRenderReason = void 0;
if (shouldRender) render(true, nextReason);
return;
}
renderFrame = view.requestAnimationFrame(() => {
renderFrame = void 0;
const nextWidth = currentWidth();
const shouldRender = forceScheduledRender || nextWidth !== void 0 && nextWidth !== scene.width;
forceScheduledRender = false;
const nextReason = scheduledRenderReason ?? "layout";
scheduledRenderReason = void 0;
if (shouldRender) render(true, nextReason);
});
};
const handleFontLoad = () => {
if (destroyed || options.measureText) return;
domText.invalidate();
scheduleRender(true);
};
const cursorBinding = () => options.definition.cursor;
const cursorIsPinned = () => cursorSession?.getState()?.pinned === true;
const interactionIsPinned = () => pinnedKey !== null || cursorIsPinned();
const configureCursorController = () => {
const nextBinding = cursorBinding();
if (nextBinding) hasCursorPresentation = true;
const nextController = nextBinding?.controller;
const nextMode = nextBinding?.mode;
const nextMatch = nextBinding?.mode === "focus" ? nextBinding.match ?? "xy" : void 0;
if (nextController === cursorSession?.controller && nextMode === cursorMode && nextMatch === cursorMatch && nextBinding?.use === cursorExtension) {
return;
}
unsubscribeCursor?.();
unsubscribeCursor = void 0;
cursorSession?.destroy();
cursorSession = nextBinding ? createChartCursorHostSession(nextBinding) : void 0;
cursorMode = nextMode;
cursorMatch = nextMatch;
cursorExtension = nextBinding?.use;
unsubscribeCursor = cursorSession?.subscribe(() => {
if (!destroyed && hasRendered) applyCursorState(false);
});
};
const applyCursorState = (notifyRestored) => {
const binding = cursorBinding();
if (!binding) {
cursorPresentation = null;
return;
}
const session = cursorSession;
if (!session) return;
const state = session.getState();
if (state?.source !== "pointer" || !session.owns(state)) {
pointerPosition = null;
}
cursorPresentation = session.resolvePresentation(scene, binding, state);
const previous = focusedPoint;
if (binding.mode === "focus") {
const focus = resolveChartFocusStrategy(options.definition.focus);
const points = session.resolveFocus(
interactionPoints(),
binding,
state,
focus
);
const point = points[0] ?? null;
if (state) focusSource = state.source;
pinnedKey = state?.pinned && point ? point.key : null;
focusedPoint = point;
paintFocus(point, points);
if (!sameChartPointIdentity(previous, point) || notifyRestored && (previous !== null || point !== null)) {
options.onFocusChange?.(point);
options.onFocusGroupChange?.(points);
}
return;
}
pinnedKey = null;
focusedPoint = null;
paintFocus(null, []);
if (previous) {
options.onFocusChange?.(null);
options.onFocusGroupChange?.([]);
}
};
const publishFocusCursor = (points, pinned = interactionIsPinned()) => {
const binding = cursorBinding();
if (binding?.mode !== "focus") return false;
const session = cursorSession;
if (!session) return false;
const point = points[0];
if (!point) {
session.clearOwnedTransient();
return true;
}
session.publish(
session.createFocusState(scene, binding, {
primary: point,
group: points,
source: focusSource,
pinned
})
);
return true;
};
const updateFocus = (points, forcePaint = false) => {
const point = points[0] ?? null;
if (publishFocusCursor(points)) return;
if (sameChartPointIdentity(point, focusedPoint)) {
focusedPoint = point;
if (forcePaint) paintFocus(point, points);
return;
}
focusedPoint = point;
paintFocus(point, points);
options.onFocusChange?.(point);
options.onFocusGroupChange?.(points);
};
const dismissTooltip = () => {
const binding = cursorBinding();
if (!focusedPoint && !pinnedKey && !cursorSession?.getState()) return;
const restoreFocus = Boolean(
tooltipInstance?.contains(container.ownerDocument.activeElement)
);
pinnedKey = null;
pointerPosition = null;
focusOwner = null;
if (binding) cursorSession?.clear();
else updateFocus([]);
const element = surface?.element;
if (restoreFocus && element && "focus" in element && typeof element.focus === "function") {
suppressNextSurfaceFocus = true;
element.focus();
}
};
const paintFocus = (point, points) => {
paintingFocus = true;
let paintedScene;
try {
const focus = point ? {
primary: point,
group: points,
source: focusSource,
pinned: interactionIsPinned()
} : null;
paintedScene = hasCursorPresentation ? surface?.paintFocus(focus, pointerPosition, cursorPresentation) : surface?.paintFocus(focus, pointerPosition);
} finally {
paintingFocus = false;
}
interactionScene = paintedScene ?? scene;
paintTooltip(point, points);
};
const resolveClientPointer = (clientX, clientY) => {
const position = surface?.clientToScene?.(scene, clientX, clientY);
if (!position) return null;
return {
position,
points: resolvePointerFocus(position.x, position.y, maxDistance())
};
};
const pointsAtPointer = (clientX, clientY) => {
const resolved = resolveClientPointer(clientX, clientY);
pointerPosition = resolved?.position ?? null;
return resolved?.points ?? [];
};
const interaction = {
clientToScene(clientX, clientY) {
if (destroyed) return null;
return surface?.clientToScene?.(scene, clientX, clientY) ?? null;
},
resolvePointer(clientX, clientY) {
if (destroyed) return null;
const resolved = resolveClientPointer(clientX, clientY);
const point = resolved?.points[0];
return resolved && point ? {
position: resolved.position,
point,
points: resolved.points
} : null;
},
setControlledFocus(target, controlledOptions = {}) {
if (destroyed) return;
focusOwner = "controlled";
if (!target) {
focusSource = controlledOptions.source ?? "programmatic";
pointerPosition = null;
pinnedKey = null;
updateFocus([]);
focusOwner = null;
return;
}
let resolution;
let targetPoint;
if (isPointerResolution(target)) {
resolution = target;
targetPoint = target.point;
} else {
resolution = null;
targetPoint = target;
}
focusSource = controlledOptions.source ?? (resolution === null ? "programmatic" : "pointer");
const points = interactionPoints();
const point = restoreChartFocusPoint(points, targetPoint);
pointerPosition = resolution?.position ?? null;
if (!point) {
pinnedKey = null;
updateFocus([]);
focusOwner = null;
return;
}
const focusPoints = focusPointsForPoint(point, points);
pinnedKey = controlledOptions.pinned && (tooltipIsSticky() || cursorBinding()?.pin === true) ? point.key : null;
if (sameChartPointIdentity(point, focusedPoint)) {
focusedPoint = point;
paintFocus(point, focusPoints);
return;
}
updateFocus(focusPoints);
}
};
const scenePositionAtPointer = (clientX, clientY) => {
const position = surface?.clientToScene?.(scene, clientX, clientY);
pointerPosition = position ?? null;
return position;
};
const updateFreeCursorAtPointer = (clientX, clientY) => {
const binding = cursorBinding();
if (binding?.mode !== "free") return false;
if (cursorIsPinned()) return true;
const position = scenePositionAtPointer(clientX, clientY);
if (!position || !plotContains(scene, position)) {
pointerPosition = null;
cursorSession?.clearOwnedTransient();
return true;
}
const session = cursorSession;
if (!session) return false;
session.publish(
session.createFreeState(scene, binding, position, "pointer", false)
);
return true;
};
const handlePointerMove = (event) => {
if (controlContains(event.target)) {
if (!interactionIsPinned()) {
pointerPosition = null;
updateFocus([]);
}
return;
}
if (options.definition.pointer === false || interactionIsPinned()) return;
focusOwner = "pointer";
focusSource = "pointer";
if (updateFreeCursorAtPointer(event.clientX, event.clientY)) return;
updateFocus(
pointsAtPointer(event.clientX, event.clientY),
tooltipTracksPointer()
);
};
const handlePointerDown = (event) => {
if (options.definition.pointer === false || interactionIsPinned()) return;
focusOwner = "pointer";
focusSource = "pointer";
updateFreeCursorAtPointer(event.clientX, event.clientY);
};
const clearPointerFocus = ({ relatedTarget }) => {
if (options.definition.pointer !== false && focusOwner === "pointer" && !interactionIsPinned() && !(view && relatedTarget instanceof view.Node && container.contains(relatedTarget))) {
pointerPosition = null;
const binding = cursorBinding();
if (binding) cursorSession?.clearOwnedTransient();
else updateFocus([]);
focusOwner = null;
}
};
const clearKeyboardFocus = ({ relatedTarget }) => {
if (focusOwner === "keyboard" && !pinnedKey && !(view && relatedTarget instanceof view.Node && container.contains(relatedTarget))) {
pointerPosition = null;
const binding = cursorBinding();
if (binding) cursorSession?.clearOwnedTransient();
else updateFocus([]);
focusOwner = null;
}
};
const handleClick = (event) => {
if (controlContains(event.target)) return;
if (options.definition.pointer === false) return;
const activeTooltip = tooltipInstance;
const NodeConstructor = container.ownerDocument.defaultView?.Node;
const originatedInTooltip = NodeConstructor ? event.composedPath().some(
(target) => target instanceof NodeConstructor && activeTooltip?.contains(target)
) : activeTooltip?.contains(event.target);
if (activeTooltip && originatedInTooltip) {
return;
}
focusOwner = "pointer";
const binding = cursorBinding();
if (binding?.mode === "free") {
const state = cursorSession?.getState();
if (!state) {
updateFreeCursorAtPointer(event.clientX, event.clientY);
}
const current = cursorSession?.getState();
if (binding.pin && current) {
if (current.pinned) {
cursorSession?.clear();
} else {
cursorSession?.publish({ ...current, pinned: true });
}
}
options.onSelect?.(null);
return;
}
const points = pointsAtPointer(event.clientX, event.clientY);
focusSource = "pointer";
const point = points[0] ?? null;
let pinChanged = false;
const canPin = tooltipIsSticky() || binding?.pin === true;
if (canPin) {
if (interactionIsPinned()) {
pinnedKey = null;
pinChanged = true;
if (binding) cursorSession?.clear();
} else if (point) {
pinnedKey = point.key;
pinChanged = true;
}
}
if (!(binding && canPin && pinChanged && !pinnedKey)) {
updateFocus(points, pinChanged);
}
options.definition.selection?.change(point, "pointer");
options.onSelect?.(point);
};
const handleKeyDown = (event) => {
if (controlContains(event.target)) return;
const binding = cursorBinding();
if (event.key === "Escape" && cursorSession?.getState()) {
event.preventDefault();
dismissTooltip();
return;
}
if (event.key === "Escape" && pinnedKey) {
event.preventDefault();
dismissTooltip();
return;
}
if (options.definition.keyboard === false || binding?.mode === "free") {
return;
}
const points = interactionPoints();
if (!points.length) return;
if (event.key === "Enter" || event.key === " ") {
if (!focusedPoint) return;
event.preventDefault();
const point2 = focusedPoint;
const canPin = tooltipIsSticky() || binding?.pin === true;
if (binding?.mode === "focus" && canPin) {
if (interactionIsPinned()) {
pinnedKey = null;
cursorSession?.clear();
} else {
pinnedKey = point2.key;
publishFocusCursor(focusPointsForPoint(point2), true);
}
} else if (tooltipIsSticky()) {
pinnedKey = pinnedKey ? null : point2.key;
paintFocus(point2, focusPointsForPoint(point2));
}
options.definition.selection?.change(point2, "keyboard");
options.onSelect?.(point2);
return;
}
const focus = resolveRendererFocusStrategy(options.definition.focus);
const point = focus ? chartPointFromNavigationOrder(
focus.navigation(points),
focusedPoint,
event.key
) : chartPointFromSceneOrder(points, focusedPoint, event.key);
if (point === void 0) return;
event.preventDefault();
pointerPosition = null;
focusOwner = "keyboard";
focusSource = "keyboard";
updateFocus(point ? focusPointsForPoint(point) : []);
};
const handleFocus = (event) => {
if (controlContains(event.target)) {
if (!pinnedKey) {
pointerPosition = null;
updateFocus([]);
}
return;
}
if (event.target === surface?.element && suppressNextSurfaceFocus) {
suppressNextSurfaceFocus = false;
return;
}
if (options.definition.keyboard !== false && cursorBinding()?.mode !== "free" && event.target === surface?.element && !focusedPoint) {
const focus = resolveRendererFocusStrategy(options.definition.focus);
const points = interactionPoints();
const point = focus ? focus.navigation(points)[0] : chartPointFromSceneOrder(points, null, "Home");
pointerPosition = null;
focusOwner = "keyboard";
focusSource = "keyboard";
updateFocus(point ? focusPointsForPoint(point) : []);
}
};
container.addEventListener("pointermove", handlePointerMove);
container.addEventListener("pointerdown", handlePointerDown);
container.addEventListener("pointercancel", clearPointerFocus);
container.addEventListener("mouseleave", clearPointerFocus);
container.addEventListener("click", handleClick);
container.addEventListener("keydown", handleKeyDown);
container.addEventListener("focusin", handleFocus);
container.addEventListener("focusout", clearKeyboardFocus);
fontSet?.addEventListener?.("loadingdone", handleFontLoad);
configureCursorController();
render();
configureObserver();
return {
interaction,
update(nextOptions) {
if (destroyed) return;
resolveTooltipInput(nextOptions.definition.tooltip);
const fontChanged = nextOptions.measureText === void 0 && domText.refresh();
const definitionChanged = options.definition !== nextOptions.definition;
const sizeChanged = options.height !== nextOptions.height || options.aspectRatio !== nextOptions.aspectRatio || options.width !== nextOptions.width || options.initialWidth !== nextOptions.initialWidth;
const layoutChanged = options.idPrefix !== nextOptions.idPrefix || options.renderer !== nextOptions.renderer || options.measureText !== nextOptions.measureText || fontChanged;
const needsRender = definitionChanged || sizeChanged || options.ariaLabel !== nextOptions.ariaLabel || options.ariaDescription !== nextOptions.ariaDescription || options.className !== nextOptions.className || options.tabIndex !== nextOptions.tabIndex || options.idPrefix !== nextOptions.idPrefix || options.renderer !== nextOptions.renderer || options.measureText !== nextOptions.measureText || fontChanged;
const observerChanged = options.width !== nextOptions.width;
const pointerDisabled = options.definition.pointer !== false && nextOptions.definition.pointer === false && focusOwner === "pointer";
options = nextOptions;
configureCursorController();
syncTooltip();
if (pointerDisabled) {
pointerPosition = null;
pinnedKey = null;
focusOwner = null;
updateFocus([]);
}
if (!tooltipIsSticky()) pinnedKey = null;
if (needsRender) {
render(
false,
layoutChanged ? "layout" : sizeChanged ? "resize" : "update"
);
} else {
if (cursorBinding()) {
applyCursorState(false);
} else if (focusedPoint) {
paintFocus(focusedPoint, focusPointsForPoint(focusedPoint));
}
}
if (observerChanged) configureObserver();
},
getScene: () => scene,
destroy() {
if (destroyed) return;
destroyed = true;
observer?.disconnect();
unsubscribeCursor?.();
unsubscribeCursor = void 0;
cursorSession?.destroy();
cursorSession = void 0;
cursorMode = void 0;
cursorMatch = void 0;
cursorExtension = void 0;
fontSet?.removeEventListener?.("loadingdone", handleFontLoad);
if (renderFrame !== void 0) {
view?.cancelAnimationFrame?.(renderFrame);
}
destroyTooltip();
destroyHostControls();
unsubscribePresentation?.();
unsubscribePresentation = void 0;
surface?.destroy();
runtime.destroy();
container.removeEventListener("pointermove", handlePointerMove);
container.removeEventListener("pointerdown", handlePointerDown);
container.removeEventListener("pointercancel", clearPointerFocus);
container.removeEventListener("mouseleave", clearPointerFocus);
container.removeEventListener("click", handleClick);
container.removeEventListener("keydown", handleKeyDown);
container.removeEventListener("focusin", handleFocus);
container.removeEventListener("focusout", clearKeyboardFocus);
container.replaceChildren();
if (ownsPosition && container.style.position === "relative") {
container.style.position = previousPosition;
}
}
};
function createScene() {
const width = currentWidth() ?? options.initialWidth ?? 640;
return runtime.render(
options.definition,
{
width,
height: options.height ?? (isPositiveFiniteNumber(options.aspectRatio) ? width / options.aspectRatio : 320)
},
{
measureText: options.measureText ?? domText.measureText,
typography: domText.typography()
}
);
}
function syncHostControls() {
const retained = /* @__PURE__ */ new Set();
for (const control of scene.controls ?? []) {
const extension = control.extension;
const identity = `${extension.id}:${control.key}`;
retained.add(identity);
let current = controlInstances.get(identity);
if (current && current.extension !== extension) {
current.instance.destroy();
controlInstances.delete(identity);
current = void 0;
}
if (!current) {
current = {
extension,
instance: extension.create({ container, surface })
};
controlInstances.set(identity, current);
}
current.instance.update(control, scene);
}
for (const [identity, current] of controlInstances) {
if (retained.has(identity)) continue;
current.instance.destroy();
controlInstances.delete(identity);
}
}
function destroyHostControls() {
for (const current of controlInstances.values()) {
current.instance.destroy();
}
controlInstances.clear();
}
function controlContains(target) {
for (const current of controlInstances.values()) {
if (current.instance.contains?.(target)) return true;
}
return false;
}
function resolvePointerFocus(x, y, maxDistance2) {
const points = interactionPoints();
const focus = resolveRendererFocusStrategy(options.definition.focus);
const focused = resolveChartPointerFocus(
interactionScene,
focus,
x,
y,
maxDistance2,
points
);
if (focused) return focused;
const presentationPoints = surface?.getPresentationPoints?.();
const candidate = presentationPoints !== void 0 ? nearestPoint(points, x, y, maxDistance2) : spatialIndex && interactionScene === scene ? spatialIndex.findNearest(x, y, maxDistance2) : findNearestPoint(interactionScene, x, y, maxDistance2, points);
const point = candidate ? restoreChartFocusPoint(points, candidate) : null;
return point ? [point] : [];
}
function interactionPoints() {
const points = surface?.getPresentationPoints?.() ?? interactionScene.points;
return viewportInteractionPoints(scene, points);
}
function focusPointsForPoint(point, points = interactionPoints()) {
return resolveRendererFocusStrategy(options.definition.focus)?.group(points, {
point
}) ?? [point];
}
function subscribeToPresentation() {
unsubscribePresentation = surface?.subscribePresentationPoints?.(
handlePresentationPoints
);
}
function handlePresentationPoints(points) {
if (destroyed || renderingSurface || paintingFocus) return;
if (cursorBinding()) {
applyCursorState(false);
return;
}
const visiblePoints = viewportInteractionPoints(scene, points);
if (pointerPosition && pinnedKey === null) {
updateFocus(
resolvePointerFocus(
pointerPosition.x,
pointerPosition.y,
maxDistance()
),
true
);
return;
}
if (!focusedPoint) return;
const point = restoreChartFocusPoint(visiblePoints, focusedPoint);
updateFocus(point ? focusPointsForPoint(point, visiblePoints) : [], true);
}
function maxDistance() {
return options.definition.maxFocusDistance ?? 48;
}
function paintTooltip(point, points) {
const input = resolveTooltipInput(options.definition.tooltip);
if (!input || !point || !surface) {
tooltipInstance?.hide();
return;
}
if (tooltipExtension !== input.extension || !tooltipInstance) {
destroyTooltip();
tooltipExtension = input.extension;
tooltipInstance = input.extension.create(
connectRendererTooltipMotion(
surface.renderer,
{
container,
dismiss: dismissTooltip,
bodyChange: () => options.onTooltipBodyChange
},
resolveTooltipMotion
)
);
}
const instance = tooltipInstance;
instance.update(input.options);
instance.paint({
point,
points,
scene,
surface,
pointer: pointerPosition,
focus: {
primary: point,
group: points,
source: focusSource,
pinned: interactionIsPinned()
},
pinned: interactionIsPinned()
});
}
function resolveTooltipMotion() {
const definition = options.definition.motion;
if (definition === false) return false;
return typeof definition === "function" ? void 0 : definition?.transition;
}
function syncTooltip() {
const input = resolveTooltipInput(options.definition.tooltip);
if (!input) {
tooltipInstance?.update(emptyTooltipOptions);
tooltipInstance?.hide();
} else if (input.extension !== tooltipExtension) {
destroyTooltip();
} else {
tooltipInstance?.update(input.options);
}
}
function destroyTooltip() {
tooltipInstance?.destroy();
tooltipInstance = void 0;
tooltipExtension = void 0;
}
function tooltipIsSticky() {
const input = resolveTooltipInput(options.definition.tooltip);
return Boolean(input && input.options.sticky !== false);
}
function tooltipTracksPointer() {
const input = resolveTooltipInput(options.definition.tooltip);
const anchor = input?.options.anchor;
return anchor === "pointer" || typeof anchor === "function" || typeof anchor === "object" && (anchor.x === "pointer" || anchor.y === "pointer");
}
}
const emptyTooltipOptions = {};
function createHostedScene(scene) {
const fallbackKeys = new Set(
(scene.controls ?? []).flatMap(
(control) => control.fallbackNodeKey ? [control.fallbackNodeKey] : []
)
);
if (!fallbackKeys.size) return scene;
return {
...scene,
nodes: scene.nodes.filter((node) => !fallbackKeys.has(node.key))
};
}
function isPointerResolution(target) {
return "position" in target && "point" in target && "points" in target;
}
function resolveTooltipInput(input) {
if (!input) return null;
const extension = "create" in input ? input : input.use;
if (extension.__chartTooltipHost !== "dom") {
throw new TypeError(
"DOM chart hosts require a tooltip extension from @tanstack/charts/tooltip."
);
}
const domExtension = extension;
return "create" in input ? {
extension: domExtension,
options: emptyTooltipOptions
} : {
extension: domExtension,
options: input
};
}
function isPositiveFiniteNumber(value) {
return typeof value === "number" && Number.isFinite(value) && value > 0;
}
function resolveRendererFocusStrategy(focus) {
if (focus === false) return focusDisabled;
return resolveChartFocusStrategy(focus);
}
function plotContains(scene, position) {
return position.x >= scene.chart.x && position.x <= scene.chart.x + scene.chart.width && position.y >= scene.chart.y && position.y <= scene.chart.y + scene.chart.height;
}
function resolveAnimation(animation, container, reason) {
const configured = animation === true ? {} : animation || void 0;
if (!configured) return void 0;
if (reason === "layout") return void 0;
if (reason === "resize" && configured.resize !== true) return void 0;
if ((configured.respectReducedMotion ?? true) && container.ownerDocument.defaultView?.matchMedia?.(
"(prefers-reduced-motion: reduce)"
).matches) {
return void 0;
}
const { resize: _resize, ...resolved } = configured;
return resolved;
}
export {
mountChartRenderer
};