waveform-analysis 0.1.63 → 0.1.65

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 (97) hide show
  1. package/README.md +21 -5
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +986 -964
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/core/constants.d.ts +1 -1
  6. package/dist/types/components/core/trackLayoutBuilder.d.ts +1 -0
  7. package/dist/types/components/core/yAxisConstants.d.ts +1 -1
  8. package/dist/types/types/chart.d.ts +6 -1
  9. package/package.json +1 -1
  10. package/src/App.vue +2 -2
  11. package/src/components/WaveformChart.css +4 -0
  12. package/src/components/WaveformChart.vue +2 -2
  13. package/src/components/core/constants.ts +3 -3
  14. package/src/components/core/layout.ts +1 -1
  15. package/src/components/core/trackLayoutBuilder.ts +42 -1
  16. package/src/components/core/useWaveformLayout.ts +2 -1
  17. package/src/components/core/useWaveformPresentation.ts +1 -1
  18. package/src/components/core/xDomain.ts +26 -2
  19. package/src/components/core/yAxisConstants.ts +1 -1
  20. package/src/components/rendering/WaveformLegend.vue +1 -1
  21. package/src/components/rendering/WaveformSeriesLayer.vue +1 -1
  22. package/src/components/rendering/WaveformTrack.css +5 -2
  23. package/src/components/rendering/WaveformTrack.vue +7 -1
  24. package/src/components/rendering/WaveformTrackAxes.vue +8 -2
  25. package/src/demo/DemoChartHost.vue +1 -0
  26. package/src/types/chart.ts +6 -1
  27. package/src/App.test.ts +0 -400
  28. package/src/components/annotation/components.test.ts +0 -372
  29. package/src/components/annotation/markup.test.ts +0 -371
  30. package/src/components/annotation/serialization.test.ts +0 -114
  31. package/src/components/core/constants.test.ts +0 -29
  32. package/src/components/core/fixedYDomainLayout.test.ts +0 -204
  33. package/src/components/core/grid.test.ts +0 -197
  34. package/src/components/core/latestTaskScheduler.test.ts +0 -51
  35. package/src/components/core/layout.test.ts +0 -373
  36. package/src/components/core/title.test.ts +0 -76
  37. package/src/components/core/useWaveformData.test.ts +0 -95
  38. package/src/components/core/useWaveformRenderSampling.test.ts +0 -17
  39. package/src/components/core/xDomain.test.ts +0 -28
  40. package/src/components/core/yAxisNice.test.ts +0 -174
  41. package/src/components/core/yAxisSlotLayout.test.ts +0 -165
  42. package/src/components/core/yDomain.test.ts +0 -56
  43. package/src/components/interaction/WaveformTooltip.test.ts +0 -276
  44. package/src/components/interaction/viewportInteractionState.test.ts +0 -101
  45. package/src/components/utils/useAnimationFrameThrottle.test.ts +0 -53
  46. package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +0 -257
  47. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +0 -358
  48. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +0 -358
  49. package/src/components/waveformChartCases/dataLifecycle.test.ts +0 -310
  50. package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +0 -389
  51. package/src/components/waveformChartCases/gridFill.test.ts +0 -95
  52. package/src/components/waveformChartCases/incompleteDataHover.test.ts +0 -155
  53. package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +0 -328
  54. package/src/components/waveformChartCases/layoutAndDomains.test.ts +0 -371
  55. package/src/components/waveformChartCases/legendVisibility.test.ts +0 -350
  56. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +0 -400
  57. package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +0 -61
  58. package/src/components/waveformChartCases/presentationAndAxes.test.ts +0 -345
  59. package/src/components/waveformChartCases/presentationMode.test.ts +0 -327
  60. package/src/components/waveformChartCases/renderingStyles.test.ts +0 -283
  61. package/src/components/waveformChartCases/sizingAndTitles.test.ts +0 -370
  62. package/src/components/waveformChartCases/trackOrder.test.ts +0 -323
  63. package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +0 -365
  64. package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -311
  65. package/src/components/waveformChartCases/viewportLifecycle.test.ts +0 -132
  66. package/src/components/waveformChartCases/viewportReset.test.ts +0 -251
  67. package/src/components/waveformChartCases/wheelZoomOut.test.ts +0 -114
  68. package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +0 -117
  69. package/src/components/waveformChartCases/xDomainStrategy.test.ts +0 -168
  70. package/src/components/waveformChartCases/yAxisLayout.test.ts +0 -103
  71. package/src/components/waveformChartCases/zoomIntentAndDataRebind.test.ts +0 -213
  72. package/src/components/waveformChartCases/zoomPointConstraints.test.ts +0 -308
  73. package/src/components/waveformLegendGeometry.test.ts +0 -21
  74. package/src/components/waveformNormalization.test.ts +0 -170
  75. package/src/core/data.test.ts +0 -241
  76. package/src/core/rendering.test.ts +0 -267
  77. package/src/core/renderingStrategies.test.ts +0 -90
  78. package/src/core/samplingReference.test.ts +0 -124
  79. package/src/core/wasmSampling.test.ts +0 -205
  80. package/src/core/workerSampling/multiresolution.test.ts +0 -69
  81. package/src/core/workerSampling/protocol.test.ts +0 -34
  82. package/src/core/workerSampling/repository.test.ts +0 -400
  83. package/src/core/workerSampling/repositoryCache.test.ts +0 -163
  84. package/src/core/workerSampling/repositorySampling.test.ts +0 -106
  85. package/src/core/workerSampling/repositoryTypedDataset.test.ts +0 -89
  86. package/src/core/workerSampling/repositoryWasmDataset.test.ts +0 -70
  87. package/src/data/simulatedWaveforms.test.ts +0 -60
  88. package/src/demo/plotMarginControls.test.ts +0 -29
  89. package/src/demo/xAxisLabelControls.test.ts +0 -86
  90. package/src/test/setup.ts +0 -82
  91. package/src/test/waveformChart.ts +0 -75
  92. package/src/utils/domain.test.ts +0 -28
  93. package/src/utils/formatters.test.ts +0 -131
  94. package/src/utils/sampling.test.ts +0 -226
  95. package/src/views/FixedYDomainDemo.test.ts +0 -44
  96. package/src/views/WasmSamplingDemo.test.ts +0 -13
  97. package/src/views/wasmSamplingDemoSampling.test.ts +0 -27
