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
|
@@ -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(
|
|
@@ -7,7 +7,6 @@ import { waveformLineDasharray, waveformPointSeriesPath } from './seriesStyle'
|
|
|
7
7
|
|
|
8
8
|
const props = defineProps<{
|
|
9
9
|
track: TrackLayout
|
|
10
|
-
paginationOpacity?: number
|
|
11
10
|
clipPathId: string
|
|
12
11
|
}>()
|
|
13
12
|
|
|
@@ -53,7 +52,7 @@ const renderedSeriesPaths = computed<RenderedSeriesPath[]>(() =>
|
|
|
53
52
|
<g
|
|
54
53
|
v-for="seriesPath in renderedSeriesPaths"
|
|
55
54
|
:key="seriesPath.presentationKey ?? seriesPath.series.id"
|
|
56
|
-
:opacity="
|
|
55
|
+
:opacity="seriesPath.opacity ?? 1"
|
|
57
56
|
:clip-path="seriesPath.animationClip ? `url(#${seriesPath.animationClip.id})` : undefined"
|
|
58
57
|
class="waveform-track__series-item waveform-chart__series"
|
|
59
58
|
:data-series-id="seriesPath.series.id"
|
|
@@ -12,7 +12,6 @@ import WaveformTrackBackdrop from './WaveformTrackBackdrop.vue'
|
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
14
14
|
track: TrackLayout
|
|
15
|
-
paginationOpacity?: number
|
|
16
15
|
clipPathId: string
|
|
17
16
|
innerWidth: number
|
|
18
17
|
zoomable: boolean
|
|
@@ -137,11 +136,7 @@ const frameInset = computed(() =>
|
|
|
137
136
|
aria-hidden="true"
|
|
138
137
|
/>
|
|
139
138
|
|
|
140
|
-
<WaveformSeriesLayer
|
|
141
|
-
:track="track"
|
|
142
|
-
:clip-path-id="clipPathId"
|
|
143
|
-
:pagination-opacity="paginationOpacity"
|
|
144
|
-
/>
|
|
139
|
+
<WaveformSeriesLayer :track="track" :clip-path-id="clipPathId" />
|
|
145
140
|
<WaveformZeroLines
|
|
146
141
|
:track="track"
|
|
147
142
|
:clip-path-id="clipPathId"
|
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 {
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
3
|
|
|
4
4
|
import { WaveformChart } from '../components'
|
|
5
5
|
import type { DemoChartModel } from './types'
|
|
6
|
+
import { applyXDomainStrategy } from '../components/core/xDomain'
|
|
6
7
|
|
|
7
8
|
const model = defineModel<DemoChartModel>('model', { required: true })
|
|
9
|
+
const xDomainStrategy = { type: 'integer-ms' as const, includeExplicit: true }
|
|
10
|
+
const panXDomain = computed(() =>
|
|
11
|
+
applyXDomainStrategy(model.value.recordDomain, xDomainStrategy, true),
|
|
12
|
+
)
|
|
8
13
|
|
|
9
14
|
const waveformChartRef = ref<{ resetViewport: (trackIndex?: number) => void }>()
|
|
10
15
|
|
|
11
|
-
function resetViewport() {
|
|
12
|
-
waveformChartRef.value?.resetViewport()
|
|
16
|
+
function resetViewport(trackIndex?: number) {
|
|
17
|
+
waveformChartRef.value?.resetViewport(trackIndex)
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
defineExpose({ resetViewport })
|
|
@@ -17,6 +22,17 @@ defineExpose({ resetViewport })
|
|
|
17
22
|
|
|
18
23
|
<template>
|
|
19
24
|
<section class="chart-panel">
|
|
25
|
+
<div
|
|
26
|
+
v-if="model.loading || model.errors.length"
|
|
27
|
+
class="remote-window-status"
|
|
28
|
+
aria-live="polite"
|
|
29
|
+
>
|
|
30
|
+
<span v-if="model.loading" role="status">正在加载当前时间范围…</span>
|
|
31
|
+
<template v-if="model.errors.length">
|
|
32
|
+
<span role="alert">{{ model.errors.join(';') }}</span>
|
|
33
|
+
<button type="button" :disabled="model.loading" @click="model.retry()">重试</button>
|
|
34
|
+
</template>
|
|
35
|
+
</div>
|
|
20
36
|
<WaveformChart
|
|
21
37
|
ref="waveformChartRef"
|
|
22
38
|
layout-preset="edge-compact"
|
|
@@ -25,15 +41,19 @@ defineExpose({ resetViewport })
|
|
|
25
41
|
v-model:hidden-series-ids="model.hiddenSeriesIds"
|
|
26
42
|
:data="model.data"
|
|
27
43
|
:min-visible-points="2"
|
|
44
|
+
:min-zoom-span="model.minZoomSpan"
|
|
45
|
+
:max-zoom-scale="null"
|
|
46
|
+
:pan-x-domain="panXDomain"
|
|
28
47
|
:integer-zoom="model.integerZoom"
|
|
29
48
|
:initial-x-domain="model.initialXDomain"
|
|
30
|
-
:x-domain-strategy="
|
|
49
|
+
:x-domain-strategy="xDomainStrategy"
|
|
31
50
|
:display-mode="model.displayMode"
|
|
32
51
|
:overlay-mode="model.overlayMode"
|
|
33
52
|
:grid="{
|
|
34
53
|
rowCount: model.rowCount,
|
|
35
54
|
columnCount: model.columnCount,
|
|
36
55
|
showPagination: true,
|
|
56
|
+
fillIncompleteLastRow: true,
|
|
37
57
|
trackLines: model.gridTrackLines,
|
|
38
58
|
}"
|
|
39
59
|
:title="model.title"
|
|
@@ -55,8 +75,33 @@ defineExpose({ resetViewport })
|
|
|
55
75
|
v-model:interaction-mode="model.interactionMode"
|
|
56
76
|
:toolbar="model.toolbarVisible"
|
|
57
77
|
pannable
|
|
78
|
+
@zoom-intent="model.handleZoomIntent"
|
|
79
|
+
@zoom-change="model.handleZoomChange"
|
|
58
80
|
@zoom-end="model.handleZoomEnd"
|
|
81
|
+
@pan-end="model.handlePanEnd"
|
|
59
82
|
@zoom-reset="model.resetWaveformViewport"
|
|
60
83
|
/>
|
|
61
84
|
</section>
|
|
62
85
|
</template>
|
|
86
|
+
|
|
87
|
+
<style scoped>
|
|
88
|
+
.chart-panel {
|
|
89
|
+
position: relative;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.remote-window-status {
|
|
93
|
+
position: absolute;
|
|
94
|
+
top: 6px;
|
|
95
|
+
right: 12px;
|
|
96
|
+
z-index: 2;
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 8px;
|
|
100
|
+
max-width: calc(100% - 24px);
|
|
101
|
+
padding: 6px 10px;
|
|
102
|
+
border: 1px solid #d0d5dd;
|
|
103
|
+
border-radius: 4px;
|
|
104
|
+
background: #fff;
|
|
105
|
+
color: #344054;
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { InjectionKey } from 'vue'
|
|
2
|
+
import { normalizeWaveformSeries } from '../core'
|
|
3
|
+
import { normalizeWaveformSeriesSources } from '../core/data'
|
|
4
|
+
import type { WaveformData } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface RemoteWindowRequest {
|
|
7
|
+
start: number
|
|
8
|
+
end: number
|
|
9
|
+
seriesIds?: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type RemoteWindowLoader = (
|
|
13
|
+
request: RemoteWindowRequest,
|
|
14
|
+
signal: AbortSignal,
|
|
15
|
+
) => Promise<WaveformData>
|
|
16
|
+
|
|
17
|
+
export const remoteWindowLoaderKey: InjectionKey<RemoteWindowLoader> = Symbol('remoteWindowLoader')
|
|
18
|
+
|
|
19
|
+
export interface MockWindowOptions {
|
|
20
|
+
delayMs?: number | ((request: RemoteWindowRequest) => number)
|
|
21
|
+
empty?: boolean | ((request: RemoteWindowRequest) => boolean)
|
|
22
|
+
fail?: boolean | ((request: RemoteWindowRequest) => boolean)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Preserve series identities even when a window has no samples. */
|
|
26
|
+
export function selectWaveformWindow(
|
|
27
|
+
data: WaveformData,
|
|
28
|
+
request: RemoteWindowRequest,
|
|
29
|
+
empty = false,
|
|
30
|
+
): WaveformData {
|
|
31
|
+
const series = normalizeWaveformSeriesSources(data, true)
|
|
32
|
+
const selected = request.seriesIds ? new Set(request.seriesIds) : undefined
|
|
33
|
+
const windows = series
|
|
34
|
+
.filter((item) => !selected || selected.has(item.id))
|
|
35
|
+
.map(({ source, ...item }) => {
|
|
36
|
+
const range = source.visibleRange([request.start, request.end])
|
|
37
|
+
return {
|
|
38
|
+
...item,
|
|
39
|
+
data: {
|
|
40
|
+
kind: 'points' as const,
|
|
41
|
+
points: empty ? [] : source.pointsInRange(range.start, range.end),
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
return data.kind === 'series'
|
|
46
|
+
? { kind: 'series', series: windows }
|
|
47
|
+
: (windows[0]?.data ?? { kind: 'points', points: [] })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mergeWaveformWindow(
|
|
51
|
+
current: WaveformData,
|
|
52
|
+
response: WaveformData,
|
|
53
|
+
seriesIds?: string[],
|
|
54
|
+
): WaveformData {
|
|
55
|
+
if (!seriesIds || current.kind !== 'series' || response.kind !== 'series') return response
|
|
56
|
+
const selected = new Set(seriesIds)
|
|
57
|
+
const byId = new Map(response.series.map((series) => [series.id, series]))
|
|
58
|
+
return {
|
|
59
|
+
kind: 'series',
|
|
60
|
+
series: current.series.map((series) =>
|
|
61
|
+
series.id && selected.has(series.id) ? (byId.get(series.id) ?? series) : series,
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Metadata is derived from the complete record, never from a loaded window. */
|
|
67
|
+
export function waveformRecordMetadata(data: WaveformData) {
|
|
68
|
+
let minimum = Infinity
|
|
69
|
+
let maximum = -Infinity
|
|
70
|
+
let minZoomSpan = Infinity
|
|
71
|
+
for (const series of normalizeWaveformSeries(data)) {
|
|
72
|
+
for (let index = 0; index < series.points.length; index++) {
|
|
73
|
+
const x = series.points[index].x
|
|
74
|
+
minimum = Math.min(minimum, x)
|
|
75
|
+
maximum = Math.max(maximum, x)
|
|
76
|
+
const previous = series.points[index - 1]?.x
|
|
77
|
+
if (previous !== undefined && x > previous) {
|
|
78
|
+
minZoomSpan = Math.min(minZoomSpan, x - previous)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
domain: (minimum < maximum ? [minimum, maximum] : [0, 1]) as [number, number],
|
|
84
|
+
minZoomSpan: Number.isFinite(minZoomSpan) ? minZoomSpan : undefined,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createMockWindowLoader(
|
|
89
|
+
source: () => WaveformData,
|
|
90
|
+
options: MockWindowOptions = {},
|
|
91
|
+
): RemoteWindowLoader {
|
|
92
|
+
return (request, signal) =>
|
|
93
|
+
new Promise((resolve, reject) => {
|
|
94
|
+
const abort = () => {
|
|
95
|
+
clearTimeout(timer)
|
|
96
|
+
signal.removeEventListener('abort', abort)
|
|
97
|
+
reject(new DOMException('Request cancelled', 'AbortError'))
|
|
98
|
+
}
|
|
99
|
+
const delay =
|
|
100
|
+
typeof options.delayMs === 'function' ? options.delayMs(request) : options.delayMs
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
signal.removeEventListener('abort', abort)
|
|
103
|
+
const fail = typeof options.fail === 'function' ? options.fail(request) : options.fail
|
|
104
|
+
if (fail) {
|
|
105
|
+
reject(new Error('模拟数据加载失败'))
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
const empty = typeof options.empty === 'function' ? options.empty(request) : options.empty
|
|
109
|
+
resolve(selectWaveformWindow(source(), request, empty))
|
|
110
|
+
}, delay ?? 80)
|
|
111
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
112
|
+
if (signal.aborted) abort()
|
|
113
|
+
})
|
|
114
|
+
}
|
package/src/demo/types.ts
CHANGED
|
@@ -14,7 +14,9 @@ import type {
|
|
|
14
14
|
WaveformTitleOptions,
|
|
15
15
|
WaveformZeroLineOptions,
|
|
16
16
|
WaveformZoomEndPayload,
|
|
17
|
+
WaveformZoomIntentPayload,
|
|
17
18
|
} from '../components'
|
|
19
|
+
import type { WaveformZoomResetPayload, WaveformPanEndPayload } from '../types'
|
|
18
20
|
import type { DemoXAxisLabelFormat, DemoXAxisLabelTimeZone } from './useDemoXAxisLabelControls'
|
|
19
21
|
|
|
20
22
|
interface SelectOption<T> {
|
|
@@ -97,6 +99,11 @@ export interface DemoChartModel {
|
|
|
97
99
|
toolbarVisible: boolean
|
|
98
100
|
data: WaveformData
|
|
99
101
|
initialXDomain?: [number, number]
|
|
102
|
+
recordDomain: [number, number]
|
|
103
|
+
minZoomSpan?: number
|
|
104
|
+
loading: boolean
|
|
105
|
+
errors: string[]
|
|
106
|
+
retry: () => Promise<void[]>
|
|
100
107
|
displayMode: WaveformDisplayMode
|
|
101
108
|
overlayMode: WaveformOverlayMode
|
|
102
109
|
rowCount: number
|
|
@@ -120,5 +127,8 @@ export interface DemoChartModel {
|
|
|
120
127
|
interactionMode: WaveformInteractionMode
|
|
121
128
|
hiddenSeriesIds: string[]
|
|
122
129
|
handleZoomEnd: (payload: WaveformZoomEndPayload) => void | Promise<void>
|
|
123
|
-
|
|
130
|
+
handleZoomIntent: (payload: WaveformZoomIntentPayload) => void
|
|
131
|
+
handleZoomChange: (domain: [number, number]) => void
|
|
132
|
+
handlePanEnd: (payload: WaveformPanEndPayload) => void | Promise<void>
|
|
133
|
+
resetWaveformViewport: (payload?: WaveformZoomResetPayload) => void | Promise<void>
|
|
124
134
|
}
|