@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.
183 lines (182 loc) • 5.83 kB
JavaScript
import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
import { createPortal } from "preact/compat";
import { useId, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { createChartRendererAdapter } from "@tanstack/charts/adapter/renderer";
import { renderChartSvg } from "@tanstack/charts/svg";
import { createSvgChartRenderer } from "@tanstack/charts/svg/renderer";
function Chart(props) {
const generatedId = useId();
const idPrefix = props.idPrefix ?? `ts-chart-${generatedId.replaceAll(/[^a-zA-Z0-9_-]/g, "")}`;
const renderer = useMemo(
() => createSvgChartRenderer(
props.renderSvg ?? renderChartSvg
),
[props.renderSvg]
);
const [tooltipBodyTarget, setTooltipBodyTarget] = useState(null);
const options = useMemo(
() => toHostOptions(props, idPrefix, renderer, setTooltipBodyTarget),
[props, idPrefix, renderer]
);
const adapterRef = useRef(null);
adapterRef.current ??= createChartRendererAdapter(options);
const adapter = adapterRef.current;
const initialMarkupRef = useRef(null);
initialMarkupRef.current ??= adapter.prerender();
const containerRef = useRef(null);
const aspectRatio = resolveAspectRatio(props.aspectRatio);
useLayoutEffect(() => {
const container = containerRef.current;
if (!container) return;
adapter.mount(container);
return () => adapter.destroy();
}, []);
useLayoutEffect(() => {
adapter.update(options);
}, [adapter, options]);
const tooltipPortal = props.renderTooltipBody && tooltipBodyTarget ? createPortal(
props.renderTooltipBody({
points: tooltipBodyTarget.points,
content: tooltipBodyTarget.content,
defaultBody: /* @__PURE__ */ jsx(DefaultTooltipBody, { content: tooltipBodyTarget.content }),
pinned: tooltipBodyTarget.pinned,
dismiss: tooltipBodyTarget.dismiss
}),
tooltipBodyTarget.element
) : null;
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
"div",
{
className: props.className ? `ts-chart-host ${props.className}` : "ts-chart-host",
style: hostStyle(props, aspectRatio),
children: /* @__PURE__ */ jsx(
"div",
{
ref: containerRef,
className: "ts-chart-surface",
style: { width: "100%", height: "100%" },
dangerouslySetInnerHTML: { __html: initialMarkupRef.current }
}
)
}
),
tooltipPortal
] });
}
function resolveAspectRatio(aspectRatio) {
return typeof aspectRatio === "number" && Number.isFinite(aspectRatio) && aspectRatio > 0 ? aspectRatio : void 0;
}
function hostStyle(props, aspectRatio) {
return Object.assign(
{
position: "relative",
width: props.width === void 0 ? "100%" : props.width,
height: props.height ?? (aspectRatio === void 0 ? 320 : void 0),
aspectRatio: props.height === void 0 ? aspectRatio : void 0
},
props.style
);
}
function toHostOptions(props, idPrefix, renderer, onTooltipBodyChange) {
const {
className: _className,
style: _style,
renderSvg: _renderSvg,
renderTooltipBody,
onRender,
...options
} = props;
const handleRender = onRender ? (context) => {
const element = context.surface.element;
const SvgElement = context.container.ownerDocument.defaultView?.SVGSVGElement;
if (!SvgElement || !(element instanceof SvgElement)) {
throw new TypeError("Expected the SVG chart surface.");
}
onRender({
container: context.container,
scene: context.scene,
svg: element,
interaction: context.interaction
});
} : void 0;
return {
...options,
idPrefix,
renderer,
onRender: handleRender,
onTooltipBodyChange: renderTooltipBody ? onTooltipBodyChange : void 0
};
}
function DefaultTooltipBody({
content
}) {
if (typeof content === "string") return content;
return /* @__PURE__ */ jsxs(Fragment, { children: [
content.title ? /* @__PURE__ */ jsxs(
"div",
{
className: "ts-chart-tooltip__title",
style: {
display: "flex",
alignItems: "center",
gap: "0.4rem",
fontWeight: 650,
marginBottom: content.rows.length ? "0.3rem" : 0
},
children: [
content.color ? /* @__PURE__ */ jsx(TooltipSwatch, { color: content.color }) : null,
content.title
]
}
) : null,
content.rows.length ? /* @__PURE__ */ jsx("div", { className: "ts-chart-tooltip__rows", "aria-hidden": "true", children: content.rows.map((row, index) => /* @__PURE__ */ jsxs(
"div",
{
className: "ts-chart-tooltip__row",
style: {
display: "grid",
gridTemplateColumns: "0.55rem minmax(0,1fr) auto",
alignItems: "center",
columnGap: "0.4rem"
},
children: [
row.color ? /* @__PURE__ */ jsx(TooltipSwatch, { color: row.color }) : /* @__PURE__ */ jsx("span", {}),
/* @__PURE__ */ jsx("span", { children: row.label }),
/* @__PURE__ */ jsx(
"span",
{
style: {
textAlign: "right",
fontVariantNumeric: "tabular-nums",
whiteSpace: "nowrap"
},
children: row.value
}
)
]
},
`${row.label}\0${index}`
)) }) : null
] });
}
function TooltipSwatch({ color }) {
return /* @__PURE__ */ jsx(
"span",
{
className: "ts-chart-tooltip__swatch",
"aria-hidden": "true",
style: {
display: "block",
width: "0.55rem",
height: "0.55rem",
borderRadius: "0.15rem",
boxShadow: "inset 0 0 0 1px rgb(0 0 0/.12)",
background: color
}
}
);
}
export {
Chart
};