waveform-analysis 0.1.70 → 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 +244 -45
- package/dist/assets/workerRuntime-5p-EI-GL.js +1 -0
- package/dist/index.cjs +2 -1
- package/dist/index.js +2924 -1915
- 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/pagination.d.ts +2 -2
- 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/pagination.ts +3 -16
- 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 +95 -10
- 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
- package/dist/assets/workerRuntime-BcYng9TR.js +0 -1
|
@@ -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,7 @@ export function resolveYAxisLayoutMetrics(
|
|
|
66
70
|
tickCount,
|
|
67
71
|
compact,
|
|
68
72
|
measureTextWidth,
|
|
73
|
+
group.maximumTick,
|
|
69
74
|
).tickTextWidth,
|
|
70
75
|
)
|
|
71
76
|
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
|
@@ -74,13 +79,9 @@ export function resolveYAxisLayoutMetrics(
|
|
|
74
79
|
Y_AXIS_TICK_PADDING +
|
|
75
80
|
tickTextWidth +
|
|
76
81
|
Y_AXIS_LABEL_GAP +
|
|
77
|
-
|
|
82
|
+
labelBandWidth / 2
|
|
78
83
|
)
|
|
79
84
|
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
|
|
85
|
+
tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + labelBandWidth + Y_AXIS_OUTER_PADDING
|
|
85
86
|
return { tickClearance, fullClearance, labelCenterX }
|
|
86
87
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
if (group.fixed) return group
|
|
18
|
+
const config = typeof options === 'number' ? { upperPaddingRatio: options } : options
|
|
19
|
+
const upper = paddingRatio(config?.upperPaddingEnabled, config?.upperPaddingRatio)
|
|
20
|
+
const lower = paddingRatio(config?.lowerPaddingEnabled, config?.lowerPaddingRatio)
|
|
21
|
+
const maxima = group.seriesList.flatMap((series) =>
|
|
22
|
+
Number.isFinite(series.rawYMaximum) ? [series.rawYMaximum!] : [],
|
|
23
|
+
)
|
|
24
|
+
const maximumTick = upper > 0 && maxima.length ? Math.max(...maxima) : undefined
|
|
25
|
+
const [a, b] = group.domain
|
|
26
|
+
const span = b - a
|
|
27
|
+
const min = lower > 0 ? a - span * lower : a
|
|
28
|
+
const max = upper > 0 ? b + span * upper : b
|
|
29
|
+
return {
|
|
30
|
+
...group,
|
|
31
|
+
maximumTick,
|
|
32
|
+
domain: viewport ?? [Number.isFinite(min) ? min : a, Number.isFinite(max) ? max : b],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function withMaximumTick(values: number[], maximum?: number): number[] {
|
|
37
|
+
if (maximum === undefined || !Number.isFinite(maximum)) return values
|
|
38
|
+
return [...values.filter((value) => !sameTick(value, maximum)), maximum].sort((a, b) => a - b)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sameTick(a: number, b: number): boolean {
|
|
42
|
+
return (
|
|
43
|
+
Math.abs(a - b) <= Number.EPSILON * 8 * Math.max(Math.abs(a), Math.abs(b), Number.MIN_VALUE)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -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
|
|
|
@@ -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,12 @@ const openFramePath = computed(() => {
|
|
|
117
111
|
:show-units="showAxisUnits"
|
|
118
112
|
:show-label-units="showLabelUnits"
|
|
119
113
|
:hide-y-axis-titles="hideYAxisTitles"
|
|
120
|
-
:contain-y-axis-endpoints="containYAxisEndpoints || displayMode === 'compact'"
|
|
121
114
|
/>
|
|
122
115
|
|
|
123
116
|
<component
|
|
124
|
-
:is="
|
|
125
|
-
v-if="!track.isEmpty"
|
|
117
|
+
:is="framePath ? 'path' : 'rect'"
|
|
126
118
|
class="waveform-track__plot-frame waveform-chart__plot-frame"
|
|
127
|
-
:d="
|
|
119
|
+
:d="framePath"
|
|
128
120
|
:x="containFrameStroke ? frameInset : undefined"
|
|
129
121
|
:y="containFrameStroke ? frameInset : undefined"
|
|
130
122
|
:width="Math.max(0, (track.width ?? innerWidth) - frameInset * 2)"
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { axisBottom, axisLeft, axisRight, select } from 'd3'
|
|
3
|
-
import { nextTick, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import { computed, nextTick, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
|
4
4
|
|
|
5
5
|
import type { WaveformAxesOptions } from '../../types'
|
|
6
6
|
import { formatXAxisLabel } from '../../utils'
|
|
7
7
|
import { formatYAxisTickLabel } from '../core/layout'
|
|
8
8
|
import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
|
|
9
|
-
import {
|
|
9
|
+
import { updateMaximumTickVisibility } from './yAxisTickVisibility'
|
|
10
|
+
import { sameTick } from '../core/yAxisPadding'
|
|
11
|
+
import { resolveYAxisLabelBandWidth, resolveYAxisLabelFontSize } from '../core/yAxisLabelStyle'
|
|
10
12
|
import { formatChannelLabel } from './channelLabel'
|
|
11
13
|
|
|
12
14
|
interface Props {
|
|
@@ -19,10 +21,19 @@ interface Props {
|
|
|
19
21
|
showUnits?: boolean
|
|
20
22
|
showLabelUnits?: boolean
|
|
21
23
|
hideYAxisTitles?: boolean
|
|
22
|
-
containYAxisEndpoints?: boolean
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const props = withDefaults(defineProps<Props>(), { showUnits: true })
|
|
27
|
+
const labelBandWidth = computed(() => resolveYAxisLabelBandWidth(props.axes?.y?.labelTextStyle))
|
|
28
|
+
const labelTextStyle = computed(() => {
|
|
29
|
+
const style = props.axes?.y?.labelTextStyle
|
|
30
|
+
return {
|
|
31
|
+
fontSize: `${resolveYAxisLabelFontSize(style)}px`,
|
|
32
|
+
fontWeight: style?.fontWeight ?? 600,
|
|
33
|
+
fontFamily: style?.fontFamily,
|
|
34
|
+
fontStyle: style?.fontStyle ?? 'normal',
|
|
35
|
+
}
|
|
36
|
+
})
|
|
26
37
|
const xAxisElement = ref<SVGGElement>()
|
|
27
38
|
const yAxisElements = ref<SVGGElement[]>([])
|
|
28
39
|
|
|
@@ -62,24 +73,24 @@ function renderAxes() {
|
|
|
62
73
|
|
|
63
74
|
const selection = select(element)
|
|
64
75
|
selection.call(yAxis)
|
|
65
|
-
const lowestTick = Math.min(...axis.tickValues)
|
|
66
|
-
const highestTick = props.containYAxisEndpoints ? Math.max(...axis.tickValues) : undefined
|
|
67
76
|
selection
|
|
68
77
|
.selectAll<SVGTextElement, number>('.tick text')
|
|
69
78
|
.attr('y', null)
|
|
70
|
-
.attr('dy',
|
|
71
|
-
.attr('dominant-baseline',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
: null,
|
|
79
|
+
.attr('dy', '0')
|
|
80
|
+
.attr('dominant-baseline', 'central')
|
|
81
|
+
selection
|
|
82
|
+
.selectAll<SVGGElement, number>('.tick')
|
|
83
|
+
.attr('data-maximum-tick', (value) =>
|
|
84
|
+
axis.maximumTick !== undefined && sameTick(value, axis.maximumTick) ? 'true' : null,
|
|
77
85
|
)
|
|
86
|
+
.attr('data-tick-value', (value) => value)
|
|
87
|
+
.attr('data-tick-y', (value) => axis.scale(value))
|
|
78
88
|
selection
|
|
79
89
|
.selectAll('path.domain')
|
|
80
90
|
.attr('display', props.axes?.y?.lineVisible === false ? 'none' : null)
|
|
81
91
|
})
|
|
82
92
|
|
|
93
|
+
scheduleVisibility()
|
|
83
94
|
if (!xAxisElement.value) return
|
|
84
95
|
const selection = select(xAxisElement.value)
|
|
85
96
|
selection.call(
|
|
@@ -103,7 +114,24 @@ function renderAxes() {
|
|
|
103
114
|
.attr('display', props.axes?.x?.lineVisible === false ? 'none' : null)
|
|
104
115
|
}
|
|
105
116
|
|
|
117
|
+
let disposed = false
|
|
118
|
+
function scheduleVisibility() {
|
|
119
|
+
if (disposed) return
|
|
120
|
+
const apply = () => {
|
|
121
|
+
const svg = yAxisElements.value[0]?.ownerSVGElement
|
|
122
|
+
if (svg) updateMaximumTickVisibility(svg)
|
|
123
|
+
}
|
|
124
|
+
apply()
|
|
125
|
+
}
|
|
126
|
+
onBeforeUnmount(() => {
|
|
127
|
+
disposed = true
|
|
128
|
+
document.fonts?.removeEventListener?.('loadingdone', scheduleVisibility)
|
|
129
|
+
})
|
|
106
130
|
onMounted(async () => {
|
|
131
|
+
document.fonts?.addEventListener?.('loadingdone', scheduleVisibility)
|
|
132
|
+
void document.fonts?.ready.then(() => {
|
|
133
|
+
if (!disposed) scheduleVisibility()
|
|
134
|
+
})
|
|
107
135
|
await nextTick()
|
|
108
136
|
renderAxes()
|
|
109
137
|
})
|
|
@@ -116,7 +144,6 @@ watch(
|
|
|
116
144
|
() => props.track.yAxisTickValues,
|
|
117
145
|
() => props.timeUnit,
|
|
118
146
|
() => props.showUnits,
|
|
119
|
-
() => props.containYAxisEndpoints,
|
|
120
147
|
() => props.axes?.x?.labelFormatter,
|
|
121
148
|
() => props.axes?.x?.lineVisible,
|
|
122
149
|
() => props.axes?.y?.lineVisible,
|
|
@@ -191,15 +218,16 @@ watch(
|
|
|
191
218
|
>
|
|
192
219
|
<rect
|
|
193
220
|
class="waveform-track__y-axis-label-bg waveform-chart__y-axis-label-bg"
|
|
194
|
-
:x="track.yAxes[0].labelX -
|
|
221
|
+
:x="track.yAxes[0].labelX - labelBandWidth / 2"
|
|
195
222
|
:y="track.height / 2 - 40"
|
|
196
|
-
:width="
|
|
223
|
+
:width="labelBandWidth"
|
|
197
224
|
height="80"
|
|
198
225
|
rx="2"
|
|
199
226
|
/>
|
|
200
227
|
<text
|
|
201
228
|
class="waveform-track__y-axis-label waveform-chart__y-axis-label"
|
|
202
|
-
:fill="track.series?.color"
|
|
229
|
+
:fill="axes?.y?.labelTextStyle?.color || track.series?.color"
|
|
230
|
+
:style="labelTextStyle"
|
|
203
231
|
:transform="`translate(${track.yAxes[0].labelX}, ${track.height / 2}) rotate(-90)`"
|
|
204
232
|
text-anchor="middle"
|
|
205
233
|
dominant-baseline="central"
|
|
@@ -218,15 +246,16 @@ watch(
|
|
|
218
246
|
>
|
|
219
247
|
<rect
|
|
220
248
|
class="waveform-track__y-axis-label-bg waveform-chart__y-axis-label-bg"
|
|
221
|
-
:x="axis.labelX -
|
|
249
|
+
:x="axis.labelX - labelBandWidth / 2"
|
|
222
250
|
:y="track.height / 2 - 40"
|
|
223
|
-
:width="
|
|
251
|
+
:width="labelBandWidth"
|
|
224
252
|
height="80"
|
|
225
253
|
rx="2"
|
|
226
254
|
/>
|
|
227
255
|
<text
|
|
228
256
|
class="waveform-track__y-axis-label waveform-chart__y-axis-label"
|
|
229
|
-
:fill="axis.seriesList[0].color"
|
|
257
|
+
:fill="axes?.y?.labelTextStyle?.color || axis.seriesList[0].color"
|
|
258
|
+
:style="labelTextStyle"
|
|
230
259
|
:transform="`translate(${axis.labelX}, ${track.height / 2}) rotate(-90)`"
|
|
231
260
|
text-anchor="middle"
|
|
232
261
|
dominant-baseline="central"
|
|
@@ -6,6 +6,7 @@ interface Props {
|
|
|
6
6
|
clipPathId: string
|
|
7
7
|
innerWidth: number
|
|
8
8
|
cleanView: boolean
|
|
9
|
+
showEmptyFrame?: boolean
|
|
9
10
|
frameNumber?: string | number
|
|
10
11
|
frameStyle: {
|
|
11
12
|
backgroundColor: string
|
|
@@ -17,7 +18,7 @@ defineProps<Props>()
|
|
|
17
18
|
|
|
18
19
|
<template>
|
|
19
20
|
<rect
|
|
20
|
-
v-if="!track.isEmpty && !cleanView"
|
|
21
|
+
v-if="(!track.isEmpty || showEmptyFrame) && !cleanView"
|
|
21
22
|
class="waveform-track__plot-background waveform-chart__plot-background"
|
|
22
23
|
:width="track.width ?? innerWidth"
|
|
23
24
|
:height="track.height"
|
|
@@ -26,7 +27,7 @@ defineProps<Props>()
|
|
|
26
27
|
/>
|
|
27
28
|
|
|
28
29
|
<g
|
|
29
|
-
v-if="!track.isEmpty && !cleanView"
|
|
30
|
+
v-if="(!track.isEmpty || showEmptyFrame) && !cleanView"
|
|
30
31
|
:clip-path="`url(#${clipPathId}-${track.index})`"
|
|
31
32
|
aria-hidden="true"
|
|
32
33
|
>
|
|
@@ -89,7 +90,11 @@ defineProps<Props>()
|
|
|
89
90
|
</g>
|
|
90
91
|
|
|
91
92
|
<text
|
|
92
|
-
v-if="
|
|
93
|
+
v-if="
|
|
94
|
+
(track.hasVisibleSeries || (track.isEmpty && showEmptyFrame)) &&
|
|
95
|
+
frameNumber !== undefined &&
|
|
96
|
+
!cleanView
|
|
97
|
+
"
|
|
93
98
|
class="waveform-track__watermark waveform-chart__watermark"
|
|
94
99
|
:x="(track.width ?? innerWidth) / 2"
|
|
95
100
|
:y="track.height / 2"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { TrackLayout } from '../core/types'
|
|
2
|
+
|
|
3
|
+
type Frame = Pick<TrackLayout, 'left' | 'top' | 'width' | 'height' | 'isEmpty'>
|
|
4
|
+
|
|
5
|
+
/** Keep the lower frame's top edge as the single owner of a shared boundary. */
|
|
6
|
+
export function compactFramePath(track: Frame, tracks: Frame[], inset = 0): string | undefined {
|
|
7
|
+
let uncovered: [number, number][] = [[0, track.width]]
|
|
8
|
+
for (const below of tracks) {
|
|
9
|
+
if (below === track) continue
|
|
10
|
+
if (Math.abs(track.top + track.height - below.top) > 1e-6) continue
|
|
11
|
+
const start = Math.max(0, below.left - track.left)
|
|
12
|
+
const end = Math.min(track.width, below.left + below.width - track.left)
|
|
13
|
+
if (end <= start) continue
|
|
14
|
+
uncovered = uncovered.flatMap(([left, right]): [number, number][] => {
|
|
15
|
+
if (end <= left || start >= right) return [[left, right]]
|
|
16
|
+
const parts: [number, number][] = []
|
|
17
|
+
if (left < start) parts.push([left, start])
|
|
18
|
+
if (right > end) parts.push([end, right])
|
|
19
|
+
return parts
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
if (uncovered.length === 1 && uncovered[0][0] === 0 && uncovered[0][1] === track.width)
|
|
23
|
+
return undefined
|
|
24
|
+
inset = Number.isFinite(inset) ? Math.max(0, inset) : 0
|
|
25
|
+
const rightEdge = Math.max(inset, track.width - inset)
|
|
26
|
+
const bottomEdge = Math.max(inset, track.height - inset)
|
|
27
|
+
const bottom = uncovered
|
|
28
|
+
.map(([left, right]) => `M${Math.max(inset, left)},${bottomEdge}H${Math.min(rightEdge, right)}`)
|
|
29
|
+
.join('')
|
|
30
|
+
return `M${inset},${bottomEdge}V${inset}H${rightEdge}V${bottomEdge}${bottom}`
|
|
31
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { sameTick } from '../core/yAxisPadding'
|
|
2
|
+
|
|
3
|
+
interface Box {
|
|
4
|
+
left: number
|
|
5
|
+
right: number
|
|
6
|
+
top: number
|
|
7
|
+
bottom: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function textBox(text: SVGTextElement): Box {
|
|
11
|
+
const rect = text.getBoundingClientRect()
|
|
12
|
+
if (rect.width && rect.height) return rect
|
|
13
|
+
const height = Number.parseFloat(getComputedStyle(text).fontSize) || 11
|
|
14
|
+
const width = (text.textContent?.length ?? 0) * height * 0.65
|
|
15
|
+
let x = Number(text.getAttribute('x')) || 0
|
|
16
|
+
let y = Number(text.getAttribute('y')) || 0
|
|
17
|
+
for (let parent: Element | null = text.parentElement; parent; parent = parent.parentElement) {
|
|
18
|
+
const match = parent
|
|
19
|
+
.getAttribute('transform')
|
|
20
|
+
?.match(/translate\(([-\d.e+]+)[ ,]+([-\d.e+]+)\)/)
|
|
21
|
+
if (match) {
|
|
22
|
+
x += Number(match[1])
|
|
23
|
+
y += Number(match[2])
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { left: x - width, right: x, top: y - height / 2, bottom: y + height / 2 }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function labelsOverlap(a: Box, b: Box): boolean {
|
|
30
|
+
return a.left < b.right && b.left < a.right && a.top < b.bottom + 4 && b.top < a.bottom + 4
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function linesOverlap(a: number, b: number, widthA = 1, widthB = 1): boolean {
|
|
34
|
+
return Math.abs(a - b) < (widthA + widthB) / 2 + 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hide(element: Element) {
|
|
38
|
+
element.setAttribute('display', 'none')
|
|
39
|
+
element.setAttribute('data-maximum-hidden', 'true')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function strokeWidth(element: Element): number {
|
|
43
|
+
return Number.parseFloat(getComputedStyle(element).strokeWidth) || 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Apply after axes and grid are in the DOM; never feed the filtered set back into layout. */
|
|
47
|
+
export function updateMaximumTickVisibility(svg: SVGSVGElement) {
|
|
48
|
+
svg.querySelectorAll('[data-maximum-hidden]').forEach((el) => {
|
|
49
|
+
el.removeAttribute('display')
|
|
50
|
+
el.removeAttribute('data-maximum-hidden')
|
|
51
|
+
})
|
|
52
|
+
svg.querySelectorAll<SVGGElement>('.waveform-track__axis--y .tick').forEach((tick) => {
|
|
53
|
+
const position = Number(tick.getAttribute('data-tick-y'))
|
|
54
|
+
const height = Number(tick.closest('.waveform-track')?.getAttribute('data-track-height'))
|
|
55
|
+
const line = tick.querySelector('line')
|
|
56
|
+
if (sameTick(position, 0) || sameTick(position, height)) line?.setAttribute('display', 'none')
|
|
57
|
+
else line?.removeAttribute('display')
|
|
58
|
+
})
|
|
59
|
+
const maximumTicks = [...svg.querySelectorAll<SVGGElement>('.tick[data-maximum-tick]')]
|
|
60
|
+
const labels = [...svg.querySelectorAll<SVGTextElement>('.waveform-track__axis--y .tick text')]
|
|
61
|
+
maximumTicks.forEach((tick) => {
|
|
62
|
+
const text = tick.querySelector('text')
|
|
63
|
+
const line = tick.querySelector('line')
|
|
64
|
+
if (!text || !line) return
|
|
65
|
+
const box = textBox(text)
|
|
66
|
+
labels.forEach((other) => {
|
|
67
|
+
if (other.parentElement?.hasAttribute('data-maximum-tick')) return
|
|
68
|
+
if (labelsOverlap(box, textBox(other))) hide(other)
|
|
69
|
+
})
|
|
70
|
+
const axis = tick.parentElement!
|
|
71
|
+
const value = Number(tick.getAttribute('data-tick-value'))
|
|
72
|
+
const position = Number(tick.getAttribute('data-tick-y'))
|
|
73
|
+
axis.querySelectorAll<SVGGElement>('.tick:not([data-maximum-tick])').forEach((other) => {
|
|
74
|
+
const otherLine = other.querySelector('line')
|
|
75
|
+
if (
|
|
76
|
+
otherLine &&
|
|
77
|
+
linesOverlap(
|
|
78
|
+
position,
|
|
79
|
+
Number(other.getAttribute('data-tick-y')),
|
|
80
|
+
strokeWidth(line),
|
|
81
|
+
strokeWidth(otherLine),
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
hide(otherLine)
|
|
85
|
+
})
|
|
86
|
+
// Only the primary Y axis owns the existing horizontal grid.
|
|
87
|
+
if (axis.getAttribute('data-y-axis-index') !== '0') return
|
|
88
|
+
const track = axis.closest('.waveform-track')!
|
|
89
|
+
track.querySelectorAll('[data-grid-direction="horizontal"]').forEach((gridLine) => {
|
|
90
|
+
const gridY = Number(gridLine.getAttribute('y1'))
|
|
91
|
+
if (linesOverlap(position, gridY, strokeWidth(line), strokeWidth(gridLine))) hide(gridLine)
|
|
92
|
+
})
|
|
93
|
+
tick.setAttribute('data-maximum-value', String(value))
|
|
94
|
+
})
|
|
95
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { WaveformPointRange } from './waveformPointSource'
|
|
2
|
+
|
|
3
|
+
/** Outside neighbors join the curve without changing visible-point statistics. */
|
|
4
|
+
export function lineBoundaryIndexes(range: WaveformPointRange, length: number): number[] {
|
|
5
|
+
if (!length || (range.start === range.end && (range.start === 0 || range.end === length)))
|
|
6
|
+
return []
|
|
7
|
+
return [range.start > 0 ? range.start - 1 : -1, range.end < length ? range.end : -1].filter(
|
|
8
|
+
(index) => index >= 0,
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function withLineBoundaries<T>(
|
|
13
|
+
points: T[],
|
|
14
|
+
range: WaveformPointRange,
|
|
15
|
+
length: number,
|
|
16
|
+
pointAt: (index: number) => T,
|
|
17
|
+
): T[] {
|
|
18
|
+
const boundaries = lineBoundaryIndexes(range, length)
|
|
19
|
+
return [
|
|
20
|
+
...boundaries.filter((index) => index < range.start).map(pointAt),
|
|
21
|
+
...points,
|
|
22
|
+
...boundaries.filter((index) => index >= range.end).map(pointAt),
|
|
23
|
+
]
|
|
24
|
+
}
|