Spaces:
Sleeping
Sleeping
| // 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); | |