waveform-analysis 0.1.71 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +260 -40
  2. package/dist/assets/{workerRuntime-o5An0M-W.js → workerRuntime-5p-EI-GL.js} +1 -1
  3. package/dist/index.cjs +2 -1
  4. package/dist/index.js +3874 -2778
  5. package/dist/style.css +1 -1
  6. package/dist/types/components/WaveformChart.vue.d.ts +24 -6
  7. package/dist/types/components/annotation/WaveformAnnotationLayer.vue.d.ts +2 -0
  8. package/dist/types/components/annotation/useWaveformChartAnnotations.d.ts +0 -1
  9. package/dist/types/components/controls/WaveformToolbar.vue.d.ts +7 -0
  10. package/dist/types/components/controls/chartExportOptions.d.ts +5 -0
  11. package/dist/types/components/controls/chartSnapshot.d.ts +2 -0
  12. package/dist/types/components/controls/snapshotTitle.d.ts +2 -0
  13. package/dist/types/components/controls/toolbarRegistry.d.ts +21 -0
  14. package/dist/types/components/controls/useChartCommands.d.ts +42 -0
  15. package/dist/types/components/controls/useChartImageExport.d.ts +17 -0
  16. package/dist/types/components/controls/useControlMode.d.ts +8 -0
  17. package/dist/types/components/core/latestTaskScheduler.d.ts +1 -1
  18. package/dist/types/components/core/layout.d.ts +6 -3
  19. package/dist/types/components/core/niceScale.d.ts +34 -0
  20. package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -1
  21. package/dist/types/components/core/trackPagination.d.ts +1 -1
  22. package/dist/types/components/core/types.d.ts +5 -0
  23. package/dist/types/components/core/useWaveformChartController.d.ts +24 -6
  24. package/dist/types/components/core/useWaveformChartLifecycle.d.ts +2 -2
  25. package/dist/types/components/core/useWaveformData.d.ts +2 -0
  26. package/dist/types/components/core/useWaveformLayout.d.ts +4 -1
  27. package/dist/types/components/core/useWaveformMeasurements.d.ts +2 -0
  28. package/dist/types/components/core/useWaveformRenderSampling.d.ts +1 -0
  29. package/dist/types/components/core/useWaveformViewportRestore.d.ts +5 -0
  30. package/dist/types/components/core/waveformChartTypes.d.ts +5 -0
  31. package/dist/types/components/core/waveformSamplingSession.d.ts +24 -0
  32. package/dist/types/components/core/yAxisLabelStyle.d.ts +3 -0
  33. package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
  34. package/dist/types/components/core/yAxisPadding.d.ts +7 -0
  35. package/dist/types/components/core/yAxisTitleLayout.d.ts +2 -0
  36. package/dist/types/components/interaction/useExtremaTickViewport.d.ts +5 -0
  37. package/dist/types/components/interaction/useWaveformViewport.d.ts +1 -0
  38. package/dist/types/components/rendering/WaveformLegend.vue.d.ts +2 -2
  39. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +2 -3
  40. package/dist/types/components/rendering/WaveformTrackAxes.vue.d.ts +1 -1
  41. package/dist/types/components/rendering/WaveformTrackBackdrop.vue.d.ts +1 -0
  42. package/dist/types/components/rendering/compactFrame.d.ts +5 -0
  43. package/dist/types/components/rendering/yAxisTickVisibility.d.ts +11 -0
  44. package/dist/types/core/lineBoundary.d.ts +4 -0
  45. package/dist/types/core/rendering.d.ts +4 -0
  46. package/dist/types/core/renderingStrategies.d.ts +2 -0
  47. package/dist/types/core/workerSampling/outputBoundaries.d.ts +4 -0
  48. package/dist/types/data/simulatedWaveforms.d.ts +1 -1
  49. package/dist/types/data/sx2Channel.d.ts +10 -0
  50. package/dist/types/demo/DemoYAxisPaddingControls.vue.d.ts +11 -0
  51. package/dist/types/demo/types.d.ts +7 -0
  52. package/dist/types/index.d.ts +2 -1
  53. package/dist/types/types/chart.d.ts +32 -8
  54. package/dist/types/types/controls.d.ts +69 -0
  55. package/dist/types/types/index.d.ts +2 -1
  56. package/dist/types/utils/formatters.d.ts +1 -0
  57. package/package.json +7 -6
  58. package/src/App.vue +61 -14
  59. package/src/components/WaveformChart.css +10 -1
  60. package/src/components/WaveformChart.vue +6 -0
  61. package/src/components/WaveformChartView.vue +27 -26
  62. package/src/components/annotation/WaveformAnnotationLayer.vue +6 -1
  63. package/src/components/annotation/useWaveformChartAnnotations.ts +7 -18
  64. package/src/components/controls/WaveformToolbar.vue +247 -0
  65. package/src/components/controls/chartExportOptions.ts +32 -0
  66. package/src/components/controls/chartSnapshot.ts +132 -0
  67. package/src/components/controls/snapshotTitle.ts +73 -0
  68. package/src/components/controls/toolbarRegistry.ts +92 -0
  69. package/src/components/controls/useChartCommands.ts +294 -0
  70. package/src/components/controls/useChartImageExport.ts +155 -0
  71. package/src/components/controls/useControlMode.ts +30 -0
  72. package/src/components/core/latestTaskScheduler.ts +14 -2
  73. package/src/components/core/layout.ts +49 -36
  74. package/src/components/core/niceScale.ts +151 -0
  75. package/src/components/core/trackLayoutBuilder.ts +41 -17
  76. package/src/components/core/trackPagination.ts +3 -3
  77. package/src/components/core/types.ts +5 -0
  78. package/src/components/core/useWaveformChartController.ts +94 -3
  79. package/src/components/core/useWaveformChartLifecycle.ts +11 -111
  80. package/src/components/core/useWaveformData.ts +4 -0
  81. package/src/components/core/useWaveformLayout.ts +25 -5
  82. package/src/components/core/useWaveformMeasurements.ts +63 -0
  83. package/src/components/core/useWaveformRenderSampling.ts +80 -59
  84. package/src/components/core/useWaveformViewportRestore.ts +105 -0
  85. package/src/components/core/useWaveformYAxisLayout.ts +3 -2
  86. package/src/components/core/waveformChartTypes.ts +5 -0
  87. package/src/components/core/waveformSamplingSession.ts +49 -0
  88. package/src/components/core/workerSamplingClient.ts +38 -6
  89. package/src/components/core/yAxisLabelStyle.ts +10 -0
  90. package/src/components/core/yAxisLayoutMetrics.ts +11 -9
  91. package/src/components/core/yAxisPadding.ts +60 -0
  92. package/src/components/core/yAxisTitleLayout.ts +11 -12
  93. package/src/components/interaction/useExtremaTickViewport.ts +43 -0
  94. package/src/components/interaction/useWaveformViewport.ts +11 -2
  95. package/src/components/rendering/WaveformTrack.css +1 -2
  96. package/src/components/rendering/WaveformTrack.vue +5 -12
  97. package/src/components/rendering/WaveformTrackAxes.vue +66 -19
  98. package/src/components/rendering/WaveformTrackBackdrop.vue +8 -3
  99. package/src/components/rendering/compactFrame.ts +31 -0
  100. package/src/components/rendering/yAxisTickVisibility.ts +156 -0
  101. package/src/core/lineBoundary.ts +24 -0
  102. package/src/core/rendering.ts +48 -25
  103. package/src/core/renderingStrategies.ts +19 -13
  104. package/src/core/waveformPointSource.ts +23 -1
  105. package/src/core/workerSampling/outputBoundaries.ts +32 -0
  106. package/src/core/workerSampling/repository.ts +26 -18
  107. package/src/core/workerSampling/repositoryDataset.ts +33 -1
  108. package/src/core/workerSampling/workerRuntime.ts +13 -3
  109. package/src/data/simulatedWaveforms.ts +67 -2
  110. package/src/data/sx2Channel.ts +315 -0
  111. package/src/demo/DemoChartHost.vue +5 -1
  112. package/src/demo/DemoGridFrameControls.vue +2 -0
  113. package/src/demo/DemoViewControls.vue +5 -2
  114. package/src/demo/DemoYAxisPaddingControls.vue +53 -0
  115. package/src/demo/embedBridge.ts +85 -0
  116. package/src/demo/types.ts +7 -0
  117. package/src/index.ts +19 -0
  118. package/src/types/chart.ts +34 -8
  119. package/src/types/controls.ts +80 -0
  120. package/src/types/index.ts +18 -0
  121. package/src/utils/formatters.ts +2 -1
  122. package/src/views/WasmSamplingDemo.vue +8 -1
