waveform-analysis 0.1.63 → 0.1.65

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 (97) hide show
  1. package/README.md +21 -5
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +986 -964
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/core/constants.d.ts +1 -1
  6. package/dist/types/components/core/trackLayoutBuilder.d.ts +1 -0
  7. package/dist/types/components/core/yAxisConstants.d.ts +1 -1
  8. package/dist/types/types/chart.d.ts +6 -1
  9. package/package.json +1 -1
  10. package/src/App.vue +2 -2
  11. package/src/components/WaveformChart.css +4 -0
  12. package/src/components/WaveformChart.vue +2 -2
  13. package/src/components/core/constants.ts +3 -3
  14. package/src/components/core/layout.ts +1 -1
  15. package/src/components/core/trackLayoutBuilder.ts +42 -1
  16. package/src/components/core/useWaveformLayout.ts +2 -1
  17. package/src/components/core/useWaveformPresentation.ts +1 -1
  18. package/src/components/core/xDomain.ts +26 -2
  19. package/src/components/core/yAxisConstants.ts +1 -1
  20. package/src/components/rendering/WaveformLegend.vue +1 -1
  21. package/src/components/rendering/WaveformSeriesLayer.vue +1 -1
  22. package/src/components/rendering/WaveformTrack.css +5 -2
  23. package/src/components/rendering/WaveformTrack.vue +7 -1
  24. package/src/components/rendering/WaveformTrackAxes.vue +8 -2
  25. package/src/demo/DemoChartHost.vue +1 -0
  26. package/src/types/chart.ts +6 -1
  27. package/src/App.test.ts +0 -400
  28. package/src/components/annotation/components.test.ts +0 -372
  29. package/src/components/annotation/markup.test.ts +0 -371
  30. package/src/components/annotation/serialization.test.ts +0 -114
  31. package/src/components/core/constants.test.ts +0 -29
  32. package/src/components/core/fixedYDomainLayout.test.ts +0 -204
  33. package/src/components/core/grid.test.ts +0 -197
  34. package/src/components/core/latestTaskScheduler.test.ts +0 -51
  35. package/src/components/core/layout.test.ts +0 -373
  36. package/src/components/core/title.test.ts +0 -76
  37. package/src/components/core/useWaveformData.test.ts +0 -95
  38. package/src/components/core/useWaveformRenderSampling.test.ts +0 -17
  39. package/src/components/core/xDomain.test.ts +0 -28
  40. package/src/components/core/yAxisNice.test.ts +0 -174
  41. package/src/components/core/yAxisSlotLayout.test.ts +0 -165
  42. package/src/components/core/yDomain.test.ts +0 -56
  43. package/src/components/interaction/WaveformTooltip.test.ts +0 -276
  44. package/src/components/interaction/viewportInteractionState.test.ts +0 -101
  45. package/src/components/utils/useAnimationFrameThrottle.test.ts +0 -53
  46. package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +0 -257
  47. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +0 -358
  48. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +0 -358
  49. package/src/components/waveformChartCases/dataLifecycle.test.ts +0 -310
  50. package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +0 -389
  51. package/src/components/waveformChartCases/gridFill.test.ts +0 -95
  52. package/src/components/waveformChartCases/incompleteDataHover.test.ts +0 -155
  53. package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +0 -328
  54. package/src/components/waveformChartCases/layoutAndDomains.test.ts +0 -371
  55. package/src/components/waveformChartCases/legendVisibility.test.ts +0 -350
  56. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +0 -400
  57. package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +0 -61
  58. package/src/components/waveformChartCases/presentationAndAxes.test.ts +0 -345
  59. package/src/components/waveformChartCases/presentationMode.test.ts +0 -327
  60. package/src/components/waveformChartCases/renderingStyles.test.ts +0 -283
  61. package/src/components/waveformChartCases/sizingAndTitles.test.ts +0 -370
  62. package/src/components/waveformChartCases/trackOrder.test.ts +0 -323
  63. package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +0 -365
  64. package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -311
  65. package/src/components/waveformChartCases/viewportLifecycle.test.ts +0 -132
  66. package/src/components/waveformChartCases/viewportReset.test.ts +0 -251
  67. package/src/components/waveformChartCases/wheelZoomOut.test.ts +0 -114
  68. package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +0 -117
  69. package/src/components/waveformChartCases/xDomainStrategy.test.ts +0 -168
  70. package/src/components/waveformChartCases/yAxisLayout.test.ts +0 -103
  71. package/src/components/waveformChartCases/zoomIntentAndDataRebind.test.ts +0 -213
  72. package/src/components/waveformChartCases/zoomPointConstraints.test.ts +0 -308
  73. package/src/components/waveformLegendGeometry.test.ts +0 -21
  74. package/src/components/waveformNormalization.test.ts +0 -170
  75. package/src/core/data.test.ts +0 -241
  76. package/src/core/rendering.test.ts +0 -267
  77. package/src/core/renderingStrategies.test.ts +0 -90
  78. package/src/core/samplingReference.test.ts +0 -124
  79. package/src/core/wasmSampling.test.ts +0 -205
  80. package/src/core/workerSampling/multiresolution.test.ts +0 -69
  81. package/src/core/workerSampling/protocol.test.ts +0 -34
  82. package/src/core/workerSampling/repository.test.ts +0 -400
  83. package/src/core/workerSampling/repositoryCache.test.ts +0 -163
  84. package/src/core/workerSampling/repositorySampling.test.ts +0 -106
  85. package/src/core/workerSampling/repositoryTypedDataset.test.ts +0 -89
  86. package/src/core/workerSampling/repositoryWasmDataset.test.ts +0 -70
  87. package/src/data/simulatedWaveforms.test.ts +0 -60
  88. package/src/demo/plotMarginControls.test.ts +0 -29
  89. package/src/demo/xAxisLabelControls.test.ts +0 -86
  90. package/src/test/setup.ts +0 -82
  91. package/src/test/waveformChart.ts +0 -75
  92. package/src/utils/domain.test.ts +0 -28
  93. package/src/utils/formatters.test.ts +0 -131
  94. package/src/utils/sampling.test.ts +0 -226
  95. package/src/views/FixedYDomainDemo.test.ts +0 -44
  96. package/src/views/WasmSamplingDemo.test.ts +0 -13
  97. 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
- })