waveform-analysis 0.1.54 → 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/README.md +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +537 -507
- package/dist/style.css +1 -1
- 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/types.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/types.ts +2 -0
- package/src/components/core/useWaveformChartController.ts +1 -0
- 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/interaction/useWaveformHover.ts +43 -6
- package/src/components/rendering/WaveformHoverLayer.vue +3 -2
- package/src/components/waveformChartCases/incompleteDataHover.test.ts +143 -55
- 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
|
@@ -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
|
+
})
|
|
@@ -62,12 +62,14 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
62
62
|
nextPoints: HoveredSeriesPoint[],
|
|
63
63
|
trackIndex: number | null,
|
|
64
64
|
queryX: number,
|
|
65
|
+
crosshairXByTrack: Record<number, number>,
|
|
65
66
|
position: { x: number; y: number },
|
|
66
67
|
) => {
|
|
67
68
|
if (isPresentationMode.value) return
|
|
68
69
|
if (!hoveredPointsMatch(nextPoints)) hoverState.points = nextPoints
|
|
69
70
|
hoverState.trackIndex = trackIndex
|
|
70
71
|
hoverState.queryX = queryX
|
|
72
|
+
hoverState.crosshairXByTrack = crosshairXByTrack
|
|
71
73
|
hoverState.position = position
|
|
72
74
|
emit('point-hover', hoverState.points.find((point) => point.point)?.point ?? null)
|
|
73
75
|
}
|
|
@@ -76,6 +78,7 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
76
78
|
hoverState.points = []
|
|
77
79
|
hoverState.trackIndex = null
|
|
78
80
|
hoverState.queryX = null
|
|
81
|
+
hoverState.crosshairXByTrack = {}
|
|
79
82
|
emit('point-hover', null)
|
|
80
83
|
}
|
|
81
84
|
const handlePointerLeave = () => {
|
|
@@ -112,9 +115,30 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
112
115
|
const source = series.source ?? pointSourceFromPoints(series.points)
|
|
113
116
|
const first = source.pointAt(0)
|
|
114
117
|
const last = source.pointAt(source.length - 1)
|
|
115
|
-
if (!first || !last
|
|
118
|
+
if (!first || !last) return undefined
|
|
119
|
+
if (xValue <= first.x) return first
|
|
120
|
+
if (xValue >= last.x) return last
|
|
116
121
|
return source.nearestPoint(xValue)
|
|
117
122
|
}
|
|
123
|
+
const crosshairXForTrack = (track: TrackLayout, xValue: number): number | undefined => {
|
|
124
|
+
let longestRange: [number, number] | undefined
|
|
125
|
+
let longestSpan = -Infinity
|
|
126
|
+
|
|
127
|
+
track.seriesList.forEach((series) => {
|
|
128
|
+
const source = series.source ?? pointSourceFromPoints(series.points)
|
|
129
|
+
const first = source.pointAt(0)
|
|
130
|
+
const last = source.pointAt(source.length - 1)
|
|
131
|
+
if (!first || !last) return
|
|
132
|
+
const span = last.x - first.x
|
|
133
|
+
if (span > longestSpan) {
|
|
134
|
+
longestSpan = span
|
|
135
|
+
longestRange = [first.x, last.x]
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
if (!longestRange) return undefined
|
|
140
|
+
return Math.max(longestRange[0], Math.min(longestRange[1], xValue))
|
|
141
|
+
}
|
|
118
142
|
|
|
119
143
|
const handleIndependentPointerMove = (event: PointerEvent, trackIndex: number) => {
|
|
120
144
|
if (isPresentationMode.value) return
|
|
@@ -135,10 +159,17 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
135
159
|
const nextPoints = track.seriesList.map((series) =>
|
|
136
160
|
createHoveredSeriesPoint(series, trackIndex, nearestPoint(series, xValue) ?? null),
|
|
137
161
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
const crosshairX = crosshairXForTrack(track, xValue)
|
|
163
|
+
commitHover(
|
|
164
|
+
nextPoints,
|
|
165
|
+
trackIndex,
|
|
166
|
+
xValue,
|
|
167
|
+
crosshairX === undefined ? {} : { [trackIndex]: crosshairX },
|
|
168
|
+
{
|
|
169
|
+
x: resolvedChartLeftMargin.value + track.left + pointerX,
|
|
170
|
+
y: titleAreaHeight.value + chartTopMargin.value + track.top + pointerY,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
142
173
|
})
|
|
143
174
|
}
|
|
144
175
|
const handleSharedPointerMove = (event: PointerEvent) => {
|
|
@@ -166,7 +197,13 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
166
197
|
createHoveredSeriesPoint(series, track.index, nearestPoint(series, xValue) ?? null),
|
|
167
198
|
),
|
|
168
199
|
)
|
|
169
|
-
|
|
200
|
+
const crosshairXByTrack = Object.fromEntries(
|
|
201
|
+
trackLayouts.value.flatMap((track) => {
|
|
202
|
+
const crosshairX = crosshairXForTrack(track, xValue)
|
|
203
|
+
return crosshairX === undefined ? [] : [[track.index, crosshairX]]
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
commitHover(nextPoints, null, xValue, crosshairXByTrack, {
|
|
170
207
|
x: resolvedChartLeftMargin.value + pointerX,
|
|
171
208
|
y: titleAreaHeight.value + chartTopMargin.value + pointerY,
|
|
172
209
|
})
|
|
@@ -20,8 +20,9 @@ const crosshairs = computed<Crosshair[]>(() => {
|
|
|
20
20
|
|
|
21
21
|
return props.tracks.flatMap((track) => {
|
|
22
22
|
const trackIsActive = props.state.trackIndex === null || props.state.trackIndex === track.index
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const crosshairX = props.state.crosshairXByTrack[track.index]
|
|
24
|
+
return trackIsActive && !track.isEmpty && track.hasVisibleSeries && crosshairX !== undefined
|
|
25
|
+
? [{ track, x: track.xScale(crosshairX) }]
|
|
25
26
|
: []
|
|
26
27
|
})
|
|
27
28
|
})
|
|
@@ -5,63 +5,151 @@ import { flushAnimationFrames } from '../../test/setup'
|
|
|
5
5
|
import { mountSizedChart } from '../../test/waveformChart'
|
|
6
6
|
|
|
7
7
|
describe('WaveformChart incomplete data hover', () => {
|
|
8
|
-
it('
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
it.each(['separated', 'compact'] as const)(
|
|
9
|
+
'clamps each track crosshair and tooltip values to data endpoints in %s mode',
|
|
10
|
+
async (displayMode) => {
|
|
11
|
+
const wrapper = await mountSizedChart(
|
|
12
|
+
{
|
|
13
|
+
kind: 'series',
|
|
14
|
+
series: [
|
|
15
|
+
{
|
|
16
|
+
name: '完整数据',
|
|
17
|
+
data: {
|
|
18
|
+
kind: 'points',
|
|
19
|
+
points: [
|
|
20
|
+
{ x: -1, y: 1 },
|
|
21
|
+
{ x: -0.5, y: 2 },
|
|
22
|
+
{ x: 1, y: 3 },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
22
25
|
},
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
{
|
|
27
|
+
name: '从零开始',
|
|
28
|
+
data: {
|
|
29
|
+
kind: 'points',
|
|
30
|
+
points: [
|
|
31
|
+
{ x: 0, y: 10 },
|
|
32
|
+
{ x: 1, y: 20 },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
32
35
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
})
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{ displayMode },
|
|
39
|
+
)
|
|
40
|
+
const overlay = wrapper.get('.waveform-chart__overlay--shared')
|
|
41
|
+
const overlayWidth = Number(overlay.attributes('width'))
|
|
42
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
43
|
+
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
44
|
+
})
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
overlay.element.dispatchEvent(
|
|
47
|
+
new MouseEvent('pointermove', {
|
|
48
|
+
clientX: overlayWidth / 4,
|
|
49
|
+
clientY: 120,
|
|
50
|
+
bubbles: true,
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
flushAnimationFrames()
|
|
54
|
+
await flushPromises()
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
const crosshairPositions = wrapper
|
|
57
|
+
.findAll('.waveform-chart__crosshair line')
|
|
58
|
+
.map((line) => Number(line.attributes('x1')))
|
|
59
|
+
expect(crosshairPositions).toHaveLength(2)
|
|
60
|
+
expect(crosshairPositions[0]).toBeCloseTo(overlayWidth / 4)
|
|
61
|
+
expect(crosshairPositions[1]).toBeCloseTo(overlayWidth / 2)
|
|
62
|
+
const tooltipRows = wrapper.findAll('.waveform-chart__tooltip-series')
|
|
63
|
+
expect(tooltipRows).toHaveLength(2)
|
|
64
|
+
expect(tooltipRows[0].text()).toContain('(x:-500 y:2)')
|
|
65
|
+
expect(tooltipRows[1].text()).toContain('(x:0 y:10)')
|
|
66
|
+
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: -0.5, y: 2 }])
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
it.each(['separated', 'compact'] as const)(
|
|
71
|
+
'uses the longest series for a shared track crosshair while clamping each tooltip row in %s mode',
|
|
72
|
+
async (displayMode) => {
|
|
73
|
+
const wrapper = await mountSizedChart(
|
|
74
|
+
{
|
|
75
|
+
kind: 'series',
|
|
76
|
+
series: [
|
|
77
|
+
{
|
|
78
|
+
name: '范围参考',
|
|
79
|
+
data: {
|
|
80
|
+
kind: 'points',
|
|
81
|
+
points: [
|
|
82
|
+
{ x: -2, y: 1 },
|
|
83
|
+
{ x: 2, y: 2 },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: '最长曲线',
|
|
89
|
+
trackId: 'same-track',
|
|
90
|
+
data: {
|
|
91
|
+
kind: 'points',
|
|
92
|
+
points: [
|
|
93
|
+
{ x: 0, y: 10 },
|
|
94
|
+
{ x: 1, y: 20 },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: '较短曲线',
|
|
100
|
+
trackId: 'same-track',
|
|
101
|
+
data: {
|
|
102
|
+
kind: 'points',
|
|
103
|
+
points: [
|
|
104
|
+
{ x: 0.25, y: 30 },
|
|
105
|
+
{ x: 0.5, y: 40 },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{ displayMode },
|
|
112
|
+
)
|
|
113
|
+
const overlay = wrapper.get('.waveform-chart__overlay--shared')
|
|
114
|
+
const overlayWidth = Number(overlay.attributes('width'))
|
|
115
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
116
|
+
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
overlay.element.dispatchEvent(
|
|
120
|
+
new MouseEvent('pointermove', {
|
|
121
|
+
clientX: overlayWidth / 8,
|
|
122
|
+
clientY: 120,
|
|
123
|
+
bubbles: true,
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
flushAnimationFrames()
|
|
127
|
+
await flushPromises()
|
|
128
|
+
|
|
129
|
+
let crosshairPositions = wrapper
|
|
130
|
+
.findAll('.waveform-chart__crosshair line')
|
|
131
|
+
.map((line) => Number(line.attributes('x1')))
|
|
132
|
+
expect(crosshairPositions).toHaveLength(2)
|
|
133
|
+
expect(crosshairPositions[1]).toBeCloseTo(overlayWidth / 2)
|
|
134
|
+
expect(wrapper.text()).toContain('最长曲线(x:0 y:10)')
|
|
135
|
+
expect(wrapper.text()).toContain('较短曲线(x:250 y:30)')
|
|
136
|
+
|
|
137
|
+
overlay.element.dispatchEvent(
|
|
138
|
+
new MouseEvent('pointermove', {
|
|
139
|
+
clientX: (overlayWidth * 7) / 8,
|
|
140
|
+
clientY: 120,
|
|
141
|
+
bubbles: true,
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
flushAnimationFrames()
|
|
145
|
+
await flushPromises()
|
|
146
|
+
|
|
147
|
+
crosshairPositions = wrapper
|
|
148
|
+
.findAll('.waveform-chart__crosshair line')
|
|
149
|
+
.map((line) => Number(line.attributes('x1')))
|
|
150
|
+
expect(crosshairPositions[1]).toBeCloseTo((overlayWidth * 3) / 4)
|
|
151
|
+
expect(wrapper.text()).toContain('最长曲线(x:1,000 y:20)')
|
|
152
|
+
expect(wrapper.text()).toContain('较短曲线(x:500 y:40)')
|
|
153
|
+
},
|
|
154
|
+
)
|
|
67
155
|
})
|
|
@@ -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',
|