waveform-analysis 0.1.71 → 0.1.73

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 (122) hide show
  1. package/README.md +260 -40
  2. package/dist/assets/{workerRuntime-o5An0M-W.js → workerRuntime-5p-EI-GL.js} +1 -1
  3. package/dist/index.cjs +2 -1
  4. package/dist/index.js +3874 -2778
  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/annotation/useWaveformChartAnnotations.d.ts +0 -1
  9. package/dist/types/components/controls/WaveformToolbar.vue.d.ts +7 -0
  10. package/dist/types/components/controls/chartExportOptions.d.ts +5 -0
  11. package/dist/types/components/controls/chartSnapshot.d.ts +2 -0
  12. package/dist/types/components/controls/snapshotTitle.d.ts +2 -0
  13. package/dist/types/components/controls/toolbarRegistry.d.ts +21 -0
  14. package/dist/types/components/controls/useChartCommands.d.ts +42 -0
  15. package/dist/types/components/controls/useChartImageExport.d.ts +17 -0
  16. package/dist/types/components/controls/useControlMode.d.ts +8 -0
  17. package/dist/types/components/core/latestTaskScheduler.d.ts +1 -1
  18. package/dist/types/components/core/layout.d.ts +6 -3
  19. package/dist/types/components/core/niceScale.d.ts +34 -0
  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 +5 -0
  23. package/dist/types/components/core/useWaveformChartController.d.ts +24 -6
  24. package/dist/types/components/core/useWaveformChartLifecycle.d.ts +2 -2
  25. package/dist/types/components/core/useWaveformData.d.ts +2 -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/useExtremaTickViewport.d.ts +5 -0
  37. package/dist/types/components/interaction/useWaveformViewport.d.ts +1 -0
  38. package/dist/types/components/rendering/WaveformLegend.vue.d.ts +2 -2
  39. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +2 -3
  40. package/dist/types/components/rendering/WaveformTrackAxes.vue.d.ts +1 -1
  41. package/dist/types/components/rendering/WaveformTrackBackdrop.vue.d.ts +1 -0
  42. package/dist/types/components/rendering/compactFrame.d.ts +5 -0
  43. package/dist/types/components/rendering/yAxisTickVisibility.d.ts +11 -0
  44. package/dist/types/core/lineBoundary.d.ts +4 -0
  45. package/dist/types/core/rendering.d.ts +4 -0
  46. package/dist/types/core/renderingStrategies.d.ts +2 -0
  47. package/dist/types/core/workerSampling/outputBoundaries.d.ts +4 -0
  48. package/dist/types/data/simulatedWaveforms.d.ts +1 -1
  49. package/dist/types/data/sx2Channel.d.ts +10 -0
  50. package/dist/types/demo/DemoYAxisPaddingControls.vue.d.ts +11 -0
  51. package/dist/types/demo/types.d.ts +7 -0
  52. package/dist/types/index.d.ts +2 -1
  53. package/dist/types/types/chart.d.ts +32 -8
  54. package/dist/types/types/controls.d.ts +69 -0
  55. package/dist/types/types/index.d.ts +2 -1
  56. package/dist/types/utils/formatters.d.ts +1 -0
  57. package/package.json +7 -6
  58. package/src/App.vue +61 -14
  59. package/src/components/WaveformChart.css +10 -1
  60. package/src/components/WaveformChart.vue +6 -0
  61. package/src/components/WaveformChartView.vue +27 -26
  62. package/src/components/annotation/WaveformAnnotationLayer.vue +6 -1
  63. package/src/components/annotation/useWaveformChartAnnotations.ts +7 -18
  64. package/src/components/controls/WaveformToolbar.vue +247 -0
  65. package/src/components/controls/chartExportOptions.ts +32 -0
  66. package/src/components/controls/chartSnapshot.ts +132 -0
  67. package/src/components/controls/snapshotTitle.ts +73 -0
  68. package/src/components/controls/toolbarRegistry.ts +92 -0
  69. package/src/components/controls/useChartCommands.ts +294 -0
  70. package/src/components/controls/useChartImageExport.ts +155 -0
  71. package/src/components/controls/useControlMode.ts +30 -0
  72. package/src/components/core/latestTaskScheduler.ts +14 -2
  73. package/src/components/core/layout.ts +49 -36
  74. package/src/components/core/niceScale.ts +151 -0
  75. package/src/components/core/trackLayoutBuilder.ts +41 -17
  76. package/src/components/core/trackPagination.ts +3 -3
  77. package/src/components/core/types.ts +5 -0
  78. package/src/components/core/useWaveformChartController.ts +94 -3
  79. package/src/components/core/useWaveformChartLifecycle.ts +11 -111
  80. package/src/components/core/useWaveformData.ts +4 -0
  81. package/src/components/core/useWaveformLayout.ts +25 -5
  82. package/src/components/core/useWaveformMeasurements.ts +63 -0
  83. package/src/components/core/useWaveformRenderSampling.ts +80 -59
  84. package/src/components/core/useWaveformViewportRestore.ts +105 -0
  85. package/src/components/core/useWaveformYAxisLayout.ts +3 -2
  86. package/src/components/core/waveformChartTypes.ts +5 -0
  87. package/src/components/core/waveformSamplingSession.ts +49 -0
  88. package/src/components/core/workerSamplingClient.ts +38 -6
  89. package/src/components/core/yAxisLabelStyle.ts +10 -0
  90. package/src/components/core/yAxisLayoutMetrics.ts +11 -9
  91. package/src/components/core/yAxisPadding.ts +60 -0
  92. package/src/components/core/yAxisTitleLayout.ts +11 -12
  93. package/src/components/interaction/useExtremaTickViewport.ts +43 -0
  94. package/src/components/interaction/useWaveformViewport.ts +11 -2
  95. package/src/components/rendering/WaveformTrack.css +1 -2
  96. package/src/components/rendering/WaveformTrack.vue +5 -12
  97. package/src/components/rendering/WaveformTrackAxes.vue +66 -19
  98. package/src/components/rendering/WaveformTrackBackdrop.vue +8 -3
  99. package/src/components/rendering/compactFrame.ts +31 -0
  100. package/src/components/rendering/yAxisTickVisibility.ts +156 -0
  101. package/src/core/lineBoundary.ts +24 -0
  102. package/src/core/rendering.ts +48 -25
  103. package/src/core/renderingStrategies.ts +19 -13
  104. package/src/core/waveformPointSource.ts +23 -1
  105. package/src/core/workerSampling/outputBoundaries.ts +32 -0
  106. package/src/core/workerSampling/repository.ts +26 -18
  107. package/src/core/workerSampling/repositoryDataset.ts +33 -1
  108. package/src/core/workerSampling/workerRuntime.ts +13 -3
  109. package/src/data/simulatedWaveforms.ts +67 -2
  110. package/src/data/sx2Channel.ts +315 -0
  111. package/src/demo/DemoChartHost.vue +5 -1
  112. package/src/demo/DemoGridFrameControls.vue +2 -0
  113. package/src/demo/DemoViewControls.vue +5 -2
  114. package/src/demo/DemoYAxisPaddingControls.vue +53 -0
  115. package/src/demo/embedBridge.ts +85 -0
  116. package/src/demo/types.ts +7 -0
  117. package/src/index.ts +19 -0
  118. package/src/types/chart.ts +34 -8
  119. package/src/types/controls.ts +80 -0
  120. package/src/types/index.ts +18 -0
  121. package/src/utils/formatters.ts +2 -1
  122. package/src/views/WasmSamplingDemo.vue +8 -1
