waveform-analysis 0.1.24 → 0.1.26

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 (40) hide show
  1. package/README.md +45 -5
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +540 -487
  4. package/dist/types/App.vue.d.ts +4 -0
  5. package/dist/types/DemoRouterApp.vue.d.ts +3 -0
  6. package/dist/types/components/core/layout.d.ts +6 -3
  7. package/dist/types/components/core/trackLayoutBuilder.d.ts +2 -0
  8. package/dist/types/components/core/useWaveformChartController.d.ts +2 -0
  9. package/dist/types/components/core/waveformChartTypes.d.ts +2 -0
  10. package/dist/types/components/core/yDomain.d.ts +7 -0
  11. package/dist/types/demo/DemoChartHost.vue.d.ts +14 -0
  12. package/dist/types/demo/DemoControlPanel.vue.d.ts +11 -0
  13. package/dist/types/demo/DemoGridFrameControls.vue.d.ts +11 -0
  14. package/dist/types/demo/DemoTitleLegendControls.vue.d.ts +11 -0
  15. package/dist/types/demo/DemoViewControls.vue.d.ts +11 -0
  16. package/dist/types/demo/types.d.ts +84 -0
  17. package/dist/types/router.d.ts +3 -0
  18. package/dist/types/views/FixedYDomainDemo.vue.d.ts +3 -0
  19. package/package.json +2 -1
  20. package/src/DemoRouterApp.vue +14 -0
  21. package/src/components/core/fixedYDomainLayout.test.ts +116 -0
  22. package/src/components/core/layout.ts +43 -10
  23. package/src/components/core/trackLayoutBuilder.ts +17 -7
  24. package/src/components/core/useWaveformChartController.ts +10 -0
  25. package/src/components/core/useWaveformLayout.ts +20 -4
  26. package/src/components/core/waveformChartTypes.ts +2 -0
  27. package/src/components/core/yDomain.test.ts +56 -0
  28. package/src/components/core/yDomain.ts +50 -0
  29. package/src/components/interaction/useWaveformViewport.ts +6 -1
  30. package/src/components/rendering/WaveformTrack.vue +1 -1
  31. package/src/components/rendering/WaveformTrackAxes.vue +5 -7
  32. package/src/components/waveformChartCases/layoutAndDomains.test.ts +25 -0
  33. package/src/components/waveformChartCases/presentationAndAxes.test.ts +26 -6
  34. package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +50 -0
  35. package/src/demoRoutes.css +197 -0
  36. package/src/main.ts +4 -2
  37. package/src/router.ts +21 -0
  38. package/src/styles.css +1 -2
  39. package/src/views/FixedYDomainDemo.test.ts +44 -0
  40. package/src/views/FixedYDomainDemo.vue +149 -0
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { DisplayTrack } from './types'
4
+ import { hasFixedYDomainForTrack, mergeYDomains, normalizeYDomain } from './yDomain'
5
+
6
+ const track: DisplayTrack = {
7
+ id: 'shared-track',
8
+ series: [],
9
+ visibleSeries: [
10
+ {
11
+ id: 'channel-a',
12
+ name: 'A',
13
+ color: '#1677ff',
14
+ lineType: 'linear',
15
+ lineStyle: 'solid',
16
+ pointType: 'none',
17
+ errorBar: { visible: false, width: 1.5, capWidth: 8 },
18
+ points: [],
19
+ xDomain: [0, 1],
20
+ yDomain: [0, 1],
21
+ hasErrorPoints: false,
22
+ },
23
+ ],
24
+ xDomain: [0, 1],
25
+ yDomain: [0, 1],
26
+ }
27
+
28
+ describe('fixed Y domains', () => {
29
+ it('normalizes valid reversed domains', () => {
30
+ expect(normalizeYDomain([97, 3])).toEqual([3, 97])
31
+ expect(normalizeYDomain([-10, 10])).toEqual([-10, 10])
32
+ })
33
+
34
+ it.each([undefined, [1, 1], [Number.NaN, 1], [0, Number.POSITIVE_INFINITY]])(
35
+ 'rejects an invalid domain: %j',
36
+ (domain) => {
37
+ expect(normalizeYDomain(domain as [number, number] | undefined)).toBeUndefined()
38
+ },
39
+ )
40
+
41
+ it('detects valid track, series, and global configuration only', () => {
42
+ expect(hasFixedYDomainForTrack(track, [-1, 1])).toBe(true)
43
+ expect(hasFixedYDomainForTrack(track, undefined, { 'shared-track': [-2, 2] })).toBe(true)
44
+ expect(hasFixedYDomainForTrack(track, undefined, { 'channel-a': [-3, 3] })).toBe(true)
45
+ expect(hasFixedYDomainForTrack(track, [0, 0], { 'channel-a': [1, 1] })).toBe(false)
46
+ })
47
+
48
+ it('merges configured and automatic series domains without padding', () => {
49
+ expect(
50
+ mergeYDomains([
51
+ [3, 97],
52
+ [-20, 40],
53
+ ]),
54
+ ).toEqual([-20, 97])
55
+ })
56
+ })
@@ -0,0 +1,50 @@
1
+ import type { DisplaySeries, DisplayTrack } from './types'
2
+
3
+ export type WaveformYDomain = [number, number]
4
+
5
+ export function normalizeYDomain(
6
+ domain: readonly [number, number] | undefined,
7
+ ): WaveformYDomain | undefined {
8
+ if (
9
+ !domain ||
10
+ !Number.isFinite(domain[0]) ||
11
+ !Number.isFinite(domain[1]) ||
12
+ domain[0] === domain[1]
13
+ ) {
14
+ return undefined
15
+ }
16
+ return domain[0] < domain[1] ? [domain[0], domain[1]] : [domain[1], domain[0]]
17
+ }
18
+
19
+ export function resolveTrackFixedYDomain(
20
+ track: Pick<DisplayTrack, 'id'>,
21
+ yDomains?: Record<string, WaveformYDomain>,
22
+ ): WaveformYDomain | undefined {
23
+ return normalizeYDomain(yDomains?.[track.id])
24
+ }
25
+
26
+ export function resolveSeriesFixedYDomain(
27
+ series: Pick<DisplaySeries, 'id'>,
28
+ yDomain?: WaveformYDomain,
29
+ yDomains?: Record<string, WaveformYDomain>,
30
+ ): WaveformYDomain | undefined {
31
+ return normalizeYDomain(yDomains?.[series.id]) ?? normalizeYDomain(yDomain)
32
+ }
33
+
34
+ export function hasFixedYDomainForTrack(
35
+ track: Pick<DisplayTrack, 'id' | 'visibleSeries'>,
36
+ yDomain?: WaveformYDomain,
37
+ yDomains?: Record<string, WaveformYDomain>,
38
+ ): boolean {
39
+ return Boolean(
40
+ resolveTrackFixedYDomain(track, yDomains) ||
41
+ track.visibleSeries.some((series) => resolveSeriesFixedYDomain(series, yDomain, yDomains)),
42
+ )
43
+ }
44
+
45
+ export function mergeYDomains(domains: readonly WaveformYDomain[]): WaveformYDomain {
46
+ return [
47
+ Math.min(...domains.map((domain) => domain[0])),
48
+ Math.max(...domains.map((domain) => domain[1])),
49
+ ]
50
+ }
@@ -3,6 +3,7 @@ import { computed, nextTick, type ComputedRef, type Ref, type ShallowRef } from
3
3
 
