feat(图表): 支持百分比堆叠柱状图

This commit is contained in:
jianneng-fit2cloud
2025-05-15 19:34:29 +08:00
committed by jianneng-fit2cloud
parent 04330f4d9f
commit 9dcb1f5e3c
5 changed files with 437 additions and 12 deletions

View File

@@ -7,15 +7,26 @@ import {
} from '@/views/chart/components/js/panel/charts/g2/bar/common'
import { useI18n } from '@/hooks/web/useI18n'
import { flow, hexColorToRGBA, hexToRgba, parseJson } from '@/views/chart/components/js/util'
import { cloneDeep } from 'lodash-es'
import { cloneDeep, defaultsDeep, isEmpty } from 'lodash-es'
import { valueFormatter } from '@/views/chart/components/js/formatter'
import { getLineDash, setGradientColor } from '@/views/chart/components/js/panel/common/common_antv'
import {
getLineDash,
setGradientColor,
TOOLTIP_ITEM_TPL,
TOOLTIP_TITLE_TPL
} from '@/views/chart/components/js/panel/common/common_antv'
import {
DEFAULT_YAXIS_EXT_STYLE,
DEFAULT_YAXIS_STYLE
} from '@/views/chart/components/editor/util/chart'
import _ from 'lodash'
import { Transform, ViewSpec } from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
import {
configTooltip,
createTooltipWrapper,
tooltipCss,
Transform,
ViewSpec
} from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
const { t } = useI18n()
const DEFAULT_DATA: any[] = []
@@ -99,6 +110,7 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
const newChart = new G2Column({ container, autoFit: true })
newChart.options(options)
newChart.on('interval:click', action)
configTooltip(newChart, chart)
return newChart
}
@@ -121,10 +133,6 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
dy: l.position === 'top' ? -10 : 0,
dx: 0
}
// contrastReverse 标签颜色在图形背景上对比度低的情况下,从指定色板选择一个对比度最优的颜色
// overlapDodgeY 对位置碰撞的标签在 y 方向上进行调整,防止标签重叠
// exceedAdjust 自动对标签做溢出检测和矫正,即当标签超出视图区域时,会对标签自动做反方向的位移
// overlapHide 对位置碰撞的标签进行隐藏,默认保留前一个,隐藏后一个
const transform = {
transform: [{ type: 'exceedAdjust' }, { type: 'overlapHide' }]
}
@@ -177,7 +185,77 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
}
}
protected configTooltip(_chart: Chart, options: ViewSpec): ViewSpec {
protected configTooltip(chart: Chart, options: ViewSpec): ViewSpec {
const customAttr: DeepPartial<ChartAttr> = parseJson(chart.customAttr)
const tooltipAttr = customAttr.tooltip
const yAxis = chart.yAxis
if (!tooltipAttr.show) {
return {
...options,
tooltip: false
}
}
const formatterMap = tooltipAttr.seriesTooltipFormatter
?.filter(i => i.show)
.reduce((pre, next) => {
pre[next.id] = next
return pre
}, {}) as Record<string, SeriesFormatter>
const tooltipOptions: ViewSpec = {
tooltip: d => d,
interaction: {
tooltip: {
mount: createTooltipWrapper(chart),
css: tooltipCss(tooltipAttr),
enterable: true,
bounding: {
x: 0,
y: 0
},
position: 'top-right',
render: (_, { title, items: originalItems }) => {
const titleHtml = TOOLTIP_TITLE_TPL.replace('{title}', title)
let tooltipItems = originalItems
if (tooltipAttr.seriesTooltipFormatter?.length) {
tooltipItems = originalItems.filter(item => formatterMap[item.quotaList[0].id])
}
const result = []
const head = originalItems[0]
tooltipItems.forEach(item => {
const formatter = formatterMap[item.quotaList[0].id] ?? yAxis[0]
const value = valueFormatter(item.value, formatter.formatterCfg)
const name = isEmpty(formatter.chartShowName)
? formatter.name
: formatter.chartShowName
result.push({ ...item, name, value })
})
head.dynamicTooltipValue?.forEach(item => {
const formatter = formatterMap[item.fieldId]
if (formatter) {
const value = valueFormatter(parseFloat(item.value), formatter.formatterCfg)
const name = isEmpty(formatter.chartShowName)
? formatter.name
: formatter.chartShowName
result.push({ color: 'grey', name, value })
}
})
const itemsHtml = result
.map(item => {
const marker = item.color
const label = item.name
const value = item.value
return TOOLTIP_ITEM_TPL.replace('{marker}', marker)
.replace('{label}', label)
.replace('{value}', value)
})
.join('')
const listHtml = `<ul class="g2-tooltip-list" style="margin: 0px; list-style-type: none; padding: 0px;">${itemsHtml}</ul>`
return `${titleHtml}${listHtml}`
}
}
}
}
defaultsDeep(options.children[0], tooltipOptions)
return options
}
@@ -300,7 +378,9 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
navPageNumFill: legendColor,
navButtonSize: legendSize,
navOrientation:
position === 'left' || position === 'right' ? 'vertical' : 'horizontal'
position === 'left' || position === 'right' ? 'vertical' : 'horizontal',
maxRows: 1,
navControllerSpacing: 20
}
}
} else {

View File

@@ -1,5 +1,5 @@
import { parseJson } from '@/views/chart/components/js/util'
import { G2Spec } from '@antv/g2'
import { Chart as G2Chart, G2Spec } from '@antv/g2'
export type ViewSpec = { children?: G2Spec[]; [key: string]: any } & G2Spec
export type Transform = {
@@ -182,3 +182,67 @@ function handleIgnoreData(data: Record<string, any>[]) {
}
}
}
export function tooltipWrapperId(container: string) {
return 'G2-TOOLTIP-WRAPPER-' + container
}
export function createTooltipWrapper(chart: Chart) {
const wrapperId = tooltipWrapperId(chart.container)
let g2TooltipWrapper = document.getElementById(wrapperId)
if (!g2TooltipWrapper) {
g2TooltipWrapper = document.createElement('div')
g2TooltipWrapper.id = wrapperId
g2TooltipWrapper.style.position = 'absolute'
g2TooltipWrapper.style.pointerEvents = 'none'
g2TooltipWrapper.style.zIndex = '9999'
g2TooltipWrapper.style.top = '0px'
document.body.appendChild(g2TooltipWrapper)
}
return g2TooltipWrapper
}
export function tooltipCss(tooltipAttr: DeepPartial<ChartTooltipAttr>) {
return {
'.g2-tooltip': {
background: tooltipAttr.backgroundColor,
'max-height': '50vh',
'overflow-y': 'auto',
position: 'fixed',
top: '0px'
},
'.g2-tooltip-title': {
color: tooltipAttr.color,
'font-size': `${tooltipAttr.fontSize}px`
},
'.g2-tooltip-list-item-name-label': {
color: tooltipAttr.color,
'font-size': `${tooltipAttr.fontSize}px`
},
'.g2-tooltip-list-item-value': {
color: tooltipAttr.color,
'font-size': `${tooltipAttr.fontSize}px`
}
}
}
export function configTooltip(newChart: G2Chart, chart: Chart) {
newChart.on('tooltip:show', event => {
const tooltipWrapper = document.getElementById(tooltipWrapperId(chart.container))
const allTooltips = tooltipWrapper?.querySelectorAll('.g2-tooltip')
if (!allTooltips) return
allTooltips.forEach(item => {
const tooltip = item as HTMLElement
const tooltipMouseleave = () => {
tooltip.style.visibility = 'hidden'
}
tooltip.removeEventListener('mouseleave', tooltipMouseleave)
tooltip.addEventListener('mouseleave', tooltipMouseleave)
if (event.client.y < tooltip.getBoundingClientRect().height) {
tooltip.style.top = '0px'
} else {
tooltip.style.top = `${event.client.y - tooltip.getBoundingClientRect().height}px`
}
})
})
}

