waveform-analysis 0.1.55 → 0.1.58
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 +3 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +565 -539
- package/dist/types/components/core/grid.d.ts +3 -0
- package/dist/types/components/core/layout.d.ts +18 -0
- package/dist/types/components/core/trackLayoutBuilder.d.ts +2 -0
- package/dist/types/components/core/trackPagination.d.ts +4 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +3 -1
- package/dist/types/components/core/useWaveformLayout.d.ts +2 -1
- package/dist/types/components/core/waveformChartTypes.d.ts +2 -0
- package/dist/types/components/core/yAxisConstants.d.ts +1 -0
- package/package.json +1 -1
- package/src/App.test.ts +2 -1
- package/src/components/WaveformChartView.vue +3 -3
- package/src/components/core/grid.test.ts +3 -0
- package/src/components/core/grid.ts +4 -0
- package/src/components/core/layout.test.ts +1 -1
- package/src/components/core/layout.ts +63 -0
- package/src/components/core/trackLayoutBuilder.ts +24 -4
- package/src/components/core/trackPagination.ts +40 -0
- package/src/components/core/useWaveformLayout.ts +28 -31
- package/src/components/core/waveformChartTypes.ts +2 -0
- package/src/components/core/yAxisConstants.ts +1 -0
- package/src/components/core/yAxisSlotLayout.test.ts +129 -0
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +3 -5
- package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +61 -0
- package/src/components/waveformChartCases/trackOrder.test.ts +62 -0
- package/src/data/simulatedWaveforms.test.ts +4 -4
- package/src/data/simulatedWaveforms.ts +12 -0
|
@@ -9,6 +9,8 @@ export interface WaveformGridOptions {
|
|
|
9
9
|
fillIncompleteLastRow?: boolean;
|
|
10
10
|
/** Stable grid slots. Empty IDs remain as empty chart frames and keep their page position. */
|
|
11
11
|
trackOrder?: string[];
|
|
12
|
+
/** Omits ordered tracks without waveform series from layout and pagination. */
|
|
13
|
+
hideEmptyTracks?: boolean;
|
|
12
14
|
trackLines?: WaveformGridTrackLines;
|
|
13
15
|
}
|
|
14
16
|
export interface WaveformGridLineOptions {
|
|
@@ -32,6 +34,7 @@ export interface NormalizedWaveformGridOptions {
|
|
|
32
34
|
showPagination: boolean;
|
|
33
35
|
fillIncompleteLastRow: boolean;
|
|
34
36
|
trackOrder?: string[];
|
|
37
|
+
hideEmptyTracks?: boolean;
|
|
35
38
|
trackLines: Record<string, NormalizedWaveformGridLineOptions>;
|
|
36
39
|
}
|
|
37
40
|
export interface GridCellGeometry {
|
|
@@ -10,6 +10,13 @@ export interface YAxisSeriesGroup {
|
|
|
10
10
|
domain: [number, number];
|
|
11
11
|
fixed: boolean;
|
|
12
12
|
}
|
|
13
|
+
export interface YAxisSlot {
|
|
14
|
+
side: 'left' | 'right';
|
|
15
|
+
sideIndex: number;
|
|
16
|
+
axisOffset: number;
|
|
17
|
+
labelOffset: number;
|
|
18
|
+
clearance: number;
|
|
19
|
+
}
|
|
13
20
|
export declare function buildYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode): YAxisSeriesGroup[];
|
|
14
21
|
export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): YAxisSeriesGroup[];
|
|
15
22
|
export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number): {
|
|
@@ -20,6 +27,17 @@ export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayM
|
|
|
20
27
|
left: number;
|
|
21
28
|
right: number;
|
|
22
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Reserves stable Y-axis positions for every visible track on the current page.
|
|
32
|
+
* Slots are ordered from the plot edge outward for each side.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildYAxisSlots(tracks: readonly DisplayTrack[], overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number, nice?: boolean): {
|
|
35
|
+
slots: YAxisSlot[];
|
|
36
|
+
clearance: {
|
|
37
|
+
left: number;
|
|
38
|
+
right: number;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
23
41
|
type PositionedTrack = Pick<TrackLayout, 'left' | 'top' | 'width' | 'height'>;
|
|
24
42
|
export declare function findClosestTrackAtPointer<T extends PositionedTrack>(tracks: readonly T[], pointerX: number, pointerY: number): T | undefined;
|
|
25
43
|
export { buildTrackLayouts, type BuildTrackLayoutsOptions } from './trackLayoutBuilder';
|
|
@@ -2,6 +2,7 @@ import { type ZoomTransform } from 'd3';
|
|
|
2
2
|
import { type ResolvedWaveformRenderingOptions } from '../../core/rendering';
|
|
3
3
|
import type { WaveformDisplayMode, WaveformOverlayMode, WaveformPoint, WaveformXDomainStrategy, WaveformXAxisLabelFormatter } from '../../types';
|
|
4
4
|
import { type GridCellGeometry, type NormalizedWaveformGridOptions } from './grid';
|
|
5
|
+
import { type YAxisSlot } from './layout';
|
|
5
6
|
import type { DisplayTrack, TrackLayout } from './types';
|
|
6
7
|
interface SeriesGridCell extends GridCellGeometry {
|
|
7
8
|
series?: DisplayTrack;
|
|
@@ -28,6 +29,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
28
29
|
linePointOverrides?: Readonly<Record<string, WaveformPoint[]>>;
|
|
29
30
|
hideSecondaryLabels: boolean;
|
|
30
31
|
yAxisLabelX: number;
|
|
32
|
+
yAxisSlots?: readonly YAxisSlot[];
|
|
31
33
|
showCompactEmptyTracks: boolean;
|
|
32
34
|
}
|
|
33
35
|
export declare function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayout[];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DisplayTrack } from './types';
|
|
2
|
+
export declare const resolvePageableTracks: (tracks: DisplayTrack[], hideEmptyTracks?: boolean) => DisplayTrack[];
|
|
3
|
+
export declare const resolveTrackFrameNumber: (frameNumber: string | number | undefined, frameNumbers: Record<string, string | number> | undefined, trackCount: number, trackIndex: number, trackId: string) => string | number | undefined;
|
|
4
|
+
export declare const createFrameNumberResolver: (getFrameNumber: () => string | number | undefined, getFrameNumbers: () => Record<string, string | number> | undefined, getTracks: () => DisplayTrack[]) => (trackId: string) => string | number | undefined;
|
|
@@ -246,6 +246,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
|
|
|
246
246
|
confirmAnnotation: (annotation: import("..").WaveformAnnotation) => void;
|
|
247
247
|
chartSeries: import("./types").DisplaySeries[];
|
|
248
248
|
chartTracks: import("./types").DisplayTrack[];
|
|
249
|
+
pageableTracks: import("./types").DisplayTrack[];
|
|
249
250
|
renderingOptions: import("../..").ResolvedWaveformRenderingOptions;
|
|
250
251
|
gridOptions: import("./grid").NormalizedWaveformGridOptions;
|
|
251
252
|
pageCount: number;
|
|
@@ -281,7 +282,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
|
|
|
281
282
|
annotationTrackLayouts: import("../annotation").AnnotationTrackLayout[];
|
|
282
283
|
renderedAnnotations: import("../annotation").RenderedAnnotation[];
|
|
283
284
|
editorSeries: import("../annotation").AnnotationSeriesInfo | undefined;
|
|
284
|
-
resolveFrameNumber: (
|
|
285
|
+
resolveFrameNumber: (trackId: string) => string | number | undefined;
|
|
285
286
|
fixedWidth: number | undefined;
|
|
286
287
|
fixedHeight: number | undefined;
|
|
287
288
|
chartWidth: number;
|
|
@@ -341,6 +342,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
|
|
|
341
342
|
yDomains?: Record<string, [number, number]> | undefined;
|
|
342
343
|
timeUnit: "s" | "ms";
|
|
343
344
|
frameNumber?: string | number | undefined;
|
|
345
|
+
frameNumbers?: Record<string, string | number> | undefined;
|
|
344
346
|
frameStyle?: import("..").WaveformFrameStyle | undefined;
|
|
345
347
|
axes?: import("..").WaveformAxesOptions | undefined;
|
|
346
348
|
annotations: import("..").WaveformAnnotation[];
|
|
@@ -23,6 +23,7 @@ interface LayoutContext {
|
|
|
23
23
|
export declare function useWaveformLayout(context: LayoutContext): {
|
|
24
24
|
chartSeries: ComputedRef<DisplaySeries[]>;
|
|
25
25
|
chartTracks: ComputedRef<DisplayTrack[]>;
|
|
26
|
+
pageableTracks: ComputedRef<DisplayTrack[]>;
|
|
26
27
|
renderingOptions: ComputedRef<import("../..").ResolvedWaveformRenderingOptions>;
|
|
27
28
|
gridOptions: ComputedRef<import("./grid").NormalizedWaveformGridOptions>;
|
|
28
29
|
pageCount: ComputedRef<number>;
|
|
@@ -58,6 +59,6 @@ export declare function useWaveformLayout(context: LayoutContext): {
|
|
|
58
59
|
annotationTrackLayouts: ComputedRef<AnnotationTrackLayout[]>;
|
|
59
60
|
renderedAnnotations: ComputedRef<import("../annotation").RenderedAnnotation[]>;
|
|
60
61
|
editorSeries: ComputedRef<AnnotationSeriesInfo | undefined>;
|
|
61
|
-
resolveFrameNumber: (
|
|
62
|
+
resolveFrameNumber: (trackId: string) => string | number | undefined;
|
|
62
63
|
};
|
|
63
64
|
export {};
|
|
@@ -22,6 +22,8 @@ export interface WaveformChartProps {
|
|
|
22
22
|
yDomains?: Record<string, [number, number]>;
|
|
23
23
|
timeUnit?: 's' | 'ms';
|
|
24
24
|
frameNumber?: string | number;
|
|
25
|
+
/** Per-track frame watermark overrides, keyed by stable trackId. */
|
|
26
|
+
frameNumbers?: Record<string, string | number>;
|
|
25
27
|
frameStyle?: WaveformFrameStyle;
|
|
26
28
|
axes?: WaveformAxesOptions;
|
|
27
29
|
annotations?: WaveformAnnotation[];
|
|
@@ -3,4 +3,5 @@ export declare const Y_AXIS_TICK_PADDING = 7;
|
|
|
3
3
|
export declare const Y_AXIS_OUTER_PADDING = 4;
|
|
4
4
|
export declare const Y_AXIS_LABEL_GAP = 0;
|
|
5
5
|
export declare const Y_AXIS_LABEL_BAND_WIDTH = 12;
|
|
6
|
+
export declare const Y_AXIS_RIGHT_LABEL_OFFSET = 10;
|
|
6
7
|
export declare function resolveYAxisSides(axisCount: number): Array<'left' | 'right'>;
|
package/package.json
CHANGED
package/src/App.test.ts
CHANGED
|
@@ -231,7 +231,7 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
|
|
|
231
231
|
tracks.map((track) =>
|
|
232
232
|
track.findAll('.waveform-chart__series').map((item) => item.attributes('data-series-name')),
|
|
233
233
|
),
|
|
234
|
-
).toEqual([['正弦基波'], ['谐波扰动', '谐波对比'], ['阻尼振荡'], ['阶跃响应']])
|
|
234
|
+
).toEqual([['正弦基波'], ['谐波扰动', '谐波对比', '谐波校正'], ['阻尼振荡'], ['阶跃响应']])
|
|
235
235
|
expect(
|
|
236
236
|
tracks
|
|
237
237
|
.slice(1)
|
|
@@ -250,6 +250,7 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
|
|
|
250
250
|
'正弦基波',
|
|
251
251
|
'谐波扰动',
|
|
252
252
|
'谐波对比',
|
|
253
|
+
'谐波校正',
|
|
253
254
|
'阻尼振荡',
|
|
254
255
|
'阶跃响应',
|
|
255
256
|
'脉冲响应',
|
|
@@ -80,7 +80,7 @@ const {
|
|
|
80
80
|
pageCount,
|
|
81
81
|
currentPage,
|
|
82
82
|
getPageSize,
|
|
83
|
-
|
|
83
|
+
pageableTracks,
|
|
84
84
|
goToPage,
|
|
85
85
|
annotationInteraction,
|
|
86
86
|
editorDraft,
|
|
@@ -230,7 +230,7 @@ function handleChartPointerLeave() {
|
|
|
230
230
|
:interactive="!isPresentationMode"
|
|
231
231
|
:display-mode="displayMode"
|
|
232
232
|
:interaction-mode="activeInteractionMode"
|
|
233
|
-
:frame-number="resolveFrameNumber(track.
|
|
233
|
+
:frame-number="resolveFrameNumber(track.id)"
|
|
234
234
|
:frame-style="frameStyle"
|
|
235
235
|
:axes="axes"
|
|
236
236
|
:clean-view="isCleanView"
|
|
@@ -326,7 +326,7 @@ function handleChartPointerLeave() {
|
|
|
326
326
|
aria-label="波形分页"
|
|
327
327
|
:current="currentPage"
|
|
328
328
|
:page-size="getPageSize(gridOptions)"
|
|
329
|
-
:total="
|
|
329
|
+
:total="pageableTracks.length"
|
|
330
330
|
:show-size-changer="false"
|
|
331
331
|
:show-quick-jumper="false"
|
|
332
332
|
@change="goToPage"
|
|
@@ -17,6 +17,7 @@ describe('waveform grid helpers', () => {
|
|
|
17
17
|
showPagination: true,
|
|
18
18
|
fillIncompleteLastRow: false,
|
|
19
19
|
trackOrder: [],
|
|
20
|
+
hideEmptyTracks: false,
|
|
20
21
|
trackLines: {},
|
|
21
22
|
})
|
|
22
23
|
expect(normalizeGridOptions({ rowCount: 0, columnCount: 99 })).toEqual({
|
|
@@ -25,8 +26,10 @@ describe('waveform grid helpers', () => {
|
|
|
25
26
|
showPagination: true,
|
|
26
27
|
fillIncompleteLastRow: false,
|
|
27
28
|
trackOrder: [],
|
|
29
|
+
hideEmptyTracks: false,
|
|
28
30
|
trackLines: {},
|
|
29
31
|
})
|
|
32
|
+
expect(normalizeGridOptions({ hideEmptyTracks: true }).hideEmptyTracks).toBe(true)
|
|
30
33
|
})
|
|
31
34
|
|
|
32
35
|
it('normalizes track order by trimming, dropping blanks, and removing duplicates', () => {
|
|
@@ -11,6 +11,8 @@ export interface WaveformGridOptions {
|
|
|
11
11
|
fillIncompleteLastRow?: boolean
|
|
12
12
|
/** Stable grid slots. Empty IDs remain as empty chart frames and keep their page position. */
|
|
13
13
|
trackOrder?: string[]
|
|
14
|
+
/** Omits ordered tracks without waveform series from layout and pagination. */
|
|
15
|
+
hideEmptyTracks?: boolean
|
|
14
16
|
trackLines?: WaveformGridTrackLines
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -38,6 +40,7 @@ export interface NormalizedWaveformGridOptions {
|
|
|
38
40
|
showPagination: boolean
|
|
39
41
|
fillIncompleteLastRow: boolean
|
|
40
42
|
trackOrder?: string[]
|
|
43
|
+
hideEmptyTracks?: boolean
|
|
41
44
|
trackLines: Record<string, NormalizedWaveformGridLineOptions>
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -95,6 +98,7 @@ export function normalizeGridOptions(options?: WaveformGridOptions): NormalizedW
|
|
|
95
98
|
showPagination: options?.showPagination ?? true,
|
|
96
99
|
fillIncompleteLastRow: options?.fillIncompleteLastRow ?? false,
|
|
97
100
|
trackOrder: normalizeTrackOrder(options?.trackOrder),
|
|
101
|
+
hideEmptyTracks: options?.hideEmptyTracks ?? false,
|
|
98
102
|
trackLines,
|
|
99
103
|
}
|
|
100
104
|
}
|
|
@@ -35,6 +35,14 @@ export interface YAxisSeriesGroup {
|
|
|
35
35
|
fixed: boolean
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface YAxisSlot {
|
|
39
|
+
side: 'left' | 'right'
|
|
40
|
+
sideIndex: number
|
|
41
|
+
axisOffset: number
|
|
42
|
+
labelOffset: number
|
|
43
|
+
clearance: number
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
function resolveAxisSides(axisCount: number): Array<'left' | 'right'> {
|
|
39
47
|
if (axisCount >= 4) return ['left', 'left', 'right', 'right']
|
|
40
48
|
if (axisCount === 3) return ['left', 'right', 'right']
|
|
@@ -181,6 +189,61 @@ export function measureTrackYAxisClearance(
|
|
|
181
189
|
)
|
|
182
190
|
}
|
|
183
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Reserves stable Y-axis positions for every visible track on the current page.
|
|
194
|
+
* Slots are ordered from the plot edge outward for each side.
|
|
195
|
+
*/
|
|
196
|
+
export function buildYAxisSlots(
|
|
197
|
+
tracks: readonly DisplayTrack[],
|
|
198
|
+
overlayMode: WaveformOverlayMode,
|
|
199
|
+
yDomain?: WaveformYDomain,
|
|
200
|
+
yDomains?: Record<string, WaveformYDomain>,
|
|
201
|
+
tickCount?: number,
|
|
202
|
+
nice = true,
|
|
203
|
+
): { slots: YAxisSlot[]; clearance: { left: number; right: number } } {
|
|
204
|
+
const widths = new Map<string, number>()
|
|
205
|
+
tracks.forEach((track) => {
|
|
206
|
+
const sideIndexes = { left: 0, right: 0 }
|
|
207
|
+
resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).forEach((group) => {
|
|
208
|
+
const sideIndex = sideIndexes[group.side]++
|
|
209
|
+
const key = `${group.side}:${sideIndex}`
|
|
210
|
+
const width = axisTextMetrics(
|
|
211
|
+
group.domain,
|
|
212
|
+
nice,
|
|
213
|
+
undefined,
|
|
214
|
+
group.seriesList[0]?.unit,
|
|
215
|
+
tickCount,
|
|
216
|
+
).tickTextWidth
|
|
217
|
+
widths.set(key, Math.max(widths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, width))
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const slots: YAxisSlot[] = []
|
|
222
|
+
const clearance = { left: 0, right: 0 }
|
|
223
|
+
;(['left', 'right'] as const).forEach((side) => {
|
|
224
|
+
const sideIndexes = Array.from(widths.keys())
|
|
225
|
+
.filter((key) => key.startsWith(`${side}:`))
|
|
226
|
+
.map((key) => Number(key.slice(side.length + 1)))
|
|
227
|
+
.sort((first, second) => first - second)
|
|
228
|
+
sideIndexes.forEach((sideIndex) => {
|
|
229
|
+
const tickTextWidth = widths.get(`${side}:${sideIndex}`) ?? Y_AXIS_CHARACTER_WIDTH
|
|
230
|
+
const axisOffset = side === 'left' && clearance.left > 0 ? -clearance.left : clearance[side]
|
|
231
|
+
const labelDistance =
|
|
232
|
+
tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
233
|
+
const labelOffset = axisOffset + labelDistance * (side === 'left' ? -1 : 1)
|
|
234
|
+
const fullClearance =
|
|
235
|
+
tickTextWidth +
|
|
236
|
+
Y_AXIS_TICK_PADDING +
|
|
237
|
+
Y_AXIS_LABEL_GAP +
|
|
238
|
+
Y_AXIS_LABEL_BAND_WIDTH +
|
|
239
|
+
Y_AXIS_OUTER_PADDING
|
|
240
|
+
slots.push({ side, sideIndex, axisOffset, labelOffset, clearance: fullClearance })
|
|
241
|
+
clearance[side] += fullClearance
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
return { slots, clearance }
|
|
245
|
+
}
|
|
246
|
+
|
|
184
247
|
type PositionedTrack = Pick<TrackLayout, 'left' | 'top' | 'width' | 'height'>
|
|
185
248
|
|
|
186
249
|
export function findClosestTrackAtPointer<T extends PositionedTrack>(
|
|
@@ -25,13 +25,19 @@ import {
|
|
|
25
25
|
type GridCellGeometry,
|
|
26
26
|
type NormalizedWaveformGridOptions,
|
|
27
27
|
} from './grid'
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
axisTextMetrics,
|
|
30
|
+
resolveYAxisSeriesGroups,
|
|
31
|
+
resolveYAxisTickCount,
|
|
32
|
+
type YAxisSlot,
|
|
33
|
+
} from './layout'
|
|
29
34
|
import type { DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
|
|
30
35
|
import { applyXDomainStrategy } from './xDomain'
|
|
31
36
|
import {
|
|
32
37
|
Y_AXIS_LABEL_BAND_WIDTH,
|
|
33
38
|
Y_AXIS_LABEL_GAP,
|
|
34
39
|
Y_AXIS_OUTER_PADDING,
|
|
40
|
+
Y_AXIS_RIGHT_LABEL_OFFSET,
|
|
35
41
|
Y_AXIS_TICK_PADDING,
|
|
36
42
|
} from './yAxisConstants'
|
|
37
43
|
|
|
@@ -61,6 +67,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
61
67
|
linePointOverrides?: Readonly<Record<string, WaveformPoint[]>>
|
|
62
68
|
hideSecondaryLabels: boolean
|
|
63
69
|
yAxisLabelX: number
|
|
70
|
+
yAxisSlots?: readonly YAxisSlot[]
|
|
64
71
|
showCompactEmptyTracks: boolean
|
|
65
72
|
}
|
|
66
73
|
|
|
@@ -117,8 +124,10 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
117
124
|
).map((group) =>
|
|
118
125
|
!group.fixed && configuredYDomain ? { ...group, domain: configuredYDomain } : group,
|
|
119
126
|
)
|
|
127
|
+
const sideIndexes = { left: 0, right: 0 }
|
|
120
128
|
const sideOffsets = { left: 0, right: 0 }
|
|
121
129
|
const yAxes: WaveformYAxisLayout[] = yAxisGroups.map((group) => {
|
|
130
|
+
const sideIndex = sideIndexes[group.side]++
|
|
122
131
|
const tickCount = resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber)
|
|
123
132
|
const niceCount = Math.max(1, tickCount - 1)
|
|
124
133
|
const scale = scaleLinear(group.domain, [cell.plotHeight, 0])
|
|
@@ -144,11 +153,22 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
144
153
|
Y_AXIS_LABEL_GAP +
|
|
145
154
|
Y_AXIS_LABEL_BAND_WIDTH +
|
|
146
155
|
Y_AXIS_OUTER_PADDING
|
|
147
|
-
const
|
|
156
|
+
const slot = options.yAxisSlots?.find(
|
|
157
|
+
(candidate) => candidate.side === group.side && candidate.sideIndex === sideIndex,
|
|
158
|
+
)
|
|
159
|
+
const x = slot
|
|
160
|
+
? (group.side === 'left' ? 0 : cell.width) + slot.axisOffset
|
|
161
|
+
: group.side === 'left'
|
|
162
|
+
? -sideOffsets.left
|
|
163
|
+
: cell.width + sideOffsets.right
|
|
148
164
|
const labelDistance =
|
|
149
165
|
tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
150
|
-
const labelX =
|
|
151
|
-
|
|
166
|
+
const labelX =
|
|
167
|
+
(slot
|
|
168
|
+
? (group.side === 'left' ? 0 : cell.width) + slot.labelOffset
|
|
169
|
+
: x + (group.side === 'left' ? -labelDistance : labelDistance)) -
|
|
170
|
+
(group.side === 'right' ? Y_AXIS_RIGHT_LABEL_OFFSET : 0)
|
|
171
|
+
if (!slot) sideOffsets[group.side] += clearance
|
|
152
172
|
return {
|
|
153
173
|
index: group.index,
|
|
154
174
|
side: group.side,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DisplayTrack } from './types'
|
|
2
|
+
|
|
3
|
+
export const resolvePageableTracks = (
|
|
4
|
+
tracks: DisplayTrack[],
|
|
5
|
+
hideEmptyTracks?: boolean,
|
|
6
|
+
): DisplayTrack[] => (hideEmptyTracks ? tracks.filter((track) => track.series.length > 0) : tracks)
|
|
7
|
+
|
|
8
|
+
export const resolveTrackFrameNumber = (
|
|
9
|
+
frameNumber: string | number | undefined,
|
|
10
|
+
frameNumbers: Record<string, string | number> | undefined,
|
|
11
|
+
trackCount: number,
|
|
12
|
+
trackIndex: number,
|
|
13
|
+
trackId: string,
|
|
14
|
+
): string | number | undefined => {
|
|
15
|
+
const mappedFrameNumber = frameNumbers?.[trackId]
|
|
16
|
+
if (mappedFrameNumber !== undefined) return mappedFrameNumber
|
|
17
|
+
if (frameNumber === undefined) return undefined
|
|
18
|
+
if (trackCount === 1) return frameNumber
|
|
19
|
+
return typeof frameNumber === 'number'
|
|
20
|
+
? frameNumber + trackIndex
|
|
21
|
+
: `${frameNumber}-${trackIndex + 1}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const createFrameNumberResolver =
|
|
25
|
+
(
|
|
26
|
+
getFrameNumber: () => string | number | undefined,
|
|
27
|
+
getFrameNumbers: () => Record<string, string | number> | undefined,
|
|
28
|
+
getTracks: () => DisplayTrack[],
|
|
29
|
+
) =>
|
|
30
|
+
(trackId: string): string | number | undefined => {
|
|
31
|
+
const tracks = getTracks()
|
|
32
|
+
const trackIndex = tracks.findIndex((track) => track.id === trackId)
|
|
33
|
+
return resolveTrackFrameNumber(
|
|
34
|
+
getFrameNumber(),
|
|
35
|
+
getFrameNumbers(),
|
|
36
|
+
tracks.length,
|
|
37
|
+
trackIndex >= 0 ? trackIndex : 0,
|
|
38
|
+
trackId,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -27,10 +27,11 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
buildTrackLayouts,
|
|
29
29
|
axisTextMetrics,
|
|
30
|
-
|
|
30
|
+
buildYAxisSlots,
|
|
31
31
|
resolveYAxisSeriesGroups,
|
|
32
32
|
} from './layout'
|
|
33
33
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
34
|
+
import { createFrameNumberResolver, resolvePageableTracks } from './trackPagination'
|
|
34
35
|
import type { PreparedWaveformSeries } from './useWaveformData'
|
|
35
36
|
import type { ResolvedWaveformChartProps } from './waveformChartTypes'
|
|
36
37
|
import { applyXDomainStrategy } from './xDomain'
|
|
@@ -107,9 +108,12 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
107
108
|
})
|
|
108
109
|
})
|
|
109
110
|
const renderingOptions = computed(() => resolveWaveformRenderingOptions(props.rendering))
|
|
110
|
-
const
|
|
111
|
+
const pageableTracks = computed(() =>
|
|
112
|
+
resolvePageableTracks(chartTracks.value, gridOptions.value.hideEmptyTracks),
|
|
113
|
+
)
|
|
114
|
+
const pageCount = computed(() => getPageCount(pageableTracks.value.length, gridOptions.value))
|
|
111
115
|
const pagedTracks = computed(() =>
|
|
112
|
-
paginateSeries(
|
|
116
|
+
paginateSeries(pageableTracks.value, currentPage.value, gridOptions.value),
|
|
113
117
|
)
|
|
114
118
|
const layoutTracks = computed(() => {
|
|
115
119
|
const tracksWithSeries = pagedTracks.value.filter((track) => track.series.length > 0)
|
|
@@ -169,33 +173,24 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
169
173
|
: 0,
|
|
170
174
|
),
|
|
171
175
|
)
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
props.axes?.y?.splitNumber,
|
|
181
|
-
props.axes?.y?.nice !== false,
|
|
182
|
-
)
|
|
183
|
-
return {
|
|
184
|
-
left: Math.max(maximum.left, clearance.left),
|
|
185
|
-
right: Math.max(maximum.right, clearance.right),
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
{ left: 0, right: 0 },
|
|
176
|
+
const yAxisSlots = computed(() =>
|
|
177
|
+
buildYAxisSlots(
|
|
178
|
+
layoutTracks.value.filter((track) => track.visibleSeries.length > 0),
|
|
179
|
+
props.overlayMode,
|
|
180
|
+
props.yDomain,
|
|
181
|
+
props.yDomains,
|
|
182
|
+
props.axes?.y?.splitNumber,
|
|
183
|
+
props.axes?.y?.nice !== false,
|
|
189
184
|
),
|
|
190
185
|
)
|
|
191
186
|
const resolvedChartLeftMargin = computed(() =>
|
|
192
187
|
props.overlayMode === 'multi-axis'
|
|
193
|
-
? Math.max(chartLeftMargin.value,
|
|
188
|
+
? Math.max(chartLeftMargin.value, yAxisSlots.value.clearance.left)
|
|
194
189
|
: chartLeftMargin.value,
|
|
195
190
|
)
|
|
196
191
|
const chartRightMargin = computed(() =>
|
|
197
192
|
props.overlayMode === 'multi-axis'
|
|
198
|
-
? Math.max(margin.right,
|
|
193
|
+
? Math.max(margin.right, yAxisSlots.value.clearance.right)
|
|
199
194
|
: margin.right,
|
|
200
195
|
)
|
|
201
196
|
const innerWidth = computed(() =>
|
|
@@ -212,7 +207,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
212
207
|
return {
|
|
213
208
|
horizontalGap:
|
|
214
209
|
props.overlayMode === 'multi-axis' && hasMultipleColumns && hasVisibleWaveformData.value
|
|
215
|
-
? Math.max(baseGap,
|
|
210
|
+
? Math.max(baseGap, yAxisSlots.value.clearance.left + yAxisSlots.value.clearance.right)
|
|
216
211
|
: hasMultipleColumns && hasVisibleWaveformData.value
|
|
217
212
|
? hasYAxisLabels.value && canReserveLabelClearance
|
|
218
213
|
? fullGap
|
|
@@ -323,7 +318,10 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
323
318
|
rendering: renderingOptions.value,
|
|
324
319
|
linePointOverrides: linePointOverrides?.value,
|
|
325
320
|
hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
|
|
326
|
-
yAxisLabelX:
|
|
321
|
+
yAxisLabelX:
|
|
322
|
+
yAxisSlots.value.slots.find((slot) => slot.side === 'left' && slot.sideIndex === 0)
|
|
323
|
+
?.labelOffset ?? yAxisMetrics.value.labelCenterX,
|
|
324
|
+
yAxisSlots: props.overlayMode === 'multi-axis' ? yAxisSlots.value.slots : undefined,
|
|
327
325
|
showCompactEmptyTracks: props.displayMode === 'compact' && hasWaveformData.value,
|
|
328
326
|
}),
|
|
329
327
|
)
|
|
@@ -363,16 +361,15 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
363
361
|
}
|
|
364
362
|
: undefined
|
|
365
363
|
})
|
|
366
|
-
const resolveFrameNumber = (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
: `${props.frameNumber}-${trackIndex + 1}`
|
|
372
|
-
}
|
|
364
|
+
const resolveFrameNumber = createFrameNumberResolver(
|
|
365
|
+
() => props.frameNumber,
|
|
366
|
+
() => props.frameNumbers,
|
|
367
|
+
() => chartTracks.value,
|
|
368
|
+
)
|
|
373
369
|
return {
|
|
374
370
|
chartSeries,
|
|
375
371
|
chartTracks,
|
|
372
|
+
pageableTracks,
|
|
376
373
|
renderingOptions,
|
|
377
374
|
gridOptions,
|
|
378
375
|
pageCount,
|
|
@@ -43,6 +43,8 @@ export interface WaveformChartProps {
|
|
|
43
43
|
yDomains?: Record<string, [number, number]>
|
|
44
44
|
timeUnit?: 's' | 'ms'
|
|
45
45
|
frameNumber?: string | number
|
|
46
|
+
/** Per-track frame watermark overrides, keyed by stable trackId. */
|
|
47
|
+
frameNumbers?: Record<string, string | number>
|
|
46
48
|
frameStyle?: WaveformFrameStyle
|
|
47
49
|
axes?: WaveformAxesOptions
|
|
48
50
|
annotations?: WaveformAnnotation[]
|
|
@@ -3,6 +3,7 @@ export const Y_AXIS_TICK_PADDING = 7
|
|
|
3
3
|
export const Y_AXIS_OUTER_PADDING = 4
|
|
4
4
|
export const Y_AXIS_LABEL_GAP = 0
|
|
5
5
|
export const Y_AXIS_LABEL_BAND_WIDTH = 12
|
|
6
|
+
export const Y_AXIS_RIGHT_LABEL_OFFSET = 10
|
|
6
7
|
|
|
7
8
|
export function resolveYAxisSides(axisCount: number): Array<'left' | 'right'> {
|
|
8
9
|
if (axisCount >= 4) return ['left', 'left', 'right', 'right']
|