@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.
207 lines (206 loc) • 6.57 kB
JavaScript
import { LitElement, html, nothing, render as renderLit } from "lit";
import { ref } from "lit/directives/ref.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
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;
class Chart extends LitElement {
static properties = {
options: { attribute: false }
};
adapter;
activeRenderSvg;
renderer;
tooltipBodyTarget;
idPrefix = `ts-chart-lit-${++nextChartId}`;
initialMarkup = "";
mounted = false;
createRenderRoot() {
return this;
}
willUpdate(changed) {
if (!changed.has("options") || !this.options) return;
const options = toHostOptions(
this.options,
this.options.idPrefix ?? this.idPrefix,
this.resolveRenderer(this.options.renderSvg ?? renderChartSvg),
this.options.renderTooltipBody ? this.handleTooltipBodyChange : void 0
);
if (!this.adapter) {
this.adapter = createChartRendererAdapter(options);
this.initialMarkup = this.adapter.prerender();
} else {
this.adapter.update(options);
}
}
firstUpdated() {
this.mountAdapter();
}
updated(changed) {
if (changed.has("options") && this.tooltipBodyTarget) {
this.renderTooltipBody(this.tooltipBodyTarget);
}
}
connectedCallback() {
super.connectedCallback();
if (this.hasUpdated && !this.mounted) {
void this.updateComplete.then(() => this.mountAdapter());
}
}
disconnectedCallback() {
this.mounted = false;
this.adapter?.destroy();
this.clearTooltipBody();
super.disconnectedCallback();
}
render() {
if (!this.options) return void 0;
const layout = resolveChartAdapterLayout(this.options);
const outerStyle = [
"display:block",
"position:relative",
`width:${this.options.width === void 0 ? "100%" : `${this.options.width}px`}`,
this.options.height !== void 0 ? `height:${this.options.height}px` : layout.aspectRatio === void 0 ? "height:320px" : `aspect-ratio:${layout.aspectRatio}`,
this.options.style
].filter(Boolean).join(";");
return html`
<div
class=${this.options.class ? `ts-chart-host ${this.options.class}` : "ts-chart-host"}
style=${outerStyle}
>
<div
class="ts-chart-surface"
style="width:100%;height:100%"
data-chart-surface
>
${unsafeHTML(this.initialMarkup)}
</div>
</div>
`;
}
resolveRenderer(renderSvg) {
if (!this.renderer || renderSvg !== this.activeRenderSvg) {
this.activeRenderSvg = renderSvg;
this.renderer = createSvgChartRenderer(renderSvg);
}
return this.renderer;
}
handleTooltipBodyChange = (target) => {
if (!target) {
this.clearTooltipBody();
return;
}
if (this.tooltipBodyTarget && this.tooltipBodyTarget.element !== target.element) {
renderLit(nothing, this.tooltipBodyTarget.element);
}
this.tooltipBodyTarget = target;
this.renderTooltipBody(target);
};
renderTooltipBody(target) {
const renderTooltipBody = this.options.renderTooltipBody;
if (!renderTooltipBody) {
renderLit(nothing, target.element);
return;
}
renderLit(
renderTooltipBody({
points: target.points,
content: target.content,
defaultBody: renderDefaultTooltipBody(target.content),
pinned: target.pinned,
dismiss: target.dismiss
}),
target.element
);
}
clearTooltipBody() {
if (!this.tooltipBodyTarget) return;
renderLit(nothing, this.tooltipBodyTarget.element);
this.tooltipBodyTarget = void 0;
}
mountAdapter() {
if (this.mounted) return;
const container = this.querySelector("[data-chart-surface]");
if (!container || !this.adapter) return;
this.adapter.mount(container);
this.mounted = true;
}
}
function defineChartElement(tagName = "tanstack-chart") {
if (!customElements.get(tagName)) {
customElements.define(tagName, Chart);
}
}
function toHostOptions(props, idPrefix, renderer, onTooltipBodyChange) {
const {
class: _class,
style: _style,
renderSvg: _renderSvg,
renderTooltipBody: _renderTooltipBody,
onRender,
...options
} = props;
return {
...options,
idPrefix,
renderer,
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 html`${content}`;
const title = content.title ? html`<div
class="ts-chart-tooltip__title"
style=${`display:flex;align-items:center;gap:0.4rem;font-weight:650;margin-bottom:${content.rows.length ? "0.3rem" : "0"}`}
>
${content.color ? renderTooltipSwatch(content.color) : nothing}${content.title}
</div>` : nothing;
const rows = content.rows.length ? html`<div class="ts-chart-tooltip__rows" aria-hidden="true">
${content.rows.map(
(row) => html`<div
class="ts-chart-tooltip__row"
style="display:grid;grid-template-columns:0.55rem minmax(0,1fr) auto;align-items:center;column-gap:0.4rem"
>
${row.color ? renderTooltipSwatch(row.color) : html`<span></span>`}<span>${row.label}</span
><span
style="text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap"
>${row.value}</span
>
</div>`
)}
</div>` : nothing;
return html`${title}${rows}`;
}
function renderTooltipSwatch(color) {
return html`<span
${ref((element) => {
if (element) element.style.background = color;
})}
class="ts-chart-tooltip__swatch"
aria-hidden="true"
style="display:block;width:0.55rem;height:0.55rem;border-radius:0.15rem;box-shadow:inset 0 0 0 1px rgb(0 0 0/.12)"
></span>`;
}
export {
Chart,
defineChartElement
};