feat(图表): 汇总表支持表头分组 #16302

This commit is contained in:
wisonic-s
2025-06-24 14:52:07 +08:00
committed by GitHub
parent ccf758f230
commit 70e51002cb
6 changed files with 127 additions and 86 deletions

View File

@@ -347,7 +347,7 @@ public class ChartDataServer implements ChartDataApi {
xAxis.addAll(viewInfo.getDrillFields());
TableHeader tableHeader = null;
Integer totalDepth = 0;
if (viewInfo.getType().equalsIgnoreCase("table-normal") || viewInfo.getType().equalsIgnoreCase("table-info")) {
if (StringUtils.equalsAnyIgnoreCase(viewInfo.getType(), "table-normal", "table-info")) {
for (ChartViewFieldDTO xAxi : xAxis) {
if (xAxi.getDeType().equals(DeTypeConstants.DE_INT) || xAxi.getDeType().equals(DeTypeConstants.DE_FLOAT)) {
CellStyle formatterCellStyle = createCellStyle(wb, xAxi.getFormatterCfg(), null);
@@ -362,7 +362,11 @@ public class ChartDataServer implements ChartDataApi {
if (tableHeaderMap.get("headerGroup") != null && Boolean.parseBoolean(tableHeaderMap.get("headerGroup").toString())) {
var tmpHeader = JsonUtil.parseObject((String) JsonUtil.toJSONString(customAttr.get("tableHeader")), TableHeader.class);
// 校验字段数量和顺序
if (validateHeaderGroup(tmpHeader, viewInfo.getXAxis())) {
var allAxis = new ArrayList<>(viewInfo.getXAxis().stream().filter(x -> !x.isHide()).toList());
if (StringUtils.equalsIgnoreCase(viewInfo.getType(), "table-normal")) {
allAxis.addAll(viewInfo.getYAxis().stream().filter(x -> !x.isHide()).toList());
}
if (validateHeaderGroup(tmpHeader, allAxis)) {
tableHeader = tmpHeader;
for (TableHeader.ColumnInfo column : tableHeader.getHeaderGroupConfig().getColumns()) {
totalDepth = Math.max(totalDepth, getDepth(column, 1));

View File

@@ -52,22 +52,27 @@ const emits = defineEmits(['onConfigChange', 'onCancelConfig'])
const onCancelConfig = () => {
emits('onCancelConfig')
}
const allAxis = computed(() => {
const axis = [...props.chart.xAxis]
if (props.chart.type === 'table-normal') {
axis.push(...props.chart.yAxis)
}
return axis
})
const onConfigChange = () => {
const allAxis = props.chart.xAxis
const showAxis = allAxis.value
?.map(axis => axis.hide !== true && axis.dataeaseName)
.filter(i => i)
const { fields, meta } = s2.dataCfg
const groupMeta = meta.filter(item => !allAxis.includes(item.field))
const groupMeta = meta.filter(item => !showAxis.includes(item.field))
emits('onConfigChange', { columns: fields.columns, meta: groupMeta })
}
const init = () => {
const chart = cloneDeep(props.chart)
const xAxis = chart.xAxis
const { headerGroupConfig } = chart.customAttr.tableHeader
const showColumns = []
xAxis?.forEach(axis => {
allAxis.value?.forEach(axis => {
axis.hide !== true && showColumns.push({ key: axis.dataeaseName })
})
if (!showColumns.length) {
@@ -109,7 +114,7 @@ const renderTable = (chart: ChartObj) => {
const { headerGroupConfig } = chart.customAttr.tableHeader
const meta = [...headerGroupConfig.meta]
const columns = headerGroupConfig.columns
const axisMap = chart.xAxis.reduce((pre, cur) => {
const axisMap = allAxis.value.reduce((pre, cur) => {
pre[cur.dataeaseName] = cur
return pre
}, {})
@@ -141,7 +146,7 @@ const renderTable = (chart: ChartObj) => {
})
})
} else {
chart.xAxis?.forEach(axis => {
allAxis.value?.forEach(axis => {
if (axis.hide !== true) {
meta.push({
field: axis.dataeaseName,

View File

@@ -96,18 +96,21 @@ const groupConfigValid = computed(() => {
if (noGroup) {
return false
}
const xAxis = props.chart.xAxis
const allAxis = [...props.chart?.xAxis]
if (props.chart.type === 'table-normal') {
allAxis.push(...props.chart?.yAxis)
}
const showColumns = []
xAxis?.forEach(axis => {
allAxis?.forEach(axis => {
axis.hide !== true && showColumns.push({ key: axis.dataeaseName })
})
if (!showColumns.length) {
return false
}
const allAxis = showColumns.map(item => item.key)
const showColumnFields = showColumns.map(item => item.key)
const leafNodes = getLeafNodes(columns as Array<ColumnNode>)
const leafKeys = leafNodes.map(item => item.key)
return isEqual(allAxis, leafKeys)
return isEqual(showColumnFields, leafKeys)
})
const init = () => {
const tableHeader = props.chart?.customAttr?.tableHeader

View File

@@ -27,7 +27,10 @@ import {
getColumns,
drawImage,
getSummaryRow,
SummaryCell
SummaryCell,
summaryRowStyle,
calcTreeWidth,
getStartPosition
} from '@/views/chart/components/js/panel/common/common_table'
const { t } = useI18n()
@@ -229,7 +232,7 @@ export class TableInfo extends S2ChartView<TableSheet> {
// 开始渲染
const newChart = new TableSheet(containerDom, s2DataConfig, s2Options)
// 总计紧贴在单元格后面
this.summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary)
summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary)
// 开启自动换行
if (basicStyle.autoWrap && !tableCell.mergeCells) {
// 调整表头宽度时,计算表头高度
@@ -512,54 +515,7 @@ export class TableInfo extends S2ChartView<TableSheet> {
}
}
protected summaryRowStyle(newChart: TableSheet, newData, tableCell, tableHeader, showSummary) {
if (!showSummary || !newData.length) return
const columns = newChart.dataCfg.fields.columns
const showHeader = tableHeader.showTableHeader === true
// 不显示表头时,减少一个表头的高度
const headerAndSummaryHeight = showHeader ? getMaxTreeDepth(columns) + 1 : 1
newChart.on(S2Event.LAYOUT_BEFORE_RENDER, () => {
const totalHeight =
tableHeader.tableTitleHeight * headerAndSummaryHeight +
tableCell.tableItemHeight * (newData.length - 1)
if (totalHeight < newChart.container.cfg.height) {
newChart.options.height =
totalHeight < newChart.container.cfg.height - 8 ? totalHeight + 8 : totalHeight
}
})
}
constructor() {
super('table-info', [])
}
}
function calcTreeWidth(node) {
if (!node.children?.length) {
return node.width
}
return node.children.reduce((pre, cur) => {
return pre + calcTreeWidth(cur)
}, 0)
}
function getStartPosition(node) {
if (!node.children?.length) {
return node.x
}
return getStartPosition(node.children[0])
}
function getMaxTreeDepth(nodes) {
if (!nodes?.length) {
return 0
}
return Math.max(
...nodes.map(node => {
if (!node.children?.length) {
return 1
}
return getMaxTreeDepth(node.children) + 1
})
)
}

View File

@@ -6,7 +6,12 @@ import {
CustomDataCell,
getSummaryRow,
SortTooltip,
SummaryCell
SummaryCell,
getLeafNodes,
getColumns,
summaryRowStyle,
calcTreeWidth,
getStartPosition
} from '@/views/chart/components/js/panel/common/common_table'
import { S2ChartView, S2DrawOptions } from '@/views/chart/components/js/panel/types/impl/s2'
import { parseJson } from '@/views/chart/components/js/util'
@@ -20,7 +25,7 @@ import {
TableSheet,
ViewMeta
} from '@antv/s2'
import { isNumber } from 'lodash-es'
import { isNumber, isEqual } from 'lodash-es'
import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common'
const { t } = useI18n()
@@ -34,7 +39,8 @@ export class TableNormal extends S2ChartView<TableSheet> {
'table-header-selector': [
...TABLE_EDITOR_PROPERTY_INNER['table-header-selector'],
'tableHeaderSort',
'showTableHeader'
'showTableHeader',
'headerGroup'
],
'basic-style-selector': [
...TABLE_EDITOR_PROPERTY_INNER['basic-style-selector'],
@@ -76,6 +82,7 @@ export class TableNormal extends S2ChartView<TableSheet> {
const columns = []
const meta = []
const drillFieldMap = {}
if (chart.drill) {
// 下钻过滤字段
const filterFields = chart.drillFilters.map(i => i.fieldId)
@@ -84,13 +91,14 @@ export class TableNormal extends S2ChartView<TableSheet> {
const drillFieldIndex = chart.xAxis.findIndex(ele => ele.id === drillFieldId)
// 当前下钻字段
const curDrillFieldId = chart.drillFields[filterFields.length].id
const curDrillField = fields.filter(ele => ele.id === curDrillFieldId)
const curDrillField = fields.find(ele => ele.id === curDrillFieldId)
filterFields.push(curDrillFieldId)
// 移除下钻字段,把当前下钻字段插入到下钻入口位置
fields = fields.filter(ele => {
return !filterFields.includes(ele.id)
})
fields.splice(drillFieldIndex, 0, ...curDrillField)
drillFieldMap[curDrillField.dataeaseName] = chart.drillFields[0].dataeaseName
fields.splice(drillFieldIndex, 0, curDrillField)
}
const axisMap = [...chart.xAxis, ...chart.yAxis].reduce((pre, cur) => {
pre[cur.dataeaseName] = cur
@@ -124,7 +132,27 @@ export class TableNormal extends S2ChartView<TableSheet> {
}
})
})
const { basicStyle, tableCell, tableHeader, tooltip } = parseJson(chart.customAttr)
// 表头分组
const { headerGroup, showTableHeader } = tableHeader
if (headerGroup && showTableHeader !== false) {
const { headerGroupConfig } = tableHeader
if (headerGroupConfig?.columns?.length) {
const allKeys = columns.map(c => drillFieldMap[c] || c)
const leafNodes = getLeafNodes(headerGroupConfig.columns as ColumnNode[])
const leafKeys = leafNodes.map(c => c.key)
if (isEqual(leafKeys, allKeys)) {
if (Object.keys(drillFieldMap).length) {
const originField = Object.values(drillFieldMap)[0]
const drillField = Object.keys(drillFieldMap)[0]
const [drillCol] = getColumns([originField], headerGroupConfig.columns as ColumnNode[])
drillCol.key = drillField
}
columns.splice(0, columns.length, ...headerGroupConfig.columns)
meta.push(...headerGroupConfig.meta)
}
}
}
// 空值处理
const newData = this.configEmptyDataStrategy(chart)
// data config
@@ -136,7 +164,6 @@ export class TableNormal extends S2ChartView<TableSheet> {
data: newData
}
const { basicStyle, tableCell, tableHeader, tooltip } = parseJson(chart.customAttr)
// options
const s2Options: S2Options = {
width: containerDom.getBoundingClientRect().width,
@@ -186,7 +213,7 @@ export class TableNormal extends S2ChartView<TableSheet> {
// 开始渲染
const newChart = new TableSheet(containerDom, s2DataConfig, s2Options)
// 总计紧贴在单元格后面
this.summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary)
summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary)
// 自适应铺满
if (basicStyle.tableColumnMode === 'adapt') {
newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, () => {
@@ -207,6 +234,13 @@ export class TableNormal extends S2ChartView<TableSheet> {
n.x = p
return p + n.width
}, 0)
// 处理分组的单元格,宽度为所有叶子节点之和
ev.colNodes.forEach(n => {
if (n.colIndex === -1) {
n.width = calcTreeWidth(n)
n.x = getStartPosition(n)
}
})
ev.colsHierarchy.width = totalWidth
newChart.store.set('lastLayoutResult', undefined)
return
@@ -227,6 +261,13 @@ export class TableNormal extends S2ChartView<TableSheet> {
n.x = p
return p + n.width
}, 0)
// 处理分组的单元格,宽度为所有叶子节点之和
ev.colNodes.forEach(n => {
if (n.colIndex === -1) {
n.width = calcTreeWidth(n)
n.x = getStartPosition(n)
}
})
if (totalWidth > containerWidth) {
// 从最后一列减掉
ev.colLeafNodes[ev.colLeafNodes.length - 1].width -= totalWidth - containerWidth
@@ -332,22 +373,6 @@ export class TableNormal extends S2ChartView<TableSheet> {
}
}
protected summaryRowStyle(newChart, newData, tableCell, tableHeader, showSummary) {
if (!showSummary || !newData.length) return
newChart.on(S2Event.LAYOUT_BEFORE_RENDER, () => {
const showHeader = tableHeader.showTableHeader === true
// 不显示表头时,减少一个表头的高度
const headerAndSummaryHeight = showHeader ? 2 : 1
const totalHeight =
tableHeader.tableTitleHeight * headerAndSummaryHeight +
tableCell.tableItemHeight * (newData.length - 1)
if (totalHeight < newChart.container.cfg.height) {
newChart.options.height =
totalHeight < newChart.container.cfg.height - 8 ? totalHeight + 8 : totalHeight
}
})
}
constructor() {
super('table-normal', [])
}

View File

@@ -2393,3 +2393,51 @@ export function drawImage() {
})
}
}
export function calcTreeWidth(node) {
if (!node.children?.length) {
return node.width
}
return node.children.reduce((pre, cur) => {
return pre + calcTreeWidth(cur)
}, 0)
}
export function getStartPosition(node) {
if (!node.children?.length) {
return node.x
}
return getStartPosition(node.children[0])
}
function getMaxTreeDepth(nodes) {
if (!nodes?.length) {
return 0
}
return Math.max(
...nodes.map(node => {
if (!node.children?.length) {
return 1
}
return getMaxTreeDepth(node.children) + 1
})
)
}
export function summaryRowStyle(newChart, newData, tableCell, tableHeader, showSummary) {
if (!showSummary || !newData.length) return
const columns = newChart.dataCfg.fields.columns
const showHeader = tableHeader.showTableHeader === true
// 不显示表头时,减少一个表头的高度
const headerAndSummaryHeight = showHeader ? getMaxTreeDepth(columns) + 1 : 1
newChart.on(S2Event.LAYOUT_BEFORE_RENDER, () => {
const totalHeight =
tableHeader.tableTitleHeight * headerAndSummaryHeight +
tableCell.tableItemHeight * (newData.length - 1)
if (totalHeight < newChart.container.cfg.height) {
newChart.options.height =
totalHeight < newChart.container.cfg.height - 8 ? totalHeight + 8 : totalHeight
}
})
}