waveform-analysis 0.1.55 → 0.1.57

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.
@@ -10,6 +10,13 @@ export interface YAxisSeriesGroup {
10
10
  domain: [number, number];
11
11
  fixed: boolean;
12
12
  }
13
+ export interface YAxisSlot {
14
+ side: 'left' | 'right';
15
+ sideIndex: number;
16
+ axisOffset: number;
17
+ labelOffset: number;
18
+ clearance: number;
19
+ }
13
20
  export declare function buildYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode): YAxisSeriesGroup[];
14
21
  export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): YAxisSeriesGroup[];
15
22
  export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number): {
@@ -20,6 +27,17 @@ export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayM
20
27
  left: number;
21
28
  right: number;
22
29
  };
30
+ /**
31
+ * Reserves stable Y-axis positions for every visible track on the current page.
32
+ * Slots are ordered from the plot edge outward for each side.
33
+ */
34
+ export declare function buildYAxisSlots(tracks: readonly DisplayTrack[], overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number, nice?: boolean): {
35
+ slots: YAxisSlot[];
36
+ clearance: {
37
+ left: number;
38
+ right: number;
39
+ };
40
+ };
23
41
  type PositionedTrack = Pick<TrackLayout, 'left' | 'top' | 'width' | 'height'>;
24
42
  export declare function findClosestTrackAtPointer<T extends PositionedTrack>(tracks: readonly T[], pointerX: number, pointerY: number): T | undefined;
25
43
  export { buildTrackLayouts, type BuildTrackLayoutsOptions } from './trackLayoutBuilder';
@@ -2,6 +2,7 @@ import { type ZoomTransform } from 'd3';
2
2
  import { type ResolvedWaveformRenderingOptions } from '../../core/rendering';
3
3
  import type { WaveformDisplayMode, WaveformOverlayMode, WaveformPoint, WaveformXDomainStrategy, WaveformXAxisLabelFormatter } from '../../types';
4
4
  import { type GridCellGeometry, type NormalizedWaveformGridOptions } from './grid';
5
+ import { type YAxisSlot } from './layout';
5
6
  import type { DisplayTrack, TrackLayout } from './types';
6
7
  interface SeriesGridCell extends GridCellGeometry {
7
8
  series?: DisplayTrack;
@@ -28,6 +29,7 @@ export interface BuildTrackLayoutsOptions {
28
29
  linePointOverrides?: Readonly<Record<string, WaveformPoint[]>>;
29
30
  hideSecondaryLabels: boolean;
30
31
  yAxisLabelX: number;
32
+ yAxisSlots?: readonly YAxisSlot[];
31
33
  showCompactEmptyTracks: boolean;
32
34
  }
33
35
  export declare function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayout[];
@@ -3,4 +3,5 @@ export declare const Y_AXIS_TICK_PADDING = 7;
3
3
  export declare const Y_AXIS_OUTER_PADDING = 4;
4
4
  export declare const Y_AXIS_LABEL_GAP = 0;
5
5
  export declare const Y_AXIS_LABEL_BAND_WIDTH = 12;
6
+ export declare const Y_AXIS_RIGHT_LABEL_OFFSET = 10;
6
7
  export declare function resolveYAxisSides(axisCount: number): Array<'left' | 'right'>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
package/src/App.test.ts CHANGED
@@ -231,7 +231,7 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
231
231
  tracks.map((track) =>
232
232
  track.findAll('.waveform-chart__series').map((item) => item.attributes('data-series-name')),
233
233
  ),
234
- ).toEqual([['正弦基波'], ['谐波扰动', '谐波对比'], ['阻尼振荡'], ['阶跃响应']])
234
+ ).toEqual([['正弦基波'], ['谐波扰动', '谐波对比', '谐波校正'], ['阻尼振荡'], ['阶跃响应']])
235
235
  expect(
236
236
  tracks
237
237
  .slice(1)
@@ -250,6 +250,7 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
250
250
  '正弦基波',
251
251
  '谐波扰动',
252
252
  '谐波对比',
253
+ '谐波校正',
253
254
  '阻尼振荡',
254
255
  '阶跃响应',
255
256
  '脉冲响应',
@@ -254,7 +254,7 @@ describe('multi-value Y-axis grouping', () => {
254
254
  })),
