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.

932 lines (931 loc) 33.3 kB
import { mountChartRenderer } from "./renderer.js"; import { createChartRuntime } from "./runtime.js"; import { resolveFocusScene } from "./focus-layer.js"; import { resolveFocusPresentation } from "./focus-presentation.js"; import { resolveMarkStateScene } from "./mark-state.js"; import { resolveMarkStateTransition } from "./mark-state-transition.js"; const defaultPaint = { fill: "black", fillOpacity: 1, stroke: null, strokeOpacity: 1, strokeWidth: 1, opacity: 1, lineCap: "butt", lineJoin: "miter", strokeDasharray: "" }; function createCanvasChartRenderer(rendererOptions = {}) { return createUniversalCanvasChartRenderer(rendererOptions); } function createUniversalCanvasChartRenderer(rendererOptions = {}) { const renderer = { id: "canvas", prerender(scene, options) { return renderCanvasShell(scene, options); }, mount(container, requestRender) { const document = container.ownerDocument; const view = document.defaultView; const root = findOrCreateRoot(container); const backgroundCanvas = findOrCreateCanvas( root, "ts-chart-canvas__background" ); const focusUnderCanvas = findOrCreateCanvas( root, "ts-chart-canvas__focus-under" ); const sceneCanvas = findOrCreateCanvas(root, "ts-chart-canvas__scene"); const focusCanvas = findOrCreateCanvas(root, "ts-chart-canvas__focus"); const canvas = findOrCreateCanvas(root, "ts-chart-canvas__base"); canvas.style.display = "none"; root.append( backgroundCanvas, focusUnderCanvas, sceneCanvas, focusCanvas, canvas ); const resolver = new CanvasPaintResolver(root); const mutationObserver = observeTheme(container, requestRender); const colorScheme = view?.matchMedia?.("(prefers-color-scheme: dark)"); const forcedColors = view?.matchMedia?.("(forced-colors: active)"); const handleEnvironmentChange = () => requestRender(true); colorScheme?.addEventListener?.("change", handleEnvironmentChange); forcedColors?.addEventListener?.("change", handleEnvironmentChange); view?.addEventListener("resize", handleEnvironmentChange); let scene; let pixelRatio = 1; let cancelAnimation = () => { }; let backgroundAnimationActive = false; let stateTransition; let markStatePainted = false; let destroyed = false; const startCoordinatedAnimation = (nextScene, animation, captureBase) => { backgroundAnimationActive = true; const cancel = animateSceneUpdate( backgroundCanvas, sceneCanvas, captureBase ? canvas : void 0, nextScene, pixelRatio, animation, resolver, root, () => { backgroundAnimationActive = false; } ); cancelAnimation = () => { backgroundAnimationActive = false; cancel(); }; }; const surface = { renderer, element: root, canvas, backgroundCanvas, focusUnderCanvas, sceneCanvas, focusCanvas, render(nextScene, options) { if (destroyed) return; cancelAnimation(); backgroundAnimationActive = false; cancelAnimation = () => { }; configureRoot(root, options); resolver.refresh(); const nextPixelRatio = resolvePixelRatio( rendererOptions.pixelRatio, view ); const canAnimate = options.animation !== void 0 && scene !== void 0 && scene.width === nextScene.width && scene.height === nextScene.height && pixelRatio === nextPixelRatio; pixelRatio = nextPixelRatio; sizeCanvas(backgroundCanvas, nextScene, pixelRatio); sizeCanvas(canvas, nextScene, pixelRatio); sizeCanvas(sceneCanvas, nextScene, pixelRatio); sizeCanvas(focusUnderCanvas, nextScene, pixelRatio); sizeCanvas(focusCanvas, nextScene, pixelRatio); root.dataset.tsChartWidth = String(nextScene.width); root.dataset.tsChartHeight = String(nextScene.height); root.dataset.tsChartPixelRatio = String(pixelRatio); clearCanvas(focusUnderCanvas, nextScene, pixelRatio); clearCanvas(focusCanvas, nextScene, pixelRatio); if (canAnimate) { startCoordinatedAnimation(nextScene, options.animation, true); } else { paintBackgroundCanvas( backgroundCanvas, nextScene, pixelRatio, resolver ); paintCanvas(sceneCanvas, nextScene, pixelRatio, resolver, root); composeBaseCanvas( canvas, backgroundCanvas, sceneCanvas, nextScene, pixelRatio ); } scene = nextScene; stateTransition = void 0; markStatePainted = false; }, clientToScene(currentScene, clientX, clientY) { const bounds = root.getBoundingClientRect(); if (!bounds.width || !bounds.height) return null; return { x: (clientX - bounds.left) / bounds.width * currentScene.width, y: (clientY - bounds.top) / bounds.height * currentScene.height }; }, paintFocus(focus, pointer, cursor) { if (!scene || destroyed) return; const state = resolveMarkStateScene(scene, focus, pointer); const resolved = resolveFocusScene(state.scene, focus); const previousTransition = stateTransition; if (state.scene !== scene || markStatePainted || previousTransition) { const interruptedBackground = backgroundAnimationActive; cancelAnimation(); backgroundAnimationActive = false; const transition = resolveMarkStateTransition( state.transition ?? previousTransition, root ); if (transition) { if (interruptedBackground) { startCoordinatedAnimation(state.scene, transition, false); } else { cancelAnimation = animateScene( sceneCanvas, state.scene, pixelRatio, transition, resolver, root ); } } else { if (interruptedBackground) { paintBackgroundCanvas( backgroundCanvas, state.scene, pixelRatio, resolver ); } paintCanvas(sceneCanvas, state.scene, pixelRatio, resolver, root); cancelAnimation = () => { }; } } markStatePainted = Boolean(focus && state.scene !== scene); stateTransition = focus ? state.transition ?? previousTransition : void 0; const presentation = resolveFocusPresentation( resolved.scene, focus, pointer, cursor ); paintFocusCanvas( focusUnderCanvas, resolved.scene, presentation.under, pixelRatio, resolver, root ); paintFocusCanvas( focusCanvas, resolved.scene, presentation.over, pixelRatio, resolver, root ); return resolved.scene; }, destroy() { if (destroyed) return; destroyed = true; cancelAnimation(); mutationObserver?.disconnect(); colorScheme?.removeEventListener?.("change", handleEnvironmentChange); forcedColors?.removeEventListener?.("change", handleEnvironmentChange); view?.removeEventListener("resize", handleEnvironmentChange); resolver.destroy(); } }; return surface; } }; return renderer; } const canvasChartRenderer = createUniversalCanvasChartRenderer(); function mountCanvasChart(container, initialOptions, runtime = createChartRuntime()) { const withRenderer = (options) => { return { ...options, renderer: canvasChartRenderer }; }; const host = mountChartRenderer( container, withRenderer(initialOptions), runtime ); return { interaction: host.interaction, update(options) { host.update(withRenderer(options)); }, getScene: host.getScene, destroy: host.destroy }; } function renderCanvasShell(scene, options) { const className = options.className ? `ts-chart ts-chart-canvas ${options.className}` : "ts-chart ts-chart-canvas"; const description = options.ariaDescription ? ` aria-description="${escapeAttribute(options.ariaDescription)}"` : ""; const width = integer(scene.width); const height = integer(scene.height); return `<div class="${escapeAttribute(className)}" role="img" aria-roledescription="chart" aria-label="${escapeAttribute(options.ariaLabel)}"${description} tabindex="${integer(options.tabIndex ?? 0)}" data-ts-chart-width="${width}" data-ts-chart-height="${height}" data-ts-chart-pixel-ratio="1" style="display:block;position:relative;width:100%;height:100%;overflow:visible"><canvas class="ts-chart-canvas__background" width="${width}" height="${height}" aria-hidden="true" style="display:block;position:absolute;inset:0;width:100%;height:100%;pointer-events:none"></canvas><canvas class="ts-chart-canvas__focus-under" width="${width}" height="${height}" aria-hidden="true" style="display:block;position:absolute;inset:0;width:100%;height:100%;pointer-events:none"></canvas><canvas class="ts-chart-canvas__scene" width="${width}" height="${height}" aria-hidden="true" style="display:block;position:absolute;inset:0;width:100%;height:100%;pointer-events:none"></canvas><canvas class="ts-chart-canvas__focus" width="${width}" height="${height}" aria-hidden="true" style="display:block;position:absolute;inset:0;width:100%;height:100%;pointer-events:none"></canvas><canvas class="ts-chart-canvas__base" width="${width}" height="${height}" aria-hidden="true" style="display:none"></canvas></div>`; } function findOrCreateRoot(container) { const existing = container.querySelector( ":scope > .ts-chart-canvas" ); if (existing) return existing; const root = container.ownerDocument.createElement("div"); container.replaceChildren(root); return root; } function findOrCreateCanvas(root, className) { const existing = root.querySelector(`.${className}`); if (existing) return existing; const canvas = root.ownerDocument.createElement("canvas"); canvas.className = className; canvas.setAttribute("aria-hidden", "true"); Object.assign(canvas.style, { display: "block", position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" }); root.append(canvas); return canvas; } function configureRoot(root, options) { root.className = options.className ? `ts-chart ts-chart-canvas ${options.className}` : "ts-chart ts-chart-canvas"; root.setAttribute("role", "img"); root.setAttribute("aria-roledescription", "chart"); root.setAttribute("aria-label", options.ariaLabel); if (options.ariaDescription) { root.setAttribute("aria-description", options.ariaDescription); } else { root.removeAttribute("aria-description"); } root.tabIndex = options.tabIndex ?? 0; Object.assign(root.style, { display: "block", position: "relative", width: "100%", height: "100%", overflow: "visible" }); } function resolvePixelRatio(configured, view) { const value = configured ?? view?.devicePixelRatio ?? 1; return Number.isFinite(value) && value > 0 ? value : 1; } function sizeCanvas(canvas, scene, pixelRatio) { const width = Math.max(1, Math.ceil(scene.width * pixelRatio)); const height = Math.max(1, Math.ceil(scene.height * pixelRatio)); if (canvas.width !== width) canvas.width = width; if (canvas.height !== height) canvas.height = height; } function clearCanvas(canvas, scene, pixelRatio) { const context = requiredContext(canvas); resetContext(context, pixelRatio); context.clearRect(0, 0, scene.width, scene.height); } function paintCanvas(canvas, scene, pixelRatio, resolver, root) { const context = requiredContext(canvas); resetContext(context, pixelRatio); context.clearRect(0, 0, scene.width, scene.height); paintScene(context, scene, resolver, root); } function paintBackgroundCanvas(canvas, scene, pixelRatio, resolver) { const context = requiredContext(canvas); resetContext(context, pixelRatio); context.clearRect(0, 0, scene.width, scene.height); paintSceneBackground(context, scene, resolver); } function composeBaseCanvas(canvas, backgroundCanvas, sceneCanvas, scene, pixelRatio) { const context = requiredContext(canvas); resetContext(context, pixelRatio); context.clearRect(0, 0, scene.width, scene.height); context.drawImage(backgroundCanvas, 0, 0, scene.width, scene.height); context.drawImage(sceneCanvas, 0, 0, scene.width, scene.height); } function paintSceneBackground(context, scene, resolver) { if (scene.theme.background === "transparent") return; const background = resolver.resolve(scene.theme.background); if (!background) return; context.fillStyle = background; context.fillRect(0, 0, scene.width, scene.height); } function resetContext(context, pixelRatio) { context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); context.globalAlpha = 1; context.setLineDash([]); } function animateScene(canvas, scene, pixelRatio, animation, resolver, root) { const document = canvas.ownerDocument; const view = document.defaultView; const duration = Math.max(0, animation.duration ?? 250); if (!view?.requestAnimationFrame || duration === 0) { paintCanvas(canvas, scene, pixelRatio, resolver, root); return () => { }; } const previous = copyCanvas(canvas); const target = sizedCanvas(document, canvas); paintCanvas(target, scene, pixelRatio, resolver, root); return crossfadeCanvasLayers( view, [{ canvas, previous, target }], duration, animation ); } function animateSceneUpdate(backgroundCanvas, sceneCanvas, baseCanvas, scene, pixelRatio, animation, resolver, root, onComplete) { const document = sceneCanvas.ownerDocument; const view = document.defaultView; const duration = Math.max(0, animation.duration ?? 250); if (!view?.requestAnimationFrame || duration === 0) { paintBackgroundCanvas(backgroundCanvas, scene, pixelRatio, resolver); paintCanvas(sceneCanvas, scene, pixelRatio, resolver, root); if (baseCanvas) { composeBaseCanvas( baseCanvas, backgroundCanvas, sceneCanvas, scene, pixelRatio ); } onComplete?.(); return () => { }; } const previousBackground = copyCanvas(backgroundCanvas); const targetBackground = sizedCanvas(document, backgroundCanvas); paintBackgroundCanvas(targetBackground, scene, pixelRatio, resolver); const previousScene = copyCanvas(sceneCanvas); const targetScene = sizedCanvas(document, sceneCanvas); paintCanvas(targetScene, scene, pixelRatio, resolver, root); if (baseCanvas) { composeBaseCanvas( baseCanvas, targetBackground, targetScene, scene, pixelRatio ); } return crossfadeCanvasLayers( view, [ { canvas: backgroundCanvas, previous: previousBackground, target: targetBackground }, { canvas: sceneCanvas, previous: previousScene, target: targetScene } ], duration, animation, onComplete ); } function crossfadeCanvasLayers(view, layers, duration, animation, onComplete) { const ease = resolveEasing(animation.easing); let frame; let canceled = false; let start; const paintFrame = (time) => { if (canceled) return; start ??= time; const progress = duration === 0 ? 1 : Math.min(1, (time - start) / duration); const eased = ease(progress); for (const { canvas, previous, target } of layers) { const context = requiredContext(canvas); context.setTransform(1, 0, 0, 1, 0, 0); context.clearRect(0, 0, canvas.width, canvas.height); context.globalAlpha = 1 - eased; context.drawImage(previous, 0, 0); context.globalAlpha = eased; context.drawImage(target, 0, 0); context.globalAlpha = 1; } if (progress < 1) { frame = view.requestAnimationFrame(paintFrame); } else { frame = void 0; onComplete?.(); } }; frame = view.requestAnimationFrame(paintFrame); return () => { canceled = true; if (frame !== void 0) view.cancelAnimationFrame(frame); }; } function copyCanvas(canvas) { const copy = sizedCanvas(canvas.ownerDocument, canvas); requiredContext(copy).drawImage(canvas, 0, 0); return copy; } function sizedCanvas(document, source) { const canvas = document.createElement("canvas"); canvas.width = source.width; canvas.height = source.height; return canvas; } function resolveEasing(easing) { if (typeof easing === "function") return easing; switch (easing) { case "ease-in": return (progress) => progress * progress; case "ease-out": return (progress) => 1 - (1 - progress) * (1 - progress); case "ease": case "ease-in-out": return (progress) => progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2; default: return (progress) => progress; } } function paintScene(context, scene, resolver, root) { const Path = root.ownerDocument.defaultView?.Path2D; const font = readFont(root); const painter = { context, resolver, scene, Path, font }; paintNodes(painter, scene.nodes, defaultPaint); } function paintFocusCanvas(canvas, scene, nodes, pixelRatio, resolver, root) { const context = requiredContext(canvas); resetContext(context, pixelRatio); context.clearRect(0, 0, scene.width, scene.height); if (!nodes.length) return; const Path = root.ownerDocument.defaultView?.Path2D; const font = readFont(root); paintNodes({ context, resolver, scene, Path, font }, nodes, defaultPaint); } function paintNodes(painter, nodes, parent) { for (let index = 0; index < nodes.length; index += 1) { const node = nodes[index]; if (!node) continue; if (node.kind === "dot") { index = paintDotRun(painter, nodes, index, parent); } else { paintNode(painter, node, parent); } } } function paintNode(painter, node, parent) { const state = resolveStyle(parent, node.style); const { context } = painter; context.save(); try { switch (node.kind) { case "group": { if (node.focus) return; context.translate(node.translateX ?? 0, node.translateY ?? 0); if (node.clip) { context.beginPath(); context.rect( node.clip.x, node.clip.y, node.clip.width, node.clip.height ); context.clip(); } paintNodes(painter, node.children, state); return; } case "rule": context.beginPath(); context.moveTo(node.x1, node.y1); context.lineTo(node.x2, node.y2); strokeCurrentPath(painter, state, boundsForNode(node)); return; case "polyline": { if (node.path) { const path = pathFromData(painter, node.path); paintPath(painter, path, state, boundsForNode(node)); } else { beginPointPath(context, node.points, false); paintCurrentPath(painter, state, boundsForNode(node)); } return; } case "area": { if (node.polygons !== void 0) { beginPolygonPath(context, node.polygons); paintCurrentPath(painter, state, boundsForNode(node), "evenodd"); } else if (node.path) { const path = pathFromData(painter, node.path); paintPath(painter, path, state, boundsForNode(node)); } else { beginPointPath(context, node.points, true); paintCurrentPath(painter, state, boundsForNode(node)); } return; } case "dot": context.beginPath(); context.arc(node.x, node.y, node.radius, 0, Math.PI * 2); paintCurrentPath(painter, state, boundsForNode(node)); return; case "rect": beginRoundedRect( context, node.x, node.y, node.width, node.height, node.radius ?? 0 ); paintCurrentPath(painter, state, boundsForNode(node)); return; case "label": paintLabel(painter, node, state); } } finally { context.restore(); } } function paintDotRun(painter, nodes, start, parent) { const first = nodes[start]; if (!first || first.kind !== "dot") return start; const state = resolveStyle(parent, first.style); if (usesGradient(state)) { paintNode(painter, first, parent); return start; } const { context } = painter; context.save(); try { context.beginPath(); let index = start; for (; index < nodes.length; index += 1) { const node = nodes[index]; if (!node || node.kind !== "dot" || !samePaintState(resolveStyle(parent, node.style), state)) { break; } context.moveTo(node.x + node.radius, node.y); context.arc(node.x, node.y, node.radius, 0, Math.PI * 2); } paintCurrentPath(painter, state, null); return index - 1; } finally { context.restore(); } } function usesGradient(state) { return Boolean( state.fill?.startsWith("url(#") || state.stroke?.startsWith("url(#") ); } function samePaintState(left, right) { return left.fill === right.fill && left.fillOpacity === right.fillOpacity && left.stroke === right.stroke && left.strokeOpacity === right.strokeOpacity && left.strokeWidth === right.strokeWidth && left.opacity === right.opacity && left.lineCap === right.lineCap && left.lineJoin === right.lineJoin && left.strokeDasharray === right.strokeDasharray; } function pathFromData(painter, data) { if (!painter.Path) { throw new Error( "Canvas rendering of curved, polar, or geographic paths requires Path2D." ); } return new painter.Path(data); } function beginPointPath(context, points, close) { context.beginPath(); points.forEach(([x, y], index) => { if (index === 0) context.moveTo(x, y); else context.lineTo(x, y); }); if (close && points.length) context.closePath(); } function beginPolygonPath(context, polygons) { context.beginPath(); for (const polygon of polygons) { for (const ring of polygon) { ring.forEach(([x, y], index) => { if (index === 0) context.moveTo(x, y); else context.lineTo(x, y); }); if (ring.length) context.closePath(); } } } function beginRoundedRect(context, x, y, width, height, radius) { const resolved = Math.max(0, Math.min(radius, width / 2, height / 2)); context.beginPath(); if (resolved === 0) { context.rect(x, y, width, height); return; } context.moveTo(x + resolved, y); context.lineTo(x + width - resolved, y); context.arcTo(x + width, y, x + width, y + resolved, resolved); context.lineTo(x + width, y + height - resolved); context.arcTo( x + width, y + height, x + width - resolved, y + height, resolved ); context.lineTo(x + resolved, y + height); context.arcTo(x, y + height, x, y + height - resolved, resolved); context.lineTo(x, y + resolved); context.arcTo(x, y, x + resolved, y, resolved); context.closePath(); } function paintPath(painter, path, state, bounds) { fillPath(painter, state, bounds, path); strokePath(painter, state, bounds, path); } function paintCurrentPath(painter, state, bounds, fillRule) { fillCurrentPath(painter, state, bounds, fillRule); strokeCurrentPath(painter, state, bounds); } function fillPath(painter, state, bounds, path) { const fill = resolvePaint(painter, state.fill, bounds); if (!fill) return; painter.context.globalAlpha = state.opacity * state.fillOpacity; painter.context.fillStyle = fill; painter.context.fill(path); } function strokePath(painter, state, bounds, path) { const stroke = resolvePaint(painter, state.stroke, bounds); if (!stroke) return; configureStroke(painter.context, state, stroke); painter.context.stroke(path); } function fillCurrentPath(painter, state, bounds, fillRule) { const fill = resolvePaint(painter, state.fill, bounds); if (!fill) return; painter.context.globalAlpha = state.opacity * state.fillOpacity; painter.context.fillStyle = fill; if (fillRule === void 0) painter.context.fill(); else painter.context.fill(fillRule); } function strokeCurrentPath(painter, state, bounds) { const stroke = resolvePaint(painter, state.stroke, bounds); if (!stroke) return; configureStroke(painter.context, state, stroke); painter.context.stroke(); } function configureStroke(context, state, stroke) { context.globalAlpha = state.opacity * state.strokeOpacity; context.strokeStyle = stroke; context.lineWidth = state.strokeWidth; context.lineCap = state.lineCap; context.lineJoin = state.lineJoin; context.setLineDash(parseDasharray(state.strokeDasharray)); } function paintLabel(painter, node, state) { const { context } = painter; const fontSize = node.fontSize ?? painter.font.size; const fontWeight = node.fontWeight ?? painter.font.weight; context.translate(node.x, node.y); if (node.rotate !== void 0) { context.rotate(node.rotate * Math.PI / 180); } context.font = [ painter.font.style, fontWeight, `${fontSize}px`, painter.font.family ].join(" "); if ("fontStretch" in context) context.fontStretch = painter.font.stretch; if ("letterSpacing" in context) { context.letterSpacing = painter.font.letterSpacing; } context.direction = painter.font.direction; context.textAlign = node.anchor === "middle" ? "center" : node.anchor === "end" ? "right" : "left"; context.textBaseline = node.baseline === "middle" ? "middle" : node.baseline === "hanging" ? "hanging" : "alphabetic"; const fill = resolvePaint(painter, state.fill, null); if (fill) { context.globalAlpha = state.opacity * state.fillOpacity; context.fillStyle = fill; context.fillText(node.text, 0, 0); } const stroke = resolvePaint(painter, state.stroke, null); if (stroke) { configureStroke(context, state, stroke); context.strokeText(node.text, 0, 0); } } function resolveStyle(parent, style) { if (!style) return parent; return { fill: style.fill === void 0 ? parent.fill : paintValue(style.fill), fillOpacity: style.fillOpacity ?? parent.fillOpacity, stroke: style.stroke === void 0 ? parent.stroke : paintValue(style.stroke), strokeOpacity: style.strokeOpacity ?? parent.strokeOpacity, strokeWidth: style.strokeWidth ?? parent.strokeWidth, opacity: parent.opacity * (style.opacity ?? 1), lineCap: style.lineCap ?? parent.lineCap, lineJoin: resolveLineJoin(style.lineJoin) ?? parent.lineJoin, strokeDasharray: style.strokeDasharray ?? parent.strokeDasharray }; } function paintValue(value) { return value === "none" ? null : value; } function resolveLineJoin(value) { if (value === "arcs") return "round"; if (value === "miter-clip") return "miter"; return value; } function resolvePaint(painter, value, bounds) { if (!value) return null; const match = /^url\(#([^)]+)\)$/.exec(value); if (!match) return painter.resolver.resolve(value); const gradient = painter.scene.gradients.find( (candidate) => candidate.id === match[1] ); if (!gradient) return null; if (!bounds) { throw new Error( `Canvas gradient "${gradient.id}" requires geometry with measurable bounds.` ); } const canvasGradient = painter.context.createLinearGradient( bounds.x + (gradient.x1 ?? 0) * bounds.width, bounds.y + (gradient.y1 ?? 1) * bounds.height, bounds.x + (gradient.x2 ?? 0) * bounds.width, bounds.y + (gradient.y2 ?? 0) * bounds.height ); for (const stop of gradient.stops) { const color = painter.resolver.resolve(stop.color); if (!color) continue; canvasGradient.addColorStop( Math.max(0, Math.min(1, stop.offset)), stop.opacity === void 0 ? color : painter.resolver.withOpacity(color, stop.opacity) ); } return canvasGradient; } function boundsForNode(node) { switch (node.kind) { case "rule": return boundsFromPoints([ [node.x1, node.y1], [node.x2, node.y2] ]); case "polyline": return boundsFromPoints(node.points); case "area": return node.polygons === void 0 ? boundsFromPoints(node.points) : boundsFromPolygons(node.polygons); case "dot": return { x: node.x - node.radius, y: node.y - node.radius, width: node.radius * 2, height: node.radius * 2 }; case "rect": return { x: node.x, y: node.y, width: node.width, height: node.height }; } } function boundsFromPolygons(polygons) { return boundsFromPoints(polygons.flatMap((polygon) => polygon.flat())); } function boundsFromPoints(points) { if (!points.length) return null; let minX = Number.POSITIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; for (const [x, y] of points) { if (!Number.isFinite(x) || !Number.isFinite(y)) continue; minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); } if (!Number.isFinite(minX) || !Number.isFinite(minY)) return null; return { x: minX, y: minY, width: Math.max(0, maxX - minX), height: Math.max(0, maxY - minY) }; } function parseDasharray(value) { if (!value || value === "none") return []; return value.split(/[,\s]+/).map(Number).filter((part) => Number.isFinite(part) && part >= 0); } function readFont(root) { const computed = root.ownerDocument.defaultView?.getComputedStyle(root); const size = Number.parseFloat(computed?.fontSize ?? ""); return { family: computed?.fontFamily || "sans-serif", size: Number.isFinite(size) && size > 0 ? size : 16, style: computed?.fontStyle || "normal", weight: computed?.fontWeight || "400", stretch: normalizeFontStretch(computed?.fontStretch), direction: computed?.direction === "rtl" ? "rtl" : computed?.direction === "ltr" ? "ltr" : "inherit", letterSpacing: computed?.letterSpacing || "0px" }; } function normalizeFontStretch(value) { if (value === "ultra-condensed" || value === "extra-condensed" || value === "condensed" || value === "semi-condensed" || value === "normal" || value === "semi-expanded" || value === "expanded" || value === "extra-expanded" || value === "ultra-expanded") { return value; } return "normal"; } class CanvasPaintResolver { #root; #probe; #cache = /* @__PURE__ */ new Map(); constructor(root) { this.#root = root; this.#probe = root.ownerDocument.createElement("span"); this.#probe.setAttribute("aria-hidden", "true"); Object.assign(this.#probe.style, { position: "absolute", width: "0", height: "0", overflow: "hidden", pointerEvents: "none", visibility: "hidden" }); root.append(this.#probe); } refresh() { this.#cache.clear(); } resolve(value) { if (value === "none") return null; const cached = this.#cache.get(value); if (cached) return cached; this.#probe.style.color = ""; this.#probe.style.color = value; if (!this.#probe.style.color) { throw new TypeError(`Invalid Canvas paint: ${value}`); } const resolved = this.#root.ownerDocument.defaultView?.getComputedStyle(this.#probe).color || value; this.#cache.set(value, resolved); return resolved; } withOpacity(color, opacity) { const value = `color-mix(in srgb, ${color} ${Math.max(0, Math.min(1, opacity)) * 100}%, transparent)`; return this.resolve(value) ?? "transparent"; } destroy() { this.#probe.remove(); this.#cache.clear(); } } function observeTheme(container, requestRender) { const Observer = container.ownerDocument.defaultView?.MutationObserver; if (!Observer) return void 0; const observer = new Observer(() => requestRender(true)); let current = container; while (current) { observer.observe(current, { attributes: true, attributeFilter: ["class", "style", "data-theme"] }); current = current.parentElement; } return observer; } function requiredContext(canvas) { const context = canvas.getContext("2d"); if (!context) { throw new Error("The Canvas renderer requires a Canvas 2D context."); } return context; } function integer(value) { return String(Math.max(0, Math.round(value))); } function escapeAttribute(value) { return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;"); } export { canvasChartRenderer, createCanvasChartRenderer, mountCanvasChart };