// Number, unit, percent, and date formatting helpers. (function (root) { const chartUtils = root.chartUtils; const schema = chartUtils.schema; const format = chartUtils.format; const formatted = ({ text, valueText, unitText = "", scaleText = "", rawValue }) => ({ text, valueText, unitText, scaleText, displayUnit: `${scaleText}${unitText}`, rawValue, }); format.appendUnit = (valueText, unit, options = {}) => { const unitText = schema.normalizeUnit(unit); if (!unitText) return String(valueText); return options.spaceBeforeUnit ? `${valueText} ${unitText}` : `${valueText}${unitText}`; }; format.number = (value, options = {}) => { const numericValue = +value; const unitText = schema.normalizeUnit(options.unit); if (!Number.isFinite(numericValue)) { const valueText = value == null ? "" : String(value); return formatted({ text: format.appendUnit(valueText, unitText, options), valueText, unitText, rawValue: value, }); } const formatter = options.format ? d3.format(options.format) : d3.format("~g"); const valueText = formatter(numericValue); return formatted({ text: format.appendUnit(valueText, unitText, options), valueText, unitText, rawValue: value, }); }; format.compactNumber = (value, options = {}) => { const numericValue = +value; const unitText = schema.normalizeUnit(options.unit); if (!Number.isFinite(numericValue)) return format.number(value, options); const absValue = Math.abs(numericValue); let scale = 1; let scaleText = ""; if (absValue >= 1000000000) { scale = 1000000000; scaleText = "B"; } else if (absValue >= 1000000) { scale = 1000000; scaleText = "M"; } else if (absValue >= 1000) { scale = 1000; scaleText = "K"; } const formatter = options.format ? d3.format(options.format) : d3.format("~g"); const valueText = formatter(numericValue / scale); return formatted({ text: format.appendUnit(`${valueText}${scaleText}`, unitText, options), valueText, unitText, scaleText, rawValue: value, }); }; format.auto = (value, options = {}) => format.compactNumber(value, options); format.autoText = (value, options = {}) => format.auto(value, options).text; format.scaledNumber = (value, options = {}) => { const numericValue = +value; const unitText = schema.normalizeUnit(options.unit); if (!Number.isFinite(numericValue)) return format.number(value, options); const thresholds = options.thresholds || [ { value: 1000000000, suffix: "B", format: options.format || "~g" }, { value: 1000000, suffix: "M", format: options.format || "~g" }, { value: 1000, suffix: "K", format: options.format || "~g" }, ]; const compareValue = options.absoluteThreshold ? Math.abs(numericValue) : numericValue; const threshold = thresholds.find(item => compareValue >= item.value); const prefix = options.prefix || ""; const suffix = options.suffix || ""; if (threshold) { const formatter = d3.format(threshold.format || options.format || "~g"); const valueText = formatter(numericValue / threshold.value); const scaleText = threshold.suffix || ""; return formatted({ text: format.appendUnit(`${prefix}${valueText}${scaleText}${suffix}`, unitText, options), valueText, unitText, scaleText, rawValue: value, }); } const valueText = options.baseFormat === null ? String(value) : d3.format(options.baseFormat || options.format || "~g")(numericValue); return formatted({ text: format.appendUnit(`${prefix}${valueText}${suffix}`, unitText, options), valueText, unitText, rawValue: value, }); }; format.scaledText = (value, options = {}) => format.scaledNumber(value, options).text; format.compactFixedText = (value, options = {}) => format.scaledText(value, { thresholds: [ { value: 1000000000, suffix: "B", format: options.largeFormat || ".1f" }, { value: 1000000, suffix: "M", format: options.largeFormat || ".1f" }, { value: 1000, suffix: "K", format: options.thousandFormat || ".0f" }, ], baseFormat: options.baseFormat === undefined ? ".0f" : options.baseFormat, ...options, }); format.adaptiveNumber = (value, options = {}) => { const numericValue = +value; const unitText = schema.normalizeUnit(options.unit); if (!Number.isFinite(numericValue)) return format.number(value, options); const ranges = options.ranges || []; const compareValue = options.absoluteThreshold ? Math.abs(numericValue) : numericValue; const selected = ranges.find(item => compareValue >= item.min); const resolveFormat = formatOption => { const resolved = typeof formatOption === "function" ? formatOption(numericValue) : formatOption; return resolved === undefined ? "~g" : resolved; }; const prefix = options.prefix || ""; const suffix = options.suffix || ""; if (selected) { const formatOption = resolveFormat(selected.format || options.format); const scaledValue = selected.divisor ? numericValue / selected.divisor : numericValue; const valueText = formatOption === null ? String(scaledValue) : d3.format(formatOption)(scaledValue); const scaleText = selected.suffix || ""; return formatted({ text: format.appendUnit(`${prefix}${valueText}${scaleText}${suffix}`, unitText, options), valueText, unitText, scaleText, rawValue: value, }); } const baseFormat = resolveFormat(options.baseFormat); const valueText = baseFormat === null ? String(value) : d3.format(baseFormat)(numericValue); return formatted({ text: format.appendUnit(`${prefix}${valueText}${suffix}`, unitText, options), valueText, unitText, rawValue: value, }); }; format.adaptiveText = (value, options = {}) => format.adaptiveNumber(value, options).text; format.fixed = (value, digits = 1, options = {}) => { const numericValue = +value; const unitText = schema.normalizeUnit(options.unit); const valueText = Number.isFinite(numericValue) ? numericValue.toFixed(digits) : String(value); return formatted({ text: format.appendUnit(valueText, unitText, options), valueText, unitText, rawValue: value, }); }; format.integer = (value, options = {}) => format.number(value, { ...options, format: ".0f" }); format.comma = (value, options = {}) => format.number(value, { ...options, format: "," }); format.withUnit = (value, channel, options = {}) => { const unit = options.unit != null ? options.unit : channel && channel.unit; const opts = { compact: true, ...options, unit }; return opts.compact ? format.compactNumber(value, opts) : format.number(value, opts); }; format.signed = (value, channel, options = {}) => { const result = format.withUnit(value, channel, options); if (+value > 0) { return { ...result, text: `+${result.text}`, valueText: `+${result.valueText}` }; } return result; }; format.percent = (value, options = {}) => { const digits = options.digits == null ? 1 : options.digits; const numericValue = +value; const valueText = Number.isFinite(numericValue) ? numericValue.toFixed(digits) : String(value); return formatted({ text: `${valueText}%`, valueText, unitText: "%", rawValue: value, }); }; format.percentage = (part, total, options = {}) => { const value = total ? (+part / +total) * 100 : 0; return format.percent(value, options); }; format.parseDate = value => { if (value instanceof Date) return value; if (typeof value === "number") return new Date(value, 0, 1); if (typeof value === "string") { const parts = value.split("-"); if (parts.length === 3) return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); if (parts.length === 2) return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, 1); if (parts.length === 1 && /^\d{4}$/.test(parts[0])) return new Date(parseInt(parts[0]), 0, 1); } return new Date(value); }; format.date = (value, options = {}) => { const date = format.parseDate(value); const output = options.output || "%Y"; const valueText = d3.timeFormat(output)(date); return formatted({ text: valueText, valueText, rawValue: value }); }; format.timeTickFormat = (dates, options = {}) => { const parsedDates = dates.map(format.parseDate); const extent = d3.extent(parsedDates); const daySpan = (extent[1] - extent[0]) / (1000 * 60 * 60 * 24); const yearSpan = daySpan / 365; const monthSpan = daySpan / 30; if (yearSpan > 2) return d => d3.timeFormat("%Y")(d); if (yearSpan > 1) { return d => `${d.getFullYear().toString().slice(-2)}Q${Math.floor(d.getMonth() / 3) + 1}`; } if (monthSpan > 6) return d => d3.timeFormat("%m %Y")(d); return d => d3.timeFormat("%d %m")(d); }; format.category = value => { const valueText = value == null ? "" : String(value); return formatted({ text: valueText, valueText, rawValue: value }); }; format.value = (value, channel, options = {}) => { if (channel && channel.type === "temporal") return format.date(value, options); if (channel && channel.type === "numerical") return format.withUnit(value, channel, options); return format.category(value, options); }; })(globalThis);