View File

@@ -4,8 +4,18 @@ import {
} from '@/views/chart/components/js/panel/charts/g2/bar/common'
import { flow, parseJson } from '@/views/chart/components/js/util'
import { StackBar } from '@/views/chart/components/js/panel/charts/g2/bar/stack-bar'
import { Transform, ViewSpec } from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
import {
createTooltipWrapper,
tooltipCss,
Transform,
ViewSpec
} from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
import { valueFormatter } from '@/views/chart/components/js/formatter'
import {
TOOLTIP_ITEM_TPL,
TOOLTIP_TITLE_TPL
} from '@/views/chart/components/js/panel/common/common_antv'
import { defaultsDeep, isEmpty } from 'lodash-es'
/**
* 分组堆叠柱状图
@@ -52,6 +62,61 @@ export class GroupStackBar extends StackBar {
]
}
}
protected configTooltip(chart: Chart, options: ViewSpec): ViewSpec {
const { tooltip } = parseJson(chart.customAttr)
if (!tooltip.show) {
return {
...options,
tooltip: false
}
}
const tooltipMap = function (a) {
return a
}
tooltipMap.title = undefined
const tooltipOptions: ViewSpec = {
tooltip: tooltipMap,
interaction: {
tooltip: {
mount: createTooltipWrapper(chart),
css: tooltipCss(tooltip),
enterable: true,
bounding: {
x: 0,
y: 0
},
position: 'top-right',
render: (_, { title, items: originalItems }) => {
const titleHtml = TOOLTIP_TITLE_TPL.replace('{title}', title)
const tooltipItems = originalItems
const result = []
tooltipItems.forEach(item => {
const value = valueFormatter(item.value, tooltip.tooltipFormatter)
const name = `${isEmpty(item.category) ? item.field : item.category}${
item.group ? '-' + item.group : ''
}`
result.push({ ...item, name, value })
})
const itemsHtml = result
.map(item => {
const marker = item.color
const label = item.name
const value = item.value
return TOOLTIP_ITEM_TPL.replace('{marker}', marker)
.replace('{label}', label)
.replace('{value}', value)
})
.join('')
const listHtml = `<ul class="g2-tooltip-list" style="margin: 0px; list-style-type: none; padding: 0px;">${itemsHtml}</ul>`
return `${titleHtml}${listHtml}`
}
}
}
}
defaultsDeep(options.children[0], tooltipOptions)
return options
}
protected setupOptions(chart: Chart, options: ViewSpec): ViewSpec {
return flow(

View File

@@ -0,0 +1,159 @@
import { BAR_AXIS_TYPE } from '@/views/chart/components/js/panel/charts/g2/bar/common'
import { flow, parseJson } from '@/views/chart/components/js/util'
import {
createTooltipWrapper,
tooltipCss,
ViewSpec
} from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
import { GroupStackBar } from '@/views/chart/components/js/panel/charts/g2/bar/group-stack-bar'
import {
TOOLTIP_ITEM_TPL,
TOOLTIP_TITLE_TPL
} from '@/views/chart/components/js/panel/common/common_antv'
import { defaultsDeep, isEmpty } from 'lodash-es'
/**
* 百分比堆叠柱状图
*/
export class PercentageStackBar extends GroupStackBar {
propertyInner = {
...this['propertyInner'],
'label-selector': ['color', 'fontSize', 'vPosition', 'reserveDecimalCount'],
'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'show', 'carousel']
}
protected configLabel(chart: Chart, options: ViewSpec): ViewSpec {
const customAttr = parseJson(chart.customAttr)
const { label: labelAttr } = customAttr
if (!labelAttr || !labelAttr.show) return options
const { children } = options
const position = {
position: labelAttr.position === 'middle' ? 'inside' : labelAttr.position,
textAlign: 'center',
dy: labelAttr.position === 'top' ? -10 : 0,
dx: 0
}
const transform = labelAttr.fullDisplay
? {}
: { transform: [{ type: 'exceedAdjust' }, { type: 'overlapHide' }] }
const label = {
text: 'value',
fillOpacity: 1,
fill: labelAttr.color,
fontSize: labelAttr.fontSize,
...position,
formatter: (value, _data, _, o) => {
// 计算与当前数据相同 field 的 value 总和
const sum =
o?.reduce(
(acc, item) => (item.field === _data.field ? acc + (item.value || 0) : acc),
0
) || 1
// 返回百分比格式化结果
return `${((value / sum) * 100).toFixed(labelAttr.reserveDecimalCount)}%`
},
...transform
}
return {
...options,
children: [
{
...children[0],
labels: [label]
},
...children.slice(1)
]
}
}
protected configTooltip(chart: Chart, options: ViewSpec): ViewSpec {
const { tooltip } = parseJson(chart.customAttr)
if (!tooltip.show) {
return {
...options,
tooltip: false
}
}
const tooltipMap = function (a) {
return a
}
tooltipMap.title = undefined
const tooltipOptions: ViewSpec = {
tooltip: tooltipMap,
interaction: {
tooltip: {
mount: createTooltipWrapper(chart),
css: tooltipCss(tooltip),
enterable: true,
bounding: {
x: 0,
y: 0
},
position: 'top-right',
render: (_, { title, items: originalItems }) => {
const titleHtml = TOOLTIP_TITLE_TPL.replace('{title}', title)
const tooltipItems = originalItems
const sum = tooltipItems?.reduce(
(acc, { value = 0 }: { value: number }) => acc + value,
0
)
const result = []
tooltipItems.forEach(item => {
const itemValue = item.value ? (item.value as number) : 0
const value = `${((itemValue / sum) * 100).toFixed(
tooltip.tooltipFormatter.decimalCount
)}%`
const name = `${isEmpty(item.category) ? item.field : item.category}${
item.group ? '-' + item.group : ''
}`
result.push({ ...item, name, value })
})
const itemsHtml = result
.map(item => {
const marker = item.color
const label = item.name
const value = item.value
return TOOLTIP_ITEM_TPL.replace('{marker}', marker)
.replace('{label}', label)
.replace('{value}', value)
})
.join('')
const listHtml = `<ul class="g2-tooltip-list" style="margin: 0px; list-style-type: none; padding: 0px;">${itemsHtml}</ul>`
return `${titleHtml}${listHtml}`
}
}
}
}
defaultsDeep(options.children[0], tooltipOptions)
return options
}
protected setupOptions(chart: Chart, options: ViewSpec): ViewSpec {
return flow(
this.configTheme,
this.configEmptyDataStrategy,
this.configColor,
this.configBasicStyle,
this.configLabel,
this.configTooltip,
this.configLegend,
this.configXAxis,
this.configYAxis,
this.configAnalyse,
this.configBarConditions
)(chart, options, {}, this)
}
constructor(name = 'percentage-bar-stack') {
super(name)
this.intervalOptions.encode = {
...this.intervalOptions.encode,
series: d => d.group
}
this.intervalOptions.transform = [{ type: 'stackY' }, { type: 'normalizeY' }]
this.axis = [...BAR_AXIS_TYPE, 'extStack']
}
}

