Ray1ee01's picture
Add runnable ChartPipeline Space
216c0a4 verified
Raw
History Blame Contribute Delete
5.63 kB
// Text sizing, measuring, and truncation helpers.
(function (root) {
const chartUtils = root.chartUtils;
const text = chartUtils.text;
text.font = (options = {}) => {
const fontWeight = options.fontWeight || options.weight || "normal";
const fontSize = text.fontSize(options.fontSize || options.size || 12);
const fontFamily = options.fontFamily || options.family || "Arial";
return `${fontWeight} ${fontSize}px ${fontFamily}`;
};
text.fontSize = value => {
if (typeof value === "number") return value;
const parsed = parseFloat(value);
return Number.isFinite(parsed) ? parsed : 12;
};
text.estimate = (value, options = {}) => {
const valueText = value == null ? "" : String(value);
const fontSize = text.fontSize(options.fontSize || options.size || 12);
const factor = options.factor == null ? 0.6 : options.factor;
return {
width: valueText.length * fontSize * factor,
height: fontSize * 1.2,
};
};
text.measure = (context, value, options = {}) => {
const valueText = value == null ? "" : String(value);
const fontSize = text.fontSize(options.fontSize || options.size || 12);
const fontFamily = options.fontFamily || options.family || null;
const fontWeight = options.fontWeight || options.weight || "normal";
if (context && typeof context.append === "function") {
const node = context.append("text")
.attr("visibility", "hidden")
.attr("x", -9999)
.attr("y", -9999)
.style("font-size", `${fontSize}px`)
.style("font-weight", fontWeight)
.text(valueText);
if (fontFamily) node.style("font-family", fontFamily);
const element = node.node();
const width = element.getComputedTextLength ? element.getComputedTextLength() : element.getBBox().width;
const height = element.getBBox().height || fontSize * 1.2;
node.remove();
return { width, height };
}
if (typeof document !== "undefined") {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.font = text.font({ fontSize, fontFamily, fontWeight });
return {
width: ctx.measureText(valueText).width,
height: fontSize * 1.2,
};
}
return text.estimate(valueText, options);
};
text.contextSize = (context, value, options = {}) => {
const valueText = value == null ? "" : String(value);
const fontSize = text.fontSize(options.fontSize || options.size || 12);
if (context && typeof context.measureText === "function") {
return {
width: context.measureText(valueText).width,
height: fontSize * 1.2,
};
}
return text.measure(null, valueText, options);
};
text.contextWidth = (context, value, options = {}) => text.contextSize(context, value, options).width;
text.nodeSize = (node, options = {}) => {
if (!node) return { width: 0, height: 0 };
const method = options.method || "auto";
const canUseComputed = typeof node.getComputedTextLength === "function";
const canUseBBox = typeof node.getBBox === "function";
let bbox = null;
let width = null;
const readBBox = () => {
if (!canUseBBox) return null;
try {
return node.getBBox();
} catch (error) {
return null;
}
};
if ((method === "computed" || method === "auto") && canUseComputed) {
try {
width = node.getComputedTextLength();
} catch (error) {
width = null;
}
}
if ((method === "bbox" || !Number.isFinite(width)) && canUseBBox) {
bbox = readBBox();
if (bbox && Number.isFinite(bbox.width)) {
width = bbox.width;
}
}
if (!Number.isFinite(width)) {
width = text.estimate(node.textContent || "", options).width;
}
if (!bbox && canUseBBox) {
bbox = readBBox();
}
const fallbackHeight = text.fontSize(options.fontSize || options.size || 12) * 1.2;
const height = bbox && Number.isFinite(bbox.height) ? bbox.height : fallbackHeight;
const x = bbox && Number.isFinite(bbox.x) ? bbox.x : 0;
const y = bbox && Number.isFinite(bbox.y) ? bbox.y : 0;
return { width, height, x, y };
};
text.nodeWidth = (node, options = {}) => text.nodeSize(node, options).width;
text.truncate = (value, maxWidth, options = {}) => {
const valueText = value == null ? "" : String(value);
const ellipsis = options.ellipsis == null ? "..." : options.ellipsis;
const measure = candidate => text.measure(options.context, candidate, options).width;
if (measure(valueText) <= maxWidth) return valueText;
if (maxWidth <= 0 || measure(ellipsis) > maxWidth) return "";
let low = 0;
let high = valueText.length;
while (low < high) {
const mid = Math.ceil((low + high) / 2);
if (measure(valueText.slice(0, mid) + ellipsis) <= maxWidth) {
low = mid;
} else {
high = mid - 1;
}
}
return valueText.slice(0, low) + ellipsis;
};
})(globalThis);