waveform-analysis 0.1.37 → 0.1.39

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.
@@ -1,6 +1,7 @@
1
1
  import type { WaveformChartProps } from './core/waveformChartTypes';
2
2
  declare const __VLS_export: import("vue").DefineComponent<WaveformChartProps, {
3
3
  resetViewport: (trackIndex?: number) => void;
4
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void;
4
5
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
5
6
  "point-hover": (point: import("..").WaveformPoint | null) => any;
6
7
  "zoom-change": (domain: [number, number]) => any;
@@ -228,6 +228,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
228
228
  finishViewportDrag: (event: PointerEvent) => void;
229
229
  cancelViewportDrag: (event?: PointerEvent) => void;
230
230
  resetViewport: (trackIndex?: number) => void;
231
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void;
231
232
  requestViewportReset: (event: MouseEvent) => void;
232
233
  toggleSeriesVisibility: (seriesId: string) => void;
233
234
  changeDraftSeries: (seriesId: string) => void;
@@ -31,6 +31,9 @@ interface LifecycleContext {
31
31
  trackLayouts: ComputedRef<TrackLayout[]>;
32
32
  innerWidth: ComputedRef<number>;
33
33
  innerHeight: ComputedRef<number>;
34
+ sharedZoomDomain: ComputedRef<[number, number]>;
35
+ initialXDomain: ComputedRef<[number, number]>;
36
+ sharedTransform: ShallowRef<ZoomTransform>;
34
37
  activeInteractionMode: ComputedRef<string | undefined>;
35
38
  hiddenSeriesIdSet: ComputedRef<Set<string>>;
36
39
  internalHiddenSeriesIds: Ref<Set<string>>;
@@ -42,6 +42,7 @@ export declare function useWaveformViewport(context: ViewportContext): {
42
42
  finishViewportDrag: (event: PointerEvent) => void;
43
43
  cancelViewportDrag: (event?: PointerEvent) => void;
44
44
  resetViewport: (trackIndex?: number) => void;
45
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void;
45
46
  requestViewportReset: (event: MouseEvent) => void;
46
47
  };
47
48
  export {};
@@ -0,0 +1,21 @@
1
+ import { type ComputedRef, type Ref, type ShallowRef } from 'vue';
2
+ import type { ZoomTransform } from 'd3';
3
+ import type { TrackLayout } from '../core/types';
4
+ import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes';
5
+ import type { AnnotationSeriesCandidate } from '../annotation';
6
+ interface ViewportDomainContext {
7
+ props: ResolvedWaveformChartProps;
8
+ trackLayouts: ComputedRef<TrackLayout[]>;
9
+ initialXDomain: ComputedRef<[number, number]>;
10
+ resolveInitialTrackDomain: (track: TrackLayout) => [number, number];
11
+ innerWidth: ComputedRef<number>;
12
+ sharedTransform: ShallowRef<ZoomTransform>;
13
+ independentTransforms: ShallowRef<ZoomTransform[]>;
14
+ cancelPendingZoom: () => void;
15
+ cancelViewportDrag: () => void;
16
+ clearHover: () => void;
17
+ editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>;
18
+ configureZoom: () => void;
19
+ }
20
+ export declare function createViewportDomainSetter(context: ViewportDomainContext): (domain: [number, number], trackIndex?: number) => void;
21
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
package/src/App.test.ts CHANGED
@@ -235,7 +235,7 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
235
235
  tracks.map((track) =>
236
236
  track.findAll('.waveform-chart__series').map((item) => item.attributes('data-series-name')),
237
237
  ),
238
- ).toEqual([['正弦基波'], ['谐波扰动'], ['阻尼振荡'], ['阶跃响应']])
238
+ ).toEqual([['正弦基波'], ['谐波扰动', '谐波对比'], ['阻尼振荡'], ['阶跃响应']])
239
239
  expect(
240
240
  tracks
241
241
  .slice(1)
@@ -253,6 +253,7 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
253
253
  expect(simulatedSeries.map((item) => item.name)).toEqual([
254
254
  '正弦基波',
255
255
  '谐波扰动',
256
+ '谐波对比',
256
257
  '阻尼振荡',
257
258
  '阶跃响应',
258
259
  '脉冲响应',
@@ -321,7 +322,6 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
321
322
  const styleSelect = frameControls.findAllComponents(Select)[0]
322
323
 
323
324
  expect(colorPickers).toHaveLength(2)
324
-
325
325
  const initialFrames = wrapper.findAll('.waveform-chart__plot-frame')
326
326
  expect(initialFrames.length > 1 && initialFrames.every((frame) => frame.attributes('stroke-width') === '2')).toBe(true)
327
327
 
@@ -36,6 +36,7 @@ const controller = useWaveformChartController(props as ResolvedWaveformChartProp
36
36
 
37
37
  defineExpose({
38
38
  resetViewport: controller.resetViewport,
39
+ setViewportDomain: controller.setViewportDomain,
39
40
  })
40
41
  </script>
41
42
 
@@ -259,6 +259,9 @@ export function useWaveformChartController(
259
259
  trackLayouts,
260
260
  innerWidth,
261
261
  innerHeight,
262
+ sharedZoomDomain,
263
+ initialXDomain,
264
+ sharedTransform,
262
265
  activeInteractionMode,
263
266
  hiddenSeriesIdSet,
264
267
  internalHiddenSeriesIds,
@@ -39,6 +39,9 @@ interface LifecycleContext {
39
39
  trackLayouts: ComputedRef<TrackLayout[]>
40
40
  innerWidth: ComputedRef<number>
41
41
  innerHeight: ComputedRef<number>
42
+ sharedZoomDomain: ComputedRef<[number, number]>
43
+ initialXDomain: ComputedRef<[number, number]>
44
+ sharedTransform: ShallowRef<ZoomTransform>
42
45
  activeInteractionMode: ComputedRef<string | undefined>
43
46
  hiddenSeriesIdSet: ComputedRef<Set<string>>
44
47
  internalHiddenSeriesIds: Ref<Set<string>>
@@ -93,6 +96,9 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
93
96
  trackLayouts,
94
97
  innerWidth,
95
98
  innerHeight,
99
+ sharedZoomDomain,
100
+ initialXDomain,
101
+ sharedTransform,
96
102
  activeInteractionMode,
97
103
  hiddenSeriesIdSet,
98
104
  internalHiddenSeriesIds,
@@ -144,25 +150,24 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
144
150
  emit('page-change', nextPage, pageCount.value)
145
151
  }
146
152
 
153
+ let pendingSharedXDomain: [number, number] | undefined
147
154
  let pendingIndependentXDomains: Array<[number, number] | undefined> | undefined
148
155
 
149
156
  function handleBeforeDataReferenceChange() {
150
- if (props.displayMode !== 'independent') return
151
- pendingIndependentXDomains = trackLayouts.value.map((track) => {
152
- const configuredDomain =
153
- props.initialXDomains?.[track.series.trackId ?? track.series.id] ??
154
- props.initialXDomains?.[track.series.id] ??
155
- props.initialXDomain
156
- if (
157
- !configuredDomain ||
158
- !Number.isFinite(configuredDomain[0]) ||
159
- !Number.isFinite(configuredDomain[1]) ||
160
- configuredDomain[0] === configuredDomain[1]
161
- ) {
162
- return undefined
163
- }
164
- return track.xScale.domain() as [number, number]
165
- })
157
+ if (props.displayMode === 'independent') {
158
+ pendingSharedXDomain = undefined
159
+ pendingIndependentXDomains = trackLayouts.value.map((track) => {
160
+ const current = track.xScale.domain() as [number, number]
161
+ const boundary = resolveInitialTrackDomain(track)
162
+ return current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? current : undefined
163
+ })
164
+ return
165
+ }
166
+ pendingIndependentXDomains = undefined
167
+ const current = sharedZoomDomain.value
168
+ const boundary = initialXDomain.value
169
+ pendingSharedXDomain =
170
+ current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? [...current] : undefined
166
171
  }
167
172
 
168
173
  function handleDataReferenceChange() {
@@ -186,10 +191,15 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
186
191
  })
187
192
  independentTransforms.value = nextTransforms
188
193
  pendingIndependentXDomains = undefined
189
- void nextTick(configureZoom)
190
- return
194
+ } else if (props.displayMode !== 'independent' && pendingSharedXDomain) {
195
+ const boundary = initialXDomain.value
196
+ const groups = trackLayouts.value
197
+ .filter((track) => track.hasVisibleSeries)
198
+ .map((track) => track.seriesList)
199
+ const domain = constrainZoomDomain(pendingSharedXDomain, boundary, groups, props)
200
+ sharedTransform.value = transformForDomain(domain, boundary, innerWidth.value)
201
+ pendingSharedXDomain = undefined
191
202
  }
192
- pendingIndependentXDomains = undefined
193
203
  configureZoom()
194
204
  })
195
205
  }
