waveform-analysis 0.1.66 → 0.1.68

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 (60) hide show
  1. package/README.md +124 -3
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +1059 -939
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/WaveformChart.vue.d.ts +4 -0
  6. package/dist/types/components/core/grid.d.ts +1 -1
  7. package/dist/types/components/core/layout.d.ts +2 -2
  8. package/dist/types/components/core/pagination.d.ts +2 -0
  9. package/dist/types/components/core/title.d.ts +2 -1
  10. package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -0
  11. package/dist/types/components/core/useWaveformChartController.d.ts +6 -0
  12. package/dist/types/components/core/useWaveformLayout.d.ts +1 -0
  13. package/dist/types/components/core/useWaveformPresentation.d.ts +1 -0
  14. package/dist/types/components/core/useWaveformYAxisLayout.d.ts +31 -0
  15. package/dist/types/components/core/waveformChartTypes.d.ts +9 -2
  16. package/dist/types/components/core/yAxisLayoutMetrics.d.ts +1 -1
  17. package/dist/types/components/data/types.d.ts +1 -1
  18. package/dist/types/components/interaction/interactionTarget.d.ts +1 -0
  19. package/dist/types/components/interaction/remoteViewport.d.ts +8 -0
  20. package/dist/types/components/interaction/useWaveformViewport.d.ts +1 -2
  21. package/dist/types/components/interaction/viewportDomain.d.ts +1 -2
  22. package/dist/types/components/interaction/viewportPan.d.ts +4 -0
  23. package/dist/types/components/rendering/WaveformLegend.vue.d.ts +2 -0
  24. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -0
  25. package/dist/types/components/rendering/WaveformTrackAxes.vue.d.ts +5 -1
  26. package/dist/types/data/recordedDampedWaveform.d.ts +1 -0
  27. package/dist/types/index.d.ts +1 -1
  28. package/dist/types/types/chart.d.ts +13 -0
  29. package/dist/types/types/index.d.ts +1 -1
  30. package/package.json +1 -1
  31. package/src/components/WaveformChart.css +5 -0
  32. package/src/components/WaveformChart.vue +2 -0
  33. package/src/components/WaveformChartView.vue +14 -3
  34. package/src/components/core/grid.ts +4 -1
  35. package/src/components/core/layout.ts +16 -2
  36. package/src/components/core/pagination.ts +17 -0
  37. package/src/components/core/title.ts +7 -3
  38. package/src/components/core/trackLayoutBuilder.ts +14 -4
  39. package/src/components/core/useWaveformChartController.ts +17 -1
  40. package/src/components/core/useWaveformChartLifecycle.ts +10 -16
  41. package/src/components/core/useWaveformLayout.ts +35 -91
  42. package/src/components/core/useWaveformPresentation.ts +17 -3
  43. package/src/components/core/useWaveformYAxisLayout.ts +140 -0
  44. package/src/components/core/waveformChartTypes.ts +12 -0
  45. package/src/components/core/yAxisLayoutMetrics.ts +11 -2
  46. package/src/components/data/types.ts +3 -0
  47. package/src/components/interaction/interactionTarget.ts +10 -0
  48. package/src/components/interaction/remoteViewport.ts +53 -0
  49. package/src/components/interaction/useWaveformViewport.ts +17 -55
  50. package/src/components/interaction/useWaveformZoom.ts +7 -7
  51. package/src/components/interaction/viewportDomain.ts +10 -14
  52. package/src/components/interaction/viewportPan.ts +82 -0
  53. package/src/components/rendering/WaveformLegend.vue +10 -1
  54. package/src/components/rendering/WaveformTrack.vue +13 -2
  55. package/src/components/rendering/WaveformTrackAxes.vue +15 -4
  56. package/src/data/recordedDampedWaveform.ts +3 -0
  57. package/src/data/simulatedWaveforms.ts +12 -10
  58. package/src/index.ts +3 -0
  59. package/src/types/chart.ts +16 -0
  60. package/src/types/index.ts +3 -0
@@ -11,6 +11,10 @@ export interface WaveformPoint {
11
11
  /** Error above Y; overrides `error` for the upper side. */
12
12
  upperError?: number;
13
13
  }
