fix(chart): preserve clean view geometry

This commit is contained in:
李启源
2026-07-21 22:26:34 +08:00
parent 6a6a387868
commit 4643a2dbfe
2 changed files with 126 additions and 81 deletions

View File

@@ -297,63 +297,87 @@ describe('WaveformChart', () => {
expect(zeroLines[0].attributes('y1')).not.toBe(zeroLines[1].attributes('y1')) expect(zeroLines[0].attributes('y1')).not.toBe(zeroLines[1].attributes('y1'))
}) })
it('uses the full drawing area and hides auxiliary layers in clean view', async () => { it('preserves a titled multi-axis plot and hides auxiliary layers in clean view', async () => {
const wrapper = await mountSizedChart( const data: WaveformData = {
{ kind: 'series',
kind: 'series', series: [
series: [ {
{ id: 'first',
id: 'first', trackId: 'overlay',
trackId: 'overlay', name: 'first',
name: 'first', data: {
data: { kind: 'points',
kind: 'points', points: [
points: [ { x: 0, y: -1 },
{ x: 0, y: -1 }, { x: 1, y: 1 },
{ x: 1, y: 1 }, ],
],
},
}, },
{ },
id: 'second', {
trackId: 'overlay', id: 'second',
name: 'second', trackId: 'overlay',
data: { name: 'second',
kind: 'points', data: {
points: [ kind: 'points',
{ x: 0, y: 1 }, points: [
{ x: 1, y: 2 }, { x: 0, y: 1 },
], { x: 1, y: 2 },
}, ],
}, },
{ },
id: 'third', {
name: 'third', id: 'third',
data: { name: 'third',
kind: 'points', data: {
points: [ kind: 'points',
{ x: 0, y: 2 }, points: [
{ x: 1, y: 3 }, { x: 0, y: 2 },
], { x: 1, y: 3 },
}, ],
}, },
], },
}, ],
{ }
cleanView: true, const sharedProps = {
grid: { rowCount: 1, columnCount: 1, showPagination: true }, grid: { rowCount: 1, columnCount: 1, showPagination: true },
title: { text: 'hidden title' }, overlayMode: 'multi-axis' as const,
frameNumber: 1, frameNumber: 1,
annotations: [{ id: 'note', seriesId: 'first', x: 0.5, y: 0, text: 'hidden note' }], annotations: [{ id: 'note', seriesId: 'first', x: 0.5, y: 0, text: 'hidden note' }],
zeroLine: { visible: true }, zeroLine: { visible: true },
}, title: { text: 'hidden title' },
) }
const regularWrapper = await mountSizedChart(data, sharedProps)
const wrapper = await mountSizedChart(data, {
...sharedProps,
cleanView: true,
})
expect(wrapper.get('.waveform-chart').attributes('data-chart-left-margin')).toBe('0') const regularTrack = regularWrapper.get('.waveform-chart__track')
expect(wrapper.get('.waveform-chart__track').attributes('data-track-height')).toBe('360') const cleanTrack = wrapper.get('.waveform-chart__track')
const geometryAttributes = [
'data-track-left',
'data-track-top',
'data-track-width',
'data-track-height',
]
expect(wrapper.get('.waveform-chart').attributes('data-chart-left-margin')).toBe(
regularWrapper.get('.waveform-chart').attributes('data-chart-left-margin'),
)
expect(wrapper.get('.waveform-chart').attributes('data-title-area-height')).toBe(
regularWrapper.get('.waveform-chart').attributes('data-title-area-height'),
)
geometryAttributes.forEach((attribute) => {
expect(cleanTrack.attributes(attribute)).toBe(regularTrack.attributes(attribute))
})
expect(wrapper.get('.waveform-chart').classes()).toContain('waveform-chart--clean')
expect(getComputedStyle(wrapper.get('.waveform-chart').element).borderColor).toBe(
'rgba(0, 0, 0, 0)',
)
expect(wrapper.findAll('.waveform-chart__series')).toHaveLength(2) expect(wrapper.findAll('.waveform-chart__series')).toHaveLength(2)
expect(wrapper.find('.waveform-chart__overlay--independent').exists()).toBe(true) expect(wrapper.find('.waveform-chart__overlay--independent').exists()).toBe(true)
expect(wrapper.find('.waveform-chart__title-area').exists()).toBe(false) expect(wrapper.get('.waveform-chart__title-area').attributes('aria-hidden')).toBe('true')
expect(wrapper.find('.waveform-chart__title-visual').exists()).toBe(false)
expect(wrapper.find('.waveform-chart__axis').exists()).toBe(false) expect(wrapper.find('.waveform-chart__axis').exists()).toBe(false)
expect(wrapper.find('.waveform-chart__grid').exists()).toBe(false) expect(wrapper.find('.waveform-chart__grid').exists()).toBe(false)
expect(wrapper.find('.waveform-chart__plot-frame').exists()).toBe(false) expect(wrapper.find('.waveform-chart__plot-frame').exists()).toBe(false)
@@ -366,6 +390,30 @@ describe('WaveformChart', () => {
expect(wrapper.find('.ant-pagination').exists()).toBe(false) expect(wrapper.find('.ant-pagination').exists()).toBe(false)
}) })
it('preserves every track geometry in a multi-column clean view', async () => {
const props = {
displayMode: 'independent' as const,
grid: { rowCount: 2, columnCount: 2 },
}
const regularWrapper = await mountSizedChart(gridSeries(4), props)
const cleanWrapper = await mountSizedChart(gridSeries(4), { ...props, cleanView: true })
const geometryAttributes = [
'data-track-left',
'data-track-top',
'data-track-width',
'data-track-height',
]
const regularTracks = regularWrapper.findAll('.waveform-chart__track')
const cleanTracks = cleanWrapper.findAll('.waveform-chart__track')
expect(cleanTracks).toHaveLength(regularTracks.length)
cleanTracks.forEach((track, index) => {
geometryAttributes.forEach((attribute) => {
expect(track.attributes(attribute)).toBe(regularTracks[index].attributes(attribute))
})
})
})
it('places start, middle, and end step transitions at the expected X positions', async () => { it('places start, middle, and end step transitions at the expected X positions', async () => {
const lineTypes = ['step-start', 'step-middle', 'step-end', 'step-after'] as const const lineTypes = ['step-start', 'step-middle', 'step-end', 'step-after'] as const
const wrapper = await mountSizedChart({ const wrapper = await mountSizedChart({

View File

@@ -288,13 +288,11 @@ const legendOrientation = computed<Exclude<WaveformLegendOrientation, 'auto'>>((
: 'vertical' : 'vertical'
}) })
const resolvedTitleText = computed(() => props.title?.text.trim() ?? '') const resolvedTitleText = computed(() => props.title?.text.trim() ?? '')
const titleVisible = computed( const titleAreaReserved = computed(
() => () =>
!isCleanView.value && Boolean(props.title) && props.title?.visible !== false && resolvedTitleText.value.length > 0,
Boolean(props.title) &&
props.title?.visible !== false &&
resolvedTitleText.value.length > 0,
) )
const titleVisible = computed(() => titleAreaReserved.value && !isCleanView.value)
const titleFontSize = computed(() => { const titleFontSize = computed(() => {
const fontSize = props.title?.textStyle?.fontSize const fontSize = props.title?.textStyle?.fontSize
return Number.isFinite(fontSize) && (fontSize ?? 0) > 0 ? (fontSize as number) : 14 return Number.isFinite(fontSize) && (fontSize ?? 0) > 0 ? (fontSize as number) : 14
@@ -343,12 +341,10 @@ const titleLayout = computed(() =>
rotation: titleRotation.value, rotation: titleRotation.value,
}), }),
) )
const titleAreaHeight = computed(() => (titleVisible.value ? titleLayout.value.areaHeight : 0)) const titleAreaHeight = computed(() => (titleAreaReserved.value ? titleLayout.value.areaHeight : 0))
const chartTopMargin = computed(() => (isCleanView.value ? 0 : margin.top)) const chartTopMargin = computed(() => margin.top)
const drawingHeight = computed(() => Math.max(0, chartHeight.value - titleAreaHeight.value)) const drawingHeight = computed(() => Math.max(0, chartHeight.value - titleAreaHeight.value))
const innerHeight = computed(() => const innerHeight = computed(() => Math.max(0, drawingHeight.value - margin.top - margin.bottom))
Math.max(0, drawingHeight.value - (isCleanView.value ? 0 : margin.top + margin.bottom)),
)
const titleAreaStyle = computed<CSSProperties>(() => ({ const titleAreaStyle = computed<CSSProperties>(() => ({
height: `${titleAreaHeight.value}px`, height: `${titleAreaHeight.value}px`,
justifyContent: justifyContent:
@@ -461,7 +457,7 @@ const hasVisibleWaveformData = computed(() =>
) )
const chartLeftMargin = computed(() => const chartLeftMargin = computed(() =>
Math.max( Math.max(
isCleanView.value ? 0 : margin.left, margin.left,
hasYAxisLabels.value hasYAxisLabels.value
? yAxisMetrics.value.fullClearance ? yAxisMetrics.value.fullClearance
: hasVisibleWaveformData.value : hasVisibleWaveformData.value
@@ -482,20 +478,14 @@ const multiAxisClearance = computed(() =>
), ),
) )
const resolvedChartLeftMargin = computed(() => const resolvedChartLeftMargin = computed(() =>
isCleanView.value props.overlayMode === 'multi-axis'
? 0 ? Math.max(chartLeftMargin.value, multiAxisClearance.value.left)
: props.overlayMode === 'multi-axis' : chartLeftMargin.value,
? Math.max(chartLeftMargin.value, multiAxisClearance.value.left)
: chartLeftMargin.value,
) )
const chartRightMargin = computed(() => const chartRightMargin = computed(() =>
props.overlayMode === 'multi-axis' props.overlayMode === 'multi-axis'
? isCleanView.value ? Math.max(margin.right, multiAxisClearance.value.right)
? 0 : margin.right,
: Math.max(margin.right, multiAxisClearance.value.right)
: isCleanView.value
? 0
: margin.right,
) )
const innerWidth = computed(() => const innerWidth = computed(() =>
Math.max(0, chartWidth.value - resolvedChartLeftMargin.value - chartRightMargin.value), Math.max(0, chartWidth.value - resolvedChartLeftMargin.value - chartRightMargin.value),
@@ -595,7 +585,6 @@ const gridCells = computed(() => {
props.displayMode, props.displayMode,
pagedTracks.value.map(Boolean), pagedTracks.value.map(Boolean),
yAxisLayout.value.horizontalGap, yAxisLayout.value.horizontalGap,
!isCleanView.value,
) )
return cells.map((cell, index) => ({ ...cell, series: pagedTracks.value[index] })) return cells.map((cell, index) => ({ ...cell, series: pagedTracks.value[index] }))
}) })
@@ -1763,7 +1752,7 @@ watch(
) )
function measureTitle() { function measureTitle() {
if (!titleVisible.value || !titleMeasureElement.value) { if (!titleAreaReserved.value || !titleMeasureElement.value) {
measuredTitleWidth.value = 0 measuredTitleWidth.value = 0
measuredTitleHeight.value = 0 measuredTitleHeight.value = 0
return return
@@ -1774,7 +1763,7 @@ function measureTitle() {
} }
watch( watch(
[resolvedTitleText, titleVisible, titleMeasureStyle], [resolvedTitleText, titleAreaReserved, titleMeasureStyle],
async () => { async () => {
measuredTitleWidth.value = 0 measuredTitleWidth.value = 0
measuredTitleHeight.value = 0 measuredTitleHeight.value = 0
@@ -1813,7 +1802,10 @@ onBeforeUnmount(() => {
:class="[ :class="[
`waveform-chart--${displayMode}`, `waveform-chart--${displayMode}`,
`waveform-chart--interaction-${activeInteractionMode}`, `waveform-chart--interaction-${activeInteractionMode}`,
{ 'waveform-chart--panning': selection?.mode === 'pan' }, {
'waveform-chart--clean': isCleanView,
'waveform-chart--panning': selection?.mode === 'pan',
},
]" ]"
:style="containerStyle" :style="containerStyle"
:data-display-mode="displayMode" :data-display-mode="displayMode"
@@ -1824,11 +1816,12 @@ onBeforeUnmount(() => {
@contextmenu.capture="handleNativeContextMenu" @contextmenu.capture="handleNativeContextMenu"
> >
<div <div
v-if="titleVisible" v-if="titleAreaReserved"
class="waveform-chart__title-area" class="waveform-chart__title-area"
:style="titleAreaStyle" :style="titleAreaStyle"
role="heading" :role="titleVisible ? 'heading' : undefined"
aria-level="2" :aria-level="titleVisible ? 2 : undefined"
:aria-hidden="isCleanView || undefined"
> >
<span <span
ref="titleMeasureElement" ref="titleMeasureElement"
@@ -1838,7 +1831,7 @@ onBeforeUnmount(() => {
> >
{{ resolvedTitleText }} {{ resolvedTitleText }}
</span> </span>
<span class="waveform-chart__title-visual" :style="titleVisualStyle"> <span v-if="titleVisible" class="waveform-chart__title-visual" :style="titleVisualStyle">
<span <span
class="waveform-chart__title-text" class="waveform-chart__title-text"
:style="titleTextStyle" :style="titleTextStyle"
@@ -2069,6 +2062,10 @@ onBeforeUnmount(() => {
border-radius: 6px; border-radius: 6px;
} }
.waveform-chart--clean {
border-color: transparent;
}
.waveform-chart__pagination { .waveform-chart__pagination {
position: absolute; position: absolute;
right: 10px; right: 10px;