waveform-analysis 0.1.75 → 0.1.77
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 +183 -42
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/style.css +1 -1
- package/dist/types/components/animation/context.d.ts +1 -0
- package/dist/types/components/animation/paginationPresentation.d.ts +7 -7
- package/dist/types/components/animation/useWaveformAnimations.d.ts +1 -18
- package/dist/types/components/core/amplitudeLimits.d.ts +6 -0
- package/dist/types/components/core/layout.d.ts +6 -9
- package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
- package/dist/types/components/core/useWaveformChartController.d.ts +2 -19
- package/dist/types/components/core/useWaveformData.d.ts +2 -2
- package/dist/types/components/core/useWaveformMeasurements.d.ts +4 -1
- package/dist/types/components/core/waveformChartTypes.d.ts +4 -2
- package/dist/types/components/core/xAxisTicks.d.ts +2 -0
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
- package/dist/types/components/core/yAxisTicks.d.ts +6 -0
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/interaction/integerZoom.d.ts +3 -3
- package/dist/types/components/interaction/stepZoom.d.ts +5 -0
- package/dist/types/components/interaction/useWaveformViewport.d.ts +0 -3
- package/dist/types/components/interaction/useWaveformZoom.d.ts +0 -2
- package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +0 -1
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +0 -1
- package/dist/types/core/data.d.ts +1 -1
- package/dist/types/demo/DemoChartHost.vue.d.ts +1 -1
- package/dist/types/demo/remoteWaveformLoader.d.ts +23 -0
- package/dist/types/demo/types.d.ts +11 -2
- package/dist/types/demo/useRemoteWaveformWindow.d.ts +22 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/chart.d.ts +8 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/formatters.d.ts +4 -4
- package/dist/types/utils/xAxisNumber.d.ts +2 -0
- package/package.json +1 -1
- package/src/App.vue +34 -100
- package/src/components/WaveformChartView.vue +0 -4
- package/src/components/animation/config.ts +1 -1
- package/src/components/animation/context.ts +9 -1
- package/src/components/animation/paginationPresentation.ts +55 -64
- package/src/components/animation/useWaveformAnimations.ts +36 -13
- package/src/components/controls/useChartCommands.ts +12 -13
- package/src/components/core/amplitudeLimits.ts +44 -0
- package/src/components/core/layout.ts +22 -34
- package/src/components/core/trackLayoutBuilder.ts +12 -5
- package/src/components/core/useWaveformChartController.ts +3 -4
- package/src/components/core/useWaveformChartLifecycle.ts +11 -2
- package/src/components/core/useWaveformData.ts +11 -5
- package/src/components/core/useWaveformLayout.ts +9 -4
- package/src/components/core/useWaveformMeasurements.ts +3 -0
- package/src/components/core/useWaveformYAxisLayout.ts +2 -0
- package/src/components/core/waveformChartTypes.ts +4 -1
- package/src/components/core/xAxisTicks.ts +69 -0
- package/src/components/core/yAxisLayoutMetrics.ts +4 -0
- package/src/components/core/yAxisTicks.ts +39 -0
- package/src/components/data/types.ts +2 -0
- package/src/components/interaction/integerZoom.ts +31 -12
- package/src/components/interaction/remoteViewport.ts +26 -4
- package/src/components/interaction/stepZoom.ts +33 -0
- package/src/components/interaction/useWaveformViewport.ts +3 -26
- package/src/components/interaction/useWaveformZoom.ts +50 -157
- package/src/components/interaction/zoomConstraints.ts +5 -5
- package/src/components/rendering/WaveformSeriesLayer.vue +1 -2
- package/src/components/rendering/WaveformTrack.vue +1 -6
- package/src/core/data.ts +5 -2
- package/src/data/simulatedWaveforms.ts +0 -10
- package/src/demo/DemoChartHost.vue +49 -4
- package/src/demo/remoteWaveformLoader.ts +114 -0
- package/src/demo/types.ts +11 -1
- package/src/demo/useRemoteWaveformWindow.ts +229 -0
- package/src/index.ts +2 -0
- package/src/types/chart.ts +10 -1
- package/src/types/index.ts +2 -0
- package/src/utils/formatters.ts +10 -5
- package/src/utils/xAxisNumber.ts +36 -0
- package/src/views/AnimationDemo.vue +14 -5
- package/dist/types/data/sx2Channel.d.ts +0 -10
- package/src/data/sx2Channel.ts +0 -315
|
@@ -13,7 +13,12 @@ import { chartExportOptions } from '../controls/chartExportOptions'
|
|
|
13
13
|
import { layoutAnnotations } from '../annotation/annotationLayout'
|
|
14
14
|
import { useWaveformInstanceId } from '@/utils/waveformId'
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
animationGridShape,
|
|
18
|
+
animationLayoutSignature,
|
|
19
|
+
wrapAnimationCommands,
|
|
20
|
+
type AnimationContext,
|
|
21
|
+
} from './context'
|
|
17
22
|
import { createPaginationPresentation } from './paginationPresentation'
|
|
18
23
|
|
|
19
24
|
export function useWaveformAnimations(context: AnimationContext) {
|
|
@@ -26,6 +31,8 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
26
31
|
const seen = new Set<string>()
|
|
27
32
|
const prefix = useWaveformInstanceId('waveform-animation')
|
|
28
33
|
let clipSequence = 0
|
|
34
|
+
let presentationGeneration = 0
|
|
35
|
+
let previousGrid = animationGridShape(context)
|
|
29
36
|
let previousAnnotations = props.annotations
|
|
30
37
|
let previousData = props.data
|
|
31
38
|
let previousLayout = ''
|
|
@@ -63,9 +70,12 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
63
70
|
states.has(track.id) ? projectTrack(states.get(track.id)!) : track,
|
|
64
71
|
)
|
|
65
72
|
})
|
|
66
|
-
const pagination = createPaginationPresentation(context, controller, config, effective, () =>
|
|
67
|
-
commit(true)
|
|
68
|
-
|
|
73
|
+
const pagination = createPaginationPresentation(context, controller, config, effective, () => {
|
|
74
|
+
commit(true)
|
|
75
|
+
for (const track of tracks.value) for (const series of track.seriesList) seen.delete(series.id)
|
|
76
|
+
skipSignature = undefined
|
|
77
|
+
annotations.clear()
|
|
78
|
+
})
|
|
69
79
|
const projectedAnnotations = computed(() => {
|
|
70
80
|
const anchors = new Map([...states.values()].flatMap((state) => [...state.anchors]))
|
|
71
81
|
const values = visibleAnnotations().flatMap((annotation) => {
|
|
@@ -100,12 +110,10 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
100
110
|
[...state.series].map(([id, series]) => [id, series.opacity] as const),
|
|
101
111
|
),
|
|
102
112
|
)
|
|
103
|
-
return annotations.project(projectedAnnotations.value, opacities)
|
|
104
|
-
...annotation,
|
|
105
|
-
seriesOpacity: (annotation.seriesOpacity ?? 1) * pagination.opacity.value,
|
|
106
|
-
}))
|
|
113
|
+
return annotations.project(projectedAnnotations.value, opacities)
|
|
107
114
|
})
|
|
108
115
|
function commit(preservePagination = false) {
|
|
116
|
+
presentationGeneration++
|
|
109
117
|
if (!preservePagination) pagination.reset()
|
|
110
118
|
const previous = new Map(states)
|
|
111
119
|
states.clear()
|
|
@@ -130,6 +138,8 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
130
138
|
if (disposed) return
|
|
131
139
|
const nextConfig = config.value
|
|
132
140
|
const dataChanged = props.data !== previousData
|
|
141
|
+
// The post-flush grid watcher waits for lifecycle page resets before requesting replay.
|
|
142
|
+
if (!dataChanged && seen.size && animationGridShape(context) !== previousGrid) return
|
|
133
143
|
if (dataChanged) {
|
|
134
144
|
pendingFrom = new Map(states)
|
|
135
145
|
previousData = props.data
|
|
@@ -162,8 +172,9 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
162
172
|
previousTracks = tracks.value
|
|
163
173
|
previousSamplingState = sampling.state.value
|
|
164
174
|
controller.setEnabled(effective.value)
|
|
165
|
-
|
|
166
|
-
if (
|
|
175
|
+
const replay = pagination.reconcile(dataChanged)
|
|
176
|
+
if (replay === 'static') return
|
|
177
|
+
if (!effective.value || (layoutChanged && !replay)) {
|
|
167
178
|
controller.finishAll()
|
|
168
179
|
commit()
|
|
169
180
|
return
|
|
@@ -224,6 +235,7 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
224
235
|
if (
|
|
225
236
|
!seen.has(id) &&
|
|
226
237
|
nextConfig.animation.enabled &&
|
|
238
|
+
nextConfig.animation.duration > 0 &&
|
|
227
239
|
skipSignature !== sampling.requestSignature.value
|
|
228
240
|
) {
|
|
229
241
|
const state = states.get(track.id)!
|
|
@@ -255,8 +267,10 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
255
267
|
invalidate()
|
|
256
268
|
},
|
|
257
269
|
}
|
|
270
|
+
const generation = presentationGeneration
|
|
258
271
|
controller.animateSeriesEnter(id, binding, track.width, {
|
|
259
272
|
...nextConfig.animation,
|
|
273
|
+
duration: pagination.enterDuration(),
|
|
260
274
|
delay:
|
|
261
275
|
nextConfig.animation.delay + visibleIds.indexOf(id) * nextConfig.animation.staggerDelay,
|
|
262
276
|
onComplete: () => {
|
|
@@ -264,7 +278,7 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
264
278
|
entering.delete(id)
|
|
265
279
|
invalidate()
|
|
266
280
|
queueMicrotask(() => {
|
|
267
|
-
if (!disposed) annotations.release(id)
|
|
281
|
+
if (!disposed && presentationGeneration === generation) annotations.release(id)
|
|
268
282
|
})
|
|
269
283
|
nextConfig.animation.onComplete?.()
|
|
270
284
|
},
|
|
@@ -311,6 +325,17 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
311
325
|
reconcile,
|
|
312
326
|
{ immediate: true },
|
|
313
327
|
)
|
|
328
|
+
watch(
|
|
329
|
+
[() => animationGridShape(context), () => props.data],
|
|
330
|
+
([shape, data], [previousShape, previousData]) => {
|
|
331
|
+
previousGrid = shape
|
|
332
|
+
if (disposed || shape === previousShape || data !== previousData || !seen.size) return
|
|
333
|
+
// Lifecycle page resets have now selected the final grid and page.
|
|
334
|
+
pagination.requestLayout()
|
|
335
|
+
reconcile()
|
|
336
|
+
},
|
|
337
|
+
{ flush: 'post' },
|
|
338
|
+
)
|
|
314
339
|
watch(
|
|
315
340
|
() => props.annotationsVisible,
|
|
316
341
|
(visible) => {
|
|
@@ -343,7 +368,6 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
343
368
|
controller.finishWhere(
|
|
344
369
|
(key) => key.scope === 'update' || key.scope === 'domain',
|
|
345
370
|
() => {
|
|
346
|
-
pagination.reset()
|
|
347
371
|
for (const track of tracks.value) {
|
|
348
372
|
const old = states.get(track.id),
|
|
349
373
|
next = staticTrack(track, visibleAnnotations(), old, true)
|
|
@@ -405,7 +429,6 @@ export function useWaveformAnimations(context: AnimationContext) {
|
|
|
405
429
|
return {
|
|
406
430
|
presentationTracks,
|
|
407
431
|
presentationAnnotations,
|
|
408
|
-
paginationOpacity: pagination.opacity,
|
|
409
432
|
stabilizeAnimations,
|
|
410
433
|
wrap: <T extends object>(commands: T) => wrapAnimationCommands(commands, stabilizeAnimations),
|
|
411
434
|
wrapPagination: pagination.wrapPagination,
|
|
@@ -16,6 +16,7 @@ import { paddedDomain } from '../../utils'
|
|
|
16
16
|
import { transformForDomain } from '../interaction/zoomConstraints'
|
|
17
17
|
import { constrainViewportZoom } from '../interaction/remoteViewport'
|
|
18
18
|
import { normalizeIntegerZoomDomain } from '../interaction/integerZoom'
|
|
19
|
+
import { resolveStepZoomDomain, sameZoomDomain } from '../interaction/stepZoom'
|
|
19
20
|
import type { useControlMode } from './useControlMode'
|
|
20
21
|
|
|
21
22
|
type Domain = [number, number]
|
|
@@ -34,7 +35,7 @@ interface Context {
|
|
|
34
35
|
legacyReset: (index?: number) => void
|
|
35
36
|
legacySet: (domain: Domain, index?: number) => void
|
|
36
37
|
}
|
|
37
|
-
const same =
|
|
38
|
+
const same = sameZoomDomain
|
|
38
39
|
export function useChartCommands(c: Context) {
|
|
39
40
|
let sequence = 0
|
|
40
41
|
const independent = computed(() => c.props.displayMode === 'independent')
|
|
@@ -104,18 +105,16 @@ export function useChartCommands(c: Context) {
|
|
|
104
105
|
paddedDomain(tracks.flatMap((t) => t.seriesList.flatMap((s) => s.xDomain))),
|
|
105
106
|
c.props.xDomainStrategy,
|
|
106
107
|
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
c.props,
|
|
118
|
-
)
|
|
108
|
+
const seriesGroups = tracks.filter((t) => t.hasVisibleSeries).map((t) => t.seriesList)
|
|
109
|
+
const xDomain =
|
|
110
|
+
before && (action === 'zoom-in' || action === 'zoom-out')
|
|
111
|
+
? resolveStepZoomDomain(before.xDomain, action, bounds, seriesGroups, c.props)
|
|
112
|
+
: constrainViewportZoom(
|
|
113
|
+
normalizeIntegerZoomDomain(domain ?? bounds, c.props),
|
|
114
|
+
bounds,
|
|
115
|
+
seriesGroups,
|
|
116
|
+
c.props,
|
|
117
|
+
)
|
|
119
118
|
const resetsY = action === 'reset' || action === 'fit'
|
|
120
119
|
const hasY =
|
|
121
120
|
resetsY &&
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { WaveformAmplitudeLimit, WaveformAmplitudeLimits } from '../../types'
|
|
2
|
+
|
|
3
|
+
export function normalizeAmplitudeLimit(
|
|
4
|
+
limit?: WaveformAmplitudeLimit,
|
|
5
|
+
): WaveformAmplitudeLimit | undefined {
|
|
6
|
+
if (!limit) return undefined
|
|
7
|
+
const lower = Number.isFinite(limit.lower) ? limit.lower : undefined
|
|
8
|
+
const upper = Number.isFinite(limit.upper) ? limit.upper : undefined
|
|
9
|
+
if (lower === undefined && upper === undefined) return undefined
|
|
10
|
+
if (lower !== undefined && upper !== undefined && lower >= upper) return undefined
|
|
11
|
+
return { lower, upper }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mergeAmplitudeLimits(
|
|
15
|
+
series: readonly { id: string }[],
|
|
16
|
+
limits?: WaveformAmplitudeLimits,
|
|
17
|
+
): WaveformAmplitudeLimit | undefined {
|
|
18
|
+
const valid = series.flatMap((item) => {
|
|
19
|
+
const limit = normalizeAmplitudeLimit(limits?.[item.id])
|
|
20
|
+
return limit ? [limit] : []
|
|
21
|
+
})
|
|
22
|
+
const lowers = valid.flatMap((limit) => (limit.lower === undefined ? [] : [limit.lower]))
|
|
23
|
+
const uppers = valid.flatMap((limit) => (limit.upper === undefined ? [] : [limit.upper]))
|
|
24
|
+
return normalizeAmplitudeLimit({
|
|
25
|
+
lower: lowers.length ? Math.min(...lowers) : undefined,
|
|
26
|
+
upper: uppers.length ? Math.max(...uppers) : undefined,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function applyAmplitudeLimits(
|
|
31
|
+
padded: [number, number],
|
|
32
|
+
limit: WaveformAmplitudeLimit,
|
|
33
|
+
automatic: [number, number] = padded,
|
|
34
|
+
): [number, number] {
|
|
35
|
+
let lower = limit.lower ?? padded[0]
|
|
36
|
+
let upper = limit.upper ?? padded[1]
|
|
37
|
+
if (lower >= upper) {
|
|
38
|
+
const fixed = limit.lower ?? limit.upper!
|
|
39
|
+
const span = Math.max(automatic[1] - automatic[0], Math.abs(fixed) * 0.1, 1)
|
|
40
|
+
if (limit.lower !== undefined) upper = lower + span
|
|
41
|
+
else lower = upper - span
|
|
42
|
+
}
|
|
43
|
+
return [lower, upper]
|
|
44
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
WaveformAmplitudeLimit,
|
|
3
|
+
WaveformAmplitudeLimits,
|
|
4
|
+
WaveformOverlayMode,
|
|
5
|
+
} from '../../types'
|
|
2
6
|
import { formatScientificAxisLabel, formatScientificAxisExponent, paddedDomain } from '../../utils'
|
|
3
7
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
4
8
|
import { MAX_MULTI_Y_AXIS_COUNT } from './constants'
|
|
@@ -8,7 +12,9 @@ import {
|
|
|
8
12
|
resolveTrackFixedYDomain,
|
|
9
13
|
type WaveformYDomain,
|
|
10
14
|
} from './yDomain'
|
|
11
|
-
import {
|
|
15
|
+
import { resolveYAxisTicks } from './yAxisTicks'
|
|
16
|
+
export { resolveYAxisTicks, type ResolvedYAxisTicks } from './yAxisTicks'
|
|
17
|
+
import { applyAmplitudeLimits, mergeAmplitudeLimits } from './amplitudeLimits'
|
|
12
18
|
import { applyYAxisPadding, withMaximumTick, type YAxisPaddingOptions } from './yAxisPadding'
|
|
13
19
|
import { resolveYAxisLabelBandWidth } from './yAxisLabelStyle'
|
|
14
20
|
|
|
@@ -28,36 +34,6 @@ export function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number)
|
|
|
28
34
|
return 5
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
export interface ResolvedYAxisTicks {
|
|
32
|
-
domain: [number, number]
|
|
33
|
-
values: number[]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function resolveYAxisTicks(
|
|
37
|
-
domain: [number, number],
|
|
38
|
-
tickCount = 5,
|
|
39
|
-
nice = true,
|
|
40
|
-
): ResolvedYAxisTicks {
|
|
41
|
-
if (!nice) {
|
|
42
|
-
// 如果不需要 nice,使用简单的线性分割
|
|
43
|
-
const effectiveTickCount =
|
|
44
|
-
typeof tickCount === 'number' && Number.isFinite(tickCount)
|
|
45
|
-
? Math.max(2, Math.floor(tickCount))
|
|
46
|
-
: 5
|
|
47
|
-
const [axisStart, axisEnd] = domain
|
|
48
|
-
return {
|
|
49
|
-
domain,
|
|
50
|
-
values: Array.from(
|
|
51
|
-
{ length: effectiveTickCount },
|
|
52
|
-
(_, index) => axisStart + ((axisEnd - axisStart) * index) / (effectiveTickCount - 1),
|
|
53
|
-
),
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 使用 ECharts 兼容的 1/2/5 系列算法
|
|
58
|
-
return resolveYAxisTicksECharts(domain, tickCount)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
37
|
export function formatYAxisTickLabel(
|
|
62
38
|
value: number,
|
|
63
39
|
domain: [number, number],
|
|
@@ -101,6 +77,7 @@ export interface YAxisSeriesGroup {
|
|
|
101
77
|
seriesList: DisplaySeries[]
|
|
102
78
|
domain: [number, number]
|
|
103
79
|
fixed: boolean
|
|
80
|
+
amplitudeLimit?: WaveformAmplitudeLimit
|
|
104
81
|
maximumTick?: number
|
|
105
82
|
minimumTick?: number
|
|
106
83
|
}
|
|
@@ -191,6 +168,7 @@ export function resolveRenderedYAxisSeriesGroups(
|
|
|
191
168
|
yDomains?: Record<string, WaveformYDomain>,
|
|
192
169
|
viewportYDomains?: Record<string, [number, number]>,
|
|
193
170
|
padding?: YAxisPaddingOptions | number,
|
|
171
|
+
amplitudeLimits?: WaveformAmplitudeLimits,
|
|
194
172
|
): YAxisSeriesGroup[] {
|
|
195
173
|
const viewportYDomain = viewportYDomains?.[track.id]
|
|
196
174
|
const axisTrack =
|
|
@@ -198,7 +176,13 @@ export function resolveRenderedYAxisSeriesGroups(
|
|
|
198
176
|
? track
|
|
199
177
|
: { ...track, visibleSeries: track.series }
|
|
200
178
|
return resolveYAxisSeriesGroups(axisTrack, overlayMode, yDomain, yDomains).map((group) => {
|
|
201
|
-
const
|
|
179
|
+
const limit = mergeAmplitudeLimits(group.seriesList, amplitudeLimits)
|
|
180
|
+
const automatic = limit ? buildYAxisSeriesGroups(axisTrack, overlayMode)[group.index]! : group
|
|
181
|
+
const padded = applyYAxisPadding(automatic, limit ? undefined : viewportYDomain, padding)
|
|
182
|
+
if (limit) {
|
|
183
|
+
padded.domain = applyAmplitudeLimits(padded.domain, limit, automatic.domain)
|
|
184
|
+
padded.amplitudeLimit = limit
|
|
185
|
+
}
|
|
202
186
|
return track.visibleSeries.length
|
|
203
187
|
? padded
|
|
204
188
|
: { ...padded, maximumTick: undefined, minimumTick: undefined }
|
|
@@ -215,10 +199,11 @@ export function axisTextMetrics(
|
|
|
215
199
|
measureTextWidth?: (text: string) => number,
|
|
216
200
|
maximumTick?: number,
|
|
217
201
|
minimumTick?: number,
|
|
202
|
+
amplitudeLimit?: WaveformAmplitudeLimit,
|
|
218
203
|
): { tickTextWidth: number; titleTextWidth: number } {
|
|
219
204
|
const resolvedTicks = tickValues
|
|
220
205
|
? { domain, values: tickValues }
|
|
221
|
-
: resolveYAxisTicks(domain, tickCount, nice)
|
|
206
|
+
: resolveYAxisTicks(domain, tickCount, nice, amplitudeLimit)
|
|
222
207
|
const inRange = (value?: number) =>
|
|
223
208
|
value !== undefined && value >= resolvedTicks.domain[0] && value <= resolvedTicks.domain[1]
|
|
224
209
|
? value
|
|
@@ -320,6 +305,7 @@ export function buildYAxisSlots(
|
|
|
320
305
|
padding?: YAxisPaddingOptions | number,
|
|
321
306
|
showUnits = true,
|
|
322
307
|
measureTextWidth?: (text: string) => number,
|
|
308
|
+
amplitudeLimits?: WaveformAmplitudeLimits,
|
|
323
309
|
): { slots: YAxisSlot[]; clearance: { left: number; right: number } } {
|
|
324
310
|
const labelBandWidth = resolveYAxisLabelBandWidth(
|
|
325
311
|
typeof padding === 'object' ? padding.labelTextStyle : undefined,
|
|
@@ -335,6 +321,7 @@ export function buildYAxisSlots(
|
|
|
335
321
|
yDomains,
|
|
336
322
|
viewportYDomains,
|
|
337
323
|
padding,
|
|
324
|
+
amplitudeLimits,
|
|
338
325
|
).forEach((group) => {
|
|
339
326
|
const sideIndex = sideIndexes[group.side]++
|
|
340
327
|
const key = `${group.side}:${sideIndex}`
|
|
@@ -348,6 +335,7 @@ export function buildYAxisSlots(
|
|
|
348
335
|
measureTextWidth,
|
|
349
336
|
group.maximumTick,
|
|
350
337
|
group.minimumTick,
|
|
338
|
+
group.amplitudeLimit,
|
|
351
339
|
)
|
|
352
340
|
widths.set(key, Math.max(widths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, tickTextWidth))
|
|
353
341
|
titleWidths.set(key, Math.max(titleWidths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, titleTextWidth))
|
|
@@ -6,6 +6,7 @@ import { scaleLinear, zoomIdentity, type ZoomTransform } from 'd3'
|
|
|
6
6
|
|
|
7
7
|
import { type ResolvedWaveformRenderingOptions } from '../../core/rendering'
|
|
8
8
|
import type {
|
|
9
|
+
WaveformAmplitudeLimits,
|
|
9
10
|
WaveformDisplayMode,
|
|
10
11
|
WaveformOverlayMode,
|
|
11
12
|
WaveformPoint,
|
|
@@ -34,7 +35,6 @@ import {
|
|
|
34
35
|
alignIntegerZoomDomain,
|
|
35
36
|
normalizeIntegerZoomDomain,
|
|
36
37
|
integerZoomTicks,
|
|
37
|
-
formatIntegerZoomLabel,
|
|
38
38
|
} from '../interaction/integerZoom'
|
|
39
39
|
import {
|
|
40
40
|
Y_AXIS_LABEL_GAP,
|
|
@@ -59,6 +59,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
59
59
|
initialXDomains?: Record<string, [number, number]>
|
|
60
60
|
xDomainStrategy?: WaveformXDomainStrategy
|
|
61
61
|
integerZoom?: boolean
|
|
62
|
+
amplitudeLimits?: WaveformAmplitudeLimits
|
|
62
63
|
fixedYDomain?: [number, number]
|
|
63
64
|
fixedYDomains?: Record<string, [number, number]>
|
|
64
65
|
yDomains?: Record<string, [number, number]>
|
|
@@ -79,6 +80,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
79
80
|
useNonEmptyBottomTracks?: boolean
|
|
80
81
|
compactYAxisLayout?: boolean
|
|
81
82
|
showAxisUnits?: boolean
|
|
83
|
+
domainTimeUnit?: 's' | 'ms'
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
function resolveIndependentXDomain(
|
|
@@ -96,7 +98,7 @@ function resolveIndependentXDomain(
|
|
|
96
98
|
explicitDomain
|
|
97
99
|
? applyXDomainStrategy(explicitDomain, strategy, true)
|
|
98
100
|
: applyXDomainStrategy(track.xDomain, strategy),
|
|
99
|
-
options,
|
|
101
|
+
{ integerZoom: options.integerZoom, timeUnit: options.domainTimeUnit ?? options.timeUnit },
|
|
100
102
|
)
|
|
101
103
|
}
|
|
102
104
|
|
|
@@ -141,13 +143,19 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
141
143
|
options.fixedYDomains,
|
|
142
144
|
options.yDomains,
|
|
143
145
|
options.yAxisPadding ?? options.yAxisUpperPaddingRatio,
|
|
146
|
+
options.amplitudeLimits,
|
|
144
147
|
)
|
|
145
148
|
const sideIndexes = { left: 0, right: 0 }
|
|
146
149
|
const sideOffsets = { left: 0, right: 0 }
|
|
147
150
|
const yAxes: WaveformYAxisLayout[] = yAxisGroups.map((group) => {
|
|
148
151
|
const sideIndex = sideIndexes[group.side]++
|
|
149
152
|
const tickCount = resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber)
|
|
150
|
-
const resolvedTicks = resolveYAxisTicks(
|
|
153
|
+
const resolvedTicks = resolveYAxisTicks(
|
|
154
|
+
group.domain,
|
|
155
|
+
tickCount,
|
|
156
|
+
options.yAxisNice !== false,
|
|
157
|
+
group.amplitudeLimit,
|
|
158
|
+
)
|
|
151
159
|
const scale = scaleLinear(resolvedTicks.domain, [cell.plotHeight, 0])
|
|
152
160
|
const majorTicks = resolvedTicks.values
|
|
153
161
|
const maximumTick =
|
|
@@ -220,8 +228,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
220
228
|
Math.max(2, Math.floor(cell.width / 100)),
|
|
221
229
|
options,
|
|
222
230
|
)
|
|
223
|
-
const xAxisLabelFormatter =
|
|
224
|
-
options.xAxisLabelFormatter ?? (options.integerZoom ? formatIntegerZoomLabel : undefined)
|
|
231
|
+
const xAxisLabelFormatter = options.xAxisLabelFormatter
|
|
225
232
|
const yMajorTicks =
|
|
226
233
|
yAxes[0]?.majorTicks ??
|
|
227
234
|
(isEmpty
|
|
@@ -32,6 +32,7 @@ import { resolveCompactPaginationBand } from './pagination'
|
|
|
32
32
|
import type { WaveformHoverState } from './types'
|
|
33
33
|
import { useWaveformChartLifecycle } from './useWaveformChartLifecycle'
|
|
34
34
|
import { usePreparedWaveformSeries } from './useWaveformData'
|
|
35
|
+
import { remotePanBoundary } from '../interaction/remoteViewport'
|
|
35
36
|
import { useWaveformLayout } from './useWaveformLayout'
|
|
36
37
|
import { useWaveformPresentation } from './useWaveformPresentation'
|
|
37
38
|
import { usePlotLabelLayout } from './usePlotLabelLayout'
|
|
@@ -82,7 +83,7 @@ export function useWaveformChartController(
|
|
|
82
83
|
const hoverThrottle = useAnimationFrameThrottle()
|
|
83
84
|
|
|
84
85
|
watch(
|
|
85
|
-
[() => props.yDomain, () => props.yDomains],
|
|
86
|
+
[() => props.yDomain, () => props.yDomains, () => props.amplitudeLimits],
|
|
86
87
|
() => {
|
|
87
88
|
sharedYDomains.value = {}
|
|
88
89
|
independentYDomains.value = {}
|
|
@@ -102,6 +103,7 @@ export function useWaveformChartController(
|
|
|
102
103
|
handleBeforeDataReferenceChange()
|
|
103
104
|
},
|
|
104
105
|
() => handleDataReferenceChange(),
|
|
106
|
+
() => Boolean(remotePanBoundary(props)),
|
|
105
107
|
)
|
|
106
108
|
|
|
107
109
|
const presentation = useWaveformPresentation({
|
|
@@ -263,8 +265,6 @@ export function useWaveformChartController(
|
|
|
263
265
|
isPresentationMode,
|
|
264
266
|
editorSeriesOptions,
|
|
265
267
|
resolveInitialTrackDomain,
|
|
266
|
-
canZoomTrack: zoom.canZoomTrack,
|
|
267
|
-
canZoomSharedTracks: zoom.canZoomSharedTracks,
|
|
268
268
|
configureZoom: zoom.configureZoom,
|
|
269
269
|
cancelPendingZoom: zoom.cancelPendingZoom,
|
|
270
270
|
clearHover: () => hover.clearHover(),
|
|
@@ -422,7 +422,6 @@ export function useWaveformChartController(
|
|
|
422
422
|
...animations.wrap(commands),
|
|
423
423
|
presentationTracks: animations.presentationTracks,
|
|
424
424
|
presentationAnnotations: animations.presentationAnnotations,
|
|
425
|
-
paginationOpacity: animations.paginationOpacity,
|
|
426
425
|
stabilizeAnimations: animations.stabilizeAnimations,
|
|
427
426
|
...imageExport,
|
|
428
427
|
margin,
|
|
@@ -16,6 +16,7 @@ import type { NormalizedWaveformGridOptions } from './grid'
|
|
|
16
16
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
17
17
|
import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes'
|
|
18
18
|
import { isEditableTarget } from '../interaction/interactionTarget'
|
|
19
|
+
import { remotePanBoundary } from '../interaction/remoteViewport'
|
|
19
20
|
|
|
20
21
|
export interface LifecycleContext {
|
|
21
22
|
preserveFullViewport?: () => boolean
|
|
@@ -64,7 +65,6 @@ export interface LifecycleContext {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
67
|
-
useWaveformMeasurements(context)
|
|
68
68
|
const {
|
|
69
69
|
props,
|
|
70
70
|
emit,
|
|
@@ -135,6 +135,15 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
135
135
|
const { handleBeforeDataReferenceChange, handleDataReferenceChange } =
|
|
136
136
|
useWaveformViewportRestore(context)
|
|
137
137
|
|
|
138
|
+
useWaveformMeasurements(context, {
|
|
139
|
+
beforeResize: () => {
|
|
140
|
+
if (remotePanBoundary(props)) handleBeforeDataReferenceChange()
|
|
141
|
+
},
|
|
142
|
+
afterResize: () => {
|
|
143
|
+
if (remotePanBoundary(props)) handleDataReferenceChange()
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
|
|
138
147
|
watch(
|
|
139
148
|
[
|
|
140
149
|
innerWidth,
|
|
@@ -163,7 +172,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
163
172
|
)
|
|
164
173
|
|
|
165
174
|
watch(
|
|
166
|
-
() => [props.displayMode, props.integerZoom
|
|
175
|
+
() => [props.displayMode, props.integerZoom],
|
|
167
176
|
() => {
|
|
168
177
|
const previousPage = currentPage.value
|
|
169
178
|
currentPage.value = 1
|
|
@@ -31,8 +31,11 @@ export interface PreparedWaveformSeries {
|
|
|
31
31
|
hasErrorPoints: boolean
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function prepareWaveformSeries(
|
|
35
|
-
|
|
34
|
+
export function prepareWaveformSeries(
|
|
35
|
+
data: WaveformData,
|
|
36
|
+
retainEmpty = false,
|
|
37
|
+
): PreparedWaveformSeries[] {
|
|
38
|
+
return normalizeWaveformSeriesSources(data, retainEmpty).map(({ source, ...series }) => {
|
|
36
39
|
const sourceMetrics = source.metrics(series.errorBar.visible)
|
|
37
40
|
return {
|
|
38
41
|
...series,
|
|
@@ -61,11 +64,14 @@ export function usePreparedWaveformSeries(
|
|
|
61
64
|
data: () => WaveformData,
|
|
62
65
|
onBeforeDataChange: () => void,
|
|
63
66
|
onDataChange: () => void,
|
|
67
|
+
retainEmpty: () => boolean = () => false,
|
|
64
68
|
) {
|
|
65
|
-
const preparedSeries = shallowRef<PreparedWaveformSeries[]>(
|
|
66
|
-
|
|
69
|
+
const preparedSeries = shallowRef<PreparedWaveformSeries[]>(
|
|
70
|
+
prepareWaveformSeries(data(), retainEmpty()),
|
|
71
|
+
)
|
|
72
|
+
watch([data, retainEmpty], ([nextData, preserveEmpty]) => {
|
|
67
73
|
onBeforeDataChange()
|
|
68
|
-
preparedSeries.value = prepareWaveformSeries(nextData)
|
|
74
|
+
preparedSeries.value = prepareWaveformSeries(nextData, preserveEmpty)
|
|
69
75
|
onDataChange()
|
|
70
76
|
})
|
|
71
77
|
return preparedSeries
|
|
@@ -52,6 +52,9 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
52
52
|
annotationInteraction,
|
|
53
53
|
linePointOverrides,
|
|
54
54
|
} = context
|
|
55
|
+
// Display-unit switches must not change the base used by existing D3 transforms.
|
|
56
|
+
const domainTimeUnit = props.timeUnit
|
|
57
|
+
const domainOptions = () => ({ integerZoom: props.integerZoom, timeUnit: domainTimeUnit })
|
|
55
58
|
const chartSeries = computed<DisplaySeries[]>(() =>
|
|
56
59
|
preparedSeries.value.map((series, index): DisplaySeries => ({
|
|
57
60
|
...series,
|
|
@@ -174,12 +177,12 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
174
177
|
props.xDomainStrategy,
|
|
175
178
|
true,
|
|
176
179
|
),
|
|
177
|
-
|
|
180
|
+
domainOptions(),
|
|
178
181
|
)
|
|
179
182
|
}
|
|
180
183
|
return alignIntegerZoomDomain(
|
|
181
184
|
applyXDomainStrategy(sharedXDomain.value, props.xDomainStrategy),
|
|
182
|
-
|
|
185
|
+
domainOptions(),
|
|
183
186
|
)
|
|
184
187
|
})
|
|
185
188
|
const resolveOriginalTrackDomain = (track: TrackLayout): [number, number] => {
|
|
@@ -201,7 +204,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
201
204
|
props.xDomainStrategy,
|
|
202
205
|
true,
|
|
203
206
|
),
|
|
204
|
-
|
|
207
|
+
domainOptions(),
|
|
205
208
|
)
|
|
206
209
|
}
|
|
207
210
|
return alignIntegerZoomDomain(
|
|
@@ -209,7 +212,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
209
212
|
paddedDomain(track.seriesList.flatMap((series) => series.xDomain)),
|
|
210
213
|
props.xDomainStrategy,
|
|
211
214
|
),
|
|
212
|
-
|
|
215
|
+
domainOptions(),
|
|
213
216
|
)
|
|
214
217
|
}
|
|
215
218
|
const initialXDomain = computed(
|
|
@@ -261,10 +264,12 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
261
264
|
effectiveXDomains: context.boundaries?.value,
|
|
262
265
|
xDomainStrategy: props.xDomainStrategy,
|
|
263
266
|
integerZoom: props.integerZoom,
|
|
267
|
+
amplitudeLimits: props.amplitudeLimits,
|
|
264
268
|
fixedYDomain: props.yDomain,
|
|
265
269
|
fixedYDomains: props.yDomains,
|
|
266
270
|
yDomains: viewportYDomains.value,
|
|
267
271
|
timeUnit: props.timeUnit,
|
|
272
|
+
domainTimeUnit,
|
|
268
273
|
xAxisLabelFormatter: props.axes?.x?.labelFormatter,
|
|
269
274
|
yAxisSplitNumber: props.axes?.y?.splitNumber,
|
|
270
275
|
yAxisNice: props.axes?.y?.nice,
|
|
@@ -15,6 +15,7 @@ export function useWaveformMeasurements(
|
|
|
15
15
|
| 'titleAreaReserved'
|
|
16
16
|
| 'titleMeasureStyle'
|
|
17
17
|
>,
|
|
18
|
+
resizeHooks: { beforeResize?: () => void; afterResize?: () => void } = {},
|
|
18
19
|
) {
|
|
19
20
|
const {
|
|
20
21
|
container,
|
|
@@ -53,8 +54,10 @@ export function useWaveformMeasurements(
|
|
|
53
54
|
onMounted(() => {
|
|
54
55
|
if (!container.value) return
|
|
55
56
|
resizeObserver.value = new ResizeObserver(([entry]) => {
|
|
57
|
+
resizeHooks.beforeResize?.()
|
|
56
58
|
observedWidth.value = Math.max(0, entry?.contentRect.width ?? 0)
|
|
57
59
|
observedHeight.value = Math.max(0, entry?.contentRect.height ?? 0)
|
|
60
|
+
resizeHooks.afterResize?.()
|
|
58
61
|
void nextTick(measureTitle)
|
|
59
62
|
})
|
|
60
63
|
resizeObserver.value.observe(container.value)
|
|
@@ -49,6 +49,7 @@ export function useWaveformYAxisLayout(context: YAxisLayoutContext) {
|
|
|
49
49
|
props.axes?.y,
|
|
50
50
|
showUnits.value,
|
|
51
51
|
isEdgeCompact.value ? measureYAxisTextWidth : undefined,
|
|
52
|
+
props.amplitudeLimits,
|
|
52
53
|
),
|
|
53
54
|
)
|
|
54
55
|
const hasYAxisLabels = computed(() =>
|
|
@@ -84,6 +85,7 @@ export function useWaveformYAxisLayout(context: YAxisLayoutContext) {
|
|
|
84
85
|
props.axes?.y,
|
|
85
86
|
showUnits.value,
|
|
86
87
|
isEdgeCompact.value ? measureYAxisTextWidth : undefined,
|
|
88
|
+
props.amplitudeLimits,
|
|
87
89
|
),
|
|
88
90
|
)
|
|
89
91
|
const resolvedChartLeftMargin = computed(() =>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WaveformAnimationProps } from '../../animation/types'
|
|
2
2
|
import type { WaveformToolbarOptions, WaveformControlState } from '../../types/controls'
|
|
3
3
|
import type {
|
|
4
|
+
WaveformAmplitudeLimits,
|
|
4
5
|
WaveformAnnotation,
|
|
5
6
|
WaveformAxesOptions,
|
|
6
7
|
WaveformData,
|
|
@@ -43,7 +44,7 @@ export interface WaveformChartProps extends WaveformAnimationProps {
|
|
|
43
44
|
lineColor?: string
|
|
44
45
|
showTooltip?: boolean
|
|
45
46
|
zoomable?: boolean
|
|
46
|
-
/**
|
|
47
|
+
/** Prefer integer display-unit endpoints; preserve any window narrower than one unit. */
|
|
47
48
|
integerZoom?: boolean
|
|
48
49
|
pannable?: boolean
|
|
49
50
|
/** Global record bounds in seconds; enables horizontal panning beyond loaded data. */
|
|
@@ -54,6 +55,8 @@ export interface WaveformChartProps extends WaveformAnimationProps {
|
|
|
54
55
|
initialXDomain?: [number, number]
|
|
55
56
|
initialXDomains?: Record<string, [number, number]>
|
|
56
57
|
xDomainStrategy?: WaveformXDomainStrategy
|
|
58
|
+
/** Strict bounds per normalized series ID; active bounds override Y viewports. */
|
|
59
|
+
amplitudeLimits?: WaveformAmplitudeLimits
|
|
57
60
|
yDomain?: [number, number]
|
|
58
61
|
yDomains?: Record<string, [number, number]>
|
|
59
62
|
timeUnit?: 's' | 'ms'
|