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

This commit is contained in:
jianneng-fit2cloud
2025-05-12 10:26:42 +08:00
committed by jianneng-fit2cloud
parent d2fa2b724f
commit bb4d98e4a4
3 changed files with 352 additions and 4 deletions

View File

@@ -1,4 +1,4 @@
import { Chart as G2Column, G2Spec } from '@antv/g2'
import { Chart as G2Column } from '@antv/g2'
import { G2ChartView, G2DrawOptions } from '@/views/chart/components/js/panel/types/impl/g2'
import {
BAR_AXIS_TYPE,
@@ -15,10 +15,10 @@ import {
DEFAULT_YAXIS_STYLE
} from '@/views/chart/components/editor/util/chart'
import _ from 'lodash'
import { ViewSpec } from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
const { t } = useI18n()
const DEFAULT_DATA: any[] = []
export type ViewSpec = { children?: G2Spec[] } & G2Spec
/**
* 柱状图
@@ -74,6 +74,7 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
shared: true
}
},
transform: [{ type: 'dodgeX' }],
data: []
}
@@ -89,7 +90,7 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
children: [
{
...this.intervalOptions,
transform: [{ type: 'dodgeX' }],
transform: [].concat(this.intervalOptions.transform),
data
}
]
@@ -531,7 +532,6 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
item['conditionColor'] = []
const quotaList = item.quotaList.map(q => q.id) ?? []
quotaList.forEach(q => {
// 定义后,在 handleConditionsStyle 函数中使用
let currentValue = item['value']
if (chart.type === 'progress-bar') {
currentValue = item['originalValue']

View File

@@ -0,0 +1,180 @@
import { parseJson } from '@/views/chart/components/js/util'
import { G2Spec } from '@antv/g2'
export type ViewSpec = { children?: G2Spec[] } & G2Spec
export function handleEmptyDataStrategy<O extends ViewSpec>(chart: Chart, options: O): O {
const { data } = options.children[0]
const isChartMix = chart.type.includes('chart-mix')
if (!data?.length) {
return options
}
const strategy = parseJson(chart.senior).functionCfg.emptyDataStrategy
if (strategy === 'ignoreData') {
if (isChartMix) {
for (let i = 0; i < data.length; i++) {
handleIgnoreData(data[i] as Record<string, any>[])
}
} else {
handleIgnoreData(data)
}
return options
}
const { yAxis, xAxisExt, extStack, extBubble } = chart
const multiDimension = yAxis?.length >= 2 || xAxisExt?.length > 0 || extStack?.length > 0
switch (strategy) {
case 'breakLine': {
if (isChartMix) {
if (data[0]) {
if (xAxisExt?.length > 0 || extStack?.length > 0) {
handleBreakLineMultiDimension(data[0] as Record<string, any>[])
}
}
if (data[1]) {
if (extBubble?.length > 0) {
handleBreakLineMultiDimension(data[1] as Record<string, any>[])
}
}
} else {
if (multiDimension) {
handleBreakLineMultiDimension(data)
}
}
return {
...options,
connectNulls: false
}
}
case 'setZero': {
if (isChartMix) {
if (data[0]) {
if (xAxisExt?.length > 0 || extStack?.length > 0) {
handleSetZeroMultiDimension(data[0] as Record<string, any>[])
} else {
handleSetZeroSingleDimension(data[0] as Record<string, any>[])
}
}
if (data[1]) {
if (extBubble?.length > 0) {
handleSetZeroMultiDimension(data[1] as Record<string, any>[], true)
} else {
handleSetZeroSingleDimension(data[1] as Record<string, any>[], true)
}
}
} else {
if (multiDimension) {
// 多维度置0
handleSetZeroMultiDimension(data)
} else {
// 单维度置0
handleSetZeroSingleDimension(data)
}
}
break
}
}
return options
}
function handleBreakLineMultiDimension(data) {
const dimensionInfoMap = new Map()
const subDimensionSet = new Set()
const quotaMap = new Map<string, { id: string }[]>()
for (let i = 0; i < data.length; i++) {
const item = data[i]
const dimensionInfo = dimensionInfoMap.get(item.field)
if (dimensionInfo) {
dimensionInfo.set.add(item.category)
} else {
dimensionInfoMap.set(item.field, { set: new Set([item.category]), index: i })
}
subDimensionSet.add(item.category)
quotaMap.set(item.category, item.quotaList)
}
// Map 是按照插入顺序排序的,所以插入索引往后推
let insertCount = 0
dimensionInfoMap.forEach((dimensionInfo, field) => {
if (dimensionInfo.set.size < subDimensionSet.size) {
let subInsertIndex = 0
subDimensionSet.forEach(dimension => {
if (!dimensionInfo.set.has(dimension)) {
data.splice(dimensionInfo.index + insertCount + subInsertIndex, 0, {
field,
value: null,
category: dimension,
quotaList: quotaMap.get(dimension as string)
})
}
subInsertIndex++
})
insertCount += subDimensionSet.size - dimensionInfo.set.size
}
})
}
function handleSetZeroMultiDimension(data: Record<string, any>[], isExt = false) {
const dimensionInfoMap = new Map()
const subDimensionSet = new Set()
const quotaMap = new Map<string, { id: string }[]>()
for (let i = 0; i < data.length; i++) {
const item = data[i]
if (item.value === null) {
item.value = 0
if (isExt) {
item.valueExt = 0
}
}
const dimensionInfo = dimensionInfoMap.get(item.field)
if (dimensionInfo) {
dimensionInfo.set.add(item.category)
} else {
dimensionInfoMap.set(item.field, { set: new Set([item.category]), index: i })
}
subDimensionSet.add(item.category)
quotaMap.set(item.category, item.quotaList)
}
let insertCount = 0
dimensionInfoMap.forEach((dimensionInfo, field) => {
if (dimensionInfo.set.size < subDimensionSet.size) {
let subInsertIndex = 0
subDimensionSet.forEach(dimension => {
if (!dimensionInfo.set.has(dimension)) {
const _temp = {
field,
value: 0,
category: dimension,
quotaList: quotaMap.get(dimension as string)
} as any
if (isExt) {
_temp.valueExt = 0
}
data.splice(dimensionInfo.index + insertCount + subInsertIndex, 0, _temp)
}
subInsertIndex++
})
insertCount += subDimensionSet.size - dimensionInfo.set.size
}
})
}
function handleSetZeroSingleDimension(data: Record<string, any>[], isExt = false) {
data.forEach(item => {
if (item.value === null) {
if (!isExt) {
item.value = 0
} else {
item.valueExt = 0
}
}
})
}
function handleIgnoreData(data: Record<string, any>[]) {
for (let i = data.length - 1; i >= 0; i--) {
const item = data[i]
if (item.value === null) {
data.splice(i, 1)
}
}
}

View File

@@ -0,0 +1,168 @@
import {
BAR_EDITOR_PROPERTY,
BAR_EDITOR_PROPERTY_INNER
} from '@/views/chart/components/js/panel/charts/g2/bar/common'
import { flow, parseJson, setUpStackSeriesColor } from '@/views/chart/components/js/util'
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 {
handleEmptyDataStrategy,
ViewSpec
} from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
/**
* 堆叠柱状图
*/
export class StackBar extends Bar {
properties = BAR_EDITOR_PROPERTY.filter(ele => ele !== 'threshold')
propertyInner = {
...this['propertyInner'],
'label-selector': [
...BAR_EDITOR_PROPERTY_INNER['label-selector'],
'vPosition',
'showTotal',
'totalColor',
'totalFontSize',
'totalFormatter',
'showStackQuota'
],
'tooltip-selector': [
'fontSize',
'color',
'backgroundColor',
'tooltipFormatter',
'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 labels = []
if (labelAttr.showStackQuota ?? true) {
labels.push({
text: 'value',
fillOpacity: 1,
fill: labelAttr.color,
fontSize: labelAttr.fontSize,
...position,
formatter: (value, _data) => valueFormatter(value, labelAttr.labelFormatter),
...transform
})
}
if (labelAttr.showTotal) {
const formatterCfg = labelAttr.labelFormatter ?? formatterItem
const groupedData = groupBy(children[0].data.value, 'field')
for (const [key, values] of Object.entries(groupedData)) {
const total = values.reduce((a, b) => a + b.value, 0)
const value = valueFormatter(total, formatterCfg)
children.push({
type: 'text',
data: [key, total],
style: {
text: value,
textAlign: 'center',
dy: -10,
fill: labelAttr.color,
fontSize: labelAttr.fontSize
},
tooltip: false
})
}
}
return {
...options,
children: [
{
...children[0],
labels: labels
},
...children.slice(1)
]
}
}
protected configTooltip(_chart: Chart, options: ViewSpec): ViewSpec {
return options
}
protected configColor(_chart: Chart, options: ViewSpec): ViewSpec {
return options
}
protected configData(chart: Chart, options: ViewSpec): ViewSpec {
const { xAxis, extStack, yAxis } = chart
const mainSort = xAxis.some(axis => axis.sort !== 'none')
const subSort = extStack.some(axis => axis.sort !== 'none')
if (mainSort || subSort) {
return options
}
const quotaSort = yAxis?.[0].sort !== 'none'
if (!quotaSort || !extStack.length || !yAxis.length) {
return options
}
const { data } = options.children[0]
const mainAxisValueMap = data.reduce((p, n) => {
p[n.field] = p[n.field] ? p[n.field] + n.value : n.value || 0
return p
}, {})
const sort = yAxis[0].sort
data.sort((p, n) => {
if (sort === 'asc') {
return mainAxisValueMap[p.field] - mainAxisValueMap[n.field]
} else {
return mainAxisValueMap[n.field] - mainAxisValueMap[p.field]
}
})
return options
}
protected configEmptyDataStrategy(chart: Chart, options: ViewSpec): ViewSpec {
const { data } = options.children[0]
if (!data?.length) return options
handleEmptyDataStrategy(chart, options)
return options
}
public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
return setUpStackSeriesColor(chart, data)
}
protected setupOptions(chart: Chart, options: ViewSpec): ViewSpec {
return flow(
this.configTheme,
this.configEmptyDataStrategy,
this.configData,
this.configColor,
this.configBasicStyle,
this.configLabel,
this.configTooltip,
this.configLegend,
this.configXAxis,
this.configYAxis,
this.configAnalyse
)(chart, options, {}, this)
}
constructor(name = 'bar-stack') {
super(name)
this.intervalOptions.transform = [{ type: 'stackY' }]
this.axis = [...this.axis, 'extStack']
}
}