waveform-analysis 0.1.48 → 0.1.51
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 +120 -5
- package/dist/assets/workerRuntime-Dt94jbqq.js +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +2683 -1153
- 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/trackLayoutBuilder.d.ts +3 -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 +56 -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/trackLayoutBuilder.ts +3 -0
- 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 +4 -4
- 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/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/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/demoRoutes.css +177 -0
- package/src/index.ts +18 -0
- package/src/router.ts +5 -0
- package/src/types/chart.ts +63 -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
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: [
|
package/src/core/rendering.ts
CHANGED
|
@@ -12,6 +12,7 @@ export {
|
|
|
12
12
|
DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
13
13
|
resolveWaveformRenderingOptions,
|
|
14
14
|
type ResolvedWaveformRenderingOptions,
|
|
15
|
+
type ResolvedWaveformSamplingOptions,
|
|
15
16
|
} from './renderingOptions'
|
|
16
17
|
|
|
17
18
|
const pointBisector = bisector((point: WaveformPoint) => point.x)
|
|
@@ -23,6 +24,8 @@ interface PointSeriesSource {
|
|
|
23
24
|
|
|
24
25
|
interface SeriesRenderSelectionOptions {
|
|
25
26
|
lineVisible: boolean
|
|
27
|
+
/** A current Worker result used only for the SVG line. */
|
|
28
|
+
linePointOverride?: WaveformPoint[]
|
|
26
29
|
pointVisible: boolean
|
|
27
30
|
errorBarVisible: boolean
|
|
28
31
|
hasErrorPoints: boolean
|
|
@@ -84,6 +87,34 @@ function selectRenderablePointsInRange(
|
|
|
84
87
|
})
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
function shouldDeferLineSampling(
|
|
91
|
+
visibleCount: number,
|
|
92
|
+
options: ResolvedWaveformRenderingOptions,
|
|
93
|
+
): boolean {
|
|
94
|
+
if (options.sampling.mode === 'wasm') return true
|
|
95
|
+
return options.sampling.mode === 'auto' && visibleCount > options.sampling.autoThreshold
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function selectSamplingPlaceholderPoints(
|
|
99
|
+
points: WaveformPoint[],
|
|
100
|
+
range: VisiblePointRange,
|
|
101
|
+
width: number,
|
|
102
|
+
options: ResolvedWaveformRenderingOptions,
|
|
103
|
+
): WaveformPoint[] {
|
|
104
|
+
const start = Math.max(0, range.start - 1)
|
|
105
|
+
const end = Math.min(points.length, range.end + 1)
|
|
106
|
+
const count = end - start
|
|
107
|
+
if (count <= 0) return []
|
|
108
|
+
if (options.sampling.strategy === 'none') return points.slice(start, end)
|
|
109
|
+
const target = Math.max(1, Math.floor(width * options.sampling.maxPointsPerPixel))
|
|
110
|
+
if (count <= target) return points.slice(start, end)
|
|
111
|
+
if (target === 1) return [points[start]!]
|
|
112
|
+
return Array.from({ length: target }, (_, index) => {
|
|
113
|
+
const offset = Math.round((index * (count - 1)) / (target - 1))
|
|
114
|
+
return points[start + offset]!
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
87
118
|
/**
|
|
88
119
|
* Select the visible source range and preserve first/min/max/last values in each X bucket.
|
|
89
120
|
* Source points must be sorted by X.
|
|
@@ -247,7 +278,10 @@ export function selectSeriesRenderPoints(
|
|
|
247
278
|
}
|
|
248
279
|
const range = resolveVisiblePointRange(points, domain)
|
|
249
280
|
const linePoints = selection.lineVisible
|
|
250
|
-
?
|
|
281
|
+
? (selection.linePointOverride ??
|
|
282
|
+
(shouldDeferLineSampling(range.end - range.start, rendering)
|
|
283
|
+
? selectSamplingPlaceholderPoints(points, range, width, rendering)
|
|
284
|
+
: selectRenderablePointsInRange(points, range, domain, width, rendering)))
|
|
251
285
|
: []
|
|
252
286
|
const errorBarVisible = selection.errorBarVisible && selection.hasErrorPoints
|
|
253
287
|
if (selection.pointVisible && errorBarVisible) {
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
WaveformRenderingOptions,
|
|
3
|
+
WaveformSamplingMode,
|
|
4
|
+
WaveformSamplingOptions,
|
|
5
|
+
WaveformSamplingStrategy,
|
|
6
|
+
} from '@/types'
|
|
7
|
+
|
|
8
|
+
export interface ResolvedWaveformSamplingOptions {
|
|
9
|
+
mode: WaveformSamplingMode
|
|
10
|
+
autoThreshold: number
|
|
11
|
+
autoHysteresis: number
|
|
12
|
+
strategy: WaveformSamplingStrategy
|
|
13
|
+
maxPointsPerPixel: number
|
|
14
|
+
rawPointLimit: number
|
|
15
|
+
wasmFailureFallback: 'error' | 'javascript'
|
|
16
|
+
}
|
|
2
17
|
|
|
3
18
|
export interface ResolvedWaveformRenderingOptions {
|
|
4
19
|
downsample: boolean
|
|
@@ -6,6 +21,7 @@ export interface ResolvedWaveformRenderingOptions {
|
|
|
6
21
|
maxPointsPerPixel: number
|
|
7
22
|
pointMinSpacing: number
|
|
8
23
|
errorBarMinSpacing: number
|
|
24
|
+
sampling: ResolvedWaveformSamplingOptions
|
|
9
25
|
}
|
|
10
26
|
|
|
11
27
|
export const DEFAULT_WAVEFORM_RENDERING_OPTIONS: ResolvedWaveformRenderingOptions = {
|
|
@@ -14,32 +30,125 @@ export const DEFAULT_WAVEFORM_RENDERING_OPTIONS: ResolvedWaveformRenderingOption
|
|
|
14
30
|
maxPointsPerPixel: 4,
|
|
15
31
|
pointMinSpacing: 10,
|
|
16
32
|
errorBarMinSpacing: 12,
|
|
33
|
+
sampling: {
|
|
34
|
+
mode: 'auto',
|
|
35
|
+
autoThreshold: 1_000,
|
|
36
|
+
autoHysteresis: 0,
|
|
37
|
+
strategy: 'peak',
|
|
38
|
+
maxPointsPerPixel: 4,
|
|
39
|
+
rawPointLimit: 100_000,
|
|
40
|
+
wasmFailureFallback: 'error',
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const samplingModes: readonly WaveformSamplingMode[] = ['auto', 'wasm', 'raw']
|
|
45
|
+
const samplingStrategies: readonly WaveformSamplingStrategy[] = [
|
|
46
|
+
'auto',
|
|
47
|
+
'none',
|
|
48
|
+
'peak',
|
|
49
|
+
'lttb',
|
|
50
|
+
'average',
|
|
51
|
+
'min',
|
|
52
|
+
'max',
|
|
53
|
+
'minmax',
|
|
54
|
+
'sum',
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
function isSamplingMode(value: unknown): value is WaveformSamplingMode {
|
|
58
|
+
return typeof value === 'string' && samplingModes.includes(value as WaveformSamplingMode)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isSamplingStrategy(value: unknown): value is WaveformSamplingStrategy {
|
|
62
|
+
return typeof value === 'string' && samplingStrategies.includes(value as WaveformSamplingStrategy)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveFiniteInteger(value: unknown, minimum: number, fallback: number): number {
|
|
66
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= minimum
|
|
67
|
+
? Math.floor(value)
|
|
68
|
+
: fallback
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolvePositiveNumber(value: unknown, fallback: number): number {
|
|
72
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveNonNegativeNumber(value: number, fallback: number): number {
|
|
76
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveSamplingOptions(
|
|
80
|
+
options: WaveformSamplingOptions | undefined,
|
|
81
|
+
legacyDownsample: boolean | undefined,
|
|
82
|
+
legacyMaxPointsPerPixel: number,
|
|
83
|
+
): ResolvedWaveformSamplingOptions {
|
|
84
|
+
const mode = isSamplingMode(options?.mode)
|
|
85
|
+
? options.mode
|
|
86
|
+
: legacyDownsample === false
|
|
87
|
+
? 'raw'
|
|
88
|
+
: DEFAULT_WAVEFORM_RENDERING_OPTIONS.sampling.mode
|
|
89
|
+
const maxPointsPerPixel = resolvePositiveNumber(
|
|
90
|
+
options?.maxPointsPerPixel,
|
|
91
|
+
legacyMaxPointsPerPixel,
|
|
92
|
+
)
|
|
93
|
+
return {
|
|
94
|
+
mode,
|
|
95
|
+
autoThreshold: resolveFiniteInteger(
|
|
96
|
+
options?.autoThreshold,
|
|
97
|
+
1,
|
|
98
|
+
DEFAULT_WAVEFORM_RENDERING_OPTIONS.sampling.autoThreshold,
|
|
99
|
+
),
|
|
100
|
+
autoHysteresis: resolveFiniteInteger(
|
|
101
|
+
options?.autoHysteresis,
|
|
102
|
+
0,
|
|
103
|
+
DEFAULT_WAVEFORM_RENDERING_OPTIONS.sampling.autoHysteresis,
|
|
104
|
+
),
|
|
105
|
+
strategy: isSamplingStrategy(options?.strategy)
|
|
106
|
+
? options.strategy
|
|
107
|
+
: DEFAULT_WAVEFORM_RENDERING_OPTIONS.sampling.strategy,
|
|
108
|
+
maxPointsPerPixel,
|
|
109
|
+
rawPointLimit: resolveFiniteInteger(
|
|
110
|
+
options?.rawPointLimit,
|
|
111
|
+
1,
|
|
112
|
+
DEFAULT_WAVEFORM_RENDERING_OPTIONS.sampling.rawPointLimit,
|
|
113
|
+
),
|
|
114
|
+
wasmFailureFallback:
|
|
115
|
+
options?.wasmFailureFallback === 'javascript' || options?.wasmFailureFallback === 'error'
|
|
116
|
+
? options.wasmFailureFallback
|
|
117
|
+
: DEFAULT_WAVEFORM_RENDERING_OPTIONS.sampling.wasmFailureFallback,
|
|
118
|
+
}
|
|
17
119
|
}
|
|
18
120
|
|
|
19
121
|
export function resolveWaveformRenderingOptions(
|
|
20
122
|
options?: WaveformRenderingOptions,
|
|
21
123
|
): ResolvedWaveformRenderingOptions {
|
|
22
124
|
const threshold = Number(options?.downsampleThreshold)
|
|
23
|
-
const
|
|
125
|
+
const legacyMaxPointsPerPixel = Number(options?.maxPointsPerPixel)
|
|
24
126
|
const pointMinSpacing = Number(options?.pointMinSpacing)
|
|
25
127
|
const errorBarMinSpacing = Number(options?.errorBarMinSpacing)
|
|
128
|
+
const resolvedLegacyMaxPointsPerPixel =
|
|
129
|
+
Number.isFinite(legacyMaxPointsPerPixel) && legacyMaxPointsPerPixel > 0
|
|
130
|
+
? legacyMaxPointsPerPixel
|
|
131
|
+
: DEFAULT_WAVEFORM_RENDERING_OPTIONS.maxPointsPerPixel
|
|
132
|
+
const sampling = resolveSamplingOptions(
|
|
133
|
+
options?.sampling,
|
|
134
|
+
typeof options?.downsample === 'boolean' ? options.downsample : undefined,
|
|
135
|
+
resolvedLegacyMaxPointsPerPixel,
|
|
136
|
+
)
|
|
26
137
|
return {
|
|
27
|
-
downsample:
|
|
138
|
+
downsample: sampling.mode !== 'raw',
|
|
28
139
|
downsampleThreshold:
|
|
29
140
|
Number.isFinite(threshold) && threshold >= 2
|
|
30
141
|
? Math.floor(threshold)
|
|
31
142
|
: DEFAULT_WAVEFORM_RENDERING_OPTIONS.downsampleThreshold,
|
|
32
|
-
maxPointsPerPixel:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
? errorBarMinSpacing
|
|
43
|
-
: DEFAULT_WAVEFORM_RENDERING_OPTIONS.errorBarMinSpacing,
|
|
143
|
+
maxPointsPerPixel: sampling.maxPointsPerPixel,
|
|
144
|
+
pointMinSpacing: resolveNonNegativeNumber(
|
|
145
|
+
pointMinSpacing,
|
|
146
|
+
DEFAULT_WAVEFORM_RENDERING_OPTIONS.pointMinSpacing,
|
|
147
|
+
),
|
|
148
|
+
errorBarMinSpacing: resolveNonNegativeNumber(
|
|
149
|
+
errorBarMinSpacing,
|
|
150
|
+
DEFAULT_WAVEFORM_RENDERING_OPTIONS.errorBarMinSpacing,
|
|
151
|
+
),
|
|
152
|
+
sampling,
|
|
44
153
|
}
|
|
45
154
|
}
|