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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createInitializedWasmSamplingBackend, initializeWasmSampling } from '../wasmSampling'
|
|
2
|
+
import { WorkerSamplingRepository } from './repository'
|
|
3
|
+
import type { WorkerSamplingRequest, WorkerSamplingResponse } from './protocol'
|
|
4
|
+
|
|
5
|
+
const repository = new WorkerSamplingRepository()
|
|
6
|
+
let wasmInitialization: Promise<void> | undefined
|
|
7
|
+
let wasmInitializationError: string | undefined
|
|
8
|
+
|
|
9
|
+
function requiresWasm(request: WorkerSamplingRequest) {
|
|
10
|
+
return (
|
|
11
|
+
request.type === 'sample-viewport' && request.series.some((series) => series.mode !== 'raw')
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function initializeWasmBackend() {
|
|
16
|
+
if (wasmInitializationError) return
|
|
17
|
+
wasmInitialization ??= initializeWasmSampling()
|
|
18
|
+
.then(() => {
|
|
19
|
+
repository.setWasmBackend(createInitializedWasmSamplingBackend())
|
|
20
|
+
})
|
|
21
|
+
.catch((error: unknown) => {
|
|
22
|
+
wasmInitializationError = error instanceof Error ? error.message : String(error)
|
|
23
|
+
})
|
|
24
|
+
await wasmInitialization
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function addWasmError(response: WorkerSamplingResponse): WorkerSamplingResponse {
|
|
28
|
+
if (response.type !== 'sample-viewport-response' || !wasmInitializationError) return response
|
|
29
|
+
return { ...response, workerError: wasmInitializationError }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const workerScope = globalThis as typeof globalThis & {
|
|
33
|
+
onmessage: ((event: MessageEvent<WorkerSamplingRequest>) => void) | null
|
|
34
|
+
postMessage: (message: WorkerSamplingResponse) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
workerScope.onmessage = async (event: MessageEvent<WorkerSamplingRequest>) => {
|
|
38
|
+
if (requiresWasm(event.data)) await initializeWasmBackend()
|
|
39
|
+
workerScope.postMessage(addWasmError(repository.handle(event.data)))
|
|
40
|
+
}
|
|
@@ -18,6 +18,25 @@ describe('simulated waveform data', () => {
|
|
|
18
18
|
)
|
|
19
19
|
expect(secondFrame).toHaveLength(2)
|
|
20
20
|
expect(new Set(first.series.map((series) => series.shotNo))).toEqual(new Set(['13300']))
|
|
21
|
+
const firstSeries = first.series[0]
|
|
22
|
+
expect(firstSeries).toMatchObject({
|
|
23
|
+
id: 'simulated-sine',
|
|
24
|
+
lineType: 'linear',
|
|
25
|
+
pointType: 'circle',
|
|
26
|
+
errorBar: { visible: true, width: 1.5, capWidth: 10 },
|
|
27
|
+
})
|
|
28
|
+
if (firstSeries?.data.kind === 'points') {
|
|
29
|
+
const lowerErrors = firstSeries.data.points.map((point) => point.lowerError)
|
|
30
|
+
const upperErrors = firstSeries.data.points.map((point) => point.upperError)
|
|
31
|
+
expect(
|
|
32
|
+
lowerErrors.every((error) => error !== undefined && error >= 0.05 && error < 0.5),
|
|
33
|
+
).toBe(true)
|
|
34
|
+
expect(
|
|
35
|
+
upperErrors.every((error) => error !== undefined && error >= 0.05 && error < 0.5),
|
|
36
|
+
).toBe(true)
|
|
37
|
+
expect(new Set(lowerErrors).size).toBeGreaterThan(900)
|
|
38
|
+
expect(new Set(upperErrors).size).toBeGreaterThan(900)
|
|
39
|
+
}
|
|
21
40
|
first.series.forEach((series) => {
|
|
22
41
|
expect(series.data.kind).toBe('points')
|
|
23
42
|
if (series.data.kind !== 'points') return
|
|
@@ -9,6 +9,7 @@ type SignalGenerator = (time: number, noise: number) => number
|
|
|
9
9
|
type ErrorGenerator = (
|
|
10
10
|
time: number,
|
|
11
11
|
value: number,
|
|
12
|
+
noise: () => number,
|
|
12
13
|
) => Pick<WaveformPoint, 'error' | 'lowerError' | 'upperError'>
|
|
13
14
|
|
|
14
15
|
interface SimulatedSeriesDefinition extends Pick<
|
|
@@ -32,6 +33,7 @@ function createPoints(
|
|
|
32
33
|
signal: SignalGenerator,
|
|
33
34
|
noise: () => number,
|
|
34
35
|
errors?: ErrorGenerator,
|
|
36
|
+
errorNoise: () => number = noise,
|
|
35
37
|
): WaveformPoint[] {
|
|
36
38
|
return Array.from({ length: POINT_COUNT }, (_, index) => {
|
|
37
39
|
const time = START_TIME + (index * (END_TIME - START_TIME)) / (POINT_COUNT - 1)
|
|
@@ -39,7 +41,7 @@ function createPoints(
|
|
|
39
41
|
return {
|
|
40
42
|
x: time,
|
|
41
43
|
y: value,
|
|
42
|
-
...errors?.(time, value),
|
|
44
|
+
...errors?.(time, value, errorNoise),
|
|
43
45
|
}
|
|
44
46
|
})
|
|
45
47
|
}
|
|
@@ -50,13 +52,13 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
50
52
|
shotNo: '13300',
|
|
51
53
|
name: '正弦基波',
|
|
52
54
|
unit: 'V',
|
|
53
|
-
lineType: '
|
|
54
|
-
pointType: '
|
|
55
|
-
errorBar: { visible: true },
|
|
55
|
+
lineType: 'linear',
|
|
56
|
+
pointType: 'circle',
|
|
57
|
+
errorBar: { visible: true, width: 1.5, capWidth: 10 },
|
|
56
58
|
signal: (time) => 1.2 * Math.sin(TWO_PI * 0.8 * time),
|
|
57
|
-
errors: (
|
|
58
|
-
lowerError: 0.
|
|
59
|
-
upperError: 0.
|
|
59
|
+
errors: (_time, _value, noise) => ({
|
|
60
|
+
lowerError: 0.05 + (noise() + 0.5) * 0.45,
|
|
61
|
+
upperError: 0.05 + (noise() + 0.5) * 0.45,
|
|
60
62
|
}),
|
|
61
63
|
},
|
|
62
64
|
{
|
|
@@ -128,10 +130,11 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
128
130
|
|
|
129
131
|
export function createSimulatedWaveformData(): WaveformData {
|
|
130
132
|
const noise = createSeededNoise(0x5eed1234)
|
|
133
|
+
const errorNoise = createSeededNoise(0xe220a11)
|
|
131
134
|
return {
|
|
132
135
|
kind: 'series',
|
|
133
136
|
series: seriesDefinitions.map(({ signal, errors, minimumX, ...series }) => {
|
|
134
|
-
const points = createPoints(signal, noise, errors)
|
|
137
|
+
const points = createPoints(signal, noise, errors, errorNoise)
|
|
135
138
|
return {
|
|
136
139
|
...series,
|
|
137
140
|
data: {
|
package/src/demoRoutes.css
CHANGED
|
@@ -138,6 +138,116 @@
|
|
|
138
138
|
min-height: 0;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
.sampling-demo {
|
|
142
|
+
display: grid;
|
|
143
|
+
grid-template-rows: auto auto minmax(0, 1fr);
|
|
144
|
+
gap: 12px;
|
|
145
|
+
width: 100%;
|
|
146
|
+
height: 100%;
|
|
147
|
+
padding: 16px;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.sampling-demo__toolbar {
|
|
152
|
+
display: flex;
|
|
153
|
+
gap: 24px;
|
|
154
|
+
align-items: center;
|
|
155
|
+
justify-content: space-between;
|
|
156
|
+
min-width: 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.sampling-demo__toolbar h1 {
|
|
160
|
+
margin: 0;
|
|
161
|
+
color: #101828;
|
|
162
|
+
font-size: 18px;
|
|
163
|
+
font-weight: 700;
|
|
164
|
+
letter-spacing: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.sampling-demo__toolbar p {
|
|
168
|
+
margin: 4px 0 0;
|
|
169
|
+
color: #667085;
|
|
170
|
+
font-size: 12px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.sampling-demo__controls {
|
|
174
|
+
display: flex;
|
|
175
|
+
gap: 16px;
|
|
176
|
+
align-items: center;
|
|
177
|
+
min-height: 48px;
|
|
178
|
+
padding: 8px 12px;
|
|
179
|
+
background: #fff;
|
|
180
|
+
border: 1px solid #e4e7ec;
|
|
181
|
+
border-radius: 6px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.sampling-demo__controls label {
|
|
185
|
+
display: grid;
|
|
186
|
+
grid-template-columns: auto 132px;
|
|
187
|
+
gap: 8px;
|
|
188
|
+
align-items: center;
|
|
189
|
+
color: #475467;
|
|
190
|
+
font-size: 12px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.sampling-demo__workspace {
|
|
194
|
+
display: grid;
|
|
195
|
+
grid-template-columns: minmax(0, 1fr) 450px;
|
|
196
|
+
gap: 12px;
|
|
197
|
+
min-width: 0;
|
|
198
|
+
min-height: 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.sampling-demo__chart {
|
|
202
|
+
min-width: 0;
|
|
203
|
+
min-height: 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.sampling-demo__chart > .waveform-chart {
|
|
207
|
+
height: 100%;
|
|
208
|
+
min-height: 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.sampling-demo__diagnostics {
|
|
212
|
+
min-width: 0;
|
|
213
|
+
overflow: auto;
|
|
214
|
+
background: #fff;
|
|
215
|
+
border: 1px solid #e4e7ec;
|
|
216
|
+
border-radius: 6px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.sampling-demo__diagnostic-header,
|
|
220
|
+
.sampling-demo__diagnostic-row {
|
|
221
|
+
display: grid;
|
|
222
|
+
grid-template-columns: 60px 54px repeat(3, minmax(46px, 1fr)) 55px 48px;
|
|
223
|
+
gap: 5px;
|
|
224
|
+
align-items: center;
|
|
225
|
+
min-width: 405px;
|
|
226
|
+
padding: 9px 10px;
|
|
227
|
+
color: #475467;
|
|
228
|
+
font-size: 11px;
|
|
229
|
+
font-variant-numeric: tabular-nums;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.sampling-demo__diagnostic-header {
|
|
233
|
+
position: sticky;
|
|
234
|
+
top: 0;
|
|
235
|
+
z-index: 1;
|
|
236
|
+
color: #667085;
|
|
237
|
+
font-weight: 650;
|
|
238
|
+
background: #f8fafc;
|
|
239
|
+
border-bottom: 1px solid #e4e7ec;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.sampling-demo__diagnostic-row + .sampling-demo__diagnostic-row {
|
|
243
|
+
border-top: 1px solid #f0f2f5;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.sampling-demo__diagnostic-row .is-hit {
|
|
247
|
+
color: #237804;
|
|
248
|
+
font-weight: 650;
|
|
249
|
+
}
|
|
250
|
+
|
|
141
251
|
@media (max-width: 900px) {
|
|
142
252
|
.demo-shell {
|
|
143
253
|
grid-template-rows: 44px minmax(0, 1fr);
|
|
@@ -194,4 +304,71 @@
|
|
|
194
304
|
border-top: 1px solid #e4e7ec;
|
|
195
305
|
border-left: 0;
|
|
196
306
|
}
|
|
307
|
+
|
|
308
|
+
.sampling-demo {
|
|
309
|
+
gap: 8px;
|
|
310
|
+
padding: 10px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.sampling-demo__toolbar {
|
|
314
|
+
display: grid;
|
|
315
|
+
gap: 8px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.sampling-demo__toolbar .ant-radio-group {
|
|
319
|
+
display: flex;
|
|
320
|
+
width: 100%;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.sampling-demo__toolbar .ant-radio-button-wrapper {
|
|
324
|
+
flex: 1 1 0;
|
|
325
|
+
padding-inline: 6px;
|
|
326
|
+
text-align: center;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.sampling-demo__controls {
|
|
330
|
+
display: grid;
|
|
331
|
+
grid-template-columns: 1fr 1fr;
|
|
332
|
+
gap: 8px 12px;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.sampling-demo__controls label {
|
|
336
|
+
grid-template-columns: 1fr;
|
|
337
|
+
gap: 3px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.sampling-demo__workspace {
|
|
341
|
+
grid-template-columns: minmax(0, 1fr);
|
|
342
|
+
grid-template-rows: minmax(280px, 1fr) 208px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.sampling-demo__diagnostic-header {
|
|
346
|
+
display: none;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.sampling-demo__diagnostic-row {
|
|
350
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
351
|
+
gap: 7px 12px;
|
|
352
|
+
min-width: 0;
|
|
353
|
+
padding: 10px;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.sampling-demo__diagnostic-row span {
|
|
357
|
+
min-width: 0;
|
|
358
|
+
overflow: hidden;
|
|
359
|
+
color: #344054;
|
|
360
|
+
text-overflow: ellipsis;
|
|
361
|
+
white-space: nowrap;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.sampling-demo__diagnostic-row span::before {
|
|
365
|
+
display: block;
|
|
366
|
+
color: #667085;
|
|
367
|
+
font-size: 10px;
|
|
368
|
+
content: attr(data-label);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.sampling-demo__diagnostic-row span:first-child {
|
|
372
|
+
grid-column: 1 / -1;
|
|
373
|
+
}
|
|
197
374
|
}
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,12 @@ export type {
|
|
|
18
18
|
WaveformZoomResetPayload,
|
|
19
19
|
WaveformAnnotationStyle,
|
|
20
20
|
WaveformAnnotation,
|
|
21
|
+
WaveformSamplingMode,
|
|
22
|
+
WaveformSamplingStrategy,
|
|
23
|
+
WaveformSamplingOptions,
|
|
24
|
+
WaveformSamplingBackend,
|
|
25
|
+
WaveformSamplingDiagnostics,
|
|
26
|
+
WaveformSamplingError,
|
|
21
27
|
WaveformRenderingOptions,
|
|
22
28
|
WaveformPlotMargin,
|
|
23
29
|
WaveformTitleTextStyle,
|
|
@@ -38,6 +44,9 @@ export type {
|
|
|
38
44
|
WaveformPointType,
|
|
39
45
|
WaveformErrorBarOptions,
|
|
40
46
|
ResolvedWaveformErrorBarOptions,
|
|
47
|
+
WaveformTypedValues,
|
|
48
|
+
TypedSampleData,
|
|
49
|
+
TypedPointData,
|
|
41
50
|
WaveformSeries,
|
|
42
51
|
WaveformData,
|
|
43
52
|
NormalizedWaveformSeries,
|
|
@@ -67,10 +76,19 @@ export {
|
|
|
67
76
|
} from './utils'
|
|
68
77
|
|
|
69
78
|
export {
|
|
79
|
+
calculateWasmRange,
|
|
80
|
+
findWasmVisibleRange,
|
|
70
81
|
DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
71
82
|
resolveWaveformRenderingOptions,
|
|
72
83
|
selectRenderablePoints,
|
|
73
84
|
type ResolvedWaveformRenderingOptions,
|
|
85
|
+
type ResolvedWaveformSamplingOptions,
|
|
86
|
+
type WasmRangeMetrics,
|
|
87
|
+
initializeWasmSampling,
|
|
88
|
+
sampleWaveformWasm,
|
|
89
|
+
type WasmSamplingRequest,
|
|
90
|
+
type WasmSamplingResult,
|
|
91
|
+
type WasmSamplingStrategy,
|
|
74
92
|
} from './core'
|
|
75
93
|
|
|
76
94
|
export { parseWaveformAnnotations, serializeWaveformAnnotations } from './components/annotation'
|
package/src/router.ts
CHANGED
|
@@ -13,6 +13,11 @@ export const routes: RouteRecordRaw[] = [
|
|
|
13
13
|
name: 'fixed-y-domain',
|
|
14
14
|
component: () => import('./views/FixedYDomainDemo.vue'),
|
|
15
15
|
},
|
|
16
|
+
{
|
|
17
|
+
path: '/wasm-sampling',
|
|
18
|
+
name: 'wasm-sampling',
|
|
19
|
+
component: () => import('./views/WasmSamplingDemo.vue'),
|
|
20
|
+
},
|
|
16
21
|
]
|
|
17
22
|
|
|
18
23
|
export const router = createRouter({
|
package/src/types/chart.ts
CHANGED
|
@@ -77,8 +77,71 @@ export interface WaveformAnnotation {
|
|
|
77
77
|
createdAt?: string
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** Selects the execution path used to obtain points for waveform rendering. */
|
|
81
|
+
export type WaveformSamplingMode = 'auto' | 'wasm' | 'raw'
|
|
82
|
+
|
|
83
|
+
/** Selects the point-reduction algorithm used by the sampling backend. */
|
|
84
|
+
export type WaveformSamplingStrategy =
|
|
85
|
+
'auto' | 'none' | 'peak' | 'lttb' | 'average' | 'min' | 'max' | 'minmax' | 'sum'
|
|
86
|
+
|
|
87
|
+
/** Controls how waveform points are selected for rendering. */
|
|
88
|
+
export interface WaveformSamplingOptions {
|
|
89
|
+
/** Defaults to `auto`. */
|
|
90
|
+
mode?: WaveformSamplingMode
|
|
91
|
+
/** Per-series visible-point threshold used by auto mode. Defaults to 1,000. */
|
|
92
|
+
autoThreshold?: number
|
|
93
|
+
/** Interaction-only hysteresis around the automatic threshold. Defaults to 0. */
|
|
94
|
+
autoHysteresis?: number
|
|
95
|
+
/** Defaults to the peak-preserving strategy. */
|
|
96
|
+
strategy?: WaveformSamplingStrategy
|
|
97
|
+
/** Upper bound for rendered points per horizontal CSS pixel. */
|
|
98
|
+
maxPointsPerPixel?: number
|
|
99
|
+
/** Visible-point count at which raw rendering should report a diagnostic. */
|
|
100
|
+
rawPointLimit?: number
|
|
101
|
+
/** Controls whether a forced WASM failure may temporarily show JavaScript results. */
|
|
102
|
+
wasmFailureFallback?: 'error' | 'javascript'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The backend that produced a series' current SVG geometry. */
|
|
106
|
+
export type WaveformSamplingBackend = 'raw' | 'javascript' | 'wasm' | 'unavailable'
|
|
107
|
+
|
|
108
|
+
/** Read-only details emitted after a sampling request settles. */
|
|
109
|
+
export interface WaveformSamplingDiagnostics {
|
|
110
|
+
seriesId: string
|
|
111
|
+
/** Internal dataset identity, useful when correlating request logs. */
|
|
112
|
+
datasetId: string
|
|
113
|
+
mode: WaveformSamplingMode
|
|
114
|
+
selectedMode: 'raw' | 'sampled'
|
|
115
|
+
backend: WaveformSamplingBackend
|
|
116
|
+
strategy: Exclude<WaveformSamplingStrategy, 'auto'>
|
|
117
|
+
sourcePointCount: number
|
|
118
|
+
visiblePointCount: number
|
|
119
|
+
renderedPointCount: number
|
|
120
|
+
durationMs: number
|
|
121
|
+
cacheHit: boolean
|
|
122
|
+
requestId: number
|
|
123
|
+
revision: number
|
|
124
|
+
rawPointLimitExceeded: boolean
|
|
125
|
+
/** Cumulative sampling runs scheduled by this chart instance. */
|
|
126
|
+
scheduledRequestCount: number
|
|
127
|
+
/** Cumulative intermediate runs replaced before they entered the Worker queue. */
|
|
128
|
+
coalescedRequestCount: number
|
|
129
|
+
/** Maximum retained pending runs; bounded to one by the latest-wins scheduler. */
|
|
130
|
+
maxPendingRequestCount: number
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Describes a sampling backend failure without exposing an implementation-specific Error object. */
|
|
134
|
+
export interface WaveformSamplingError {
|
|
135
|
+
message: string
|
|
136
|
+
mode: WaveformSamplingMode
|
|
137
|
+
fallback: 'javascript' | 'none'
|
|
138
|
+
seriesIds: string[]
|
|
139
|
+
}
|
|
140
|
+
|
|
80
141
|
/** Controls how many source points are included in the rendered SVG path. */
|
|
81
142
|
export interface WaveformRenderingOptions {
|
|
143
|
+
/** Sampling execution and strategy configuration. Takes precedence over legacy fields. */
|
|
144
|
+
sampling?: WaveformSamplingOptions
|
|
82
145
|
/** Disable only when every source point must be rendered. */
|
|
83
146
|
downsample?: boolean
|
|
84
147
|
/** Visible point count below which the complete range is rendered. */
|
package/src/types/data.ts
CHANGED
|
@@ -27,8 +27,29 @@ export interface ResolvedWaveformErrorBarOptions {
|
|
|
27
27
|
capWidth: number
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Numeric storage accepted by the compact waveform input formats. */
|
|
31
|
+
export type WaveformTypedValues = Float32Array | Float64Array
|
|
32
|
+
|
|
33
|
+
/** Compact, evenly spaced samples. X is derived as `startTime + index / sampleRate`. */
|
|
34
|
+
export interface TypedSampleData {
|
|
35
|
+
kind: 'typed-samples'
|
|
36
|
+
values: WaveformTypedValues
|
|
37
|
+
sampleRate: number
|
|
38
|
+
startTime?: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Compact points with explicit X coordinates. X uses Float64 precision by contract. */
|
|
42
|
+
export interface TypedPointData {
|
|
43
|
+
kind: 'typed-points'
|
|
44
|
+
x: Float64Array
|
|
45
|
+
y: WaveformTypedValues
|
|
46
|
+
error?: WaveformTypedValues
|
|
47
|
+
lowerError?: WaveformTypedValues
|
|
48
|
+
upperError?: WaveformTypedValues
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
/**
|
|
31
|
-
*
|
|
52
|
+
* 单波形数据格式(对象数组或紧凑 TypedArray)
|
|
32
53
|
*/
|
|
33
54
|
export type SingleWaveformData =
|
|
34
55
|
| {
|
|
@@ -41,6 +62,8 @@ export type SingleWaveformData =
|
|
|
41
62
|
kind: 'points'
|
|
42
63
|
points: WaveformPoint[]
|
|
43
64
|
}
|
|
65
|
+
| TypedSampleData
|
|
66
|
+
| TypedPointData
|
|
44
67
|
|
|
45
68
|
/**
|
|
46
69
|
* 波形系列
|
package/src/types/index.ts
CHANGED
|
@@ -13,6 +13,12 @@ export type {
|
|
|
13
13
|
WaveformZoomResetPayload,
|
|
14
14
|
WaveformAnnotationStyle,
|
|
15
15
|
WaveformAnnotation,
|
|
16
|
+
WaveformSamplingMode,
|
|
17
|
+
WaveformSamplingStrategy,
|
|
18
|
+
WaveformSamplingOptions,
|
|
19
|
+
WaveformSamplingBackend,
|
|
20
|
+
WaveformSamplingDiagnostics,
|
|
21
|
+
WaveformSamplingError,
|
|
16
22
|
WaveformRenderingOptions,
|
|
17
23
|
WaveformPlotMargin,
|
|
18
24
|
WaveformTitleTextStyle,
|
|
@@ -36,6 +42,9 @@ export type {
|
|
|
36
42
|
WaveformPointType,
|
|
37
43
|
WaveformErrorBarOptions,
|
|
38
44
|
ResolvedWaveformErrorBarOptions,
|
|
45
|
+
WaveformTypedValues,
|
|
46
|
+
TypedSampleData,
|
|
47
|
+
TypedPointData,
|
|
39
48
|
WaveformSeries,
|
|
40
49
|
WaveformData,
|
|
41
50
|
NormalizedWaveformSeries,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { routes } from '../router'
|
|
4
|
+
|
|
5
|
+
describe('WASM sampling demo route', () => {
|
|
6
|
+
it('registers the hash-demo route', () => {
|
|
7
|
+
expect(routes).toEqual(
|
|
8
|
+
expect.arrayContaining([
|
|
9
|
+
expect.objectContaining({ path: '/wasm-sampling', name: 'wasm-sampling' }),
|
|
10
|
+
]),
|
|
11
|
+
)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, reactive, ref } from 'vue'
|
|
3
|
+
import { InputNumber, RadioButton, RadioGroup, Select } from 'ant-design-vue'
|
|
4
|
+
|
|
5
|
+
import { WaveformChart } from '../components'
|
|
6
|
+
import type {
|
|
7
|
+
WaveformData,
|
|
8
|
+
WaveformRenderingOptions,
|
|
9
|
+
WaveformSamplingDiagnostics,
|
|
10
|
+
WaveformSamplingMode,
|
|
11
|
+
WaveformSamplingStrategy,
|
|
12
|
+
} from '../types'
|
|
13
|
+
|
|
14
|
+
const SERIES_COUNT = 10
|
|
15
|
+
const POINTS_PER_SERIES = 100_000
|
|
16
|
+
|
|
17
|
+
const colors = [
|
|
18
|
+
'#1677ff',
|
|
19
|
+
'#d4380d',
|
|
20
|
+
'#389e0d',
|
|
21
|
+
'#d46b08',
|
|
22
|
+
'#08979c',
|
|
23
|
+
'#c41d7f',
|
|
24
|
+
'#531dab',
|
|
25
|
+
'#096dd9',
|
|
26
|
+
'#ad6800',
|
|
27
|
+
'#237804',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const strategyOptions: Array<{ label: string; value: WaveformSamplingStrategy }> = [
|
|
31
|
+
{ label: '自动(peak)', value: 'auto' },
|
|
32
|
+
{ label: 'Peak', value: 'peak' },
|
|
33
|
+
{ label: 'LTTB', value: 'lttb' },
|
|
34
|
+
{ label: 'MinMax', value: 'minmax' },
|
|
35
|
+
{ label: 'Min', value: 'min' },
|
|
36
|
+
{ label: 'Max', value: 'max' },
|
|
37
|
+
{ label: 'Average', value: 'average' },
|
|
38
|
+
{ label: 'Sum', value: 'sum' },
|
|
39
|
+
{ label: 'None', value: 'none' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
function createDenseData(): WaveformData {
|
|
43
|
+
return {
|
|
44
|
+
kind: 'series',
|
|
45
|
+
series: Array.from({ length: SERIES_COUNT }, (_, seriesIndex) => {
|
|
46
|
+
const values = new Float32Array(POINTS_PER_SERIES)
|
|
47
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
48
|
+
const t = index / 1_000
|
|
49
|
+
const pulse = index % (8_123 + seriesIndex * 113) < 3 ? 3.5 + seriesIndex / 4 : 0
|
|
50
|
+
values[index] =
|
|
51
|
+
Math.sin(t * (0.8 + seriesIndex / 14)) * (1.2 + seriesIndex / 10) +
|
|
52
|
+
Math.cos(t * (2.1 + seriesIndex / 20)) * 0.35 +
|
|
53
|
+
pulse
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
id: `dense-${seriesIndex + 1}`,
|
|
57
|
+
trackId: 'dense-overlay',
|
|
58
|
+
name: `通道 ${String(seriesIndex + 1).padStart(2, '0')}`,
|
|
59
|
+
color: colors[seriesIndex],
|
|
60
|
+
data: { kind: 'typed-samples' as const, values, sampleRate: 1_000 },
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = createDenseData()
|
|
67
|
+
const mode = ref<WaveformSamplingMode>('auto')
|
|
68
|
+
const strategy = ref<WaveformSamplingStrategy>('peak')
|
|
69
|
+
const autoThreshold = ref(1_000)
|
|
70
|
+
const maxPointsPerPixel = ref(3)
|
|
71
|
+
const diagnostics = reactive<Record<string, WaveformSamplingDiagnostics | undefined>>({})
|
|
72
|
+
|
|
73
|
+
const rendering = computed<WaveformRenderingOptions>(() => ({
|
|
74
|
+
sampling: {
|
|
75
|
+
mode: mode.value,
|
|
76
|
+
strategy: strategy.value,
|
|
77
|
+
autoThreshold: autoThreshold.value,
|
|
78
|
+
maxPointsPerPixel: maxPointsPerPixel.value,
|
|
79
|
+
rawPointLimit: 100_000,
|
|
80
|
+
wasmFailureFallback: 'javascript',
|
|
81
|
+
},
|
|
82
|
+
}))
|
|
83
|
+
const rows = computed(() =>
|
|
84
|
+
Array.from({ length: SERIES_COUNT }, (_, index) => {
|
|
85
|
+
const id = `dense-${index + 1}`
|
|
86
|
+
return { id, name: `通道 ${String(index + 1).padStart(2, '0')}`, diagnostic: diagnostics[id] }
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
function onSamplingComplete(diagnostic: WaveformSamplingDiagnostics) {
|
|
91
|
+
diagnostics[diagnostic.seriesId] = diagnostic
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pointCount(value: number | undefined) {
|
|
95
|
+
return value === undefined ? '--' : value.toLocaleString('en-US')
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<main class="sampling-demo">
|
|
101
|
+
<header class="sampling-demo__toolbar">
|
|
102
|
+
<div>
|
|
103
|
+
<h1>WASM 多分辨率采样</h1>
|
|
104
|
+
<p>10 条 TypedArray 通道,每条 {{ POINTS_PER_SERIES.toLocaleString('en-US') }} 点</p>
|
|
105
|
+
</div>
|
|
106
|
+
<RadioGroup v-model:value="mode" button-style="solid" aria-label="采样执行模式">
|
|
107
|
+
<RadioButton value="auto">Auto</RadioButton>
|
|
108
|
+
<RadioButton value="wasm">WASM</RadioButton>
|
|
109
|
+
<RadioButton value="raw">Raw</RadioButton>
|
|
110
|
+
</RadioGroup>
|
|
111
|
+
</header>
|
|
112
|
+
|
|
113
|
+
<section class="sampling-demo__controls" aria-label="采样参数">
|
|
114
|
+
<label>
|
|
115
|
+
<span>策略</span>
|
|
116
|
+
<Select v-model:value="strategy" :options="strategyOptions" size="small" />
|
|
117
|
+
</label>
|
|
118
|
+
<label>
|
|
119
|
+
<span>阈值</span>
|
|
120
|
+
<InputNumber
|
|
121
|
+
v-model:value="autoThreshold"
|
|
122
|
+
:min="1"
|
|
123
|
+
:max="100000"
|
|
124
|
+
:step="100"
|
|
125
|
+
size="small"
|
|
126
|
+
/>
|
|
127
|
+
</label>
|
|
128
|
+
<label>
|
|
129
|
+
<span>每像素点数</span>
|
|
130
|
+
<InputNumber
|
|
131
|
+
v-model:value="maxPointsPerPixel"
|
|
132
|
+
:min="0.25"
|
|
133
|
+
:max="16"
|
|
134
|
+
:step="0.25"
|
|
135
|
+
size="small"
|
|
136
|
+
/>
|
|
137
|
+
</label>
|
|
138
|
+
</section>
|
|
139
|
+
|
|
140
|
+
<section class="sampling-demo__workspace">
|
|
141
|
+
<div class="sampling-demo__chart">
|
|
142
|
+
<WaveformChart
|
|
143
|
+
:data="data"
|
|
144
|
+
:rendering="rendering"
|
|
145
|
+
:grid="{ rowCount: 1, columnCount: 1, showPagination: false }"
|
|
146
|
+
:legend="{ position: 'top-right', orientation: 'vertical' }"
|
|
147
|
+
:title="{ visible: true, text: '多系列采样与缓存诊断', align: 'left' }"
|
|
148
|
+
x-label="时间(s)"
|
|
149
|
+
time-unit="s"
|
|
150
|
+
@sampling-complete="onSamplingComplete"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<aside class="sampling-demo__diagnostics" aria-label="每系列采样诊断">
|
|
155
|
+
<div class="sampling-demo__diagnostic-header">
|
|
156
|
+
<span>系列</span>
|
|
157
|
+
<span>后端</span>
|
|
158
|
+
<span>源点</span>
|
|
159
|
+
<span>可见</span>
|
|
160
|
+
<span>渲染</span>
|
|
161
|
+
<span>耗时</span>
|
|
162
|
+
<span>缓存</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div v-for="row in rows" :key="row.id" class="sampling-demo__diagnostic-row">
|
|
165
|
+
<span data-label="系列">{{ row.name }}</span>
|
|
166
|
+
<span data-label="后端">{{ row.diagnostic?.backend ?? '--' }}</span>
|
|
167
|
+
<span data-label="源点">{{ pointCount(row.diagnostic?.sourcePointCount) }}</span>
|
|
168
|
+
<span data-label="可见">{{ pointCount(row.diagnostic?.visiblePointCount) }}</span>
|
|
169
|
+
<span data-label="渲染">{{ pointCount(row.diagnostic?.renderedPointCount) }}</span>
|
|
170
|
+
<span data-label="耗时">{{
|
|
171
|
+
row.diagnostic ? `${row.diagnostic.durationMs.toFixed(2)} ms` : '--'
|
|
172
|
+
}}</span>
|
|
173
|
+
<span data-label="缓存" :class="{ 'is-hit': row.diagnostic?.cacheHit }">
|
|
174
|
+
{{ row.diagnostic ? (row.diagnostic.cacheHit ? '命中' : '未命中') : '--' }}
|
|
175
|
+
</span>
|
|
176
|
+
</div>
|
|
177
|
+
</aside>
|
|
178
|
+
</section>
|
|
179
|
+
</main>
|
|
180
|
+
</template>
|