waveform-analysis 0.1.59 → 0.1.60
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/dist/index.cjs +1 -1
- package/dist/index.js +600 -584
- package/dist/types/components/core/layout.d.ts +9 -2
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +10 -0
- package/package.json +1 -1
- package/src/components/core/layout.test.ts +24 -0
- package/src/components/core/layout.ts +75 -13
- package/src/components/core/trackLayoutBuilder.ts +7 -13
- package/src/components/core/useWaveformLayout.ts +30 -58
- package/src/components/core/yAxisLayoutMetrics.ts +75 -0
- package/src/components/core/yAxisSlotLayout.test.ts +37 -1
- package/src/components/rendering/WaveformTrackAxes.vue +8 -4
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +23 -24
- package/src/components/waveformChartCases/yAxisLayout.test.ts +103 -0
|
@@ -3,6 +3,12 @@ import type { DisplaySeries, DisplayTrack, TrackLayout } from './types';
|
|
|
3
3
|
import { type WaveformYDomain } from './yDomain';
|
|
4
4
|
export { MAX_MULTI_Y_AXIS_COUNT } from './constants';
|
|
5
5
|
export declare function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number): number;
|
|
6
|
+
export interface ResolvedYAxisTicks {
|
|
7
|
+
domain: [number, number];
|
|
8
|
+
values: number[];
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveYAxisTicks(domain: [number, number], tickCount?: number, nice?: boolean): ResolvedYAxisTicks;
|
|
11
|
+
export declare function formatYAxisTickLabel(value: number, domain: [number, number], tickValues: readonly number[], unit?: string): string;
|
|
6
12
|
export interface YAxisSeriesGroup {
|
|
7
13
|
index: number;
|
|
8
14
|
side: 'left' | 'right';
|
|
@@ -19,7 +25,8 @@ export interface YAxisSlot {
|
|
|
19
25
|
}
|
|
20
26
|
export declare function buildYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode): YAxisSeriesGroup[];
|
|
21
27
|
export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): YAxisSeriesGroup[];
|
|
22
|
-
export declare function
|
|
28
|
+
export declare function resolveRenderedYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, viewportYDomains?: Record<string, [number, number]>): YAxisSeriesGroup[];
|
|
29
|
+
export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number, includeWithoutLastTick?: boolean): {
|
|
23
30
|
tickTextWidth: number;
|
|
24
31
|
};
|
|
25
32
|
export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number, nice?: boolean): number;
|
|
@@ -31,7 +38,7 @@ export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayM
|
|
|
31
38
|
* Reserves stable Y-axis positions for every visible track on the current page.
|
|
32
39
|
* Slots are ordered from the plot edge outward for each side.
|
|
33
40
|
*/
|
|
34
|
-
export declare function buildYAxisSlots(tracks: readonly DisplayTrack[], overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number, nice?: boolean): {
|
|
41
|
+
export declare function buildYAxisSlots(tracks: readonly DisplayTrack[], overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number, nice?: boolean, includeWithoutLastTick?: boolean, viewportYDomains?: Record<string, [number, number]>): {
|
|
35
42
|
slots: YAxisSlot[];
|
|
36
43
|
clearance: {
|
|
37
44
|
left: number;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { WaveformOverlayMode } from '../data/types';
|
|
2
|
+
import type { DisplayTrack } from './types';
|
|
3
|
+
import type { WaveformYDomain } from './yDomain';
|
|
4
|
+
export interface YAxisLayoutMetrics {
|
|
5
|
+
tickClearance: number;
|
|
6
|
+
fullClearance: number;
|
|
7
|
+
labelCenterX: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function resolveViewportYDomains(displayMode: 'independent' | 'separated' | 'compact', chartTracks: DisplayTrack[], sharedYDomains: Record<string, [number, number]>, independentYDomains: Record<number, [number, number]>): Record<string, [number, number]>;
|
|
10
|
+
export declare function resolveYAxisLayoutMetrics(chartTracks: DisplayTrack[], overlayMode: WaveformOverlayMode, yDomain: WaveformYDomain | undefined, yDomains: Record<string, WaveformYDomain> | undefined, viewportYDomains: Record<string, [number, number]>, nice: boolean, tickCount: number, compact: boolean): YAxisLayoutMetrics;
|
package/package.json
CHANGED
|
@@ -4,11 +4,14 @@ import { describe, expect, it } from 'vitest'
|
|
|
4
4
|
import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
|
|
5
5
|
import type { DisplaySeries, DisplayTrack } from './types'
|
|
6
6
|
import {
|
|
7
|
+
axisTextMetrics,
|
|
7
8
|
buildTrackLayouts,
|
|
8
9
|
buildYAxisSeriesGroups,
|
|
10
|
+
formatYAxisTickLabel,
|
|
9
11
|
findClosestTrackAtPointer,
|
|
10
12
|
MAX_MULTI_Y_AXIS_COUNT,
|
|
11
13
|
measureYAxisGroupClearance,
|
|
14
|
+
resolveYAxisTicks,
|
|
12
15
|
resolveYAxisTickCount,
|
|
13
16
|
} from './layout'
|
|
14
17
|
|
|
@@ -143,6 +146,27 @@ describe('multi-value Y-axis grouping', () => {
|
|
|
143
146
|
expect(resolveYAxisTickCount(220)).toBe(5)
|
|
144
147
|
})
|
|
145
148
|
|
|
149
|
+
it('measures the same final endpoint label that Y-axis rendering uses', () => {
|
|
150
|
+
const ticks = resolveYAxisTicks([-0.7434, -0.3434], 5, false)
|
|
151
|
+
const labels = ticks.values.map((value) =>
|
|
152
|
+
formatYAxisTickLabel(value, ticks.domain, ticks.values, 'V'),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
expect(labels).toEqual(['-0.7434', '-0.6434', '-0.5434', '-0.4434', '(V) -0.3434'])
|
|
156
|
+
expect(axisTextMetrics(ticks.domain, false, undefined, 'V', 5).tickTextWidth).toBe(77)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('includes the compact-axis unit label after its top tick is hidden', () => {
|
|
160
|
+
const ticks = resolveYAxisTicks([0.12345, 100], 5, false)
|
|
161
|
+
const compactTicks = ticks.values.slice(0, -1)
|
|
162
|
+
|
|
163
|
+
expect(formatYAxisTickLabel(compactTicks.at(-1)!, ticks.domain, compactTicks, 'V')).toBe(
|
|
164
|
+
'(V) 75.031',
|
|
165
|
+
)
|
|
166
|
+
expect(axisTextMetrics(ticks.domain, false, undefined, 'V', 5).tickTextWidth).toBe(49)
|
|
167
|
+
expect(axisTextMetrics(ticks.domain, false, undefined, 'V', 5, true).tickTextWidth).toBe(70)
|
|
168
|
+
})
|
|
169
|
+
|
|
146
170
|
it('keeps every overlaid series on one axis in single-axis mode', () => {
|
|
147
171
|
const groups = buildYAxisSeriesGroups(
|
|
148
172
|
track([series('a', 0, 1), series('b', 10, 20)]),
|
|
@@ -27,6 +27,47 @@ export function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number)
|
|
|
27
27
|
return 5
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export interface ResolvedYAxisTicks {
|
|
31
|
+
domain: [number, number]
|
|
32
|
+
values: number[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveYAxisTicks(
|
|
36
|
+
domain: [number, number],
|
|
37
|
+
tickCount = 5,
|
|
38
|
+
nice = true,
|
|
39
|
+
): ResolvedYAxisTicks {
|
|
40
|
+
const effectiveTickCount =
|
|
41
|
+
typeof tickCount === 'number' && Number.isFinite(tickCount)
|
|
42
|
+
? Math.max(2, Math.floor(tickCount))
|
|
43
|
+
: 5
|
|
44
|
+
const scale = scaleLinear(domain, [1, 0])
|
|
45
|
+
if (nice) scale.nice(Math.max(1, effectiveTickCount - 1))
|
|
46
|
+
const resolvedDomain = scale.domain() as [number, number]
|
|
47
|
+
const [axisStart, axisEnd] = resolvedDomain
|
|
48
|
+
return {
|
|
49
|
+
domain: resolvedDomain,
|
|
50
|
+
values: Array.from(
|
|
51
|
+
{ length: effectiveTickCount },
|
|
52
|
+
(_, index) => axisStart + ((axisEnd - axisStart) * index) / (effectiveTickCount - 1),
|
|
53
|
+
),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatYAxisTickLabel(
|
|
58
|
+
value: number,
|
|
59
|
+
domain: [number, number],
|
|
60
|
+
tickValues: readonly number[],
|
|
61
|
+
unit?: string,
|
|
62
|
+
): string {
|
|
63
|
+
return formatScientificAxisLabel(value, {
|
|
64
|
+
axisMin: domain[0],
|
|
65
|
+
axisMax: domain[1],
|
|
66
|
+
topTickValue: Math.max(...tickValues),
|
|
67
|
+
unit,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
30
71
|
export interface YAxisSeriesGroup {
|
|
31
72
|
index: number
|
|
32
73
|
side: 'left' | 'right'
|
|
@@ -114,26 +155,38 @@ export function resolveYAxisSeriesGroups(
|
|
|
114
155
|
})
|
|
115
156
|
}
|
|
116
157
|
|
|
158
|
+
export function resolveRenderedYAxisSeriesGroups(
|
|
159
|
+
track: DisplayTrack,
|
|
160
|
+
overlayMode: WaveformOverlayMode,
|
|
161
|
+
yDomain?: WaveformYDomain,
|
|
162
|
+
yDomains?: Record<string, WaveformYDomain>,
|
|
163
|
+
viewportYDomains?: Record<string, [number, number]>,
|
|
164
|
+
): YAxisSeriesGroup[] {
|
|
165
|
+
const viewportYDomain = viewportYDomains?.[track.id]
|
|
166
|
+
return resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).map((group) =>
|
|
167
|
+
!group.fixed && viewportYDomain ? { ...group, domain: viewportYDomain } : group,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
117
171
|
export function axisTextMetrics(
|
|
118
172
|
domain: [number, number],
|
|
119
173
|
nice = true,
|
|
120
174
|
tickValues?: number[],
|
|
121
175
|
unit?: string,
|
|
122
|
-
tickCount =
|
|
176
|
+
tickCount = 5,
|
|
177
|
+
includeWithoutLastTick = false,
|
|
123
178
|
): { tickTextWidth: number } {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const values = tickValues ?? scale.ticks(effectiveTickCount)
|
|
132
|
-
const topTickValue = Math.max(...values)
|
|
179
|
+
const resolvedTicks = tickValues
|
|
180
|
+
? { domain, values: tickValues }
|
|
181
|
+
: resolveYAxisTicks(domain, tickCount, nice)
|
|
182
|
+
const tickValueSets = [resolvedTicks.values]
|
|
183
|
+
if (includeWithoutLastTick && resolvedTicks.values.length > 1) {
|
|
184
|
+
tickValueSets.push(resolvedTicks.values.slice(0, -1))
|
|
185
|
+
}
|
|
133
186
|
const maximumTickCharacters = Math.max(
|
|
134
187
|
1,
|
|
135
|
-
...
|
|
136
|
-
(value) =>
|
|
188
|
+
...tickValueSets.flatMap((values) =>
|
|
189
|
+
values.map((value) => formatYAxisTickLabel(value, resolvedTicks.domain, values, unit).length),
|
|
137
190
|
),
|
|
138
191
|
)
|
|
139
192
|
return {
|
|
@@ -200,11 +253,19 @@ export function buildYAxisSlots(
|
|
|
200
253
|
yDomains?: Record<string, WaveformYDomain>,
|
|
201
254
|
tickCount?: number,
|
|
202
255
|
nice = true,
|
|
256
|
+
includeWithoutLastTick = false,
|
|
257
|
+
viewportYDomains?: Record<string, [number, number]>,
|
|
203
258
|
): { slots: YAxisSlot[]; clearance: { left: number; right: number } } {
|
|
204
259
|
const widths = new Map<string, number>()
|
|
205
260
|
tracks.forEach((track) => {
|
|
206
261
|
const sideIndexes = { left: 0, right: 0 }
|
|
207
|
-
|
|
262
|
+
resolveRenderedYAxisSeriesGroups(
|
|
263
|
+
track,
|
|
264
|
+
overlayMode,
|
|
265
|
+
yDomain,
|
|
266
|
+
yDomains,
|
|
267
|
+
viewportYDomains,
|
|
268
|
+
).forEach((group) => {
|
|
208
269
|
const sideIndex = sideIndexes[group.side]++
|
|
209
270
|
const key = `${group.side}:${sideIndex}`
|
|
210
271
|
const width = axisTextMetrics(
|
|
@@ -213,6 +274,7 @@ export function buildYAxisSlots(
|
|
|
213
274
|
undefined,
|
|
214
275
|
group.seriesList[0]?.unit,
|
|
215
276
|
tickCount,
|
|
277
|
+
includeWithoutLastTick,
|
|
216
278
|
).tickTextWidth
|
|
217
279
|
widths.set(key, Math.max(widths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, width))
|
|
218
280
|
})
|
|
@@ -27,7 +27,8 @@ import {
|
|
|
27
27
|
} from './grid'
|
|
28
28
|
import {
|
|
29
29
|
axisTextMetrics,
|
|
30
|
-
|
|
30
|
+
resolveRenderedYAxisSeriesGroups,
|
|
31
|
+
resolveYAxisTicks,
|
|
31
32
|
resolveYAxisTickCount,
|
|
32
33
|
type YAxisSlot,
|
|
33
34
|
} from './layout'
|
|
@@ -115,28 +116,21 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
115
116
|
? (options.independentTransforms[index] ?? zoomIdentity)
|
|
116
117
|
: zoomIdentity
|
|
117
118
|
const xScale = transform.rescaleX(baseXScale)
|
|
118
|
-
const
|
|
119
|
-
const yAxisGroups = resolveYAxisSeriesGroups(
|
|
119
|
+
const yAxisGroups = resolveRenderedYAxisSeriesGroups(
|
|
120
120
|
displayTrack,
|
|
121
121
|
options.overlayMode,
|
|
122
122
|
options.fixedYDomain,
|
|
123
123
|
options.fixedYDomains,
|
|
124
|
-
|
|
125
|
-
!group.fixed && configuredYDomain ? { ...group, domain: configuredYDomain } : group,
|
|
124
|
+
options.yDomains,
|
|
126
125
|
)
|
|
127
126
|
const sideIndexes = { left: 0, right: 0 }
|
|
128
127
|
const sideOffsets = { left: 0, right: 0 }
|
|
129
128
|
const yAxes: WaveformYAxisLayout[] = yAxisGroups.map((group) => {
|
|
130
129
|
const sideIndex = sideIndexes[group.side]++
|
|
131
130
|
const tickCount = resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber)
|
|
132
|
-
const
|
|
133
|
-
const scale = scaleLinear(
|
|
134
|
-
|
|
135
|
-
const [axisStart, axisEnd] = scale.domain()
|
|
136
|
-
const majorTicks = Array.from(
|
|
137
|
-
{ length: tickCount },
|
|
138
|
-
(_, index) => axisStart + ((axisEnd - axisStart) * index) / (tickCount - 1),
|
|
139
|
-
)
|
|
131
|
+
const resolvedTicks = resolveYAxisTicks(group.domain, tickCount, options.yAxisNice !== false)
|
|
132
|
+
const scale = scaleLinear(resolvedTicks.domain, [cell.plotHeight, 0])
|
|
133
|
+
const majorTicks = resolvedTicks.values
|
|
140
134
|
const showAxisEnd = options.displayMode !== 'compact' || cell.row === 0
|
|
141
135
|
const visibleMajorTicks = showAxisEnd ? majorTicks : majorTicks.slice(0, -1)
|
|
142
136
|
const tickValues = visibleMajorTicks
|
|
@@ -7,16 +7,7 @@ import {
|
|
|
7
7
|
type AnnotationSeriesInfo,
|
|
8
8
|
type AnnotationTrackLayout,
|
|
9
9
|
} from '../annotation'
|
|
10
|
-
import {
|
|
11
|
-
channelColors,
|
|
12
|
-
margin,
|
|
13
|
-
MINIMUM_PLOT_WIDTH,
|
|
14
|
-
Y_AXIS_CHARACTER_WIDTH,
|
|
15
|
-
Y_AXIS_LABEL_BAND_WIDTH,
|
|
16
|
-
Y_AXIS_LABEL_GAP,
|
|
17
|
-
Y_AXIS_OUTER_PADDING,
|
|
18
|
-
Y_AXIS_TICK_PADDING,
|
|
19
|
-
} from './constants'
|
|
10
|
+
import { channelColors, margin, MINIMUM_PLOT_WIDTH } from './constants'
|
|
20
11
|
import {
|
|
21
12
|
getGridGap,
|
|
22
13
|
getPageCount,
|
|
@@ -24,17 +15,13 @@ import {
|
|
|
24
15
|
paginateSeries,
|
|
25
16
|
resolveGridCellGeometry,
|
|
26
17
|
} from './grid'
|
|
27
|
-
import {
|
|
28
|
-
buildTrackLayouts,
|
|
29
|
-
axisTextMetrics,
|
|
30
|
-
buildYAxisSlots,
|
|
31
|
-
resolveYAxisSeriesGroups,
|
|
32
|
-
} from './layout'
|
|
18
|
+
import { buildTrackLayouts, buildYAxisSlots, resolveYAxisTickCount } from './layout'
|
|
33
19
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
34
20
|
import { createFrameNumberResolver, resolvePageableTracks } from './trackPagination'
|
|
35
21
|
import type { PreparedWaveformSeries } from './useWaveformData'
|
|
36
22
|
import type { ResolvedWaveformChartProps } from './waveformChartTypes'
|
|
37
23
|
import { applyXDomainStrategy } from './xDomain'
|
|
24
|
+
import { resolveYAxisLayoutMetrics, resolveViewportYDomains } from './yAxisLayoutMetrics'
|
|
38
25
|
import type { useWaveformAnnotationInteraction } from '../annotation'
|
|
39
26
|
interface LayoutContext {
|
|
40
27
|
props: ResolvedWaveformChartProps
|
|
@@ -121,38 +108,29 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
121
108
|
? tracksWithSeries
|
|
122
109
|
: pagedTracks.value
|
|
123
110
|
})
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
const fullClearance =
|
|
149
|
-
tickTextWidth +
|
|
150
|
-
Y_AXIS_TICK_PADDING +
|
|
151
|
-
Y_AXIS_LABEL_GAP +
|
|
152
|
-
Y_AXIS_LABEL_BAND_WIDTH +
|
|
153
|
-
Y_AXIS_OUTER_PADDING
|
|
154
|
-
return { tickClearance, fullClearance, labelCenterX }
|
|
155
|
-
})
|
|
111
|
+
const yAxisTickCount = computed(() =>
|
|
112
|
+
resolveYAxisTickCount(innerHeight.value, props.axes?.y?.splitNumber),
|
|
113
|
+
)
|
|
114
|
+
const viewportYDomains = computed(() =>
|
|
115
|
+
resolveViewportYDomains(
|
|
116
|
+
props.displayMode,
|
|
117
|
+
chartTracks.value,
|
|
118
|
+
sharedYDomains.value,
|
|
119
|
+
independentYDomains.value,
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
const yAxisMetrics = computed(() =>
|
|
123
|
+
resolveYAxisLayoutMetrics(
|
|
124
|
+
chartTracks.value,
|
|
125
|
+
props.overlayMode,
|
|
126
|
+
props.yDomain,
|
|
127
|
+
props.yDomains,
|
|
128
|
+
viewportYDomains.value,
|
|
129
|
+
props.axes?.y?.nice !== false,
|
|
130
|
+
yAxisTickCount.value,
|
|
131
|
+
props.displayMode === 'compact',
|
|
132
|
+
),
|
|
133
|
+
)
|
|
156
134
|
const hasYAxisLabels = computed(() =>
|
|
157
135
|
chartTracks.value.some(
|
|
158
136
|
(track) =>
|
|
@@ -179,8 +157,10 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
179
157
|
props.overlayMode,
|
|
180
158
|
props.yDomain,
|
|
181
159
|
props.yDomains,
|
|
182
|
-
|
|
160
|
+
yAxisTickCount.value,
|
|
183
161
|
props.axes?.y?.nice !== false,
|
|
162
|
+
props.displayMode === 'compact',
|
|
163
|
+
viewportYDomains.value,
|
|
184
164
|
),
|
|
185
165
|
)
|
|
186
166
|
const resolvedChartLeftMargin = computed(() =>
|
|
@@ -302,15 +282,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
302
282
|
xDomainStrategy: props.xDomainStrategy,
|
|
303
283
|
fixedYDomain: props.yDomain,
|
|
304
284
|
fixedYDomains: props.yDomains,
|
|
305
|
-
yDomains:
|
|
306
|
-
props.displayMode === 'independent'
|
|
307
|
-
? Object.fromEntries(
|
|
308
|
-
chartTracks.value.flatMap((track, index) => {
|
|
309
|
-
const domain = independentYDomains.value[index]
|
|
310
|
-
return domain ? [[track.id, domain]] : []
|
|
311
|
-
}),
|
|
312
|
-
)
|
|
313
|
-
: sharedYDomains.value,
|
|
285
|
+
yDomains: viewportYDomains.value,
|
|
314
286
|
timeUnit: props.timeUnit,
|
|
315
287
|
xAxisLabelFormatter: props.axes?.x?.labelFormatter,
|
|
316
288
|
yAxisSplitNumber: props.axes?.y?.splitNumber,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { WaveformOverlayMode } from '../data/types'
|
|
2
|
+
import {
|
|
3
|
+
Y_AXIS_CHARACTER_WIDTH,
|
|
4
|
+
Y_AXIS_LABEL_BAND_WIDTH,
|
|
5
|
+
Y_AXIS_LABEL_GAP,
|
|
6
|
+
Y_AXIS_OUTER_PADDING,
|
|
7
|
+
Y_AXIS_TICK_PADDING,
|
|
8
|
+
} from './constants'
|
|
9
|
+
import { axisTextMetrics, resolveRenderedYAxisSeriesGroups } from './layout'
|
|
10
|
+
import type { DisplayTrack } from './types'
|
|
11
|
+
import type { WaveformYDomain } from './yDomain'
|
|
12
|
+
|
|
13
|
+
export interface YAxisLayoutMetrics {
|
|
14
|
+
tickClearance: number
|
|
15
|
+
fullClearance: number
|
|
16
|
+
labelCenterX: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveViewportYDomains(
|
|
20
|
+
displayMode: 'independent' | 'separated' | 'compact',
|
|
21
|
+
chartTracks: DisplayTrack[],
|
|
22
|
+
sharedYDomains: Record<string, [number, number]>,
|
|
23
|
+
independentYDomains: Record<number, [number, number]>,
|
|
24
|
+
): Record<string, [number, number]> {
|
|
25
|
+
if (displayMode !== 'independent') return sharedYDomains
|
|
26
|
+
return Object.fromEntries(
|
|
27
|
+
chartTracks.flatMap((track, index) => {
|
|
28
|
+
const domain = independentYDomains[index]
|
|
29
|
+
return domain ? [[track.id, domain]] : []
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveYAxisLayoutMetrics(
|
|
35
|
+
chartTracks: DisplayTrack[],
|
|
36
|
+
overlayMode: WaveformOverlayMode,
|
|
37
|
+
yDomain: WaveformYDomain | undefined,
|
|
38
|
+
yDomains: Record<string, WaveformYDomain> | undefined,
|
|
39
|
+
viewportYDomains: Record<string, [number, number]>,
|
|
40
|
+
nice: boolean,
|
|
41
|
+
tickCount: number,
|
|
42
|
+
compact: boolean,
|
|
43
|
+
): YAxisLayoutMetrics {
|
|
44
|
+
const axisText = chartTracks
|
|
45
|
+
.filter((track) => track.visibleSeries.length > 0)
|
|
46
|
+
.flatMap((track) =>
|
|
47
|
+
resolveRenderedYAxisSeriesGroups(track, overlayMode, yDomain, yDomains, viewportYDomains),
|
|
48
|
+
)
|
|
49
|
+
.map(
|
|
50
|
+
(group) =>
|
|
51
|
+
axisTextMetrics(
|
|
52
|
+
group.domain,
|
|
53
|
+
nice,
|
|
54
|
+
undefined,
|
|
55
|
+
group.seriesList[0]?.unit,
|
|
56
|
+
tickCount,
|
|
57
|
+
compact,
|
|
58
|
+
).tickTextWidth,
|
|
59
|
+
)
|
|
60
|
+
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
|
61
|
+
const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
|
|
62
|
+
const labelCenterX = -(
|
|
63
|
+
Y_AXIS_TICK_PADDING +
|
|
64
|
+
tickTextWidth +
|
|
65
|
+
Y_AXIS_LABEL_GAP +
|
|
66
|
+
Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
67
|
+
)
|
|
68
|
+
const fullClearance =
|
|
69
|
+
tickTextWidth +
|
|
70
|
+
Y_AXIS_TICK_PADDING +
|
|
71
|
+
Y_AXIS_LABEL_GAP +
|
|
72
|
+
Y_AXIS_LABEL_BAND_WIDTH +
|
|
73
|
+
Y_AXIS_OUTER_PADDING
|
|
74
|
+
return { tickClearance, fullClearance, labelCenterX }
|
|
75
|
+
}
|
|
@@ -2,7 +2,7 @@ import { zoomIdentity } from 'd3'
|
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
|
|
5
|
-
import { buildTrackLayouts, buildYAxisSlots } from './layout'
|
|
5
|
+
import { buildTrackLayouts, buildYAxisSlots, resolveRenderedYAxisSeriesGroups } from './layout'
|
|
6
6
|
import type { DisplaySeries, DisplayTrack } from './types'
|
|
7
7
|
import { Y_AXIS_RIGHT_LABEL_OFFSET } from './yAxisConstants'
|
|
8
8
|
|
|
@@ -126,4 +126,40 @@ describe('Y-axis slots', () => {
|
|
|
126
126
|
expect(fourAxisSlots.slots[1]!.axisOffset).toBeLessThan(0)
|
|
127
127
|
expect(fourAxisSlots.slots[3]!.axisOffset).toBeGreaterThan(0)
|
|
128
128
|
})
|
|
129
|
+
|
|
130
|
+
it('uses dynamic Y domains when measuring multi-axis slots', () => {
|
|
131
|
+
const negativeVoltage = series('negative-voltage', 0, 1)
|
|
132
|
+
negativeVoltage.unit = 'V'
|
|
133
|
+
const longUnit = series('long-unit', 0, 1)
|
|
134
|
+
longUnit.unit = 'volt-amperes-per-phase'
|
|
135
|
+
const sourceTrack = track([negativeVoltage, longUnit])
|
|
136
|
+
const viewportYDomains = { [sourceTrack.id]: [-0.7434, -0.3434] as [number, number] }
|
|
137
|
+
|
|
138
|
+
expect(
|
|
139
|
+
resolveRenderedYAxisSeriesGroups(
|
|
140
|
+
sourceTrack,
|
|
141
|
+
'multi-axis',
|
|
142
|
+
undefined,
|
|
143
|
+
undefined,
|
|
144
|
+
viewportYDomains,
|
|
145
|
+
).map((group) => group.domain),
|
|
146
|
+
).toEqual([
|
|
147
|
+
[-0.7434, -0.3434],
|
|
148
|
+
[-0.7434, -0.3434],
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
const slots = buildYAxisSlots(
|
|
152
|
+
[sourceTrack],
|
|
153
|
+
'multi-axis',
|
|
154
|
+
undefined,
|
|
155
|
+
undefined,
|
|
156
|
+
5,
|
|
157
|
+
false,
|
|
158
|
+
false,
|
|
159
|
+
viewportYDomains,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
expect(slots.clearance.left).toBe(100)
|
|
163
|
+
expect(slots.clearance.right).toBeGreaterThan(180)
|
|
164
|
+
})
|
|
129
165
|
})
|
|
@@ -3,7 +3,8 @@ import { axisBottom, axisLeft, axisRight, select } from 'd3'
|
|
|
3
3
|
import { nextTick, onMounted, ref, watch } from 'vue'
|
|
4
4
|
|
|
5
5
|
import type { WaveformAxesOptions } from '../../types'
|
|
6
|
-
import {
|
|
6
|
+
import { formatXAxisLabel } from '../../utils'
|
|
7
|
+
import { formatYAxisTickLabel } from '../core/layout'
|
|
7
8
|
import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
|
|
8
9
|
import { Y_AXIS_LABEL_BAND_WIDTH } from '../core/yAxisConstants'
|
|
9
10
|
|
|
@@ -43,12 +44,15 @@ function renderAxes() {
|
|
|
43
44
|
props.track.yAxes.forEach((axis, index) => {
|
|
44
45
|
const element = yAxisElements.value[index]
|
|
45
46
|
if (!element) return
|
|
46
|
-
const [axisMin, axisMax] = axis.scale.domain()
|
|
47
|
-
const topTickValue = Math.max(...axis.tickValues)
|
|
48
47
|
const unit = axis.seriesList[0]?.unit
|
|
49
48
|
const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
|
|
50
49
|
.tickFormat((value) =>
|
|
51
|
-
|
|
50
|
+
formatYAxisTickLabel(
|
|
51
|
+
Number(value),
|
|
52
|
+
axis.scale.domain() as [number, number],
|
|
53
|
+
axis.tickValues,
|
|
54
|
+
unit,
|
|
55
|
+
),
|
|
52
56
|
)
|
|
53
57
|
.tickSize(-4)
|
|
54
58
|
.tickPadding(7)
|
|
@@ -6,26 +6,6 @@ import { resizeObservers } from '../../test/setup'
|
|
|
6
6
|
import { gridSeries, mountSizedChart } from '../../test/waveformChart'
|
|
7
7
|
|
|
8
8
|
describe('WaveformChart', () => {
|
|
9
|
-
it('applies the configured Y-axis split number', async () => {
|
|
10
|
-
const wrapper = await mountSizedChart(
|
|
11
|
-
{
|
|
12
|
-
kind: 'points',
|
|
13
|
-
points: [
|
|
14
|
-
{ x: 0, y: 3 },
|
|
15
|
-
{ x: 1, y: 97 },
|
|
16
|
-
],
|
|
17
|
-
},
|
|
18
|
-
{ axes: { y: { splitNumber: 5 } } },
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
expect(
|
|
22
|
-
wrapper
|
|
23
|
-
.get('.waveform-chart__axis--y')
|
|
24
|
-
.findAll('.tick text')
|
|
25
|
-
.map((tick) => tick.text()),
|
|
26
|
-
).toEqual(['0', '25', '50', '75', '100'])
|
|
27
|
-
})
|
|
28
|
-
|
|
29
9
|
it('expands the Y-axis label gutter for signed values and long exponents', async () => {
|
|
30
10
|
const wrapper = await mountSizedChart(
|
|
31
11
|
{
|
|
@@ -52,10 +32,13 @@ describe('WaveformChart', () => {
|
|
|
52
32
|
const labelBackgroundX = Number(
|
|
53
33
|
tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
|
|
54
34
|
)
|
|
35
|
+
const longestTickLabel = Math.max(
|
|
36
|
+
...tracks[0].findAll('.waveform-chart__axis--y .tick text').map((tick) => tick.text().length),
|
|
37
|
+
)
|
|
55
38
|
|
|
56
|
-
expect(labelX).
|
|
39
|
+
expect(Math.abs(labelX)).toBeGreaterThanOrEqual(7 + longestTickLabel * 7 + 6)
|
|
57
40
|
expect(labelBackgroundX).toBe(labelX - 6)
|
|
58
|
-
expect(Number(wrapper.attributes('data-chart-left-margin'))).
|
|
41
|
+
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBeGreaterThanOrEqual(80)
|
|
59
42
|
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(72)
|
|
60
43
|
})
|
|
61
44
|
|
|
@@ -118,12 +101,28 @@ describe('WaveformChart', () => {
|
|
|
118
101
|
{ grid: { rowCount: 1, columnCount: 1 } },
|
|
119
102
|
)
|
|
120
103
|
const initialMargin = wrapper.attributes('data-chart-left-margin')
|
|
121
|
-
|
|
104
|
+
const initialTrack = wrapper.get('.waveform-chart__track')
|
|
105
|
+
const initialLongestTickLabel = Math.max(
|
|
106
|
+
...initialTrack
|
|
107
|
+
.findAll('.waveform-chart__axis--y .tick text')
|
|
108
|
+
.map((tick) => tick.text().length),
|
|
109
|
+
)
|
|
110
|
+
expect(initialTrack.attributes('data-y-axis-label-x')).toBe(
|
|
111
|
+
String(-(7 + initialLongestTickLabel * 7 + 6)),
|
|
112
|
+
)
|
|
122
113
|
|
|
123
114
|
await wrapper.get('.ant-pagination-next button').trigger('click')
|
|
124
115
|
|
|
125
116
|
expect(wrapper.attributes('data-chart-left-margin')).toBe(initialMargin)
|
|
126
|
-
|
|
117
|
+
const pagedTrack = wrapper.get('.waveform-chart__track')
|
|
118
|
+
const pagedLongestTickLabel = Math.max(
|
|
119
|
+
...pagedTrack
|
|
120
|
+
.findAll('.waveform-chart__axis--y .tick text')
|
|
121
|
+
.map((tick) => tick.text().length),
|
|
122
|
+
)
|
|
123
|
+
expect(pagedTrack.attributes('data-y-axis-label-x')).toBe(
|
|
124
|
+
String(-(7 + pagedLongestTickLabel * 7 + 6)),
|
|
125
|
+
)
|
|
127
126
|
})
|
|
128
127
|
|
|
129
128
|
it('hides only secondary-column Y-axis labels when the grid is too narrow', async () => {
|