waveform-analysis 0.1.74 → 0.1.76

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 (150) hide show
  1. package/README.md +422 -60
  2. package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
  3. package/dist/index.cjs +1 -2
  4. package/dist/index.js +1 -7119
  5. package/dist/style.css +1 -1
  6. package/dist/types/animation/WaveformAnimationController.d.ts +30 -0
  7. package/dist/types/animation/core/Animation.d.ts +13 -0
  8. package/dist/types/animation/core/Animator.d.ts +23 -0
  9. package/dist/types/animation/core/easing.d.ts +3 -0
  10. package/dist/types/animation/core/errors.d.ts +4 -0
  11. package/dist/types/animation/core/handle.d.ts +7 -0
  12. package/dist/types/animation/core/owner.d.ts +3 -0
  13. package/dist/types/animation/core/propertyGroups.d.ts +17 -0
  14. package/dist/types/animation/core/task.d.ts +29 -0
  15. package/dist/types/animation/core/types.d.ts +74 -0
  16. package/dist/types/animation/core/validation.d.ts +30 -0
  17. package/dist/types/animation/core/values.d.ts +8 -0
  18. package/dist/types/animation/presets/annotation.d.ts +4 -0
  19. package/dist/types/animation/presets/lineDrawing.d.ts +7 -0
  20. package/dist/types/animation/presets/shared.d.ts +14 -0
  21. package/dist/types/animation/presets/transitions.d.ts +4 -0
  22. package/dist/types/animation/types.d.ts +84 -0
  23. package/dist/types/components/WaveformChart.vue.d.ts +3 -2
  24. package/dist/types/components/animation/annotationPresentation.d.ts +17 -0
  25. package/dist/types/components/animation/chartExportVersion.d.ts +2 -0
  26. package/dist/types/components/animation/config.d.ts +31 -0
  27. package/dist/types/components/animation/context.d.ts +20 -0
  28. package/dist/types/components/animation/geometry.d.ts +10 -0
  29. package/dist/types/components/animation/paginationPresentation.d.ts +13 -0
  30. package/dist/types/components/animation/pathBuffer.d.ts +4 -0
  31. package/dist/types/components/animation/trackPresentation.d.ts +35 -0
  32. package/dist/types/components/animation/useWaveformAnimations.d.ts +27 -0
  33. package/dist/types/components/annotation/WaveformAnnotationContextMenu.vue.d.ts +2 -2
  34. package/dist/types/components/annotation/types.d.ts +7 -0
  35. package/dist/types/components/controls/useChartImageExport.d.ts +2 -0
  36. package/dist/types/components/core/amplitudeLimits.d.ts +6 -0
  37. package/dist/types/components/core/layout.d.ts +6 -9
  38. package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
  39. package/dist/types/components/core/types.d.ts +8 -0
  40. package/dist/types/components/core/useWaveformChartController.d.ts +26 -1
  41. package/dist/types/components/core/useWaveformData.d.ts +2 -2
  42. package/dist/types/components/core/useWaveformMeasurements.d.ts +4 -1
  43. package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
  44. package/dist/types/components/core/waveformChartTypes.d.ts +6 -3
  45. package/dist/types/components/core/xAxisTicks.d.ts +2 -0
  46. package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
  47. package/dist/types/components/core/yAxisTicks.d.ts +6 -0
  48. package/dist/types/components/data/types.d.ts +1 -1
  49. package/dist/types/components/interaction/integerZoom.d.ts +3 -3
  50. package/dist/types/components/interaction/stepZoom.d.ts +5 -0
  51. package/dist/types/components/interaction/useWaveformViewport.d.ts +0 -3
  52. package/dist/types/components/interaction/useWaveformZoom.d.ts +0 -2
  53. package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +1 -0
  54. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -3
  55. package/dist/types/components/rendering/updateAxis.d.ts +5 -0
  56. package/dist/types/components/rendering/updateGrid.d.ts +3 -0
  57. package/dist/types/components/rendering/yAxisTickVisibility.d.ts +2 -0
  58. package/dist/types/components/utils/assignElement.d.ts +2 -0
  59. package/dist/types/core/data.d.ts +1 -1
  60. package/dist/types/demo/DemoChartHost.vue.d.ts +1 -1
  61. package/dist/types/demo/remoteWaveformLoader.d.ts +23 -0
  62. package/dist/types/demo/types.d.ts +11 -2
  63. package/dist/types/demo/useRemoteWaveformWindow.d.ts +22 -0
  64. package/dist/types/index.d.ts +10 -1
  65. package/dist/types/types/chart.d.ts +8 -1
  66. package/dist/types/types/index.d.ts +1 -1
  67. package/dist/types/utils/formatters.d.ts +4 -4
  68. package/dist/types/utils/xAxisNumber.d.ts +2 -0
  69. package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
  70. package/package.json +5 -2
  71. package/src/App.vue +34 -100
  72. package/src/DemoRouterApp.vue +1 -0
  73. package/src/animation/WaveformAnimationController.ts +328 -0
  74. package/src/animation/core/Animation.ts +37 -0
  75. package/src/animation/core/Animator.ts +200 -0
  76. package/src/animation/core/easing.ts +74 -0
  77. package/src/animation/core/errors.ts +24 -0
  78. package/src/animation/core/handle.ts +34 -0
  79. package/src/animation/core/owner.ts +9 -0
  80. package/src/animation/core/propertyGroups.ts +143 -0
  81. package/src/animation/core/task.ts +153 -0
  82. package/src/animation/core/types.ts +81 -0
  83. package/src/animation/core/validation.ts +173 -0
  84. package/src/animation/core/values.ts +38 -0
  85. package/src/animation/presets/annotation.ts +66 -0
  86. package/src/animation/presets/lineDrawing.ts +51 -0
  87. package/src/animation/presets/shared.ts +58 -0
  88. package/src/animation/presets/transitions.ts +77 -0
  89. package/src/animation/types.ts +81 -0
  90. package/src/components/WaveformChart.vue +1 -0
  91. package/src/components/WaveformChartView.vue +13 -6
  92. package/src/components/animation/annotationPresentation.ts +145 -0
  93. package/src/components/animation/chartExportVersion.ts +5 -0
  94. package/src/components/animation/config.ts +87 -0
  95. package/src/components/animation/context.ts +56 -0
  96. package/src/components/animation/geometry.ts +148 -0
  97. package/src/components/animation/paginationPresentation.ts +114 -0
  98. package/src/components/animation/pathBuffer.ts +61 -0
  99. package/src/components/animation/trackPresentation.ts +172 -0
  100. package/src/components/animation/useWaveformAnimations.ts +420 -0
  101. package/src/components/annotation/WaveformAnnotationLayer.vue +43 -30
  102. package/src/components/annotation/types.ts +3 -0
  103. package/src/components/controls/useChartCommands.ts +12 -13
  104. package/src/components/controls/useChartImageExport.ts +4 -0
  105. package/src/components/core/amplitudeLimits.ts +44 -0
  106. package/src/components/core/layout.ts +22 -34
  107. package/src/components/core/trackLayoutBuilder.ts +21 -23
  108. package/src/components/core/types.ts +4 -0
  109. package/src/components/core/useWaveformChartController.ts +27 -23
  110. package/src/components/core/useWaveformChartLifecycle.ts +11 -2
  111. package/src/components/core/useWaveformData.ts +11 -5
  112. package/src/components/core/useWaveformLayout.ts +9 -4
  113. package/src/components/core/useWaveformMeasurements.ts +3 -0
  114. package/src/components/core/useWaveformRenderSampling.ts +23 -1
  115. package/src/components/core/useWaveformYAxisLayout.ts +2 -0
  116. package/src/components/core/waveformChartTypes.ts +6 -2
  117. package/src/components/core/xAxisTicks.ts +69 -0
  118. package/src/components/core/yAxisLayoutMetrics.ts +4 -0
  119. package/src/components/core/yAxisTicks.ts +39 -0
  120. package/src/components/data/types.ts +2 -0
  121. package/src/components/interaction/integerZoom.ts +31 -12
  122. package/src/components/interaction/remoteViewport.ts +26 -4
  123. package/src/components/interaction/stepZoom.ts +33 -0
  124. package/src/components/interaction/useWaveformViewport.ts +3 -26
  125. package/src/components/interaction/useWaveformZoom.ts +50 -157
  126. package/src/components/interaction/zoomConstraints.ts +5 -5
  127. package/src/components/rendering/WaveformSeriesLayer.vue +13 -1
  128. package/src/components/rendering/WaveformTrack.vue +6 -1
  129. package/src/components/rendering/WaveformTrackAxes.vue +64 -51
  130. package/src/components/rendering/WaveformTrackBackdrop.vue +35 -49
  131. package/src/components/rendering/updateAxis.ts +57 -0
  132. package/src/components/rendering/updateGrid.ts +42 -0
  133. package/src/components/rendering/yAxisTickVisibility.ts +45 -18
  134. package/src/components/utils/assignElement.ts +7 -0
  135. package/src/core/data.ts +5 -2
  136. package/src/data/simulatedWaveforms.ts +0 -10
  137. package/src/demo/DemoChartHost.vue +49 -4
  138. package/src/demo/remoteWaveformLoader.ts +114 -0
  139. package/src/demo/types.ts +11 -1
  140. package/src/demo/useRemoteWaveformWindow.ts +229 -0
  141. package/src/index.ts +54 -0
  142. package/src/router.ts +1 -0
  143. package/src/types/chart.ts +10 -1
  144. package/src/types/index.ts +2 -0
  145. package/src/utils/formatters.ts +10 -5
  146. package/src/utils/xAxisNumber.ts +36 -0
  147. package/src/views/AnimationDemo.vue +129 -0
  148. package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
  149. package/dist/types/data/sx2Channel.d.ts +0 -10
  150. package/src/data/sx2Channel.ts +0 -315