4
4
  import { MINIMUM_SELECTION_SIZE } from '../core/constants'
5
5
  import type { DisplayTrack, TrackLayout } from '../core/types'
6
+ import { hasFixedYDomainForTrack } from '../core/yDomain'
6
7
  import type {
7
8
  ResolvedWaveformChartProps,
8
9
  ViewportSelectionState,
@@ -190,9 +191,13 @@ export function useWaveformViewport(context: ViewportContext) {
190
191
  const nextIndependentDomains = { ...independentYDomains.value }
191
192
  const nextSharedDomains = { ...sharedYDomains.value }
192
193
  targets.forEach((target) => {
194
+ const chartTrack = chartTracks.value.find((item) => item.id === target.id)
195
+ if (chartTrack && hasFixedYDomainForTrack(chartTrack, props.yDomain, props.yDomains)) {
196
+ return
197
+ }
193
198
  const key = target.series.trackId ?? target.series.id
194
199
  const source = active.yDomains[key] ?? (target.yScale.domain() as [number, number])
195
- const boundary = chartTracks.value.find((item) => item.id === key)?.yDomain ?? source
200
+ const boundary = chartTrack?.yDomain ?? source
196
201
  const ySpan = source[1] - source[0]
197
202
  const nextY = clampDomain(
198
203
  [source[0] + (dy / height) * ySpan, source[1] + (dy / height) * ySpan],
@@ -94,7 +94,7 @@ const resolvedFrameStyle = computed(() => {
94
94
  />
95
95
 
96
96
  <rect
97
- v-if="!track.isEmpty && !cleanView"
97
+ v-if="!track.isEmpty"
98
98
  class="waveform-track__plot-frame waveform-chart__plot-frame"
99
99
  :width="track.width ?? innerWidth"
100
100
  :height="track.height"
@@ -103,14 +103,14 @@ watch(
103
103
 
104
104
  <template>
105
105
  <g
106
- v-if="track.showXAxis && !cleanView"
106
+ v-if="track.showXAxis"
107
107
  ref="xAxisElement"
108
108
  class="waveform-track__axis waveform-track__axis--x waveform-chart__axis waveform-chart__axis--x"
109
109
  :class="{ 'waveform-track__axis--line-hidden': axes?.x?.lineVisible === false }"
110
110
  :transform="`translate(0, ${track.height})`"
111
111
  />
112
112
  <g
113
- v-if="track.showXAxis && !cleanView"
113
+ v-if="track.showXAxis"
114
114
  class="waveform-track__axis-endpoints waveform-chart__axis-endpoints"
115
115
  :transform="`translate(0, ${track.height})`"
116
116
  font-family="sans-serif"
@@ -137,7 +137,7 @@ watch(
137
137
  </text>
138
138
  </g>
139
139
  <text
140
- v-if="track.showXAxis && track.xAxisExponent && !cleanView"
140
+ v-if="track.showXAxis && track.xAxisExponent"
141
141
  class="waveform-track__axis-exponent waveform-track__axis-exponent--x waveform-chart__axis-exponent waveform-chart__axis-exponent--x"
142
142
  :x="track.width ?? innerWidth"
143
143
  :y="track.height + 27"
@@ -148,7 +148,7 @@ watch(
148
148
  </text>
149
149
 
150
150
  <g
151
- v-for="axis in track.isEmpty || cleanView ? [] : track.yAxes"
151
+ v-for="axis in track.isEmpty ? [] : track.yAxes"
152
152
  :key="`y-axis-${track.index}-${axis.index}`"
153
153
  :ref="(element) => setYAxisElement(element, axis.index)"
154
154
  class="waveform-track__axis waveform-track__axis--y waveform-chart__axis waveform-chart__axis--y"
@@ -161,9 +161,7 @@ watch(
161
161
  :transform="`translate(${axis.x}, 0)`"
162
162
  />
163
163
  <text
164
- v-for="axis in track.isEmpty || cleanView
165
- ? []
166
- : track.yAxes.filter((item) => item.exponentLabel)"
164
+ v-for="axis in track.isEmpty ? [] : track.yAxes.filter((item) => item.exponentLabel)"
167
165
  :key="`y-axis-exponent-${track.index}-${axis.index}`"
168
166
  class="waveform-track__axis-exponent waveform-track__axis-exponent--y waveform-chart__axis-exponent waveform-chart__axis-exponent--y"
169
167
  :data-y-axis-index="axis.index"
@@ -294,6 +294,31 @@ describe('WaveformChart', () => {
294
294
  expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
295
295
  })
296
296
 
297
+ it('reacts to exact fixed Y-domain props and returns to automatic bounds', async () => {
298
+ const wrapper = await mountSizedChart({
299
+ kind: 'points',
300
+ points: [
301
+ { x: 0, y: 0 },
302
+ { x: 1, y: 100 },
303
+ ],
304
+ })
305
+ const yTickLabels = () =>
306
+ wrapper
307
+ .get('.waveform-chart__axis--y')
308
+ .findAll('.tick text')
309
+ .map((tick) => tick.text())
310
+
311
+ await wrapper.setProps({ yDomain: [3, 97] })
312
+ await flushPromises()
313
+ expect(yTickLabels()).toContain('3.00')
314
+ expect(yTickLabels()).toContain('97.00')
315
+
316
+ await wrapper.setProps({ yDomain: undefined })
317
+ await flushPromises()
318
+ expect(yTickLabels()).not.toContain('3.00')
319
+ expect(yTickLabels()).not.toContain('97.00')
320
+ })
321
+
297
322
  it('keeps annotations bound to their channel while paging', async () => {
298
323
  const wrapper = await mountSizedChart(gridSeries(3), {
299
324
  grid: { rowCount: 1, columnCount: 1 },
@@ -146,7 +146,7 @@ describe('WaveformChart', () => {
146
146
  kind: 'points',
147
147
  points: [
148
148
  { x: 0, y: -1 },
149
- { x: 1, y: 1 },
149
+ { x: 10_000, y: 1 },
150
150
  ],
151
151
  },
152
152
  },
@@ -227,8 +227,8 @@ describe('WaveformChart', () => {
227
227
  data: {
228
228
  kind: 'points',
229
229
  points: [
230
- { x: 0, y: 1 },
231
- { x: 1, y: 2 },
230
+ { x: 0, y: 10_000 },
231
+ { x: 10_000, y: 20_000 },
232
232
  ],
233
233
  },
234
234
  },
@@ -239,7 +239,7 @@ describe('WaveformChart', () => {
239
239
  kind: 'points',
240
240
  points: [
241
241
  { x: 0, y: 2 },
242
- { x: 1, y: 3 },
242
+ { x: 10_000, y: 3 },
243
243
  ],
244
244
  },
245
245
  },
@@ -252,6 +252,8 @@ describe('WaveformChart', () => {
252
252
  annotations: [{ id: 'note', seriesId: 'first', x: 0.5, y: 0, text: 'hidden note' }],
253
253
  zeroLine: { visible: true },
254
254
  title: { text: 'hidden title' },
255
+ axes: { x: { lineVisible: false }, y: { lineVisible: false } },
256
+ frameStyle: { borderColor: '#dc2626', borderWidth: 2, borderStyle: 'dashed' as const },
255
257
  }
256
258
  const regularWrapper = await mountSizedChart(data, sharedProps)
257
259
  const wrapper = await mountSizedChart(data, {
@@ -285,13 +287,31 @@ describe('WaveformChart', () => {
285
287
  expect(wrapper.find('.waveform-chart__overlay--independent').exists()).toBe(true)
286
288
  expect(wrapper.get('.waveform-chart__title-area').attributes('aria-hidden')).toBe('true')
287
289
  expect(wrapper.find('.waveform-chart__title-visual').exists()).toBe(false)
288
- expect(wrapper.find('.waveform-chart__axis').exists()).toBe(false)
290
+ const xAxis = wrapper.get('.waveform-chart__axis--x')
291
+ const yAxes = wrapper.findAll('.waveform-chart__axis--y')
292
+ expect(yAxes).toHaveLength(2)
293
+ expect(xAxis.findAll('.tick line').length).toBeGreaterThan(0)
294
+ expect(xAxis.findAll('.tick text').length).toBeGreaterThan(0)
295
+ yAxes.forEach((axis) => {
296
+ expect(axis.findAll('.tick line').length).toBeGreaterThan(0)
297
+ expect(axis.findAll('.tick text').length).toBeGreaterThan(0)
298
+ expect(axis.get('path.domain').attributes('display')).toBe('none')
299
+ })
300
+ expect(xAxis.get('path.domain').attributes('display')).toBe('none')
301
+ expect(wrapper.findAll('.waveform-chart__axis-endpoint')).toHaveLength(2)
302
+ expect(wrapper.get('.waveform-chart__axis-exponent--x').text()).not.toBe('')
303
+ expect(wrapper.get('.waveform-chart__axis-exponent--y').text()).not.toBe('')
289
304
  expect(wrapper.find('.waveform-chart__grid').exists()).toBe(false)
290
- expect(wrapper.find('.waveform-chart__plot-frame').exists()).toBe(false)
305
+ expect(wrapper.get('.waveform-chart__plot-frame').attributes()).toMatchObject({
306
+ stroke: '#dc2626',
307
+ 'stroke-width': '2',
308
+ 'stroke-dasharray': '6 4',
309
+ })
291
310
  expect(wrapper.find('.waveform-chart__plot-background').exists()).toBe(false)
292
311
  expect(wrapper.find('.waveform-chart__watermark').exists()).toBe(false)
293
312
  expect(wrapper.find('.waveform-chart__legend-layer').exists()).toBe(false)
294
313
  expect(wrapper.find('.waveform-chart__label').exists()).toBe(false)
314
+ expect(wrapper.find('.waveform-chart__y-axis-label').exists()).toBe(false)
295
315
  expect(wrapper.find('.waveform-chart__zero-line').exists()).toBe(false)
296
316
  expect(wrapper.find('.waveform-annotation-layer').exists()).toBe(false)
297
317
  expect(wrapper.find('.ant-pagination').exists()).toBe(false)
@@ -224,6 +224,56 @@ describe('WaveformChart', () => {
224
224
  window.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space' }))
225
225
  })
226
226
 
227
+ it('keeps a fixed Y domain through panning and viewport reset', async () => {
228
+ const wrapper = await mountSizedChart(
229
+ {
230
+ kind: 'points',
231
+ points: [
232
+ { x: 0, y: 0 },
233
+ { x: 1, y: 100 },
234
+ ],
235
+ },
236
+ { pannable: true, yDomain: [3, 97] },
237
+ )
238
+ const overlay = wrapper.get('.waveform-chart__overlay--independent')
239
+ const width = Number(overlay.attributes('width'))
240
+ const height = Number(overlay.attributes('height'))
241
+ Object.defineProperty(overlay.element, 'getBoundingClientRect', {
242
+ value: () => ({ left: 0, top: 0, width, height }),
243
+ })
244
+ const yTickLabels = () =>
245
+ wrapper
246
+ .get('.waveform-chart__axis--y')
247
+ .findAll('.tick text')
248
+ .map((tick) => tick.text())
249
+ const initialLabels = yTickLabels()
250
+
251
+ await wrapper.trigger('pointerenter')
252
+ window.dispatchEvent(new KeyboardEvent('keydown', { code: 'Space', cancelable: true }))
253
+ const down = new MouseEvent('pointerdown', {
254
+ button: 0,
255
+ clientX: width / 2,
256
+ clientY: height / 2,
257
+ bubbles: true,
258
+ })
259
+ Object.defineProperty(down, 'pointerId', { value: 34 })
260
+ overlay.element.dispatchEvent(down)
261
+ const move = new MouseEvent('pointermove', {
262
+ clientX: width / 2 + 20,
263
+ clientY: height / 2 + 40,
264
+ bubbles: true,
265
+ })
266
+ Object.defineProperty(move, 'pointerId', { value: 34 })
267
+ overlay.element.dispatchEvent(move)
268
+ await flushPromises()
269
+
270
+ expect(yTickLabels()).toEqual(initialLabels)
271
+ ;(wrapper.vm as unknown as { resetViewport: () => void }).resetViewport()
272
+ await flushPromises()
273
+ expect(yTickLabels()).toEqual(initialLabels)
274
+ window.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space' }))
275
+ })
276
+
227
277
  it('does not activate pannable on a chart that the pointer is outside', async () => {
228
278
  const data: WaveformData = {
229
279
  kind: 'points',
@@ -0,0 +1,197 @@
1
+ .demo-shell {
2
+ display: grid;
3
+ grid-template-rows: 48px minmax(0, 1fr);
4
+ width: 100%;
5
+ height: 100%;
6
+ overflow: hidden;
7
+ }
8
+
9
+ .demo-shell__header {
10
+ z-index: 40;
11
+ display: flex;
12
+ gap: 24px;
13
+ align-items: center;
14
+ min-width: 0;
15
+ padding: 0 16px;
16
+ background: #fff;
17
+ border-bottom: 1px solid #e4e7ec;
18
+ }
19
+
20
+ .demo-shell__brand {
21
+ flex: 0 0 auto;
22
+ color: #101828;
23
+ font-size: 14px;
24
+ font-weight: 700;
25
+ text-decoration: none;
26
+ }
27
+
28
+ .demo-shell__nav {
29
+ display: flex;
30
+ align-self: stretch;
31
+ min-width: 0;
32
+ }
33
+
34
+ .demo-shell__nav a {
35
+ display: inline-flex;
36
+ align-items: center;
37
+ padding: 0 14px;
38
+ color: #475467;
39
+ font-size: 13px;
40
+ text-decoration: none;
41
+ border-bottom: 2px solid transparent;
42
+ }
43
+
44
+ .demo-shell__nav a:hover,
45
+ .demo-shell__nav a:focus-visible,
46
+ .demo-shell__nav a.router-link-exact-active {
47
+ color: #0958d9;
48
+ }
49
+
50
+ .demo-shell__nav a.router-link-exact-active {
51
+ background: #f0f7ff;
52
+ border-bottom-color: #1677ff;
53
+ }
54
+
55
+ .demo-shell__content {
56
+ min-width: 0;
57
+ min-height: 0;
58
+ overflow: hidden;
59
+ }
60
+
61
+ .fixed-domain-demo {
62
+ display: grid;
63
+ grid-template-rows: auto auto minmax(0, 1fr);
64
+ gap: 12px;
65
+ width: 100%;
66
+ height: 100%;
67
+ padding: 16px;
68
+ overflow: hidden;
69
+ }
70
+
71
+ .fixed-domain-demo__toolbar {
72
+ display: flex;
73
+ gap: 24px;
74
+ align-items: center;
75
+ justify-content: space-between;
76
+ min-width: 0;
77
+ }
78
+
79
+ .fixed-domain-demo__toolbar h1 {
80
+ margin: 0;
81
+ color: #101828;
82
+ font-size: 18px;
83
+ font-weight: 700;
84
+ letter-spacing: 0;
85
+ }
86
+
87
+ .fixed-domain-demo__toolbar p {
88
+ margin: 4px 0 0;
89
+ color: #667085;
90
+ font-size: 12px;
91
+ }
92
+
93
+ .fixed-domain-demo__range-controls {
94
+ display: flex;
95
+ gap: 16px;
96
+ align-items: center;
97
+ min-height: 48px;
98
+ padding: 8px 12px;
99
+ background: #fff;
100
+ border: 1px solid #e4e7ec;
101
+ border-radius: 6px;
102
+ }
103
+
104
+ .fixed-domain-demo__range-controls label {
105
+ display: flex;
106
+ gap: 8px;
107
+ align-items: center;
108
+ color: #475467;
109
+ font-size: 12px;
110
+ }
111
+
112
+ .fixed-domain-demo__range-controls .ant-input-number {
113
+ width: 112px;
114
+ }
115
+
116
+ .fixed-domain-demo__range-controls strong {
117
+ color: #344054;
118
+ font-size: 12px;
119
+ }
120
+
121
+ .fixed-domain-demo__range-controls strong:not(:first-child) {
122
+ padding-left: 16px;
123
+ border-left: 1px solid #e4e7ec;
124
+ }
125
+
126
+ .fixed-domain-demo__auto-state {
127
+ color: #667085;
128
+ font-size: 12px;
129
+ }
130
+
131
+ .fixed-domain-demo__chart {
132
+ min-width: 0;
133
+ min-height: 0;
134
+ overflow: hidden;
135
+ }
136
+
137
+ .fixed-domain-demo__chart > .waveform-chart {
138
+ min-height: 0;
139
+ }
140
+
141
+ @media (max-width: 900px) {
142
+ .demo-shell {
143
+ grid-template-rows: 44px minmax(0, 1fr);
144
+ }
145
+
146
+ .demo-shell__header {
147
+ gap: 8px;
148
+ padding: 0 8px;
149
+ }
150
+
151
+ .demo-shell__brand {
152
+ display: none;
153
+ }
154
+
155
+ .demo-shell__nav {
156
+ width: 100%;
157
+ }
158
+
159
+ .demo-shell__nav a {
160
+ flex: 1 1 0;
161
+ justify-content: center;
162
+ padding: 0 6px;
163
+ }
164
+
165
+ .fixed-domain-demo {
166
+ gap: 8px;
167
+ padding: 10px;
168
+ }
169
+
170
+ .fixed-domain-demo__toolbar {
171
+ display: grid;
172
+ gap: 8px;
173
+ }
174
+
175
+ .fixed-domain-demo__toolbar .ant-radio-group {
176
+ display: flex;
177
+ width: 100%;
178
+ }
179
+
180
+ .fixed-domain-demo__toolbar .ant-radio-button-wrapper {
181
+ flex: 1 1 0;
182
+ padding-inline: 6px;
183
+ text-align: center;
184
+ }
185
+
186
+ .fixed-domain-demo__range-controls {
187
+ flex-wrap: wrap;
188
+ gap: 8px 12px;
189
+ }
190
+
191
+ .fixed-domain-demo__range-controls strong:not(:first-child) {
192
+ width: 100%;
193
+ padding: 8px 0 0;
194
+ border-top: 1px solid #e4e7ec;
195
+ border-left: 0;
196
+ }
197
+ }
package/src/main.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { createApp } from 'vue'
2
2
  import 'ant-design-vue/dist/antd.css'
3
3
 
4
- import App from './App.vue'
4
+ import DemoRouterApp from './DemoRouterApp.vue'
5
+ import { router } from './router'
6
+ import './demoRoutes.css'
5
7
  import './styles.css'
6
8
 
7
- createApp(App).mount('#app')
9
+ createApp(DemoRouterApp).use(router).mount('#app')
package/src/router.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router'
2
+
3
+ import App from './App.vue'
4
+
5
+ export const routes: RouteRecordRaw[] = [
6
+ {
7
+ path: '/',
8
+ name: 'workspace',
9
+ component: App,
10
+ },
11
+ {
12
+ path: '/fixed-y-domain',
13
+ name: 'fixed-y-domain',
14
+ component: () => import('./views/FixedYDomainDemo.vue'),
15
+ },
16
+ ]
17
+
18
+ export const router = createRouter({
19
+ history: createWebHashHistory(),
20
+ routes,
21
+ })
package/src/styles.css CHANGED
@@ -35,8 +35,7 @@ body {
35
35
  grid-template-columns: 280px minmax(0, 1fr);
36
36
  gap: 12px;
37
37
  width: 100%;
38
- height: 100vh;
39
- height: 100dvh;
38
+ height: 100%;
40
39
  padding: 12px;
41
40
  overflow: hidden;
42
41
  }
@@ -0,0 +1,44 @@
1
+ import { flushPromises, mount } from '@vue/test-utils'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { WaveformChart } from '../components'
5
+ import { routes } from '../router'
6
+ import FixedYDomainDemo from './FixedYDomainDemo.vue'
7
+
8
+ describe('fixed Y-domain demo route', () => {
9
+ it('registers the example route', () => {
10
+ expect(routes).toEqual(
11
+ expect.arrayContaining([
12
+ expect.objectContaining({
13
+ path: '/fixed-y-domain',
14
+ name: 'fixed-y-domain',
15
+ }),
16
+ ]),
17
+ )
18
+ })
19
+
20
+ it('switches between global, automatic, and per-channel ranges', async () => {
21
+ const wrapper = mount(FixedYDomainDemo)
22
+ await flushPromises()
23
+ const chart = () => wrapper.getComponent(WaveformChart)
24
+ const modeInputs = wrapper.get('[aria-label="Y 轴范围模式"]').findAll('input[type="radio"]')
25
+
26
+ expect(chart().props('yDomain')).toEqual([-80, 80])
27
+ expect(chart().props('yDomains')).toBeUndefined()
28
+
29
+ await modeInputs[0]?.setValue(true)
30
+ await flushPromises()
31
+ expect(chart().props('yDomain')).toBeUndefined()
32
+ expect(chart().props('yDomains')).toBeUndefined()
33
+
34
+ await modeInputs[2]?.setValue(true)
35
+ await flushPromises()
36
+ expect(chart().props('yDomain')).toBeUndefined()
37
+ expect(chart().props('yDomains')).toEqual({
38
+ voltage: [-65, 65],
39
+ current: [-260, 260],
40
+ })
41
+
42
+ wrapper.unmount()
43
+ })
44
+ })