@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.
1,438 lines • 97.1 kB
JavaScript
import { focusedNodeKeys, resolveFocusScene } from "./focus-layer.js";
import { resolveFocusGuides } from "./focus-presentation.js";
import { resolveMarkStateScene } from "./mark-state.js";
import { reconcileChartSvg, reconcileChartSvgFragment } from "./reconcile.js";
import { chartSceneSource } from "./scene-source.js";
import {
sceneMotionNode
} from "./scene-motion-internal.js";
import { viewportTranslationChanged } from "./scene-point-map.js";
import { createChartSpring } from "./spring.js";
import { renderChartSvgWithResources } from "./svg-resources.js";
import { svgClientToScene } from "./svg-coordinates.js";
import { valueKey } from "./scales.js";
import { resolveRollingPathPlan } from "./motion-path.js";
import {
chartRendererMotion
} from "./renderer-motion-internal.js";
import { renderFocusGuideLayer } from "./svg-renderer.js";
import {
detachSvgFocusGuideLayers,
ensureSvgFocusGuideLayer,
removeSvgFocusGuideLayer,
restoreSvgFocusGuideLayers
} from "./svg-focus-guide-layer.js";
import { stagger } from "./motion-definition.js";
const defaultDuration = 1100;
const defaultStaggerRatio = 0.4;
const defaultEasing = cubicBezier(0.85, 0, 0.15, 1);
const springSafetyLimit = 1e4;
let clipId = 0;
function createSvgMotionDriver(options = {}) {
const transition = resolveTransition(options.transition, defaultDuration);
const resolved = {
transition
};
return createSvgMotionRuntime(resolved, {
initial: options.initial ?? true,
resize: options.resize ?? false,
respectReducedMotion: options.respectReducedMotion ?? true
});
}
function createSvgMotionRuntime(options, policy) {
const runtimes = /* @__PURE__ */ new WeakMap();
return {
id: "svg-motion",
...policy,
createTooltip: (context) => createTooltipMotionController(options, policy, context),
animateSvg(context) {
let runtime = runtimes.get(context.container);
if (!runtime) {
runtime = {
elements: /* @__PURE__ */ new WeakMap(),
points: /* @__PURE__ */ new Map()
};
runtimes.set(context.container, runtime);
}
const timing = createTimingResolver(
options,
context.scene,
context.transition || context.markTransitions ? {
...context.transition ? { default: { transition: context.transition } } : {},
...context.markTransitions ? {
marks: Object.fromEntries(
Object.entries(context.markTransitions).map(
([markId, transition]) => [markId, { transition }]
)
)
} : {}
} : void 0
);
if (context.phase === "update" && context.markup) {
return reconcileMotionSvg(context, options, timing, runtime);
}
const root = context.container.querySelector("svg.ts-chart");
if (!root) return () => {
};
const points = new Map(
context.scene.points.map((point) => [point.key, point])
);
const tracks = [
...createBarTracks(root, context.scene, points, timing, runtime),
...createCartesianPathTracks(root, context.scene, timing),
...createRadialPathTracks(root, context.scene, timing),
...createArcTracks(root, context.scene, timing)
];
const presentation = createPresentationTracks(
root,
context.scene,
context.presentationPoints ?? [],
timing,
context.setPresentationPoints,
"enter",
runtime
);
return runTracks(root, [...tracks, ...presentation.tracks], {
publish: presentation.publish,
finish: () => context.setPresentationPoints?.(context.scene.points)
});
},
animateSvgFragment(context) {
let runtime = runtimes.get(context.container);
if (!runtime) {
runtime = {
elements: /* @__PURE__ */ new WeakMap(),
points: /* @__PURE__ */ new Map()
};
runtimes.set(context.container, runtime);
}
const nextRoot = parseSvgFragment(context.root, context.markup);
if (!nextRoot || context.root.namespaceURI !== nextRoot.namespaceURI || context.root.localName !== nextRoot.localName) {
if (nextRoot) context.root.replaceWith(nextRoot);
return () => {
};
}
const tracks = [];
reconcileMotionElement(context.root, nextRoot, tracks, {
scene: context.scene,
previousScene: context.previousScene,
timingFor: createTimingResolver(
options,
context.scene,
context.transition ? { default: { transition: context.transition } } : void 0
),
options,
runtime,
pathPlans: {
elements: /* @__PURE__ */ new Map(),
points: /* @__PURE__ */ new Map()
}
});
return runTracks(context.root, tracks);
}
};
}
function createTooltipMotionController(options, policy, context) {
const view = context.container.ownerDocument.defaultView;
let presenceAnimation;
let presencePhase;
let movementAnimation;
let movementFrame;
let springMovement;
let hideGeneration = 0;
const prefersReducedMotion = () => policy.respectReducedMotion && Boolean(view?.matchMedia?.("(prefers-reduced-motion: reduce)").matches);
const transition = (override) => {
if (override === false || prefersReducedMotion()) return void 0;
const inherited = override ?? context.transition();
if (inherited === false) return void 0;
return resolveTransition(
inherited,
defaultDuration,
void 0,
options.transition
);
};
const now = () => view?.performance.now() ?? Date.now();
const stopMovement = (element) => {
if (movementFrame !== void 0) {
view?.cancelAnimationFrame?.(movementFrame);
movementFrame = void 0;
}
movementAnimation?.cancel();
movementAnimation = void 0;
springMovement = void 0;
element?.style.removeProperty("translate");
};
const sampleSpringMovement = (timestamp, element) => {
const movement = springMovement;
if (!movement) return emptyTooltipMovement;
const elapsed = Math.max(0, timestamp - movement.startedAt);
const x = movement.spring.sample(elapsed, {
from: movement.fromX,
to: 0,
velocity: movement.velocityX
});
const y = movement.spring.sample(elapsed, {
from: movement.fromY,
to: 0,
velocity: movement.velocityY
});
const snapshot = {
x: x.value,
y: y.value,
velocityX: x.velocity,
velocityY: y.velocity
};
element.style.translate = `${snapshot.x}px ${snapshot.y}px`;
if (x.done && y.done) {
springMovement = void 0;
element.style.removeProperty("translate");
return emptyTooltipMovement;
}
return snapshot;
};
const sampleSpringFrame = (timestamp, element) => {
movementFrame = void 0;
sampleSpringMovement(timestamp, element);
if (!springMovement) return;
movementFrame = view?.requestAnimationFrame((nextTimestamp) => {
sampleSpringFrame(nextTimestamp, element);
});
};
const captureMovement = (element) => {
let snapshot;
if (springMovement) {
snapshot = sampleSpringMovement(now(), element);
} else if (movementAnimation) {
snapshot = {
...readTooltipTranslate(element),
velocityX: 0,
velocityY: 0
};
} else {
snapshot = emptyTooltipMovement;
}
stopMovement(element);
return snapshot;
};
const animateMovement = (element, resolved, movement) => {
const { x, y, velocityX, velocityY } = movement;
if (Math.abs(x) < 0.5 && Math.abs(y) < 0.5) {
element.style.removeProperty("translate");
return;
}
if (resolved.type === "tween") {
element.style.translate = `${x}px ${y}px`;
if (typeof element.animate !== "function") {
element.style.removeProperty("translate");
return;
}
const sampled = tooltipMotionSamples(resolved);
const animation = element.animate(
sampled.values.map((progress, index) => ({
offset: sampled.offsets[index],
translate: `${interpolate(x, 0, progress)}px ${interpolate(y, 0, progress)}px`
})),
{
duration: sampled.duration,
easing: "linear",
fill: "both"
}
);
movementAnimation = animation;
animation.onfinish = () => {
if (movementAnimation !== animation) return;
element.style.removeProperty("translate");
movementAnimation = void 0;
};
return;
}
if (!view?.requestAnimationFrame) {
element.style.removeProperty("translate");
return;
}
springMovement = {
spring: resolved.spring,
startedAt: now(),
fromX: x,
fromY: y,
velocityX,
velocityY
};
element.style.translate = `${x}px ${y}px`;
movementFrame = view.requestAnimationFrame((timestamp) => {
sampleSpringFrame(timestamp, element);
});
};
const animatePresence = (element, resolved, from, to) => {
if (typeof element.animate !== "function") return void 0;
const sampled = tooltipMotionSamples(resolved);
return element.animate(
sampled.values.map((progress, index) => ({
offset: sampled.offsets[index],
opacity: interpolate(from.opacity, to.opacity, progress),
transform: `scale(${interpolate(from.scale, to.scale, progress)})`
})),
{
duration: sampled.duration,
easing: "linear",
fill: "both"
}
);
};
return {
beforePaint(element) {
const wasHidden = element.hasAttribute("hidden");
const resumingExit = presencePhase === "exit";
const previousLeft = finiteStyleNumber(element.style.left);
const previousTop = finiteStyleNumber(element.style.top);
const movement = captureMovement(element);
const presence = resumingExit ? readTooltipPresenceState(element) : void 0;
if (resumingExit) {
presenceAnimation?.cancel();
presenceAnimation = void 0;
presencePhase = void 0;
}
hideGeneration += 1;
return {
wasHidden,
showPresence: wasHidden || resumingExit,
previousLeft,
previousTop,
movementX: movement.x,
movementY: movement.y,
velocityX: movement.velocityX,
velocityY: movement.velocityY,
presence
};
},
afterPaint(element, snapshot, override) {
const resolved = transition(override);
if (!resolved) {
stopMovement(element);
presenceAnimation?.cancel();
presenceAnimation = void 0;
presencePhase = void 0;
element.style.removeProperty("opacity");
element.style.removeProperty("transform");
return;
}
const nextLeft = finiteStyleNumber(element.style.left);
const nextTop = finiteStyleNumber(element.style.top);
animateMovement(element, resolved, {
x: snapshot.wasHidden || snapshot.previousLeft === void 0 || nextLeft === void 0 ? 0 : snapshot.previousLeft + snapshot.movementX - nextLeft,
y: snapshot.wasHidden || snapshot.previousTop === void 0 || nextTop === void 0 ? 0 : snapshot.previousTop + snapshot.movementY - nextTop,
velocityX: snapshot.velocityX,
velocityY: snapshot.velocityY
});
if (!snapshot.showPresence) return;
presenceAnimation?.cancel();
presencePhase = "enter";
const animation = animatePresence(
element,
resolved,
snapshot.presence ?? { opacity: 0, scale: 0.96 },
{ opacity: 1, scale: 1 }
);
presenceAnimation = animation;
if (!animation) {
presencePhase = void 0;
element.style.removeProperty("opacity");
element.style.removeProperty("transform");
return;
}
animation.onfinish = () => {
if (presenceAnimation !== animation) return;
element.style.removeProperty("opacity");
element.style.removeProperty("transform");
presenceAnimation = void 0;
presencePhase = void 0;
};
},
hide(element, override, complete) {
const resolved = transition(override);
if (!resolved) {
stopMovement(element);
presenceAnimation?.cancel();
presenceAnimation = void 0;
presencePhase = void 0;
element.style.removeProperty("opacity");
element.style.removeProperty("transform");
return false;
}
const generation = ++hideGeneration;
const from = readTooltipPresenceState(element);
presenceAnimation?.cancel();
presencePhase = "exit";
const animation = animatePresence(element, resolved, from, {
opacity: 0,
scale: 0.96
});
presenceAnimation = animation;
if (!animation) {
presencePhase = void 0;
stopMovement(element);
return false;
}
animation.onfinish = () => {
if (generation !== hideGeneration) return;
complete();
element.style.removeProperty("opacity");
element.style.removeProperty("transform");
presenceAnimation = void 0;
presencePhase = void 0;
stopMovement(element);
};
return true;
},
destroy(element) {
hideGeneration += 1;
presenceAnimation?.cancel();
presenceAnimation = void 0;
presencePhase = void 0;
stopMovement(element);
element?.style.removeProperty("opacity");
element?.style.removeProperty("transform");
}
};
}
const emptyTooltipMovement = {
x: 0,
y: 0,
velocityX: 0,
velocityY: 0
};
function tooltipMotionSamples(transition) {
if (transition.type === "tween") {
const offsets2 = Array.from({ length: 31 }, (_, index) => index / 30);
return {
duration: transition.duration,
offsets: offsets2,
values: offsets2.map(transition.easing)
};
}
const offsets = [];
const values = [];
let duration = 0;
for (let elapsed = 0; elapsed <= 2e3; elapsed += 16) {
const sample = transition.spring.sample(elapsed);
duration = elapsed;
offsets.push(elapsed);
values.push(sample.value);
if (sample.done && elapsed > 0) break;
}
if (duration === 0) {
return { duration: 0, offsets: [0, 1], values: [0, 1] };
}
offsets[offsets.length - 1] = duration;
values[values.length - 1] = 1;
return {
duration,
offsets: offsets.map((elapsed) => elapsed / duration),
values
};
}
function interpolate(from, to, progress) {
return from + (to - from) * progress;
}
function finiteStyleNumber(value) {
const number = Number.parseFloat(value);
return Number.isFinite(number) ? number : void 0;
}
function readTooltipPresenceState(element) {
const style = element.ownerDocument.defaultView?.getComputedStyle(element);
return {
opacity: finiteStyleNumber(style?.opacity ?? "") ?? 1,
scale: readTransformScale(style?.transform ?? "")
};
}
function readTooltipTranslate(element) {
const style = element.ownerDocument.defaultView?.getComputedStyle(element);
const value = style?.translate || element.style.translate;
if (!value || value === "none") return { x: 0, y: 0 };
const values = value.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi)?.map(Number);
return {
x: values?.[0] ?? 0,
y: values?.[1] ?? 0
};
}
function readTransformScale(value) {
if (!value || value === "none") return 1;
const values = value.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi)?.map(Number);
if (!values?.length) return 1;
if (value.startsWith("matrix3d(")) {
return Math.hypot(values[0] ?? 1, values[1] ?? 0, values[2] ?? 0);
}
if (value.startsWith("matrix(")) {
return Math.hypot(values[0] ?? 1, values[1] ?? 0);
}
return value.startsWith("scale(") ? values[0] ?? 1 : 1;
}
function parseSvgFragment(current, markup) {
const template = current.ownerDocument.createElement("template");
template.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg">${markup}</svg>`;
return template.content.firstElementChild?.firstElementChild ?? void 0;
}
function motion(options = {}) {
return createMotionSvgChartRenderer(
createSvgMotionDriver(options),
renderChartSvgWithResources
);
}
function createMotionSvgChartRenderer(motion2, renderSvg = renderChartSvgWithResources) {
const renderer = {
id: `svg:${motion2.id}`,
[chartRendererMotion]: { createTooltip: motion2.createTooltip },
prerender: renderSvg,
mount(container) {
const adoptedRoot = container.firstElementChild?.matches("svg.ts-chart") ?? false;
let cancelAnimation = () => {
};
let cancelFocusAnimation = () => {
};
const visibleFocusGuides = /* @__PURE__ */ new Set();
let scene;
let presentationPoints;
const presentationListeners = /* @__PURE__ */ new Set();
let renderOptions;
let stateTransition;
let stateTransitions;
let stateScene;
let dataMotionRevision = 0;
let dataMotionActive = false;
let stateFlushQueued = false;
let destroyed = false;
let pendingStateFocus;
let desiredStateFocus;
const svgElement = () => {
const svg = container.querySelector("svg.ts-chart");
if (!svg) {
throw new Error(
"The motion SVG renderer must produce an svg.ts-chart root element."
);
}
return svg;
};
const publishPresentationPoints = (points) => {
presentationPoints = points;
for (const listener of presentationListeners) listener(points);
};
const queuePendingStateFocus = () => {
if (stateFlushQueued || !pendingStateFocus) return;
stateFlushQueued = true;
queueMicrotask(() => {
stateFlushQueued = false;
if (destroyed || dataMotionActive || !pendingStateFocus) return;
const pending = pendingStateFocus;
pendingStateFocus = void 0;
applyStateFocus(pending.focus, pending.pointer, pending.cursor);
});
};
const applyStateFocus = (focus, pointer, cursor, resolved = scene ? resolveMarkStateScene(scene, focus, pointer) : void 0) => {
if (!scene || !renderOptions || !resolved) return;
const presented = resolveFocusScene(resolved.scene, focus);
cancelFocusAnimation();
cancelFocusAnimation = () => {
};
const previousTransition = stateTransition;
const previousTransitions = stateTransitions;
if (presented.scene !== scene || stateScene || previousTransition) {
const focusGuideLayers = detachSvgFocusGuideLayers(svgElement());
cancelAnimation();
if (presentationPoints !== scene.points) {
publishPresentationPoints(scene.points);
}
const transition = resolved.transition ?? previousTransition;
const markTransitions = resolved.transitions ?? previousTransitions;
const reduced = motion2.respectReducedMotion && (transition?.respectReducedMotion ?? true) && (container.ownerDocument.defaultView?.matchMedia?.(
"(prefers-reduced-motion: reduce)"
).matches ?? false);
const markup = renderSvg(presented.scene, renderOptions);
cancelAnimation = reduced ? reconcileChartSvg(container, markup) : motion2.animateSvg({
container,
scene: presented.scene,
previousScene: stateScene ?? scene,
presentationPoints: scene.points,
markup,
phase: "update",
transition: markTransitions ? void 0 : transition,
markTransitions
});
restoreSvgFocusGuideLayers(svgElement(), focusGuideLayers);
stateScene = focus && presented.scene !== scene ? presented.scene : void 0;
}
stateTransition = focus ? resolved.transition ?? previousTransition : void 0;
stateTransitions = focus ? resolved.transitions ?? previousTransitions : void 0;
paintMotionSvgFocus(svgElement(), presented.scene, focus);
cancelFocusAnimation = paintMotionSvgFocusGuides({
container,
svg: svgElement(),
scene: presented.scene,
focus,
pointer,
cursor,
idPrefix: renderOptions.idPrefix,
motion: motion2,
visible: visibleFocusGuides
});
return presented.scene;
};
const surface = {
renderer,
get element() {
return svgElement();
},
render(nextScene, options) {
const previousScene = scene;
const initial = previousScene === void 0;
const resized = Boolean(
previousScene && (previousScene.width !== nextScene.width || previousScene.height !== nextScene.height)
);
const reduced = motion2.respectReducedMotion && (container.ownerDocument.defaultView?.matchMedia?.(
"(prefers-reduced-motion: reduce)"
).matches ?? false);
const animate = !reduced && (initial ? motion2.initial && (!adoptedRoot || motion2.initial === "always") : motion2.resize || !resized);
const viewportMoved = Boolean(
previousScene && viewportTranslationChanged(previousScene, nextScene)
);
const markup = renderSvg(nextScene, options);
cancelAnimation();
const previousPresentation = presentationPoints ?? previousScene?.points ?? [];
cancelFocusAnimation();
cancelFocusAnimation = () => {
};
const retainsFocusGuideLayers = Boolean(
previousScene?.focusGuides?.length
);
const focusGuideLayers = retainsFocusGuideLayers ? detachSvgFocusGuideLayers(svgElement()) : {};
for (const placement of visibleFocusGuides) {
if (!nextScene.focusGuides?.some(
(guide) => guide.placement === placement
)) {
visibleFocusGuides.delete(placement);
}
}
const revision = ++dataMotionRevision;
stateScene = void 0;
stateTransition = void 0;
scene = nextScene;
renderOptions = options;
dataMotionActive = animate && !viewportMoved;
pendingStateFocus = dataMotionActive ? desiredStateFocus : void 0;
if (animate && !viewportMoved) {
if (initial) reconcileChartSvg(container, markup);
cancelAnimation = motion2.animateSvg({
container,
scene: nextScene,
previousScene,
presentationPoints: previousPresentation,
markup,
phase: initial ? "initial" : "update",
setPresentationPoints(points) {
publishPresentationPoints(
points
);
if (revision === dataMotionRevision && points === nextScene.points) {
dataMotionActive = false;
queuePendingStateFocus();
}
}
});
} else {
reconcileChartSvg(container, markup);
publishPresentationPoints(nextScene.points);
dataMotionActive = false;
}
if (retainsFocusGuideLayers) {
restoreSvgFocusGuideLayers(
svgElement(),
focusGuideLayers,
(placement) => nextScene.focusGuides?.some(
(guide) => guide.placement === placement
) === true
);
}
scene = nextScene;
stateScene = void 0;
renderOptions = options;
stateTransition = void 0;
stateTransitions = void 0;
},
clientToScene(currentScene, clientX, clientY) {
return svgClientToScene(svgElement(), currentScene, clientX, clientY);
},
getPresentationPoints() {
if (!scene || !presentationPoints || presentationPoints === scene.points) {
return void 0;
}
return presentationPoints;
},
subscribePresentationPoints(listener) {
presentationListeners.add(listener);
return () => presentationListeners.delete(listener);
},
paintFocus(focus, pointer, cursor) {
if (!scene || !renderOptions) return;
desiredStateFocus = {
focus,
pointer: pointer ?? null,
cursor: cursor ?? null
};
const resolved = resolveMarkStateScene(scene, focus, pointer);
if (dataMotionActive) {
pendingStateFocus = desiredStateFocus;
paintMotionSvgFocus(svgElement(), resolved.scene, focus);
cancelFocusAnimation();
cancelFocusAnimation = paintMotionSvgFocusGuides({
container,
svg: svgElement(),
scene: resolved.scene,
focus,
pointer,
cursor,
idPrefix: renderOptions.idPrefix,
motion: motion2,
visible: visibleFocusGuides
});
return resolved.scene;
}
pendingStateFocus = void 0;
return applyStateFocus(
focus,
pointer ?? null,
cursor ?? null,
resolved
);
},
destroy() {
destroyed = true;
dataMotionRevision += 1;
pendingStateFocus = void 0;
desiredStateFocus = void 0;
cancelAnimation();
presentationListeners.clear();
cancelFocusAnimation();
}
};
return surface;
}
};
return renderer;
}
function paintMotionSvgFocus(svg, scene, focus) {
const sceneLayers = collectMotionFocusLayers(scene.nodes);
const elements = svg.querySelectorAll(
"[data-ts-focus-layer]:not([data-ts-focus-guide-layer])"
);
elements.forEach((element, index) => {
const layer = sceneLayers[index];
if (layer?.focus?.retarget) {
const hasChildren = element.children.length > 0;
element.setAttribute("visibility", hasChildren ? "visible" : "hidden");
element.querySelectorAll("[data-ts-key]").forEach((child) => child.setAttribute("visibility", "visible"));
return;
}
const visible = layer ? focusedNodeKeys(layer, focus) : /* @__PURE__ */ new Set();
element.setAttribute(
"visibility",
focus && visible.size ? "visible" : "hidden"
);
element.querySelectorAll("[data-ts-key]").forEach((child) => {
const key = child.dataset.tsKey;
child.setAttribute(
"visibility",
key && visible.has(key) ? "visible" : "hidden"
);
});
});
}
function paintMotionSvgFocusGuides(options) {
const {
container,
svg,
scene,
focus,
pointer,
cursor,
idPrefix = "",
motion: motion2,
visible
} = options;
const presentation = resolveFocusGuides(scene, focus, pointer, cursor);
const reduced = motion2.respectReducedMotion && (container.ownerDocument.defaultView?.matchMedia?.(
"(prefers-reduced-motion: reduce)"
).matches ?? false);
const cancellations = [];
for (const placement of ["under", "over"]) {
if (!scene.focusGuides?.some((guide) => guide.placement === placement)) {
removeSvgFocusGuideLayer(svg, placement);
visible.delete(placement);
continue;
}
const layer = ensureSvgFocusGuideLayer(svg, placement);
const nodes = presentation[placement];
if (!nodes.length) {
layer.setAttribute("visibility", "hidden");
visible.delete(placement);
continue;
}
const markup = renderFocusGuideLayer(nodes, placement, idPrefix);
if (reduced || !visible.has(placement)) {
reconcileChartSvgFragment(layer, markup);
} else {
cancellations.push(
motion2.animateSvgFragment({
container,
root: layer,
scene,
markup
})
);
}
visible.add(placement);
}
return () => cancellations.forEach((cancel) => cancel());
}
function collectMotionFocusLayers(nodes) {
const layers = [];
for (const node of nodes) {
if (node.kind !== "group") continue;
if (node.focus) layers.push(node);
else layers.push(...collectMotionFocusLayers(node.children));
}
return layers;
}
function createBarTracks(root, scene, points, timingFor, runtime) {
const groups = [
...root.querySelectorAll(
"g.ts-chart__bar-y, g.ts-chart__bar-x"
)
];
const tracks = [];
groups.forEach((group, seriesIndex) => {
const horizontal = group.classList.contains("ts-chart__bar-x");
const rectangles = [...group.children].filter(
(element) => element.localName === "rect"
);
const seriesKey = group.getAttribute("data-ts-key") ?? `series:${seriesIndex}`;
rectangles.forEach((rectangle, datumIndex) => {
const key = rectangle.getAttribute("data-ts-key") ?? `${seriesKey}:${datumIndex}`;
const point = points.get(key);
const targetX = numberAttribute(rectangle, "x");
const targetY = numberAttribute(rectangle, "y");
const targetWidth = numberAttribute(rectangle, "width");
const targetHeight = numberAttribute(rectangle, "height");
const baseline = resolveBarBaseline(
scene,
point,
horizontal,
horizontal ? targetX : targetY + targetHeight
);
const timing = timingFor({
phase: "enter",
role: "bar",
key,
markId: point?.markId ?? motionMarkId(scene, seriesKey),
seriesKey,
seriesIndex,
datumIndex,
datumCount: rectangles.length,
datum: point?.datum,
point
});
rectangle.dataset.tsMotionRole = "bar";
const names = horizontal ? ["x", "width"] : ["y", "height"];
const from = [baseline, 0];
const to = horizontal ? [targetX, targetWidth] : [targetY, targetHeight];
const states = names.flatMap(
(name, index) => elementValueStates(runtime, rectangle, name, [from[index] ?? 0])
);
const apply = (values) => {
rectangle.setAttribute(names[0], formatNumber(values[0] ?? 0));
rectangle.setAttribute(names[1], formatNumber(values[1] ?? 0));
};
const finish = () => {
rectangle.setAttribute("x", formatNumber(targetX));
rectangle.setAttribute("y", formatNumber(targetY));
rectangle.setAttribute("width", formatNumber(targetWidth));
rectangle.setAttribute("height", formatNumber(targetHeight));
delete rectangle.dataset.tsMotionRole;
};
apply(from);
tracks.push({
...timing,
values: bindMotionValues(states, from, to),
apply,
finish,
cancel: () => delete rectangle.dataset.tsMotionRole
});
});
});
return tracks;
}
function createCartesianPathTracks(root, scene, timingFor) {
const groups = [
...root.querySelectorAll(
"g.ts-chart__line:not(.ts-chart__radial-line), g.ts-chart__area:not(.ts-chart__radial-area)"
)
];
return groups.map((group, seriesIndex) => {
const role = group.classList.contains("ts-chart__area") ? "area" : "line";
const seriesKey = group.getAttribute("data-ts-key") ?? `${role}:${seriesIndex}`;
const timing = timingFor({
phase: "enter",
role,
key: seriesKey,
markId: motionMarkId(scene, seriesKey),
seriesKey,
seriesIndex,
datumIndex: 0,
datumCount: 1,
datum: void 0,
point: void 0
});
const horizontal = scenePathAffinity(scene, seriesKey) === "y";
const baseline = resolvePathBaseline(scene, horizontal);
const previousTransform = group.getAttribute("transform");
group.dataset.tsMotionRole = role;
const apply = (values) => {
const progress = values[0] ?? 0;
const transform = horizontal ? `matrix(${formatNumber(progress)} 0 0 1 ${formatNumber(baseline * (1 - progress))} 0)` : `matrix(1 0 0 ${formatNumber(progress)} 0 ${formatNumber(baseline * (1 - progress))})`;
group.setAttribute(
"transform",
previousTransform ? `${previousTransform} ${transform}` : transform
);
};
const cleanup = () => {
if (previousTransform === null) group.removeAttribute("transform");
else group.setAttribute("transform", previousTransform);
delete group.dataset.tsMotionRole;
};
apply([0]);
return {
...timing,
values: bindMotionValues(void 0, [0], [1]),
apply,
finish: cleanup,
cancel: cleanup
};
});
}
function createRadialPathTracks(root, scene, timingFor) {
const groups = [
...root.querySelectorAll(
"g.ts-chart__radial-line, g.ts-chart__radial-area, g.ts-chart__radial-dot"
)
];
return groups.map((group, seriesIndex) => {
const role = group.classList.contains(
"ts-chart__radial-area"
) ? "area" : group.classList.contains("ts-chart__radial-dot") ? "dot" : "line";
const seriesKey = group.getAttribute("data-ts-key") ?? `${role}:${seriesIndex}`;
const timing = timingFor({
phase: "enter",
role,
key: seriesKey,
markId: motionMarkId(scene, seriesKey),
seriesKey,
seriesIndex,
datumIndex: 0,
datumCount: 1,
datum: void 0,
point: void 0
});
const previousTransform = group.getAttribute("transform");
group.dataset.tsMotionRole = role;
const apply = (values) => {
const progress = values[0] ?? 0;
const transform = `scale(${formatNumber(progress)})`;
group.setAttribute(
"transform",
previousTransform ? `${previousTransform} ${transform}` : transform
);
};
const cleanup = () => {
if (previousTransform === null) group.removeAttribute("transform");
else group.setAttribute("transform", previousTransform);
delete group.dataset.tsMotionRole;
};
apply([0]);
return {
...timing,
values: bindMotionValues(void 0, [0], [1]),
apply,
finish: cleanup,
cancel: cleanup
};
});
}
function createArcTracks(root, scene, timingFor) {
const groups = [...root.querySelectorAll("g.ts-chart__arc")];
return groups.flatMap((group, seriesIndex) => {
const seriesKey = group.getAttribute("data-ts-key") ?? `arc:${seriesIndex}`;
const geometry = sceneArcGeometry(scene, seriesKey);
if (!geometry) return [];
const role = group.classList.contains("ts-chart__bar") ? "bar" : "arc";
const timing = timingFor({
phase: "enter",
role,
key: seriesKey,
markId: motionMarkId(scene, seriesKey),
seriesKey,
seriesIndex,
datumIndex: 0,
datumCount: 1,
datum: void 0,
point: void 0
});
const document = root.ownerDocument;
let definitions = root.querySelector("defs");
if (!definitions) {
definitions = document.createElementNS(
"http://www.w3.org/2000/svg",
"defs"
);
definitions.dataset.tsMotionDefs = "";
root.prepend(definitions);
}
const clip = document.createElementNS(
"http://www.w3.org/2000/svg",
"clipPath"
);
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
const id = `ts-chart-motion-clip-${++clipId}`;
clip.id = id;
clip.append(path);
definitions.append(clip);
const previousClip = group.getAttribute("clip-path");
group.setAttribute("clip-path", `url(#${id})`);
group.dataset.tsMotionRole = role;
const apply = (values) => {
const progress = Math.max(0, Math.min(1, values[0] ?? 0));
path.setAttribute(
"d",
radialSweepClipPath(
geometry.startAngle,
geometry.sweep * progress,
geometry.radius
)
);
};
const cleanup = () => {
if (previousClip === null) group.removeAttribute("clip-path");
else group.setAttribute("clip-path", previousClip);
delete group.dataset.tsMotionRole;
clip.remove();
if (definitions?.dataset.tsMotionDefs !== void 0 && !definitions.children.length) {
definitions.remove();
}
};
apply([0]);
return [
{
...timing,
values: bindMotionValues(void 0, [0], [1]),
apply,
finish: cleanup,
cancel: cleanup
}
];
});
}
function resolvePathBaseline(scene, horizontal) {
const chartStart = horizontal ? scene.chart.x : scene.chart.y;
const chartSize = horizontal ? scene.chart.width : scene.chart.height;
const fallback = horizontal ? chartStart : chartStart + chartSize;
const scale = scene.scales[horizontal ? "x" : "y"];
if (!scale) return fallback;
const zero = scale.map(0);
return Number.isFinite(zero) && zero >= chartStart && zero <= chartStart + chartSize ? zero : fallback;
}
function scenePathAffinity(scene, key) {
const node = findSceneNodeContext(scene.nodes, key)?.node;
if (!node) return void 0;
const visit = (candidate) => {
if (candidate.kind === "group") {
for (const child of candidate.children) {
const affinity = visit(child);
if (affinity) return affinity;
}
return void 0;
}
return "interaction" in candidate ? candidate.interaction?.affinity : void 0;
};
return visit(node);
}
function sceneArcGeometry(scene, key) {
const node = findSceneNodeContext(scene.nodes, key)?.node;
if (!node) return void 0;
const pointSets = [];
const visit = (candidate) => {
if (candidate.kind === "group") {
candidate.children.forEach(visit);
return;
}
if (candidate.kind === "area" && candidate.points.length) {
pointSets.push(candidate.points);
}
};
visit(node);
const firstSet = pointSets.find((points) => points.length > 1);
const firstPoint = firstSet?.[0];
if (!firstSet || !firstPoint) return void 0;
const startAngle = polarPointAngle(firstPoint);
let direction = 0;
for (let index = 1; index < firstSet.length; index += 1) {
const point = firstSet[index];
if (!point) continue;
const delta = signedAngleDelta(startAngle, polarPointAngle(point));
if (Math.abs(delta) > 1e-4) {
direction = Math.sign(delta);
break;
}
}
if (!direction) return void 0;
let sweep = 0;
let radius = 0;
for (const points of pointSets) {
for (const point of points) {
const angle = polarPointAngle(point);
const distance = direction > 0 ? positiveAngle(angle - startAngle) : positiveAngle(startAngle - angle);
sweep = Math.max(sweep, distance);
radius = Math.max(radius, Math.hypot(point[0], point[1]));
}
}
const tau = Math.PI * 2;
if (sweep > tau - Math.PI / 12) sweep = tau;
if (sweep <= 1e-4 || radius <= 0) return void 0;
return { startAngle, sweep: sweep * direction, radius: radius + 2 };
}
function polarPointAngle(point) {
return Math.atan2(point[0], -point[1]);
}
function signedAngleDelta(from, to) {
const tau = Math.PI * 2;
return ((to - from + Math.PI) % tau + tau) % tau - Math.PI;
}
function positiveAngle(angle) {
const tau = Math.PI * 2;
return (angle % tau + tau) % tau;
}
function radialSweepClipPath(startAngle, sweep, radius) {
if (Math.abs(sweep) <= 1e-6) return "M0 0Z";
const steps = Math.max(1, Math.ceil(Math.abs(sweep) / (Math.PI / 24)));
let path = "M0 0";
for (let index = 0; index <= steps; index += 1) {
const angle = startAngle + sweep * index / steps;
path += `L${formatNumber(Math.sin(angle) * radius)} ${formatNumber(-Math.cos(angle) * radius)}`;
}
return `${path}Z`;
}
function reconcileMotionSvg(context, options, timingFor, runtime) {
const template = context.container.ownerDocument.createElement("template");
template.innerHTML = context.markup ?? "";
const nextRoot = template.content.firstElementChild;
const currentRoot = context.container.firstElementChild;
if (!nextRoot || !currentRoot || currentRoot.namespaceURI !== nextRoot.namespaceURI || currentRoot.localName !== nextRoot.localName) {
if (nextRoot) context.container.replaceChildren(nextRoot);
context.setPresentationPoints?.(context.scene.points);
return () => {
};
}
const tracks = [];
const pathPlans = createRollingPathPlans(
currentRoot,
nextRoot,
context.previousScene,
context.scene,
timingFor
);
reconcileMotionElement(currentRoot, nextRoot, tracks, {
scene: context.scene,
previousScene: context.previousScene,
timingFor,
options,
runtime,
pathPlans
});
const root = currentRoot;
const presentation = createPresentationTracks(
root,
context.scene,
context.presentationPoints ?? context.previousScene?.points ?? [],
timingFor,
context.setPresentationPoints,
"update",
runtime,
pathPlans
);
return runTracks(root, [...tracks, ...presentation.tracks], {
publish: presentation.publish,
finish: () => context.setPresentationPoints?.(context.scene.points)
});
}
function createRollingPathPlans(currentRoot, nextRoot, previousScene, scene, timingFor) {
const elements = /* @__PURE__ */ new Map();
const points = /* @__PURE__ */ new Map();
if (!previousScene) return { elements, points };
const currentPaths = keyedElementMap(
currentRoot,
"g.ts-chart__line path, g.ts-chart__area path"
);
const nextPaths = keyedElementMap(
nextRoot,
"g.ts-chart__line path, g.ts-chart__area path"
);
for (const [key] of nextPaths) {
const currentPath = currentPaths.get(key);
if (!currentPath) continue;
const motionContext = elementTimingContext(currentPath, "update", scene);
if (!motionContext) continue;
const timing = timingFor(motionContext);
if (!isRollingPathMotion(timing.path)) continue;
const previous = scenePathSnapshot(previousScene, key);
const next = scenePathSnapshot(scene, key);
let outcome = previous && next ? resolveRollingPathPlan(previous, next, timing.path) : {
kind: "fallback",
fallback: timing.path.fallback ?? "snap",
reason: "missing-semantic-points"
};
if (outcome.kind === "transform") {
outcome = {
...outcome,
transform: composeRollingTransform(
parseRollingTransform(currentPath.getAttribute("transform")),
outcome.transform
)
};
}
const planned = {
key,
outcome,
points: next?.points ?? [],
previousPoints: previous?.points ?? [],
timing
};
elements.set(key, planned);
for (const point of previous?.points ?? []) {
points.set(pointIdentity(point), planned);
}
for (const point of next?.points ?? []) {
points.set(pointIdentity(point), planned);
}
}
return { elements, points };
}
function scenePathSnapshot(scene, key) {
const context = findSceneNodeContext(scene.nodes, key);
const node = context?.node;
if (!node || node.kind !== "polyline" && node.kind !== "area") {
return void 0;
}
const interaction = node.interaction;
const points = interaction && "points" in interaction ? interaction.points ?? [] : [];
if (!points.length) return void 0;
return {
kind: node.kind,
points,
geometry: node.points,
chart: scene.chart,
yScale: scene.scales.y,
viewportTranslate: {
x: context.translateX,
y: context.translateY
},
clipped: context.clipped,
customPath: node.path !== void 0
};
}
function findSceneNodeContext(nodes, key, translateX = 0, translateY = 0, clipped = false) {
for (const node of nodes) {
if (node.key === key) return { node, translateX, translateY, clipped };
if (node.kind === "group") {
const nested = findSceneNodeContext(
node.children,
key,
translateX + (node.translateX ?? 0),
translateY + (node.translateY ?? 0),
clipped || node.clip !== void 0
);
if (nested) return nested;
}
}
return void 0;
}
function isRollingPathMotion(path) {
return typeof path === "object" && path.update === "rolling";
}
function composeRollingTransform(current, next) {
return {
x: current.x + next.x,
yScale: current.yScale * next.yScale,
y: current.yScale * next.y + current.y
};
}
function parseRollingTransform(value) {
if (!value) return { x: 0, yScale: 1, y: 0 };
const translated = translatedX(value);
if (translated !== void 0) return { x: translated, yScale: 1, y: 0 };
const match = /^matrix\(\s*1(?:\.0+)?\s+0(?:\.0+)?\s+0(?:\.0+)?\s+(-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)\s+(-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)\s+(-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)\s*\)$/i.exec(
value
);
if (!match) return { x: 0, yScale: 1, y: 0 };
const yScale = Number(match[1]);
const x = Number(match[2]);
const y = Number(match[3]);
return Number.isFinite(x) && Number.isFinite(yScale) && Number.isFinite(y) ? { x, yScale, y } : { x: 0, yScale: 1, y: 0 };
}
const motionAttributes = /* @__PURE__ */ new Set([
"cx",
"cy",
"d",
"fill-opacity",
"font-size",
"font-weight",
"height",
"opacity",
"r",
"rx",
"stroke-opacity",
"stroke-width",
"transform",
"width",
"x",
"x1",
"x2",
"y",
"y1",
"y2"
]);
const rollingPointGeometryAttributes = /* @__PURE__ */ new Set([
"cx",
"cy",
"height",
"width",
"x",
"x1",
"x2",
"y",
"y1",
"y2"
]);
function reconcileMotionElement(current, next, tracks, context) {
addUpdateTrack(current, next, tracks, context);
if (!next.firstElementChild) {
if (current.firstElementChild) {
for (const child of [...current.children]) {
addExitMotionTrack(child, tracks, context);
}
} else if (current.textContent !== next.textContent) {
current.textContent = next.textContent;
}
return;
}
const currentChildren = [...current.children];
const nextChildren = [...next.children];
const currentByIdentity = indexMotionChildren(currentChildren);
const nextIdentities = motionIdentities(nextChildren);
const retained = /* @__PURE__ */ new Set();
let cursor = current.firstElementChild;
nextChildren.forEach((nextChild, index) => {
const matched = currentByIdentity.get(nextIdentities[index]);
let rendered;
if (matched && matched.namespaceURI === nextChild.namespaceURI && matched.localName === nextChild.localName) {
rendered = matched;
retained.add(matched);
if (rendered !== cursor) current.insertBefore(rendered, cursor);
reconcileMotionElement(rendered, nextChild, tracks, context);
} else {
rendered = nextChild.cloneNode(true);
current.insertBefore(rendered, cursor);
addEnterMotionTrack(rendered, tracks, context);
}
cursor = rendered.nextElementSibling;
});
for (const child of currentChildren) {
if (!retained.has(child) && child.parentElement === current) {
addExitMotionTrack(child, tracks, context);
}
}
}
function addUpdateTrack(current, next, tracks, context) {
let timingContext = elementTimingContext(current, "update", context.scene);
let timing;
const pathKey = current.getAttribute("data-ts-key");
const rolling = pathKey ? context.pathPlans.elements.get(pathKey) : void 0;
const rollingTransform = rolling?.outcome.kind === "transform" ? rolling.outcome.transform : void 0;
const rollingSnap = rolling?.outcome.kind === "fallback" && rolling.outcome.fallback === "snap";
const pointRolling = timingContext?.point ? context.pathPlans.points.get(pointIdentity(timingContext.point)) : void 0;
const pointRollingSnap = pointRolling?.outcome.kind === "fallback" && pointRolling.outcome.fallback === "snap";
const semanticPath = addSemanticPathUpdateTrack(
current,
next,
tracks,
context,
timingContext
);
const nextNames = new Set(next.getAttributeNames());
for (const name of current.getAttributeNames()) {
if (!nextNames.has(name) && !(rollingTransform !== void 0 && name === "transform")) {
current.removeAttribute(name);
}
}
const attributes = [];
for (const name of nextNames) {
const target = next.getAttribute(name);
const previous = current.getAttribute(name);
if (target === previous) continue;
if (semanticPath && name === "d") continue;
if (pointRollingSnap && rollingPointGeometryAttributes.has(name) && target !== null) {
current.setAttribute(name, target);
continue;
}
if ((rollingTransform !== void 0 || rollingSnap) && name === "d" && target !== null) {
current.setAttribute(name, target);
continue;
}
const parsed = previous !== null && target !== null && motionAttributes.has(name) ? parseMotionAttribute(previous, target) : void 0;
if (parsed) attributes.push({ name, ...parsed, target });
else if (target !== null) current.setAttribute(name, target);
}
if (rollingTransform && timingContext && rolling) {
current.setAttribute("data-ts-motion-role", timingContext.role);
const apply = (values) => {
current.setAttribute(
"transform",
`matrix(1 0 0 ${formatNumber(values[1] ?? 1)} ${formatNumber(values[0] ?? 0)} ${formatNumber(values[2] ?? 0)})`
);
};
const from2 = [
rollingTransform.x,
rollingTransform.yScale,
rollingTransform.y
];
apply(from2);
tracks.push({
...rolling.timing,
values: bindMotionValues(void 0, from2, [0, 1, 0]),
apply,
finish() {
current.removeAttribute("transform");
current.removeAttribute("data-ts-motion-role");
},
cancel() {
current.removeAttribute("data-ts-motion-role");
}
});
}
if (!attributes.length) return;
timingContext ??= elementTimingContext(current, "update", context.scene);
if (!timingContext) {
finishMotionAttributes(current, attributes);
return;
}
timing ??= pointRolling?.outcome.kind === "transform" ? pointRolling.timing : context.timingFor(timingContext);
current.setAttribute("data-ts-motion-role", timingContext.role);
const states = attributes.flatMap(
(attribute) => elementValueStates(
context