waveform-analysis 0.1.66 → 0.1.67
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 +124 -3
- package/dist/index.cjs +1 -1
- package/dist/index.js +1059 -939
- package/dist/style.css +1 -1
- package/dist/types/components/WaveformChart.vue.d.ts +4 -0
- package/dist/types/components/core/grid.d.ts +1 -1
- package/dist/types/components/core/layout.d.ts +2 -2
- package/dist/types/components/core/pagination.d.ts +2 -0
- package/dist/types/components/core/title.d.ts +2 -1
- package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +6 -0
- package/dist/types/components/core/useWaveformLayout.d.ts +1 -0
- package/dist/types/components/core/useWaveformPresentation.d.ts +1 -0
- package/dist/types/components/core/useWaveformYAxisLayout.d.ts +31 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +9 -2
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +1 -1
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/interaction/interactionTarget.d.ts +1 -0
- package/dist/types/components/interaction/remoteViewport.d.ts +8 -0
- package/dist/types/components/interaction/useWaveformViewport.d.ts +1 -2
- package/dist/types/components/interaction/viewportDomain.d.ts +1 -2
- package/dist/types/components/interaction/viewportPan.d.ts +4 -0
- package/dist/types/components/rendering/WaveformLegend.vue.d.ts +2 -0
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -0
- package/dist/types/components/rendering/WaveformTrackAxes.vue.d.ts +5 -1
- package/dist/types/data/recordedDampedWaveform.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/chart.d.ts +13 -0
- package/dist/types/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/WaveformChart.css +5 -0
- package/src/components/WaveformChart.vue +2 -0
- package/src/components/WaveformChartView.vue +14 -3
- package/src/components/core/grid.ts +4 -1
- package/src/components/core/layout.ts +16 -2
- package/src/components/core/pagination.ts +17 -0
- package/src/components/core/title.ts +7 -3
- package/src/components/core/trackLayoutBuilder.ts +14 -4
- package/src/components/core/useWaveformChartController.ts +17 -1
- package/src/components/core/useWaveformChartLifecycle.ts +10 -16
- package/src/components/core/useWaveformLayout.ts +35 -91
- package/src/components/core/useWaveformPresentation.ts +17 -3
- package/src/components/core/useWaveformYAxisLayout.ts +140 -0
- package/src/components/core/waveformChartTypes.ts +12 -0
- package/src/components/core/yAxisLayoutMetrics.ts +11 -2
- package/src/components/data/types.ts +3 -0
- package/src/components/interaction/interactionTarget.ts +10 -0
- package/src/components/interaction/remoteViewport.ts +53 -0
- package/src/components/interaction/useWaveformViewport.ts +17 -55
- package/src/components/interaction/useWaveformZoom.ts +7 -7
- package/src/components/interaction/viewportDomain.ts +10 -14
- package/src/components/interaction/viewportPan.ts +82 -0
- package/src/components/rendering/WaveformLegend.vue +10 -1
- package/src/components/rendering/WaveformTrack.vue +13 -2
- package/src/components/rendering/WaveformTrackAxes.vue +15 -4
- package/src/data/recordedDampedWaveform.ts +3 -0
- package/src/data/simulatedWaveforms.ts +12 -10
- package/src/index.ts +3 -0
- package/src/types/chart.ts +16 -0
- package/src/types/index.ts +3 -0
|
@@ -93,6 +93,7 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
93
93
|
return position === 'top' || position === 'bottom' ? 'horizontal' : 'vertical'
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
const isEdgeCompact = computed(() => props.layoutPreset === 'edge-compact')
|
|
96
97
|
const resolvedTitleText = computed(() => props.title?.text.trim() ?? '')
|
|
97
98
|
const titleAreaReserved = computed(
|
|
98
99
|
() =>
|
|
@@ -146,10 +147,17 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
146
147
|
}))
|
|
147
148
|
const titleLayout = computed(() =>
|
|
148
149
|
calculateRotatedTitleLayout({
|
|
149
|
-
naturalWidth:
|
|
150
|
+
naturalWidth:
|
|
151
|
+
isEdgeCompact.value && !titleIsRotated.value && measuredTitleHeight.value > 0
|
|
152
|
+
? Math.min(
|
|
153
|
+
measuredTitleWidth.value || titleAvailableWidth.value,
|
|
154
|
+
titleAvailableWidth.value,
|
|
155
|
+
)
|
|
156
|
+
: measuredTitleWidth.value || estimatedTitleWidth.value,
|
|
150
157
|
naturalHeight: measuredTitleHeight.value || titleFontSize.value * TITLE_LINE_HEIGHT,
|
|
151
158
|
availableWidth: titleAvailableWidth.value,
|
|
152
159
|
rotation: titleRotation.value,
|
|
160
|
+
edgeCompact: isEdgeCompact.value,
|
|
153
161
|
}),
|
|
154
162
|
)
|
|
155
163
|
const titleAreaHeight = computed(() =>
|
|
@@ -159,13 +167,18 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
159
167
|
typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback
|
|
160
168
|
const resolvedPlotMargin = computed(() => ({
|
|
161
169
|
top: resolvePlotMargin(props.plotMargin.top, margin.top),
|
|
162
|
-
bottom:
|
|
170
|
+
bottom: Math.max(
|
|
171
|
+
isEdgeCompact.value ? 44 : 0,
|
|
172
|
+
resolvePlotMargin(props.plotMargin.bottom, margin.bottom),
|
|
173
|
+
),
|
|
163
174
|
}))
|
|
164
175
|
const chartTopMargin = computed(() => resolvedPlotMargin.value.top)
|
|
165
176
|
const drawingHeight = computed(() =>
|
|
166
177
|
Math.max(0, chartHeight.value - titleAreaHeight.value - paginationBandHeight.value),
|
|
167
178
|
)
|
|
168
|
-
const xAxisTitleY = computed(() =>
|
|
179
|
+
const xAxisTitleY = computed(() =>
|
|
180
|
+
Math.max(0, drawingHeight.value - X_AXIS_TITLE_BOTTOM_OFFSET + (isEdgeCompact.value ? 3 : 0)),
|
|
181
|
+
)
|
|
169
182
|
const innerHeight = computed(() =>
|
|
170
183
|
Math.max(
|
|
171
184
|
0,
|
|
@@ -202,6 +215,7 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
202
215
|
chartHeight,
|
|
203
216
|
containerStyle,
|
|
204
217
|
isCleanView,
|
|
218
|
+
isEdgeCompact,
|
|
205
219
|
isPresentationMode,
|
|
206
220
|
resolvedZeroLine,
|
|
207
221
|
legendBackgroundColor,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { margin, MINIMUM_PLOT_WIDTH } from './constants'
|
|
4
|
+
import { getGridGap, type NormalizedWaveformGridOptions } from './grid'
|
|
5
|
+
import { buildYAxisSlots } from './layout'
|
|
6
|
+
import type { DisplayTrack } from './types'
|
|
7
|
+
import type { ResolvedWaveformChartProps } from './waveformChartTypes'
|
|
8
|
+
import { resolveYAxisLayoutMetrics } from './yAxisLayoutMetrics'
|
|
9
|
+
import { Y_AXIS_OUTER_PADDING } from './yAxisConstants'
|
|
10
|
+
|
|
11
|
+
interface YAxisLayoutContext {
|
|
12
|
+
props: ResolvedWaveformChartProps
|
|
13
|
+
chartTracks: ComputedRef<DisplayTrack[]>
|
|
14
|
+
layoutTracks: ComputedRef<DisplayTrack[]>
|
|
15
|
+
chartWidth: ComputedRef<number>
|
|
16
|
+
gridOptions: ComputedRef<NormalizedWaveformGridOptions>
|
|
17
|
+
viewportYDomains: ComputedRef<Record<string, [number, number]>>
|
|
18
|
+
yAxisTickCount: ComputedRef<number>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useWaveformYAxisLayout(context: YAxisLayoutContext) {
|
|
22
|
+
const {
|
|
23
|
+
props,
|
|
24
|
+
chartTracks,
|
|
25
|
+
layoutTracks,
|
|
26
|
+
chartWidth,
|
|
27
|
+
gridOptions,
|
|
28
|
+
viewportYDomains,
|
|
29
|
+
yAxisTickCount,
|
|
30
|
+
} = context
|
|
31
|
+
const isEdgeCompact = computed(() => props.layoutPreset === 'edge-compact')
|
|
32
|
+
const showUnits = computed(() => !isEdgeCompact.value || props.unitDisplayMode === 'axis')
|
|
33
|
+
const yAxisMetrics = computed(() =>
|
|
34
|
+
resolveYAxisLayoutMetrics(
|
|
35
|
+
chartTracks.value,
|
|
36
|
+
props.overlayMode,
|
|
37
|
+
props.yDomain,
|
|
38
|
+
props.yDomains,
|
|
39
|
+
viewportYDomains.value,
|
|
40
|
+
props.axes?.y?.nice !== false,
|
|
41
|
+
yAxisTickCount.value,
|
|
42
|
+
props.displayMode === 'compact',
|
|
43
|
+
props.axes?.y?.upperPaddingRatio,
|
|
44
|
+
showUnits.value,
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
const hasYAxisLabels = computed(() =>
|
|
48
|
+
chartTracks.value.some(
|
|
49
|
+
(track) =>
|
|
50
|
+
track.visibleSeries.length === 1 &&
|
|
51
|
+
Boolean(track.visibleSeries[0]?.name.trim() || props.yLabel),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
const hasVisibleWaveformData = computed(() =>
|
|
55
|
+
chartTracks.value.some((track) => track.visibleSeries.length > 0),
|
|
56
|
+
)
|
|
57
|
+
const chartLeftMargin = computed(() =>
|
|
58
|
+
Math.max(
|
|
59
|
+
margin.left,
|
|
60
|
+
hasYAxisLabels.value
|
|
61
|
+
? yAxisMetrics.value.fullClearance
|
|
62
|
+
: hasVisibleWaveformData.value
|
|
63
|
+
? yAxisMetrics.value.tickClearance
|
|
64
|
+
: 0,
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
const yAxisSlots = computed(() =>
|
|
68
|
+
buildYAxisSlots(
|
|
69
|
+
layoutTracks.value.filter((track) => track.visibleSeries.length > 0),
|
|
70
|
+
props.overlayMode,
|
|
71
|
+
props.yDomain,
|
|
72
|
+
props.yDomains,
|
|
73
|
+
yAxisTickCount.value,
|
|
74
|
+
props.axes?.y?.nice !== false,
|
|
75
|
+
props.displayMode === 'compact',
|
|
76
|
+
viewportYDomains.value,
|
|
77
|
+
props.axes?.y?.upperPaddingRatio,
|
|
78
|
+
showUnits.value,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
const resolvedChartLeftMargin = computed(() =>
|
|
82
|
+
isEdgeCompact.value
|
|
83
|
+
? margin.right + Math.max(0, compactLeftClearance.value - Y_AXIS_OUTER_PADDING)
|
|
84
|
+
: props.overlayMode === 'multi-axis'
|
|
85
|
+
? Math.max(chartLeftMargin.value, yAxisSlots.value.clearance.left)
|
|
86
|
+
: chartLeftMargin.value,
|
|
87
|
+
)
|
|
88
|
+
// The same slot places the title and reserves its entire 20px band. The outer
|
|
89
|
+
// padding belongs to the chart edge, rather than the old fixed 48px minimum.
|
|
90
|
+
const compactLeftClearance = computed(() =>
|
|
91
|
+
!hasVisibleWaveformData.value
|
|
92
|
+
? 0
|
|
93
|
+
: props.overlayMode === 'multi-axis' || hasYAxisLabels.value
|
|
94
|
+
? yAxisSlots.value.clearance.left
|
|
95
|
+
: yAxisMetrics.value.tickClearance,
|
|
96
|
+
)
|
|
97
|
+
const chartRightMargin = computed(() =>
|
|
98
|
+
props.overlayMode === 'multi-axis'
|
|
99
|
+
? Math.max(margin.right, yAxisSlots.value.clearance.right)
|
|
100
|
+
: margin.right,
|
|
101
|
+
)
|
|
102
|
+
const innerWidth = computed(() =>
|
|
103
|
+
Math.max(0, chartWidth.value - resolvedChartLeftMargin.value - chartRightMargin.value),
|
|
104
|
+
)
|
|
105
|
+
const yAxisLayout = computed(() => {
|
|
106
|
+
const baseGap = getGridGap(props.displayMode)
|
|
107
|
+
const columnCount = gridOptions.value.columnCount
|
|
108
|
+
const hasMultipleColumns = columnCount > 1
|
|
109
|
+
const fullGap = Math.max(
|
|
110
|
+
baseGap,
|
|
111
|
+
isEdgeCompact.value ? yAxisSlots.value.clearance.left : yAxisMetrics.value.fullClearance,
|
|
112
|
+
)
|
|
113
|
+
const tickGap = Math.max(baseGap, yAxisMetrics.value.tickClearance)
|
|
114
|
+
const plotWidth = (innerWidth.value - fullGap * Math.max(0, columnCount - 1)) / columnCount
|
|
115
|
+
const canReserveLabelClearance = plotWidth >= MINIMUM_PLOT_WIDTH
|
|
116
|
+
return {
|
|
117
|
+
horizontalGap:
|
|
118
|
+
props.overlayMode === 'multi-axis' && hasMultipleColumns && hasVisibleWaveformData.value
|
|
119
|
+
? Math.max(baseGap, yAxisSlots.value.clearance.left + yAxisSlots.value.clearance.right)
|
|
120
|
+
: hasMultipleColumns && hasVisibleWaveformData.value
|
|
121
|
+
? hasYAxisLabels.value && canReserveLabelClearance
|
|
122
|
+
? fullGap
|
|
123
|
+
: tickGap
|
|
124
|
+
: baseGap,
|
|
125
|
+
hideSecondaryLabels:
|
|
126
|
+
props.overlayMode !== 'multi-axis' &&
|
|
127
|
+
hasMultipleColumns &&
|
|
128
|
+
hasYAxisLabels.value &&
|
|
129
|
+
!canReserveLabelClearance,
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
return {
|
|
133
|
+
yAxisMetrics,
|
|
134
|
+
yAxisSlots,
|
|
135
|
+
resolvedChartLeftMargin,
|
|
136
|
+
innerWidth,
|
|
137
|
+
yAxisLayout,
|
|
138
|
+
hasVisibleWaveformData,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -3,6 +3,8 @@ import type {
|
|
|
3
3
|
WaveformAxesOptions,
|
|
4
4
|
WaveformData,
|
|
5
5
|
WaveformDisplayMode,
|
|
6
|
+
WaveformUnitDisplayMode,
|
|
7
|
+
WaveformLayoutPreset,
|
|
6
8
|
WaveformFrameStyle,
|
|
7
9
|
WaveformInteractionMode,
|
|
8
10
|
WaveformLegendOptions,
|
|
@@ -18,6 +20,7 @@ import type {
|
|
|
18
20
|
WaveformZeroLineOptions,
|
|
19
21
|
WaveformZoomIntentPayload,
|
|
20
22
|
WaveformZoomEndPayload,
|
|
23
|
+
WaveformPanEndPayload,
|
|
21
24
|
WaveformZoomResetPayload,
|
|
22
25
|
} from '../data/types'
|
|
23
26
|
import type { WaveformGridOptions } from './grid'
|
|
@@ -26,6 +29,10 @@ export interface WaveformChartProps {
|
|
|
26
29
|
data: WaveformData
|
|
27
30
|
displayMode?: WaveformDisplayMode
|
|
28
31
|
overlayMode?: WaveformOverlayMode
|
|
32
|
+
/** Default axis units; optional chart-wide single-series legend units. */
|
|
33
|
+
unitDisplayMode?: WaveformUnitDisplayMode
|
|
34
|
+
/** Default spacing or compact title/time labels with a separate pagination band. */
|
|
35
|
+
layoutPreset?: WaveformLayoutPreset
|
|
29
36
|
width?: number
|
|
30
37
|
height?: number
|
|
31
38
|
xLabel?: string
|
|
@@ -36,6 +43,8 @@ export interface WaveformChartProps {
|
|
|
36
43
|
/** Snap viewport endpoints outwards to integer X-axis display units. */
|
|
37
44
|
integerZoom?: boolean
|
|
38
45
|
pannable?: boolean
|
|
46
|
+
/** Global record bounds in seconds; enables horizontal panning beyond loaded data. */
|
|
47
|
+
panXDomain?: [number, number]
|
|
39
48
|
minZoomSpan?: number
|
|
40
49
|
minVisiblePoints?: number
|
|
41
50
|
maxZoomScale?: number | null
|
|
@@ -68,6 +77,8 @@ export interface WaveformChartProps {
|
|
|
68
77
|
type DefaultedProp =
|
|
69
78
|
| 'displayMode'
|
|
70
79
|
| 'overlayMode'
|
|
80
|
+
| 'unitDisplayMode'
|
|
81
|
+
| 'layoutPreset'
|
|
71
82
|
| 'yLabel'
|
|
72
83
|
| 'lineColor'
|
|
73
84
|
| 'showTooltip'
|
|
@@ -97,6 +108,7 @@ export interface WaveformChartEmit {
|
|
|
97
108
|
(event: 'zoom-intent', payload: WaveformZoomIntentPayload): void
|
|
98
109
|
(event: 'zoom-change', domain: [number, number]): void
|
|
99
110
|
(event: 'zoom-end', payload: WaveformZoomEndPayload): void
|
|
111
|
+
(event: 'pan-end', payload: WaveformPanEndPayload): void
|
|
100
112
|
(event: 'zoom-reset', payload: WaveformZoomResetPayload): void
|
|
101
113
|
(event: 'update:annotations', annotations: WaveformAnnotation[]): void
|
|
102
114
|
(event: 'update:hidden-series-ids', ids: string[]): void
|
|
@@ -40,11 +40,20 @@ export function resolveYAxisLayoutMetrics(
|
|
|
40
40
|
nice: boolean,
|
|
41
41
|
tickCount: number,
|
|
42
42
|
compact: boolean,
|
|
43
|
+
upperPaddingRatio = 0,
|
|
44
|
+
showUnits = true,
|
|
43
45
|
): YAxisLayoutMetrics {
|
|
44
46
|
const axisText = chartTracks
|
|
45
47
|
.filter((track) => track.visibleSeries.length > 0)
|
|
46
48
|
.flatMap((track) =>
|
|
47
|
-
resolveRenderedYAxisSeriesGroups(
|
|
49
|
+
resolveRenderedYAxisSeriesGroups(
|
|
50
|
+
track,
|
|
51
|
+
overlayMode,
|
|
52
|
+
yDomain,
|
|
53
|
+
yDomains,
|
|
54
|
+
viewportYDomains,
|
|
55
|
+
upperPaddingRatio,
|
|
56
|
+
),
|
|
48
57
|
)
|
|
49
58
|
.map(
|
|
50
59
|
(group) =>
|
|
@@ -52,7 +61,7 @@ export function resolveYAxisLayoutMetrics(
|
|
|
52
61
|
group.domain,
|
|
53
62
|
nice,
|
|
54
63
|
undefined,
|
|
55
|
-
group.seriesList[0]?.unit,
|
|
64
|
+
showUnits ? group.seriesList[0]?.unit : undefined,
|
|
56
65
|
tickCount,
|
|
57
66
|
compact,
|
|
58
67
|
).tickTextWidth,
|
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
export type {
|
|
8
8
|
WaveformPoint,
|
|
9
9
|
WaveformDisplayMode,
|
|
10
|
+
WaveformUnitDisplayMode,
|
|
11
|
+
WaveformLayoutPreset,
|
|
10
12
|
WaveformOverlayMode,
|
|
11
13
|
WaveformInteractionMode,
|
|
12
14
|
WaveformXDomainStrategy,
|
|
13
15
|
WaveformZoomIntentPayload,
|
|
14
16
|
WaveformZoomEndPayload,
|
|
17
|
+
WaveformPanEndPayload,
|
|
15
18
|
WaveformZoomResetPayload,
|
|
16
19
|
WaveformAnnotationStyle,
|
|
17
20
|
WaveformAnnotation,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
|
|
2
|
+
import {
|
|
3
|
+
constrainZoomDomain,
|
|
4
|
+
resolveMinimumZoomSpan,
|
|
5
|
+
type ZoomSeriesGroup,
|
|
6
|
+
} from './zoomConstraints'
|
|
7
|
+
|
|
8
|
+
export function clampViewportDomain(
|
|
9
|
+
domain: [number, number],
|
|
10
|
+
boundary: [number, number],
|
|
11
|
+
): [number, number] {
|
|
12
|
+
const span = domain[1] - domain[0]
|
|
13
|
+
const boundarySpan = boundary[1] - boundary[0]
|
|
14
|
+
if (span >= boundarySpan) return [...boundary]
|
|
15
|
+
if (domain[0] < boundary[0]) return [boundary[0], boundary[0] + span]
|
|
16
|
+
if (domain[1] > boundary[1]) return [boundary[1] - span, boundary[1]]
|
|
17
|
+
return domain
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function remotePanBoundary(props: Pick<ResolvedWaveformChartProps, 'panXDomain'>) {
|
|
21
|
+
const domain = props.panXDomain
|
|
22
|
+
return domain && domain.every(Number.isFinite) && domain[0] < domain[1] ? domain : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Restore remote windows without requiring points inside a not-yet-loaded interval. */
|
|
26
|
+
export function restoreViewportDomain(
|
|
27
|
+
domain: [number, number],
|
|
28
|
+
baseDomain: [number, number],
|
|
29
|
+
groups: readonly ZoomSeriesGroup[],
|
|
30
|
+
props: ResolvedWaveformChartProps,
|
|
31
|
+
): [number, number] {
|
|
32
|
+
const boundary = remotePanBoundary(props)
|
|
33
|
+
return boundary
|
|
34
|
+
? clampViewportDomain(domain, boundary)
|
|
35
|
+
: constrainZoomDomain(domain, baseDomain, groups, props)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Wheel/box zoom keeps the remote position and uses loaded data for scale limits. */
|
|
39
|
+
export function constrainViewportZoom(
|
|
40
|
+
domain: [number, number],
|
|
41
|
+
baseDomain: [number, number],
|
|
42
|
+
groups: readonly ZoomSeriesGroup[],
|
|
43
|
+
props: ResolvedWaveformChartProps,
|
|
44
|
+
) {
|
|
45
|
+
const boundary = remotePanBoundary(props)
|
|
46
|
+
if (!boundary) return constrainZoomDomain(domain, baseDomain, groups, props)
|
|
47
|
+
return constrainZoomDomain(domain, boundary, [], {
|
|
48
|
+
...props,
|
|
49
|
+
minVisiblePoints: 0,
|
|
50
|
+
maxZoomScale: null,
|
|
51
|
+
minZoomSpan: resolveMinimumZoomSpan(baseDomain, groups, props),
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -2,17 +2,17 @@ import { pointer, zoomIdentity, type ZoomTransform } from 'd3'
|
|
|
2
2
|
import { computed, nextTick, shallowRef, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
|
3
3
|
import { MINIMUM_SELECTION_SIZE } from '../core/constants'
|
|
4
4
|
import type { DisplayTrack, TrackLayout } from '../core/types'
|
|
5
|
-
import { hasFixedYDomainForTrack } from '../core/yDomain'
|
|
6
5
|
import type { ResolvedWaveformChartProps, ViewportSelectionState } from '../core/waveformChartTypes'
|
|
7
6
|
import type { WaveformChartEmit } from '../core/waveformChartTypes'
|
|
8
7
|
import type { AnnotationSeriesCandidate } from '../annotation'
|
|
9
8
|
import { tryReleasePointerCapture } from './pointerCapture'
|
|
10
9
|
import { transitionViewportInteraction } from './viewportInteractionState'
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
10
|
+
import { createViewportDomainSetter } from './viewportDomain'
|
|
11
|
+
import { constrainViewportZoom } from './remoteViewport'
|
|
12
|
+
import { createViewportPan } from './viewportPan'
|
|
13
|
+
import { transformForDomain } from './zoomConstraints'
|
|
14
14
|
import { emitBoxZoomIntent } from './zoomEventPayload'
|
|
15
|
-
interface ViewportContext {
|
|
15
|
+
export interface ViewportContext {
|
|
16
16
|
props: ResolvedWaveformChartProps
|
|
17
17
|
emit: WaveformChartEmit
|
|
18
18
|
selection: Ref<ViewportSelectionState | null>
|
|
@@ -141,54 +141,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
141
141
|
trackLayouts.value.find((item) => item.hasVisibleSeries)
|
|
142
142
|
if (track) beginViewportDrag(event, track.index, false)
|
|
143
143
|
}
|
|
144
|
-
const applyPan = (
|
|
145
|
-
const width = track.width || 1
|
|
146
|
-
const height = track.height || 1
|
|
147
|
-
const dx = active.currentX - active.startX
|
|
148
|
-
const dy = active.currentY - active.startY
|
|
149
|
-
const xSpan = active.xDomain[1] - active.xDomain[0]
|
|
150
|
-
const sourceXDomain = active.independent
|
|
151
|
-
? resolveInitialTrackDomain(track)
|
|
152
|
-
: initialXDomain.value
|
|
153
|
-
const nextX = alignIntegerPanDomain(
|
|
154
|
-
clampDomain(
|
|
155
|
-
[active.xDomain[0] - (dx / width) * xSpan, active.xDomain[1] - (dx / width) * xSpan],
|
|
156
|
-
sourceXDomain,
|
|
157
|
-
),
|
|
158
|
-
props,
|
|
159
|
-
)
|
|
160
|
-
if (active.independent) {
|
|
161
|
-
const nextTransforms = [...independentTransforms.value]
|
|
162
|
-
nextTransforms[track.index] = transformForDomain(nextX, sourceXDomain, width)
|
|
163
|
-
independentTransforms.value = nextTransforms
|
|
164
|
-
} else {
|
|
165
|
-
sharedTransform.value = transformForDomain(nextX, sourceXDomain, innerWidth.value)
|
|
166
|
-
}
|
|
167
|
-
const targets = active.independent
|
|
168
|
-
? [track]
|
|
169
|
-
: trackLayouts.value.filter((target) => target.hasVisibleSeries)
|
|
170
|
-
const nextIndependentDomains = { ...independentYDomains.value }
|
|
171
|
-
const nextSharedDomains = { ...sharedYDomains.value }
|
|
172
|
-
targets.forEach((target) => {
|
|
173
|
-
const chartTrack = chartTracks.value.find((item) => item.id === target.id)
|
|
174
|
-
if (chartTrack && hasFixedYDomainForTrack(chartTrack, props.yDomain, props.yDomains)) {
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
const key = target.series?.trackId ?? target.series?.id ?? target.id
|
|
178
|
-
const source = active.yDomains[key] ?? (target.yScale.domain() as [number, number])
|
|
179
|
-
const boundary = chartTrack?.yDomain ?? source
|
|
180
|
-
const ySpan = source[1] - source[0]
|
|
181
|
-
const nextY = clampDomain(
|
|
182
|
-
[source[0] + (dy / height) * ySpan, source[1] + (dy / height) * ySpan],
|
|
183
|
-
boundary,
|
|
184
|
-
)
|
|
185
|
-
if (active.independent) nextIndependentDomains[target.index] = nextY
|
|
186
|
-
else nextSharedDomains[key] = nextY
|
|
187
|
-
})
|
|
188
|
-
if (active.independent) independentYDomains.value = nextIndependentDomains
|
|
189
|
-
else sharedYDomains.value = nextSharedDomains
|
|
190
|
-
emit('zoom-change', nextX)
|
|
191
|
-
}
|
|
144
|
+
const applyPan = createViewportPan(context)
|
|
192
145
|
const updateViewportDrag = (event: PointerEvent) => {
|
|
193
146
|
if (isPresentationMode.value) return
|
|
194
147
|
const active = selection.value
|
|
@@ -238,7 +191,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
238
191
|
const groups = active.independent
|
|
239
192
|
? [track.seriesList]
|
|
240
193
|
: trackLayouts.value.filter((item) => item.hasVisibleSeries).map((item) => item.seriesList)
|
|
241
|
-
const xDomain =
|
|
194
|
+
const xDomain = constrainViewportZoom(
|
|
242
195
|
[track.xScale.invert(left), track.xScale.invert(right)],
|
|
243
196
|
baseXDomain,
|
|
244
197
|
groups,
|
|
@@ -320,7 +273,16 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
320
273
|
activeOverlay.value = undefined
|
|
321
274
|
event.preventDefault()
|
|
322
275
|
if (completed.kind === 'pan') {
|
|
323
|
-
applyPan(completed, track)
|
|
276
|
+
const domain = applyPan(completed, track)
|
|
277
|
+
if (domain.some((value, index) => Math.abs(value - completed.xDomain[index]) > 1e-12)) {
|
|
278
|
+
emit('pan-end', {
|
|
279
|
+
start: domain[0],
|
|
280
|
+
end: domain[1],
|
|
281
|
+
...(completed.independent
|
|
282
|
+
? { trackIndex: track.index, seriesIds: track.seriesList.map((series) => series.id) }
|
|
283
|
+
: {}),
|
|
284
|
+
})
|
|
285
|
+
}
|
|
324
286
|
void nextTick(configureZoom)
|
|
325
287
|
return
|
|
326
288
|
}
|
|
@@ -15,13 +15,9 @@ import { WHEEL_ZOOM_DEBOUNCE_MS, ZOOM_CONSTRAINTS } from '../core/constants'
|
|
|
15
15
|
import type { TrackLayout } from '../core/types'
|
|
16
16
|
import type { ResolvedWaveformChartProps, WaveformChartEmit } from '../core/waveformChartTypes'
|
|
17
17
|
import { useAnimationFrameThrottle } from '../utils/useAnimationFrameThrottle'
|
|
18
|
+
import { constrainViewportZoom } from './remoteViewport'
|
|
18
19
|
import { normalizeIntegerZoomDomain } from './integerZoom'
|
|
19
|
-
import {
|
|
20
|
-
constrainZoomDomain,
|
|
21
|
-
resolveMinimumZoomSpan,
|
|
22
|
-
transformForDomain,
|
|
23
|
-
type ZoomSeriesGroup,
|
|
24
|
-
} from './zoomConstraints'
|
|
20
|
+
import { resolveMinimumZoomSpan, transformForDomain, type ZoomSeriesGroup } from './zoomConstraints'
|
|
25
21
|
import {
|
|
26
22
|
emitIndependentWheelZoomEnds,
|
|
27
23
|
emitSharedWheelZoomEnd,
|
|
@@ -240,7 +236,11 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
240
236
|
number,
|
|
241
237
|
number,
|
|
242
238
|
]
|
|
243
|
-
return transformForDomain(
|
|
239
|
+
return transformForDomain(
|
|
240
|
+
constrainViewportZoom(requested, domain, groups, props),
|
|
241
|
+
domain,
|
|
242
|
+
width,
|
|
243
|
+
)
|
|
244
244
|
}
|
|
245
245
|
const canZoomTrack = (track: TrackLayout): boolean => {
|
|
246
246
|
const minimum = Number(props.minVisiblePoints)
|
|
@@ -4,7 +4,8 @@ import type { ZoomTransform } from 'd3'
|
|
|
4
4
|
import type { TrackLayout } from '../core/types'
|
|
5
5
|
import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
|
|
6
6
|
import type { AnnotationSeriesCandidate } from '../annotation'
|
|
7
|
-
import {
|
|
7
|
+
import { transformForDomain } from './zoomConstraints'
|
|
8
|
+
import { constrainViewportZoom } from './remoteViewport'
|
|
8
9
|
|
|
9
10
|
interface ViewportDomainContext {
|
|
10
11
|
props: ResolvedWaveformChartProps
|
|
@@ -21,17 +22,7 @@ interface ViewportDomainContext {
|
|
|
21
22
|
configureZoom: () => void
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export
|
|
25
|
-
domain: [number, number],
|
|
26
|
-
boundary: [number, number],
|
|
27
|
-
): [number, number] {
|
|
28
|
-
const span = domain[1] - domain[0]
|
|
29
|
-
const boundarySpan = boundary[1] - boundary[0]
|
|
30
|
-
if (span >= boundarySpan) return [...boundary]
|
|
31
|
-
if (domain[0] < boundary[0]) return [boundary[0], boundary[0] + span]
|
|
32
|
-
if (domain[1] > boundary[1]) return [boundary[1] - span, boundary[1]]
|
|
33
|
-
return domain
|
|
34
|
-
}
|
|
25
|
+
export { clampViewportDomain } from './remoteViewport'
|
|
35
26
|
|
|
36
27
|
export function createViewportDomainSetter(context: ViewportDomainContext) {
|
|
37
28
|
return (domain: [number, number], trackIndex?: number) => {
|
|
@@ -49,7 +40,12 @@ export function createViewportDomainSetter(context: ViewportDomainContext) {
|
|
|
49
40
|
const track = context.trackLayouts.value.find((item) => item.index === index)
|
|
50
41
|
if (!track) return
|
|
51
42
|
const boundary = context.resolveInitialTrackDomain(track)
|
|
52
|
-
const constrained =
|
|
43
|
+
const constrained = constrainViewportZoom(
|
|
44
|
+
domain,
|
|
45
|
+
boundary,
|
|
46
|
+
[track.seriesList],
|
|
47
|
+
context.props,
|
|
48
|
+
)
|
|
53
49
|
nextTransforms[index] = transformForDomain(constrained, boundary, track.width)
|
|
54
50
|
})
|
|
55
51
|
context.independentTransforms.value = nextTransforms
|
|
@@ -58,7 +54,7 @@ export function createViewportDomainSetter(context: ViewportDomainContext) {
|
|
|
58
54
|
const groups = context.trackLayouts.value
|
|
59
55
|
.filter((track) => track.hasVisibleSeries)
|
|
60
56
|
.map((track) => track.seriesList)
|
|
61
|
-
const constrained =
|
|
57
|
+
const constrained = constrainViewportZoom(domain, boundary, groups, context.props)
|
|
62
58
|
context.sharedTransform.value = transformForDomain(
|
|
63
59
|
constrained,
|
|
64
60
|
boundary,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { TrackLayout } from '../core/types'
|
|
2
|
+
import { hasFixedYDomainForTrack } from '../core/yDomain'
|
|
3
|
+
import type { ViewportSelectionState } from '../core/waveformChartTypes'
|
|
4
|
+
import type { ViewportContext } from './useWaveformViewport'
|
|
5
|
+
import { clampViewportDomain as clampDomain } from './viewportDomain'
|
|
6
|
+
import { alignIntegerPanDomain } from './integerZoom'
|
|
7
|
+
import { remotePanBoundary } from './remoteViewport'
|
|
8
|
+
import { transformForDomain } from './zoomConstraints'
|
|
9
|
+
|
|
10
|
+
export function createViewportPan(context: ViewportContext) {
|
|
11
|
+
const {
|
|
12
|
+
props,
|
|
13
|
+
emit,
|
|
14
|
+
resolveInitialTrackDomain,
|
|
15
|
+
initialXDomain,
|
|
16
|
+
independentTransforms,
|
|
17
|
+
sharedTransform,
|
|
18
|
+
innerWidth,
|
|
19
|
+
trackLayouts,
|
|
20
|
+
chartTracks,
|
|
21
|
+
independentYDomains,
|
|
22
|
+
sharedYDomains,
|
|
23
|
+
} = context
|
|
24
|
+
const applyPan = (active: ViewportSelectionState, track: TrackLayout) => {
|
|
25
|
+
const width = track.width || 1
|
|
26
|
+
const height = track.height || 1
|
|
27
|
+
const dx = active.currentX - active.startX
|
|
28
|
+
const dy = active.currentY - active.startY
|
|
29
|
+
const xSpan = active.xDomain[1] - active.xDomain[0]
|
|
30
|
+
const sourceXDomain = active.independent
|
|
31
|
+
? resolveInitialTrackDomain(track)
|
|
32
|
+
: initialXDomain.value
|
|
33
|
+
const boundary = remotePanBoundary(props) ?? sourceXDomain
|
|
34
|
+
const nextX = clampDomain(
|
|
35
|
+
alignIntegerPanDomain(
|
|
36
|
+
clampDomain(
|
|
37
|
+
[active.xDomain[0] - (dx / width) * xSpan, active.xDomain[1] - (dx / width) * xSpan],
|
|
38
|
+
boundary,
|
|
39
|
+
),
|
|
40
|
+
props,
|
|
41
|
+
),
|
|
42
|
+
boundary,
|
|
43
|
+
)
|
|
44
|
+
if (active.independent) {
|
|
45
|
+
const nextTransforms = [...independentTransforms.value]
|
|
46
|
+
nextTransforms[track.index] = transformForDomain(nextX, sourceXDomain, width)
|
|
47
|
+
independentTransforms.value = nextTransforms
|
|
48
|
+
} else {
|
|
49
|
+
sharedTransform.value = transformForDomain(nextX, sourceXDomain, innerWidth.value)
|
|
50
|
+
}
|
|
51
|
+
if (remotePanBoundary(props)) {
|
|
52
|
+
emit('zoom-change', nextX)
|
|
53
|
+
return nextX
|
|
54
|
+
}
|
|
55
|
+
const targets = active.independent
|
|
56
|
+
? [track]
|
|
57
|
+
: trackLayouts.value.filter((target) => target.hasVisibleSeries)
|
|
58
|
+
const nextIndependentDomains = { ...independentYDomains.value }
|
|
59
|
+
const nextSharedDomains = { ...sharedYDomains.value }
|
|
60
|
+
targets.forEach((target) => {
|
|
61
|
+
const chartTrack = chartTracks.value.find((item) => item.id === target.id)
|
|
62
|
+
if (chartTrack && hasFixedYDomainForTrack(chartTrack, props.yDomain, props.yDomains)) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const key = target.series?.trackId ?? target.series?.id ?? target.id
|
|
66
|
+
const source = active.yDomains[key] ?? (target.yScale.domain() as [number, number])
|
|
67
|
+
const boundary = chartTrack?.yDomain ?? source
|
|
68
|
+
const ySpan = source[1] - source[0]
|
|
69
|
+
const nextY = clampDomain(
|
|
70
|
+
[source[0] + (dy / height) * ySpan, source[1] + (dy / height) * ySpan],
|
|
71
|
+
boundary,
|
|
72
|
+
)
|
|
73
|
+
if (active.independent) nextIndependentDomains[target.index] = nextY
|
|
74
|
+
else nextSharedDomains[key] = nextY
|
|
75
|
+
})
|
|
76
|
+
if (active.independent) independentYDomains.value = nextIndependentDomains
|
|
77
|
+
else sharedYDomains.value = nextSharedDomains
|
|
78
|
+
emit('zoom-change', nextX)
|
|
79
|
+
return nextX
|
|
80
|
+
}
|
|
81
|
+
return applyPan
|
|
82
|
+
}
|
|
@@ -18,11 +18,13 @@ interface Props {
|
|
|
18
18
|
width: number
|
|
19
19
|
height: number
|
|
20
20
|
interactive?: boolean
|
|
21
|
+
showUnits?: boolean
|
|
21
22
|
hiddenSeriesIds?: string[]
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const props = withDefaults(defineProps<Props>(), {
|
|
25
26
|
interactive: false,
|
|
27
|
+
showUnits: false,
|
|
26
28
|
hiddenSeriesIds: () => [],
|
|
27
29
|
})
|
|
28
30
|
const emit = defineEmits<{
|
|
@@ -30,6 +32,11 @@ const emit = defineEmits<{
|
|
|
30
32
|
}>()
|
|
31
33
|
const hiddenSeriesIdSet = computed(() => new Set(props.hiddenSeriesIds))
|
|
32
34
|
|
|
35
|
+
function legendLabel(series: DisplaySeries): string {
|
|
36
|
+
const unit = series.unit?.trim()
|
|
37
|
+
return props.showUnits && unit && unit !== '--' ? `${series.name} (${unit})` : series.name
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
function isHidden(seriesId: string): boolean {
|
|
34
41
|
return hiddenSeriesIdSet.value.has(seriesId)
|
|
35
42
|
}
|
|
@@ -113,7 +120,9 @@ function toggleSeries(seriesId: string) {
|
|
|
113
120
|
transform="translate(13 8)"
|
|
114
121
|
/>
|
|
115
122
|
</svg>
|
|
116
|
-
<span class="waveform-legend__label" :title="item
|
|
123
|
+
<span class="waveform-legend__label" :title="legendLabel(item)">{{
|
|
124
|
+
legendLabel(item)
|
|
125
|
+
}}</span>
|
|
117
126
|
</button>
|
|
118
127
|
</div>
|
|
119
128
|
</div>
|