waveform-analysis 0.1.62 → 0.1.64

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 (83) hide show
  1. package/README.md +9 -2
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +953 -946
  4. package/dist/style.css +1 -1
  5. package/dist/types/types/chart.d.ts +2 -1
  6. package/package.json +1 -1
  7. package/src/components/WaveformChart.vue +1 -1
  8. package/src/components/core/xDomain.ts +26 -2
  9. package/src/components/rendering/WaveformLegend.vue +1 -1
  10. package/src/components/rendering/WaveformSeriesLayer.vue +1 -1
  11. package/src/demo/DemoChartHost.vue +1 -0
  12. package/src/types/chart.ts +2 -1
  13. package/src/App.test.ts +0 -400
  14. package/src/components/annotation/components.test.ts +0 -372
  15. package/src/components/annotation/markup.test.ts +0 -371
  16. package/src/components/annotation/serialization.test.ts +0 -114
  17. package/src/components/core/constants.test.ts +0 -29
  18. package/src/components/core/fixedYDomainLayout.test.ts +0 -204
  19. package/src/components/core/grid.test.ts +0 -197
  20. package/src/components/core/latestTaskScheduler.test.ts +0 -51
  21. package/src/components/core/layout.test.ts +0 -373
  22. package/src/components/core/title.test.ts +0 -76
  23. package/src/components/core/useWaveformData.test.ts +0 -95
  24. package/src/components/core/useWaveformRenderSampling.test.ts +0 -17
  25. package/src/components/core/xDomain.test.ts +0 -28
  26. package/src/components/core/yAxisNice.test.ts +0 -174
  27. package/src/components/core/yAxisSlotLayout.test.ts +0 -165
  28. package/src/components/core/yDomain.test.ts +0 -56
  29. package/src/components/interaction/WaveformTooltip.test.ts +0 -276
  30. package/src/components/interaction/viewportInteractionState.test.ts +0 -101
  31. package/src/components/utils/useAnimationFrameThrottle.test.ts +0 -53
  32. package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +0 -257
  33. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +0 -358
  34. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +0 -358
  35. package/src/components/waveformChartCases/dataLifecycle.test.ts +0 -310
  36. package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +0 -389
  37. package/src/components/waveformChartCases/gridFill.test.ts +0 -95
  38. package/src/components/waveformChartCases/incompleteDataHover.test.ts +0 -155
  39. package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +0 -328
  40. package/src/components/waveformChartCases/layoutAndDomains.test.ts +0 -371
  41. package/src/components/waveformChartCases/legendVisibility.test.ts +0 -350
  42. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +0 -400
  43. package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +0 -61
  44. package/src/components/waveformChartCases/presentationAndAxes.test.ts +0 -345
  45. package/src/components/waveformChartCases/presentationMode.test.ts +0 -327
  46. package/src/components/waveformChartCases/renderingStyles.test.ts +0 -283
  47. package/src/components/waveformChartCases/sizingAndTitles.test.ts +0 -370
  48. package/src/components/waveformChartCases/trackOrder.test.ts +0 -323
  49. package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +0 -365
  50. package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -311
  51. package/src/components/waveformChartCases/viewportLifecycle.test.ts +0 -132
  52. package/src/components/waveformChartCases/viewportReset.test.ts +0 -251
  53. package/src/components/waveformChartCases/wheelZoomOut.test.ts +0 -114
  54. package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +0 -117
  55. package/src/components/waveformChartCases/xDomainStrategy.test.ts +0 -168
  56. package/src/components/waveformChartCases/yAxisLayout.test.ts +0 -103
  57. package/src/components/waveformChartCases/zoomIntentAndDataRebind.test.ts +0 -213
  58. package/src/components/waveformChartCases/zoomPointConstraints.test.ts +0 -308
  59. package/src/components/waveformLegendGeometry.test.ts +0 -21
  60. package/src/components/waveformNormalization.test.ts +0 -170
  61. package/src/core/data.test.ts +0 -241
  62. package/src/core/rendering.test.ts +0 -267
  63. package/src/core/renderingStrategies.test.ts +0 -90
  64. package/src/core/samplingReference.test.ts +0 -124
  65. package/src/core/wasmSampling.test.ts +0 -205
  66. package/src/core/workerSampling/multiresolution.test.ts +0 -69
  67. package/src/core/workerSampling/protocol.test.ts +0 -34
  68. package/src/core/workerSampling/repository.test.ts +0 -400
  69. package/src/core/workerSampling/repositoryCache.test.ts +0 -163
  70. package/src/core/workerSampling/repositorySampling.test.ts +0 -106
  71. package/src/core/workerSampling/repositoryTypedDataset.test.ts +0 -89
  72. package/src/core/workerSampling/repositoryWasmDataset.test.ts +0 -70
  73. package/src/data/simulatedWaveforms.test.ts +0 -60
  74. package/src/demo/plotMarginControls.test.ts +0 -29
  75. package/src/demo/xAxisLabelControls.test.ts +0 -86
  76. package/src/test/setup.ts +0 -82
  77. package/src/test/waveformChart.ts +0 -75
  78. package/src/utils/domain.test.ts +0 -28
  79. package/src/utils/formatters.test.ts +0 -131
  80. package/src/utils/sampling.test.ts +0 -226
  81. package/src/views/FixedYDomainDemo.test.ts +0 -44
  82. package/src/views/WasmSamplingDemo.test.ts +0 -13
  83. package/src/views/wasmSamplingDemoSampling.test.ts +0 -27
