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

@@ -12,8 +12,13 @@ import {
selectSeriesRenderPoints,
type ResolvedWaveformRenderingOptions,
} from '../../core/rendering'
import type { WaveformDisplayMode, WaveformOverlayMode, WaveformPoint } from '../../types'
import { buildMinorTicks, formatEndpointTime } from '../../utils'
import type {
WaveformDisplayMode,
WaveformOverlayMode,
WaveformPoint,
WaveformXAxisLabelFormatter,
} from '../../types'
import { buildMinorTicks, formatXAxisLabel } from '../../utils'
import {
getBottomRowCellIndexes,
type GridCellGeometry,
@@ -45,6 +50,7 @@ export interface BuildTrackLayoutsOptions {
fixedYDomains?: Record<string, [number, number]>
yDomains?: Record<string, [number, number]>
timeUnit: 's' | 'ms'
xAxisLabelFormatter?: WaveformXAxisLabelFormatter
rendering: ResolvedWaveformRenderingOptions
hideSecondaryLabels: boolean
yAxisLabelX: number
@@ -147,8 +153,20 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
const yAxisTickValues = yAxes[0]?.tickValues ?? []
const domain = xScale.domain() as [number, number]
const endpointLabels = {
start: formatEndpointTime(domain[0], domain, options.timeUnit),
end: formatEndpointTime(domain[1], domain, options.timeUnit),
start: formatXAxisLabel(
domain[0],
domain,
options.timeUnit,
'start',
options.xAxisLabelFormatter,
),
end: formatXAxisLabel(
domain[1],
domain,
options.timeUnit,
'end',
options.xAxisLabelFormatter,
),
}
const leftClearance = endpointLabels.start.length * 7 + 10
const rightClearance = endpointLabels.end.length * 7 + 10

View File

@@ -285,6 +285,7 @@ export function useWaveformLayout(context: LayoutContext) {
)
: sharedYDomains.value,
timeUnit: props.timeUnit,
xAxisLabelFormatter: props.axes?.x?.labelFormatter,
rendering: renderingOptions.value,
hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
yAxisLabelX: yAxisMetrics.value.labelCenterX,

View File

@@ -3,7 +3,7 @@ import { axisBottom, axisLeft, axisRight, select } from 'd3'
import { nextTick, onMounted, ref, watch } from 'vue'
import type { WaveformAxesOptions } from '../../types'
import { formatAxisTime, formatScientificAxisLabel } from '../../utils'
import { formatScientificAxisLabel, formatXAxisLabel } from '../../utils'
import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
interface Props {
@@ -66,10 +66,12 @@ function renderAxes() {
axisBottom(props.track.xScale)
.tickValues(props.track.xAxisTickValues)
.tickFormat((value) =>
formatAxisTime(
formatXAxisLabel(
Number(value),
props.timeUnit,
props.track.xScale.domain() as [number, number],
props.timeUnit,
'tick',
props.axes?.x?.labelFormatter,
),
)
.tickSize(-4)
@@ -93,6 +95,7 @@ watch(
() => props.track.xAxisTickValues,
() => props.track.yAxisTickValues,
() => props.timeUnit,
() => props.axes?.x?.labelFormatter,
() => props.axes?.x?.lineVisible,
() => props.axes?.y?.lineVisible,
],

View File

@@ -1,7 +1,8 @@
import { flushPromises } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { flushAnimationFrames } from '../../test/setup'
import type { WaveformXAxisLabelFormatter } from '../../types'
import { type WaveformData } from '../waveform'
import { gridSeries, mountSizedChart } from '../../test/waveformChart'
@@ -162,7 +163,7 @@ describe('WaveformChart', () => {
expect(wrapper.get('.waveform-chart__axis-endpoint--end').attributes('x')).toBe(
String(trackWidth),
)
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4990')
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4990.3')
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
})
@@ -267,6 +268,70 @@ describe('WaveformChart', () => {
expect(endpointGroup.attributes('font-size')).toBe(xAxis.attributes('font-size'))
})
it('formats X-axis ticks and endpoints with display-unit values and source context', async () => {
const data: WaveformData = {
kind: 'points',
points: [
{ x: 0.125, y: 0 },
{ x: 1.875, y: 1 },
],
}
const originalPoints = data.points.map((point) => ({ ...point }))
const labelFormatter: WaveformXAxisLabelFormatter = (value, context) =>
`${context.kind}:${value / 10}`
const formatter = vi.fn(labelFormatter)
const wrapper = await mountSizedChart(data, {
timeUnit: 'ms',
axes: { x: { labelFormatter: formatter } },
})
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('start:12.5')
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('end:187.5')
expect(
wrapper
.get('.waveform-chart__axis--x')
.findAll('.tick text')
.every((tick) => tick.text().startsWith('tick:')),
).toBe(true)
const startCall = formatter.mock.calls.find(([, context]) => context.kind === 'start')
const tickCall = formatter.mock.calls.find(([, context]) => context.kind === 'tick')
const endCall = formatter.mock.calls.find(([, context]) => context.kind === 'end')
expect(startCall).toEqual([
125,
{
kind: 'start',
rawValue: 0.125,
timeUnit: 'ms',
domain: [0.125, 1.875],
displayDomain: [125, 1875],
},
])
expect(tickCall?.[0]).toBeTypeOf('number')
expect(tickCall?.[1]).toMatchObject({
kind: 'tick',
timeUnit: 'ms',
domain: [0.125, 1.875],
displayDomain: [125, 1875],
})
expect(endCall?.[1]).toMatchObject({ kind: 'end', rawValue: 1.875 })
expect(data.points).toEqual(originalPoints)
await wrapper.setProps({
axes: { x: { labelFormatter: (value: number) => `updated:${value}` } },
})
await flushPromises()
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('updated:125')
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('updated:1875')
expect(
wrapper
.get('.waveform-chart__axis--x')
.findAll('.tick text')
.every((tick) => tick.text().startsWith('updated:')),
).toBe(true)
expect(data.points).toEqual(originalPoints)
})
it('keeps zoom-change domains in source seconds', async () => {
const wrapper = await mountSizedChart({
kind: 'points',
@@ -307,6 +372,8 @@ describe('WaveformChart', () => {
)
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe(initialStart)
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).not.toBe(initialEnd)
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toMatch(/^-?\d+$/)
expect(
Number.isFinite(Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())),
).toBe(true)
})
})