waveform-analysis 0.1.35 → 0.1.37
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 +9 -5
- package/dist/index.cjs +1 -1
- package/dist/index.js +909 -816
- package/dist/style.css +1 -1
- package/dist/types/components/annotation/WaveformAnnotationEditor.vue.d.ts +4 -0
- package/dist/types/components/annotation/useWaveformChartAnnotations.d.ts +3 -1
- package/dist/types/components/core/constants.d.ts +3 -1
- package/dist/types/components/core/types.d.ts +2 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +2 -0
- package/dist/types/components/interaction/WaveformTooltip.vue.d.ts +1 -0
- package/dist/types/types/data.d.ts +3 -0
- package/package.json +1 -1
- package/src/App.test.ts +3 -2
- package/src/App.vue +1 -1
- package/src/components/WaveformChartView.vue +10 -1
- package/src/components/annotation/WaveformAnnotationEditor.css +35 -104
- package/src/components/annotation/WaveformAnnotationEditor.vue +87 -37
- package/src/components/annotation/components.test.ts +134 -20
- package/src/components/annotation/markup.test.ts +18 -2
- package/src/components/annotation/markup.ts +33 -0
- package/src/components/annotation/useWaveformChartAnnotations.ts +53 -12
- package/src/components/core/constants.test.ts +29 -0
- package/src/components/core/constants.ts +31 -9
- package/src/components/core/types.ts +2 -0
- package/src/components/interaction/WaveformTooltip.test.ts +17 -9
- package/src/components/interaction/WaveformTooltip.vue +41 -45
- package/src/components/interaction/useWaveformHover.ts +1 -0
- package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +13 -3
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +4 -4
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +42 -2
- package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +24 -14
- package/src/components/waveformChartCases/legendVisibility.test.ts +10 -2
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +1 -1
- package/src/components/waveformChartCases/presentationMode.test.ts +10 -2
- package/src/components/waveformChartCases/renderingStyles.test.ts +1 -1
- package/src/components/waveformChartCases/xDomainStrategy.test.ts +60 -0
- package/src/core/data.test.ts +22 -0
- package/src/core/data.ts +1 -0
- package/src/data/simulatedWaveforms.test.ts +1 -0
- package/src/data/simulatedWaveforms.ts +7 -1
- package/src/types/data.ts +3 -0
- package/src/views/FixedYDomainDemo.vue +2 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
-
import { resolveWaveformPointErrors } from '../../core'
|
|
4
3
|
import { formatTooltipNumber, formatTooltipTime } from '../../utils'
|
|
5
4
|
import type { WaveformPoint } from '../data/types'
|
|
6
5
|
|
|
7
6
|
interface SeriesPoint {
|
|
8
7
|
trackIndex: number
|
|
9
8
|
name: string
|
|
9
|
+
shotNo?: string
|
|
10
10
|
color: string
|
|
11
11
|
unit?: string
|
|
12
12
|
point: WaveformPoint
|
|
@@ -34,7 +34,7 @@ const props = defineProps<Props>()
|
|
|
34
34
|
const tooltipGap = 12
|
|
35
35
|
const containerPadding = 8
|
|
36
36
|
const tooltipPlacementWidth = 238
|
|
37
|
-
const tooltipMaxWidth =
|
|
37
|
+
const tooltipMaxWidth = 560
|
|
38
38
|
const tooltipHorizontalPadding = 20
|
|
39
39
|
const tooltipLineHeight = 20
|
|
40
40
|
|
|
@@ -45,18 +45,18 @@ function estimateLineCount(text: string, width: number): number {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function formatSeriesText(seriesPoint: SeriesPoint): string {
|
|
48
|
-
const
|
|
49
|
-
return `${seriesPoint.
|
|
50
|
-
seriesPoint.unit ? `
|
|
51
|
-
}
|
|
48
|
+
const timeText = formatTooltipTime(props.hoveredPoint?.x ?? seriesPoint.point.x, props.timeUnit)
|
|
49
|
+
return `${seriesPoint.shotNo?.trim() || '未配置炮号'}: ${seriesPoint.name}${
|
|
50
|
+
seriesPoint.unit ? `(${seriesPoint.unit})` : ''
|
|
51
|
+
} (x:${timeText} y:${formatTooltipNumber(seriesPoint.point.y)})`
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function estimateTooltipHeight(width: number
|
|
54
|
+
function estimateTooltipHeight(width: number): number {
|
|
55
55
|
const seriesLines = props.seriesPoints.reduce(
|
|
56
56
|
(total, seriesPoint) => total + estimateLineCount(formatSeriesText(seriesPoint), width),
|
|
57
57
|
0,
|
|
58
58
|
)
|
|
59
|
-
return 16 + tooltipLineHeight *
|
|
59
|
+
return 16 + tooltipLineHeight * seriesLines + 5
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const tooltipStyle = computed(() => {
|
|
@@ -84,25 +84,18 @@ const tooltipStyle = computed(() => {
|
|
|
84
84
|
: { left: `${containerPadding}px`, maxWidth: `${availableWidth}px` }
|
|
85
85
|
|
|
86
86
|
const maxWidth = Number.parseFloat(horizontalStyle.maxWidth)
|
|
87
|
-
const timeText = `${props.timeUnit}: ${formatTooltipTime(props.hoveredPoint.x, props.timeUnit)}`
|
|
88
|
-
|
|
89
87
|
return {
|
|
90
88
|
...horizontalStyle,
|
|
91
89
|
top: `${Math.max(
|
|
92
90
|
8,
|
|
93
91
|
Math.min(
|
|
94
92
|
props.position.y - 18,
|
|
95
|
-
props.containerHeight - estimateTooltipHeight(maxWidth
|
|
93
|
+
props.containerHeight - estimateTooltipHeight(maxWidth) - 8,
|
|
96
94
|
),
|
|
97
95
|
)}px`,
|
|
98
96
|
}
|
|
99
97
|
})
|
|
100
98
|
|
|
101
|
-
function formatError(point: WaveformPoint): string | null {
|
|
102
|
-
const { lower, upper } = resolveWaveformPointErrors(point)
|
|
103
|
-
if (lower === 0 && upper === 0) return null
|
|
104
|
-
return `(+${formatTooltipNumber(upper)} / -${formatTooltipNumber(lower)})`
|
|
105
|
-
}
|
|
106
99
|
</script>
|
|
107
100
|
|
|
108
101
|
<template>
|
|
@@ -111,9 +104,6 @@ function formatError(point: WaveformPoint): string | null {
|
|
|
111
104
|
class="waveform-tooltip waveform-chart__tooltip"
|
|
112
105
|
:style="tooltipStyle"
|
|
113
106
|
>
|
|
114
|
-
<span class="waveform-tooltip__time waveform-chart__tooltip-time">
|
|
115
|
-
{{ timeUnit }}: {{ formatTooltipTime(hoveredPoint.x, timeUnit) }}
|
|
116
|
-
</span>
|
|
117
107
|
<span
|
|
118
108
|
v-for="seriesPoint in seriesPoints"
|
|
119
109
|
:key="`${seriesPoint.trackIndex}-${seriesPoint.name}`"
|
|
@@ -121,12 +111,12 @@ function formatError(point: WaveformPoint): string | null {
|
|
|
121
111
|
>
|
|
122
112
|
<i :style="{ backgroundColor: seriesPoint.color }" />
|
|
123
113
|
<span class="waveform-tooltip__series-content">
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
</span>
|
|
114
|
+
<span class="waveform-tooltip__series-label">{{ seriesPoint.shotNo?.trim() || '未配置炮号' }}: {{
|
|
115
|
+
seriesPoint.name
|
|
116
|
+
}}<template v-if="seriesPoint.unit">({{ seriesPoint.unit }})</template></span>
|
|
117
|
+
<span class="waveform-tooltip__value">(x:{{ formatTooltipTime(hoveredPoint.x, timeUnit) }} y:{{
|
|
118
|
+
formatTooltipNumber(seriesPoint.point.y)
|
|
119
|
+
}})</span>
|
|
130
120
|
</span>
|
|
131
121
|
</span>
|
|
132
122
|
</div>
|
|
@@ -138,26 +128,17 @@ function formatError(point: WaveformPoint): string | null {
|
|
|
138
128
|
position: absolute;
|
|
139
129
|
z-index: 2;
|
|
140
130
|
display: grid;
|
|
141
|
-
gap:
|
|
131
|
+
gap: 2px;
|
|
142
132
|
width: max-content;
|
|
143
|
-
max-width: min(
|
|
144
|
-
padding:
|
|
145
|
-
color: #
|
|
146
|
-
font:
|
|
147
|
-
12px/1.45 ui-monospace,
|
|
148
|
-
SFMono-Regular,
|
|
149
|
-
Consolas,
|
|
150
|
-
monospace;
|
|
133
|
+
max-width: min(560px, calc(100% - 16px));
|
|
134
|
+
padding: 9px 12px;
|
|
135
|
+
color: #505050;
|
|
136
|
+
font: 14px/1.35 Arial, sans-serif;
|
|
151
137
|
pointer-events: none;
|
|
152
138
|
background: #fff;
|
|
153
|
-
border: 1px solid #
|
|
154
|
-
border-radius:
|
|
155
|
-
box-shadow: 0
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.waveform-tooltip__time {
|
|
159
|
-
padding-bottom: 4px;
|
|
160
|
-
border-bottom: 1px solid #f0f0f0;
|
|
139
|
+
border: 1px solid #e3e7eb;
|
|
140
|
+
border-radius: 4px;
|
|
141
|
+
box-shadow: 0 3px 10px rgb(16 24 40 / 15%);
|
|
161
142
|
}
|
|
162
143
|
|
|
163
144
|
.waveform-tooltip__series {
|
|
@@ -176,9 +157,24 @@ function formatError(point: WaveformPoint): string | null {
|
|
|
176
157
|
}
|
|
177
158
|
|
|
178
159
|
.waveform-tooltip__series-content {
|
|
160
|
+
display: flex;
|
|
161
|
+
flex-wrap: nowrap;
|
|
162
|
+
gap: 12px;
|
|
163
|
+
align-items: baseline;
|
|
164
|
+
min-width: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.waveform-tooltip__series-label {
|
|
168
|
+
flex: 1 1 auto;
|
|
179
169
|
min-width: 0;
|
|
180
|
-
overflow
|
|
181
|
-
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
text-overflow: ellipsis;
|
|
172
|
+
white-space: nowrap;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.waveform-tooltip__value {
|
|
176
|
+
flex: 0 0 auto;
|
|
177
|
+
white-space: nowrap;
|
|
182
178
|
}
|
|
183
179
|
|
|
184
180
|
.waveform-tooltip__series strong {
|
|
@@ -186,7 +182,7 @@ function formatError(point: WaveformPoint): string | null {
|
|
|
186
182
|
}
|
|
187
183
|
|
|
188
184
|
.waveform-tooltip__value {
|
|
189
|
-
|
|
185
|
+
color: #555;
|
|
190
186
|
}
|
|
191
187
|
|
|
192
188
|
.waveform-tooltip__series small {
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { flushPromises } from '@vue/test-utils'
|
|
1
|
+
import { DOMWrapper, flushPromises } from '@vue/test-utils'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import { flushAnimationFrames } from '../../test/setup'
|
|
5
5
|
|
|
6
6
|
import { mountSizedChart } from '../../test/waveformChart'
|
|
7
7
|
|
|
8
|
+
function getAnnotationEditor() {
|
|
9
|
+
const editor = Array.from(document.body.querySelectorAll<HTMLElement>('[role="dialog"]'))
|
|
10
|
+
.filter((element) => element.closest('.waveform-annotation-editor'))
|
|
11
|
+
.at(-1)
|
|
12
|
+
if (!editor) throw new Error('Expected annotation editor modal to be mounted')
|
|
13
|
+
return new DOMWrapper(editor)
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
describe('WaveformChart', () => {
|
|
9
17
|
it('edits and immediately deletes existing annotations without mutating props', async () => {
|
|
10
18
|
const sourceAnnotation = {
|
|
@@ -31,8 +39,10 @@ describe('WaveformChart', () => {
|
|
|
31
39
|
})
|
|
32
40
|
await wrapper.get('.waveform-annotation-context-menu button').trigger('click')
|
|
33
41
|
await flushPromises()
|
|
34
|
-
|
|
35
|
-
await
|
|
42
|
+
const editor = getAnnotationEditor()
|
|
43
|
+
await editor.get('textarea[aria-label="标注文本"]').setValue('新文字')
|
|
44
|
+
await editor.get('button.ant-btn-primary').trigger('click')
|
|
45
|
+
await flushPromises()
|
|
36
46
|
|
|
37
47
|
const updated = wrapper.emitted('update:annotations')?.at(-1)?.[0] as
|
|
38
48
|
Array<{ text: string }> | undefined
|
|
@@ -179,10 +179,10 @@ describe('WaveformChart', () => {
|
|
|
179
179
|
await flushPromises()
|
|
180
180
|
|
|
181
181
|
const tooltip = wrapper.get('.waveform-chart__tooltip')
|
|
182
|
-
expect(tooltip.text()).toContain('BT2_2M
|
|
183
|
-
expect(tooltip.text()).toContain('2
|
|
184
|
-
expect(tooltip.text()).toContain('BT1_2M
|
|
185
|
-
expect(tooltip.text()).toContain('4
|
|
182
|
+
expect(tooltip.text()).toContain('未配置炮号: BT2_2M')
|
|
183
|
+
expect(tooltip.text()).toContain('(x:1,000 y:2)')
|
|
184
|
+
expect(tooltip.text()).toContain('未配置炮号: BT1_2M')
|
|
185
|
+
expect(tooltip.text()).toContain('(x:1,000 y:4)')
|
|
186
186
|
const crosshairLines = wrapper.findAll('.waveform-chart__crosshair line')
|
|
187
187
|
expect(crosshairLines).toHaveLength(2)
|
|
188
188
|
crosshairLines.forEach((line) => {
|
|
@@ -178,7 +178,7 @@ describe('WaveformChart', () => {
|
|
|
178
178
|
await flushPromises()
|
|
179
179
|
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1, y: 5 }])
|
|
180
180
|
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
|
|
181
|
-
expect(wrapper.get('.waveform-
|
|
181
|
+
expect(wrapper.get('.waveform-tooltip__series-label').text()).toContain('未配置炮号:')
|
|
182
182
|
const crosshairLines = wrapper.findAll('.waveform-chart__crosshair line')
|
|
183
183
|
expect(crosshairLines).toHaveLength(1)
|
|
184
184
|
expect(crosshairLines[0].attributes('x1')).toBe(crosshairLines[0].attributes('x2'))
|
|
@@ -189,6 +189,46 @@ describe('WaveformChart', () => {
|
|
|
189
189
|
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
|
|
190
190
|
})
|
|
191
191
|
|
|
192
|
+
it('clears hover when the pointer leaves the chart and restores it only after a new plot move', async () => {
|
|
193
|
+
const wrapper = await mountSizedChart(
|
|
194
|
+
{
|
|
195
|
+
kind: 'points',
|
|
196
|
+
points: [
|
|
197
|
+
{ x: 0, y: 0 },
|
|
198
|
+
{ x: 1, y: 5 },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
{ grid: { rowCount: 1, columnCount: 1 } },
|
|
202
|
+
)
|
|
203
|
+
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
204
|
+
const overlayWidth = Number(overlay.attributes('width'))
|
|
205
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
206
|
+
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
overlay.element.dispatchEvent(
|
|
210
|
+
new MouseEvent('pointermove', { clientX: overlayWidth, clientY: 120, bubbles: true }),
|
|
211
|
+
)
|
|
212
|
+
flushAnimationFrames()
|
|
213
|
+
await flushPromises()
|
|
214
|
+
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
|
|
215
|
+
|
|
216
|
+
await wrapper.trigger('pointerleave')
|
|
217
|
+
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
|
|
218
|
+
expect(wrapper.find('.waveform-chart__crosshair').exists()).toBe(false)
|
|
219
|
+
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
|
|
220
|
+
|
|
221
|
+
await wrapper.trigger('pointerenter')
|
|
222
|
+
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
|
|
223
|
+
|
|
224
|
+
overlay.element.dispatchEvent(
|
|
225
|
+
new MouseEvent('pointermove', { clientX: 0, clientY: 120, bubbles: true }),
|
|
226
|
+
)
|
|
227
|
+
flushAnimationFrames()
|
|
228
|
+
await flushPromises()
|
|
229
|
+
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
|
|
230
|
+
})
|
|
231
|
+
|
|
192
232
|
it('hides the numeric tooltip and crosshair when showTooltip is disabled', async () => {
|
|
193
233
|
const wrapper = await mountSizedChart(
|
|
194
234
|
{
|
|
@@ -303,7 +343,7 @@ describe('WaveformChart', () => {
|
|
|
303
343
|
flushAnimationFrames()
|
|
304
344
|
await flushPromises()
|
|
305
345
|
|
|
306
|
-
expect(wrapper.get('.waveform-
|
|
346
|
+
expect(wrapper.get('.waveform-tooltip__series-label').text()).toContain('未配置炮号:')
|
|
307
347
|
expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(1)
|
|
308
348
|
expect(wrapper.get('.waveform-chart__line').element).toBe(pathBeforeHover)
|
|
309
349
|
expect(chartUpdate).not.toHaveBeenCalled()
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { flushPromises } from '@vue/test-utils'
|
|
1
|
+
import { DOMWrapper, flushPromises } from '@vue/test-utils'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import { flushAnimationFrames, pendingAnimationFrameCount } from '../../test/setup'
|
|
5
5
|
|
|
6
6
|
import { mountSizedChart } from '../../test/waveformChart'
|
|
7
7
|
|
|
8
|
+
function getAnnotationEditor() {
|
|
9
|
+
const editor = Array.from(document.body.querySelectorAll<HTMLElement>('[role="dialog"]'))
|
|
10
|
+
.filter((element) => element.closest('.waveform-annotation-editor'))
|
|
11
|
+
.at(-1)
|
|
12
|
+
if (!editor) throw new Error('Expected annotation editor modal to be mounted')
|
|
13
|
+
return new DOMWrapper(editor)
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
describe('WaveformChart', () => {
|
|
9
17
|
it('zooms only the active independent track and resets when the mode changes', async () => {
|
|
10
18
|
const wrapper = await mountSizedChart({
|
|
@@ -172,9 +180,10 @@ describe('WaveformChart', () => {
|
|
|
172
180
|
)
|
|
173
181
|
await flushPromises()
|
|
174
182
|
|
|
175
|
-
|
|
176
|
-
await
|
|
177
|
-
await
|
|
183
|
+
const editor = getAnnotationEditor()
|
|
184
|
+
await editor.get('textarea[aria-label="标注文本"]').setValue('峰值点')
|
|
185
|
+
await editor.get('button.ant-btn-primary').trigger('click')
|
|
186
|
+
await flushPromises()
|
|
178
187
|
|
|
179
188
|
const annotations = wrapper.emitted('update:annotations')?.at(-1)?.[0] as
|
|
180
189
|
Array<{ seriesId: string; x: number; y: number; text: string }> | undefined
|
|
@@ -209,10 +218,10 @@ describe('WaveformChart', () => {
|
|
|
209
218
|
}),
|
|
210
219
|
)
|
|
211
220
|
await flushPromises()
|
|
212
|
-
|
|
213
|
-
expect(
|
|
214
|
-
expect(
|
|
215
|
-
const textarea =
|
|
221
|
+
const editor = getAnnotationEditor()
|
|
222
|
+
expect(editor.attributes('role')).toBe('dialog')
|
|
223
|
+
expect(editor.find('.waveform-annotation-editor__content').exists()).toBe(true)
|
|
224
|
+
const textarea = editor.get('textarea[aria-label="标注文本"]')
|
|
216
225
|
const textareaContextMenu = new MouseEvent('contextmenu', {
|
|
217
226
|
bubbles: true,
|
|
218
227
|
cancelable: true,
|
|
@@ -220,7 +229,8 @@ describe('WaveformChart', () => {
|
|
|
220
229
|
expect(textarea.element.dispatchEvent(textareaContextMenu)).toBe(true)
|
|
221
230
|
expect(textareaContextMenu.defaultPrevented).toBe(false)
|
|
222
231
|
await textarea.setValue('右键标注')
|
|
223
|
-
await
|
|
232
|
+
await editor.get('button.ant-btn-primary').trigger('click')
|
|
233
|
+
await flushPromises()
|
|
224
234
|
|
|
225
235
|
// Annotation snaps to nearest sample point (x=1, y=5)
|
|
226
236
|
expect(wrapper.emitted('update:annotations')?.at(-1)?.[0]).toMatchObject([
|
|
@@ -278,20 +288,20 @@ describe('WaveformChart', () => {
|
|
|
278
288
|
)
|
|
279
289
|
await flushPromises()
|
|
280
290
|
expect(
|
|
281
|
-
|
|
282
|
-
.
|
|
291
|
+
getAnnotationEditor()
|
|
292
|
+
.get('select[aria-label="选择标注波形"]')
|
|
283
293
|
.findAll('option')
|
|
284
294
|
.map((item) => item.text()),
|
|
285
295
|
).toEqual(['通道 A'])
|
|
286
296
|
|
|
287
|
-
await
|
|
297
|
+
await getAnnotationEditor().get('.ant-modal-close').trigger('click')
|
|
288
298
|
overlays[0].element.dispatchEvent(
|
|
289
299
|
new MouseEvent('contextmenu', { clientX: 356, clientY: 230, bubbles: true }),
|
|
290
300
|
)
|
|
291
301
|
await flushPromises()
|
|
292
302
|
expect(
|
|
293
|
-
|
|
294
|
-
.
|
|
303
|
+
getAnnotationEditor()
|
|
304
|
+
.get('select[aria-label="选择标注波形"]')
|
|
295
305
|
.findAll('option')
|
|
296
306
|
.map((item) => item.text()),
|
|
297
307
|
).toEqual(['通道 B'])
|
|
@@ -7,6 +7,14 @@ import WaveformChartView from '../WaveformChartView.vue'
|
|
|
7
7
|
|
|
8
8
|
import { gridSeries, mountSizedChart, visibilitySeries } from '../../test/waveformChart'
|
|
9
9
|
|
|
10
|
+
function annotationEditorExists() {
|
|
11
|
+
return Boolean(
|
|
12
|
+
Array.from(document.body.querySelectorAll<HTMLElement>('[role="dialog"]'))
|
|
13
|
+
.filter((element) => element.closest('.waveform-annotation-editor'))
|
|
14
|
+
.at(-1),
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
describe('WaveformChart', () => {
|
|
11
19
|
it('resolves legend positions by stable track id across pages', async () => {
|
|
12
20
|
const wrapper = await mountSizedChart(
|
|
@@ -273,7 +281,7 @@ describe('WaveformChart', () => {
|
|
|
273
281
|
)
|
|
274
282
|
flushAnimationFrames()
|
|
275
283
|
await flushPromises()
|
|
276
|
-
expect(
|
|
284
|
+
expect(annotationEditorExists()).toBe(true)
|
|
277
285
|
|
|
278
286
|
const controller = wrapper.getComponent(WaveformChartView).props('controller') as unknown as {
|
|
279
287
|
annotationInteraction: { editorDraft: { value: { annotation: { seriesId: string } } | null } }
|
|
@@ -287,7 +295,7 @@ describe('WaveformChart', () => {
|
|
|
287
295
|
expect(item).toBeDefined()
|
|
288
296
|
await item!.trigger('click')
|
|
289
297
|
await flushPromises()
|
|
290
|
-
expect(
|
|
298
|
+
expect(annotationEditorExists()).toBe(false)
|
|
291
299
|
})
|
|
292
300
|
|
|
293
301
|
it('renders independent cells with separate x axes and overlays', async () => {
|
|
@@ -208,7 +208,7 @@ describe('WaveformChart', () => {
|
|
|
208
208
|
legend
|
|
209
209
|
.findAll('.waveform-legend__swatch')
|
|
210
210
|
.map((swatch) => swatch.get('path').attributes('stroke')),
|
|
211
|
-
).toEqual(['#0960bd', '#
|
|
211
|
+
).toEqual(['#0960bd', '#2ca02c'])
|
|
212
212
|
expect(wrapper.findAll('.waveform-chart__watermark').map((item) => item.text())).toEqual([
|
|
213
213
|
'1',
|
|
214
214
|
'2',
|
|
@@ -5,6 +5,14 @@ import { flushAnimationFrames } from '../../test/setup'
|
|
|
5
5
|
import { mountSizedChart } from '../../test/waveformChart'
|
|
6
6
|
import type { WaveformData, WaveformDisplayMode } from '../data/types'
|
|
7
7
|
|
|
8
|
+
function annotationEditorExists() {
|
|
9
|
+
return Boolean(
|
|
10
|
+
Array.from(document.body.querySelectorAll<HTMLElement>('[role="dialog"]'))
|
|
11
|
+
.filter((element) => element.closest('.waveform-annotation-editor'))
|
|
12
|
+
.at(-1),
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
const presentationData: WaveformData = {
|
|
9
17
|
kind: 'series',
|
|
10
18
|
series: [
|
|
@@ -282,13 +290,13 @@ describe('WaveformChart presentation mode', () => {
|
|
|
282
290
|
}),
|
|
283
291
|
)
|
|
284
292
|
await flushPromises()
|
|
285
|
-
expect(
|
|
293
|
+
expect(annotationEditorExists()).toBe(true)
|
|
286
294
|
|
|
287
295
|
await wrapper.setProps({ presentationMode: true })
|
|
288
296
|
await flushPromises()
|
|
289
297
|
|
|
290
298
|
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
|
|
291
|
-
expect(
|
|
299
|
+
expect(annotationEditorExists()).toBe(false)
|
|
292
300
|
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
|
|
293
301
|
|
|
294
302
|
await wrapper.setProps({ presentationMode: false })
|
|
@@ -181,7 +181,7 @@ describe('WaveformChart', () => {
|
|
|
181
181
|
])
|
|
182
182
|
expect(stepSwatchPaths[0]?.attributes()).toMatchObject({
|
|
183
183
|
d: 'M1 8H25',
|
|
184
|
-
stroke: '#
|
|
184
|
+
stroke: '#2ca02c',
|
|
185
185
|
'stroke-width': '1.5',
|
|
186
186
|
})
|
|
187
187
|
expect(stepSwatchPaths[1]?.attributes()).toMatchObject({
|
|
@@ -93,4 +93,64 @@ describe('WaveformChart x domain strategy', () => {
|
|
|
93
93
|
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('10000')
|
|
94
94
|
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('15000')
|
|
95
95
|
})
|
|
96
|
+
|
|
97
|
+
it('keeps an explicit viewport wider than the data and leaves uncovered time blank', async () => {
|
|
98
|
+
const wrapper = await mountSizedChart(
|
|
99
|
+
{
|
|
100
|
+
kind: 'points',
|
|
101
|
+
points: [
|
|
102
|
+
{ x: 0, y: 0 },
|
|
103
|
+
{ x: 10, y: 1 },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{ timeUnit: 's', initialXDomain: [-5, 20] },
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const track = wrapper.get('.waveform-chart__track')
|
|
110
|
+
const width = Number(track.attributes('data-track-width'))
|
|
111
|
+
const path = track.get('.waveform-chart__line').attributes('d') ?? ''
|
|
112
|
+
const xCoordinates = Array.from(path.matchAll(/(?:M|L)([\d.-]+)/g)).map((match) =>
|
|
113
|
+
Number(match[1]),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
expect(track.get('.waveform-chart__axis-endpoint--start').text()).toBe('-5')
|
|
117
|
+
expect(track.get('.waveform-chart__axis-endpoint--end').text()).toBe('20')
|
|
118
|
+
expect(xCoordinates.length).toBeGreaterThan(0)
|
|
119
|
+
expect(Math.min(...xCoordinates)).toBeGreaterThanOrEqual(0)
|
|
120
|
+
expect(Math.max(...xCoordinates)).toBeLessThanOrEqual(width)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('applies independent explicit viewports per track, including empty time regions', async () => {
|
|
124
|
+
const wrapper = await mountSizedChart(
|
|
125
|
+
{
|
|
126
|
+
kind: 'series',
|
|
127
|
+
series: [
|
|
128
|
+
{
|
|
129
|
+
id: 'first',
|
|
130
|
+
name: '第一轨',
|
|
131
|
+
data: { kind: 'points', points: [{ x: 0, y: 0 }, { x: 10, y: 1 }] },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'second',
|
|
135
|
+
name: '第二轨',
|
|
136
|
+
data: { kind: 'points', points: [{ x: 100, y: 0 }, { x: 110, y: 1 }] },
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
displayMode: 'independent',
|
|
142
|
+
grid: { rowCount: 1, columnCount: 2 },
|
|
143
|
+
timeUnit: 's',
|
|
144
|
+
initialXDomains: { first: [-5, 20], second: [95, 120] },
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
149
|
+
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-5')
|
|
150
|
+
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('20')
|
|
151
|
+
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('95')
|
|
152
|
+
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('120')
|
|
153
|
+
expect(tracks[0]?.find('.waveform-chart__line').exists()).toBe(true)
|
|
154
|
+
expect(tracks[1]?.find('.waveform-chart__line').exists()).toBe(true)
|
|
155
|
+
})
|
|
96
156
|
})
|
package/src/core/data.test.ts
CHANGED
|
@@ -102,4 +102,26 @@ describe('waveform data normalization', () => {
|
|
|
102
102
|
'solid',
|
|
103
103
|
])
|
|
104
104
|
})
|
|
105
|
+
|
|
106
|
+
it('preserves trimmed shot numbers and omits blank values', () => {
|
|
107
|
+
const result = normalizeWaveformSeries({
|
|
108
|
+
kind: 'series',
|
|
109
|
+
series: [
|
|
110
|
+
{
|
|
111
|
+
id: 'with-shot',
|
|
112
|
+
shotNo: ' 13300 ',
|
|
113
|
+
name: '通道 A',
|
|
114
|
+
data: { kind: 'points', points: [{ x: 0, y: 1 }] },
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'without-shot',
|
|
118
|
+
shotNo: ' ',
|
|
119
|
+
name: '通道 B',
|
|
120
|
+
data: { kind: 'points', points: [{ x: 0, y: 2 }] },
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(result.map((series) => series.shotNo)).toEqual(['13300', undefined])
|
|
126
|
+
})
|
|
105
127
|
})
|
package/src/core/data.ts
CHANGED
|
@@ -132,6 +132,7 @@ export function normalizeWaveformSeries(data: WaveformData): NormalizedWaveformS
|
|
|
132
132
|
|
|
133
133
|
return {
|
|
134
134
|
id: uniqueId,
|
|
135
|
+
shotNo: series.shotNo?.trim() || undefined,
|
|
135
136
|
trackId: series.trackId?.trim() || undefined,
|
|
136
137
|
name: series.name,
|
|
137
138
|
unit: series.unit,
|
|
@@ -13,6 +13,7 @@ describe('simulated waveform data', () => {
|
|
|
13
13
|
|
|
14
14
|
expect(first.series).toHaveLength(6)
|
|
15
15
|
expect(new Set(first.series.map((series) => series.id)).size).toBe(6)
|
|
16
|
+
expect(new Set(first.series.map((series) => series.shotNo))).toEqual(new Set(['13300']))
|
|
16
17
|
first.series.forEach((series) => {
|
|
17
18
|
expect(series.data.kind).toBe('points')
|
|
18
19
|
if (series.data.kind !== 'points') return
|
|
@@ -13,7 +13,7 @@ type ErrorGenerator = (
|
|
|
13
13
|
|
|
14
14
|
interface SimulatedSeriesDefinition extends Pick<
|
|
15
15
|
WaveformSeries,
|
|
16
|
-
'id' | 'name' | 'unit' | 'lineType' | 'pointType' | 'errorBar'
|
|
16
|
+
'id' | 'shotNo' | 'name' | 'unit' | 'lineType' | 'pointType' | 'errorBar'
|
|
17
17
|
> {
|
|
18
18
|
signal: SignalGenerator
|
|
19
19
|
errors?: ErrorGenerator
|
|
@@ -46,6 +46,7 @@ function createPoints(
|
|
|
46
46
|
const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
47
47
|
{
|
|
48
48
|
id: 'simulated-sine',
|
|
49
|
+
shotNo: '13300',
|
|
49
50
|
name: '正弦基波',
|
|
50
51
|
unit: 'V',
|
|
51
52
|
lineType: 'none',
|
|
@@ -59,6 +60,7 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
59
60
|
},
|
|
60
61
|
{
|
|
61
62
|
id: 'simulated-harmonic',
|
|
63
|
+
shotNo: '13300',
|
|
62
64
|
name: '谐波扰动',
|
|
63
65
|
unit: 'V',
|
|
64
66
|
lineType: 'linear',
|
|
@@ -68,6 +70,7 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
68
70
|
},
|
|
69
71
|
{
|
|
70
72
|
id: 'simulated-damped',
|
|
73
|
+
shotNo: '13300',
|
|
71
74
|
name: '阻尼振荡',
|
|
72
75
|
unit: 'A',
|
|
73
76
|
lineType: 'linear',
|
|
@@ -79,6 +82,7 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
79
82
|
},
|
|
80
83
|
{
|
|
81
84
|
id: 'simulated-step',
|
|
85
|
+
shotNo: '13300',
|
|
82
86
|
name: '阶跃响应',
|
|
83
87
|
unit: 'V',
|
|
84
88
|
lineType: 'linear',
|
|
@@ -87,6 +91,7 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
87
91
|
},
|
|
88
92
|
{
|
|
89
93
|
id: 'simulated-pulse',
|
|
94
|
+
shotNo: '13300',
|
|
90
95
|
name: '脉冲响应',
|
|
91
96
|
unit: 'V',
|
|
92
97
|
lineType: 'linear',
|
|
@@ -95,6 +100,7 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
95
100
|
},
|
|
96
101
|
{
|
|
97
102
|
id: 'simulated-noise',
|
|
103
|
+
shotNo: '13300',
|
|
98
104
|
name: '带噪信号',
|
|
99
105
|
unit: 'A',
|
|
100
106
|
lineType: 'linear',
|
package/src/types/data.ts
CHANGED
|
@@ -47,6 +47,8 @@ export type SingleWaveformData =
|
|
|
47
47
|
*/
|
|
48
48
|
export interface WaveformSeries {
|
|
49
49
|
id?: string
|
|
50
|
+
/** 炮号,未提供时 Tooltip 显示“未配置炮号”。 */
|
|
51
|
+
shotNo?: string
|
|
50
52
|
/** 相同 trackId 的系列叠加在同一图框中;默认每个系列独占一个图框。 */
|
|
51
53
|
trackId?: string
|
|
52
54
|
name: string
|
|
@@ -74,6 +76,7 @@ export type WaveformData =
|
|
|
74
76
|
*/
|
|
75
77
|
export interface NormalizedWaveformSeries {
|
|
76
78
|
id: string
|
|
79
|
+
shotNo?: string
|
|
77
80
|
trackId?: string
|
|
78
81
|
name: string
|
|
79
82
|
unit?: string
|
|
@@ -19,6 +19,7 @@ const chartData: WaveformData = {
|
|
|
19
19
|
series: [
|
|
20
20
|
{
|
|
21
21
|
id: 'voltage',
|
|
22
|
+
shotNo: '13300',
|
|
22
23
|
name: '电压',
|
|
23
24
|
unit: 'V',
|
|
24
25
|
color: '#1677ff',
|
|
@@ -33,6 +34,7 @@ const chartData: WaveformData = {
|
|
|
33
34
|
},
|
|
34
35
|
{
|
|
35
36
|
id: 'current',
|
|
37
|
+
shotNo: '13300',
|
|
36
38
|
name: '电流',
|
|
37
39
|
unit: 'mA',
|
|
38
40
|
color: '#d4380d',
|