@@ -1,328 +0,0 @@
1
- import { DOMWrapper, flushPromises } from '@vue/test-utils'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { flushAnimationFrames, pendingAnimationFrameCount } from '../../test/setup'
5
-
6
- import { mountSizedChart } from '../../test/waveformChart'
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
-
16
- describe('WaveformChart', () => {
17
- it('zooms only the active independent track and resets when the mode changes', async () => {
18
- const wrapper = await mountSizedChart({
19
- kind: 'series',
20
- series: [
21
- {
22
- name: 'A',
23
- data: {
24
- kind: 'points',
25
- points: [
26
- { x: 0, y: 0 },
27
- { x: 2, y: 1 },
28
- ],
29
- },
30
- },
31
- {
32
- name: 'B',
33
- data: {
34
- kind: 'points',
35
- points: [
36
- { x: 0, y: 2 },
37
- { x: 2, y: 3 },
38
- ],
39
- },
40
- },
41
- ],
42
- })
43
- const endpoints = () =>
44
- wrapper.findAll('.waveform-chart__axis-endpoint--end').map((item) => item.text())
45
- const initialEndpoints = endpoints()
46
- const firstOverlay = wrapper.findAll('.waveform-chart__overlay--independent')[0]
47
- Object.defineProperty(firstOverlay.element, 'getBoundingClientRect', {
48
- value: () => ({ left: 0, top: 0, width: 712, height: 130 }),
49
- })
50
-
51
- firstOverlay.element.dispatchEvent(
52
- new WheelEvent('wheel', {
53
- deltaY: -4000,
54
- clientX: 356,
55
- clientY: 65,
56
- bubbles: true,
57
- cancelable: true,
58
- }),
59
- )
60
- flushAnimationFrames()
61
- await flushPromises()
62
-
63
- expect(endpoints()[0]).not.toBe(initialEndpoints[0])
64
- expect(endpoints()[1]).toBe(initialEndpoints[1])
65
-
66
- await wrapper.setProps({ displayMode: 'separated' })
67
- await flushPromises()
68
-
69
- expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(1)
70
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
71
- })
72
-
73
- it('updates rendering props and disables zoom interaction', async () => {
74
- const wrapper = await mountSizedChart(
75
- {
76
- kind: 'points',
77
- points: [
78
- { x: 0, y: 1 },
79
- { x: 1, y: 2 },
80
- ],
81
- },
82
- { zoomable: true, lineColor: '#ff0000' },
83
- )
84
-
85
- expect(wrapper.get('.waveform-chart__line').attributes('stroke')).toBe('#ff0000')
86
- expect(wrapper.get('.waveform-chart__overlay').classes()).toContain('is-zoomable')
87
-
88
- await wrapper.setProps({ zoomable: false, lineColor: '#00aa00' })
89
- await flushPromises()
90
-
91
- expect(wrapper.get('.waveform-chart__line').attributes('stroke')).toBe('#00aa00')
92
- expect(wrapper.get('.waveform-chart__overlay').classes()).not.toContain('is-zoomable')
93
- })
94
-
95
- it('binds zoom only while zooming is enabled', async () => {
96
- const wrapper = await mountSizedChart({
97
- kind: 'points',
98
- points: [
99
- { x: 0, y: 0 },
100
- { x: 2, y: 1 },
101
- ],
102
- })
103
- const overlay = wrapper.get('.waveform-chart__overlay')
104
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
105
- value: () => ({ left: 0, top: 0, width: 712, height: 290 }),
106
- })
107
- const initialZoomEventCount = wrapper.emitted('zoom-change')?.length ?? 0
108
- overlay.element.dispatchEvent(
109
- new WheelEvent('wheel', {
110
- deltaY: -4000,
111
- clientX: 356,
112
- clientY: 145,
113
- bubbles: true,
114
- cancelable: true,
115
- }),
116
- )
117
- overlay.element.dispatchEvent(
118
- new WheelEvent('wheel', {
119
- deltaY: -200,
120
- clientX: 356,
121
- clientY: 145,
122
- bubbles: true,
123
- cancelable: true,
124
- }),
125
- )
126
- expect(pendingAnimationFrameCount()).toBe(1)
127
- expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(initialZoomEventCount)
128
- flushAnimationFrames()
129
- await flushPromises()
130
-
131
- expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(initialZoomEventCount + 1)
132
-
133
- await wrapper.setProps({ zoomable: false })
134
- await flushPromises()
135
- const zoomEventCount = wrapper.emitted('zoom-change')?.length ?? 0
136
- overlay.element.dispatchEvent(
137
- new WheelEvent('wheel', {
138
- deltaY: -4000,
139
- clientX: 356,
140
- clientY: 145,
141
- bubbles: true,
142
- cancelable: true,
143
- }),
144
- )
145
- flushAnimationFrames()
146
- await flushPromises()
147
-
148
- expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(zoomEventCount)
149
- expect(overlay.classes()).not.toContain('is-zoomable')
150
- })
151
-
152
- it('creates a controlled annotation from externally selected annotation mode', async () => {
153
- const wrapper = await mountSizedChart(
154
- {
155
- kind: 'points',
156
- points: [
157
- { x: 0, y: 0 },
158
- { x: 1, y: 5 },
159
- ],
160
- },
161
- { interactionMode: 'annotation' },
162
- )
163
- const overlay = wrapper.get('.waveform-chart__overlay')
164
- const overlayWidth = Number(overlay.attributes('width'))
165
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
166
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
167
- })
168
- const path = wrapper.get('.waveform-chart__line').attributes('d') ?? ''
169
- const endpoint = path.match(/L([\d.-]+),([\d.-]+)$/)
170
- expect(endpoint).not.toBeNull()
171
-
172
- expect(wrapper.attributes('data-interaction-mode')).toBe('annotation')
173
- overlay.element.dispatchEvent(
174
- new MouseEvent('click', {
175
- clientX: Number(endpoint?.[1]),
176
- clientY: Number(endpoint?.[2]),
177
- bubbles: true,
178
- cancelable: true,
179
- }),
180
- )
181
- await flushPromises()
182
-
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()
187
-
188
- const annotations = wrapper.emitted('update:annotations')?.at(-1)?.[0] as
189
- Array<{ seriesId: string; x: number; y: number; text: string }> | undefined
190
- expect(annotations).toHaveLength(1)
191
- expect(annotations?.[0]).toMatchObject({ seriesId: 'series-0', x: 1, y: 5, text: '峰值点' })
192
- expect(wrapper.emitted('annotation-create')).toHaveLength(1)
193
- expect(wrapper.attributes('data-interaction-mode')).toBe('annotation')
194
- })
195
-
196
- it('supports right-click creation anywhere in the plot without drawing an anchor or guide line', async () => {
197
- const wrapper = await mountSizedChart(
198
- {
199
- kind: 'points',
200
- points: [
201
- { x: 0, y: 0 },
202
- { x: 1, y: 5 },
203
- ],
204
- },
205
- { grid: { rowCount: 1, columnCount: 1 } },
206
- )
207
- const overlay = wrapper.get('.waveform-chart__overlay')
208
- const overlayWidth = Number(overlay.attributes('width'))
209
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
210
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
211
- })
212
-
213
- overlay.element.dispatchEvent(
214
- new MouseEvent('contextmenu', {
215
- clientX: overlayWidth / 2,
216
- clientY: 145,
217
- bubbles: true,
218
- }),
219
- )
220
- await flushPromises()
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="标注文本"]')
225
- const textareaContextMenu = new MouseEvent('contextmenu', {
226
- bubbles: true,
227
- cancelable: true,
228
- })
229
- expect(textarea.element.dispatchEvent(textareaContextMenu)).toBe(true)
230
- expect(textareaContextMenu.defaultPrevented).toBe(false)
231
- await textarea.setValue('右键标注')
232
- await editor.get('button.ant-btn-primary').trigger('click')
233
- await flushPromises()
234
-
235
- // Annotation snaps to nearest sample point (x=1, y=5)
236
- expect(wrapper.emitted('update:annotations')?.at(-1)?.[0]).toMatchObject([
237
- { seriesId: 'series-0', x: 1, y: 5 },
238
- ])
239
- await wrapper.setProps({
240
- annotations: [{ id: 'right-click', seriesId: 'series-0', x: 1, y: 5, text: '右键标注' }],
241
- })
242
- expect(wrapper.find('.waveform-annotation__vertical-line').exists()).toBe(false)
243
- expect(wrapper.find('.waveform-annotation__anchor').exists()).toBe(false)
244
- expect(wrapper.get('.waveform-annotation__arrow').attributes('x2')).not.toBe(
245
- String(overlayWidth / 2),
246
- )
247
- })
248
-
249
- it('limits modal channel options to the graph frame under the pointer', async () => {
250
- const wrapper = await mountSizedChart(
251
- {
252
- kind: 'series',
253
- series: [
254
- {
255
- id: 'channel-a',
256
- name: '通道 A',
257
- data: {
258
- kind: 'points',
259
- points: [
260
- { x: 0, y: 0 },
261
- { x: 1, y: 1 },
262
- ],
263
- },
264
- },
265
- {
266
- id: 'channel-b',
267
- name: '通道 B',
268
- data: {
269
- kind: 'points',
270
- points: [
271
- { x: 0, y: 10 },
272
- { x: 1, y: 11 },
273
- ],
274
- },
275
- },
276
- ],
277
- },
278
- { displayMode: 'separated' },
279
- )
280
- const overlays = wrapper.findAll('.waveform-chart__overlay--shared')
281
- expect(overlays.length).toBeGreaterThan(0)
282
- Object.defineProperty(overlays[0].element, 'getBoundingClientRect', {
283
- value: () => ({ left: 0, top: 0, width: 712, height: 290 }),
284
- })
285
-
286
- overlays[0].element.dispatchEvent(
287
- new MouseEvent('contextmenu', { clientX: 356, clientY: 30, bubbles: true }),
288
- )
289
- await flushPromises()
290
- expect(
291
- getAnnotationEditor()
292
- .get('select[aria-label="选择标注波形"]')
293
- .findAll('option')
294
- .map((item) => item.text()),
295
- ).toEqual(['通道 A'])
296
-
297
- await getAnnotationEditor().get('.ant-modal-close').trigger('click')
298
- overlays[0].element.dispatchEvent(
299
- new MouseEvent('contextmenu', { clientX: 356, clientY: 230, bubbles: true }),
300
- )
301
- await flushPromises()
302
- expect(
303
- getAnnotationEditor()
304
- .get('select[aria-label="选择标注波形"]')
305
- .findAll('option')
306
- .map((item) => item.text()),
307
- ).toEqual(['通道 B'])
308
- })
309
-
310
- it('intelligently chooses placement to avoid clipping at boundaries', async () => {
311
- const wrapper = await mountSizedChart(
312
- {
313
- kind: 'points',
314
- points: [
315
- { x: 0, y: 0 },
316
- { x: 1, y: 5 },
317
- ],
318
- },
319
- { annotations: [{ id: 'top-edge', seriesId: 'series-0', x: 0.5, y: 5, text: '顶部标注' }] },
320
- )
321
-
322
- // Smart placement chooses 'bottom' when annotation is near top boundary
323
- expect(wrapper.get('.waveform-annotation').attributes('data-placement')).toBe('bottom')
324
- expect(wrapper.get('.waveform-annotation__arrow').attributes('x1')).toBe(
325
- wrapper.get('.waveform-annotation__arrow').attributes('x2'),
326
- )
327
- })
328
- })
@@ -1,371 +0,0 @@
1
- import { flushPromises } from '@vue/test-utils'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { resizeObservers } from '../../test/setup'
5
-
6
- import { gridSeries, mountSizedChart } from '../../test/waveformChart'
7
-
8
- describe('WaveformChart', () => {
9
- it('expands the Y-axis label gutter for signed values and long exponents', async () => {
10
- const wrapper = await mountSizedChart(
11
- {
12
- kind: 'series',
13
- series: Array.from({ length: 4 }, (_, index) => ({
14
- id: `wide-axis-${index}`,
15
- name: `宽范围 ${index + 1}`,
16
- data: {
17
- kind: 'points',
18
- points: [
19
- { x: 0, y: -1e120 },
20
- { x: 0.5, y: 0 },
21
- { x: 1, y: 1e120 },
22
- ],
23
- },
24
- })),
25
- },
26
- { displayMode: 'compact', grid: { rowCount: 2, columnCount: 2 } },
27
- )
28
- const tracks = wrapper.findAll('.waveform-chart__track')
29
- const labelX = Number(tracks[0].attributes('data-y-axis-label-x'))
30
- const firstWidth = Number(tracks[0].attributes('data-track-width'))
31
- const secondLeft = Number(tracks[1].attributes('data-track-left'))
32
- const labelBackgroundX = Number(
33
- tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
34
- )
35
- const longestTickLabel = Math.max(
36
- ...tracks[0].findAll('.waveform-chart__axis--y .tick text').map((tick) => tick.text().length),
37
- )
38
-
39
- expect(Math.abs(labelX)).toBeGreaterThanOrEqual(7 + longestTickLabel * 7 + 6)
40
- expect(labelBackgroundX).toBe(labelX - 6)
41
- expect(Number(wrapper.attributes('data-chart-left-margin'))).toBeGreaterThanOrEqual(80)
42
- expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(72)
43
- })
44
-
45
- it('keeps a tick-only gutter when channel labels are empty', async () => {
46
- const wrapper = await mountSizedChart(
47
- {
48
- kind: 'series',
49
- series: Array.from({ length: 2 }, (_, index) => ({
50
- id: `unnamed-${index}`,
51
- name: '',
52
- data: {
53
- kind: 'points',
54
- points: [
55
- { x: 0, y: -1e120 },
56
- { x: 1, y: 1e120 },
57
- ],
58
- },
59
- })),
60
- },
61
- { yLabel: '', grid: { rowCount: 1, columnCount: 2 } },
62
- )
63
- const tracks = wrapper.findAll('.waveform-chart__track')
64
- const firstWidth = Number(tracks[0].attributes('data-track-width'))
65
- const secondLeft = Number(tracks[1].attributes('data-track-left'))
66
-
67
- expect(wrapper.findAll('.waveform-chart__y-axis-label')).toHaveLength(0)
68
- expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(80)
69
- expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(60)
70
- })
71
-
72
- it('updates the Y-axis label position while preserving the chart gutter between pages', async () => {
73
- const wrapper = await mountSizedChart(
74
- {
75
- kind: 'series',
76
- series: [
77
- {
78
- id: 'short-axis',
79
- name: '短范围',
80
- data: {
81
- kind: 'points',
82
- points: [
83
- { x: 0, y: 0 },
84
- { x: 1, y: 1 },
85
- ],
86
- },
87
- },
88
- {
89
- id: 'wide-axis',
90
- name: '宽范围',
91
- data: {
92
- kind: 'points',
93
- points: [
94
- { x: 0, y: -1e120 },
95
- { x: 1, y: 1e120 },
96
- ],
97
- },
98
- },
99
- ],
100
- },
101
- { grid: { rowCount: 1, columnCount: 1 } },
102
- )
103
- const initialMargin = wrapper.attributes('data-chart-left-margin')
104
- const initialTrack = wrapper.get('.waveform-chart__track')
105
- const initialLongestTickLabel = Math.max(
106
- ...initialTrack
107
- .findAll('.waveform-chart__axis--y .tick text')
108
- .map((tick) => tick.text().length),
109
- )
110
- expect(initialTrack.attributes('data-y-axis-label-x')).toBe(
111
- String(-(7 + initialLongestTickLabel * 7 + 6)),
112
- )
113
-
114
- await wrapper.get('.ant-pagination-next button').trigger('click')
115
-
116
- expect(wrapper.attributes('data-chart-left-margin')).toBe(initialMargin)
117
- const pagedTrack = wrapper.get('.waveform-chart__track')
118
- const pagedLongestTickLabel = Math.max(
119
- ...pagedTrack
120
- .findAll('.waveform-chart__axis--y .tick text')
121
- .map((tick) => tick.text().length),
122
- )
123
- expect(pagedTrack.attributes('data-y-axis-label-x')).toBe(
124
- String(-(7 + pagedLongestTickLabel * 7 + 6)),
125
- )
126
- })
127
-
128
- it('hides only secondary-column Y-axis labels when the grid is too narrow', async () => {
129
- const wrapper = await mountSizedChart(gridSeries(4), {
130
- displayMode: 'independent',
131
- grid: { rowCount: 2, columnCount: 2 },
132
- })
133
-
134
- resizeObservers.at(-1)?.resize(120, 360)
135
- await flushPromises()
136
-
137
- expect(wrapper.findAll('.waveform-chart__y-axis-label')).toHaveLength(2)
138
- expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(4)
139
- const tracks = wrapper.findAll('.waveform-chart__track')
140
- const firstWidth = Number(tracks[0].attributes('data-track-width'))
141
- const secondLeft = Number(tracks[1].attributes('data-track-left'))
142
- expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(32)
143
- })
144
-
145
- it('uses one shared overlay and bottom-row x axes for separated and compact grids', async () => {
146
- for (const displayMode of ['separated', 'compact'] as const) {
147
- const wrapper = await mountSizedChart(gridSeries(4), {
148
- displayMode,
149
- grid: { rowCount: 2, columnCount: 2 },
150
- })
151
- expect(wrapper.findAll('.waveform-chart__overlay--shared')).toHaveLength(1)
152
- expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(2)
153
- const tracks = wrapper.findAll('.waveform-chart__track')
154
- const firstTop = Number(tracks[0].attributes('data-track-top'))
155
- const secondRowTop = Number(tracks[2].attributes('data-track-top'))
156
- const firstHeight = Number(tracks[0].attributes('data-track-height'))
157
- if (displayMode === 'compact') expect(secondRowTop).toBe(firstTop + firstHeight)
158
- else expect(secondRowTop).toBeGreaterThan(firstTop + firstHeight)
159
- }
160
- })
161
-
162
- it('keeps unused compact cells visually empty while preserving bottom X axes', async () => {
163
- const data = gridSeries(4)
164
- if (data.kind === 'series') {
165
- data.series.forEach((series, index) => {
166
- series.data = {
167
- kind: 'points',
168
- points: [
169
- { x: 100, y: index },
170
- { x: 200, y: index + 1 },
171
- ],
172
- }
173
- })
174
- }
175
- const wrapper = await mountSizedChart(data, {
176
- displayMode: 'compact',
177
- grid: { rowCount: 2, columnCount: 3 },
178
- })
179
-
180
- const tracks = wrapper.findAll('.waveform-chart__track')
181
- const emptyTracks = wrapper.findAll('.waveform-chart__track--empty')
182
- const firstTrackTop = Number(tracks[0].attributes('data-track-top'))
183
- const firstTrackHeight = Number(tracks[0].attributes('data-track-height'))
184
- const secondRowFirstTrackTop = Number(tracks[3].attributes('data-track-top'))
185
-
186
- expect(tracks).toHaveLength(6)
187
- expect(emptyTracks).toHaveLength(2)
188
- expect(wrapper.find('.waveform-chart__grid-slot-placeholder').exists()).toBe(false)
189
- expect(secondRowFirstTrackTop).toBe(firstTrackTop + firstTrackHeight)
190
- expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(3)
191
- expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(4)
192
- expect(emptyTracks[0].findAll('.waveform-chart__grid')).toHaveLength(0)
193
- expect(emptyTracks[0].find('.waveform-chart__plot-background').exists()).toBe(false)
194
- expect(emptyTracks[0].find('.waveform-chart__plot-frame').exists()).toBe(false)
195
- expect(emptyTracks[0].find('.waveform-chart__axis--y').exists()).toBe(false)
196
- expect(emptyTracks[0].find('.waveform-chart__line').exists()).toBe(false)
197
- expect(emptyTracks[0].find('.waveform-chart__y-axis-label').exists()).toBe(false)
198
- expect(emptyTracks[0].find('.waveform-chart__watermark').exists()).toBe(false)
199
-
200
- const populatedBottomTrack = tracks[3]
201
- expect(emptyTracks[0].get('.waveform-chart__axis-endpoint--start').text()).toBe(
202
- populatedBottomTrack.get('.waveform-chart__axis-endpoint--start').text(),
203
- )
204
- expect(emptyTracks[0].get('.waveform-chart__axis-endpoint--end').text()).toBe(
205
- populatedBottomTrack.get('.waveform-chart__axis-endpoint--end').text(),
206
- )
207
- })
208
-
209
- it('shows the global empty state when compact data has no valid channels', async () => {
210
- const wrapper = await mountSizedChart(
211
- { kind: 'samples', values: [1], sampleRate: -1 },
212
- { displayMode: 'compact', grid: { rowCount: 2, columnCount: 3 } },
213
- )
214
-
215
- expect(wrapper.get('.waveform-chart__empty').text()).toContain('暂无有效波形数据')
216
- expect(wrapper.findAll('.waveform-chart__track')).toHaveLength(0)
217
- expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(0)
218
- expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(0)
219
- })
220
-
221
- it('resets the page when the grid configuration changes', async () => {
222
- const wrapper = await mountSizedChart(gridSeries(5), {
223
- grid: { rowCount: 1, columnCount: 1 },
224
- })
225
- await wrapper.get('.ant-pagination-next button').trigger('click')
226
- expect(wrapper.get('.ant-pagination-item-2').classes()).toContain('ant-pagination-item-active')
227
-
228
- await wrapper.setProps({ grid: { rowCount: 2, columnCount: 1 } })
229
- await flushPromises()
230
- expect(wrapper.get('.ant-pagination-item-1').classes()).toContain('ant-pagination-item-active')
231
- })
232
-
233
- it('keeps the shared x domain stable while paging separated and compact grids', async () => {
234
- for (const displayMode of ['separated', 'compact'] as const) {
235
- const wrapper = await mountSizedChart(gridSeries(3), {
236
- displayMode,
237
- grid: { rowCount: 1, columnCount: 1 },
238
- })
239
- const initialEnd = wrapper.get('.waveform-chart__axis-endpoint--end').text()
240
- await wrapper.get('.ant-pagination-next button').trigger('click')
241
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe(initialEnd)
242
- }
243
- })
244
-
245
- it('uses the shared initial x domain for every independent track by default', async () => {
246
- const wrapper = await mountSizedChart(
247
- {
248
- kind: 'series',
249
- series: [
250
- {
251
- id: 'narrow-track',
252
- name: '窄范围',
253
- data: {
254
- kind: 'points',
255
- points: [
256
- { x: 0, y: 0 },
257
- { x: 0.006, y: 1 },
258
- ],
259
- },
260
- },
261
- {
262
- id: 'wide-track',
263
- name: '宽范围',
264
- data: {
265
- kind: 'points',
266
- points: [
267
- { x: -8, y: 0 },
268
- { x: 5, y: 1 },
269
- ],
270
- },
271
- },
272
- ],
273
- },
274
- {
275
- displayMode: 'independent',
276
- grid: { rowCount: 1, columnCount: 2 },
277
- initialXDomain: [-8, 5],
278
- },
279
- )
280
-
281
- const tracks = wrapper.findAll('.waveform-chart__track')
282
- expect(tracks[0]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-8000')
283
- expect(tracks[0]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
284
- expect(tracks[1]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-8000')
285
- expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
286
- })
287
-
288
- it('uses an explicit initial x domain override for an independent track', async () => {
289
- const wrapper = await mountSizedChart(
290
- {
291
- kind: 'series',
292
- series: [
293
- {
294
- id: 'narrow-track',
295
- name: '窄范围',
296
- data: {
297
- kind: 'points',
298
- points: [
299
- { x: 0, y: 0 },
300
- { x: 0.006, y: 1 },
301
- ],
302
- },
303
- },
304
- ],
305
- },
306
- {
307
- displayMode: 'independent',
308
- initialXDomain: [-8, 5],
309
- initialXDomains: { 'narrow-track': [0, 1] },
310
- },
311
- )
312
-
313
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
314
- })
315
-
316
- it('reacts to nice fixed Y-domain props and returns to automatic bounds', async () => {
317
- const wrapper = await mountSizedChart({
318
- kind: 'points',
319
- points: [
320
- { x: 0, y: 0 },
321
- { x: 1, y: 100 },
322
- ],
323
- })
324
- const yTickLabels = () =>
325
- wrapper
326
- .get('.waveform-chart__axis--y')
327
- .findAll('.tick text')
328
- .map((tick) => tick.text())
329
-
330
- await wrapper.setProps({ yDomain: [3, 97] })
331
- await flushPromises()
332
- expect(yTickLabels()).toContain('0')
333
- expect(yTickLabels()).toContain('100')
334
-
335
- await wrapper.setProps({ yDomain: undefined })
336
- await flushPromises()
337
- expect(yTickLabels()).not.toContain('3')
338
- expect(yTickLabels()).not.toContain('97')
339
- })
340
-
341
- it('keeps annotations bound to their channel while paging', async () => {
342
- const wrapper = await mountSizedChart(gridSeries(3), {
343
- grid: { rowCount: 1, columnCount: 1 },
344
- annotations: [
345
- { id: 'channel-2-note', seriesId: 'channel-1', x: 1, y: 2, text: '当前页标注' },
346
- ],
347
- })
348
- expect(wrapper.find('[data-annotation-id="channel-2-note"]').exists()).toBe(false)
349
- await wrapper.get('.ant-pagination-next button').trigger('click')
350
- expect(wrapper.find('[data-annotation-id="channel-2-note"]').exists()).toBe(true)
351
- })
352
-
353
- it('renders a path for sample data and responds to width changes', async () => {
354
- const wrapper = await mountSizedChart({
355
- kind: 'samples',
356
- values: [0, 1, -1, 0.5],
357
- sampleRate: 4,
358
- })
359
- const initialPath = wrapper.get('.waveform-chart__line').attributes('d')
360
-
361
- expect(initialPath).toContain('L')
362
- expect(wrapper.get('.waveform-chart__svg').attributes('width')).toBe('800')
363
- expect(wrapper.attributes('data-interaction-mode')).toBeUndefined()
364
-
365
- resizeObservers.at(-1)?.resize(500, 360)
366
- await flushPromises()
367
-
368
- expect(wrapper.get('.waveform-chart__svg').attributes('width')).toBe('500')
369
- expect(wrapper.get('.waveform-chart__line').attributes('d')).not.toBe(initialPath)
370
- })
371
- })