waveform-analysis 0.1.27 → 0.1.28

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 (28) hide show
  1. package/README.md +1 -1
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +274 -318
  4. package/dist/types/components/core/layout.d.ts +2 -4
  5. package/dist/types/components/core/types.d.ts +0 -3
  6. package/dist/types/utils/formatters.d.ts +9 -10
  7. package/dist/types/utils/index.d.ts +1 -1
  8. package/package.json +1 -1
  9. package/src/components/core/layout.test.ts +7 -8
  10. package/src/components/core/layout.ts +10 -20
  11. package/src/components/core/trackLayoutBuilder.ts +3 -23
  12. package/src/components/core/types.ts +0 -3
  13. package/src/components/core/useWaveformLayout.ts +5 -27
  14. package/src/components/interaction/WaveformTooltip.test.ts +16 -0
  15. package/src/components/rendering/WaveformTrackAxes.vue +4 -26
  16. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +7 -7
  17. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +2 -2
  18. package/src/components/waveformChartCases/dataLifecycle.test.ts +4 -4
  19. package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +29 -22
  20. package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +1 -1
  21. package/src/components/waveformChartCases/layoutAndDomains.test.ts +15 -15
  22. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +8 -8
  23. package/src/components/waveformChartCases/presentationAndAxes.test.ts +6 -4
  24. package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +8 -8
  25. package/src/components/waveformChartCases/wheelZoomOut.test.ts +3 -3
  26. package/src/utils/formatters.test.ts +40 -31
  27. package/src/utils/formatters.ts +45 -45
  28. package/src/utils/index.ts +0 -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;
@@ -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;
@@ -3,14 +3,14 @@
3
3
  */
4
4
  export type TimeUnit = 'ms' | 's';
5
5
  export interface ScientificAxisLabelOptions {
6
+ /** @deprecated Y-axis labels always use five significant digits before localization. */
6
7
  precision?: number;
7
8
  axisMin?: number;
8
9
  axisMax?: number;
10
+ topTickValue?: number;
9
11
  }
10
12
  /** @deprecated Use ScientificAxisLabelOptions. */
11
- export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions & {
12
- topTickValue?: number;
13
- };
13
+ export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions;
14
14
  /** Whether an axis magnitude should use one shared scientific exponent. */
15
15
  export declare function shouldUseScientificAxisLabel(maxAbsoluteValue: number): boolean;
16
16
  export declare function resolveScientificAxisExponent(axisMin?: number, axisMax?: number): number | null;
@@ -41,22 +41,21 @@ export declare function displayTime(value: number, timeUnit: TimeUnit): number;
41
41
  */
42
42
  export declare function endpointFractionDigits(domain: [number, number], timeUnit: TimeUnit): number;
43
43
  /**
44
- * 按完整 X 轴显示域格式化端点时间
44
+ * 将 X 轴端点时间格式化为当前显示单位下的普通整数
45
45
  * @param value 时间值(秒)
46
- * @param domain 数据域
46
+ * @param _domain 数据域(为保持现有调用签名而保留)
47
47
  * @param timeUnit 时间单位
48
48
  * @returns 格式化的时间字符串
49
49
  */
