@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.
205 lines (204 loc) • 7.15 kB
JavaScript
const presentationProperties = [
"color",
"fill",
"fill-opacity",
"font-family",
"font-size",
"font-weight",
"opacity",
"stroke",
"stroke-opacity",
"stroke-width",
"stroke-dasharray",
"stop-color",
"stop-opacity"
];
function serializeChartSvg(target, options = {}) {
const svg = resolveSvg(target);
const clone = svg.cloneNode(true);
inlinePresentation(svg, clone);
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
if (!options.includeFocus) {
clone.querySelectorAll("[data-ts-focus-layer]").forEach((layer) => layer.remove());
}
const dimensions = svgDimensions(svg);
const width = options.width ?? (dimensions.width || svg.clientWidth);
const height = options.height ?? (dimensions.height || svg.clientHeight);
if (width > 0) clone.setAttribute("width", String(width));
if (height > 0) clone.setAttribute("height", String(height));
const Serializer = svg.ownerDocument.defaultView?.XMLSerializer ?? XMLSerializer;
return new Serializer().serializeToString(clone);
}
function downloadChartSvg(target, filename = "chart.svg", options) {
const contents = serializeChartSvg(target, options);
const BlobConstructor = target.ownerDocument.defaultView?.Blob ?? Blob;
downloadBlob(
target.ownerDocument,
new BlobConstructor([contents], { type: "image/svg+xml;charset=utf-8" }),
filename
);
}
async function renderChartImage(target, options = {}) {
const svg = findSvg(target);
const canvasSurface = svg ? null : findCanvasSurface(target);
if (!svg && !canvasSurface) {
throw new Error("Expected a TanStack Chart SVG or Canvas surface");
}
const source = svg ?? canvasSurface;
const document = source.ownerDocument;
const view = document.defaultView;
if (!view) throw new Error("Chart image export requires a browser document");
const dimensions = svg ? svgDimensions(svg) : canvasSurfaceDimensions(canvasSurface);
const width = options.width ?? dimensions.width;
const height = options.height ?? dimensions.height;
if (!(width > 0 && height > 0)) {
throw new Error("Chart image export requires non-zero dimensions");
}
const scale = Math.max(0.1, options.scale ?? 2);
const canvas = document.createElement("canvas");
canvas.width = Math.round(width * scale);
canvas.height = Math.round(height * scale);
const context = canvas.getContext("2d");
if (!context) throw new Error("Canvas 2D is unavailable");
context.scale(scale, scale);
if (options.background) {
context.fillStyle = options.background;
context.fillRect(0, 0, width, height);
}
if (svg) {
const serialized = serializeChartSvg(svg, {
...options,
width,
height
});
const url = view.URL.createObjectURL(
new view.Blob([serialized], { type: "image/svg+xml;charset=utf-8" })
);
try {
const image = await loadImage(view, url);
context.drawImage(image, 0, 0, width, height);
} finally {
view.URL.revokeObjectURL(url);
}
} else {
const base = canvasSurface.querySelector(
".ts-chart-canvas__base"
);
const background = canvasSurface.querySelector(
".ts-chart-canvas__background"
);
const focusUnder = canvasSurface.querySelector(
".ts-chart-canvas__focus-under"
);
const scene = canvasSurface.querySelector(
".ts-chart-canvas__scene"
);
if (!scene) throw new Error("Expected a Canvas chart scene layer");
if (!options.includeFocus && base) {
context.drawImage(base, 0, 0, width, height);
} else {
if (background) {
context.drawImage(background, 0, 0, width, height);
}
if (options.includeFocus && focusUnder) {
context.drawImage(focusUnder, 0, 0, width, height);
}
context.drawImage(scene, 0, 0, width, height);
const focus = canvasSurface.querySelector(
".ts-chart-canvas__focus"
);
if (options.includeFocus && focus) {
context.drawImage(focus, 0, 0, width, height);
}
}
}
return new Promise((resolve, reject) => {
canvas.toBlob(
(blob) => blob ? resolve(blob) : reject(new Error("Canvas export failed")),
options.type ?? "image/png",
options.quality
);
});
}
async function downloadChartImage(target, filename = "chart.png", options) {
downloadBlob(
target.ownerDocument,
await renderChartImage(target, options),
filename
);
}
function resolveSvg(target) {
const svg = findSvg(target);
if (!svg) throw new Error("Expected a TanStack Chart SVG");
return svg;
}
function findSvg(target) {
return target.localName === "svg" ? target : target.querySelector("svg.ts-chart");
}
function findCanvasSurface(target) {
return target.classList.contains("ts-chart-canvas") ? target : target.querySelector(".ts-chart-canvas");
}
function svgDimensions(svg) {
const values = (svg.getAttribute("viewBox") ?? "").trim().split(/\s+/).map(Number);
return {
width: Number.isFinite(values[2]) ? values[2] : 0,
height: Number.isFinite(values[3]) ? values[3] : 0
};
}
function canvasSurfaceDimensions(surface) {
const scene = surface.querySelector(
".ts-chart-canvas__scene"
);
const bounds = surface.getBoundingClientRect();
const pixelRatio = positiveNumber(surface.dataset.tsChartPixelRatio) || 1;
return {
width: positiveNumber(surface.dataset.tsChartWidth) || bounds.width || (scene ? scene.width / pixelRatio : 0),
height: positiveNumber(surface.dataset.tsChartHeight) || bounds.height || (scene ? scene.height / pixelRatio : 0)
};
}
function positiveNumber(value) {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number : 0;
}
function inlinePresentation(source, clone) {
const view = source.ownerDocument.defaultView;
if (view) {
const computed = view.getComputedStyle(source);
for (const property of presentationProperties) {
const authored = source.getAttribute(property) || source.getAttribute("style") || "";
if (property === "font-family" || authored.includes("var(") || authored.includes("currentColor")) {
const value = computed.getPropertyValue(property);
if (value) clone.setAttribute(property, value);
}
}
}
const sourceChildren = [...source.children];
const cloneChildren = [...clone.children];
sourceChildren.forEach((child, index) => {
const cloneChild = cloneChildren[index];
if (cloneChild) inlinePresentation(child, cloneChild);
});
}
function loadImage(view, source) {
return new Promise((resolve, reject) => {
const image = view.document.createElement("img");
image.onload = () => resolve(image);
image.onerror = () => reject(new Error("Unable to decode chart SVG"));
image.src = source;
});
}
function downloadBlob(document, blob, filename) {
const Url = document.defaultView?.URL ?? URL;
const url = Url.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
queueMicrotask(() => Url.revokeObjectURL(url));
}
export {
downloadChartImage,
downloadChartSvg,
renderChartImage,
serializeChartSvg
};