@@ -1,174 +0,0 @@
1
- import { zoomIdentity } from 'd3'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
5
- import type { WaveformData } from '../../types'
6
- import { buildTrackLayouts } from './trackLayoutBuilder'
7
- import type { DisplaySeries, DisplayTrack } from './types'
8
- import { prepareWaveformSeries } from './useWaveformData'
9
-
10
- function displaySeries(id: string, domain: [number, number]): DisplaySeries {
11
- return {
12
- id,
13
- name: id,
14
- color: '#1677ff',
15
- lineType: 'linear',
16
- lineStyle: 'solid',
17
- pointType: 'none',
18
- errorBar: { visible: false, width: 1.5, capWidth: 8 },
19
- points: [
20
- { x: 0, y: domain[0] },
21
- { x: 1, y: domain[1] },
22
- ],
23
- xDomain: [0, 1],
24
- yDomain: domain,
25
- hasErrorPoints: false,
26
- }
27
- }
28
-
29
- function layout(
30
- series: DisplaySeries[],
31
- options: { overlayMode?: 'single-axis' | 'multi-axis'; fixedYDomain?: [number, number] } = {},
32
- ) {
33
- const track: DisplayTrack = {
34
- id: 'track',
35
- series,
36
- visibleSeries: series,
37
- xDomain: [0, 1],
38
- yDomain: [
39
- Math.min(...series.map((item) => item.yDomain[0])),
40
- Math.max(...series.map((item) => item.yDomain[1])),
41
- ],
42
- }
43
- return buildTrackLayouts({
44
- cells: [
45
- {
46
- slotIndex: 0,
47
- row: 0,
48
- column: 0,
49
- left: 0,
50
- top: 0,
51
- width: 120,
52
- height: 100,
53
- plotHeight: 100,
54
- cellHeight: 130,
55
- xAxisBand: 30,
56
- series: track,
57
- },
58
- ],
59
- grid: {
60
- rowCount: 1,
61
- columnCount: 1,
62
- showPagination: false,
63
- fillIncompleteLastRow: false,
64
- trackLines: {},
65
- },
66
- displayMode: 'independent',
67
- overlayMode: options.overlayMode ?? 'single-axis',
68
- independentTransforms: [zoomIdentity],
69
- sharedZoomDomain: [0, 1],
70
- fixedYDomain: options.fixedYDomain,
71
- timeUnit: 'ms',
72
- rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
73
- hideSecondaryLabels: false,
74
- yAxisLabelX: -50,
75
- yAxisNice: false,
76
- showCompactEmptyTracks: false,
77
- })[0]!
78
- }
79
-
80
- describe('Y-axis nice domains', () => {
81
- it('defaults to a nice domain and can preserve the actual data endpoints', () => {
82
- const source = displaySeries('source', [3, 97])
83
- const track: DisplayTrack = {
84
- id: 'track',
85
- series: [source],
86
- visibleSeries: [source],
87
- xDomain: [0, 1],
88
- yDomain: [3, 97],
89
- }
90
- const defaults = buildTrackLayouts({
91
- cells: [
92
- {
93
- slotIndex: 0,
94
- row: 0,
95
- column: 0,
96
- left: 0,
97
- top: 0,
98
- width: 120,
99
- height: 100,
100
- plotHeight: 100,
101
- cellHeight: 130,
102
- xAxisBand: 30,
103
- series: track,
104
- },
105
- ],
106
- grid: {
107
- rowCount: 1,
108
- columnCount: 1,
109
- showPagination: false,
110
- fillIncompleteLastRow: false,
111
- trackLines: {},
112
- },
113
- displayMode: 'independent',
114
- overlayMode: 'single-axis',
115
- independentTransforms: [zoomIdentity],
116
- sharedZoomDomain: [0, 1],
117
- timeUnit: 'ms',
118
- rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
119
- hideSecondaryLabels: false,
120
- yAxisLabelX: -50,
121
- showCompactEmptyTracks: false,
122
- })[0]!
123
-
124
- expect(defaults.yScale.domain()).toEqual([0, 100])
125
- expect(layout([source]).yScale.domain()).toEqual([3, 97])
126
- })
127
-
128
- it('merges visible single-axis series but keeps multi-axis series independent', () => {
129
- const first = displaySeries('first', [3, 40])
130
- const second = displaySeries('second', [10, 97])
131
-
132
- expect(layout([first, second]).yScale.domain()).toEqual([3, 97])
133
- expect(
134
- layout([first, second], { overlayMode: 'multi-axis' }).yAxes.map((axis) =>
135
- axis.scale.domain(),
136
- ),
137
- ).toEqual([
138
- [3, 40],
139
- [10, 97],
140
- ])
141
- })
142
-
143
- it('keeps explicit domains and constant-data safety ranges unchanged', () => {
144
- expect(
145
- layout([displaySeries('source', [3, 97])], { fixedYDomain: [3, 97] }).yScale.domain(),
146
- ).toEqual([3, 97])
147
- expect(layout([displaySeries('constant', [9.5, 10.5])]).yScale.domain()).toEqual([9.5, 10.5])
148
- })
149
-
150
- it('continues to include visible error-bar bounds in the preserved domain', () => {
151
- const input: WaveformData = {
152
- kind: 'series',
153
- series: [
154
- {
155
- id: 'error-series',
156
- name: 'error series',
157
- errorBar: { visible: true },
158
- data: {
159
- kind: 'points',
160
- points: [
161
- { x: 0, y: 3, lowerError: 2 },
162
- { x: 1, y: 7, upperError: 4 },
163
- ],
164
- },
165
- },
166
- ],
167
- }
168
- const [prepared] = prepareWaveformSeries(input)
169
- const errorSeries: DisplaySeries = { ...prepared!, color: prepared!.color ?? '#1677ff' }
170
-
171
- expect(prepared?.yDomain).toEqual([1, 11])
172
- expect(layout([errorSeries]).yScale.domain()).toEqual([1, 11])
173
- })
174
- })
@@ -1,165 +0,0 @@
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, resolveRenderedYAxisSeriesGroups } 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
-
130
- it('uses dynamic Y domains when measuring multi-axis slots', () => {
131
- const negativeVoltage = series('negative-voltage', 0, 1)
132
- negativeVoltage.unit = 'V'
133
- const longUnit = series('long-unit', 0, 1)
134
- longUnit.unit = 'volt-amperes-per-phase'
135
- const sourceTrack = track([negativeVoltage, longUnit])
136
- const viewportYDomains = { [sourceTrack.id]: [-0.7434, -0.3434] as [number, number] }
137
-
138
- expect(
139
- resolveRenderedYAxisSeriesGroups(
140
- sourceTrack,
141
- 'multi-axis',
142
- undefined,
143
- undefined,
144
- viewportYDomains,
145
- ).map((group) => group.domain),
146
- ).toEqual([
147
- [-0.7434, -0.3434],
148
- [-0.7434, -0.3434],
149
- ])
150
-
151
- const slots = buildYAxisSlots(
152
- [sourceTrack],
153
- 'multi-axis',
154
- undefined,
155
- undefined,
156
- 5,
157
- false,
158
- false,
159
- viewportYDomains,
160
- )
161
-
162
- expect(slots.clearance.left).toBe(100)
163
- expect(slots.clearance.right).toBeGreaterThan(180)
164
- })
165
- })
@@ -1,56 +0,0 @@
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
- })
@@ -1,276 +0,0 @@
1
- import { mount } from '@vue/test-utils'
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
- import { nextTick } from 'vue'
4
- import { resizeObservers } from '../../test/setup'
5
-
6
- import WaveformTooltip from './WaveformTooltip.vue'
7
-
8
- describe('WaveformTooltip', () => {
9
- const point = { x: 1, y: 12 }
10
- let measuredWidth = 238
11
- let measuredHeight = 40
12
-
13
- beforeEach(() => {
14
- measuredWidth = 238
15
- measuredHeight = 40
16
- vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (
17
- this: HTMLElement,
18
- ) {
19
- return {
20
- width: Math.min(measuredWidth, Number.parseFloat(this.style.maxWidth) || measuredWidth),
21
- height: measuredHeight,
22
- } as DOMRect
23
- })
24
- })
25
- afterEach(() => vi.restoreAllMocks())
26
-
27
- function mountTooltip(positionX: number, containerWidth = 400) {
28
- return mount(WaveformTooltip, {
29
- props: {
30
- visible: true,
31
- position: { x: positionX, y: 100 },
32
- timeUnit: 's',
33
- hoveredPoint: point,
34
- seriesPoints: [{ trackIndex: 0, name: 'Temperature', color: '#f00', point }],
35
- containerWidth,
36
- containerHeight: 300,
37
- },
38
- })
39
- }
40
-
41
- it('positions the tooltip to the right when enough space remains', async () => {
42
- const wrapper = mountTooltip(100)
43
- await nextTick()
44
- const tooltip = wrapper.get('.waveform-tooltip')
45
-
46
- expect(tooltip.attributes('style')).toContain('left: 112px')
47
- expect(tooltip.attributes('style')).not.toContain('right:')
48
- })
49
-
50
- it('flips the tooltip to the left near the right boundary', async () => {
51
- const wrapper = mountTooltip(370)
52
- await nextTick()
53
- const tooltip = wrapper.get('.waveform-tooltip')
54
-
55
- expect(tooltip.attributes('style')).toContain('right: 42px')
56
- expect(tooltip.attributes('style')).not.toContain('left:')
57
- })
58
-
59
- it('keeps the tooltip inside the left boundary when neither side has enough space', async () => {
60
- const wrapper = mountTooltip(100, 200)
61
- await nextTick()
62
- const tooltip = wrapper.get('.waveform-tooltip')
63
-
64
- expect(tooltip.attributes('style')).toContain('left: 8px')
65
- expect(tooltip.attributes('style')).toContain('max-width: 184px')
66
- expect(tooltip.attributes('style')).not.toContain('right:')
67
- })
68
-
69
- it('keeps short content content-sized while exposing the available width cap', async () => {
70
- const wrapper = mountTooltip(100)
71
- await nextTick()
72
- const tooltip = wrapper.get('.waveform-tooltip')
73
-
74
- expect(tooltip.attributes('style')).toContain('left: 112px')
75
- expect(tooltip.attributes('style')).toContain('max-width: 384px')
76
- })
77
-
78
- it('keeps long series content in a wrapping content container', () => {
79
- const longName = 'ENG8KJXAc-very-long-series-name-10001'
80
- const pointWithErrors = { x: 1, y: 12, error: 1, upperError: 2 }
81
- const wrapper = mount(WaveformTooltip, {
82
- props: {
83
- visible: true,
84
- position: { x: 100, y: 100 },
85
- timeUnit: 'ms',
86
- hoveredPoint: pointWithErrors,
87
- seriesPoints: [
88
- {
89
- trackIndex: 0,
90
- name: longName,
91
- color: '#f00',
92
- unit: 'very-long-unit',
93
- point: pointWithErrors,
94
- },
95
- ],
96
- containerWidth: 400,
97
- containerHeight: 300,
98
- },
99
- })
100
-
101
- const tooltip = wrapper.get('.waveform-tooltip')
102
- expect(tooltip.get('.waveform-tooltip__series-content').text()).toContain(longName)
103
- expect(tooltip.get('.waveform-tooltip__series-content').classes()).toContain(
104
- 'waveform-tooltip__series-content',
105
- )
106
- expect(tooltip.attributes('style')).toContain('max-width: 384px')
107
- })
108
-
109
- it('flips before long content is squeezed and restores the right side when moving back', async () => {
110
- measuredWidth = 420
111
- const wrapper = mountTooltip(500, 800)
112
- await nextTick()
113
- const tooltip = wrapper.get('.waveform-tooltip')
114
- expect(tooltip.attributes('style')).toContain('right: 312px')
115
- expect(tooltip.attributes('style')).toContain('max-width: 560px')
116
- await wrapper.setProps({ position: { x: 100, y: 100 } })
117
- expect(tooltip.attributes('style')).toContain('left: 112px')
118
- expect(tooltip.attributes('style')).toContain('max-width: 560px')
119
- })
120
-
121
- it('remeasures changing series, formatted values, container size and visibility', async () => {
122
- const wrapper = mountTooltip(500, 800)
123
- await nextTick()
124
- expect(wrapper.get('.waveform-tooltip').attributes('style')).toContain('left: 512px')
125
- measuredWidth = 450
126
- measuredHeight = 90
127
- await wrapper.setProps({
128
- timeUnit: 'ms',
129
- position: { x: 500, y: 290 },
130
- seriesPoints: [
131
- { trackIndex: 0, name: 'very-long-channel-name', color: '#f00', point },
132
- { trackIndex: 1, name: 'second-channel', color: '#0f0', point: { x: 100, y: 123456 } },
133
- ],
134
- })
135
- await nextTick()
136
- expect(wrapper.get('.waveform-tooltip').attributes('style')).toContain('right: 312px')
137
- expect(wrapper.get('.waveform-tooltip').attributes('style')).toContain('top: 202px')
138
- await wrapper.setProps({ containerWidth: 200, position: { x: 100, y: 290 } })
139
- await nextTick()
140
- expect(wrapper.get('.waveform-tooltip').attributes('style')).toContain('left: 8px')
141
- expect(wrapper.get('.waveform-tooltip').attributes('style')).toContain('max-width: 184px')
142
- const observer = resizeObservers.at(-1)!
143
- await wrapper.setProps({ visible: false })
144
- expect(observer.disconnect).toHaveBeenCalled()
145
- measuredWidth = 100
146
- await wrapper.setProps({ visible: true, containerWidth: 800 })
147
- await nextTick()
148
- expect(wrapper.get('.waveform-tooltip').attributes('style')).toContain('left: 112px')
149
- wrapper.unmount()
150
- expect(resizeObservers.at(-1)!.disconnect).toHaveBeenCalled()
151
- })
152
-
153
- it('updates placement when rendered dimensions change without a prop update', async () => {
154
- const wrapper = mountTooltip(500, 800)
155
- await nextTick()
156
- measuredWidth = 420
157
- measuredHeight = 250
158
- resizeObservers.at(-1)!.resize(420, 250)
159
- await nextTick()
160
- expect(wrapper.get('.waveform-tooltip').attributes('style')).toContain('right: 312px')
161
- expect(wrapper.get('.waveform-tooltip').attributes('style')).toContain('top: 42px')
162
- wrapper.unmount()
163
- })
164
-
165
- it('omits units and errors from the strict x/y value format', () => {
166
- const pointWithErrors = { x: 1, y: 12, error: 1, upperError: 2 }
167
- const wrapper = mount(WaveformTooltip, {
168
- props: {
169
- visible: true,
170
- position: { x: 10, y: 10 },
171
- timeUnit: 's',
172
- hoveredPoint: pointWithErrors,
173
- seriesPoints: [
174
- {
175
- trackIndex: 0,
176
- name: '温度',
177
- color: '#f00',
178
- unit: 'C',
179
- point: pointWithErrors,
180
- },
181
- ],
182
- containerWidth: 400,
183
- containerHeight: 300,
184
- },
185
- })
186
-
187
- expect(wrapper.get('.waveform-tooltip__value').text()).toBe('(x:1 y:12)')
188
- expect(wrapper.get('.waveform-tooltip__value').text()).not.toContain('C')
189
- expect(wrapper.get('.waveform-tooltip__value').text()).not.toContain('+2')
190
- })
191
-
192
- it('renders the configured shot number, channel, and formatted coordinates', () => {
193
- const hoveredPoint = { x: 1, y: -1405.4932 }
194
- const wrapper = mount(WaveformTooltip, {
195
- props: {
196
- visible: true,
197
- position: { x: 10, y: 10 },
198
- timeUnit: 'ms',
199
- hoveredPoint,
200
- seriesPoints: [
201
- {
202
- trackIndex: 0,
203
- name: 'ENG8KJXAc(10001)',
204
- shotNo: '炮 7',
205
- color: '#ffb43b',
206
- unit: 'A',
207
- point: hoveredPoint,
208
- },
209
- ],
210
- containerWidth: 400,
211
- containerHeight: 300,
212
- },
213
- })
214
-
215
- expect(wrapper.get('.waveform-tooltip__series-label').text()).toBe('炮 7: ENG8KJXAc(10001)(A)')
216
- expect(wrapper.get('.waveform-tooltip__value').text()).toBe('(x:1,000 y:-1,405.4932)')
217
- })
218
-
219
- it('renders each series row with the formatted x coordinate and series label', () => {
220
- const wrapper = mount(WaveformTooltip, {
221
- props: {
222
- visible: true,
223
- position: { x: 10, y: 10 },
224
- timeUnit: 's',
225
- hoveredPoint: { x: 1.234567, y: 12 },
226
- seriesPoints: [
227
- { trackIndex: 0, name: '温度', color: '#f00', point: { x: 1.234567, y: 12 } },
228
- ],
229
- containerWidth: 400,
230
- containerHeight: 300,
231
- },
232
- })
233
-
234
- expect(wrapper.find('.waveform-tooltip__time').exists()).toBe(false)
235
- expect(wrapper.get('.waveform-tooltip__series-label').text()).toBe('未配置炮号: 温度')
236
- expect(wrapper.get('.waveform-tooltip__value').text()).toBe('(x:1.2346 y:12)')
237
- })
238
-
239
- it('uses the fallback shot number when shotNo is blank', () => {
240
- const point = { x: 1, y: 12 }
241
- const wrapper = mount(WaveformTooltip, {
242
- props: {
243
- visible: true,
244
- position: { x: 10, y: 10 },
245
- timeUnit: 's',
246
- hoveredPoint: point,
247
- seriesPoints: [{ trackIndex: 0, name: '温度', color: '#f00', point }],
248
- containerWidth: 400,
249
- containerHeight: 300,
250
- },
251
- })
252
-
253
- expect(wrapper.get('.waveform-tooltip__series-label').text()).toBe('未配置炮号: 温度')
254
- })
255
-
256
- it('uses each series sample time and marks unavailable series without synthesizing values', () => {
257
- const wrapper = mount(WaveformTooltip, {
258
- props: {
259
- visible: true,
260
- position: { x: 10, y: 10 },
261
- timeUnit: 's',
262
- hoveredPoint: { x: 1, y: 12 },
263
- seriesPoints: [
264
- { trackIndex: 0, name: '完整数据', color: '#f00', point: { x: 1, y: 12 } },
265
- { trackIndex: 1, name: '异步采样', color: '#0a0', point: { x: 1.25, y: 8 } },
266
- { trackIndex: 2, name: '缺失数据', color: '#00f', point: null },
267
- ],
268
- containerWidth: 400,
269
- containerHeight: 300,
270
- },
271
- })
272
-
273
- const values = wrapper.findAll('.waveform-tooltip__value').map((value) => value.text())
274
- expect(values).toEqual(['(x:1 y:12)', '(x:1.25 y:8)', '无数据'])
275
- })
276
- })