View File

@@ -7,9 +7,16 @@ import { Bar } from '@/views/chart/components/js/panel/charts/g2/bar/bar'
import { formatterItem, valueFormatter } from '@/views/chart/components/js/formatter'
import { groupBy } from 'lodash'
import {
createTooltipWrapper,
handleEmptyDataStrategy,
tooltipCss,
ViewSpec
} from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
import {
TOOLTIP_ITEM_TPL,
TOOLTIP_TITLE_TPL
} from '@/views/chart/components/js/panel/common/common_antv'
import { defaultsDeep, isEmpty } from 'lodash-es'
/**
* 堆叠柱状图
@@ -98,7 +105,57 @@ export class StackBar extends Bar {
}
}
protected configTooltip(_chart: Chart, options: ViewSpec): ViewSpec {
protected configTooltip(chart: Chart, options: ViewSpec): ViewSpec {
const { tooltip } = parseJson(chart.customAttr)
if (!tooltip.show) {
return {
...options,
tooltip: false
}
}
const tooltipMap = function (a) {
return a
}
tooltipMap.title = undefined
const tooltipOptions: ViewSpec = {
tooltip: tooltipMap,
interaction: {
tooltip: {
mount: createTooltipWrapper(chart),
css: tooltipCss(tooltip),
enterable: true,
bounding: {
x: 0,
y: 0
},
position: 'top-right',
render: (_, { title, items: originalItems }) => {
const titleHtml = TOOLTIP_TITLE_TPL.replace('{title}', title)
const tooltipItems = originalItems
const result = []
tooltipItems.forEach(item => {
const value = valueFormatter(item.value, tooltip.tooltipFormatter)
const name = isEmpty(item.category) ? item.field : item.category
result.push({ ...item, name, value })
})
const itemsHtml = result
.map(item => {
const marker = item.color
const label = item.name
const value = item.value
return TOOLTIP_ITEM_TPL.replace('{marker}', marker)
.replace('{label}', label)
.replace('{value}', value)
})
.join('')
const listHtml = `<ul class="g2-tooltip-list" style="margin: 0px; list-style-type: none; padding: 0px;">${itemsHtml}</ul>`
return `${titleHtml}${listHtml}`
}
}
}
}
defaultsDeep(options.children[0], tooltipOptions)
return options
}