feat(chart): support configurable plot margins
All checks were successful
Package component / package (push) Successful in 7m2s
All checks were successful
Package component / package (push) Successful in 7m2s
This commit is contained in:
@@ -23,6 +23,7 @@ const props = withDefaults(defineProps<WaveformChartProps>(), {
|
||||
interactionMode: undefined,
|
||||
grid: () => ({ rowCount: 2, columnCount: 1, showPagination: true }),
|
||||
rendering: () => ({}),
|
||||
plotMargin: () => ({}),
|
||||
legend: () => ({ position: 'top-right', orientation: 'auto' }),
|
||||
defaultHiddenSeriesIds: () => [],
|
||||
cleanView: false,
|
||||
|
||||
@@ -19,6 +19,7 @@ const {
|
||||
overlayMode,
|
||||
resolvedChartLeftMargin,
|
||||
titleAreaHeight,
|
||||
resolvedPlotMargin,
|
||||
pointerInsideChart,
|
||||
handleNativeContextMenu,
|
||||
titleAreaReserved,
|
||||
@@ -118,6 +119,8 @@ const {
|
||||
:data-presentation-mode="isPresentationMode"
|
||||
:data-overlay-mode="overlayMode"
|
||||
:data-chart-left-margin="resolvedChartLeftMargin"
|
||||
:data-plot-margin-top="resolvedPlotMargin.top"
|
||||
:data-plot-margin-bottom="resolvedPlotMargin.bottom"
|
||||
:data-title-area-height="titleAreaHeight"
|
||||
@pointerenter="pointerInsideChart = true"
|
||||
@pointerleave="pointerInsideChart = false"
|
||||
@@ -287,18 +290,18 @@ const {
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<text
|
||||
v-if="resolvedXLabel && !isCleanView"
|
||||
class="waveform-chart__label waveform-chart__x-label"
|
||||
:x="innerWidth / 2"
|
||||
:y="xAxisTitleY"
|
||||
text-anchor="middle"
|
||||
>
|
||||
{{ resolvedXLabel }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<text
|
||||
v-if="resolvedXLabel && !isCleanView"
|
||||
class="waveform-chart__label waveform-chart__x-label"
|
||||
:x="resolvedChartLeftMargin + innerWidth / 2"
|
||||
:y="xAxisTitleY"
|
||||
text-anchor="middle"
|
||||
>
|
||||
{{ resolvedXLabel }}
|
||||
</text>
|
||||
|
||||
<text
|
||||
v-if="hasChartArea && !hasWaveformData"
|
||||
class="waveform-chart__empty"
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
/** 图表边距 */
|
||||
export const margin = { top: 18, right: 24, bottom: 52, left: 64 }
|
||||
|
||||
/** Distance from the drawing SVG bottom edge to the X-axis title baseline. */
|
||||
export const X_AXIS_TITLE_BOTTOM_OFFSET = 12
|
||||
|
||||
/**
|
||||
* 图表最小高度(像素)
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
normalizeGridOptions,
|
||||
paginateSeries,
|
||||
resolveGridCellGeometry,
|
||||
X_AXIS_BAND,
|
||||
} from './grid'
|
||||
import {
|
||||
buildTrackLayouts,
|
||||
@@ -316,7 +315,6 @@ export function useWaveformLayout(context: LayoutContext) {
|
||||
)
|
||||
: [],
|
||||
)
|
||||
const xAxisTitleY = computed(() => innerHeight.value + X_AXIS_BAND + 10)
|
||||
const editorSeries = computed<AnnotationSeriesInfo | undefined>(() => {
|
||||
const seriesId = annotationInteraction.editorDraft.value?.annotation.seriesId
|
||||
const series = chartSeries.value.find((item) => item.id === seriesId)
|
||||
@@ -360,7 +358,6 @@ export function useWaveformLayout(context: LayoutContext) {
|
||||
resolveSeriesYScale,
|
||||
annotationTrackLayouts,
|
||||
renderedAnnotations,
|
||||
xAxisTitleY,
|
||||
editorSeries,
|
||||
resolveFrameNumber,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TITLE_CHAR_WIDTH_RATIO,
|
||||
TITLE_DEFAULT_FONT_SIZE,
|
||||
TITLE_LINE_HEIGHT,
|
||||
X_AXIS_TITLE_BOTTOM_OFFSET,
|
||||
ZERO_LINE_DEFAULTS,
|
||||
} from './constants'
|
||||
import { calculateRotatedTitleLayout, TITLE_AREA_HORIZONTAL_PADDING } from './title'
|
||||
@@ -144,11 +145,23 @@ export function useWaveformPresentation(context: PresentationContext) {
|
||||
const titleAreaHeight = computed(() =>
|
||||
titleAreaReserved.value ? titleLayout.value.areaHeight : 0,
|
||||
)
|
||||
const chartTopMargin = computed(() => margin.top)
|
||||
const resolvePlotMargin = (value: number | undefined, fallback: number) =>
|
||||
typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback
|
||||
const resolvedPlotMargin = computed(() => ({
|
||||
top: resolvePlotMargin(props.plotMargin.top, margin.top),
|
||||
bottom: resolvePlotMargin(props.plotMargin.bottom, margin.bottom),
|
||||
}))
|
||||
const chartTopMargin = computed(() => resolvedPlotMargin.value.top)
|
||||
const drawingHeight = computed(() =>
|
||||
Math.max(0, chartHeight.value - titleAreaHeight.value - paginationBandHeight.value),
|
||||
)
|
||||
const innerHeight = computed(() => Math.max(0, drawingHeight.value - margin.top - margin.bottom))
|
||||
const xAxisTitleY = computed(() => Math.max(0, drawingHeight.value - X_AXIS_TITLE_BOTTOM_OFFSET))
|
||||
const innerHeight = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
drawingHeight.value - resolvedPlotMargin.value.top - resolvedPlotMargin.value.bottom,
|
||||
),
|
||||
)
|
||||
const titleAreaStyle = computed<CSSProperties>(() => ({
|
||||
height: `${titleAreaHeight.value}px`,
|
||||
justifyContent:
|
||||
@@ -193,8 +206,10 @@ export function useWaveformPresentation(context: PresentationContext) {
|
||||
titleMeasureStyle,
|
||||
titleLayout,
|
||||
titleAreaHeight,
|
||||
resolvedPlotMargin,
|
||||
chartTopMargin,
|
||||
drawingHeight,
|
||||
xAxisTitleY,
|
||||
innerHeight,
|
||||
titleAreaStyle,
|
||||
titleVisualStyle,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
WaveformInteractionMode,
|
||||
WaveformLegendOptions,
|
||||
WaveformOverlayMode,
|
||||
WaveformPlotMargin,
|
||||
WaveformPoint,
|
||||
WaveformRenderingOptions,
|
||||
WaveformTitleOptions,
|
||||
@@ -42,6 +43,7 @@ export interface WaveformChartProps {
|
||||
interactionMode?: WaveformInteractionMode
|
||||
grid?: WaveformGridOptions
|
||||
rendering?: WaveformRenderingOptions
|
||||
plotMargin?: WaveformPlotMargin
|
||||
title?: WaveformTitleOptions
|
||||
legend?: WaveformLegendOptions
|
||||
hiddenSeriesIds?: string[]
|
||||
@@ -65,6 +67,7 @@ type DefaultedProp =
|
||||
| 'annotationsVisible'
|
||||
| 'grid'
|
||||
| 'rendering'
|
||||
| 'plotMargin'
|
||||
| 'legend'
|
||||
| 'defaultHiddenSeriesIds'
|
||||
| 'cleanView'
|
||||
|
||||
@@ -13,6 +13,7 @@ export type {
|
||||
WaveformAnnotationStyle,
|
||||
WaveformAnnotation,
|
||||
WaveformRenderingOptions,
|
||||
WaveformPlotMargin,
|
||||
WaveformTitleTextStyle,
|
||||
WaveformTitleOptions,
|
||||
WaveformLegendPosition,
|
||||
|
||||
@@ -11,6 +11,7 @@ export type {
|
||||
WaveformAnnotationStyle,
|
||||
WaveformAnnotation,
|
||||
WaveformRenderingOptions,
|
||||
WaveformPlotMargin,
|
||||
WaveformTitleTextStyle,
|
||||
WaveformTitleOptions,
|
||||
WaveformLegendPosition,
|
||||
|
||||
@@ -12,6 +12,7 @@ export type {
|
||||
WaveformAnnotationStyle,
|
||||
WaveformAnnotation,
|
||||
WaveformRenderingOptions,
|
||||
WaveformPlotMargin,
|
||||
WaveformTitleTextStyle,
|
||||
WaveformTitleOptions,
|
||||
WaveformFrameStyle,
|
||||
|
||||
@@ -105,6 +105,65 @@ describe('WaveformChart', () => {
|
||||
expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('300')
|
||||
})
|
||||
|
||||
it('configures plot top and bottom margins and reacts to updates', async () => {
|
||||
const wrapper = await mountSizedChart(
|
||||
{ kind: 'samples', values: [0, 1], sampleRate: 1 },
|
||||
{
|
||||
displayMode: 'compact',
|
||||
grid: { rowCount: 1, columnCount: 1 },
|
||||
plotMargin: { top: 30, bottom: 70 },
|
||||
},
|
||||
)
|
||||
|
||||
expect(wrapper.attributes('data-plot-margin-top')).toBe('30')
|
||||
expect(wrapper.attributes('data-plot-margin-bottom')).toBe('70')
|
||||
expect(wrapper.get('.waveform-chart__overlay').attributes('height')).toBe('260')
|
||||
expect(wrapper.get('.waveform-chart__svg > g').attributes('transform')).toContain(', 30)')
|
||||
|
||||
await wrapper.setProps({ plotMargin: { top: 12 } })
|
||||
|
||||
expect(wrapper.attributes('data-plot-margin-top')).toBe('12')
|
||||
expect(wrapper.attributes('data-plot-margin-bottom')).toBe('52')
|
||||
expect(wrapper.get('.waveform-chart__overlay').attributes('height')).toBe('296')
|
||||
})
|
||||
|
||||
it('falls back to default plot margins for invalid values', async () => {
|
||||
const wrapper = await mountSizedChart(
|
||||
{ kind: 'samples', values: [0, 1], sampleRate: 1 },
|
||||
{
|
||||
displayMode: 'compact',
|
||||
grid: { rowCount: 1, columnCount: 1 },
|
||||
plotMargin: { top: -1, bottom: Number.NaN },
|
||||
},
|
||||
)
|
||||
|
||||
expect(wrapper.attributes('data-plot-margin-top')).toBe('18')
|
||||
expect(wrapper.attributes('data-plot-margin-bottom')).toBe('52')
|
||||
expect(wrapper.get('.waveform-chart__overlay').attributes('height')).toBe('290')
|
||||
})
|
||||
|
||||
it('keeps the chart title and X-axis title fixed when plot margins change', async () => {
|
||||
const wrapper = await mountSizedChart(
|
||||
{ kind: 'samples', values: [0, 1], sampleRate: 1 },
|
||||
{
|
||||
displayMode: 'compact',
|
||||
grid: { rowCount: 1, columnCount: 1 },
|
||||
title: { text: '固定标题' },
|
||||
},
|
||||
)
|
||||
const titleAreaStyle = wrapper.get('.waveform-chart__title-area').attributes('style')
|
||||
const xAxisTitle = wrapper.get('.waveform-chart__x-label')
|
||||
const initialX = xAxisTitle.attributes('x')
|
||||
const initialY = xAxisTitle.attributes('y')
|
||||
|
||||
await wrapper.setProps({ plotMargin: { top: 60, bottom: 90 } })
|
||||
|
||||
expect(wrapper.get('.waveform-chart__title-area').attributes('style')).toBe(titleAreaStyle)
|
||||
expect(wrapper.get('.waveform-chart__x-label').attributes('x')).toBe(initialX)
|
||||
expect(wrapper.get('.waveform-chart__x-label').attributes('y')).toBe(initialY)
|
||||
expect(wrapper.get('.waveform-chart__svg > g').attributes('transform')).toContain(', 60)')
|
||||
})
|
||||
|
||||
it('does not render or reserve space for missing, hidden, or blank titles', async () => {
|
||||
for (const title of [undefined, { visible: false, text: '隐藏标题' }, { text: ' ' }]) {
|
||||
const wrapper = await mountSizedChart(
|
||||
|
||||
Reference in New Issue
Block a user