2026-08-05 10:58:40 +08:00
|
|
|
|
import type { WaveformXAxisLabelFormatter, WaveformXAxisLabelKind } from '../types'
|
|
|
|
|
|
|
2026-07-20 10:21:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 时间单位类型
|
|
|
|
|
|
*/
|
|
|
|
|
|
export type TimeUnit = 'ms' | 's'
|
|
|
|
|
|
|
2026-07-20 15:35:19 +08:00
|
|
|
|
export interface ScientificAxisLabelOptions {
|
2026-08-04 11:28:42 +08:00
|
|
|
|
/** @deprecated Y-axis labels always use five significant digits before localization. */
|
2026-07-20 10:21:30 +08:00
|
|
|
|
precision?: number
|
|
|
|
|
|
axisMin?: number
|
|
|
|
|
|
axisMax?: number
|
2026-08-04 11:28:42 +08:00
|
|
|
|
topTickValue?: number
|
2026-07-20 15:35:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @deprecated Use ScientificAxisLabelOptions. */
|
2026-08-04 11:28:42 +08:00
|
|
|
|
export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions
|
2026-07-20 10:21:30 +08:00
|
|
|
|
|
2026-08-04 11:28:42 +08:00
|
|
|
|
const SCIENTIFIC_MIN_ABSOLUTE_VALUE = 0.001
|
|
|
|
|
|
const SCIENTIFIC_MAX_PLAIN_ABSOLUTE_VALUE = 1000
|
|
|
|
|
|
const Y_AXIS_NUMBER_FORMATTER = new Intl.NumberFormat('zh-CN', {
|
|
|
|
|
|
maximumFractionDigits: 4,
|
|
|
|
|
|
})
|
2026-07-20 10:21:30 +08:00
|
|
|
|
const TOOLTIP_NUMBER_FORMATTER = new Intl.NumberFormat('zh-CN', {
|
|
|
|
|
|
maximumFractionDigits: 4,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function formatFixedNumber(value: number, precision: number): string {
|
|
|
|
|
|
const formatted = value.toFixed(Math.max(0, precision))
|
|
|
|
|
|
return /^-0(?:\.0+)?$/.test(formatted) ? formatted.slice(1) : formatted
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Whether an axis magnitude should use one shared scientific exponent. */
|
2026-07-20 15:35:19 +08:00
|
|
|
|
export function shouldUseScientificAxisLabel(maxAbsoluteValue: number): boolean {
|
2026-07-20 10:21:30 +08:00
|
|
|
|
return (
|
|
|
|
|
|
Number.isFinite(maxAbsoluteValue) &&
|
|
|
|
|
|
(maxAbsoluteValue >= SCIENTIFIC_MAX_PLAIN_ABSOLUTE_VALUE ||
|
|
|
|
|
|
(maxAbsoluteValue > 0 && maxAbsoluteValue < SCIENTIFIC_MIN_ABSOLUTE_VALUE))
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-20 15:35:19 +08:00
|
|
|
|
export function resolveScientificAxisExponent(axisMin?: number, axisMax?: number): number | null {
|
2026-07-20 10:21:30 +08:00
|
|
|
|
if (typeof axisMin !== 'number' || typeof axisMax !== 'number') return null
|
|
|
|
|
|
|
2026-08-04 11:28:42 +08:00
|
|
|
|
const maxValue = Math.max(axisMin, axisMax)
|
|
|
|
|
|
const absoluteMaxValue = Math.abs(maxValue)
|
|
|
|
|
|
return shouldUseScientificAxisLabel(absoluteMaxValue)
|
|
|
|
|
|
? Math.floor(Math.log10(absoluteMaxValue))
|
2026-07-20 10:21:30 +08:00
|
|
|
|
: null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatExponent(exponent: number): string {
|
|
|
|
|
|
const sign = exponent >= 0 ? '+' : '-'
|
|
|
|
|
|
return `E${sign}${Math.abs(exponent).toString().padStart(2, '0')}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 11:28:42 +08:00
|
|
|
|
function formatYAxisNumber(value: number): string {
|
|
|
|
|
|
if (Object.is(value, -0)) return '0'
|
|
|
|
|
|
const roundedValue = value === 0 ? 0 : Number(value.toPrecision(5))
|
|
|
|
|
|
const formatted = Y_AXIS_NUMBER_FORMATTER.format(roundedValue)
|
|
|
|
|
|
return formatted === '-0' ? '0' : formatted
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-20 10:21:30 +08:00
|
|
|
|
/** Format a Y-axis tick, sharing one exponent derived from the complete axis domain. */
|
2026-07-20 15:35:19 +08:00
|
|
|
|
export function formatScientificAxisLabel(
|
2026-07-20 10:21:30 +08:00
|
|
|
|
value: number,
|
2026-07-20 15:35:19 +08:00
|
|
|
|
options: ScientificAxisLabelOptions = {},
|
2026-07-20 10:21:30 +08:00
|
|
|
|
): string {
|
|
|
|
|
|
if (!Number.isFinite(value)) return String(value)
|
|
|
|
|
|
|
2026-07-20 15:35:19 +08:00
|
|
|
|
const exponent = resolveScientificAxisExponent(options.axisMin, options.axisMax)
|
2026-07-20 10:21:30 +08:00
|
|
|
|
const scaledValue = exponent === null ? value : value / 10 ** exponent
|
2026-08-04 11:28:42 +08:00
|
|
|
|
const formattedValue = formatYAxisNumber(scaledValue)
|
|
|
|
|
|
const topTickValue = options.topTickValue ?? options.axisMax
|
|
|
|
|
|
return exponent !== null && value === topTickValue
|
|
|
|
|
|
? `${formatExponent(exponent)} ${formattedValue}`
|
|
|
|
|
|
: formattedValue
|
2026-07-20 15:35:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Return the shared E-style multiplier for an axis, or null for a plain axis. */
|
|
|
|
|
|
export function formatScientificAxisExponent(axisMin?: number, axisMax?: number): string | null {
|
|
|
|
|
|
const exponent = resolveScientificAxisExponent(axisMin, axisMax)
|
|
|
|
|
|
return exponent === null ? null : formatExponent(exponent)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @deprecated Use shouldUseScientificAxisLabel. */
|
|
|
|
|
|
export const shouldUseScientificYAxisLabel = shouldUseScientificAxisLabel
|
2026-07-20 10:21:30 +08:00
|
|
|
|
|
2026-07-20 15:35:19 +08:00
|
|
|
|
/** @deprecated Use formatScientificAxisLabel. */
|
|
|
|
|
|
export function formatScientificYAxisLabel(
|
|
|
|
|
|
value: number,
|
|
|
|
|
|
options: ScientificYAxisLabelOptions = {},
|
|
|
|
|
|
): string {
|
|
|
|
|
|
return formatScientificAxisLabel(value, options)
|
2026-07-20 10:21:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Format tooltip values as localized plain numbers with at most four decimal places. */
|
|
|
|
|
|
export function formatTooltipNumber(value: number): string {
|
|
|
|
|
|
if (!Number.isFinite(value)) return String(value)
|
|
|
|
|
|
if (Object.is(value, -0)) return '0'
|
|
|
|
|
|
return TOOLTIP_NUMBER_FORMATTER.format(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Convert a number to complete plain decimal text without forcing exponential notation. */
|
|
|
|
|
|
export function formatPlainNumber(value: number): string {
|
|
|
|
|
|
if (!Number.isFinite(value)) return String(value)
|
|
|
|
|
|
if (Object.is(value, -0)) return '0'
|
|
|
|
|
|
|
|
|
|
|
|
const [mantissaPart, exponentPart] = value.toExponential().split('e')
|
|
|
|
|
|
const exponent = Number(exponentPart)
|
|
|
|
|
|
const isNegative = mantissaPart.startsWith('-')
|
|
|
|
|
|
const digits = mantissaPart.replace('-', '').replace('.', '')
|
|
|
|
|
|
const decimalIndex = exponent + 1
|
|
|
|
|
|
let plainText: string
|
|
|
|
|
|
|
|
|
|
|
|
if (decimalIndex <= 0) {
|
|
|
|
|
|
plainText = `0.${'0'.repeat(Math.abs(decimalIndex))}${digits}`
|
|
|
|
|
|
} else if (decimalIndex >= digits.length) {
|
|
|
|
|
|
plainText = `${digits}${'0'.repeat(decimalIndex - digits.length)}`
|
|
|
|
|
|
} else {
|
|
|
|
|
|
plainText = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return `${isNegative ? '-' : ''}${plainText}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据时间单位转换显示值
|
|
|
|
|
|
* @param value 原始时间值(秒)
|
|
|
|
|
|
* @param timeUnit 时间单位
|
|
|
|
|
|
* @returns 转换后的显示值
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function displayTime(value: number, timeUnit: TimeUnit): number {
|
|
|
|
|
|
return timeUnit === 'ms' ? value * 1000 : value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 10:58:40 +08:00
|
|
|
|
/** Format an X-axis value without changing its source coordinate. */
|
|
|
|
|
|
export function formatXAxisLabel(
|
|
|
|
|
|
rawValue: number,
|
|
|
|
|
|
domain: [number, number],
|
|
|
|
|
|
timeUnit: TimeUnit,
|
|
|
|
|
|
kind: WaveformXAxisLabelKind,
|
|
|
|
|
|
formatter?: WaveformXAxisLabelFormatter,
|
|
|
|
|
|
): string {
|
|
|
|
|
|
const value = displayTime(rawValue, timeUnit)
|
|
|
|
|
|
if (!formatter) return formatPlainNumber(value)
|
|
|
|
|
|
|
|
|
|
|
|
return formatter(value, {
|
|
|
|
|
|
kind,
|
|
|
|
|
|
rawValue,
|
|
|
|
|
|
timeUnit,
|
|
|
|
|
|
domain: [domain[0], domain[1]],
|
|
|
|
|
|
displayDomain: [displayTime(domain[0], timeUnit), displayTime(domain[1], timeUnit)],
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-20 10:21:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 计算端点标签的小数位数
|
|
|
|
|
|
* @param domain 数据域 [最小值, 最大值]
|
|
|
|
|
|
* @param timeUnit 时间单位
|
|
|
|
|
|
* @returns 小数位数
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function endpointFractionDigits(domain: [number, number], timeUnit: TimeUnit): number {
|
|
|
|
|
|
const displayedSpan = Math.abs(
|
|
|
|
|
|
displayTime(domain[1], timeUnit) - displayTime(domain[0], timeUnit),
|
|
|
|
|
|
)
|
|
|
|
|
|
if (!Number.isFinite(displayedSpan) || displayedSpan <= 0) return 0
|
|
|
|
|
|
return Math.min(4, Math.max(0, Math.ceil(-Math.log10(displayedSpan / 100))))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-05 10:58:40 +08:00
|
|
|
|
* 将 X 轴端点时间格式化为当前显示单位下的普通十进制文本
|
2026-07-20 10:21:30 +08:00
|
|
|
|
* @param value 时间值(秒)
|
2026-08-04 11:28:42 +08:00
|
|
|
|
* @param _domain 数据域(为保持现有调用签名而保留)
|
2026-07-20 10:21:30 +08:00
|
|
|
|
* @param timeUnit 时间单位
|
|
|
|
|
|
* @returns 格式化的时间字符串
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function formatEndpointTime(
|
|
|
|
|
|
value: number,
|
2026-08-04 11:28:42 +08:00
|
|
|
|
_domain: [number, number],
|
2026-07-20 10:21:30 +08:00
|
|
|
|
timeUnit: TimeUnit,
|
|
|
|
|
|
): string {
|
2026-08-05 10:58:40 +08:00
|
|
|
|
return formatXAxisLabel(value, _domain, timeUnit, 'end')
|
2026-07-20 10:21:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-05 10:58:40 +08:00
|
|
|
|
* 将 X 轴时间刻度格式化为当前显示单位下的普通十进制文本
|
2026-07-20 10:21:30 +08:00
|
|
|
|
* @param value 时间值(秒)
|
|
|
|
|
|
* @param timeUnit 时间单位
|
2026-08-04 11:28:42 +08:00
|
|
|
|
* @param _domain 数据域(为保持现有调用签名而保留)
|
2026-07-20 10:21:30 +08:00
|
|
|
|
* @returns 格式化的时间字符串
|
|
|
|
|
|
*/
|
2026-07-20 15:35:19 +08:00
|
|
|
|
export function formatAxisTime(
|
|
|
|
|
|
value: number,
|
|
|
|
|
|
timeUnit: TimeUnit,
|
2026-08-04 11:28:42 +08:00
|
|
|
|
_domain?: [number, number],
|
2026-07-20 15:35:19 +08:00
|
|
|
|
): string {
|
2026-08-05 10:58:40 +08:00
|
|
|
|
return formatXAxisLabel(value, _domain ?? [value, value], timeUnit, 'tick')
|
2026-07-20 15:35:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-20 10:21:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 格式化悬浮提示时间(4 位小数,本地化格式)
|
|
|
|
|
|
* @param value 时间值(秒)
|
|
|
|
|
|
* @param timeUnit 时间单位
|
|
|
|
|
|
* @returns 格式化的时间字符串
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function formatTooltipTime(value: number, timeUnit: TimeUnit): string {
|
2026-08-04 11:28:42 +08:00
|
|
|
|
return formatTooltipNumber(displayTime(value, timeUnit))
|
2026-07-20 10:21:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Format an annotation X coordinate in the selected display time unit. */
|
|
|
|
|
|
export function formatAnnotationTime(value: number, timeUnit: TimeUnit): string {
|
|
|
|
|
|
if (!Number.isFinite(value)) return String(value)
|
|
|
|
|
|
return formatFixedNumber(displayTime(value, timeUnit), 3)
|
|
|
|
|
|
}
|