@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.
245 lines (244 loc) • 6.77 kB
JavaScript
import {
Fragment,
Teleport,
defineComponent,
h,
onBeforeUnmount,
onMounted,
onUpdated,
ref,
shallowRef,
useId
} from "vue";
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";
const ChartImplementation = defineComponent({
name: "TanStackChart",
inheritAttrs: false,
props: {
definition: { required: true },
ariaLabel: { type: String, required: true },
ariaDescription: String,
height: Number,
aspectRatio: Number,
width: Number,
initialWidth: Number,
tabIndex: Number,
idPrefix: String,
className: String,
renderSvg: Function,
measureText: Function,
onFocusChange: Function,
onFocusGroupChange: Function,
onSelect: Function,
onRender: Function
},
setup(componentProps, { attrs, slots }) {
const currentProps = () => ({
...componentProps,
class: attrs.class,
style: attrs.style
});
const props = currentProps();
const generatedId = useId();
const generatedPrefix = `ts-chart-${generatedId.replaceAll(/[^a-zA-Z0-9_-]/g, "")}`;
const currentIdPrefix = () => currentProps().idPrefix ?? generatedPrefix;
const tooltipBodyTarget = shallowRef(null);
const defaultTooltipBody = () => renderDefaultTooltipBody(tooltipBodyTarget.value?.content ?? "");
const handleTooltipBodyChange = (target) => {
tooltipBodyTarget.value = target;
};
let activeRenderSvg = props.renderSvg ?? renderChartSvg;
let renderer = createSvgChartRenderer(activeRenderSvg);
const resolveRenderer = (renderSvg) => {
if (renderSvg !== activeRenderSvg) {
activeRenderSvg = renderSvg;
renderer = createSvgChartRenderer(renderSvg);
}
return renderer;
};
const adapter = createChartRendererAdapter(
toHostOptions(
props,
currentIdPrefix(),
resolveRenderer,
slots.tooltipBody ? handleTooltipBodyChange : void 0
)
);
const initialMarkup = adapter.prerender();
const container = ref();
onMounted(() => {
if (!container.value) return;
adapter.update(
toHostOptions(
currentProps(),
currentIdPrefix(),
resolveRenderer,
slots.tooltipBody ? handleTooltipBodyChange : void 0
)
);
adapter.mount(container.value);
});
onUpdated(
() => adapter.update(
toHostOptions(
currentProps(),
currentIdPrefix(),
resolveRenderer,
slots.tooltipBody ? handleTooltipBodyChange : void 0
)
)
);
onBeforeUnmount(() => adapter.destroy());
return () => {
const props2 = currentProps();
const layout = resolveChartAdapterLayout(props2);
const target = tooltipBodyTarget.value;
return [
h(
"div",
{
class: ["ts-chart-host", props2.class],
style: [
{
position: "relative",
width: props2.width === void 0 ? "100%" : props2.width,
height: props2.height ?? (layout.aspectRatio === void 0 ? 320 : void 0),
aspectRatio: props2.height === void 0 ? layout.aspectRatio : void 0
},
props2.style
]
},
[
h("div", {
ref: container,
class: "ts-chart-surface",
style: { width: "100%", height: "100%" },
innerHTML: initialMarkup
})
]
),
target && slots.tooltipBody ? h(
Teleport,
{ to: target.element },
slots.tooltipBody({
points: target.points,
content: target.content,
pinned: target.pinned,
dismiss: target.dismiss,
defaultBody: defaultTooltipBody
})
) : null
];
};
}
});
const Chart = ChartImplementation;
function toHostOptions(props, idPrefix, resolveRenderer, onTooltipBodyChange) {
const {
class: _class,
style: _style,
renderSvg,
onRender,
...options
} = props;
return {
...options,
idPrefix,
renderer: resolveRenderer(renderSvg ?? renderChartSvg),
onRender: adaptOnRender(onRender),
onTooltipBodyChange
};
}
function adaptOnRender(onRender) {
if (!onRender) return void 0;
return (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.");
}
onRender({
container: context.container,
scene: context.scene,
svg,
interaction: context.interaction
});
};
}
function renderDefaultTooltipBody(content) {
if (typeof content === "string") {
return h(Fragment, null, [content]);
}
return h(Fragment, null, [
content.title ? h(
"div",
{
class: "ts-chart-tooltip__title",
style: {
display: "flex",
alignItems: "center",
gap: "0.4rem",
fontWeight: 650,
marginBottom: content.rows.length ? "0.3rem" : 0
}
},
[
content.color ? renderTooltipSwatch(content.color) : null,
content.title
]
) : null,
content.rows.length ? h(
"div",
{ class: "ts-chart-tooltip__rows", "aria-hidden": "true" },
content.rows.map(
(row) => h(
"div",
{
class: "ts-chart-tooltip__row",
style: {
display: "grid",
gridTemplateColumns: "0.55rem minmax(0,1fr) auto",
alignItems: "center",
columnGap: "0.4rem"
}
},
[
row.color ? renderTooltipSwatch(row.color) : h("span"),
h("span", row.label),
h(
"span",
{
style: {
textAlign: "right",
fontVariantNumeric: "tabular-nums",
whiteSpace: "nowrap"
}
},
row.value
)
]
)
)
) : null
]);
}
function renderTooltipSwatch(color) {
return h("span", {
class: "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
};