waveform-analysis 0.1.75 → 0.1.77

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 (78) hide show
  1. package/README.md +183 -42
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/animation/context.d.ts +1 -0
  6. package/dist/types/components/animation/paginationPresentation.d.ts +7 -7
  7. package/dist/types/components/animation/useWaveformAnimations.d.ts +1 -18
  8. package/dist/types/components/core/amplitudeLimits.d.ts +6 -0
  9. package/dist/types/components/core/layout.d.ts +6 -9
  10. package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
  11. package/dist/types/components/core/useWaveformChartController.d.ts +2 -19
  12. package/dist/types/components/core/useWaveformData.d.ts +2 -2
  13. package/dist/types/components/core/useWaveformMeasurements.d.ts +4 -1
  14. package/dist/types/components/core/waveformChartTypes.d.ts +4 -2
  15. package/dist/types/components/core/xAxisTicks.d.ts +2 -0
  16. package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
  17. package/dist/types/components/core/yAxisTicks.d.ts +6 -0
  18. package/dist/types/components/data/types.d.ts +1 -1
  19. package/dist/types/components/interaction/integerZoom.d.ts +3 -3
  20. package/dist/types/components/interaction/stepZoom.d.ts +5 -0
  21. package/dist/types/components/interaction/useWaveformViewport.d.ts +0 -3
  22. package/dist/types/components/interaction/useWaveformZoom.d.ts +0 -2
  23. package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +0 -1
  24. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +0 -1
  25. package/dist/types/core/data.d.ts +1 -1
  26. package/dist/types/demo/DemoChartHost.vue.d.ts +1 -1
  27. package/dist/types/demo/remoteWaveformLoader.d.ts +23 -0
  28. package/dist/types/demo/types.d.ts +11 -2
  29. package/dist/types/demo/useRemoteWaveformWindow.d.ts +22 -0
  30. package/dist/types/index.d.ts +1 -1
  31. package/dist/types/types/chart.d.ts +8 -1
  32. package/dist/types/types/index.d.ts +1 -1
  33. package/dist/types/utils/formatters.d.ts +4 -4
  34. package/dist/types/utils/xAxisNumber.d.ts +2 -0
  35. package/package.json +1 -1
  36. package/src/App.vue +34 -100
  37. package/src/components/WaveformChartView.vue +0 -4
  38. package/src/components/animation/config.ts +1 -1
  39. package/src/components/animation/context.ts +9 -1
  40. package/src/components/animation/paginationPresentation.ts +55 -64
  41. package/src/components/animation/useWaveformAnimations.ts +36 -13
  42. package/src/components/controls/useChartCommands.ts +12 -13
  43. package/src/components/core/amplitudeLimits.ts +44 -0
  44. package/src/components/core/layout.ts +22 -34
  45. package/src/components/core/trackLayoutBuilder.ts +12 -5
  46. package/src/components/core/useWaveformChartController.ts +3 -4
  47. package/src/components/core/useWaveformChartLifecycle.ts +11 -2
  48. package/src/components/core/useWaveformData.ts +11 -5
  49. package/src/components/core/useWaveformLayout.ts +9 -4
  50. package/src/components/core/useWaveformMeasurements.ts +3 -0
  51. package/src/components/core/useWaveformYAxisLayout.ts +2 -0
  52. package/src/components/core/waveformChartTypes.ts +4 -1
  53. package/src/components/core/xAxisTicks.ts +69 -0
  54. package/src/components/core/yAxisLayoutMetrics.ts +4 -0
  55. package/src/components/core/yAxisTicks.ts +39 -0
  56. package/src/components/data/types.ts +2 -0
  57. package/src/components/interaction/integerZoom.ts +31 -12
  58. package/src/components/interaction/remoteViewport.ts +26 -4
  59. package/src/components/interaction/stepZoom.ts +33 -0
  60. package/src/components/interaction/useWaveformViewport.ts +3 -26
  61. package/src/components/interaction/useWaveformZoom.ts +50 -157
  62. package/src/components/interaction/zoomConstraints.ts +5 -5
  63. package/src/components/rendering/WaveformSeriesLayer.vue +1 -2
  64. package/src/components/rendering/WaveformTrack.vue +1 -6
  65. package/src/core/data.ts +5 -2
  66. package/src/data/simulatedWaveforms.ts +0 -10
  67. package/src/demo/DemoChartHost.vue +49 -4
  68. package/src/demo/remoteWaveformLoader.ts +114 -0
  69. package/src/demo/types.ts +11 -1
  70. package/src/demo/useRemoteWaveformWindow.ts +229 -0
  71. package/src/index.ts +2 -0
  72. package/src/types/chart.ts +10 -1
  73. package/src/types/index.ts +2 -0
  74. package/src/utils/formatters.ts +10 -5
  75. package/src/utils/xAxisNumber.ts +36 -0
  76. package/src/views/AnimationDemo.vue +14 -5
  77. package/dist/types/data/sx2Channel.d.ts +0 -10
  78. package/src/data/sx2Channel.ts +0 -315
