waveform-analysis 0.1.70 → 0.1.72

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 (120) hide show
  1. package/README.md +244 -45
  2. package/dist/assets/workerRuntime-5p-EI-GL.js +1 -0
  3. package/dist/index.cjs +2 -1
  4. package/dist/index.js +2924 -1915
  5. package/dist/style.css +1 -1
  6. package/dist/types/components/WaveformChart.vue.d.ts +24 -6
  7. package/dist/types/components/annotation/WaveformAnnotationLayer.vue.d.ts +2 -0
  8. package/dist/types/components/controls/WaveformToolbar.vue.d.ts +7 -0
  9. package/dist/types/components/controls/chartExportOptions.d.ts +5 -0
  10. package/dist/types/components/controls/chartSnapshot.d.ts +2 -0
  11. package/dist/types/components/controls/snapshotTitle.d.ts +2 -0
  12. package/dist/types/components/controls/toolbarRegistry.d.ts +21 -0
  13. package/dist/types/components/controls/useChartCommands.d.ts +42 -0
  14. package/dist/types/components/controls/useChartImageExport.d.ts +17 -0
  15. package/dist/types/components/controls/useControlMode.d.ts +8 -0
  16. package/dist/types/components/core/latestTaskScheduler.d.ts +1 -1
  17. package/dist/types/components/core/layout.d.ts +5 -3
  18. package/dist/types/components/core/niceScale.d.ts +34 -0
  19. package/dist/types/components/core/pagination.d.ts +2 -2
  20. package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -1
  21. package/dist/types/components/core/trackPagination.d.ts +1 -1
  22. package/dist/types/components/core/types.d.ts +3 -0
  23. package/dist/types/components/core/useWaveformChartController.d.ts +24 -5
  24. package/dist/types/components/core/useWaveformChartLifecycle.d.ts +2 -2
  25. package/dist/types/components/core/useWaveformData.d.ts +1 -0
  26. package/dist/types/components/core/useWaveformLayout.d.ts +4 -1
  27. package/dist/types/components/core/useWaveformMeasurements.d.ts +2 -0
  28. package/dist/types/components/core/useWaveformRenderSampling.d.ts +1 -0
  29. package/dist/types/components/core/useWaveformViewportRestore.d.ts +5 -0
  30. package/dist/types/components/core/waveformChartTypes.d.ts +5 -0
  31. package/dist/types/components/core/waveformSamplingSession.d.ts +24 -0
  32. package/dist/types/components/core/yAxisLabelStyle.d.ts +3 -0
  33. package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
  34. package/dist/types/components/core/yAxisPadding.d.ts +7 -0
  35. package/dist/types/components/core/yAxisTitleLayout.d.ts +2 -0
  36. package/dist/types/components/interaction/useWaveformViewport.d.ts +1 -0
  37. package/dist/types/components/rendering/WaveformLegend.vue.d.ts +2 -2
  38. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +2 -3
  39. package/dist/types/components/rendering/WaveformTrackAxes.vue.d.ts +0 -1
  40. package/dist/types/components/rendering/WaveformTrackBackdrop.vue.d.ts +1 -0
  41. package/dist/types/components/rendering/compactFrame.d.ts +5 -0
  42. package/dist/types/components/rendering/yAxisTickVisibility.d.ts +11 -0
  43. package/dist/types/core/lineBoundary.d.ts +4 -0
  44. package/dist/types/core/rendering.d.ts +4 -0
  45. package/dist/types/core/renderingStrategies.d.ts +2 -0
  46. package/dist/types/core/workerSampling/outputBoundaries.d.ts +4 -0
  47. package/dist/types/data/simulatedWaveforms.d.ts +1 -1
  48. package/dist/types/demo/DemoYAxisPaddingControls.vue.d.ts +11 -0
  49. package/dist/types/demo/types.d.ts +6 -0
  50. package/dist/types/index.d.ts +2 -1
  51. package/dist/types/types/chart.d.ts +30 -8
  52. package/dist/types/types/controls.d.ts +69 -0
  53. package/dist/types/types/index.d.ts +2 -1
  54. package/dist/types/utils/formatters.d.ts +1 -0
  55. package/package.json +7 -6
  56. package/src/App.vue +58 -14
  57. package/src/components/WaveformChart.css +10 -1
  58. package/src/components/WaveformChart.vue +6 -0
  59. package/src/components/WaveformChartView.vue +27 -23
  60. package/src/components/annotation/WaveformAnnotationLayer.vue +6 -1
  61. package/src/components/annotation/useWaveformChartAnnotations.ts +6 -1
  62. package/src/components/controls/WaveformToolbar.vue +248 -0
  63. package/src/components/controls/chartExportOptions.ts +32 -0
  64. package/src/components/controls/chartSnapshot.ts +132 -0
  65. package/src/components/controls/snapshotTitle.ts +73 -0
  66. package/src/components/controls/toolbarRegistry.ts +92 -0
  67. package/src/components/controls/useChartCommands.ts +294 -0
  68. package/src/components/controls/useChartImageExport.ts +155 -0
  69. package/src/components/controls/useControlMode.ts +30 -0
  70. package/src/components/core/latestTaskScheduler.ts +14 -2
  71. package/src/components/core/layout.ts +35 -34
  72. package/src/components/core/niceScale.ts +151 -0
  73. package/src/components/core/pagination.ts +3 -16
  74. package/src/components/core/trackLayoutBuilder.ts +34 -17
  75. package/src/components/core/trackPagination.ts +3 -3
  76. package/src/components/core/types.ts +3 -0
  77. package/src/components/core/useWaveformChartController.ts +95 -10
  78. package/src/components/core/useWaveformChartLifecycle.ts +11 -111
  79. package/src/components/core/useWaveformData.ts +2 -0
  80. package/src/components/core/useWaveformLayout.ts +17 -5
  81. package/src/components/core/useWaveformMeasurements.ts +63 -0
  82. package/src/components/core/useWaveformRenderSampling.ts +80 -59
  83. package/src/components/core/useWaveformViewportRestore.ts +105 -0
  84. package/src/components/core/useWaveformYAxisLayout.ts +3 -2
  85. package/src/components/core/waveformChartTypes.ts +5 -0
  86. package/src/components/core/waveformSamplingSession.ts +49 -0
  87. package/src/components/core/workerSamplingClient.ts +38 -6
  88. package/src/components/core/yAxisLabelStyle.ts +10 -0
  89. package/src/components/core/yAxisLayoutMetrics.ts +10 -9
  90. package/src/components/core/yAxisPadding.ts +45 -0
  91. package/src/components/core/yAxisTitleLayout.ts +11 -12
  92. package/src/components/interaction/useWaveformViewport.ts +11 -2
  93. package/src/components/rendering/WaveformTrack.css +1 -2
  94. package/src/components/rendering/WaveformTrack.vue +4 -12
  95. package/src/components/rendering/WaveformTrackAxes.vue +48 -19
  96. package/src/components/rendering/WaveformTrackBackdrop.vue +8 -3
  97. package/src/components/rendering/compactFrame.ts +31 -0
  98. package/src/components/rendering/yAxisTickVisibility.ts +95 -0
  99. package/src/core/lineBoundary.ts +24 -0
  100. package/src/core/rendering.ts +48 -25
  101. package/src/core/renderingStrategies.ts +19 -13
  102. package/src/core/waveformPointSource.ts +23 -1
  103. package/src/core/workerSampling/outputBoundaries.ts +32 -0
  104. package/src/core/workerSampling/repository.ts +26 -18
  105. package/src/core/workerSampling/repositoryDataset.ts +33 -1
  106. package/src/core/workerSampling/workerRuntime.ts +13 -3
  107. package/src/data/simulatedWaveforms.ts +57 -2
  108. package/src/demo/DemoChartHost.vue +5 -1
  109. package/src/demo/DemoGridFrameControls.vue +2 -0
  110. package/src/demo/DemoViewControls.vue +5 -2
  111. package/src/demo/DemoYAxisPaddingControls.vue +46 -0
  112. package/src/demo/embedBridge.ts +85 -0
  113. package/src/demo/types.ts +6 -0
  114. package/src/index.ts +18 -0
  115. package/src/types/chart.ts +32 -8
  116. package/src/types/controls.ts +80 -0
  117. package/src/types/index.ts +18 -0
  118. package/src/utils/formatters.ts +2 -1
  119. package/src/views/WasmSamplingDemo.vue +8 -1
  120. package/dist/assets/workerRuntime-BcYng9TR.js +0 -1
