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,371 +0,0 @@
|
|
|
1
|
-
import { scaleLinear } from 'd3'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import type { WaveformAnnotation } from '../../types'
|
|
5
|
-
import {
|
|
6
|
-
ANNOTATION_TEXT_HORIZONTAL_PADDING,
|
|
7
|
-
ANNOTATION_TEXT_PADDING,
|
|
8
|
-
ANNOTATION_TEXT_VERTICAL_PADDING,
|
|
9
|
-
findAnnotationSeriesCandidates,
|
|
10
|
-
findNearestPointByX,
|
|
11
|
-
findNearestAnnotationPoint,
|
|
12
|
-
interpolateAnnotationPoint,
|
|
13
|
-
layoutAnnotations,
|
|
14
|
-
resolveAnnotationStyle,
|
|
15
|
-
wrapAnnotationText,
|
|
16
|
-
} from './markup'
|
|
17
|
-
import type { AnnotationTrackLayout } from './types'
|
|
18
|
-
|
|
19
|
-
const createTrack = (
|
|
20
|
-
index: number,
|
|
21
|
-
id: string,
|
|
22
|
-
top: number,
|
|
23
|
-
points: Array<{ x: number; y: number }>,
|
|
24
|
-
height = 100,
|
|
25
|
-
): AnnotationTrackLayout => ({
|
|
26
|
-
index,
|
|
27
|
-
series: { id, points },
|
|
28
|
-
top,
|
|
29
|
-
height,
|
|
30
|
-
xScale: scaleLinear([0, 2], [0, 200]),
|
|
31
|
-
yScale: scaleLinear([0, 10], [height, 0]),
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
describe('waveform annotation markup', () => {
|
|
35
|
-
it('interpolates a line value at the pointer X', () => {
|
|
36
|
-
expect(
|
|
37
|
-
interpolateAnnotationPoint(
|
|
38
|
-
[
|
|
39
|
-
{ x: 0, y: 0 },
|
|
40
|
-
{ x: 2, y: 10 },
|
|
41
|
-
],
|
|
42
|
-
1,
|
|
43
|
-
),
|
|
44
|
-
).toEqual({
|
|
45
|
-
x: 1,
|
|
46
|
-
y: 5,
|
|
47
|
-
})
|
|
48
|
-
expect(
|
|
49
|
-
interpolateAnnotationPoint(
|
|
50
|
-
[
|
|
51
|
-
{ x: 0, y: 0 },
|
|
52
|
-
{ x: 2, y: 10 },
|
|
53
|
-
],
|
|
54
|
-
3,
|
|
55
|
-
),
|
|
56
|
-
).toBeNull()
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('snaps outside and between samples to the nearest complete-data point', () => {
|
|
60
|
-
const points = [
|
|
61
|
-
{ x: 1, y: 10 },
|
|
62
|
-
{ x: 2, y: 20 },
|
|
63
|
-
{ x: 4, y: 40 },
|
|
64
|
-
]
|
|
65
|
-
expect(findNearestPointByX(points, -10)).toEqual(points[0])
|
|
66
|
-
expect(findNearestPointByX(points, 3.1)).toEqual(points[2])
|
|
67
|
-
expect(findNearestPointByX(points, 10)).toEqual(points[2])
|
|
68
|
-
expect(findNearestPointByX(points, 2)).toEqual(points[1])
|
|
69
|
-
expect(findNearestPointByX([{ x: 2, y: 20 }], 2.1)).toEqual({ x: 2, y: 20 })
|
|
70
|
-
expect(findNearestPointByX(points, Number.NaN)).toBeNull()
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('snaps annotations to nearest sample points while using interpolation for distance calculation', () => {
|
|
74
|
-
const track = createTrack(0, 'series', 0, [
|
|
75
|
-
{ x: 0, y: 0 },
|
|
76
|
-
{ x: 2, y: 10 },
|
|
77
|
-
])
|
|
78
|
-
const candidates = findAnnotationSeriesCandidates([track], 0.75, 75, 62.5)
|
|
79
|
-
|
|
80
|
-
// Should snap to nearest actual sample point for the anchor
|
|
81
|
-
expect(candidates[0].point).toEqual({ x: 0, y: 0 })
|
|
82
|
-
expect(candidates[0].xValue).toBeUndefined()
|
|
83
|
-
// Distance is calculated using interpolated position for accurate series selection
|
|
84
|
-
expect(candidates[0].distance).toBe(0)
|
|
85
|
-
expect(findNearestPointByX(track.series.points, 1.25)).toEqual({ x: 2, y: 10 })
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('interpolates start, middle, and end step lines at their visual transitions', () => {
|
|
89
|
-
const points = [
|
|
90
|
-
{ x: 0, y: 2 },
|
|
91
|
-
{ x: 2, y: 10 },
|
|
92
|
-
]
|
|
93
|
-
|
|
94
|
-
expect(interpolateAnnotationPoint(points, 0.5, 'step-start')).toEqual({ x: 0.5, y: 10 })
|
|
95
|
-
expect(interpolateAnnotationPoint(points, 0.5, 'step-middle')).toEqual({ x: 0.5, y: 2 })
|
|
96
|
-
expect(interpolateAnnotationPoint(points, 1, 'step-middle')).toEqual({ x: 1, y: 10 })
|
|
97
|
-
expect(interpolateAnnotationPoint(points, 1.5, 'step-middle')).toEqual({ x: 1.5, y: 10 })
|
|
98
|
-
expect(interpolateAnnotationPoint(points, 1, 'step-end')).toEqual({ x: 1, y: 2 })
|
|
99
|
-
expect(interpolateAnnotationPoint(points, 1, 'step-after')).toEqual({ x: 1, y: 2 })
|
|
100
|
-
expect(interpolateAnnotationPoint(points, 2, 'step-start')).toEqual({ x: 2, y: 10 })
|
|
101
|
-
expect(interpolateAnnotationPoint(points, 2, 'step-after')).toEqual({ x: 2, y: 10 })
|
|
102
|
-
expect(interpolateAnnotationPoint(points, 1, 'none')).toBeNull()
|
|
103
|
-
expect(interpolateAnnotationPoint(points, 2, 'none')).toEqual({ x: 2, y: 10 })
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('uses the nearest screen-space sample for point-only series', () => {
|
|
107
|
-
const pointOnly = createTrack(0, 'points', 0, [
|
|
108
|
-
{ x: 0, y: 2 },
|
|
109
|
-
{ x: 2, y: 10 },
|
|
110
|
-
])
|
|
111
|
-
pointOnly.series.lineType = 'none'
|
|
112
|
-
|
|
113
|
-
expect(findAnnotationSeriesCandidates([pointOnly], 1, 5, 80)).toMatchObject([
|
|
114
|
-
{ point: { x: 0, y: 2 }, screenX: 0, screenY: 80, distance: 5 },
|
|
115
|
-
])
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('sorts line candidates by screen distance and keeps series metadata', () => {
|
|
119
|
-
const first = createTrack(0, 'first', 0, [
|
|
120
|
-
{ x: 0, y: 0 },
|
|
121
|
-
{ x: 2, y: 10 },
|
|
122
|
-
])
|
|
123
|
-
first.series.name = '第一通道'
|
|
124
|
-
first.series.color = '#f00'
|
|
125
|
-
first.series.unit = 'V'
|
|
126
|
-
const second = createTrack(1, 'second', 0, [
|
|
127
|
-
{ x: 0, y: 4 },
|
|
128
|
-
{ x: 2, y: 8 },
|
|
129
|
-
])
|
|
130
|
-
const candidates = findAnnotationSeriesCandidates([first, second], 1, 100, 50)
|
|
131
|
-
|
|
132
|
-
expect(candidates[0]).toMatchObject({
|
|
133
|
-
seriesId: 'first',
|
|
134
|
-
name: '第一通道',
|
|
135
|
-
color: '#f00',
|
|
136
|
-
unit: 'V',
|
|
137
|
-
point: { x: 2, y: 10 }, // Snaps to nearest sample point
|
|
138
|
-
distance: 0,
|
|
139
|
-
})
|
|
140
|
-
expect(candidates[1].point).toEqual({ x: 2, y: 8 }) // Snaps to nearest sample point
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('uses equal horizontal and vertical annotation padding', () => {
|
|
144
|
-
expect(ANNOTATION_TEXT_HORIZONTAL_PADDING).toBe(ANNOTATION_TEXT_PADDING)
|
|
145
|
-
expect(ANNOTATION_TEXT_VERTICAL_PADDING).toBe(ANNOTATION_TEXT_PADDING)
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('finds the closest visible sample across candidate tracks', () => {
|
|
149
|
-
const hit = findNearestAnnotationPoint(
|
|
150
|
-
[
|
|
151
|
-
createTrack(0, 'a', 0, [
|
|
152
|
-
{ x: 0, y: 1 },
|
|
153
|
-
{ x: 1, y: 2 },
|
|
154
|
-
]),
|
|
155
|
-
createTrack(1, 'b', 100, [
|
|
156
|
-
{ x: 0, y: 1 },
|
|
157
|
-
{ x: 1, y: 8 },
|
|
158
|
-
]),
|
|
159
|
-
],
|
|
160
|
-
100,
|
|
161
|
-
120,
|
|
162
|
-
12,
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
expect(hit).toMatchObject({ seriesId: 'b', trackIndex: 1 })
|
|
166
|
-
expect(hit?.point).toEqual({ x: 1, y: 8 })
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('returns no hit outside the configured screen radius', () => {
|
|
170
|
-
const hit = findNearestAnnotationPoint([createTrack(0, 'a', 0, [{ x: 1, y: 2 }])], 160, 10, 12)
|
|
171
|
-
|
|
172
|
-
expect(hit).toBeNull()
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('wraps labels by Unicode characters and resolves style defaults', () => {
|
|
176
|
-
expect(wrapAnnotationText('1234567890中文')).toEqual(['1234567890', '中文'])
|
|
177
|
-
expect(resolveAnnotationStyle({ textColor: '#ffffff' })).toEqual({
|
|
178
|
-
borderColor: '#1677ff',
|
|
179
|
-
textColor: '#ffffff',
|
|
180
|
-
backgroundColor: 'rgba(255, 255, 255, 0.92)',
|
|
181
|
-
})
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('filters invalid entries and keeps coincident labels on the default layout', () => {
|
|
185
|
-
const track = createTrack(
|
|
186
|
-
0,
|
|
187
|
-
'a',
|
|
188
|
-
0,
|
|
189
|
-
[
|
|
190
|
-
{ x: 0, y: 1 },
|
|
191
|
-
{ x: 1, y: 2 },
|
|
192
|
-
],
|
|
193
|
-
300,
|
|
194
|
-
)
|
|
195
|
-
const annotations: WaveformAnnotation[] = [
|
|
196
|
-
{ id: 'first', seriesId: 'a', x: 1, y: 2, text: '第一个标注' },
|
|
197
|
-
{ id: 'second', seriesId: 'a', x: 1, y: 2, text: '第二个标注' },
|
|
198
|
-
{ id: 'unknown', seriesId: 'missing', x: 1, y: 2, text: '不显示' },
|
|
199
|
-
{ id: 'invalid', seriesId: 'a', x: Number.NaN, y: 2, text: '不显示' },
|
|
200
|
-
]
|
|
201
|
-
|
|
202
|
-
const rendered = layoutAnnotations(annotations, [track], 300, 300)
|
|
203
|
-
|
|
204
|
-
expect(rendered.map((item) => item.annotation.id)).toEqual(['first', 'second'])
|
|
205
|
-
expect(rendered[0].box.lineEndX).toBe(rendered[0].anchorX)
|
|
206
|
-
expect(rendered[0].box.lineEndY).not.toBe(rendered[0].anchorY)
|
|
207
|
-
expect(rendered[0].placement).toBe('top')
|
|
208
|
-
expect(rendered[0].box).toMatchObject({
|
|
209
|
-
x: rendered[1].box.x,
|
|
210
|
-
y: rendered[1].box.y,
|
|
211
|
-
})
|
|
212
|
-
rendered.forEach((item) => {
|
|
213
|
-
expect(item.box.x).toBeGreaterThanOrEqual(0)
|
|
214
|
-
expect(item.box.y).toBeGreaterThanOrEqual(0)
|
|
215
|
-
expect(item.box.x + item.box.width).toBeLessThanOrEqual(300)
|
|
216
|
-
expect(item.box.y + item.box.height).toBeLessThanOrEqual(300)
|
|
217
|
-
})
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('uses the default placement and clamps labels to the plot boundary', () => {
|
|
221
|
-
const track = createTrack(
|
|
222
|
-
0,
|
|
223
|
-
'a',
|
|
224
|
-
0,
|
|
225
|
-
[
|
|
226
|
-
{ x: 0, y: 0 },
|
|
227
|
-
{ x: 1, y: 5 },
|
|
228
|
-
],
|
|
229
|
-
200,
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
const centered = layoutAnnotations(
|
|
233
|
-
[{ id: 'centered', seriesId: 'a', x: 1, y: 5, text: '居中' }],
|
|
234
|
-
[track],
|
|
235
|
-
200,
|
|
236
|
-
200,
|
|
237
|
-
)[0]
|
|
238
|
-
expect(centered.placement).toBe('top')
|
|
239
|
-
expect(centered.box.x + centered.box.width / 2).toBe(centered.anchorX)
|
|
240
|
-
expect(centered.box.lineEndX).toBe(centered.anchorX)
|
|
241
|
-
expect(centered.box.lineEndY).toBe(centered.box.y + centered.box.height)
|
|
242
|
-
|
|
243
|
-
const nearTop = layoutAnnotations(
|
|
244
|
-
[{ id: 'top-edge', seriesId: 'a', x: 1, y: 9.5, text: '顶部' }],
|
|
245
|
-
[track],
|
|
246
|
-
200,
|
|
247
|
-
200,
|
|
248
|
-
)[0]
|
|
249
|
-
// Smart placement chooses 'bottom' when near top boundary
|
|
250
|
-
expect(nearTop.placement).toBe('bottom')
|
|
251
|
-
expect(nearTop.box.lineEndX).toBe(nearTop.anchorX)
|
|
252
|
-
expect(nearTop.box.y).toBeGreaterThanOrEqual(0)
|
|
253
|
-
expect(nearTop.box.lineEndY).toBeLessThanOrEqual(nearTop.box.y + nearTop.box.height)
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('intelligently chooses placement to avoid boundaries', () => {
|
|
257
|
-
const track = createTrack(
|
|
258
|
-
0,
|
|
259
|
-
'a',
|
|
260
|
-
0,
|
|
261
|
-
[
|
|
262
|
-
{ x: 0, y: 0 },
|
|
263
|
-
{ x: 1, y: 5 },
|
|
264
|
-
],
|
|
265
|
-
200,
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
const nearTop = layoutAnnotations(
|
|
269
|
-
[{ id: 'top-space', seriesId: 'a', x: 1, y: 7.5, text: '顶部空间不足' }],
|
|
270
|
-
[track],
|
|
271
|
-
200,
|
|
272
|
-
200,
|
|
273
|
-
)[0]
|
|
274
|
-
// Smart placement chooses 'bottom' when top space is insufficient
|
|
275
|
-
expect(nearTop.placement).toBe('bottom')
|
|
276
|
-
expect(nearTop.box.y).toBeGreaterThanOrEqual(0)
|
|
277
|
-
expect(nearTop.box.y + nearTop.box.height).toBeLessThanOrEqual(200)
|
|
278
|
-
|
|
279
|
-
const nearBottom = layoutAnnotations(
|
|
280
|
-
[{ id: 'bottom-space', seriesId: 'a', x: 1, y: 0.5, text: '底部空间不足' }],
|
|
281
|
-
[track],
|
|
282
|
-
200,
|
|
283
|
-
200,
|
|
284
|
-
)[0]
|
|
285
|
-
// Smart placement chooses 'top' when bottom space is insufficient
|
|
286
|
-
expect(nearBottom.placement).toBe('top')
|
|
287
|
-
expect(nearBottom.box.y).toBeGreaterThanOrEqual(0)
|
|
288
|
-
expect(nearBottom.box.y + nearBottom.box.height).toBeLessThanOrEqual(200)
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
it('keeps a clipped fallback when the plot is smaller than the annotation box', () => {
|
|
292
|
-
const track = createTrack(0, 'a', 0, [{ x: 1, y: 5 }])
|
|
293
|
-
const rendered = layoutAnnotations(
|
|
294
|
-
[{ id: 'tiny-plot', seriesId: 'a', x: 1, y: 5, text: '无法完整放置' }],
|
|
295
|
-
[track],
|
|
296
|
-
40,
|
|
297
|
-
20,
|
|
298
|
-
)[0]
|
|
299
|
-
|
|
300
|
-
expect(rendered).toBeDefined()
|
|
301
|
-
expect(rendered.box.x).toBeGreaterThanOrEqual(0)
|
|
302
|
-
expect(rendered.box.y).toBeGreaterThanOrEqual(0)
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
it('applies a persisted label offset without moving the data anchor', () => {
|
|
306
|
-
const track = createTrack(
|
|
307
|
-
0,
|
|
308
|
-
'a',
|
|
309
|
-
0,
|
|
310
|
-
[
|
|
311
|
-
{ x: 0, y: 0 },
|
|
312
|
-
{ x: 1, y: 5 },
|
|
313
|
-
],
|
|
314
|
-
300,
|
|
315
|
-
)
|
|
316
|
-
const baseline = layoutAnnotations(
|
|
317
|
-
[{ id: 'baseline', seriesId: 'a', x: 1, y: 5, text: '偏移' }],
|
|
318
|
-
[track],
|
|
319
|
-
300,
|
|
320
|
-
300,
|
|
321
|
-
)[0]
|
|
322
|
-
const rendered = layoutAnnotations(
|
|
323
|
-
[
|
|
324
|
-
{
|
|
325
|
-
id: 'offset',
|
|
326
|
-
seriesId: 'a',
|
|
327
|
-
x: 1,
|
|
328
|
-
y: 5,
|
|
329
|
-
text: '偏移',
|
|
330
|
-
labelOffsetX: 24,
|
|
331
|
-
labelOffsetY: 18,
|
|
332
|
-
},
|
|
333
|
-
],
|
|
334
|
-
[track],
|
|
335
|
-
300,
|
|
336
|
-
300,
|
|
337
|
-
)[0]
|
|
338
|
-
|
|
339
|
-
expect(rendered.anchorX).toBe(100)
|
|
340
|
-
expect(rendered.anchorY).toBe(150)
|
|
341
|
-
expect(rendered.box.x).toBe(baseline.box.x + 24)
|
|
342
|
-
expect(rendered.box.y).toBe(baseline.box.y + 18)
|
|
343
|
-
expect(rendered.box.lineEndX).not.toBe(rendered.anchorX)
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
it('uses later directional candidates when vertical candidates collide', () => {
|
|
347
|
-
const track = createTrack(
|
|
348
|
-
0,
|
|
349
|
-
'a',
|
|
350
|
-
0,
|
|
351
|
-
[
|
|
352
|
-
{ x: 0, y: 0 },
|
|
353
|
-
{ x: 1, y: 5 },
|
|
354
|
-
],
|
|
355
|
-
200,
|
|
356
|
-
)
|
|
357
|
-
const rendered = layoutAnnotations(
|
|
358
|
-
[
|
|
359
|
-
{ id: 'one', seriesId: 'a', x: 1, y: 5, text: '同一位置一' },
|
|
360
|
-
{ id: 'two', seriesId: 'a', x: 1, y: 5, text: '同一位置二' },
|
|
361
|
-
],
|
|
362
|
-
[track],
|
|
363
|
-
200,
|
|
364
|
-
200,
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
expect(rendered[0].placement).toBe('top')
|
|
368
|
-
expect(rendered[1].placement).toBe('top')
|
|
369
|
-
expect(rendered[1].box).toMatchObject({ x: rendered[0].box.x, y: rendered[0].box.y })
|
|
370
|
-
})
|
|
371
|
-
})
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import type { WaveformAnnotation } from '../../types'
|
|
4
|
-
import { parseWaveformAnnotations, serializeWaveformAnnotations } from './serialization'
|
|
5
|
-
|
|
6
|
-
describe('waveform annotation serialization', () => {
|
|
7
|
-
it('round-trips every annotation field through a versioned document', () => {
|
|
8
|
-
const source: WaveformAnnotation[] = [
|
|
9
|
-
{
|
|
10
|
-
id: 'note-1',
|
|
11
|
-
seriesId: 'channel-a',
|
|
12
|
-
x: 1.25,
|
|
13
|
-
y: -3.5,
|
|
14
|
-
text: '峰值',
|
|
15
|
-
labelOffsetX: 12,
|
|
16
|
-
labelOffsetY: -8,
|
|
17
|
-
createdAt: '2026-07-21T12:00:00.000Z',
|
|
18
|
-
style: {
|
|
19
|
-
borderColor: '#1677ff',
|
|
20
|
-
textColor: '#333333',
|
|
21
|
-
backgroundColor: 'rgba(255, 255, 255, 0.92)',
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
]
|
|
25
|
-
const sourceSnapshot = JSON.parse(JSON.stringify(source))
|
|
26
|
-
|
|
27
|
-
const parsed = parseWaveformAnnotations(serializeWaveformAnnotations(source))
|
|
28
|
-
|
|
29
|
-
expect(JSON.parse(serializeWaveformAnnotations(source))).toMatchObject({ version: 1 })
|
|
30
|
-
expect(source).toEqual(sourceSnapshot)
|
|
31
|
-
expect(parsed).toEqual(source)
|
|
32
|
-
expect(parsed).not.toBe(source)
|
|
33
|
-
expect(parsed[0]).not.toBe(source[0])
|
|
34
|
-
expect(parsed[0].style).not.toBe(source[0].style)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('allows annotations for series that are not currently loaded', () => {
|
|
38
|
-
expect(
|
|
39
|
-
parseWaveformAnnotations(
|
|
40
|
-
JSON.stringify({
|
|
41
|
-
version: 1,
|
|
42
|
-
annotations: [{ id: 'future', seriesId: 'missing', x: 1, y: 2, text: '稍后显示' }],
|
|
43
|
-
}),
|
|
44
|
-
),
|
|
45
|
-
).toEqual([{ id: 'future', seriesId: 'missing', x: 1, y: 2, text: '稍后显示' }])
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it.each([
|
|
49
|
-
['invalid JSON', '{'],
|
|
50
|
-
['non-object root', '[]'],
|
|
51
|
-
['unsupported version', JSON.stringify({ version: 2, annotations: [] })],
|
|
52
|
-
['missing annotation array', JSON.stringify({ version: 1 })],
|
|
53
|
-
[
|
|
54
|
-
'invalid annotation entry',
|
|
55
|
-
JSON.stringify({ version: 1, annotations: [{ id: 'a', seriesId: 's', x: 1 }] }),
|
|
56
|
-
],
|
|
57
|
-
[
|
|
58
|
-
'non-finite coordinate',
|
|
59
|
-
'{"version":1,"annotations":[{"id":"a","seriesId":"s","x":1e400,"y":2,"text":"a"}]}',
|
|
60
|
-
],
|
|
61
|
-
[
|
|
62
|
-
'overlong text',
|
|
63
|
-
JSON.stringify({
|
|
64
|
-
version: 1,
|
|
65
|
-
annotations: [{ id: 'a', seriesId: 's', x: 1, y: 2, text: 'a'.repeat(41) }],
|
|
66
|
-
}),
|
|
67
|
-
],
|
|
68
|
-
[
|
|
69
|
-
'duplicate IDs',
|
|
70
|
-
JSON.stringify({
|
|
71
|
-
version: 1,
|
|
72
|
-
annotations: [
|
|
73
|
-
{ id: 'a', seriesId: 's', x: 1, y: 2, text: 'one' },
|
|
74
|
-
{ id: 'a', seriesId: 's', x: 2, y: 3, text: 'two' },
|
|
75
|
-
],
|
|
76
|
-
}),
|
|
77
|
-
],
|
|
78
|
-
])('rejects %s without returning partial data', (_label, json) => {
|
|
79
|
-
expect(() => parseWaveformAnnotations(json)).toThrow('Invalid waveform annotation file')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('rejects invalid optional fields and serialization input', () => {
|
|
83
|
-
expect(() =>
|
|
84
|
-
parseWaveformAnnotations(
|
|
85
|
-
JSON.stringify({
|
|
86
|
-
version: 1,
|
|
87
|
-
annotations: [
|
|
88
|
-
{
|
|
89
|
-
id: 'a',
|
|
90
|
-
seriesId: 's',
|
|
91
|
-
x: 1,
|
|
92
|
-
y: 2,
|
|
93
|
-
text: 'a',
|
|
94
|
-
labelOffsetX: '12',
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
}),
|
|
98
|
-
),
|
|
99
|
-
).toThrow('labelOffsetX')
|
|
100
|
-
|
|
101
|
-
expect(() =>
|
|
102
|
-
parseWaveformAnnotations(
|
|
103
|
-
JSON.stringify({
|
|
104
|
-
version: 1,
|
|
105
|
-
annotations: [{ id: 'a', seriesId: 's', x: 1, y: 2, text: 'a', style: [] }],
|
|
106
|
-
}),
|
|
107
|
-
),
|
|
108
|
-
).toThrow('style must be an object')
|
|
109
|
-
|
|
110
|
-
expect(() =>
|
|
111
|
-
serializeWaveformAnnotations([{ id: 'a', seriesId: 's', x: Number.NaN, y: 2, text: 'a' }]),
|
|
112
|
-
).toThrow('x must be a finite number')
|
|
113
|
-
})
|
|
114
|
-
})
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { channelColors } from './constants'
|
|
4
|
-
|
|
5
|
-
describe('channelColors', () => {
|
|
6
|
-
it('contains the referenced dark palette followed by twenty supplemental colors', () => {
|
|
7
|
-
expect(channelColors).toHaveLength(30)
|
|
8
|
-
expect(channelColors.slice(0, 10)).toEqual([
|
|
9
|
-
'#0960bd',
|
|
10
|
-
'#ff7f0e',
|
|
11
|
-
'#2ca02c',
|
|
12
|
-
'#d62728',
|
|
13
|
-
'#9467bd',
|
|
14
|
-
'#8c564b',
|
|
15
|
-
'#e377c2',
|
|
16
|
-
'#7f7f7f',
|
|
17
|
-
'#bcbd22',
|
|
18
|
-
'#17becf',
|
|
19
|
-
])
|
|
20
|
-
expect(new Set(channelColors).size).toBe(channelColors.length)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('wraps around after the thirtieth color for automatic series assignment', () => {
|
|
24
|
-
expect(channelColors[0]).toBe('#0960bd')
|
|
25
|
-
expect(channelColors[29]).toBe('#31572c')
|
|
26
|
-
expect(channelColors[30 % channelColors.length]).toBe(channelColors[0])
|
|
27
|
-
expect(channelColors[31 % channelColors.length]).toBe(channelColors[1])
|
|
28
|
-
})
|
|
29
|
-
})
|