waveform-analysis 0.1.77 → 0.1.79

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.
@@ -274,6 +274,7 @@ export declare function useWaveformChartController(inputProps: ResolvedWaveformC
274
274
  innerWidth: number;
275
275
  hasVisibleWaveformData: boolean;
276
276
  hasWaveformData: boolean;
277
+ hasWaveformSamples: boolean;
277
278
  hasChartArea: boolean;
278
279
  resolvedXLabel: string;
279
280
  activeInteractionMode: import("..").WaveformInteractionMode | undefined;
@@ -35,6 +35,7 @@ export declare function useWaveformLayout(context: LayoutContext): {
35
35
  innerWidth: ComputedRef<number>;
36
36
  hasVisibleWaveformData: ComputedRef<boolean>;
37
37
  hasWaveformData: ComputedRef<boolean>;
38
+ hasWaveformSamples: ComputedRef<boolean>;
38
39
  hasChartArea: ComputedRef<boolean>;
39
40
  resolvedXLabel: ComputedRef<string>;
40
41
  activeInteractionMode: ComputedRef<import("..").WaveformInteractionMode | undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -46,6 +46,7 @@ const {
46
46
  chartWidth,
47
47
  drawingHeight,
48
48
  hasWaveformData,
49
+ hasWaveformSamples,
49
50
  trackLayouts,
50
51
  presentationTracks,
51
52
  presentationAnnotations,
@@ -186,7 +187,7 @@ function handleChartPointerLeave() {
186
187
  :width="chartWidth"
187
188
  :height="drawingHeight"
188
189
  role="img"
189
- :aria-label="hasWaveformData ? '波形折线图' : '暂无波形数据'"
190
+ :aria-label="hasWaveformSamples ? '波形折线图' : '暂无波形数据'"
190
191
  >
191
192
  <defs>
192
193
  <clipPath
@@ -343,7 +344,7 @@ function handleChartPointerLeave() {
343
344
  </text>
344
345
 
345
346
  <text
346
- v-if="hasChartArea && !hasWaveformData"
347
+ v-if="hasChartArea && !hasWaveformSamples"
347
348
  class="waveform-chart__empty"
348
349
  :x="chartWidth / 2"
349
350
  :y="drawingHeight / 2"
@@ -92,7 +92,7 @@ export function transitionTrack(
92
92
  const series = new Map<string, SeriesGeometry>()
93
93
  for (const [id, next] of to.series) {
94
94
  const previous = from.series.get(id)
95
- if (!previous) {
95
+ if (!previous || (next.path.series.source?.length ?? next.path.series.points.length) === 0) {
96
96
  series.set(id, next)
97
97
  continue
98
98
  }
@@ -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({
@@ -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,
@@ -53,19 +53,22 @@ function minimumPointSpan(
53
53
  required: number,
54
54
  ): number {
55
55
  if (required <= 1) return 0
56
- let minimumSpan = 0
56
+ let minimumSpan = Number.POSITIVE_INFINITY
57
+ let hasData = false
57
58
  for (const group of groups) {
58
59
  const values = uniqueXValues(group, boundary)
59
- if (values.length < required) return boundary[1] - boundary[0]
60
+ if (!group.some((series) => (series.source?.length ?? series.points.length) > 0)) continue
61
+ hasData = true
62
+ if (values.length < required) continue
60
63
  let groupSpan = Number.POSITIVE_INFINITY
61
64
  for (let index = 0; index + required <= values.length; index += 1) {
62
65
  groupSpan = Math.min(groupSpan, values[index + required - 1] - values[index])
63
66
  }
64
67
  const endpointTolerance =
65
68
  Number.EPSILON * Math.max(1, Math.abs(boundary[0]), Math.abs(boundary[1])) * 16
66
- minimumSpan = Math.max(minimumSpan, groupSpan + endpointTolerance)
69
+ minimumSpan = Math.min(minimumSpan, groupSpan + endpointTolerance)
67
70
  }
68
- return minimumSpan
71
+ return Number.isFinite(minimumSpan) ? minimumSpan : hasData ? boundary[1] - boundary[0] : 0
69
72
  }
70
73
 
71
74
  export function resolveMinimumZoomSpan(
@@ -101,24 +104,25 @@ function expandToMinimumPoints(
101
104
  if (required <= 0) return domain
102
105
  const endpointTolerance =
103
106
  Number.EPSILON * Math.max(1, Math.abs(boundary[0]), Math.abs(boundary[1])) * 16
104
- let expanded = domain
107
+ let best: [number, number] | undefined
108
+ let bestSpan = Number.POSITIVE_INFINITY
109
+ let bestCenterDistance = Number.POSITIVE_INFINITY
110
+ let hasData = false
111
+ const requestedCenter = (domain[0] + domain[1]) / 2
105
112
  for (const group of groups) {
106
113
  const values = uniqueXValues(group, boundary)
107
- if (values.length < required) return [...boundary]
108
- const visibleCount = values.filter(
109
- (value) => value >= expanded[0] && value <= expanded[1],
110
- ).length
111
- if (visibleCount >= required) continue
112
- let best: [number, number] | undefined
113
- let bestSpan = Number.POSITIVE_INFINITY
114
- let bestCenterDistance = Number.POSITIVE_INFINITY
115
- const requestedCenter = (expanded[0] + expanded[1]) / 2
114
+ if (!group.some((series) => (series.source?.length ?? series.points.length) > 0)) continue
115
+ hasData = true
116
+ if (values.length < required) continue
117
+ const visibleCount = values.filter((value) => value >= domain[0] && value <= domain[1]).length
118
+ // Shared tracks need one supporting frame, rather than every frame.
119
+ if (visibleCount >= required) return domain
116
120
  for (let index = 0; index + required <= values.length; index += 1) {
117
121
  const candidate: [number, number] = [
118
- Math.max(boundary[0], Math.min(expanded[0], values[index] - endpointTolerance)),
122
+ Math.max(boundary[0], Math.min(domain[0], values[index] - endpointTolerance)),
119
123
  Math.min(
120
124
  boundary[1],
121
- Math.max(expanded[1], values[index + required - 1] + endpointTolerance),
125
+ Math.max(domain[1], values[index + required - 1] + endpointTolerance),
122
126
  ),
123
127
  ]
124
128
  const span = candidate[1] - candidate[0]
@@ -129,9 +133,8 @@ function expandToMinimumPoints(
129
133
  bestCenterDistance = centerDistance
130
134
  }
131
135
  }
132
- expanded = best ?? [...boundary]
133
136
  }
134
- return expanded
137
+ return best ?? (hasData ? [...boundary] : domain)
135
138
  }
136
139
 
137
140
  export function constrainZoomDomain(
@@ -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