Ray1ee01's picture
Add runnable ChartPipeline Space
216c0a4 verified
Raw
History Blame Contribute Delete
10.6 kB
// 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);