waveform-analysis 0.1.55 → 0.1.58

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 (29) hide show
  1. package/README.md +3 -1
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +565 -539
  4. package/dist/types/components/core/grid.d.ts +3 -0
  5. package/dist/types/components/core/layout.d.ts +18 -0
  6. package/dist/types/components/core/trackLayoutBuilder.d.ts +2 -0
  7. package/dist/types/components/core/trackPagination.d.ts +4 -0
  8. package/dist/types/components/core/useWaveformChartController.d.ts +3 -1
  9. package/dist/types/components/core/useWaveformLayout.d.ts +2 -1
  10. package/dist/types/components/core/waveformChartTypes.d.ts +2 -0
  11. package/dist/types/components/core/yAxisConstants.d.ts +1 -0
  12. package/package.json +1 -1
  13. package/src/App.test.ts +2 -1
  14. package/src/components/WaveformChartView.vue +3 -3
  15. package/src/components/core/grid.test.ts +3 -0
  16. package/src/components/core/grid.ts +4 -0
  17. package/src/components/core/layout.test.ts +1 -1
  18. package/src/components/core/layout.ts +63 -0
  19. package/src/components/core/trackLayoutBuilder.ts +24 -4
  20. package/src/components/core/trackPagination.ts +40 -0
  21. package/src/components/core/useWaveformLayout.ts +28 -31
  22. package/src/components/core/waveformChartTypes.ts +2 -0
  23. package/src/components/core/yAxisConstants.ts +1 -0
  24. package/src/components/core/yAxisSlotLayout.test.ts +129 -0
  25. package/src/components/waveformChartCases/layoutAndDomains.test.ts +3 -5
  26. package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +61 -0
  27. package/src/components/waveformChartCases/trackOrder.test.ts +62 -0
  28. package/src/data/simulatedWaveforms.test.ts +4 -4
  29. package/src/data/simulatedWaveforms.ts +12 -0
