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,372 +0,0 @@
1
- import { flushPromises, mount } from '@vue/test-utils'
2
- import { InputNumber } from 'ant-design-vue'
3
- import { afterEach, describe, expect, it, vi } from 'vitest'
4
- import { ColorPicker } from 'vue3-colorpicker'
5
- import { defineComponent, h } from 'vue'
6
-
7
- import WaveformAnnotationContextMenu from './WaveformAnnotationContextMenu.vue'
8
- import WaveformAnnotationEditor from './WaveformAnnotationEditor.vue'
9
- import WaveformAnnotationLayer from './WaveformAnnotationLayer.vue'
10
-
11
- const modalStub = defineComponent({
12
- props: [
13
- 'visible',
14
- 'width',
15
- 'maskClosable',
16
- 'keyboard',
17
- 'wrapClassName',
18
- 'cancelText',
19
- 'okText',
20
- 'okButtonProps',
21
- 'rootClassName',
22
- ],
23
- emits: ['cancel', 'ok'],
24
- setup(props, { emit, slots }) {
25
- return () => {
26
- const title = slots.title?.() ?? []
27
- const titleId = (title[0]?.props as { id?: string } | undefined)?.id
28
- return h('div', { class: ['ant-modal-root', props.rootClassName] }, [
29
- h(
30
- 'div',
31
- {
32
- class: ['ant-modal-wrap', props.wrapClassName],
33
- role: 'dialog',
34
- 'aria-modal': 'true',
35
- 'aria-labelledby': titleId,
36
- onClick: (event: MouseEvent) => {
37
- if (event.target === event.currentTarget && props.maskClosable) emit('cancel')
38
- },
39
- },
40
- [
41
- h('div', { class: 'ant-modal' }, [
42
- h('div', { class: 'ant-modal-header' }, [
43
- h('div', { class: 'ant-modal-title' }, title),
44
- ]),
45
- h('button', { class: 'ant-modal-close', onClick: () => emit('cancel') }, '×'),
46
- slots.default?.(),
47
- h('div', { class: 'ant-modal-footer' }, [
48
- h('button', { class: 'ant-btn', onClick: () => emit('cancel') }, props.cancelText),
49
- h(
50
- 'button',
51
- {
52
- class: 'ant-btn ant-btn-primary',
53
- disabled: props.okButtonProps?.disabled,
54
- onClick: () => emit('ok'),
55
- },
56
- props.okText,
57
- ),
58
- ]),
59
- ]),
60
- ],
61
- ),
62
- ])
63
- }
64
- },
65
- })
66
- const mountEditor = (options: { props: Record<string, unknown> }) =>
67
- mount(WaveformAnnotationEditor, {
68
- props: options.props as never,
69
- global: { stubs: { Modal: modalStub, AModal: modalStub } },
70
- })
71
-
72
- describe('waveform annotation controls', () => {
73
- afterEach(() => {
74
- document.body.innerHTML = ''
75
- })
76
- it('keeps dialog title ids unique across editor instances', () => {
77
- const first = mountEditor({
78
- props: {
79
- annotation: { id: 'first', seriesId: 'a', x: 1, y: 2, text: '说明' },
80
- mode: 'edit',
81
- },
82
- })
83
- const second = mountEditor({
84
- props: {
85
- annotation: { id: 'second', seriesId: 'a', x: 1, y: 2, text: '说明' },
86
- mode: 'edit',
87
- },
88
- })
89
-
90
- const firstTitleId = first.get('.ant-modal-title [id]').attributes('id')
91
- const secondTitleId = second.get('.ant-modal-title [id]').attributes('id')
92
-
93
- expect(firstTitleId).toBeTruthy()
94
- expect(secondTitleId).toBeTruthy()
95
- expect(firstTitleId).not.toBe(secondTitleId)
96
- expect(first.get('[role="dialog"]').attributes('aria-labelledby')).toBe(firstTitleId)
97
- expect(second.get('[role="dialog"]').attributes('aria-labelledby')).toBe(secondTitleId)
98
- })
99
-
100
- it('uses a dedicated root class to isolate teleported modal styles', () => {
101
- const wrapper = mountEditor({
102
- props: {
103
- annotation: { id: 'isolated', seriesId: 'a', x: 1, y: 2, text: '说明' },
104
- mode: 'add',
105
- },
106
- })
107
-
108
- expect(wrapper.get('.ant-modal-root').classes()).toContain('waveform-annotation-editor-root')
109
- expect(wrapper.get('.ant-modal-wrap').classes()).toContain('waveform-annotation-editor')
110
- })
111
-
112
- it.each(['add', 'edit'] as const)('shows the X-axis snapping hint in %s mode', (mode) => {
113
- const wrapper = mountEditor({
114
- props: {
115
- annotation: { id: `hint-${mode}`, seriesId: 'a', x: 1, y: 2, text: '说明' },
116
- mode,
117
- },
118
- })
119
-
120
- expect(wrapper.get('[role="note"]').text()).toBe('修改 X 轴后失焦时会自动吸附最近的采样点')
121
- })
122
-
123
- it('allows changing the annotation series inside the editor', async () => {
124
- const wrapper = mountEditor({
125
- props: {
126
- annotation: { id: 'note', seriesId: 'a', x: 1, y: 2, text: '说明' },
127
- mode: 'edit',
128
- series: { id: 'a', name: '通道 A', color: '#f00', unit: 'V' },
129
- seriesOptions: [
130
- {
131
- trackIndex: 0,
132
- seriesId: 'a',
133
- name: '通道 A',
134
- color: '#f00',
135
- unit: 'V',
136
- point: { x: 1, y: 2 },
137
- screenX: 100,
138
- screenY: 50,
139
- distance: 0,
140
- xValue: 1,
141
- },
142
- {
143
- trackIndex: 1,
144
- seriesId: 'b',
145
- name: '通道 B',
146
- color: '#00f',
147
- unit: 'A',
148
- point: { x: 1, y: 3 },
149
- screenX: 100,
150
- screenY: 60,
151
- distance: 10,
152
- xValue: 1,
153
- },
154
- ],
155
- },
156
- })
157
- await flushPromises()
158
-
159
- expect(
160
- (wrapper.get('select[aria-label="选择标注波形"]').element as HTMLSelectElement).value,
161
- ).toBe('a')
162
- await wrapper.get('select[aria-label="选择标注波形"]').setValue('b')
163
- expect(wrapper.emitted('series-change')).toEqual([['b']])
164
- expect(wrapper.get('.waveform-annotation-editor__series').text()).toContain('通道 A')
165
- })
166
-
167
- it('validates text and emits an immutable edited annotation with style defaults', async () => {
168
- const annotation = { id: 'note', seriesId: 'a', x: 1, y: 2, text: '' }
169
- const wrapper = mountEditor({
170
- props: { annotation, mode: 'add' },
171
- })
172
- await flushPromises()
173
-
174
- expect(wrapper.get('textarea').attributes('maxlength')).toBe('40')
175
- expect(wrapper.get('.waveform-annotation-editor__coordinates').text()).toContain('X1000.000')
176
- expect(wrapper.get('.waveform-annotation-editor__coordinates').text()).toContain('Y2')
177
- expect(wrapper.get('button.ant-btn-primary').attributes('disabled')).toBeDefined()
178
- await vi.waitFor(() => expect(wrapper.findAllComponents(ColorPicker)).toHaveLength(3), {
179
- timeout: 5000,
180
- })
181
- const colorPickers = wrapper.findAllComponents(ColorPicker)
182
- expect(wrapper.findAll('.waveform-annotation-editor__color-field')).toHaveLength(3)
183
- expect(colorPickers.map((picker) => picker.props('pureColor'))).toEqual([
184
- '#1677ff',
185
- '#333333',
186
- 'rgba(255, 255, 255, 0.92)',
187
- ])
188
- colorPickers.forEach((picker) => {
189
- expect(picker.props()).toMatchObject({
190
- useType: 'pure',
191
- pickerType: 'chrome',
192
- format: 'rgb',
193
- disableAlpha: false,
194
- blurClose: true,
195
- })
196
- })
197
-
198
- await wrapper.get('textarea').setValue('新标注')
199
- colorPickers[0].vm.$emit('update:pureColor', 'rgba(22, 119, 255, 0.7)')
200
- colorPickers[1].vm.$emit('update:pureColor', 'rgba(51, 51, 51, 0.8)')
201
- colorPickers[2].vm.$emit('update:pureColor', 'rgba(255, 255, 255, 0.5)')
202
- await wrapper.vm.$nextTick()
203
- await wrapper.get('button.ant-btn-primary').trigger('click')
204
-
205
- const emitted = wrapper.emitted('confirm')?.[0]?.[0] as
206
- | {
207
- text: string
208
- style?: { borderColor?: string; textColor?: string; backgroundColor?: string }
209
- }
210
- | undefined
211
- expect(emitted).toMatchObject({
212
- text: '新标注',
213
- style: {
214
- borderColor: 'rgba(22, 119, 255, 0.7)',
215
- textColor: 'rgba(51, 51, 51, 0.8)',
216
- backgroundColor: 'rgba(255, 255, 255, 0.5)',
217
- },
218
- })
219
- expect(annotation.text).toBe('')
220
- })
221
-
222
- it('formats annotation coordinates using the selected display context', () => {
223
- const wrapper = mountEditor({
224
- props: {
225
- annotation: { id: 'note', seriesId: 'series', x: 1, y: 0.0000001, text: '说明' },
226
- mode: 'edit',
227
- timeUnit: 's',
228
- },
229
- })
230
-
231
- const coordinates = wrapper.get('.waveform-annotation-editor__coordinates').text()
232
- expect(coordinates).toContain('X1.000')
233
- expect(coordinates).toContain('Y0.0000001')
234
- expect(coordinates).not.toContain('e-')
235
- })
236
-
237
- it('emits valid manual time input and disables save for invalid input', async () => {
238
- const wrapper = mountEditor({
239
- props: {
240
- annotation: { id: 'note', seriesId: 'series', x: 1, y: 2, text: '说明' },
241
- mode: 'edit',
242
- timeUnit: 'ms',
243
- },
244
- })
245
- const input = wrapper.getComponent(InputNumber)
246
- expect(input.props('controls')).toBe(true)
247
- await input.vm.$emit('update:value', 1500)
248
- expect(wrapper.emitted('time-change')).toBeUndefined()
249
- await input.vm.$emit('blur')
250
- expect(wrapper.emitted('time-change')).toEqual([['1500']])
251
- await input.vm.$emit('update:value', null)
252
- await input.vm.$emit('blur')
253
- expect(wrapper.get('[role="alert"]').text()).toBe('请输入有效的时间')
254
- expect(wrapper.get('button.ant-btn-primary').attributes('disabled')).toBeDefined()
255
- })
256
-
257
- it('shows time validation errors and blocks confirmation', async () => {
258
- const wrapper = mountEditor({
259
- props: {
260
- annotation: { id: 'note', seriesId: 'series', x: 1, y: 2, text: '说明' },
261
- mode: 'edit',
262
- timeError: '时间超出当前波形范围',
263
- },
264
- })
265
-
266
- const input = wrapper.getComponent(InputNumber)
267
- await input.vm.$emit('blur')
268
- expect(
269
- wrapper.get('.waveform-annotation-editor__coordinate-input').attributes('aria-invalid'),
270
- ).toBe('true')
271
- expect(wrapper.get('[role="alert"]').text()).toBe('时间超出当前波形范围')
272
- expect(wrapper.get('button.ant-btn-primary').attributes('disabled')).toBeDefined()
273
- await wrapper.setProps({ timeError: '' })
274
- expect(wrapper.find('[role="alert"]').exists()).toBe(false)
275
- })
276
-
277
- it('hydrates hexadecimal and rgba annotation colors', async () => {
278
- const wrapper = mountEditor({
279
- props: {
280
- annotation: {
281
- id: 'colored-note',
282
- seriesId: 'a',
283
- x: 1,
284
- y: 2,
285
- text: '已有标注',
286
- style: {
287
- borderColor: '#ff0000',
288
- textColor: 'rgba(0, 0, 0, 0.75)',
289
- backgroundColor: 'rgba(255, 255, 255, 0.4)',
290
- },
291
- },
292
- mode: 'edit',
293
- },
294
- })
295
- await flushPromises()
296
- await vi.waitFor(() => expect(wrapper.findAllComponents(ColorPicker)).toHaveLength(3), {
297
- timeout: 5000,
298
- })
299
-
300
- expect(
301
- wrapper.findAllComponents(ColorPicker).map((picker) => picker.props('pureColor')),
302
- ).toEqual(['#ff0000', 'rgba(0, 0, 0, 0.75)', 'rgba(255, 255, 255, 0.4)'])
303
- })
304
-
305
- it('supports modal dismissal and live character counting', async () => {
306
- const editor = mountEditor({
307
- props: {
308
- annotation: { id: 'note', seriesId: 'a', x: 1, y: 2, text: '原文字' },
309
- mode: 'edit',
310
- },
311
- })
312
-
313
- expect(editor.get('[role="dialog"]').attributes('aria-modal')).toBe('true')
314
- expect(editor.get('.ant-modal-title').text()).toBe('编辑标注')
315
- await editor.get('textarea').setValue('三字说明')
316
- expect(editor.get('.waveform-annotation-editor__label-row').text()).toContain('4/40')
317
-
318
- await editor.get('textarea').trigger('keydown', { key: 'Escape' })
319
- await editor.get('.ant-modal-wrap').trigger('click')
320
- expect(editor.emitted('cancel')).toHaveLength(2)
321
- })
322
-
323
- it('supports cancellation and context menu actions', async () => {
324
- const editor = mountEditor({
325
- props: {
326
- annotation: { id: 'note', seriesId: 'a', x: 1, y: 2, text: '原文字' },
327
- mode: 'edit',
328
- },
329
- })
330
- await editor.get('.ant-modal-close').trigger('click')
331
- expect(editor.emitted('cancel')).toHaveLength(1)
332
-
333
- const menu = mount(WaveformAnnotationContextMenu, {
334
- props: { visible: true, x: 10, y: 20, canEdit: true },
335
- })
336
- await menu.findAll('button')[0].trigger('click')
337
- await menu.findAll('button')[1].trigger('click')
338
- expect(menu.emitted('edit')).toHaveLength(1)
339
- expect(menu.emitted('delete')).toHaveLength(1)
340
- })
341
-
342
- it('centers single-line and multiline annotation text inside its box', () => {
343
- const wrapper = mount(WaveformAnnotationLayer, {
344
- props: {
345
- visible: true,
346
- annotations: [
347
- {
348
- annotation: { id: 'centered', seriesId: 'a', x: 1, y: 2, text: '第一行\n第二行' },
349
- trackIndex: 0,
350
- anchorX: 80,
351
- anchorY: 80,
352
- placement: 'top',
353
- lines: ['第一行', '第二行'],
354
- box: { x: 20, y: 20, width: 80, height: 42, lineEndX: 60, lineEndY: 62 },
355
- style: {
356
- borderColor: '#1677ff',
357
- textColor: '#333333',
358
- backgroundColor: '#ffffff',
359
- },
360
- },
361
- ],
362
- },
363
- })
364
-
365
- const text = wrapper.get('.waveform-annotation__text')
366
- expect(text.attributes('text-anchor')).toBe('middle')
367
- expect(text.attributes('dominant-baseline')).toBe('central')
368
- expect(text.attributes('x')).toBe('60')
369
- expect(text.attributes('y')).toBe('33')
370
- expect(wrapper.findAll('tspan').map((line) => line.attributes('x'))).toEqual(['60', '60'])
371
- })
372
- })