@@ -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
  }
@@ -1,5 +1,6 @@
1
1
  import type { WaveformData, WaveformPoint, WaveformSeries } from '../types'
2
2
  import { recordedDampedPoints } from './recordedDampedWaveform'
3
+ import { sx2Channel } from './sx2Channel'
3
4
 
4
5
  const POINT_COUNT = 1_000
5
6
  const START_TIME = -5
@@ -140,14 +141,78 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
140
141
  signal: (time, noise) => 0.7 * Math.sin(TWO_PI * 0.45 * time) + noise * 0.36,
141
142
  errors: () => ({ error: 0.12 }),
142
143
  },
144
+ {
145
+ id: 'simulated-mega-voltage',
146
+ shotNo: '13300',
147
+ name: '百万量级电压',
148
+ unit: 'V',
149
+ lineType: 'linear',
150
+ pointType: 'none',
151
+ signal: (time) => 2.4e6 * Math.sin(TWO_PI * 0.6 * time),
152
+ },
153
+ {
154
+ id: 'simulated-micro-current',
155
+ shotNo: '13300',
156
+ name: '微量级电流',
157
+ unit: 'A',
158
+ lineType: 'linear',
159
+ pointType: 'none',
160
+ signal: (time, noise) => 3.2e-6 * Math.sin(TWO_PI * 0.45 * time) + noise * 2e-7,
161
+ },
162
+ {
163
+ id: 'simulated-giga-pressure',
164
+ shotNo: '13300',
165
+ name: '十亿量级压力脉冲',
166
+ unit: 'Pa',
167
+ lineType: 'linear',
168
+ pointType: 'none',
169
+ signal: (time) => 1.8e9 + 4e8 * Math.exp(-((time - 0.75) ** 2) / 0.4),
170
+ },
171
+ {
172
+ id: 'simulated-nano-displacement',
173
+ shotNo: '13300',
174
+ name: '纳量级阻尼位移',
175
+ unit: 'm',
176
+ lineType: 'linear',
177
+ pointType: 'none',
178
+ signal: (time) => 6e-9 * Math.exp(-(time + 5) * 0.2) * Math.cos(TWO_PI * 0.8 * time),
179
+ },
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
+ },
143
189
  ]
