mirror of
https://github.com/dataease/dataease.git
synced 2026-06-16 19:33:11 +08:00
feat(图表): 支持堆叠柱状图
This commit is contained in:
committed by
jianneng-fit2cloud
parent
d2fa2b724f
commit
bb4d98e4a4
@@ -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']
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user