255
255
  ).toEqual([
256
256
  { side: 'left', labelOffset: 55 },
257
- { side: 'right', labelOffset: 55 },
257
+ { side: 'right', labelOffset: 45 },
258
258
  ])
259
259
  })
260
260
 
@@ -35,6 +35,14 @@ export interface YAxisSeriesGroup {
35
35
  fixed: boolean
36
36
  }
37
37
 
38
+ export interface YAxisSlot {
39
+ side: 'left' | 'right'
40
+ sideIndex: number
41
+ axisOffset: number
42
+ labelOffset: number
43
+ clearance: number
44
+ }
45
+
38
46
  function resolveAxisSides(axisCount: number): Array<'left' | 'right'> {
39
47
  if (axisCount >= 4) return ['left', 'left', 'right', 'right']
40
48
  if (axisCount === 3) return ['left', 'right', 'right']
@@ -181,6 +189,61 @@ export function measureTrackYAxisClearance(
181
189
  )
182
190
  }
183
191
 
192
+ /**
193
+ * Reserves stable Y-axis positions for every visible track on the current page.
194
+ * Slots are ordered from the plot edge outward for each side.
195
+ */
196
+ export function buildYAxisSlots(
197
+ tracks: readonly DisplayTrack[],
198
+ overlayMode: WaveformOverlayMode,
199
+ yDomain?: WaveformYDomain,
200
+ yDomains?: Record<string, WaveformYDomain>,
201
+ tickCount?: number,
202
+ nice = true,
203
+ ): { slots: YAxisSlot[]; clearance: { left: number; right: number } } {
204
+ const widths = new Map<string, number>()
205
+ tracks.forEach((track) => {
206
+ const sideIndexes = { left: 0, right: 0 }
207
+ resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).forEach((group) => {
208
+ const sideIndex = sideIndexes[group.side]++
209
+ const key = `${group.side}:${sideIndex}`
210
+ const width = axisTextMetrics(
211
+ group.domain,
212
+ nice,
213
+ undefined,
214
+ group.seriesList[0]?.unit,
215
+ tickCount,
216
+ ).tickTextWidth
217
+ widths.set(key, Math.max(widths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, width))
218
+ })
219
+ })
220
+
221
+ const slots: YAxisSlot[] = []
222
+ const clearance = { left: 0, right: 0 }
223
+ ;(['left', 'right'] as const).forEach((side) => {
224
+ const sideIndexes = Array.from(widths.keys())
225
+ .filter((key) => key.startsWith(`${side}:`))
226
+ .map((key) => Number(key.slice(side.length + 1)))
227
+ .sort((first, second) => first - second)
228
+ sideIndexes.forEach((sideIndex) => {
229
+ const tickTextWidth = widths.get(`${side}:${sideIndex}`) ?? Y_AXIS_CHARACTER_WIDTH
230
+ const axisOffset = side === 'left' && clearance.left > 0 ? -clearance.left : clearance[side]
231
+ const labelDistance =
232
+ tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
233
+ const labelOffset = axisOffset + labelDistance * (side === 'left' ? -1 : 1)
234
+ const fullClearance =
235
+ tickTextWidth +
236
+ Y_AXIS_TICK_PADDING +
237
+ Y_AXIS_LABEL_GAP +
238
+ Y_AXIS_LABEL_BAND_WIDTH +
239
+ Y_AXIS_OUTER_PADDING
240
+ slots.push({ side, sideIndex, axisOffset, labelOffset, clearance: fullClearance })
241
+ clearance[side] += fullClearance
242
+ })
243
+ })
244
+ return { slots, clearance }
245
+ }
246
+
184
247
  type PositionedTrack = Pick<TrackLayout, 'left' | 'top' | 'width' | 'height'>