@@ -13,7 +13,12 @@ import { chartExportOptions } from '../controls/chartExportOptions'
13
13
  import { layoutAnnotations } from '../annotation/annotationLayout'
14
14
  import { useWaveformInstanceId } from '@/utils/waveformId'
15
15
 
16
- import { animationLayoutSignature, wrapAnimationCommands, type AnimationContext } from './context'
16
+ import {
17
+ animationGridShape,
18
+ animationLayoutSignature,
19
+ wrapAnimationCommands,
20
+ type AnimationContext,
21
+ } from './context'
17
22
  import { createPaginationPresentation } from './paginationPresentation'
18
23
 
19
24
  export function useWaveformAnimations(context: AnimationContext) {
@@ -26,6 +31,8 @@ export function useWaveformAnimations(context: AnimationContext) {
26
31
  const seen = new Set<string>()
27
32
  const prefix = useWaveformInstanceId('waveform-animation')
28
33
  let clipSequence = 0
34
+ let presentationGeneration = 0
35
+ let previousGrid = animationGridShape(context)
29
36
  let previousAnnotations = props.annotations
30
37
  let previousData = props.data
31
38
  let previousLayout = ''
@@ -63,9 +70,12 @@ export function useWaveformAnimations(context: AnimationContext) {
63
70
  states.has(track.id) ? projectTrack(states.get(track.id)!) : track,
64
71
  )
65
72
  })
