waveform-analysis 0.1.75 → 0.1.76
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 +178 -23
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- 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 +1 -1
- 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/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 -2
- 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 -3
- 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/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/dist/types/data/sx2Channel.d.ts +0 -10
- package/src/data/sx2Channel.ts +0 -315
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveXAxisTicks } from '../core/xAxisTicks'
|
|
2
|
+
import { formatPlainNumber } from '../../utils/formatters'
|
|
2
3
|
|
|
3
4
|
export interface IntegerZoomOptions {
|
|
4
5
|
integerZoom?: boolean
|
|
@@ -10,17 +11,20 @@ export function integerZoomTicks(
|
|
|
10
11
|
count: number,
|
|
11
12
|
options: IntegerZoomOptions,
|
|
12
13
|
) {
|
|
13
|
-
if (!options.integerZoom) return
|
|
14
|
+
if (!options.integerZoom) return resolveXAxisTicks(domain, count)
|
|
14
15
|
const factor = options.timeUnit === 's' ? 1 : 1000
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
16
|
+
const fractional = hasFractionalZoomDomain(domain, options)
|
|
17
|
+
return resolveXAxisTicks(
|
|
18
|
+
domain.map((value) => value * factor) as [number, number],
|
|
19
|
+
count,
|
|
20
|
+
fractional ? 0 : 1,
|
|
21
|
+
)
|
|
22
|
+
.filter((value) => fractional || Number.isInteger(value))
|
|
19
23
|
.map((value) => value / factor)
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export function formatIntegerZoomLabel(value: number): string {
|
|
23
|
-
return
|
|
27
|
+
return formatPlainNumber(Number(stableInteger(value).toPrecision(15)))
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
function stableInteger(value: number): number {
|
|
@@ -30,7 +34,17 @@ function stableInteger(value: number): number {
|
|
|
30
34
|
: value
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
function displayDomain(domain: [number, number], options: IntegerZoomOptions): [number, number] {
|
|
38
|
+
const factor = options.timeUnit === 's' ? 1 : 1000
|
|
39
|
+
return [stableInteger(domain[0] * factor), stableInteger(domain[1] * factor)]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Fractional viewports, including those panned across an integer, need decimal ticks. */
|
|
43
|
+
function hasFractionalZoomDomain(domain: [number, number], options: IntegerZoomOptions): boolean {
|
|
44
|
+
return displayDomain(domain, options).some((value) => !Number.isInteger(value))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Align outwards only when the displayed span reaches one unit. */
|
|
34
48
|
export function alignIntegerZoomDomain(
|
|
35
49
|
domain: [number, number],
|
|
36
50
|
options: IntegerZoomOptions,
|
|
@@ -41,24 +55,29 @@ export function alignIntegerZoomDomain(
|
|
|
41
55
|
const end = stableInteger(Math.max(domain[0], domain[1]) * factor)
|
|
42
56
|
if (!Number.isSafeInteger(Math.floor(start)) || !Number.isSafeInteger(Math.ceil(end)))
|
|
43
57
|
return domain
|
|
58
|
+
if (end > start && end - start < 1) return [start / factor, end / factor]
|
|
44
59
|
const left = Math.floor(start)
|
|
45
60
|
return [left / factor, Math.max(left + 1, Math.ceil(end)) / factor]
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
/** Recover
|
|
63
|
+
/** Recover endpoints after a D3 transform without applying a new zoom or changing pan span. */
|
|
49
64
|
export function normalizeIntegerZoomDomain(
|
|
50
65
|
domain: [number, number],
|
|
51
66
|
options: IntegerZoomOptions,
|
|
52
67
|
): [number, number] {
|
|
53
|
-
|
|
68
|
+
if (!options.integerZoom) return domain
|
|
69
|
+
const factor = options.timeUnit === 's' ? 1 : 1000
|
|
70
|
+
return displayDomain(domain, options).map((value) => value / factor) as [number, number]
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
/**
|
|
73
|
+
/** Integer viewports snap translation; sub-unit viewports pan freely across integers. */
|
|
57
74
|
export function alignIntegerPanDomain(
|
|
58
75
|
domain: [number, number],
|
|
59
76
|
options: IntegerZoomOptions,
|
|
60
77
|
): [number, number] {
|
|
61
78
|
if (!options.integerZoom) return domain
|
|
62
79
|
const factor = options.timeUnit === 's' ? 1 : 1000
|
|
63
|
-
|
|
80
|
+
if (Math.abs(domain[1] - domain[0]) * factor < 1) return domain
|
|
81
|
+
const start = Math.round(domain[0] * factor) / factor
|
|
82
|
+
return [start, start + (domain[1] - domain[0])]
|
|
64
83
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
|
|
2
|
+
import { hasMinimumVisibleXValues } from '../../core/rendering'
|
|
3
|
+
import { normalizeIntegerZoomDomain } from './integerZoom'
|
|
2
4
|
import {
|
|
3
5
|
constrainZoomDomain,
|
|
4
6
|
resolveMinimumZoomSpan,
|
|
@@ -32,7 +34,13 @@ export function restoreViewportDomain(
|
|
|
32
34
|
const boundary = remotePanBoundary(props)
|
|
33
35
|
return boundary
|
|
34
36
|
? clampViewportDomain(domain, boundary)
|
|
35
|
-
:
|
|
37
|
+
: normalizeIntegerZoomDomain(
|
|
38
|
+
constrainZoomDomain(domain, baseDomain, groups, {
|
|
39
|
+
...props,
|
|
40
|
+
integerZoom: false,
|
|
41
|
+
}),
|
|
42
|
+
props,
|
|
43
|
+
)
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/** Wheel/box zoom keeps the remote position and uses loaded data for scale limits. */
|
|
@@ -44,10 +52,24 @@ export function constrainViewportZoom(
|
|
|
44
52
|
) {
|
|
45
53
|
const boundary = remotePanBoundary(props)
|
|
46
54
|
if (!boundary) return constrainZoomDomain(domain, baseDomain, groups, props)
|
|
47
|
-
|
|
55
|
+
const availableGroups = groups.filter((group) =>
|
|
56
|
+
hasMinimumVisibleXValues(group, boundary, props.minVisiblePoints),
|
|
57
|
+
)
|
|
58
|
+
// Only constrain against loaded groups that overlap the requested window. An
|
|
59
|
+
// unloaded remote window must remain free to move beyond the cached samples.
|
|
60
|
+
const loadedGroups = availableGroups.filter((group) =>
|
|
61
|
+
group.some((series) => {
|
|
62
|
+
const source = series.source
|
|
63
|
+
const length = source?.length ?? series.points.length
|
|
64
|
+
if (!length) return false
|
|
65
|
+
const first = source ? source.pointAt(0)! : series.points[0]
|
|
66
|
+
const last = source ? source.pointAt(length - 1)! : series.points[length - 1]
|
|
67
|
+
return first.x <= Math.max(...domain) && last.x >= Math.min(...domain)
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
return constrainZoomDomain(domain, boundary, loadedGroups, {
|
|
48
71
|
...props,
|
|
49
|
-
minVisiblePoints: 0,
|
|
50
72
|
maxZoomScale: null,
|
|
51
|
-
minZoomSpan: resolveMinimumZoomSpan(baseDomain,
|
|
73
|
+
minZoomSpan: resolveMinimumZoomSpan(baseDomain, availableGroups, props),
|
|
52
74
|
})
|
|
53
75
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
|
|
2
|
+
import { normalizeIntegerZoomDomain } from './integerZoom'
|
|
3
|
+
import { constrainViewportZoom } from './remoteViewport'
|
|
4
|
+
import type { ZoomSeriesGroup } from './zoomConstraints'
|
|
5
|
+
|
|
6
|
+
export const sameZoomDomain = (a: [number, number], b: [number, number]) =>
|
|
7
|
+
a.every((value, index) => Math.abs(value - b[index]!) < 1e-12)
|
|
8
|
+
|
|
9
|
+
/** Toolbar and wheel steps share their scale factor and constraints. */
|
|
10
|
+
export function resolveStepZoomDomain(
|
|
11
|
+
current: [number, number],
|
|
12
|
+
action: 'zoom-in' | 'zoom-out',
|
|
13
|
+
boundary: [number, number],
|
|
14
|
+
groups: readonly ZoomSeriesGroup[],
|
|
15
|
+
props: ResolvedWaveformChartProps,
|
|
16
|
+
anchorRatio = 0.5,
|
|
17
|
+
): [number, number] {
|
|
18
|
+
const [start, end] = current
|
|
19
|
+
const ratio = Math.max(0, Math.min(1, anchorRatio))
|
|
20
|
+
const anchor = start + (end - start) * ratio
|
|
21
|
+
const span = (end - start) * (action === 'zoom-in' ? 0.5 : 2)
|
|
22
|
+
const constrained = constrainViewportZoom(
|
|
23
|
+
normalizeIntegerZoomDomain([anchor - span * ratio, anchor + span * (1 - ratio)], props),
|
|
24
|
+
boundary,
|
|
25
|
+
groups,
|
|
26
|
+
props,
|
|
27
|
+
)
|
|
28
|
+
// An unsatisfiable point constraint may expand to the entire boundary.
|
|
29
|
+
// A zoom-in step must not turn into a zoom-out operation.
|
|
30
|
+
return action === 'zoom-in' && constrained[1] - constrained[0] > end - start
|
|
31
|
+
? [...current]
|
|
32
|
+
: constrained
|
|
33
|
+
}
|
|
@@ -12,6 +12,7 @@ import { constrainViewportZoom } from './remoteViewport'
|
|
|
12
12
|
import { createViewportPan } from './viewportPan'
|
|
13
13
|
import { transformForDomain } from './zoomConstraints'
|
|
14
14
|
import { emitBoxZoomIntent } from './zoomEventPayload'
|
|
15
|
+
import { sameZoomDomain } from './stepZoom'
|
|
15
16
|
export interface ViewportContext {
|
|
16
17
|
clearBoundaries?: (trackIndex?: number) => void
|
|
17
18
|
props: ResolvedWaveformChartProps
|
|
@@ -32,8 +33,6 @@ export interface ViewportContext {
|
|
|
32
33
|
isPresentationMode: ComputedRef<boolean>
|
|
33
34
|
editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
|
|
34
35
|
resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
|
|
35
|
-
canZoomTrack: (track: TrackLayout) => boolean
|
|
36
|
-
canZoomSharedTracks: () => boolean
|
|
37
36
|
configureZoom: () => void
|
|
38
37
|
cancelPendingZoom: () => void
|
|
39
38
|
clearHover: () => void
|
|
@@ -59,8 +58,6 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
59
58
|
isPresentationMode,
|
|
60
59
|
editorSeriesOptions,
|
|
61
60
|
resolveInitialTrackDomain,
|
|
62
|
-
canZoomTrack,
|
|
63
|
-
canZoomSharedTracks,
|
|
64
61
|
configureZoom,
|
|
65
62
|
cancelPendingZoom,
|
|
66
63
|
clearHover,
|
|
@@ -184,7 +181,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
184
181
|
}
|
|
185
182
|
const applyBoxZoom = (active: ViewportSelectionState) => {
|
|
186
183
|
const track = trackLayouts.value.find((item) => item.index === active.trackIndex)
|
|
187
|
-
if (!track
|
|
184
|
+
if (!track) return
|
|
188
185
|
const offsetX = active.independent ? 0 : track.left
|
|
189
186
|
const left = Math.max(
|
|
190
187
|
0,
|
|
@@ -205,6 +202,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
205
202
|
groups,
|
|
206
203
|
props,
|
|
207
204
|
)
|
|
205
|
+
if (sameZoomDomain(track.xScale.domain() as [number, number], xDomain)) return
|
|
208
206
|
if (active.independent) {
|
|
209
207
|
const next = [...independentTransforms.value]
|
|
210
208
|
next[track.index] = transformForDomain(xDomain, baseXDomain, track.width)
|
|
@@ -333,26 +331,6 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
333
331
|
editorSeriesOptions,
|
|
334
332
|
configureZoom,
|
|
335
333
|
})
|
|
336
|
-
const requestViewportReset = (event: MouseEvent) => {
|
|
337
|
-
if (isPresentationMode.value || !props.zoomable || !isZoomMode.value) return
|
|
338
|
-
if (props.displayMode === 'independent') {
|
|
339
|
-
const target = event.target instanceof Element ? event.target : null
|
|
340
|
-
const overlay = target?.closest('[data-independent-overlay-index]')
|
|
341
|
-
const trackIndex = Number(overlay?.getAttribute('data-independent-overlay-index'))
|
|
342
|
-
const track = trackLayouts.value.find((item) => item.index === trackIndex)
|
|
343
|
-
if (!track) return
|
|
344
|
-
event.preventDefault()
|
|
345
|
-
resetViewport(trackIndex)
|
|
346
|
-
emit('zoom-reset', {
|
|
347
|
-
trackIndex,
|
|
348
|
-
seriesIds: track.legendSeries.map((series) => series.id),
|
|
349
|
-
})
|
|
350
|
-
return
|
|
351
|
-
}
|
|
352
|
-
event.preventDefault()
|
|
353
|
-
resetViewport()
|
|
354
|
-
emit('zoom-reset', {})
|
|
355
|
-
}
|
|
356
334
|
return {
|
|
357
335
|
selectionBox,
|
|
358
336
|
beginViewportDrag,
|
|
@@ -362,6 +340,5 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
362
340
|
cancelViewportDrag,
|
|
363
341
|
resetViewport,
|
|
364
342
|
setViewportDomain,
|
|
365
|
-
requestViewportReset,
|
|
366
343
|
}
|
|
367
344
|
}
|
|
@@ -1,23 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
scaleLinear,
|
|
3
|
-
select,
|
|
4
|
-
zoom,
|
|
5
|
-
zoomIdentity,
|
|
6
|
-
zoomTransform,
|
|
7
|
-
type D3ZoomEvent,
|
|
8
|
-
type ZoomBehavior,
|
|
9
|
-
type ZoomTransform,
|
|
10
|
-
} from 'd3'
|
|
1
|
+
import { pointer, scaleLinear, select, zoomIdentity, zoomTransform, type ZoomTransform } from 'd3'
|
|
11
2
|
import type { ComputedRef, ShallowRef } from 'vue'
|
|
12
3
|
|
|
13
|
-
import {
|
|
14
|
-
import { WHEEL_ZOOM_DEBOUNCE_MS, ZOOM_CONSTRAINTS } from '../core/constants'
|
|
4
|
+
import { WHEEL_ZOOM_DEBOUNCE_MS } from '../core/constants'
|
|
15
5
|
import type { TrackLayout } from '../core/types'
|
|
16
6
|
import type { ResolvedWaveformChartProps, WaveformChartEmit } from '../core/waveformChartTypes'
|
|
17
7
|
import { useAnimationFrameThrottle } from '../utils/useAnimationFrameThrottle'
|
|
18
|
-
import {
|
|
8
|
+
import { resolveStepZoomDomain, sameZoomDomain } from './stepZoom'
|
|
19
9
|
import { normalizeIntegerZoomDomain } from './integerZoom'
|
|
20
|
-
import {
|
|
10
|
+
import { transformForDomain, type ZoomSeriesGroup } from './zoomConstraints'
|
|
21
11
|
import {
|
|
22
12
|
emitIndependentWheelZoomEnds,
|
|
23
13
|
emitSharedWheelZoomEnd,
|
|
@@ -57,16 +47,13 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
57
47
|
initialXDomain,
|
|
58
48
|
sharedZoomDomain,
|
|
59
49
|
innerWidth,
|
|
60
|
-
innerHeight,
|
|
61
50
|
hasChartArea,
|
|
62
51
|
isZoomMode,
|
|
63
52
|
isPresentationMode,
|
|
64
53
|
resolveInitialTrackDomain,
|
|
65
54
|
cancelPendingHover,
|
|
66
55
|
} = context
|
|
67
|
-
const zoomBehaviors = new Map<number | 'shared', ZoomBehavior<SVGRectElement, unknown>>()
|
|
68
56
|
const zoomThrottle = useAnimationFrameThrottle()
|
|
69
|
-
let synchronizingZoomTransform = false
|
|
70
57
|
let pendingSharedZoomTransform: ZoomTransform | null = null
|
|
71
58
|
let pendingSharedZoomGesture: ZoomGestureKind | null = null
|
|
72
59
|
let lastSharedZoomGesture: ZoomGestureKind | null = null
|
|
@@ -75,11 +62,10 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
75
62
|
const lastIndependentWheelSeriesIds = new Map<string, string[]>()
|
|
76
63
|
let wheelZoomEndTimer: ReturnType<typeof setTimeout> | undefined
|
|
77
64
|
let preservePendingWheelDuringDataRebind = false
|
|
78
|
-
let zoomBindingEpoch = 0
|
|
79
65
|
|
|
80
66
|
const scheduleZoomCommit = () => zoomThrottle.schedule(commitPendingZoom)
|
|
81
|
-
const handleSharedZoom = (event:
|
|
82
|
-
if (
|
|
67
|
+
const handleSharedZoom = (event: { transform: ZoomTransform }) => {
|
|
68
|
+
if (isPresentationMode.value) return
|
|
83
69
|
cancelPendingHover()
|
|
84
70
|
pendingSharedZoomTransform = event.transform
|
|
85
71
|
pendingSharedZoomGesture = 'wheel'
|
|
@@ -91,11 +77,8 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
91
77
|
emit('zoom-intent', { start, end, gesture: 'wheel' })
|
|
92
78
|
scheduleZoomCommit()
|
|
93
79
|
}
|
|
94
|
-
const handleIndependentZoom = (
|
|
95
|
-
|
|
96
|
-
trackIndex: number,
|
|
97
|
-
) => {
|
|
98
|
-
if (synchronizingZoomTransform || isPresentationMode.value) return
|
|
80
|
+
const handleIndependentZoom = (event: { transform: ZoomTransform }, trackIndex: number) => {
|
|
81
|
+
if (isPresentationMode.value) return
|
|
99
82
|
cancelPendingHover()
|
|
100
83
|
pendingIndependentZoomTransforms.set(trackIndex, event.transform)
|
|
101
84
|
pendingIndependentZoomGestures.set(trackIndex, 'wheel')
|
|
@@ -166,16 +149,6 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
166
149
|
emitSharedWheelZoomEnd(emit, sharedZoomDomain.value, trackLayouts.value)
|
|
167
150
|
lastSharedZoomGesture = null
|
|
168
151
|
}
|
|
169
|
-
const flushPendingZoom = () => {
|
|
170
|
-
zoomThrottle.flush()
|
|
171
|
-
commitPendingZoom()
|
|
172
|
-
if (lastSharedZoomGesture === 'wheel' || lastIndependentWheelSeriesIds.size) return
|
|
173
|
-
emitZoomEnd()
|
|
174
|
-
}
|
|
175
|
-
const handleD3ZoomEnd = (bindingEpoch: number) => {
|
|
176
|
-
if (bindingEpoch !== zoomBindingEpoch) return
|
|
177
|
-
flushPendingZoom()
|
|
178
|
-
}
|
|
179
152
|
function scheduleWheelZoomEnd() {
|
|
180
153
|
if (wheelZoomEndTimer !== undefined) clearTimeout(wheelZoomEndTimer)
|
|
181
154
|
wheelZoomEndTimer = setTimeout(() => {
|
|
@@ -207,81 +180,50 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
207
180
|
}
|
|
208
181
|
}
|
|
209
182
|
const clearZoomBindings = (preservePendingWheel = false) => {
|
|
210
|
-
zoomBindingEpoch += 1
|
|
211
183
|
if (!preservePendingWheel) cancelPendingZoom()
|
|
212
184
|
svgElement.value
|
|
213
185
|
?.querySelectorAll<SVGRectElement>('.waveform-chart__overlay')
|
|
214
186
|
.forEach((overlay) => select(overlay).on('.zoom', null))
|
|
215
|
-
zoomBehaviors.clear()
|
|
216
187
|
}
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
): number => {
|
|
221
|
-
const domainSpan = Math.abs(domain[1] - domain[0])
|
|
222
|
-
if (!Number.isFinite(domainSpan) || domainSpan <= 0) return ZOOM_CONSTRAINTS.MIN_SCALE
|
|
223
|
-
const minimumSpan = resolveMinimumZoomSpan(domain, groups, props)
|
|
224
|
-
return Math.max(
|
|
225
|
-
ZOOM_CONSTRAINTS.MIN_SCALE,
|
|
226
|
-
minimumSpan > 0 ? domainSpan / minimumSpan : Number.POSITIVE_INFINITY,
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
const constrainTransform = (
|
|
230
|
-
transform: ZoomTransform,
|
|
231
|
-
domain: [number, number],
|
|
188
|
+
const bindWheelZoom = (
|
|
189
|
+
overlay: SVGRectElement,
|
|
190
|
+
baseDomain: [number, number],
|
|
232
191
|
width: number,
|
|
233
192
|
groups: readonly ZoomSeriesGroup[],
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (event.type !== 'wheel') return false
|
|
265
|
-
const deltaY = (event as WheelEvent).deltaY
|
|
266
|
-
const currentScale = zoomTransform(event.currentTarget as Element).k
|
|
267
|
-
|
|
268
|
-
// 放大操作:需要检查是否还有足够的数据点支持继续放大
|
|
269
|
-
if (deltaY < 0) return canZoomIn
|
|
270
|
-
|
|
271
|
-
// 缩小操作:只要当前缩放级别大于最小比例就允许
|
|
272
|
-
// 即使已达到最小可见点数限制,用户也应该能够缩小视图
|
|
273
|
-
if (deltaY > 0) {
|
|
274
|
-
// 使用小的容差避免浮点精度问题导致无法缩小到边界
|
|
275
|
-
const scaleTolerance = 1e-6
|
|
276
|
-
return currentScale > minimumScale * (1 + scaleTolerance)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return false
|
|
193
|
+
transform: ZoomTransform,
|
|
194
|
+
trackIndex?: number,
|
|
195
|
+
) => {
|
|
196
|
+
select(overlay)
|
|
197
|
+
.property('__zoom', transform)
|
|
198
|
+
.on(
|
|
199
|
+
'wheel.zoom',
|
|
200
|
+
(event: WheelEvent) => {
|
|
201
|
+
if (!Number.isFinite(event.deltaY) || event.deltaY === 0) return
|
|
202
|
+
const current = zoomTransform(overlay)
|
|
203
|
+
.rescaleX(scaleLinear(baseDomain, [0, width]))
|
|
204
|
+
.domain() as [number, number]
|
|
205
|
+
const next = resolveStepZoomDomain(
|
|
206
|
+
current,
|
|
207
|
+
event.deltaY < 0 ? 'zoom-in' : 'zoom-out',
|
|
208
|
+
baseDomain,
|
|
209
|
+
groups,
|
|
210
|
+
props,
|
|
211
|
+
pointer(event, overlay)[0] / width,
|
|
212
|
+
)
|
|
213
|
+
if (sameZoomDomain(current, next)) return
|
|
214
|
+
event.preventDefault()
|
|
215
|
+
event.stopImmediatePropagation()
|
|
216
|
+
const transform = transformForDomain(next, baseDomain, width)
|
|
217
|
+
select(overlay).property('__zoom', transform)
|
|
218
|
+
if (trackIndex === undefined) handleSharedZoom({ transform })
|
|
219
|
+
else handleIndependentZoom({ transform }, trackIndex)
|
|
220
|
+
},
|
|
221
|
+
{ passive: false },
|
|
222
|
+
)
|
|
280
223
|
}
|
|
281
224
|
const configureZoom = () => {
|
|
282
225
|
const preservePendingWheel = preservePendingWheelDuringDataRebind
|
|
283
226
|
clearZoomBindings(preservePendingWheel)
|
|
284
|
-
const bindingEpoch = zoomBindingEpoch
|
|
285
227
|
if (
|
|
286
228
|
isPresentationMode.value ||
|
|
287
229
|
!props.zoomable ||
|
|
@@ -299,35 +241,14 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
299
241
|
if (!overlay) return
|
|
300
242
|
const dataDomain = resolveInitialTrackDomain(track)
|
|
301
243
|
const groups = [track.seriesList]
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
.constrain((transform) => constrainTransform(transform, dataDomain, track.width, groups))
|
|
311
|
-
.extent([
|
|
312
|
-
[0, 0],
|
|
313
|
-
[track.width, track.height],
|
|
314
|
-
])
|
|
315
|
-
.translateExtent([
|
|
316
|
-
[0, 0],
|
|
317
|
-
[track.width, track.height],
|
|
318
|
-
])
|
|
319
|
-
.on('zoom', (event) => handleIndependentZoom(event, track.index))
|
|
320
|
-
.on('end', () => handleD3ZoomEnd(bindingEpoch))
|
|
321
|
-
zoomBehaviors.set(track.index, behavior)
|
|
322
|
-
synchronizingZoomTransform = true
|
|
323
|
-
try {
|
|
324
|
-
select(overlay)
|
|
325
|
-
.call(behavior)
|
|
326
|
-
.on('dblclick.zoom', null)
|
|
327
|
-
.call(behavior.transform, independentTransforms.value[track.index] ?? zoomIdentity)
|
|
328
|
-
} finally {
|
|
329
|
-
synchronizingZoomTransform = false
|
|
330
|
-
}
|
|
244
|
+
bindWheelZoom(
|
|
245
|
+
overlay,
|
|
246
|
+
dataDomain,
|
|
247
|
+
track.width,
|
|
248
|
+
groups,
|
|
249
|
+
independentTransforms.value[track.index] ?? zoomIdentity,
|
|
250
|
+
track.index,
|
|
251
|
+
)
|
|
331
252
|
})
|
|
332
253
|
return
|
|
333
254
|
}
|
|
@@ -336,40 +257,12 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
336
257
|
const groups = trackLayouts.value
|
|
337
258
|
.filter((track) => track.hasVisibleSeries)
|
|
338
259
|
.map((track) => track.seriesList)
|
|
339
|
-
|
|
340
|
-
const behavior = zoom<SVGRectElement, unknown>()
|
|
341
|
-
.filter((event) => canHandleWheelZoom(event, canZoomSharedTracks(), minimumScale))
|
|
342
|
-
.scaleExtent([minimumScale, resolveMaximumZoomScale(initialXDomain.value, groups)])
|
|
343
|
-
.constrain((transform) =>
|
|
344
|
-
constrainTransform(transform, initialXDomain.value, innerWidth.value, groups),
|
|
345
|
-
)
|
|
346
|
-
.extent([
|
|
347
|
-
[0, 0],
|
|
348
|
-
[innerWidth.value, innerHeight.value],
|
|
349
|
-
])
|
|
350
|
-
.translateExtent([
|
|
351
|
-
[0, 0],
|
|
352
|
-
[innerWidth.value, innerHeight.value],
|
|
353
|
-
])
|
|
354
|
-
.on('zoom', handleSharedZoom)
|
|
355
|
-
.on('end', () => handleD3ZoomEnd(bindingEpoch))
|
|
356
|
-
zoomBehaviors.set('shared', behavior)
|
|
357
|
-
synchronizingZoomTransform = true
|
|
358
|
-
try {
|
|
359
|
-
select(overlay)
|
|
360
|
-
.call(behavior)
|
|
361
|
-
.on('dblclick.zoom', null)
|
|
362
|
-
.call(behavior.transform, sharedTransform.value)
|
|
363
|
-
} finally {
|
|
364
|
-
synchronizingZoomTransform = false
|
|
365
|
-
}
|
|
260
|
+
bindWheelZoom(overlay, initialXDomain.value, innerWidth.value, groups, sharedTransform.value)
|
|
366
261
|
}
|
|
367
262
|
|
|
368
263
|
return {
|
|
369
264
|
cancelPendingZoom,
|
|
370
265
|
clearZoomBindings,
|
|
371
|
-
canZoomTrack,
|
|
372
|
-
canZoomSharedTracks,
|
|
373
266
|
configureZoom,
|
|
374
267
|
prepareForDataChange,
|
|
375
268
|
}
|
|
@@ -73,7 +73,7 @@ export function resolveMinimumZoomSpan(
|
|
|
73
73
|
groups: readonly ZoomSeriesGroup[],
|
|
74
74
|
options: ZoomConstraintOptions,
|
|
75
75
|
): number {
|
|
76
|
-
const normalized =
|
|
76
|
+
const normalized = normalizedBoundary(boundary)
|
|
77
77
|
const boundarySpan = normalized[1] - normalized[0]
|
|
78
78
|
if (!Number.isFinite(boundarySpan) || boundarySpan <= 0) return 0
|
|
79
79
|
const configuredSpan =
|
|
@@ -89,8 +89,7 @@ export function resolveMinimumZoomSpan(
|
|
|
89
89
|
: options.maxZoomScale !== null && configuredSpan === 0 && required === 0
|
|
90
90
|
? boundarySpan / ZOOM_CONSTRAINTS.DEFAULT_MAX_SCALE
|
|
91
91
|
: 0
|
|
92
|
-
|
|
93
|
-
return Math.max(configuredSpan, pointSpan, scaleSpan, integerSpan)
|
|
92
|
+
return Math.max(configuredSpan, pointSpan, scaleSpan)
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
function expandToMinimumPoints(
|
|
@@ -141,7 +140,7 @@ export function constrainZoomDomain(
|
|
|
141
140
|
groups: readonly ZoomSeriesGroup[],
|
|
142
141
|
options: ZoomConstraintOptions,
|
|
143
142
|
): [number, number] {
|
|
144
|
-
const normalized =
|
|
143
|
+
const normalized = normalizedBoundary(boundary)
|
|
145
144
|
const boundarySpan = normalized[1] - normalized[0]
|
|
146
145
|
if (!Number.isFinite(boundarySpan) || boundarySpan <= 0) return normalized
|
|
147
146
|
if (Math.abs(domain[1] - domain[0]) >= boundarySpan * (1 - 1e-12)) return normalized
|
|
@@ -159,7 +158,8 @@ export function constrainZoomDomain(
|
|
|
159
158
|
const span = Math.max(minimumSpan, Math.min(boundarySpan, expanded[1] - expanded[0]))
|
|
160
159
|
const center = (expanded[0] + expanded[1]) / 2
|
|
161
160
|
const start = Math.max(normalized[0], Math.min(center - span / 2, normalized[1] - span))
|
|
162
|
-
|
|
161
|
+
const aligned = alignIntegerZoomDomain([start, start + span], options)
|
|
162
|
+
return [Math.max(normalized[0], aligned[0]), Math.min(normalized[1], aligned[1])]
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
export function transformForDomain(
|
package/src/core/data.ts
CHANGED
|
@@ -215,10 +215,11 @@ function normalizedSeriesOptions(
|
|
|
215
215
|
/** Internal normalized series which preserves compact TypedArray storage for chart rendering. */
|
|
216
216
|
export function normalizeWaveformSeriesSources(
|
|
217
217
|
data: WaveformData,
|
|
218
|
+
retainEmpty = false,
|
|
218
219
|
): NormalizedWaveformSourceSeries[] {
|
|
219
220
|
if (data.kind !== 'series') {
|
|
220
221
|
const source = createWaveformPointSource(data, normalizeWaveformData)
|
|
221
|
-
return source.length
|
|
222
|
+
return source.length || retainEmpty
|
|
222
223
|
? [
|
|
223
224
|
{
|
|
224
225
|
...normalizedSeriesOptions('series-0', { name: '' }),
|
|
@@ -239,7 +240,9 @@ export function normalizeWaveformSeriesSources(
|
|
|
239
240
|
}
|
|
240
241
|
usedIds.add(uniqueId)
|
|
241
242
|
const source = createWaveformPointSource(series.data, normalizeWaveformData)
|
|
242
|
-
return source.length
|
|
243
|
+
return source.length || retainEmpty
|
|
244
|
+
? [{ ...normalizedSeriesOptions(uniqueId, series), source }]
|
|
245
|
+
: []
|
|
243
246
|
})
|
|
244
247
|
}
|
|
245
248
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { WaveformData, WaveformPoint, WaveformSeries } from '../types'
|
|
2
2
|
import { recordedDampedPoints } from './recordedDampedWaveform'
|
|
3
|
-
import { sx2Channel } from './sx2Channel'
|
|
4
3
|
|
|
5
4
|
const POINT_COUNT = 1_000
|
|
6
5
|
const START_TIME = -5
|
|
@@ -177,15 +176,6 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
177
176
|
pointType: 'none',
|
|
178
177
|
signal: (time) => 6e-9 * Math.exp(-(time + 5) * 0.2) * Math.cos(TWO_PI * 0.8 * time),
|
|
179
178
|
},
|
|
180
|
-
{
|
|
181
|
-
id: `recorded-${sx2Channel.chnl_id}`,
|
|
182
|
-
shotNo: String(sx2Channel.shot),
|
|
183
|
-
name: sx2Channel.chnl,
|
|
184
|
-
unit: sx2Channel.dat_unit,
|
|
185
|
-
lineType: 'linear',
|
|
186
|
-
pointType: 'none',
|
|
187
|
-
signal: sx2Channel.data.map((value, index) => [sx2Channel.time[index]!, value]),
|
|
188
|
-
},
|
|
189
179
|
]
|
|
190
180
|
|
|
191
181
|
export function createSimulatedWaveformData(trackCount?: number): WaveformData {
|