@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.
180 lines (179 loc) • 7.22 kB
JavaScript
import { resolveChartAdapterLayout } from "@tanstack/charts/adapter";
import { createChartRendererAdapter } from "@tanstack/charts/adapter/renderer";
import { renderChartSvg } from "@tanstack/charts/svg";
import { createSvgChartRenderer } from "@tanstack/charts/svg/renderer";
let nextChartId = 0;
function charts(Alpine) {
Alpine.directive(
"chart",
(element, { expression }, { evaluateLater, effect, cleanup }) => {
const evaluate = evaluateLater(expression);
const surface = element.ownerDocument.createElement("div");
surface.className = "ts-chart-surface";
surface.style.width = "100%";
surface.style.height = "100%";
const hadHostClass = element.classList.contains("ts-chart-host");
element.classList.add("ts-chart-host");
element.append(surface);
const initialStyle = {
position: element.style.position,
width: element.style.width,
height: element.style.height,
aspectRatio: element.style.aspectRatio
};
let adapter;
let activeRenderSvg;
let renderer = createSvgChartRenderer(renderChartSvg);
let activeRenderTooltipBody;
let onTooltipBodyChange;
const generatedId = `ts-chart-alpine-${++nextChartId}`;
effect(() => {
evaluate((value) => {
const options = value;
if (options.renderSvg !== activeRenderSvg) {
activeRenderSvg = options.renderSvg;
renderer = createSvgChartRenderer(
options.renderSvg ?? renderChartSvg
);
}
if (options.renderTooltipBody !== activeRenderTooltipBody) {
activeRenderTooltipBody = options.renderTooltipBody;
onTooltipBodyChange = options.renderTooltipBody ? createTooltipBodyChangeHandler(options.renderTooltipBody) : void 0;
}
const hostOptions = {
renderer,
definition: options.definition,
ariaLabel: options.ariaLabel,
ariaDescription: options.ariaDescription,
className: options.className,
tabIndex: options.tabIndex,
idPrefix: options.idPrefix ?? generatedId,
height: options.height,
aspectRatio: options.aspectRatio,
width: options.width,
initialWidth: options.initialWidth,
measureText: options.measureText,
onFocusChange: options.onFocusChange,
onFocusGroupChange: options.onFocusGroupChange,
onSelect: options.onSelect,
onRender: options.onRender ? (context) => {
const svg = context.surface.element;
const SvgElement = context.container.ownerDocument.defaultView?.SVGSVGElement;
if (!SvgElement || !(svg instanceof SvgElement)) {
throw new TypeError("Expected the SVG chart surface.");
}
options.onRender?.({
container: context.container,
scene: context.scene,
svg,
interaction: context.interaction
});
} : void 0,
onTooltipBodyChange
};
applyLayout(element, options, initialStyle);
if (adapter) {
adapter.update(hostOptions);
} else {
adapter = createChartRendererAdapter(hostOptions);
adapter.mount(surface);
}
});
});
cleanup(() => {
adapter?.destroy();
surface.remove();
if (!hadHostClass) element.classList.remove("ts-chart-host");
Object.assign(element.style, initialStyle);
});
}
);
}
function createTooltipBodyChangeHandler(renderTooltipBody) {
let activeElement;
return (target) => {
if (activeElement) activeElement.replaceChildren();
activeElement = target?.element;
if (!target) return;
const defaultBody = createDefaultTooltipBody(
target.element.ownerDocument,
target.content
);
const content = renderTooltipBody({
points: target.points,
content: target.content,
defaultBody,
pinned: target.pinned,
dismiss: target.dismiss
});
const fragment = target.element.ownerDocument.createDocumentFragment();
appendTooltipBody(fragment, content);
target.element.replaceChildren(fragment);
};
}
function appendTooltipBody(parent, content) {
if (content === null || content === void 0) return;
if (Array.isArray(content)) {
for (const child of content) appendTooltipBody(parent, child);
return;
}
if (typeof content === "string" || typeof content === "number") {
parent.append(String(content));
return;
}
parent.append(content);
}
function createDefaultTooltipBody(document, content) {
const fragment = document.createDocumentFragment();
if (typeof content === "string") {
fragment.append(content);
return fragment;
}
if (content.title) {
const title = document.createElement("div");
title.className = "ts-chart-tooltip__title";
title.style.cssText = `display:flex;align-items:center;gap:.4rem;font-weight:650;margin-bottom:${content.rows.length ? ".3rem" : "0"}`;
if (content.color)
title.append(createTooltipSwatch(document, content.color));
title.append(content.title);
fragment.append(title);
}
if (content.rows.length) {
const rows = document.createElement("div");
rows.className = "ts-chart-tooltip__rows";
rows.setAttribute("aria-hidden", "true");
for (const row of content.rows) {
const line = document.createElement("div");
line.className = "ts-chart-tooltip__row";
line.style.cssText = "display:grid;grid-template-columns:.55rem minmax(0,1fr) auto;align-items:center;column-gap:.4rem";
const swatch = row.color ? createTooltipSwatch(document, row.color) : document.createElement("span");
const label = document.createElement("span");
label.textContent = row.label;
const value = document.createElement("span");
value.textContent = row.value;
value.style.cssText = "text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap";
line.append(swatch, label, value);
rows.append(line);
}
fragment.append(rows);
}
return fragment;
}
function createTooltipSwatch(document, color) {
const swatch = document.createElement("span");
swatch.className = "ts-chart-tooltip__swatch";
swatch.setAttribute("aria-hidden", "true");
swatch.style.cssText = "display:block;width:.55rem;height:.55rem;border-radius:.15rem;box-shadow:inset 0 0 0 1px rgb(0 0 0/.12)";
swatch.style.background = color;
return swatch;
}
function applyLayout(element, options, initial) {
const layout = resolveChartAdapterLayout(options);
element.style.position = initial.position || "relative";
element.style.width = options.width === void 0 ? initial.width || "100%" : `${options.width}px`;
element.style.height = options.height !== void 0 ? `${options.height}px` : layout.aspectRatio === void 0 ? initial.height || "320px" : initial.height;
element.style.aspectRatio = options.height === void 0 && layout.aspectRatio !== void 0 ? String(layout.aspectRatio) : initial.aspectRatio;
}
export {
charts
};