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,5 @@
1
+ import { pointSourceFromPoints, type WaveformPointSource } from './waveformPointSource'
2
+ import { withLineBoundaries } from './lineBoundary'
1
3
  import { bisector } from 'd3'
2
4
 
3
5
  import type { WaveformPoint } from '../types'
@@ -20,6 +22,7 @@ const acceptAllPoints = () => true
20
22
 
21
23
  interface PointSeriesSource {
22
24
  points: WaveformPoint[]
25
+ source?: WaveformPointSource
23
26
  }
24
27
 
25
28
  interface SeriesRenderSelectionOptions {
@@ -58,9 +61,10 @@ export function hasMinimumVisibleXValues(
58
61
  const required = Math.ceil(minimum)
59
62
  const xValues = new Set<number>()
60
63
  for (const series of seriesList) {
61
- const range = resolveVisiblePointRange(series.points, domain)
64
+ const source = series.source ?? pointSourceFromPoints(series.points)
65
+ const range = source.visibleRange(domain)
62
66
  for (let index = range.start; index < range.end; index += 1) {
63
- xValues.add(series.points[index].x)
67
+ xValues.add(source.pointAt(index)!.x)
64
68
  if (xValues.size >= required) return true
65
69
  }
66
70
  }
@@ -68,18 +72,20 @@ export function hasMinimumVisibleXValues(
68
72
  }
69
73
 
70
74
  function selectRenderablePointsInRange(
71
- points: WaveformPoint[],
75
+ points: WaveformPointSource,
72
76
  range: VisiblePointRange,
73
77
  domain: [number, number],
74
78
  width: number,
75
79
  options: ResolvedWaveformRenderingOptions,
76
80
  ): WaveformPoint[] {
81
+ if (range.start === range.end && (range.start === 0 || range.end === points.length)) return []
77
82
  const start = Math.max(0, range.start - 1)
78
83
  const end = Math.min(points.length, range.end + 1)
79
84
  const visibleCount = end - start
80
85
  if (visibleCount <= 0) return []
81
86
  return resolveRenderablePointSelectionStrategy({ visibleCount, width, options })({
82
- points,
87
+ points: [],
88
+ source: points,
83
89
  range,
84
90
  domain,
85
91
  width,
@@ -96,25 +102,25 @@ function shouldDeferLineSampling(
96
102
  }
97
103
 
98
104
  function selectSamplingPlaceholderPoints(
99
- points: WaveformPoint[],
105
+ points: WaveformPointSource,
100
106
  range: VisiblePointRange,
101
107
  width: number,
102
108
  options: ResolvedWaveformRenderingOptions,
103
109
  ): WaveformPoint[] {
104
- const start = Math.max(0, range.start - 1)
105
- const end = Math.min(points.length, range.end + 1)
110
+ const { start, end } = range
106
111
  const count = end - start
107
- if (count <= 0) return []
108
- if (options.sampling.strategy === 'none') return points.slice(start, end)
109
112
  const target =
110
113
  options.sampling.maxPointCount ??
111
114
  Math.max(1, Math.floor(width * options.sampling.maxPointsPerPixel))
112
- if (count <= target) return points.slice(start, end)
113
- if (target === 1) return [points[start]!]
114
- return Array.from({ length: target }, (_, index) => {
115
- const offset = Math.round((index * (count - 1)) / (target - 1))
116
- return points[start + offset]!
117
- })
115
+ const interior =
116
+ count <= target || options.sampling.strategy === 'none'
117
+ ? points.pointsInRange(start, end)
118
+ : Array.from({ length: target }, (_, index) =>
119
+ points.pointAt(
120
+ start + (target === 1 ? 0 : Math.round((index * (count - 1)) / (target - 1))),
121
+ )!,
122
+ )
123
+ return withLineBoundaries(interior, range, points.length, (index) => points.pointAt(index)!)
118
124
  }
119
125
 
120
126
  /**
@@ -129,7 +135,7 @@ export function selectRenderablePoints(
129
135
  ): WaveformPoint[] {
130
136
  if (!points.length || width <= 0) return []
131
137
  return selectRenderablePointsInRange(
132
- points,
138
+ pointSourceFromPoints(points),
133
139
  resolveVisiblePointRange(points, domain),
134
140
  domain,
135
141
  width,
@@ -138,7 +144,7 @@ export function selectRenderablePoints(
138
144
  }
139
145
 
140
146
  function selectDecorationPointsInRange(
141
- points: WaveformPoint[],
147
+ points: WaveformPointSource,
142
148
  range: VisiblePointRange,
143
149
  domain: [number, number],
144
150
  width: number,
@@ -148,10 +154,10 @@ function selectDecorationPointsInRange(
148
154
  priorityPredicate?: (point: WaveformPoint) => boolean,
149
155
  ): WaveformPoint[] {
150
156
  if (!downsample || minSpacing === 0) {
151
- if (predicate === acceptAllPoints) return points.slice(range.start, range.end)
157
+ if (predicate === acceptAllPoints) return points.pointsInRange(range.start, range.end)
152
158
  const visiblePoints: WaveformPoint[] = []
153
159
  for (let index = range.start; index < range.end; index += 1) {
154
- if (predicate(points[index])) visiblePoints.push(points[index])
160
+ if (predicate(points.pointAt(index)!)) visiblePoints.push(points.pointAt(index)!)
155
161
  }
156
162
  return visiblePoints
157
163
  }
@@ -161,7 +167,7 @@ function selectDecorationPointsInRange(
161
167
  const span = domainEnd - domainStart
162
168
  if (span <= 0) {
163
169
  for (let index = range.start; index < range.end; index += 1) {
164
- if (predicate(points[index])) return [points[index]]
170
+ if (predicate(points.pointAt(index)!)) return [points.pointAt(index)!]
165
171
  }
166
172
  return []
167
173
  }
@@ -208,7 +214,7 @@ function selectDecorationPointsInRange(
208
214
  }
209
215
 
210
216
  for (let index = range.start; index < range.end; index += 1) {
211
- const point = points[index]
217
+ const point = points.pointAt(index)!
212
218
  if (!predicate(point)) continue
213
219
  first ??= point
214
220
  last = point
@@ -252,7 +258,7 @@ export function selectDecorationPoints(
252
258
  ): WaveformPoint[] {
253
259
  if (!points.length || width <= 0) return []
254
260
  return selectDecorationPointsInRange(
255
- points,
261
+ pointSourceFromPoints(points),
256
262
  resolveVisiblePointRange(points, domain),
257
263
  domain,
258
264
  width,
@@ -268,8 +274,8 @@ function hasPointError(point: WaveformPoint): boolean {
268
274
  return lower !== 0 || upper !== 0
269
275
  }
270
276
 
271
- export function selectSeriesRenderPoints(
272
- points: WaveformPoint[],
277
+ export function selectSeriesRenderSourcePoints(
278
+ points: WaveformPointSource,
273
279
  domain: [number, number],
274
280
  width: number,
275
281
  rendering: ResolvedWaveformRenderingOptions,
@@ -278,7 +284,7 @@ export function selectSeriesRenderPoints(
278
284
  if (!points.length || width <= 0) {
279
285
  return { linePoints: [], pointRenderPoints: [], errorBarRenderPoints: [] }
280
286
  }
281
- const range = resolveVisiblePointRange(points, domain)
287
+ const range = points.visibleRange(domain)
282
288
  const linePoints = selection.lineVisible
283
289
  ? (selection.linePointOverride ??
284
290
  (shouldDeferLineSampling(range.end - range.start, rendering)
@@ -329,3 +335,20 @@ export function selectSeriesRenderPoints(
329
335
  : [],
330
336
  }
331
337
  }
338
+
339
+ /** Public array adapter; internal layout passes its normalized source directly. */
340
+ export function selectSeriesRenderPoints(
341
+ points: WaveformPoint[],
342
+ domain: [number, number],
343
+ width: number,
344
+ rendering: ResolvedWaveformRenderingOptions,
345
+ selection: SeriesRenderSelectionOptions,
346
+ ): SeriesRenderPointSelection {
347
+ return selectSeriesRenderSourcePoints(
348
+ pointSourceFromPoints(points),
349
+ domain,
350
+ width,
351
+ rendering,
352
+ selection,
353
+ )
354
+ }
@@ -1,3 +1,4 @@
1
+ import { pointSourceFromPoints, type WaveformPointSource } from './waveformPointSource'
1
2
  import type { WaveformPoint } from '../types'
2
3
  import type { ResolvedWaveformRenderingOptions } from './renderingOptions'
3
4
 
@@ -7,6 +8,7 @@ export interface VisiblePointRange {
7
8
  }
8
9
 
9
10
  export interface RenderablePointSelectionContext {
11
+ source?: WaveformPointSource
10
12
  points: WaveformPoint[]
11
13
  range: VisiblePointRange
12
14
  domain: [number, number]
@@ -25,17 +27,15 @@ function selectionBounds(range: VisiblePointRange, pointCount: number) {
25
27
  }
26
28
  }
27
29
 
28
- function pushUniquePoint(target: WaveformPoint[], point: WaveformPoint | undefined) {
29
- if (point && target[target.length - 1] !== point) target.push(point)
30
- }
31
-
32
30
  export const completePointSelectionStrategy: RenderablePointSelectionStrategy = (context) => {
33
- const { start, end } = selectionBounds(context.range, context.points.length)
34
- return context.points.slice(start, end)
31
+ const source = context.source ?? pointSourceFromPoints(context.points)
32
+ const { start, end } = selectionBounds(context.range, source.length)
33
+ return source.pointsInRange(start, end)
35
34
  }
36
35
 
37
36
  export const peakPreservingPointSelectionStrategy: RenderablePointSelectionStrategy = (context) => {
38
- const { points, range, domain, width, options } = context
37
+ const { range, domain, width, options } = context
38
+ const points = context.source ?? pointSourceFromPoints(context.points)
39
39
  const { start, end } = selectionBounds(range, points.length)
40
40
  const visibleCount = end - start
41
41
  if (visibleCount <= 0) return []
@@ -48,6 +48,12 @@ export const peakPreservingPointSelectionStrategy: RenderablePointSelectionStrat
48
48
  )
49
49
  const bucketCount = Math.max(1, Math.floor(maximumPointCount / 4))
50
50
  const result: WaveformPoint[] = []
51
+ let lastSelectedIndex = -1
52
+ const pushIndex = (index: number) => {
53
+ if (index < 0 || index >= points.length || index === lastSelectedIndex) return
54
+ result.push(points.pointAt(index)!)
55
+ lastSelectedIndex = index
56
+ }
51
57
  const span = domainEnd - domainStart || 1
52
58
  const bucketIndexes = Array.from({ length: 4 }, () => -1)
53
59
  let activeBucket = -1
@@ -82,13 +88,13 @@ export const peakPreservingPointSelectionStrategy: RenderablePointSelectionStrat
82
88
  bucketIndexes[position + 1] = value
83
89
  }
84
90
  for (let index = 0; index < count; index += 1) {
85
- pushUniquePoint(result, points[bucketIndexes[index]])
91
+ pushIndex(bucketIndexes[index])
86
92
  }
87
93
  }
88
94
 
89
- pushUniquePoint(result, points[start])
95
+ pushIndex(start)
90
96
  for (let index = range.start; index < range.end; index += 1) {
91
- const point = points[index]
97
+ const point = points.pointAt(index)!
92
98
  const bucket = Math.min(
93
99
  bucketCount - 1,
94
100
  Math.max(0, Math.floor(((point.x - domainStart) / span) * bucketCount)),
@@ -103,11 +109,11 @@ export const peakPreservingPointSelectionStrategy: RenderablePointSelectionStrat
103
109
  continue
104
110
  }
105
111
  lastIndex = index
106
- if (point.y < points[minimumIndex].y) minimumIndex = index
107
- if (point.y > points[maximumIndex].y) maximumIndex = index
112
+ if (point.y < points.pointAt(minimumIndex)!.y) minimumIndex = index
113
+ if (point.y > points.pointAt(maximumIndex)!.y) maximumIndex = index
108
114
  }
109
115
  flushBucket()
110
- pushUniquePoint(result, points[end - 1])
116
+ pushIndex(end - 1)
111
117
  return result
112
118
  }
113
119
 
@@ -288,6 +288,26 @@ function compactSamples(data: Extract<SingleWaveformData, { kind: 'typed-samples
288
288
  function compactPoints(data: Extract<SingleWaveformData, { kind: 'typed-points' }>) {
289
289
  if (!typedPointsAreValid(data))
290
290
  return new CompactWaveformPointSource(new Float64Array(), new Float64Array())
291
+ // Validate once before the common dense/sorted path; always take private copies.
292
+ let denseOrdered = true
293
+ for (let index = 0; index < data.x.length; index += 1) {
294
+ if (
295
+ !Number.isFinite(data.x[index]) ||
296
+ !Number.isFinite(data.y[index]) ||
297
+ (index > 0 && data.x[index]! < data.x[index - 1]!)
298
+ ) {
299
+ denseOrdered = false
300
+ break
301
+ }
302
+ }
303
+ if (denseOrdered)
304
+ return new CompactWaveformPointSource(
305
+ data.x.slice(),
306
+ data.y.slice(),
307
+ data.error?.slice(),
308
+ data.lowerError?.slice(),
309
+ data.upperError?.slice(),
310
+ )
291
311
  const indexes: number[] = []
292
312
  let ordered = true
293
313
  let previousX = Number.NEGATIVE_INFINITY
@@ -356,11 +376,13 @@ export function lazyPointArray(source: WaveformPointSource): WaveformPoint[] {
356
376
  }
357
377
  return point
358
378
  }
359
- return new Proxy(Array.from<WaveformPoint>({ length: source.length }), {
379
+ // Keep the compatibility facade sparse: no point-sized backing array is allocated for compact data.
380
+ return new Proxy<WaveformPoint[]>([], {
360
381
  get(target, property, receiver) {
361
382
  if (typeof property === 'string' && /^(0|[1-9]\d*)$/.test(property)) {
362
383
  return valueAt(Number(property))
363
384
  }
385
+ if (property === 'length') return source.length
364
386
  return Reflect.get(target, property, receiver)
365
387
  },
366
388
  has(target, property) {
@@ -0,0 +1,32 @@
1
+ import type { WaveformPointRange } from '../waveformPointSource'
2
+ import type { WorkerSamplingOutput } from './protocol'
3
+ import { datasetLength, datasetXAt, type StoredDataset } from './repositoryDataset'
4
+
5
+ export function addOutputBoundaries(
6
+ output: WorkerSamplingOutput,
7
+ range: WaveformPointRange,
8
+ dataset: StoredDataset,
9
+ ): WorkerSamplingOutput {
10
+ const length = datasetLength(dataset)
11
+ if (range.start === range.end && (range.start === 0 || range.end === length)) return output
12
+ const prefix = range.start > 0 ? [range.start - 1] : []
13
+ const suffix = range.end < length ? [range.end] : []
14
+ if (output.kind === 'source-indexes') {
15
+ const values = Array.from(output.sourceIndexes)
16
+ return {
17
+ kind: 'source-indexes',
18
+ sourceIndexes: Uint32Array.from([...prefix, ...values, ...suffix]),
19
+ }
20
+ }
21
+ const x = Array.from(output.x)
22
+ const y = Array.from(output.y)
23
+ if (prefix.length) {
24
+ x.unshift(datasetXAt(dataset, prefix[0]!))
25
+ y.unshift(dataset.y[prefix[0]!]!)
26
+ }
27
+ if (suffix.length) {
28
+ x.push(datasetXAt(dataset, suffix[0]!))
29
+ y.push(dataset.y[suffix[0]!]!)
30
+ }
31
+ return { kind: 'aggregates', x: Float64Array.from(x), y: Float64Array.from(y) }
32
+ }
@@ -1,3 +1,4 @@
1
+ import { addOutputBoundaries } from './outputBoundaries'
1
2
  import type { WaveformPoint } from '../../types'
2
3
  import type { WorkerSamplingDataset } from '../waveformPointSource'
3
4
  import { javascriptSamplingBackend } from './javascriptBackend'
@@ -194,13 +195,17 @@ export class WorkerSamplingRepository {
194
195
  requestId,
195
196
  request,
196
197
  'ok',
197
- {
198
- kind: 'source-indexes',
199
- sourceIndexes: Uint32Array.from(
200
- { length: visiblePointCount },
201
- (_, index) => range.start + index,
202
- ),
203
- },
198
+ addOutputBoundaries(
199
+ {
200
+ kind: 'source-indexes',
201
+ sourceIndexes: Uint32Array.from(
202
+ { length: visiblePointCount },
203
+ (_, index) => range.start + index,
204
+ ),
205
+ },
206
+ range,
207
+ dataset,
208
+ ),
204
209
  'raw',
205
210
  'raw',
206
211
  visiblePointCount,
@@ -256,18 +261,21 @@ export class WorkerSamplingRepository {
256
261
  )
257
262
  : undefined
258
263
  : dataset.index?.sample(range.start, range.end, strategy, target)
259
- const output =
264
+ const output = addOutputBoundaries(
260
265
  indexed ??
261
- outputWithGlobalIndexes(
262
- backend.sample({
263
- x: datasetXRange(dataset, range.start, range.end),
264
- y: dataset.y.subarray(range.start, range.end),
265
- points: dataset.points?.slice(range.start, range.end) ?? [],
266
- strategy,
267
- targetPointCount: target,
268
- }),
269
- range.start,
270
- )
266
+ outputWithGlobalIndexes(
267
+ backend.sample({
268
+ x: datasetXRange(dataset, range.start, range.end),
269
+ y: dataset.y.subarray(range.start, range.end),
270
+ points: dataset.points?.slice(range.start, range.end) ?? [],
271
+ strategy,
272
+ targetPointCount: target,
273
+ }),
274
+ range.start,
275
+ ),
276
+ range,
277
+ dataset,
278
+ )
271
279
  this.cache.set(key, output)
272
280
  if (dataset.wasmDatasetHandle !== undefined && isWasmDatasetBackend(backend)) {
273
281
  dataset.wasmIndexBytes = backend.datasetIndexBytes(dataset.wasmDatasetHandle)
@@ -69,8 +69,16 @@ export function isWasmDatasetBackend(
69
69
  }
70
70
 
71
71
  function copyFiniteNumericValues(sourceX: Float64Array, sourceY: Float32Array | Float64Array) {
72
- const indexes: number[] = []
73
72
  const length = Math.min(sourceX.length, sourceY.length)
73
+ let denseOrdered = sourceX.length === sourceY.length
74
+ for (let index = 0; index < length && denseOrdered; index += 1) {
75
+ denseOrdered =
76
+ Number.isFinite(sourceX[index]) &&
77
+ Number.isFinite(sourceY[index]) &&
78
+ (index === 0 || sourceX[index]! >= sourceX[index - 1]!)
79
+ }
80
+ if (denseOrdered) return { x: sourceX.slice(), y: Float64Array.from(sourceY) }
81
+ const indexes: number[] = []
74
82
  for (let index = 0; index < length; index += 1) {
75
83
  if (Number.isFinite(sourceX[index]) && Number.isFinite(sourceY[index])) indexes.push(index)
76
84
  }
@@ -120,6 +128,30 @@ export function createStoredDataset(
120
128
  },
121
129
  }
122
130
  }
131
+ // Compact, already-valid sample messages need neither an index array nor a sort.
132
+ if (!numeric.sourceIndexes && numeric.values.every(Number.isFinite)) {
133
+ const y = Float64Array.from(numeric.values)
134
+ let min = Infinity
135
+ let max = -Infinity
136
+ for (const value of y) {
137
+ min = Math.min(min, value)
138
+ max = Math.max(max, value)
139
+ }
140
+ return {
141
+ y,
142
+ sampleStartTime: numeric.startTime,
143
+ sampleRate: numeric.sampleRate,
144
+ revision,
145
+ metrics: {
146
+ inputPointCount: y.length,
147
+ validPointCount: y.length,
148
+ xDomain: y.length
149
+ ? [numeric.startTime, numeric.startTime + (y.length - 1) / numeric.sampleRate]
150
+ : null,
151
+ yDomain: y.length ? [min, max] : null,
152
+ },
153
+ }
154
+ }
123
155
  const indexes = Array.from({ length: numeric.values.length }, (_, index) => index).filter(
124
156
  (index) => Number.isFinite(numeric.values[index]),
125
157
  )
@@ -31,10 +31,20 @@ function addWasmError(response: WorkerSamplingResponse): WorkerSamplingResponse
31
31
 
32
32
  const workerScope = globalThis as typeof globalThis & {
33
33
  onmessage: ((event: MessageEvent<WorkerSamplingRequest>) => void) | null
34
- postMessage: (message: WorkerSamplingResponse) => void
34
+ postMessage: (
35
+ message: WorkerSamplingResponse | { type: 'request-error'; requestId: number; message: string },
36
+ ) => void
35
37
  }
36
38
 
37
39
  workerScope.onmessage = async (event: MessageEvent<WorkerSamplingRequest>) => {
38
- if (requiresWasm(event.data)) await initializeWasmBackend()
39
- workerScope.postMessage(addWasmError(repository.handle(event.data)))
40
+ try {
41
+ if (requiresWasm(event.data)) await initializeWasmBackend()
42
+ workerScope.postMessage(addWasmError(repository.handle(event.data)))
43
+ } catch (error) {
44
+ workerScope.postMessage({
45
+ type: 'request-error',
46
+ requestId: event.data.requestId,
47
+ message: error instanceof Error ? error.message : String(error),
48
+ })
49
+ }
40
50
  }
@@ -140,14 +140,69 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
140
140
  signal: (time, noise) => 0.7 * Math.sin(TWO_PI * 0.45 * time) + noise * 0.36,
141
141
  errors: () => ({ error: 0.12 }),
142
142
  },
143
+ {
144
+ id: 'simulated-mega-voltage',
145
+ shotNo: '13300',
146
+ name: '百万量级电压',
147
+ unit: 'V',
148
+ lineType: 'linear',
149
+ pointType: 'none',
150
+ signal: (time) => 2.4e6 * Math.sin(TWO_PI * 0.6 * time),
151
+ },
152
+ {
153
+ id: 'simulated-micro-current',
154
+ shotNo: '13300',
155
+ name: '微量级电流',
156
+ unit: 'A',
157
+ lineType: 'linear',
158
+ pointType: 'none',
159
+ signal: (time, noise) => 3.2e-6 * Math.sin(TWO_PI * 0.45 * time) + noise * 2e-7,
160
+ },
161
+ {
162
+ id: 'simulated-giga-pressure',
163
+ shotNo: '13300',
164
+ name: '十亿量级压力脉冲',
165
+ unit: 'Pa',
166
+ lineType: 'linear',
167
+ pointType: 'none',
168
+ signal: (time) => 1.8e9 + 4e8 * Math.exp(-((time - 0.75) ** 2) / 0.4),
169
+ },
170
+ {
171
+ id: 'simulated-nano-displacement',
172
+ shotNo: '13300',
173
+ name: '纳量级阻尼位移',
174
+ unit: 'm',
175
+ lineType: 'linear',
176
+ pointType: 'none',
177
+ signal: (time) => 6e-9 * Math.exp(-(time + 5) * 0.2) * Math.cos(TWO_PI * 0.8 * time),
178
+ },
143
179
  ]
144
180
 
145
- export function createSimulatedWaveformData(): WaveformData {
181
+ export function createSimulatedWaveformData(trackCount?: number): WaveformData {
146
182
  const noise = createSeededNoise(0x5eed1234)
147
183
  const errorNoise = createSeededNoise(0xe220a11)
184
+ const definitions = [...seriesDefinitions]
185
+ const existingTrackCount = new Set(definitions.map((series) => series.trackId ?? series.id)).size
186
+ const extraTrackCount =
187
+ trackCount !== undefined && Number.isFinite(trackCount)
188
+ ? Math.max(0, Math.min(100, Math.floor(trackCount)) - existingTrackCount)
189
+ : 0
190
+ for (let index = 0; index < extraTrackCount; index += 1) {
191
+ const channel = existingTrackCount + index + 1
192
+ definitions.push({
193
+ id: `simulated-extra-${channel}`,
194
+ name: `模拟通道 ${channel}`,
195
+ unit: 'V',
196
+ lineType: 'linear',
197
+ pointType: 'none',
198
+ signal: (time) =>
199
+ (1 + index * 0.3) * Math.sin(TWO_PI * (0.2 + index * 0.15) * time + index) +
200
+ 0.2 * Math.cos(TWO_PI * 1.4 * time),
201
+ })
202
+ }
148
203
  return {
149
204
  kind: 'series',
150
- series: seriesDefinitions.map(({ signal, errors, minimumX, ...series }) => {
205
+ series: definitions.map(({ signal, errors, minimumX, ...series }) => {
151
206
  const points = createPoints(signal, noise, errors, errorNoise)
152
207
  return {
153
208
  ...series,
@@ -19,6 +19,8 @@ defineExpose({ resetViewport })
19
19
  <section class="chart-panel">
20
20
  <WaveformChart
21
21
  ref="waveformChartRef"
22
+ layout-preset="edge-compact"
23
+ unit-display-mode="channel-label"
22
24
  v-model:annotations="model.annotations"
23
25
  v-model:hidden-series-ids="model.hiddenSeriesIds"
24
26
  :data="model.data"
@@ -50,7 +52,9 @@ defineExpose({ resetViewport })
50
52
  :zero-line="model.zeroLine"
51
53
  :frame-number="model.frameWatermarkVisible ? 1 : undefined"
52
54
  :annotations-visible="model.annotationsVisible"
53
- :interaction-mode="model.interactionMode"
55
+ v-model:interaction-mode="model.interactionMode"
56
+ :toolbar="model.toolbarVisible"
57
+ pannable
54
58
  @zoom-end="model.handleZoomEnd"
55
59
  @zoom-reset="model.resetWaveformViewport"
56
60
  />
@@ -2,12 +2,14 @@
2
2
  import { InputNumber, Select, Switch } from 'ant-design-vue'
3
3
  import { ColorPicker } from 'vue3-colorpicker'
4
4
 
5
+ import DemoYAxisPaddingControls from './DemoYAxisPaddingControls.vue'
5
6
  import type { DemoControlPanelModel } from './types'
6
7
 
7
8
  const model = defineModel<DemoControlPanelModel>('model', { required: true })
8
9
  </script>
9
10
 
10
11
  <template>
12
+ <DemoYAxisPaddingControls v-model:model="model" />
11
13
  <section class="control-section">
12
14
  <h2>绘图区边距</h2>
13
15
  <div class="plot-margin-controls">
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { Button, InputNumber, Radio, Select, Switch } from 'ant-design-vue'
2
+ import { InputNumber, Radio, Select, Switch } from 'ant-design-vue'
3
3
 
4
4
  import type { DemoControlPanelModel } from './types'
5
5
 
@@ -36,7 +36,10 @@ const model = defineModel<DemoControlPanelModel>('model', { required: true })
36
36
  </section>
37
37
  <section class="control-section">
38
38
  <h2>视图</h2>
39
- <Button block aria-label="重置波形视图" @click="model.resetWaveformViewport"> 重置视图 </Button>
39
+ <label class="frame-style-control frame-style-control--switch">
40
+ <span>内置工具栏</span>
41
+ <Switch v-model:checked="model.toolbarVisible" size="small" aria-label="显示内置工具栏" />
42
+ </label>
40
43
  <div class="auxiliary-style-controls" style="margin-top: 10px">
41
44
  <label class="frame-style-control frame-style-control--switch">
42
45
  <span>整数缩放</span>
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ import { InputNumber, Switch } from 'ant-design-vue'
3
+ import type { DemoControlPanelModel } from './types'
4
+ const model = defineModel<DemoControlPanelModel>('model', { required: true })
5
+ </script>
6
+
7
+ <template>
8
+ <section class="control-section">
9
+ <h2>Y 轴留白</h2>
10
+ <div v-for="side in ['upper', 'lower'] as const" :key="side" class="y-axis-padding-control">
11
+ <span>{{ side === 'upper' ? '上方留白' : '下方留白' }}</span>
12
+ <Switch
13
+ v-model:checked="model[`${side}PaddingEnabled`]"
14
+ size="small"
15
+ :aria-label="side === 'upper' ? '上方留白' : '下方留白'"
16
+ />
17
+ <InputNumber
18
+ v-model:value="model[`${side}PaddingPercent`]"
19
+ :min="0"
20
+ :step="1"
21
+ :disabled="!model[`${side}PaddingEnabled`]"
22
+ addon-after="%"
23
+ size="small"
24
+ :aria-label="side === 'upper' ? '上方留白比例' : '下方留白比例'"
25
+ />
26
+ </div>
27
+ <small>仅作用于自动 Y 范围;刻度取整可能增加实际留白。</small>
28
+ </section>
29
+ </template>
30
+
31
+ <style scoped>
32
+ .y-axis-padding-control {
33
+ display: grid;
34
+ grid-template-columns: 60px 28px minmax(0, 1fr);
35
+ align-items: center;
36
+ gap: 8px;
37
+ margin-bottom: 10px;
38
+ font-size: 12px;
39
+ }
40
+ .y-axis-padding-control :deep(.ant-switch) {
41
+ justify-self: start;
42
+ }
43
+ .y-axis-padding-control :deep(.ant-input-number) {
44
+ width: 100%;
45
+ }
46
+ </style>