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,358 +0,0 @@
1
- import { flushPromises, mount } from '@vue/test-utils'
2
- import { describe, expect, it, vi } from 'vitest'
3
-
4
- import { flushAnimationFrames, pendingAnimationFrameCount } from '../../test/setup'
5
- import WaveformChart from '../WaveformChart.vue'
6
-
7
- import { gridSeries, mountSizedChart } from '../../test/waveformChart'
8
-
9
- describe('WaveformChart', () => {
10
- it('suppresses native context menus across the waveform component', async () => {
11
- const wrapper = await mountSizedChart(gridSeries(2), {
12
- title: { text: '波形标题' },
13
- grid: { rowCount: 1, columnCount: 1, showPagination: true },
14
- })
15
-
16
- for (const selector of [
17
- '.waveform-chart__title-area',
18
- '.waveform-chart__grid',
19
- '.waveform-chart__overlay',
20
- '.waveform-chart__pagination',
21
- ]) {
22
- const event = new MouseEvent('contextmenu', { bubbles: true, cancelable: true })
23
- const dispatched = wrapper.get(selector).element.dispatchEvent(event)
24
-
25
- expect(dispatched).toBe(false)
26
- expect(event.defaultPrevented).toBe(true)
27
- }
28
-
29
- const sharedWrapper = await mountSizedChart(
30
- {
31
- kind: 'points',
32
- points: [
33
- { x: 0, y: 0 },
34
- { x: 1, y: 1 },
35
- ],
36
- },
37
- { displayMode: 'separated' },
38
- )
39
- const sharedEvent = new MouseEvent('contextmenu', { bubbles: true, cancelable: true })
40
- const sharedDispatched = sharedWrapper
41
- .get('.waveform-chart__overlay--shared')
42
- .element.dispatchEvent(sharedEvent)
43
-
44
- expect(sharedDispatched).toBe(false)
45
- expect(sharedEvent.defaultPrevented).toBe(true)
46
- })
47
-
48
- it('preserves native context menus for editable controls', async () => {
49
- const wrapper = await mountSizedChart({ kind: 'samples', values: [0, 1], sampleRate: 1 })
50
- const editableElements = [
51
- document.createElement('input'),
52
- document.createElement('textarea'),
53
- document.createElement('div'),
54
- ]
55
- editableElements[2]?.setAttribute('contenteditable', 'true')
56
-
57
- for (const element of editableElements) {
58
- wrapper.element.appendChild(element)
59
- const event = new MouseEvent('contextmenu', { bubbles: true, cancelable: true })
60
- const dispatched = element.dispatchEvent(event)
61
-
62
- expect(dispatched).toBe(true)
63
- expect(event.defaultPrevented).toBe(false)
64
- }
65
- })
66
-
67
- it('applies size fallbacks for minimum, negative, and non-finite values', async () => {
68
- const minimumWrapper = mount(WaveformChart, {
69
- props: {
70
- data: { kind: 'samples', values: [0, 1], sampleRate: 1 },
71
- width: -20,
72
- height: -20,
73
- },
74
- })
75
-
76
- expect(minimumWrapper.attributes('style')).toContain('width: 0px')
77
- expect(minimumWrapper.attributes('style')).toContain('height: 180px')
78
- expect(minimumWrapper.get('.waveform-chart__svg').attributes('height')).toBe('180')
79
-
80
- const adaptiveWrapper = mount(WaveformChart, {
81
- props: {
82
- data: { kind: 'samples', values: [0, 1], sampleRate: 1 },
83
- width: Number.POSITIVE_INFINITY,
84
- height: Number.NaN,
85
- },
86
- })
87
-
88
- expect(adaptiveWrapper.attributes('style')).toContain('width: 100%')
89
- expect(adaptiveWrapper.attributes('style')).toContain('height: 100%')
90
- })
91
-
92
- it('keeps a 100k-point SVG path bounded by the plot width', async () => {
93
- const sourcePoints = Array.from({ length: 100_000 }, (_, index) => ({
94
- x: index / 1_000,
95
- y: index === 50_001 ? 100 : Math.sin(index / 50),
96
- }))
97
- const wrapper = await mountSizedChart(
98
- { kind: 'points', points: sourcePoints },
99
- { rendering: { downsampleThreshold: 1_000, maxPointsPerPixel: 4 } },
100
- )
101
- const overlayWidth = Number(wrapper.get('.waveform-chart__overlay').attributes('width'))
102
- await vi.waitFor(
103
- () => {
104
- expect(wrapper.emitted('sampling-complete')?.length).toBeGreaterThan(0)
105
- const path = wrapper.get('.waveform-chart__line').attributes('d') ?? ''
106
- const renderedPointCount = path.match(/[ML]/g)?.length ?? 0
107
- expect(renderedPointCount).toBeGreaterThan(0)
108
- expect(renderedPointCount).toBeLessThanOrEqual(Math.floor(overlayWidth * 4) + 2)
109
- expect(path).toContain(',0')
110
- },
111
- { timeout: 5_000 },
112
- )
113
- })
114
-
115
- it('bounds dense decorations by pixel spacing while keeping one SVG path per series', async () => {
116
- const sourcePoints = Array.from({ length: 1_000 }, (_, index) => ({
117
- x: index,
118
- y: Math.sin(index / 20),
119
- error: 0.1,
120
- }))
121
- const wrapper = await mountSizedChart(
122
- {
123
- kind: 'series',
124
- series: [
125
- {
126
- id: 'dense-decorations',
127
- name: '密集标记',
128
- pointType: 'triangle',
129
- errorBar: { visible: true },
130
- data: { kind: 'points', points: sourcePoints },
131
- },
132
- ],
133
- },
134
- { rendering: { pointMinSpacing: 10, errorBarMinSpacing: 12 } },
135
- )
136
- const overlayWidth = Number(wrapper.get('.waveform-chart__overlay').attributes('width'))
137
- const pointPaths = wrapper.findAll('.waveform-chart__point')
138
- const errorBarPaths = wrapper.findAll('.waveform-chart__error-bar')
139
- const pointCount = pointPaths[0]?.attributes('d')?.match(/M/g)?.length ?? 0
140
- const errorBarCount = (errorBarPaths[0]?.attributes('d')?.match(/M/g)?.length ?? 0) / 3
141
-
142
- expect(pointPaths).toHaveLength(1)
143
- expect(errorBarPaths).toHaveLength(1)
144
- expect(pointCount).toBeLessThanOrEqual(Math.ceil(overlayWidth / 10) + 2)
145
- expect(errorBarCount).toBeLessThanOrEqual(Math.ceil(overlayWidth / 12) + 2)
146
- })
147
-
148
- it('renders explicit points and supports a single point', async () => {
149
- const wrapper = await mountSizedChart({ kind: 'points', points: [{ x: 3, y: 8 }] })
150
-
151
- expect(wrapper.get('.waveform-chart__line').attributes('d')).toContain('M')
152
- expect(wrapper.find('.waveform-chart__empty').exists()).toBe(false)
153
- })
154
-
155
- it('shows an empty state for empty and invalid data', async () => {
156
- const wrapper = await mountSizedChart({ kind: 'samples', values: [1], sampleRate: -1 })
157
-
158
- expect(wrapper.get('.waveform-chart__empty').text()).toContain('暂无有效波形数据')
159
- expect(wrapper.find('.waveform-chart__line').exists()).toBe(false)
160
- })
161
-
162
- it('emits the nearest point on hover and clears it on leave', async () => {
163
- const wrapper = await mountSizedChart(
164
- {
165
- kind: 'points',
166
- points: [
167
- { x: 0, y: 0 },
168
- { x: 1, y: 5 },
169
- ],
170
- },
171
- { grid: { rowCount: 1, columnCount: 1 } },
172
- )
173
- const overlay = wrapper.get('.waveform-chart__overlay')
174
- const overlayWidth = Number(overlay.attributes('width'))
175
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
176
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
177
- })
178
-
179
- overlay.element.dispatchEvent(
180
- new MouseEvent('pointermove', { clientX: 700, clientY: 120, bubbles: true }),
181
- )
182
- flushAnimationFrames()
183
- await flushPromises()
184
- expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1, y: 5 }])
185
- expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
186
- expect(wrapper.get('.waveform-tooltip__series-label').text()).toContain('未配置炮号:')
187
- const crosshairLines = wrapper.findAll('.waveform-chart__crosshair line')
188
- expect(crosshairLines).toHaveLength(1)
189
- expect(crosshairLines[0].attributes('x1')).toBe(crosshairLines[0].attributes('x2'))
190
- expect(crosshairLines[0].attributes('y1')).toBe('0')
191
- expect(wrapper.find('.waveform-chart__crosshair circle').exists()).toBe(false)
192
-
193
- await overlay.trigger('pointerleave')
194
- expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
195
- })
196
-
197
- it('clears hover when the pointer leaves the chart and restores it only after a new plot move', async () => {
198
- const wrapper = await mountSizedChart(
199
- {
200
- kind: 'points',
201
- points: [
202
- { x: 0, y: 0 },
203
- { x: 1, y: 5 },
204
- ],
205
- },
206
- { grid: { rowCount: 1, columnCount: 1 } },
207
- )
208
- const overlay = wrapper.get('.waveform-chart__overlay')
209
- const overlayWidth = Number(overlay.attributes('width'))
210
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
211
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
212
- })
213
-
214
- overlay.element.dispatchEvent(
215
- new MouseEvent('pointermove', { clientX: overlayWidth, clientY: 120, bubbles: true }),
216
- )
217
- flushAnimationFrames()
218
- await flushPromises()
219
- expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
220
-
221
- await wrapper.trigger('pointerleave')
222
- expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
223
- expect(wrapper.find('.waveform-chart__crosshair').exists()).toBe(false)
224
- expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
225
-
226
- await wrapper.trigger('pointerenter')
227
- expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
228
-
229
- overlay.element.dispatchEvent(
230
- new MouseEvent('pointermove', { clientX: 0, clientY: 120, bubbles: true }),
231
- )
232
- flushAnimationFrames()
233
- await flushPromises()
234
- expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
235
- })
236
-
237
- it('hides the numeric tooltip and crosshair when showTooltip is disabled', async () => {
238
- const wrapper = await mountSizedChart(
239
- {
240
- kind: 'points',
241
- points: [
242
- { x: 0, y: 0 },
243
- { x: 1, y: 5 },
244
- ],
245
- },
246
- { grid: { rowCount: 1, columnCount: 1 }, showTooltip: false },
247
- )
248
- const overlay = wrapper.get('.waveform-chart__overlay')
249
- const overlayWidth = Number(overlay.attributes('width'))
250
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
251
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
252
- })
253
-
254
- overlay.element.dispatchEvent(
255
- new MouseEvent('pointermove', { clientX: 700, clientY: 120, bubbles: true }),
256
- )
257
- flushAnimationFrames()
258
- await flushPromises()
259
-
260
- expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
261
- expect(wrapper.find('.waveform-chart__crosshair').exists()).toBe(false)
262
-
263
- await wrapper.setProps({ showTooltip: true })
264
- await flushPromises()
265
- expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
266
- expect(wrapper.find('.waveform-chart__crosshair').exists()).toBe(true)
267
- })
268
-
269
- it('coalesces pointer moves per frame and cancels pending hover work', async () => {
270
- const wrapper = await mountSizedChart(
271
- {
272
- kind: 'points',
273
- points: [
274
- { x: 0, y: 0 },
275
- { x: 1, y: 5 },
276
- ],
277
- },
278
- { grid: { rowCount: 1, columnCount: 1 } },
279
- )
280
- const overlay = wrapper.get('.waveform-chart__overlay')
281
- const overlayWidth = Number(overlay.attributes('width'))
282
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
283
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
284
- })
285
- const emittedBeforeMove = wrapper.emitted('point-hover')?.length ?? 0
286
-
287
- overlay.element.dispatchEvent(
288
- new MouseEvent('pointermove', { clientX: 0, clientY: 100, bubbles: true }),
289
- )
290
- overlay.element.dispatchEvent(
291
- new MouseEvent('pointermove', { clientX: overlayWidth, clientY: 120, bubbles: true }),
292
- )
293
-
294
- expect(pendingAnimationFrameCount()).toBe(1)
295
- expect(wrapper.emitted('point-hover')?.length ?? 0).toBe(emittedBeforeMove)
296
- flushAnimationFrames()
297
- await flushPromises()
298
- expect(wrapper.emitted('point-hover')).toHaveLength(emittedBeforeMove + 1)
299
- expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1, y: 5 }])
300
-
301
- overlay.element.dispatchEvent(
302
- new MouseEvent('pointermove', { clientX: 0, clientY: 100, bubbles: true }),
303
- )
304
- expect(pendingAnimationFrameCount()).toBe(1)
305
- await overlay.trigger('pointerleave')
306
- const emittedAfterLeave = wrapper.emitted('point-hover')?.length ?? 0
307
- expect(pendingAnimationFrameCount()).toBe(0)
308
- flushAnimationFrames()
309
- await flushPromises()
310
- expect(wrapper.emitted('point-hover')).toHaveLength(emittedAfterLeave)
311
- expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
312
-
313
- overlay.element.dispatchEvent(
314
- new MouseEvent('pointermove', { clientX: overlayWidth, clientY: 100, bubbles: true }),
315
- )
316
- expect(pendingAnimationFrameCount()).toBe(1)
317
- wrapper.unmount()
318
- expect(pendingAnimationFrameCount()).toBe(0)
319
- })
320
-
321
- it('isolates hover rendering from the chart, track, and waveform path subtrees', async () => {
322
- const wrapper = await mountSizedChart(
323
- {
324
- kind: 'points',
325
- points: [
326
- { x: 0, y: 0 },
327
- { x: 1, y: 5 },
328
- ],
329
- },
330
- { grid: { rowCount: 1, columnCount: 1 } },
331
- )
332
- const overlay = wrapper.get('.waveform-chart__overlay')
333
- const overlayWidth = Number(overlay.attributes('width'))
334
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
335
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
336
- })
337
-
338
- const track = wrapper.getComponent({ name: 'WaveformTrack' })
339
- const seriesLayer = wrapper.getComponent({ name: 'WaveformSeriesLayer' })
340
- const chartUpdate = vi.spyOn(wrapper.vm.$, 'update')
341
- const trackUpdate = vi.spyOn(track.vm.$, 'update')
342
- const seriesLayerUpdate = vi.spyOn(seriesLayer.vm.$, 'update')
343
- const pathBeforeHover = wrapper.get('.waveform-chart__line').element
344
-
345
- overlay.element.dispatchEvent(
346
- new MouseEvent('pointermove', { clientX: overlayWidth, clientY: 120, bubbles: true }),
347
- )
348
- flushAnimationFrames()
349
- await flushPromises()
350
-
351
- expect(wrapper.get('.waveform-tooltip__series-label').text()).toContain('未配置炮号:')
352
- expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(1)
353
- expect(wrapper.get('.waveform-chart__line').element).toBe(pathBeforeHover)
354
- expect(chartUpdate).not.toHaveBeenCalled()
355
- expect(trackUpdate).not.toHaveBeenCalled()
356
- expect(seriesLayerUpdate).not.toHaveBeenCalled()
357
- })
358
- })
@@ -1,310 +0,0 @@
1
- import { flushPromises } from '@vue/test-utils'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { type WaveformData } from '../waveform'
5
-
6
- import { gridSeries, mountSizedChart } from '../../test/waveformChart'
7
-
8
- describe('WaveformChart', () => {
9
- it('keeps other independent tracks unchanged when one data window is replaced', async () => {
10
- const wrapper = await mountSizedChart(gridSeries(2), {
11
- displayMode: 'independent',
12
- grid: { rowCount: 1, columnCount: 2 },
13
- })
14
- const originalEndpoints = wrapper
15
- .findAll('.waveform-chart__axis-endpoint--end')
16
- .map((endpoint) => endpoint.text())
17
-
18
- await wrapper.setProps({
19
- data: {
20
- kind: 'series',
21
- series: [
22
- {
23
- id: 'channel-0',
24
- name: '通道 1',
25
- data: {
26
- kind: 'points',
27
- points: [
28
- { x: 0.25, y: 0 },
29
- { x: 0.75, y: 1 },
30
- ],
31
- },
32
- },
33
- {
34
- id: 'channel-1',
35
- name: '通道 2',
36
- data: {
37
- kind: 'points',
38
- points: [
39
- { x: 0, y: 1 },
40
- { x: 1, y: 2 },
41
- ],
42
- },
43
- },
44
- ],
45
- },
46
- })
47
- await flushPromises()
48
-
49
- const nextEndpoints = wrapper
50
- .findAll('.waveform-chart__axis-endpoint--end')
51
- .map((endpoint) => endpoint.text())
52
- expect(nextEndpoints[0]).not.toBe(originalEndpoints[0])
53
- expect(nextEndpoints[1]).toBe(originalEndpoints[1])
54
- })
55
-
56
- it('rebuilds cached domains only when the data reference changes', async () => {
57
- const firstData: WaveformData = {
58
- kind: 'points',
59
- points: [
60
- { x: 0, y: 0 },
61
- { x: 1, y: 1 },
62
- ],
63
- }
64
- const wrapper = await mountSizedChart(firstData)
65
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
66
-
67
- firstData.points.push({ x: 2, y: 2 })
68
- await flushPromises()
69
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
70
-
71
- await wrapper.setProps({ data: { ...firstData, points: [...firstData.points] } })
72
- await flushPromises()
73
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
74
- })
75
-
76
- it('keeps controlled annotations when replacing the loaded data window', async () => {
77
- const annotations = [
78
- { id: 'window-note', seriesId: 'series-0', x: 0.5, y: 0.5, text: '窗口标注' },
79
- ]
80
- const wrapper = await mountSizedChart(
81
- {
82
- kind: 'points',
83
- points: [
84
- { x: 0, y: 0 },
85
- { x: 1, y: 1 },
86
- ],
87
- },
88
- { annotations },
89
- )
90
- expect(wrapper.find('[data-annotation-id="window-note"]').exists()).toBe(true)
91
-
92
- await wrapper.setProps({
93
- data: {
94
- kind: 'points',
95
- points: [
96
- { x: 2, y: 0 },
97
- { x: 3, y: 1 },
98
- ],
99
- },
100
- })
101
- await flushPromises()
102
-
103
- expect(wrapper.find('[data-annotation-id="window-note"]').exists()).toBe(false)
104
- expect(annotations).toEqual([
105
- { id: 'window-note', seriesId: 'series-0', x: 0.5, y: 0.5, text: '窗口标注' },
106
- ])
107
-
108
- await wrapper.setProps({
109
- data: {
110
- kind: 'points',
111
- points: [
112
- { x: 0, y: 0 },
113
- { x: 1, y: 1 },
114
- ],
115
- },
116
- })
117
- await flushPromises()
118
- expect(wrapper.find('[data-annotation-id="window-note"]').exists()).toBe(true)
119
- })
120
-
121
- it('renders named multi-channel paths as independent tracks by default', async () => {
122
- const wrapper = await mountSizedChart({
123
- kind: 'series',
124
- series: [
125
- {
126
- name: 'BT2_2M',
127
- unit: 'T',
128
- data: {
129
- kind: 'points',
130
- points: [
131
- { x: 0, y: 0 },
132
- { x: 1, y: 2 },
133
- ],
134
- },
135
- },
136
- {
137
- name: 'BT1_2M',
138
- unit: 'T',
139
- data: {
140
- kind: 'points',
141
- points: [
142
- { x: 0, y: -1 },
143
- { x: 2, y: 3 },
144
- ],
145
- },
146
- },
147
- ],
148
- })
149
- const paths = wrapper.findAll('.waveform-chart__line')
150
-
151
- expect(paths).toHaveLength(2)
152
- expect(wrapper.findAll('.waveform-chart__svg')).toHaveLength(1)
153
- expect(wrapper.findAll('.waveform-chart__track')).toHaveLength(2)
154
- expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(2)
155
- expect(wrapper.findAll('.waveform-chart__overlay--independent')).toHaveLength(2)
156
- expect(paths[0].attributes('stroke')).toBe('#0960bd')
157
- expect(paths[1].attributes('stroke')).toBe('#ff7f0e')
158
- expect(paths[0].attributes('data-series-name')).toBe('BT2_2M')
159
- const yAxisLabels = wrapper.findAll('.waveform-chart__y-axis-label')
160
- expect(yAxisLabels.map((label) => label.text())).toEqual(['BT2_2M', 'BT1_2M'])
161
- expect(
162
- wrapper
163
- .findAll('.waveform-chart__axis--y .tick text')
164
- .map((label) => label.text())
165
- .some((label) => label.includes('(T)')),
166
- ).toBe(true)
167
- expect(yAxisLabels.map((label) => label.attributes('fill'))).toEqual(['#0960bd', '#ff7f0e'])
168
- const labelX = Number(
169
- yAxisLabels[0].attributes('transform')?.match(/^translate\(([-\d.]+),/)?.[1],
170
- )
171
- expect(labelX).toBeLessThan(-46)
172
- expect(Number(wrapper.get('.waveform-chart__y-axis-label-bg').attributes('x'))).toBe(labelX - 6)
173
- expect(yAxisLabels.every((label) => !label.text().includes('(T)'))).toBe(true)
174
- expect(wrapper.findAll('.waveform-chart__track-label')).toHaveLength(0)
175
- expect(
176
- wrapper.findAll('.waveform-chart__axis-endpoint--end').map((item) => item.text()),
177
- ).toEqual(['1000', '2000'])
178
- })
179
-
180
- it('keeps the zero Y-axis label on upper compact tracks', async () => {
181
- const wrapper = await mountSizedChart(
182
- {
183
- kind: 'series',
184
- series: [
185
- {
186
- name: 'first',
187
- data: {
188
- kind: 'points',
189
- points: [
190
- { x: 0, y: 0 },
191
- { x: 1, y: 1 },
192
- ],
193
- },
194
- },
195
- {
196
- name: 'second',
197
- data: {
198
- kind: 'points',
199
- points: [
200
- { x: 0, y: -1 },
201
- { x: 1, y: 2 },
202
- ],
203
- },
204
- },
205
- ],
206
- },
207
- { displayMode: 'compact' },
208
- )
209
-
210
- const tracks = wrapper.findAll('.waveform-chart__track')
211
- const firstAxisTicks = tracks[0].findAll('.waveform-chart__axis--y .tick')
212
- const zeroTicks = firstAxisTicks.filter((tick) => Number(tick.text()) === 0)
213
- const firstTrackHeight = Number(tracks[0].attributes('data-track-height'))
214
- const zeroTickY = Number(
215
- zeroTicks[0].attributes('transform')?.match(/translate\(0,\s*([\d.]+)\)/)?.[1],
216
- )
217
-
218
- expect(zeroTicks).toHaveLength(1)
219
- expect(Math.abs(zeroTickY - firstTrackHeight)).toBeLessThanOrEqual(1)
220
- })
221
-
222
- it.each(['independent', 'separated', 'compact'] as const)(
223
- 'shows a non-zero Y-axis start value once in %s mode',
224
- async (displayMode) => {
225
- const wrapper = await mountSizedChart(
226
- {
227
- kind: 'series',
228
- series: [
229
- {
230
- name: 'first',
231
- data: {
232
- kind: 'points',
233
- points: [
234
- { x: 0, y: 0.11 },
235
- { x: 1, y: 0.89 },
236
- ],
237
- },
238
- },
239
- {
240
- name: 'second',
241
- data: {
242
- kind: 'points',
243
- points: [
244
- { x: 0, y: 4 },
245
- { x: 1, y: 5 },
246
- ],
247
- },
248
- },
249
- ],
250
- },
251
- { displayMode },
252
- )
253
-
254
- const firstTrack = wrapper.findAll('.waveform-chart__track')[0]
255
- const firstTrackHeight = Number(firstTrack.attributes('data-track-height'))
256
- const startTicks = firstTrack.findAll('.waveform-chart__axis--y .tick').filter((tick) => {
257
- const match = tick.attributes('transform')?.match(/translate\(0,\s*([\d.]+)\)/)
258
- return match ? Math.abs(Number(match[1]) - firstTrackHeight) <= 1 : false
259
- })
260
-
261
- expect(startTicks).toHaveLength(1)
262
- expect(Number(startTicks[0].text())).toBe(0)
263
- },
264
- )
265
-
266
- it.each(['independent', 'separated'] as const)(
267
- 'shows the Y-axis end value once on every track in %s mode',
268
- async (displayMode) => {
269
- const wrapper = await mountSizedChart(
270
- {
271
- kind: 'series',
272
- series: [
273
- {
274
- name: 'first',
275
- data: {
276
- kind: 'points',
277
- points: [
278
- { x: 0, y: 0.11 },
279
- { x: 1, y: 0.89 },
280
- ],
281
- },
282
- },
283
- {
284
- name: 'second',
285
- data: {
286
- kind: 'points',
287
- points: [
288
- { x: 0, y: 4.11 },
289
- { x: 1, y: 4.89 },
290
- ],
291
- },
292
- },
293
- ],
294
- },
295
- { displayMode },
296
- )
297
-
298
- const expectedEndValues = [1, 5]
299
- wrapper.findAll('.waveform-chart__track').forEach((track, index) => {
300
- const endTicks = track.findAll('.waveform-chart__axis--y .tick').filter((tick) => {
301
- const match = tick.attributes('transform')?.match(/translate\(0,\s*([\d.]+)\)/)
302
- return match ? Math.abs(Number(match[1])) <= 1 : false
303
- })
304
-
305
- expect(endTicks).toHaveLength(1)
306
- expect(Number(endTicks[0].text())).toBe(expectedEndValues[index])
307
- })
308
- },
309
- )
310
- })