@@ -0,0 +1,57 @@
1
+ import { select } from 'd3'
2
+ import type { Axis, NumberValue, ScaleLinear } from 'd3'
3
+
4
+ const previous = new WeakMap<
5
+ SVGGElement,
6
+ { ticks: number[]; range: number[]; orientation: string }
7
+ >()
8
+
9
+ /** Avoid invalidating native SVG layout when an attribute already has the required value. */
10
+ export function updateSvgAttribute(element: Element, name: string, value: string | number | null) {
11
+ const next = value === null ? null : String(value)
12
+ if (element.getAttribute(name) === next) return
13
+ if (next === null) element.removeAttribute(name)
14
+ else element.setAttribute(name, next)
15
+ }
16
+
17
+ /** Domain animation moves existing ticks; unchanged labels keep their native text nodes. */
18
+ export function updateAxis(
19
+ element: SVGGElement,
20
+ axis: Axis<NumberValue>,
21
+ scale: ScaleLinear<number, number>,
22
+ ticks: number[],
23
+ orientation: 'bottom' | 'left' | 'right',
24
+ format: (value: number) => string,
25
+ ) {
26
+ const range = scale.range()
27
+ const cached = previous.get(element)
28
+ const selection = select(element)
29
+ if (
30
+ !cached ||
31
+ cached.ticks !== ticks ||
32
+ cached.orientation !== orientation ||
33
+ range.some((value, index) => value !== cached.range[index])
34
+ ) {
35
+ selection.call(axis)
36
+ previous.set(element, { ticks, range, orientation })
37
+ } else {
38
+ const horizontal = orientation === 'bottom'
39
+ const offset = axis.offset()
40
+ selection
41
+ .selectAll<SVGGElement, number>('.tick')
42
+ .each(function (value) {
43
+ updateSvgAttribute(
44
+ this,
45
+ 'transform',
46
+ horizontal
47
+ ? `translate(${scale(value) + offset},0)`
48
+ : `translate(0,${scale(value) + offset})`,
49
+ )
50
+ })
51
+ .select<SVGTextElement>('text')
52
+ .each(function (value) {
53
+ const label = format(value)
54
+ if (this.textContent !== label) this.textContent = label
55
+ })
56
+ }
57
+ }
@@ -0,0 +1,42 @@
1
+ import { updateSvgAttribute } from './updateAxis'
2
+ import type { ScaleLinear } from 'd3'
3
+
4
+ const nodes = new WeakMap<SVGGElement, Map<number, SVGLineElement>>()
5
+
6
+ /** Keep each grid line independently discoverable by the axis collision pass. */
7
+ export function updateGrid(
8
+ group: SVGGElement,
9
+ ticks: readonly number[],
10
+ scale: ScaleLinear<number, number>,
11
+ direction: 'vertical' | 'horizontal',
12
+ extent: number,
13
+ color: string | undefined,
14
+ ) {
15
+ let lines = nodes.get(group)
16
+ if (!lines) nodes.set(group, (lines = new Map()))
17
+ const current = new Set(ticks)
18
+ for (const [tick, line] of lines) {
19
+ if (!current.has(tick)) {
20
+ line.remove()
21
+ lines.delete(tick)
22
+ }
23
+ }
24
+ let previous: SVGLineElement | undefined
25
+ for (const tick of ticks) {
26
+ let line = lines.get(tick)
27
+ if (!line) {
28
+ line = document.createElementNS('http://www.w3.org/2000/svg', 'line')
29
+ line.setAttribute('data-grid-direction', direction)
30
+ lines.set(tick, line)
31
+ }
32
+ const next = previous ? previous.nextSibling : group.firstChild
33
+ if (next !== line) group.insertBefore(line, next)
34
+ const position = scale(tick)
35
+ updateSvgAttribute(line, 'stroke', color ?? null)
36
+ updateSvgAttribute(line, 'x1', direction === 'vertical' ? position : 0)
37
+ updateSvgAttribute(line, 'x2', direction === 'vertical' ? position : extent)
38
+ updateSvgAttribute(line, 'y1', direction === 'horizontal' ? position : 0)
39
+ updateSvgAttribute(line, 'y2', direction === 'horizontal' ? position : extent)
40
+ previous = line
41
+ }
42
+ }
@@ -1,5 +1,6 @@
1
1
  import { sameTick } from '../core/yAxisPadding'
