waveform-analysis 0.1.59 → 0.1.61
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.
- package/dist/index.cjs +1 -1
- package/dist/index.js +1083 -1068
- package/dist/style.css +1 -1
- package/dist/types/components/core/layout.d.ts +9 -2
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +10 -0
- package/package.json +1 -1
- package/src/components/core/layout.test.ts +24 -0
- package/src/components/core/layout.ts +75 -13
- package/src/components/core/trackLayoutBuilder.ts +7 -13
- package/src/components/core/useWaveformLayout.ts +30 -58
- package/src/components/core/yAxisLayoutMetrics.ts +75 -0
- package/src/components/core/yAxisSlotLayout.test.ts +37 -1
- package/src/components/interaction/WaveformTooltip.test.ts +93 -11
- package/src/components/interaction/WaveformTooltip.vue +43 -47
- package/src/components/rendering/WaveformTrackAxes.vue +8 -4
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +23 -24
- package/src/components/waveformChartCases/yAxisLayout.test.ts +103 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { WaveformOverlayMode } from '../data/types'
|
|
2
|
+
import {
|
|
3
|
+
Y_AXIS_CHARACTER_WIDTH,
|
|
4
|
+
Y_AXIS_LABEL_BAND_WIDTH,
|
|
5
|
+
Y_AXIS_LABEL_GAP,
|
|
6
|
+
Y_AXIS_OUTER_PADDING,
|
|
7
|
+
Y_AXIS_TICK_PADDING,
|
|
8
|
+
} from './constants'
|
|
9
|
+
import { axisTextMetrics, resolveRenderedYAxisSeriesGroups } from './layout'
|
|
10
|
+
import type { DisplayTrack } from './types'
|
|
11
|
+
import type { WaveformYDomain } from './yDomain'
|
|
12
|
+
|
|
13
|
+
export interface YAxisLayoutMetrics {
|
|
14
|
+
tickClearance: number
|
|
15
|
+
fullClearance: number
|
|
16
|
+
labelCenterX: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveViewportYDomains(
|
|
20
|
+
displayMode: 'independent' | 'separated' | 'compact',
|
|
21
|
+
chartTracks: DisplayTrack[],
|
|
22
|
+
sharedYDomains: Record<string, [number, number]>,
|
|
23
|
+
independentYDomains: Record<number, [number, number]>,
|
|
24
|
+
): Record<string, [number, number]> {
|
|
25
|
+
if (displayMode !== 'independent') return sharedYDomains
|
|
26
|
+
return Object.fromEntries(
|
|
27
|
+
chartTracks.flatMap((track, index) => {
|
|
28
|
+
const domain = independentYDomains[index]
|
|
29
|
+
return domain ? [[track.id, domain]] : []
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveYAxisLayoutMetrics(
|
|
35
|
+
chartTracks: DisplayTrack[],
|
|
36
|
+
overlayMode: WaveformOverlayMode,
|
|
37
|
+
yDomain: WaveformYDomain | undefined,
|
|
38
|
+
yDomains: Record<string, WaveformYDomain> | undefined,
|
|
39
|
+
viewportYDomains: Record<string, [number, number]>,
|
|
40
|
+
nice: boolean,
|
|
41
|
+
tickCount: number,
|
|
42
|
+
compact: boolean,
|
|
43
|
+
): YAxisLayoutMetrics {
|
|
44
|
+
const axisText = chartTracks
|
|
45
|
+
.filter((track) => track.visibleSeries.length > 0)
|
|
46
|
+
.flatMap((track) =>
|
|
47
|
+
resolveRenderedYAxisSeriesGroups(track, overlayMode, yDomain, yDomains, viewportYDomains),
|
|
48
|
+
)
|
|
49
|
+
.map(
|
|
50
|
+
(group) =>
|
|
51
|
+
axisTextMetrics(
|
|
52
|
+
group.domain,
|
|
53
|
+
nice,
|
|
54
|
+
undefined,
|
|
55
|
+
group.seriesList[0]?.unit,
|
|
56
|
+
tickCount,
|
|
57
|
+
compact,
|
|
58
|
+
).tickTextWidth,
|
|
59
|
+
)
|
|
60
|
+
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
|
61
|
+
const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
|
|
62
|
+
const labelCenterX = -(
|
|
63
|
+
Y_AXIS_TICK_PADDING +
|
|
64
|
+
tickTextWidth +
|
|
65
|
+
Y_AXIS_LABEL_GAP +
|
|
66
|
+
Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
67
|
+
)
|
|
68
|
+
const fullClearance =
|
|
69
|
+
tickTextWidth +
|
|
70
|
+
Y_AXIS_TICK_PADDING +
|
|
71
|
+
Y_AXIS_LABEL_GAP +
|
|
72
|
+
Y_AXIS_LABEL_BAND_WIDTH +
|
|
73
|
+
Y_AXIS_OUTER_PADDING
|
|
74
|
+
return { tickClearance, fullClearance, labelCenterX }
|
|
75
|
+
}
|
|
@@ -2,7 +2,7 @@ import { zoomIdentity } from 'd3'
|
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
|
|
5
|
-
import { buildTrackLayouts, buildYAxisSlots } from './layout'
|
|
5
|
+
import { buildTrackLayouts, buildYAxisSlots, resolveRenderedYAxisSeriesGroups } from './layout'
|
|
6
6
|
import type { DisplaySeries, DisplayTrack } from './types'
|
|
7
7
|
import { Y_AXIS_RIGHT_LABEL_OFFSET } from './yAxisConstants'
|
|
8
8
|
|
|
@@ -126,4 +126,40 @@ describe('Y-axis slots', () => {
|
|
|
126
126
|
expect(fourAxisSlots.slots[1]!.axisOffset).toBeLessThan(0)
|
|
127
127
|
expect(fourAxisSlots.slots[3]!.axisOffset).toBeGreaterThan(0)
|
|
128
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
|
+
})
|
|
129
165
|
})
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { nextTick } from 'vue'
|
|
4
|
+
import { resizeObservers } from '../../test/setup'
|
|
3
5
|
|
|
4
6
|
import WaveformTooltip from './WaveformTooltip.vue'
|
|
5
7
|
|
|
6
8
|
describe('WaveformTooltip', () => {
|
|
7
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())
|
|
8
26
|
|
|
9
27
|
function mountTooltip(positionX: number, containerWidth = 400) {
|
|
10
28
|
return mount(WaveformTooltip, {
|
|
@@ -20,33 +38,41 @@ describe('WaveformTooltip', () => {
|
|
|
20
38
|
})
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
it('positions the tooltip to the right when enough space remains', () => {
|
|
24
|
-
const
|
|
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')
|
|
25
45
|
|
|
26
46
|
expect(tooltip.attributes('style')).toContain('left: 112px')
|
|
27
47
|
expect(tooltip.attributes('style')).not.toContain('right:')
|
|
28
48
|
})
|
|
29
49
|
|
|
30
|
-
it('flips the tooltip to the left near the right boundary', () => {
|
|
31
|
-
const
|
|
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')
|
|
32
54
|
|
|
33
55
|
expect(tooltip.attributes('style')).toContain('right: 42px')
|
|
34
56
|
expect(tooltip.attributes('style')).not.toContain('left:')
|
|
35
57
|
})
|
|
36
58
|
|
|
37
|
-
it('keeps the tooltip inside the left boundary when neither side has enough space', () => {
|
|
38
|
-
const
|
|
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')
|
|
39
63
|
|
|
40
64
|
expect(tooltip.attributes('style')).toContain('left: 8px')
|
|
41
65
|
expect(tooltip.attributes('style')).toContain('max-width: 184px')
|
|
42
66
|
expect(tooltip.attributes('style')).not.toContain('right:')
|
|
43
67
|
})
|
|
44
68
|
|
|
45
|
-
it('keeps short content content-sized while exposing the available width cap', () => {
|
|
46
|
-
const
|
|
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')
|
|
47
73
|
|
|
48
74
|
expect(tooltip.attributes('style')).toContain('left: 112px')
|
|
49
|
-
expect(tooltip.attributes('style')).toContain('max-width:
|
|
75
|
+
expect(tooltip.attributes('style')).toContain('max-width: 384px')
|
|
50
76
|
})
|
|
51
77
|
|
|
52
78
|
it('keeps long series content in a wrapping content container', () => {
|
|
@@ -77,7 +103,63 @@ describe('WaveformTooltip', () => {
|
|
|
77
103
|
expect(tooltip.get('.waveform-tooltip__series-content').classes()).toContain(
|
|
78
104
|
'waveform-tooltip__series-content',
|
|
79
105
|
)
|
|
80
|
-
expect(tooltip.attributes('style')).toContain('max-width:
|
|
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()
|
|
81
163
|
})
|
|
82
164
|
|
|
83
165
|
it('omits units and errors from the strict x/y value format', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, onUpdated, ref, watch } from 'vue'
|
|
3
3
|
import { formatTooltipNumber, formatTooltipTime } from '../../utils'
|
|
4
4
|
import type { WaveformPoint } from '../data/types'
|
|
5
5
|
|
|
@@ -33,64 +33,60 @@ const props = defineProps<Props>()
|
|
|
33
33
|
|
|
34
34
|
const tooltipGap = 12
|
|
35
35
|
const containerPadding = 8
|
|
36
|
-
const tooltipPlacementWidth = 238
|
|
37
36
|
const tooltipMaxWidth = 560
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
function
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const valueText = seriesPoint.point
|
|
49
|
-
? `(x:${formatTooltipTime(seriesPoint.point.x, props.timeUnit)} y:${formatTooltipNumber(seriesPoint.point.y)})`
|
|
50
|
-
: '无数据'
|
|
51
|
-
return `${seriesPoint.shotNo?.trim() || '未配置炮号'}: ${seriesPoint.name}${
|
|
52
|
-
seriesPoint.unit ? `(${seriesPoint.unit})` : ''
|
|
53
|
-
} ${valueText}`
|
|
37
|
+
const tooltipElement = ref<HTMLElement | null>(null)
|
|
38
|
+
const tooltipSize = ref({ width: 0, height: 0 })
|
|
39
|
+
|
|
40
|
+
function measureTooltip() {
|
|
41
|
+
const bounds = tooltipElement.value?.getBoundingClientRect()
|
|
42
|
+
const width = bounds?.width ?? 0
|
|
43
|
+
const height = bounds?.height ?? 0
|
|
44
|
+
if (width !== tooltipSize.value.width || height !== tooltipSize.value.height) {
|
|
45
|
+
tooltipSize.value = { width, height }
|
|
46
|
+
}
|
|
54
47
|
}
|
|
55
48
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
// The width cap depends only on the container, never on the cursor's side.
|
|
50
|
+
// Measuring the rendered box therefore cannot cause a shrink/flip feedback loop.
|
|
51
|
+
watch(
|
|
52
|
+
tooltipElement,
|
|
53
|
+
(element, _, onCleanup) => {
|
|
54
|
+
measureTooltip()
|
|
55
|
+
if (!element) return
|
|
56
|
+
const observer = new ResizeObserver(measureTooltip)
|
|
57
|
+
observer.observe(element)
|
|
58
|
+
onCleanup(() => observer.disconnect())
|
|
59
|
+
},
|
|
60
|
+
{ flush: 'post' },
|
|
61
|
+
)
|
|
62
|
+
onUpdated(measureTooltip)
|
|
63
63
|
|
|
64
64
|
const tooltipStyle = computed(() => {
|
|
65
65
|
if (!props.visible || !props.hoveredPoint) return { display: 'none' }
|
|
66
66
|
|
|
67
67
|
const rightPlacement = props.position.x + tooltipGap
|
|
68
|
-
const leftPlacement = props.position.x - tooltipGap -
|
|
69
|
-
const rightAvailableWidth = props.containerWidth - containerPadding - rightPlacement
|
|
70
|
-
const leftAvailableWidth = props.position.x - tooltipGap - containerPadding
|
|
68
|
+
const leftPlacement = props.position.x - tooltipGap - tooltipSize.value.width
|
|
71
69
|
const availableWidth = Math.max(
|
|
72
70
|
1,
|
|
73
71
|
Math.min(tooltipMaxWidth, props.containerWidth - containerPadding * 2),
|
|
74
72
|
)
|
|
75
73
|
const horizontalStyle =
|
|
76
|
-
rightPlacement +
|
|
77
|
-
? {
|
|
78
|
-
left: `${rightPlacement}px`,
|
|
79
|
-
maxWidth: `${Math.min(tooltipMaxWidth, rightAvailableWidth)}px`,
|
|
80
|
-
}
|
|
74
|
+
rightPlacement + tooltipSize.value.width <= props.containerWidth - containerPadding
|
|
75
|
+
? { left: `${rightPlacement}px` }
|
|
81
76
|
: leftPlacement >= containerPadding
|
|
82
|
-
? {
|
|
83
|
-
|
|
84
|
-
maxWidth: `${Math.min(tooltipMaxWidth, leftAvailableWidth)}px`,
|
|
85
|
-
}
|
|
86
|
-
: { left: `${containerPadding}px`, maxWidth: `${availableWidth}px` }
|
|
77
|
+
? { right: `${props.containerWidth - props.position.x + tooltipGap}px` }
|
|
78
|
+
: { left: `${containerPadding}px` }
|
|
87
79
|
|
|
88
|
-
const maxWidth = Number.parseFloat(horizontalStyle.maxWidth)
|
|
89
80
|
return {
|
|
90
81
|
...horizontalStyle,
|
|
82
|
+
maxWidth: `${availableWidth}px`,
|
|
83
|
+
visibility: tooltipSize.value.width > 0 ? ('visible' as const) : ('hidden' as const),
|
|
91
84
|
top: `${Math.max(
|
|
92
|
-
|
|
93
|
-
Math.min(
|
|
85
|
+
containerPadding,
|
|
86
|
+
Math.min(
|
|
87
|
+
props.position.y - 18,
|
|
88
|
+
props.containerHeight - tooltipSize.value.height - containerPadding,
|
|
89
|
+
),
|
|
94
90
|
)}px`,
|
|
95
91
|
}
|
|
96
92
|
})
|
|
@@ -99,6 +95,7 @@ const tooltipStyle = computed(() => {
|
|
|
99
95
|
<template>
|
|
100
96
|
<div
|
|
101
97
|
v-if="visible && hoveredPoint"
|
|
98
|
+
ref="tooltipElement"
|
|
102
99
|
class="waveform-tooltip waveform-chart__tooltip"
|
|
103
100
|
:style="tooltipStyle"
|
|
104
101
|
>
|
|
@@ -162,7 +159,7 @@ const tooltipStyle = computed(() => {
|
|
|
162
159
|
|
|
163
160
|
.waveform-tooltip__series-content {
|
|
164
161
|
display: flex;
|
|
165
|
-
flex-wrap:
|
|
162
|
+
flex-wrap: wrap;
|
|
166
163
|
gap: 12px;
|
|
167
164
|
align-items: baseline;
|
|
168
165
|
min-width: 0;
|
|
@@ -171,14 +168,13 @@ const tooltipStyle = computed(() => {
|
|
|
171
168
|
.waveform-tooltip__series-label {
|
|
172
169
|
flex: 1 1 auto;
|
|
173
170
|
min-width: 0;
|
|
174
|
-
overflow:
|
|
175
|
-
text-overflow: ellipsis;
|
|
176
|
-
white-space: nowrap;
|
|
171
|
+
overflow-wrap: anywhere;
|
|
177
172
|
}
|
|
178
173
|
|
|
179
174
|
.waveform-tooltip__value {
|
|
180
|
-
flex: 0
|
|
181
|
-
|
|
175
|
+
flex: 0 1 auto;
|
|
176
|
+
min-width: 0;
|
|
177
|
+
overflow-wrap: anywhere;
|
|
182
178
|
}
|
|
183
179
|
|
|
184
180
|
.waveform-tooltip__series strong {
|
|
@@ -3,7 +3,8 @@ import { axisBottom, axisLeft, axisRight, select } from 'd3'
|
|
|
3
3
|
import { nextTick, onMounted, ref, watch } from 'vue'
|
|
4
4
|
|
|
5
5
|
import type { WaveformAxesOptions } from '../../types'
|
|
6
|
-
import {
|
|
6
|
+
import { formatXAxisLabel } from '../../utils'
|
|
7
|
+
import { formatYAxisTickLabel } from '../core/layout'
|
|
7
8
|
import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
|
|
8
9
|
import { Y_AXIS_LABEL_BAND_WIDTH } from '../core/yAxisConstants'
|
|
9
10
|
|
|
@@ -43,12 +44,15 @@ function renderAxes() {
|
|
|
43
44
|
props.track.yAxes.forEach((axis, index) => {
|
|
44
45
|
const element = yAxisElements.value[index]
|
|
45
46
|
if (!element) return
|
|
46
|
-
const [axisMin, axisMax] = axis.scale.domain()
|
|
47
|
-
const topTickValue = Math.max(...axis.tickValues)
|
|
48
47
|
const unit = axis.seriesList[0]?.unit
|
|
49
48
|
const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
|
|
50
49
|
.tickFormat((value) =>
|
|
51
|
-
|
|
50
|
+
formatYAxisTickLabel(
|
|
51
|
+
Number(value),
|
|
52
|
+
axis.scale.domain() as [number, number],
|
|
53
|
+
axis.tickValues,
|
|
54
|
+
unit,
|
|
55
|
+
),
|
|
52
56
|
)
|
|
53
57
|
.tickSize(-4)
|
|
54
58
|
.tickPadding(7)
|
|
@@ -6,26 +6,6 @@ import { resizeObservers } from '../../test/setup'
|
|
|
6
6
|
import { gridSeries, mountSizedChart } from '../../test/waveformChart'
|
|
7
7
|
|
|
8
8
|
describe('WaveformChart', () => {
|
|
9
|
-
it('applies the configured Y-axis split number', async () => {
|
|
10
|
-
const wrapper = await mountSizedChart(
|
|
11
|
-
{
|
|
12
|
-
kind: 'points',
|
|
13
|
-
points: [
|
|
14
|
-
{ x: 0, y: 3 },
|
|
15
|
-
{ x: 1, y: 97 },
|
|
16
|
-
],
|
|
17
|
-
},
|
|
18
|
-
{ axes: { y: { splitNumber: 5 } } },
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
expect(
|
|
22
|
-
wrapper
|
|
23
|
-
.get('.waveform-chart__axis--y')
|
|
24
|
-
.findAll('.tick text')
|
|
25
|
-
.map((tick) => tick.text()),
|
|
26
|
-
).toEqual(['0', '25', '50', '75', '100'])
|
|
27
|
-
})
|
|
28
|
-
|
|
29
9
|
it('expands the Y-axis label gutter for signed values and long exponents', async () => {
|
|
30
10
|
const wrapper = await mountSizedChart(
|
|
31
11
|
{
|
|
@@ -52,10 +32,13 @@ describe('WaveformChart', () => {
|
|
|
52
32
|
const labelBackgroundX = Number(
|
|
53
33
|
tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
|
|
54
34
|
)
|
|
35
|
+
const longestTickLabel = Math.max(
|
|
36
|
+
...tracks[0].findAll('.waveform-chart__axis--y .tick text').map((tick) => tick.text().length),
|
|
37
|
+
)
|
|
55
38
|
|
|
56
|
-
expect(labelX).
|
|
39
|
+
expect(Math.abs(labelX)).toBeGreaterThanOrEqual(7 + longestTickLabel * 7 + 6)
|
|
57
40
|
expect(labelBackgroundX).toBe(labelX - 6)
|
|
58
|
-
expect(Number(wrapper.attributes('data-chart-left-margin'))).
|
|
41
|
+
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBeGreaterThanOrEqual(80)
|
|
59
42
|
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(72)
|
|
60
43
|
})
|
|
61
44
|
|
|
@@ -118,12 +101,28 @@ describe('WaveformChart', () => {
|
|
|
118
101
|
{ grid: { rowCount: 1, columnCount: 1 } },
|
|
119
102
|
)
|
|
120
103
|
const initialMargin = wrapper.attributes('data-chart-left-margin')
|
|
121
|
-
|
|
104
|
+
const initialTrack = wrapper.get('.waveform-chart__track')
|
|
105
|
+
const initialLongestTickLabel = Math.max(
|
|
106
|
+
...initialTrack
|
|
107
|
+
.findAll('.waveform-chart__axis--y .tick text')
|
|
108
|
+
.map((tick) => tick.text().length),
|
|
109
|
+
)
|
|
110
|
+
expect(initialTrack.attributes('data-y-axis-label-x')).toBe(
|
|
111
|
+
String(-(7 + initialLongestTickLabel * 7 + 6)),
|
|
112
|
+
)
|
|
122
113
|
|
|
123
114
|
await wrapper.get('.ant-pagination-next button').trigger('click')
|
|
124
115
|
|
|
125
116
|
expect(wrapper.attributes('data-chart-left-margin')).toBe(initialMargin)
|
|
126
|
-
|
|
117
|
+
const pagedTrack = wrapper.get('.waveform-chart__track')
|
|
118
|
+
const pagedLongestTickLabel = Math.max(
|
|
119
|
+
...pagedTrack
|
|
120
|
+
.findAll('.waveform-chart__axis--y .tick text')
|
|
121
|
+
.map((tick) => tick.text().length),
|
|
122
|
+
)
|
|
123
|
+
expect(pagedTrack.attributes('data-y-axis-label-x')).toBe(
|
|
124
|
+
String(-(7 + pagedLongestTickLabel * 7 + 6)),
|
|
125
|
+
)
|
|
127
126
|
})
|
|
128
127
|
|
|
129
128
|
it('hides only secondary-column Y-axis labels when the grid is too narrow', async () => {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { flushPromises } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { resizeObservers } from '../../test/setup'
|
|
5
|
+
|
|
6
|
+
import { mountSizedChart } from '../../test/waveformChart'
|
|
7
|
+
|
|
8
|
+
describe('WaveformChart Y-axis layout', () => {
|
|
9
|
+
it('applies the configured Y-axis split number', async () => {
|
|
10
|
+
const wrapper = await mountSizedChart(
|
|
11
|
+
{
|
|
12
|
+
kind: 'points',
|
|
13
|
+
points: [
|
|
14
|
+
{ x: 0, y: 3 },
|
|
15
|
+
{ x: 1, y: 97 },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{ axes: { y: { splitNumber: 5 } } },
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
expect(
|
|
22
|
+
wrapper
|
|
23
|
+
.get('.waveform-chart__axis--y')
|
|
24
|
+
.findAll('.tick text')
|
|
25
|
+
.map((tick) => tick.text()),
|
|
26
|
+
).toEqual(['0', '25', '50', '75', '100'])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('reserves the rendered endpoint label width across tracks, pages, and resizes', async () => {
|
|
30
|
+
const wrapper = await mountSizedChart(
|
|
31
|
+
{
|
|
32
|
+
kind: 'series',
|
|
33
|
+
series: ['WE_2M', 'IP_VV_2M', 'SX2_7_07', 'BT1_2M'].map((name, index) => ({
|
|
34
|
+
id: name,
|
|
35
|
+
name,
|
|
36
|
+
unit: 'V',
|
|
37
|
+
data: {
|
|
38
|
+
kind: 'points' as const,
|
|
39
|
+
points: [
|
|
40
|
+
{ x: 0, y: -0.7434 - index },
|
|
41
|
+
{ x: 1, y: -0.3434 - index },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
})),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
axes: { y: { lineVisible: false, nice: false } },
|
|
48
|
+
grid: { rowCount: 1, columnCount: 1 },
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
const firstTrack = wrapper.get('.waveform-chart__track')
|
|
52
|
+
|
|
53
|
+
expect(wrapper.attributes('data-chart-left-margin')).toBe('100')
|
|
54
|
+
expect(firstTrack.attributes('data-y-axis-label-x')).toBe('-90')
|
|
55
|
+
expect(firstTrack.findAll('.waveform-chart__axis--y .tick text').at(-1)?.text()).toBe(
|
|
56
|
+
'(V) -0.3434',
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
resizeObservers.at(-1)?.resize(520, 280)
|
|
60
|
+
await flushPromises()
|
|
61
|
+
expect(wrapper.attributes('data-chart-left-margin')).toBe('100')
|
|
62
|
+
expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-90')
|
|
63
|
+
|
|
64
|
+
await wrapper.get('.ant-pagination-next button').trigger('click')
|
|
65
|
+
expect(wrapper.attributes('data-chart-left-margin')).toBe('100')
|
|
66
|
+
expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-90')
|
|
67
|
+
expect(wrapper.get('.waveform-chart__axis--y').findAll('.tick text').at(-1)?.text()).toBe(
|
|
68
|
+
'(V) -1.3434',
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('measures the compact-row unit label after omitting the top endpoint', async () => {
|
|
73
|
+
const wrapper = await mountSizedChart(
|
|
74
|
+
{
|
|
75
|
+
kind: 'series',
|
|
76
|
+
series: ['first', 'second'].map((id) => ({
|
|
77
|
+
id,
|
|
78
|
+
name: id,
|
|
79
|
+
unit: 'V',
|
|
80
|
+
data: {
|
|
81
|
+
kind: 'points' as const,
|
|
82
|
+
points: [
|
|
83
|
+
{ x: 0, y: 0.12345 },
|
|
84
|
+
{ x: 1, y: 100 },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
})),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
displayMode: 'compact',
|
|
91
|
+
axes: { y: { nice: false } },
|
|
92
|
+
grid: { rowCount: 2, columnCount: 1 },
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
96
|
+
|
|
97
|
+
expect(wrapper.attributes('data-chart-left-margin')).toBe('93')
|
|
98
|
+
expect(tracks[1]?.attributes('data-y-axis-label-x')).toBe('-83')
|
|
99
|
+
expect(tracks[1]?.findAll('.waveform-chart__axis--y .tick text').at(-1)?.text()).toBe(
|
|
100
|
+
'(V) 75.031',
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
})
|