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,283 +0,0 @@
1
- import { flushPromises } from '@vue/test-utils'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { flushAnimationFrames } from '../../test/setup'
5
- import { type WaveformData } from '../waveform'
6
-
7
- import { mountSizedChart } from '../../test/waveformChart'
8
-
9
- describe('WaveformChart', () => {
10
- it('places start, middle, and end step transitions at the expected X positions', async () => {
11
- const lineTypes = ['step-start', 'step-middle', 'step-end', 'step-after'] as const
12
- const wrapper = await mountSizedChart({
13
- kind: 'series',
14
- series: lineTypes.map((lineType) => ({
15
- id: lineType,
16
- trackId: 'steps',
17
- name: lineType,
18
- lineType,
19
- data: {
20
- kind: 'points',
21
- points: [
22
- { x: 0, y: 0 },
23
- { x: 2, y: 10 },
24
- ],
25
- },
26
- })),
27
- })
28
- const pathCoordinates = (lineType: (typeof lineTypes)[number]) =>
29
- Array.from(
30
- (
31
- wrapper.get(`.waveform-chart__line[data-series-id="${lineType}"]`).attributes('d') ?? ''
32
- ).matchAll(/[ML]([\d.-]+),([\d.-]+)/g),
33
- (match) => ({ x: Number(match[1]), y: Number(match[2]) }),
34
- )
35
-
36
- const start = pathCoordinates('step-start')
37
- const middle = pathCoordinates('step-middle')
38
- const end = pathCoordinates('step-end')
39
- const after = pathCoordinates('step-after')
40
- expect(start.map((point) => point.x)).toEqual([start[0]?.x, start[0]?.x, start.at(-1)?.x])
41
- expect(middle[1]?.x).toBe(middle[2]?.x)
42
- expect(middle[1]?.x).toBe((middle[0]!.x + middle.at(-1)!.x) / 2)
43
- expect(end.map((point) => point.x)).toEqual([end[0]?.x, end.at(-1)?.x, end.at(-1)?.x])
44
- expect(after).toEqual(end)
45
-
46
- wrapper.unmount()
47
- })
48
-
49
- it('renders per-series lines, point symbols, error bars, and matching legend swatches', async () => {
50
- const wrapper = await mountSizedChart({
51
- kind: 'series',
52
- series: [
53
- {
54
- id: 'triangle-errors',
55
- trackId: 'styled-track',
56
- name: '三角误差',
57
- lineType: 'none',
58
- pointType: 'triangle',
59
- errorBar: { visible: true },
60
- data: {
61
- kind: 'points',
62
- points: [
63
- { x: 0, y: 1, error: 0.25 },
64
- { x: 1, y: 2, error: 0.5 },
65
- ],
66
- },
67
- },
68
- {
69
- id: 'line-only',
70
- trackId: 'styled-track',
71
- name: '纯线',
72
- lineType: 'linear',
73
- lineStyle: 'dashed',
74
- pointType: 'none',
75
- data: {
76
- kind: 'points',
77
- points: [
78
- { x: 0, y: 2 },
79
- { x: 1, y: 3 },
80
- ],
81
- },
82
- },
83
- {
84
- id: 'step-errors',
85
- trackId: 'styled-track',
86
- name: '阶梯误差',
87
- lineType: 'step-after',
88
- lineStyle: 'dash-dot',
89
- pointType: 'circle',
90
- errorBar: { visible: true, color: '#222222', width: 2, capWidth: 10 },
91
- data: {
92
- kind: 'points',
93
- points: [
94
- { x: 0, y: 3, lowerError: 0.5, upperError: 1 },
95
- { x: 1, y: 4 },
96
- ],
97
- },
98
- },
99
- {
100
- id: 'errors-only',
101
- trackId: 'styled-track',
102
- name: '纯误差棒',
103
- lineType: 'none',
104
- pointType: 'none',
105
- errorBar: { visible: true },
106
- data: {
107
- kind: 'points',
108
- points: [
109
- { x: 0, y: 4, error: 0.5 },
110
- { x: 1, y: 5, error: 0.5 },
111
- ],
112
- },
113
- },
114
- ],
115
- })
116
-
117
- expect(wrapper.find('.waveform-chart__line[data-series-id="triangle-errors"]').exists()).toBe(
118
- false,
119
- )
120
- const stepLine = wrapper.get('.waveform-chart__line[data-series-id="step-errors"]')
121
- expect(stepLine.attributes('data-line-type')).toBe('step-after')
122
- expect(stepLine.attributes('data-line-style')).toBe('dash-dot')
123
- expect(stepLine.attributes('stroke-dasharray')).toBe('8 5 1.5 5')
124
- expect(
125
- wrapper
126
- .get('.waveform-chart__line[data-series-id="line-only"]')
127
- .attributes('stroke-dasharray'),
128
- ).toBe('8 5')
129
- expect(stepLine.attributes('d')).toMatch(/^M[\d.-]+,([\d.-]+)L[\d.-]+,\1L/)
130
- expect(
131
- wrapper
132
- .get('.waveform-chart__points[data-series-id="triangle-errors"]')
133
- .attributes('data-point-type'),
134
- ).toBe('triangle')
135
- expect(wrapper.findAll('.waveform-chart__point')).toHaveLength(2)
136
- expect(
137
- wrapper
138
- .get('.waveform-chart__points[data-series-id="triangle-errors"] .waveform-chart__point')
139
- .attributes('d'),
140
- ).toMatch(/M.*M/)
141
- expect(wrapper.findAll('.waveform-chart__error-bar')).toHaveLength(3)
142
- expect(
143
- wrapper
144
- .get('.waveform-chart__error-bars[data-series-id="step-errors"] .waveform-chart__error-bar')
145
- .attributes('stroke'),
146
- ).toBe('#222222')
147
- const errorsOnlySeries = wrapper.get('.waveform-chart__series[data-series-id="errors-only"]')
148
- expect(errorsOnlySeries.find('.waveform-chart__line').exists()).toBe(false)
149
- expect(errorsOnlySeries.find('.waveform-chart__point').exists()).toBe(false)
150
- expect(errorsOnlySeries.find('.waveform-chart__error-bar').exists()).toBe(true)
151
-
152
- const swatches = wrapper.findAll('.waveform-legend__swatch')
153
- expect(swatches.map((swatch) => swatch.attributes('data-line-type'))).toEqual([
154
- 'none',
155
- 'linear',
156
- 'step-after',
157
- 'none',
158
- ])
159
- expect(swatches[0]?.attributes('data-error-bar-visible')).toBe('true')
160
- expect(swatches.map((swatch) => swatch.attributes('data-line-style'))).toEqual([
161
- 'solid',
162
- 'dashed',
163
- 'dash-dot',
164
- 'solid',
165
- ])
166
- expect(swatches[1]?.get('.waveform-legend__line').attributes('stroke-dasharray')).toBe('8 5')
167
- expect(swatches[2]?.get('.waveform-legend__line').attributes('stroke-dasharray')).toBe(
168
- '8 5 1.5 5',
169
- )
170
- expect(swatches[2]?.attributes('data-error-bar-visible')).toBe('true')
171
- expect(swatches[3]?.attributes('data-error-bar-visible')).toBe('true')
172
- expect(swatches[0]!.findAll('path').map((path) => path.classes())).toEqual([
173
- ['waveform-legend__error-bar'],
174
- ['waveform-legend__point'],
175
- ])
176
- const stepSwatchPaths = swatches[2]!.findAll('path')
177
- expect(stepSwatchPaths.map((path) => path.classes())).toEqual([
178
- ['waveform-legend__line'],
179
- ['waveform-legend__error-bar'],
180
- ['waveform-legend__point'],
181
- ])
182
- expect(stepSwatchPaths[0]?.attributes()).toMatchObject({
183
- d: 'M1 8H25',
184
- stroke: '#2ca02c',
185
- 'stroke-width': '1.5',
186
- })
187
- expect(stepSwatchPaths[1]?.attributes()).toMatchObject({
188
- d: 'M8 2H18M13 2V14M8 14H18',
189
- stroke: '#222222',
190
- 'stroke-width': '2',
191
- 'stroke-linecap': 'butt',
192
- })
193
- expect(stepSwatchPaths[2]?.attributes('transform')).toBe('translate(13 8)')
194
- expect(swatches[3]!.findAll('path').map((path) => path.classes())).toEqual([
195
- ['waveform-legend__error-bar'],
196
- ])
197
-
198
- const renderedSeriesNodes = wrapper
199
- .findAll('.waveform-chart__line, .waveform-chart__point, .waveform-chart__error-bar')
200
- .map((node) => node.element)
201
- const overlay = wrapper.get('.waveform-chart__overlay--independent')
202
- const overlayWidth = Number(overlay.attributes('width'))
203
- const overlayHeight = Number(overlay.attributes('height'))
204
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
205
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: overlayHeight }),
206
- })
207
- overlay.element.dispatchEvent(
208
- new MouseEvent('pointermove', {
209
- clientX: overlayWidth / 2,
210
- clientY: overlayHeight / 2,
211
- bubbles: true,
212
- }),
213
- )
214
- flushAnimationFrames()
215
- await flushPromises()
216
- expect(
217
- wrapper
218
- .findAll('.waveform-chart__line, .waveform-chart__point, .waveform-chart__error-bar')
219
- .map((node) => node.element),
220
- ).toEqual(renderedSeriesNodes)
221
-
222
- wrapper.unmount()
223
- })
224
-
225
- it('binds overlaid series to at most four value axes in multi-axis mode', async () => {
226
- const data: WaveformData = {
227
- kind: 'series',
228
- series: Array.from({ length: 5 }, (_, index) => ({
229
- id: `overlaid-${index}`,
230
- trackId: 'shared-frame',
231
- name: `叠加通道 ${index + 1}`,
232
- data: {
233
- kind: 'points',
234
- points: [
235
- { x: 0, y: index * 100 },
236
- { x: 1, y: index * 100 + 10 },
237
- ],
238
- },
239
- })),
240
- }
241
- const wrapper = await mountSizedChart(data, { overlayMode: 'multi-axis' })
242
-
243
- expect(wrapper.attributes('data-overlay-mode')).toBe('multi-axis')
244
- expect(
245
- wrapper
246
- .findAll('.waveform-chart__axis--y')
247
- .map((axis) => axis.attributes('data-y-axis-side')),
248
- ).toEqual(['left', 'left', 'right', 'right'])
249
- expect(
250
- wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-y-axis-index')),
251
- ).toEqual(['0', '1', '2', '3', '3'])
252
- expect(wrapper.findAll('.waveform-track__multi-axis-title')).toHaveLength(4)
253
-
254
- wrapper.unmount()
255
- })
256
-
257
- it('does not render empty multi-axis title backgrounds', async () => {
258
- const data: WaveformData = {
259
- kind: 'series',
260
- series: [
261
- {
262
- id: 'empty-title-a',
263
- trackId: 'shared-frame',
264
- name: '',
265
- data: { kind: 'samples', values: [0, 1], sampleRate: 1 },
266
- },
267
- {
268
- id: 'empty-title-b',
269
- trackId: 'shared-frame',
270
- name: ' ',
271
- data: { kind: 'samples', values: [10, 20], sampleRate: 1 },
272
- },
273
- ],
274
- }
275
- const wrapper = await mountSizedChart(data, { overlayMode: 'multi-axis', yLabel: '' })
276
-
277
- expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(2)
278
- expect(wrapper.findAll('.waveform-track__multi-axis-title')).toHaveLength(0)
279
- expect(wrapper.findAll('.waveform-chart__y-axis-label-bg')).toHaveLength(0)
280
-
281
- wrapper.unmount()
282
- })
283
- })
@@ -1,370 +0,0 @@
1
- import { flushPromises, mount } from '@vue/test-utils'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { flushAnimationFrames, resizeObservers } from '../../test/setup'
5
- import WaveformChart from '../WaveformChart.vue'
6
- import WaveformChartView from '../WaveformChartView.vue'
7
-
8
- import { mountSizedChart } from '../../test/waveformChart'
9
-
10
- describe('WaveformChart', () => {
11
- it('uses fixed dimensions when both width and height are specified', async () => {
12
- const wrapper = mount(WaveformChart, {
13
- props: {
14
- data: { kind: 'samples', values: [0, 1], sampleRate: 1 },
15
- width: 640,
16
- height: 420,
17
- },
18
- })
19
-
20
- expect(wrapper.attributes('style')).toContain('width: 640px')
21
- expect(wrapper.attributes('style')).toContain('height: 420px')
22
- expect(wrapper.get('.waveform-chart__svg').attributes('width')).toBe('640')
23
- expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('420')
24
-
25
- resizeObservers.at(-1)?.resize(640, 420)
26
- await flushPromises()
27
-
28
- expect(wrapper.get('.waveform-chart__svg').attributes('width')).toBe('640')
29
- expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('420')
30
- })
31
-
32
- it('allows fixed width and adaptive height to work independently', async () => {
33
- const wrapper = mount(WaveformChart, {
34
- props: {
35
- data: { kind: 'samples', values: [0, 1], sampleRate: 1 },
36
- width: 640,
37
- },
38
- })
39
-
40
- expect(wrapper.attributes('style')).toContain('width: 640px')
41
- expect(wrapper.attributes('style')).toContain('height: 100%')
42
-
43
- resizeObservers.at(-1)?.resize(640, 500)
44
- await flushPromises()
45
-
46
- expect(wrapper.get('.waveform-chart__svg').attributes('width')).toBe('640')
47
- expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('500')
48
- })
49
-
50
- it('reserves a pagination band based on the chart width instead of the viewport', async () => {
51
- const data = {
52
- kind: 'series' as const,
53
- series: [0, 1].map((index) => ({
54
- id: `series-${index}`,
55
- name: `series ${index}`,
56
- data: { kind: 'samples' as const, values: [index, index + 1], sampleRate: 1 },
57
- })),
58
- }
59
- const narrow = mount(WaveformChart, {
60
- props: { data, width: 390, height: 360, grid: { rowCount: 1, columnCount: 1 } },
61
- })
62
- const wide = mount(WaveformChart, {
63
- props: { data, width: 640, height: 360, grid: { rowCount: 1, columnCount: 1 } },
64
- })
65
-
66
- expect(narrow.get('.waveform-chart__svg').attributes('height')).toBe('320')
67
- expect(wide.get('.waveform-chart__svg').attributes('height')).toBe('360')
68
- })
69
-
70
- it('allows adaptive width and fixed height to work independently', async () => {
71
- const wrapper = mount(WaveformChart, {
72
- props: {
73
- data: { kind: 'samples', values: [0, 1], sampleRate: 1 },
74
- height: 420,
75
- },
76
- })
77
-
78
- expect(wrapper.attributes('style')).toContain('width: 100%')
79
- expect(wrapper.attributes('style')).toContain('height: 420px')
80
-
81
- resizeObservers.at(-1)?.resize(900, 420)
82
- await flushPromises()
83
-
84
- expect(wrapper.get('.waveform-chart__svg').attributes('width')).toBe('900')
85
- expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('420')
86
- })
87
-
88
- it('fills both dimensions and responds to container size changes by default', async () => {
89
- const wrapper = mount(WaveformChart, {
90
- props: { data: { kind: 'samples', values: [0, 1], sampleRate: 1 } },
91
- })
92
-
93
- expect(wrapper.attributes('style')).toContain('width: 100%')
94
- expect(wrapper.attributes('style')).toContain('height: 100%')
95
- expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('180')
96
-
97
- resizeObservers.at(-1)?.resize(800, 520)
98
- await flushPromises()
99
- expect(wrapper.get('.waveform-chart__svg').attributes('width')).toBe('800')
100
- expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('520')
101
-
102
- resizeObservers.at(-1)?.resize(500, 300)
103
- await flushPromises()
104
- expect(wrapper.get('.waveform-chart__svg').attributes('width')).toBe('500')
105
- expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('300')
106
- })
107
-
108
- it('configures plot top and bottom margins and reacts to updates', async () => {
109
- const wrapper = await mountSizedChart(
110
- { kind: 'samples', values: [0, 1], sampleRate: 1 },
111
- {
112
- displayMode: 'compact',
113
- grid: { rowCount: 1, columnCount: 1 },
114
- plotMargin: { top: 30, bottom: 70 },
115
- },
116
- )
117
-
118
- expect(wrapper.attributes('data-plot-margin-top')).toBe('30')
119
- expect(wrapper.attributes('data-plot-margin-bottom')).toBe('70')
120
- expect(wrapper.get('.waveform-chart__overlay').attributes('height')).toBe('260')
121
- expect(wrapper.get('.waveform-chart__svg > g').attributes('transform')).toContain(', 30)')
122
-
123
- await wrapper.setProps({ plotMargin: { top: 12 } })
124
-
125
- expect(wrapper.attributes('data-plot-margin-top')).toBe('12')
126
- expect(wrapper.attributes('data-plot-margin-bottom')).toBe('52')
127
- expect(wrapper.get('.waveform-chart__overlay').attributes('height')).toBe('296')
128
- })
129
-
130
- it('falls back to default plot margins for invalid values', async () => {
131
- const wrapper = await mountSizedChart(
132
- { kind: 'samples', values: [0, 1], sampleRate: 1 },
133
- {
134
- displayMode: 'compact',
135
- grid: { rowCount: 1, columnCount: 1 },
136
- plotMargin: { top: -1, bottom: Number.NaN },
137
- },
138
- )
139
-
140
- expect(wrapper.attributes('data-plot-margin-top')).toBe('18')
141
- expect(wrapper.attributes('data-plot-margin-bottom')).toBe('52')
142
- expect(wrapper.get('.waveform-chart__overlay').attributes('height')).toBe('290')
143
- })
144
-
145
- it('keeps the chart title and X-axis title fixed when plot margins change', async () => {
146
- const wrapper = await mountSizedChart(
147
- { kind: 'samples', values: [0, 1], sampleRate: 1 },
148
- {
149
- displayMode: 'compact',
150
- grid: { rowCount: 1, columnCount: 1 },
151
- title: { text: '固定标题' },
152
- },
153
- )
154
- const titleAreaStyle = wrapper.get('.waveform-chart__title-area').attributes('style')
155
- const xAxisTitle = wrapper.get('.waveform-chart__x-label')
156
- const initialX = xAxisTitle.attributes('x')
157
- const initialY = xAxisTitle.attributes('y')
158
-
159
- await wrapper.setProps({ plotMargin: { top: 60, bottom: 90 } })
160
-
161
- expect(wrapper.get('.waveform-chart__title-area').attributes('style')).toBe(titleAreaStyle)
162
- expect(wrapper.get('.waveform-chart__x-label').attributes('x')).toBe(initialX)
163
- expect(wrapper.get('.waveform-chart__x-label').attributes('y')).toBe(initialY)
164
- expect(wrapper.get('.waveform-chart__svg > g').attributes('transform')).toContain(', 60)')
165
- })
166
-
167
- it('does not render or reserve space for missing, hidden, or blank titles', async () => {
168
- for (const title of [undefined, { visible: false, text: '隐藏标题' }, { text: ' ' }]) {
169
- const wrapper = await mountSizedChart(
170
- { kind: 'samples', values: [0, 1], sampleRate: 1 },
171
- title ? { title } : {},
172
- )
173
-
174
- expect(wrapper.find('.waveform-chart__title-area').exists()).toBe(false)
175
- expect(wrapper.attributes('data-title-area-height')).toBe('0')
176
- expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('360')
177
- }
178
- })
179
-
180
- it.each(['independent', 'separated', 'compact'] as const)(
181
- 'keeps the titled empty state inside the drawing area in %s mode',
182
- async (displayMode) => {
183
- const wrapper = await mountSizedChart(
184
- { kind: 'samples', values: [1], sampleRate: -1 },
185
- { displayMode, title: { text: '空数据标题' } },
186
- )
187
-
188
- expect(wrapper.get('.waveform-chart__title-text').text()).toBe('空数据标题')
189
- expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('316')
190
- expect(wrapper.get('.waveform-chart__empty').attributes('y')).toBe('158')
191
- },
192
- )
193
-
194
- it('renders one chart title with alignment and all supported text styles', async () => {
195
- const wrapper = await mountSizedChart(
196
- { kind: 'samples', values: [0, 1], sampleRate: 1 },
197
- {
198
- title: {
199
- text: ' shot: #4712 ',
200
- align: 'right',
201
- textStyle: {
202
- color: '#c026d3',
203
- fontSize: 18,
204
- fontFamily: 'Consolas',
205
- rotation: 0,
206
- fontWeight: 700,
207
- fontStyle: 'italic',
208
- textDecoration: 'underline',
209
- letterSpacing: '2px',
210
- },
211
- },
212
- },
213
- )
214
-
215
- const area = wrapper.get('.waveform-chart__title-area')
216
- const visual = wrapper.get('.waveform-chart__title-visual')
217
- const title = wrapper.get('.waveform-chart__title-text')
218
- expect(area.attributes('role')).toBe('heading')
219
- expect(area.attributes('style')).toContain('justify-content: flex-end')
220
- expect(title.text()).toBe('shot: #4712')
221
- expect(title.attributes('style')).toContain('color: rgb(192, 38, 211)')
222
- expect(title.attributes('style')).toContain('font-size: 18px')
223
- expect(title.attributes('style')).toContain('font-family: Consolas')
224
- expect(title.attributes('style')).toContain('font-weight: 700')
225
- expect(title.attributes('style')).toContain('font-style: italic')
226
- expect(title.attributes('style')).toContain('text-decoration: underline')
227
- expect(title.attributes('style')).toContain('letter-spacing: 2px')
228
- expect(visual.attributes('style')).toContain('width: 752px')
229
- expect(title.attributes('style')).toContain('width: 752px')
230
- expect(title.attributes('style')).toContain('rotate(0deg)')
231
- expect(wrapper.attributes('data-title-area-height')).toBe('44')
232
- expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('316')
233
- })
234
-
235
- it('normalizes invalid title numbers and wraps long titles at narrow widths', async () => {
236
- const wrapper = await mountSizedChart(
237
- { kind: 'samples', values: [0, 1], sampleRate: 1 },
238
- {
239
- title: {
240
- text: '这是一个用于验证窄屏省略行为的很长波形分析标题',
241
- textStyle: { fontSize: Number.NaN, rotation: Number.POSITIVE_INFINITY },
242
- },
243
- },
244
- )
245
- resizeObservers.at(-1)?.resize(160, 360)
246
- await flushPromises()
247
-
248
- const title = wrapper.get('.waveform-chart__title-text')
249
- expect(title.attributes('style')).toContain('font-size: 14px')
250
- expect(title.attributes('style')).toContain('Microsoft YaHei')
251
- expect(title.attributes('style')).toContain('font-weight: 400')
252
- expect(title.attributes('style')).toContain('rotate(0deg)')
253
- expect(title.attributes('style')).toContain('white-space: normal')
254
- expect(title.attributes('style')).toContain('overflow-wrap: anywhere')
255
- expect(title.attributes('title')).toBeUndefined()
256
- expect(title.attributes('data-title-wrapped')).toBe('true')
257
- expect(Number(wrapper.attributes('data-title-area-height'))).toBeGreaterThan(44)
258
- })
259
-
260
- it.each([45, 90, -90, 180])(
261
- 'scales a complete long title into the rotated title area at %s degrees',
262
- async (rotation) => {
263
- const wrapper = await mountSizedChart(
264
- { kind: 'samples', values: [0, 1], sampleRate: 1 },
265
- {
266
- title: {
267
- text: '这是一个用于验证旋转缩放行为的完整波形分析标题',
268
- textStyle: { rotation },
269
- },
270
- },
271
- )
272
- const titleHeight = Number(wrapper.attributes('data-title-area-height'))
273
- const title = wrapper.get('.waveform-chart__title-text')
274
-
275
- expect(titleHeight).toBeGreaterThanOrEqual(44)
276
- expect(titleHeight).toBeLessThanOrEqual(160)
277
- expect(Number(wrapper.get('.waveform-chart__svg').attributes('height'))).toBe(
278
- 360 - titleHeight,
279
- )
280
- expect(title.text()).toBe('这是一个用于验证旋转缩放行为的完整波形分析标题')
281
- expect(title.attributes('style')).toContain(`rotate(${rotation}deg)`)
282
- expect(title.attributes('style')).toContain('white-space: nowrap')
283
- expect(Number(title.attributes('data-title-scale'))).toBeLessThanOrEqual(1)
284
- expect(title.attributes('data-title-wrapped')).toBeUndefined()
285
- },
286
- )
287
-
288
- it('updates fixed and adaptive drawing heights when the title changes', async () => {
289
- const fixedWrapper = mount(WaveformChart, {
290
- props: {
291
- data: { kind: 'samples', values: [0, 1], sampleRate: 1 },
292
- height: 420,
293
- title: { text: '固定高度标题' },
294
- },
295
- })
296
- expect(fixedWrapper.get('.waveform-chart__svg').attributes('height')).toBe('376')
297
-
298
- await fixedWrapper.setProps({ title: { visible: false, text: '固定高度标题' } })
299
- expect(fixedWrapper.get('.waveform-chart__svg').attributes('height')).toBe('420')
300
-
301
- const adaptiveWrapper = await mountSizedChart(
302
- { kind: 'samples', values: [0, 1], sampleRate: 1 },
303
- { title: { text: '自适应高度标题' } },
304
- )
305
- expect(adaptiveWrapper.get('.waveform-chart__svg').attributes('height')).toBe('316')
306
- resizeObservers.at(-1)?.resize(800, 500)
307
- await flushPromises()
308
- expect(adaptiveWrapper.get('.waveform-chart__svg').attributes('height')).toBe('456')
309
- })
310
-
311
- it('includes the title offset in root-relative tooltip positioning', async () => {
312
- const wrapper = await mountSizedChart(
313
- {
314
- kind: 'points',
315
- points: [
316
- { x: 0, y: 0 },
317
- { x: 1, y: 5 },
318
- ],
319
- },
320
- { title: { text: 'shot: #4712' }, grid: { rowCount: 1, columnCount: 1 } },
321
- )
322
- const overlay = wrapper.get('.waveform-chart__overlay')
323
- const overlayWidth = Number(overlay.attributes('width'))
324
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
325
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: 246 }),
326
- })
327
-
328
- overlay.element.dispatchEvent(
329
- new MouseEvent('pointermove', { clientX: overlayWidth / 2, clientY: 100, bubbles: true }),
330
- )
331
- flushAnimationFrames()
332
- await flushPromises()
333
-
334
- const tooltipTop = Number.parseFloat(
335
- (wrapper.get('.waveform-chart__tooltip').element as HTMLElement).style.top,
336
- )
337
- expect(tooltipTop).toBeGreaterThanOrEqual(44)
338
- })
339
-
340
- it('includes the title offset in annotation editor anchors', async () => {
341
- const wrapper = await mountSizedChart(
342
- {
343
- kind: 'points',
344
- points: [
345
- { x: 0, y: 0 },
346
- { x: 1, y: 5 },
347
- ],
348
- },
349
- { title: { text: 'shot: #4712' }, grid: { rowCount: 1, columnCount: 1 } },
350
- )
351
- const overlay = wrapper.get('.waveform-chart__overlay')
352
- const overlayWidth = Number(overlay.attributes('width'))
353
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
354
- value: () => ({ left: 0, top: 0, width: overlayWidth, height: 246 }),
355
- })
356
- overlay.element.dispatchEvent(
357
- new MouseEvent('contextmenu', {
358
- clientX: overlayWidth / 2,
359
- clientY: 100,
360
- bubbles: true,
361
- }),
362
- )
363
- await flushPromises()
364
-
365
- const controller = wrapper.getComponent(WaveformChartView).props('controller') as unknown as {
366
- annotationInteraction: { editorDraft: { value: { anchor: { y: number } } | null } }
367
- }
368
- expect(controller.annotationInteraction.editorDraft.value?.anchor.y).toBe(162)
369
- })
370
- })