feat(图表): 明细表支持字段分组

This commit is contained in:
wisonic
2025-02-21 18:42:15 +08:00
committed by wisonic-s
parent 1a0ad0e24c
commit 74aa20c14f
9 changed files with 783 additions and 9 deletions

View File

@@ -1929,6 +1929,16 @@ Scatter chart (bubble) chart: {a} (series name), {b} (data name), {c} (value arr
central_point: 'Center point',
full_display: 'Full display',
show_hover_style: 'Show mouse hover style',
table_header_group: 'Header grouping',
table_header_group_config: 'Header grouping config',
cancel_group: 'Cancel grouping',
cancel_all_group: 'Cancel all grouping',
group_name: 'Group name',
merge_group: 'Merge group',
table_header_group_config_tip:
'Field additions, deletions, positional changes, and explicit and implicit modifications can cause grouping to become invalid.',
group_name_edit_tip: 'Group names are 1-20 characters in length',
group_name_error_tip: 'Please input valid group name',
merge_cells: 'Merge cells',
length_limit: 'Length limit',
radar_point: 'Enable auxiliary points',

View File

@@ -1884,6 +1884,15 @@ export default {
central_point: '中心點',
full_display: '全量顯示',
show_hover_style: '顯示滑鼠懸浮樣式',
table_header_group: '表頭分組',
table_header_group_config: '表頭分組設置',
cancel_group: '取消分組',
cancel_all_group: '取消所有分組',
group_name: '分組名稱',
merge_group: '合併分組',
table_header_group_config_tip: '字段的增刪,位置變動,顯隱修改都會導致分組失效',
group_name_edit_tip: '分組名稱長度為 1-20 個字符',
group_name_error_tip: '請輸入正確的分組名稱',
merge_cells: '合併儲存格',
length_limit: '長度限制',
radar_point: '開啟輔助點',

View File

@@ -1894,6 +1894,15 @@ export default {
central_point: '中心点',
full_display: '全量显示',
show_hover_style: '显示鼠标悬浮样式',
table_header_group: '表头分组',
table_header_group_config: '表头分组设置',
cancel_group: '取消分组',
cancel_all_group: '取消所有分组',
group_name: '分组名称',
merge_group: '合并分组',
table_header_group_config_tip: '字段的增删,位置变动,显隐修改都会导致分组失效',
group_name_edit_tip: '分组名称长度为 1-20 个字符',
group_name_error_tip: '请输入正确的分组名称',
merge_cells: '合并单元格',
length_limit: '长度限制',
radar_point: '开启辅助点',

View File

@@ -441,6 +441,32 @@ declare interface ChartTableHeaderAttr {
isBolder: boolean
isCornerBolder: boolean
isColBolder: boolean
/**
* 表头分组开关
*/
headerGroup: boolean
/**
* 表头分组设置
*/
headerGroupConfig: {
/**
* 分组结构
*/
columns: Columns
/**
* 分组名称
*/
meta: {
/**
* 字段id
*/
field: string
/**
* 名称
*/
name: string
}[]
}
}
/**
* 单元格属性
@@ -1276,3 +1302,10 @@ declare interface ConversionTagAtt {
*/
precision: number
}
declare interface ColumnNode {
key: string
children?: Columns
}
declare type Columns = Array<string | ColumnNode>

View File

@@ -0,0 +1,522 @@
<template>
<div id="table-container"></div>
<div class="button-group">
<el-button :effect="themes" @click="onCancelConfig">{{ t('chart.cancel') }}</el-button>
<el-button type="primary" @click="onConfigChange">{{ t('chart.confirm') }}</el-button>
</div>
<div id="menu-group" class="group-menu"></div>
</template>
<script setup lang="ts">
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { formatterItem, valueFormatter } from '@/views/chart/components/js/formatter'
import {
BaseTooltip,
ColumnNode,
S2DataConfig,
S2Event,
S2Options,
TableSheet,
TooltipShowOptions
} from '@antv/s2'
import { ElMessageBox } from 'element-plus-secondary'
import { cloneDeep, isEqual, isNumber } from 'lodash-es'
import { nextTick, onMounted, PropType } from 'vue'
import { uuid } from 'vue-uuid'
import { useI18n } from '@/hooks/web/useI18n'
import {
getColumns,
getCustomTheme,
getLeafNodes
} from '@/views/chart/components/js/panel/common/common_table'
const { t } = useI18n()
const dvMainStore = dvMainStoreWithOut()
const props = defineProps({
chart: {
type: Object as PropType<ChartObj>,
required: true
},
themes: {
type: String as PropType<EditorTheme>,
default: 'dark'
},
propertyInner: {
type: Array<string>
}
})
const emits = defineEmits(['onConfigChange', 'onCancelConfig'])
const onCancelConfig = () => {
emits('onCancelConfig')
}
const onConfigChange = () => {
const allAxis = props.chart.xAxis
?.map(axis => axis.hide !== true && axis.dataeaseName)
.filter(i => i)
const { fields, meta } = s2.dataCfg
const groupMeta = meta.filter(item => !allAxis.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 => {
axis.hide !== true && showColumns.push({ key: axis.dataeaseName })
})
if (!showColumns.length) {
return
}
if (headerGroupConfig?.columns?.length) {
const allAxis = showColumns.map(item => item.key)
const leafNodes = getLeafNodes(headerGroupConfig.columns as Array<ColumnNode>)
const leafKeys = leafNodes.map(item => item.key)
if (!isEqual(allAxis, leafKeys)) {
const { columns, meta } = headerGroupConfig
columns.splice(0, columns.length, ...showColumns)
meta.splice(0, meta.length)
}
} else {
chart.customAttr.tableHeader.headerGroupConfig = {
columns: [...showColumns],
meta: []
}
}
nextTick(() => {
renderTable(chart)
})
}
let s2: TableSheet
const renderTable = (chart: ChartObj) => {
const data = dvMainStore.getViewDataDetails(chart.id)
const containerDom = document.getElementById('table-container')
let realData = []
if (data?.tableRow?.length) {
realData = data.tableRow.slice(0, 10)
}
const { headerGroupConfig } = chart.customAttr.tableHeader
const meta = [...headerGroupConfig.meta]
const columns = headerGroupConfig.columns
const axisMap = chart.xAxis.reduce((pre, cur) => {
pre[cur.dataeaseName] = cur
return pre
}, {})
if (data?.fields?.length) {
data.fields.forEach(ele => {
const f = axisMap[ele.dataeaseName]
if (f?.hide === true) {
return
}
meta.push({
field: ele.dataeaseName,
name: ele.chartShowName ?? ele.name,
formatter: function (value) {
if (!f) {
return value
}
if (value === null || value === undefined) {
return value
}
if (![2, 3].includes(f.deType) || !isNumber(value)) {
return value
}
let formatCfg = f.formatterCfg
if (!formatCfg) {
formatCfg = formatterItem
}
return valueFormatter(value, formatCfg)
}
})
})
} else {
chart.xAxis?.forEach(axis => {
if (axis.hide !== true) {
meta.push({
field: axis.dataeaseName,
name: axis.chartShowName ?? axis.name
})
}
})
}
// // data config
const s2DataConfig: S2DataConfig = {
fields: {
columns
},
meta,
data: realData
}
// options
const s2Options: S2Options = {
width: containerDom.getBoundingClientRect().width,
height: containerDom.offsetHeight,
tooltip: {
getContainer: () => containerDom,
renderTooltip: sheet => new GroupMenu(sheet),
style: {
position: 'absolute',
borderRadius: '4px'
}
}
}
s2 = new TableSheet(containerDom, s2DataConfig, s2Options)
const theme = getCustomTheme(chart)
s2.setTheme(theme)
const groupMenuContainer = document.getElementById('menu-group')
s2.on(S2Event.COL_CELL_CONTEXT_MENU, e => {
e.preventDefault()
const curColumns = s2.dataCfg.fields.columns as Array<ColumnNode>
const curMeta = s2.dataCfg.meta
const activeCells = s2.interaction.getActiveCells()
const colKeys = activeCells?.map(cell => cell.getMeta().field)
const activeColumns = getColumns(colKeys, curColumns)
const curCell = s2.getCell(e.target)
groupMenuContainer.innerText = ''
// 右键点击的目标单元格不在已选的单元格中,清空已选单元格,隐藏菜单
if (activeColumns?.length) {
const index = activeColumns.findIndex(cell => cell.key === curCell.getMeta().field)
if (index === -1) {
s2.interaction.clearState()
s2.hideTooltip()
return
}
}
//只有一个cell并且colIndex为-1那就是组合的显示取消分组按钮和重命名按钮
if (activeColumns?.length === 1 && curCell.getMeta().colIndex === -1) {
const cancelBtn = document.createElement('span')
groupMenuContainer.appendChild(cancelBtn)
cancelBtn.innerText = t('chart.cancel_group')
cancelBtn.onclick = () => {
s2.hideTooltip()
const parent = curCell.getMeta().parent
if (parent?.id === 'root') {
const startIndex = curColumns.findIndex(cell => cell.key === curCell.getMeta().field)
const [curCol] = getColumns([curCell.getMeta().field], curColumns)
curColumns.splice(startIndex, 1, ...curCol.children)
const index = curMeta.findIndex(meta => meta.field === curCell.getMeta().field)
curMeta.splice(index, 1)
s2.setDataCfg({
fields: {
columns: curColumns
},
meta: curMeta
})
s2.render(true)
} else {
const [parentColumn] = getColumns([parent.field], curColumns)
if (parentColumn) {
const startIndex = parentColumn.children?.findIndex(
cell => cell.key === curCell.getMeta().field
)
const [curCol] = getColumns([curCell.getMeta().field], parentColumn.children)
parentColumn.children?.splice(startIndex, 1, ...curCol.children)
const index = curMeta.findIndex(meta => meta.field === curCell.getMeta().field)
curMeta.splice(index, 1)
s2.setDataCfg({
fields: {
columns: curColumns
},
meta: curMeta
})
s2.render(true)
}
}
s2.interaction.clearState()
}
const cancelAllBtn = document.createElement('span')
groupMenuContainer.appendChild(cancelAllBtn)
cancelAllBtn.innerText = t('chart.cancel_all_group')
cancelAllBtn.onclick = () => {
s2.hideTooltip()
const parent = curCell.getMeta().parent
if (parent?.id === 'root') {
const [curCol] = getColumns([curCell.getMeta().field], curColumns)
const leafNodes = getLeafNodes(curCol.children)
const startIndex = curColumns.findIndex(cell => cell.key === curCell.getMeta().field)
curColumns.splice(startIndex, 1, ...leafNodes)
const noneLeafNodes = getNonLeafNodes([curCol])
const newMeta = curMeta.filter(meta => !noneLeafNodes.includes(meta.field))
s2.setDataCfg({
fields: {
columns: curColumns
},
meta: newMeta
})
s2.render(true)
} else {
const [parentColumn] = getColumns([parent.field], curColumns)
if (parentColumn) {
const [curCol] = getColumns([curCell.getMeta().field], parentColumn.children)
const leafNodes = getLeafNodes(curCol.children)
const startIndex = parentColumn.children?.findIndex(
cell => cell.key === curCell.getMeta().field
)
parentColumn.children?.splice(startIndex, 1, ...leafNodes)
const noneLeafNodes = getNonLeafNodes([curCol])
const newMeta = curMeta.filter(meta => !noneLeafNodes.includes(meta.field))
s2.setDataCfg({
fields: {
columns: curColumns
},
meta: newMeta
})
s2.render(true)
}
}
s2.interaction.clearState()
}
const renameBtn = document.createElement('span')
groupMenuContainer.appendChild(renameBtn)
renameBtn.innerText = t('chart.rename')
renameBtn.onclick = () => {
s2.hideTooltip()
const cellMeta = curMeta.find(meta => meta.field === curCell.getMeta().field)
ElMessageBox.prompt('', t('chart.group_name'), {
confirmButtonText: t('chart.confirm'),
cancelButtonText: t('chart.cancel'),
showClose: false,
showInput: true,
inputPlaceholder: t('chart.group_name_edit_tip'),
inputValue: cellMeta.name,
inputErrorMessage: t('chart.group_name_error_tip'),
// 正则校验,长度 1-20
inputValidator: val => {
if (val?.length < 1 || val?.length > 20) {
return t('chart.group_name_error_tip')
}
return true
}
})
.then(res => {
cellMeta.name = res.value
s2.setDataCfg({
meta: curMeta
})
s2.render(true)
})
.catch(() => {
// do nothing
})
}
s2.showTooltip({
position: {
x: e.x,
y: e.y
},
content: groupMenuContainer
})
return
}
//如果有多个cell都在同一个层级并且parent相同那就是可以进行合并分组操作
if (activeColumns?.length > 1) {
const sameParent = activeCells.every(
cell => cell.getMeta().parent === curCell.getMeta().parent
)
if (!sameParent) {
return
}
let upDepth = -1
let tmpCell = curCell
while (tmpCell?.getMeta?.()?.parent || tmpCell?.parent) {
upDepth++
tmpCell = tmpCell?.getMeta?.()?.parent || tmpCell?.parent
}
let startIndex = -1
let endIndex = -1
const parent = curCell.getMeta().parent
// 分组的节点
if (parent.colIndex !== -1) {
activeColumns.forEach(cell => {
const index = parent.children.findIndex(item => item.getMeta().field === cell.key)
if (index < startIndex || startIndex === -1) {
startIndex = index
}
if (index > endIndex || endIndex === -1) {
endIndex = index
}
})
} else {
activeColumns.forEach(cell => {
const index = parent.children.findIndex(item => item.key === cell.key)
if (index < startIndex || startIndex === -1) {
startIndex = index
}
if (index > endIndex || endIndex === -1) {
endIndex = index
}
})
}
const totalColumns = []
if (parent?.id === 'root') {
totalColumns.push(...curColumns.slice(startIndex, endIndex + 1))
} else {
const [parentColumn] = getColumns([parent.field], curColumns)
totalColumns.push(...parentColumn.children?.slice(startIndex, endIndex + 1))
}
const chiildDepth = getTreesMaxDepth(totalColumns)
// 最大分组为 3 级
if (chiildDepth + upDepth > 1) {
return
}
const mergeBtn = document.createElement('span')
groupMenuContainer.appendChild(mergeBtn)
mergeBtn.innerText = t('chart.merge_group')
mergeBtn.onclick = () => {
s2.hideTooltip()
ElMessageBox.prompt('', t('chart.group_name'), {
confirmButtonText: t('chart.confirm'),
cancelButtonText: t('chart.cancel'),
showClose: false,
showInput: true,
inputPlaceholder: t('chart.group_name_edit_tip'),
inputErrorMessage: t('chart.group_name_error_tip'),
// 正则校验,长度 1-20
inputValidator: val => {
if (val?.length < 1 || val?.length > 20) {
return t('chart.group_name_error_tip')
}
return true
}
})
.then(res => {
if (parent?.id === 'root') {
const newKey = uuid.v4()
curColumns?.splice(startIndex, endIndex - startIndex + 1, {
key: newKey,
children: totalColumns
})
curMeta.push({
field: newKey,
name: res.value
})
s2.setDataCfg({
fields: {
columns: curColumns
},
meta: curMeta
})
s2.render(true)
} else {
const [parentColumn] = getColumns([parent.field], curColumns)
const newKey = uuid.v4()
parentColumn.children?.splice(startIndex, endIndex - startIndex + 1, {
key: newKey,
children: totalColumns
})
curMeta.push({
field: newKey,
name: res.value
})
s2.setDataCfg({
fields: {
columns: curColumns
},
meta: curMeta
})
s2.render(true)
}
s2.interaction.clearState()
})
.catch(() => {
// do nothing
})
}
s2.showTooltip({
position: {
x: e.x,
y: e.y
},
content: groupMenuContainer
})
return
}
})
s2.render()
}
const getNonLeafNodes = (tree: Array<ColumnNode>): string[] => {
const result: string[] = []
const inorderTraversal = (node: ColumnNode) => {
// 如果有子节点,则为非叶子节点
if (node.children?.length > 0) {
result.push(node.key)
// 递归处理子节点
for (let i = 0; i < node.children.length; i++) {
inorderTraversal(node.children[i] as ColumnNode)
}
}
}
// 遍历树中所有节点
tree.forEach(node => inorderTraversal(node))
return result
}
const getTreesMaxDepth = (nodes: Array<ColumnNode>): number => {
if (!nodes?.length) {
return 0
}
// 获取单个节点的最大子树深度
const getNodeMaxDepth = (node: ColumnNode): number => {
if (!node.children || node.children.length === 0) {
return 0
}
const childrenDepths = node.children.map(child => getNodeMaxDepth(child as ColumnNode))
return Math.max(...childrenDepths) + 1
}
// 计算所有根节点的最大深度
const rootDepths = nodes.map(node => getNodeMaxDepth(node))
return Math.max(...rootDepths)
}
onMounted(() => {
init()
})
class GroupMenu extends BaseTooltip {
show<T = string | Element>(showOptions: TooltipShowOptions<T>): void {
super.show(showOptions)
this.container.style.display = 'flex'
}
hide(): void {
if (this.container) {
this.container.style.display = 'none'
}
}
}
</script>
<style scoped lang="less">
#table-container {
position: relative;
width: 100%;
height: 40vh;
}
.group-menu {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
color: black;
font-size: 14px;
:deep(span) {
cursor: pointer;
padding: 5px 10px;
&:hover {
background-color: var(--ed-fill-color-light);
}
}
}
.button-group {
display: flex;
justify-content: end;
}
</style>

View File

@@ -4,6 +4,7 @@ import icon_italic_outlined from '@/assets/svg/icon_italic_outlined.svg'
import icon_leftAlignment_outlined from '@/assets/svg/icon_left-alignment_outlined.svg'
import icon_centerAlignment_outlined from '@/assets/svg/icon_center-alignment_outlined.svg'
import icon_rightAlignment_outlined from '@/assets/svg/icon_right-alignment_outlined.svg'
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
import { computed, onMounted, PropType, reactive, watch } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { COLOR_PANEL, DEFAULT_TABLE_HEADER } from '@/views/chart/components/editor/util/chart'
@@ -12,6 +13,8 @@ import { cloneDeep, defaultsDeep } from 'lodash-es'
import { convertToAlphaColor, isAlphaColor } from '@/views/chart/components/js/util'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { storeToRefs } from 'pinia'
import TableHeaderGroupConfig from './TableHeaderGroupConfig.vue'
const dvMainStore = dvMainStoreWithOut()
const { mobileInPc } = storeToRefs(dvMainStore)
const { t } = useI18n()
@@ -58,7 +61,8 @@ const fontSizeList = computed(() => {
})
const state = reactive({
tableHeaderForm: {} as ChartTableHeaderAttr
tableHeaderForm: {} as ChartTableHeaderAttr,
showTableHeaderGroupConfig: false
})
const emit = defineEmits(['onTableHeaderChange'])
@@ -67,6 +71,13 @@ const changeTableHeader = prop => {
emit('onTableHeaderChange', state.tableHeaderForm, prop)
}
const changeHeaderGroupConfig = (headerGroupConfig: ChartTableHeaderAttr['headerGroupConfig']) => {
state.tableHeaderForm.headerGroupConfig = headerGroupConfig
state.showTableHeaderGroupConfig = false
log(headerGroupConfig)
changeTableHeader('headerGroupConfig')
}
const init = () => {
const tableHeader = props.chart?.customAttr?.tableHeader
if (tableHeader) {
@@ -723,7 +734,67 @@ onMounted(() => {
{{ t('chart.table_header_show_vertical_border') }}
</el-checkbox>
</el-form-item>
<el-form-item
v-if="showProperty('headerGroup')"
class="form-item"
:class="'form-item-' + themes"
>
<el-checkbox
size="small"
:effect="themes"
v-model="state.tableHeaderForm.headerGroup"
@change="changeTableHeader('headerGroup')"
>
{{ t('chart.table_header_group') }}
</el-checkbox>
</el-form-item>
<el-form-item
v-if="showProperty('headerGroup') && state.tableHeaderForm.headerGroup"
class="form-item"
:class="'form-item-' + themes"
>
<div class="header-group-config">
<span>{{ t('chart.table_header_group_config') }}</span>
<div class="group-icon">
<span v-if="state.tableHeaderForm.headerGroupConfig?.columns?.length">
{{ t('visualization.already_setting') }}
</span>
<div
class="icon-btn"
:class="{
dark: themes === 'dark'
}"
>
<el-icon @click="state.showTableHeaderGroupConfig = true">
<Icon>
<icon_edit_outlined class="svg-icon" />
</Icon>
</el-icon>
</div>
</div>
</div>
</el-form-item>
</el-form>
<el-dialog
v-model="state.showTableHeaderGroupConfig"
:effect="themes"
destroy-on-close
append-to-body
:show-close="false"
:class="themes === 'dark' ? 'table-header-group-config-dialog' : ''"
>
<template #header>
{{ t('chart.table_header_group_config') }}
<span style="font-size: 12px">({{ t('chart.table_header_group_config_tip') }})</span>
</template>
<table-header-group-config
:chart="chart"
:themes="themes"
:tableHeaderForm="state.tableHeaderForm"
@onConfigChange="changeHeaderGroupConfig"
@onCancelConfig="() => (state.showTableHeaderGroupConfig = false)"
/>
</el-dialog>
</template>
<style lang="less" scoped>
@@ -804,4 +875,28 @@ onMounted(() => {
border-color: rgba(255, 255, 255, 0.15);
}
}
.header-group-config {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding-left: 22px;
font-size: 12px;
.group-icon {
display: flex;
justify-content: center;
flex-direction: row;
align-items: center;
}
}
</style>
<style lang="less">
.table-header-group-config-dialog {
.ed-dialog__header,
.ed-dialog__body {
color: #a6a6a6;
background-color: #1a1a1a;
margin-right: 0;
}
}
</style>

View File

@@ -448,7 +448,12 @@ export const DEFAULT_TABLE_HEADER: ChartTableHeaderAttr = {
isColItalic: false,
isBolder: true,
isCornerBolder: true,
isColBolder: true
isColBolder: true,
headerGroup: false,
headerGroupConfig: {
columns: [],
meta: []
}
}
export const DEFAULT_TABLE_CELL: ChartTableCellAttr = {
tableFontColor: '#000000',
@@ -1671,7 +1676,10 @@ export const DEFAULT_BASIC_STYLE: ChartBasicStyle = {
maxLines: 3,
radarShowPoint: true,
radarPointSize: 4,
radarAreaColor: true
radarAreaColor: true,
circleBorderColor: '#fff',
circleBorderWidth: 0,
circlePadding: 0
}
export const BASE_VIEW_CONFIG = {

View File

@@ -14,7 +14,7 @@ import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util'
import { S2ChartView, S2DrawOptions } from '../../types/impl/s2'
import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common'
import { useI18n } from '@/hooks/web/useI18n'
import { isNumber, merge } from 'lodash-es'
import { isEqual, isNumber, merge } from 'lodash-es'
import {
copyContent,
CustomDataCell,
@@ -24,7 +24,10 @@ import {
SortTooltip,
configSummaryRow,
summaryRowStyle,
configEmptyDataStyle
configEmptyDataStyle,
getValidLeafNodes,
getLeafNodes,
getColumns
} from '@/views/chart/components/js/panel/common/common_table'
const { t } = useI18n()
@@ -63,7 +66,8 @@ export class TableInfo extends S2ChartView<TableSheet> {
'table-header-selector': [
...TABLE_EDITOR_PROPERTY_INNER['table-header-selector'],
'tableHeaderSort',
'showTableHeader'
'showTableHeader',
'headerGroup'
],
'basic-style-selector': [
'tableColumnMode',
@@ -103,6 +107,7 @@ export class TableInfo extends S2ChartView<TableSheet> {
pre[cur.dataeaseName] = cur
return pre
}, {})
const drillFieldMap = {}
if (chart.drill) {
// 下钻过滤字段
const filterFields = chart.drillFilters.map(i => i.fieldId)
@@ -111,13 +116,14 @@ export class TableInfo 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)
}
fields.forEach(ele => {
const f = axisMap[ele.dataeaseName]
@@ -146,6 +152,27 @@ export class TableInfo extends S2ChartView<TableSheet> {
}
})
})
const { basicStyle, tableCell, tableHeader, tooltip } = parseJson(chart.customAttr)
// 表头分组
const { headerGroup } = tableHeader
if (headerGroup) {
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
@@ -157,7 +184,6 @@ export class TableInfo extends S2ChartView<TableSheet> {
data: newData
}
const { basicStyle, tableCell, tableHeader, tooltip } = parseJson(chart.customAttr)
// options
const s2Options: S2Options = {
width: containerDom.getBoundingClientRect().width,
@@ -302,6 +328,13 @@ export class TableInfo 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
@@ -334,6 +367,13 @@ export class TableInfo 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
}
@@ -451,3 +491,19 @@ export class TableInfo extends S2ChartView<TableSheet> {
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])
}

View File

@@ -1963,3 +1963,35 @@ export const configEmptyDataStyle = (newChart, basicStyle, newData, container) =
}
})
}
export const getLeafNodes = (tree: Array<ColumnNode>): ColumnNode[] => {
const result: ColumnNode[] = []
const inorderTraversal = node => {
if (!node.children?.length) {
// 叶子节点,添加到结果数组
result.push(node)
return
}
// 中序遍历
for (let i = 0; i < node.children?.length; i++) {
inorderTraversal(node.children[i])
}
}
// 遍历树中所有节点
tree.forEach(node => inorderTraversal(node))
return result
}
export const getColumns = (fields, cols: Array<ColumnNode>) => {
const result = []
for (let i = 0; i < cols.length; i++) {
if (fields.includes(cols[i].key)) {
result.push(cols[i])
}
if (cols[i].children?.length) {
result.push(...getColumns(fields, cols[i].children as Array<ColumnNode>))
}
}
return result
}