waveform-analysis 0.1.63 → 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.
- package/README.md +9 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +953 -946
- package/dist/style.css +1 -1
- package/dist/types/types/chart.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/WaveformChart.vue +1 -1
- package/src/components/core/xDomain.ts +26 -2
- package/src/components/rendering/WaveformLegend.vue +1 -1
- package/src/components/rendering/WaveformSeriesLayer.vue +1 -1
- package/src/demo/DemoChartHost.vue +1 -0
- package/src/types/chart.ts +2 -1
- package/src/App.test.ts +0 -400
- package/src/components/annotation/components.test.ts +0 -372
- package/src/components/annotation/markup.test.ts +0 -371
- package/src/components/annotation/serialization.test.ts +0 -114
- package/src/components/core/constants.test.ts +0 -29
- package/src/components/core/fixedYDomainLayout.test.ts +0 -204
- package/src/components/core/grid.test.ts +0 -197
- package/src/components/core/latestTaskScheduler.test.ts +0 -51
- package/src/components/core/layout.test.ts +0 -373
- package/src/components/core/title.test.ts +0 -76
- package/src/components/core/useWaveformData.test.ts +0 -95
- package/src/components/core/useWaveformRenderSampling.test.ts +0 -17
- package/src/components/core/xDomain.test.ts +0 -28
- package/src/components/core/yAxisNice.test.ts +0 -174
- package/src/components/core/yAxisSlotLayout.test.ts +0 -165
- package/src/components/core/yDomain.test.ts +0 -56
- package/src/components/interaction/WaveformTooltip.test.ts +0 -276
- package/src/components/interaction/viewportInteractionState.test.ts +0 -101
- package/src/components/utils/useAnimationFrameThrottle.test.ts +0 -53
- package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +0 -257
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +0 -358
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +0 -358
- package/src/components/waveformChartCases/dataLifecycle.test.ts +0 -310
- package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +0 -389
- package/src/components/waveformChartCases/gridFill.test.ts +0 -95
- package/src/components/waveformChartCases/incompleteDataHover.test.ts +0 -155
- package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +0 -328
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +0 -371
- package/src/components/waveformChartCases/legendVisibility.test.ts +0 -350
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +0 -400
- package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +0 -61
- package/src/components/waveformChartCases/presentationAndAxes.test.ts +0 -345
- package/src/components/waveformChartCases/presentationMode.test.ts +0 -327
- package/src/components/waveformChartCases/renderingStyles.test.ts +0 -283
- package/src/components/waveformChartCases/sizingAndTitles.test.ts +0 -370
- package/src/components/waveformChartCases/trackOrder.test.ts +0 -323
- package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +0 -365
- package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -311
- package/src/components/waveformChartCases/viewportLifecycle.test.ts +0 -132
- package/src/components/waveformChartCases/viewportReset.test.ts +0 -251
- package/src/components/waveformChartCases/wheelZoomOut.test.ts +0 -114
- package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +0 -117
- package/src/components/waveformChartCases/xDomainStrategy.test.ts +0 -168
- package/src/components/waveformChartCases/yAxisLayout.test.ts +0 -103
- package/src/components/waveformChartCases/zoomIntentAndDataRebind.test.ts +0 -213
- package/src/components/waveformChartCases/zoomPointConstraints.test.ts +0 -308
- package/src/components/waveformLegendGeometry.test.ts +0 -21
- package/src/components/waveformNormalization.test.ts +0 -170
- package/src/core/data.test.ts +0 -241
- package/src/core/rendering.test.ts +0 -267
- package/src/core/renderingStrategies.test.ts +0 -90
- package/src/core/samplingReference.test.ts +0 -124
- package/src/core/wasmSampling.test.ts +0 -205
- package/src/core/workerSampling/multiresolution.test.ts +0 -69
- package/src/core/workerSampling/protocol.test.ts +0 -34
- package/src/core/workerSampling/repository.test.ts +0 -400
- package/src/core/workerSampling/repositoryCache.test.ts +0 -163
- package/src/core/workerSampling/repositorySampling.test.ts +0 -106
- package/src/core/workerSampling/repositoryTypedDataset.test.ts +0 -89
- package/src/core/workerSampling/repositoryWasmDataset.test.ts +0 -70
- package/src/data/simulatedWaveforms.test.ts +0 -60
- package/src/demo/plotMarginControls.test.ts +0 -29
- package/src/demo/xAxisLabelControls.test.ts +0 -86
- package/src/test/setup.ts +0 -82
- package/src/test/waveformChart.ts +0 -75
- package/src/utils/domain.test.ts +0 -28
- package/src/utils/formatters.test.ts +0 -131
- package/src/utils/sampling.test.ts +0 -226
- package/src/views/FixedYDomainDemo.test.ts +0 -44
- package/src/views/WasmSamplingDemo.test.ts +0 -13
- 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
|
-
})
|