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.
Files changed (31) hide show
  1. package/README.md +2 -2
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +535 -575
  4. package/dist/types/components/core/layout.d.ts +2 -4
  5. package/dist/types/components/core/types.d.ts +0 -3
  6. package/dist/types/utils/formatters.d.ts +9 -10
  7. package/dist/types/utils/index.d.ts +1 -1
  8. package/package.json +1 -1
  9. package/src/components/core/layout.test.ts +7 -8
  10. package/src/components/core/layout.ts +10 -20
  11. package/src/components/core/trackLayoutBuilder.ts +4 -23
  12. package/src/components/core/types.ts +0 -3
  13. package/src/components/core/useWaveformLayout.ts +5 -27
  14. package/src/components/interaction/WaveformTooltip.test.ts +16 -0
  15. package/src/components/interaction/useWaveformZoom.ts +12 -8
  16. package/src/components/rendering/WaveformTrackAxes.vue +4 -26
  17. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +7 -7
  18. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +2 -2
  19. package/src/components/waveformChartCases/dataLifecycle.test.ts +4 -4
  20. package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +29 -22
  21. package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +1 -1
  22. package/src/components/waveformChartCases/layoutAndDomains.test.ts +16 -14
  23. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +8 -8
  24. package/src/components/waveformChartCases/presentationAndAxes.test.ts +6 -4
  25. package/src/components/waveformChartCases/presentationMode.test.ts +1 -1
  26. package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +15 -0
  27. package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +59 -11
  28. package/src/components/waveformChartCases/wheelZoomOut.test.ts +114 -0
  29. package/src/utils/formatters.test.ts +40 -31
  30. package/src/utils/formatters.ts +45 -45
  31. 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('4.99')
166
- expect(wrapper.get('.waveform-chart__axis-exponent--x').text()).toBe('E+03')
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: [0, 254], exponent: 'E+02' },
173
- { values: [0, 0.0002], exponent: 'E-04' },
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 wrapper = await mountSizedChart({
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 exponentLabel = wrapper.find('.waveform-chart__axis-exponent--y')
191
+ const exponentLabels = labels.filter((label) => label.startsWith('E'))
186
192
 
187
193
  if (exponent === null) {
188
- expect(exponentLabel.exists()).toBe(false)
194
+ expect(exponentLabels).toHaveLength(0)
189
195
  } else {
190
- expect(exponentLabel.text()).toBe(exponent)
191
- expect(labels.every((label) => !label.startsWith('E'))).toBe(true)
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('1.00')
216
- expect(millisecondsChart.get('.waveform-chart__axis-exponent--x').text()).toBe('E+03')
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.00')
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.00')
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('2.00')
252
+ expect(end.text()).toBe('1999')
246
253
  expect(middleTickLabels.length).toBeGreaterThan(0)
247
- expect(middleTickLabels).not.toContain('0.00')
248
- expect(wrapper.get('.waveform-chart__axis-exponent--x').text()).toBe('E+03')
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()).toContain('.')
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('2.00')
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(-103)
36
+ expect(labelX).toBe(-74)
37
37
  expect(labelBackgroundX).toBe(labelX - 12)
38
- expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(119)
39
- expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(119)
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(89)
66
- expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(89)
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(39)
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 each track x domain in independent mode instead of the shared initial domain', async () => {
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--end').text()).toBe('6.00')
266
- expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5.00')
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('1.00')
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.00')
314
- expect(yTickLabels()).toContain('97.00')
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.00')
319
- expect(yTickLabels()).not.toContain('97.00')
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: 254 },
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
- .findAll('.waveform-chart__axis-exponent--y')
56
- .map((label) => [label.attributes('data-y-axis-index'), label.text()]),
57
- ).toEqual([
58
- ['1', 'E+02'],
59
- ['2', 'E-04'],
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.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
58
- expect(second.get('.waveform-chart__axis-exponent--y').text()).toBe('E+04')
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.get('.waveform-chart__axis-exponent--x').text()).not.toBe('')
303
- expect(wrapper.get('.waveform-chart__axis-exponent--y').text()).not.toBe('')
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',
@@ -241,7 +241,7 @@ describe('WaveformChart presentation mode', () => {
241
241
  await flushPromises()
242
242
  overlay.element.dispatchEvent(
243
243
  new WheelEvent('wheel', {
244
- deltaY: -200,
244
+ deltaY: 200,
245
245
  clientX: width / 2,
246
246
  clientY: height / 2,
247
247
  bubbles: true,
@@ -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.00')
83
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2.00')
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('keeps one global domain while loading narrower and wider windows', async () => {
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 currentDomain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
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
- expect(currentDomain[1] - currentDomain[0]).toBeCloseTo(2.5)
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.00')
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.00')
295
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2.00')
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.00')
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.00')
326
- expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1.00')
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.009)).toBe(true)
19
- expect(shouldUseScientificAxisLabel(0.01)).toBe(false)
20
- expect(shouldUseScientificAxisLabel(99.99)).toBe(false)
21
- expect(shouldUseScientificAxisLabel(100)).toBe(true)
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('shares one separate exponent across an axis', () => {
25
- const positiveAxis = { axisMin: 0, axisMax: 254 }
26
- expect(formatScientificAxisLabel(127, positiveAxis)).toBe('1.27')
27
- expect(formatScientificAxisLabel(254, positiveAxis)).toBe('2.54')
28
- expect(formatScientificAxisExponent(0, 254)).toBe('E+02')
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.0002 }
32
- expect(formatScientificAxisLabel(0.0001, tinyAxis)).toBe('1.00')
33
- expect(formatScientificAxisLabel(0.0002, tinyAxis)).toBe('2.00')
34
- expect(formatScientificAxisExponent(0, 0.0002)).toBe('E-04')
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
- const negativeAxis = { axisMin: -254, axisMax: 0 }
37
- expect(formatScientificAxisLabel(-254, negativeAxis)).toBe('-2.54')
38
- expect(formatScientificAxisLabel(0, negativeAxis)).toBe('0.00')
39
- expect(formatScientificAxisExponent(-254, 0)).toBe('E+02')
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 plain axes at two decimals and removes negative zero', () => {
43
- expect(formatScientificAxisLabel(99.99, { axisMin: 0, axisMax: 99.99 })).toBe('99.99')
44
- expect(formatScientificAxisLabel(0.01, { axisMin: 0, axisMax: 0.01 })).toBe('0.01')
45
- expect(formatScientificAxisLabel(-0.001, { axisMin: -1, axisMax: 1 })).toBe('0.00')
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 from the selected display unit', () => {
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.5, 'ms', domain)).toBe('0.50')
54
- expect(formatEndpointTime(1, domain, 'ms')).toBe('1.00')
55
- expect(formatAxisTimeExponent(domain, 'ms')).toBe('E+03')
56
- expect(formatAxisTime(0.5, 's', domain)).toBe('0.50')
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('1.00')
62
- expect(formatAxisTimeExponent(tinyDomain, 's')).toBe('E-06')
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')