waveform-analysis 0.1.47 → 0.1.50
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.
- package/README.md +127 -5
- package/dist/assets/workerRuntime-Dt94jbqq.js +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +2701 -1165
- package/dist/style.css +1 -1
- package/dist/types/components/WaveformChart.vue.d.ts +14 -0
- package/dist/types/components/annotation/WaveformAnnotationEditor.vue.d.ts +0 -1
- package/dist/types/components/annotation/markup.d.ts +5 -6
- package/dist/types/components/annotation/types.d.ts +2 -0
- package/dist/types/components/core/latestTaskScheduler.d.ts +19 -0
- package/dist/types/components/core/layout.d.ts +2 -2
- package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -1
- package/dist/types/components/core/types.d.ts +2 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
- package/dist/types/components/core/useWaveformData.d.ts +2 -0
- package/dist/types/components/core/useWaveformLayout.d.ts +2 -0
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +19 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +8 -1
- package/dist/types/components/core/workerSamplingClient.d.ts +8 -0
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/interaction/zoomConstraints.d.ts +2 -0
- package/dist/types/components/waveform.d.ts +1 -1
- package/dist/types/core/compactSamplePointSource.d.ts +3 -0
- package/dist/types/core/data.d.ts +7 -1
- package/dist/types/core/index.d.ts +2 -1
- package/dist/types/core/rendering.d.ts +3 -1
- package/dist/types/core/renderingOptions.d.ts +11 -1
- package/dist/types/core/samplingReference.d.ts +36 -0
- package/dist/types/core/samplingReferenceValues.d.ts +4 -0
- package/dist/types/core/samplingReferenceVectors.d.ts +17 -0
- package/dist/types/core/wasmSampling.d.ts +48 -0
- package/dist/types/core/waveformPointSource.d.ts +53 -0
- package/dist/types/core/workerSampling/index.d.ts +5 -0
- package/dist/types/core/workerSampling/javascriptBackend.d.ts +3 -0
- package/dist/types/core/workerSampling/multiresolution.d.ts +29 -0
- package/dist/types/core/workerSampling/protocol.d.ts +153 -0
- package/dist/types/core/workerSampling/repository.d.ts +36 -0
- package/dist/types/core/workerSampling/repositoryDataset.d.ts +29 -0
- package/dist/types/core/workerSampling/repositorySupport.d.ts +27 -0
- package/dist/types/core/workerSampling/samplingCache.d.ts +24 -0
- package/dist/types/core/workerSampling/workerRuntime.d.ts +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/types/chart.d.ts +58 -0
- package/dist/types/types/data.d.ts +20 -2
- package/dist/types/types/index.d.ts +2 -2
- package/dist/types/views/WasmSamplingDemo.vue.d.ts +3 -0
- package/package.json +5 -4
- package/src/App.test.ts +11 -7
- package/src/App.vue +2 -2
- package/src/DemoRouterApp.vue +1 -0
- package/src/components/WaveformChart.css +2 -0
- package/src/components/annotation/WaveformAnnotationEditor.css +2 -2
- package/src/components/annotation/WaveformAnnotationEditor.vue +8 -4
- package/src/components/annotation/markup.ts +45 -26
- package/src/components/annotation/types.ts +2 -0
- package/src/components/annotation/useWaveformChartAnnotations.ts +12 -15
- package/src/components/core/latestTaskScheduler.test.ts +51 -0
- package/src/components/core/latestTaskScheduler.ts +75 -0
- package/src/components/core/layout.ts +15 -6
- package/src/components/core/trackLayoutBuilder.ts +8 -2
- package/src/components/core/types.ts +2 -0
- package/src/components/core/useWaveformChartController.ts +14 -0
- package/src/components/core/useWaveformData.test.ts +95 -0
- package/src/components/core/useWaveformData.ts +23 -32
- package/src/components/core/useWaveformLayout.ts +16 -10
- package/src/components/core/useWaveformRenderSampling.test.ts +17 -0
- package/src/components/core/useWaveformRenderSampling.ts +394 -0
- package/src/components/core/waveformChartTypes.ts +13 -0
- package/src/components/core/workerSamplingClient.ts +148 -0
- package/src/components/core/yAxisNice.test.ts +174 -0
- package/src/components/data/types.ts +9 -0
- package/src/components/interaction/WaveformTooltip.test.ts +1 -3
- package/src/components/interaction/WaveformTooltip.vue +13 -12
- package/src/components/interaction/useWaveformHover.ts +7 -7
- package/src/components/interaction/zoomConstraints.ts +5 -1
- package/src/components/waveform.ts +9 -0
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +15 -4
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +11 -6
- package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -3
- package/src/components/waveformChartCases/trackOrder.test.ts +130 -0
- package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +117 -0
- package/src/components/waveformChartCases/xDomainStrategy.test.ts +14 -2
- package/src/core/compactSamplePointSource.ts +76 -0
- package/src/core/data.test.ts +114 -0
- package/src/core/data.ts +177 -75
- package/src/core/index.ts +11 -0
- package/src/core/rendering.test.ts +87 -12
- package/src/core/rendering.ts +35 -1
- package/src/core/renderingOptions.ts +124 -15
- package/src/core/samplingReference.test.ts +124 -0
- package/src/core/samplingReference.ts +243 -0
- package/src/core/samplingReferenceValues.ts +169 -0
- package/src/core/samplingReferenceVectors.ts +46 -0
- package/src/core/wasmSampling.test.ts +205 -0
- package/src/core/wasmSampling.ts +230 -0
- package/src/core/waveformPointSource.ts +395 -0
- package/src/core/workerSampling/index.ts +30 -0
- package/src/core/workerSampling/javascriptBackend.ts +15 -0
- package/src/core/workerSampling/multiresolution.test.ts +69 -0
- package/src/core/workerSampling/multiresolution.ts +300 -0
- package/src/core/workerSampling/protocol.test.ts +34 -0
- package/src/core/workerSampling/protocol.ts +207 -0
- package/src/core/workerSampling/repository.test.ts +400 -0
- package/src/core/workerSampling/repository.ts +400 -0
- package/src/core/workerSampling/repositoryCache.test.ts +163 -0
- package/src/core/workerSampling/repositoryDataset.ts +194 -0
- package/src/core/workerSampling/repositorySupport.ts +225 -0
- package/src/core/workerSampling/repositoryTypedDataset.test.ts +89 -0
- package/src/core/workerSampling/repositoryWasmDataset.test.ts +70 -0
- package/src/core/workerSampling/samplingCache.ts +94 -0
- package/src/core/workerSampling/workerRuntime.ts +40 -0
- package/src/data/simulatedWaveforms.test.ts +19 -0
- package/src/data/simulatedWaveforms.ts +11 -8
- package/src/demo/xAxisLabelControls.test.ts +1 -1
- package/src/demoRoutes.css +177 -0
- package/src/index.ts +18 -0
- package/src/router.ts +5 -0
- package/src/types/chart.ts +65 -0
- package/src/types/data.ts +24 -1
- package/src/types/index.ts +9 -0
- package/src/views/WasmSamplingDemo.test.ts +13 -0
- package/src/views/WasmSamplingDemo.vue +180 -0
|
@@ -128,12 +128,24 @@ describe('WaveformChart x domain strategy', () => {
|
|
|
128
128
|
{
|
|
129
129
|
id: 'first',
|
|
130
130
|
name: '第一轨',
|
|
131
|
-
data: {
|
|
131
|
+
data: {
|
|
132
|
+
kind: 'points',
|
|
133
|
+
points: [
|
|
134
|
+
{ x: 0, y: 0 },
|
|
135
|
+
{ x: 10, y: 1 },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
132
138
|
},
|
|
133
139
|
{
|
|
134
140
|
id: 'second',
|
|
135
141
|
name: '第二轨',
|
|
136
|
-
data: {
|
|
142
|
+
data: {
|
|
143
|
+
kind: 'points',
|
|
144
|
+
points: [
|
|
145
|
+
{ x: 100, y: 0 },
|
|
146
|
+
{ x: 110, y: 1 },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
137
149
|
},
|
|
138
150
|
],
|
|
139
151
|
},
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { WaveformPoint, WaveformTypedValues } from '../types'
|
|
2
|
+
import type { WaveformPointSource, WorkerSamplingDataset } from './waveformPointSource'
|
|
3
|
+
|
|
4
|
+
function bound(length: number, xAt: (index: number) => number, value: number, upper: boolean) {
|
|
5
|
+
let start = 0
|
|
6
|
+
let end = length
|
|
7
|
+
while (start < end) {
|
|
8
|
+
const middle = start + Math.floor((end - start) / 2)
|
|
9
|
+
if (upper ? xAt(middle) <= value : xAt(middle) < value) start = middle + 1
|
|
10
|
+
else end = middle
|
|
11
|
+
}
|
|
12
|
+
return start
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createCompactSamplePointSource(
|
|
16
|
+
y: WaveformTypedValues,
|
|
17
|
+
sampleRate: number,
|
|
18
|
+
startTime: number,
|
|
19
|
+
sourceIndexes?: Uint32Array,
|
|
20
|
+
): WaveformPointSource {
|
|
21
|
+
const xAt = (index: number) => startTime + (sourceIndexes?.[index] ?? index) / sampleRate
|
|
22
|
+
const pointAt = (index: number) =>
|
|
23
|
+
index < 0 || index >= y.length ? undefined : { x: xAt(index), y: y[index]! }
|
|
24
|
+
return {
|
|
25
|
+
isCompact: true,
|
|
26
|
+
length: y.length,
|
|
27
|
+
pointAt,
|
|
28
|
+
pointsInRange(start = 0, end = y.length) {
|
|
29
|
+
const rangeStart = Math.max(0, Math.min(y.length, Math.floor(start)))
|
|
30
|
+
const rangeEnd = Math.max(rangeStart, Math.min(y.length, Math.ceil(end)))
|
|
31
|
+
return Array.from({ length: rangeEnd - rangeStart }, (_, offset) =>
|
|
32
|
+
pointAt(rangeStart + offset),
|
|
33
|
+
).filter((point): point is WaveformPoint => point !== undefined)
|
|
34
|
+
},
|
|
35
|
+
visibleRange(domain: [number, number]) {
|
|
36
|
+
const start = Math.min(domain[0], domain[1])
|
|
37
|
+
const end = Math.max(domain[0], domain[1])
|
|
38
|
+
return {
|
|
39
|
+
start: bound(y.length, xAt, start, false),
|
|
40
|
+
end: bound(y.length, xAt, end, true),
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
nearestPoint(x: number) {
|
|
44
|
+
if (!Number.isFinite(x) || !y.length) return undefined
|
|
45
|
+
if (x <= xAt(0)) return pointAt(0)
|
|
46
|
+
if (x >= xAt(y.length - 1)) return pointAt(y.length - 1)
|
|
47
|
+
const right = bound(y.length, xAt, x, false)
|
|
48
|
+
const left = Math.max(0, right - 1)
|
|
49
|
+
return Math.abs(x - xAt(left)) < Math.abs(xAt(right) - x) ? pointAt(left) : pointAt(right)
|
|
50
|
+
},
|
|
51
|
+
metrics() {
|
|
52
|
+
let yMinimum = Number.POSITIVE_INFINITY
|
|
53
|
+
let yMaximum = Number.NEGATIVE_INFINITY
|
|
54
|
+
for (const value of y) {
|
|
55
|
+
yMinimum = Math.min(yMinimum, value)
|
|
56
|
+
yMaximum = Math.max(yMaximum, value)
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
xMinimum: y.length ? xAt(0) : Number.POSITIVE_INFINITY,
|
|
60
|
+
xMaximum: y.length ? xAt(y.length - 1) : Number.NEGATIVE_INFINITY,
|
|
61
|
+
yMinimum,
|
|
62
|
+
yMaximum,
|
|
63
|
+
hasErrorPoints: false,
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
toWorkerDataset(): WorkerSamplingDataset {
|
|
67
|
+
return {
|
|
68
|
+
kind: 'samples',
|
|
69
|
+
values: y.slice(),
|
|
70
|
+
sampleRate,
|
|
71
|
+
startTime,
|
|
72
|
+
...(sourceIndexes ? { sourceIndexes: sourceIndexes.slice() } : {}),
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/core/data.test.ts
CHANGED
|
@@ -17,6 +17,97 @@ describe('waveform data normalization', () => {
|
|
|
17
17
|
])
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
+
it('normalizes typed samples without mutating their values or buffer', () => {
|
|
21
|
+
const values = new Float32Array([1, Number.NaN, 3])
|
|
22
|
+
const snapshot = values.slice()
|
|
23
|
+
|
|
24
|
+
expect(
|
|
25
|
+
normalizeWaveformData({
|
|
26
|
+
kind: 'typed-samples',
|
|
27
|
+
values,
|
|
28
|
+
sampleRate: 2,
|
|
29
|
+
startTime: 1,
|
|
30
|
+
}),
|
|
31
|
+
).toEqual([
|
|
32
|
+
{ x: 1, y: 1 },
|
|
33
|
+
{ x: 2, y: 3 },
|
|
34
|
+
])
|
|
35
|
+
expect(values).toEqual(snapshot)
|
|
36
|
+
expect(values.buffer.byteLength).toBe(snapshot.buffer.byteLength)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('rejects malformed typed sample input without coercing numeric fields', () => {
|
|
40
|
+
expect(
|
|
41
|
+
normalizeWaveformData({
|
|
42
|
+
kind: 'typed-samples',
|
|
43
|
+
values: new Float64Array([1]),
|
|
44
|
+
sampleRate: Number.POSITIVE_INFINITY,
|
|
45
|
+
}),
|
|
46
|
+
).toEqual([])
|
|
47
|
+
expect(
|
|
48
|
+
normalizeWaveformData({
|
|
49
|
+
kind: 'typed-samples',
|
|
50
|
+
values: new Float64Array([1]),
|
|
51
|
+
sampleRate: 1,
|
|
52
|
+
startTime: Number.NaN,
|
|
53
|
+
}),
|
|
54
|
+
).toEqual([])
|
|
55
|
+
expect(
|
|
56
|
+
normalizeWaveformData({
|
|
57
|
+
kind: 'typed-samples',
|
|
58
|
+
values: new Int32Array([1]),
|
|
59
|
+
sampleRate: 1,
|
|
60
|
+
} as unknown as Parameters<typeof normalizeWaveformData>[0]),
|
|
61
|
+
).toEqual([])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('normalizes typed points, errors, and unordered coordinates without mutation', () => {
|
|
65
|
+
const x = new Float64Array([2, 1, 1, Number.NaN])
|
|
66
|
+
const y = new Float32Array([20, 10, 11, 12])
|
|
67
|
+
const error = new Float64Array([1, -1, Number.NaN, 4])
|
|
68
|
+
const lowerError = new Float32Array([2, 3, 4, 5])
|
|
69
|
+
const upperError = new Float64Array([3, 4, 5, 6])
|
|
70
|
+
const snapshots = [x.slice(), y.slice(), error.slice(), lowerError.slice(), upperError.slice()]
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
normalizeWaveformData({ kind: 'typed-points', x, y, error, lowerError, upperError }),
|
|
74
|
+
).toEqual([
|
|
75
|
+
{ x: 1, y: 10, lowerError: 3, upperError: 4 },
|
|
76
|
+
{ x: 1, y: 11, lowerError: 4, upperError: 5 },
|
|
77
|
+
{ x: 2, y: 20, error: 1, lowerError: 2, upperError: 3 },
|
|
78
|
+
])
|
|
79
|
+
expect(x).toEqual(snapshots[0])
|
|
80
|
+
expect(y).toEqual(snapshots[1])
|
|
81
|
+
expect(error).toEqual(snapshots[2])
|
|
82
|
+
expect(lowerError).toEqual(snapshots[3])
|
|
83
|
+
expect(upperError).toEqual(snapshots[4])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('rejects typed points with unsupported arrays or mismatched field lengths', () => {
|
|
87
|
+
expect(
|
|
88
|
+
normalizeWaveformData({
|
|
89
|
+
kind: 'typed-points',
|
|
90
|
+
x: new Float64Array([0, 1]),
|
|
91
|
+
y: new Float32Array([1]),
|
|
92
|
+
}),
|
|
93
|
+
).toEqual([])
|
|
94
|
+
expect(
|
|
95
|
+
normalizeWaveformData({
|
|
96
|
+
kind: 'typed-points',
|
|
97
|
+
x: new Float64Array([0]),
|
|
98
|
+
y: new Float64Array([1]),
|
|
99
|
+
upperError: new Float32Array([1, 2]),
|
|
100
|
+
}),
|
|
101
|
+
).toEqual([])
|
|
102
|
+
expect(
|
|
103
|
+
normalizeWaveformData({
|
|
104
|
+
kind: 'typed-points',
|
|
105
|
+
x: new Float32Array([0]),
|
|
106
|
+
y: new Float32Array([1]),
|
|
107
|
+
} as unknown as Parameters<typeof normalizeWaveformData>[0]),
|
|
108
|
+
).toEqual([])
|
|
109
|
+
})
|
|
110
|
+
|
|
20
111
|
it('skips sorting already ordered points and normalizes errors', () => {
|
|
21
112
|
const sortSpy = vi.spyOn(Array.prototype, 'sort')
|
|
22
113
|
try {
|
|
@@ -124,4 +215,27 @@ describe('waveform data normalization', () => {
|
|
|
124
215
|
|
|
125
216
|
expect(result.map((series) => series.shotNo)).toEqual(['13300', undefined])
|
|
126
217
|
})
|
|
218
|
+
|
|
219
|
+
it('accepts compact inputs inside regular series containers', () => {
|
|
220
|
+
const result = normalizeWaveformSeries({
|
|
221
|
+
kind: 'series',
|
|
222
|
+
series: [
|
|
223
|
+
{
|
|
224
|
+
id: 'typed',
|
|
225
|
+
name: 'Typed',
|
|
226
|
+
data: {
|
|
227
|
+
kind: 'typed-points',
|
|
228
|
+
x: new Float64Array([0, 1]),
|
|
229
|
+
y: new Float32Array([2, 3]),
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
expect(result).toHaveLength(1)
|
|
236
|
+
expect(result[0]?.points).toEqual([
|
|
237
|
+
{ x: 0, y: 2 },
|
|
238
|
+
{ x: 1, y: 3 },
|
|
239
|
+
])
|
|
240
|
+
})
|
|
127
241
|
})
|
package/src/core/data.ts
CHANGED
|
@@ -4,8 +4,10 @@ import type {
|
|
|
4
4
|
WaveformPoint,
|
|
5
5
|
WaveformLineStyle,
|
|
6
6
|
NormalizedWaveformSeries,
|
|
7
|
+
WaveformTypedValues,
|
|
7
8
|
} from '../types'
|
|
8
9
|
import { ERROR_BAR_DEFAULTS } from '../components/core/constants'
|
|
10
|
+
import { createWaveformPointSource, type WaveformPointSource } from './waveformPointSource'
|
|
9
11
|
|
|
10
12
|
const DEFAULT_ERROR_BAR_WIDTH = ERROR_BAR_DEFAULTS.WIDTH
|
|
11
13
|
const DEFAULT_ERROR_BAR_CAP_WIDTH = ERROR_BAR_DEFAULTS.CAP_WIDTH
|
|
@@ -31,6 +33,95 @@ function normalizeWaveformPoint(point: WaveformPoint): WaveformPoint {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function isWaveformTypedValues(value: unknown): value is WaveformTypedValues {
|
|
37
|
+
return value instanceof Float32Array || value instanceof Float64Array
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeSampleValues(
|
|
41
|
+
values: ArrayLike<number>,
|
|
42
|
+
sampleRate: number,
|
|
43
|
+
startTime: number | undefined,
|
|
44
|
+
): WaveformPoint[] {
|
|
45
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0) return []
|
|
46
|
+
|
|
47
|
+
const normalizedStartTime = Number.isFinite(startTime) ? (startTime ?? 0) : 0
|
|
48
|
+
const points: WaveformPoint[] = []
|
|
49
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
50
|
+
const value = values[index]
|
|
51
|
+
if (!Number.isFinite(value)) continue
|
|
52
|
+
points.push({ x: normalizedStartTime + index / sampleRate, y: value })
|
|
53
|
+
}
|
|
54
|
+
return points
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeTypedSamples(data: {
|
|
58
|
+
values: unknown
|
|
59
|
+
sampleRate: number
|
|
60
|
+
startTime?: number
|
|
61
|
+
}): WaveformPoint[] {
|
|
62
|
+
if (
|
|
63
|
+
!isWaveformTypedValues(data.values) ||
|
|
64
|
+
!Number.isFinite(data.sampleRate) ||
|
|
65
|
+
data.sampleRate <= 0 ||
|
|
66
|
+
(data.startTime !== undefined && !Number.isFinite(data.startTime))
|
|
67
|
+
) {
|
|
68
|
+
return []
|
|
69
|
+
}
|
|
70
|
+
return normalizeSampleValues(data.values, data.sampleRate, data.startTime)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function typedPointValuesHaveMatchingLengths(data: {
|
|
74
|
+
x: unknown
|
|
75
|
+
y: unknown
|
|
76
|
+
error?: unknown
|
|
77
|
+
lowerError?: unknown
|
|
78
|
+
upperError?: unknown
|
|
79
|
+
}): data is {
|
|
80
|
+
x: Float64Array
|
|
81
|
+
y: WaveformTypedValues
|
|
82
|
+
error?: WaveformTypedValues
|
|
83
|
+
lowerError?: WaveformTypedValues
|
|
84
|
+
upperError?: WaveformTypedValues
|
|
85
|
+
} {
|
|
86
|
+
if (!(data.x instanceof Float64Array) || !isWaveformTypedValues(data.y)) return false
|
|
87
|
+
const length = data.x.length
|
|
88
|
+
const values = [data.error, data.lowerError, data.upperError]
|
|
89
|
+
return (
|
|
90
|
+
values.every(
|
|
91
|
+
(value) => value === undefined || (isWaveformTypedValues(value) && value.length === length),
|
|
92
|
+
) && data.y.length === length
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeTypedPoints(data: {
|
|
97
|
+
x: Float64Array
|
|
98
|
+
y: WaveformTypedValues
|
|
99
|
+
error?: WaveformTypedValues
|
|
100
|
+
lowerError?: WaveformTypedValues
|
|
101
|
+
upperError?: WaveformTypedValues
|
|
102
|
+
}): WaveformPoint[] {
|
|
103
|
+
const points: WaveformPoint[] = []
|
|
104
|
+
let previousX = Number.NEGATIVE_INFINITY
|
|
105
|
+
let sorted = true
|
|
106
|
+
for (let index = 0; index < data.x.length; index += 1) {
|
|
107
|
+
const x = data.x[index]
|
|
108
|
+
const y = data.y[index]
|
|
109
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue
|
|
110
|
+
const normalized = normalizeWaveformPoint({
|
|
111
|
+
x,
|
|
112
|
+
y,
|
|
113
|
+
error: data.error?.[index],
|
|
114
|
+
lowerError: data.lowerError?.[index],
|
|
115
|
+
upperError: data.upperError?.[index],
|
|
116
|
+
})
|
|
117
|
+
if (normalized.x < previousX) sorted = false
|
|
118
|
+
previousX = normalized.x
|
|
119
|
+
points.push(normalized)
|
|
120
|
+
}
|
|
121
|
+
if (!sorted) points.sort((left, right) => left.x - right.x)
|
|
122
|
+
return points
|
|
123
|
+
}
|
|
124
|
+
|
|
34
125
|
export function resolveWaveformPointErrors(point: WaveformPoint): {
|
|
35
126
|
lower: number
|
|
36
127
|
upper: number
|
|
@@ -44,24 +135,21 @@ export function resolveWaveformPointErrors(point: WaveformPoint): {
|
|
|
44
135
|
|
|
45
136
|
/**
|
|
46
137
|
* 规范化单波形数据
|
|
47
|
-
* @param data
|
|
138
|
+
* @param data 输入数据(对象数组或 TypedArray 格式)
|
|
48
139
|
* @returns 规范化后的点数组
|
|
49
140
|
*/
|
|
50
141
|
export function normalizeWaveformData(data: SingleWaveformData): WaveformPoint[] {
|
|
51
142
|
if (data.kind === 'samples') {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const startTime = Number.isFinite(data.startTime) ? (data.startTime ?? 0) : 0
|
|
55
|
-
const points: WaveformPoint[] = []
|
|
56
|
-
for (let index = 0; index < data.values.length; index += 1) {
|
|
57
|
-
const value = data.values[index]
|
|
58
|
-
if (!Number.isFinite(value)) continue
|
|
59
|
-
points.push({ x: startTime + index / data.sampleRate, y: value })
|
|
60
|
-
}
|
|
143
|
+
return normalizeSampleValues(data.values, data.sampleRate, data.startTime)
|
|
144
|
+
}
|
|
61
145
|
|
|
62
|
-
|
|
146
|
+
if (data.kind === 'typed-samples') {
|
|
147
|
+
return normalizeTypedSamples(data)
|
|
63
148
|
}
|
|
64
149
|
|
|
150
|
+
if (data.kind === 'typed-points')
|
|
151
|
+
return typedPointValuesHaveMatchingLengths(data) ? normalizeTypedPoints(data) : []
|
|
152
|
+
|
|
65
153
|
const points: WaveformPoint[] = []
|
|
66
154
|
let previousX = Number.NEGATIVE_INFINITY
|
|
67
155
|
let sorted = true
|
|
@@ -77,78 +165,92 @@ export function normalizeWaveformData(data: SingleWaveformData): WaveformPoint[]
|
|
|
77
165
|
return points
|
|
78
166
|
}
|
|
79
167
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
168
|
+
export interface NormalizedWaveformSourceSeries extends Omit<NormalizedWaveformSeries, 'points'> {
|
|
169
|
+
source: WaveformPointSource
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizedSeriesOptions(
|
|
173
|
+
id: string,
|
|
174
|
+
series: {
|
|
175
|
+
shotNo?: string
|
|
176
|
+
trackId?: string
|
|
177
|
+
name: string
|
|
178
|
+
unit?: string
|
|
179
|
+
color?: string
|
|
180
|
+
lineType?: NormalizedWaveformSeries['lineType']
|
|
181
|
+
lineStyle?: WaveformLineStyle
|
|
182
|
+
pointType?: NormalizedWaveformSeries['pointType']
|
|
183
|
+
errorBar?: { visible?: boolean; color?: string; width?: number; capWidth?: number }
|
|
184
|
+
},
|
|
185
|
+
) {
|
|
186
|
+
const requestedLineType = series.lineType ?? 'linear'
|
|
187
|
+
const lineStyle = normalizeLineStyle(series.lineStyle)
|
|
188
|
+
const requestedPointType = series.pointType ?? 'none'
|
|
189
|
+
const errorBarVisible = series.errorBar?.visible === true
|
|
190
|
+
const lineType =
|
|
191
|
+
requestedLineType === 'none' && requestedPointType === 'none' && !errorBarVisible
|
|
192
|
+
? 'linear'
|
|
193
|
+
: requestedLineType
|
|
194
|
+
const width = Number(series.errorBar?.width)
|
|
195
|
+
const capWidth = Number(series.errorBar?.capWidth)
|
|
196
|
+
return {
|
|
197
|
+
id,
|
|
198
|
+
shotNo: series.shotNo?.trim() || undefined,
|
|
199
|
+
trackId: series.trackId?.trim() || undefined,
|
|
200
|
+
name: series.name,
|
|
201
|
+
unit: series.unit,
|
|
202
|
+
color: series.color,
|
|
203
|
+
lineType,
|
|
204
|
+
lineStyle,
|
|
205
|
+
pointType: requestedPointType,
|
|
206
|
+
errorBar: {
|
|
207
|
+
visible: errorBarVisible,
|
|
208
|
+
color: series.errorBar?.color,
|
|
209
|
+
width: Number.isFinite(width) && width > 0 ? width : DEFAULT_ERROR_BAR_WIDTH,
|
|
210
|
+
capWidth: Number.isFinite(capWidth) && capWidth > 0 ? capWidth : DEFAULT_ERROR_BAR_CAP_WIDTH,
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Internal normalized series which preserves compact TypedArray storage for chart rendering. */
|
|
216
|
+
export function normalizeWaveformSeriesSources(
|
|
217
|
+
data: WaveformData,
|
|
218
|
+
): NormalizedWaveformSourceSeries[] {
|
|
86
219
|
if (data.kind !== 'series') {
|
|
87
|
-
const
|
|
88
|
-
return
|
|
220
|
+
const source = createWaveformPointSource(data, normalizeWaveformData)
|
|
221
|
+
return source.length
|
|
89
222
|
? [
|
|
90
223
|
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
lineType: 'linear',
|
|
94
|
-
lineStyle: 'solid',
|
|
95
|
-
pointType: 'none',
|
|
96
|
-
errorBar: {
|
|
97
|
-
visible: false,
|
|
98
|
-
width: DEFAULT_ERROR_BAR_WIDTH,
|
|
99
|
-
capWidth: DEFAULT_ERROR_BAR_CAP_WIDTH,
|
|
100
|
-
},
|
|
101
|
-
points,
|
|
224
|
+
...normalizedSeriesOptions('series-0', { name: '' }),
|
|
225
|
+
source,
|
|
102
226
|
},
|
|
103
227
|
]
|
|
104
228
|
: []
|
|
105
229
|
}
|
|
106
230
|
|
|
107
231
|
const usedIds = new Set<string>()
|
|
232
|
+
return data.series.flatMap((series, index) => {
|
|
233
|
+
const id = series.id?.trim() || `series-${index}`
|
|
234
|
+
let uniqueId = id
|
|
235
|
+
let suffix = 1
|
|
236
|
+
while (usedIds.has(uniqueId)) {
|
|
237
|
+
uniqueId = `${id}-${suffix}`
|
|
238
|
+
suffix += 1
|
|
239
|
+
}
|
|
240
|
+
usedIds.add(uniqueId)
|
|
241
|
+
const source = createWaveformPointSource(series.data, normalizeWaveformData)
|
|
242
|
+
return source.length ? [{ ...normalizedSeriesOptions(uniqueId, series), source }] : []
|
|
243
|
+
})
|
|
244
|
+
}
|
|
108
245
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
usedIds.add(uniqueId)
|
|
121
|
-
|
|
122
|
-
const requestedLineType = series.lineType ?? 'linear'
|
|
123
|
-
const lineStyle = normalizeLineStyle((series as { lineStyle?: unknown }).lineStyle)
|
|
124
|
-
const requestedPointType = series.pointType ?? 'none'
|
|
125
|
-
const errorBarVisible = series.errorBar?.visible === true
|
|
126
|
-
const lineType =
|
|
127
|
-
requestedLineType === 'none' && requestedPointType === 'none' && !errorBarVisible
|
|
128
|
-
? 'linear'
|
|
129
|
-
: requestedLineType
|
|
130
|
-
const width = Number(series.errorBar?.width)
|
|
131
|
-
const capWidth = Number(series.errorBar?.capWidth)
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
id: uniqueId,
|
|
135
|
-
shotNo: series.shotNo?.trim() || undefined,
|
|
136
|
-
trackId: series.trackId?.trim() || undefined,
|
|
137
|
-
name: series.name,
|
|
138
|
-
unit: series.unit,
|
|
139
|
-
color: series.color,
|
|
140
|
-
lineType,
|
|
141
|
-
lineStyle,
|
|
142
|
-
pointType: requestedPointType,
|
|
143
|
-
errorBar: {
|
|
144
|
-
visible: errorBarVisible,
|
|
145
|
-
color: series.errorBar?.color,
|
|
146
|
-
width: Number.isFinite(width) && width > 0 ? width : DEFAULT_ERROR_BAR_WIDTH,
|
|
147
|
-
capWidth:
|
|
148
|
-
Number.isFinite(capWidth) && capWidth > 0 ? capWidth : DEFAULT_ERROR_BAR_CAP_WIDTH,
|
|
149
|
-
},
|
|
150
|
-
points: normalizeWaveformData(series.data),
|
|
151
|
-
}
|
|
152
|
-
})
|
|
153
|
-
.filter((series) => series.points.length > 0)
|
|
246
|
+
/**
|
|
247
|
+
* 规范化波形系列数据
|
|
248
|
+
* @param data 输入数据(单波形或多通道)
|
|
249
|
+
* @returns 规范化后的系列数组
|
|
250
|
+
*/
|
|
251
|
+
export function normalizeWaveformSeries(data: WaveformData): NormalizedWaveformSeries[] {
|
|
252
|
+
return normalizeWaveformSeriesSources(data).map(({ source, ...series }) => ({
|
|
253
|
+
...series,
|
|
254
|
+
points: source.pointsInRange(),
|
|
255
|
+
}))
|
|
154
256
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -10,4 +10,15 @@ export {
|
|
|
10
10
|
selectDecorationPoints,
|
|
11
11
|
selectRenderablePoints,
|
|
12
12
|
type ResolvedWaveformRenderingOptions,
|
|
13
|
+
type ResolvedWaveformSamplingOptions,
|
|
13
14
|
} from './rendering'
|
|
15
|
+
export {
|
|
16
|
+
calculateWasmRange,
|
|
17
|
+
findWasmVisibleRange,
|
|
18
|
+
initializeWasmSampling,
|
|
19
|
+
sampleWaveformWasm,
|
|
20
|
+
type WasmRangeMetrics,
|
|
21
|
+
type WasmSamplingRequest,
|
|
22
|
+
type WasmSamplingResult,
|
|
23
|
+
type WasmSamplingStrategy,
|
|
24
|
+
} from './wasmSampling'
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import type { WaveformPoint } from '../types'
|
|
3
|
+
import type { WaveformPoint, WaveformRenderingOptions } from '../types'
|
|
4
4
|
import {
|
|
5
|
+
DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
5
6
|
hasMinimumVisibleXValues,
|
|
6
7
|
resolveWaveformRenderingOptions,
|
|
7
8
|
selectDecorationPoints,
|
|
@@ -49,25 +50,80 @@ describe('waveform rendering selection', () => {
|
|
|
49
50
|
pointMinSpacing: -1,
|
|
50
51
|
errorBarMinSpacing: Number.POSITIVE_INFINITY,
|
|
51
52
|
}),
|
|
52
|
-
).toEqual(
|
|
53
|
-
downsample: true,
|
|
54
|
-
downsampleThreshold: 2_000,
|
|
55
|
-
maxPointsPerPixel: 4,
|
|
56
|
-
pointMinSpacing: 10,
|
|
57
|
-
errorBarMinSpacing: 12,
|
|
58
|
-
})
|
|
53
|
+
).toEqual(DEFAULT_WAVEFORM_RENDERING_OPTIONS)
|
|
59
54
|
})
|
|
60
55
|
|
|
61
56
|
it('accepts custom decoration spacing and uses zero to disable it', () => {
|
|
62
57
|
expect(resolveWaveformRenderingOptions({ pointMinSpacing: 6, errorBarMinSpacing: 0 })).toEqual({
|
|
63
|
-
|
|
64
|
-
downsampleThreshold: 2_000,
|
|
65
|
-
maxPointsPerPixel: 4,
|
|
58
|
+
...DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
66
59
|
pointMinSpacing: 6,
|
|
67
60
|
errorBarMinSpacing: 0,
|
|
68
61
|
})
|
|
69
62
|
})
|
|
70
63
|
|
|
64
|
+
it('normalizes sampling options with strict runtime validation', () => {
|
|
65
|
+
const options: WaveformRenderingOptions = {
|
|
66
|
+
sampling: {
|
|
67
|
+
mode: 'wasm',
|
|
68
|
+
autoThreshold: 1_001.9,
|
|
69
|
+
autoHysteresis: 20.8,
|
|
70
|
+
strategy: 'minmax',
|
|
71
|
+
maxPointsPerPixel: 2.5,
|
|
72
|
+
rawPointLimit: 50_000.2,
|
|
73
|
+
wasmFailureFallback: 'javascript',
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
expect(resolveWaveformRenderingOptions(options)).toMatchObject({
|
|
78
|
+
downsample: true,
|
|
79
|
+
maxPointsPerPixel: 2.5,
|
|
80
|
+
sampling: {
|
|
81
|
+
mode: 'wasm',
|
|
82
|
+
autoThreshold: 1_001,
|
|
83
|
+
autoHysteresis: 20,
|
|
84
|
+
strategy: 'minmax',
|
|
85
|
+
maxPointsPerPixel: 2.5,
|
|
86
|
+
rawPointLimit: 50_000,
|
|
87
|
+
wasmFailureFallback: 'javascript',
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(
|
|
92
|
+
resolveWaveformRenderingOptions({
|
|
93
|
+
sampling: {
|
|
94
|
+
mode: 'invalid',
|
|
95
|
+
autoThreshold: 0,
|
|
96
|
+
autoHysteresis: -1,
|
|
97
|
+
strategy: 'invalid',
|
|
98
|
+
maxPointsPerPixel: Number.POSITIVE_INFINITY,
|
|
99
|
+
rawPointLimit: 0,
|
|
100
|
+
wasmFailureFallback: 'fallback',
|
|
101
|
+
},
|
|
102
|
+
} as unknown as WaveformRenderingOptions),
|
|
103
|
+
).toMatchObject({ sampling: DEFAULT_WAVEFORM_RENDERING_OPTIONS.sampling })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('maps legacy downsample flags and gives explicit sampling mode precedence', () => {
|
|
107
|
+
expect(resolveWaveformRenderingOptions({ downsample: false }).sampling.mode).toBe('raw')
|
|
108
|
+
expect(resolveWaveformRenderingOptions({ downsample: true }).sampling.mode).toBe('auto')
|
|
109
|
+
expect(
|
|
110
|
+
resolveWaveformRenderingOptions({ downsample: false, sampling: { mode: 'wasm' } }),
|
|
111
|
+
).toMatchObject({ downsample: true, sampling: { mode: 'wasm' } })
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('prefers nested sampling density over the legacy rendering field', () => {
|
|
115
|
+
const legacy = resolveWaveformRenderingOptions({ maxPointsPerPixel: 2 })
|
|
116
|
+
const nested = resolveWaveformRenderingOptions({
|
|
117
|
+
maxPointsPerPixel: 2,
|
|
118
|
+
sampling: { maxPointsPerPixel: 3 },
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(legacy.maxPointsPerPixel).toBe(2)
|
|
122
|
+
expect(legacy.sampling.maxPointsPerPixel).toBe(2)
|
|
123
|
+
expect(nested.maxPointsPerPixel).toBe(3)
|
|
124
|
+
expect(nested.sampling.maxPointsPerPixel).toBe(3)
|
|
125
|
+
})
|
|
126
|
+
|
|
71
127
|
it('selects evenly distributed source points for dense decorations', () => {
|
|
72
128
|
const selected = selectDecorationPoints(points, [0, 9_999], 100, 10, true)
|
|
73
129
|
|
|
@@ -135,7 +191,10 @@ describe('waveform rendering selection', () => {
|
|
|
135
191
|
densePoints,
|
|
136
192
|
[99_999, 0],
|
|
137
193
|
500,
|
|
138
|
-
resolveWaveformRenderingOptions({
|
|
194
|
+
resolveWaveformRenderingOptions({
|
|
195
|
+
downsampleThreshold: 100,
|
|
196
|
+
sampling: { autoThreshold: 200_000 },
|
|
197
|
+
}),
|
|
139
198
|
{
|
|
140
199
|
lineVisible: true,
|
|
141
200
|
pointVisible: true,
|
|
@@ -152,6 +211,22 @@ describe('waveform rendering selection', () => {
|
|
|
152
211
|
expect(selected.errorBarRenderPoints.every((point) => (point.error ?? 0) > 0)).toBe(true)
|
|
153
212
|
})
|
|
154
213
|
|
|
214
|
+
it('uses a width-bounded placeholder while large-series Worker sampling is pending', () => {
|
|
215
|
+
const rendering = resolveWaveformRenderingOptions({
|
|
216
|
+
sampling: { mode: 'auto', autoThreshold: 1_000, maxPointsPerPixel: 2 },
|
|
217
|
+
})
|
|
218
|
+
const selected = selectSeriesRenderPoints(points, [0, 9_999], 100, rendering, {
|
|
219
|
+
lineVisible: true,
|
|
220
|
+
pointVisible: false,
|
|
221
|
+
errorBarVisible: false,
|
|
222
|
+
hasErrorPoints: false,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
expect(selected.linePoints).toHaveLength(200)
|
|
226
|
+
expect(selected.linePoints[0]).toBe(points[0])
|
|
227
|
+
expect(selected.linePoints.at(-1)).toBe(points.at(-1))
|
|
228
|
+
})
|
|
229
|
+
|
|
155
230
|
it('counts unique visible x values across series and reversed domains', () => {
|
|
156
231
|
const first = {
|
|
157
232
|
points: [
|