UNPKG

@tanstack/charts

Version:

<div align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/api/readme/charts.png?theme=dark" /> <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/

208 lines (207 loc) 8.21 kB
const defaultFontSize = 16; const defaultFontWeight = 400; const defaultOuterInset = 4; const defaultTypography = { fontFamily: "sans-serif", fontStyle: "normal", fontStretch: "normal", letterSpacing: 0, direction: "inherit", fontScale: 1 }; function physicalTextAnchor(side, direction) { if (side === "middle") return "middle"; const startsAtLeft = direction !== "rtl"; if (side === "left") return startsAtLeft ? "start" : "end"; return startsAtLeft ? "end" : "start"; } function logicalTextAnchorOffset(width, anchor, direction) { if (anchor === "middle") return -width / 2; const startsAtLeft = direction !== "rtl"; return anchor === "start" === startsAtLeft ? 0 : -width; } function estimateSceneText(text, style) { const fontScale = finitePositive(style.fontScale, 1); const fontSize = finiteNonNegative(style.fontSize, defaultFontSize) * fontScale; const fontWeight = finiteNonNegative(style.fontWeight, defaultFontWeight); const letterSpacing = finiteNumber(style.letterSpacing, 0) * fontScale; if (!text || fontSize === 0) { return { x: 0, y: 0, width: 0, height: 0 }; } let emWidth = 0; for (const character of text) { emWidth += estimateCharacterWidth(character); } const clampedWeight = Math.min(900, Math.max(100, fontWeight)); const weightFactor = 1 + (clampedWeight - 400) / 12500; const width = Math.max( 0, emWidth * fontSize * weightFactor + Math.max(0, Array.from(text).length - 1) * letterSpacing ); const height = fontSize; const x = logicalTextAnchorOffset(width, style.anchor, style.direction); const y = style.baseline === "middle" ? -height / 2 : style.baseline === "hanging" ? 0 : -fontSize * 0.8; return { x, y, width, height }; } function measureSceneLabelBounds(label, measureText = estimateSceneText) { const fontSize = finiteNonNegative(label.fontSize, defaultFontSize); const anchor = label.anchor ?? "start"; const baseline = label.baseline ?? "auto"; const measured = label.text.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : measureText(label.text, { fontSize, fontWeight: label.fontWeight, ...defaultTypography, anchor, baseline }); const x = finiteNumber(measured.x, 0); const y = finiteNumber(measured.y, 0); const width = finiteNonNegative(measured.width, 0); const height = finiteNonNegative(measured.height, 0); const bounds = { x: label.x + x, y: label.y + y, width, height }; if (!label.rotate) { return bounds; } return rotateBounds(bounds, label.x, label.y, label.rotate); } function withChartTextTypography(measureText = estimateSceneText, typography = {}) { const resolved = { ...defaultTypography, ...typography, fontFamily: typography.fontFamily || defaultTypography.fontFamily, fontStyle: typography.fontStyle || defaultTypography.fontStyle, fontStretch: typography.fontStretch || defaultTypography.fontStretch, letterSpacing: finiteNumber(typography.letterSpacing, 0), fontScale: finitePositive(typography.fontScale, 1) }; return (text, options) => measureText(text, { ...options, ...resolved }); } function resolveGuideMargins(axes, plot, options = {}) { const inset = finiteNonNegative(options.inset, defaultOuterInset); const measureText = options.measureText ?? estimateSceneText; let top = inset; let right = inset; let bottom = inset; let left = inset; visitLabels(axes, 0, 0, (label, translateX, translateY) => { if (!label.text) return; const bounds = measureSceneLabelBounds(label, measureText); const boundsLeft = bounds.x + translateX; const boundsTop = bounds.y + translateY; const boundsRight = boundsLeft + bounds.width; const boundsBottom = boundsTop + bounds.height; const plotRight = plot.x + plot.width; const plotBottom = plot.y + plot.height; top = Math.max(top, plot.y - boundsTop + inset); right = Math.max(right, boundsRight - plotRight + inset); bottom = Math.max(bottom, boundsBottom - plotBottom + inset); left = Math.max(left, plot.x - boundsLeft + inset); }); const strokeMargin = { top, right, bottom, left }; includeGuideStrokeMargins(strokeMargin, axes, plot); return strokeMargin; } function includeGuideStrokeMargins(margin, guides, plot) { visitRules(guides, 0, 0, (rule, translateX, translateY) => { const style = rule.style; const extendsGeometry = style?.strokeWidth !== void 0 || style?.lineCap !== void 0 && style.lineCap !== "butt"; if (!extendsGeometry || style?.stroke === "none") return; const strokeWidth = style?.strokeWidth ?? 1; if (!Number.isFinite(strokeWidth) || strokeWidth <= 0) return; const halfWidth = strokeWidth / 2; const x1 = rule.x1 + translateX; const x2 = rule.x2 + translateX; const y1 = rule.y1 + translateY; const y2 = rule.y2 + translateY; const dx = x2 - x1; const dy = y2 - y1; const length = Math.hypot(dx, dy); const unitX = length > 0 ? dx / length : 0; const unitY = length > 0 ? dy / length : 0; const cap = style?.lineCap && style.lineCap !== "butt" ? halfWidth : 0; const extendX = Math.abs(unitY) * halfWidth + Math.abs(unitX) * cap; const extendY = Math.abs(unitX) * halfWidth + Math.abs(unitY) * cap; const left = Math.min(x1, x2) - extendX; const right = Math.max(x1, x2) + extendX; const top = Math.min(y1, y2) - extendY; const bottom = Math.max(y1, y2) + extendY; margin.top = Math.max(margin.top, plot.y - top); margin.right = Math.max(margin.right, right - plot.x - plot.width); margin.bottom = Math.max(margin.bottom, bottom - plot.y - plot.height); margin.left = Math.max(margin.left, plot.x - left); }); } function visitLabels(node, translateX, translateY, visit) { if (node.kind === "label") { visit(node, translateX, translateY); return; } if (node.kind !== "group") return; const childTranslateX = translateX + (node.translateX ?? 0); const childTranslateY = translateY + (node.translateY ?? 0); for (const child of node.children) { visitLabels(child, childTranslateX, childTranslateY, visit); } } function visitRules(node, translateX, translateY, visit) { if (node.kind === "rule") { visit(node, translateX, translateY); return; } if (node.kind !== "group") return; const childTranslateX = translateX + (node.translateX ?? 0); const childTranslateY = translateY + (node.translateY ?? 0); for (const child of node.children) { visitRules(child, childTranslateX, childTranslateY, visit); } } function rotateBounds(bounds, originX, originY, degrees) { const radians = degrees * Math.PI / 180; const cosine = Math.cos(radians); const sine = Math.sin(radians); const centerX = bounds.x + bounds.width / 2 - originX; const centerY = bounds.y + bounds.height / 2 - originY; const width = Math.abs(bounds.width * cosine) + Math.abs(bounds.height * sine); const height = Math.abs(bounds.width * sine) + Math.abs(bounds.height * cosine); const rotatedCenterX = centerX * cosine - centerY * sine + originX; const rotatedCenterY = centerX * sine + centerY * cosine + originY; return { x: rotatedCenterX - width / 2, y: rotatedCenterY - height / 2, width, height }; } function estimateCharacterWidth(character) { if (/\s/u.test(character)) return 0.33; if (/[\u0300-\u036f]/u.test(character)) return 0; if (/[ilI1|!.,:;'`]/u.test(character)) return 0.28; if (/[mwMW@#%&]/u.test(character)) return 0.9; if (/[A-Z]/u.test(character)) return 0.64; if (/[0-9]/u.test(character)) return 0.56; if (character.codePointAt(0) > 127) return 1; return 0.54; } function finiteNonNegative(value, fallback) { return value !== void 0 && Number.isFinite(value) && value >= 0 ? value : fallback; } function finiteNumber(value, fallback) { return value !== void 0 && Number.isFinite(value) ? value : fallback; } function finitePositive(value, fallback) { return value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback; } export { estimateSceneText, includeGuideStrokeMargins, logicalTextAnchorOffset, measureSceneLabelBounds, physicalTextAnchor, resolveGuideMargins, withChartTextTypography };