waveform-analysis 0.1.36 → 0.1.38

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 (31) hide show
  1. package/README.md +34 -33
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +902 -806
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/annotation/WaveformAnnotationEditor.vue.d.ts +4 -0
  6. package/dist/types/components/annotation/useWaveformChartAnnotations.d.ts +3 -1
  7. package/dist/types/components/core/constants.d.ts +3 -1
  8. package/dist/types/components/core/useWaveformChartController.d.ts +2 -0
  9. package/package.json +1 -1
  10. package/src/App.test.ts +4 -3
  11. package/src/App.vue +1 -1
  12. package/src/components/WaveformChartView.vue +10 -1
  13. package/src/components/annotation/WaveformAnnotationEditor.css +35 -104
  14. package/src/components/annotation/WaveformAnnotationEditor.vue +87 -37
  15. package/src/components/annotation/components.test.ts +134 -20
  16. package/src/components/annotation/markup.test.ts +18 -2
  17. package/src/components/annotation/markup.ts +33 -0
  18. package/src/components/annotation/useWaveformChartAnnotations.ts +53 -12
  19. package/src/components/core/constants.test.ts +29 -0
  20. package/src/components/core/constants.ts +31 -9
  21. package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +13 -3
  22. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +40 -0
  23. package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +24 -14
  24. package/src/components/waveformChartCases/legendVisibility.test.ts +44 -19
  25. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +29 -3
  26. package/src/components/waveformChartCases/presentationMode.test.ts +26 -5
  27. package/src/components/waveformChartCases/renderingStyles.test.ts +1 -1
  28. package/src/components/waveformChartCases/xDomainStrategy.test.ts +60 -0
  29. package/src/data/simulatedWaveforms.test.ts +7 -3
  30. package/src/data/simulatedWaveforms.ts +14 -1
  31. package/src/test/waveformChart.ts +12 -0
@@ -1,10 +1,18 @@
1
- import { flushPromises } from '@vue/test-utils'
1
+ import { DOMWrapper, flushPromises } from '@vue/test-utils'
2
2
  import { describe, expect, it } from 'vitest'
3
3
 
4
4
  import { flushAnimationFrames, pendingAnimationFrameCount } from '../../test/setup'
5
5
 
6
6
  import { mountSizedChart } from '../../test/waveformChart'
7
7
 
