UNPKG

@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
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