@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.
130 lines (129 loc) • 5.18 kB
JavaScript
import { estimateSceneText } from "./guide-layout.js";
function createDomTextMeasurer(container) {
const view = container.ownerDocument.defaultView;
const CanvasContext = view?.CanvasRenderingContext2D;
const context = CanvasContext ? container.ownerDocument.createElement("canvas").getContext("2d") : null;
let style = readFontStyle();
let signature = fontSignature(style);
const cache = /* @__PURE__ */ new Map();
return {
measureText(text, options) {
if (!context) return estimateSceneText(text, options);
const key = `${signature}\0${options.fontSize}\0${options.fontWeight ?? ""}\0${options.fontFamily}\0${options.fontStyle}\0${options.fontStretch}\0${options.letterSpacing}\0${options.direction}\0${options.locale ?? ""}\0${options.fontScale}\0${options.anchor}\0${options.baseline}\0${text}`;
const cached = cache.get(key);
if (cached) return cached;
configureContext(context, style.weight, options);
const measured = context.measureText(text);
const metrics = paintedBounds(measured, options);
cache.set(key, metrics);
return metrics;
},
typography() {
return {
fontFamily: style.family,
fontStyle: style.style,
fontStretch: style.stretch,
letterSpacing: style.letterSpacing,
direction: style.direction
};
},
refresh() {
const nextStyle = readFontStyle();
const nextSignature = fontSignature(nextStyle);
if (nextSignature === signature) return false;
style = nextStyle;
signature = nextSignature;
cache.clear();
return true;
},
invalidate() {
cache.clear();
}
};
function readFontStyle() {
const computed = view?.getComputedStyle(container);
return {
family: computed?.fontFamily || "sans-serif",
style: computed?.fontStyle || "normal",
stretch: normalizeFontStretch(computed?.fontStretch),
weight: computed?.fontWeight || "400",
direction: computed?.direction === "rtl" ? "rtl" : computed?.direction === "ltr" ? "ltr" : "inherit",
letterSpacing: finiteCssPixels(computed?.letterSpacing)
};
}
}
function configureContext(context, defaultWeight, options) {
const fontScale = positiveFinite(options.fontScale, 1);
const fontSize = options.fontSize * fontScale;
const weight = options.fontWeight ?? defaultWeight;
context.font = [
options.fontStyle,
weight,
`${fontSize}px`,
options.fontFamily
].join(" ");
if ("fontStretch" in context) {
context.fontStretch = normalizeFontStretch(options.fontStretch);
}
context.textAlign = options.anchor === "middle" ? "center" : options.anchor;
context.textBaseline = options.baseline === "auto" ? "alphabetic" : options.baseline;
context.direction = options.direction;
if ("letterSpacing" in context) {
context.letterSpacing = `${options.letterSpacing * fontScale}px`;
}
}
function paintedBounds(measured, options) {
const fontSize = options.fontSize * positiveFinite(options.fontScale, 1);
const left = measured.actualBoundingBoxLeft;
const right = measured.actualBoundingBoxRight;
const ascent = measured.actualBoundingBoxAscent;
const descent = measured.actualBoundingBoxDescent;
if ([left, right, ascent, descent].every((value) => Number.isFinite(value)) && (left + right > 0 || measured.width === 0) && (ascent + descent > 0 || measured.width === 0)) {
return {
x: -left,
y: -ascent,
width: left + right,
height: ascent + descent
};
}
const width = Number.isFinite(measured.width) ? Math.max(0, measured.width) : 0;
const x = options.anchor === "middle" ? -width / 2 : options.anchor === "end" ? -width : 0;
const y = options.baseline === "middle" ? -fontSize / 2 : options.baseline === "hanging" ? 0 : -fontSize * 0.8;
return { x, y, width, height: fontSize };
}
function fontSignature(style) {
return [
style.family,
style.style,
style.stretch,
style.weight,
style.direction,
style.letterSpacing
].join("\0");
}
function normalizeFontStretch(value) {
if (value === "ultra-condensed" || value === "extra-condensed" || value === "condensed" || value === "semi-condensed" || value === "normal" || value === "semi-expanded" || value === "expanded" || value === "extra-expanded" || value === "ultra-expanded") {
return value;
}
const percentage = Number.parseFloat(value ?? "");
if (!Number.isFinite(percentage)) return "normal";
if (percentage <= 50) return "ultra-condensed";
if (percentage <= 62.5) return "extra-condensed";
if (percentage <= 75) return "condensed";
if (percentage <= 87.5) return "semi-condensed";
if (percentage < 112.5) return "normal";
if (percentage < 125) return "semi-expanded";
if (percentage < 150) return "expanded";
if (percentage < 200) return "extra-expanded";
return "ultra-expanded";
}
function finiteCssPixels(value) {
const parsed = Number.parseFloat(value ?? "");
return Number.isFinite(parsed) ? parsed : 0;
}
function positiveFinite(value, fallback) {
return value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback;
}
export {
createDomTextMeasurer
};