@@ -0,0 +1,151 @@
1
+ /**
2
+ * ECharts-compatible Nice Number algorithm for axis tick calculation
3
+ * Based on "Nice Numbers for Graph Labels" from Graphics Gems
4
+ *
5
+ * References:
6
+ * - https://github.com/cenfun/nice-ticks
7
+ * - https://github.com/qianfan-Zhao/nice_number
8
+ * - https://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks
9
+ */
10
+
11
+ /**
12
+ * 将数字转换为"优雅"的数字(1, 2, 5 系列)
13
+ * @param value - 原始值
14
+ * @param round - 是否向上取整
15
+ * @returns 优雅的数字
16
+ */
17
+ function niceNumber(value: number, round: boolean): number {
18
+ if (value === 0) return 0
19
+
20
+ const exponent = Math.floor(Math.log10(Math.abs(value)))
21
+ const fraction = Math.abs(value) / Math.pow(10, exponent)
22
+
23
+ let niceFraction: number
24
+ if (round) {
25
+ // 向上取整到 1, 2, 5, 10
26
+ if (fraction < 1.5) niceFraction = 1
27
+ else if (fraction < 3) niceFraction = 2
28
+ else if (fraction < 7) niceFraction = 5
29
+ else niceFraction = 10
30
+ } else {
31
+ // 向下取整到 1, 2, 5, 10
32
+ if (fraction <= 1) niceFraction = 1
33
+ else if (fraction <= 2) niceFraction = 2
34
+ else if (fraction <= 5) niceFraction = 5
35
+ else niceFraction = 10
36
+ }
37
+
38
+ return niceFraction * Math.pow(10, exponent) * Math.sign(value)
39
+ }
40
+
41
+ export interface NiceScaleResult {
42
+ /** 扩展后的最小值 */
43
+ niceMin: number
44
+ /** 扩展后的最大值 */
45
+ niceMax: number
46
+ /** 刻度间隔 */
47
+ tickSpacing: number
48
+ /** 刻度值数组 */
49
+ ticks: number[]
50
+ }
51
+
52
+ /**
53
+ * 计算 ECharts 风格的优雅刻度
54
+ * @param min - 数据最小值
55
+ * @param max - 数据最大值
56
+ * @param maxTicks - 期望的最大刻度数(类似 ECharts 的 splitNumber,默认 5)
57
+ * @returns 刻度计算结果
58
+ */
59
+ export function calculateNiceScale(min: number, max: number, maxTicks = 5): NiceScaleResult {
60
+ // 处理边界情况
61
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
62
+ return { niceMin: 0, niceMax: 1, tickSpacing: 1, ticks: [0, 1] }
63
+ }
64
+
65
+ if (min === max) {
66
+ if (min === 0) {
67
+ // 特殊处理零点
68
+ return {
69
+ niceMin: -1,
70
+ niceMax: 1,
71
+ tickSpacing: 0.5,
72
+ ticks: [-1, -0.5, 0, 0.5, 1],
73
+ }
74
+ }
75
+ const value = min
76
+ const spacing = Math.abs(value) * 0.2
77
+ return {
78
+ niceMin: value - spacing,
79
+ niceMax: value + spacing,
80
+ tickSpacing: spacing,
81
+ ticks: [value - spacing, value, value + spacing],
82
+ }
83
+ }
84
+
85
+ // 确保 min < max
86
+ if (min > max) {
87
+ ;[min, max] = [max, min]
88
+ }
89
+
90
+ const range = max - min
91
+
92
+ // 计算优雅的刻度间隔
93
+ const roughTickSpacing = range / Math.max(1, maxTicks - 1)
94
+ const tickSpacing = niceNumber(roughTickSpacing, true)
95
+
96
+ // 计算优雅的边界
97
+ const niceMin = Math.floor(min / tickSpacing) * tickSpacing
98
+ const niceMax = Math.ceil(max / tickSpacing) * tickSpacing
99
+
100
+ // 生成刻度值
101
+ const ticks: number[] = []
102
+ const tickCount = Math.round((niceMax - niceMin) / tickSpacing) + 1
103
+
104
+ // 防止刻度过多(安全检查)
105
+ if (tickCount > 100) {
106
+ // 退回到简单的线性刻度
107
+ const safeTickCount = Math.min(maxTicks + 2, 10)
108
+ const safeSpacing = (max - min) / (safeTickCount - 1)
109
+ for (let i = 0; i < safeTickCount; i++) {
110
+ ticks.push(min + safeSpacing * i)
111
+ }
112
+ return {
113
+ niceMin: min,
114
+ niceMax: max,
115
+ tickSpacing: safeSpacing,
116
+ ticks,
117
+ }
118
+ }
119
+
120
+ // 生成刻度值(处理浮点数精度问题)
121
+ for (let i = 0; i < tickCount; i++) {
122
+ const tick = niceMin + tickSpacing * i
123
+ // 修正浮点数误差 - 使用更智能的四舍五入
124
+ // 确定合适的精度(基于刻度间隔的量级)
125
+ const precision = Math.max(0, -Math.floor(Math.log10(Math.abs(tickSpacing))) + 1)
126
+ const roundedTick = Number(tick.toFixed(precision))
127
+ ticks.push(roundedTick)
128
+ }
129
+
130
+ return {
131
+ // Keep floating-point multiplication from shrinking the enclosing data domain.
132
+ niceMin: Math.min(min, ticks[0] ?? niceMin),
133
+ niceMax: Math.max(max, ticks.at(-1) ?? niceMax),
134
+ tickSpacing,
135
+ ticks,
136
+ }
137
+ }
138
+
139
+ /**
140
+ * ECharts 兼容的刻度计算(与 resolveYAxisTicks 接口兼容)
141
+ */
142
+ export function resolveYAxisTicksECharts(
143
+ domain: [number, number],
144
+ tickCount = 5,
145
+ ): { domain: [number, number]; values: number[] } {
146
+ const result = calculateNiceScale(domain[0], domain[1], tickCount)
147
+ return {
148
+ domain: [result.niceMin, result.niceMax],
149
+ values: result.ticks,
150
+ }
151
+ }
@@ -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,19 @@ 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 minimumTick =
167
+ group.minimumTick !== undefined &&
168
+ group.minimumTick >= resolvedTicks.domain[0] &&
169
+ group.minimumTick <= resolvedTicks.domain[1]
170
+ ? group.minimumTick
171
+ : undefined
172
+ const tickValues = withMaximumTick(majorTicks, maximumTick, minimumTick)
159
173
  const { tickTextWidth } = axisTextMetrics(
160
174
  scale.domain() as [number, number],
161
175
  false,
@@ -167,7 +181,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
167
181
  tickTextWidth +
168
182
  Y_AXIS_TICK_PADDING +
169
183
  Y_AXIS_LABEL_GAP +
170
- Y_AXIS_LABEL_BAND_WIDTH +
184
+ resolveYAxisLabelBandWidth(options.yAxisPadding?.labelTextStyle) +
171
185
  Y_AXIS_OUTER_PADDING
172
186
  const slot = options.yAxisSlots?.find(
173
187
  (candidate) => candidate.side === group.side && candidate.sideIndex === sideIndex,
@@ -178,7 +192,10 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
178
192
  ? -sideOffsets.left
179
193
  : cell.width + sideOffsets.right
180
194
  const labelDistance =
181
- tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
195
+ tickTextWidth +
196
+ Y_AXIS_TICK_PADDING +
197
+ Y_AXIS_LABEL_GAP +
198
+ resolveYAxisLabelBandWidth(options.yAxisPadding?.labelTextStyle) / 2
182
199
  const labelX =
183
200
  (slot
184
201
  ? (group.side === 'left' ? 0 : cell.width) + slot.labelOffset
@@ -191,6 +208,8 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
191
208
  x,
192
209
  labelX,
193
210
  scale,
211
+ maximumTick,
212
+ minimumTick,
194
213
  majorTicks,
195
214
  minorTicks: buildMinorTicks(majorTicks, 2),
196
215
  tickValues,
@@ -207,7 +226,11 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
207
226
  )
208
227
  const xAxisLabelFormatter =
209
228
  options.xAxisLabelFormatter ?? (options.integerZoom ? formatIntegerZoomLabel : undefined)
210
- const yMajorTicks = yAxes[0]?.majorTicks ?? []
229
+ const yMajorTicks =
230
+ yAxes[0]?.majorTicks ??
231
+ (isEmpty
232
+ ? fallbackYScale.ticks(resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber))
233
+ : [])
211
234
  const yAxisTickValues = yAxes[0]?.tickValues ?? []
212
235
  const domain = xScale.domain() as [number, number]
213
236
  const endpointLabels = {
@@ -225,8 +248,8 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
225
248
  axis.seriesList.some((series) => series.id === trackSeries.id),
226
249
  )
227
250
  const seriesYScale = yAxis?.scale ?? yScale
228
- const renderPoints = selectSeriesRenderPoints(
229
- trackSeries.points,
251
+ const renderPoints = selectSeriesRenderSourcePoints(
252
+ trackSeries.source ?? pointSourceFromPoints(trackSeries.points),
230
253
  domain,
231
254
  cell.width,
232
255
  options.rendering,
@@ -278,7 +301,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
278
301
  xAxisLabelFormatter,
279
302
  xMinorTicks: buildMinorTicks(xMajorTicks, 3, domain),
280
303
  yMajorTicks,
281
- yMinorTicks: yAxes[0]?.minorTicks ?? [],
304
+ yMinorTicks: yAxes[0]?.minorTicks ?? (isEmpty ? buildMinorTicks(yMajorTicks, 2) : []),
282
305
  yAxisTickValues,
283
306
  xAxisTickValues,
284
307
  endpointLabels,
@@ -297,6 +320,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
297
320
  })
298
321
 
299
322
  alignLeftYAxisTitles(layouts, options.yLabel, {
323
+ labelTextStyle: options.yAxisPadding?.labelTextStyle,
300
324
  showUnits: options.showAxisUnits,
301
325
  compact: options.compactYAxisLayout,
302
326
  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,8 @@ export interface DisplaySeries {
27
27
  points: WaveformPoint[]
28
28
  source?: WaveformPointSource
29
29
  xDomain: [number, number]
30
+ rawYMaximum?: number
31
+ rawYMinimum?: number
30
32
  yDomain: [number, number]
31
33
  hasErrorPoints: boolean
32
34
  }
@@ -38,6 +40,7 @@ export interface DisplayTrack {
38
40
  /** Series currently participating in layout, rendering, and interaction. */
39
41
  visibleSeries: DisplaySeries[]
40
42
  xDomain: [number, number]
43
+ rawYMaximum?: number
41
44
  yDomain: [number, number]
42
45
  }
43
46
 
@@ -51,6 +54,8 @@ export interface TrackSeriesPath {
51
54
  }
52
55
 
53
56
  export interface WaveformYAxisLayout {
57
+ maximumTick?: number
58
+ minimumTick?: number
54
59
  index: number
55
60
  side: 'left' | 'right'
56
61
  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,
@@ -229,6 +242,7 @@ export function useWaveformChartController(
229
242
  })
230
243
 
231
244
  const viewport = useWaveformViewport({
245
+ clearBoundaries: (index) => commands.clearBoundaries(index),
232
246
  props,
233
247
  emit,
234
248
  selection,
@@ -272,7 +286,50 @@ export function useWaveformChartController(
272
286
  resolveTrackAtPointer: annotations.resolveTrackAtPointer,
273
287
  })
274
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
+
275
331
  const lifecycle = useWaveformChartLifecycle({
332
+ preserveFullViewport: () => Object.keys(boundaries.value).length > 0,
276
333
  props,
277
334
  emit,
278
335
  container,
@@ -317,7 +374,39 @@ export function useWaveformChartController(
317
374
  clearZoomBindings: zoom.clearZoomBindings,
318
375
  })
319
376
  handleBeforeDataReferenceChange = lifecycle.handleBeforeDataReferenceChange
320
- 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
+ })
321
410
 
322
411
  return reactive({
323
412
  ...toRefs(props),
@@ -329,6 +418,8 @@ export function useWaveformChartController(
329
418
  ...annotations,
330
419
  ...viewport,
331
420
  ...hover,
421
+ ...commands,
422
+ ...imageExport,
332
423
  margin,
333
424
  getPageSize,
334
425
  currentPage,