14
+ /** Optional compact title and time-label placement at the card edges. */
15
+ export type WaveformLayoutPreset = 'default' | 'edge-compact';
16
+ /** Controls unit placement without changing series metadata. */
17
+ export type WaveformUnitDisplayMode = 'axis' | 'legend-single-series';
14
18
  /**
15
19
  * 显示模式
16
20
  * - independent: 每个波形独立 Y 轴和缩放
@@ -44,6 +48,13 @@ export interface WaveformZoomEndPayload {
44
48
  seriesIds?: string[];
45
49
  gesture?: 'wheel' | 'box';
46
50
  }
51
+ /** Final horizontal viewport after a pan; endpoints are always seconds. */
52
+ export interface WaveformPanEndPayload {
53
+ start: number;
54
+ end: number;
55
+ trackIndex?: number;
56
+ seriesIds?: string[];
57
+ }
47
58
  /** Describes the X-axis viewport targeted by a user zoom gesture before rendering settles. */
48
59
  export interface WaveformZoomIntentPayload {
49
60
  start: number;
@@ -221,6 +232,8 @@ export interface WaveformAxesOptions {
221
232
  splitNumber?: number;
222
233
  /** Expands Y-axis domains to equal, human-friendly intervals. Defaults to true. */
223
234
  nice?: boolean;
235
+ /** Adds this fraction of the automatic Y span above its maximum. Defaults to 0. */
236
+ upperPaddingRatio?: number;
224
237
  };
225
238
  }
226
239
  /** Styling and visibility options for the horizontal zero-value reference line. */
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * 类型定义统一导出
3
3
  */
4
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformZoomIntentPayload, WaveformZoomEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformSamplingMode, WaveformSamplingStrategy, WaveformSamplingOptions, WaveformSamplingBackend, WaveformSamplingDiagnostics, WaveformSamplingError, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, } from './chart';
4
+ export type { WaveformPoint, WaveformDisplayMode, WaveformUnitDisplayMode, WaveformLayoutPreset, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformZoomIntentPayload, WaveformZoomEndPayload, WaveformPanEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformSamplingMode, WaveformSamplingStrategy, WaveformSamplingOptions, WaveformSamplingBackend, WaveformSamplingDiagnostics, WaveformSamplingError, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, } from './chart';
5
5
  export type { SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformTypedValues, TypedSampleData, TypedPointData, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './data';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -117,6 +117,11 @@
117
117
  font-size: 12px;
118
118
  }
119
119
 
120
+ .waveform-chart--edge-compact .waveform-chart__x-label {
121
+ font-size: 18px;
122
+ font-weight: 700;
123
+ }
124
+
120
125
  .waveform-chart__label {
121
126
  font-weight: 700;
122
127
  }
