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.

129 lines (128 loc) 6.82 kB
function renderChartSvgWithHooks(scene, options, hooks) { const idPrefix = options.idPrefix ?? ""; const className = options.className ? `ts-chart ${options.className}` : "ts-chart"; const description = options.ariaDescription ? `<desc>${escapeText(options.ariaDescription)}</desc>` : ""; const definitions = hooks?.renderDefinitions?.(scene, idPrefix) ?? ""; const background = scene.theme.background === "transparent" ? "" : renderNode( { kind: "rect", key: "background", x: 0, y: 0, width: scene.width, height: scene.height, style: { fill: scene.theme.background } }, hooks, idPrefix ); return `<svg class="${escapeAttribute(className)}" width="100%" height="100%" viewBox="0 0 ${number(scene.width)} ${number(scene.height)}" role="img" aria-roledescription="chart" aria-label="${escapeAttribute(options.ariaLabel)}" tabindex="${number(options.tabIndex ?? 0)}" style="display:block;overflow:visible">${description}${definitions}${background}${renderSceneNodes(scene.nodes, idPrefix, hooks)}</svg>`; } function renderSceneNodes(nodes, idPrefix = "", hooks) { return nodes.map((node) => renderNode(node, hooks, idPrefix)).join(""); } function renderFocusGuideLayer(nodes, placement, idPrefix = "", hooks) { const visibility = nodes.length ? "visible" : "hidden"; return `<g data-ts-key="focus-guide-layer:${placement}" class="ts-chart__focus-guide-layer ts-chart__focus-guide-layer--${placement}" data-ts-focus-layer="${placement}" data-ts-focus-guide-layer="${placement}" aria-hidden="true" visibility="${visibility}">${renderSceneNodes(nodes, idPrefix, hooks ?? focusGuideRenderHooks)}</g>`; } const focusGuideRenderHooks = { renderGroup: renderFocusGuideClip }; function renderNode(node, hooks, idPrefix) { const common = renderCommon(node, hooks, idPrefix); switch (node.kind) { case "group": { const transform = node.translateX === void 0 && node.translateY === void 0 ? "" : ` transform="translate(${number(node.translateX ?? 0)} ${number(node.translateY ?? 0)})"`; const extension = hooks?.renderGroup?.(node, idPrefix); const focus = node.focus ? ` data-ts-focus-layer="${node.focus.placement}"${node.focus.retarget ? ' data-ts-focus-retarget="true"' : ""} visibility="hidden"` : ""; return `<g${common}${transform}${focus}${extension?.attributes ?? ""}>${extension?.content ?? ""}${node.children.map((child) => renderNode(child, hooks, idPrefix)).join("")}</g>`; } case "rule": return `<line${common} x1="${number(node.x1)}" y1="${number(node.y1)}" x2="${number(node.x2)}" y2="${number(node.y2)}"/>`; case "polyline": { const path = node.path ?? node.points.map( ([x, y], index) => `${index === 0 ? "M" : "L"}${number(x)},${number(y)}` ).join(""); return `<path${common} d="${path}" vector-effect="non-scaling-stroke"/>`; } case "area": { const path = node.polygons !== void 0 ? polygonsPath(node.polygons) : node.path ?? pointsPath(node.points, true); const fillRule = node.polygons === void 0 ? "" : ' fill-rule="evenodd"'; return `<path${common} d="${path}"${fillRule} vector-effect="non-scaling-stroke"/>`; } case "dot": return `<circle${common} cx="${number(node.x)}" cy="${number(node.y)}" r="${number(node.radius)}"/>`; case "rect": return `<rect${common} x="${number(node.x)}" y="${number(node.y)}" width="${number(node.width)}" height="${number(node.height)}"${node.radius === void 0 ? "" : ` rx="${number(node.radius)}"`}/>`; case "label": { const transform = node.rotate === void 0 ? "" : ` transform="rotate(${number(node.rotate)} ${number(node.x)} ${number(node.y)})"`; const anchor = node.anchor ? ` text-anchor="${node.anchor}"` : ""; const baseline = node.baseline ? ` dominant-baseline="${node.baseline}"` : ""; const fontSize = node.fontSize === void 0 ? "" : ` font-size="${number(node.fontSize)}"`; const fontWeight = node.fontWeight === void 0 ? "" : ` font-weight="${number(node.fontWeight)}"`; return `<text${common} x="${number(node.x)}" y="${number(node.y)}"${anchor}${baseline}${transform}${fontSize}${fontWeight} font-family="inherit">${escapeText(node.text)}</text>`; } } } function polygonsPath(polygons) { return polygons.flatMap((polygon) => polygon).filter((ring) => ring.length > 0).map((ring) => pointsPath(ring, true)).join(""); } function pointsPath(points, close) { return `${points.map( ([x, y], index) => `${index === 0 ? "M" : "L"}${number(x)},${number(y)}` ).join("")}${close ? "Z" : ""}`; } function renderFocusGuideClip(node, idPrefix) { if (!node.clip) return void 0; const prefix = idPrefix.replaceAll(/[^a-zA-Z0-9_-]/g, ""); const id = `${prefix ? `${prefix}-` : ""}ts-chart-clip-${stableId(node.key)}`; return { attributes: ` clip-path="url(#${id})"`, content: `<defs data-ts-key="${escapeAttribute(`${node.key}:clip-defs`)}"><clipPath id="${id}"><rect x="${number(node.clip.x)}" y="${number(node.clip.y)}" width="${number(node.clip.width)}" height="${number(node.clip.height)}"/></clipPath></defs>` }; } function stableId(value) { let hash = 2166136261; for (let index = 0; index < value.length; index += 1) { hash = Math.imul(hash ^ value.charCodeAt(index), 16777619); } return (hash >>> 0).toString(36); } function renderCommon(node, hooks, idPrefix) { const key = ` data-ts-key="${escapeAttribute(node.key)}"`; const className = node.className ? ` class="${escapeAttribute(node.className)}"` : ""; const ariaHidden = node.ariaHidden ? ' aria-hidden="true"' : ""; return `${key}${className}${ariaHidden}${renderStyle(node.style, hooks, idPrefix)}`; } function renderStyle(style, hooks, idPrefix) { if (!style) return ""; const paint = (value) => value && hooks?.resolvePaint ? hooks.resolvePaint(value, idPrefix) : value; const attributes = [ ["fill", paint(style.fill)], ["fill-opacity", style.fillOpacity], ["stroke", paint(style.stroke)], ["stroke-opacity", style.strokeOpacity], ["stroke-width", style.strokeWidth], ["opacity", style.opacity], ["stroke-linecap", style.lineCap], ["stroke-linejoin", style.lineJoin], ["stroke-dasharray", style.strokeDasharray] ]; return attributes.filter((entry) => entry[1] != null).map( ([name, value]) => ` ${name}="${typeof value === "number" ? number(value) : escapeAttribute(value)}"` ).join(""); } function number(value) { return String(Math.round(value * 100) / 100); } function escapeText(value) { return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"); } function escapeAttribute(value) { return escapeText(value).replaceAll('"', "&quot;"); } export { renderChartSvgWithHooks, renderFocusGuideLayer, renderSceneNodes };