diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/g2/bar/bar.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/g2/bar/bar.ts index e413d62652..ed269d15d9 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/g2/bar/bar.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/g2/bar/bar.ts @@ -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 { shared: true } }, + transform: [{ type: 'dodgeX' }], data: [] } @@ -89,7 +90,7 @@ export class Bar extends G2ChartView { children: [ { ...this.intervalOptions, - transform: [{ type: 'dodgeX' }], + transform: [].concat(this.intervalOptions.transform), data } ] @@ -531,7 +532,6 @@ export class Bar extends G2ChartView { 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'] diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/g2/bar/barUtil.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/g2/bar/barUtil.ts new file mode 100644 index 0000000000..e472bba2ad --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/g2/bar/barUtil.ts @@ -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(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[]) + } + } 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[]) + } + } + if (data[1]) { + if (extBubble?.length > 0) { + handleBreakLineMultiDimension(data[1] as Record[]) + } + } + } 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[]) + } else { + handleSetZeroSingleDimension(data[0] as Record[]) + } + } + if (data[1]) { + if (extBubble?.length > 0) { + handleSetZeroMultiDimension(data[1] as Record[], true) + } else { + handleSetZeroSingleDimension(data[1] as Record[], 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() + 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[], isExt = false) { + const dimensionInfoMap = new Map() + const subDimensionSet = new Set() + const quotaMap = new Map() + 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[], isExt = false) { + data.forEach(item => { + if (item.value === null) { + if (!isExt) { + item.value = 0 + } else { + item.valueExt = 0 + } + } + }) +} + +function handleIgnoreData(data: Record[]) { + for (let i = data.length - 1; i >= 0; i--) { + const item = data[i] + if (item.value === null) { + data.splice(i, 1) + } + } +} diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/g2/bar/stack-bar.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/g2/bar/stack-bar.ts new file mode 100644 index 0000000000..872e371674 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/g2/bar/stack-bar.ts @@ -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'] + } +}