2
2
  import { formatYAxisTickLabel } from '../core/layout'
3
+ import { updateSvgAttribute } from './updateAxis'
3
4
 
4
5
  interface Box {
5
6
  left: number
@@ -42,14 +43,25 @@ export function linesOverlap(a: number, b: number, widthA = 1, widthB = 1): bool
42
43
  }
43
44
 
44
45
  function hide(element: Element) {
45
- element.setAttribute('display', 'none')
46
- element.setAttribute('data-maximum-hidden', 'true')
46
+ updateSvgAttribute(element, 'display', 'none')
47
+ updateSvgAttribute(element, 'data-maximum-hidden', 'true')
47
48
  }
48
49
 
49
50
  function strokeWidth(element: Element): number {
50
51
  return Number.parseFloat(getComputedStyle(element).strokeWidth) || 1
51
52
  }
52
53
 
54
+ const pending = new WeakSet<SVGSVGElement>()
55
+ /** Coalesce sibling axes after Vue's current DOM patch, without adding a late animation frame. */
56
+ export function scheduleMaximumTickVisibility(svg: SVGSVGElement) {
57
+ if (pending.has(svg)) return
58
+ pending.add(svg)
59
+ queueMicrotask(() => {
60
+ pending.delete(svg)
61
+ updateMaximumTickVisibility(svg)
62
+ })
63
+ }
64
+
53
65
  function formatCompactLabels(svg: SVGSVGElement) {
54
66
  svg.querySelectorAll<SVGTextElement>('text[data-axis-maximum-label]').forEach((maximum) => {
55
67
  const axis = maximum.parentElement!.parentElement!
@@ -64,13 +76,14 @@ function formatCompactLabels(svg: SVGSVGElement) {
64
76
  Number(axis.getAttribute('data-label-domain-max')),
65
77
  ]
66
78
  labels.forEach((text) => {
67
- text.textContent = formatYAxisTickLabel(
79
+ const label = formatYAxisTickLabel(
68
80
  value(text),
69
81
  domain,
70
82
  visibleValues,
71
83
  axis.getAttribute('data-label-unit') ?? undefined,
72
84
  axis.getAttribute('data-y-axis-side') === 'right' ? 'right' : 'left',
73
85
  )
86
+ if (text.textContent !== label) text.textContent = label
74
87
  })
75
88
  })
76
89
  }
@@ -78,24 +91,38 @@ function formatCompactLabels(svg: SVGSVGElement) {
78
91
  /** Apply after axes and grid are in the DOM; never feed the filtered set back into layout. */
79
92
  export function updateMaximumTickVisibility(svg: SVGSVGElement) {
80
93
  svg.querySelectorAll('[data-maximum-hidden]').forEach((el) => {
81
- el.removeAttribute('display')
82
- el.removeAttribute('data-maximum-hidden')
94
+ updateSvgAttribute(el, 'display', null)
95
+ updateSvgAttribute(el, 'data-maximum-hidden', null)
83
96
  })
84
97
  svg.querySelectorAll('.waveform-track__axis--y .tick text').forEach((text) => {
85
- text.removeAttribute('display')
98
+ updateSvgAttribute(text, 'display', null)
86
99
  })
87
100
  formatCompactLabels(svg)
101
+ const boxes = new Map<SVGTextElement, Box>()
102
+ const widths = new Map<Element, number>()
103
+ const box = (text: SVGTextElement) => {
104
+ if (!boxes.has(text)) boxes.set(text, textBox(text))
105
+ return boxes.get(text)!
106
+ }
107
+ const width = (element: Element) => {
108
+ if (!widths.has(element)) widths.set(element, strokeWidth(element))
109
+ return widths.get(element)!
110
+ }
88
111
  const minimumLabels = [...svg.querySelectorAll<SVGTextElement>('text[data-axis-minimum-label]')]
89
112
  svg.querySelectorAll<SVGTextElement>('text[data-axis-maximum-label]').forEach((maximum) => {
90
- const box = textBox(maximum)
91
- if (minimumLabels.some((minimum) => labelsOverlap(box, textBox(minimum)))) hide(maximum)
113
+ const maximumBox = box(maximum)
114
+ if (minimumLabels.some((minimum) => labelsOverlap(maximumBox, box(minimum)))) hide(maximum)
92
115
  })
93
116
  svg.querySelectorAll<SVGGElement>('.waveform-track__axis--y .tick').forEach((tick) => {
94
117
  const position = Number(tick.getAttribute('data-tick-y'))
95
118
  const height = Number(tick.closest('.waveform-track')?.getAttribute('data-track-height'))
96
119
  const line = tick.querySelector('line')
97
- if (sameTick(position, 0) || sameTick(position, height)) line?.setAttribute('display', 'none')
98
- else line?.removeAttribute('display')
120
+ if (line)
121
+ updateSvgAttribute(
122
+ line,
123
+ 'display',
124
+ sameTick(position, 0) || sameTick(position, height) ? 'none' : null,
125
+ )
99
126
  })
100
127
  const minimumTicks = [...svg.querySelectorAll<SVGGElement>('.tick[data-minimum-tick]')]
101
128
  const maximumTicks = [...svg.querySelectorAll<SVGGElement>('.tick[data-maximum-tick]')]
@@ -105,7 +132,7 @@ export function updateMaximumTickVisibility(svg: SVGSVGElement) {
105
132
  if (!text) return
106
133
  maximumTicks.forEach((maximum) => {
107
134
  const other = maximum.querySelector('text')
108
- if (other && other !== text && labelsOverlap(textBox(text), textBox(other))) hide(other)
135
+ if (other && other !== text && labelsOverlap(box(text), box(other))) hide(other)
109
136
  })
110
137
  })
111
138
  const extremeTicks = [...new Set([...minimumTicks, ...maximumTicks])]
@@ -113,14 +140,14 @@ export function updateMaximumTickVisibility(svg: SVGSVGElement) {
113
140
  const text = tick.querySelector('text')
114
141
  const line = tick.querySelector('line')
115
142
  if (!text || !line) return
116
- const box = textBox(text)
143
+ const textBounds = box(text)
117
144
  labels.forEach((other) => {
118
145
  if (text.getAttribute('display') === 'none') return
119
146
  if (other.getAttribute('display') === 'none') return
120
147
  if (other.parentElement?.matches('[data-minimum-tick], [data-maximum-tick]')) return
121
148
  if (!tick.hasAttribute('data-minimum-tick') && other.hasAttribute('data-axis-minimum-label'))
122
149
  return
123
- if (labelsOverlap(box, textBox(other))) hide(other)
150
+ if (labelsOverlap(textBounds, box(other))) hide(other)
124
151
  })
125
152
  const axis = tick.parentElement!
126
153
  const value = Number(tick.getAttribute('data-tick-value'))
@@ -134,8 +161,8 @@ export function updateMaximumTickVisibility(svg: SVGSVGElement) {
134
161
  linesOverlap(
135
162
  position,
136
163
  Number(other.getAttribute('data-tick-y')),
137
- strokeWidth(line),
138
- strokeWidth(otherLine),
164
+ width(line),
165
+ width(otherLine),
139
166
  )
140
167
  )
141
168
  hide(otherLine)
@@ -145,12 +172,12 @@ export function updateMaximumTickVisibility(svg: SVGSVGElement) {
145
172
  const track = axis.closest('.waveform-track')!
146
173
  track.querySelectorAll('[data-grid-direction="horizontal"]').forEach((gridLine) => {
147
174
  const gridY = Number(gridLine.getAttribute('y1'))
148
- if (linesOverlap(position, gridY, strokeWidth(line), strokeWidth(gridLine))) hide(gridLine)
175
+ if (linesOverlap(position, gridY, width(line), width(gridLine))) hide(gridLine)
149
176
  })
150
177
  if (tick.hasAttribute('data-maximum-tick'))
151
- tick.setAttribute('data-maximum-value', String(value))
178
+ updateSvgAttribute(tick, 'data-maximum-value', value)
152
179
  if (tick.hasAttribute('data-minimum-tick'))
153
- tick.setAttribute('data-minimum-value', String(value))
180
+ updateSvgAttribute(tick, 'data-minimum-value', value)
154
181
  })
155
182
  formatCompactLabels(svg)
156
183
  }
@@ -0,0 +1,7 @@
1
+ import type { ComponentPublicInstance, Ref } from 'vue'
2
+ export function assignElement<T extends Element>(
3
+ target: Ref<T | undefined>,
4
+ element: Element | ComponentPublicInstance | null,
5
+ ) {
6
+ target.value = element instanceof Element ? (element as T) : undefined
7
+ }
package/src/core/data.ts CHANGED
@@ -215,10 +215,11 @@ function normalizedSeriesOptions(
215
215
  /** Internal normalized series which preserves compact TypedArray storage for chart rendering. */
216
216
  export function normalizeWaveformSeriesSources(
217
217
  data: WaveformData,
218
+ retainEmpty = false,
218
219
  ): NormalizedWaveformSourceSeries[] {
219
220
  if (data.kind !== 'series') {
220
221
  const source = createWaveformPointSource(data, normalizeWaveformData)
221
- return source.length
222
+ return source.length || retainEmpty
222
223
  ? [
223
224
  {
224
225
  ...normalizedSeriesOptions('series-0', { name: '' }),
@@ -239,7 +240,9 @@ export function normalizeWaveformSeriesSources(
239
240
  }
240
241
  usedIds.add(uniqueId)
241
242
  const source = createWaveformPointSource(series.data, normalizeWaveformData)
242
- return source.length ? [{ ...normalizedSeriesOptions(uniqueId, series), source }] : []
243
+ return source.length || retainEmpty
244
+ ? [{ ...normalizedSeriesOptions(uniqueId, series), source }]
245
+ : []
243
246
  })
244
247
  }
245
248
 
@@ -1,6 +1,5 @@
1
1
  import type { WaveformData, WaveformPoint, WaveformSeries } from '../types'
2
2
  import { recordedDampedPoints } from './recordedDampedWaveform'
3
- import { sx2Channel } from './sx2Channel'
4
3
 
5
4
  const POINT_COUNT = 1_000
6
5
  const START_TIME = -5
@@ -177,15 +176,6 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
177
176
  pointType: 'none',
178
177
  signal: (time) => 6e-9 * Math.exp(-(time + 5) * 0.2) * Math.cos(TWO_PI * 0.8 * time),
179
178
  },
180
- {
181
- id: `recorded-${sx2Channel.chnl_id}`,
182
- shotNo: String(sx2Channel.shot),
183
- name: sx2Channel.chnl,
184
- unit: sx2Channel.dat_unit,
185
- lineType: 'linear',
186
- pointType: 'none',
187
- signal: sx2Channel.data.map((value, index) => [sx2Channel.time[index]!, value]),
188
- },
189
179
  ]
190
180
 
191
181
  export function createSimulatedWaveformData(trackCount?: number): WaveformData {
@@ -1,15 +1,20 @@
1
1
  <script setup lang="ts">
2
- import { ref } from 'vue'
2
+ import { computed, ref } from 'vue'
3
3
 
4
4
  import { WaveformChart } from '../components'
5
5
  import type { DemoChartModel } from './types'
6
+ import { applyXDomainStrategy } from '../components/core/xDomain'
6
7
 
7
8
  const model = defineModel<DemoChartModel>('model', { required: true })
9
+ const xDomainStrategy = { type: 'integer-ms' as const, includeExplicit: true }
10
+ const panXDomain = computed(() =>
11
+ applyXDomainStrategy(model.value.recordDomain, xDomainStrategy, true),
12
+ )
8
13
 
9
14
  const waveformChartRef = ref<{ resetViewport: (trackIndex?: number) => void }>()
10
15
 
11
- function resetViewport() {
12
- waveformChartRef.value?.resetViewport()
16
+ function resetViewport(trackIndex?: number) {
17
+ waveformChartRef.value?.resetViewport(trackIndex)
13
18
  }
14
19
 
15
20
  defineExpose({ resetViewport })
@@ -17,6 +22,17 @@ defineExpose({ resetViewport })
17
22
 
18
23
  <template>
19
24
  <section class="chart-panel">
25
+ <div
26
+ v-if="model.loading || model.errors.length"
27
+ class="remote-window-status"
28
+ aria-live="polite"
29
+ >
30
+ <span v-if="model.loading" role="status">正在加载当前时间范围…</span>
31
+ <template v-if="model.errors.length">
32
+ <span role="alert">{{ model.errors.join(';') }}</span>
33
+ <button type="button" :disabled="model.loading" @click="model.retry()">重试</button>
34
+ </template>
35
+ </div>
20
36
  <WaveformChart
21
37
  ref="waveformChartRef"
22
38
  layout-preset="edge-compact"
@@ -25,15 +41,19 @@ defineExpose({ resetViewport })
25
41
  v-model:hidden-series-ids="model.hiddenSeriesIds"
26
42
  :data="model.data"
27
43
  :min-visible-points="2"
44
+ :min-zoom-span="model.minZoomSpan"
45
+ :max-zoom-scale="null"
46
+ :pan-x-domain="panXDomain"
28
47
  :integer-zoom="model.integerZoom"
29
48
  :initial-x-domain="model.initialXDomain"
30
- :x-domain-strategy="{ type: 'integer-ms', includeExplicit: true }"
49
+ :x-domain-strategy="xDomainStrategy"
31
50
  :display-mode="model.displayMode"
32
51
  :overlay-mode="model.overlayMode"
33
52
  :grid="{
34
53
  rowCount: model.rowCount,
35
54
  columnCount: model.columnCount,
36
55
  showPagination: true,
56
+ fillIncompleteLastRow: true,
37
57
  trackLines: model.gridTrackLines,
38
58
  }"
39
59
  :title="model.title"
@@ -55,8 +75,33 @@ defineExpose({ resetViewport })
55
75
  v-model:interaction-mode="model.interactionMode"
56
76
  :toolbar="model.toolbarVisible"
57
77
  pannable
78
+ @zoom-intent="model.handleZoomIntent"
79
+ @zoom-change="model.handleZoomChange"
58
80
  @zoom-end="model.handleZoomEnd"
81
+ @pan-end="model.handlePanEnd"
59
82
  @zoom-reset="model.resetWaveformViewport"
60
83
  />
61
84
  </section>
62
85
  </template>
86
+
87
+ <style scoped>
88
+ .chart-panel {
89
+ position: relative;
90
+ }
91
+
92
+ .remote-window-status {
93
+ position: absolute;
94
+ top: 6px;
95
+ right: 12px;
96
+ z-index: 2;
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 8px;
100
+ max-width: calc(100% - 24px);
101
+ padding: 6px 10px;
102
+ border: 1px solid #d0d5dd;
103
+ border-radius: 4px;
104
+ background: #fff;
105
+ color: #344054;
106
+ }
107
+ </style>
@@ -0,0 +1,114 @@
1
+ import type { InjectionKey } from 'vue'
2
+ import { normalizeWaveformSeries } from '../core'
3
+ import { normalizeWaveformSeriesSources } from '../core/data'
4
+ import type { WaveformData } from '../types'
5
+
6
+ export interface RemoteWindowRequest {
7
+ start: number
8
+ end: number
9
+ seriesIds?: string[]
10
+ }
11
+
12
+ export type RemoteWindowLoader = (
13
+ request: RemoteWindowRequest,
14
+ signal: AbortSignal,
15
+ ) => Promise<WaveformData>
16
+
17
+ export const remoteWindowLoaderKey: InjectionKey<RemoteWindowLoader> = Symbol('remoteWindowLoader')
18
+
19
+ export interface MockWindowOptions {
20
+ delayMs?: number | ((request: RemoteWindowRequest) => number)
21
+ empty?: boolean | ((request: RemoteWindowRequest) => boolean)
22
+ fail?: boolean | ((request: RemoteWindowRequest) => boolean)
23
+ }
24
+
25
+ /** Preserve series identities even when a window has no samples. */
26
+ export function selectWaveformWindow(
27
+ data: WaveformData,
28
+ request: RemoteWindowRequest,
29
+ empty = false,
30
+ ): WaveformData {
31
+ const series = normalizeWaveformSeriesSources(data, true)
32
+ const selected = request.seriesIds ? new Set(request.seriesIds) : undefined
33
+ const windows = series
34
+ .filter((item) => !selected || selected.has(item.id))
35
+ .map(({ source, ...item }) => {
36
+ const range = source.visibleRange([request.start, request.end])
37
+ return {
38
+ ...item,
39
+ data: {
40
+ kind: 'points' as const,
41
+ points: empty ? [] : source.pointsInRange(range.start, range.end),
42
+ },
43
+ }
44
+ })
45
+ return data.kind === 'series'
46
+ ? { kind: 'series', series: windows }
47
+ : (windows[0]?.data ?? { kind: 'points', points: [] })
48
+ }
49
+
50
+ export function mergeWaveformWindow(
51
+ current: WaveformData,
52
+ response: WaveformData,
53
+ seriesIds?: string[],
54
+ ): WaveformData {
55
+ if (!seriesIds || current.kind !== 'series' || response.kind !== 'series') return response
56
+ const selected = new Set(seriesIds)
57
+ const byId = new Map(response.series.map((series) => [series.id, series]))
58
+ return {
59
+ kind: 'series',
60
+ series: current.series.map((series) =>
61
+ series.id && selected.has(series.id) ? (byId.get(series.id) ?? series) : series,
62
+ ),
63
+ }
64
+ }
65
+
66
+ /** Metadata is derived from the complete record, never from a loaded window. */
67
+ export function waveformRecordMetadata(data: WaveformData) {
68
+ let minimum = Infinity
69
+ let maximum = -Infinity
70
+ let minZoomSpan = Infinity
71
+ for (const series of normalizeWaveformSeries(data)) {
72
+ for (let index = 0; index < series.points.length; index++) {
73
+ const x = series.points[index].x
74
+ minimum = Math.min(minimum, x)
75
+ maximum = Math.max(maximum, x)
76
+ const previous = series.points[index - 1]?.x
77
+ if (previous !== undefined && x > previous) {
78
+ minZoomSpan = Math.min(minZoomSpan, x - previous)
79
+ }
80
+ }
81
+ }
82
+ return {
83
+ domain: (minimum < maximum ? [minimum, maximum] : [0, 1]) as [number, number],
84
+ minZoomSpan: Number.isFinite(minZoomSpan) ? minZoomSpan : undefined,
85
+ }
86
+ }
87
+
88
+ export function createMockWindowLoader(
89
+ source: () => WaveformData,
90
+ options: MockWindowOptions = {},
91
+ ): RemoteWindowLoader {
92
+ return (request, signal) =>
93
+ new Promise((resolve, reject) => {
94
+ const abort = () => {
95
+ clearTimeout(timer)
96
+ signal.removeEventListener('abort', abort)
97
+ reject(new DOMException('Request cancelled', 'AbortError'))
98
+ }
99
+ const delay =
100
+ typeof options.delayMs === 'function' ? options.delayMs(request) : options.delayMs
101
+ const timer = setTimeout(() => {
102
+ signal.removeEventListener('abort', abort)
103
+ const fail = typeof options.fail === 'function' ? options.fail(request) : options.fail
104
+ if (fail) {
105
+ reject(new Error('模拟数据加载失败'))
106
+ return
107
+ }
108
+ const empty = typeof options.empty === 'function' ? options.empty(request) : options.empty
109
+ resolve(selectWaveformWindow(source(), request, empty))
110
+ }, delay ?? 80)
111
+ signal.addEventListener('abort', abort, { once: true })
112
+ if (signal.aborted) abort()
113
+ })
114
+ }
package/src/demo/types.ts CHANGED
@@ -14,7 +14,9 @@ import type {
14
14
  WaveformTitleOptions,
15
15
  WaveformZeroLineOptions,
16
16
  WaveformZoomEndPayload,
17
+ WaveformZoomIntentPayload,
17
18
  } from '../components'
19
+ import type { WaveformZoomResetPayload, WaveformPanEndPayload } from '../types'
18
20
  import type { DemoXAxisLabelFormat, DemoXAxisLabelTimeZone } from './useDemoXAxisLabelControls'
19
21
 
20
22
  interface SelectOption<T> {
@@ -97,6 +99,11 @@ export interface DemoChartModel {
97
99
  toolbarVisible: boolean
98
100
  data: WaveformData
99
101
  initialXDomain?: [number, number]
102
+ recordDomain: [number, number]
103
+ minZoomSpan?: number
104
+ loading: boolean
105
+ errors: string[]
106
+ retry: () => Promise<void[]>
100
107
  displayMode: WaveformDisplayMode
101
108
  overlayMode: WaveformOverlayMode
102
109
  rowCount: number
@@ -120,5 +127,8 @@ export interface DemoChartModel {
120
127
  interactionMode: WaveformInteractionMode
121
128
  hiddenSeriesIds: string[]
122
129
  handleZoomEnd: (payload: WaveformZoomEndPayload) => void | Promise<void>
123
- resetWaveformViewport: () => void
130
+ handleZoomIntent: (payload: WaveformZoomIntentPayload) => void
131
+ handleZoomChange: (domain: [number, number]) => void
132
+ handlePanEnd: (payload: WaveformPanEndPayload) => void | Promise<void>
133
+ resetWaveformViewport: (payload?: WaveformZoomResetPayload) => void | Promise<void>
124
134
  }