feat(chart): support configurable x-axis labels
All checks were successful
Package component / package (push) Successful in 4m35s

This commit is contained in:
李启源
2026-08-05 10:58:40 +08:00
parent c5aa3c662a
commit fbf10fdb88
18 changed files with 505 additions and 66 deletions

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { WaveformXAxisLabelFormatter, WaveformXAxisLabelFormatterContext } from '../types'
import {
formatAnnotationTime,
formatAxisTime,
@@ -9,6 +10,7 @@ import {
formatScientificAxisLabel,
formatTooltipNumber,
formatTooltipTime,
formatXAxisLabel,
resolveScientificAxisExponent,
shouldUseScientificAxisLabel,
} from './formatters'
@@ -55,20 +57,50 @@ describe('waveform number formatters', () => {
expect(formatScientificAxisExponent(0, 0)).toBeNull()
})
it('formats X-axis ticks and endpoints as plain integers in the selected display unit', () => {
it('formats X-axis ticks and endpoints as complete plain values in the display unit', () => {
const domain: [number, number] = [0, 1]
expect(formatAxisTime(0.5004, 'ms', domain)).toBe('500')
expect(formatAxisTime(0.5004, 'ms', domain)).toBe('500.4')
expect(formatEndpointTime(1, domain, 'ms')).toBe('1000')
expect(formatAxisTime(0.5, 's', domain)).toBe('1')
expect(formatAxisTime(0.5, 's', domain)).toBe('0.5')
expect(formatEndpointTime(1, domain, 's')).toBe('1')
const tinyDomain: [number, number] = [0, 0.000001]
expect(formatEndpointTime(0.000001, tinyDomain, 's')).toBe('0')
expect(formatAxisTime(-0.000001, 's', tinyDomain)).toBe('0')
expect(formatEndpointTime(0.000001, tinyDomain, 's')).toBe('0.000001')
expect(formatAxisTime(-0.000001, 's', tinyDomain)).toBe('-0.000001')
expect(formatAxisTime(-0, 's')).toBe('0')
expect(formatAxisTime(1e21, 's')).toBe('1000000000000000000000')
expect(formatAxisTime(Number.POSITIVE_INFINITY, 's')).toBe('Infinity')
})
it('passes display values and complete source context to X-axis label formatters', () => {
const domain: [number, number] = [0.125, 1.875]
const formatter: WaveformXAxisLabelFormatter = (value, context) =>
`${context.kind}:${value / 10}`
expect(formatXAxisLabel(0.5, domain, 'ms', 'tick', formatter)).toBe('tick:50')
expect(formatXAxisLabel(domain[0], domain, 'ms', 'start', formatter)).toBe('start:12.5')
expect(formatXAxisLabel(domain[1], domain, 'ms', 'end', formatter)).toBe('end:187.5')
let receivedContext: WaveformXAxisLabelFormatterContext | undefined
formatXAxisLabel(0.5, domain, 'ms', 'tick', (value, context) => {
receivedContext = context
return String(value)
})
expect(receivedContext).toEqual({
kind: 'tick',
rawValue: 0.5,
timeUnit: 'ms',
domain: [0.125, 1.875],
displayDomain: [125, 1875],
})
expect(() =>
formatXAxisLabel(0.5, domain, 's', 'tick', () => {
throw new Error('formatter failed')
}),
).toThrow('formatter failed')
})
it('formats tooltip and raw values for their display contexts', () => {
expect(formatTooltipNumber(12345.67891)).toBe('12,345.6789')
expect(formatTooltipNumber(-0)).toBe('0')

View File

@@ -1,3 +1,5 @@
import type { WaveformXAxisLabelFormatter, WaveformXAxisLabelKind } from '../types'
/**
* 时间单位类型
*/
@@ -22,10 +24,6 @@ const Y_AXIS_NUMBER_FORMATTER = new Intl.NumberFormat('zh-CN', {
const TOOLTIP_NUMBER_FORMATTER = new Intl.NumberFormat('zh-CN', {
maximumFractionDigits: 4,
})
const X_AXIS_TIME_FORMATTER = new Intl.NumberFormat('zh-CN', {
maximumFractionDigits: 0,
useGrouping: false,
})
function formatFixedNumber(value: number, precision: number): string {
const formatted = value.toFixed(Math.max(0, precision))
@@ -103,13 +101,6 @@ export function formatTooltipNumber(value: number): string {
return TOOLTIP_NUMBER_FORMATTER.format(value)
}
/** Format an X-axis time value as a plain integer without grouping separators. */
function formatAxisTimeValue(value: number): string {
if (!Number.isFinite(value)) return String(value)
const formatted = X_AXIS_TIME_FORMATTER.format(value)
return formatted === '-0' ? '0' : formatted
}
/** 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)
@@ -143,6 +134,26 @@ export function displayTime(value: number, timeUnit: TimeUnit): number {
return timeUnit === 'ms' ? value * 1000 : value
}
/** 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)],
})
}
/**
* 计算端点标签的小数位数
* @param domain 数据域 [最小值, 最大值]
@@ -158,7 +169,7 @@ export function endpointFractionDigits(domain: [number, number], timeUnit: TimeU
}
/**
* 将 X 轴端点时间格式化为当前显示单位下的普通整数
* 将 X 轴端点时间格式化为当前显示单位下的普通十进制文本
* @param value 时间值(秒)
* @param _domain 数据域(为保持现有调用签名而保留)
* @param timeUnit 时间单位
@@ -169,11 +180,11 @@ export function formatEndpointTime(
_domain: [number, number],
timeUnit: TimeUnit,
): string {
return formatAxisTimeValue(displayTime(value, timeUnit))
return formatXAxisLabel(value, _domain, timeUnit, 'end')
}
/**
* 将 X 轴时间刻度格式化为当前显示单位下的普通整数
* 将 X 轴时间刻度格式化为当前显示单位下的普通十进制文本
* @param value 时间值(秒)
* @param timeUnit 时间单位
* @param _domain 数据域(为保持现有调用签名而保留)
@@ -184,8 +195,7 @@ export function formatAxisTime(
timeUnit: TimeUnit,
_domain?: [number, number],
): string {
void _domain
return formatAxisTimeValue(displayTime(value, timeUnit))
return formatXAxisLabel(value, _domain ?? [value, value], timeUnit, 'tick')
}
/**

View File

@@ -11,6 +11,7 @@ export {
endpointFractionDigits,
formatEndpointTime,
formatAxisTime,
formatXAxisLabel,
formatTooltipTime,
formatAnnotationTime,
formatPlainNumber,