waveform-analysis 0.1.55 → 0.1.57
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 +510 -495
- 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/yAxisConstants.d.ts +1 -0
- package/package.json +1 -1
- package/src/App.test.ts +2 -1
- 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/useWaveformLayout.ts +16 -22
- 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/data/simulatedWaveforms.test.ts +4 -4
- package/src/data/simulatedWaveforms.ts +12 -0
|
@@ -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[];
|
|
@@ -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
|
'脉冲响应',
|
|
@@ -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,
|
|
@@ -27,7 +27,7 @@ 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'
|
|
@@ -169,33 +169,24 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
169
169
|
: 0,
|
|
170
170
|
),
|
|
171
171
|
)
|
|
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 },
|
|
172
|
+
const yAxisSlots = computed(() =>
|
|
173
|
+
buildYAxisSlots(
|
|
174
|
+
layoutTracks.value.filter((track) => track.visibleSeries.length > 0),
|
|
175
|
+
props.overlayMode,
|
|
176
|
+
props.yDomain,
|
|
177
|
+
props.yDomains,
|
|
178
|
+
props.axes?.y?.splitNumber,
|
|
179
|
+
props.axes?.y?.nice !== false,
|
|
189
180
|
),
|
|
190
181
|
)
|
|
191
182
|
const resolvedChartLeftMargin = computed(() =>
|
|
192
183
|
props.overlayMode === 'multi-axis'
|
|
193
|
-
? Math.max(chartLeftMargin.value,
|
|
184
|
+
? Math.max(chartLeftMargin.value, yAxisSlots.value.clearance.left)
|
|
194
185
|
: chartLeftMargin.value,
|
|
195
186
|
)
|
|
196
187
|
const chartRightMargin = computed(() =>
|
|
197
188
|
props.overlayMode === 'multi-axis'
|
|
198
|
-
? Math.max(margin.right,
|
|
189
|
+
? Math.max(margin.right, yAxisSlots.value.clearance.right)
|
|
199
190
|
: margin.right,
|
|
200
191
|
)
|
|
201
192
|
const innerWidth = computed(() =>
|
|
@@ -212,7 +203,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
212
203
|
return {
|
|
213
204
|
horizontalGap:
|
|
214
205
|
props.overlayMode === 'multi-axis' && hasMultipleColumns && hasVisibleWaveformData.value
|
|
215
|
-
? Math.max(baseGap,
|
|
206
|
+
? Math.max(baseGap, yAxisSlots.value.clearance.left + yAxisSlots.value.clearance.right)
|
|
216
207
|
: hasMultipleColumns && hasVisibleWaveformData.value
|
|
217
208
|
? hasYAxisLabels.value && canReserveLabelClearance
|
|
218
209
|
? fullGap
|
|
@@ -323,7 +314,10 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
323
314
|
rendering: renderingOptions.value,
|
|
324
315
|
linePointOverrides: linePointOverrides?.value,
|
|
325
316
|
hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
|
|
326
|
-
yAxisLabelX:
|
|
317
|
+
yAxisLabelX:
|
|
318
|
+
yAxisSlots.value.slots.find((slot) => slot.side === 'left' && slot.sideIndex === 0)
|
|
319
|
+
?.labelOffset ?? yAxisMetrics.value.labelCenterX,
|
|
320
|
+
yAxisSlots: props.overlayMode === 'multi-axis' ? yAxisSlots.value.slots : undefined,
|
|
327
321
|
showCompactEmptyTracks: props.displayMode === 'compact' && hasWaveformData.value,
|
|
328
322
|
}),
|
|
329
323
|
)
|
|
@@ -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']
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { zoomIdentity } from 'd3'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
|
|
5
|
+
import { buildTrackLayouts, buildYAxisSlots } from './layout'
|
|
6
|
+
import type { DisplaySeries, DisplayTrack } from './types'
|
|
7
|
+
import { Y_AXIS_RIGHT_LABEL_OFFSET } from './yAxisConstants'
|
|
8
|
+
|
|
9
|
+
function series(id: string, minimum: number, maximum: number): DisplaySeries {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
name: id,
|
|
13
|
+
color: '#1677ff',
|
|
14
|
+
lineType: 'linear',
|
|
15
|
+
lineStyle: 'solid',
|
|
16
|
+
pointType: 'none',
|
|
17
|
+
errorBar: { visible: false, width: 1.5, capWidth: 8 },
|
|
18
|
+
points: [
|
|
19
|
+
{ x: 0, y: minimum },
|
|
20
|
+
{ x: 1, y: maximum },
|
|
21
|
+
],
|
|
22
|
+
xDomain: [0, 1],
|
|
23
|
+
yDomain: [minimum, maximum],
|
|
24
|
+
hasErrorPoints: false,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function track(seriesList: DisplaySeries[]): DisplayTrack {
|
|
29
|
+
return {
|
|
30
|
+
id: seriesList[0]!.id,
|
|
31
|
+
series: seriesList,
|
|
32
|
+
visibleSeries: seriesList,
|
|
33
|
+
xDomain: [0, 1],
|
|
34
|
+
yDomain: [0, 1],
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('Y-axis slots', () => {
|
|
39
|
+
it('aligns matching slots across single and multi-axis tracks', () => {
|
|
40
|
+
const tracks = [
|
|
41
|
+
track([series('single', -1e120, 1e120)]),
|
|
42
|
+
track([series('left', 0, 1), series('right', 1000, 3000)]),
|
|
43
|
+
track([series('left-two', 0, 1), series('right-two', 1000, 3000)]),
|
|
44
|
+
]
|
|
45
|
+
const yAxisSlots = buildYAxisSlots(tracks, 'multi-axis')
|
|
46
|
+
const layouts = buildTrackLayouts({
|
|
47
|
+
cells: tracks.map((series, index) => ({
|
|
48
|
+
slotIndex: index,
|
|
49
|
+
row: index,
|
|
50
|
+
column: 0,
|
|
51
|
+
left: 0,
|
|
52
|
+
top: index * 100,
|
|
53
|
+
width: 600,
|
|
54
|
+
height: 100,
|
|
55
|
+
plotHeight: 100,
|
|
56
|
+
cellHeight: 130,
|
|
57
|
+
xAxisBand: 30,
|
|
58
|
+
series,
|
|
59
|
+
})),
|
|
60
|
+
grid: {
|
|
61
|
+
rowCount: 3,
|
|
62
|
+
columnCount: 1,
|
|
63
|
+
showPagination: false,
|
|
64
|
+
fillIncompleteLastRow: false,
|
|
65
|
+
trackLines: {},
|
|
66
|
+
},
|
|
67
|
+
displayMode: 'independent',
|
|
68
|
+
overlayMode: 'multi-axis',
|
|
69
|
+
independentTransforms: [zoomIdentity, zoomIdentity, zoomIdentity],
|
|
70
|
+
sharedZoomDomain: [0, 1],
|
|
71
|
+
timeUnit: 'ms',
|
|
72
|
+
rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
73
|
+
hideSecondaryLabels: false,
|
|
74
|
+
yAxisLabelX: yAxisSlots.slots.find((slot) => slot.side === 'left')!.labelOffset,
|
|
75
|
+
yAxisSlots: yAxisSlots.slots,
|
|
76
|
+
showCompactEmptyTracks: false,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
expect(layouts.map((layout) => layout.yAxes[0]?.labelX)).toEqual([
|
|
80
|
+
layouts[0]?.yAxes[0]?.labelX,
|
|
81
|
+
layouts[0]?.yAxes[0]?.labelX,
|
|
82
|
+
layouts[0]?.yAxes[0]?.labelX,
|
|
83
|
+
])
|
|
84
|
+
expect(layouts[1]?.yAxes[1]?.labelX).toBe(layouts[2]?.yAxes[1]?.labelX)
|
|
85
|
+
expect(layouts[1]?.yAxes[1]?.labelX).toBe(
|
|
86
|
+
600 +
|
|
87
|
+
yAxisSlots.slots.find((slot) => slot.side === 'right')!.labelOffset -
|
|
88
|
+
Y_AXIS_RIGHT_LABEL_OFFSET,
|
|
89
|
+
)
|
|
90
|
+
expect(yAxisSlots.slots).toMatchObject([
|
|
91
|
+
{ side: 'left', sideIndex: 0, axisOffset: 0 },
|
|
92
|
+
{ side: 'right', sideIndex: 0, axisOffset: 0 },
|
|
93
|
+
])
|
|
94
|
+
expect(yAxisSlots.clearance.left).toBeGreaterThan(yAxisSlots.clearance.right)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('reserves outward second slots for three and four value axes', () => {
|
|
98
|
+
const threeAxisSlots = buildYAxisSlots(
|
|
99
|
+
[track([series('left', 0, 1), series('right-one', 10, 20), series('right-two', 30, 40)])],
|
|
100
|
+
'multi-axis',
|
|
101
|
+
)
|
|
102
|
+
const fourAxisSlots = buildYAxisSlots(
|
|
103
|
+
[
|
|
104
|
+
track([
|
|
105
|
+
series('left-one', 0, 1),
|
|
106
|
+
series('left-two', 10, 20),
|
|
107
|
+
series('right-one', 30, 40),
|
|
108
|
+
series('right-two', 50, 60),
|
|
109
|
+
]),
|
|
110
|
+
],
|
|
111
|
+
'multi-axis',
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
expect(threeAxisSlots.slots).toMatchObject([
|
|
115
|
+
{ side: 'left', sideIndex: 0, axisOffset: 0 },
|
|
116
|
+
{ side: 'right', sideIndex: 0, axisOffset: 0 },
|
|
117
|
+
{ side: 'right', sideIndex: 1 },
|
|
118
|
+
])
|
|
119
|
+
expect(threeAxisSlots.slots[2]!.axisOffset).toBeGreaterThan(0)
|
|
120
|
+
expect(fourAxisSlots.slots).toMatchObject([
|
|
121
|
+
{ side: 'left', sideIndex: 0, axisOffset: 0 },
|
|
122
|
+
{ side: 'left', sideIndex: 1 },
|
|
123
|
+
{ side: 'right', sideIndex: 0, axisOffset: 0 },
|
|
124
|
+
{ side: 'right', sideIndex: 1 },
|
|
125
|
+
])
|
|
126
|
+
expect(fourAxisSlots.slots[1]!.axisOffset).toBeLessThan(0)
|
|
127
|
+
expect(fourAxisSlots.slots[3]!.axisOffset).toBeGreaterThan(0)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -86,7 +86,7 @@ describe('WaveformChart', () => {
|
|
|
86
86
|
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(60)
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
-
it('
|
|
89
|
+
it('updates the Y-axis label position while preserving the chart gutter between pages', async () => {
|
|
90
90
|
const wrapper = await mountSizedChart(
|
|
91
91
|
{
|
|
92
92
|
kind: 'series',
|
|
@@ -118,14 +118,12 @@ describe('WaveformChart', () => {
|
|
|
118
118
|
{ grid: { rowCount: 1, columnCount: 1 } },
|
|
119
119
|
)
|
|
120
120
|
const initialMargin = wrapper.attributes('data-chart-left-margin')
|
|
121
|
-
|
|
121
|
+
expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-34')
|
|
122
122
|
|
|
123
123
|
await wrapper.get('.ant-pagination-next button').trigger('click')
|
|
124
124
|
|
|
125
125
|
expect(wrapper.attributes('data-chart-left-margin')).toBe(initialMargin)
|
|
126
|
-
expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe(
|
|
127
|
-
initialLabelX,
|
|
128
|
-
)
|
|
126
|
+
expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-62')
|
|
129
127
|
})
|
|
130
128
|
|
|
131
129
|
it('hides only secondary-column Y-axis labels when the grid is too narrow', async () => {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { mountSizedChart } from '../../test/waveformChart'
|
|
4
|
+
|
|
5
|
+
describe('WaveformChart multi-axis labels', () => {
|
|
6
|
+
it('aligns single-axis and multi-axis Y-axis titles in one column', async () => {
|
|
7
|
+
const wrapper = await mountSizedChart(
|
|
8
|
+
{
|
|
9
|
+
kind: 'series',
|
|
10
|
+
series: [
|
|
11
|
+
{
|
|
12
|
+
id: 'single',
|
|
13
|
+
trackId: 'single-track',
|
|
14
|
+
name: '单轴',
|
|
15
|
+
data: {
|
|
16
|
+
kind: 'points',
|
|
17
|
+
points: [
|
|
18
|
+
{ x: 0, y: -1e120 },
|
|
19
|
+
{ x: 1, y: 1e120 },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'left',
|
|
25
|
+
trackId: 'multi-track',
|
|
26
|
+
name: '左轴',
|
|
27
|
+
data: {
|
|
28
|
+
kind: 'points',
|
|
29
|
+
points: [
|
|
30
|
+
{ x: 0, y: 0 },
|
|
31
|
+
{ x: 1, y: 1 },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'right',
|
|
37
|
+
trackId: 'multi-track',
|
|
38
|
+
name: '右轴',
|
|
39
|
+
data: {
|
|
40
|
+
kind: 'points',
|
|
41
|
+
points: [
|
|
42
|
+
{ x: 0, y: 1000 },
|
|
43
|
+
{ x: 1, y: 3000 },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{ overlayMode: 'multi-axis', grid: { rowCount: 2, columnCount: 1 } },
|
|
50
|
+
)
|
|
51
|
+
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
52
|
+
const titleX = (transform: string | undefined) =>
|
|
53
|
+
Number(transform?.match(/translate\(([^,]+)/)?.[1])
|
|
54
|
+
|
|
55
|
+
expect(titleX(tracks[0]!.get('.waveform-chart__y-axis-label').attributes('transform'))).toBe(
|
|
56
|
+
titleX(
|
|
57
|
+
tracks[1]!.findAll('.waveform-track__multi-axis-title text')[0]!.attributes('transform'),
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import { createSimulatedWaveformData } from './simulatedWaveforms'
|
|
4
4
|
|
|
5
5
|
describe('simulated waveform data', () => {
|
|
6
|
-
it('creates deterministic, finite
|
|
6
|
+
it('creates deterministic, finite eight-channel data with a three-series second frame', () => {
|
|
7
7
|
const first = createSimulatedWaveformData()
|
|
8
8
|
const second = createSimulatedWaveformData()
|
|
9
9
|
|
|
@@ -11,12 +11,12 @@ describe('simulated waveform data', () => {
|
|
|
11
11
|
expect(first.kind).toBe('series')
|
|
12
12
|
if (first.kind !== 'series') return
|
|
13
13
|
|
|
14
|
-
expect(first.series).toHaveLength(
|
|
15
|
-
expect(new Set(first.series.map((series) => series.id)).size).toBe(
|
|
14
|
+
expect(first.series).toHaveLength(8)
|
|
15
|
+
expect(new Set(first.series.map((series) => series.id)).size).toBe(8)
|
|
16
16
|
const secondFrame = first.series.filter(
|
|
17
17
|
(series) => series.trackId === 'simulated-harmonic-frame',
|
|
18
18
|
)
|
|
19
|
-
expect(secondFrame).toHaveLength(
|
|
19
|
+
expect(secondFrame).toHaveLength(3)
|
|
20
20
|
expect(new Set(first.series.map((series) => series.shotNo))).toEqual(new Set(['13300']))
|
|
21
21
|
const firstSeries = first.series[0]
|
|
22
22
|
expect(firstSeries).toMatchObject({
|
|
@@ -84,6 +84,18 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
84
84
|
signal: (time) =>
|
|
85
85
|
0.62 * Math.sin(TWO_PI * 0.55 * time + 0.55) + 0.18 * Math.sin(TWO_PI * 2.2 * time - 0.2),
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
id: 'simulated-harmonic-correction',
|
|
89
|
+
trackId: 'simulated-harmonic-frame',
|
|
90
|
+
shotNo: '13300',
|
|
91
|
+
name: '谐波校正',
|
|
92
|
+
unit: 'V',
|
|
93
|
+
color: '#722ed1',
|
|
94
|
+
lineType: 'linear',
|
|
95
|
+
pointType: 'none',
|
|
96
|
+
signal: (time) =>
|
|
97
|
+
0.48 * Math.sin(TWO_PI * 0.55 * time - 0.35) + 0.12 * Math.sin(TWO_PI * 2.2 * time + 0.7),
|
|
98
|
+
},
|
|
87
99
|
{
|
|
88
100
|
id: 'simulated-damped',
|
|
89
101
|
shotNo: '13300',
|