waveform-analysis 0.1.76 → 0.1.78
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 +22 -29
- 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/useWaveformChartController.d.ts +2 -18
- package/dist/types/components/core/useWaveformLayout.d.ts +1 -0
- package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +0 -1
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +0 -1
- package/package.json +1 -1
- package/src/components/WaveformChartView.vue +3 -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/trackPresentation.ts +1 -1
- package/src/components/animation/useWaveformAnimations.ts +36 -13
- package/src/components/controls/useChartCommands.ts +9 -7
- package/src/components/core/useWaveformChartController.ts +1 -3
- package/src/components/core/useWaveformData.ts +13 -1
- package/src/components/core/useWaveformLayout.ts +4 -0
- package/src/components/interaction/zoomConstraints.ts +2 -0
- package/src/components/rendering/WaveformSeriesLayer.vue +1 -2
- package/src/components/rendering/WaveformTrack.vue +1 -6
- package/src/demo/remoteWaveformLoader.ts +10 -5
- package/src/views/AnimationDemo.vue +14 -5
|
@@ -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,
|
|
@@ -122,13 +122,15 @@ export function useChartCommands(c: Context) {
|
|
|
122
122
|
? tracks.some((t) => c.independentYDomains.value[t.index])
|
|
123
123
|
: Object.keys(c.sharedYDomains.value).length > 0)
|
|
124
124
|
const boundaryChanged = tracks.length && !same(bounds, boundary(tracks))
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
? '
|
|
129
|
-
:
|
|
130
|
-
? '
|
|
131
|
-
:
|
|
125
|
+
const hasSeries = tracks.some((t) => t.seriesList.length > 0)
|
|
126
|
+
const status: WaveformTargetResult['status'] =
|
|
127
|
+
!hasSeries || (action === 'fit' && !nonempty)
|
|
128
|
+
? 'empty-data'
|
|
129
|
+
: c.props.presentationMode || !c.props.zoomable
|
|
130
|
+
? 'disabled'
|
|
131
|
+
: before && same(before.xDomain, xDomain) && !hasY && !boundaryChanged
|
|
132
|
+
? 'unchanged'
|
|
133
|
+
: 'applied'
|
|
132
134
|
return { id, tracks, before, bounds, xDomain, status, resetsY }
|
|
133
135
|
})
|
|
134
136
|
return { entries }
|
|
@@ -32,7 +32,6 @@ 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'
|
|
36
35
|
import { useWaveformLayout } from './useWaveformLayout'
|
|
37
36
|
import { useWaveformPresentation } from './useWaveformPresentation'
|
|
38
37
|
import { usePlotLabelLayout } from './usePlotLabelLayout'
|
|
@@ -103,7 +102,7 @@ export function useWaveformChartController(
|
|
|
103
102
|
handleBeforeDataReferenceChange()
|
|
104
103
|
},
|
|
105
104
|
() => handleDataReferenceChange(),
|
|
106
|
-
() =>
|
|
105
|
+
() => true,
|
|
107
106
|
)
|
|
108
107
|
|
|
109
108
|
const presentation = useWaveformPresentation({
|
|
@@ -422,7 +421,6 @@ export function useWaveformChartController(
|
|
|
422
421
|
...animations.wrap(commands),
|
|
423
422
|
presentationTracks: animations.presentationTracks,
|
|
424
423
|
presentationAnnotations: animations.presentationAnnotations,
|
|
425
|
-
paginationOpacity: animations.paginationOpacity,
|
|
426
424
|
stabilizeAnimations: animations.stabilizeAnimations,
|
|
427
425
|
...imageExport,
|
|
428
426
|
margin,
|
|
@@ -71,7 +71,19 @@ export function usePreparedWaveformSeries(
|
|
|
71
71
|
)
|
|
72
72
|
watch([data, retainEmpty], ([nextData, preserveEmpty]) => {
|
|
73
73
|
onBeforeDataChange()
|
|
74
|
-
preparedSeries.value
|
|
74
|
+
const previousById = new Map(preparedSeries.value.map((series) => [series.id, series]))
|
|
75
|
+
preparedSeries.value = prepareWaveformSeries(nextData, preserveEmpty).map((series) => {
|
|
76
|
+
const previous = previousById.get(series.id)
|
|
77
|
+
// Preserve axes, never geometry, across empty windows of the same channel/track.
|
|
78
|
+
if (series.source.length || !previous || series.trackId !== previous.trackId) return series
|
|
79
|
+
return {
|
|
80
|
+
...series,
|
|
81
|
+
xDomain: previous.xDomain,
|
|
82
|
+
yDomain: previous.yDomain,
|
|
83
|
+
rawYMaximum: previous.rawYMaximum,
|
|
84
|
+
rawYMinimum: previous.rawYMinimum,
|
|
85
|
+
}
|
|
86
|
+
})
|
|
75
87
|
onDataChange()
|
|
76
88
|
})
|
|
77
89
|
return preparedSeries
|
|
@@ -149,6 +149,9 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
149
149
|
yAxisTickCount,
|
|
150
150
|
})
|
|
151
151
|
const hasWaveformData = computed(() => chartSeries.value.length > 0)
|
|
152
|
+
const hasWaveformSamples = computed(() =>
|
|
153
|
+
chartSeries.value.some((series) => (series.source?.length ?? series.points.length) > 0),
|
|
154
|
+
)
|
|
152
155
|
const hasChartArea = computed(() => innerWidth.value > 0 && innerHeight.value > 0)
|
|
153
156
|
const resolvedXLabel = computed(() => props.xLabel ?? `时间(${props.timeUnit})`)
|
|
154
157
|
const activeInteractionMode = computed(() => props.interactionMode)
|
|
@@ -349,6 +352,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
349
352
|
innerWidth,
|
|
350
353
|
hasVisibleWaveformData,
|
|
351
354
|
hasWaveformData,
|
|
355
|
+
hasWaveformSamples,
|
|
352
356
|
hasChartArea,
|
|
353
357
|
resolvedXLabel,
|
|
354
358
|
activeInteractionMode,
|
|
@@ -56,6 +56,7 @@ function minimumPointSpan(
|
|
|
56
56
|
let minimumSpan = 0
|
|
57
57
|
for (const group of groups) {
|
|
58
58
|
const values = uniqueXValues(group, boundary)
|
|
59
|
+
if (!group.some((series) => (series.source?.length ?? series.points.length) > 0)) continue
|
|
59
60
|
if (values.length < required) return boundary[1] - boundary[0]
|
|
60
61
|
let groupSpan = Number.POSITIVE_INFINITY
|
|
61
62
|
for (let index = 0; index + required <= values.length; index += 1) {
|
|
@@ -104,6 +105,7 @@ function expandToMinimumPoints(
|
|
|
104
105
|
let expanded = domain
|
|
105
106
|
for (const group of groups) {
|
|
106
107
|
const values = uniqueXValues(group, boundary)
|
|
108
|
+
if (!group.some((series) => (series.source?.length ?? series.points.length) > 0)) continue
|
|
107
109
|
if (values.length < required) return [...boundary]
|
|
108
110
|
const visibleCount = values.filter(
|
|
109
111
|
(value) => value >= expanded[0] && value <= expanded[1],
|
|
@@ -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"
|
|
@@ -52,14 +52,19 @@ export function mergeWaveformWindow(
|
|
|
52
52
|
response: WaveformData,
|
|
53
53
|
seriesIds?: string[],
|
|
54
54
|
): WaveformData {
|
|
55
|
-
if (
|
|
56
|
-
const selected = new Set(seriesIds)
|
|
55
|
+
if (current.kind !== 'series' || response.kind !== 'series') return response
|
|
56
|
+
const selected = seriesIds ? new Set(seriesIds) : undefined
|
|
57
57
|
const byId = new Map(response.series.map((series) => [series.id, series]))
|
|
58
|
+
const knownIds = new Set(current.series.map((series) => series.id))
|
|
58
59
|
return {
|
|
59
60
|
kind: 'series',
|
|
60
|
-
series:
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
series: [
|
|
62
|
+
...current.series.map((series) => {
|
|
63
|
+
if (selected && (!series.id || !selected.has(series.id))) return series
|
|
64
|
+
return byId.get(series.id) ?? { ...series, data: { kind: 'points' as const, points: [] } }
|
|
65
|
+
}),
|
|
66
|
+
...response.series.filter((series) => !knownIds.has(series.id)),
|
|
67
|
+
],
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -13,6 +13,7 @@ const enter = ref(true)
|
|
|
13
13
|
const update = ref(true)
|
|
14
14
|
const annotationEnabled = ref(true)
|
|
15
15
|
const effect = ref<AnnotationAnimationType>('scale')
|
|
16
|
+
const gridShape = ref('1x1')
|
|
16
17
|
const direction = ref<LineDrawingDirection>('left-to-right')
|
|
17
18
|
let generation = 0
|
|
18
19
|
function generate(count = 500): WaveformData {
|
|
@@ -55,13 +56,13 @@ function toggleAnnotation() {
|
|
|
55
56
|
<main class="animation-demo">
|
|
56
57
|
<h1>波形动画</h1>
|
|
57
58
|
<p>
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
分别控制入场、更新和标注效果。分页与网格调整重播波形绘制,标注等待对应系列完成,
|
|
60
|
+
坐标轴立即更新。拖动、缩放与导出会提交最终画面。
|
|
60
61
|
</p>
|
|
61
62
|
<div class="animation-demo__controls">
|
|
62
63
|
<label><input v-model="enabled" type="checkbox" /> 动画</label>
|
|
63
|
-
<label><input v-model="enter" type="checkbox" />
|
|
64
|
-
<label><input v-model="update" type="checkbox" />
|
|
64
|
+
<label><input v-model="enter" type="checkbox" /> 入场 / 切页 / 网格</label>
|
|
65
|
+
<label><input v-model="update" type="checkbox" /> 更新</label>
|
|
65
66
|
<label><input v-model="annotationEnabled" type="checkbox" /> 标注</label>
|
|
66
67
|
<label
|
|
67
68
|
>入场方向
|
|
@@ -80,6 +81,14 @@ function toggleAnnotation() {
|
|
|
80
81
|
<option value="bounce">弹跳</option>
|
|
81
82
|
</select></label
|
|
82
83
|
>
|
|
84
|
+
<label
|
|
85
|
+
>网格
|
|
86
|
+
<select v-model="gridShape" aria-label="网格">
|
|
87
|
+
<option value="1x1">1×1</option>
|
|
88
|
+
<option value="1x2">1×2</option>
|
|
89
|
+
<option value="2x2">2×2</option>
|
|
90
|
+
</select>
|
|
91
|
+
</label>
|
|
83
92
|
<button @click="key++">重新入场</button>
|
|
84
93
|
<button @click="replace(true)">更新振幅</button>
|
|
85
94
|
<button @click="replace(false)">改变点数</button>
|
|
@@ -89,7 +98,7 @@ function toggleAnnotation() {
|
|
|
89
98
|
:key="key"
|
|
90
99
|
:data="data"
|
|
91
100
|
:height="360"
|
|
92
|
-
:grid="{ rowCount:
|
|
101
|
+
:grid="{ rowCount: Number(gridShape[0]), columnCount: Number(gridShape[2]) }"
|
|
93
102
|
:animation-enabled="enabled"
|
|
94
103
|
:animation="{ enabled: enter, direction }"
|
|
95
104
|
:animation-update="{ enabled: update }"
|