fix(图表): 优化组合图图例位置过于贴近图表内容区域的问题

This commit is contained in:
jianneng-fit2cloud
2026-06-04 19:46:39 +08:00
parent 3bc5c65b77
commit e197f3e7c2
4 changed files with 178 additions and 243 deletions

View File

@@ -1,3 +1,8 @@
import type { G2Spec } from '@antv/g2'
import { parseJson } from '@/views/chart/components/js/util'
type MixLegendRelation = [string, string]
export const CHART_MIX_EDITOR_PROPERTY: EditorProperty[] = [
'background-overall-component',
'border-style',
@@ -74,3 +79,158 @@ export const CHART_MIX_AXIS_TYPE: AxisType[] = [
'extLabel',
'extTooltip'
]
export const configMixCustomLegend = (
chart: Chart,
options: G2Spec,
leftRelations: MixLegendRelation[] = [],
rightRelations: MixLegendRelation[] = []
): G2Spec => {
const { legend } = parseJson(chart.customStyle) || {}
if (!legend?.show || !options.children?.length) {
return options
}
const unionRelations = [...leftRelations, ...rightRelations].filter(
([key, value]) => key !== undefined && key !== null && Boolean(value)
)
if (!unionRelations.length) {
return options
}
const hPosition = ['left', 'center', 'right'].includes(legend.hPosition)
? legend.hPosition
: 'center'
const rawVPosition = ['top', 'center', 'bottom'].includes(legend.vPosition)
? legend.vPosition
: 'bottom'
const vPosition = hPosition === 'center' && rawVPosition === 'center' ? 'top' : rawVPosition
const getPositiveNumber = (value: unknown, defaultValue: number) => {
const numberValue = Number(value)
return Number.isFinite(numberValue) && numberValue > 0 ? numberValue : defaultValue
}
const legendFontSize = getPositiveNumber(legend.fontSize, 12)
const legendMarkerSize = getPositiveNumber(legend.size, 4)
const legendIcon = legend.icon || 'circle'
const legendColor = legend.color || '#333333'
const legendChartGap = 8
const getLegendChartGap = (direction: 'col' | 'row', legendFirst = false) =>
direction === 'col' && !legendFirst ? 4 : legendChartGap
const getLegendRatio = (direction: 'col' | 'row', legendFirst = false) => {
const chartContainer = chart.container as unknown
const containerDom =
typeof document === 'undefined' || !chartContainer
? undefined
: typeof chartContainer === 'string'
? document.getElementById(chartContainer)
: typeof (chartContainer as HTMLElement).getBoundingClientRect === 'function'
? (chartContainer as HTMLElement)
: undefined
const containerRect = containerDom?.getBoundingClientRect()
const mainSize = direction === 'col' ? containerRect?.height : containerRect?.width
const getTextWidth = text => {
return Array.from(`${text ?? ''}`).reduce((width, char) => {
return width + (char.charCodeAt(0) > 255 ? legendFontSize : legendFontSize * 0.6)
}, 0)
}
const crossGap = getLegendChartGap(direction, legendFirst)
// spaceFlex 按比例切分子层,这里把图例字号/图标尺寸换算成近似像素层高,避免图例放大后覆盖绘图区
const legendLineSize = Math.ceil(Math.max(legendFontSize * 1.3, legendMarkerSize) + crossGap)
const legendMainSize =
direction === 'col'
? Math.max(24, legendLineSize)
: Math.max(
80,
...unionRelations.map(([name]) => getTextWidth(name) + legendMarkerSize + 40)
)
if (!mainSize || mainSize <= 0) {
const fallbackLegendRatio = Math.max(2, Math.ceil(legendMainSize / 16))
return legendFirst ? [fallbackLegendRatio, 20] : [20, fallbackLegendRatio]
}
// ratio 使用像素等价值,让图例层随内容增长,同时至少给绘图区保留 1px避免极小容器下异常
const safeLegendSize = Math.max(1, Math.min(legendMainSize, mainSize - 1))
const chartMainSize = Math.max(mainSize - safeLegendSize, 1)
return legendFirst ? [safeLegendSize, chartMainSize] : [chartMainSize, safeLegendSize]
}
// 双轴组合图左右 mark 使用独立 color scaleG2 内置 legend 无法直接合并,因此手工生成 legends 子层
const legendMark: any = {
position: 'top',
type: 'legends',
key: 'legend',
scale: {
color: {
type: 'ordinal',
domain: [],
range: [],
relations: unionRelations
}
},
layout: {
justifyContent: 'center',
alignItems: 'center'
},
crossPadding: 0,
itemMarker: legendIcon,
itemMarkerSize: legendMarkerSize,
itemLabelFontSize: legendFontSize,
itemLabelFill: legendColor,
itemLabelOpacity: 1,
itemLabelFillOpacity: 1
}
unionRelations.forEach(([key, value]) => {
legendMark.scale.color.domain.push(key)
legendMark.scale.color.range.push(value)
})
if (hPosition === 'center') {
options.direction = 'col'
legendMark.maxRows = 1
if (vPosition === 'top') {
legendMark.position = 'top'
legendMark.crossPadding = getLegendChartGap('col', true)
options.ratio = getLegendRatio('col', true)
options.children.unshift(legendMark)
}
if (vPosition === 'bottom') {
legendMark.position = 'bottom'
legendMark.crossPadding = getLegendChartGap('col')
options.ratio = getLegendRatio('col')
options.children.push(legendMark)
}
return options
}
if (vPosition === 'center') {
options.direction = 'row'
legendMark.maxCols = 1
if (hPosition === 'left') {
legendMark.position = 'left'
legendMark.crossPadding = getLegendChartGap('row', true)
options.ratio = getLegendRatio('row', true)
options.children.unshift(legendMark)
}
if (hPosition === 'right') {
legendMark.position = 'right'
legendMark.crossPadding = getLegendChartGap('row')
options.ratio = getLegendRatio('row')
options.children.push(legendMark)
}
return options
}
legendMark.maxRows = 1
if (vPosition === 'top') {
legendMark.position = 'top'
legendMark.crossPadding = getLegendChartGap('col', true)
options.ratio = getLegendRatio('col', true)
options.children.unshift(legendMark)
}
if (vPosition === 'bottom') {
legendMark.position = 'bottom'
legendMark.crossPadding = getLegendChartGap('col')
options.ratio = getLegendRatio('col')
options.children.push(legendMark)
}
if (hPosition === 'left') {
legendMark.layout.justifyContent = 'flex-start'
}
if (hPosition === 'right') {
legendMark.layout.justifyContent = 'flex-end'
}
return options
}

View File

@@ -20,7 +20,11 @@ import {
TOOLTIP_ITEM_TPL,
TOOLTIP_TITLE_TPL
} from '../../../common/common_antv'
import { CHART_MIX_EDITOR_PROPERTY, CHART_MIX_EDITOR_PROPERTY_INNER } from './common'
import {
CHART_MIX_EDITOR_PROPERTY,
CHART_MIX_EDITOR_PROPERTY_INNER,
configMixCustomLegend
} from './common'
import { registerSymbol, Symbols } from '@antv/g2/esm/utils/marker'
import G2TooltipCarousel from '@/views/chart/components/js/G2TooltipCarousel'
import {
@@ -456,89 +460,10 @@ export class GroupLineMix extends G2ChartView {
}
protected configLegend(chart: Chart, options: G2Spec): G2Spec {
const { legend } = parseJson(chart.customStyle)
if (!legend.show) {
return options
}
const [leftLineMark, _, lineMark] = options.children[0].children
const leftRelations = leftLineMark.scale.color.relations
const rightRelations = lineMark.scale.color.relations
const unionRelations = [...leftRelations, ...rightRelations]
const legendMark = {
position: 'top',
type: 'legends',
key: 'legend',
scale: {
color: {
type: 'ordinal',
domain: [],
range: [],
relations: unionRelations
}
},
layout: {
justifyContent: 'center',
alignItems: 'center'
},
itemMarker: legend.icon,
itemMarkerSize: legend.size,
itemLabelFontSize: legend.fontSize,
itemLabelFill: legend.color,
itemLabelOpacity: 1,
itemLabelFillOpacity: 1
}
unionRelations.forEach(([key, value]) => {
legendMark.scale.color.domain.push(key)
legendMark.scale.color.range.push(value)
})
if (legend.hPosition === 'center') {
options.direction = 'col'
legendMark.maxRows = 1
if (legend.vPosition === 'top') {
legendMark.position = 'top'
options.ratio = [1, 20]
options.children.unshift(legendMark)
}
if (legend.vPosition === 'bottom') {
legendMark.position = 'bottom'
options.ratio = [20, 1]
options.children.push(legendMark)
}
return options
}
if (legend.vPosition === 'center') {
options.direction = 'row'
legendMark.maxCols = 1
if (legend.hPosition === 'left') {
legendMark.position = 'left'
options.ratio = [1, 20]
options.children.unshift(legendMark)
}
if (legend.hPosition === 'right') {
legendMark.position = 'right'
options.ratio = [20, 1]
options.children.push(legendMark)
}
return options
}
legendMark.maxRows = 1
if (legend.vPosition === 'top') {
legendMark.position = 'top'
options.ratio = [1, 20]
options.children.unshift(legendMark)
}
if (legend.vPosition === 'bottom') {
legendMark.position = 'bottom'
options.ratio = [20, 1]
options.children.push(legendMark)
}
if (legend.hPosition === 'left') {
legendMark.layout.justifyContent = 'flex-start'
}
if (legend.hPosition === 'right') {
legendMark.layout.justifyContent = 'flex-end'
}
return options
return configMixCustomLegend(chart, options, leftRelations, rightRelations)
}
protected configLabel(chart: Chart, options: G2Spec): G2Spec {

View File

@@ -23,7 +23,11 @@ import {
TOOLTIP_ITEM_TPL,
TOOLTIP_TITLE_TPL
} from '../../../common/common_antv'
import { CHART_MIX_EDITOR_PROPERTY, CHART_MIX_EDITOR_PROPERTY_INNER } from './common'
import {
CHART_MIX_EDITOR_PROPERTY,
CHART_MIX_EDITOR_PROPERTY_INNER,
configMixCustomLegend
} from './common'
import { registerSymbol, Symbols } from '@antv/g2/esm/utils/marker'
import G2TooltipCarousel from '@/views/chart/components/js/G2TooltipCarousel'
import {
@@ -402,89 +406,10 @@ export class GroupLineMix extends G2ChartView {
}
protected configLegend(chart: Chart, options: G2Spec): G2Spec {
const { legend } = parseJson(chart.customStyle)
if (!legend.show) {
return options
}
const [intervalMark, lineMark] = options.children[0].children
const leftRelations = intervalMark.scale.color.relations
const rightRelations = lineMark.scale.color.relations
const unionRelations = [...leftRelations, ...rightRelations]
const legendMark = {
position: 'top',
type: 'legends',
key: 'legend',
scale: {
color: {
type: 'ordinal',
domain: [],
range: [],
relations: unionRelations
}
},
layout: {
justifyContent: 'center',
alignItems: 'center'
},
itemMarker: legend.icon,
itemMarkerSize: legend.size,
itemLabelFontSize: legend.fontSize,
itemLabelFill: legend.color,
itemLabelOpacity: 1,
itemLabelFillOpacity: 1
}
unionRelations.forEach(([key, value]) => {
legendMark.scale.color.domain.push(key)
legendMark.scale.color.range.push(value)
})
if (legend.hPosition === 'center') {
options.direction = 'col'
legendMark.maxRows = 1
if (legend.vPosition === 'top') {
legendMark.position = 'top'
options.ratio = [1, 20]
options.children.unshift(legendMark)
}
if (legend.vPosition === 'bottom') {
legendMark.position = 'bottom'
options.ratio = [20, 1]
options.children.push(legendMark)
}
return options
}
if (legend.vPosition === 'center') {
options.direction = 'row'
legendMark.maxCols = 1
if (legend.hPosition === 'left') {
legendMark.position = 'left'
options.ratio = [1, 20]
options.children.unshift(legendMark)
}
if (legend.hPosition === 'right') {
legendMark.position = 'right'
options.ratio = [20, 1]
options.children.push(legendMark)
}
return options
}
legendMark.maxRows = 1
if (legend.vPosition === 'top') {
legendMark.position = 'top'
options.ratio = [1, 20]
options.children.unshift(legendMark)
}
if (legend.vPosition === 'bottom') {
legendMark.position = 'bottom'
options.ratio = [20, 1]
options.children.push(legendMark)
}
if (legend.hPosition === 'left') {
legendMark.layout.justifyContent = 'flex-start'
}
if (legend.hPosition === 'right') {
legendMark.layout.justifyContent = 'flex-end'
}
return options
return configMixCustomLegend(chart, options, leftRelations, rightRelations)
}
protected configLabel(chart: Chart, options: G2Spec): G2Spec {

View File

@@ -22,7 +22,11 @@ import {
TOOLTIP_ITEM_TPL,
TOOLTIP_TITLE_TPL
} from '../../../common/common_antv'
import { CHART_MIX_EDITOR_PROPERTY, CHART_MIX_EDITOR_PROPERTY_INNER } from './common'
import {
CHART_MIX_EDITOR_PROPERTY,
CHART_MIX_EDITOR_PROPERTY_INNER,
configMixCustomLegend
} from './common'
import { registerSymbol, Symbols } from '@antv/g2/esm/utils/marker'
import G2TooltipCarousel from '@/views/chart/components/js/G2TooltipCarousel'
import {
@@ -396,89 +400,10 @@ export class StackLineMix extends G2ChartView {
}
protected configLegend(chart: Chart, options: G2Spec): G2Spec {
const { legend } = parseJson(chart.customStyle)
if (!legend.show) {
return options
}
const [intervalMark, lineMark] = options.children[0].children
const leftRelations = intervalMark.scale.color.relations
const rightRelations = lineMark.scale.color.relations
const unionRelations = [...leftRelations, ...rightRelations]
const legendMark = {
position: 'top',
type: 'legends',
key: 'legend',
scale: {
color: {
type: 'ordinal',
domain: [],
range: [],
relations: unionRelations
}
},
layout: {
justifyContent: 'center',
alignItems: 'center'
},
itemMarker: legend.icon,
itemMarkerSize: legend.size,
itemLabelFontSize: legend.fontSize,
itemLabelFill: legend.color,
itemLabelOpacity: 1,
itemLabelFillOpacity: 1
}
unionRelations.forEach(([key, value]) => {
legendMark.scale.color.domain.push(key)
legendMark.scale.color.range.push(value)
})
if (legend.hPosition === 'center') {
options.direction = 'col'
legendMark.maxRows = 1
if (legend.vPosition === 'top') {
legendMark.position = 'top'
options.ratio = [1, 20]
options.children.unshift(legendMark)
}
if (legend.vPosition === 'bottom') {
legendMark.position = 'bottom'
options.ratio = [20, 1]
options.children.push(legendMark)
}
return options
}
if (legend.vPosition === 'center') {
options.direction = 'row'
legendMark.maxCols = 1
if (legend.hPosition === 'left') {
legendMark.position = 'left'
options.ratio = [1, 20]
options.children.unshift(legendMark)
}
if (legend.hPosition === 'right') {
legendMark.position = 'right'
options.ratio = [20, 1]
options.children.push(legendMark)
}
return options
}
legendMark.maxRows = 1
if (legend.vPosition === 'top') {
legendMark.position = 'top'
options.ratio = [1, 20]
options.children.unshift(legendMark)
}
if (legend.vPosition === 'bottom') {
legendMark.position = 'bottom'
options.ratio = [20, 1]
options.children.push(legendMark)
}
if (legend.hPosition === 'left') {
legendMark.layout.justifyContent = 'flex-start'
}
if (legend.hPosition === 'right') {
legendMark.layout.justifyContent = 'flex-end'
}
return options
return configMixCustomLegend(chart, options, leftRelations, rightRelations)
}
protected configLabel(chart: Chart, options: G2Spec): G2Spec {