144
190
 
145
- export function createSimulatedWaveformData(): WaveformData {
191
+ export function createSimulatedWaveformData(trackCount?: number): WaveformData {
146
192
  const noise = createSeededNoise(0x5eed1234)
147
193
  const errorNoise = createSeededNoise(0xe220a11)
194
+ const definitions = [...seriesDefinitions]
195
+ const existingTrackCount = new Set(definitions.map((series) => series.trackId ?? series.id)).size
196
+ const extraTrackCount =
197
+ trackCount !== undefined && Number.isFinite(trackCount)
198
+ ? Math.max(0, Math.min(100, Math.floor(trackCount)) - existingTrackCount)
199
+ : 0
200
+ for (let index = 0; index < extraTrackCount; index += 1) {
201
+ const channel = existingTrackCount + index + 1
202
+ definitions.push({
203
+ id: `simulated-extra-${channel}`,
204
+ name: `模拟通道 ${channel}`,
205
+ unit: 'V',
206
+ lineType: 'linear',
207
+ pointType: 'none',
208
+ signal: (time) =>
209
+ (1 + index * 0.3) * Math.sin(TWO_PI * (0.2 + index * 0.15) * time + index) +
210
+ 0.2 * Math.cos(TWO_PI * 1.4 * time),
211
+ })
212
+ }
148
213
  return {
149
214
  kind: 'series',
150
- series: seriesDefinitions.map(({ signal, errors, minimumX, ...series }) => {
215
+ series: definitions.map(({ signal, errors, minimumX, ...series }) => {
151
216
  const points = createPoints(signal, noise, errors, errorNoise)
152
217
  return {
153
218
  ...series,