8
+ function getAnnotationEditor() {
9
+ const editor = Array.from(document.body.querySelectorAll<HTMLElement>('[role="dialog"]'))
10
+ .filter((element) => element.closest('.waveform-annotation-editor'))
11
+ .at(-1)
12
+ if (!editor) throw new Error('Expected annotation editor modal to be mounted')
13
+ return new DOMWrapper(editor)
14
+ }
15
+
8
16
  describe('WaveformChart', () => {
9
17
  it('zooms only the active independent track and resets when the mode changes', async () => {
10
18
  const wrapper = await mountSizedChart({
@@ -172,9 +180,10 @@ describe('WaveformChart', () => {
172
180
  )
173
181
  await flushPromises()
174
182
 
175
- expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
176
- await wrapper.get('textarea[aria-label="标注文本"]').setValue('峰值点')
177
- await wrapper.get('.waveform-annotation-editor button.is-primary').trigger('click')
183
+ const editor = getAnnotationEditor()
184
+ await editor.get('textarea[aria-label="标注文本"]').setValue('峰值点')
185
+ await editor.get('button.ant-btn-primary').trigger('click')
186
+ await flushPromises()
178
187
 
179
188
  const annotations = wrapper.emitted('update:annotations')?.at(-1)?.[0] as
180
189
  Array<{ seriesId: string; x: number; y: number; text: string }> | undefined
@@ -209,10 +218,10 @@ describe('WaveformChart', () => {
209
218
  }),
210
219
  )
211
220
  await flushPromises()
212
- expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
213
- expect(wrapper.get('.waveform-annotation-editor').attributes('aria-modal')).toBe('true')
214
- expect(wrapper.find('.waveform-annotation-editor__panel').exists()).toBe(true)
215
- const textarea = wrapper.get('textarea[aria-label="标注文本"]')
221
+ const editor = getAnnotationEditor()
222
+ expect(editor.attributes('role')).toBe('dialog')
223
+ expect(editor.find('.waveform-annotation-editor__content').exists()).toBe(true)
224
+ const textarea = editor.get('textarea[aria-label="标注文本"]')
216
225
  const textareaContextMenu = new MouseEvent('contextmenu', {
217
226
  bubbles: true,
218
227
  cancelable: true,
@@ -220,7 +229,8 @@ describe('WaveformChart', () => {
220
229
  expect(textarea.element.dispatchEvent(textareaContextMenu)).toBe(true)
221
230
  expect(textareaContextMenu.defaultPrevented).toBe(false)
222
231
  await textarea.setValue('右键标注')
223
- await wrapper.get('.waveform-annotation-editor button.is-primary').trigger('click')
232
+ await editor.get('button.ant-btn-primary').trigger('click')
233
+ await flushPromises()
224
234
 
225
235
  // Annotation snaps to nearest sample point (x=1, y=5)
226
236
  expect(wrapper.emitted('update:annotations')?.at(-1)?.[0]).toMatchObject([
@@ -278,20 +288,20 @@ describe('WaveformChart', () => {
278
288
  )
279
289
  await flushPromises()
280
290
  expect(
281
- wrapper
282
- .find('select[aria-label="选择标注波形"]')
291
+ getAnnotationEditor()
292
+ .get('select[aria-label="选择标注波形"]')
283
293
  .findAll('option')
284
294
  .map((item) => item.text()),
285
295
  ).toEqual(['通道 A'])
286
296
 
287
- await wrapper.get('button[aria-label="关闭标注编辑器"]').trigger('click')
297
+ await getAnnotationEditor().get('.ant-modal-close').trigger('click')
288
298
  overlays[0].element.dispatchEvent(
289
299
  new MouseEvent('contextmenu', { clientX: 356, clientY: 230, bubbles: true }),
290
300
  )
291
301
  await flushPromises()
292
302
  expect(
293
- wrapper
294
- .find('select[aria-label="选择标注波形"]')
303
+ getAnnotationEditor()
304
+ .get('select[aria-label="选择标注波形"]')
295
305
  .findAll('option')
296
306
  .map((item) => item.text()),
297
307
  ).toEqual(['通道 B'])
@@ -7,14 +7,39 @@ import WaveformChartView from '../WaveformChartView.vue'
7
7
 
8
8
  import { gridSeries, mountSizedChart, visibilitySeries } from '../../test/waveformChart'
9
9
 
10
+ function annotationEditorExists() {
11
+ return Boolean(
12
+ Array.from(document.body.querySelectorAll<HTMLElement>('[role="dialog"]'))
13
+ .filter((element) => element.closest('.waveform-annotation-editor'))
14
+ .at(-1),
15
+ )
16
+ }
17
+
10
18
  describe('WaveformChart', () => {
19
+ it('shows legends when a track contains at least two series', async () => {
20
+ const oneSeries = visibilitySeries()
21
+ if (oneSeries.kind === 'series') oneSeries.series = oneSeries.series.slice(0, 1)
22
+ const withoutLegend = await mountSizedChart(oneSeries, {
23
+ grid: { rowCount: 1, columnCount: 1 },
24
+ })
25
+ expect(withoutLegend.findAll('.waveform-chart__legend')).toHaveLength(0)
26
+
27
+ const twoSeries = visibilitySeries()
28
+ if (twoSeries.kind === 'series') twoSeries.series = twoSeries.series.slice(0, 2)
29
+ const withLegend = await mountSizedChart(twoSeries, {
30
+ grid: { rowCount: 1, columnCount: 1 },
31
+ })
32
+ expect(withLegend.findAll('.waveform-chart__legend')).toHaveLength(1)
33
+ expect(withLegend.find('.waveform-chart__legend').attributes('data-position')).toBe('top-right')
34
+ })
35
+
11
36
  it('resolves legend positions by stable track id across pages', async () => {
12
37
  const wrapper = await mountSizedChart(
13
38
  {
14
39
  kind: 'series',
15
- series: Array.from({ length: 8 }, (_, index) => ({
40
+ series: Array.from({ length: 12 }, (_, index) => ({
16
41
  id: `series-${index}`,
17
- trackId: `frame-${Math.floor(index / 2)}`,
42
+ trackId: `frame-${Math.floor(index / 3)}`,
18
43
  name: `series ${index}`,
19
44
  data: {
20
45
  kind: 'points' as const,
@@ -67,9 +92,9 @@ describe('WaveformChart', () => {
67
92
  const wrapper = await mountSizedChart(
68
93
  {
69
94
  kind: 'series',
70
- series: Array.from({ length: 4 }, (_, index) => ({
95
+ series: Array.from({ length: 6 }, (_, index) => ({
71
96
  id: `series-${index}`,
72
- trackId: `frame-${Math.floor(index / 2)}`,
97
+ trackId: `frame-${Math.floor(index / 3)}`,
73
98
  name: `series ${index}`,
74
99
  data: {
75
100
  kind: 'points',
@@ -104,7 +129,7 @@ describe('WaveformChart', () => {
104
129
  })
105
130
 
106
131
  const items = wrapper.findAll('.waveform-chart__legend-item')
107
- expect(items).toHaveLength(2)
132
+ expect(items).toHaveLength(3)
108
133
  expect(items.every((item) => item.attributes('disabled') !== undefined)).toBe(true)
109
134
  expect(wrapper.get('.waveform-legend__panel').classes()).not.toContain(
110
135
  'waveform-legend__panel--interactive',
@@ -120,7 +145,7 @@ describe('WaveformChart', () => {
120
145
  overlayMode: 'multi-axis',
121
146
  })
122
147
 
123
- expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(2)
148
+ expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(3)
124
149
  expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(true)
125
150
  const annotationLayer = wrapper.get('.waveform-annotation-layer').element
126
151
  const legendLayer = wrapper.get('.waveform-chart__legend-layer').element
@@ -135,8 +160,8 @@ describe('WaveformChart', () => {
135
160
 
136
161
  expect(
137
162
  wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
138
- ).toEqual(['low'])
139
- expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(1)
163
+ ).toEqual(['low', 'mid'])
164
+ expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(2)
140
165
  expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(false)
141
166
  expect(wrapper.findAll('.waveform-chart__legend-item')[1].classes()).toContain('is-hidden')
142
167
  expect(wrapper.findAll('.waveform-chart__legend-item')[1].attributes('aria-pressed')).toBe(
@@ -162,12 +187,12 @@ describe('WaveformChart', () => {
162
187
  )
163
188
  flushAnimationFrames()
164
189
  await flushPromises()
165
- expect(wrapper.findAll('.waveform-chart__tooltip-series')).toHaveLength(1)
190
+ expect(wrapper.findAll('.waveform-chart__tooltip-series')).toHaveLength(2)
166
191
  expect(wrapper.get('.waveform-chart__tooltip-series').text()).toContain('低量程')
167
192
 
168
193
  await wrapper.findAll('.waveform-chart__legend-item')[1].trigger('click')
169
194
  await flushPromises()
170
- expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(2)
195
+ expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(3)
171
196
  expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(true)
172
197
  expect(wrapper.emitted('series-visibility-change')?.at(-1)).toEqual([
173
198
  { seriesId: 'high', visible: true, hiddenSeriesIds: [] },
@@ -183,20 +208,20 @@ describe('WaveformChart', () => {
183
208
 
184
209
  expect(
185
210
  wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
186
- ).toEqual(['low'])
211
+ ).toEqual(['low', 'mid'])
187
212
  await wrapper.findAll('.waveform-chart__legend-item')[0].trigger('click')
188
213
  expect(wrapper.emitted('update:hidden-series-ids')?.at(-1)).toEqual([
189
214
  ['high', 'temporarily-absent', 'low'],
190
215
  ])
191
216
  expect(
192
217
  wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
193
- ).toEqual(['low'])
218
+ ).toEqual(['low', 'mid'])
194
219
 
195
220
  await wrapper.setProps({ hiddenSeriesIds: ['low'] })
196
221
  await flushPromises()
197
222
  expect(
198
223
  wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
199
- ).toEqual(['high'])
224
+ ).toEqual(['high', 'mid'])
200
225
  })
201
226
 
202
227
  it('retains uncontrolled visibility by stable ID and clears removed IDs', async () => {
@@ -215,7 +240,7 @@ describe('WaveformChart', () => {
215
240
  await flushPromises()
216
241
  expect(
217
242
  wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
218
- ).toEqual(['low'])
243
+ ).toEqual(['mid', 'low'])
219
244
 
220
245
  await wrapper.setProps({
221
246
  data: {
@@ -226,12 +251,12 @@ describe('WaveformChart', () => {
226
251
  await flushPromises()
227
252
  await wrapper.setProps({ data: original })
228
253
  await flushPromises()
229
- expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(2)
254
+ expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(3)
230
255
  })
231
256
 
232
257
  it('keeps a recoverable legend and stops chart interaction when every series is hidden', async () => {
233
258
  const wrapper = await mountSizedChart(visibilitySeries(), {
234
- defaultHiddenSeriesIds: ['low', 'high'],
259
+ defaultHiddenSeriesIds: ['low', 'high', 'mid'],
235
260
  grid: { rowCount: 1, columnCount: 1 },
236
261
  legend: { interactive: true },
237
262
  overlayMode: 'multi-axis',
@@ -240,7 +265,7 @@ describe('WaveformChart', () => {
240
265
  expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(0)
241
266
  expect(wrapper.findAll('.waveform-chart__axis')).toHaveLength(0)
242
267
  expect(wrapper.findAll('.waveform-chart__overlay')).toHaveLength(0)
243
- expect(wrapper.findAll('.waveform-chart__legend-item')).toHaveLength(2)
268
+ expect(wrapper.findAll('.waveform-chart__legend-item')).toHaveLength(3)
244
269
  expect(wrapper.get('.waveform-track__no-visible-series').text()).toBe('暂无可见曲线')
245
270
 
246
271
  await wrapper.findAll('.waveform-chart__legend-item')[0].trigger('click')
@@ -273,7 +298,7 @@ describe('WaveformChart', () => {
273
298
  )
274
299
  flushAnimationFrames()
275
300
  await flushPromises()
276
- expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
301
+ expect(annotationEditorExists()).toBe(true)
277
302
 
278
303
  const controller = wrapper.getComponent(WaveformChartView).props('controller') as unknown as {
279
304
  annotationInteraction: { editorDraft: { value: { annotation: { seriesId: string } } | null } }
@@ -287,7 +312,7 @@ describe('WaveformChart', () => {
287
312
  expect(item).toBeDefined()
288
313
  await item!.trigger('click')
289
314
  await flushPromises()
290
- expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(false)
315
+ expect(annotationEditorExists()).toBe(false)
291
316
  })
292
317
 
293
318
  it('renders independent cells with separate x axes and overlays', async () => {
@@ -177,6 +177,18 @@ describe('WaveformChart', () => {
177
177
  ],
178
178
  },
179
179
  },
180
+ {
181
+ id: 'reference',
182
+ trackId: 'frame-1',
183
+ name: 'REF_CH_1',
184
+ data: {
185
+ kind: 'points',
186
+ points: [
187
+ { x: 0, y: 0.25 },
188
+ { x: 1, y: 1.25 },
189
+ ],
190
+ },
191
+ },
180
192
  ],
181
193
  },
182
194
  { frameNumber: 1, grid: { rowCount: 2, columnCount: 1 } },
@@ -186,7 +198,7 @@ describe('WaveformChart', () => {
186
198
  expect(tracks).toHaveLength(2)
187
199
  expect(
188
200
  tracks[0].findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
189
- ).toEqual(['primary', 'comparison'])
201
+ ).toEqual(['primary', 'comparison', 'reference'])
190
202
  expect(
191
203
  tracks[1].findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
192
204
  ).toEqual(['second-frame'])
@@ -203,12 +215,13 @@ describe('WaveformChart', () => {
203
215
  expect(legend.findAll('.waveform-chart__legend-item').map((item) => item.text())).toEqual([
204
216
  'BT2_2M',
205
217
  'TEST_CH_1',
218
+ 'REF_CH_1',
206
219
  ])
207
220
  expect(
208
221
  legend
209
222
  .findAll('.waveform-legend__swatch')
210
223
  .map((swatch) => swatch.get('path').attributes('stroke')),
211
- ).toEqual(['#0960bd', '#389e0d'])
224
+ ).toEqual(['#0960bd', '#2ca02c', '#d62728'])
212
225
  expect(wrapper.findAll('.waveform-chart__watermark').map((item) => item.text())).toEqual([
213
226
  '1',
214
227
  '2',
@@ -232,10 +245,11 @@ describe('WaveformChart', () => {
232
245
  await flushPromises()
233
246
 
234
247
  const tooltipSeries = wrapper.findAll('.waveform-chart__tooltip-series')
235
- expect(tooltipSeries).toHaveLength(2)
248
+ expect(tooltipSeries).toHaveLength(3)
236
249
  expect(tooltipSeries.map((item) => item.text())).toEqual([
237
250
  expect.stringContaining('BT2_2M'),
238
251
  expect.stringContaining('TEST_CH_1'),
252
+ expect.stringContaining('REF_CH_1'),
239
253
  ])
240
254
  })
241
255
 
@@ -268,6 +282,18 @@ describe('WaveformChart', () => {
268
282
  ],
269
283
  },
270
284
  },
285
+ {
286
+ id: 'third',
287
+ trackId: 'shared',
288
+ name: 'third',
289
+ data: {
290
+ kind: 'points',
291
+ points: [
292
+ { x: 0, y: 2 },
293
+ { x: 1, y: 3 },
294
+ ],
295
+ },
296
+ },
271
297
  ],
272
298
  },
273
299
  { grid: { rowCount: 1, columnCount: 1 } },
@@ -5,6 +5,14 @@ import { flushAnimationFrames } from '../../test/setup'
5
5
  import { mountSizedChart } from '../../test/waveformChart'
6
6
  import type { WaveformData, WaveformDisplayMode } from '../data/types'
7
7
 
8
+ function annotationEditorExists() {
9
+ return Boolean(
10
+ Array.from(document.body.querySelectorAll<HTMLElement>('[role="dialog"]'))
11
+ .filter((element) => element.closest('.waveform-annotation-editor'))
12
+ .at(-1),
13
+ )
14
+ }
15
+
8
16
  const presentationData: WaveformData = {
9
17
  kind: 'series',
10
18
  series: [
@@ -34,6 +42,19 @@ const presentationData: WaveformData = {
34
42
  ],
35
43
  },
36
44
  },
45
+ {
46
+ id: 'mid',
47
+ trackId: 'shared',
48
+ name: '中量程',
49
+ data: {
50
+ kind: 'points',
51
+ points: [
52
+ { x: 0, y: 50 },
53
+ { x: 1, y: 100 },
54
+ { x: 2, y: 75 },
55
+ ],
56
+ },
57
+ },
37
58
  {
38
59
  id: 'other',
39
60
  name: '其他通道',
@@ -142,9 +163,9 @@ describe('WaveformChart presentation mode', () => {
142
163
  cancelable: true,
143
164
  }),
144
165
  )
145
- wrapper.get('.waveform-chart__svg').element.dispatchEvent(
146
- new MouseEvent('dblclick', { bubbles: true, cancelable: true }),
147
- )
166
+ wrapper
167
+ .get('.waveform-chart__svg')
168
+ .element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }))
148
169
  await wrapper.get('[data-annotation-id="note"]').trigger('contextmenu', {
149
170
  clientX: width / 2,
150
171
  clientY: height / 2,
@@ -282,13 +303,13 @@ describe('WaveformChart presentation mode', () => {
282
303
  }),
283
304
  )
284
305
  await flushPromises()
285
- expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
306
+ expect(annotationEditorExists()).toBe(true)
286
307
 
287
308
  await wrapper.setProps({ presentationMode: true })
288
309
  await flushPromises()
289
310
 
290
311
  expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
291
- expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(false)
312
+ expect(annotationEditorExists()).toBe(false)
292
313
  expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
293
314
 
294
315
  await wrapper.setProps({ presentationMode: false })
@@ -181,7 +181,7 @@ describe('WaveformChart', () => {
181
181
  ])
182
182
  expect(stepSwatchPaths[0]?.attributes()).toMatchObject({
183
183
  d: 'M1 8H25',
184
- stroke: '#389e0d',
184
+ stroke: '#2ca02c',
185
185
  'stroke-width': '1.5',
186
186
  })
187
187
  expect(stepSwatchPaths[1]?.attributes()).toMatchObject({
@@ -93,4 +93,64 @@ describe('WaveformChart x domain strategy', () => {
93
93
  expect(tracks[1]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('10000')
94
94
  expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('15000')
95
95
  })
96
+
97
+ it('keeps an explicit viewport wider than the data and leaves uncovered time blank', async () => {
98
+ const wrapper = await mountSizedChart(
99
+ {
100
+ kind: 'points',
101
+ points: [
102
+ { x: 0, y: 0 },
103
+ { x: 10, y: 1 },
104
+ ],
105
+ },
106
+ { timeUnit: 's', initialXDomain: [-5, 20] },
107
+ )
108
+
109
+ const track = wrapper.get('.waveform-chart__track')
110
+ const width = Number(track.attributes('data-track-width'))
111
+ const path = track.get('.waveform-chart__line').attributes('d') ?? ''
112
+ const xCoordinates = Array.from(path.matchAll(/(?:M|L)([\d.-]+)/g)).map((match) =>
113
+ Number(match[1]),
114
+ )
115
+
116
+ expect(track.get('.waveform-chart__axis-endpoint--start').text()).toBe('-5')
117
+ expect(track.get('.waveform-chart__axis-endpoint--end').text()).toBe('20')
118
+ expect(xCoordinates.length).toBeGreaterThan(0)
119
+ expect(Math.min(...xCoordinates)).toBeGreaterThanOrEqual(0)
120
+ expect(Math.max(...xCoordinates)).toBeLessThanOrEqual(width)
121
+ })
122
+
123
+ it('applies independent explicit viewports per track, including empty time regions', async () => {
124
+ const wrapper = await mountSizedChart(
125
+ {
126
+ kind: 'series',
127
+ series: [
128
+ {
129
+ id: 'first',
130
+ name: '第一轨',
131
+ data: { kind: 'points', points: [{ x: 0, y: 0 }, { x: 10, y: 1 }] },
132
+ },
133
+ {
134
+ id: 'second',
135
+ name: '第二轨',
136
+ data: { kind: 'points', points: [{ x: 100, y: 0 }, { x: 110, y: 1 }] },
137
+ },
138
+ ],
139
+ },
140
+ {
141
+ displayMode: 'independent',
142
+ grid: { rowCount: 1, columnCount: 2 },
143
+ timeUnit: 's',
144
+ initialXDomains: { first: [-5, 20], second: [95, 120] },
145
+ },
146
+ )
147
+
148
+ const tracks = wrapper.findAll('.waveform-chart__track')
149
+ expect(tracks[0]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-5')
150
+ expect(tracks[0]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('20')
151
+ expect(tracks[1]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('95')
152
+ expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('120')
153
+ expect(tracks[0]?.find('.waveform-chart__line').exists()).toBe(true)
154
+ expect(tracks[1]?.find('.waveform-chart__line').exists()).toBe(true)
155
+ })
96
156
  })
@@ -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 six-channel data', () => {
6
+ it('creates deterministic, finite seven-channel data with a two-series second frame', () => {
7
7
  const first = createSimulatedWaveformData()
8
8
  const second = createSimulatedWaveformData()
9
9
 
@@ -11,8 +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(6)
15
- expect(new Set(first.series.map((series) => series.id)).size).toBe(6)
14
+ expect(first.series).toHaveLength(7)
15
+ expect(new Set(first.series.map((series) => series.id)).size).toBe(7)
16
+ const secondFrame = first.series.filter(
17
+ (series) => series.trackId === 'simulated-harmonic-frame',
18
+ )
19
+ expect(secondFrame).toHaveLength(2)
16
20
  expect(new Set(first.series.map((series) => series.shotNo))).toEqual(new Set(['13300']))
17
21
  first.series.forEach((series) => {
18
22
  expect(series.data.kind).toBe('points')
@@ -13,7 +13,7 @@ type ErrorGenerator = (
13
13
 
14
14
  interface SimulatedSeriesDefinition extends Pick<
15
15
  WaveformSeries,
16
- 'id' | 'shotNo' | 'name' | 'unit' | 'lineType' | 'pointType' | 'errorBar'
16
+ 'id' | 'trackId' | 'shotNo' | 'name' | 'unit' | 'color' | 'lineType' | 'pointType' | 'errorBar'
17
17
  > {
18
18
  signal: SignalGenerator
19
19
  errors?: ErrorGenerator
@@ -60,6 +60,7 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
60
60
  },
61
61
  {
62
62
  id: 'simulated-harmonic',
63
+ trackId: 'simulated-harmonic-frame',
63
64
  shotNo: '13300',
64
65
  name: '谐波扰动',
65
66
  unit: 'V',
@@ -68,6 +69,18 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
68
69
  signal: (time) =>
69
70
  0.9 * Math.sin(TWO_PI * 0.55 * time) + 0.28 * Math.sin(TWO_PI * 2.2 * time + 0.4),
70
71
  },
72
+ {
73
+ id: 'simulated-harmonic-reference',
74
+ trackId: 'simulated-harmonic-frame',
75
+ shotNo: '13300',
76
+ name: '谐波对比',
77
+ unit: 'V',
78
+ color: '#2ca02c',
79
+ lineType: 'linear',
80
+ pointType: 'none',
81
+ signal: (time) =>
82
+ 0.62 * Math.sin(TWO_PI * 0.55 * time + 0.55) + 0.18 * Math.sin(TWO_PI * 2.2 * time - 0.2),
83
+ },
71
84
  {
72
85
  id: 'simulated-damped',
73
86
  shotNo: '13300',
@@ -58,6 +58,18 @@ export function visibilitySeries(): WaveformData {
58
58
  ],
59
59
  },
60
60
  },
61
+ {
62
+ id: 'mid',
63
+ trackId: 'shared-frame',
64
+ name: '中量程',
65
+ data: {
66
+ kind: 'points',
67
+ points: [
68
+ { x: 0, y: 100 },
69
+ { x: 1, y: 200 },
70
+ ],
71
+ },
72
+ },
61
73
  ],
62
74
  }
63
75
  }