@@ -0,0 +1,129 @@
1
+ import { zoomIdentity } from 'd3'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
5
+ import { buildTrackLayouts, buildYAxisSlots } from './layout'
6
+ import type { DisplaySeries, DisplayTrack } from './types'
7
+ import { Y_AXIS_RIGHT_LABEL_OFFSET } from './yAxisConstants'
8
+
9
+ function series(id: string, minimum: number, maximum: number): DisplaySeries {
10
+ return {
11
+ id,
12
+ name: id,
13
+ color: '#1677ff',
14
+ lineType: 'linear',
15
+ lineStyle: 'solid',
16
+ pointType: 'none',
17
+ errorBar: { visible: false, width: 1.5, capWidth: 8 },
18
+ points: [
19
+ { x: 0, y: minimum },
20
+ { x: 1, y: maximum },
21
+ ],
22
+ xDomain: [0, 1],
23
+ yDomain: [minimum, maximum],
24
+ hasErrorPoints: false,
25
+ }
26
+ }
27
+
28
+ function track(seriesList: DisplaySeries[]): DisplayTrack {
29
+ return {
30
+ id: seriesList[0]!.id,
31
+ series: seriesList,
32
+ visibleSeries: seriesList,
33
+ xDomain: [0, 1],
34
+ yDomain: [0, 1],
35
+ }
36
+ }
37
+
38
+ describe('Y-axis slots', () => {
39
+ it('aligns matching slots across single and multi-axis tracks', () => {
40
+ const tracks = [
41
+ track([series('single', -1e120, 1e120)]),
42
+ track([series('left', 0, 1), series('right', 1000, 3000)]),
43
+ track([series('left-two', 0, 1), series('right-two', 1000, 3000)]),
44
+ ]
45
+ const yAxisSlots = buildYAxisSlots(tracks, 'multi-axis')
46
+ const layouts = buildTrackLayouts({
47
+ cells: tracks.map((series, index) => ({
48
+ slotIndex: index,
49
+ row: index,
50
+ column: 0,
51
+ left: 0,
52
+ top: index * 100,
53
+ width: 600,
54
+ height: 100,
55
+ plotHeight: 100,
56
+ cellHeight: 130,
57
+ xAxisBand: 30,
58
+ series,
59
+ })),
60
+ grid: {
61
+ rowCount: 3,
62
+ columnCount: 1,
63
+ showPagination: false,
64
+ fillIncompleteLastRow: false,
65
+ trackLines: {},
66
+ },
67
+ displayMode: 'independent',
68
+ overlayMode: 'multi-axis',
69
+ independentTransforms: [zoomIdentity, zoomIdentity, zoomIdentity],
70
+ sharedZoomDomain: [0, 1],
71
+ timeUnit: 'ms',
72
+ rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
73
+ hideSecondaryLabels: false,
74
+ yAxisLabelX: yAxisSlots.slots.find((slot) => slot.side === 'left')!.labelOffset,
75
+ yAxisSlots: yAxisSlots.slots,
76
+ showCompactEmptyTracks: false,
77
+ })
78
+
79
+ expect(layouts.map((layout) => layout.yAxes[0]?.labelX)).toEqual([
80
+ layouts[0]?.yAxes[0]?.labelX,
81
+ layouts[0]?.yAxes[0]?.labelX,
82
+ layouts[0]?.yAxes[0]?.labelX,
83
+ ])
84
+ expect(layouts[1]?.yAxes[1]?.labelX).toBe(layouts[2]?.yAxes[1]?.labelX)
85
+ expect(layouts[1]?.yAxes[1]?.labelX).toBe(
86
+ 600 +
87
+ yAxisSlots.slots.find((slot) => slot.side === 'right')!.labelOffset -
88
+ Y_AXIS_RIGHT_LABEL_OFFSET,
89
+ )
90
+ expect(yAxisSlots.slots).toMatchObject([
91
+ { side: 'left', sideIndex: 0, axisOffset: 0 },
92
+ { side: 'right', sideIndex: 0, axisOffset: 0 },
93
+ ])
94
+ expect(yAxisSlots.clearance.left).toBeGreaterThan(yAxisSlots.clearance.right)
95
+ })
96
+
97
+ it('reserves outward second slots for three and four value axes', () => {
98
+ const threeAxisSlots = buildYAxisSlots(
99
+ [track([series('left', 0, 1), series('right-one', 10, 20), series('right-two', 30, 40)])],
100
+ 'multi-axis',
101
+ )
102
+ const fourAxisSlots = buildYAxisSlots(
103
+ [
104
+ track([
105
+ series('left-one', 0, 1),
106
+ series('left-two', 10, 20),
107
+ series('right-one', 30, 40),
108
+ series('right-two', 50, 60),
109
+ ]),
110
+ ],
111
+ 'multi-axis',
112
+ )
113
+
114
+ expect(threeAxisSlots.slots).toMatchObject([
115
+ { side: 'left', sideIndex: 0, axisOffset: 0 },
116
+ { side: 'right', sideIndex: 0, axisOffset: 0 },
117
+ { side: 'right', sideIndex: 1 },
118
+ ])
119
+ expect(threeAxisSlots.slots[2]!.axisOffset).toBeGreaterThan(0)
120
+ expect(fourAxisSlots.slots).toMatchObject([
121
+ { side: 'left', sideIndex: 0, axisOffset: 0 },
122
+ { side: 'left', sideIndex: 1 },
123
+ { side: 'right', sideIndex: 0, axisOffset: 0 },
124
+ { side: 'right', sideIndex: 1 },
125
+ ])
126
+ expect(fourAxisSlots.slots[1]!.axisOffset).toBeLessThan(0)
127
+ expect(fourAxisSlots.slots[3]!.axisOffset).toBeGreaterThan(0)
128
+ })
129
+ })
@@ -86,7 +86,7 @@ describe('WaveformChart', () => {
86
86
  expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(60)
87
87
  })
88
88
 
