waveform-analysis 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +57 -33
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +451 -491
  4. package/dist/types/components/core/layout.d.ts +2 -4
  5. package/dist/types/components/core/trackLayoutBuilder.d.ts +2 -1
  6. package/dist/types/components/core/types.d.ts +0 -3
  7. package/dist/types/demo/types.d.ts +9 -0
  8. package/dist/types/demo/useDemoXAxisLabelControls.d.ts +32 -0
  9. package/dist/types/index.d.ts +1 -1
  10. package/dist/types/types/chart.d.ts +15 -0
  11. package/dist/types/types/index.d.ts +1 -1
  12. package/dist/types/utils/formatters.d.ts +12 -10
  13. package/dist/types/utils/index.d.ts +1 -1
  14. package/package.json +1 -1
  15. package/src/App.vue +7 -1
  16. package/src/components/core/layout.test.ts +7 -8
  17. package/src/components/core/layout.ts +10 -20
  18. package/src/components/core/trackLayoutBuilder.ts +24 -26
  19. package/src/components/core/types.ts +0 -3
  20. package/src/components/core/useWaveformLayout.ts +6 -27
  21. package/src/components/interaction/WaveformTooltip.test.ts +16 -0
  22. package/src/components/rendering/WaveformTrackAxes.vue +10 -29
  23. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +7 -7
  24. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +2 -2
  25. package/src/components/waveformChartCases/dataLifecycle.test.ts +4 -4
  26. package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +97 -23
  27. package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +1 -1
  28. package/src/components/waveformChartCases/layoutAndDomains.test.ts +15 -15
  29. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +8 -8
  30. package/src/components/waveformChartCases/presentationAndAxes.test.ts +6 -4
  31. package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +8 -8
  32. package/src/components/waveformChartCases/wheelZoomOut.test.ts +3 -3
  33. package/src/demo/DemoGridFrameControls.vue +70 -0
  34. package/src/demo/types.ts +9 -0
  35. package/src/demo/useDemoXAxisLabelControls.ts +77 -0
  36. package/src/demo/xAxisLabelControls.test.ts +86 -0
  37. package/src/index.ts +3 -0
  38. package/src/styles.css +8 -0
  39. package/src/types/chart.ts +21 -0
  40. package/src/types/index.ts +3 -0
  41. package/src/utils/formatters.test.ts +72 -31
  42. package/src/utils/formatters.ts +55 -45
  43. package/src/utils/index.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  import type { WaveformOverlayMode } from '../../types';
2
2
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types';
3
3
  import { type WaveformYDomain } from './yDomain';
4
- export { MAX_MULTI_Y_AXIS_COUNT, Y_AXIS_EXPONENT_GAP } from './constants';
4
+ export { MAX_MULTI_Y_AXIS_COUNT } from './constants';
5
5
  export interface YAxisSeriesGroup {
6
6
  index: number;
7
7
  side: 'left' | 'right';
@@ -11,9 +11,7 @@ export interface YAxisSeriesGroup {
11
11
  }
12
12
  export declare function buildYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode): YAxisSeriesGroup[];
13
13
  export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): YAxisSeriesGroup[];
14
- export declare function axisTextMetrics(domain: [number, number], nice?: boolean): {
15
- exponentLabel: string | null;
16
- exponentWidth: number;
14
+ export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[]): {
17
15
  tickTextWidth: number;
18
16
  };
19
17
  export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup): number;
@@ -1,6 +1,6 @@
1
1
  import { type ZoomTransform } from 'd3';
2
2
  import { type ResolvedWaveformRenderingOptions } from '../../core/rendering';
3
- import type { WaveformDisplayMode, WaveformOverlayMode } from '../../types';
3
+ import type { WaveformDisplayMode, WaveformOverlayMode, WaveformXAxisLabelFormatter } from '../../types';
4
4
  import { type GridCellGeometry, type NormalizedWaveformGridOptions } from './grid';
5
5
  import type { DisplayTrack, TrackLayout } from './types';
