waveform-analysis 0.1.26 → 0.1.28
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 +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +535 -575
- package/dist/types/components/core/layout.d.ts +2 -4
- package/dist/types/components/core/types.d.ts +0 -3
- package/dist/types/utils/formatters.d.ts +9 -10
- package/dist/types/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/core/layout.test.ts +7 -8
- package/src/components/core/layout.ts +10 -20
- package/src/components/core/trackLayoutBuilder.ts +4 -23
- package/src/components/core/types.ts +0 -3
- package/src/components/core/useWaveformLayout.ts +5 -27
- package/src/components/interaction/WaveformTooltip.test.ts +16 -0
- package/src/components/interaction/useWaveformZoom.ts +12 -8
- package/src/components/rendering/WaveformTrackAxes.vue +4 -26
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +7 -7
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +2 -2
- package/src/components/waveformChartCases/dataLifecycle.test.ts +4 -4
- package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +29 -22
- package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +1 -1
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +16 -14
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +8 -8
- package/src/components/waveformChartCases/presentationAndAxes.test.ts +6 -4
- package/src/components/waveformChartCases/presentationMode.test.ts +1 -1
- package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +15 -0
- package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +59 -11
- package/src/components/waveformChartCases/wheelZoomOut.test.ts +114 -0
- package/src/utils/formatters.test.ts +40 -31
- package/src/utils/formatters.ts +45 -45
- package/src/utils/index.ts +0 -1
|
@@ -162,35 +162,42 @@ describe('WaveformChart', () => {
|
|
|
162
162
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').attributes('x')).toBe(
|
|
163
163
|
String(trackWidth),
|
|
164
164
|
)
|
|
165
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
166
|
-
expect(wrapper.
|
|
165
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4990')
|
|
166
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
167
167
|
})
|
|
168
168
|
|
|
169
169
|
it('uses one shared scientific exponent only for large and tiny Y-axis domains', async () => {
|
|
170
170
|
const cases = [
|
|
171
|
-
{ values: [0, 50], exponent: null },
|
|
172
|
-
{ values: [
|
|
173
|
-
{
|
|
171
|
+
{ values: [0, 50], yDomain: [0, 50] as [number, number], exponent: null },
|
|
172
|
+
{ values: [1000, 3000], yDomain: [1000, 3000] as [number, number], exponent: 'E+03' },
|
|
173
|
+
{
|
|
174
|
+
values: [0.0001, 0.0003],
|
|
175
|
+
yDomain: [0.0001, 0.0003] as [number, number],
|
|
176
|
+
exponent: 'E-04',
|
|
177
|
+
},
|
|
174
178
|
]
|
|
175
179
|
|
|
176
|
-
for (const { values, exponent } of cases) {
|
|
177
|
-
const
|
|
178
|
-
kind: 'points',
|
|
180
|
+
for (const { values, yDomain, exponent } of cases) {
|
|
181
|
+
const data = {
|
|
182
|
+
kind: 'points' as const,
|
|
179
183
|
points: values.map((y, x) => ({ x, y })),
|
|
180
|
-
}
|
|
184
|
+
}
|
|
185
|
+
const originalValues = data.points.map((point) => point.y)
|
|
186
|
+
const wrapper = await mountSizedChart(data, { yDomain })
|
|
181
187
|
const labels = wrapper
|
|
182
188
|
.get('.waveform-chart__axis--y')
|
|
183
189
|
.findAll('.tick text')
|
|
184
190
|
.map((tick) => tick.text())
|
|
185
|
-
const
|
|
191
|
+
const exponentLabels = labels.filter((label) => label.startsWith('E'))
|
|
186
192
|
|
|
187
193
|
if (exponent === null) {
|
|
188
|
-
expect(
|
|
194
|
+
expect(exponentLabels).toHaveLength(0)
|
|
189
195
|
} else {
|
|
190
|
-
expect(
|
|
191
|
-
expect(
|
|
192
|
-
expect(labels.every((label) => /^-?\d+\.\d{2}$/.test(label))).toBe(true)
|
|
196
|
+
expect(exponentLabels).toHaveLength(1)
|
|
197
|
+
expect(exponentLabels[0]).toMatch(new RegExp(`^${exponent.replace('+', '\\+')} `))
|
|
193
198
|
}
|
|
199
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
|
200
|
+
expect(data.points.map((point) => point.y)).toEqual(originalValues)
|
|
194
201
|
|
|
195
202
|
wrapper.unmount()
|
|
196
203
|
}
|
|
@@ -212,13 +219,13 @@ describe('WaveformChart', () => {
|
|
|
212
219
|
|
|
213
220
|
expect(millisecondsChart.text()).toContain('时间(ms)')
|
|
214
221
|
expect(millisecondTicks.length).toBeGreaterThan(0)
|
|
215
|
-
expect(millisecondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
216
|
-
expect(millisecondsChart.
|
|
222
|
+
expect(millisecondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
223
|
+
expect(millisecondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
217
224
|
expect(millisecondsChart.find('.waveform-chart__watermark').exists()).toBe(false)
|
|
218
225
|
|
|
219
226
|
const secondsChart = await mountSizedChart(data, { timeUnit: 's', xLabel: 'Elapsed time' })
|
|
220
227
|
expect(secondsChart.text()).toContain('Elapsed time')
|
|
221
|
-
expect(secondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1
|
|
228
|
+
expect(secondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1')
|
|
222
229
|
expect(secondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
223
230
|
})
|
|
224
231
|
|
|
@@ -237,15 +244,15 @@ describe('WaveformChart', () => {
|
|
|
237
244
|
|
|
238
245
|
expect(start.attributes('x')).toBe('0')
|
|
239
246
|
expect(start.attributes('text-anchor')).toBe('start')
|
|
240
|
-
expect(start.text()).toBe('0
|
|
247
|
+
expect(start.text()).toBe('0')
|
|
241
248
|
expect(end.attributes('x')).toBe(
|
|
242
249
|
wrapper.get('.waveform-chart__track').attributes('data-track-width'),
|
|
243
250
|
)
|
|
244
251
|
expect(end.attributes('text-anchor')).toBe('end')
|
|
245
|
-
expect(end.text()).toBe('
|
|
252
|
+
expect(end.text()).toBe('1999')
|
|
246
253
|
expect(middleTickLabels.length).toBeGreaterThan(0)
|
|
247
|
-
expect(middleTickLabels).not.toContain('0
|
|
248
|
-
expect(wrapper.
|
|
254
|
+
expect(middleTickLabels).not.toContain('0')
|
|
255
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
249
256
|
expect(wrapper.findAll('.waveform-chart__grid--major line').length).toBeGreaterThan(
|
|
250
257
|
middleTickLabels.length,
|
|
251
258
|
)
|
|
@@ -300,6 +307,6 @@ describe('WaveformChart', () => {
|
|
|
300
307
|
)
|
|
301
308
|
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe(initialStart)
|
|
302
309
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).not.toBe(initialEnd)
|
|
303
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).
|
|
310
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toMatch(/^-?\d+$/)
|
|
304
311
|
})
|
|
305
312
|
})
|
|
@@ -59,7 +59,7 @@ describe('WaveformChart', () => {
|
|
|
59
59
|
await flushPromises()
|
|
60
60
|
|
|
61
61
|
expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(1)
|
|
62
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
62
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
it('updates rendering props and disables zoom interaction', async () => {
|
|
@@ -33,10 +33,10 @@ describe('WaveformChart', () => {
|
|
|
33
33
|
tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
expect(labelX).toBe(-
|
|
36
|
+
expect(labelX).toBe(-74)
|
|
37
37
|
expect(labelBackgroundX).toBe(labelX - 12)
|
|
38
|
-
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(
|
|
39
|
-
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(
|
|
38
|
+
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(90)
|
|
39
|
+
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(90)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
it('keeps a tick-only gutter when channel labels are empty', async () => {
|
|
@@ -62,8 +62,8 @@ describe('WaveformChart', () => {
|
|
|
62
62
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
|
63
63
|
|
|
64
64
|
expect(wrapper.findAll('.waveform-chart__y-axis-label')).toHaveLength(0)
|
|
65
|
-
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(
|
|
66
|
-
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(
|
|
65
|
+
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(64)
|
|
66
|
+
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(60)
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
it('keeps the Y-axis label gutter stable while paging between value ranges', async () => {
|
|
@@ -122,7 +122,7 @@ describe('WaveformChart', () => {
|
|
|
122
122
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
123
123
|
const firstWidth = Number(tracks[0].attributes('data-track-width'))
|
|
124
124
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
|
125
|
-
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(
|
|
125
|
+
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(32)
|
|
126
126
|
})
|
|
127
127
|
|
|
128
128
|
it('uses one shared overlay and bottom-row x axes for separated and compact grids', async () => {
|
|
@@ -225,7 +225,7 @@ describe('WaveformChart', () => {
|
|
|
225
225
|
}
|
|
226
226
|
})
|
|
227
227
|
|
|
228
|
-
it('uses
|
|
228
|
+
it('uses the shared initial x domain for every independent track by default', async () => {
|
|
229
229
|
const wrapper = await mountSizedChart(
|
|
230
230
|
{
|
|
231
231
|
kind: 'series',
|
|
@@ -262,8 +262,10 @@ describe('WaveformChart', () => {
|
|
|
262
262
|
)
|
|
263
263
|
|
|
264
264
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
265
|
-
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--
|
|
266
|
-
expect(tracks[
|
|
265
|
+
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-8000')
|
|
266
|
+
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
|
|
267
|
+
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-8000')
|
|
268
|
+
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
|
|
267
269
|
})
|
|
268
270
|
|
|
269
271
|
it('uses an explicit initial x domain override for an independent track', async () => {
|
|
@@ -291,7 +293,7 @@ describe('WaveformChart', () => {
|
|
|
291
293
|
},
|
|
292
294
|
)
|
|
293
295
|
|
|
294
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
296
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
295
297
|
})
|
|
296
298
|
|
|
297
299
|
it('reacts to exact fixed Y-domain props and returns to automatic bounds', async () => {
|
|
@@ -310,13 +312,13 @@ describe('WaveformChart', () => {
|
|
|
310
312
|
|
|
311
313
|
await wrapper.setProps({ yDomain: [3, 97] })
|
|
312
314
|
await flushPromises()
|
|
313
|
-
expect(yTickLabels()).toContain('3
|
|
314
|
-
expect(yTickLabels()).toContain('97
|
|
315
|
+
expect(yTickLabels()).toContain('3')
|
|
316
|
+
expect(yTickLabels()).toContain('97')
|
|
315
317
|
|
|
316
318
|
await wrapper.setProps({ yDomain: undefined })
|
|
317
319
|
await flushPromises()
|
|
318
|
-
expect(yTickLabels()).not.toContain('3
|
|
319
|
-
expect(yTickLabels()).not.toContain('97
|
|
320
|
+
expect(yTickLabels()).not.toContain('3')
|
|
321
|
+
expect(yTickLabels()).not.toContain('97')
|
|
320
322
|
})
|
|
321
323
|
|
|
322
324
|
it('keeps annotations bound to their channel while paging', async () => {
|
|
@@ -30,7 +30,7 @@ describe('WaveformChart', () => {
|
|
|
30
30
|
kind: 'points',
|
|
31
31
|
points: [
|
|
32
32
|
{ x: 0, y: 0 },
|
|
33
|
-
{ x: 1, y:
|
|
33
|
+
{ x: 1, y: 2540 },
|
|
34
34
|
],
|
|
35
35
|
},
|
|
36
36
|
},
|
|
@@ -51,13 +51,13 @@ describe('WaveformChart', () => {
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
expect(
|
|
54
|
-
wrapper
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
])
|
|
54
|
+
wrapper.findAll('.waveform-chart__axis--y').map((axis) =>
|
|
55
|
+
axis
|
|
56
|
+
.findAll('.tick text')
|
|
57
|
+
.map((label) => label.text())
|
|
58
|
+
.filter((label) => label.startsWith('E')),
|
|
59
|
+
),
|
|
60
|
+
).toEqual([[], [expect.stringMatching(/^E\+03 /)], [expect.stringMatching(/^E-04 /)]])
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
it('reprojects annotations with the Y axis assigned to their series', async () => {
|
|
@@ -54,8 +54,9 @@ describe('WaveformChart', () => {
|
|
|
54
54
|
],
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
expect(first.
|
|
58
|
-
expect(second.get('.waveform-chart__axis
|
|
57
|
+
expect(first.get('.waveform-chart__axis--y').text()).not.toContain('E')
|
|
58
|
+
expect(second.get('.waveform-chart__axis--y').text()).toContain('E+04 ')
|
|
59
|
+
expect(second.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
|
59
60
|
})
|
|
60
61
|
|
|
61
62
|
it('renders a configurable zero line only when the Y domain contains zero', async () => {
|
|
@@ -299,8 +300,9 @@ describe('WaveformChart', () => {
|
|
|
299
300
|
})
|
|
300
301
|
expect(xAxis.get('path.domain').attributes('display')).toBe('none')
|
|
301
302
|
expect(wrapper.findAll('.waveform-chart__axis-endpoint')).toHaveLength(2)
|
|
302
|
-
expect(wrapper.
|
|
303
|
-
expect(
|
|
303
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
304
|
+
expect(yAxes.some((axis) => axis.text().includes('E+04 '))).toBe(true)
|
|
305
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
|
304
306
|
expect(wrapper.find('.waveform-chart__grid').exists()).toBe(false)
|
|
305
307
|
expect(wrapper.get('.waveform-chart__plot-frame').attributes()).toMatchObject({
|
|
306
308
|
stroke: '#dc2626',
|
|
@@ -38,6 +38,21 @@ describe('WaveformChart', () => {
|
|
|
38
38
|
expect(domain[1]).toBeLessThanOrEqual(1)
|
|
39
39
|
expect(endpoints()[0]).not.toBe(initialEndpoints[0])
|
|
40
40
|
expect(endpoints()[1]).toBe(initialEndpoints[1])
|
|
41
|
+
|
|
42
|
+
overlays[0].element.dispatchEvent(
|
|
43
|
+
new WheelEvent('wheel', {
|
|
44
|
+
deltaY: 4_000,
|
|
45
|
+
clientX: firstWidth - 1,
|
|
46
|
+
clientY: 130,
|
|
47
|
+
bubbles: true,
|
|
48
|
+
cancelable: true,
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
flushAnimationFrames()
|
|
52
|
+
await flushPromises()
|
|
53
|
+
|
|
54
|
+
expect(endpoints()[0]).toBe(initialEndpoints[0])
|
|
55
|
+
expect(endpoints()[1]).toBe(initialEndpoints[1])
|
|
41
56
|
})
|
|
42
57
|
|
|
43
58
|
it('emits one zoom-end payload after a shared zoom gesture completes', async () => {
|
|
@@ -79,8 +79,8 @@ describe('WaveformChart', () => {
|
|
|
79
79
|
|
|
80
80
|
expect(wrapper.emitted('zoom-change')).toBeUndefined()
|
|
81
81
|
expect(wrapper.emitted('zoom-end')).toBeUndefined()
|
|
82
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0
|
|
83
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
82
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
|
|
83
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
it('ignores independent viewport dragging', async () => {
|
|
@@ -207,7 +207,7 @@ describe('WaveformChart', () => {
|
|
|
207
207
|
expect(secondDomain[1] - secondDomain[0]).toBeCloseTo(10)
|
|
208
208
|
})
|
|
209
209
|
|
|
210
|
-
it('
|
|
210
|
+
it('zooms a shared viewport back out after loading a narrower data window', async () => {
|
|
211
211
|
const wrapper = await mountSizedChart(
|
|
212
212
|
{
|
|
213
213
|
kind: 'points',
|
|
@@ -256,9 +256,57 @@ describe('WaveformChart', () => {
|
|
|
256
256
|
|
|
257
257
|
const eventCount = wrapper.emitted('zoom-change')?.length ?? 0
|
|
258
258
|
await dispatchWheel(4000)
|
|
259
|
-
const
|
|
259
|
+
const zoomedOutDomain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
|
260
|
+
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(eventCount + 1)
|
|
261
|
+
expect(zoomedOutDomain[1] - zoomedOutDomain[0]).toBeCloseTo(100)
|
|
262
|
+
|
|
263
|
+
const fullDomainEventCount = wrapper.emitted('zoom-change')?.length ?? 0
|
|
264
|
+
await dispatchWheel(4000)
|
|
265
|
+
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(fullDomainEventCount)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('allows zooming out when the minimum visible point count blocks zooming in', async () => {
|
|
269
|
+
const wrapper = await mountSizedChart({
|
|
270
|
+
kind: 'points',
|
|
271
|
+
points: [
|
|
272
|
+
{ x: 0, y: 0 },
|
|
273
|
+
{ x: 1, y: 10 },
|
|
274
|
+
{ x: 2, y: 20 },
|
|
275
|
+
],
|
|
276
|
+
})
|
|
277
|
+
const overlay = wrapper.get('.waveform-chart__overlay--independent')
|
|
278
|
+
const width = Number(overlay.attributes('width'))
|
|
279
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
280
|
+
value: () => ({ left: 0, top: 0, width, height: 290 }),
|
|
281
|
+
})
|
|
282
|
+
const dispatchWheel = async (deltaY: number) => {
|
|
283
|
+
overlay.element.dispatchEvent(
|
|
284
|
+
new WheelEvent('wheel', {
|
|
285
|
+
deltaY,
|
|
286
|
+
clientX: width / 2,
|
|
287
|
+
clientY: 145,
|
|
288
|
+
bubbles: true,
|
|
289
|
+
cancelable: true,
|
|
290
|
+
}),
|
|
291
|
+
)
|
|
292
|
+
flushAnimationFrames()
|
|
293
|
+
await flushPromises()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await dispatchWheel(-4000)
|
|
297
|
+
const zoomedDomain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
|
298
|
+
expect(zoomedDomain[1] - zoomedDomain[0]).toBeCloseTo(0.05)
|
|
299
|
+
|
|
300
|
+
await wrapper.setProps({ minVisiblePoints: 10 })
|
|
301
|
+
await flushPromises()
|
|
302
|
+
const eventCount = wrapper.emitted('zoom-change')?.length ?? 0
|
|
303
|
+
await dispatchWheel(-1000)
|
|
260
304
|
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(eventCount)
|
|
261
|
-
|
|
305
|
+
|
|
306
|
+
await dispatchWheel(4000)
|
|
307
|
+
const zoomedOutDomain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
|
308
|
+
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(eventCount + 1)
|
|
309
|
+
expect(zoomedOutDomain).toEqual([0, 2])
|
|
262
310
|
})
|
|
263
311
|
|
|
264
312
|
it('resets a shared viewport on double-click and emits zoom-reset', async () => {
|
|
@@ -285,14 +333,14 @@ describe('WaveformChart', () => {
|
|
|
285
333
|
)
|
|
286
334
|
flushAnimationFrames()
|
|
287
335
|
await flushPromises()
|
|
288
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe('0
|
|
336
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe('0')
|
|
289
337
|
|
|
290
338
|
overlay.element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }))
|
|
291
339
|
await flushPromises()
|
|
292
340
|
|
|
293
341
|
expect(wrapper.emitted('zoom-reset')).toHaveLength(1)
|
|
294
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0
|
|
295
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
342
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
|
|
343
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
|
296
344
|
})
|
|
297
345
|
|
|
298
346
|
it('exposes resetViewport for independent tracks', async () => {
|
|
@@ -316,13 +364,13 @@ describe('WaveformChart', () => {
|
|
|
316
364
|
)
|
|
317
365
|
flushAnimationFrames()
|
|
318
366
|
await flushPromises()
|
|
319
|
-
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).not.toBe('0
|
|
367
|
+
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).not.toBe('0')
|
|
320
368
|
|
|
321
369
|
const chart = wrapper.vm as unknown as { resetViewport: () => void }
|
|
322
370
|
chart.resetViewport()
|
|
323
371
|
await flushPromises()
|
|
324
372
|
|
|
325
|
-
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).toBe('0
|
|
326
|
-
expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('
|
|
373
|
+
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).toBe('0')
|
|
374
|
+
expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
|
|
327
375
|
})
|
|
328
376
|
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { flushPromises } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { flushAnimationFrames } from '../../test/setup'
|
|
5
|
+
import { mountSizedChart } from '../../test/waveformChart'
|
|
6
|
+
|
|
7
|
+
describe('WaveformChart wheel zoom out', () => {
|
|
8
|
+
it('restores an independent initial domain after replacing data with a narrow window', async () => {
|
|
9
|
+
const createData = (start: number, end: number) => ({
|
|
10
|
+
kind: 'series' as const,
|
|
11
|
+
series: [
|
|
12
|
+
{
|
|
13
|
+
id: 'channel-a',
|
|
14
|
+
name: '通道 A',
|
|
15
|
+
data: {
|
|
16
|
+
kind: 'points' as const,
|
|
17
|
+
points: [
|
|
18
|
+
{ x: start, y: 0 },
|
|
19
|
+
{ x: end, y: 1 },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
})
|
|
25
|
+
const wrapper = await mountSizedChart(createData(-5, 5), {
|
|
26
|
+
displayMode: 'independent',
|
|
27
|
+
initialXDomain: [-5, 5],
|
|
28
|
+
})
|
|
29
|
+
const overlay = wrapper.get('.waveform-chart__overlay--independent')
|
|
30
|
+
const overlayWidth = Number(overlay.attributes('width'))
|
|
31
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
32
|
+
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
33
|
+
})
|
|
34
|
+
const dispatchWheel = async (deltaY: number) => {
|
|
35
|
+
overlay.element.dispatchEvent(
|
|
36
|
+
new WheelEvent('wheel', {
|
|
37
|
+
deltaY,
|
|
38
|
+
clientX: overlayWidth / 2,
|
|
39
|
+
clientY: 145,
|
|
40
|
+
bubbles: true,
|
|
41
|
+
cancelable: true,
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
flushAnimationFrames()
|
|
45
|
+
await flushPromises()
|
|
46
|
+
}
|
|
47
|
+
const endpoints = () => ({
|
|
48
|
+
start: wrapper.get('.waveform-chart__axis-endpoint--start').text(),
|
|
49
|
+
end: wrapper.get('.waveform-chart__axis-endpoint--end').text(),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(endpoints()).toEqual({ start: '-5000', end: '5000' })
|
|
53
|
+
await dispatchWheel(-4000)
|
|
54
|
+
expect(endpoints()).not.toEqual({ start: '-5000', end: '5000' })
|
|
55
|
+
|
|
56
|
+
await wrapper.setProps({ data: createData(-0.125, 0.125) })
|
|
57
|
+
await flushPromises()
|
|
58
|
+
await dispatchWheel(4000)
|
|
59
|
+
|
|
60
|
+
expect(endpoints()).toEqual({ start: '-5000', end: '5000' })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('emits one zoom-end payload after zooming a shared viewport out', async () => {
|
|
64
|
+
vi.useFakeTimers()
|
|
65
|
+
try {
|
|
66
|
+
const wrapper = await mountSizedChart({
|
|
67
|
+
kind: 'points',
|
|
68
|
+
points: [
|
|
69
|
+
{ x: 0, y: 0 },
|
|
70
|
+
{ x: 2, y: 1 },
|
|
71
|
+
],
|
|
72
|
+
})
|
|
73
|
+
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
74
|
+
const overlayWidth = Number(overlay.attributes('width'))
|
|
75
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
76
|
+
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
77
|
+
})
|
|
78
|
+
const dispatchWheel = (deltaY: number) =>
|
|
79
|
+
overlay.element.dispatchEvent(
|
|
80
|
+
new WheelEvent('wheel', {
|
|
81
|
+
deltaY,
|
|
82
|
+
clientX: overlayWidth / 2,
|
|
83
|
+
clientY: 145,
|
|
84
|
+
bubbles: true,
|
|
85
|
+
cancelable: true,
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
dispatchWheel(-4000)
|
|
90
|
+
flushAnimationFrames()
|
|
91
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
92
|
+
await flushPromises()
|
|
93
|
+
const zoomedDomain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
|
94
|
+
const zoomedSpan = zoomedDomain[1] - zoomedDomain[0]
|
|
95
|
+
|
|
96
|
+
dispatchWheel(1000)
|
|
97
|
+
flushAnimationFrames()
|
|
98
|
+
expect(wrapper.emitted('zoom-end')).toHaveLength(1)
|
|
99
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
100
|
+
await flushPromises()
|
|
101
|
+
|
|
102
|
+
const zoomedOutPayload = wrapper.emitted('zoom-end')?.at(-1)?.[0] as {
|
|
103
|
+
start: number
|
|
104
|
+
end: number
|
|
105
|
+
gesture: string
|
|
106
|
+
}
|
|
107
|
+
expect(wrapper.emitted('zoom-end')).toHaveLength(2)
|
|
108
|
+
expect(zoomedOutPayload.end - zoomedOutPayload.start).toBeGreaterThan(zoomedSpan)
|
|
109
|
+
expect(zoomedOutPayload.gesture).toBe('wheel')
|
|
110
|
+
} finally {
|
|
111
|
+
vi.useRealTimers()
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -3,69 +3,78 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import {
|
|
4
4
|
formatAnnotationTime,
|
|
5
5
|
formatAxisTime,
|
|
6
|
-
formatAxisTimeExponent,
|
|
7
6
|
formatEndpointTime,
|
|
8
7
|
formatPlainNumber,
|
|
9
8
|
formatScientificAxisExponent,
|
|
10
9
|
formatScientificAxisLabel,
|
|
11
10
|
formatTooltipNumber,
|
|
11
|
+
formatTooltipTime,
|
|
12
|
+
resolveScientificAxisExponent,
|
|
12
13
|
shouldUseScientificAxisLabel,
|
|
13
14
|
} from './formatters'
|
|
14
15
|
|
|
15
16
|
describe('waveform number formatters', () => {
|
|
16
17
|
it('uses the reference Y-axis scientific notation boundaries', () => {
|
|
17
18
|
expect(shouldUseScientificAxisLabel(0)).toBe(false)
|
|
18
|
-
expect(shouldUseScientificAxisLabel(0.
|
|
19
|
-
expect(shouldUseScientificAxisLabel(0.
|
|
20
|
-
expect(shouldUseScientificAxisLabel(
|
|
21
|
-
expect(shouldUseScientificAxisLabel(
|
|
19
|
+
expect(shouldUseScientificAxisLabel(0.000999)).toBe(true)
|
|
20
|
+
expect(shouldUseScientificAxisLabel(0.001)).toBe(false)
|
|
21
|
+
expect(shouldUseScientificAxisLabel(999.999)).toBe(false)
|
|
22
|
+
expect(shouldUseScientificAxisLabel(1000)).toBe(true)
|
|
22
23
|
})
|
|
23
24
|
|
|
24
|
-
it('
|
|
25
|
-
const positiveAxis = { axisMin:
|
|
26
|
-
expect(formatScientificAxisLabel(
|
|
27
|
-
expect(formatScientificAxisLabel(
|
|
28
|
-
expect(
|
|
25
|
+
it('prefixes one shared exponent to the largest visible tick', () => {
|
|
26
|
+
const positiveAxis = { axisMin: 1000, axisMax: 3000 }
|
|
27
|
+
expect(formatScientificAxisLabel(1000, positiveAxis)).toBe('1')
|
|
28
|
+
expect(formatScientificAxisLabel(2000, positiveAxis)).toBe('2')
|
|
29
|
+
expect(formatScientificAxisLabel(3000, positiveAxis)).toBe('E+03 3')
|
|
30
|
+
expect(formatScientificAxisLabel(2000, { ...positiveAxis, topTickValue: 2000 })).toBe('E+03 2')
|
|
31
|
+
expect(formatScientificAxisExponent(1000, 3000)).toBe('E+03')
|
|
29
32
|
expect(formatScientificAxisExponent(0, 1e120)).toBe('E+120')
|
|
30
33
|
|
|
31
|
-
const tinyAxis = { axisMin: 0, axisMax: 0.
|
|
32
|
-
expect(formatScientificAxisLabel(0.0001, tinyAxis)).toBe('1
|
|
33
|
-
expect(formatScientificAxisLabel(0.0002, tinyAxis)).toBe('2
|
|
34
|
-
expect(
|
|
34
|
+
const tinyAxis = { axisMin: 0.0001, axisMax: 0.0003 }
|
|
35
|
+
expect(formatScientificAxisLabel(0.0001, tinyAxis)).toBe('1')
|
|
36
|
+
expect(formatScientificAxisLabel(0.0002, tinyAxis)).toBe('2')
|
|
37
|
+
expect(formatScientificAxisLabel(0.0003, tinyAxis)).toBe('E-04 3')
|
|
38
|
+
expect(formatScientificAxisExponent(0.0001, 0.0003)).toBe('E-04')
|
|
39
|
+
})
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
expect(
|
|
38
|
-
expect(
|
|
39
|
-
expect(
|
|
41
|
+
it('derives the exponent from Math.max(axisMin, axisMax)', () => {
|
|
42
|
+
expect(resolveScientificAxisExponent(-9000, -1000)).toBe(3)
|
|
43
|
+
expect(resolveScientificAxisExponent(-10_000, 3000)).toBe(3)
|
|
44
|
+
expect(resolveScientificAxisExponent(-1000, 0)).toBeNull()
|
|
45
|
+
expect(resolveScientificAxisExponent(0, 0)).toBeNull()
|
|
40
46
|
})
|
|
41
47
|
|
|
42
|
-
it('keeps
|
|
43
|
-
expect(formatScientificAxisLabel(
|
|
44
|
-
expect(formatScientificAxisLabel(
|
|
45
|
-
expect(formatScientificAxisLabel(
|
|
48
|
+
it('keeps five significant digits, removes trailing zeroes, and limits decimals to four', () => {
|
|
49
|
+
expect(formatScientificAxisLabel(123.456, { axisMin: 0, axisMax: 999 })).toBe('123.46')
|
|
50
|
+
expect(formatScientificAxisLabel(1234.56, { axisMin: 0, axisMax: 9999 })).toBe('1.2346')
|
|
51
|
+
expect(formatScientificAxisLabel(3000, { axisMin: 1000, axisMax: 3000 })).toBe('E+03 3')
|
|
52
|
+
expect(formatScientificAxisLabel(-0, { axisMin: -1, axisMax: 1 })).toBe('0')
|
|
46
53
|
expect(formatScientificAxisLabel(Number.NaN)).toBe('NaN')
|
|
47
54
|
expect(formatScientificAxisLabel(Number.POSITIVE_INFINITY)).toBe('Infinity')
|
|
48
55
|
expect(formatScientificAxisExponent(0, 0)).toBeNull()
|
|
49
56
|
})
|
|
50
57
|
|
|
51
|
-
it('formats X-axis ticks and endpoints
|
|
58
|
+
it('formats X-axis ticks and endpoints as plain integers in the selected display unit', () => {
|
|
52
59
|
const domain: [number, number] = [0, 1]
|
|
53
|
-
expect(formatAxisTime(0.
|
|
54
|
-
expect(formatEndpointTime(1, domain, 'ms')).toBe('
|
|
55
|
-
expect(
|
|
56
|
-
expect(
|
|
57
|
-
expect(formatEndpointTime(1, domain, 's')).toBe('1.00')
|
|
58
|
-
expect(formatAxisTimeExponent(domain, 's')).toBeNull()
|
|
60
|
+
expect(formatAxisTime(0.5004, 'ms', domain)).toBe('500')
|
|
61
|
+
expect(formatEndpointTime(1, domain, 'ms')).toBe('1000')
|
|
62
|
+
expect(formatAxisTime(0.5, 's', domain)).toBe('1')
|
|
63
|
+
expect(formatEndpointTime(1, domain, 's')).toBe('1')
|
|
59
64
|
|
|
60
65
|
const tinyDomain: [number, number] = [0, 0.000001]
|
|
61
|
-
expect(formatEndpointTime(0.000001, tinyDomain, 's')).toBe('
|
|
62
|
-
expect(
|
|
66
|
+
expect(formatEndpointTime(0.000001, tinyDomain, 's')).toBe('0')
|
|
67
|
+
expect(formatAxisTime(-0.000001, 's', tinyDomain)).toBe('0')
|
|
68
|
+
expect(formatAxisTime(1e21, 's')).toBe('1000000000000000000000')
|
|
69
|
+
expect(formatAxisTime(Number.POSITIVE_INFINITY, 's')).toBe('Infinity')
|
|
63
70
|
})
|
|
64
71
|
|
|
65
72
|
it('formats tooltip and raw values for their display contexts', () => {
|
|
66
73
|
expect(formatTooltipNumber(12345.67891)).toBe('12,345.6789')
|
|
67
74
|
expect(formatTooltipNumber(-0)).toBe('0')
|
|
68
75
|
expect(formatTooltipNumber(Number.POSITIVE_INFINITY)).toBe('Infinity')
|
|
76
|
+
expect(formatTooltipTime(1.234567, 's')).toBe('1.2346')
|
|
77
|
+
expect(formatTooltipTime(1, 'ms')).toBe('1,000')
|
|
69
78
|
expect(formatPlainNumber(0.0000001)).toBe('0.0000001')
|
|
70
79
|
expect(formatPlainNumber(1e21)).toBe('1000000000000000000000')
|
|
71
80
|
expect(formatPlainNumber(-0)).toBe('0')
|