89
- it('keeps the Y-axis label gutter stable while paging between value ranges', async () => {
89
+ it('updates the Y-axis label position while preserving the chart gutter between pages', async () => {
90
90
  const wrapper = await mountSizedChart(
91
91
  {
92
92
  kind: 'series',
@@ -118,14 +118,12 @@ describe('WaveformChart', () => {
118
118
  { grid: { rowCount: 1, columnCount: 1 } },
119
119
  )
120
120
  const initialMargin = wrapper.attributes('data-chart-left-margin')
121
- const initialLabelX = wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')
121
+ expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-34')
122
122
 
123
123
  await wrapper.get('.ant-pagination-next button').trigger('click')
124
124
 
125
125
  expect(wrapper.attributes('data-chart-left-margin')).toBe(initialMargin)
126
- expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe(
127
- initialLabelX,
128
- )
126
+ expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-62')
129
127
  })
130
128
 
131
129
  it('hides only secondary-column Y-axis labels when the grid is too narrow', async () => {
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { mountSizedChart } from '../../test/waveformChart'
4
+
5
+ describe('WaveformChart multi-axis labels', () => {
6
+ it('aligns single-axis and multi-axis Y-axis titles in one column', async () => {
7
+ const wrapper = await mountSizedChart(
8
+ {
9
+ kind: 'series',
10
+ series: [
11
+ {
12
+ id: 'single',
13
+ trackId: 'single-track',
14
+ name: '单轴',
15
+ data: {
16
+ kind: 'points',
17
+ points: [
18
+ { x: 0, y: -1e120 },
19
+ { x: 1, y: 1e120 },
20
+ ],
21
+ },
22
+ },
23
+ {
24
+ id: 'left',
25
+ trackId: 'multi-track',
26
+ name: '左轴',
27
+ data: {
28
+ kind: 'points',
29
+ points: [
30
+ { x: 0, y: 0 },
31
+ { x: 1, y: 1 },
32
+ ],
33
+ },
34
+ },
35
+ {
36
+ id: 'right',
37
+ trackId: 'multi-track',
38
+ name: '右轴',
39
+ data: {
40
+ kind: 'points',
41
+ points: [
42
+ { x: 0, y: 1000 },
43
+ { x: 1, y: 3000 },
44
+ ],
45
+ },
46
+ },
47
+ ],
48
+ },
49
+ { overlayMode: 'multi-axis', grid: { rowCount: 2, columnCount: 1 } },
50
+ )
51
+ const tracks = wrapper.findAll('.waveform-chart__track')
52
+ const titleX = (transform: string | undefined) =>
53
+ Number(transform?.match(/translate\(([^,]+)/)?.[1])
54
+
55
+ expect(titleX(tracks[0]!.get('.waveform-chart__y-axis-label').attributes('transform'))).toBe(
56
+ titleX(
57
+ tracks[1]!.findAll('.waveform-track__multi-axis-title text')[0]!.attributes('transform'),
58
+ ),
59
+ )
60
+ })
61
+ })
@@ -104,6 +104,68 @@ describe('WaveformChart track order', () => {
104
104
  expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(2)
105
105
  })
106
106
 
107
+ it('keeps incremental watermarks stable when empty ordered tracks are hidden', async () => {
108
+ const wrapper = await mountSizedChart(mergedFourTrackSeries(), {
109
+ frameNumber: 1,
110
+ grid: {
111
+ rowCount: 2,
112
+ columnCount: 1,
113
+ hideEmptyTracks: true,
114
+ trackOrder: ['frame-1', 'frame-2', 'frame-3', 'frame-4'],
115
+ },
116
+ })
117
+
118
+ expect(
119
+ wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id')),
120
+ ).toEqual(['frame-1', 'frame-3'])
121
+ expect(
122
+ wrapper.findAll('.waveform-chart__watermark').map((watermark) => watermark.text()),
123
+ ).toEqual(['1', '3'])
124
+
125
+ await wrapper.get('.ant-pagination-next button').trigger('click')
126
+
127
+ expect(
128
+ wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id')),
129
+ ).toEqual(['frame-4'])
130
+ expect(
131
+ wrapper.findAll('.waveform-chart__watermark').map((watermark) => watermark.text()),
132
+ ).toEqual(['4'])
133
+ })
134
+
135
+ it('uses stable track IDs for explicit frame watermark overrides', async () => {
136
+ const wrapper = await mountSizedChart(mergedFourTrackSeries(), {
137
+ frameNumber: 1,
138
+ frameNumbers: { 'frame-1': 'A', 'frame-3': 'C', 'frame-4': 'D' },
139
+ grid: {
140
+ rowCount: 2,
141
+ columnCount: 1,
142
+ hideEmptyTracks: true,
143
+ trackOrder: ['frame-1', 'frame-2', 'frame-3', 'frame-4'],
144
+ },
145
+ })
146
+
147
+ expect(
148
+ wrapper.findAll('.waveform-chart__watermark').map((watermark) => watermark.text()),
149
+ ).toEqual(['A', 'C'])
150
+
151
+ await wrapper.get('.ant-pagination-next button').trigger('click')
152
+
153
+ expect(
154
+ wrapper.findAll('.waveform-chart__watermark').map((watermark) => watermark.text()),
155
+ ).toEqual(['D'])
156
+ })
157
+
158
+ it('keeps the existing incremental watermark fallback when frame mappings are absent', async () => {
159
+ const wrapper = await mountSizedChart(orderedSeries(['frame-1', 'frame-2']), {
160
+ frameNumber: 7,
161
+ grid: { rowCount: 2, columnCount: 1 },
162
+ })
163
+
164
+ expect(
165
+ wrapper.findAll('.waveform-chart__watermark').map((watermark) => watermark.text()),
166
+ ).toEqual(['7', '8'])
167
+ })
168
+
107
169
  it('appends data tracks absent from trackOrder in input order', async () => {
108
170
  const wrapper = await mountSizedChart(
109
171
  {
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
3
3
  import { createSimulatedWaveformData } from './simulatedWaveforms'
4
4
 
5
5
  describe('simulated waveform data', () => {
6
- it('creates deterministic, finite seven-channel data with a two-series second frame', () => {
6
+ it('creates deterministic, finite eight-channel data with a three-series second frame', () => {
7
7
  const first = createSimulatedWaveformData()
8
8
  const second = createSimulatedWaveformData()
9
9
 
@@ -11,12 +11,12 @@ describe('simulated waveform data', () => {
11
11
  expect(first.kind).toBe('series')
12
12
  if (first.kind !== 'series') return
13
13
 
14
- expect(first.series).toHaveLength(7)
15
- expect(new Set(first.series.map((series) => series.id)).size).toBe(7)
14
+ expect(first.series).toHaveLength(8)
15
+ expect(new Set(first.series.map((series) => series.id)).size).toBe(8)
16
16
  const secondFrame = first.series.filter(
17
17
  (series) => series.trackId === 'simulated-harmonic-frame',
18
18
  )
19
- expect(secondFrame).toHaveLength(2)
19
+ expect(secondFrame).toHaveLength(3)
20
20
  expect(new Set(first.series.map((series) => series.shotNo))).toEqual(new Set(['13300']))
21
21
  const firstSeries = first.series[0]
22
22
  expect(firstSeries).toMatchObject({
@@ -84,6 +84,18 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
84
84
  signal: (time) =>
85
85
  0.62 * Math.sin(TWO_PI * 0.55 * time + 0.55) + 0.18 * Math.sin(TWO_PI * 2.2 * time - 0.2),
86
86
  },
87
+ {
88
+ id: 'simulated-harmonic-correction',
89
+ trackId: 'simulated-harmonic-frame',
90
+ shotNo: '13300',
91
+ name: '谐波校正',
92
+ unit: 'V',
93
+ color: '#722ed1',
94
+ lineType: 'linear',
95
+ pointType: 'none',
96
+ signal: (time) =>
97
+ 0.48 * Math.sin(TWO_PI * 0.55 * time - 0.35) + 0.12 * Math.sin(TWO_PI * 2.2 * time + 0.7),
98
+ },
87
99
  {
88
100
  id: 'simulated-damped',
89
101
  shotNo: '13300',