waveform-analysis 0.1.71 → 0.1.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -40
- package/dist/assets/{workerRuntime-o5An0M-W.js → workerRuntime-5p-EI-GL.js} +1 -1
- package/dist/index.cjs +2 -1
- package/dist/index.js +3874 -2778
- 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/annotation/useWaveformChartAnnotations.d.ts +0 -1
- 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 +6 -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 +5 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +24 -6
- package/dist/types/components/core/useWaveformChartLifecycle.d.ts +2 -2
- package/dist/types/components/core/useWaveformData.d.ts +2 -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/useExtremaTickViewport.d.ts +5 -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 +1 -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/data/sx2Channel.d.ts +10 -0
- package/dist/types/demo/DemoYAxisPaddingControls.vue.d.ts +11 -0
- package/dist/types/demo/types.d.ts +7 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/types/chart.d.ts +32 -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 +61 -14
- package/src/components/WaveformChart.css +10 -1
- package/src/components/WaveformChart.vue +6 -0
- package/src/components/WaveformChartView.vue +27 -26
- package/src/components/annotation/WaveformAnnotationLayer.vue +6 -1
- package/src/components/annotation/useWaveformChartAnnotations.ts +7 -18
- package/src/components/controls/WaveformToolbar.vue +247 -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 +49 -36
- package/src/components/core/niceScale.ts +151 -0
- package/src/components/core/trackLayoutBuilder.ts +41 -17
- package/src/components/core/trackPagination.ts +3 -3
- package/src/components/core/types.ts +5 -0
- package/src/components/core/useWaveformChartController.ts +94 -3
- package/src/components/core/useWaveformChartLifecycle.ts +11 -111
- package/src/components/core/useWaveformData.ts +4 -0
- package/src/components/core/useWaveformLayout.ts +25 -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 +11 -9
- package/src/components/core/yAxisPadding.ts +60 -0
- package/src/components/core/yAxisTitleLayout.ts +11 -12
- package/src/components/interaction/useExtremaTickViewport.ts +43 -0
- package/src/components/interaction/useWaveformViewport.ts +11 -2
- package/src/components/rendering/WaveformTrack.css +1 -2
- package/src/components/rendering/WaveformTrack.vue +5 -12
- package/src/components/rendering/WaveformTrackAxes.vue +66 -19
- package/src/components/rendering/WaveformTrackBackdrop.vue +8 -3
- package/src/components/rendering/compactFrame.ts +31 -0
- package/src/components/rendering/yAxisTickVisibility.ts +156 -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 +67 -2
- package/src/data/sx2Channel.ts +315 -0
- 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 +53 -0
- package/src/demo/embedBridge.ts +85 -0
- package/src/demo/types.ts +7 -0
- package/src/index.ts +19 -0
- package/src/types/chart.ts +34 -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,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
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { WaveformOverlayMode } from '../data/types'
|
|
2
2
|
import {
|
|
3
3
|
Y_AXIS_CHARACTER_WIDTH,
|
|
4
|
-
Y_AXIS_LABEL_BAND_WIDTH,
|
|
5
4
|
Y_AXIS_LABEL_GAP,
|
|
6
5
|
Y_AXIS_OUTER_PADDING,
|
|
7
6
|
Y_AXIS_TICK_PADDING,
|
|
@@ -9,6 +8,8 @@ import {
|
|
|
9
8
|
import { axisTextMetrics, resolveRenderedYAxisSeriesGroups } from './layout'
|
|
10
9
|
import type { DisplayTrack } from './types'
|
|
11
10
|
import type { WaveformYDomain } from './yDomain'
|
|
11
|
+
import type { YAxisPaddingOptions } from './yAxisPadding'
|
|
12
|
+
import { resolveYAxisLabelBandWidth } from './yAxisLabelStyle'
|
|
12
13
|
|
|
13
14
|
export interface YAxisLayoutMetrics {
|
|
14
15
|
tickClearance: number
|
|
@@ -40,10 +41,13 @@ export function resolveYAxisLayoutMetrics(
|
|
|
40
41
|
nice: boolean,
|
|
41
42
|
tickCount: number,
|
|
42
43
|
compact: boolean,
|
|
43
|
-
|
|
44
|
+
padding?: YAxisPaddingOptions | number,
|
|
44
45
|
showUnits = true,
|
|
45
46
|
measureTextWidth?: (text: string) => number,
|
|
46
47
|
): YAxisLayoutMetrics {
|
|
48
|
+
const labelBandWidth = resolveYAxisLabelBandWidth(
|
|
49
|
+
typeof padding === 'object' ? padding.labelTextStyle : undefined,
|
|
50
|
+
)
|
|
47
51
|
const axisText = chartTracks
|
|
48
52
|
.filter((track) => track.series.length > 0)
|
|
49
53
|
.flatMap((track) =>
|
|
@@ -53,7 +57,7 @@ export function resolveYAxisLayoutMetrics(
|
|
|
53
57
|
yDomain,
|
|
54
58
|
yDomains,
|
|
55
59
|
viewportYDomains,
|
|
56
|
-
|
|
60
|
+
padding,
|
|
57
61
|
),
|
|
58
62
|
)
|
|
59
63
|
.map(
|
|
@@ -66,6 +70,8 @@ export function resolveYAxisLayoutMetrics(
|
|
|
66
70
|
tickCount,
|
|
67
71
|
compact,
|
|
68
72
|
measureTextWidth,
|
|
73
|
+
group.maximumTick,
|
|
74
|
+
group.minimumTick,
|
|
69
75
|
).tickTextWidth,
|
|
70
76
|
)
|
|
71
77
|
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
|
@@ -74,13 +80,9 @@ export function resolveYAxisLayoutMetrics(
|
|
|
74
80
|
Y_AXIS_TICK_PADDING +
|
|
75
81
|
tickTextWidth +
|
|
76
82
|
Y_AXIS_LABEL_GAP +
|
|
77
|
-
|
|
83
|
+
labelBandWidth / 2
|
|
78
84
|
)
|
|
79
85
|
const fullClearance =
|
|
80
|
-
tickTextWidth +
|
|
81
|
-
Y_AXIS_TICK_PADDING +
|
|
82
|
-
Y_AXIS_LABEL_GAP +
|
|
83
|
-
Y_AXIS_LABEL_BAND_WIDTH +
|
|
84
|
-
Y_AXIS_OUTER_PADDING
|
|
86
|
+
tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + labelBandWidth + Y_AXIS_OUTER_PADDING
|
|
85
87
|
return { tickClearance, fullClearance, labelCenterX }
|
|
86
88
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { WaveformAxesOptions } from '../../types'
|
|
2
|
+
import type { YAxisSeriesGroup } from './layout'
|
|
3
|
+
|
|
4
|
+
export type YAxisPaddingOptions = WaveformAxesOptions['y']
|
|
5
|
+
|
|
6
|
+
export function paddingRatio(enabled?: boolean, ratio?: number): number {
|
|
7
|
+
if (enabled === false) return 0
|
|
8
|
+
const value = ratio ?? (enabled === true ? 0.1 : 0)
|
|
9
|
+
return Number.isFinite(value) && value > 0 ? value : 0
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function applyYAxisPadding(
|
|
13
|
+
group: YAxisSeriesGroup,
|
|
14
|
+
viewport: [number, number] | undefined,
|
|
15
|
+
options: YAxisPaddingOptions | number,
|
|
16
|
+
): YAxisSeriesGroup {
|
|
17
|
+
const config = typeof options === 'number' ? { upperPaddingRatio: options } : options
|
|
18
|
+
if (group.fixed) return withYAxisExtremaTicks(group, config?.extremaTicksVisible)
|
|
19
|
+
const upper = paddingRatio(config?.upperPaddingEnabled, config?.upperPaddingRatio)
|
|
20
|
+
const lower = paddingRatio(config?.lowerPaddingEnabled, config?.lowerPaddingRatio)
|
|
21
|
+
const [a, b] = group.domain
|
|
22
|
+
const span = b - a
|
|
23
|
+
const min = lower > 0 ? a - span * lower : a
|
|
24
|
+
const max = upper > 0 ? b + span * upper : b
|
|
25
|
+
return withYAxisExtremaTicks(
|
|
26
|
+
{
|
|
27
|
+
...group,
|
|
28
|
+
domain: viewport ?? [Number.isFinite(min) ? min : a, Number.isFinite(max) ? max : b],
|
|
29
|
+
},
|
|
30
|
+
config?.extremaTicksVisible,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function withYAxisExtremaTicks(group: YAxisSeriesGroup, visible = true): YAxisSeriesGroup {
|
|
35
|
+
const maxima = group.seriesList.flatMap((series) =>
|
|
36
|
+
Number.isFinite(series.rawYMaximum) ? [series.rawYMaximum!] : [],
|
|
37
|
+
)
|
|
38
|
+
const minima = group.seriesList.flatMap((series) =>
|
|
39
|
+
Number.isFinite(series.rawYMinimum) ? [series.rawYMinimum!] : [],
|
|
40
|
+
)
|
|
41
|
+
return {
|
|
42
|
+
...group,
|
|
43
|
+
maximumTick: visible && maxima.length ? Math.max(...maxima) : undefined,
|
|
44
|
+
minimumTick: visible && minima.length ? Math.min(...minima) : undefined,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function withMaximumTick(values: number[], maximum?: number, minimum?: number): number[] {
|
|
49
|
+
if (minimum !== undefined && Number.isFinite(minimum)) {
|
|
50
|
+
values = [...values.filter((value) => !sameTick(value, minimum)), minimum].sort((a, b) => a - b)
|
|
51
|
+
}
|
|
52
|
+
if (maximum === undefined || !Number.isFinite(maximum)) return values
|
|
53
|
+
return [...values.filter((value) => !sameTick(value, maximum)), maximum].sort((a, b) => a - b)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function sameTick(a: number, b: number): boolean {
|
|
57
|
+
return (
|
|
58
|
+
Math.abs(a - b) <= Number.EPSILON * 8 * Math.max(Math.abs(a), Math.abs(b), Number.MIN_VALUE)
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import { formatYAxisTickLayoutLabel } from './layout'
|
|
2
2
|
import type { TrackLayout, WaveformYAxisLayout } from './types'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Y_AXIS_TICK_PADDING,
|
|
7
|
-
} from './yAxisConstants'
|
|
3
|
+
import type { WaveformYAxisLabelTextStyle } from '../../types'
|
|
4
|
+
import { resolveYAxisLabelBandWidth, resolveYAxisLabelFontSize } from './yAxisLabelStyle'
|
|
5
|
+
import { Y_AXIS_CHARACTER_WIDTH, Y_AXIS_TICK_PADDING } from './yAxisConstants'
|
|
8
6
|
|
|
9
|
-
// Match the
|
|
10
|
-
const TITLE_FONT_SIZE = 12
|
|
7
|
+
// Match the 11px ticks in WaveformTrack.css.
|
|
11
8
|
const TICK_FONT_SIZE = 11
|
|
12
9
|
const TEXT_GAP = 2
|
|
13
10
|
|
|
14
11
|
interface TitleLayoutOptions {
|
|
12
|
+
labelTextStyle?: WaveformYAxisLabelTextStyle
|
|
15
13
|
showUnits?: boolean
|
|
16
14
|
compact?: boolean
|
|
17
15
|
measureTextWidth?: (text: string) => number
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
function estimateTitleWidth(title: string): number {
|
|
18
|
+
function estimateTitleWidth(title: string, fontSize: number): number {
|
|
21
19
|
return Array.from(title).reduce((width, character) => {
|
|
22
20
|
// Latin labels average well below one em; full-width characters occupy one em.
|
|
23
21
|
const em = /[\u2e80-\u9fff\uf900-\ufaff\uff01-\uff60]/u.test(character)
|
|
@@ -27,7 +25,7 @@ function estimateTitleWidth(title: string): number {
|
|
|
27
25
|
: /[ilI1., :;|'!]/u.test(character)
|
|
28
26
|
? 0.4
|
|
29
27
|
: 0.65
|
|
30
|
-
return width + em *
|
|
28
|
+
return width + em * fontSize
|
|
31
29
|
}, 0)
|
|
32
30
|
}
|
|
33
31
|
|
|
@@ -41,7 +39,8 @@ function titleDistance(
|
|
|
41
39
|
): number {
|
|
42
40
|
const series = axis.seriesList[0]
|
|
43
41
|
const title = series?.name.trim() || yLabel || ''
|
|
44
|
-
const halfTitleHeight =
|
|
42
|
+
const halfTitleHeight =
|
|
43
|
+
estimateTitleWidth(title, resolveYAxisLabelFontSize(options.labelTextStyle)) / 2
|
|
45
44
|
const titleTop = track.height / 2 - halfTitleHeight - TEXT_GAP
|
|
46
45
|
const titleBottom = track.height / 2 + halfTitleHeight + TEXT_GAP
|
|
47
46
|
const lowestTick = Math.min(...axis.tickValues)
|
|
@@ -63,11 +62,11 @@ function titleDistance(
|
|
|
63
62
|
)
|
|
64
63
|
return [options.measureTextWidth?.(text) ?? text.length * Y_AXIS_CHARACTER_WIDTH]
|
|
65
64
|
})
|
|
66
|
-
// The
|
|
65
|
+
// The title band leaves 4px beside each glyph.
|
|
67
66
|
return (
|
|
68
67
|
Math.max(Y_AXIS_CHARACTER_WIDTH, ...tickWidths) +
|
|
69
68
|
Y_AXIS_TICK_PADDING +
|
|
70
|
-
|
|
69
|
+
resolveYAxisLabelBandWidth(options.labelTextStyle) / 2
|
|
71
70
|
)
|
|
72
71
|
}
|
|
73
72
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { watch, type ComputedRef, type ShallowRef } from 'vue'
|
|
2
|
+
import type { ZoomTransform } from 'd3'
|
|
3
|
+
|
|
4
|
+
import type { TrackLayout } from '../core/types'
|
|
5
|
+
|
|
6
|
+
/** Keep X coordinates stable when toggling extrema changes the reserved Y-label width. */
|
|
7
|
+
export function useExtremaTickViewport(
|
|
8
|
+
visible: () => boolean,
|
|
9
|
+
innerWidth: ComputedRef<number>,
|
|
10
|
+
tracks: ComputedRef<TrackLayout[]>,
|
|
11
|
+
sharedTransform: ShallowRef<ZoomTransform>,
|
|
12
|
+
independentTransforms: ShallowRef<ZoomTransform[]>,
|
|
13
|
+
) {
|
|
14
|
+
const resizeTransform = (transform: ZoomTransform, previous: number, next: number) =>
|
|
15
|
+
previous > 0 && next > 0
|
|
16
|
+
? transform.translate(((next / previous - 1) * transform.x) / transform.k, 0)
|
|
17
|
+
: transform
|
|
18
|
+
|
|
19
|
+
watch(
|
|
20
|
+
() => ({
|
|
21
|
+
visible: visible(),
|
|
22
|
+
width: innerWidth.value,
|
|
23
|
+
tracks: tracks.value.map((track) => ({
|
|
24
|
+
id: track.id,
|
|
25
|
+
index: track.index,
|
|
26
|
+
width: track.width,
|
|
27
|
+
})),
|
|
28
|
+
}),
|
|
29
|
+
(next, previous) => {
|
|
30
|
+
if (next.visible === previous.visible) return
|
|
31
|
+
sharedTransform.value = resizeTransform(sharedTransform.value, previous.width, next.width)
|
|
32
|
+
const transforms = [...independentTransforms.value]
|
|
33
|
+
next.tracks.forEach((track) => {
|
|
34
|
+
const prior = previous.tracks.find((item) => item.id === track.id)
|
|
35
|
+
const transform = transforms[track.index]
|
|
36
|
+
if (prior && transform) {
|
|
37
|
+
transforms[track.index] = resizeTransform(transform, prior.width, track.width)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
independentTransforms.value = transforms
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -13,6 +13,7 @@ import { createViewportPan } from './viewportPan'
|
|
|
13
13
|
import { transformForDomain } from './zoomConstraints'
|
|
14
14
|
import { emitBoxZoomIntent } from './zoomEventPayload'
|
|
15
15
|
export interface ViewportContext {
|
|
16
|
+
clearBoundaries?: (trackIndex?: number) => void
|
|
16
17
|
props: ResolvedWaveformChartProps
|
|
17
18
|
emit: WaveformChartEmit
|
|
18
19
|
selection: Ref<ViewportSelectionState | null>
|
|
@@ -104,8 +105,15 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
104
105
|
)
|
|
105
106
|
const beginViewportDrag = (event: PointerEvent, trackIndex: number, independent: boolean) => {
|
|
106
107
|
if (isPresentationMode.value) return
|
|
107
|
-
const panRequested =
|
|
108
|
-
|
|
108
|
+
const panRequested =
|
|
109
|
+
props.pannable &&
|
|
110
|
+
(props.interactionMode === 'pan' || (isZoomMode.value && spacePressed.value))
|
|
111
|
+
if (
|
|
112
|
+
(!props.zoomable && !panRequested) ||
|
|
113
|
+
(!isZoomMode.value && !panRequested) ||
|
|
114
|
+
event.button !== 0
|
|
115
|
+
)
|
|
116
|
+
return
|
|
109
117
|
const overlay = event.currentTarget as SVGRectElement
|
|
110
118
|
const track = trackLayouts.value.find((item) => item.index === trackIndex)
|
|
111
119
|
if (!track) return
|
|
@@ -291,6 +299,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
291
299
|
}
|
|
292
300
|
}
|
|
293
301
|
const resetViewport = (trackIndex?: number) => {
|
|
302
|
+
context.clearBoundaries?.(trackIndex)
|
|
294
303
|
cancelPendingZoom()
|
|
295
304
|
cancelViewportDrag()
|
|
296
305
|
if (props.displayMode === 'independent' && trackIndex !== undefined) {
|
|
@@ -20,15 +20,14 @@ interface Props {
|
|
|
20
20
|
interactionMode?: WaveformInteractionMode
|
|
21
21
|
frameNumber?: string | number
|
|
22
22
|
frameStyle?: WaveformFrameStyle
|
|
23
|
+
framePath?: string
|
|
23
24
|
axes?: WaveformAxesOptions
|
|
24
25
|
timeUnit: 's' | 'ms'
|
|
25
26
|
yLabel?: string
|
|
26
27
|
showAxisUnits?: boolean
|
|
27
28
|
showLabelUnits?: boolean
|
|
28
29
|
hideYAxisTitles?: boolean
|
|
29
|
-
containYAxisEndpoints?: boolean
|
|
30
30
|
containFrameStroke?: boolean
|
|
31
|
-
omitFrameTop?: boolean
|
|
32
31
|
cleanView?: boolean
|
|
33
32
|
zeroLine?: Required<WaveformZeroLineOptions>
|
|
34
33
|
}
|
|
@@ -77,12 +76,6 @@ const resolvedFrameStyle = computed(() => {
|
|
|
77
76
|
const frameInset = computed(() =>
|
|
78
77
|
props.containFrameStroke ? resolvedFrameStyle.value.borderWidth / 2 : 0,
|
|
79
78
|
)
|
|
80
|
-
const openFramePath = computed(() => {
|
|
81
|
-
const inset = frameInset.value
|
|
82
|
-
const right = Math.max(inset, (props.track.width ?? props.innerWidth) - inset)
|
|
83
|
-
const bottom = Math.max(inset, props.track.height - inset)
|
|
84
|
-
return `M ${inset} ${inset} V ${bottom} H ${right} V ${inset}`
|
|
85
|
-
})
|
|
86
79
|
</script>
|
|
87
80
|
|
|
88
81
|
<template>
|
|
@@ -106,6 +99,7 @@ const openFramePath = computed(() => {
|
|
|
106
99
|
:clean-view="cleanView"
|
|
107
100
|
:frame-number="frameNumber"
|
|
108
101
|
:frame-style="resolvedFrameStyle"
|
|
102
|
+
show-empty-frame
|
|
109
103
|
/>
|
|
110
104
|
<WaveformTrackAxes
|
|
111
105
|
:track="track"
|
|
@@ -117,14 +111,13 @@ const openFramePath = computed(() => {
|
|
|
117
111
|
:show-units="showAxisUnits"
|
|
118
112
|
:show-label-units="showLabelUnits"
|
|
119
113
|
:hide-y-axis-titles="hideYAxisTitles"
|
|
120
|
-
:
|
|
114
|
+
:hide-overlapping-maximum-label="displayMode === 'compact'"
|
|
121
115
|
/>
|
|
122
116
|
|
|
123
117
|
<component
|
|
124
|
-
:is="
|
|
125
|
-
v-if="!track.isEmpty"
|
|
118
|
+
:is="framePath ? 'path' : 'rect'"
|
|
126
119
|
class="waveform-track__plot-frame waveform-chart__plot-frame"
|
|
127
|
-
:d="
|
|
120
|
+
:d="framePath"
|
|
128
121
|
:x="containFrameStroke ? frameInset : undefined"
|
|
129
122
|
:y="containFrameStroke ? frameInset : undefined"
|
|
130
123
|
:width="Math.max(0, (track.width ?? innerWidth) - frameInset * 2)"
|