66
- const pagination = createPaginationPresentation(context, controller, config, effective, () =>
67
- commit(true),
68
- )
73
+ const pagination = createPaginationPresentation(context, controller, config, effective, () => {
74
+ commit(true)
75
+ for (const track of tracks.value) for (const series of track.seriesList) seen.delete(series.id)
76
+ skipSignature = undefined
77
+ annotations.clear()
78
+ })
69
79
  const projectedAnnotations = computed(() => {
70
80
  const anchors = new Map([...states.values()].flatMap((state) => [...state.anchors]))
71
81
  const values = visibleAnnotations().flatMap((annotation) => {
@@ -100,12 +110,10 @@ export function useWaveformAnimations(context: AnimationContext) {
100
110
  [...state.series].map(([id, series]) => [id, series.opacity] as const),
101
111
  ),
102
112
  )
103
- return annotations.project(projectedAnnotations.value, opacities).map((annotation) => ({
104
- ...annotation,
105
- seriesOpacity: (annotation.seriesOpacity ?? 1) * pagination.opacity.value,
106
- }))
113
+ return annotations.project(projectedAnnotations.value, opacities)
107
114
  })
108
115
  function commit(preservePagination = false) {
116
+ presentationGeneration++
109
117
  if (!preservePagination) pagination.reset()
110
118
  const previous = new Map(states)
111
119
  states.clear()
@@ -130,6 +138,8 @@ export function useWaveformAnimations(context: AnimationContext) {
130
138
  if (disposed) return
131
139
  const nextConfig = config.value
132
140
  const dataChanged = props.data !== previousData
141
+ // The post-flush grid watcher waits for lifecycle page resets before requesting replay.
142
+ if (!dataChanged && seen.size && animationGridShape(context) !== previousGrid) return
133
143
  if (dataChanged) {
134
144
  pendingFrom = new Map(states)
135
145
  previousData = props.data
@@ -162,8 +172,9 @@ export function useWaveformAnimations(context: AnimationContext) {
162
172
  previousTracks = tracks.value
163
173
  previousSamplingState = sampling.state.value
164
174
  controller.setEnabled(effective.value)
165
- if (pagination.reconcile(dataChanged)) return
166
- if (!effective.value || layoutChanged) {
175
+ const replay = pagination.reconcile(dataChanged)
176
+ if (replay === 'static') return
177
+ if (!effective.value || (layoutChanged && !replay)) {
167
178
  controller.finishAll()
168
179
  commit()
169
180
  return
@@ -224,6 +235,7 @@ export function useWaveformAnimations(context: AnimationContext) {
224
235
  if (
225
236
  !seen.has(id) &&
226
237
  nextConfig.animation.enabled &&
238
+ nextConfig.animation.duration > 0 &&
227
239
  skipSignature !== sampling.requestSignature.value
228
240
  ) {
229
241
  const state = states.get(track.id)!
@@ -255,8 +267,10 @@ export function useWaveformAnimations(context: AnimationContext) {
255
267
  invalidate()
256
268
  },
257
269
  }
270
+ const generation = presentationGeneration
258
271
  controller.animateSeriesEnter(id, binding, track.width, {
259
272
  ...nextConfig.animation,
273
+ duration: pagination.enterDuration(),
260
274
  delay:
261
275
  nextConfig.animation.delay + visibleIds.indexOf(id) * nextConfig.animation.staggerDelay,
262
276
  onComplete: () => {
@@ -264,7 +278,7 @@ export function useWaveformAnimations(context: AnimationContext) {
264
278
  entering.delete(id)
265
279
  invalidate()
266
280
  queueMicrotask(() => {
267
- if (!disposed) annotations.release(id)
281
+ if (!disposed && presentationGeneration === generation) annotations.release(id)
268
282
  })
269
283
  nextConfig.animation.onComplete?.()
270
284
  },
@@ -311,6 +325,17 @@ export function useWaveformAnimations(context: AnimationContext) {
311
325
  reconcile,
312
326
  { immediate: true },
313
327
  )
328
+ watch(
329
+ [() => animationGridShape(context), () => props.data],
330
+ ([shape, data], [previousShape, previousData]) => {
331
+ previousGrid = shape
332
+ if (disposed || shape === previousShape || data !== previousData || !seen.size) return
333
+ // Lifecycle page resets have now selected the final grid and page.
334
+ pagination.requestLayout()
335
+ reconcile()
336
+ },
337
+ { flush: 'post' },
338
+ )
314
339
  watch(
315
340
  () => props.annotationsVisible,
316
341
  (visible) => {
@@ -343,7 +368,6 @@ export function useWaveformAnimations(context: AnimationContext) {
343
368
  controller.finishWhere(
344
369
  (key) => key.scope === 'update' || key.scope === 'domain',
345
370
  () => {
346
- pagination.reset()
347
371
  for (const track of tracks.value) {
348
372
  const old = states.get(track.id),
349
373
  next = staticTrack(track, visibleAnnotations(), old, true)
@@ -405,7 +429,6 @@ export function useWaveformAnimations(context: AnimationContext) {
405
429
  return {
406
430
  presentationTracks,
407
431
  presentationAnnotations,
408
- paginationOpacity: pagination.opacity,
409
432
  stabilizeAnimations,
410
433
  wrap: <T extends object>(commands: T) => wrapAnimationCommands(commands, stabilizeAnimations),
411
434
  wrapPagination: pagination.wrapPagination,
@@ -16,6 +16,7 @@ import { paddedDomain } from '../../utils'
16
16
  import { transformForDomain } from '../interaction/zoomConstraints'
17
17
  import { constrainViewportZoom } from '../interaction/remoteViewport'
18
18
  import { normalizeIntegerZoomDomain } from '../interaction/integerZoom'
19
+ import { resolveStepZoomDomain, sameZoomDomain } from '../interaction/stepZoom'
19
20
  import type { useControlMode } from './useControlMode'
20
21
 
21
22
  type Domain = [number, number]
@@ -34,7 +35,7 @@ interface Context {
34
35
  legacyReset: (index?: number) => void
35
36
  legacySet: (domain: Domain, index?: number) => void
36
37
  }
37
- const same = (a: Domain, b: Domain) => a.every((v, i) => Math.abs(v - b[i]!) < 1e-12)
38
+ const same = sameZoomDomain
38
39
  export function useChartCommands(c: Context) {
39
40
  let sequence = 0
40
41
  const independent = computed(() => c.props.displayMode === 'independent')
@@ -104,18 +105,16 @@ export function useChartCommands(c: Context) {
104
105
  paddedDomain(tracks.flatMap((t) => t.seriesList.flatMap((s) => s.xDomain))),
105
106
  c.props.xDomainStrategy,
106
107
  )
107
- let requested: Domain = domain ?? bounds
108
- if (before && (action === 'zoom-in' || action === 'zoom-out')) {
109
- const [a, b] = before.xDomain
110
- const half = (b - a) * (action === 'zoom-in' ? 0.25 : 1)
111
- requested = [(a + b) / 2 - half, (a + b) / 2 + half]
112
- }
113
- const xDomain = constrainViewportZoom(
114
- normalizeIntegerZoomDomain(requested, c.props),
115
- bounds,
116
- tracks.filter((t) => t.hasVisibleSeries).map((t) => t.seriesList),
117
- c.props,
118
- )
108
+ const seriesGroups = tracks.filter((t) => t.hasVisibleSeries).map((t) => t.seriesList)
109
+ const xDomain =
110
+ before && (action === 'zoom-in' || action === 'zoom-out')
111
+ ? resolveStepZoomDomain(before.xDomain, action, bounds, seriesGroups, c.props)
112
+ : constrainViewportZoom(
113
+ normalizeIntegerZoomDomain(domain ?? bounds, c.props),
114
+ bounds,
115
+ seriesGroups,
116
+ c.props,
117
+ )
119
118
  const resetsY = action === 'reset' || action === 'fit'
120
119
  const hasY =
121
120
  resetsY &&
@@ -0,0 +1,44 @@
1
+ import type { WaveformAmplitudeLimit, WaveformAmplitudeLimits } from '../../types'
2
+
3
+ export function normalizeAmplitudeLimit(
4
+ limit?: WaveformAmplitudeLimit,
5
+ ): WaveformAmplitudeLimit | undefined {
6
+ if (!limit) return undefined
7
+ const lower = Number.isFinite(limit.lower) ? limit.lower : undefined
8
+ const upper = Number.isFinite(limit.upper) ? limit.upper : undefined
9
+ if (lower === undefined && upper === undefined) return undefined
10
+ if (lower !== undefined && upper !== undefined && lower >= upper) return undefined
11
+ return { lower, upper }
12
+ }
13
+
14
+ export function mergeAmplitudeLimits(
15
+ series: readonly { id: string }[],
16
+ limits?: WaveformAmplitudeLimits,
17
+ ): WaveformAmplitudeLimit | undefined {
18
+ const valid = series.flatMap((item) => {
19
+ const limit = normalizeAmplitudeLimit(limits?.[item.id])
20
+ return limit ? [limit] : []
21
+ })
22
+ const lowers = valid.flatMap((limit) => (limit.lower === undefined ? [] : [limit.lower]))
23
+ const uppers = valid.flatMap((limit) => (limit.upper === undefined ? [] : [limit.upper]))
24
+ return normalizeAmplitudeLimit({
25
+ lower: lowers.length ? Math.min(...lowers) : undefined,
26
+ upper: uppers.length ? Math.max(...uppers) : undefined,
27
+ })
28
+ }
29
+
30
+ export function applyAmplitudeLimits(
31
+ padded: [number, number],
32
+ limit: WaveformAmplitudeLimit,
33
+ automatic: [number, number] = padded,
34
+ ): [number, number] {
35
+ let lower = limit.lower ?? padded[0]
36
+ let upper = limit.upper ?? padded[1]
37
+ if (lower >= upper) {
38
+ const fixed = limit.lower ?? limit.upper!
39
+ const span = Math.max(automatic[1] - automatic[0], Math.abs(fixed) * 0.1, 1)
40
+ if (limit.lower !== undefined) upper = lower + span
41
+ else lower = upper - span
42
+ }
43
+ return [lower, upper]
44
+ }
@@ -1,4 +1,8 @@
1
- import type { WaveformOverlayMode } from '../../types'
1
+ import type {
2
+ WaveformAmplitudeLimit,
3
+ WaveformAmplitudeLimits,
4
+ WaveformOverlayMode,
5
+ } from '../../types'
2
6
  import { formatScientificAxisLabel, formatScientificAxisExponent, paddedDomain } from '../../utils'
3
7
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
4
8
  import { MAX_MULTI_Y_AXIS_COUNT } from './constants'
@@ -8,7 +12,9 @@ import {
8
12
  resolveTrackFixedYDomain,
9
13
  type WaveformYDomain,
10
14
  } from './yDomain'
11
- import { resolveYAxisTicksECharts } from './niceScale'
15
+ import { resolveYAxisTicks } from './yAxisTicks'
16
+ export { resolveYAxisTicks, type ResolvedYAxisTicks } from './yAxisTicks'
17
+ import { applyAmplitudeLimits, mergeAmplitudeLimits } from './amplitudeLimits'
12
18
  import { applyYAxisPadding, withMaximumTick, type YAxisPaddingOptions } from './yAxisPadding'
13
19
  import { resolveYAxisLabelBandWidth } from './yAxisLabelStyle'
14
20
 
@@ -28,36 +34,6 @@ export function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number)
28
34
  return 5
29
35
  }
30
36
 
31
- export interface ResolvedYAxisTicks {
32
- domain: [number, number]
33
- values: number[]
34
- }
35
-
36
- export function resolveYAxisTicks(
37
- domain: [number, number],
38
- tickCount = 5,
39
- nice = true,
40
- ): ResolvedYAxisTicks {
41
- if (!nice) {
42
- // 如果不需要 nice,使用简单的线性分割
43
- const effectiveTickCount =
44
- typeof tickCount === 'number' && Number.isFinite(tickCount)
45
- ? Math.max(2, Math.floor(tickCount))
46
- : 5
47
- const [axisStart, axisEnd] = domain
48
- return {
49
- domain,
50
- values: Array.from(
51
- { length: effectiveTickCount },
52
- (_, index) => axisStart + ((axisEnd - axisStart) * index) / (effectiveTickCount - 1),
53
- ),
54
- }
55
- }
56
-
57
- // 使用 ECharts 兼容的 1/2/5 系列算法
58
- return resolveYAxisTicksECharts(domain, tickCount)
59
- }
60
-
61
37
  export function formatYAxisTickLabel(
62
38
  value: number,
63
39
  domain: [number, number],
@@ -101,6 +77,7 @@ export interface YAxisSeriesGroup {
101
77
  seriesList: DisplaySeries[]
102
78
  domain: [number, number]
103
79
  fixed: boolean
80
+ amplitudeLimit?: WaveformAmplitudeLimit
104
81
  maximumTick?: number
105
82
  minimumTick?: number
106
83
  }
@@ -191,6 +168,7 @@ export function resolveRenderedYAxisSeriesGroups(
191
168
  yDomains?: Record<string, WaveformYDomain>,
192
169
  viewportYDomains?: Record<string, [number, number]>,
193
170
  padding?: YAxisPaddingOptions | number,
171
+ amplitudeLimits?: WaveformAmplitudeLimits,
194
172
  ): YAxisSeriesGroup[] {
195
173
  const viewportYDomain = viewportYDomains?.[track.id]
196
174
  const axisTrack =
@@ -198,7 +176,13 @@ export function resolveRenderedYAxisSeriesGroups(
198
176
  ? track
199
177
  : { ...track, visibleSeries: track.series }
200
178
  return resolveYAxisSeriesGroups(axisTrack, overlayMode, yDomain, yDomains).map((group) => {
201
- const padded = applyYAxisPadding(group, viewportYDomain, padding)
179
+ const limit = mergeAmplitudeLimits(group.seriesList, amplitudeLimits)
180
+ const automatic = limit ? buildYAxisSeriesGroups(axisTrack, overlayMode)[group.index]! : group
181
+ const padded = applyYAxisPadding(automatic, limit ? undefined : viewportYDomain, padding)
182
+ if (limit) {
183
+ padded.domain = applyAmplitudeLimits(padded.domain, limit, automatic.domain)
184
+ padded.amplitudeLimit = limit
185
+ }
202
186
  return track.visibleSeries.length
203
187
  ? padded
204
188
  : { ...padded, maximumTick: undefined, minimumTick: undefined }
@@ -215,10 +199,11 @@ export function axisTextMetrics(
215
199
  measureTextWidth?: (text: string) => number,
216
200
  maximumTick?: number,
217
201
  minimumTick?: number,
202
+ amplitudeLimit?: WaveformAmplitudeLimit,
218
203
  ): { tickTextWidth: number; titleTextWidth: number } {
219
204
  const resolvedTicks = tickValues
220
205
  ? { domain, values: tickValues }
221
- : resolveYAxisTicks(domain, tickCount, nice)
206
+ : resolveYAxisTicks(domain, tickCount, nice, amplitudeLimit)
222
207
  const inRange = (value?: number) =>
223
208
  value !== undefined && value >= resolvedTicks.domain[0] && value <= resolvedTicks.domain[1]
224
209
  ? value
@@ -320,6 +305,7 @@ export function buildYAxisSlots(
320
305
  padding?: YAxisPaddingOptions | number,
321
306
  showUnits = true,
322
307
  measureTextWidth?: (text: string) => number,
308
+ amplitudeLimits?: WaveformAmplitudeLimits,
323
309
  ): { slots: YAxisSlot[]; clearance: { left: number; right: number } } {
324
310
  const labelBandWidth = resolveYAxisLabelBandWidth(
325
311
  typeof padding === 'object' ? padding.labelTextStyle : undefined,
@@ -335,6 +321,7 @@ export function buildYAxisSlots(
335
321
  yDomains,
336
322
  viewportYDomains,
337
323
  padding,
324
+ amplitudeLimits,
338
325
  ).forEach((group) => {
339
326
  const sideIndex = sideIndexes[group.side]++
340
327
  const key = `${group.side}:${sideIndex}`
@@ -348,6 +335,7 @@ export function buildYAxisSlots(
348
335
  measureTextWidth,
349
336
  group.maximumTick,
350
337
  group.minimumTick,
338
+ group.amplitudeLimit,
351
339
  )
352
340
  widths.set(key, Math.max(widths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, tickTextWidth))
353
341
  titleWidths.set(key, Math.max(titleWidths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, titleTextWidth))
@@ -6,6 +6,7 @@ import { scaleLinear, zoomIdentity, type ZoomTransform } from 'd3'
6
6
 
7
7
  import { type ResolvedWaveformRenderingOptions } from '../../core/rendering'
8
8
  import type {
9
+ WaveformAmplitudeLimits,
9
10
  WaveformDisplayMode,
10
11
  WaveformOverlayMode,
11
12
  WaveformPoint,
@@ -34,7 +35,6 @@ import {
34
35
  alignIntegerZoomDomain,
35
36
  normalizeIntegerZoomDomain,
36
37
  integerZoomTicks,
37
- formatIntegerZoomLabel,
38
38
  } from '../interaction/integerZoom'
39
39
  import {
40
40
  Y_AXIS_LABEL_GAP,
@@ -59,6 +59,7 @@ export interface BuildTrackLayoutsOptions {
59
59
  initialXDomains?: Record<string, [number, number]>
60
60
  xDomainStrategy?: WaveformXDomainStrategy
61
61
  integerZoom?: boolean
62
+ amplitudeLimits?: WaveformAmplitudeLimits
62
63
  fixedYDomain?: [number, number]
63
64
  fixedYDomains?: Record<string, [number, number]>
64
65
  yDomains?: Record<string, [number, number]>
@@ -79,6 +80,7 @@ export interface BuildTrackLayoutsOptions {
79
80
  useNonEmptyBottomTracks?: boolean
80
81
  compactYAxisLayout?: boolean
81
82
  showAxisUnits?: boolean
83
+ domainTimeUnit?: 's' | 'ms'
82
84
  }
83
85
 
84
86
  function resolveIndependentXDomain(
@@ -96,7 +98,7 @@ function resolveIndependentXDomain(
96
98
  explicitDomain
97
99
  ? applyXDomainStrategy(explicitDomain, strategy, true)
98
100
  : applyXDomainStrategy(track.xDomain, strategy),
99
- options,
101
+ { integerZoom: options.integerZoom, timeUnit: options.domainTimeUnit ?? options.timeUnit },
100
102
  )
101
103
  }
102
104
 
@@ -141,13 +143,19 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
141
143
  options.fixedYDomains,
142
144
  options.yDomains,
143
145
  options.yAxisPadding ?? options.yAxisUpperPaddingRatio,
146
+ options.amplitudeLimits,
144
147
  )
145
148
  const sideIndexes = { left: 0, right: 0 }
146
149
  const sideOffsets = { left: 0, right: 0 }
147
150
  const yAxes: WaveformYAxisLayout[] = yAxisGroups.map((group) => {
148
151
  const sideIndex = sideIndexes[group.side]++
149
152
  const tickCount = resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber)
150
- const resolvedTicks = resolveYAxisTicks(group.domain, tickCount, options.yAxisNice !== false)
153
+ const resolvedTicks = resolveYAxisTicks(
154
+ group.domain,
155
+ tickCount,
156
+ options.yAxisNice !== false,
157
+ group.amplitudeLimit,
158
+ )
151
159
  const scale = scaleLinear(resolvedTicks.domain, [cell.plotHeight, 0])
152
160
  const majorTicks = resolvedTicks.values
153
161
  const maximumTick =
@@ -220,8 +228,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
220
228
  Math.max(2, Math.floor(cell.width / 100)),
221
229
  options,
222
230
  )
223
- const xAxisLabelFormatter =
224
- options.xAxisLabelFormatter ?? (options.integerZoom ? formatIntegerZoomLabel : undefined)
231
+ const xAxisLabelFormatter = options.xAxisLabelFormatter
225
232
  const yMajorTicks =
226
233
  yAxes[0]?.majorTicks ??
227
234
  (isEmpty
@@ -32,6 +32,7 @@ import { resolveCompactPaginationBand } from './pagination'
32
32
  import type { WaveformHoverState } from './types'
33
33
  import { useWaveformChartLifecycle } from './useWaveformChartLifecycle'
34
34
  import { usePreparedWaveformSeries } from './useWaveformData'
35
+ import { remotePanBoundary } from '../interaction/remoteViewport'
35
36
  import { useWaveformLayout } from './useWaveformLayout'
36
37
  import { useWaveformPresentation } from './useWaveformPresentation'
37
38
  import { usePlotLabelLayout } from './usePlotLabelLayout'
@@ -82,7 +83,7 @@ export function useWaveformChartController(
82
83
  const hoverThrottle = useAnimationFrameThrottle()
83
84
 
84
85
  watch(
85
- [() => props.yDomain, () => props.yDomains],
86
+ [() => props.yDomain, () => props.yDomains, () => props.amplitudeLimits],
86
87
  () => {
87
88
  sharedYDomains.value = {}
88
89
  independentYDomains.value = {}
@@ -102,6 +103,7 @@ export function useWaveformChartController(
102
103
  handleBeforeDataReferenceChange()
103
104
  },
104
105
  () => handleDataReferenceChange(),
106
+ () => Boolean(remotePanBoundary(props)),
105
107
  )
106
108
 
107
109
  const presentation = useWaveformPresentation({
@@ -263,8 +265,6 @@ export function useWaveformChartController(
263
265
  isPresentationMode,
264
266
  editorSeriesOptions,
265
267
  resolveInitialTrackDomain,
266
- canZoomTrack: zoom.canZoomTrack,
267
- canZoomSharedTracks: zoom.canZoomSharedTracks,
268
268
  configureZoom: zoom.configureZoom,
269
269
  cancelPendingZoom: zoom.cancelPendingZoom,
270
270
  clearHover: () => hover.clearHover(),
@@ -422,7 +422,6 @@ export function useWaveformChartController(
422
422
  ...animations.wrap(commands),
423
423
  presentationTracks: animations.presentationTracks,
424
424
  presentationAnnotations: animations.presentationAnnotations,
425
- paginationOpacity: animations.paginationOpacity,
426
425
  stabilizeAnimations: animations.stabilizeAnimations,
427
426
  ...imageExport,
428
427
  margin,
@@ -16,6 +16,7 @@ import type { NormalizedWaveformGridOptions } from './grid'
16
16
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
17
17
  import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes'
18
18
  import { isEditableTarget } from '../interaction/interactionTarget'
19
+ import { remotePanBoundary } from '../interaction/remoteViewport'
19
20
 
20
21
  export interface LifecycleContext {
21
22
  preserveFullViewport?: () => boolean
@@ -64,7 +65,6 @@ export interface LifecycleContext {
64
65
  }
65
66
 
66
67
  export function useWaveformChartLifecycle(context: LifecycleContext) {
67
- useWaveformMeasurements(context)
68
68
  const {
69
69
  props,
70
70
  emit,
@@ -135,6 +135,15 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
135
135
  const { handleBeforeDataReferenceChange, handleDataReferenceChange } =
136
136
  useWaveformViewportRestore(context)
137
137
 
138
+ useWaveformMeasurements(context, {
139
+ beforeResize: () => {
140
+ if (remotePanBoundary(props)) handleBeforeDataReferenceChange()
141
+ },
142
+ afterResize: () => {
143
+ if (remotePanBoundary(props)) handleDataReferenceChange()
144
+ },
145
+ })
146
+
138
147
  watch(
139
148
  [
140
149
  innerWidth,
@@ -163,7 +172,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
163
172
  )
164
173
 
165
174
  watch(
166
- () => [props.displayMode, props.integerZoom, props.integerZoom ? props.timeUnit : undefined],
175
+ () => [props.displayMode, props.integerZoom],
167
176
  () => {
168
177
  const previousPage = currentPage.value
169
178
  currentPage.value = 1
@@ -31,8 +31,11 @@ export interface PreparedWaveformSeries {
31
31
  hasErrorPoints: boolean
32
32
  }
33
33
 
34
- export function prepareWaveformSeries(data: WaveformData): PreparedWaveformSeries[] {
35
- return normalizeWaveformSeriesSources(data).map(({ source, ...series }) => {
34
+ export function prepareWaveformSeries(
35
+ data: WaveformData,
36
+ retainEmpty = false,
37
+ ): PreparedWaveformSeries[] {
38
+ return normalizeWaveformSeriesSources(data, retainEmpty).map(({ source, ...series }) => {
36
39
  const sourceMetrics = source.metrics(series.errorBar.visible)
37
40
  return {
38
41
  ...series,
@@ -61,11 +64,14 @@ export function usePreparedWaveformSeries(
61
64
  data: () => WaveformData,
62
65
  onBeforeDataChange: () => void,
63
66
  onDataChange: () => void,
67
+ retainEmpty: () => boolean = () => false,
64
68
  ) {
65
- const preparedSeries = shallowRef<PreparedWaveformSeries[]>(prepareWaveformSeries(data()))
66
- watch(data, (nextData) => {
69
+ const preparedSeries = shallowRef<PreparedWaveformSeries[]>(
70
+ prepareWaveformSeries(data(), retainEmpty()),
71
+ )
72
+ watch([data, retainEmpty], ([nextData, preserveEmpty]) => {
67
73
  onBeforeDataChange()
68
- preparedSeries.value = prepareWaveformSeries(nextData)
74
+ preparedSeries.value = prepareWaveformSeries(nextData, preserveEmpty)
69
75
  onDataChange()
70
76
  })
71
77
  return preparedSeries
@@ -52,6 +52,9 @@ export function useWaveformLayout(context: LayoutContext) {
52
52
  annotationInteraction,
53
53
  linePointOverrides,
54
54
  } = context
55
+ // Display-unit switches must not change the base used by existing D3 transforms.
56
+ const domainTimeUnit = props.timeUnit
57
+ const domainOptions = () => ({ integerZoom: props.integerZoom, timeUnit: domainTimeUnit })
55
58
  const chartSeries = computed<DisplaySeries[]>(() =>
56
59
  preparedSeries.value.map((series, index): DisplaySeries => ({
57
60
  ...series,
@@ -174,12 +177,12 @@ export function useWaveformLayout(context: LayoutContext) {
174
177
  props.xDomainStrategy,
175
178
  true,
176
179
  ),
177
- props,
180
+ domainOptions(),
178
181
  )
179
182
  }
180
183
  return alignIntegerZoomDomain(
181
184
  applyXDomainStrategy(sharedXDomain.value, props.xDomainStrategy),
182
- props,
185
+ domainOptions(),
183
186
  )
184
187
  })
185
188
  const resolveOriginalTrackDomain = (track: TrackLayout): [number, number] => {
@@ -201,7 +204,7 @@ export function useWaveformLayout(context: LayoutContext) {
201
204
  props.xDomainStrategy,
202
205
  true,
203
206
  ),
204
- props,
207
+ domainOptions(),
205
208
  )
206
209
  }
207
210
  return alignIntegerZoomDomain(
@@ -209,7 +212,7 @@ export function useWaveformLayout(context: LayoutContext) {
209
212
  paddedDomain(track.seriesList.flatMap((series) => series.xDomain)),
210
213
  props.xDomainStrategy,
211
214
  ),
212
- props,
215
+ domainOptions(),
213
216
  )
214
217
  }
215
218
  const initialXDomain = computed(
@@ -261,10 +264,12 @@ export function useWaveformLayout(context: LayoutContext) {
261
264
  effectiveXDomains: context.boundaries?.value,
262
265
  xDomainStrategy: props.xDomainStrategy,
263
266
  integerZoom: props.integerZoom,
267
+ amplitudeLimits: props.amplitudeLimits,
264
268
  fixedYDomain: props.yDomain,
265
269
  fixedYDomains: props.yDomains,
266
270
  yDomains: viewportYDomains.value,
267
271
  timeUnit: props.timeUnit,
272
+ domainTimeUnit,
268
273
  xAxisLabelFormatter: props.axes?.x?.labelFormatter,
269
274
  yAxisSplitNumber: props.axes?.y?.splitNumber,
270
275
  yAxisNice: props.axes?.y?.nice,
@@ -15,6 +15,7 @@ export function useWaveformMeasurements(
15
15
  | 'titleAreaReserved'
16
16
  | 'titleMeasureStyle'
17
17
  >,
18
+ resizeHooks: { beforeResize?: () => void; afterResize?: () => void } = {},
18
19
  ) {
19
20
  const {
20
21
  container,
@@ -53,8 +54,10 @@ export function useWaveformMeasurements(
53
54
  onMounted(() => {
54
55
  if (!container.value) return
55
56
  resizeObserver.value = new ResizeObserver(([entry]) => {
57
+ resizeHooks.beforeResize?.()
56
58
  observedWidth.value = Math.max(0, entry?.contentRect.width ?? 0)
57
59
  observedHeight.value = Math.max(0, entry?.contentRect.height ?? 0)
60
+ resizeHooks.afterResize?.()
58
61
  void nextTick(measureTitle)
59
62
  })
60
63
  resizeObserver.value.observe(container.value)
@@ -49,6 +49,7 @@ export function useWaveformYAxisLayout(context: YAxisLayoutContext) {
49
49
  props.axes?.y,
50
50
  showUnits.value,
51
51
  isEdgeCompact.value ? measureYAxisTextWidth : undefined,
52
+ props.amplitudeLimits,
52
53
  ),
53
54
  )
54
55
  const hasYAxisLabels = computed(() =>
@@ -84,6 +85,7 @@ export function useWaveformYAxisLayout(context: YAxisLayoutContext) {
84
85
  props.axes?.y,
85
86
  showUnits.value,
86
87
  isEdgeCompact.value ? measureYAxisTextWidth : undefined,
88
+ props.amplitudeLimits,
87
89
  ),
88
90
  )
89
91
  const resolvedChartLeftMargin = computed(() =>
@@ -1,6 +1,7 @@
1
1
  import type { WaveformAnimationProps } from '../../animation/types'
2
2
  import type { WaveformToolbarOptions, WaveformControlState } from '../../types/controls'
3
3
  import type {
4
+ WaveformAmplitudeLimits,
4
5
  WaveformAnnotation,
5
6
  WaveformAxesOptions,
6
7
  WaveformData,
@@ -43,7 +44,7 @@ export interface WaveformChartProps extends WaveformAnimationProps {
43
44
  lineColor?: string
44
45
  showTooltip?: boolean
45
46
  zoomable?: boolean
46
- /** Snap viewport endpoints outwards to integer X-axis display units. */
47
+ /** Prefer integer display-unit endpoints; preserve any window narrower than one unit. */
47
48
  integerZoom?: boolean
48
49
  pannable?: boolean
49
50
  /** Global record bounds in seconds; enables horizontal panning beyond loaded data. */
@@ -54,6 +55,8 @@ export interface WaveformChartProps extends WaveformAnimationProps {
54
55
  initialXDomain?: [number, number]
55
56
  initialXDomains?: Record<string, [number, number]>
56
57
  xDomainStrategy?: WaveformXDomainStrategy
58
+ /** Strict bounds per normalized series ID; active bounds override Y viewports. */
59
+ amplitudeLimits?: WaveformAmplitudeLimits
57
60
  yDomain?: [number, number]
58
61
  yDomains?: Record<string, [number, number]>
59
62
  timeUnit?: 's' | 'ms'