waveform-analysis 0.1.46 → 0.1.48
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 +44 -30
- package/dist/index.cjs +1 -1
- package/dist/index.js +460 -461
- package/dist/types/components/core/grid.d.ts +3 -0
- package/dist/types/components/core/layout.d.ts +2 -2
- package/dist/types/components/core/trackLayoutBuilder.d.ts +1 -0
- package/dist/types/components/core/types.d.ts +2 -1
- package/dist/types/components/interaction/useWaveformViewport.d.ts +2 -1
- package/dist/types/types/chart.d.ts +2 -0
- 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/layout.ts +15 -6
- package/src/components/core/trackLayoutBuilder.ts +17 -23
- package/src/components/core/types.ts +2 -1
- package/src/components/core/useWaveformLayout.ts +24 -12
- package/src/components/core/yAxisNice.test.ts +174 -0
- 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 +261 -0
- package/src/demo/xAxisLabelControls.test.ts +1 -1
- package/src/types/chart.ts +2 -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 {
|
|
@@ -15,8 +15,8 @@ export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMod
|
|
|
15
15
|
export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number): {
|
|
16
16
|
tickTextWidth: number;
|
|
17
17
|
};
|
|
18
|
-
export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number): number;
|
|
19
|
-
export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number): {
|
|
18
|
+
export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number, nice?: boolean): number;
|
|
19
|
+
export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number, nice?: boolean): {
|
|
20
20
|
left: number;
|
|
21
21
|
right: number;
|
|
22
22
|
};
|
|
@@ -22,6 +22,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
22
22
|
timeUnit: 's' | 'ms';
|
|
23
23
|
xAxisLabelFormatter?: WaveformXAxisLabelFormatter;
|
|
24
24
|
yAxisSplitNumber?: number;
|
|
25
|
+
yAxisNice?: boolean;
|
|
25
26
|
rendering: ResolvedWaveformRenderingOptions;
|
|
26
27
|
hideSecondaryLabels: boolean;
|
|
27
28
|
yAxisLabelX: number;
|
|
@@ -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;
|
|
@@ -152,6 +152,8 @@ export interface WaveformAxesOptions {
|
|
|
152
152
|
lineVisible?: boolean;
|
|
153
153
|
/** Target total number of equal Y-axis ticks, including endpoints. Defaults to 5. */
|
|
154
154
|
splitNumber?: number;
|
|
155
|
+
/** Expands Y-axis domains to equal, human-friendly intervals. Defaults to true. */
|
|
156
|
+
nice?: boolean;
|
|
155
157
|
};
|
|
156
158
|
}
|
|
157
159
|
/** Styling and visibility options for the horizontal zero-value reference line. */
|
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)
|
|
@@ -133,9 +133,13 @@ export function axisTextMetrics(
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export function measureYAxisGroupClearance(
|
|
136
|
+
export function measureYAxisGroupClearance(
|
|
137
|
+
group: YAxisSeriesGroup,
|
|
138
|
+
tickCount?: number,
|
|
139
|
+
nice = true,
|
|
140
|
+
): number {
|
|
137
141
|
return (
|
|
138
|
-
axisTextMetrics(group.domain,
|
|
142
|
+
axisTextMetrics(group.domain, nice, undefined, group.seriesList[0]?.unit, tickCount)
|
|
139
143
|
.tickTextWidth +
|
|
140
144
|
Y_AXIS_TICK_PADDING +
|
|
141
145
|
Y_AXIS_LABEL_GAP +
|
|
@@ -144,9 +148,13 @@ export function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?:
|
|
|
144
148
|
)
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
function measureYAxisGroupTickClearance(
|
|
151
|
+
function measureYAxisGroupTickClearance(
|
|
152
|
+
group: YAxisSeriesGroup,
|
|
153
|
+
tickCount?: number,
|
|
154
|
+
nice = true,
|
|
155
|
+
): number {
|
|
148
156
|
return (
|
|
149
|
-
axisTextMetrics(group.domain,
|
|
157
|
+
axisTextMetrics(group.domain, nice, undefined, group.seriesList[0]?.unit, tickCount)
|
|
150
158
|
.tickTextWidth +
|
|
151
159
|
Y_AXIS_TICK_PADDING +
|
|
152
160
|
Y_AXIS_OUTER_PADDING
|
|
@@ -159,13 +167,14 @@ export function measureTrackYAxisClearance(
|
|
|
159
167
|
yDomain?: WaveformYDomain,
|
|
160
168
|
yDomains?: Record<string, WaveformYDomain>,
|
|
161
169
|
tickCount?: number,
|
|
170
|
+
nice = true,
|
|
162
171
|
): { left: number; right: number } {
|
|
163
172
|
return resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).reduce(
|
|
164
173
|
(clearance, group) => {
|
|
165
174
|
clearance[group.side] +=
|
|
166
175
|
overlayMode === 'multi-axis' || track.visibleSeries.length === 1
|
|
167
|
-
? measureYAxisGroupClearance(group, tickCount)
|
|
168
|
-
: measureYAxisGroupTickClearance(group, tickCount)
|
|
176
|
+
? measureYAxisGroupClearance(group, tickCount, nice)
|
|
177
|
+
: measureYAxisGroupTickClearance(group, tickCount, nice)
|
|
169
178
|
return clearance
|
|
170
179
|
},
|
|
171
180
|
{ left: 0, right: 0 },
|
|
@@ -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,
|
|
@@ -55,6 +55,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
55
55
|
timeUnit: 's' | 'ms'
|
|
56
56
|
xAxisLabelFormatter?: WaveformXAxisLabelFormatter
|
|
57
57
|
yAxisSplitNumber?: number
|
|
58
|
+
yAxisNice?: boolean
|
|
58
59
|
rendering: ResolvedWaveformRenderingOptions
|
|
59
60
|
hideSecondaryLabels: boolean
|
|
60
61
|
yAxisLabelX: number
|
|
@@ -81,33 +82,24 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
81
82
|
const bottomCells = getBottomRowCellIndexes(visibleCells, options.grid.columnCount)
|
|
82
83
|
|
|
83
84
|
return visibleCells.flatMap((cell, index) => {
|
|
84
|
-
const isEmpty = !cell.series
|
|
85
|
-
if (
|
|
86
|
-
|
|
85
|
+
const isEmpty = !cell.series || cell.series.series.length === 0
|
|
86
|
+
if (!cell.series && (options.displayMode !== 'compact' || !options.showCompactEmptyTracks))
|
|
87
|
+
return []
|
|
88
|
+
const displayTrack: DisplayTrack = cell.series ?? {
|
|
87
89
|
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: [],
|
|
90
|
+
series: [],
|
|
91
|
+
visibleSeries: [],
|
|
95
92
|
xDomain: [0, 1],
|
|
96
93
|
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
94
|
}
|
|
106
95
|
const hasVisibleSeries = !isEmpty && displayTrack.visibleSeries.length > 0
|
|
107
|
-
const series = displayTrack.visibleSeries[0] ?? displayTrack.series[0] ??
|
|
96
|
+
const series = displayTrack.visibleSeries[0] ?? displayTrack.series[0] ?? null
|
|
108
97
|
const baseXScale =
|
|
109
98
|
options.displayMode === 'independent'
|
|
110
|
-
? scaleLinear(
|
|
99
|
+
? scaleLinear(
|
|
100
|
+
resolveIndependentXDomain(displayTrack, series?.id ?? displayTrack.id, options),
|
|
101
|
+
[0, cell.width],
|
|
102
|
+
)
|
|
111
103
|
: scaleLinear(options.sharedZoomDomain, [0, cell.width])
|
|
112
104
|
const transform =
|
|
113
105
|
options.displayMode === 'independent'
|
|
@@ -128,7 +120,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
128
120
|
const tickCount = resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber)
|
|
129
121
|
const niceCount = Math.max(1, tickCount - 1)
|
|
130
122
|
const scale = scaleLinear(group.domain, [cell.plotHeight, 0])
|
|
131
|
-
scale.nice(niceCount)
|
|
123
|
+
if (options.yAxisNice !== false) scale.nice(niceCount)
|
|
132
124
|
const [axisStart, axisEnd] = scale.domain()
|
|
133
125
|
const majorTicks = Array.from(
|
|
134
126
|
{ length: tickCount },
|
|
@@ -167,7 +159,9 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
167
159
|
seriesList: group.seriesList,
|
|
168
160
|
}
|
|
169
161
|
})
|
|
170
|
-
const
|
|
162
|
+
const fallbackYScale = scaleLinear(displayTrack.yDomain, [cell.plotHeight, 0])
|
|
163
|
+
if (options.yAxisNice !== false) fallbackYScale.nice()
|
|
164
|
+
const yScale = yAxes[0]?.scale ?? fallbackYScale
|
|
171
165
|
const xMajorTicks = xScale.ticks(Math.max(2, Math.floor(cell.width / 100)))
|
|
172
166
|
const yMajorTicks = yAxes[0]?.majorTicks ?? []
|
|
173
167
|
const yAxisTickValues = yAxes[0]?.tickValues ?? []
|
|
@@ -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,17 +102,22 @@ 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(() =>
|
|
107
113
|
paginateSeries(chartTracks.value, currentPage.value, gridOptions.value),
|
|
108
114
|
)
|
|
115
|
+
const layoutTracks = computed(() => {
|
|
116
|
+
const tracksWithSeries = pagedTracks.value.filter((track) => track.series.length > 0)
|
|
117
|
+
return gridOptions.value.fillIncompleteLastRow && tracksWithSeries.length > 0
|
|
118
|
+
? tracksWithSeries
|
|
119
|
+
: pagedTracks.value
|
|
120
|
+
})
|
|
109
121
|
const yAxisMetrics = computed(() => {
|
|
110
122
|
const axisText = chartTracks.value
|
|
111
123
|
.filter((track) => track.visibleSeries.length > 0)
|
|
@@ -116,7 +128,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
116
128
|
(group) =>
|
|
117
129
|
axisTextMetrics(
|
|
118
130
|
group.domain,
|
|
119
|
-
|
|
131
|
+
props.axes?.y?.nice !== false,
|
|
120
132
|
undefined,
|
|
121
133
|
group.seriesList[0]?.unit,
|
|
122
134
|
props.axes?.y?.splitNumber,
|
|
@@ -167,6 +179,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
167
179
|
props.yDomain,
|
|
168
180
|
props.yDomains,
|
|
169
181
|
props.axes?.y?.splitNumber,
|
|
182
|
+
props.axes?.y?.nice !== false,
|
|
170
183
|
)
|
|
171
184
|
return {
|
|
172
185
|
left: Math.max(maximum.left, clearance.left),
|
|
@@ -217,9 +230,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
217
230
|
const hasChartArea = computed(() => innerWidth.value > 0 && innerHeight.value > 0)
|
|
218
231
|
const resolvedXLabel = computed(() => props.xLabel ?? `时间(${props.timeUnit})`)
|
|
219
232
|
const activeInteractionMode = computed(() => props.interactionMode)
|
|
220
|
-
const isZoomMode = computed(
|
|
221
|
-
() => activeInteractionMode.value === 'zoom' || activeInteractionMode.value === undefined,
|
|
222
|
-
)
|
|
233
|
+
const isZoomMode = computed(() => activeInteractionMode.value !== 'annotation')
|
|
223
234
|
const sharedXDomain = computed(() => {
|
|
224
235
|
const values: number[] = []
|
|
225
236
|
chartTracks.value.forEach((track) => {
|
|
@@ -245,8 +256,8 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
245
256
|
})
|
|
246
257
|
const resolveInitialTrackDomain = (track: TrackLayout): [number, number] => {
|
|
247
258
|
const configuredDomain =
|
|
248
|
-
props.initialXDomains?.[track.series
|
|
249
|
-
props.initialXDomains?.[track.series.id] ??
|
|
259
|
+
props.initialXDomains?.[track.series?.trackId ?? track.series?.id ?? track.id] ??
|
|
260
|
+
(track.series ? props.initialXDomains?.[track.series.id] : undefined) ??
|
|
250
261
|
props.initialXDomain
|
|
251
262
|
if (
|
|
252
263
|
configuredDomain &&
|
|
@@ -279,10 +290,10 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
279
290
|
innerHeight.value,
|
|
280
291
|
gridOptions.value,
|
|
281
292
|
props.displayMode,
|
|
282
|
-
|
|
293
|
+
layoutTracks.value.map(Boolean),
|
|
283
294
|
yAxisLayout.value.horizontalGap,
|
|
284
295
|
)
|
|
285
|
-
return cells.map((cell, index) => ({ ...cell, series:
|
|
296
|
+
return cells.map((cell, index) => ({ ...cell, series: layoutTracks.value[index] }))
|
|
286
297
|
})
|
|
287
298
|
const trackLayouts = computed<TrackLayout[]>(() =>
|
|
288
299
|
buildTrackLayouts({
|
|
@@ -309,6 +320,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
309
320
|
timeUnit: props.timeUnit,
|
|
310
321
|
xAxisLabelFormatter: props.axes?.x?.labelFormatter,
|
|
311
322
|
yAxisSplitNumber: props.axes?.y?.splitNumber,
|
|
323
|
+
yAxisNice: props.axes?.y?.nice,
|
|
312
324
|
rendering: renderingOptions.value,
|
|
313
325
|
hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
|
|
314
326
|
yAxisLabelX: yAxisMetrics.value.labelCenterX,
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { zoomIdentity } from 'd3'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
|
|
5
|
+
import type { WaveformData } from '../../types'
|
|
6
|
+
import { buildTrackLayouts } from './trackLayoutBuilder'
|
|
7
|
+
import type { DisplaySeries, DisplayTrack } from './types'
|
|
8
|
+
import { prepareWaveformSeries } from './useWaveformData'
|
|
9
|
+
|
|
10
|
+
function displaySeries(id: string, domain: [number, number]): DisplaySeries {
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
name: id,
|
|
14
|
+
color: '#1677ff',
|
|
15
|
+
lineType: 'linear',
|
|
16
|
+
lineStyle: 'solid',
|
|
17
|
+
pointType: 'none',
|
|
18
|
+
errorBar: { visible: false, width: 1.5, capWidth: 8 },
|
|
19
|
+
points: [
|
|
20
|
+
{ x: 0, y: domain[0] },
|
|
21
|
+
{ x: 1, y: domain[1] },
|
|
22
|
+
],
|
|
23
|
+
xDomain: [0, 1],
|
|
24
|
+
yDomain: domain,
|
|
25
|
+
hasErrorPoints: false,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function layout(
|
|
30
|
+
series: DisplaySeries[],
|
|
31
|
+
options: { overlayMode?: 'single-axis' | 'multi-axis'; fixedYDomain?: [number, number] } = {},
|
|
32
|
+
) {
|
|
33
|
+
const track: DisplayTrack = {
|
|
34
|
+
id: 'track',
|
|
35
|
+
series,
|
|
36
|
+
visibleSeries: series,
|
|
37
|
+
xDomain: [0, 1],
|
|
38
|
+
yDomain: [
|
|
39
|
+
Math.min(...series.map((item) => item.yDomain[0])),
|
|
40
|
+
Math.max(...series.map((item) => item.yDomain[1])),
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
return buildTrackLayouts({
|
|
44
|
+
cells: [
|
|
45
|
+
{
|
|
46
|
+
slotIndex: 0,
|
|
47
|
+
row: 0,
|
|
48
|
+
column: 0,
|
|
49
|
+
left: 0,
|
|
50
|
+
top: 0,
|
|
51
|
+
width: 120,
|
|
52
|
+
height: 100,
|
|
53
|
+
plotHeight: 100,
|
|
54
|
+
cellHeight: 130,
|
|
55
|
+
xAxisBand: 30,
|
|
56
|
+
series: track,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
grid: {
|
|
60
|
+
rowCount: 1,
|
|
61
|
+
columnCount: 1,
|
|
62
|
+
showPagination: false,
|
|
63
|
+
fillIncompleteLastRow: false,
|
|
64
|
+
trackLines: {},
|
|
65
|
+
},
|
|
66
|
+
displayMode: 'independent',
|
|
67
|
+
overlayMode: options.overlayMode ?? 'single-axis',
|
|
68
|
+
independentTransforms: [zoomIdentity],
|
|
69
|
+
sharedZoomDomain: [0, 1],
|
|
70
|
+
fixedYDomain: options.fixedYDomain,
|
|
71
|
+
timeUnit: 'ms',
|
|
72
|
+
rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
73
|
+
hideSecondaryLabels: false,
|
|
74
|
+
yAxisLabelX: -50,
|
|
75
|
+
yAxisNice: false,
|
|
76
|
+
showCompactEmptyTracks: false,
|
|
77
|
+
})[0]!
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('Y-axis nice domains', () => {
|
|
81
|
+
it('defaults to a nice domain and can preserve the actual data endpoints', () => {
|
|
82
|
+
const source = displaySeries('source', [3, 97])
|
|
83
|
+
const track: DisplayTrack = {
|
|
84
|
+
id: 'track',
|
|
85
|
+
series: [source],
|
|
86
|
+
visibleSeries: [source],
|
|
87
|
+
xDomain: [0, 1],
|
|
88
|
+
yDomain: [3, 97],
|
|
89
|
+
}
|
|
90
|
+
const defaults = buildTrackLayouts({
|
|
91
|
+
cells: [
|
|
92
|
+
{
|
|
93
|
+
slotIndex: 0,
|
|
94
|
+
row: 0,
|
|
95
|
+
column: 0,
|
|
96
|
+
left: 0,
|
|
97
|
+
top: 0,
|
|
98
|
+
width: 120,
|
|
99
|
+
height: 100,
|
|
100
|
+
plotHeight: 100,
|
|
101
|
+
cellHeight: 130,
|
|
102
|
+
xAxisBand: 30,
|
|
103
|
+
series: track,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
grid: {
|
|
107
|
+
rowCount: 1,
|
|
108
|
+
columnCount: 1,
|
|
109
|
+
showPagination: false,
|
|
110
|
+
fillIncompleteLastRow: false,
|
|
111
|
+
trackLines: {},
|
|
112
|
+
},
|
|
113
|
+
displayMode: 'independent',
|
|
114
|
+
overlayMode: 'single-axis',
|
|
115
|
+
independentTransforms: [zoomIdentity],
|
|
116
|
+
sharedZoomDomain: [0, 1],
|
|
117
|
+
timeUnit: 'ms',
|
|
118
|
+
rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
119
|
+
hideSecondaryLabels: false,
|
|
120
|
+
yAxisLabelX: -50,
|
|
121
|
+
showCompactEmptyTracks: false,
|
|
122
|
+
})[0]!
|
|
123
|
+
|
|
124
|
+
expect(defaults.yScale.domain()).toEqual([0, 100])
|
|
125
|
+
expect(layout([source]).yScale.domain()).toEqual([3, 97])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('merges visible single-axis series but keeps multi-axis series independent', () => {
|
|
129
|
+
const first = displaySeries('first', [3, 40])
|
|
130
|
+
const second = displaySeries('second', [10, 97])
|
|
131
|
+
|
|
132
|
+
expect(layout([first, second]).yScale.domain()).toEqual([3, 97])
|
|
133
|
+
expect(
|
|
134
|
+
layout([first, second], { overlayMode: 'multi-axis' }).yAxes.map((axis) =>
|
|
135
|
+
axis.scale.domain(),
|
|
136
|
+
),
|
|
137
|
+
).toEqual([
|
|
138
|
+
[3, 40],
|
|
139
|
+
[10, 97],
|
|
140
|
+
])
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('keeps explicit domains and constant-data safety ranges unchanged', () => {
|
|
144
|
+
expect(
|
|
145
|
+
layout([displaySeries('source', [3, 97])], { fixedYDomain: [3, 97] }).yScale.domain(),
|
|
146
|
+
).toEqual([3, 97])
|
|
147
|
+
expect(layout([displaySeries('constant', [9.5, 10.5])]).yScale.domain()).toEqual([9.5, 10.5])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('continues to include visible error-bar bounds in the preserved domain', () => {
|
|
151
|
+
const input: WaveformData = {
|
|
152
|
+
kind: 'series',
|
|
153
|
+
series: [
|
|
154
|
+
{
|
|
155
|
+
id: 'error-series',
|
|
156
|
+
name: 'error series',
|
|
157
|
+
errorBar: { visible: true },
|
|
158
|
+
data: {
|
|
159
|
+
kind: 'points',
|
|
160
|
+
points: [
|
|
161
|
+
{ x: 0, y: 3, lowerError: 2 },
|
|
162
|
+
{ x: 1, y: 7, upperError: 4 },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
const [prepared] = prepareWaveformSeries(input)
|
|
169
|
+
const errorSeries: DisplaySeries = { ...prepared!, color: prepared!.color ?? '#1677ff' }
|
|
170
|
+
|
|
171
|
+
expect(prepared?.yDomain).toEqual([1, 11])
|
|
172
|
+
expect(layout([errorSeries]).yScale.domain()).toEqual([1, 11])
|
|
173
|
+
})
|
|
174
|
+
})
|
|
@@ -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,
|