185
248
 
186
249
  export function findClosestTrackAtPointer<T extends PositionedTrack>(
@@ -25,13 +25,19 @@ import {
25
25
  type GridCellGeometry,
26
26
  type NormalizedWaveformGridOptions,
27
27
  } from './grid'
28
- import { axisTextMetrics, resolveYAxisSeriesGroups, resolveYAxisTickCount } from './layout'
28
+ import {
29
+ axisTextMetrics,
30
+ resolveYAxisSeriesGroups,
31
+ resolveYAxisTickCount,
32
+ type YAxisSlot,
33
+ } from './layout'
29
34
  import type { DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
30
35
  import { applyXDomainStrategy } from './xDomain'
31
36
  import {
32
37
  Y_AXIS_LABEL_BAND_WIDTH,
33
38
  Y_AXIS_LABEL_GAP,
34
39
  Y_AXIS_OUTER_PADDING,
40
+ Y_AXIS_RIGHT_LABEL_OFFSET,
35
41
  Y_AXIS_TICK_PADDING,
36
42
  } from './yAxisConstants'
37
43
 
@@ -61,6 +67,7 @@ export interface BuildTrackLayoutsOptions {
61
67
  linePointOverrides?: Readonly<Record<string, WaveformPoint[]>>
62
68
  hideSecondaryLabels: boolean
63
69
  yAxisLabelX: number
70
+ yAxisSlots?: readonly YAxisSlot[]
64
71
  showCompactEmptyTracks: boolean
65
72
  }
66
73
 
@@ -117,8 +124,10 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
117
124
  ).map((group) =>
118
125
  !group.fixed && configuredYDomain ? { ...group, domain: configuredYDomain } : group,
119
126
  )
127
+ const sideIndexes = { left: 0, right: 0 }
120
128
  const sideOffsets = { left: 0, right: 0 }
121
129
  const yAxes: WaveformYAxisLayout[] = yAxisGroups.map((group) => {
130
+ const sideIndex = sideIndexes[group.side]++
122
131
  const tickCount = resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber)
123
132
  const niceCount = Math.max(1, tickCount - 1)
124
133
  const scale = scaleLinear(group.domain, [cell.plotHeight, 0])
@@ -144,11 +153,22 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
144
153
  Y_AXIS_LABEL_GAP +
145
154
  Y_AXIS_LABEL_BAND_WIDTH +
146
155
  Y_AXIS_OUTER_PADDING
147
- const x = group.side === 'left' ? -sideOffsets.left : cell.width + sideOffsets.right
156
+ const slot = options.yAxisSlots?.find(
157
+ (candidate) => candidate.side === group.side && candidate.sideIndex === sideIndex,
158
+ )
159
+ const x = slot
160
+ ? (group.side === 'left' ? 0 : cell.width) + slot.axisOffset
161
+ : group.side === 'left'
162
+ ? -sideOffsets.left
163
+ : cell.width + sideOffsets.right
148
164
  const labelDistance =
149
165
  tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
150
- const labelX = x + (group.side === 'left' ? -labelDistance : labelDistance)
151
- sideOffsets[group.side] += clearance
166
+ const labelX =
167
+ (slot
168
+ ? (group.side === 'left' ? 0 : cell.width) + slot.labelOffset
169
+ : x + (group.side === 'left' ? -labelDistance : labelDistance)) -
170
+ (group.side === 'right' ? Y_AXIS_RIGHT_LABEL_OFFSET : 0)
171
+ if (!slot) sideOffsets[group.side] += clearance
152
172
  return {
153
173
  index: group.index,
154
174
  side: group.side,
@@ -27,7 +27,7 @@ import {
27
27
  import {
28
28
  buildTrackLayouts,
29
29
  axisTextMetrics,
30
- measureTrackYAxisClearance,
30
+ buildYAxisSlots,
31
31
  resolveYAxisSeriesGroups,
32
32
  } from './layout'
33
33
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
@@ -169,33 +169,24 @@ export function useWaveformLayout(context: LayoutContext) {
169
169
  : 0,
170
170
  ),
171
171
  )
172
- const multiAxisClearance = computed(() =>
173
- chartTracks.value.reduce(
174
- (maximum, track) => {
175
- const clearance = measureTrackYAxisClearance(
176
- track,
177
- props.overlayMode,
178
- props.yDomain,
179
- props.yDomains,
180
- props.axes?.y?.splitNumber,
181
- props.axes?.y?.nice !== false,
182
- )
183
- return {
184
- left: Math.max(maximum.left, clearance.left),
185
- right: Math.max(maximum.right, clearance.right),
186
- }
187
- },
188
- { left: 0, right: 0 },
172
+ const yAxisSlots = computed(() =>
173
+ buildYAxisSlots(
174
+ layoutTracks.value.filter((track) => track.visibleSeries.length > 0),
175
+ props.overlayMode,
176
+ props.yDomain,
177
+ props.yDomains,
178
+ props.axes?.y?.splitNumber,
179
+ props.axes?.y?.nice !== false,
189
180
  ),
190
181
  )
191
182
  const resolvedChartLeftMargin = computed(() =>
192
183
  props.overlayMode === 'multi-axis'
193
- ? Math.max(chartLeftMargin.value, multiAxisClearance.value.left)
184
+ ? Math.max(chartLeftMargin.value, yAxisSlots.value.clearance.left)
194
185
  : chartLeftMargin.value,
195
186
  )
196
187
  const chartRightMargin = computed(() =>
197
188
  props.overlayMode === 'multi-axis'
198
- ? Math.max(margin.right, multiAxisClearance.value.right)
189
+ ? Math.max(margin.right, yAxisSlots.value.clearance.right)
199
190
  : margin.right,
200
191
  )
201
192
  const innerWidth = computed(() =>
@@ -212,7 +203,7 @@ export function useWaveformLayout(context: LayoutContext) {
212
203
  return {
213
204
  horizontalGap:
214
205
  props.overlayMode === 'multi-axis' && hasMultipleColumns && hasVisibleWaveformData.value
215
- ? Math.max(baseGap, multiAxisClearance.value.left + multiAxisClearance.value.right)
206
+ ? Math.max(baseGap, yAxisSlots.value.clearance.left + yAxisSlots.value.clearance.right)
216
207
  : hasMultipleColumns && hasVisibleWaveformData.value
217
208
  ? hasYAxisLabels.value && canReserveLabelClearance
218
209
  ? fullGap
@@ -323,7 +314,10 @@ export function useWaveformLayout(context: LayoutContext) {
323
314
  rendering: renderingOptions.value,
324
315
  linePointOverrides: linePointOverrides?.value,
325
316
  hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
326
- yAxisLabelX: yAxisMetrics.value.labelCenterX,
317
+ yAxisLabelX:
318
+ yAxisSlots.value.slots.find((slot) => slot.side === 'left' && slot.sideIndex === 0)
319
+ ?.labelOffset ?? yAxisMetrics.value.labelCenterX,
320
+ yAxisSlots: props.overlayMode === 'multi-axis' ? yAxisSlots.value.slots : undefined,
327
321
  showCompactEmptyTracks: props.displayMode === 'compact' && hasWaveformData.value,
328
322
  }),
329
323
  )
@@ -3,6 +3,7 @@ export const Y_AXIS_TICK_PADDING = 7
3
3
  export const Y_AXIS_OUTER_PADDING = 4
4
4
  export const Y_AXIS_LABEL_GAP = 0
5
5
  export const Y_AXIS_LABEL_BAND_WIDTH = 12
6
+ export const Y_AXIS_RIGHT_LABEL_OFFSET = 10
6
7
 
7
8
  export function resolveYAxisSides(axisCount: number): Array<'left' | 'right'> {
8
9
  if (axisCount >= 4) return ['left', 'left', 'right', 'right']
@@ -0,0 +1,129 @@
1
+ import { zoomIdentity } from 'd3'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
5
+ import { buildTrackLayouts, buildYAxisSlots } from './layout'
6
+ import type { DisplaySeries, DisplayTrack } from './types'
7
+ import { Y_AXIS_RIGHT_LABEL_OFFSET } from './yAxisConstants'
8
+
9
+ function series(id: string, minimum: number, maximum: number): DisplaySeries {
10
+ return {
11
+ id,
12
+ name: id,
13
+ color: '#1677ff',
14
+ lineType: 'linear',
15
+ lineStyle: 'solid',
16
+ pointType: 'none',
17
+ errorBar: { visible: false, width: 1.5, capWidth: 8 },
18
+ points: [
19
+ { x: 0, y: minimum },
20
+ { x: 1, y: maximum },
21
+ ],
22
+ xDomain: [0, 1],
23
+ yDomain: [minimum, maximum],
24
+ hasErrorPoints: false,
25
+ }
26
+ }
27
+
28
+ function track(seriesList: DisplaySeries[]): DisplayTrack {
29
+ return {
30
+ id: seriesList[0]!.id,
31
+ series: seriesList,
32
+ visibleSeries: seriesList,
33
+ xDomain: [0, 1],
34
+ yDomain: [0, 1],
35
+ }
36
+ }
37
+
38
+ describe('Y-axis slots', () => {
39
+ it('aligns matching slots across single and multi-axis tracks', () => {
40
+ const tracks = [
41
+ track([series('single', -1e120, 1e120)]),
42
+ track([series('left', 0, 1), series('right', 1000, 3000)]),
43
+ track([series('left-two', 0, 1), series('right-two', 1000, 3000)]),
44
+ ]
45
+ const yAxisSlots = buildYAxisSlots(tracks, 'multi-axis')
46
+ const layouts = buildTrackLayouts({
47
+ cells: tracks.map((series, index) => ({
48
+ slotIndex: index,
49
+ row: index,
50
+ column: 0,
51
+ left: 0,
52
+ top: index * 100,
53
+ width: 600,
54
+ height: 100,
55
+ plotHeight: 100,
56
+ cellHeight: 130,
57
+ xAxisBand: 30,
58
+ series,
59
+ })),
60
+ grid: {
61
+ rowCount: 3,
62
+ columnCount: 1,
63
+ showPagination: false,
64
+ fillIncompleteLastRow: false,
65
+ trackLines: {},
66
+ },
67
+ displayMode: 'independent',
68
+ overlayMode: 'multi-axis',
69
+ independentTransforms: [zoomIdentity, zoomIdentity, zoomIdentity],
70
+ sharedZoomDomain: [0, 1],
71
+ timeUnit: 'ms',
72
+ rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
73
+ hideSecondaryLabels: false,
74
+ yAxisLabelX: yAxisSlots.slots.find((slot) => slot.side === 'left')!.labelOffset,
75
+ yAxisSlots: yAxisSlots.slots,
76
+ showCompactEmptyTracks: false,
77
+ })
78
+
79
+ expect(layouts.map((layout) => layout.yAxes[0]?.labelX)).toEqual([
80
+ layouts[0]?.yAxes[0]?.labelX,
81
+ layouts[0]?.yAxes[0]?.labelX,
82
+ layouts[0]?.yAxes[0]?.labelX,
83
+ ])
84
+ expect(layouts[1]?.yAxes[1]?.labelX).toBe(layouts[2]?.yAxes[1]?.labelX)
85
+ expect(layouts[1]?.yAxes[1]?.labelX).toBe(
86
+ 600 +
87
+ yAxisSlots.slots.find((slot) => slot.side === 'right')!.labelOffset -
88
+ Y_AXIS_RIGHT_LABEL_OFFSET,
89
+ )
90
+ expect(yAxisSlots.slots).toMatchObject([
91
+ { side: 'left', sideIndex: 0, axisOffset: 0 },
92
+ { side: 'right', sideIndex: 0, axisOffset: 0 },
93
+ ])
94
+ expect(yAxisSlots.clearance.left).toBeGreaterThan(yAxisSlots.clearance.right)
95
+ })
96
+
97
+ it('reserves outward second slots for three and four value axes', () => {
98
+ const threeAxisSlots = buildYAxisSlots(
99
+ [track([series('left', 0, 1), series('right-one', 10, 20), series('right-two', 30, 40)])],
100
+ 'multi-axis',
101
+ )
102
+ const fourAxisSlots = buildYAxisSlots(
103
+ [
104
+ track([
105
+ series('left-one', 0, 1),
106
+ series('left-two', 10, 20),
107
+ series('right-one', 30, 40),
108
+ series('right-two', 50, 60),
109
+ ]),
110
+ ],
111
+ 'multi-axis',
112
+ )
113
+
114
+ expect(threeAxisSlots.slots).toMatchObject([
115
+ { side: 'left', sideIndex: 0, axisOffset: 0 },
116
+ { side: 'right', sideIndex: 0, axisOffset: 0 },
117
+ { side: 'right', sideIndex: 1 },
118
+ ])
119
+ expect(threeAxisSlots.slots[2]!.axisOffset).toBeGreaterThan(0)
120
+ expect(fourAxisSlots.slots).toMatchObject([
121
+ { side: 'left', sideIndex: 0, axisOffset: 0 },
122
+ { side: 'left', sideIndex: 1 },
123
+ { side: 'right', sideIndex: 0, axisOffset: 0 },
124
+ { side: 'right', sideIndex: 1 },
125
+ ])
126
+ expect(fourAxisSlots.slots[1]!.axisOffset).toBeLessThan(0)
127
+ expect(fourAxisSlots.slots[3]!.axisOffset).toBeGreaterThan(0)
128
+ })
129
+ })
@@ -86,7 +86,7 @@ describe('WaveformChart', () => {
86
86
  expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(60)
87
87
  })
88
88
 
89
- it('keeps the Y-axis label gutter stable while paging between value ranges', async () => {
89
+ it('updates the Y-axis label position while preserving the chart gutter between pages', async () => {
90
90
  const wrapper = await mountSizedChart(
91
91
  {
92
92
  kind: 'series',
@@ -118,14 +118,12 @@ describe('WaveformChart', () => {
118
118
  { grid: { rowCount: 1, columnCount: 1 } },
119
119
  )
120
120
  const initialMargin = wrapper.attributes('data-chart-left-margin')
121
- const initialLabelX = wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')
121
+ expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-34')
122
122
 
123
123
  await wrapper.get('.ant-pagination-next button').trigger('click')
124
124
 
125
125
  expect(wrapper.attributes('data-chart-left-margin')).toBe(initialMargin)
126
- expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe(
127
- initialLabelX,
128
- )
126
+ expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-62')
129
127
  })
130
128
 
131
129
  it('hides only secondary-column Y-axis labels when the grid is too narrow', async () => {
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { mountSizedChart } from '../../test/waveformChart'
4
+
5
+ describe('WaveformChart multi-axis labels', () => {
6
+ it('aligns single-axis and multi-axis Y-axis titles in one column', async () => {
7
+ const wrapper = await mountSizedChart(
8
+ {
9
+ kind: 'series',
10
+ series: [
11
+ {
12
+ id: 'single',
13
+ trackId: 'single-track',
14
+ name: '单轴',
15
+ data: {
16
+ kind: 'points',
17
+ points: [
18
+ { x: 0, y: -1e120 },
19
+ { x: 1, y: 1e120 },
20
+ ],
21
+ },
22
+ },
23
+ {
24
+ id: 'left',
25
+ trackId: 'multi-track',
26
+ name: '左轴',
27
+ data: {
28
+ kind: 'points',
29
+ points: [
30
+ { x: 0, y: 0 },
31
+ { x: 1, y: 1 },
32
+ ],
33
+ },
34
+ },
35
+ {
36
+ id: 'right',
37
+ trackId: 'multi-track',
38
+ name: '右轴',
39
+ data: {
40
+ kind: 'points',
41
+ points: [
42
+ { x: 0, y: 1000 },
43
+ { x: 1, y: 3000 },
44
+ ],
45
+ },
46
+ },
47
+ ],
48
+ },
49
+ { overlayMode: 'multi-axis', grid: { rowCount: 2, columnCount: 1 } },
50
+ )
51
+ const tracks = wrapper.findAll('.waveform-chart__track')
52
+ const titleX = (transform: string | undefined) =>
53
+ Number(transform?.match(/translate\(([^,]+)/)?.[1])
54
+
55
+ expect(titleX(tracks[0]!.get('.waveform-chart__y-axis-label').attributes('transform'))).toBe(
56
+ titleX(
57
+ tracks[1]!.findAll('.waveform-track__multi-axis-title text')[0]!.attributes('transform'),
58
+ ),
59
+ )
60
+ })
61
+ })
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
3
3
  import { createSimulatedWaveformData } from './simulatedWaveforms'
4
4
 
5
5
  describe('simulated waveform data', () => {
6
- it('creates deterministic, finite seven-channel data with a two-series second frame', () => {
6
+ it('creates deterministic, finite eight-channel data with a three-series second frame', () => {
7
7
  const first = createSimulatedWaveformData()
8
8
  const second = createSimulatedWaveformData()
9
9
 
@@ -11,12 +11,12 @@ describe('simulated waveform data', () => {
11
11
  expect(first.kind).toBe('series')
12
12
  if (first.kind !== 'series') return
13
13
 
14
- expect(first.series).toHaveLength(7)
15
- expect(new Set(first.series.map((series) => series.id)).size).toBe(7)
14
+ expect(first.series).toHaveLength(8)
15
+ expect(new Set(first.series.map((series) => series.id)).size).toBe(8)
16
16
  const secondFrame = first.series.filter(
17
17
  (series) => series.trackId === 'simulated-harmonic-frame',
18
18
  )
19
- expect(secondFrame).toHaveLength(2)
19
+ expect(secondFrame).toHaveLength(3)
20
20
  expect(new Set(first.series.map((series) => series.shotNo))).toEqual(new Set(['13300']))
21
21
  const firstSeries = first.series[0]
22
22
  expect(firstSeries).toMatchObject({
@@ -84,6 +84,18 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
84
84
  signal: (time) =>
85
85
  0.62 * Math.sin(TWO_PI * 0.55 * time + 0.55) + 0.18 * Math.sin(TWO_PI * 2.2 * time - 0.2),
86
86
  },
87
+ {
88
+ id: 'simulated-harmonic-correction',
89
+ trackId: 'simulated-harmonic-frame',
90
+ shotNo: '13300',
91
+ name: '谐波校正',
92
+ unit: 'V',
93
+ color: '#722ed1',
94
+ lineType: 'linear',
95
+ pointType: 'none',
96
+ signal: (time) =>
97
+ 0.48 * Math.sin(TWO_PI * 0.55 * time - 0.35) + 0.12 * Math.sin(TWO_PI * 2.2 * time + 0.7),
98
+ },
87
99
  {
88
100
  id: 'simulated-damped',
89
101
  shotNo: '13300',