waveform-analysis 0.1.43 → 0.1.47
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 +37 -30
- package/dist/index.cjs +1 -1
- package/dist/index.js +456 -461
- package/dist/style.css +1 -1
- package/dist/types/components/core/constants.d.ts +2 -2
- package/dist/types/components/core/grid.d.ts +3 -0
- package/dist/types/components/core/types.d.ts +4 -2
- package/dist/types/components/core/yAxisConstants.d.ts +2 -2
- package/dist/types/components/interaction/WaveformTooltip.vue.d.ts +1 -1
- package/dist/types/components/interaction/useWaveformViewport.d.ts +2 -1
- package/package.json +1 -1
- package/src/App.test.ts +2 -7
- package/src/components/WaveformChart.css +1 -1
- package/src/components/WaveformChartView.vue +2 -2
- package/src/components/core/constants.ts +3 -3
- package/src/components/core/grid.test.ts +13 -1
- package/src/components/core/grid.ts +14 -4
- package/src/components/core/layout.test.ts +3 -3
- package/src/components/core/layout.ts +2 -2
- package/src/components/core/trackLayoutBuilder.ts +14 -23
- package/src/components/core/types.ts +4 -2
- package/src/components/core/useWaveformChartController.ts +1 -0
- package/src/components/core/useWaveformLayout.ts +12 -6
- package/src/components/core/yAxisConstants.ts +2 -2
- package/src/components/interaction/WaveformHoverHost.vue +3 -1
- package/src/components/interaction/WaveformTooltip.test.ts +23 -0
- package/src/components/interaction/WaveformTooltip.vue +9 -6
- package/src/components/interaction/useWaveformHover.ts +21 -15
- package/src/components/interaction/useWaveformViewport.ts +20 -15
- package/src/components/interaction/useWaveformZoom.ts +1 -1
- package/src/components/rendering/WaveformHoverLayer.vue +5 -11
- package/src/components/rendering/WaveformTrack.vue +1 -0
- package/src/components/rendering/WaveformTrackAxes.vue +8 -6
- package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -1
- package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +10 -0
- package/src/components/waveformChartCases/incompleteDataHover.test.ts +67 -0
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +5 -5
- package/src/components/waveformChartCases/legendVisibility.test.ts +1 -1
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +50 -1
- package/src/components/waveformChartCases/trackOrder.test.ts +131 -0
- package/src/data/simulatedWaveforms.test.ts +7 -2
- package/src/data/simulatedWaveforms.ts +12 -7
|
@@ -62,18 +62,21 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
62
62
|
const commitHover = (
|
|
63
63
|
nextPoints: HoveredSeriesPoint[],
|
|
64
64
|
trackIndex: number | null,
|
|
65
|
+
queryX: 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
|
|
71
|
+
hoverState.queryX = queryX
|
|
70
72
|
hoverState.position = position
|
|
71
|
-
emit('point-hover', hoverState.points
|
|
73
|
+
emit('point-hover', hoverState.points.find((point) => point.point)?.point ?? null)
|
|
72
74
|
}
|
|
73
75
|
const clearHover = () => {
|
|
74
76
|
cancelPendingHover()
|
|
75
77
|
hoverState.points = []
|
|
76
78
|
hoverState.trackIndex = null
|
|
79
|
+
hoverState.queryX = null
|
|
77
80
|
emit('point-hover', null)
|
|
78
81
|
}
|
|
79
82
|
const handlePointerLeave = () => {
|
|
@@ -82,7 +85,7 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
82
85
|
const createHoveredSeriesPoint = (
|
|
83
86
|
series: DisplaySeries,
|
|
84
87
|
trackIndex: number,
|
|
85
|
-
point: WaveformPoint,
|
|
88
|
+
point: WaveformPoint | null,
|
|
86
89
|
): HoveredSeriesPoint => ({
|
|
87
90
|
id: series.id,
|
|
88
91
|
shotNo: series.shotNo,
|
|
@@ -106,8 +109,12 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
106
109
|
clearHover()
|
|
107
110
|
return true
|
|
108
111
|
}
|
|
109
|
-
const nearestPoint = (series: DisplaySeries, xValue: number): WaveformPoint | undefined =>
|
|
110
|
-
series.points[
|
|
112
|
+
const nearestPoint = (series: DisplaySeries, xValue: number): WaveformPoint | undefined => {
|
|
113
|
+
const firstPoint = series.points[0]
|
|
114
|
+
const lastPoint = series.points.at(-1)
|
|
115
|
+
if (!firstPoint || !lastPoint || xValue < firstPoint.x || xValue > lastPoint.x) return undefined
|
|
116
|
+
return series.points[pointBisector.center(series.points, xValue)]
|
|
117
|
+
}
|
|
111
118
|
|
|
112
119
|
const handleIndependentPointerMove = (event: PointerEvent, trackIndex: number) => {
|
|
113
120
|
if (isPresentationMode.value) return
|
|
@@ -125,11 +132,10 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
125
132
|
const currentTrack = trackLayouts.value[trackIndex]
|
|
126
133
|
if (!currentTrack || !currentTrack.hasVisibleSeries || currentTrack !== track) return
|
|
127
134
|
const xValue = track.xScale.invert(Math.max(0, Math.min(innerWidth.value, pointerX)))
|
|
128
|
-
const nextPoints = track.seriesList.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
commitHover(nextPoints, trackIndex, {
|
|
135
|
+
const nextPoints = track.seriesList.map((series) =>
|
|
136
|
+
createHoveredSeriesPoint(series, trackIndex, nearestPoint(series, xValue) ?? null),
|
|
137
|
+
)
|
|
138
|
+
commitHover(nextPoints, trackIndex, xValue, {
|
|
133
139
|
x: resolvedChartLeftMargin.value + track.left + pointerX,
|
|
134
140
|
y: titleAreaHeight.value + chartTopMargin.value + track.top + pointerY,
|
|
135
141
|
})
|
|
@@ -147,7 +153,8 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
147
153
|
scheduleHover(() => {
|
|
148
154
|
const resolvedTrack = resolveTrackAtPointer(pointerX, pointerY)
|
|
149
155
|
const referenceTrack =
|
|
150
|
-
resolvedTrack
|
|
156
|
+
(resolvedTrack?.hasVisibleSeries ? resolvedTrack : undefined) ??
|
|
157
|
+
trackLayouts.value.find((track) => track.hasVisibleSeries)
|
|
151
158
|
if (!referenceTrack) return
|
|
152
159
|
const localPointerX = Math.max(
|
|
153
160
|
0,
|
|
@@ -155,12 +162,11 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
155
162
|
)
|
|
156
163
|
const xValue = referenceTrack.xScale.invert(localPointerX)
|
|
157
164
|
const nextPoints = trackLayouts.value.flatMap((track) =>
|
|
158
|
-
track.seriesList.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}),
|
|
165
|
+
track.seriesList.map((series) =>
|
|
166
|
+
createHoveredSeriesPoint(series, track.index, nearestPoint(series, xValue) ?? null),
|
|
167
|
+
),
|
|
162
168
|
)
|
|
163
|
-
commitHover(nextPoints, null, {
|
|
169
|
+
commitHover(nextPoints, null, xValue, {
|
|
164
170
|
x: resolvedChartLeftMargin.value + pointerX,
|
|
165
171
|
y: titleAreaHeight.value + chartTopMargin.value + pointerY,
|
|
166
172
|
})
|
|
@@ -3,11 +3,8 @@ import { computed, nextTick, shallowRef, type ComputedRef, type Ref, type Shallo
|
|
|
3
3
|
import { MINIMUM_SELECTION_SIZE } from '../core/constants'
|
|
4
4
|
import type { DisplayTrack, TrackLayout } from '../core/types'
|
|
5
5
|
import { hasFixedYDomainForTrack } from '../core/yDomain'
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
ViewportSelectionState,
|
|
9
|
-
WaveformChartEmit,
|
|
10
|
-
} from '../core/waveformChartTypes'
|
|
6
|
+
import type { ResolvedWaveformChartProps, ViewportSelectionState } from '../core/waveformChartTypes'
|
|
7
|
+
import type { WaveformChartEmit } from '../core/waveformChartTypes'
|
|
11
8
|
import type { AnnotationSeriesCandidate } from '../annotation'
|
|
12
9
|
import { tryReleasePointerCapture } from './pointerCapture'
|
|
13
10
|
import { transitionViewportInteraction } from './viewportInteractionState'
|
|
@@ -104,10 +101,12 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
104
101
|
}
|
|
105
102
|
const currentYDomains = (): Record<string, [number, number]> =>
|
|
106
103
|
Object.fromEntries(
|
|
107
|
-
trackLayouts.value
|
|
108
|
-
track
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
trackLayouts.value
|
|
105
|
+
.filter((track) => track.hasVisibleSeries)
|
|
106
|
+
.map((track) => [
|
|
107
|
+
track.series?.trackId ?? track.series?.id ?? track.id,
|
|
108
|
+
track.yScale.domain() as [number, number],
|
|
109
|
+
]),
|
|
111
110
|
)
|
|
112
111
|
const beginViewportDrag = (event: PointerEvent, trackIndex: number, independent: boolean) => {
|
|
113
112
|
if (isPresentationMode.value) return
|
|
@@ -142,8 +141,10 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
142
141
|
const beginSharedViewportDrag = (event: PointerEvent) => {
|
|
143
142
|
if (!sharedOverlayElement.value) return
|
|
144
143
|
const [x, y] = pointer(event, sharedOverlayElement.value)
|
|
144
|
+
const pointedTrack = resolveTrackAtPointer(x, y)
|
|
145
145
|
const track =
|
|
146
|
-
|
|
146
|
+
(pointedTrack?.hasVisibleSeries ? pointedTrack : undefined) ??
|
|
147
|
+
trackLayouts.value.find((item) => item.hasVisibleSeries)
|
|
147
148
|
if (track) beginViewportDrag(event, track.index, false)
|
|
148
149
|
}
|
|
149
150
|
const applyPan = (active: ViewportSelectionState, track: TrackLayout) => {
|
|
@@ -166,7 +167,9 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
166
167
|
} else {
|
|
167
168
|
sharedTransform.value = transformForDomain(nextX, sourceXDomain, innerWidth.value)
|
|
168
169
|
}
|
|
169
|
-
const targets = active.independent
|
|
170
|
+
const targets = active.independent
|
|
171
|
+
? [track]
|
|
172
|
+
: trackLayouts.value.filter((target) => target.hasVisibleSeries)
|
|
170
173
|
const nextIndependentDomains = { ...independentYDomains.value }
|
|
171
174
|
const nextSharedDomains = { ...sharedYDomains.value }
|
|
172
175
|
targets.forEach((target) => {
|
|
@@ -174,7 +177,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
174
177
|
if (chartTrack && hasFixedYDomainForTrack(chartTrack, props.yDomain, props.yDomains)) {
|
|
175
178
|
return
|
|
176
179
|
}
|
|
177
|
-
const key = target.series
|
|
180
|
+
const key = target.series?.trackId ?? target.series?.id ?? target.id
|
|
178
181
|
const source = active.yDomains[key] ?? (target.yScale.domain() as [number, number])
|
|
179
182
|
const boundary = chartTrack?.yDomain ?? source
|
|
180
183
|
const ySpan = source[1] - source[0]
|
|
@@ -251,16 +254,18 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
251
254
|
} else {
|
|
252
255
|
sharedTransform.value = transformForDomain(xDomain, baseXDomain, innerWidth.value)
|
|
253
256
|
}
|
|
254
|
-
const targets = active.independent
|
|
257
|
+
const targets = active.independent
|
|
258
|
+
? [track]
|
|
259
|
+
: trackLayouts.value.filter((target) => target.hasVisibleSeries)
|
|
255
260
|
const yRanges = Object.fromEntries(
|
|
256
261
|
targets.map((target) => [
|
|
257
|
-
target.series
|
|
262
|
+
target.series?.trackId ?? target.series?.id ?? target.id,
|
|
258
263
|
target.yScale.domain() as [number, number],
|
|
259
264
|
]),
|
|
260
265
|
)
|
|
261
266
|
emit('zoom-change', xDomain)
|
|
262
267
|
if (active.independent) {
|
|
263
|
-
const yDomain = yRanges[track.series
|
|
268
|
+
const yDomain = yRanges[track.series?.trackId ?? track.series?.id ?? track.id]
|
|
264
269
|
emit('zoom-end', {
|
|
265
270
|
start: xDomain[0],
|
|
266
271
|
end: xDomain[1],
|
|
@@ -177,7 +177,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
177
177
|
end: domain[1],
|
|
178
178
|
yRanges: Object.fromEntries(
|
|
179
179
|
visibleTracks.map((track) => [
|
|
180
|
-
track.series
|
|
180
|
+
track.series?.trackId ?? track.series?.id ?? track.id,
|
|
181
181
|
track.yScale.domain() as [number, number],
|
|
182
182
|
]),
|
|
183
183
|
),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { TrackLayout, WaveformHoverState } from '../core/types'
|
|
5
5
|
|
|
6
6
|
const props = defineProps<{
|
|
7
7
|
state: WaveformHoverState
|
|
@@ -11,23 +11,17 @@ const props = defineProps<{
|
|
|
11
11
|
}>()
|
|
12
12
|
|
|
13
13
|
interface Crosshair {
|
|
14
|
-
point: HoveredSeriesPoint
|
|
15
14
|
track: TrackLayout
|
|
16
15
|
x: number
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
const crosshairs = computed<Crosshair[]>(() => {
|
|
20
|
-
if (!props.visible) return []
|
|
21
|
-
|
|
22
|
-
const pointByTrack = new Map<number, HoveredSeriesPoint>()
|
|
23
|
-
props.state.points.forEach((point) => {
|
|
24
|
-
if (!pointByTrack.has(point.trackIndex)) pointByTrack.set(point.trackIndex, point)
|
|
25
|
-
})
|
|
19
|
+
if (!props.visible || props.state.queryX === null) return []
|
|
26
20
|
|
|
27
21
|
return props.tracks.flatMap((track) => {
|
|
28
|
-
const
|
|
29
|
-
return
|
|
30
|
-
? [{
|
|
22
|
+
const trackIsActive = props.state.trackIndex === null || props.state.trackIndex === track.index
|
|
23
|
+
return trackIsActive && !track.isEmpty && track.hasVisibleSeries
|
|
24
|
+
? [{ track, x: track.xScale(props.state.queryX!) }]
|
|
31
25
|
: []
|
|
32
26
|
})
|
|
33
27
|
})
|
|
@@ -67,6 +67,7 @@ const resolvedFrameStyle = computed(() => {
|
|
|
67
67
|
class="waveform-track waveform-chart__track"
|
|
68
68
|
:class="{ 'waveform-track--empty waveform-chart__track--empty': track.isEmpty }"
|
|
69
69
|
:data-track-index="track.index"
|
|
70
|
+
:data-track-id="track.id"
|
|
70
71
|
:data-track-empty="track.isEmpty || undefined"
|
|
71
72
|
:data-track-left="track.left"
|
|
72
73
|
:data-track-width="track.width"
|
|
@@ -5,6 +5,7 @@ import { nextTick, onMounted, ref, watch } from 'vue'
|
|
|
5
5
|
import type { WaveformAxesOptions } from '../../types'
|
|
6
6
|
import { formatScientificAxisLabel, formatXAxisLabel } from '../../utils'
|
|
7
7
|
import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
|
|
8
|
+
import { Y_AXIS_LABEL_BAND_WIDTH } from '../core/yAxisConstants'
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
track: TrackLayout
|
|
@@ -163,26 +164,27 @@ watch(
|
|
|
163
164
|
track.hasVisibleSeries &&
|
|
164
165
|
track.seriesList.length === 1 &&
|
|
165
166
|
track.showYAxisLabel &&
|
|
167
|
+
track.series &&
|
|
166
168
|
resolveYAxisLabel(track.series) &&
|
|
167
169
|
shouldShowYAxisLabel(track.height, track.index)
|
|
168
170
|
"
|
|
169
171
|
>
|
|
170
172
|
<rect
|
|
171
173
|
class="waveform-track__y-axis-label-bg waveform-chart__y-axis-label-bg"
|
|
172
|
-
:x="track.yAxisLabelX -
|
|
174
|
+
:x="track.yAxisLabelX - Y_AXIS_LABEL_BAND_WIDTH / 2"
|
|
173
175
|
:y="track.height / 2 - 40"
|
|
174
|
-
width="
|
|
176
|
+
:width="Y_AXIS_LABEL_BAND_WIDTH"
|
|
175
177
|
height="80"
|
|
176
178
|
rx="2"
|
|
177
179
|
/>
|
|
178
180
|
<text
|
|
179
181
|
class="waveform-track__y-axis-label waveform-chart__y-axis-label"
|
|
180
|
-
:fill="track.series
|
|
182
|
+
:fill="track.series?.color"
|
|
181
183
|
:transform="`translate(${track.yAxisLabelX}, ${track.height / 2}) rotate(-90)`"
|
|
182
184
|
text-anchor="middle"
|
|
183
185
|
dominant-baseline="central"
|
|
184
186
|
>
|
|
185
|
-
{{ resolveYAxisLabel(track.series) }}
|
|
187
|
+
{{ track.series ? resolveYAxisLabel(track.series) : '' }}
|
|
186
188
|
</text>
|
|
187
189
|
</g>
|
|
188
190
|
|
|
@@ -194,9 +196,9 @@ watch(
|
|
|
194
196
|
>
|
|
195
197
|
<rect
|
|
196
198
|
class="waveform-track__y-axis-label-bg waveform-chart__y-axis-label-bg"
|
|
197
|
-
:x="axis.labelX -
|
|
199
|
+
:x="axis.labelX - Y_AXIS_LABEL_BAND_WIDTH / 2"
|
|
198
200
|
:y="track.height / 2 - 40"
|
|
199
|
-
width="
|
|
201
|
+
:width="Y_AXIS_LABEL_BAND_WIDTH"
|
|
200
202
|
height="80"
|
|
201
203
|
rx="2"
|
|
202
204
|
/>
|
|
@@ -170,7 +170,7 @@ describe('WaveformChart', () => {
|
|
|
170
170
|
)
|
|
171
171
|
expect(labelX).toBeLessThan(-46)
|
|
172
172
|
expect(Number(wrapper.get('.waveform-chart__y-axis-label-bg').attributes('x'))).toBe(
|
|
173
|
-
labelX -
|
|
173
|
+
labelX - 6,
|
|
174
174
|
)
|
|
175
175
|
expect(yAxisLabels.every((label) => !label.text().includes('(T)'))).toBe(true)
|
|
176
176
|
expect(wrapper.findAll('.waveform-chart__track-label')).toHaveLength(0)
|
|
@@ -22,6 +22,16 @@ describe('WaveformChart', () => {
|
|
|
22
22
|
|
|
23
23
|
expect(wrapper.findAll('.waveform-chart__grid--major line').length).toBeGreaterThan(0)
|
|
24
24
|
expect(wrapper.findAll('.waveform-chart__grid--minor line').length).toBeGreaterThan(0)
|
|
25
|
+
expect(
|
|
26
|
+
wrapper.findAll('.waveform-chart__grid--major [data-grid-direction="horizontal"]'),
|
|
27
|
+
).toHaveLength(5)
|
|
28
|
+
expect(
|
|
29
|
+
wrapper.findAll('.waveform-chart__grid--minor [data-grid-direction="horizontal"]'),
|
|
30
|
+
).toHaveLength(4)
|
|
31
|
+
expect(
|
|
32
|
+
wrapper.findAll('.waveform-chart__grid--minor [data-grid-direction="vertical"]'),
|
|
33
|
+
).not.toHaveLength(0)
|
|
34
|
+
expect(getComputedStyle(wrapper.element).borderTopWidth).toBe('1.2px')
|
|
25
35
|
expect(wrapper.find('.waveform-chart__plot-frame').exists()).toBe(true)
|
|
26
36
|
expect(wrapper.get('.waveform-chart__plot-frame').attributes()).toMatchObject({
|
|
27
37
|
fill: 'none',
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { flushPromises } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { flushAnimationFrames } from '../../test/setup'
|
|
5
|
+
import { mountSizedChart } from '../../test/waveformChart'
|
|
6
|
+
|
|
7
|
+
describe('WaveformChart incomplete data hover', () => {
|
|
8
|
+
it('keeps the shared crosshair at the query time and marks out-of-range series unavailable', async () => {
|
|
9
|
+
const wrapper = await mountSizedChart(
|
|
10
|
+
{
|
|
11
|
+
kind: 'series',
|
|
12
|
+
series: [
|
|
13
|
+
{
|
|
14
|
+
name: '完整数据',
|
|
15
|
+
data: {
|
|
16
|
+
kind: 'points',
|
|
17
|
+
points: [
|
|
18
|
+
{ x: -1, y: 1 },
|
|
19
|
+
{ x: -0.5, y: 2 },
|
|
20
|
+
{ x: 1, y: 3 },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: '从零开始',
|
|
26
|
+
data: {
|
|
27
|
+
kind: 'points',
|
|
28
|
+
points: [
|
|
29
|
+
{ x: 0, y: 10 },
|
|
30
|
+
{ x: 1, y: 20 },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{ displayMode: 'separated' },
|
|
37
|
+
)
|
|
38
|
+
const overlay = wrapper.get('.waveform-chart__overlay--shared')
|
|
39
|
+
const overlayWidth = Number(overlay.attributes('width'))
|
|
40
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
41
|
+
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
overlay.element.dispatchEvent(
|
|
45
|
+
new MouseEvent('pointermove', {
|
|
46
|
+
clientX: overlayWidth / 4,
|
|
47
|
+
clientY: 120,
|
|
48
|
+
bubbles: true,
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
flushAnimationFrames()
|
|
52
|
+
await flushPromises()
|
|
53
|
+
|
|
54
|
+
const crosshairPositions = wrapper
|
|
55
|
+
.findAll('.waveform-chart__crosshair line')
|
|
56
|
+
.map((line) => Number(line.attributes('x1')))
|
|
57
|
+
expect(crosshairPositions).toHaveLength(2)
|
|
58
|
+
expect(crosshairPositions[0]).toBeCloseTo(overlayWidth / 4)
|
|
59
|
+
expect(crosshairPositions[1]).toBeCloseTo(overlayWidth / 4)
|
|
60
|
+
const tooltipRows = wrapper.findAll('.waveform-chart__tooltip-series')
|
|
61
|
+
expect(tooltipRows).toHaveLength(2)
|
|
62
|
+
expect(tooltipRows[0].text()).toContain('(x:-500 y:2)')
|
|
63
|
+
expect(tooltipRows[1].text()).toContain('无数据')
|
|
64
|
+
expect(tooltipRows[1].text()).not.toContain('x:')
|
|
65
|
+
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: -0.5, y: 2 }])
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -53,10 +53,10 @@ describe('WaveformChart', () => {
|
|
|
53
53
|
tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
|
|
54
54
|
)
|
|
55
55
|
|
|
56
|
-
expect(labelX).toBe(-
|
|
57
|
-
expect(labelBackgroundX).toBe(labelX -
|
|
58
|
-
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(
|
|
59
|
-
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(
|
|
56
|
+
expect(labelX).toBe(-62)
|
|
57
|
+
expect(labelBackgroundX).toBe(labelX - 6)
|
|
58
|
+
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(80)
|
|
59
|
+
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(72)
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
it('keeps a tick-only gutter when channel labels are empty', async () => {
|
|
@@ -82,7 +82,7 @@ describe('WaveformChart', () => {
|
|
|
82
82
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
|
83
83
|
|
|
84
84
|
expect(wrapper.findAll('.waveform-chart__y-axis-label')).toHaveLength(0)
|
|
85
|
-
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(
|
|
85
|
+
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(80)
|
|
86
86
|
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(60)
|
|
87
87
|
})
|
|
88
88
|
|
|
@@ -343,7 +343,7 @@ describe('WaveformChart', () => {
|
|
|
343
343
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
|
344
344
|
const labelX = Number(tracks[1].attributes('data-y-axis-label-x'))
|
|
345
345
|
|
|
346
|
-
expect(secondLeft - (firstLeft + firstWidth)).toBeGreaterThanOrEqual(Math.abs(labelX) +
|
|
346
|
+
expect(secondLeft - (firstLeft + firstWidth)).toBeGreaterThanOrEqual(Math.abs(labelX) + 6)
|
|
347
347
|
expect(tracks[1].find('.waveform-chart__y-axis-label-bg').exists()).toBe(true)
|
|
348
348
|
}
|
|
349
349
|
})
|
|
@@ -144,6 +144,55 @@ describe('WaveformChart', () => {
|
|
|
144
144
|
expect(wrapper.get('.ant-pagination-next').classes()).toContain('ant-pagination-disabled')
|
|
145
145
|
})
|
|
146
146
|
|
|
147
|
+
it('keeps ordered empty tracks in their page slots after channels are merged', async () => {
|
|
148
|
+
const wrapper = await mountSizedChart(
|
|
149
|
+
{
|
|
150
|
+
kind: 'series',
|
|
151
|
+
series: ['frame-1', 'frame-1', 'frame-3', 'frame-4', 'frame-5', 'frame-6'].map(
|
|
152
|
+
(trackId, index) => ({
|
|
153
|
+
id: `channel-${index + 1}`,
|
|
154
|
+
trackId,
|
|
155
|
+
name: `channel-${index + 1}`,
|
|
156
|
+
data: {
|
|
157
|
+
kind: 'points' as const,
|
|
158
|
+
points: [
|
|
159
|
+
{ x: 0, y: index },
|
|
160
|
+
{ x: 1, y: index + 1 },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
grid: {
|
|
168
|
+
rowCount: 2,
|
|
169
|
+
columnCount: 1,
|
|
170
|
+
trackOrder: ['frame-1', 'frame-2', 'frame-3', 'frame-4', 'frame-5', 'frame-6'],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
const trackIds = () =>
|
|
175
|
+
wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id'))
|
|
176
|
+
const lineIds = () =>
|
|
177
|
+
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id'))
|
|
178
|
+
const nextButton = () => wrapper.get('.ant-pagination-next button')
|
|
179
|
+
|
|
180
|
+
expect(trackIds()).toEqual(['frame-1', 'frame-2'])
|
|
181
|
+
expect(wrapper.findAll('.waveform-chart__track--empty')).toHaveLength(1)
|
|
182
|
+
expect(lineIds()).toEqual(['channel-1', 'channel-2'])
|
|
183
|
+
expect(wrapper.findAll('.waveform-chart__legend')).toHaveLength(1)
|
|
184
|
+
|
|
185
|
+
await nextButton().trigger('click')
|
|
186
|
+
expect(trackIds()).toEqual(['frame-3', 'frame-4'])
|
|
187
|
+
expect(lineIds()).toEqual(['channel-3', 'channel-4'])
|
|
188
|
+
expect(wrapper.emitted('page-change')?.at(-1)).toEqual([2, 3])
|
|
189
|
+
|
|
190
|
+
await nextButton().trigger('click')
|
|
191
|
+
expect(trackIds()).toEqual(['frame-5', 'frame-6'])
|
|
192
|
+
expect(lineIds()).toEqual(['channel-5', 'channel-6'])
|
|
193
|
+
expect(wrapper.emitted('page-change')?.at(-1)).toEqual([3, 3])
|
|
194
|
+
})
|
|
195
|
+
|
|
147
196
|
it('overlays series with the same track ID without changing the next frame', async () => {
|
|
148
197
|
const wrapper = await mountSizedChart(
|
|
149
198
|
{
|
|
@@ -346,6 +395,6 @@ describe('WaveformChart', () => {
|
|
|
346
395
|
'waveform-legend__panel--horizontal',
|
|
347
396
|
)
|
|
348
397
|
|
|
349
|
-
expect(wrapper.attributes('data-chart-left-margin')).toBe('
|
|
398
|
+
expect(wrapper.attributes('data-chart-left-margin')).toBe('80')
|
|
350
399
|
})
|
|
351
400
|
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { flushPromises } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { flushAnimationFrames } from '../../test/setup'
|
|
5
|
+
import { mountSizedChart } from '../../test/waveformChart'
|
|
6
|
+
|
|
7
|
+
describe('WaveformChart track order', () => {
|
|
8
|
+
it('keeps a merged source slot empty without adding legend or tooltip data', async () => {
|
|
9
|
+
const wrapper = await mountSizedChart(
|
|
10
|
+
{
|
|
11
|
+
kind: 'series',
|
|
12
|
+
series: [
|
|
13
|
+
{
|
|
14
|
+
id: 'frame-1-a',
|
|
15
|
+
trackId: 'frame-1',
|
|
16
|
+
name: '合并前通道',
|
|
17
|
+
data: {
|
|
18
|
+
kind: 'points',
|
|
19
|
+
points: [
|
|
20
|
+
{ x: 0, y: 1 },
|
|
21
|
+
{ x: 1, y: 2 },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'frame-1-b',
|
|
27
|
+
trackId: 'frame-1',
|
|
28
|
+
name: '合并后通道',
|
|
29
|
+
data: {
|
|
30
|
+
kind: 'points',
|
|
31
|
+
points: [
|
|
32
|
+
{ x: 0, y: 3 },
|
|
33
|
+
{ x: 1, y: 4 },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'frame-3',
|
|
39
|
+
trackId: 'frame-3',
|
|
40
|
+
name: '后续通道',
|
|
41
|
+
data: {
|
|
42
|
+
kind: 'points',
|
|
43
|
+
points: [
|
|
44
|
+
{ x: 0, y: 5 },
|
|
45
|
+
{ x: 1, y: 6 },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
displayMode: 'separated',
|
|
53
|
+
grid: { rowCount: 3, trackOrder: ['frame-1', 'frame-2', 'frame-3'] },
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
const trackIds = () =>
|
|
57
|
+
wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id'))
|
|
58
|
+
|
|
59
|
+
expect(trackIds()).toEqual(['frame-1', 'frame-2', 'frame-3'])
|
|
60
|
+
expect(wrapper.findAll('.waveform-chart__track--empty')).toHaveLength(1)
|
|
61
|
+
expect(
|
|
62
|
+
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
|
63
|
+
).toEqual(['frame-1-a', 'frame-1-b', 'frame-3'])
|
|
64
|
+
expect(wrapper.findAll('.waveform-chart__legend')).toHaveLength(1)
|
|
65
|
+
|
|
66
|
+
const overlay = wrapper.get('.waveform-chart__overlay--shared')
|
|
67
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
68
|
+
value: () => ({ left: 0, top: 0, width: 712, height: 290 }),
|
|
69
|
+
})
|
|
70
|
+
overlay.element.dispatchEvent(
|
|
71
|
+
new MouseEvent('pointermove', { clientX: 356, clientY: 150, bubbles: true }),
|
|
72
|
+
)
|
|
73
|
+
flushAnimationFrames()
|
|
74
|
+
await flushPromises()
|
|
75
|
+
|
|
76
|
+
expect(wrapper.findAll('.waveform-chart__tooltip-series')).toHaveLength(3)
|
|
77
|
+
expect(wrapper.get('.waveform-chart__tooltip').text()).not.toContain('frame-2')
|
|
78
|
+
expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(2)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('appends data tracks absent from trackOrder in input order', async () => {
|
|
82
|
+
const wrapper = await mountSizedChart(
|
|
83
|
+
{
|
|
84
|
+
kind: 'series',
|
|
85
|
+
series: [
|
|
86
|
+
{
|
|
87
|
+
id: 'later-a',
|
|
88
|
+
trackId: 'later-a',
|
|
89
|
+
name: 'later-a',
|
|
90
|
+
data: {
|
|
91
|
+
kind: 'points',
|
|
92
|
+
points: [
|
|
93
|
+
{ x: 0, y: 1 },
|
|
94
|
+
{ x: 1, y: 2 },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'frame-1',
|
|
100
|
+
trackId: 'frame-1',
|
|
101
|
+
name: 'frame-1',
|
|
102
|
+
data: {
|
|
103
|
+
kind: 'points',
|
|
104
|
+
points: [
|
|
105
|
+
{ x: 0, y: 3 },
|
|
106
|
+
{ x: 1, y: 4 },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'later-b',
|
|
112
|
+
trackId: 'later-b',
|
|
113
|
+
name: 'later-b',
|
|
114
|
+
data: {
|
|
115
|
+
kind: 'points',
|
|
116
|
+
points: [
|
|
117
|
+
{ x: 0, y: 5 },
|
|
118
|
+
{ x: 1, y: 6 },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{ grid: { rowCount: 2, columnCount: 2, trackOrder: ['frame-2', 'frame-1'] } },
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
expect(
|
|
128
|
+
wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id')),
|
|
129
|
+
).toEqual(['frame-2', 'frame-1', 'later-a', 'later-b'])
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -21,8 +21,13 @@ describe('simulated waveform data', () => {
|
|
|
21
21
|
first.series.forEach((series) => {
|
|
22
22
|
expect(series.data.kind).toBe('points')
|
|
23
23
|
if (series.data.kind !== 'points') return
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
if (series.id === 'simulated-step') {
|
|
25
|
+
expect(series.data.points).toHaveLength(500)
|
|
26
|
+
expect(series.data.points.every((point) => point.x >= 0)).toBe(true)
|
|
27
|
+
} else {
|
|
28
|
+
expect(series.data.points).toHaveLength(1_000)
|
|
29
|
+
expect(series.data.points[0]?.x).toBe(-5)
|
|
30
|
+
}
|
|
26
31
|
expect(series.data.points.at(-1)?.x).toBe(5)
|
|
27
32
|
series.data.points.forEach((point) => {
|
|
28
33
|
expect(Number.isFinite(point.x)).toBe(true)
|
|
@@ -17,6 +17,7 @@ interface SimulatedSeriesDefinition extends Pick<
|
|
|
17
17
|
> {
|
|
18
18
|
signal: SignalGenerator
|
|
19
19
|
errors?: ErrorGenerator
|
|
20
|
+
minimumX?: number
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function createSeededNoise(seed: number) {
|
|
@@ -100,6 +101,7 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
100
101
|
unit: 'V',
|
|
101
102
|
lineType: 'linear',
|
|
102
103
|
pointType: 'none',
|
|
104
|
+
minimumX: 0,
|
|
103
105
|
signal: (time) => (time < 0 ? 0 : 1 - Math.exp(-time * 2.4)),
|
|
104
106
|
},
|
|
105
107
|
{
|
|
@@ -128,12 +130,15 @@ export function createSimulatedWaveformData(): WaveformData {
|
|
|
128
130
|
const noise = createSeededNoise(0x5eed1234)
|
|
129
131
|
return {
|
|
130
132
|
kind: 'series',
|
|
131
|
-
series: seriesDefinitions.map(({ signal, errors, ...series }) =>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
series: seriesDefinitions.map(({ signal, errors, minimumX, ...series }) => {
|
|
134
|
+
const points = createPoints(signal, noise, errors)
|
|
135
|
+
return {
|
|
136
|
+
...series,
|
|
137
|
+
data: {
|
|
138
|
+
kind: 'points' as const,
|
|
139
|
+
points: minimumX === undefined ? points : points.filter((point) => point.x >= minimumX),
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}),
|
|
138
143
|
}
|
|
139
144
|
}
|