@@ -11,6 +11,7 @@ import type {
11
11
  import type { AnnotationSeriesCandidate } from '../annotation'
12
12
  import { tryReleasePointerCapture } from './pointerCapture'
13
13
  import { transitionViewportInteraction } from './viewportInteractionState'
14
+ import { createViewportDomainSetter } from './viewportDomain'
14
15
  import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
15
16
  interface ViewportContext {
16
17
  props: ResolvedWaveformChartProps
@@ -344,6 +345,20 @@ export function useWaveformViewport(context: ViewportContext) {
344
345
  editorSeriesOptions.value = []
345
346
  void nextTick(configureZoom)
346
347
  }
348
+ const setViewportDomain = createViewportDomainSetter({
349
+ props,
350
+ trackLayouts,
351
+ initialXDomain,
352
+ resolveInitialTrackDomain,
353
+ innerWidth,
354
+ sharedTransform,
355
+ independentTransforms,
356
+ cancelPendingZoom,
357
+ cancelViewportDrag,
358
+ clearHover,
359
+ editorSeriesOptions,
360
+ configureZoom,
361
+ })
347
362
  const requestViewportReset = (event: MouseEvent) => {
348
363
  if (isPresentationMode.value || !props.zoomable || !isZoomMode.value) return
349
364
  if (props.displayMode === 'independent') {
@@ -372,6 +387,7 @@ export function useWaveformViewport(context: ViewportContext) {
372
387
  finishViewportDrag,
373
388
  cancelViewportDrag,
374
389
  resetViewport,
390
+ setViewportDomain,
375
391
  requestViewportReset,
376
392
  }
377
393
  }
@@ -0,0 +1,60 @@
1
+ import { nextTick, type ComputedRef, type Ref, type ShallowRef } from 'vue'
2
+ import type { ZoomTransform } from 'd3'
3
+
4
+ import type { TrackLayout } from '../core/types'
5
+ import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
6
+ import type { AnnotationSeriesCandidate } from '../annotation'
7
+ import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
8
+
9
+ interface ViewportDomainContext {
10
+ props: ResolvedWaveformChartProps
11
+ trackLayouts: ComputedRef<TrackLayout[]>
12
+ initialXDomain: ComputedRef<[number, number]>
13
+ resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
14
+ innerWidth: ComputedRef<number>
15
+ sharedTransform: ShallowRef<ZoomTransform>
16
+ independentTransforms: ShallowRef<ZoomTransform[]>
17
+ cancelPendingZoom: () => void
18
+ cancelViewportDrag: () => void
19
+ clearHover: () => void
20
+ editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
21
+ configureZoom: () => void
22
+ }
23
+
24
+ export function createViewportDomainSetter(context: ViewportDomainContext) {
25
+ return (domain: [number, number], trackIndex?: number) => {
26
+ if (!Number.isFinite(domain[0]) || !Number.isFinite(domain[1]) || domain[0] === domain[1])
27
+ return
28
+ context.cancelPendingZoom()
29
+ context.cancelViewportDrag()
30
+ if (context.props.displayMode === 'independent') {
31
+ const indexes =
32
+ trackIndex === undefined
33
+ ? context.trackLayouts.value.map((track) => track.index)
34
+ : [trackIndex]
35
+ const nextTransforms = [...context.independentTransforms.value]
36
+ indexes.forEach((index) => {
37
+ const track = context.trackLayouts.value.find((item) => item.index === index)
38
+ if (!track) return
39
+ const boundary = context.resolveInitialTrackDomain(track)
40
+ const constrained = constrainZoomDomain(domain, boundary, [track.seriesList], context.props)
41
+ nextTransforms[index] = transformForDomain(constrained, boundary, track.width)
42
+ })
43
+ context.independentTransforms.value = nextTransforms
44
+ } else {
45
+ const boundary = context.initialXDomain.value
46
+ const groups = context.trackLayouts.value
47
+ .filter((track) => track.hasVisibleSeries)
48
+ .map((track) => track.seriesList)
49
+ const constrained = constrainZoomDomain(domain, boundary, groups, context.props)
50
+ context.sharedTransform.value = transformForDomain(
51
+ constrained,
52
+ boundary,
53
+ context.innerWidth.value,
54
+ )
55
+ }
56
+ context.clearHover()
57
+ context.editorSeriesOptions.value = []
58
+ void nextTick(context.configureZoom)
59
+ }
60
+ }
@@ -16,13 +16,30 @@ function annotationEditorExists() {
16
16
  }
17
17
 
18
18
  describe('WaveformChart', () => {
19
+ it('shows legends when a track contains at least two series', async () => {
20
+ const oneSeries = visibilitySeries()
21
+ if (oneSeries.kind === 'series') oneSeries.series = oneSeries.series.slice(0, 1)
22
+ const withoutLegend = await mountSizedChart(oneSeries, {
23
+ grid: { rowCount: 1, columnCount: 1 },
24
+ })
25
+ expect(withoutLegend.findAll('.waveform-chart__legend')).toHaveLength(0)
26
+
27
+ const twoSeries = visibilitySeries()
28
+ if (twoSeries.kind === 'series') twoSeries.series = twoSeries.series.slice(0, 2)
29
+ const withLegend = await mountSizedChart(twoSeries, {
30
+ grid: { rowCount: 1, columnCount: 1 },
31
+ })
32
+ expect(withLegend.findAll('.waveform-chart__legend')).toHaveLength(1)
33
+ expect(withLegend.find('.waveform-chart__legend').attributes('data-position')).toBe('top-right')
34
+ })
35
+
19
36
  it('resolves legend positions by stable track id across pages', async () => {
20
37
  const wrapper = await mountSizedChart(
21
38
  {
22
39
  kind: 'series',
23
- series: Array.from({ length: 8 }, (_, index) => ({
40
+ series: Array.from({ length: 12 }, (_, index) => ({
24
41
  id: `series-${index}`,
25
- trackId: `frame-${Math.floor(index / 2)}`,
42
+ trackId: `frame-${Math.floor(index / 3)}`,
26
43
  name: `series ${index}`,
27
44
  data: {
28
45
  kind: 'points' as const,
@@ -75,9 +92,9 @@ describe('WaveformChart', () => {
75
92
  const wrapper = await mountSizedChart(
76
93
  {
77
94
  kind: 'series',
78
- series: Array.from({ length: 4 }, (_, index) => ({
95
+ series: Array.from({ length: 6 }, (_, index) => ({
79
96
  id: `series-${index}`,
80
- trackId: `frame-${Math.floor(index / 2)}`,
97
+ trackId: `frame-${Math.floor(index / 3)}`,
81
98
  name: `series ${index}`,
82
99
  data: {
83
100
  kind: 'points',
@@ -112,7 +129,7 @@ describe('WaveformChart', () => {
112
129
  })
113
130
 
114
131
  const items = wrapper.findAll('.waveform-chart__legend-item')
115
- expect(items).toHaveLength(2)
132
+ expect(items).toHaveLength(3)
116
133
  expect(items.every((item) => item.attributes('disabled') !== undefined)).toBe(true)
117
134
  expect(wrapper.get('.waveform-legend__panel').classes()).not.toContain(
118
135
  'waveform-legend__panel--interactive',
@@ -128,7 +145,7 @@ describe('WaveformChart', () => {
128
145
  overlayMode: 'multi-axis',
129
146
  })
130
147
 
131
- expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(2)
148
+ expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(3)
132
149
  expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(true)
133
150
  const annotationLayer = wrapper.get('.waveform-annotation-layer').element
134
151
  const legendLayer = wrapper.get('.waveform-chart__legend-layer').element
@@ -143,8 +160,8 @@ describe('WaveformChart', () => {
143
160
 
144
161
  expect(
145
162
  wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
146
- ).toEqual(['low'])
147
- expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(1)
163
+ ).toEqual(['low', 'mid'])
164
+ expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(2)
148
165
  expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(false)
149
166
  expect(wrapper.findAll('.waveform-chart__legend-item')[1].classes()).toContain('is-hidden')
150
167
  expect(wrapper.findAll('.waveform-chart__legend-item')[1].attributes('aria-pressed')).toBe(
@@ -170,12 +187,12 @@ describe('WaveformChart', () => {
170
187
  )
171
188
  flushAnimationFrames()
172
189
  await flushPromises()
173
- expect(wrapper.findAll('.waveform-chart__tooltip-series')).toHaveLength(1)
190
+ expect(wrapper.findAll('.waveform-chart__tooltip-series')).toHaveLength(2)
174
191
  expect(wrapper.get('.waveform-chart__tooltip-series').text()).toContain('低量程')
175
192
 
176
193
  await wrapper.findAll('.waveform-chart__legend-item')[1].trigger('click')
177
194
  await flushPromises()
178
- expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(2)
195
+ expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(3)
179
196
  expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(true)
180
197
  expect(wrapper.emitted('series-visibility-change')?.at(-1)).toEqual([
181
198
  { seriesId: 'high', visible: true, hiddenSeriesIds: [] },
@@ -191,20 +208,20 @@ describe('WaveformChart', () => {
191
208
 
192
209
  expect(
193
210
  wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
194
- ).toEqual(['low'])
211
+ ).toEqual(['low', 'mid'])
195
212
  await wrapper.findAll('.waveform-chart__legend-item')[0].trigger('click')
196
213
  expect(wrapper.emitted('update:hidden-series-ids')?.at(-1)).toEqual([
197
214
  ['high', 'temporarily-absent', 'low'],
198
215
  ])
199
216
  expect(
200
217
  wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
201
- ).toEqual(['low'])
218
+ ).toEqual(['low', 'mid'])
202
219
 
203
220
  await wrapper.setProps({ hiddenSeriesIds: ['low'] })
204
221
  await flushPromises()
205
222
  expect(
206
223
  wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
207
- ).toEqual(['high'])
224
+ ).toEqual(['high', 'mid'])
208
225
  })
209
226
 
210
227
  it('retains uncontrolled visibility by stable ID and clears removed IDs', async () => {
@@ -223,7 +240,7 @@ describe('WaveformChart', () => {
223
240
  await flushPromises()
224
241
  expect(
225
242
  wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
226
- ).toEqual(['low'])
243
+ ).toEqual(['mid', 'low'])
227
244
 
228
245
  await wrapper.setProps({
229
246
  data: {
@@ -234,12 +251,12 @@ describe('WaveformChart', () => {
234
251
  await flushPromises()
235
252
  await wrapper.setProps({ data: original })
236
253
  await flushPromises()
237
- expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(2)
254
+ expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(3)
238
255
  })
239
256
 
240
257
  it('keeps a recoverable legend and stops chart interaction when every series is hidden', async () => {
241
258
  const wrapper = await mountSizedChart(visibilitySeries(), {
242
- defaultHiddenSeriesIds: ['low', 'high'],
259
+ defaultHiddenSeriesIds: ['low', 'high', 'mid'],
243
260
  grid: { rowCount: 1, columnCount: 1 },
244
261
  legend: { interactive: true },
245
262
  overlayMode: 'multi-axis',
@@ -248,7 +265,7 @@ describe('WaveformChart', () => {
248
265
  expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(0)
249
266
  expect(wrapper.findAll('.waveform-chart__axis')).toHaveLength(0)
250
267
  expect(wrapper.findAll('.waveform-chart__overlay')).toHaveLength(0)
251
- expect(wrapper.findAll('.waveform-chart__legend-item')).toHaveLength(2)
268
+ expect(wrapper.findAll('.waveform-chart__legend-item')).toHaveLength(3)
252
269
  expect(wrapper.get('.waveform-track__no-visible-series').text()).toBe('暂无可见曲线')
253
270
 
254
271
  await wrapper.findAll('.waveform-chart__legend-item')[0].trigger('click')
@@ -177,6 +177,18 @@ describe('WaveformChart', () => {
177
177
  ],
178
178
  },
179
179
  },
180
+ {
181
+ id: 'reference',
182
+ trackId: 'frame-1',
183
+ name: 'REF_CH_1',
184
+ data: {
185
+ kind: 'points',
186
+ points: [
187
+ { x: 0, y: 0.25 },
188
+ { x: 1, y: 1.25 },
189
+ ],
190
+ },
191
+ },
180
192
  ],
181
193
  },
182
194
  { frameNumber: 1, grid: { rowCount: 2, columnCount: 1 } },
@@ -186,7 +198,7 @@ describe('WaveformChart', () => {
186
198
  expect(tracks).toHaveLength(2)
187
199
  expect(
188
200
  tracks[0].findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
189
- ).toEqual(['primary', 'comparison'])
201
+ ).toEqual(['primary', 'comparison', 'reference'])
190
202
  expect(
191
203
  tracks[1].findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
192
204
  ).toEqual(['second-frame'])
@@ -203,12 +215,13 @@ describe('WaveformChart', () => {
203
215
  expect(legend.findAll('.waveform-chart__legend-item').map((item) => item.text())).toEqual([
204
216
  'BT2_2M',
205
217
  'TEST_CH_1',
218
+ 'REF_CH_1',
206
219
  ])
207
220
  expect(
208
221
  legend
209
222
  .findAll('.waveform-legend__swatch')
210
223
  .map((swatch) => swatch.get('path').attributes('stroke')),
211
- ).toEqual(['#0960bd', '#2ca02c'])
224
+ ).toEqual(['#0960bd', '#2ca02c', '#d62728'])
212
225
  expect(wrapper.findAll('.waveform-chart__watermark').map((item) => item.text())).toEqual([
213
226
  '1',
214
227
  '2',
@@ -232,10 +245,11 @@ describe('WaveformChart', () => {
232
245
  await flushPromises()
233
246
 
234
247
  const tooltipSeries = wrapper.findAll('.waveform-chart__tooltip-series')
235
- expect(tooltipSeries).toHaveLength(2)
248
+ expect(tooltipSeries).toHaveLength(3)
236
249
  expect(tooltipSeries.map((item) => item.text())).toEqual([
237
250
  expect.stringContaining('BT2_2M'),
238
251
  expect.stringContaining('TEST_CH_1'),
252
+ expect.stringContaining('REF_CH_1'),
239
253
  ])
240
254
  })
241
255
 
@@ -268,6 +282,18 @@ describe('WaveformChart', () => {
268
282
  ],
269
283
  },
270
284
  },
285
+ {
286
+ id: 'third',
287
+ trackId: 'shared',
288
+ name: 'third',
289
+ data: {
290
+ kind: 'points',
291
+ points: [
292
+ { x: 0, y: 2 },
293
+ { x: 1, y: 3 },
294
+ ],
295
+ },
296
+ },
271
297
  ],
272
298
  },
273
299
  { grid: { rowCount: 1, columnCount: 1 } },
@@ -42,6 +42,19 @@ const presentationData: WaveformData = {
42
42
  ],
43
43
  },
44
44
  },
45
+ {
46
+ id: 'mid',
47
+ trackId: 'shared',
48
+ name: '中量程',
49
+ data: {
50
+ kind: 'points',
51
+ points: [
52
+ { x: 0, y: 50 },
53
+ { x: 1, y: 100 },
54
+ { x: 2, y: 75 },
55
+ ],
56
+ },
57
+ },
45
58
  {
46
59
  id: 'other',
47
60
  name: '其他通道',
@@ -150,9 +163,9 @@ describe('WaveformChart presentation mode', () => {
150
163
  cancelable: true,
151
164
  }),
152
165
  )
153
- wrapper.get('.waveform-chart__svg').element.dispatchEvent(
154
- new MouseEvent('dblclick', { bubbles: true, cancelable: true }),
155
- )
166
+ wrapper
167
+ .get('.waveform-chart__svg')
168
+ .element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }))
156
169
  await wrapper.get('[data-annotation-id="note"]').trigger('contextmenu', {
157
170
  clientX: width / 2,
158
171
  clientY: height / 2,
@@ -119,6 +119,108 @@ describe('WaveformChart viewport reset', () => {
119
119
  expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
120
120
  })
121
121
 
122
+ it('keeps a controlled shared viewport when the data reference changes', async () => {
123
+ const wrapper = await mountSizedChart(
124
+ {
125
+ kind: 'points',
126
+ points: [
127
+ { x: 0, y: 0 },
128
+ { x: 1, y: 1 },
129
+ ],
130
+ },
131
+ { displayMode: 'separated' },
132
+ )
133
+ const chart = wrapper.vm as unknown as {
134
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void
135
+ }
136
+ chart.setViewportDomain([0.25, 0.75])
137
+ await flushPromises()
138
+
139
+ const startBefore = Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())
140
+ const endBefore = Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())
141
+ expect(startBefore).toBeGreaterThan(0)
142
+ expect(endBefore).toBeLessThan(1000)
143
+
144
+ await wrapper.setProps({
145
+ data: {
146
+ kind: 'points',
147
+ points: [
148
+ { x: 0, y: 10 },
149
+ { x: 1, y: 11 },
150
+ ],
151
+ },
152
+ })
153
+ await flushPromises()
154
+
155
+ expect(Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())).toBeCloseTo(
156
+ startBefore,
157
+ 10,
158
+ )
159
+ expect(Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())).toBeCloseTo(
160
+ endBefore,
161
+ 10,
162
+ )
163
+ })
164
+
165
+ it('keeps independently controlled track viewports when the data reference changes', async () => {
166
+ const wrapper = await mountSizedChart(gridSeries(2), {
167
+ displayMode: 'independent',
168
+ grid: { rowCount: 1, columnCount: 2 },
169
+ })
170
+ const chart = wrapper.vm as unknown as {
171
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void
172
+ }
173
+ chart.setViewportDomain([0.2, 0.8], 0)
174
+ chart.setViewportDomain([0.1, 0.9], 1)
175
+ await flushPromises()
176
+
177
+ const endpoints = () =>
178
+ wrapper.findAll('.waveform-chart__track').map((track) => ({
179
+ start: track.get('.waveform-chart__axis-endpoint--start').text(),
180
+ end: track.get('.waveform-chart__axis-endpoint--end').text(),
181
+ }))
182
+ const endpointsBefore = endpoints()
183
+ expect(Number(endpointsBefore[0].start)).toBeGreaterThan(0)
184
+ expect(Number(endpointsBefore[0].end)).toBeLessThan(1000)
185
+ expect(Number(endpointsBefore[1].start)).toBeGreaterThan(0)
186
+ expect(Number(endpointsBefore[1].end)).toBeLessThan(1000)
187
+
188
+ await wrapper.setProps({ data: gridSeries(2) })
189
+ await flushPromises()
190
+
191
+ const endpointsAfter = endpoints()
192
+ endpointsAfter.forEach((endpoint, index) => {
193
+ expect(Number(endpoint.start)).toBeCloseTo(Number(endpointsBefore[index].start), 10)
194
+ expect(Number(endpoint.end)).toBeCloseTo(Number(endpointsBefore[index].end), 10)
195
+ })
196
+ })
197
+
198
+ it('ignores invalid domains and applies viewport constraints', async () => {
199
+ const wrapper = await mountSizedChart(
200
+ {
201
+ kind: 'points',
202
+ points: [
203
+ { x: 0, y: 0 },
204
+ { x: 1, y: 1 },
205
+ ],
206
+ },
207
+ { displayMode: 'separated', minZoomSpan: 0.2 },
208
+ )
209
+ const chart = wrapper.vm as unknown as {
210
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void
211
+ }
212
+ chart.setViewportDomain([Number.NaN, 0.5])
213
+ await flushPromises()
214
+ expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
215
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
216
+
217
+ chart.setViewportDomain([0.49, 0.51])
218
+ await flushPromises()
219
+ const start = Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())
220
+ const end = Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())
221
+ expect(end - start).toBeGreaterThanOrEqual(200)
222
+ })
223
+
122
224
  it('resets shared and independent explicit domains to their included nice bounds', async () => {
123
225
  const shared = await mountSizedChart(
124
226
  {