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.
Files changed (42) hide show
  1. package/README.md +9 -5
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +909 -816
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/annotation/WaveformAnnotationEditor.vue.d.ts +4 -0
  6. package/dist/types/components/annotation/useWaveformChartAnnotations.d.ts +3 -1
  7. package/dist/types/components/core/constants.d.ts +3 -1
  8. package/dist/types/components/core/types.d.ts +2 -0
  9. package/dist/types/components/core/useWaveformChartController.d.ts +2 -0
  10. package/dist/types/components/interaction/WaveformTooltip.vue.d.ts +1 -0
  11. package/dist/types/types/data.d.ts +3 -0
  12. package/package.json +1 -1
  13. package/src/App.test.ts +3 -2
  14. package/src/App.vue +1 -1
  15. package/src/components/WaveformChartView.vue +10 -1
  16. package/src/components/annotation/WaveformAnnotationEditor.css +35 -104
  17. package/src/components/annotation/WaveformAnnotationEditor.vue +87 -37
  18. package/src/components/annotation/components.test.ts +134 -20
  19. package/src/components/annotation/markup.test.ts +18 -2
  20. package/src/components/annotation/markup.ts +33 -0
  21. package/src/components/annotation/useWaveformChartAnnotations.ts +53 -12
  22. package/src/components/core/constants.test.ts +29 -0
  23. package/src/components/core/constants.ts +31 -9
  24. package/src/components/core/types.ts +2 -0
  25. package/src/components/interaction/WaveformTooltip.test.ts +17 -9
  26. package/src/components/interaction/WaveformTooltip.vue +41 -45
  27. package/src/components/interaction/useWaveformHover.ts +1 -0
  28. package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +13 -3
  29. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +4 -4
  30. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +42 -2
  31. package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +24 -14
  32. package/src/components/waveformChartCases/legendVisibility.test.ts +10 -2
  33. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +1 -1
  34. package/src/components/waveformChartCases/presentationMode.test.ts +10 -2
  35. package/src/components/waveformChartCases/renderingStyles.test.ts +1 -1
  36. package/src/components/waveformChartCases/xDomainStrategy.test.ts +60 -0
  37. package/src/core/data.test.ts +22 -0
  38. package/src/core/data.ts +1 -0
  39. package/src/data/simulatedWaveforms.test.ts +1 -0
  40. package/src/data/simulatedWaveforms.ts +7 -1
  41. package/src/types/data.ts +3 -0
  42. 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 = 320
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 error = formatError(seriesPoint.point)
49
- return `${seriesPoint.name ? `${seriesPoint.name}: ` : ''}${formatTooltipNumber(seriesPoint.point.y)}${
50
- seriesPoint.unit ? ` ${seriesPoint.unit}` : ''
51
- }${error ? ` ${error}` : ''}`
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, timeText: string): 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 * (estimateLineCount(timeText, width) + seriesLines) + 5
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, timeText) - 8,
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
- <strong v-if="seriesPoint.name">{{ seriesPoint.name }}:</strong>
125
- <span class="waveform-tooltip__value">
126
- {{ formatTooltipNumber(seriesPoint.point.y)
127
- }}{{ seriesPoint.unit ? ` ${seriesPoint.unit}` : '' }}
128
- <small v-if="formatError(seriesPoint.point)">{{ formatError(seriesPoint.point) }}</small>
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: 3px;
131
+ gap: 2px;
142
132
  width: max-content;
143
- max-width: min(320px, calc(100% - 16px));
144
- padding: 8px 10px;
145
- color: #333;
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 #d9d9d9;
154
- border-radius: 2px;
155
- box-shadow: 0 4px 12px rgb(16 24 40 / 12%);
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-wrap: anywhere;
181
- white-space: normal;
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
- overflow-wrap: anywhere;
185
+ color: #555;
190
186
  }
191
187
 
192
188
  .waveform-tooltip__series small {
@@ -85,6 +85,7 @@ export function useWaveformHover(context: HoverContext) {
85
85
  point: WaveformPoint,
86
86
  ): HoveredSeriesPoint => ({
87
87
  id: series.id,
88
+ shotNo: series.shotNo,
88
89
  name: series.name,
89
90
  color: series.color,
90
91
  unit: series.unit,
@@ -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
- await wrapper.get('textarea[aria-label="标注文本"]').setValue('新文字')
35
- await wrapper.get('.waveform-annotation-editor button.is-primary').trigger('click')
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 T')
184
- expect(tooltip.text()).toContain('BT1_2M:')
185
- expect(tooltip.text()).toContain('4 T')
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-chart__tooltip-time').text()).toBe('ms: 1,000')
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-chart__tooltip-time').text()).toBe('ms: 1,000')
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
- expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
176
- await wrapper.get('textarea[aria-label="标注文本"]').setValue('峰值点')
177
- await wrapper.get('.waveform-annotation-editor button.is-primary').trigger('click')
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
- expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
213
- expect(wrapper.get('.waveform-annotation-editor').attributes('aria-modal')).toBe('true')
214
- expect(wrapper.find('.waveform-annotation-editor__panel').exists()).toBe(true)
215
- const textarea = wrapper.get('textarea[aria-label="标注文本"]')
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 wrapper.get('.waveform-annotation-editor button.is-primary').trigger('click')
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
- wrapper
282
- .find('select[aria-label="选择标注波形"]')
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 wrapper.get('button[aria-label="关闭标注编辑器"]').trigger('click')
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
- wrapper
294
- .find('select[aria-label="选择标注波形"]')
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(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
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(wrapper.find('.waveform-annotation-editor').exists()).toBe(false)
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', '#389e0d'])
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(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
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(wrapper.find('.waveform-annotation-editor').exists()).toBe(false)
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: '#389e0d',
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
  })
@@ -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',