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.
Files changed (27) hide show
  1. package/README.md +22 -29
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/animation/context.d.ts +1 -0
  6. package/dist/types/components/animation/paginationPresentation.d.ts +7 -7
  7. package/dist/types/components/animation/useWaveformAnimations.d.ts +1 -18
  8. package/dist/types/components/core/useWaveformChartController.d.ts +2 -18
  9. package/dist/types/components/core/useWaveformLayout.d.ts +1 -0
  10. package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +0 -1
  11. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +0 -1
  12. package/package.json +1 -1
  13. package/src/components/WaveformChartView.vue +3 -4
  14. package/src/components/animation/config.ts +1 -1
  15. package/src/components/animation/context.ts +9 -1
  16. package/src/components/animation/paginationPresentation.ts +55 -64
  17. package/src/components/animation/trackPresentation.ts +1 -1
  18. package/src/components/animation/useWaveformAnimations.ts +36 -13
  19. package/src/components/controls/useChartCommands.ts +9 -7
  20. package/src/components/core/useWaveformChartController.ts +1 -3
  21. package/src/components/core/useWaveformData.ts +13 -1
  22. package/src/components/core/useWaveformLayout.ts +4 -0
  23. package/src/components/interaction/zoomConstraints.ts +2 -0
  24. package/src/components/rendering/WaveformSeriesLayer.vue +1 -2
  25. package/src/components/rendering/WaveformTrack.vue +1 -6
  26. package/src/demo/remoteWaveformLoader.ts +10 -5
  27. 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 { animationLayoutSignature, wrapAnimationCommands, type AnimationContext } from './context'
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).map((annotation) => ({
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
- if (pagination.reconcile(dataChanged)) return
166
- if (!effective.value || layoutChanged) {
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 status: WaveformTargetResult['status'] = !nonempty
126
- ? 'empty-data'
127
- : c.props.presentationMode || !c.props.zoomable
128
- ? 'disabled'
129
- : before && same(before.xDomain, xDomain) && !hasY && !boundaryChanged
130
- ? 'unchanged'
131
- : 'applied'
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
- () => Boolean(remotePanBoundary(props)),
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 = prepareWaveformSeries(nextData, preserveEmpty)
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="(seriesPath.opacity ?? 1) * (paginationOpacity ?? 1)"
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 (!seriesIds || current.kind !== 'series' || response.kind !== 'series') return response
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: current.series.map((series) =>
61
- series.id && selected.has(series.id) ? (byId.get(series.id) ?? series) : series,
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" /> 入场</label>
64
- <label><input v-model="update" type="checkbox" /> 更新 / 切页</label>
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: 1, columnCount: 1 }"
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 }"