waveform-analysis 0.1.65 → 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 +146 -5
- package/dist/index.cjs +1 -1
- package/dist/index.js +1326 -1118
- package/dist/style.css +1 -1
- package/dist/types/components/WaveformChart.vue.d.ts +7 -2
- package/dist/types/components/core/constants.d.ts +2 -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 +5 -0
- package/dist/types/components/core/types.d.ts +2 -1
- package/dist/types/components/core/useWaveformChartController.d.ts +9 -0
- package/dist/types/components/core/useWaveformLayout.d.ts +1 -0
- package/dist/types/components/core/useWaveformPresentation.d.ts +3 -0
- package/dist/types/components/core/useWaveformYAxisLayout.d.ts +31 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +11 -2
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +1 -1
- package/dist/types/components/core/yAxisTitleLayout.d.ts +4 -0
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/interaction/integerZoom.d.ts +12 -0
- 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 -1
- package/dist/types/components/interaction/viewportPan.d.ts +4 -0
- package/dist/types/components/interaction/zoomConstraints.d.ts +2 -1
- package/dist/types/components/rendering/WaveformLegend.vue.d.ts +2 -0
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +6 -6
- package/dist/types/components/rendering/WaveformTrackAxes.vue.d.ts +5 -1
- package/dist/types/components/rendering/WaveformTrackBackdrop.vue.d.ts +0 -4
- package/dist/types/components/rendering/WaveformZeroLines.vue.d.ts +12 -0
- package/dist/types/data/recordedDampedWaveform.d.ts +1 -0
- package/dist/types/demo/types.d.ts +3 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/types/chart.d.ts +17 -0
- package/dist/types/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/App.vue +6 -0
- package/src/components/WaveformChart.css +5 -0
- package/src/components/WaveformChart.vue +3 -0
- package/src/components/WaveformChartView.vue +14 -3
- package/src/components/core/constants.ts +2 -0
- 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 +39 -59
- package/src/components/core/types.ts +2 -0
- package/src/components/core/useWaveformChartController.ts +17 -1
- package/src/components/core/useWaveformChartLifecycle.ts +11 -17
- package/src/components/core/useWaveformLayout.ts +67 -107
- package/src/components/core/useWaveformPresentation.ts +27 -3
- package/src/components/core/useWaveformYAxisLayout.ts +140 -0
- package/src/components/core/waveformChartTypes.ts +15 -0
- package/src/components/core/yAxisLayoutMetrics.ts +11 -2
- package/src/components/core/yAxisTitleLayout.ts +78 -0
- package/src/components/data/types.ts +3 -0
- package/src/components/interaction/integerZoom.ts +64 -0
- package/src/components/interaction/interactionTarget.ts +10 -0
- package/src/components/interaction/remoteViewport.ts +53 -0
- package/src/components/interaction/useWaveformViewport.ts +16 -58
- package/src/components/interaction/useWaveformZoom.ts +13 -11
- package/src/components/interaction/viewportDomain.ts +11 -3
- package/src/components/interaction/viewportPan.ts +82 -0
- package/src/components/interaction/zoomConstraints.ts +7 -5
- package/src/components/rendering/WaveformLegend.vue +10 -1
- package/src/components/rendering/WaveformTrack.vue +24 -6
- package/src/components/rendering/WaveformTrackAxes.vue +16 -5
- package/src/components/rendering/WaveformTrackBackdrop.vue +2 -34
- package/src/components/rendering/WaveformZeroLines.vue +53 -0
- package/src/data/recordedDampedWaveform.ts +3 -0
- package/src/data/simulatedWaveforms.ts +12 -10
- package/src/demo/DemoChartHost.vue +1 -0
- package/src/demo/DemoGridFrameControls.vue +12 -0
- package/src/demo/DemoViewControls.vue +4 -0
- package/src/demo/types.ts +3 -0
- package/src/index.ts +4 -0
- package/src/styles.css +3 -3
- package/src/types/chart.ts +20 -0
- package/src/types/index.ts +3 -0
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
} from './grid'
|
|
28
28
|
import {
|
|
29
29
|
axisTextMetrics,
|
|
30
|
-
formatYAxisTickLabel,
|
|
31
30
|
resolveRenderedYAxisSeriesGroups,
|
|
32
31
|
resolveYAxisTicks,
|
|
33
32
|
resolveYAxisTickCount,
|
|
@@ -35,9 +34,15 @@ import {
|
|
|
35
34
|
} from './layout'
|
|
36
35
|
import type { DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
|
|
37
36
|
import { applyXDomainStrategy } from './xDomain'
|
|
37
|
+
import { alignLeftYAxisTitles } from './yAxisTitleLayout'
|
|
38
|
+
import {
|
|
39
|
+
alignIntegerZoomDomain,
|
|
40
|
+
normalizeIntegerZoomDomain,
|
|
41
|
+
integerZoomTicks,
|
|
42
|
+
formatIntegerZoomLabel,
|
|
43
|
+
} from '../interaction/integerZoom'
|
|
38
44
|
import {
|
|
39
45
|
Y_AXIS_LABEL_BAND_WIDTH,
|
|
40
|
-
Y_AXIS_CHARACTER_WIDTH,
|
|
41
46
|
Y_AXIS_LABEL_GAP,
|
|
42
47
|
Y_AXIS_OUTER_PADDING,
|
|
43
48
|
Y_AXIS_RIGHT_LABEL_OFFSET,
|
|
@@ -58,6 +63,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
58
63
|
initialXDomain?: [number, number]
|
|
59
64
|
initialXDomains?: Record<string, [number, number]>
|
|
60
65
|
xDomainStrategy?: WaveformXDomainStrategy
|
|
66
|
+
integerZoom?: boolean
|
|
61
67
|
fixedYDomain?: [number, number]
|
|
62
68
|
fixedYDomains?: Record<string, [number, number]>
|
|
63
69
|
yDomains?: Record<string, [number, number]>
|
|
@@ -65,6 +71,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
65
71
|
xAxisLabelFormatter?: WaveformXAxisLabelFormatter
|
|
66
72
|
yAxisSplitNumber?: number
|
|
67
73
|
yAxisNice?: boolean
|
|
74
|
+
yAxisUpperPaddingRatio?: number
|
|
68
75
|
rendering: ResolvedWaveformRenderingOptions
|
|
69
76
|
/** Latest sampling result for SVG lines only; source series remain complete for interaction. */
|
|
70
77
|
linePointOverrides?: Readonly<Record<string, WaveformPoint[]>>
|
|
@@ -73,6 +80,9 @@ export interface BuildTrackLayoutsOptions {
|
|
|
73
80
|
yLabel?: string
|
|
74
81
|
yAxisSlots?: readonly YAxisSlot[]
|
|
75
82
|
showCompactEmptyTracks: boolean
|
|
83
|
+
useNonEmptyBottomTracks?: boolean
|
|
84
|
+
compactYAxisLayout?: boolean
|
|
85
|
+
showAxisUnits?: boolean
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
function resolveIndependentXDomain(
|
|
@@ -85,13 +95,21 @@ function resolveIndependentXDomain(
|
|
|
85
95
|
options.initialXDomains?.[track.id] ??
|
|
86
96
|
options.initialXDomains?.[seriesId] ??
|
|
87
97
|
options.initialXDomain
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
return alignIntegerZoomDomain(
|
|
99
|
+
explicitDomain
|
|
100
|
+
? applyXDomainStrategy(explicitDomain, strategy, true)
|
|
101
|
+
: applyXDomainStrategy(track.xDomain, strategy),
|
|
102
|
+
options,
|
|
103
|
+
)
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayout[] {
|
|
94
|
-
const visibleCells = options.cells.map((cell) => ({
|
|
107
|
+
const visibleCells = options.cells.map((cell) => ({
|
|
108
|
+
...cell,
|
|
109
|
+
hasSeries: options.useNonEmptyBottomTracks
|
|
110
|
+
? Boolean(cell.series?.series.length)
|
|
111
|
+
: Boolean(cell.series),
|
|
112
|
+
}))
|
|
95
113
|
const bottomCells = getBottomRowCellIndexes(visibleCells, options.grid.columnCount)
|
|
96
114
|
|
|
97
115
|
const layouts: TrackLayout[] = visibleCells.flatMap((cell, index) => {
|
|
@@ -119,12 +137,14 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
119
137
|
? (options.independentTransforms[index] ?? zoomIdentity)
|
|
120
138
|
: zoomIdentity
|
|
121
139
|
const xScale = transform.rescaleX(baseXScale)
|
|
140
|
+
xScale.domain(normalizeIntegerZoomDomain(xScale.domain() as [number, number], options))
|
|
122
141
|
const yAxisGroups = resolveRenderedYAxisSeriesGroups(
|
|
123
142
|
displayTrack,
|
|
124
143
|
options.overlayMode,
|
|
125
144
|
options.fixedYDomain,
|
|
126
145
|
options.fixedYDomains,
|
|
127
146
|
options.yDomains,
|
|
147
|
+
options.yAxisUpperPaddingRatio,
|
|
128
148
|
)
|
|
129
149
|
const sideIndexes = { left: 0, right: 0 }
|
|
130
150
|
const sideOffsets = { left: 0, right: 0 }
|
|
@@ -141,7 +161,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
141
161
|
scale.domain() as [number, number],
|
|
142
162
|
false,
|
|
143
163
|
tickValues,
|
|
144
|
-
group.seriesList[0]?.unit,
|
|
164
|
+
options.showAxisUnits !== false ? group.seriesList[0]?.unit : undefined,
|
|
145
165
|
tickCount,
|
|
146
166
|
)
|
|
147
167
|
const clearance =
|
|
@@ -164,7 +184,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
164
184
|
(slot
|
|
165
185
|
? (group.side === 'left' ? 0 : cell.width) + slot.labelOffset
|
|
166
186
|
: x + (group.side === 'left' ? -labelDistance : labelDistance)) -
|
|
167
|
-
(group.side === 'right' ? Y_AXIS_RIGHT_LABEL_OFFSET : 0)
|
|
187
|
+
(group.side === 'right' && !options.compactYAxisLayout ? Y_AXIS_RIGHT_LABEL_OFFSET : 0)
|
|
168
188
|
if (!slot) sideOffsets[group.side] += clearance
|
|
169
189
|
return {
|
|
170
190
|
index: group.index,
|
|
@@ -181,25 +201,19 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
181
201
|
const fallbackYScale = scaleLinear(displayTrack.yDomain, [cell.plotHeight, 0])
|
|
182
202
|
if (options.yAxisNice !== false) fallbackYScale.nice()
|
|
183
203
|
const yScale = yAxes[0]?.scale ?? fallbackYScale
|
|
184
|
-
const xMajorTicks =
|
|
204
|
+
const xMajorTicks = integerZoomTicks(
|
|
205
|
+
xScale.domain() as [number, number],
|
|
206
|
+
Math.max(2, Math.floor(cell.width / 100)),
|
|
207
|
+
options,
|
|
208
|
+
)
|
|
209
|
+
const xAxisLabelFormatter =
|
|
210
|
+
options.xAxisLabelFormatter ?? (options.integerZoom ? formatIntegerZoomLabel : undefined)
|
|
185
211
|
const yMajorTicks = yAxes[0]?.majorTicks ?? []
|
|
186
212
|
const yAxisTickValues = yAxes[0]?.tickValues ?? []
|
|
187
213
|
const domain = xScale.domain() as [number, number]
|
|
188
214
|
const endpointLabels = {
|
|
189
|
-
start: formatXAxisLabel(
|
|
190
|
-
|
|
191
|
-
domain,
|
|
192
|
-
options.timeUnit,
|
|
193
|
-
'start',
|
|
194
|
-
options.xAxisLabelFormatter,
|
|
195
|
-
),
|
|
196
|
-
end: formatXAxisLabel(
|
|
197
|
-
domain[1],
|
|
198
|
-
domain,
|
|
199
|
-
options.timeUnit,
|
|
200
|
-
'end',
|
|
201
|
-
options.xAxisLabelFormatter,
|
|
202
|
-
),
|
|
215
|
+
start: formatXAxisLabel(domain[0], domain, options.timeUnit, 'start', xAxisLabelFormatter),
|
|
216
|
+
end: formatXAxisLabel(domain[1], domain, options.timeUnit, 'end', xAxisLabelFormatter),
|
|
203
217
|
}
|
|
204
218
|
const leftClearance = endpointLabels.start.length * 7 + 10
|
|
205
219
|
const rightClearance = endpointLabels.end.length * 7 + 10
|
|
@@ -262,6 +276,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
262
276
|
yScale,
|
|
263
277
|
yAxes,
|
|
264
278
|
xMajorTicks,
|
|
279
|
+
xAxisLabelFormatter,
|
|
265
280
|
xMinorTicks: buildMinorTicks(xMajorTicks, 3, domain),
|
|
266
281
|
yMajorTicks,
|
|
267
282
|
yMinorTicks: yAxes[0]?.minorTicks ?? [],
|
|
@@ -283,41 +298,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
283
298
|
}
|
|
284
299
|
})
|
|
285
300
|
|
|
286
|
-
|
|
287
|
-
const titleDistances = new Map<string, number>()
|
|
288
|
-
for (const track of layouts) {
|
|
289
|
-
let leftIndex = 0
|
|
290
|
-
for (const axis of track.yAxes) {
|
|
291
|
-
if (axis.side !== 'left') continue
|
|
292
|
-
const key = `${track.column}:${leftIndex++}`
|
|
293
|
-
const series = axis.seriesList[0]
|
|
294
|
-
const title = series?.name.trim() || options.yLabel || ''
|
|
295
|
-
const halfTitleHeight = title.length * 8 + 6
|
|
296
|
-
const nearbyTicks = axis.tickValues.filter(
|
|
297
|
-
(value) => Math.abs(axis.scale(value) - track.height / 2) <= halfTitleHeight,
|
|
298
|
-
)
|
|
299
|
-
const tickWidth = Math.max(
|
|
300
|
-
Y_AXIS_CHARACTER_WIDTH,
|
|
301
|
-
...nearbyTicks.map(
|
|
302
|
-
(value) =>
|
|
303
|
-
formatYAxisTickLabel(
|
|
304
|
-
value,
|
|
305
|
-
axis.scale.domain() as [number, number],
|
|
306
|
-
axis.tickValues,
|
|
307
|
-
series?.unit,
|
|
308
|
-
).length * Y_AXIS_CHARACTER_WIDTH,
|
|
309
|
-
),
|
|
310
|
-
)
|
|
311
|
-
const distance = tickWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
312
|
-
titleDistances.set(key, Math.max(titleDistances.get(key) ?? 0, distance))
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
for (const track of layouts) {
|
|
316
|
-
let leftIndex = 0
|
|
317
|
-
for (const axis of track.yAxes) {
|
|
318
|
-
if (axis.side !== 'left') continue
|
|
319
|
-
axis.labelX = axis.x - titleDistances.get(`${track.column}:${leftIndex++}`)!
|
|
320
|
-
}
|
|
321
|
-
}
|
|
301
|
+
if (!options.compactYAxisLayout) alignLeftYAxisTitles(layouts, options.yLabel)
|
|
322
302
|
return layouts
|
|
323
303
|
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
WaveformLineStyle,
|
|
6
6
|
WaveformPoint,
|
|
7
7
|
WaveformPointType,
|
|
8
|
+
WaveformXAxisLabelFormatter,
|
|
8
9
|
} from '../../types'
|
|
9
10
|
import type { WaveformPointSource } from '../../core/waveformPointSource'
|
|
10
11
|
import type { NormalizedWaveformGridLineOptions } from './grid'
|
|
@@ -114,6 +115,7 @@ export interface TrackLayout {
|
|
|
114
115
|
yMinorTicks: number[]
|
|
115
116
|
yAxisTickValues: number[]
|
|
116
117
|
xAxisTickValues: number[]
|
|
118
|
+
xAxisLabelFormatter?: WaveformXAxisLabelFormatter
|
|
117
119
|
endpointLabels: { start: string; end: string }
|
|
118
120
|
path: string | null
|
|
119
121
|
seriesPaths: TrackSeriesPath[]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { zoomIdentity, type ZoomTransform } from 'd3'
|
|
2
2
|
import {
|
|
3
|
+
computed,
|
|
3
4
|
reactive,
|
|
4
5
|
markRaw,
|
|
5
6
|
ref,
|
|
@@ -21,6 +22,7 @@ import { useWaveformZoom } from '../interaction/useWaveformZoom'
|
|
|
21
22
|
import { useAnimationFrameThrottle } from '../utils/useAnimationFrameThrottle'
|
|
22
23
|
import { margin } from './constants'
|
|
23
24
|
import { getPageSize } from './grid'
|
|
25
|
+
import { resolveCompactPaginationBand } from './pagination'
|
|
24
26
|
import type { WaveformHoverState } from './types'
|
|
25
27
|
import { useWaveformChartLifecycle } from './useWaveformChartLifecycle'
|
|
26
28
|
import { usePreparedWaveformSeries } from './useWaveformData'
|
|
@@ -165,9 +167,22 @@ export function useWaveformChartController(
|
|
|
165
167
|
linePointOverrides,
|
|
166
168
|
})
|
|
167
169
|
|
|
170
|
+
const paginationVisible = computed(
|
|
171
|
+
() => gridOptions.value.showPagination && pageCount.value > 1 && !isCleanView.value,
|
|
172
|
+
)
|
|
168
173
|
watchEffect(() => {
|
|
169
174
|
paginationBandHeight.value =
|
|
170
|
-
|
|
175
|
+
props.layoutPreset === 'edge-compact'
|
|
176
|
+
? resolveCompactPaginationBand(
|
|
177
|
+
paginationVisible.value,
|
|
178
|
+
chartWidth.value,
|
|
179
|
+
pageCount.value,
|
|
180
|
+
resolvedChartLeftMargin.value + innerWidth.value / 2,
|
|
181
|
+
layout.resolvedXLabel.value,
|
|
182
|
+
)
|
|
183
|
+
: gridOptions.value.showPagination && pageCount.value > 1 && chartWidth.value <= 520
|
|
184
|
+
? 40
|
|
185
|
+
: 0
|
|
171
186
|
})
|
|
172
187
|
|
|
173
188
|
const zoom = useWaveformZoom({
|
|
@@ -306,6 +321,7 @@ export function useWaveformChartController(
|
|
|
306
321
|
return reactive({
|
|
307
322
|
...toRefs(props),
|
|
308
323
|
...presentation,
|
|
324
|
+
paginationVisible,
|
|
309
325
|
...layout,
|
|
310
326
|
...annotations,
|
|
311
327
|
...viewport,
|
|
@@ -13,7 +13,9 @@ import type { AnnotationSeriesCandidate, useWaveformAnnotationInteraction } from
|
|
|
13
13
|
import type { NormalizedWaveformGridOptions } from './grid'
|
|
14
14
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
15
15
|
import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes'
|
|
16
|
-
import {
|
|
16
|
+
import { transformForDomain } from '../interaction/zoomConstraints'
|
|
17
|
+
import { remotePanBoundary, restoreViewportDomain } from '../interaction/remoteViewport'
|
|
18
|
+
import { isEditableTarget } from '../interaction/interactionTarget'
|
|
17
19
|
import { seriesIdentity } from '../interaction/zoomEventPayload'
|
|
18
20
|
|
|
19
21
|
interface LifecycleContext {
|
|
@@ -61,17 +63,6 @@ interface LifecycleContext {
|
|
|
61
63
|
clearZoomBindings: () => void
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
function isEditableTarget(target: EventTarget | null): boolean {
|
|
65
|
-
return (
|
|
66
|
-
target instanceof Element &&
|
|
67
|
-
Boolean(
|
|
68
|
-
target.closest(
|
|
69
|
-
'button, input, select, textarea, [contenteditable]:not([contenteditable="false"])',
|
|
70
|
-
),
|
|
71
|
-
)
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
66
|
export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
76
67
|
const {
|
|
77
68
|
props,
|
|
@@ -161,7 +152,8 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
161
152
|
trackLayouts.value.flatMap((track) => {
|
|
162
153
|
const current = track.xScale.domain() as [number, number]
|
|
163
154
|
const boundary = resolveInitialTrackDomain(track)
|
|
164
|
-
return
|
|
155
|
+
return remotePanBoundary(props) ||
|
|
156
|
+
current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
|
|
165
157
|
? [[seriesIdentity(track.seriesList.map((series) => series.id)), current]]
|
|
166
158
|
: []
|
|
167
159
|
}),
|
|
@@ -172,7 +164,9 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
172
164
|
const current = sharedZoomDomain.value
|
|
173
165
|
const boundary = initialXDomain.value
|
|
174
166
|
pendingSharedXDomain =
|
|
175
|
-
current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
|
|
167
|
+
remotePanBoundary(props) || current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
|
|
168
|
+
? [...current]
|
|
169
|
+
: undefined
|
|
176
170
|
}
|
|
177
171
|
|
|
178
172
|
function handleDataReferenceChange() {
|
|
@@ -193,7 +187,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
193
187
|
)
|
|
194
188
|
if (!previousDomain) return
|
|
195
189
|
const boundary = resolveInitialTrackDomain(track)
|
|
196
|
-
const domain =
|
|
190
|
+
const domain = restoreViewportDomain(previousDomain, boundary, [track.seriesList], props)
|
|
197
191
|
nextTransforms[track.index] = transformForDomain(domain, boundary, track.width)
|
|
198
192
|
})
|
|
199
193
|
independentTransforms.value = nextTransforms
|
|
@@ -203,7 +197,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
203
197
|
const groups = trackLayouts.value
|
|
204
198
|
.filter((track) => track.hasVisibleSeries)
|
|
205
199
|
.map((track) => track.seriesList)
|
|
206
|
-
const domain =
|
|
200
|
+
const domain = restoreViewportDomain(pendingSharedXDomain, boundary, groups, props)
|
|
207
201
|
sharedTransform.value = transformForDomain(domain, boundary, innerWidth.value)
|
|
208
202
|
pendingSharedXDomain = undefined
|
|
209
203
|
}
|
|
@@ -239,7 +233,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
239
233
|
)
|
|
240
234
|
|
|
241
235
|
watch(
|
|
242
|
-
() => props.displayMode,
|
|
236
|
+
() => [props.displayMode, props.integerZoom, props.integerZoom ? props.timeUnit : undefined],
|
|
243
237
|
() => {
|
|
244
238
|
const previousPage = currentPage.value
|
|
245
239
|
currentPage.value = 1
|
|
@@ -7,21 +7,17 @@ import {
|
|
|
7
7
|
type AnnotationSeriesInfo,
|
|
8
8
|
type AnnotationTrackLayout,
|
|
9
9
|
} from '../annotation'
|
|
10
|
-
import { channelColors
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
getPageCount,
|
|
14
|
-
normalizeGridOptions,
|
|
15
|
-
paginateSeries,
|
|
16
|
-
resolveGridCellGeometry,
|
|
17
|
-
} from './grid'
|
|
18
|
-
import { buildTrackLayouts, buildYAxisSlots, resolveYAxisTickCount } from './layout'
|
|
10
|
+
import { channelColors } from './constants'
|
|
11
|
+
import { getPageCount, normalizeGridOptions, paginateSeries, resolveGridCellGeometry } from './grid'
|
|
12
|
+
import { buildTrackLayouts, resolveYAxisTickCount } from './layout'
|
|
19
13
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
20
14
|
import { createFrameNumberResolver, resolvePageableTracks } from './trackPagination'
|
|
21
15
|
import type { PreparedWaveformSeries } from './useWaveformData'
|
|
22
16
|
import type { ResolvedWaveformChartProps } from './waveformChartTypes'
|
|
23
17
|
import { applyXDomainStrategy } from './xDomain'
|
|
24
|
-
import {
|
|
18
|
+
import { alignIntegerZoomDomain, normalizeIntegerZoomDomain } from '../interaction/integerZoom'
|
|
19
|
+
import { resolveViewportYDomains } from './yAxisLayoutMetrics'
|
|
20
|
+
import { useWaveformYAxisLayout } from './useWaveformYAxisLayout'
|
|
25
21
|
import type { useWaveformAnnotationInteraction } from '../annotation'
|
|
26
22
|
interface LayoutContext {
|
|
27
23
|
props: ResolvedWaveformChartProps
|
|
@@ -94,6 +90,11 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
94
90
|
}
|
|
95
91
|
})
|
|
96
92
|
})
|
|
93
|
+
const showLegendUnits = computed(
|
|
94
|
+
() =>
|
|
95
|
+
props.unitDisplayMode === 'legend-single-series' &&
|
|
96
|
+
!chartTracks.value.some((track) => track.series.length > 1),
|
|
97
|
+
)
|
|
97
98
|
const renderingOptions = computed(() => resolveWaveformRenderingOptions(props.rendering))
|
|
98
99
|
const pageableTracks = computed(() =>
|
|
99
100
|
resolvePageableTracks(chartTracks.value, gridOptions.value.hideEmptyTracks),
|
|
@@ -119,86 +120,21 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
119
120
|
independentYDomains.value,
|
|
120
121
|
),
|
|
121
122
|
)
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
track.visibleSeries.length === 1 &&
|
|
138
|
-
Boolean(track.visibleSeries[0]?.name.trim() || props.yLabel),
|
|
139
|
-
),
|
|
140
|
-
)
|
|
141
|
-
const hasVisibleWaveformData = computed(() =>
|
|
142
|
-
chartTracks.value.some((track) => track.visibleSeries.length > 0),
|
|
143
|
-
)
|
|
144
|
-
const chartLeftMargin = computed(() =>
|
|
145
|
-
Math.max(
|
|
146
|
-
margin.left,
|
|
147
|
-
hasYAxisLabels.value
|
|
148
|
-
? yAxisMetrics.value.fullClearance
|
|
149
|
-
: hasVisibleWaveformData.value
|
|
150
|
-
? yAxisMetrics.value.tickClearance
|
|
151
|
-
: 0,
|
|
152
|
-
),
|
|
153
|
-
)
|
|
154
|
-
const yAxisSlots = computed(() =>
|
|
155
|
-
buildYAxisSlots(
|
|
156
|
-
layoutTracks.value.filter((track) => track.visibleSeries.length > 0),
|
|
157
|
-
props.overlayMode,
|
|
158
|
-
props.yDomain,
|
|
159
|
-
props.yDomains,
|
|
160
|
-
yAxisTickCount.value,
|
|
161
|
-
props.axes?.y?.nice !== false,
|
|
162
|
-
props.displayMode === 'compact',
|
|
163
|
-
viewportYDomains.value,
|
|
164
|
-
),
|
|
165
|
-
)
|
|
166
|
-
const resolvedChartLeftMargin = computed(() =>
|
|
167
|
-
props.overlayMode === 'multi-axis'
|
|
168
|
-
? Math.max(chartLeftMargin.value, yAxisSlots.value.clearance.left)
|
|
169
|
-
: chartLeftMargin.value,
|
|
170
|
-
)
|
|
171
|
-
const chartRightMargin = computed(() =>
|
|
172
|
-
props.overlayMode === 'multi-axis'
|
|
173
|
-
? Math.max(margin.right, yAxisSlots.value.clearance.right)
|
|
174
|
-
: margin.right,
|
|
175
|
-
)
|
|
176
|
-
const innerWidth = computed(() =>
|
|
177
|
-
Math.max(0, chartWidth.value - resolvedChartLeftMargin.value - chartRightMargin.value),
|
|
178
|
-
)
|
|
179
|
-
const yAxisLayout = computed(() => {
|
|
180
|
-
const baseGap = getGridGap(props.displayMode)
|
|
181
|
-
const columnCount = gridOptions.value.columnCount
|
|
182
|
-
const hasMultipleColumns = columnCount > 1
|
|
183
|
-
const fullGap = Math.max(baseGap, yAxisMetrics.value.fullClearance)
|
|
184
|
-
const tickGap = Math.max(baseGap, yAxisMetrics.value.tickClearance)
|
|
185
|
-
const plotWidth = (innerWidth.value - fullGap * Math.max(0, columnCount - 1)) / columnCount
|
|
186
|
-
const canReserveLabelClearance = plotWidth >= MINIMUM_PLOT_WIDTH
|
|
187
|
-
return {
|
|
188
|
-
horizontalGap:
|
|
189
|
-
props.overlayMode === 'multi-axis' && hasMultipleColumns && hasVisibleWaveformData.value
|
|
190
|
-
? Math.max(baseGap, yAxisSlots.value.clearance.left + yAxisSlots.value.clearance.right)
|
|
191
|
-
: hasMultipleColumns && hasVisibleWaveformData.value
|
|
192
|
-
? hasYAxisLabels.value && canReserveLabelClearance
|
|
193
|
-
? fullGap
|
|
194
|
-
: tickGap
|
|
195
|
-
: baseGap,
|
|
196
|
-
hideSecondaryLabels:
|
|
197
|
-
props.overlayMode !== 'multi-axis' &&
|
|
198
|
-
hasMultipleColumns &&
|
|
199
|
-
hasYAxisLabels.value &&
|
|
200
|
-
!canReserveLabelClearance,
|
|
201
|
-
}
|
|
123
|
+
const {
|
|
124
|
+
yAxisMetrics,
|
|
125
|
+
yAxisSlots,
|
|
126
|
+
resolvedChartLeftMargin,
|
|
127
|
+
innerWidth,
|
|
128
|
+
yAxisLayout,
|
|
129
|
+
hasVisibleWaveformData,
|
|
130
|
+
} = useWaveformYAxisLayout({
|
|
131
|
+
props,
|
|
132
|
+
chartTracks,
|
|
133
|
+
layoutTracks,
|
|
134
|
+
chartWidth,
|
|
135
|
+
gridOptions,
|
|
136
|
+
viewportYDomains,
|
|
137
|
+
yAxisTickCount,
|
|
202
138
|
})
|
|
203
139
|
const hasWaveformData = computed(() => chartSeries.value.length > 0)
|
|
204
140
|
const hasChartArea = computed(() => innerWidth.value > 0 && innerHeight.value > 0)
|
|
@@ -220,13 +156,19 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
220
156
|
Number.isFinite(domain[1]) &&
|
|
221
157
|
domain[0] !== domain[1]
|
|
222
158
|
) {
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
159
|
+
return alignIntegerZoomDomain(
|
|
160
|
+
applyXDomainStrategy(
|
|
161
|
+
domain[0] < domain[1] ? domain : [domain[1], domain[0]],
|
|
162
|
+
props.xDomainStrategy,
|
|
163
|
+
true,
|
|
164
|
+
),
|
|
165
|
+
props,
|
|
227
166
|
)
|
|
228
167
|
}
|
|
229
|
-
return
|
|
168
|
+
return alignIntegerZoomDomain(
|
|
169
|
+
applyXDomainStrategy(sharedXDomain.value, props.xDomainStrategy),
|
|
170
|
+
props,
|
|
171
|
+
)
|
|
230
172
|
})
|
|
231
173
|
const resolveInitialTrackDomain = (track: TrackLayout): [number, number] => {
|
|
232
174
|
const configuredDomain =
|
|
@@ -239,24 +181,32 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
239
181
|
Number.isFinite(configuredDomain[1]) &&
|
|
240
182
|
configuredDomain[0] !== configuredDomain[1]
|
|
241
183
|
) {
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
184
|
+
return alignIntegerZoomDomain(
|
|
185
|
+
applyXDomainStrategy(
|
|
186
|
+
configuredDomain[0] < configuredDomain[1]
|
|
187
|
+
? configuredDomain
|
|
188
|
+
: [configuredDomain[1], configuredDomain[0]],
|
|
189
|
+
props.xDomainStrategy,
|
|
190
|
+
true,
|
|
191
|
+
),
|
|
192
|
+
props,
|
|
248
193
|
)
|
|
249
194
|
}
|
|
250
|
-
return
|
|
251
|
-
|
|
252
|
-
|
|
195
|
+
return alignIntegerZoomDomain(
|
|
196
|
+
applyXDomainStrategy(
|
|
197
|
+
paddedDomain(track.seriesList.flatMap((series) => series.xDomain)),
|
|
198
|
+
props.xDomainStrategy,
|
|
199
|
+
),
|
|
200
|
+
props,
|
|
253
201
|
)
|
|
254
202
|
}
|
|
255
|
-
const sharedZoomDomain = computed(
|
|
256
|
-
(
|
|
203
|
+
const sharedZoomDomain = computed(() =>
|
|
204
|
+
normalizeIntegerZoomDomain(
|
|
257
205
|
sharedTransform.value
|
|
258
206
|
.rescaleX(scaleLinear(initialXDomain.value, [0, innerWidth.value]))
|
|
259
207
|
.domain() as [number, number],
|
|
208
|
+
props,
|
|
209
|
+
),
|
|
260
210
|
)
|
|
261
211
|
const gridCells = computed(() => {
|
|
262
212
|
const cells = resolveGridCellGeometry(
|
|
@@ -264,8 +214,12 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
264
214
|
innerHeight.value,
|
|
265
215
|
gridOptions.value,
|
|
266
216
|
props.displayMode,
|
|
267
|
-
layoutTracks.value.map(
|
|
217
|
+
layoutTracks.value.map((track) =>
|
|
218
|
+
props.layoutPreset === 'edge-compact' ? track.series.length > 0 : Boolean(track),
|
|
219
|
+
),
|
|
268
220
|
yAxisLayout.value.horizontalGap,
|
|
221
|
+
true,
|
|
222
|
+
props.layoutPreset === 'edge-compact' && !isCleanView.value,
|
|
269
223
|
)
|
|
270
224
|
return cells.map((cell, index) => ({ ...cell, series: layoutTracks.value[index] }))
|
|
271
225
|
})
|
|
@@ -273,6 +227,9 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
273
227
|
buildTrackLayouts({
|
|
274
228
|
cells: gridCells.value,
|
|
275
229
|
grid: gridOptions.value,
|
|
230
|
+
useNonEmptyBottomTracks: props.layoutPreset === 'edge-compact',
|
|
231
|
+
compactYAxisLayout: props.layoutPreset === 'edge-compact',
|
|
232
|
+
showAxisUnits: props.layoutPreset !== 'edge-compact' || props.unitDisplayMode === 'axis',
|
|
276
233
|
displayMode: props.displayMode,
|
|
277
234
|
overlayMode: props.overlayMode,
|
|
278
235
|
independentTransforms: independentTransforms.value,
|
|
@@ -280,6 +237,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
280
237
|
initialXDomain: props.initialXDomain ? initialXDomain.value : undefined,
|
|
281
238
|
initialXDomains: props.initialXDomains,
|
|
282
239
|
xDomainStrategy: props.xDomainStrategy,
|
|
240
|
+
integerZoom: props.integerZoom,
|
|
283
241
|
fixedYDomain: props.yDomain,
|
|
284
242
|
fixedYDomains: props.yDomains,
|
|
285
243
|
yDomains: viewportYDomains.value,
|
|
@@ -287,6 +245,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
287
245
|
xAxisLabelFormatter: props.axes?.x?.labelFormatter,
|
|
288
246
|
yAxisSplitNumber: props.axes?.y?.splitNumber,
|
|
289
247
|
yAxisNice: props.axes?.y?.nice,
|
|
248
|
+
yAxisUpperPaddingRatio: props.axes?.y?.upperPaddingRatio,
|
|
290
249
|
rendering: renderingOptions.value,
|
|
291
250
|
linePointOverrides: linePointOverrides?.value,
|
|
292
251
|
hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
|
|
@@ -342,6 +301,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
342
301
|
return {
|
|
343
302
|
chartSeries,
|
|
344
303
|
chartTracks,
|
|
304
|
+
showLegendUnits,
|
|
345
305
|
pageableTracks,
|
|
346
306
|
renderingOptions,
|
|
347
307
|
gridOptions,
|
|
@@ -53,6 +53,8 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
53
53
|
const isPresentationMode = computed(() => props.presentationMode === true)
|
|
54
54
|
const resolvedZeroLine = computed(() => {
|
|
55
55
|
const width = props.zeroLine.width
|
|
56
|
+
const opacity = props.zeroLine.opacity
|
|
57
|
+
const threshold = props.zeroLine.boundaryThreshold
|
|
56
58
|
return {
|
|
57
59
|
visible: props.zeroLine.visible !== false,
|
|
58
60
|
color: props.zeroLine.color || ZERO_LINE_DEFAULTS.COLOR,
|
|
@@ -61,6 +63,14 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
61
63
|
? width
|
|
62
64
|
: ZERO_LINE_DEFAULTS.WIDTH,
|
|
63
65
|
dash: props.zeroLine.dash ?? ZERO_LINE_DEFAULTS.DASH,
|
|
66
|
+
opacity:
|
|
67
|
+
typeof opacity === 'number' && Number.isFinite(opacity) && opacity >= 0 && opacity <= 1
|
|
68
|
+
? opacity
|
|
69
|
+
: ZERO_LINE_DEFAULTS.OPACITY,
|
|
70
|
+
boundaryThreshold:
|
|
71
|
+
typeof threshold === 'number' && Number.isFinite(threshold) && threshold >= 0
|
|
72
|
+
? threshold
|
|
73
|
+
: ZERO_LINE_DEFAULTS.BOUNDARY_THRESHOLD,
|
|
64
74
|
}
|
|
65
75
|
})
|
|
66
76
|
const legendBackgroundColor = computed(
|
|
@@ -83,6 +93,7 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
83
93
|
return position === 'top' || position === 'bottom' ? 'horizontal' : 'vertical'
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
const isEdgeCompact = computed(() => props.layoutPreset === 'edge-compact')
|
|
86
97
|
const resolvedTitleText = computed(() => props.title?.text.trim() ?? '')
|
|
87
98
|
const titleAreaReserved = computed(
|
|
88
99
|
() =>
|
|
@@ -136,10 +147,17 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
136
147
|
}))
|
|
137
148
|
const titleLayout = computed(() =>
|
|
138
149
|
calculateRotatedTitleLayout({
|
|
139
|
-
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,
|
|
140
157
|
naturalHeight: measuredTitleHeight.value || titleFontSize.value * TITLE_LINE_HEIGHT,
|
|
141
158
|
availableWidth: titleAvailableWidth.value,
|
|
142
159
|
rotation: titleRotation.value,
|
|
160
|
+
edgeCompact: isEdgeCompact.value,
|
|
143
161
|
}),
|
|
144
162
|
)
|
|
145
163
|
const titleAreaHeight = computed(() =>
|
|
@@ -149,13 +167,18 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
149
167
|
typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback
|
|
150
168
|
const resolvedPlotMargin = computed(() => ({
|
|
151
169
|
top: resolvePlotMargin(props.plotMargin.top, margin.top),
|
|
152
|
-
bottom:
|
|
170
|
+
bottom: Math.max(
|
|
171
|
+
isEdgeCompact.value ? 44 : 0,
|
|
172
|
+
resolvePlotMargin(props.plotMargin.bottom, margin.bottom),
|
|
173
|
+
),
|
|
153
174
|
}))
|
|
154
175
|
const chartTopMargin = computed(() => resolvedPlotMargin.value.top)
|
|
155
176
|
const drawingHeight = computed(() =>
|
|
156
177
|
Math.max(0, chartHeight.value - titleAreaHeight.value - paginationBandHeight.value),
|
|
157
178
|
)
|
|
158
|
-
const xAxisTitleY = computed(() =>
|
|
179
|
+
const xAxisTitleY = computed(() =>
|
|
180
|
+
Math.max(0, drawingHeight.value - X_AXIS_TITLE_BOTTOM_OFFSET + (isEdgeCompact.value ? 3 : 0)),
|
|
181
|
+
)
|
|
159
182
|
const innerHeight = computed(() =>
|
|
160
183
|
Math.max(
|
|
161
184
|
0,
|
|
@@ -192,6 +215,7 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
192
215
|
chartHeight,
|
|
193
216
|
containerStyle,
|
|
194
217
|
isCleanView,
|
|
218
|
+
isEdgeCompact,
|
|
195
219
|
isPresentationMode,
|
|
196
220
|
resolvedZeroLine,
|
|
197
221
|
legendBackgroundColor,
|