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