waveform-analysis 0.1.71 → 0.1.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +241 -39
- package/dist/assets/{workerRuntime-o5An0M-W.js → workerRuntime-5p-EI-GL.js} +1 -1
- package/dist/index.cjs +2 -1
- package/dist/index.js +2922 -1910
- package/dist/style.css +1 -1
- package/dist/types/components/WaveformChart.vue.d.ts +24 -6
- package/dist/types/components/annotation/WaveformAnnotationLayer.vue.d.ts +2 -0
- package/dist/types/components/controls/WaveformToolbar.vue.d.ts +7 -0
- package/dist/types/components/controls/chartExportOptions.d.ts +5 -0
- package/dist/types/components/controls/chartSnapshot.d.ts +2 -0
- package/dist/types/components/controls/snapshotTitle.d.ts +2 -0
- package/dist/types/components/controls/toolbarRegistry.d.ts +21 -0
- package/dist/types/components/controls/useChartCommands.d.ts +42 -0
- package/dist/types/components/controls/useChartImageExport.d.ts +17 -0
- package/dist/types/components/controls/useControlMode.d.ts +8 -0
- package/dist/types/components/core/latestTaskScheduler.d.ts +1 -1
- package/dist/types/components/core/layout.d.ts +5 -3
- package/dist/types/components/core/niceScale.d.ts +34 -0
- package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -1
- package/dist/types/components/core/trackPagination.d.ts +1 -1
- package/dist/types/components/core/types.d.ts +3 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +24 -5
- package/dist/types/components/core/useWaveformChartLifecycle.d.ts +2 -2
- package/dist/types/components/core/useWaveformData.d.ts +1 -0
- package/dist/types/components/core/useWaveformLayout.d.ts +4 -1
- package/dist/types/components/core/useWaveformMeasurements.d.ts +2 -0
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +1 -0
- package/dist/types/components/core/useWaveformViewportRestore.d.ts +5 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +5 -0
- package/dist/types/components/core/waveformSamplingSession.d.ts +24 -0
- package/dist/types/components/core/yAxisLabelStyle.d.ts +3 -0
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
- package/dist/types/components/core/yAxisPadding.d.ts +7 -0
- package/dist/types/components/core/yAxisTitleLayout.d.ts +2 -0
- package/dist/types/components/interaction/useWaveformViewport.d.ts +1 -0
- package/dist/types/components/rendering/WaveformLegend.vue.d.ts +2 -2
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +2 -3
- package/dist/types/components/rendering/WaveformTrackAxes.vue.d.ts +0 -1
- package/dist/types/components/rendering/WaveformTrackBackdrop.vue.d.ts +1 -0
- package/dist/types/components/rendering/compactFrame.d.ts +5 -0
- package/dist/types/components/rendering/yAxisTickVisibility.d.ts +11 -0
- package/dist/types/core/lineBoundary.d.ts +4 -0
- package/dist/types/core/rendering.d.ts +4 -0
- package/dist/types/core/renderingStrategies.d.ts +2 -0
- package/dist/types/core/workerSampling/outputBoundaries.d.ts +4 -0
- package/dist/types/data/simulatedWaveforms.d.ts +1 -1
- package/dist/types/demo/DemoYAxisPaddingControls.vue.d.ts +11 -0
- package/dist/types/demo/types.d.ts +6 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/types/chart.d.ts +30 -8
- package/dist/types/types/controls.d.ts +69 -0
- package/dist/types/types/index.d.ts +2 -1
- package/dist/types/utils/formatters.d.ts +1 -0
- package/package.json +7 -6
- package/src/App.vue +58 -14
- package/src/components/WaveformChart.css +10 -1
- package/src/components/WaveformChart.vue +6 -0
- package/src/components/WaveformChartView.vue +27 -23
- package/src/components/annotation/WaveformAnnotationLayer.vue +6 -1
- package/src/components/annotation/useWaveformChartAnnotations.ts +6 -1
- package/src/components/controls/WaveformToolbar.vue +248 -0
- package/src/components/controls/chartExportOptions.ts +32 -0
- package/src/components/controls/chartSnapshot.ts +132 -0
- package/src/components/controls/snapshotTitle.ts +73 -0
- package/src/components/controls/toolbarRegistry.ts +92 -0
- package/src/components/controls/useChartCommands.ts +294 -0
- package/src/components/controls/useChartImageExport.ts +155 -0
- package/src/components/controls/useControlMode.ts +30 -0
- package/src/components/core/latestTaskScheduler.ts +14 -2
- package/src/components/core/layout.ts +35 -34
- package/src/components/core/niceScale.ts +151 -0
- package/src/components/core/trackLayoutBuilder.ts +34 -17
- package/src/components/core/trackPagination.ts +3 -3
- package/src/components/core/types.ts +3 -0
- package/src/components/core/useWaveformChartController.ts +94 -3
- package/src/components/core/useWaveformChartLifecycle.ts +11 -111
- package/src/components/core/useWaveformData.ts +2 -0
- package/src/components/core/useWaveformLayout.ts +17 -5
- package/src/components/core/useWaveformMeasurements.ts +63 -0
- package/src/components/core/useWaveformRenderSampling.ts +80 -59
- package/src/components/core/useWaveformViewportRestore.ts +105 -0
- package/src/components/core/useWaveformYAxisLayout.ts +3 -2
- package/src/components/core/waveformChartTypes.ts +5 -0
- package/src/components/core/waveformSamplingSession.ts +49 -0
- package/src/components/core/workerSamplingClient.ts +38 -6
- package/src/components/core/yAxisLabelStyle.ts +10 -0
- package/src/components/core/yAxisLayoutMetrics.ts +10 -9
- package/src/components/core/yAxisPadding.ts +45 -0
- package/src/components/core/yAxisTitleLayout.ts +11 -12
- package/src/components/interaction/useWaveformViewport.ts +11 -2
- package/src/components/rendering/WaveformTrack.css +1 -2
- package/src/components/rendering/WaveformTrack.vue +4 -12
- package/src/components/rendering/WaveformTrackAxes.vue +48 -19
- package/src/components/rendering/WaveformTrackBackdrop.vue +8 -3
- package/src/components/rendering/compactFrame.ts +31 -0
- package/src/components/rendering/yAxisTickVisibility.ts +95 -0
- package/src/core/lineBoundary.ts +24 -0
- package/src/core/rendering.ts +48 -25
- package/src/core/renderingStrategies.ts +19 -13
- package/src/core/waveformPointSource.ts +23 -1
- package/src/core/workerSampling/outputBoundaries.ts +32 -0
- package/src/core/workerSampling/repository.ts +26 -18
- package/src/core/workerSampling/repositoryDataset.ts +33 -1
- package/src/core/workerSampling/workerRuntime.ts +13 -3
- package/src/data/simulatedWaveforms.ts +57 -2
- package/src/demo/DemoChartHost.vue +5 -1
- package/src/demo/DemoGridFrameControls.vue +2 -0
- package/src/demo/DemoViewControls.vue +5 -2
- package/src/demo/DemoYAxisPaddingControls.vue +46 -0
- package/src/demo/embedBridge.ts +85 -0
- package/src/demo/types.ts +6 -0
- package/src/index.ts +18 -0
- package/src/types/chart.ts +32 -8
- package/src/types/controls.ts +80 -0
- package/src/types/index.ts +18 -0
- package/src/utils/formatters.ts +2 -1
- package/src/views/WasmSamplingDemo.vue +8 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { nextTick, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
2
|
+
import type { LifecycleContext } from './useWaveformChartLifecycle'
|
|
3
|
+
|
|
4
|
+
export function useWaveformMeasurements(
|
|
5
|
+
context: Pick<
|
|
6
|
+
LifecycleContext,
|
|
7
|
+
| 'container'
|
|
8
|
+
| 'titleMeasureElement'
|
|
9
|
+
| 'resizeObserver'
|
|
10
|
+
| 'observedWidth'
|
|
11
|
+
| 'observedHeight'
|
|
12
|
+
| 'measuredTitleWidth'
|
|
13
|
+
| 'measuredTitleHeight'
|
|
14
|
+
| 'resolvedTitleText'
|
|
15
|
+
| 'titleAreaReserved'
|
|
16
|
+
| 'titleMeasureStyle'
|
|
17
|
+
>,
|
|
18
|
+
) {
|
|
19
|
+
const {
|
|
20
|
+
container,
|
|
21
|
+
titleMeasureElement,
|
|
22
|
+
resizeObserver,
|
|
23
|
+
observedWidth,
|
|
24
|
+
observedHeight,
|
|
25
|
+
measuredTitleWidth,
|
|
26
|
+
measuredTitleHeight,
|
|
27
|
+
resolvedTitleText,
|
|
28
|
+
titleAreaReserved,
|
|
29
|
+
titleMeasureStyle,
|
|
30
|
+
} = context
|
|
31
|
+
function measureTitle() {
|
|
32
|
+
if (!titleAreaReserved.value || !titleMeasureElement.value) {
|
|
33
|
+
measuredTitleWidth.value = 0
|
|
34
|
+
measuredTitleHeight.value = 0
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const bounds = titleMeasureElement.value.getBoundingClientRect()
|
|
38
|
+
measuredTitleWidth.value = titleMeasureElement.value.scrollWidth || bounds.width
|
|
39
|
+
measuredTitleHeight.value = titleMeasureElement.value.scrollHeight || bounds.height
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
watch(
|
|
43
|
+
[resolvedTitleText, titleAreaReserved, titleMeasureStyle],
|
|
44
|
+
async () => {
|
|
45
|
+
measuredTitleWidth.value = 0
|
|
46
|
+
measuredTitleHeight.value = 0
|
|
47
|
+
await nextTick()
|
|
48
|
+
measureTitle()
|
|
49
|
+
},
|
|
50
|
+
{ immediate: true },
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
onMounted(() => {
|
|
54
|
+
if (!container.value) return
|
|
55
|
+
resizeObserver.value = new ResizeObserver(([entry]) => {
|
|
56
|
+
observedWidth.value = Math.max(0, entry?.contentRect.width ?? 0)
|
|
57
|
+
observedHeight.value = Math.max(0, entry?.contentRect.height ?? 0)
|
|
58
|
+
void nextTick(measureTitle)
|
|
59
|
+
})
|
|
60
|
+
resizeObserver.value.observe(container.value)
|
|
61
|
+
})
|
|
62
|
+
onBeforeUnmount(() => resizeObserver.value?.disconnect())
|
|
63
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createWaveformSamplingSession } from './waveformSamplingSession'
|
|
2
|
+
import { lineBoundaryIndexes, withLineBoundaries } from '../../core/lineBoundary'
|
|
1
3
|
import { computed, onScopeDispose, shallowRef, watch, type ComputedRef, type ShallowRef } from 'vue'
|
|
2
4
|
|
|
3
5
|
import {
|
|
@@ -11,12 +13,8 @@ import { pointSourceFromPoints, type WaveformPointSource } from '../../core/wave
|
|
|
11
13
|
import type { PreparedWaveformSeries } from './useWaveformData'
|
|
12
14
|
import type { DisplaySeries, TrackLayout } from './types'
|
|
13
15
|
import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes'
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
createWorkerSamplingClient,
|
|
17
|
-
isDatasetResponse,
|
|
18
|
-
type WorkerSamplingClient,
|
|
19
|
-
} from './workerSamplingClient'
|
|
16
|
+
import { resolveAutoSelectedMode } from './latestTaskScheduler'
|
|
17
|
+
import { isDatasetResponse, type WorkerSamplingClient } from './workerSamplingClient'
|
|
20
18
|
|
|
21
19
|
interface SamplingTarget {
|
|
22
20
|
series: DisplaySeries
|
|
@@ -62,7 +60,8 @@ function rawDiagnostics(target: SamplingTarget, requestId: number): WorkerSampli
|
|
|
62
60
|
strategy: sampling.strategy === 'auto' ? 'peak' : sampling.strategy,
|
|
63
61
|
sourcePointCount: target.source.length,
|
|
64
62
|
visiblePointCount,
|
|
65
|
-
renderedPointCount:
|
|
63
|
+
renderedPointCount:
|
|
64
|
+
visiblePointCount + lineBoundaryIndexes(target.visibleRange, target.source.length).length,
|
|
66
65
|
durationMs: 0,
|
|
67
66
|
cacheHit: false,
|
|
68
67
|
requestId,
|
|
@@ -75,7 +74,7 @@ function rawDiagnostics(target: SamplingTarget, requestId: number): WorkerSampli
|
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
function
|
|
77
|
+
function fallbackInteriorPoints(target: SamplingTarget) {
|
|
79
78
|
const { source } = target
|
|
80
79
|
const { start, end } = target.visibleRange
|
|
81
80
|
const count = end - start
|
|
@@ -104,18 +103,17 @@ function errorPayload(
|
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
export function useWaveformRenderSampling(context: SamplingContext) {
|
|
106
|
+
const settledSignature = shallowRef<string>()
|
|
107
107
|
const dataEpoch = shallowRef(0)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
let autoModeSettleTimer: ReturnType<typeof setTimeout> | undefined
|
|
118
|
-
let hasInitialSamplingRun = false
|
|
108
|
+
const session = createWaveformSamplingSession()
|
|
109
|
+
const {
|
|
110
|
+
revisions,
|
|
111
|
+
backendBySeries,
|
|
112
|
+
selectedModeBySeries,
|
|
113
|
+
emittedErrors,
|
|
114
|
+
pendingDiagnostics,
|
|
115
|
+
scheduler: samplingScheduler,
|
|
116
|
+
} = session
|
|
119
117
|
|
|
120
118
|
const targets = computed(() => {
|
|
121
119
|
const sampling = context.renderingOptions.value.sampling
|
|
@@ -169,7 +167,7 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
169
167
|
)
|
|
170
168
|
|
|
171
169
|
const flushDiagnostics = () => {
|
|
172
|
-
diagnosticsTimer = undefined
|
|
170
|
+
session.diagnosticsTimer = undefined
|
|
173
171
|
pendingDiagnostics.forEach((diagnostic) => {
|
|
174
172
|
const previousBackend = backendBySeries.get(diagnostic.seriesId)
|
|
175
173
|
if (previousBackend && previousBackend !== diagnostic.backend) {
|
|
@@ -193,7 +191,7 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
193
191
|
maxPendingRequestCount: samplingScheduler.metrics.maxPending,
|
|
194
192
|
}),
|
|
195
193
|
)
|
|
196
|
-
if (!diagnosticsTimer) diagnosticsTimer = setTimeout(flushDiagnostics, 100)
|
|
194
|
+
if (!session.diagnosticsTimer) session.diagnosticsTimer = setTimeout(flushDiagnostics, 100)
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
const emitErrorOnce = (payload: WaveformSamplingError) => {
|
|
@@ -213,7 +211,7 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
213
211
|
if (revisions.has(target.request.datasetId)) return
|
|
214
212
|
const response = await workerClient.send({
|
|
215
213
|
type: 'register-dataset',
|
|
216
|
-
requestId: ++requestId,
|
|
214
|
+
requestId: ++session.requestId,
|
|
217
215
|
datasetId: target.request.datasetId,
|
|
218
216
|
revision: 0,
|
|
219
217
|
dataset: target.source.toWorkerDataset(),
|
|
@@ -249,15 +247,22 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
249
247
|
previousSelectedMode,
|
|
250
248
|
) === 'raw'
|
|
251
249
|
if (target.request.mode === 'raw' || autoRaw) {
|
|
252
|
-
nextOverrides[target.series.id] =
|
|
253
|
-
target.visibleRange.start,
|
|
254
|
-
target.visibleRange
|
|
250
|
+
nextOverrides[target.series.id] = withLineBoundaries(
|
|
251
|
+
target.source.pointsInRange(target.visibleRange.start, target.visibleRange.end),
|
|
252
|
+
target.visibleRange,
|
|
253
|
+
target.source.length,
|
|
254
|
+
(index) => target.source.pointAt(index)!,
|
|
255
255
|
)
|
|
256
256
|
selectedModeBySeries.set(target.series.id, 'raw')
|
|
257
257
|
diagnostics.push(rawDiagnostics(target, token))
|
|
258
258
|
} else {
|
|
259
259
|
if (!nextOverrides[target.series.id]?.length) {
|
|
260
|
-
nextOverrides[target.series.id] =
|
|
260
|
+
nextOverrides[target.series.id] = withLineBoundaries(
|
|
261
|
+
fallbackInteriorPoints(target),
|
|
262
|
+
target.visibleRange,
|
|
263
|
+
target.source.length,
|
|
264
|
+
(index) => target.source.pointAt(index)!,
|
|
265
|
+
)
|
|
261
266
|
}
|
|
262
267
|
sampledTargets.push({
|
|
263
268
|
...target,
|
|
@@ -273,18 +278,18 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
273
278
|
}
|
|
274
279
|
|
|
275
280
|
context.linePointOverrides.value = nextOverrides
|
|
276
|
-
client
|
|
277
|
-
await registerTargets(client, sampledTargets, token)
|
|
278
|
-
if (!samplingScheduler.isCurrent(token)) return
|
|
279
|
-
const batch = sampledTargets.map((target) => ({
|
|
280
|
-
...target.request,
|
|
281
|
-
revision: revisions.get(target.request.datasetId) ?? 1,
|
|
282
|
-
}))
|
|
281
|
+
const client = session.getClient()
|
|
283
282
|
let response: SampleViewportResponse
|
|
284
283
|
try {
|
|
284
|
+
await registerTargets(client, sampledTargets, token)
|
|
285
|
+
if (!samplingScheduler.isCurrent(token)) return
|
|
286
|
+
const batch = sampledTargets.map((target) => ({
|
|
287
|
+
...target.request,
|
|
288
|
+
revision: revisions.get(target.request.datasetId) ?? 1,
|
|
289
|
+
}))
|
|
285
290
|
const result = await client.send({
|
|
286
291
|
type: 'sample-viewport',
|
|
287
|
-
requestId: ++requestId,
|
|
292
|
+
requestId: ++session.requestId,
|
|
288
293
|
series: batch,
|
|
289
294
|
})
|
|
290
295
|
if (result.type !== 'sample-viewport-response') return
|
|
@@ -295,7 +300,10 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
295
300
|
errorPayload(
|
|
296
301
|
error instanceof Error ? error.message : String(error),
|
|
297
302
|
context.renderingOptions.value.sampling.mode,
|
|
298
|
-
'
|
|
303
|
+
context.renderingOptions.value.sampling.mode === 'wasm' &&
|
|
304
|
+
context.renderingOptions.value.sampling.wasmFailureFallback === 'error'
|
|
305
|
+
? 'none'
|
|
306
|
+
: 'javascript',
|
|
299
307
|
sampledTargets.map((target) => target.series.id),
|
|
300
308
|
),
|
|
301
309
|
)
|
|
@@ -322,12 +330,18 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
322
330
|
)
|
|
323
331
|
}
|
|
324
332
|
})
|
|
325
|
-
if (
|
|
333
|
+
if (
|
|
334
|
+
response.workerError &&
|
|
335
|
+
response.results.every((result) => result.status !== 'wasm-unavailable')
|
|
336
|
+
) {
|
|
326
337
|
emitErrorOnce(
|
|
327
338
|
errorPayload(
|
|
328
339
|
response.workerError,
|
|
329
340
|
context.renderingOptions.value.sampling.mode,
|
|
330
|
-
'
|
|
341
|
+
context.renderingOptions.value.sampling.mode === 'wasm' &&
|
|
342
|
+
context.renderingOptions.value.sampling.wasmFailureFallback === 'error'
|
|
343
|
+
? 'none'
|
|
344
|
+
: 'javascript',
|
|
331
345
|
sampledTargets.map((target) => target.series.id),
|
|
332
346
|
),
|
|
333
347
|
)
|
|
@@ -340,7 +354,10 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
340
354
|
errorPayload(
|
|
341
355
|
client.workerFailureReason,
|
|
342
356
|
context.renderingOptions.value.sampling.mode,
|
|
343
|
-
'
|
|
357
|
+
context.renderingOptions.value.sampling.mode === 'wasm' &&
|
|
358
|
+
context.renderingOptions.value.sampling.wasmFailureFallback === 'error'
|
|
359
|
+
? 'none'
|
|
360
|
+
: 'javascript',
|
|
344
361
|
sampledTargets.map((target) => target.series.id),
|
|
345
362
|
),
|
|
346
363
|
)
|
|
@@ -349,21 +366,33 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
349
366
|
emitDiagnostics(diagnostics)
|
|
350
367
|
}
|
|
351
368
|
|
|
369
|
+
const scheduleSampling = (hysteresis: boolean) => {
|
|
370
|
+
settledSignature.value = undefined
|
|
371
|
+
const signature = targetSignature.value
|
|
372
|
+
samplingScheduler.schedule(async (token) => {
|
|
373
|
+
try {
|
|
374
|
+
await runSampling(token, hysteresis)
|
|
375
|
+
} finally {
|
|
376
|
+
if (samplingScheduler.isCurrent(token)) settledSignature.value = signature
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
352
381
|
watch(
|
|
353
382
|
targetSignature,
|
|
354
383
|
() => {
|
|
355
|
-
if (autoModeSettleTimer) clearTimeout(autoModeSettleTimer)
|
|
356
|
-
const useAutoHysteresis = hasInitialSamplingRun
|
|
357
|
-
hasInitialSamplingRun = true
|
|
358
|
-
|
|
384
|
+
if (session.autoModeSettleTimer) clearTimeout(session.autoModeSettleTimer)
|
|
385
|
+
const useAutoHysteresis = session.hasInitialSamplingRun
|
|
386
|
+
session.hasInitialSamplingRun = true
|
|
387
|
+
scheduleSampling(useAutoHysteresis)
|
|
359
388
|
if (
|
|
360
389
|
useAutoHysteresis &&
|
|
361
390
|
context.renderingOptions.value.sampling.mode === 'auto' &&
|
|
362
391
|
context.renderingOptions.value.sampling.autoHysteresis > 0
|
|
363
392
|
) {
|
|
364
|
-
autoModeSettleTimer = setTimeout(() => {
|
|
365
|
-
autoModeSettleTimer = undefined
|
|
366
|
-
|
|
393
|
+
session.autoModeSettleTimer = setTimeout(() => {
|
|
394
|
+
session.autoModeSettleTimer = undefined
|
|
395
|
+
scheduleSampling(false)
|
|
367
396
|
}, AUTO_MODE_SETTLE_DELAY_MS)
|
|
368
397
|
}
|
|
369
398
|
},
|
|
@@ -372,24 +401,16 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
372
401
|
watch(
|
|
373
402
|
context.preparedSeries,
|
|
374
403
|
() => {
|
|
375
|
-
|
|
404
|
+
session.invalidate()
|
|
376
405
|
dataEpoch.value += 1
|
|
377
406
|
context.linePointOverrides.value = {}
|
|
378
|
-
revisions.clear()
|
|
379
|
-
backendBySeries.clear()
|
|
380
|
-
selectedModeBySeries.clear()
|
|
381
|
-
emittedErrors.clear()
|
|
382
|
-
client?.dispose()
|
|
383
|
-
client = undefined
|
|
384
407
|
},
|
|
385
408
|
{ flush: 'sync' },
|
|
386
409
|
)
|
|
387
|
-
onScopeDispose(() =>
|
|
388
|
-
if (diagnosticsTimer) clearTimeout(diagnosticsTimer)
|
|
389
|
-
if (autoModeSettleTimer) clearTimeout(autoModeSettleTimer)
|
|
390
|
-
samplingScheduler.dispose()
|
|
391
|
-
client?.dispose()
|
|
392
|
-
})
|
|
410
|
+
onScopeDispose(() => session.dispose())
|
|
393
411
|
|
|
394
|
-
return {
|
|
412
|
+
return {
|
|
413
|
+
linePointOverrides: context.linePointOverrides,
|
|
414
|
+
ready: () => settledSignature.value === targetSignature.value && !session.autoModeSettleTimer,
|
|
415
|
+
}
|
|
395
416
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { remotePanBoundary, restoreViewportDomain } from '../interaction/remoteViewport'
|
|
2
|
+
import { nextTick } from 'vue'
|
|
3
|
+
import { zoomIdentity } from 'd3'
|
|
4
|
+
import { transformForDomain } from '../interaction/zoomConstraints'
|
|
5
|
+
import { seriesIdentity } from '../interaction/zoomEventPayload'
|
|
6
|
+
import type { LifecycleContext } from './useWaveformChartLifecycle'
|
|
7
|
+
|
|
8
|
+
export function useWaveformViewportRestore(
|
|
9
|
+
context: Pick<
|
|
10
|
+
LifecycleContext,
|
|
11
|
+
| 'preserveFullViewport'
|
|
12
|
+
| 'props'
|
|
13
|
+
| 'trackLayouts'
|
|
14
|
+
| 'resolveInitialTrackDomain'
|
|
15
|
+
| 'sharedZoomDomain'
|
|
16
|
+
| 'initialXDomain'
|
|
17
|
+
| 'independentTransforms'
|
|
18
|
+
| 'chartTracks'
|
|
19
|
+
| 'clearHover'
|
|
20
|
+
| 'editorSeriesOptions'
|
|
21
|
+
| 'sharedTransform'
|
|
22
|
+
| 'innerWidth'
|
|
23
|
+
| 'configureZoom'
|
|
24
|
+
>,
|
|
25
|
+
) {
|
|
26
|
+
const {
|
|
27
|
+
props,
|
|
28
|
+
trackLayouts,
|
|
29
|
+
resolveInitialTrackDomain,
|
|
30
|
+
sharedZoomDomain,
|
|
31
|
+
initialXDomain,
|
|
32
|
+
independentTransforms,
|
|
33
|
+
chartTracks,
|
|
34
|
+
clearHover,
|
|
35
|
+
editorSeriesOptions,
|
|
36
|
+
sharedTransform,
|
|
37
|
+
innerWidth,
|
|
38
|
+
configureZoom,
|
|
39
|
+
} = context
|
|
40
|
+
let pendingSharedXDomain: [number, number] | undefined
|
|
41
|
+
let pendingIndependentXDomains: Map<string, [number, number]> | undefined
|
|
42
|
+
|
|
43
|
+
function handleBeforeDataReferenceChange() {
|
|
44
|
+
if (props.displayMode === 'independent') {
|
|
45
|
+
pendingSharedXDomain = undefined
|
|
46
|
+
pendingIndependentXDomains = new Map(
|
|
47
|
+
trackLayouts.value.flatMap((track) => {
|
|
48
|
+
const current = track.xScale.domain() as [number, number]
|
|
49
|
+
const boundary = resolveInitialTrackDomain(track)
|
|
50
|
+
return context.preserveFullViewport?.() || remotePanBoundary(props) ||
|
|
51
|
+
current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
|
|
52
|
+
? [[seriesIdentity(track.seriesList.map((series) => series.id)), current]]
|
|
53
|
+
: []
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
pendingIndependentXDomains = undefined
|
|
59
|
+
const current = sharedZoomDomain.value
|
|
60
|
+
const boundary = initialXDomain.value
|
|
61
|
+
pendingSharedXDomain =
|
|
62
|
+
context.preserveFullViewport?.() || remotePanBoundary(props) ||
|
|
63
|
+
current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
|
|
64
|
+
? [...current]
|
|
65
|
+
: undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleDataReferenceChange() {
|
|
69
|
+
if (props.displayMode === 'independent') {
|
|
70
|
+
const currentTransforms = independentTransforms.value
|
|
71
|
+
independentTransforms.value = chartTracks.value.map(
|
|
72
|
+
(_track, index) => currentTransforms[index] ?? zoomIdentity,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
clearHover()
|
|
76
|
+
editorSeriesOptions.value = []
|
|
77
|
+
void nextTick(() => {
|
|
78
|
+
if (props.displayMode === 'independent' && pendingIndependentXDomains) {
|
|
79
|
+
const nextTransforms = chartTracks.value.map(() => zoomIdentity)
|
|
80
|
+
trackLayouts.value.forEach((track) => {
|
|
81
|
+
const previousDomain = pendingIndependentXDomains?.get(
|
|
82
|
+
seriesIdentity(track.seriesList.map((series) => series.id)),
|
|
83
|
+
)
|
|
84
|
+
if (!previousDomain) return
|
|
85
|
+
const boundary = resolveInitialTrackDomain(track)
|
|
86
|
+
const domain = restoreViewportDomain(previousDomain, boundary, [track.seriesList], props)
|
|
87
|
+
nextTransforms[track.index] = transformForDomain(domain, boundary, track.width)
|
|
88
|
+
})
|
|
89
|
+
independentTransforms.value = nextTransforms
|
|
90
|
+
pendingIndependentXDomains = undefined
|
|
91
|
+
} else if (props.displayMode !== 'independent' && pendingSharedXDomain) {
|
|
92
|
+
const boundary = initialXDomain.value
|
|
93
|
+
const groups = trackLayouts.value
|
|
94
|
+
.filter((track) => track.hasVisibleSeries)
|
|
95
|
+
.map((track) => track.seriesList)
|
|
96
|
+
const domain = restoreViewportDomain(pendingSharedXDomain, boundary, groups, props)
|
|
97
|
+
sharedTransform.value = transformForDomain(domain, boundary, innerWidth.value)
|
|
98
|
+
pendingSharedXDomain = undefined
|
|
99
|
+
}
|
|
100
|
+
configureZoom()
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { handleBeforeDataReferenceChange, handleDataReferenceChange }
|
|
105
|
+
}
|
|
@@ -32,6 +32,7 @@ export function useWaveformYAxisLayout(context: YAxisLayoutContext) {
|
|
|
32
32
|
const isEdgeCompact = computed(() => props.layoutPreset === 'edge-compact')
|
|
33
33
|
const showUnits = computed(
|
|
34
34
|
() =>
|
|
35
|
+
props.unitDisplayMode !== 'channel-label' &&
|
|
35
36
|
props.unitDisplayMode !== 'channel-label-or-legend' &&
|
|
36
37
|
(!isEdgeCompact.value || props.unitDisplayMode === 'axis'),
|
|
37
38
|
)
|
|
@@ -45,7 +46,7 @@ export function useWaveformYAxisLayout(context: YAxisLayoutContext) {
|
|
|
45
46
|
props.axes?.y?.nice !== false,
|
|
46
47
|
yAxisTickCount.value,
|
|
47
48
|
props.displayMode === 'compact',
|
|
48
|
-
props.axes?.y
|
|
49
|
+
props.axes?.y,
|
|
49
50
|
showUnits.value,
|
|
50
51
|
isEdgeCompact.value ? measureYAxisTextWidth : undefined,
|
|
51
52
|
),
|
|
@@ -80,7 +81,7 @@ export function useWaveformYAxisLayout(context: YAxisLayoutContext) {
|
|
|
80
81
|
props.axes?.y?.nice !== false,
|
|
81
82
|
props.displayMode === 'compact',
|
|
82
83
|
viewportYDomains.value,
|
|
83
|
-
props.axes?.y
|
|
84
|
+
props.axes?.y,
|
|
84
85
|
showUnits.value,
|
|
85
86
|
isEdgeCompact.value ? measureYAxisTextWidth : undefined,
|
|
86
87
|
),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WaveformToolbarOptions, WaveformControlState } from '../../types/controls'
|
|
1
2
|
import type {
|
|
2
3
|
WaveformAnnotation,
|
|
3
4
|
WaveformAxesOptions,
|
|
@@ -26,6 +27,7 @@ import type {
|
|
|
26
27
|
import type { WaveformGridOptions } from './grid'
|
|
27
28
|
|
|
28
29
|
export interface WaveformChartProps {
|
|
30
|
+
toolbar?: boolean | WaveformToolbarOptions
|
|
29
31
|
data: WaveformData
|
|
30
32
|
displayMode?: WaveformDisplayMode
|
|
31
33
|
overlayMode?: WaveformOverlayMode
|
|
@@ -104,6 +106,9 @@ export type ResolvedWaveformChartProps = Readonly<
|
|
|
104
106
|
>
|
|
105
107
|
|
|
106
108
|
export interface WaveformChartEmit {
|
|
109
|
+
(event: 'update:interactionMode', mode: WaveformInteractionMode): void
|
|
110
|
+
(event: 'interaction-mode-change', mode: WaveformInteractionMode): void
|
|
111
|
+
(event: 'control-state-change', state: WaveformControlState): void
|
|
107
112
|
(event: 'point-hover', point: WaveformPoint | null): void
|
|
108
113
|
(event: 'zoom-intent', payload: WaveformZoomIntentPayload): void
|
|
109
114
|
(event: 'zoom-change', domain: [number, number]): void
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createLatestTaskScheduler } from './latestTaskScheduler'
|
|
2
|
+
import { createWorkerSamplingClient, type WorkerSamplingClient } from './workerSamplingClient'
|
|
3
|
+
import type { WorkerSamplingDiagnostics } from '../../core/workerSampling'
|
|
4
|
+
|
|
5
|
+
/** Owns resources for one immutable data reference. Invalidation cancels old work before reuse. */
|
|
6
|
+
export function createWaveformSamplingSession() {
|
|
7
|
+
const scheduler = createLatestTaskScheduler()
|
|
8
|
+
const revisions = new Map<string, number>()
|
|
9
|
+
const backendBySeries = new Map<string, WorkerSamplingDiagnostics['backend']>()
|
|
10
|
+
const selectedModeBySeries = new Map<string, 'raw' | 'sampled'>()
|
|
11
|
+
const emittedErrors = new Set<string>()
|
|
12
|
+
const pendingDiagnostics = new Map<string, WorkerSamplingDiagnostics>()
|
|
13
|
+
const session = {
|
|
14
|
+
scheduler,
|
|
15
|
+
revisions,
|
|
16
|
+
backendBySeries,
|
|
17
|
+
selectedModeBySeries,
|
|
18
|
+
emittedErrors,
|
|
19
|
+
pendingDiagnostics,
|
|
20
|
+
client: undefined as WorkerSamplingClient | undefined,
|
|
21
|
+
requestId: 0,
|
|
22
|
+
diagnosticsTimer: undefined as ReturnType<typeof setTimeout> | undefined,
|
|
23
|
+
autoModeSettleTimer: undefined as ReturnType<typeof setTimeout> | undefined,
|
|
24
|
+
hasInitialSamplingRun: false,
|
|
25
|
+
getClient() {
|
|
26
|
+
return (session.client ??= createWorkerSamplingClient())
|
|
27
|
+
},
|
|
28
|
+
invalidate() {
|
|
29
|
+
scheduler.cancelPending()
|
|
30
|
+
clearTimeout(session.diagnosticsTimer)
|
|
31
|
+
clearTimeout(session.autoModeSettleTimer)
|
|
32
|
+
session.diagnosticsTimer = undefined
|
|
33
|
+
session.autoModeSettleTimer = undefined
|
|
34
|
+
session.hasInitialSamplingRun = false
|
|
35
|
+
pendingDiagnostics.clear()
|
|
36
|
+
revisions.clear()
|
|
37
|
+
backendBySeries.clear()
|
|
38
|
+
selectedModeBySeries.clear()
|
|
39
|
+
emittedErrors.clear()
|
|
40
|
+
session.client?.dispose()
|
|
41
|
+
session.client = undefined
|
|
42
|
+
},
|
|
43
|
+
dispose() {
|
|
44
|
+
session.invalidate()
|
|
45
|
+
scheduler.dispose()
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
return session
|
|
49
|
+
}
|
|
@@ -30,6 +30,7 @@ function workerFailureMessage(event?: ErrorEvent) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function createWorkerSamplingClient(): WorkerSamplingClient {
|
|
33
|
+
let disposed = false
|
|
33
34
|
let worker: Worker | undefined
|
|
34
35
|
let fallback: WorkerSamplingRepository | undefined
|
|
35
36
|
let workerFailureReason: string | undefined
|
|
@@ -37,7 +38,8 @@ export function createWorkerSamplingClient(): WorkerSamplingClient {
|
|
|
37
38
|
const datasets = new Map<string, StoredDataset>()
|
|
38
39
|
|
|
39
40
|
const fallbackRepository = () => {
|
|
40
|
-
fallback
|
|
41
|
+
if (fallback) return fallback
|
|
42
|
+
fallback = new WorkerSamplingRepository()
|
|
41
43
|
for (const [datasetId, dataset] of datasets) {
|
|
42
44
|
fallback.handle({
|
|
43
45
|
type: 'register-dataset',
|
|
@@ -83,12 +85,26 @@ export function createWorkerSamplingClient(): WorkerSamplingClient {
|
|
|
83
85
|
const resolveFallbackRequests = () => {
|
|
84
86
|
const outstanding = [...pending.values()]
|
|
85
87
|
pending.clear()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
let repository: WorkerSamplingRepository
|
|
89
|
+
try {
|
|
90
|
+
repository = fallbackRepository()
|
|
91
|
+
} catch (error) {
|
|
92
|
+
outstanding.forEach(({ reject }) =>
|
|
93
|
+
reject(error instanceof Error ? error : new Error(String(error))),
|
|
94
|
+
)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
outstanding.forEach(({ request, resolve, reject }) => {
|
|
98
|
+
try {
|
|
99
|
+
resolve(repository.handle(request))
|
|
100
|
+
} catch (error) {
|
|
101
|
+
reject(error instanceof Error ? error : new Error(String(error)))
|
|
102
|
+
}
|
|
103
|
+
})
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
const fallBackToJavascript = (event?: ErrorEvent) => {
|
|
91
|
-
if (fallback) return
|
|
107
|
+
if (disposed || fallback) return
|
|
92
108
|
workerFailureReason = workerFailureMessage(event)
|
|
93
109
|
worker?.terminate()
|
|
94
110
|
worker = undefined
|
|
@@ -101,9 +117,17 @@ export function createWorkerSamplingClient(): WorkerSamplingClient {
|
|
|
101
117
|
worker = new Worker(new URL('../../core/workerSampling/workerRuntime.ts', import.meta.url), {
|
|
102
118
|
type: 'module',
|
|
103
119
|
})
|
|
104
|
-
worker.onmessage = (
|
|
120
|
+
worker.onmessage = (
|
|
121
|
+
event: MessageEvent<
|
|
122
|
+
WorkerSamplingResponse | { type: 'request-error'; requestId: number; message: string }
|
|
123
|
+
>,
|
|
124
|
+
) => {
|
|
105
125
|
const request = pending.get(event.data.requestId)
|
|
106
126
|
if (!request) return
|
|
127
|
+
if (event.data.type === 'request-error') {
|
|
128
|
+
fallBackToJavascript({ message: event.data.message } as ErrorEvent)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
107
131
|
pending.delete(event.data.requestId)
|
|
108
132
|
request.resolve(event.data)
|
|
109
133
|
}
|
|
@@ -119,8 +143,9 @@ export function createWorkerSamplingClient(): WorkerSamplingClient {
|
|
|
119
143
|
return workerFailureReason
|
|
120
144
|
},
|
|
121
145
|
send(request) {
|
|
146
|
+
if (disposed) return Promise.reject(new Error('Sampling client was disposed.'))
|
|
122
147
|
recordDatasetRequest(request)
|
|
123
|
-
if (fallback) return Promise.resolve(
|
|
148
|
+
if (fallback) return Promise.resolve().then(() => fallback!.handle(request))
|
|
124
149
|
return new Promise<WorkerSamplingResponse>((resolve, reject) => {
|
|
125
150
|
pending.set(request.requestId, { request, resolve, reject })
|
|
126
151
|
try {
|
|
@@ -133,6 +158,13 @@ export function createWorkerSamplingClient(): WorkerSamplingClient {
|
|
|
133
158
|
})
|
|
134
159
|
},
|
|
135
160
|
dispose() {
|
|
161
|
+
disposed = true
|
|
162
|
+
if (worker) {
|
|
163
|
+
worker.onmessage = null
|
|
164
|
+
worker.onerror = null
|
|
165
|
+
worker.onmessageerror = null
|
|
166
|
+
}
|
|
167
|
+
fallback?.handle({ type: 'dispose-all', requestId: 0 })
|
|
136
168
|
worker?.terminate()
|
|
137
169
|
worker = undefined
|
|
138
170
|
pending.forEach(({ reject }) => reject(new Error('Sampling client was disposed.')))
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { WaveformYAxisLabelTextStyle } from '../../types'
|
|
2
|
+
|
|
3
|
+
export function resolveYAxisLabelFontSize(style?: WaveformYAxisLabelTextStyle): number {
|
|
4
|
+
const size = style?.fontSize
|
|
5
|
+
return typeof size === 'number' && Number.isFinite(size) && size > 0 ? size : 12
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveYAxisLabelBandWidth(style?: WaveformYAxisLabelTextStyle): number {
|
|
9
|
+
return resolveYAxisLabelFontSize(style) + 8
|
|
10
|
+
}
|