waveform-analysis 0.1.46 → 0.1.47

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.
@@ -7,6 +7,8 @@ export interface WaveformGridOptions {
7
7
  columnCount?: number;
8
8
  showPagination?: boolean;
9
9
  fillIncompleteLastRow?: boolean;
10
+ /** Stable grid slots. Empty IDs remain as empty chart frames and keep their page position. */
11
+ trackOrder?: string[];
10
12
  trackLines?: WaveformGridTrackLines;
11
13
  }
12
14
  export interface WaveformGridLineOptions {
@@ -29,6 +31,7 @@ export interface NormalizedWaveformGridOptions {
29
31
  columnCount: number;
30
32
  showPagination: boolean;
31
33
  fillIncompleteLastRow: boolean;
34
+ trackOrder?: string[];
32
35
  trackLines: Record<string, NormalizedWaveformGridLineOptions>;
33
36
  }
34
37
  export interface GridCellGeometry {
@@ -76,7 +76,8 @@ export interface TrackLayout {
76
76
  /** Stable track key derived from trackId, or from the series id when trackId is omitted. */
77
77
  id: string;
78
78
  index: number;
79
- series: DisplaySeries;
79
+ /** Primary visible series, if this grid slot has waveform data. */
80
+ series: DisplaySeries | null;
80
81
  /** Visible series used by rendering and interaction code. */
81
82
  seriesList: DisplaySeries[];
82
83
  /** Complete series list used by the legend. */
@@ -1,7 +1,8 @@
1
1
  import { type ZoomTransform } from 'd3';
2
2
  import { type ComputedRef, type Ref, type ShallowRef } from 'vue';
3
3
  import type { DisplayTrack, TrackLayout } from '../core/types';
4
- import type { ResolvedWaveformChartProps, ViewportSelectionState, WaveformChartEmit } from '../core/waveformChartTypes';
4
+ import type { ResolvedWaveformChartProps, ViewportSelectionState } from '../core/waveformChartTypes';
5
+ import type { WaveformChartEmit } from '../core/waveformChartTypes';
5
6
  import type { AnnotationSeriesCandidate } from '../annotation';
6
7
  interface ViewportContext {
7
8
  props: ResolvedWaveformChartProps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -222,7 +222,7 @@ function handleChartPointerLeave() {
222
222
  <!-- 轨道渲染 -->
223
223
  <WaveformTrack
224
224
  v-for="track in trackLayouts"
225
- :key="`${track.index}-${track.series.name}`"
225
+ :key="`${track.index}-${track.id}`"
226
226
  :track="track"
227
227
  :clip-path-id="clipPathId"
228
228
  :inner-width="innerWidth"
@@ -277,7 +277,7 @@ function handleChartPointerLeave() {
277
277
  <g v-if="!isCleanView" class="waveform-chart__legend-layer">
278
278
  <g
279
279
  v-for="track in trackLayouts"
280
- :key="`legend-${track.index}-${track.series.name}`"
280
+ :key="`legend-${track.index}-${track.id}`"
281
281
  class="waveform-chart__legend-track"
282
282
  :data-legend-track-index="track.index"
283
283
  :data-legend-track-id="track.id"
@@ -16,6 +16,7 @@ describe('waveform grid helpers', () => {
16
16
  columnCount: 1,
17
17
  showPagination: true,
18
18
  fillIncompleteLastRow: false,
19
+ trackOrder: [],
19
20
  trackLines: {},
20
21
  })
21
22
  expect(normalizeGridOptions({ rowCount: 0, columnCount: 99 })).toEqual({
@@ -23,10 +24,17 @@ describe('waveform grid helpers', () => {
23
24
  columnCount: 10,
24
25
  showPagination: true,
25
26
  fillIncompleteLastRow: false,
27
+ trackOrder: [],
26
28
  trackLines: {},
27
29
  })
28
30
  })
29
31
 
32
+ it('normalizes track order by trimming, dropping blanks, and removing duplicates', () => {
33
+ expect(
34
+ normalizeGridOptions({ trackOrder: [' voltage ', '', 'current', 'voltage', ' '] }),
35
+ ).toMatchObject({ trackOrder: ['voltage', 'current'] })
36
+ })
37
+
30
38
  it('normalizes per-track grid line visibility with visible defaults', () => {
31
39
  expect(
32
40
  normalizeGridOptions({
@@ -170,7 +178,11 @@ describe('waveform grid helpers', () => {
170
178
 
171
179
  it('keeps full pages and the disabled option byte-for-byte compatible', () => {
172
180
  const base = normalizeGridOptions({ rowCount: 2, columnCount: 2 })
173
- const filled = normalizeGridOptions({ rowCount: 2, columnCount: 2, fillIncompleteLastRow: true })
181
+ const filled = normalizeGridOptions({
182
+ rowCount: 2,
183
+ columnCount: 2,
184
+ fillIncompleteLastRow: true,
185
+ })
174
186
  const fullSlots = [true, true, true, true]
175
187
  expect(resolveGridCellGeometry(400, 300, filled, 'independent', fullSlots)).toEqual(
176
188
  resolveGridCellGeometry(400, 300, base, 'independent', fullSlots),
@@ -9,6 +9,8 @@ export interface WaveformGridOptions {
9
9
  columnCount?: number
10
10
  showPagination?: boolean
11
11
  fillIncompleteLastRow?: boolean
12
+ /** Stable grid slots. Empty IDs remain as empty chart frames and keep their page position. */
13
+ trackOrder?: string[]
12
14
  trackLines?: WaveformGridTrackLines
13
15
  }
14
16
 
@@ -35,6 +37,7 @@ export interface NormalizedWaveformGridOptions {
35
37
  columnCount: number
36
38
  showPagination: boolean
37
39
  fillIncompleteLastRow: boolean
40
+ trackOrder?: string[]
38
41
  trackLines: Record<string, NormalizedWaveformGridLineOptions>
39
42
  }
40
43
 
@@ -59,6 +62,15 @@ const normalizeCount = (value: unknown, fallback: number) => {
59
62
  return Math.min(GRID_MAX_COUNT, Math.max(GRID_MIN_COUNT, Math.floor(numeric)))
60
63
  }
61
64
 
65
+ const normalizeTrackOrder = (trackOrder: unknown): string[] => {
66
+ if (!Array.isArray(trackOrder)) return []
67
+ const normalized = trackOrder
68
+ .filter((trackId): trackId is string => typeof trackId === 'string')
69
+ .map((trackId) => trackId.trim())
70
+ .filter(Boolean)
71
+ return Array.from(new Set(normalized))
72
+ }
73
+
62
74
  export function normalizeGridOptions(options?: WaveformGridOptions): NormalizedWaveformGridOptions {
63
75
  const trackLines = Object.fromEntries(
64
76
  Object.entries(options?.trackLines ?? {}).map(([trackId, lines]) => [
@@ -82,6 +94,7 @@ export function normalizeGridOptions(options?: WaveformGridOptions): NormalizedW
82
94
  columnCount: normalizeCount(options?.columnCount, 1),
83
95
  showPagination: options?.showPagination ?? true,
84
96
  fillIncompleteLastRow: options?.fillIncompleteLastRow ?? false,
97
+ trackOrder: normalizeTrackOrder(options?.trackOrder),
85
98
  trackLines,
86
99
  }
87
100
  }
@@ -148,10 +161,7 @@ export function resolveGridCellGeometry(
148
161
  }
149
162
  const totalVerticalGap = Math.max(0, rowCount - 1) * defaultGap
150
163
  const totalAxisBand = axisRows.size * X_AXIS_BAND
151
- const plotHeight = Math.max(
152
- 1,
153
- (innerHeight - totalVerticalGap - totalAxisBand) / rowCount,
154
- )
164
+ const plotHeight = Math.max(1, (innerHeight - totalVerticalGap - totalAxisBand) / rowCount)
155
165
 
156
166
  return Array.from({ length: filledLastRow ? seriesCount : pageSize }, (_, slotIndex) => {
157
167
  const row = Math.floor(slotIndex / options.columnCount)
@@ -26,7 +26,7 @@ import {
26
26
  type NormalizedWaveformGridOptions,
27
27
  } from './grid'
28
28
  import { axisTextMetrics, resolveYAxisSeriesGroups, resolveYAxisTickCount } from './layout'
29
- import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
29
+ import type { DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
30
30
  import { applyXDomainStrategy } from './xDomain'
31
31
  import {
32
32
  Y_AXIS_LABEL_BAND_WIDTH,
@@ -81,33 +81,24 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
81
81
  const bottomCells = getBottomRowCellIndexes(visibleCells, options.grid.columnCount)
82
82
 
83
83
  return visibleCells.flatMap((cell, index) => {
84
- const isEmpty = !cell.series
85
- if (isEmpty && (options.displayMode !== 'compact' || !options.showCompactEmptyTracks)) return []
86
- const emptySeries: DisplaySeries = {
84
+ const isEmpty = !cell.series || cell.series.series.length === 0
85
+ if (!cell.series && (options.displayMode !== 'compact' || !options.showCompactEmptyTracks))
86
+ return []
87
+ const displayTrack: DisplayTrack = cell.series ?? {
87
88
  id: `empty-grid-slot-${cell.slotIndex}`,
88
- name: '',
89
- color: 'transparent',
90
- lineType: 'linear',
91
- lineStyle: 'solid',
92
- pointType: 'none',
93
- errorBar: { visible: false, width: 1.5, capWidth: 8 },
94
- points: [],
89
+ series: [],
90
+ visibleSeries: [],
95
91
  xDomain: [0, 1],
96
92
  yDomain: [0, 1],
97
- hasErrorPoints: false,
98
- }
99
- const displayTrack: DisplayTrack = cell.series ?? {
100
- id: emptySeries.id,
101
- series: [emptySeries],
102
- visibleSeries: [emptySeries],
103
- xDomain: emptySeries.xDomain,
104
- yDomain: emptySeries.yDomain,
105
93
  }
106
94
  const hasVisibleSeries = !isEmpty && displayTrack.visibleSeries.length > 0
107
- const series = displayTrack.visibleSeries[0] ?? displayTrack.series[0] ?? emptySeries
95
+ const series = displayTrack.visibleSeries[0] ?? displayTrack.series[0] ?? null
108
96
  const baseXScale =
109
97
  options.displayMode === 'independent'
110
- ? scaleLinear(resolveIndependentXDomain(displayTrack, series.id, options), [0, cell.width])
98
+ ? scaleLinear(
99
+ resolveIndependentXDomain(displayTrack, series?.id ?? displayTrack.id, options),
100
+ [0, cell.width],
101
+ )
111
102
  : scaleLinear(options.sharedZoomDomain, [0, cell.width])
112
103
  const transform =
113
104
  options.displayMode === 'independent'
@@ -86,7 +86,8 @@ export interface TrackLayout {
86
86
  /** Stable track key derived from trackId, or from the series id when trackId is omitted. */
87
87
  id: string
88
88
  index: number
89
- series: DisplaySeries
89
+ /** Primary visible series, if this grid slot has waveform data. */
90
+ series: DisplaySeries | null
90
91
  /** Visible series used by rendering and interaction code. */
91
92
  seriesList: DisplaySeries[]
92
93
  /** Complete series list used by the legend. */
@@ -75,6 +75,7 @@ export function useWaveformLayout(context: LayoutContext) {
75
75
  (index === 0 ? props.lineColor : channelColors[index % channelColors.length]),
76
76
  })),
77
77
  )
78
+ const gridOptions = computed(() => normalizeGridOptions(props.grid))
78
79
  const chartTracks = computed<DisplayTrack[]>(() => {
79
80
  const groupedSeries = new Map<string, DisplaySeries[]>()
80
81
  chartSeries.value.forEach((series) => {
@@ -83,7 +84,13 @@ export function useWaveformLayout(context: LayoutContext) {
83
84
  if (trackSeries) trackSeries.push(series)
84
85
  else groupedSeries.set(trackId, [series])
85
86
  })
86
- return Array.from(groupedSeries, ([id, series]) => {
87
+ const trackOrder = gridOptions.value.trackOrder ?? []
88
+ const orderedTrackIds = [
89
+ ...trackOrder,
90
+ ...Array.from(groupedSeries.keys()).filter((trackId) => !trackOrder.includes(trackId)),
91
+ ]
92
+ return orderedTrackIds.map((id) => {
93
+ const series = groupedSeries.get(id) ?? []
87
94
  const visibleSeries = series.filter((item) => !hiddenSeriesIdSet.value.has(item.id))
88
95
  const xDomainValues: number[] = []
89
96
  const yDomainValues: number[] = []
@@ -95,12 +102,11 @@ export function useWaveformLayout(context: LayoutContext) {
95
102
  id,
96
103
  series,
97
104
  visibleSeries,
98
- xDomain: paddedDomain(xDomainValues),
99
- yDomain: paddedDomain(yDomainValues),
105
+ xDomain: xDomainValues.length ? paddedDomain(xDomainValues) : [0, 1],
106
+ yDomain: yDomainValues.length ? paddedDomain(yDomainValues) : [0, 1],
100
107
  }
101
108
  })
102
109
  })
103
- const gridOptions = computed(() => normalizeGridOptions(props.grid))
104
110
  const renderingOptions = computed(() => resolveWaveformRenderingOptions(props.rendering))
105
111
  const pageCount = computed(() => getPageCount(chartTracks.value.length, gridOptions.value))
106
112
  const pagedTracks = computed(() =>
@@ -245,8 +251,8 @@ export function useWaveformLayout(context: LayoutContext) {
245
251
  })
246
252
  const resolveInitialTrackDomain = (track: TrackLayout): [number, number] => {
247
253
  const configuredDomain =
248
- props.initialXDomains?.[track.series.trackId ?? track.series.id] ??
249
- props.initialXDomains?.[track.series.id] ??
254
+ props.initialXDomains?.[track.series?.trackId ?? track.series?.id ?? track.id] ??
255
+ (track.series ? props.initialXDomains?.[track.series.id] : undefined) ??
250
256
  props.initialXDomain
251
257
  if (
252
258
  configuredDomain &&
@@ -153,7 +153,8 @@ export function useWaveformHover(context: HoverContext) {
153
153
  scheduleHover(() => {
154
154
  const resolvedTrack = resolveTrackAtPointer(pointerX, pointerY)
155
155
  const referenceTrack =
156
- resolvedTrack ?? trackLayouts.value.find((track) => track.hasVisibleSeries)
156
+ (resolvedTrack?.hasVisibleSeries ? resolvedTrack : undefined) ??
157
+ trackLayouts.value.find((track) => track.hasVisibleSeries)
157
158
  if (!referenceTrack) return
158
159
  const localPointerX = Math.max(
159
160
  0,
@@ -3,11 +3,8 @@ import { computed, nextTick, shallowRef, type ComputedRef, type Ref, type Shallo
3
3
  import { MINIMUM_SELECTION_SIZE } from '../core/constants'
4
4
  import type { DisplayTrack, TrackLayout } from '../core/types'
5
5
  import { hasFixedYDomainForTrack } from '../core/yDomain'
6
- import type {
7
- ResolvedWaveformChartProps,
8
- ViewportSelectionState,
9
- WaveformChartEmit,
10
- } from '../core/waveformChartTypes'
6
+ import type { ResolvedWaveformChartProps, ViewportSelectionState } from '../core/waveformChartTypes'
7
+ import type { WaveformChartEmit } from '../core/waveformChartTypes'
11
8
  import type { AnnotationSeriesCandidate } from '../annotation'
12
9
  import { tryReleasePointerCapture } from './pointerCapture'
13
10
  import { transitionViewportInteraction } from './viewportInteractionState'
@@ -104,10 +101,12 @@ export function useWaveformViewport(context: ViewportContext) {
104
101
  }
105
102
  const currentYDomains = (): Record<string, [number, number]> =>
106
103
  Object.fromEntries(
107
- trackLayouts.value.map((track) => [
108
- track.series.trackId ?? track.series.id,
109
- track.yScale.domain() as [number, number],
110
- ]),
104
+ trackLayouts.value
105
+ .filter((track) => track.hasVisibleSeries)
106
+ .map((track) => [
107
+ track.series?.trackId ?? track.series?.id ?? track.id,
108
+ track.yScale.domain() as [number, number],
109
+ ]),
111
110
  )
112
111
  const beginViewportDrag = (event: PointerEvent, trackIndex: number, independent: boolean) => {
113
112
  if (isPresentationMode.value) return
@@ -142,8 +141,10 @@ export function useWaveformViewport(context: ViewportContext) {
142
141
  const beginSharedViewportDrag = (event: PointerEvent) => {
143
142
  if (!sharedOverlayElement.value) return
144
143
  const [x, y] = pointer(event, sharedOverlayElement.value)
144
+ const pointedTrack = resolveTrackAtPointer(x, y)
145
145
  const track =
146
- resolveTrackAtPointer(x, y) ?? trackLayouts.value.find((item) => item.hasVisibleSeries)
146
+ (pointedTrack?.hasVisibleSeries ? pointedTrack : undefined) ??
147
+ trackLayouts.value.find((item) => item.hasVisibleSeries)
147
148
  if (track) beginViewportDrag(event, track.index, false)
148
149
  }
149
150
  const applyPan = (active: ViewportSelectionState, track: TrackLayout) => {
@@ -166,7 +167,9 @@ export function useWaveformViewport(context: ViewportContext) {
166
167
  } else {
167
168
  sharedTransform.value = transformForDomain(nextX, sourceXDomain, innerWidth.value)
168
169
  }
169
- const targets = active.independent ? [track] : trackLayouts.value
170
+ const targets = active.independent
171
+ ? [track]
172
+ : trackLayouts.value.filter((target) => target.hasVisibleSeries)
170
173
  const nextIndependentDomains = { ...independentYDomains.value }
171
174
  const nextSharedDomains = { ...sharedYDomains.value }
172
175
  targets.forEach((target) => {
@@ -174,7 +177,7 @@ export function useWaveformViewport(context: ViewportContext) {
174
177
  if (chartTrack && hasFixedYDomainForTrack(chartTrack, props.yDomain, props.yDomains)) {
175
178
  return
176
179
  }
177
- const key = target.series.trackId ?? target.series.id
180
+ const key = target.series?.trackId ?? target.series?.id ?? target.id
178
181
  const source = active.yDomains[key] ?? (target.yScale.domain() as [number, number])
179
182
  const boundary = chartTrack?.yDomain ?? source
180
183
  const ySpan = source[1] - source[0]
@@ -251,16 +254,18 @@ export function useWaveformViewport(context: ViewportContext) {
251
254
  } else {
252
255
  sharedTransform.value = transformForDomain(xDomain, baseXDomain, innerWidth.value)
253
256
  }
254
- const targets = active.independent ? [track] : trackLayouts.value
257
+ const targets = active.independent
258
+ ? [track]
259
+ : trackLayouts.value.filter((target) => target.hasVisibleSeries)
255
260
  const yRanges = Object.fromEntries(
256
261
  targets.map((target) => [
257
- target.series.trackId ?? target.series.id,
262
+ target.series?.trackId ?? target.series?.id ?? target.id,
258
263
  target.yScale.domain() as [number, number],
259
264
  ]),
260
265
  )
261
266
  emit('zoom-change', xDomain)
262
267
  if (active.independent) {
263
- const yDomain = yRanges[track.series.trackId ?? track.series.id]
268
+ const yDomain = yRanges[track.series?.trackId ?? track.series?.id ?? track.id]
264
269
  emit('zoom-end', {
265
270
  start: xDomain[0],
266
271
  end: xDomain[1],
@@ -177,7 +177,7 @@ export function useWaveformZoom(context: ZoomContext) {
177
177
  end: domain[1],
178
178
  yRanges: Object.fromEntries(
179
179
  visibleTracks.map((track) => [
180
- track.series.trackId ?? track.series.id,
180
+ track.series?.trackId ?? track.series?.id ?? track.id,
181
181
  track.yScale.domain() as [number, number],
182
182
  ]),
183
183
  ),
@@ -67,6 +67,7 @@ const resolvedFrameStyle = computed(() => {
67
67
  class="waveform-track waveform-chart__track"
68
68
  :class="{ 'waveform-track--empty waveform-chart__track--empty': track.isEmpty }"
69
69
  :data-track-index="track.index"
70
+ :data-track-id="track.id"
70
71
  :data-track-empty="track.isEmpty || undefined"
71
72
  :data-track-left="track.left"
72
73
  :data-track-width="track.width"
@@ -164,6 +164,7 @@ watch(
164
164
  track.hasVisibleSeries &&
165
165
  track.seriesList.length === 1 &&
166
166
  track.showYAxisLabel &&
167
+ track.series &&
167
168
  resolveYAxisLabel(track.series) &&
168
169
  shouldShowYAxisLabel(track.height, track.index)
169
170
  "
@@ -178,12 +179,12 @@ watch(
178
179
  />
179
180
  <text
180
181
  class="waveform-track__y-axis-label waveform-chart__y-axis-label"
181
- :fill="track.series.color"
182
+ :fill="track.series?.color"
182
183
  :transform="`translate(${track.yAxisLabelX}, ${track.height / 2}) rotate(-90)`"
183
184
  text-anchor="middle"
184
185
  dominant-baseline="central"
185
186
  >
186
- {{ resolveYAxisLabel(track.series) }}
187
+ {{ track.series ? resolveYAxisLabel(track.series) : '' }}
187
188
  </text>
188
189
  </g>
189
190
 
@@ -144,6 +144,55 @@ describe('WaveformChart', () => {
144
144
  expect(wrapper.get('.ant-pagination-next').classes()).toContain('ant-pagination-disabled')
145
145
  })
146
146
 
147
+ it('keeps ordered empty tracks in their page slots after channels are merged', async () => {
148
+ const wrapper = await mountSizedChart(
149
+ {
150
+ kind: 'series',
151
+ series: ['frame-1', 'frame-1', 'frame-3', 'frame-4', 'frame-5', 'frame-6'].map(
152
+ (trackId, index) => ({
153
+ id: `channel-${index + 1}`,
154
+ trackId,
155
+ name: `channel-${index + 1}`,
156
+ data: {
157
+ kind: 'points' as const,
158
+ points: [
159
+ { x: 0, y: index },
160
+ { x: 1, y: index + 1 },
161
+ ],
162
+ },
163
+ }),
164
+ ),
165
+ },
166
+ {
167
+ grid: {
168
+ rowCount: 2,
169
+ columnCount: 1,
170
+ trackOrder: ['frame-1', 'frame-2', 'frame-3', 'frame-4', 'frame-5', 'frame-6'],
171
+ },
172
+ },
173
+ )
174
+ const trackIds = () =>
175
+ wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id'))
176
+ const lineIds = () =>
177
+ wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id'))
178
+ const nextButton = () => wrapper.get('.ant-pagination-next button')
179
+
180
+ expect(trackIds()).toEqual(['frame-1', 'frame-2'])
181
+ expect(wrapper.findAll('.waveform-chart__track--empty')).toHaveLength(1)
182
+ expect(lineIds()).toEqual(['channel-1', 'channel-2'])
183
+ expect(wrapper.findAll('.waveform-chart__legend')).toHaveLength(1)
184
+
185
+ await nextButton().trigger('click')
186
+ expect(trackIds()).toEqual(['frame-3', 'frame-4'])
187
+ expect(lineIds()).toEqual(['channel-3', 'channel-4'])
188
+ expect(wrapper.emitted('page-change')?.at(-1)).toEqual([2, 3])
189
+
190
+ await nextButton().trigger('click')
191
+ expect(trackIds()).toEqual(['frame-5', 'frame-6'])
192
+ expect(lineIds()).toEqual(['channel-5', 'channel-6'])
193
+ expect(wrapper.emitted('page-change')?.at(-1)).toEqual([3, 3])
194
+ })
195
+
147
196
  it('overlays series with the same track ID without changing the next frame', async () => {
148
197
  const wrapper = await mountSizedChart(
149
198
  {
@@ -0,0 +1,131 @@
1
+ import { flushPromises } from '@vue/test-utils'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { flushAnimationFrames } from '../../test/setup'
5
+ import { mountSizedChart } from '../../test/waveformChart'
6
+
7
+ describe('WaveformChart track order', () => {
8
+ it('keeps a merged source slot empty without adding legend or tooltip data', async () => {
9
+ const wrapper = await mountSizedChart(
10
+ {
11
+ kind: 'series',
12
+ series: [
13
+ {
14
+ id: 'frame-1-a',
15
+ trackId: 'frame-1',
16
+ name: '合并前通道',
17
+ data: {
18
+ kind: 'points',
19
+ points: [
20
+ { x: 0, y: 1 },
21
+ { x: 1, y: 2 },
22
+ ],
23
+ },
24
+ },
25
+ {
26
+ id: 'frame-1-b',
27
+ trackId: 'frame-1',
28
+ name: '合并后通道',
29
+ data: {
30
+ kind: 'points',
31
+ points: [
32
+ { x: 0, y: 3 },
33
+ { x: 1, y: 4 },
34
+ ],
35
+ },
36
+ },
37
+ {
38
+ id: 'frame-3',
39
+ trackId: 'frame-3',
40
+ name: '后续通道',
41
+ data: {
42
+ kind: 'points',
43
+ points: [
44
+ { x: 0, y: 5 },
45
+ { x: 1, y: 6 },
46
+ ],
47
+ },
48
+ },
49
+ ],
50
+ },
51
+ {
52
+ displayMode: 'separated',
53
+ grid: { rowCount: 3, trackOrder: ['frame-1', 'frame-2', 'frame-3'] },
54
+ },
55
+ )
56
+ const trackIds = () =>
57
+ wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id'))
58
+
59
+ expect(trackIds()).toEqual(['frame-1', 'frame-2', 'frame-3'])
60
+ expect(wrapper.findAll('.waveform-chart__track--empty')).toHaveLength(1)
61
+ expect(
62
+ wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
63
+ ).toEqual(['frame-1-a', 'frame-1-b', 'frame-3'])
64
+ expect(wrapper.findAll('.waveform-chart__legend')).toHaveLength(1)
65
+
66
+ const overlay = wrapper.get('.waveform-chart__overlay--shared')
67
+ Object.defineProperty(overlay.element, 'getBoundingClientRect', {
68
+ value: () => ({ left: 0, top: 0, width: 712, height: 290 }),
69
+ })
70
+ overlay.element.dispatchEvent(
71
+ new MouseEvent('pointermove', { clientX: 356, clientY: 150, bubbles: true }),
72
+ )
73
+ flushAnimationFrames()
74
+ await flushPromises()
75
+
76
+ expect(wrapper.findAll('.waveform-chart__tooltip-series')).toHaveLength(3)
77
+ expect(wrapper.get('.waveform-chart__tooltip').text()).not.toContain('frame-2')
78
+ expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(2)
79
+ })
80
+
81
+ it('appends data tracks absent from trackOrder in input order', async () => {
82
+ const wrapper = await mountSizedChart(
83
+ {
84
+ kind: 'series',
85
+ series: [
86
+ {
87
+ id: 'later-a',
88
+ trackId: 'later-a',
89
+ name: 'later-a',
90
+ data: {
91
+ kind: 'points',
92
+ points: [
93
+ { x: 0, y: 1 },
94
+ { x: 1, y: 2 },
95
+ ],
96
+ },
97
+ },
98
+ {
99
+ id: 'frame-1',
100
+ trackId: 'frame-1',
101
+ name: 'frame-1',
102
+ data: {
103
+ kind: 'points',
104
+ points: [
105
+ { x: 0, y: 3 },
106
+ { x: 1, y: 4 },
107
+ ],
108
+ },
109
+ },
110
+ {
111
+ id: 'later-b',
112
+ trackId: 'later-b',
113
+ name: 'later-b',
114
+ data: {
115
+ kind: 'points',
116
+ points: [
117
+ { x: 0, y: 5 },
118
+ { x: 1, y: 6 },
119
+ ],
120
+ },
121
+ },
122
+ ],
123
+ },
124
+ { grid: { rowCount: 2, columnCount: 2, trackOrder: ['frame-2', 'frame-1'] } },
125
+ )
126
+
127
+ expect(
128
+ wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id')),
129
+ ).toEqual(['frame-2', 'frame-1', 'later-a', 'later-b'])
130
+ })
131
+ })