@@ -1,3 +1,6 @@
1
+ import { withMaximumTick, type YAxisPaddingOptions } from './yAxisPadding'
2
+ import { selectSeriesRenderSourcePoints } from '../../core/rendering'
3
+ import { pointSourceFromPoints } from '../../core/waveformPointSource'
1
4
  import {
2
5
  curveStep,
3
6
  curveStepAfter,
@@ -8,10 +11,7 @@ import {
8
11
  type ZoomTransform,
9
12
  } from 'd3'
10
13
 
11
- import {
12
- selectSeriesRenderPoints,
13
- type ResolvedWaveformRenderingOptions,
14
- } from '../../core/rendering'
14
+ import { type ResolvedWaveformRenderingOptions } from '../../core/rendering'
15
15
  import type {
16
16
  WaveformDisplayMode,
17
17
  WaveformOverlayMode,
@@ -36,6 +36,7 @@ import type { DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
36
36
  import { applyXDomainStrategy } from './xDomain'
37
37
  import { alignLeftYAxisTitles } from './yAxisTitleLayout'
38
38
  import { measureYAxisTextWidth } from './yAxisTextWidth'
39
+ import { resolveYAxisLabelBandWidth } from './yAxisLabelStyle'
39
40
  import {
40
41
  alignIntegerZoomDomain,
41
42
  normalizeIntegerZoomDomain,
@@ -43,7 +44,6 @@ import {
43
44
  formatIntegerZoomLabel,
44
45
  } from '../interaction/integerZoom'
45
46
  import {
46
- Y_AXIS_LABEL_BAND_WIDTH,
47
47
  Y_AXIS_LABEL_GAP,
48
48
  Y_AXIS_OUTER_PADDING,
49
49
  Y_AXIS_RIGHT_LABEL_OFFSET,
@@ -61,6 +61,7 @@ export interface BuildTrackLayoutsOptions {
61
61
  overlayMode: WaveformOverlayMode
62
62
  independentTransforms: ZoomTransform[]
63
63
  sharedZoomDomain: [number, number]
64
+ effectiveXDomains?: Record<string, [number, number]>
64
65
  initialXDomain?: [number, number]
65
66
  initialXDomains?: Record<string, [number, number]>
66
67
  xDomainStrategy?: WaveformXDomainStrategy
@@ -72,6 +73,7 @@ export interface BuildTrackLayoutsOptions {
72
73
  xAxisLabelFormatter?: WaveformXAxisLabelFormatter
73
74
  yAxisSplitNumber?: number
74
75
  yAxisNice?: boolean
76
+ yAxisPadding?: YAxisPaddingOptions
75
77
  yAxisUpperPaddingRatio?: number
76
78
  rendering: ResolvedWaveformRenderingOptions
77
79
  /** Latest sampling result for SVG lines only; source series remain complete for interaction. */
@@ -80,7 +82,7 @@ export interface BuildTrackLayoutsOptions {
80
82
  yAxisLabelX: number
81
83
  yLabel?: string
82
84
  yAxisSlots?: readonly YAxisSlot[]
83
- showCompactEmptyTracks: boolean
85
+ showEmptyTracks: boolean
84
86
  useNonEmptyBottomTracks?: boolean
85
87
  compactYAxisLayout?: boolean
86
88
  showAxisUnits?: boolean
@@ -91,6 +93,7 @@ function resolveIndependentXDomain(
91
93
  seriesId: string,
92
94
  options: BuildTrackLayoutsOptions,
93
95
  ): [number, number] {
96
+ if (options.effectiveXDomains?.[track.id]) return options.effectiveXDomains[track.id]
94
97
  const strategy = options.xDomainStrategy ?? { type: 'data' }
95
98
  const explicitDomain =
96
99
  options.initialXDomains?.[track.id] ??
@@ -115,13 +118,12 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
115
118
 
116
119
  const layouts: TrackLayout[] = visibleCells.flatMap((cell, index) => {
117
120
  const isEmpty = !cell.series || cell.series.series.length === 0
118
- if (!cell.series && (options.displayMode !== 'compact' || !options.showCompactEmptyTracks))
119
- return []
121
+ if (!cell.series && !options.showEmptyTracks) return []
120
122
  const displayTrack: DisplayTrack = cell.series ?? {
121
123
  id: `empty-grid-slot-${cell.slotIndex}`,
122
124
  series: [],
123
125
  visibleSeries: [],
124
- xDomain: [0, 1],
126
+ xDomain: options.initialXDomain ?? options.sharedZoomDomain,
125
127
  yDomain: [0, 1],
126
128
  }
127
129
  const hasVisibleSeries = !isEmpty && displayTrack.visibleSeries.length > 0
@@ -145,7 +147,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
145
147
  options.fixedYDomain,
146
148
  options.fixedYDomains,
147
149
  options.yDomains,
148
- options.yAxisUpperPaddingRatio,
150
+ options.yAxisPadding ?? options.yAxisUpperPaddingRatio,
149
151
  )
150
152
  const sideIndexes = { left: 0, right: 0 }
151
153
  const sideOffsets = { left: 0, right: 0 }
@@ -155,7 +157,13 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
155
157
  const resolvedTicks = resolveYAxisTicks(group.domain, tickCount, options.yAxisNice !== false)
156
158
  const scale = scaleLinear(resolvedTicks.domain, [cell.plotHeight, 0])
157
159
  const majorTicks = resolvedTicks.values
158
- const tickValues = majorTicks
160
+ const maximumTick =
161
+ group.maximumTick !== undefined &&
162
+ group.maximumTick >= resolvedTicks.domain[0] &&
163
+ group.maximumTick <= resolvedTicks.domain[1]
164
+ ? group.maximumTick
165
+ : undefined
166
+ const tickValues = withMaximumTick(majorTicks, maximumTick)
159
167
  const { tickTextWidth } = axisTextMetrics(
160
168
  scale.domain() as [number, number],
161
169
  false,
@@ -167,7 +175,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
167
175
  tickTextWidth +
168
176
  Y_AXIS_TICK_PADDING +
169
177
  Y_AXIS_LABEL_GAP +
170
- Y_AXIS_LABEL_BAND_WIDTH +
178
+ resolveYAxisLabelBandWidth(options.yAxisPadding?.labelTextStyle) +
171
179
  Y_AXIS_OUTER_PADDING
172
180
  const slot = options.yAxisSlots?.find(
173
181
  (candidate) => candidate.side === group.side && candidate.sideIndex === sideIndex,
@@ -178,7 +186,10 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
178
186
  ? -sideOffsets.left
179
187
  : cell.width + sideOffsets.right
180
188
  const labelDistance =
181
- tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
189
+ tickTextWidth +
190
+ Y_AXIS_TICK_PADDING +
191
+ Y_AXIS_LABEL_GAP +
192
+ resolveYAxisLabelBandWidth(options.yAxisPadding?.labelTextStyle) / 2
182
193
  const labelX =
183
194
  (slot
184
195
  ? (group.side === 'left' ? 0 : cell.width) + slot.labelOffset
@@ -191,6 +202,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
191
202
  x,
192
203
  labelX,
193
204
  scale,
205
+ maximumTick,
194
206
  majorTicks,
195
207
  minorTicks: buildMinorTicks(majorTicks, 2),
196
208
  tickValues,
@@ -207,7 +219,11 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
207
219
  )
208
220
  const xAxisLabelFormatter =
209
221
  options.xAxisLabelFormatter ?? (options.integerZoom ? formatIntegerZoomLabel : undefined)
210
- const yMajorTicks = yAxes[0]?.majorTicks ?? []
222
+ const yMajorTicks =
223
+ yAxes[0]?.majorTicks ??
224
+ (isEmpty
225
+ ? fallbackYScale.ticks(resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber))
226
+ : [])
211
227
  const yAxisTickValues = yAxes[0]?.tickValues ?? []
212
228
  const domain = xScale.domain() as [number, number]
213
229
  const endpointLabels = {
@@ -225,8 +241,8 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
225
241
  axis.seriesList.some((series) => series.id === trackSeries.id),
226
242
  )
227
243
  const seriesYScale = yAxis?.scale ?? yScale
228
- const renderPoints = selectSeriesRenderPoints(
229
- trackSeries.points,
244
+ const renderPoints = selectSeriesRenderSourcePoints(
245
+ trackSeries.source ?? pointSourceFromPoints(trackSeries.points),
230
246
  domain,
231
247
  cell.width,
232
248
  options.rendering,
@@ -278,7 +294,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
278
294
  xAxisLabelFormatter,
279
295
  xMinorTicks: buildMinorTicks(xMajorTicks, 3, domain),
280
296
  yMajorTicks,
281
- yMinorTicks: yAxes[0]?.minorTicks ?? [],
297
+ yMinorTicks: yAxes[0]?.minorTicks ?? (isEmpty ? buildMinorTicks(yMajorTicks, 2) : []),
282
298
  yAxisTickValues,
283
299
  xAxisTickValues,
284
300
  endpointLabels,
@@ -297,6 +313,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
297
313
  })
298
314
 
299
315
  alignLeftYAxisTitles(layouts, options.yLabel, {
316
+ labelTextStyle: options.yAxisPadding?.labelTextStyle,
300
317
  showUnits: options.showAxisUnits,
301
318
  compact: options.compactYAxisLayout,
302
319
  measureTextWidth: options.compactYAxisLayout ? measureYAxisTextWidth : undefined,
@@ -27,14 +27,14 @@ export const createFrameNumberResolver =
27
27
  getFrameNumbers: () => Record<string, string | number> | undefined,
28
28
  getTracks: () => DisplayTrack[],
29
29
  ) =>
30
- (trackId: string): string | number | undefined => {
30
+ (trackId: string, slotIndex = 0): string | number | undefined => {
31
31
  const tracks = getTracks()
32
32
  const trackIndex = tracks.findIndex((track) => track.id === trackId)
33
33
  return resolveTrackFrameNumber(
34
34
  getFrameNumber(),
35
35
  getFrameNumbers(),
36
- tracks.length,
37
- trackIndex >= 0 ? trackIndex : 0,
36
+ Math.max(tracks.length, slotIndex + 1),
37
+ trackIndex >= 0 ? trackIndex : slotIndex,
38
38
  trackId,
39
39
  )
40
40
  }
@@ -27,6 +27,7 @@ export interface DisplaySeries {
27
27
  points: WaveformPoint[]
28
28
  source?: WaveformPointSource
29
29
  xDomain: [number, number]
30
+ rawYMaximum?: number
30
31
  yDomain: [number, number]
31
32
  hasErrorPoints: boolean
32
33
  }
@@ -38,6 +39,7 @@ export interface DisplayTrack {
38
39
  /** Series currently participating in layout, rendering, and interaction. */
39
40
  visibleSeries: DisplaySeries[]
40
41
  xDomain: [number, number]
42
+ rawYMaximum?: number
41
43
  yDomain: [number, number]
42
44
  }
43
45
 
@@ -51,6 +53,7 @@ export interface TrackSeriesPath {
51
53
  }
52
54
 
53
55
  export interface WaveformYAxisLayout {
56
+ maximumTick?: number
54
57
  index: number
55
58
  side: 'left' | 'right'
56
59
  x: number
@@ -1,7 +1,13 @@
1
+ import { chartExportOptions } from '../controls/chartExportOptions'
2
+ import { useChartImageExport } from '../controls/useChartImageExport'
3
+ import { useControlMode } from '../controls/useControlMode'
4
+ import { useChartCommands } from '../controls/useChartCommands'
1
5
  import { zoomIdentity, type ZoomTransform } from 'd3'
2
6
  import {
3
7
  computed,
4
8
  reactive,
9
+ nextTick,
10
+ onBeforeUnmount,
5
11
  markRaw,
6
12
  ref,
7
13
  shallowReactive,
@@ -44,9 +50,15 @@ function assignElement<T extends Element>(
44
50
  }
45
51
 
46
52
  export function useWaveformChartController(
47
- props: ResolvedWaveformChartProps,
53
+ inputProps: ResolvedWaveformChartProps,
48
54
  emit: WaveformChartEmit,
49
55
  ) {
56
+ const mode = useControlMode(inputProps, emit)
57
+ const props = reactive({
58
+ ...toRefs(inputProps),
59
+ interactionMode: mode.mode,
60
+ }) as ResolvedWaveformChartProps
61
+ const boundaries = shallowRef<Record<string, [number, number]>>({})
50
62
  const container = ref<HTMLDivElement>()
51
63
  const svgElement = ref<SVGSVGElement>()
52
64
  const titleMeasureElement = ref<HTMLSpanElement>()
@@ -137,6 +149,7 @@ export function useWaveformChartController(
137
149
  independentYDomains,
138
150
  annotationInteraction: markRaw(annotationInteraction),
139
151
  linePointOverrides,
152
+ boundaries,
140
153
  })
141
154
  const {
142
155
  chartSeries,
@@ -164,7 +177,7 @@ export function useWaveformChartController(
164
177
  presentation.titleAreaStyle,
165
178
  )
166
179
 
167
- useWaveformRenderSampling({
180
+ const sampling = useWaveformRenderSampling({
168
181
  props,
169
182
  emit,
170
183
  instanceId: clipPathId,
@@ -180,13 +193,7 @@ export function useWaveformChartController(
180
193
  watchEffect(() => {
181
194
  paginationBandHeight.value =
182
195
  props.layoutPreset === 'edge-compact'
183
- ? resolveCompactPaginationBand(
184
- paginationVisible.value,
185
- chartWidth.value,
186
- pageCount.value,
187
- resolvedChartLeftMargin.value + innerWidth.value / 2,
188
- layout.resolvedXLabel.value,
189
- )
196
+ ? resolveCompactPaginationBand(paginationVisible.value)
190
197
  : gridOptions.value.showPagination && pageCount.value > 1 && chartWidth.value <= 520
191
198
  ? 40
192
199
  : 0
@@ -235,6 +242,7 @@ export function useWaveformChartController(
235
242
  })
236
243
 
237
244
  const viewport = useWaveformViewport({
245
+ clearBoundaries: (index) => commands.clearBoundaries(index),
238
246
  props,
239
247
  emit,
240
248
  selection,
@@ -278,7 +286,50 @@ export function useWaveformChartController(
278
286
  resolveTrackAtPointer: annotations.resolveTrackAtPointer,
279
287
  })
280
288
 
289
+ const cancelControls = () => {
290
+ viewport.cancelViewportDrag()
291
+ zoom.cancelPendingZoom()
292
+ hover.clearHover()
293
+ annotations.cancelAnnotation()
294
+ }
295
+ const commands = useChartCommands({
296
+ props,
297
+ emit,
298
+ layout,
299
+ mode,
300
+ boundaries,
301
+ independentTransforms,
302
+ sharedTransform,
303
+ independentYDomains,
304
+ sharedYDomains,
305
+ cancel: cancelControls,
306
+ configure: zoom.configureZoom,
307
+ legacyReset: viewport.resetViewport,
308
+ legacySet: viewport.setViewportDomain,
309
+ })
310
+ watch(
311
+ [mode.mode, () => props.zoomable, () => props.pannable, () => props.annotationsVisible],
312
+ cancelControls,
313
+ { flush: 'sync' },
314
+ )
315
+ watch([currentPage, () => props.displayMode], () => {
316
+ boundaries.value = {}
317
+ cancelControls()
318
+ })
319
+ watch(
320
+ [
321
+ () => props.initialXDomain,
322
+ () => props.initialXDomains,
323
+ hiddenSeriesIdSet,
324
+ () => props.xDomainStrategy,
325
+ ],
326
+ commands.restoreInitialBoundaries,
327
+ { deep: true },
328
+ )
329
+ onBeforeUnmount(cancelControls)
330
+
281
331
  const lifecycle = useWaveformChartLifecycle({
332
+ preserveFullViewport: () => Object.keys(boundaries.value).length > 0,
282
333
  props,
283
334
  emit,
284
335
  container,
@@ -323,7 +374,39 @@ export function useWaveformChartController(
323
374
  clearZoomBindings: zoom.clearZoomBindings,
324
375
  })
325
376
  handleBeforeDataReferenceChange = lifecycle.handleBeforeDataReferenceChange
326
- handleDataReferenceChange = lifecycle.handleDataReferenceChange
377
+ handleDataReferenceChange = () => {
378
+ boundaries.value = {}
379
+ viewport.cancelViewportDrag()
380
+ lifecycle.handleDataReferenceChange()
381
+ void nextTick(zoom.configureZoom)
382
+ }
383
+
384
+ const imageExport = useChartImageExport({
385
+ container: () => container.value,
386
+ svg: () => svgElement.value,
387
+ size: () => ({
388
+ width: chartWidth.value,
389
+ height: chartHeight.value,
390
+ titleHeight: titleAreaHeight.value,
391
+ }),
392
+ version: () => [
393
+ props.data,
394
+ JSON.stringify({
395
+ ...chartExportOptions(props),
396
+ hidden: [...hiddenSeriesIdSet.value],
397
+ width: chartWidth.value,
398
+ height: chartHeight.value,
399
+ titleHeight: titleAreaHeight.value,
400
+ page: currentPage.value,
401
+ shared: sharedTransform.value,
402
+ independent: independentTransforms.value,
403
+ sharedY: sharedYDomains.value,
404
+ independentY: independentYDomains.value,
405
+ boundaries: boundaries.value,
406
+ }),
407
+ ],
408
+ ready: sampling.ready,
409
+ })
327
410
 
328
411
  return reactive({
329
412
  ...toRefs(props),
@@ -335,6 +418,8 @@ export function useWaveformChartController(
335
418
  ...annotations,
336
419
  ...viewport,
337
420
  ...hover,
421
+ ...commands,
422
+ ...imageExport,
338
423
  margin,
339
424
  getPageSize,
340
425
  currentPage,
@@ -1,3 +1,5 @@
1
+ import { useWaveformMeasurements } from './useWaveformMeasurements'
2
+ import { useWaveformViewportRestore } from './useWaveformViewportRestore'
1
3
  import { zoomIdentity, type ZoomTransform } from 'd3'
2
4
  import {
3
5
  nextTick,
@@ -13,12 +15,10 @@ import type { AnnotationSeriesCandidate, useWaveformAnnotationInteraction } from
13
15
  import type { NormalizedWaveformGridOptions } from './grid'
14
16
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
15
17
  import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes'
16
- import { transformForDomain } from '../interaction/zoomConstraints'
17
- import { remotePanBoundary, restoreViewportDomain } from '../interaction/remoteViewport'
18
18
  import { isEditableTarget } from '../interaction/interactionTarget'
19
- import { seriesIdentity } from '../interaction/zoomEventPayload'
20
19
 
21
- interface LifecycleContext {
20
+ export interface LifecycleContext {
21
+ preserveFullViewport?: () => boolean
22
22
  props: ResolvedWaveformChartProps
23
23
  emit: WaveformChartEmit
24
24
  container: Ref<HTMLDivElement | undefined>
@@ -64,19 +64,10 @@ interface LifecycleContext {
64
64
  }
65
65
 
66
66
  export function useWaveformChartLifecycle(context: LifecycleContext) {
67
+ useWaveformMeasurements(context)
67
68
  const {
68
69
  props,
69
70
  emit,
70
- container,
71
- titleMeasureElement,
72
- resizeObserver,
73
- observedWidth,
74
- observedHeight,
75
- measuredTitleWidth,
76
- measuredTitleHeight,
77
- resolvedTitleText,
78
- titleAreaReserved,
79
- titleMeasureStyle,
80
71
  spacePressed,
81
72
  pointerInsideChart,
82
73
  currentPage,
@@ -85,18 +76,13 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
85
76
  gridOptions,
86
77
  chartSeries,
87
78
  chartTracks,
88
- trackLayouts,
89
79
  innerWidth,
90
80
  innerHeight,
91
- sharedZoomDomain,
92
- initialXDomain,
93
- sharedTransform,
94
81
  activeInteractionMode,
95
82
  hiddenSeriesIdSet,
96
83
  internalHiddenSeriesIds,
97
84
  independentTransforms,
98
85
  independentYDomains,
99
- resolveInitialTrackDomain,
100
86
  annotationInteraction,
101
87
  editorSeriesOptions,
102
88
  isPresentationMode,
@@ -110,6 +96,10 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
110
96
  } = context
111
97
 
112
98
  function handleInteractionKeyDown(event: KeyboardEvent) {
99
+ if (event.key === 'Escape') {
100
+ cancelViewportDrag()
101
+ cancelAnnotation()
102
+ }
113
103
  if (
114
104
  isPresentationMode.value ||
115
105
  event.code !== 'Space' ||
@@ -142,68 +132,8 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
142
132
  emit('page-change', nextPage, pageCount.value)
143
133
  }
144
134
 
145
- let pendingSharedXDomain: [number, number] | undefined
146
- let pendingIndependentXDomains: Map<string, [number, number]> | undefined
147
-
148
- function handleBeforeDataReferenceChange() {
149
- if (props.displayMode === 'independent') {
150
- pendingSharedXDomain = undefined
151
- pendingIndependentXDomains = new Map(
152
- trackLayouts.value.flatMap((track) => {
153
- const current = track.xScale.domain() as [number, number]
154
- const boundary = resolveInitialTrackDomain(track)
155
- return remotePanBoundary(props) ||
156
- current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
157
- ? [[seriesIdentity(track.seriesList.map((series) => series.id)), current]]
158
- : []
159
- }),
160
- )
161
- return
162
- }
163
- pendingIndependentXDomains = undefined
164
- const current = sharedZoomDomain.value
165
- const boundary = initialXDomain.value
166
- pendingSharedXDomain =
167
- remotePanBoundary(props) || current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
168
- ? [...current]
169
- : undefined
170
- }
171
-
172
- function handleDataReferenceChange() {
173
- if (props.displayMode === 'independent') {
174
- const currentTransforms = independentTransforms.value
175
- independentTransforms.value = chartTracks.value.map(
176
- (_track, index) => currentTransforms[index] ?? zoomIdentity,
177
- )
178
- }
179
- clearHover()
180
- editorSeriesOptions.value = []
181
- void nextTick(() => {
182
- if (props.displayMode === 'independent' && pendingIndependentXDomains) {
183
- const nextTransforms = chartTracks.value.map(() => zoomIdentity)
184
- trackLayouts.value.forEach((track) => {
185
- const previousDomain = pendingIndependentXDomains?.get(
186
- seriesIdentity(track.seriesList.map((series) => series.id)),
187
- )
188
- if (!previousDomain) return
189
- const boundary = resolveInitialTrackDomain(track)
190
- const domain = restoreViewportDomain(previousDomain, boundary, [track.seriesList], props)
191
- nextTransforms[track.index] = transformForDomain(domain, boundary, track.width)
192
- })
193
- independentTransforms.value = nextTransforms
194
- pendingIndependentXDomains = undefined
195
- } else if (props.displayMode !== 'independent' && pendingSharedXDomain) {
196
- const boundary = initialXDomain.value
197
- const groups = trackLayouts.value
198
- .filter((track) => track.hasVisibleSeries)
199
- .map((track) => track.seriesList)
200
- const domain = restoreViewportDomain(pendingSharedXDomain, boundary, groups, props)
201
- sharedTransform.value = transformForDomain(domain, boundary, innerWidth.value)
202
- pendingSharedXDomain = undefined
203
- }
204
- configureZoom()
205
- })
206
- }
135
+ const { handleBeforeDataReferenceChange, handleDataReferenceChange } =
136
+ useWaveformViewportRestore(context)
207
137
 
208
138
  watch(
209
139
  [
@@ -347,45 +277,15 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
347
277
  { deep: true },
348
278
  )
349
279
 
350
- function measureTitle() {
351
- if (!titleAreaReserved.value || !titleMeasureElement.value) {
352
- measuredTitleWidth.value = 0
353
- measuredTitleHeight.value = 0
354
- return
355
- }
356
- const bounds = titleMeasureElement.value.getBoundingClientRect()
357
- measuredTitleWidth.value = titleMeasureElement.value.scrollWidth || bounds.width
358
- measuredTitleHeight.value = titleMeasureElement.value.scrollHeight || bounds.height
359
- }
360
-
361
- watch(
362
- [resolvedTitleText, titleAreaReserved, titleMeasureStyle],
363
- async () => {
364
- measuredTitleWidth.value = 0
365
- measuredTitleHeight.value = 0
366
- await nextTick()
367
- measureTitle()
368
- },
369
- { immediate: true },
370
- )
371
-
372
280
  onMounted(() => {
373
281
  window.addEventListener('keydown', handleInteractionKeyDown)
374
282
  window.addEventListener('keyup', handleInteractionKeyUp)
375
- if (!container.value) return
376
- resizeObserver.value = new ResizeObserver(([entry]) => {
377
- observedWidth.value = Math.max(0, entry?.contentRect.width ?? 0)
378
- observedHeight.value = Math.max(0, entry?.contentRect.height ?? 0)
379
- void nextTick(measureTitle)
380
- })
381
- resizeObserver.value.observe(container.value)
382
283
  })
383
284
 
384
285
  onBeforeUnmount(() => {
385
286
  window.removeEventListener('keydown', handleInteractionKeyDown)
386
287
  window.removeEventListener('keyup', handleInteractionKeyUp)
387
288
  cancelPendingHover()
388
- resizeObserver.value?.disconnect()
389
289
  clearZoomBindings()
390
290
  editorSeriesOptions.value = []
391
291
  })
@@ -25,6 +25,7 @@ export interface PreparedWaveformSeries {
25
25
  points: WaveformPoint[]
26
26
  source: WaveformPointSource
27
27
  xDomain: [number, number]
28
+ rawYMaximum?: number
28
29
  yDomain: [number, number]
29
30
  hasErrorPoints: boolean
30
31
  }
@@ -48,6 +49,7 @@ export function prepareWaveformSeries(data: WaveformData): PreparedWaveformSerie
48
49
  ? [sourceMetrics.yMinimum, sourceMetrics.yMaximum]
49
50
  : [],
50
51
  ),
52
+ rawYMaximum: Number.isFinite(sourceMetrics.yMaximum) ? sourceMetrics.yMaximum : undefined,
51
53
  hasErrorPoints: sourceMetrics.hasErrorPoints,
52
54
  }
53
55
  })
@@ -20,6 +20,7 @@ import { resolveViewportYDomains } from './yAxisLayoutMetrics'
20
20
  import { useWaveformYAxisLayout } from './useWaveformYAxisLayout'
21
21
  import type { useWaveformAnnotationInteraction } from '../annotation'
22
22
  interface LayoutContext {
23
+ boundaries?: ShallowRef<Record<string, [number, number]>>
23
24
  props: ResolvedWaveformChartProps
24
25
  preparedSeries: ShallowRef<PreparedWaveformSeries[]>
25
26
  currentPage: Ref<number>
@@ -147,7 +148,7 @@ export function useWaveformLayout(context: LayoutContext) {
147
148
  const hasChartArea = computed(() => innerWidth.value > 0 && innerHeight.value > 0)
148
149
  const resolvedXLabel = computed(() => props.xLabel ?? `时间(${props.timeUnit})`)
149
150
  const activeInteractionMode = computed(() => props.interactionMode)
150
- const isZoomMode = computed(() => activeInteractionMode.value !== 'annotation')
151
+ const isZoomMode = computed(() => activeInteractionMode.value === 'zoom')
151
152
  const sharedXDomain = computed(() => {
152
153
  const values: number[] = []
153
154
  const tracks = chartTracks.value.some((track) => track.visibleSeries.length)
@@ -158,7 +159,7 @@ export function useWaveformLayout(context: LayoutContext) {
158
159
  })
159
160
  return paddedDomain(values)
160
161
  })
161
- const initialXDomain = computed<[number, number]>(() => {
162
+ const originalXDomain = computed<[number, number]>(() => {
162
163
  const domain = props.initialXDomain
163
164
  if (
164
165
  domain &&
@@ -180,7 +181,7 @@ export function useWaveformLayout(context: LayoutContext) {
180
181
  props,
181
182
  )
182
183
  })
183
- const resolveInitialTrackDomain = (track: TrackLayout): [number, number] => {
184
+ const resolveOriginalTrackDomain = (track: TrackLayout): [number, number] => {
184
185
  const configuredDomain =
185
186
  props.initialXDomains?.[track.series?.trackId ?? track.series?.id ?? track.id] ??
186
187
  (track.series ? props.initialXDomains?.[track.series.id] : undefined) ??
@@ -210,6 +211,13 @@ export function useWaveformLayout(context: LayoutContext) {
210
211
  props,
211
212
  )
212
213
  }
214
+ const initialXDomain = computed(
215
+ () =>
216
+ (props.displayMode !== 'independent' ? context.boundaries?.value.shared : undefined) ??
217
+ originalXDomain.value,
218
+ )
219
+ const resolveInitialTrackDomain = (track: TrackLayout): [number, number] =>
220
+ context.boundaries?.value[track.id] ?? resolveOriginalTrackDomain(track)
213
221
  const sharedZoomDomain = computed(() =>
214
222
  normalizeIntegerZoomDomain(
215
223
  sharedTransform.value
@@ -240,6 +248,7 @@ export function useWaveformLayout(context: LayoutContext) {
240
248
  useNonEmptyBottomTracks: props.layoutPreset === 'edge-compact',
241
249
  compactYAxisLayout: props.layoutPreset === 'edge-compact',
242
250
  showAxisUnits:
251
+ props.unitDisplayMode !== 'channel-label' &&
243
252
  props.unitDisplayMode !== 'channel-label-or-legend' &&
244
253
  (props.layoutPreset !== 'edge-compact' || props.unitDisplayMode === 'axis'),
245
254
  displayMode: props.displayMode,
@@ -248,6 +257,7 @@ export function useWaveformLayout(context: LayoutContext) {
248
257
  sharedZoomDomain: sharedZoomDomain.value,
249
258
  initialXDomain: props.initialXDomain ? initialXDomain.value : undefined,
250
259
  initialXDomains: props.initialXDomains,
260
+ effectiveXDomains: context.boundaries?.value,
251
261
  xDomainStrategy: props.xDomainStrategy,
252
262
  integerZoom: props.integerZoom,
253
263
  fixedYDomain: props.yDomain,
@@ -257,7 +267,7 @@ export function useWaveformLayout(context: LayoutContext) {
257
267
  xAxisLabelFormatter: props.axes?.x?.labelFormatter,
258
268
  yAxisSplitNumber: props.axes?.y?.splitNumber,
259
269
  yAxisNice: props.axes?.y?.nice,
260
- yAxisUpperPaddingRatio: props.axes?.y?.upperPaddingRatio,
270
+ yAxisPadding: props.axes?.y,
261
271
  rendering: renderingOptions.value,
262
272
  linePointOverrides: linePointOverrides?.value,
263
273
  hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
@@ -266,7 +276,7 @@ export function useWaveformLayout(context: LayoutContext) {
266
276
  yAxisSlots.value.slots.find((slot) => slot.side === 'left' && slot.sideIndex === 0)
267
277
  ?.labelOffset ?? yAxisMetrics.value.labelCenterX,
268
278
  yAxisSlots: yAxisSlots.value.slots,
269
- showCompactEmptyTracks: props.displayMode === 'compact' && hasWaveformData.value,
279
+ showEmptyTracks: hasWaveformData.value,
270
280
  }),
271
281
  )
272
282
  const annotationLayoutsForTrack = (track: TrackLayout): AnnotationTrackLayout[] =>
@@ -329,6 +339,8 @@ export function useWaveformLayout(context: LayoutContext) {
329
339
  activeInteractionMode,
330
340
  isZoomMode,
331
341
  initialXDomain,
342
+ originalXDomain,
343
+ resolveOriginalTrackDomain,
332
344
  sharedZoomDomain,
333
345
  resolveInitialTrackDomain,
334
346
  gridCells,