6
6
  interface SeriesGridCell extends GridCellGeometry {
@@ -19,6 +19,7 @@ export interface BuildTrackLayoutsOptions {
19
19
  fixedYDomains?: Record<string, [number, number]>;
20
20
  yDomains?: Record<string, [number, number]>;
21
21
  timeUnit: 's' | 'ms';
22
+ xAxisLabelFormatter?: WaveformXAxisLabelFormatter;
22
23
  rendering: ResolvedWaveformRenderingOptions;
23
24
  hideSecondaryLabels: boolean;
24
25
  yAxisLabelX: number;
@@ -41,8 +41,6 @@ export interface WaveformYAxisLayout {
41
41
  side: 'left' | 'right';
42
42
  x: number;
43
43
  labelX: number;
44
- exponentX: number;
45
- exponentLabel: string | null;
46
44
  scale: ScaleLinear<number, number>;
47
45
  majorTicks: number[];
48
46
  minorTicks: number[];
@@ -102,7 +100,6 @@ export interface TrackLayout {
102
100
  start: string;
103
101
  end: string;
104
102
  };
105
- xAxisExponent: string | null;
106
103
  path: string | null;
107
104
  seriesPaths: TrackSeriesPath[];
108
105
  showXAxis: boolean;
@@ -1,4 +1,5 @@
1
1
  import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformGridTrackLines, WaveformInteractionMode, WaveformLegendOrientation, WaveformLegendPosition, WaveformLineStyle, WaveformOverlayMode, WaveformTitleOptions, WaveformZeroLineOptions, WaveformZoomEndPayload } from '../components';
2
+ import type { DemoXAxisLabelFormat, DemoXAxisLabelTimeZone } from './useDemoXAxisLabelControls';
2
3
  interface SelectOption<T> {
3
4
  label: string;
4
5
  value: T;
@@ -27,6 +28,14 @@ export interface DemoControlPanelModel {
27
28
  verticalGridColor: string;
28
29
  xAxisLineVisible: boolean;
29
30
  yAxisLineVisible: boolean;
31
+ xAxisLabelFormatterEnabled: boolean;
32
+ xAxisLabelFormat: DemoXAxisLabelFormat;
33
+ xAxisLabelFormatOptions: Array<SelectOption<DemoXAxisLabelFormat>>;
34
+ xAxisLabelMultiplier: number;
35
+ xAxisLabelFractionDigits: number;
36
+ xAxisLabelTimeZone: DemoXAxisLabelTimeZone;
37
+ xAxisLabelTimeZoneOptions: Array<SelectOption<DemoXAxisLabelTimeZone>>;
38
+ xAxisLabelShowMilliseconds: boolean;
30
39
  frameBorderColor: string;
31
40
  frameBackgroundColor: string;
32
41
  frameBorderWidth: number;
@@ -0,0 +1,32 @@
1
+ import type { WaveformXAxisLabelFormatter } from '../types';
2
+ export type DemoXAxisLabelFormat = 'number' | 'datetime';
3
+ export type DemoXAxisLabelTimeZone = 'local' | 'Asia/Shanghai' | 'UTC';
4
+ export declare function formatDemoTimestamp(value: number, timeZone: DemoXAxisLabelTimeZone, showMilliseconds: boolean): string;
5
+ export declare function useDemoXAxisLabelControls(): {
6
+ controlModel: {
7
+ xAxisLabelFormatterEnabled: import("vue").Ref<boolean, boolean>;
8
+ xAxisLabelFormat: import("vue").Ref<DemoXAxisLabelFormat, DemoXAxisLabelFormat>;
9
+ xAxisLabelFormatOptions: ({
10
+ label: string;
11
+ value: "number";
12
+ } | {
13
+ label: string;
14
+ value: "datetime";
15
+ })[];
16
+ xAxisLabelMultiplier: import("vue").Ref<number, number>;
17
+ xAxisLabelFractionDigits: import("vue").Ref<number, number>;
18
+ xAxisLabelTimeZone: import("vue").Ref<DemoXAxisLabelTimeZone, DemoXAxisLabelTimeZone>;
19
+ xAxisLabelTimeZoneOptions: ({
20
+ label: string;
21
+ value: "Asia/Shanghai";
22
+ } | {
23
+ label: string;
24
+ value: "local";
25
+ } | {
26
+ label: string;
27
+ value: "UTC";
28
+ })[];
29
+ xAxisLabelShowMilliseconds: import("vue").Ref<boolean, boolean>;
30
+ };
31
+ xAxisLabelFormatter: import("vue").ComputedRef<WaveformXAxisLabelFormatter | undefined>;
32
+ };
@@ -3,7 +3,7 @@
3
3
  * @packageDocumentation
4
4
  */
5
5
  export { default as WaveformChart } from './components/WaveformChart.vue';
6
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './types';
6
+ export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './types';
7
7
  export type { WaveformGridOptions, WaveformGridLineOptions, WaveformGridTrackLines, } from './components/core/grid';
8
8
  export { normalizeWaveformData, normalizeWaveformSeries } from './core';
9
9
  export { paddedDomain, buildMinorTicks, displayTime, formatEndpointTime, formatAxisTime, formatTooltipTime, resolveTrackGeometry, clamp, type TimeUnit, type TrackGeometry, } from './utils';
@@ -106,10 +106,25 @@ export interface WaveformFrameStyle {
106
106
  borderStyle?: 'solid' | 'dashed' | 'dotted';
107
107
  backgroundColor?: string;
108
108
  }
109
+ export type WaveformXAxisLabelKind = 'tick' | 'start' | 'end';
110
+ /** Context passed to custom X-axis label formatters. */
111
+ export interface WaveformXAxisLabelFormatterContext {
112
+ kind: WaveformXAxisLabelKind;
113
+ /** X coordinate in the source data, before time-unit conversion. */
114
+ rawValue: number;
115
+ timeUnit: 's' | 'ms';
116
+ /** Current visible X domain in source coordinates. */
117
+ domain: [number, number];
118
+ /** Current visible X domain converted to the selected display unit. */
119
+ displayDomain: [number, number];
120
+ }
121
+ export type WaveformXAxisLabelFormatter = (value: number, context: WaveformXAxisLabelFormatterContext) => string;
109
122
  /** Controls axis baseline visibility while preserving tick marks and axis text. */
110
123
  export interface WaveformAxesOptions {
111
124
  x?: {
112
125
  lineVisible?: boolean;
126
+ /** Formats display-unit X values for ticks and visible-range endpoints. */
127
+ labelFormatter?: WaveformXAxisLabelFormatter;
113
128
  };
114
129
  y?: {
115
130
  lineVisible?: boolean;
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * 类型定义统一导出
3
3
  */
4
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformAxesOptions, WaveformZeroLineOptions, } from './chart';
4
+ export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, } from './chart';
5
5
  export type { SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './data';
@@ -1,16 +1,17 @@
1
+ import type { WaveformXAxisLabelFormatter, WaveformXAxisLabelKind } from '../types';
1
2
  /**
2
3
  * 时间单位类型
3
4
  */
4
5
  export type TimeUnit = 'ms' | 's';
5
6
  export interface ScientificAxisLabelOptions {
7
+ /** @deprecated Y-axis labels always use five significant digits before localization. */
6
8
  precision?: number;
7
9
  axisMin?: number;
8
10
  axisMax?: number;
11
+ topTickValue?: number;
9
12
  }
10
13
  /** @deprecated Use ScientificAxisLabelOptions. */
11
- export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions & {
12
- topTickValue?: number;
13
- };
14
+ export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions;
14
15
  /** Whether an axis magnitude should use one shared scientific exponent. */
15
16
  export declare function shouldUseScientificAxisLabel(maxAbsoluteValue: number): boolean;
16
17
  export declare function resolveScientificAxisExponent(axisMin?: number, axisMax?: number): number | null;
@@ -33,6 +34,8 @@ export declare function formatPlainNumber(value: number): string;
33
34
  * @returns 转换后的显示值
34
35
  */
35
36
  export declare function displayTime(value: number, timeUnit: TimeUnit): number;
37
+ /** Format an X-axis value without changing its source coordinate. */
38
+ export declare function formatXAxisLabel(rawValue: number, domain: [number, number], timeUnit: TimeUnit, kind: WaveformXAxisLabelKind, formatter?: WaveformXAxisLabelFormatter): string;
36
39
  /**
37
40
  * 计算端点标签的小数位数
38
41
  * @param domain 数据域 [最小值, 最大值]
@@ -41,22 +44,21 @@ export declare function displayTime(value: number, timeUnit: TimeUnit): number;
41
44
  */
42
45
  export declare function endpointFractionDigits(domain: [number, number], timeUnit: TimeUnit): number;
43
46
  /**
44
- * 按完整 X 轴显示域格式化端点时间
47
+ * 将 X 轴端点时间格式化为当前显示单位下的普通十进制文本
45
48
  * @param value 时间值(秒)
46
- * @param domain 数据域
49
+ * @param _domain 数据域(为保持现有调用签名而保留)
47
50
  * @param timeUnit 时间单位
48
51
  * @returns 格式化的时间字符串
49
52
  */
50
- export declare function formatEndpointTime(value: number, domain: [number, number], timeUnit: TimeUnit): string;
53
+ export declare function formatEndpointTime(value: number, _domain: [number, number], timeUnit: TimeUnit): string;
51
54
  /**
52
- * 按完整 X 轴显示域格式化时间刻度
55
+ * 将 X 轴时间刻度格式化为当前显示单位下的普通十进制文本
53
56
  * @param value 时间值(秒)
54
57
  * @param timeUnit 时间单位
58
+ * @param _domain 数据域(为保持现有调用签名而保留)
55
59
  * @returns 格式化的时间字符串
56
60
  */
57
- export declare function formatAxisTime(value: number, timeUnit: TimeUnit, domain?: [number, number]): string;
58
- /** Return the shared multiplier for an X-axis domain in its selected display unit. */
59
- export declare function formatAxisTimeExponent(domain: [number, number], timeUnit: TimeUnit): string | null;
61
+ export declare function formatAxisTime(value: number, timeUnit: TimeUnit, _domain?: [number, number]): string;
60
62
  /**
61
63
  * 格式化悬浮提示时间(4 位小数,本地化格式)
62
64
  * @param value 时间值(秒)
@@ -2,6 +2,6 @@
2
2
  * 工具函数模块统一导出
3
3
  */
4
4
  export { paddedDomain, buildMinorTicks } from './domain';
5
- export { displayTime, endpointFractionDigits, formatEndpointTime, formatAxisTime, formatAxisTimeExponent, formatTooltipTime, formatAnnotationTime, formatPlainNumber, formatScientificAxisLabel, formatScientificAxisExponent, formatScientificYAxisLabel, formatTooltipNumber, resolveScientificAxisExponent, shouldUseScientificAxisLabel, shouldUseScientificYAxisLabel, type ScientificAxisLabelOptions, type ScientificYAxisLabelOptions, type TimeUnit, } from './formatters';
5
+ export { displayTime, endpointFractionDigits, formatEndpointTime, formatAxisTime, formatXAxisLabel, formatTooltipTime, formatAnnotationTime, formatPlainNumber, formatScientificAxisLabel, formatScientificAxisExponent, formatScientificYAxisLabel, formatTooltipNumber, resolveScientificAxisExponent, shouldUseScientificAxisLabel, shouldUseScientificYAxisLabel, type ScientificAxisLabelOptions, type ScientificYAxisLabelOptions, type TimeUnit, } from './formatters';
6
6
  export { resolveTrackGeometry, clamp, type TrackGeometry } from './geometry';
7
7
  export { downsampleLTTB, downsampleMinMax, adaptiveSampling, calculateSamplingThreshold, } from './sampling';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
package/src/App.vue CHANGED
@@ -23,6 +23,7 @@ import { createSimulatedWaveformData } from './data/simulatedWaveforms'
23
23
  import DemoChartHost from './demo/DemoChartHost.vue'
24
24
  import DemoControlPanel from './demo/DemoControlPanel.vue'
25
25
  import type { DemoChartModel, DemoControlPanelModel } from './demo/types'
26
+ import { useDemoXAxisLabelControls } from './demo/useDemoXAxisLabelControls'
26
27
 
27
28
  const fullChartData = createSimulatedWaveformData()
28
29
  const displayMode = ref<WaveformDisplayMode>('independent')
@@ -40,6 +41,7 @@ const verticalGridVisible = ref(true)
40
41
  const verticalGridColor = ref('#dfe5ef')
41
42
  const xAxisLineVisible = ref(false)
42
43
  const yAxisLineVisible = ref(false)
44
+ const { controlModel: xAxisLabelControlModel, xAxisLabelFormatter } = useDemoXAxisLabelControls()
43
45
  const annotations = ref<WaveformAnnotation[]>([])
44
46
  const annotationsVisible = ref(true)
45
47
  const cleanView = ref(false)
@@ -112,7 +114,10 @@ const frameStyle = computed<WaveformFrameStyle>(() => ({
112
114
  backgroundColor: frameBackgroundColor.value,
113
115
  }))
114
116
  const axes = computed<WaveformAxesOptions>(() => ({
115
- x: { lineVisible: xAxisLineVisible.value },
117
+ x: {
118
+ lineVisible: xAxisLineVisible.value,
119
+ ...(xAxisLabelFormatter.value ? { labelFormatter: xAxisLabelFormatter.value } : {}),
120
+ },
116
121
  y: { lineVisible: yAxisLineVisible.value },
117
122
  }))
118
123
  const zeroLine = computed<WaveformZeroLineOptions>(() => ({
@@ -308,6 +313,7 @@ const controlPanelModel = reactive({
308
313
  verticalGridColor,
309
314
  xAxisLineVisible,
310
315
  yAxisLineVisible,
316
+ ...xAxisLabelControlModel,
311
317
  frameBorderColor,
312
318
  frameBackgroundColor,
313
319
  frameBorderWidth,
@@ -192,7 +192,7 @@ describe('multi-value Y-axis grouping', () => {
192
192
  ])
193
193
  })
194
194
 
195
- it('places left and right scientific exponents eight pixels outside their tick labels', () => {
195
+ it('reserves label clearance for exponent-prefixed ticks on both axis sides', () => {
196
196
  const layout = buildTrackLayouts({
197
197
  cells: [
198
198
  {
@@ -206,7 +206,7 @@ describe('multi-value Y-axis grouping', () => {
206
206
  plotHeight: 300,
207
207
  cellHeight: 330,
208
208
  xAxisBand: 30,
209
- series: track([series('left', 0, 254), series('right', 0, 254)]),
209
+ series: track([series('left', 1000, 3000), series('right', 1000, 3000)]),
210
210
  },
211
211
  ],
212
212
  grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
@@ -222,14 +222,13 @@ describe('multi-value Y-axis grouping', () => {
222
222
  })[0]
223
223
 
224
224
  expect(
225
- layout?.yAxes.map(({ side, x, exponentX, exponentLabel }) => ({
225
+ layout?.yAxes.map(({ side, x, labelX }) => ({
226
226
  side,
227
- offset: Math.abs(exponentX - x),
228
- exponentLabel,
227
+ labelOffset: Math.abs(labelX - x),
229
228
  })),
230
229
  ).toEqual([
231
- { side: 'left', offset: 43, exponentLabel: 'E+02' },
232
- { side: 'right', offset: 43, exponentLabel: 'E+02' },
230
+ { side: 'left', labelOffset: 67 },
231
+ { side: 'right', labelOffset: 67 },
233
232
  ])
234
233
  })
235
234
 
@@ -237,7 +236,7 @@ describe('multi-value Y-axis grouping', () => {
237
236
  const [group] = buildYAxisSeriesGroups(track([series('long', -1e120, 1e120)]), 'multi-axis')
238
237
 
239
238
  expect(group).toBeDefined()
240
- expect(measureYAxisGroupClearance(group!)).toBe(119)
239
+ expect(measureYAxisGroupClearance(group!)).toBe(90)
241
240
  })
242
241
  })
243
242
 
@@ -1,9 +1,9 @@
1
1
  import { scaleLinear } from 'd3'
2
2
 
3
3
  import type { WaveformOverlayMode } from '../../types'
4
- import { formatScientificAxisExponent, formatScientificAxisLabel, paddedDomain } from '../../utils'
4
+ import { formatScientificAxisLabel, paddedDomain } from '../../utils'
5
5
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
6
- import { MAX_MULTI_Y_AXIS_COUNT, Y_AXIS_EXPONENT_GAP } from './constants'
6
+ import { MAX_MULTI_Y_AXIS_COUNT } from './constants'
7
7
  import {
8
8
  mergeYDomains,
9
9
  resolveSeriesFixedYDomain,
@@ -12,7 +12,7 @@ import {
12
12
  } from './yDomain'
13
13
 
14
14
  // 导出常量供外部使用
15
- export { MAX_MULTI_Y_AXIS_COUNT, Y_AXIS_EXPONENT_GAP } from './constants'
15
+ export { MAX_MULTI_Y_AXIS_COUNT } from './constants'
16
16
 
17
17
  const Y_AXIS_CHARACTER_WIDTH = 7
18
18
  const Y_AXIS_TICK_PADDING = 7
@@ -102,36 +102,27 @@ export function resolveYAxisSeriesGroups(
102
102
  export function axisTextMetrics(
103
103
  domain: [number, number],
104
104
  nice = true,
105
- ): {
106
- exponentLabel: string | null
107
- exponentWidth: number
108
- tickTextWidth: number
109
- } {
105
+ tickValues?: number[],
106
+ ): { tickTextWidth: number } {
110
107
  const scale = scaleLinear(domain, [1, 0])
111
108
  if (nice) scale.nice()
112
109
  const [axisMin, axisMax] = scale.domain()
113
- const values = scale.ticks(10)
110
+ const values = tickValues ?? Array.from(new Set([axisMin, ...scale.ticks(10), axisMax]))
111
+ const topTickValue = Math.max(...values)
114
112
  const maximumTickCharacters = Math.max(
115
113
  1,
116
- ...values.map((value) => formatScientificAxisLabel(value, { axisMin, axisMax }).length),
114
+ ...values.map(
115
+ (value) => formatScientificAxisLabel(value, { axisMin, axisMax, topTickValue }).length,
116
+ ),
117
117
  )
118
- const exponentLabel = formatScientificAxisExponent(axisMin, axisMax)
119
118
  return {
120
- exponentLabel,
121
- exponentWidth: exponentLabel ? exponentLabel.length * Y_AXIS_CHARACTER_WIDTH : 0,
122
119
  tickTextWidth: maximumTickCharacters * Y_AXIS_CHARACTER_WIDTH,
123
120
  }
124
121
  }
125
122
 
126
- function axisExponentClearance(domain: [number, number], nice: boolean): number {
127
- const { exponentLabel, exponentWidth } = axisTextMetrics(domain, nice)
128
- return exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
129
- }
130
-
131
123
  export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
132
124
  return (
133
125
  axisTextMetrics(group.domain, !group.fixed).tickTextWidth +
134
- axisExponentClearance(group.domain, !group.fixed) +
135
126
  Y_AXIS_TICK_PADDING +
136
127
  Y_AXIS_LABEL_GAP +
137
128
  Y_AXIS_LABEL_BAND_WIDTH +
@@ -142,7 +133,6 @@ export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
142
133
  function measureYAxisGroupTickClearance(group: YAxisSeriesGroup): number {
143
134
  return (
144
135
  axisTextMetrics(group.domain, !group.fixed).tickTextWidth +
145
- axisExponentClearance(group.domain, !group.fixed) +
146
136
  Y_AXIS_TICK_PADDING +
147
137
  Y_AXIS_OUTER_PADDING
148
138
  )
@@ -12,8 +12,13 @@ import {
12
12
  selectSeriesRenderPoints,
13
13
  type ResolvedWaveformRenderingOptions,
14
14
  } from '../../core/rendering'
15
- import type { WaveformDisplayMode, WaveformOverlayMode, WaveformPoint } from '../../types'
16
- import { buildMinorTicks, formatAxisTimeExponent, formatEndpointTime } from '../../utils'
15
+ import type {
16
+ WaveformDisplayMode,
17
+ WaveformOverlayMode,
18
+ WaveformPoint,
19
+ WaveformXAxisLabelFormatter,
20
+ } from '../../types'
21
+ import { buildMinorTicks, formatXAxisLabel } from '../../utils'
17
22
  import {
18
23
  getBottomRowCellIndexes,
19
24
  type GridCellGeometry,
@@ -21,7 +26,6 @@ import {
21
26
  } from './grid'
22
27
  import { axisTextMetrics, resolveYAxisSeriesGroups } from './layout'
23
28
  import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
24
- import { Y_AXIS_EXPONENT_GAP } from './constants'
25
29
  import {
26
30
  Y_AXIS_LABEL_BAND_WIDTH,
27
31
  Y_AXIS_LABEL_GAP,
@@ -46,6 +50,7 @@ export interface BuildTrackLayoutsOptions {
46
50
  fixedYDomains?: Record<string, [number, number]>
47
51
  yDomains?: Record<string, [number, number]>
48
52
  timeUnit: 's' | 'ms'
53
+ xAxisLabelFormatter?: WaveformXAxisLabelFormatter
49
54
  rendering: ResolvedWaveformRenderingOptions
50
55
  hideSecondaryLabels: boolean
51
56
  yAxisLabelX: number
@@ -118,31 +123,16 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
118
123
  const tickValues = Array.from(
119
124
  new Set([axisStart, ...visibleMajorTicks, ...(showAxisEnd ? [axisEnd] : [])]),
120
125
  )
121
- const { exponentLabel, exponentWidth, tickTextWidth } = axisTextMetrics(
122
- group.domain,
123
- !group.fixed,
124
- )
125
- const exponentClearance = exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
126
+ const { tickTextWidth } = axisTextMetrics(group.domain, !group.fixed, tickValues)
126
127
  const clearance =
127
128
  tickTextWidth +
128
129
  Y_AXIS_TICK_PADDING +
129
- exponentClearance +
130
130
  Y_AXIS_LABEL_GAP +
131
131
  Y_AXIS_LABEL_BAND_WIDTH +
132
132
  Y_AXIS_OUTER_PADDING
133
133
  const x = group.side === 'left' ? -sideOffsets.left : cell.width + sideOffsets.right
134
- const exponentX =
135
- x +
136
- (group.side === 'left'
137
- ? -(Y_AXIS_TICK_PADDING + tickTextWidth + Y_AXIS_EXPONENT_GAP)
138
- : Y_AXIS_TICK_PADDING + tickTextWidth + Y_AXIS_EXPONENT_GAP)
139
134
  const labelDistance =
140
- tickTextWidth +
141
- Y_AXIS_TICK_PADDING +
142
- exponentClearance +
143
- exponentWidth +
144
- Y_AXIS_LABEL_GAP +
145
- Y_AXIS_LABEL_BAND_WIDTH / 2
135
+ tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
146
136
  const labelX = x + (group.side === 'left' ? -labelDistance : labelDistance)
147
137
  sideOffsets[group.side] += clearance
148
138
  return {
@@ -150,8 +140,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
150
140
  side: group.side,
151
141
  x,
152
142
  labelX,
153
- exponentX,
154
- exponentLabel,
155
143
  scale,
156
144
  majorTicks,
157
145
  minorTicks: buildMinorTicks(majorTicks),
@@ -165,10 +153,21 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
165
153
  const yAxisTickValues = yAxes[0]?.tickValues ?? []
166
154
  const domain = xScale.domain() as [number, number]
167
155
  const endpointLabels = {
168
- start: formatEndpointTime(domain[0], domain, options.timeUnit),
169
- end: formatEndpointTime(domain[1], domain, options.timeUnit),
156
+ start: formatXAxisLabel(
157
+ domain[0],
158
+ domain,
159
+ options.timeUnit,
160
+ 'start',
161
+ options.xAxisLabelFormatter,
162
+ ),
163
+ end: formatXAxisLabel(
164
+ domain[1],
165
+ domain,
166
+ options.timeUnit,
167
+ 'end',
168
+ options.xAxisLabelFormatter,
169
+ ),
170
170
  }
171
- const xAxisExponent = formatAxisTimeExponent(domain, options.timeUnit)
172
171
  const leftClearance = endpointLabels.start.length * 7 + 10
173
172
  const rightClearance = endpointLabels.end.length * 7 + 10
174
173
  const xAxisTickValues = xMajorTicks.filter((tick) => {
@@ -235,7 +234,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
235
234
  yAxisTickValues,
236
235
  xAxisTickValues,
237
236
  endpointLabels,
238
- xAxisExponent,
239
237
  path: seriesPaths[0]?.path ?? null,
240
238
  seriesPaths,
241
239
  gridLines: options.grid.trackLines[displayTrack.id] ?? {
@@ -51,8 +51,6 @@ export interface WaveformYAxisLayout {
51
51
  side: 'left' | 'right'
52
52
  x: number
53
53
  labelX: number
54
- exponentX: number
55
- exponentLabel: string | null
56
54
  scale: ScaleLinear<number, number>
57
55
  majorTicks: number[]
58
56
  minorTicks: number[]
@@ -109,7 +107,6 @@ export interface TrackLayout {
109
107
  yAxisTickValues: number[]
110
108
  xAxisTickValues: number[]
111
109
  endpointLabels: { start: string; end: string }
112
- xAxisExponent: string | null
113
110
  path: string | null
114
111
  seriesPaths: TrackSeriesPath[]
115
112
  showXAxis: boolean
@@ -2,7 +2,7 @@ import { scaleLinear, type ZoomTransform } from 'd3'
2
2
  import { computed, type ComputedRef, type Ref, type ShallowRef } from 'vue'
3
3
 
4
4
  import { resolveWaveformRenderingOptions } from '../../core'
5
- import { formatScientificAxisExponent, formatScientificAxisLabel, paddedDomain } from '../../utils'
5
+ import { paddedDomain } from '../../utils'
6
6
  import {
7
7
  layoutAnnotations,
8
8
  type AnnotationSeriesInfo,
@@ -28,9 +28,9 @@ import {
28
28
  } from './grid'
29
29
  import {
30
30
  buildTrackLayouts,
31
+ axisTextMetrics,
31
32
  measureTrackYAxisClearance,
32
33
  resolveYAxisSeriesGroups,
33
- Y_AXIS_EXPONENT_GAP,
34
34
  } from './layout'
35
35
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
36
36
  import type { PreparedWaveformSeries } from './useWaveformData'
@@ -112,40 +112,18 @@ export function useWaveformLayout(context: LayoutContext) {
112
112
  .flatMap((track) =>
113
113
  resolveYAxisSeriesGroups(track, props.overlayMode, props.yDomain, props.yDomains),
114
114
  )
115
- .map((group) => {
116
- const scale = scaleLinear(group.domain, [1, 0])
117
- if (!group.fixed) scale.nice()
118
- const [axisMin, axisMax] = scale.domain()
119
- return {
120
- exponentLabel: formatScientificAxisExponent(axisMin, axisMax),
121
- tickLabels: scale
122
- .ticks(10)
123
- .map((value) => formatScientificAxisLabel(value, { axisMin, axisMax })),
124
- }
125
- })
126
- const maximumCharacters = Math.max(
127
- 1,
128
- ...axisText.flatMap(({ tickLabels }) => tickLabels).map((label) => label.length),
129
- )
130
- const tickTextWidth = maximumCharacters * Y_AXIS_CHARACTER_WIDTH
131
- const maximumExponentWidth = Math.max(
132
- 0,
133
- ...axisText.map(({ exponentLabel }) => (exponentLabel?.length ?? 0) * Y_AXIS_CHARACTER_WIDTH),
134
- )
135
- const exponentClearance = maximumExponentWidth ? maximumExponentWidth + Y_AXIS_EXPONENT_GAP : 0
136
- const tickClearance =
137
- tickTextWidth + Y_AXIS_TICK_PADDING + exponentClearance + Y_AXIS_OUTER_PADDING
115
+ .map((group) => axisTextMetrics(group.domain, !group.fixed).tickTextWidth)
116
+ const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
117
+ const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
138
118
  const labelCenterX = -(
139
119
  Y_AXIS_TICK_PADDING +
140
120
  tickTextWidth +
141
- exponentClearance +
142
121
  Y_AXIS_LABEL_GAP +
143
122
  Y_AXIS_LABEL_BAND_WIDTH / 2
144
123
  )
145
124
  const fullClearance =
146
125
  tickTextWidth +
147
126
  Y_AXIS_TICK_PADDING +
148
- exponentClearance +
149
127
  Y_AXIS_LABEL_GAP +
150
128
  Y_AXIS_LABEL_BAND_WIDTH +
151
129
  Y_AXIS_OUTER_PADDING
@@ -307,6 +285,7 @@ export function useWaveformLayout(context: LayoutContext) {
307
285
  )
308
286
  : sharedYDomains.value,
309
287
  timeUnit: props.timeUnit,
288
+ xAxisLabelFormatter: props.axes?.x?.labelFormatter,
310
289
  rendering: renderingOptions.value,
311
290
  hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
312
291
  yAxisLabelX: yAxisMetrics.value.labelCenterX,
@@ -91,6 +91,22 @@ describe('WaveformTooltip', () => {
91
91
  expect(wrapper.get('.waveform-tooltip__value').text()).toBe('-1,405.4932 A')
92
92
  })
93
93
 
94
+ it('formats tooltip time with at most four decimal places', () => {
95
+ const wrapper = mount(WaveformTooltip, {
96
+ props: {
97
+ visible: true,
98
+ position: { x: 10, y: 10 },
99
+ timeUnit: 's',
100
+ hoveredPoint: { x: 1.234567, y: 12 },
101
+ seriesPoints: [],
102
+ containerWidth: 400,
103
+ containerHeight: 300,
104
+ },
105
+ })
106
+
107
+ expect(wrapper.get('.waveform-tooltip__time').text()).toBe('s: 1.2346')
108
+ })
109
+
94
110
  it('omits the error label when both resolved errors are zero', () => {
95
111
  const point = { x: 1, y: 12 }
96
112
  const wrapper = mount(WaveformTooltip, {