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.
- package/README.md +37 -30
- package/dist/index.cjs +1 -1
- package/dist/index.js +423 -430
- package/dist/types/components/core/grid.d.ts +3 -0
- package/dist/types/components/core/types.d.ts +2 -1
- package/dist/types/components/interaction/useWaveformViewport.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/WaveformChartView.vue +2 -2
- package/src/components/core/grid.test.ts +13 -1
- package/src/components/core/grid.ts +14 -4
- package/src/components/core/trackLayoutBuilder.ts +12 -21
- package/src/components/core/types.ts +2 -1
- package/src/components/core/useWaveformLayout.ts +12 -6
- package/src/components/interaction/useWaveformHover.ts +2 -1
- package/src/components/interaction/useWaveformViewport.ts +20 -15
- package/src/components/interaction/useWaveformZoom.ts +1 -1
- package/src/components/rendering/WaveformTrack.vue +1 -0
- package/src/components/rendering/WaveformTrackAxes.vue +3 -2
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +49 -0
- package/src/components/waveformChartCases/trackOrder.test.ts +131 -0
|
@@ -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
|
|
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
|
|
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
|
@@ -222,7 +222,7 @@ function handleChartPointerLeave() {
|
|
|
222
222
|
<!-- 轨道渲染 -->
|
|
223
223
|
<WaveformTrack
|
|
224
224
|
v-for="track in trackLayouts"
|
|
225
|
-
:key="`${track.index}-${track.
|
|
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.
|
|
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({
|
|
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 {
|
|
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 (
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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] ??
|
|
95
|
+
const series = displayTrack.visibleSeries[0] ?? displayTrack.series[0] ?? null
|
|
108
96
|
const baseXScale =
|
|
109
97
|
options.displayMode === 'independent'
|
|
110
|
-
? scaleLinear(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
108
|
-
track
|
|
109
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
})
|