@@ -10,6 +10,8 @@ import WaveformChartView from './WaveformChartView.vue'
10
10
  const props = withDefaults(defineProps<WaveformChartProps>(), {
11
11
  displayMode: 'independent',
12
12
  overlayMode: 'single-axis',
13
+ unitDisplayMode: 'axis',
14
+ layoutPreset: 'default',
13
15
  yLabel: '幅值',
14
16
  lineColor: '#0960bd',
15
17
  showTooltip: true,
@@ -13,10 +13,14 @@ const {
13
13
  displayMode,
14
14
  activeInteractionMode,
15
15
  isCleanView,
16
+ isEdgeCompact,
17
+ paginationVisible,
16
18
  isPresentationMode,
17
19
  selection,
18
20
  containerStyle,
19
21
  overlayMode,
22
+ unitDisplayMode,
23
+ showLegendUnits,
20
24
  resolvedChartLeftMargin,
21
25
  titleAreaHeight,
22
26
  resolvedPlotMargin,
@@ -77,7 +81,6 @@ const {
77
81
  xAxisTitleY,
78
82
  hasChartArea,
79
83
  gridOptions,
80
- pageCount,
81
84
  currentPage,
82
85
  getPageSize,
83
86
  pageableTracks,
@@ -116,6 +119,7 @@ function handleChartPointerLeave() {
116
119
  `waveform-chart--interaction-${activeInteractionMode}`,
117
120
  {
118
121
  'waveform-chart--clean': isCleanView,
122
+ 'waveform-chart--edge-compact': isEdgeCompact,
119
123
  'waveform-chart--presentation': isPresentationMode,
120
124
  'waveform-chart--panning': selection?.kind === 'pan',
121
125
  },
@@ -233,6 +237,9 @@ function handleChartPointerLeave() {
233
237
  :frame-number="resolveFrameNumber(track.id)"
234
238
  :frame-style="frameStyle"
235
239
  :axes="axes"
240
+ :show-axis-units="unitDisplayMode === 'axis'"
241
+ :contain-y-axis-endpoints="isEdgeCompact"
242
+ :contain-frame-stroke="isEdgeCompact"
236
243
  :clean-view="isCleanView"
237
244
  :zero-line="resolvedZeroLine"
238
245
  :time-unit="timeUnit"
@@ -284,8 +291,12 @@ function handleChartPointerLeave() {
284
291
  :transform="`translate(${track.left}, ${track.top})`"
285
292
  >
286
293
  <WaveformLegend
287
- v-if="!track.isEmpty && track.legendSeries.length > 1"
294
+ v-if="
295
+ !track.isEmpty &&
296
+ (track.legendSeries.length > 1 || unitDisplayMode === 'legend-single-series')
297
+ "
288
298
  :series="track.legendSeries"
299
+ :show-units="showLegendUnits"
289
300
  :position="resolveLegendPosition(track.id)"
290
301
  :orientation="resolveLegendOrientation(resolveLegendPosition(track.id))"
291
302
  :background-color="legendBackgroundColor"
@@ -321,7 +332,7 @@ function handleChartPointerLeave() {
321
332
  </svg>
322
333
 
323
334
  <Pagination
324
- v-if="gridOptions.showPagination && pageCount > 1 && !isCleanView"
335
+ v-if="paginationVisible"
325
336
  class="waveform-chart__pagination"
326
337
  aria-label="波形分页"
327
338
  :current="currentPage"
@@ -134,6 +134,7 @@ export function resolveGridCellGeometry(
134
134
  slotHasSeries: boolean[] = [],
135
135
  horizontalGap?: number,
136
136
  showXAxis = true,
137
+ bottomAxisBandInMargin = false,
137
138
  ): GridCellGeometry[] {
138
139
  const pageSize = getPageSize(options)
139
140
  const seriesCount = slotHasSeries.filter(Boolean).length
@@ -164,7 +165,9 @@ export function resolveGridCellGeometry(
164
165
  }
165
166
  }
166
167
  const totalVerticalGap = Math.max(0, rowCount - 1) * defaultGap
167
- const totalAxisBand = axisRows.size * X_AXIS_BAND
168
+ // The final row's ticks may use the chart bottom margin; intermediate bands stay in-grid.
169
+ const bottomBandCredit = bottomAxisBandInMargin && axisRows.has(rowCount - 1) ? X_AXIS_BAND : 0
170
+ const totalAxisBand = axisRows.size * X_AXIS_BAND - bottomBandCredit
168
171
  const plotHeight = Math.max(1, (innerHeight - totalVerticalGap - totalAxisBand) / rowCount)
169
172
 
170
173
  return Array.from({ length: filledLastRow ? seriesCount : pageSize }, (_, slotIndex) => {
@@ -161,10 +161,21 @@ export function resolveRenderedYAxisSeriesGroups(
161
161
  yDomain?: WaveformYDomain,
162
162
  yDomains?: Record<string, WaveformYDomain>,
163
163
  viewportYDomains?: Record<string, [number, number]>,
164
+ upperPaddingRatio = 0,
164
165
  ): YAxisSeriesGroup[] {
165
166
  const viewportYDomain = viewportYDomains?.[track.id]
166
167
  return resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).map((group) =>
167
- !group.fixed && viewportYDomain ? { ...group, domain: viewportYDomain } : group,
168
+ !group.fixed && viewportYDomain
169
+ ? { ...group, domain: viewportYDomain }
170
+ : !group.fixed && Number.isFinite(upperPaddingRatio) && upperPaddingRatio > 0
171
+ ? {
172
+ ...group,
173
+ domain: [
174
+ group.domain[0],
175
+ group.domain[1] + (group.domain[1] - group.domain[0]) * upperPaddingRatio,
176
+ ] as WaveformYDomain,
177
+ }
178
+ : group,
168
179
  )
169
180
  }
170
181
 
@@ -255,6 +266,8 @@ export function buildYAxisSlots(
255
266
  nice = true,
256
267
  includeWithoutLastTick = false,
257
268
  viewportYDomains?: Record<string, [number, number]>,
269
+ upperPaddingRatio = 0,
270
+ showUnits = true,
258
271
  ): { slots: YAxisSlot[]; clearance: { left: number; right: number } } {
259
272
  const widths = new Map<string, number>()
260
273
  tracks.forEach((track) => {
@@ -265,6 +278,7 @@ export function buildYAxisSlots(
265
278
  yDomain,
266
279
  yDomains,
267
280
  viewportYDomains,
281
+ upperPaddingRatio,
268
282
  ).forEach((group) => {
269
283
  const sideIndex = sideIndexes[group.side]++
270
284
  const key = `${group.side}:${sideIndex}`
@@ -272,7 +286,7 @@ export function buildYAxisSlots(
272
286
  group.domain,
273
287
  nice,
274
288
  undefined,
275
- group.seriesList[0]?.unit,
289
+ showUnits ? group.seriesList[0]?.unit : undefined,
276
290
  tickCount,
277
291
  includeWithoutLastTick,
278
292
  ).tickTextWidth
@@ -0,0 +1,17 @@
1
+ /** Conservative Ant pagination sizing, including first/last pages and both ellipses. */
2
+ export function resolveCompactPaginationBand(
3
+ visible: boolean,
4
+ width: number,
5
+ pageCount: number,
6
+ timeLabelCenter: number,
7
+ timeLabel: string,
8
+ ): 0 | 16 | 40 {
9
+ if (!visible) return 0
10
+ if (width <= 520) return 40
11
+ const controls = Math.min(pageCount, 9) + 2
12
+ const paginationWidth = controls * 32 + controls * 8
13
+ // A full 18px em per character also accommodates wide glyphs and Chinese labels.
14
+ const labelRight = timeLabelCenter + (Array.from(timeLabel).length * 18) / 2
15
+ const availableWidth = width - 10 - labelRight - 8
16
+ return availableWidth >= paginationWidth ? 16 : 40
17
+ }
@@ -20,6 +20,7 @@ export interface RotatedTitleLayoutOptions {
20
20
  naturalHeight: number
21
21
  availableWidth: number
22
22
  rotation: number
23
+ edgeCompact?: boolean
23
24
  }
24
25
 
25
26
  function clampPositive(value: number): number {
@@ -31,7 +32,10 @@ export function calculateRotatedTitleLayout({
31
32
  naturalHeight,
32
33
  availableWidth,
33
34
  rotation,
35
+ edgeCompact = false,
34
36
  }: RotatedTitleLayoutOptions): RotatedTitleLayout {
37
+ const verticalPadding = edgeCompact ? 8 : TITLE_AREA_VERTICAL_PADDING
38
+ const minimumHeight = edgeCompact ? 0 : TITLE_AREA_MIN_HEIGHT
35
39
  const safeNaturalWidth = clampPositive(naturalWidth)
36
40
  const safeNaturalHeight = clampPositive(naturalHeight)
37
41
  const safeAvailableWidth = clampPositive(availableWidth)
@@ -53,13 +57,13 @@ export function calculateRotatedTitleLayout({
53
57
  textHeight,
54
58
  visualWidth: textWidth,
55
59
  visualHeight: textHeight,
56
- areaHeight: Math.max(TITLE_AREA_MIN_HEIGHT, textHeight + TITLE_AREA_VERTICAL_PADDING),
60
+ areaHeight: Math.max(minimumHeight, textHeight + verticalPadding),
57
61
  scale: 1,
58
62
  wrapped: lineCount > 1,
59
63
  }
60
64
  }
61
65
 
62
- const maximumVisualHeight = TITLE_AREA_MAX_HEIGHT - TITLE_AREA_VERTICAL_PADDING
66
+ const maximumVisualHeight = TITLE_AREA_MAX_HEIGHT - verticalPadding
63
67
  const naturalVisualWidth = safeNaturalWidth * absoluteCosine + safeNaturalHeight * absoluteSine
64
68
  const naturalVisualHeight = safeNaturalWidth * absoluteSine + safeNaturalHeight * absoluteCosine
65
69
  const scale = Math.min(
@@ -71,7 +75,7 @@ export function calculateRotatedTitleLayout({
71
75
  const visualHeight = naturalVisualHeight * scale
72
76
  const areaHeight = Math.min(
73
77
  TITLE_AREA_MAX_HEIGHT,
74
- Math.max(TITLE_AREA_MIN_HEIGHT, visualHeight + TITLE_AREA_VERTICAL_PADDING),
78
+ Math.max(minimumHeight, visualHeight + verticalPadding),
75
79
  )
76
80
 
77
81
  return {
@@ -71,6 +71,7 @@ export interface BuildTrackLayoutsOptions {
71
71
  xAxisLabelFormatter?: WaveformXAxisLabelFormatter
72
72
  yAxisSplitNumber?: number
73
73
  yAxisNice?: boolean
74
+ yAxisUpperPaddingRatio?: number
74
75
  rendering: ResolvedWaveformRenderingOptions
75
76
  /** Latest sampling result for SVG lines only; source series remain complete for interaction. */
76
77
  linePointOverrides?: Readonly<Record<string, WaveformPoint[]>>
@@ -79,6 +80,9 @@ export interface BuildTrackLayoutsOptions {
79
80
  yLabel?: string
80
81
  yAxisSlots?: readonly YAxisSlot[]
81
82
  showCompactEmptyTracks: boolean
83
+ useNonEmptyBottomTracks?: boolean
84
+ compactYAxisLayout?: boolean
85
+ showAxisUnits?: boolean
82
86
  }
83
87
 
84
88
  function resolveIndependentXDomain(
@@ -100,7 +104,12 @@ function resolveIndependentXDomain(
100
104
  }
101
105
 
102
106
  export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayout[] {
103
- const visibleCells = options.cells.map((cell) => ({ ...cell, hasSeries: Boolean(cell.series) }))
107
+ const visibleCells = options.cells.map((cell) => ({
108
+ ...cell,
109
+ hasSeries: options.useNonEmptyBottomTracks
110
+ ? Boolean(cell.series?.series.length)
111
+ : Boolean(cell.series),
112
+ }))
104
113
  const bottomCells = getBottomRowCellIndexes(visibleCells, options.grid.columnCount)
105
114
 
106
115
  const layouts: TrackLayout[] = visibleCells.flatMap((cell, index) => {
@@ -135,6 +144,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
135
144
  options.fixedYDomain,
136
145
  options.fixedYDomains,
137
146
  options.yDomains,
147
+ options.yAxisUpperPaddingRatio,
138
148
  )
139
149
  const sideIndexes = { left: 0, right: 0 }
140
150
  const sideOffsets = { left: 0, right: 0 }
@@ -151,7 +161,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
151
161
  scale.domain() as [number, number],
152
162
  false,
153
163
  tickValues,
154
- group.seriesList[0]?.unit,
164
+ options.showAxisUnits !== false ? group.seriesList[0]?.unit : undefined,
155
165
  tickCount,
156
166
  )
157
167
  const clearance =
@@ -174,7 +184,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
174
184
  (slot
175
185
  ? (group.side === 'left' ? 0 : cell.width) + slot.labelOffset
176
186
  : x + (group.side === 'left' ? -labelDistance : labelDistance)) -
177
- (group.side === 'right' ? Y_AXIS_RIGHT_LABEL_OFFSET : 0)
187
+ (group.side === 'right' && !options.compactYAxisLayout ? Y_AXIS_RIGHT_LABEL_OFFSET : 0)
178
188
  if (!slot) sideOffsets[group.side] += clearance
179
189
  return {
180
190
  index: group.index,
@@ -288,6 +298,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
288
298
  }
289
299
  })
290
300
 
291
- alignLeftYAxisTitles(layouts, options.yLabel)
301
+ if (!options.compactYAxisLayout) alignLeftYAxisTitles(layouts, options.yLabel)
292
302
  return layouts
293
303
  }
@@ -1,5 +1,6 @@
1
1
  import { zoomIdentity, type ZoomTransform } from 'd3'
2
2
  import {
3
+ computed,
3
4
  reactive,
4
5
  markRaw,
5
6
  ref,
@@ -21,6 +22,7 @@ import { useWaveformZoom } from '../interaction/useWaveformZoom'
21
22
  import { useAnimationFrameThrottle } from '../utils/useAnimationFrameThrottle'
22
23
  import { margin } from './constants'
23
24
  import { getPageSize } from './grid'
25
+ import { resolveCompactPaginationBand } from './pagination'
24
26
  import type { WaveformHoverState } from './types'
25
27
  import { useWaveformChartLifecycle } from './useWaveformChartLifecycle'
26
28
  import { usePreparedWaveformSeries } from './useWaveformData'
@@ -165,9 +167,22 @@ export function useWaveformChartController(
165
167
  linePointOverrides,
166
168
  })
167
169
 
170
+ const paginationVisible = computed(
171
+ () => gridOptions.value.showPagination && pageCount.value > 1 && !isCleanView.value,
172
+ )
168
173
  watchEffect(() => {
169
174
  paginationBandHeight.value =
170
- gridOptions.value.showPagination && pageCount.value > 1 && chartWidth.value <= 520 ? 40 : 0
175
+ props.layoutPreset === 'edge-compact'
176
+ ? resolveCompactPaginationBand(
177
+ paginationVisible.value,
178
+ chartWidth.value,
179
+ pageCount.value,
180
+ resolvedChartLeftMargin.value + innerWidth.value / 2,
181
+ layout.resolvedXLabel.value,
182
+ )
183
+ : gridOptions.value.showPagination && pageCount.value > 1 && chartWidth.value <= 520
184
+ ? 40
185
+ : 0
171
186
  })
172
187
 
173
188
  const zoom = useWaveformZoom({
@@ -306,6 +321,7 @@ export function useWaveformChartController(
306
321
  return reactive({
307
322
  ...toRefs(props),
308
323
  ...presentation,
324
+ paginationVisible,
309
325
  ...layout,
310
326
  ...annotations,
311
327
  ...viewport,
@@ -13,7 +13,9 @@ import type { AnnotationSeriesCandidate, useWaveformAnnotationInteraction } from
13
13
  import type { NormalizedWaveformGridOptions } from './grid'
14
14
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
15
15
  import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes'
16
- import { constrainZoomDomain, transformForDomain } from '../interaction/zoomConstraints'
16
+ import { transformForDomain } from '../interaction/zoomConstraints'
17
+ import { remotePanBoundary, restoreViewportDomain } from '../interaction/remoteViewport'
18
+ import { isEditableTarget } from '../interaction/interactionTarget'
17
19
  import { seriesIdentity } from '../interaction/zoomEventPayload'
18
20
 
19
21
  interface LifecycleContext {
@@ -61,17 +63,6 @@ interface LifecycleContext {
61
63
  clearZoomBindings: () => void
62
64
  }
63
65
 
64
- function isEditableTarget(target: EventTarget | null): boolean {
65
- return (
66
- target instanceof Element &&
67
- Boolean(
68
- target.closest(
69
- 'button, input, select, textarea, [contenteditable]:not([contenteditable="false"])',
70
- ),
71
- )
72
- )
73
- }
74
-
75
66
  export function useWaveformChartLifecycle(context: LifecycleContext) {
76
67
  const {
77
68
  props,
@@ -161,7 +152,8 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
161
152
  trackLayouts.value.flatMap((track) => {
162
153
  const current = track.xScale.domain() as [number, number]
163
154
  const boundary = resolveInitialTrackDomain(track)
164
- return current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
155
+ return remotePanBoundary(props) ||
156
+ current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
165
157
  ? [[seriesIdentity(track.seriesList.map((series) => series.id)), current]]
166
158
  : []
167
159
  }),
@@ -172,7 +164,9 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
172
164
  const current = sharedZoomDomain.value
173
165
  const boundary = initialXDomain.value
174
166
  pendingSharedXDomain =
175
- current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? [...current] : undefined
167
+ remotePanBoundary(props) || current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
168
+ ? [...current]
169
+ : undefined
176
170
  }
177
171
 
178
172
  function handleDataReferenceChange() {
@@ -193,7 +187,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
193
187
  )
194
188
  if (!previousDomain) return
195
189
  const boundary = resolveInitialTrackDomain(track)
196
- const domain = constrainZoomDomain(previousDomain, boundary, [track.seriesList], props)
190
+ const domain = restoreViewportDomain(previousDomain, boundary, [track.seriesList], props)
197
191
  nextTransforms[track.index] = transformForDomain(domain, boundary, track.width)
198
192
  })
199
193
  independentTransforms.value = nextTransforms
@@ -203,7 +197,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
203
197
  const groups = trackLayouts.value
204
198
  .filter((track) => track.hasVisibleSeries)
205
199
  .map((track) => track.seriesList)
206
- const domain = constrainZoomDomain(pendingSharedXDomain, boundary, groups, props)
200
+ const domain = restoreViewportDomain(pendingSharedXDomain, boundary, groups, props)
207
201
  sharedTransform.value = transformForDomain(domain, boundary, innerWidth.value)
208
202
  pendingSharedXDomain = undefined
209
203
  }
@@ -7,22 +7,17 @@ import {
7
7
  type AnnotationSeriesInfo,
8
8
  type AnnotationTrackLayout,
9
9
  } from '../annotation'
10
- import { channelColors, margin, MINIMUM_PLOT_WIDTH } from './constants'
11
- import {
12
- getGridGap,
13
- getPageCount,
14
- normalizeGridOptions,
15
- paginateSeries,
16
- resolveGridCellGeometry,
17
- } from './grid'
18
- import { buildTrackLayouts, buildYAxisSlots, resolveYAxisTickCount } from './layout'
10
+ import { channelColors } from './constants'
11
+ import { getPageCount, normalizeGridOptions, paginateSeries, resolveGridCellGeometry } from './grid'
12
+ import { buildTrackLayouts, resolveYAxisTickCount } from './layout'
19
13
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
20
14
  import { createFrameNumberResolver, resolvePageableTracks } from './trackPagination'
21
15
  import type { PreparedWaveformSeries } from './useWaveformData'
22
16
  import type { ResolvedWaveformChartProps } from './waveformChartTypes'
23
17
  import { applyXDomainStrategy } from './xDomain'
24
18
  import { alignIntegerZoomDomain, normalizeIntegerZoomDomain } from '../interaction/integerZoom'
25
- import { resolveYAxisLayoutMetrics, resolveViewportYDomains } from './yAxisLayoutMetrics'
19
+ import { resolveViewportYDomains } from './yAxisLayoutMetrics'
20
+ import { useWaveformYAxisLayout } from './useWaveformYAxisLayout'
26
21
  import type { useWaveformAnnotationInteraction } from '../annotation'
27
22
  interface LayoutContext {
28
23
  props: ResolvedWaveformChartProps
@@ -95,6 +90,11 @@ export function useWaveformLayout(context: LayoutContext) {
95
90
  }
96
91
  })
97
92
  })
93
+ const showLegendUnits = computed(
94
+ () =>
95
+ props.unitDisplayMode === 'legend-single-series' &&
96
+ !chartTracks.value.some((track) => track.series.length > 1),
97
+ )
98
98
  const renderingOptions = computed(() => resolveWaveformRenderingOptions(props.rendering))
99
99
  const pageableTracks = computed(() =>
100
100
  resolvePageableTracks(chartTracks.value, gridOptions.value.hideEmptyTracks),
@@ -120,86 +120,21 @@ export function useWaveformLayout(context: LayoutContext) {
120
120
  independentYDomains.value,
121
121
  ),
122
122
  )
123
- const yAxisMetrics = computed(() =>
124
- resolveYAxisLayoutMetrics(
125
- chartTracks.value,
126
- props.overlayMode,
127
- props.yDomain,
128
- props.yDomains,
129
- viewportYDomains.value,
130
- props.axes?.y?.nice !== false,
131
- yAxisTickCount.value,
132
- props.displayMode === 'compact',
133
- ),
134
- )
135
- const hasYAxisLabels = computed(() =>
136
- chartTracks.value.some(
137
- (track) =>
138
- track.visibleSeries.length === 1 &&
139
- Boolean(track.visibleSeries[0]?.name.trim() || props.yLabel),
140
- ),
141
- )
142
- const hasVisibleWaveformData = computed(() =>
143
- chartTracks.value.some((track) => track.visibleSeries.length > 0),
144
- )
145
- const chartLeftMargin = computed(() =>
146
- Math.max(
147
- margin.left,
148
- hasYAxisLabels.value
149
- ? yAxisMetrics.value.fullClearance
150
- : hasVisibleWaveformData.value
151
- ? yAxisMetrics.value.tickClearance
152
- : 0,
153
- ),
154
- )
155
- const yAxisSlots = computed(() =>
156
- buildYAxisSlots(
157
- layoutTracks.value.filter((track) => track.visibleSeries.length > 0),
158
- props.overlayMode,
159
- props.yDomain,
160
- props.yDomains,
161
- yAxisTickCount.value,
162
- props.axes?.y?.nice !== false,
163
- props.displayMode === 'compact',
164
- viewportYDomains.value,
165
- ),
166
- )
167
- const resolvedChartLeftMargin = computed(() =>
168
- props.overlayMode === 'multi-axis'
169
- ? Math.max(chartLeftMargin.value, yAxisSlots.value.clearance.left)
170
- : chartLeftMargin.value,
171
- )
172
- const chartRightMargin = computed(() =>
173
- props.overlayMode === 'multi-axis'
174
- ? Math.max(margin.right, yAxisSlots.value.clearance.right)
175
- : margin.right,
176
- )
177
- const innerWidth = computed(() =>
178
- Math.max(0, chartWidth.value - resolvedChartLeftMargin.value - chartRightMargin.value),
179
- )
180
- const yAxisLayout = computed(() => {
181
- const baseGap = getGridGap(props.displayMode)
182
- const columnCount = gridOptions.value.columnCount
183
- const hasMultipleColumns = columnCount > 1
184
- const fullGap = Math.max(baseGap, yAxisMetrics.value.fullClearance)
185
- const tickGap = Math.max(baseGap, yAxisMetrics.value.tickClearance)
186
- const plotWidth = (innerWidth.value - fullGap * Math.max(0, columnCount - 1)) / columnCount
187
- const canReserveLabelClearance = plotWidth >= MINIMUM_PLOT_WIDTH
188
- return {
189
- horizontalGap:
190
- props.overlayMode === 'multi-axis' && hasMultipleColumns && hasVisibleWaveformData.value
191
- ? Math.max(baseGap, yAxisSlots.value.clearance.left + yAxisSlots.value.clearance.right)
192
- : hasMultipleColumns && hasVisibleWaveformData.value
193
- ? hasYAxisLabels.value && canReserveLabelClearance
194
- ? fullGap
195
- : tickGap
196
- : baseGap,
197
- hideSecondaryLabels:
198
- props.overlayMode !== 'multi-axis' &&
199
- hasMultipleColumns &&
200
- hasYAxisLabels.value &&
201
- !canReserveLabelClearance,
202
- }
123
+ const {
124
+ yAxisMetrics,
125
+ yAxisSlots,
126
+ resolvedChartLeftMargin,
127
+ innerWidth,
128
+ yAxisLayout,
129
+ hasVisibleWaveformData,
130
+ } = useWaveformYAxisLayout({
131
+ props,
132
+ chartTracks,
133
+ layoutTracks,
134
+ chartWidth,
135
+ gridOptions,
136
+ viewportYDomains,
137
+ yAxisTickCount,
203
138
  })
204
139
  const hasWaveformData = computed(() => chartSeries.value.length > 0)
205
140
  const hasChartArea = computed(() => innerWidth.value > 0 && innerHeight.value > 0)
@@ -279,8 +214,12 @@ export function useWaveformLayout(context: LayoutContext) {
279
214
  innerHeight.value,
280
215
  gridOptions.value,
281
216
  props.displayMode,
282
- layoutTracks.value.map(Boolean),
217
+ layoutTracks.value.map((track) =>
218
+ props.layoutPreset === 'edge-compact' ? track.series.length > 0 : Boolean(track),
219
+ ),
283
220
  yAxisLayout.value.horizontalGap,
221
+ true,
222
+ props.layoutPreset === 'edge-compact' && !isCleanView.value,
284
223
  )
285
224
  return cells.map((cell, index) => ({ ...cell, series: layoutTracks.value[index] }))
286
225
  })
@@ -288,6 +227,9 @@ export function useWaveformLayout(context: LayoutContext) {
288
227
  buildTrackLayouts({
289
228
  cells: gridCells.value,
290
229
  grid: gridOptions.value,
230
+ useNonEmptyBottomTracks: props.layoutPreset === 'edge-compact',
231
+ compactYAxisLayout: props.layoutPreset === 'edge-compact',
232
+ showAxisUnits: props.layoutPreset !== 'edge-compact' || props.unitDisplayMode === 'axis',
291
233
  displayMode: props.displayMode,
292
234
  overlayMode: props.overlayMode,
293
235
  independentTransforms: independentTransforms.value,
@@ -303,6 +245,7 @@ export function useWaveformLayout(context: LayoutContext) {
303
245
  xAxisLabelFormatter: props.axes?.x?.labelFormatter,
304
246
  yAxisSplitNumber: props.axes?.y?.splitNumber,
305
247
  yAxisNice: props.axes?.y?.nice,
248
+ yAxisUpperPaddingRatio: props.axes?.y?.upperPaddingRatio,
306
249
  rendering: renderingOptions.value,
307
250
  linePointOverrides: linePointOverrides?.value,
308
251
  hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
@@ -358,6 +301,7 @@ export function useWaveformLayout(context: LayoutContext) {
358
301
  return {
359
302
  chartSeries,
360
303
  chartTracks,
304
+ showLegendUnits,
361
305
  pageableTracks,
362
306
  renderingOptions,
363
307
  gridOptions,