50
- export declare function formatEndpointTime(value: number, domain: [number, number], timeUnit: TimeUnit): string;
50
+ export declare function formatEndpointTime(value: number, _domain: [number, number], timeUnit: TimeUnit): string;
51
51
  /**
52
- * 按完整 X 轴显示域格式化时间刻度
52
+ * 将 X 轴时间刻度格式化为当前显示单位下的普通整数
53
53
  * @param value 时间值(秒)
54
54
  * @param timeUnit 时间单位
55
+ * @param _domain 数据域(为保持现有调用签名而保留)
55
56
  * @returns 格式化的时间字符串
56
57
  */
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;
58
+ export declare function formatAxisTime(value: number, timeUnit: TimeUnit, _domain?: [number, number]): string;
60
59
  /**
61
60
  * 格式化悬浮提示时间(4 位小数,本地化格式)
62
61
  * @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, 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.28",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -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
  )
@@ -13,7 +13,7 @@ import {
13
13
  type ResolvedWaveformRenderingOptions,
14
14
  } from '../../core/rendering'
15
15
  import type { WaveformDisplayMode, WaveformOverlayMode, WaveformPoint } from '../../types'
16
- import { buildMinorTicks, formatAxisTimeExponent, formatEndpointTime } from '../../utils'
16
+ import { buildMinorTicks, formatEndpointTime } from '../../utils'
17
17
  import {
18
18
  getBottomRowCellIndexes,
19
19
  type GridCellGeometry,
@@ -21,7 +21,6 @@ import {
21
21
  } from './grid'
22
22
  import { axisTextMetrics, resolveYAxisSeriesGroups } from './layout'
23
23
  import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
24
- import { Y_AXIS_EXPONENT_GAP } from './constants'
25
24
  import {
26
25
  Y_AXIS_LABEL_BAND_WIDTH,
27
26
  Y_AXIS_LABEL_GAP,
@@ -118,31 +117,16 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
118
117
  const tickValues = Array.from(
119
118
  new Set([axisStart, ...visibleMajorTicks, ...(showAxisEnd ? [axisEnd] : [])]),
120
119
  )
121
- const { exponentLabel, exponentWidth, tickTextWidth } = axisTextMetrics(
122
- group.domain,
123
- !group.fixed,
124
- )
125
- const exponentClearance = exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
120
+ const { tickTextWidth } = axisTextMetrics(group.domain, !group.fixed, tickValues)
126
121
  const clearance =
127
122
  tickTextWidth +
128
123
  Y_AXIS_TICK_PADDING +
129
- exponentClearance +
130
124
  Y_AXIS_LABEL_GAP +
131
125
  Y_AXIS_LABEL_BAND_WIDTH +
132
126
  Y_AXIS_OUTER_PADDING
133
127
  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
128
  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
129
+ tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
146
130
  const labelX = x + (group.side === 'left' ? -labelDistance : labelDistance)
147
131
  sideOffsets[group.side] += clearance
148
132
  return {
@@ -150,8 +134,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
150
134
  side: group.side,
151
135
  x,
152
136
  labelX,
153
- exponentX,
154
- exponentLabel,
155
137
  scale,
156
138
  majorTicks,
157
139
  minorTicks: buildMinorTicks(majorTicks),
@@ -168,7 +150,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
168
150
  start: formatEndpointTime(domain[0], domain, options.timeUnit),
169
151
  end: formatEndpointTime(domain[1], domain, options.timeUnit),
170
152
  }
171
- const xAxisExponent = formatAxisTimeExponent(domain, options.timeUnit)
172
153
  const leftClearance = endpointLabels.start.length * 7 + 10
173
154
  const rightClearance = endpointLabels.end.length * 7 + 10
174
155
  const xAxisTickValues = xMajorTicks.filter((tick) => {
@@ -235,7 +216,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
235
216
  yAxisTickValues,
236
217
  xAxisTickValues,
237
218
  endpointLabels,
238
- xAxisExponent,
239
219
  path: seriesPaths[0]?.path ?? null,
240
220
  seriesPaths,
241
221
  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
@@ -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, {
@@ -43,8 +43,11 @@ function renderAxes() {
43
43
  const element = yAxisElements.value[index]
44
44
  if (!element) return
45
45
  const [axisMin, axisMax] = axis.scale.domain()
46
+ const topTickValue = Math.max(...axis.tickValues)
46
47
  const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
47
- .tickFormat((value) => formatScientificAxisLabel(Number(value), { axisMin, axisMax }))
48
+ .tickFormat((value) =>
49
+ formatScientificAxisLabel(Number(value), { axisMin, axisMax, topTickValue }),
50
+ )
48
51
  .tickSize(-4)
49
52
  .tickPadding(7)
50
53
  .tickSizeOuter(0)
@@ -136,17 +139,6 @@ watch(
136
139
  {{ track.endpointLabels.end }}
137
140
  </text>
138
141
  </g>
139
- <text
140
- v-if="track.showXAxis && track.xAxisExponent"
141
- class="waveform-track__axis-exponent waveform-track__axis-exponent--x waveform-chart__axis-exponent waveform-chart__axis-exponent--x"
142
- :x="track.width ?? innerWidth"
143
- :y="track.height + 27"
144
- text-anchor="end"
145
- aria-hidden="true"
146
- >
147
- {{ track.xAxisExponent }}
148
- </text>
149
-
150
142
  <g
151
143
  v-for="axis in track.isEmpty ? [] : track.yAxes"
152
144
  :key="`y-axis-${track.index}-${axis.index}`"
@@ -160,20 +152,6 @@ watch(
160
152
  :data-y-axis-side="axis.side"
161
153
  :transform="`translate(${axis.x}, 0)`"
162
154
  />
163
- <text
164
- v-for="axis in track.isEmpty ? [] : track.yAxes.filter((item) => item.exponentLabel)"
165
- :key="`y-axis-exponent-${track.index}-${axis.index}`"
166
- class="waveform-track__axis-exponent waveform-track__axis-exponent--y waveform-chart__axis-exponent waveform-chart__axis-exponent--y"
167
- :data-y-axis-index="axis.index"
168
- :x="axis.exponentX"
169
- y="0"
170
- dy="0.32em"
171
- :text-anchor="axis.side === 'left' ? 'end' : 'start'"
172
- aria-hidden="true"
173
- >
174
- {{ axis.exponentLabel }}
175
- </text>
176
-
177
155
  <g
178
156
  v-if="
179
157
  !cleanView &&
@@ -72,7 +72,7 @@ describe('WaveformChart', () => {
72
72
  expect(endTicks[3]).toHaveLength(0)
73
73
  })
74
74
 
75
- it('keeps one separate shared exponent for every compact Y axis', async () => {
75
+ it('prefixes one shared exponent to the largest visible tick on every compact Y axis', async () => {
76
76
  const wrapper = await mountSizedChart(
77
77
  {
78
78
  kind: 'series',
@@ -83,7 +83,7 @@ describe('WaveformChart', () => {
83
83
  kind: 'points',
84
84
  points: [
85
85
  { x: 0, y: 0 },
86
- { x: 1, y: 254 },
86
+ { x: 1, y: 2540 },
87
87
  ],
88
88
  },
89
89
  },
@@ -103,14 +103,14 @@ describe('WaveformChart', () => {
103
103
  )
104
104
 
105
105
  const axes = wrapper.findAll('.waveform-chart__axis--y')
106
- const exponents = wrapper.findAll('.waveform-chart__axis-exponent--y')
107
106
  expect(axes).toHaveLength(2)
108
- expect(exponents.map((label) => label.text())).toEqual(['E+02', 'E-04'])
109
- axes.forEach((axis) => {
107
+ axes.forEach((axis, index) => {
110
108
  const labels = axis.findAll('.tick text').map((tick) => tick.text())
111
- expect(labels.every((label) => !label.startsWith('E'))).toBe(true)
112
- expect(labels.every((label) => /^-?\d+\.\d{2}$/.test(label))).toBe(true)
109
+ const exponentLabels = labels.filter((label) => label.startsWith('E'))
110
+ expect(exponentLabels).toHaveLength(1)
111
+ expect(exponentLabels[0]).toMatch(index === 0 ? /^E\+03 / : /^E-04 /)
113
112
  })
113
+ expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
114
114
  })
115
115
 
116
116
  it('prefers a trimmed series name and falls back to yLabel for unnamed data', async () => {
@@ -178,7 +178,7 @@ describe('WaveformChart', () => {
178
178
  await flushPromises()
179
179
  expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1, y: 5 }])
180
180
  expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
181
- expect(wrapper.get('.waveform-chart__tooltip').text()).toContain('ms: 1,000.0000')
181
+ expect(wrapper.get('.waveform-chart__tooltip-time').text()).toBe('ms: 1,000')
182
182
  const crosshairLines = wrapper.findAll('.waveform-chart__crosshair line')
183
183
  expect(crosshairLines).toHaveLength(1)
184
184
  expect(crosshairLines[0].attributes('x1')).toBe(crosshairLines[0].attributes('x2'))
@@ -303,7 +303,7 @@ describe('WaveformChart', () => {
303
303
  flushAnimationFrames()
304
304
  await flushPromises()
305
305
 
306
- expect(wrapper.get('.waveform-chart__tooltip').text()).toContain('ms: 1,000.0000')
306
+ expect(wrapper.get('.waveform-chart__tooltip-time').text()).toBe('ms: 1,000')
307
307
  expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(1)
308
308
  expect(wrapper.get('.waveform-chart__line').element).toBe(pathBeforeHover)
309
309
  expect(chartUpdate).not.toHaveBeenCalled()
@@ -62,15 +62,15 @@ describe('WaveformChart', () => {
62
62
  ],
63
63
  }
64
64
  const wrapper = await mountSizedChart(firstData)
65
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
65
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
66
66
 
67
67
  firstData.points.push({ x: 2, y: 2 })
68
68
  await flushPromises()
69
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
69
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
70
70
 
71
71
  await wrapper.setProps({ data: { ...firstData, points: [...firstData.points] } })
72
72
  await flushPromises()
73
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2.00')
73
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
74
74
  })
75
75
 
76
76
  it('keeps controlled annotations when replacing the loaded data window', async () => {
@@ -170,7 +170,7 @@ describe('WaveformChart', () => {
170
170
  expect(wrapper.findAll('.waveform-chart__track-label')).toHaveLength(0)
171
171
  expect(
172
172
  wrapper.findAll('.waveform-chart__axis-endpoint--end').map((item) => item.text()),
173
- ).toEqual(['1.00', '2.00'])
173
+ ).toEqual(['1000', '2000'])
174
174
  })
175
175
 
176
176
  it('keeps the zero Y-axis label on upper compact tracks', async () => {
@@ -162,35 +162,42 @@ describe('WaveformChart', () => {
162
162
  expect(wrapper.get('.waveform-chart__axis-endpoint--end').attributes('x')).toBe(
163
163
  String(trackWidth),
164
164
  )
165
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4.99')
166
- expect(wrapper.get('.waveform-chart__axis-exponent--x').text()).toBe('E+03')
165
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4990')
166
+ expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
167
167
  })
168
168
 
169
169
  it('uses one shared scientific exponent only for large and tiny Y-axis domains', async () => {
170
170
  const cases = [
171
- { values: [0, 50], exponent: null },
172
- { values: [0, 254], exponent: 'E+02' },
173
- { values: [0, 0.0002], exponent: 'E-04' },
171
+ { values: [0, 50], yDomain: [0, 50] as [number, number], exponent: null },
172
+ { values: [1000, 3000], yDomain: [1000, 3000] as [number, number], exponent: 'E+03' },
173
+ {
174
+ values: [0.0001, 0.0003],
175
+ yDomain: [0.0001, 0.0003] as [number, number],
176
+ exponent: 'E-04',
177
+ },
174
178
  ]
175
179
 
176
- for (const { values, exponent } of cases) {
177
- const wrapper = await mountSizedChart({
178
- kind: 'points',
180
+ for (const { values, yDomain, exponent } of cases) {
181
+ const data = {
182
+ kind: 'points' as const,
179
183
  points: values.map((y, x) => ({ x, y })),
180
- })
184
+ }
185
+ const originalValues = data.points.map((point) => point.y)
186
+ const wrapper = await mountSizedChart(data, { yDomain })
181
187
  const labels = wrapper
182
188
  .get('.waveform-chart__axis--y')
183
189
  .findAll('.tick text')
184
190
  .map((tick) => tick.text())
185
- const exponentLabel = wrapper.find('.waveform-chart__axis-exponent--y')
191
+ const exponentLabels = labels.filter((label) => label.startsWith('E'))
186
192
 
187
193
  if (exponent === null) {
188
- expect(exponentLabel.exists()).toBe(false)
194
+ expect(exponentLabels).toHaveLength(0)
189
195
  } else {
190
- expect(exponentLabel.text()).toBe(exponent)
191
- expect(labels.every((label) => !label.startsWith('E'))).toBe(true)
192
- expect(labels.every((label) => /^-?\d+\.\d{2}$/.test(label))).toBe(true)
196
+ expect(exponentLabels).toHaveLength(1)
197
+ expect(exponentLabels[0]).toMatch(new RegExp(`^${exponent.replace('+', '\\+')} `))
193
198
  }
199
+ expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
200
+ expect(data.points.map((point) => point.y)).toEqual(originalValues)
194
201
 
195
202
  wrapper.unmount()
196
203
  }
@@ -212,13 +219,13 @@ describe('WaveformChart', () => {
212
219
 
213
220
  expect(millisecondsChart.text()).toContain('时间(ms)')
214
221
  expect(millisecondTicks.length).toBeGreaterThan(0)
215
- expect(millisecondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
216
- expect(millisecondsChart.get('.waveform-chart__axis-exponent--x').text()).toBe('E+03')
222
+ expect(millisecondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
223
+ expect(millisecondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
217
224
  expect(millisecondsChart.find('.waveform-chart__watermark').exists()).toBe(false)
218
225
 
219
226
  const secondsChart = await mountSizedChart(data, { timeUnit: 's', xLabel: 'Elapsed time' })
220
227
  expect(secondsChart.text()).toContain('Elapsed time')
221
- expect(secondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
228
+ expect(secondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1')
222
229
  expect(secondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
223
230
  })
224
231
 
@@ -237,15 +244,15 @@ describe('WaveformChart', () => {
237
244
 
238
245
  expect(start.attributes('x')).toBe('0')
239
246
  expect(start.attributes('text-anchor')).toBe('start')
240
- expect(start.text()).toBe('0.00')
247
+ expect(start.text()).toBe('0')
241
248
  expect(end.attributes('x')).toBe(
242
249
  wrapper.get('.waveform-chart__track').attributes('data-track-width'),
243
250
  )
244
251
  expect(end.attributes('text-anchor')).toBe('end')
245
- expect(end.text()).toBe('2.00')
252
+ expect(end.text()).toBe('1999')
246
253
  expect(middleTickLabels.length).toBeGreaterThan(0)
247
- expect(middleTickLabels).not.toContain('0.00')
248
- expect(wrapper.get('.waveform-chart__axis-exponent--x').text()).toBe('E+03')
254
+ expect(middleTickLabels).not.toContain('0')
255
+ expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
249
256
  expect(wrapper.findAll('.waveform-chart__grid--major line').length).toBeGreaterThan(
250
257
  middleTickLabels.length,
251
258
  )
@@ -300,6 +307,6 @@ describe('WaveformChart', () => {
300
307
  )
301
308
  expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe(initialStart)
302
309
  expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).not.toBe(initialEnd)
303
- expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toContain('.')
310
+ expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toMatch(/^-?\d+$/)
304
311
  })
305
312
  })
@@ -59,7 +59,7 @@ describe('WaveformChart', () => {
59
59
  await flushPromises()
60
60
 
61
61
  expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(1)
62
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2.00')
62
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
63
63
  })
64
64
 
65
65
  it('updates rendering props and disables zoom interaction', async () => {