feat(图表): 汇总表总计,支持对数值类字段进行自定义汇总

#12878
This commit is contained in:
ulleo
2025-03-21 13:57:17 +08:00
committed by dataeaseShu
parent e6844cc700
commit 7c814454fe
12 changed files with 383 additions and 108 deletions

View File

@@ -1388,6 +1388,7 @@ export default {
table_show_col_tooltip: 'Turn on column header tooltip',
table_show_cell_tooltip: 'Turn on cell tooltip',
table_show_header_tooltip: 'Turn on header tooltip',
table_summary: 'Total',
table_show_summary: 'Show total',
table_summary_label: 'Total label',
table_header_show_horizon_border: 'Header horizontal border',

View File

@@ -1352,6 +1352,7 @@ export default {
table_show_col_tooltip: '開啟列頭提示',
table_show_cell_tooltip: '開啟單元格提示',
table_show_header_tooltip: '開啟表頭提示',
table_summary: '總計',
table_show_summary: '顯示總計',
table_summary_label: '總計標籤',
table_header_show_horizon_border: '表頭橫邊框線',

View File

@@ -1357,6 +1357,7 @@ export default {
table_show_col_tooltip: '开启列头提示',
table_show_cell_tooltip: '开启单元格提示',
table_show_header_tooltip: '开启表头提示',
table_summary: '总计',
table_show_summary: '显示总计',
table_summary_label: '总计标签',
table_header_show_horizon_border: '表头横边框线',

View File

@@ -293,6 +293,12 @@ declare interface ChartBasicStyle {
* 汇总表总计标签
*/
summaryLabel: string
seriesSummary?: Array<{
show: boolean
field: string
summary: string
}>
/**
* 符号地图符号大小最小值
*/

View File

@@ -31,6 +31,7 @@ declare type EditorProperty =
| 'flow-map-line-selector'
| 'flow-map-point-selector'
| 'bubble-animate'
| 'summary-selector'
declare type EditorPropertyInner = {
[key in EditorProperty]?: string[]
}

View File

@@ -9,6 +9,7 @@ import YAxisSelector from '@/views/chart/components/editor/editor-style/componen
import DualYAxisSelector from '@/views/chart/components/editor/editor-style/components/DualYAxisSelector.vue'
import TitleSelector from '@/views/chart/components/editor/editor-style/components/TitleSelector.vue'
import LegendSelector from '@/views/chart/components/editor/editor-style/components/LegendSelector.vue'
import SummarySelector from '@/views/chart/components/editor/editor-style/components/SummarySelector.vue'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { storeToRefs } from 'pinia'
import CollapseSwitchItem from '@/components/collapse-switch-item/src/CollapseSwitchItem.vue'
@@ -616,6 +617,23 @@ watch(
@onChangeYAxisExtForm="onChangeYAxisExtForm"
/>
</collapse-switch-item>
<collapse-switch-item
:themes="themes"
v-if="showProperties('summary-selector')"
v-model="chart.customAttr.basicStyle.showSummary"
:change-model="chart.customAttr.basicStyle"
@modelChange="val => onBasicStyleChange({ data: val }, 'showSummary')"
:title="t('chart.table_summary')"
name="summary"
>
<summary-selector
:property-inner="propertyInnerAll['summary-selector']"
:themes="themes"
:chart="chart"
@onBasicStyleChange="onBasicStyleChange"
/>
</collapse-switch-item>
</el-collapse>
</el-row>
</div>

View File

@@ -860,7 +860,7 @@ onMounted(() => {
<template #append>%</template>
</el-input>
</el-form-item>
<el-form-item
<!-- <el-form-item
v-if="showProperty('showSummary')"
class="form-item"
:class="'form-item-' + themes"
@@ -887,7 +887,7 @@ onMounted(() => {
:max-length="10"
@blur="changeBasicStyle('summaryLabel')"
/>
</el-form-item>
</el-form-item>-->
<el-form-item v-if="showProperty('autoWrap')" class="form-item" :class="'form-item-' + themes">
<el-checkbox
size="small"

View File

@@ -0,0 +1,186 @@
<script setup lang="ts">
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { useI18n } from '@/hooks/web/useI18n'
import { computed, onMounted, PropType, reactive, watch } from 'vue'
import { DEFAULT_BASIC_STYLE } from '@/views/chart/components/editor/util/chart'
import { cloneDeep, defaultsDeep, filter, find } from 'lodash-es'
const dvMainStore = dvMainStoreWithOut()
const { t } = useI18n()
const props = defineProps({
chart: {
type: Object as PropType<ChartObj>,
required: true
},
themes: {
type: String as PropType<EditorTheme>,
default: 'dark'
},
propertyInner: {
type: Array<string>
}
})
const showProperty = prop => props.propertyInner?.includes(prop)
const state = reactive({
basicStyleForm: JSON.parse(JSON.stringify(DEFAULT_BASIC_STYLE)) as ChartBasicStyle,
currentAxis: undefined as string,
currentAxisSummary: undefined as {
show: boolean
field: string
summary: string
}
})
const emit = defineEmits(['onBasicStyleChange'])
const changeBasicStyle = (prop?: string, requestData = false) => {
emit('onBasicStyleChange', { data: state.basicStyleForm, requestData }, prop)
}
watch(
[
() => props.chart.customAttr.basicStyle.showSummary,
() => props.chart.xAxis,
() => props.chart.yAxis
],
() => {
init()
},
{
deep: true
}
)
function getAxisList() {
return props.chart.type === 'table-info'
? filter(props.chart.xAxis, axis => [2, 3, 4].includes(axis.deType))
: props.chart.yAxis
}
const computedAxis = computed(() => {
return getAxisList()
})
const summaryTypes = [
{ key: 'sum', name: t('chart.sum') },
{ key: 'avg', name: t('chart.avg') },
{ key: 'max', name: t('chart.max') },
{ key: 'min', name: t('chart.min') }
// { key: 'stddev_pop', name: t('chart.stddev_pop') },
// { key: 'var_pop', name: t('chart.var_pop') }
]
function onSelectAxis(value) {
state.currentAxisSummary = find(state.basicStyleForm.seriesSummary, s => s.field === value)
}
const init = () => {
const basicStyle = cloneDeep(props.chart.customAttr.basicStyle)
state.basicStyleForm = defaultsDeep(basicStyle, cloneDeep(DEFAULT_BASIC_STYLE)) as ChartBasicStyle
const axisList = getAxisList()
const tempList = []
for (let i = 0; i < axisList.length; i++) {
const axis = axisList[i]
let savedAxis = find(state.basicStyleForm.seriesSummary, s => s.field === axis.dataeaseName)
if (savedAxis) {
if (savedAxis.summary == undefined) {
savedAxis.summary = 'sum'
}
if (savedAxis.show == undefined) {
savedAxis.show = true
}
} else {
savedAxis = {
field: axis.dataeaseName,
summary: 'sum',
show: true
}
}
tempList.push(savedAxis)
}
state.basicStyleForm.seriesSummary = tempList
if (state.basicStyleForm.seriesSummary.length > 0 && state.basicStyleForm.showSummary) {
state.currentAxis = state.basicStyleForm.seriesSummary[0].field
onSelectAxis(state.currentAxis)
} else {
state.currentAxis = undefined
state.currentAxisSummary = undefined
}
}
onMounted(() => {
init()
})
</script>
<template>
<div style="width: 100%">
<el-form
ref="summaryForm"
:disabled="!state.basicStyleForm.showSummary"
:model="state.basicStyleForm"
label-position="top"
>
<el-form-item
v-if="showProperty('summaryLabel')"
:label="t('chart.table_summary_label')"
:class="'form-item-' + themes"
class="form-item"
>
<el-input
v-model="state.basicStyleForm.summaryLabel"
type="text"
:effect="themes"
:max-length="10"
@blur="changeBasicStyle('summaryLabel')"
/>
</el-form-item>
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-select
v-model="state.currentAxis"
:class="'form-item-' + themes"
class="form-item"
@change="onSelectAxis"
>
<el-option
v-for="c in computedAxis"
:key="c.dataeaseName"
:value="c.dataeaseName"
:label="c.chartShowName ?? c.description"
/>
</el-select>
</el-form-item>
<template v-if="state.currentAxis && state.currentAxisSummary">
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-checkbox
size="small"
:effect="themes"
v-model="state.currentAxisSummary.show"
@change="changeBasicStyle('seriesSummary')"
>
{{ t('chart.table_show_summary') }}
</el-checkbox>
</el-form-item>
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-select
v-model="state.currentAxisSummary.summary"
:class="'form-item-' + themes"
class="form-item"
@change="changeBasicStyle('seriesSummary')"
>
<el-option v-for="c in summaryTypes" :key="c.key" :value="c.key" :label="c.name" />
</el-select>
</el-form-item>
</template>
</el-form>
</div>
</template>
<style scoped lang="less"></style>

View File

@@ -6,6 +6,7 @@ export const TABLE_EDITOR_PROPERTY: EditorProperty[] = [
'table-cell-selector',
'title-selector',
'tooltip-selector',
'summary-selector',
'function-cfg',
'threshold',
'scroll-cfg',

View File

@@ -58,9 +58,7 @@ export class TableInfo extends S2ChartView<TableSheet> {
'alpha',
'tablePageMode',
'showHoverStyle',
'autoWrap',
'showSummary',
'summaryLabel'
'autoWrap'
],
'table-cell-selector': [
...TABLE_EDITOR_PROPERTY_INNER['table-cell-selector'],
@@ -68,7 +66,8 @@ export class TableInfo extends S2ChartView<TableSheet> {
'tableColumnFreezeHead',
'tableRowFreezeHead',
'mergeCells'
]
],
'summary-selector': ['showSummary', 'summaryLabel']
}
axis: AxisType[] = ['xAxis', 'filter', 'drill']
axisConfig: AxisConfig = {

View File

@@ -39,8 +39,6 @@ export class TableNormal extends S2ChartView<TableSheet> {
'basic-style-selector': [
...TABLE_EDITOR_PROPERTY_INNER['basic-style-selector'],
'tablePageMode',
'showSummary',
'summaryLabel',
'showHoverStyle'
],
'table-cell-selector': [
@@ -48,7 +46,8 @@ export class TableNormal extends S2ChartView<TableSheet> {
'tableFreeze',
'tableColumnFreezeHead',
'tableRowFreezeHead'
]
],
'summary-selector': ['showSummary', 'summaryLabel']
}
axis: AxisType[] = ['xAxis', 'yAxis', 'drill', 'filter']
axisConfig: AxisConfig = {

View File

@@ -43,14 +43,30 @@ import {
updateShapeAttr,
ViewMeta
} from '@antv/s2'
import { cloneDeep, filter, find, intersection, keys, merge, repeat } from 'lodash-es'
import { createVNode, render } from 'vue'
import {
cloneDeep,
filter,
find,
intersection,
keys,
map,
maxBy,
meanBy,
merge,
minBy,
repeat,
sumBy,
size,
sum
} from 'lodash-es'
import {createVNode, render} from 'vue'
import TableTooltip from '@/views/chart/components/editor/common/TableTooltip.vue'
import Exceljs from 'exceljs'
import { saveAs } from 'file-saver'
import { ElMessage } from 'element-plus-secondary'
import { useI18n } from '@/hooks/web/useI18n'
const { t: i18nt } = useI18n()
import {saveAs} from 'file-saver'
import {ElMessage} from 'element-plus-secondary'
import {useI18n} from '@/hooks/web/useI18n'
const {t: i18nt} = useI18n()
export function getCustomTheme(chart: Chart): S2Theme {
const headerColor = hexColorToRGBA(
@@ -199,7 +215,7 @@ export function getCustomTheme(chart: Chart): S2Theme {
let customAttr: DeepPartial<ChartAttr>
if (chart.customAttr) {
customAttr = parseJson(chart.customAttr)
const { basicStyle, tableHeader, tableCell } = customAttr
const {basicStyle, tableHeader, tableCell} = customAttr
// basic
if (basicStyle) {
const tableBorderColor = basicStyle.tableBorderColor
@@ -251,7 +267,7 @@ export function getCustomTheme(chart: Chart): S2Theme {
}
const fontStyle = tableHeader.isItalic ? 'italic' : 'normal'
const fontWeight = tableHeader.isBolder === false ? 'normal' : 'bold'
const { tableHeaderAlign, tableTitleFontSize } = tableHeader
const {tableHeaderAlign, tableTitleFontSize} = tableHeader
const tmpTheme: S2Theme = {
cornerCell: {
cell: {
@@ -366,7 +382,7 @@ export function getCustomTheme(chart: Chart): S2Theme {
}
const fontStyle = tableCell.isItalic ? 'italic' : 'normal'
const fontWeight = tableCell.isBolder === false ? 'normal' : 'bold'
const { tableItemAlign, tableItemFontSize, enableTableCrossBG } = tableCell
const {tableItemAlign, tableItemFontSize, enableTableCrossBG} = tableCell
const tmpTheme: S2Theme = {
rowCell: {
cell: {
@@ -476,7 +492,7 @@ export function getStyle(chart: Chart, dataConfig: S2DataConfig): Style {
let customAttr: DeepPartial<ChartAttr>
if (chart.customAttr) {
customAttr = parseJson(chart.customAttr)
const { basicStyle, tableHeader, tableCell } = customAttr
const {basicStyle, tableHeader, tableCell} = customAttr
style.colCfg = {
height: tableHeader.tableTitleHeight
}
@@ -497,7 +513,7 @@ export function getStyle(chart: Chart, dataConfig: S2DataConfig): Style {
}, {}) || {}
// 下钻字段使用入口字段的宽度
if (chart.drill) {
const { xAxis } = parseJson(chart)
const {xAxis} = parseJson(chart)
const curDrillField = chart.drillFields[chart.drillFilters.length]
const drillEnterFieldIndex = xAxis.findIndex(
item => item.id === chart.drillFilters[0].fieldId
@@ -589,7 +605,7 @@ export function getCurrentField(valueFieldList: Axis[], field: ChartViewField) {
}
export function getConditions(chart: Chart) {
const { threshold } = parseJson(chart.senior)
const {threshold} = parseJson(chart.senior)
if (!threshold.enable) {
return
}
@@ -601,7 +617,7 @@ export function getConditions(chart: Chart) {
const dimFields = [...chart.xAxis, ...chart.xAxisExt].map(i => i.dataeaseName)
if (conditions?.length > 0) {
const { tableCell, basicStyle, tableHeader } = parseJson(chart.customAttr)
const {tableCell, basicStyle, tableHeader} = parseJson(chart.customAttr)
// 合并单元格时斑马纹失效
const enableTableCrossBG = chart.type === 'table-info' ? tableCell.enableTableCrossBG && !tableCell.mergeCells : tableCell.enableTableCrossBG
const valueColor = isAlphaColor(tableCell.tableFontColor)
@@ -610,8 +626,8 @@ export function getConditions(chart: Chart) {
const valueBgColor = enableTableCrossBG
? null
: isAlphaColor(tableCell.tableItemBgColor)
? tableCell.tableItemBgColor
: hexColorToRGBA(tableCell.tableItemBgColor, basicStyle.alpha)
? tableCell.tableItemBgColor
: hexColorToRGBA(tableCell.tableItemBgColor, basicStyle.alpha)
const headerValueColor = tableHeader.tableHeaderFontColor
const headerValueBgColor = isAlphaColor(tableHeader.tableHeaderBgColor)
? tableHeader.tableHeaderBgColor
@@ -662,7 +678,7 @@ export function getConditions(chart: Chart) {
if (isTransparent(fill)) {
return null
}
return { fill }
return {fill}
}
})
}
@@ -876,9 +892,10 @@ export function handleTableEmptyStrategy(chart: Chart) {
}
return newData
}
export class SortTooltip extends BaseTooltip {
show(showOptions) {
const { iconName } = showOptions
const {iconName} = showOptions
if (iconName) {
this.showSortTooltip(showOptions)
return
@@ -887,9 +904,9 @@ export class SortTooltip extends BaseTooltip {
}
showSortTooltip(showOptions) {
const { position, options, meta, event } = showOptions
const { enterable } = getTooltipDefaultOptions(options)
const { autoAdjustBoundary, adjustPosition } = this.spreadsheet.options.tooltip || {}
const {position, options, meta, event} = showOptions
const {enterable} = getTooltipDefaultOptions(options)
const {autoAdjustBoundary, adjustPosition} = this.spreadsheet.options.tooltip || {}
this.visible = true
this.options = showOptions
const container = this['getContainer']()
@@ -903,14 +920,14 @@ export class SortTooltip extends BaseTooltip {
this.spreadsheet.tooltip.container.appendChild(childElement)
render(vNode, childElement)
const { x, y } = getAutoAdjustPosition({
const {x, y} = getAutoAdjustPosition({
spreadsheet: this.spreadsheet,
position,
tooltipContainer: container,
autoAdjustBoundary
})
this.position = adjustPosition?.({ position: { x, y }, event }) ?? {
this.position = adjustPosition?.({position: {x, y}, event}) ?? {
x,
y
}
@@ -930,6 +947,7 @@ export class SortTooltip extends BaseTooltip {
})
}
}
const SORT_DEFAULT =
'<svg t="1711681787276" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4355" width="200" height="200"><path d="M922.345786 372.183628l-39.393195 38.687114L676.138314 211.079416l0 683.909301-54.713113 0L621.425202 129.010259l53.320393 0L922.345786 372.183628zM349.254406 894.989741 101.654214 651.815349l39.393195-38.687114 206.814276 199.792349L347.861686 129.010259l54.713113 0 0 765.978459L349.254406 894.988718z" fill="{fill}" p-id="4356"></path></svg>'
const SORT_UP =
@@ -942,7 +960,7 @@ function svg2Base64(svg) {
}
export function configHeaderInteraction(chart: Chart, option: S2Options) {
const { tableHeaderFontColor, tableHeaderSort } = parseJson(chart.customAttr).tableHeader
const {tableHeaderFontColor, tableHeaderSort} = parseJson(chart.customAttr).tableHeader
if (!tableHeaderSort) {
return
}
@@ -994,7 +1012,7 @@ export function configHeaderInteraction(chart: Chart, option: S2Options) {
return iconName === `customSortDefault${randomSuffix}`
},
onClick: props => {
const { meta, event } = props
const {meta, event} = props
meta.spreadsheet.showTooltip({
position: {
x: event.clientX,
@@ -1022,7 +1040,7 @@ export function configHeaderInteraction(chart: Chart, option: S2Options) {
}
export function configTooltip(chart: Chart, option: S2Options) {
const { tooltip } = parseJson(chart.customAttr)
const {tooltip} = parseJson(chart.customAttr)
const textFontFamily = chart.fontFamily ? chart.fontFamily : FONT_FAMILY
option.tooltip = {
...option.tooltip,
@@ -1037,7 +1055,7 @@ export function configTooltip(chart: Chart, option: S2Options) {
opacity: 0.95,
position: 'absolute'
},
adjustPosition: ({ event }) => {
adjustPosition: ({event}) => {
return getTooltipPosition(event)
}
}
@@ -1052,7 +1070,7 @@ export function copyContent(s2Instance: SpreadSheet, event, fieldMeta) {
let content = ''
// 多选
if (selectState.stateName === InteractionStateName.SELECTED) {
const { cells } = selectState
const {cells} = selectState
if (!cells?.length) {
return
}
@@ -1147,13 +1165,13 @@ export function copyContent(s2Instance: SpreadSheet, event, fieldMeta) {
function getTooltipPosition(event) {
const s2Instance = event.s2Instance
const { x, y } = event
const result = { x: x + 15, y }
const {x, y} = event
const result = {x: x + 15, y}
if (!s2Instance) {
return result
}
const { height, width } = s2Instance.getCanvasElement().getBoundingClientRect()
const { offsetHeight, offsetWidth } = s2Instance.tooltip.getContainer()
const {height, width} = s2Instance.getCanvasElement().getBoundingClientRect()
const {offsetHeight, offsetWidth} = s2Instance.tooltip.getContainer()
if (offsetWidth > width) {
result.x = 0
}
@@ -1181,8 +1199,8 @@ function getTooltipPosition(event) {
}
export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) {
const { layoutResult } = instance.facet
const { meta, fields } = instance.dataCfg
const {layoutResult} = instance.facet
const {meta, fields} = instance.dataCfg
const rowLength = fields?.rows?.length || 0
const colLength = fields?.columns?.length || 0
const colNums = layoutResult.colLeafNodes.length + rowLength + 1
@@ -1202,27 +1220,27 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) {
fields.columns?.forEach((column, index) => {
const cell = worksheet.getCell(index + 1, 1)
cell.value = metaMap[column]?.name ?? column
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
if (rowLength >= 2) {
worksheet.mergeCells(index + 1, 1, index + 1, rowLength)
}
cell.border = {
right: { style: 'thick', color: { argb: '00000000' } }
right: {style: 'thick', color: {argb: '00000000'}}
}
})
fields?.rows?.forEach((row, index) => {
const cell = worksheet.getCell(colLength + 1, index + 1)
cell.value = metaMap[row]?.name ?? row
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
cell.border = {
bottom: { style: 'thick', color: { argb: '00000000' } }
bottom: {style: 'thick', color: {argb: '00000000'}}
}
if (index === fields.rows.length - 1) {
cell.border.right = { style: 'thick', color: { argb: '00000000' } }
cell.border.right = {style: 'thick', color: {argb: '00000000'}}
}
})
// 行头
const { rowLeafNodes, rowsHierarchy, rowNodes } = layoutResult
const {rowLeafNodes, rowsHierarchy, rowNodes} = layoutResult
const maxColIndex = rowsHierarchy.maxLevel + 1
const notLeafNodeHeightMap: Record<string, number> = {}
rowLeafNodes.forEach(node => {
@@ -1233,17 +1251,17 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) {
notLeafNodeHeightMap[curNode.id] = height + 1
curNode = curNode.parent
}
const { rowIndex } = node
const {rowIndex} = node
const writeRowIndex = rowIndex + 1 + colLength + 1
const writeColIndex = node.level + 1
const cell = worksheet.getCell(writeRowIndex, writeColIndex)
cell.value = node.label
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
if (writeColIndex < maxColIndex) {
worksheet.mergeCells(writeRowIndex, writeColIndex, writeRowIndex, maxColIndex)
}
cell.border = {
right: { style: 'thick', color: { argb: '00000000' } }
right: {style: 'thick', color: {argb: '00000000'}}
}
})
@@ -1265,7 +1283,7 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) {
const value = node.label
const cell = worksheet.getCell(writeRowIndex, node.level + 1)
cell.value = value
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
if (mergeColCount > 1 || height > 1) {
worksheet.mergeCells(
writeRowIndex,
@@ -1277,7 +1295,7 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) {
})
// 列头
const { colLeafNodes, colNodes, colsHierarchy } = layoutResult
const {colLeafNodes, colNodes, colsHierarchy} = layoutResult
const maxColHeight = colsHierarchy.maxLevel + 1
const notLeafNodeWidthMap: Record<string, number> = {}
colLeafNodes.forEach(node => {
@@ -1288,7 +1306,7 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) {
notLeafNodeWidthMap[curNode.id] = width + 1
curNode = curNode.parent
}
const { colIndex } = node
const {colIndex} = node
const writeRowIndex = node.level + 1
const writeColIndex = colIndex + 1 + rowLength
const cell = worksheet.getCell(writeRowIndex, writeColIndex)
@@ -1297,12 +1315,12 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) {
value = metaMap[value].name
}
cell.value = value
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
if (writeRowIndex < maxColHeight) {
worksheet.mergeCells(writeRowIndex, writeColIndex, maxColHeight, writeColIndex)
}
cell.border = {
bottom: { style: 'thick', color: { argb: '00000000' } }
bottom: {style: 'thick', color: {argb: '00000000'}}
}
})
const getNodeStartColIndex = (node: Node) => {
@@ -1324,7 +1342,7 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) {
const writeColIndex = colIndex + rowLength
const cell = worksheet.getCell(writeRowIndex, writeColIndex)
cell.value = value
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
if (mergeRowCount > 1 || width > 1) {
worksheet.mergeCells(
writeRowIndex,
@@ -1338,12 +1356,12 @@ export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) {
for (let rowIndex = 0; rowIndex < rowLeafNodes.length; rowIndex++) {
for (let colIndex = 0; colIndex < colLeafNodes.length; colIndex++) {
const dataCellMeta = layoutResult.getCellMeta(rowIndex, colIndex)
const { fieldValue } = dataCellMeta
const {fieldValue} = dataCellMeta
if (fieldValue === 0 || fieldValue) {
const meta = metaMap[dataCellMeta.valueField]
const cell = worksheet.getCell(rowIndex + maxColHeight + 1, rowLength + colIndex + 1)
const value = meta?.formatter?.(fieldValue) || fieldValue.toString()
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
cell.value = value
}
}
@@ -1361,7 +1379,7 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) {
ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed'))
return
}
const { meta, fields } = instance.dataCfg
const {meta, fields} = instance.dataCfg
const colLength = fields?.columns?.length || 0
const workbook = new Exceljs.Workbook()
const worksheet = workbook.addWorksheet(i18nt('chart.chart_data'))
@@ -1376,33 +1394,33 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) {
fields.columns?.forEach((column, index) => {
const cell = worksheet.getCell(index + 1, 1)
cell.value = metaMap[column]?.name ?? column
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
cell.border = {
right: { style: 'thick', color: { argb: '00000000' } }
right: {style: 'thick', color: {argb: '00000000'}}
}
})
const maxColHeight = layoutResult.colsHierarchy.maxLevel + 1
const rowName = fields?.rows?.map(row => metaMap[row]?.name ?? row).join('/')
const cell = worksheet.getCell(colLength + 1, 1)
cell.value = rowName
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
cell.border = {
right: { style: 'thick', color: { argb: '00000000' } },
bottom: { style: 'thick', color: { argb: '00000000' } }
right: {style: 'thick', color: {argb: '00000000'}},
bottom: {style: 'thick', color: {argb: '00000000'}}
}
//行头
const { rowLeafNodes } = layoutResult
const {rowLeafNodes} = layoutResult
rowLeafNodes.forEach((node, index) => {
const cell = worksheet.getCell(maxColHeight + index + 1, 1)
cell.value = repeat(' ', node.level) + node.label
cell.alignment = { vertical: 'middle', horizontal: 'left' }
cell.alignment = {vertical: 'middle', horizontal: 'left'}
cell.border = {
right: { style: 'thick', color: { argb: '00000000' } }
right: {style: 'thick', color: {argb: '00000000'}}
}
})
// 列头
const notLeafNodeWidthMap: Record<string, number> = {}
const { colLeafNodes } = layoutResult
const {colLeafNodes} = layoutResult
colLeafNodes.forEach(node => {
let curNode = node.parent
while (curNode) {
@@ -1410,7 +1428,7 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) {
notLeafNodeWidthMap[curNode.id] = width + 1
curNode = curNode.parent
}
const { colIndex } = node
const {colIndex} = node
const writeRowIndex = node.level + 1
const writeColIndex = colIndex + 1 + 1
const cell = worksheet.getCell(writeRowIndex, writeColIndex)
@@ -1419,12 +1437,12 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) {
value = metaMap[value].name
}
cell.value = value
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
if (writeRowIndex < maxColHeight) {
worksheet.mergeCells(writeRowIndex, writeColIndex, maxColHeight, writeColIndex)
}
cell.border = {
bottom: { style: 'thick', color: { argb: '00000000' } }
bottom: {style: 'thick', color: {argb: '00000000'}}
}
})
const colNodes = layoutResult.colNodes
@@ -1446,7 +1464,7 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) {
const writeColIndex = colIndex + 1
const cell = worksheet.getCell(writeRowIndex, writeColIndex)
cell.value = node.label
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
if (mergeRowCount > 1 || width > 1) {
worksheet.mergeCells(
writeRowIndex,
@@ -1460,12 +1478,12 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) {
for (let rowIndex = 0; rowIndex < rowLeafNodes.length; rowIndex++) {
for (let colIndex = 0; colIndex < colLeafNodes.length; colIndex++) {
const dataCellMeta = layoutResult.getCellMeta(rowIndex, colIndex)
const { fieldValue } = dataCellMeta
const {fieldValue} = dataCellMeta
if (fieldValue === 0 || fieldValue) {
const meta = metaMap[dataCellMeta.valueField]
const cell = worksheet.getCell(rowIndex + maxColHeight + 1, colIndex + 1 + 1)
const value = meta?.formatter?.(fieldValue) || fieldValue.toString()
cell.alignment = { vertical: 'middle', horizontal: 'center' }
cell.alignment = {vertical: 'middle', horizontal: 'center'}
cell.value = value
}
}
@@ -1476,8 +1494,9 @@ export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) {
})
saveAs(dataBlob, `${chart.title ?? '透视表'}.xlsx`)
}
export async function exportPivotExcel(instance: PivotSheet, chart: ChartObj) {
const { fields } = instance.dataCfg
const {fields} = instance.dataCfg
const rowLength = fields?.rows?.length || 0
const valueLength = fields?.values?.length || 0
if (!(rowLength && valueLength)) {
@@ -1492,8 +1511,8 @@ export async function exportPivotExcel(instance: PivotSheet, chart: ChartObj) {
}
export function configMergeCells(chart: Chart, options: S2Options, dataConfig: S2DataConfig) {
const { mergeCells } = parseJson(chart.customAttr).tableCell
const { showIndex } = parseJson(chart.customAttr).tableHeader
const {mergeCells} = parseJson(chart.customAttr).tableCell
const {showIndex} = parseJson(chart.customAttr).tableHeader
if (mergeCells) {
options.frozenColCount = 0
options.frozenRowCount = 0
@@ -1601,7 +1620,7 @@ class CustomMergedCell extends MergedCell {
const allPoints = getPolygonPoints(this.cells)
// 处理条件样式,这里没有用透明度
// 因为合并的单元格是单独的图层,透明度降低的话会显示底下未合并的单元格,需要单独处理被覆盖的单元格
const { backgroundColor: fill, backgroundColorOpacity: fillOpacity } = this.getBackgroundColor()
const {backgroundColor: fill, backgroundColorOpacity: fillOpacity} = this.getBackgroundColor()
const cellTheme = this.theme.dataCell.cell
this.backgroundShape = renderPolygon(this, {
points: allPoints,
@@ -1610,6 +1629,7 @@ class CustomMergedCell extends MergedCell {
lineHeight: cellTheme.horizontalBorderWidth
})
}
drawTextShape(): void {
if (this.meta.deFieldType === 7) {
drawImage.apply(this)
@@ -1674,11 +1694,11 @@ const drawTextShape = (cell, isHeader) => {
// 用户配置的最大行数
const maxLines = cell.meta.maxLines ?? 1
const {
options: { placeholder }
options: {placeholder}
} = cell.spreadsheet
const emptyPlaceholder = getEmptyPlaceholder(this, placeholder)
// 单元格文本
const { formattedValue } = cell.getFormattedFieldValue()
const {formattedValue} = cell.getFormattedFieldValue()
// 获取文本样式
const textStyle = cell.getTextStyle()
// 宽度能放几个字符,就放几个,放不下就换行
@@ -1758,7 +1778,7 @@ export const calculateHeaderHeight = (info, newChart, tableHeader, basicStyle, l
if (tableHeader.showTableHeader === false) return
const ev = layoutResult || newChart.facet.layoutResult
const maxLines = basicStyle.maxLines ?? 1
const textStyle = { ...newChart.theme.cornerCell.text }
const textStyle = {...newChart.theme.cornerCell.text}
const sourceText = info.info.meta.value
let maxHeight = getWrapTextHeight(
getWrapText(sourceText, textStyle, info.info.resizedWidth, ev.spreadsheet),
@@ -1779,10 +1799,10 @@ export const calculateHeaderHeight = (info, newChart, tableHeader, basicStyle, l
maxLines
)
return wrapTextHeight > maxHeightNode.height
? { height: wrapTextHeight, colIndex: currentNode.colIndex }
? {height: wrapTextHeight, colIndex: currentNode.colIndex}
: maxHeightNode
},
{ height: 0 }
{height: 0}
)
// 使用最大高度
@@ -1873,30 +1893,71 @@ export const configSummaryRow = (
// 设置汇总行高度和表头一致
const heightByField = {}
heightByField[newData.length] = tableHeader.tableTitleHeight
s2Options.style.rowCfg = { heightByField }
s2Options.style.rowCfg = {heightByField}
// 计算汇总加入到数据里,冻结最后一行
s2Options.frozenTrailingRowCount = 1
const yAxis = chart.yAxis
const xAxis = chart.xAxis
const summaryObj = newData.reduce(
(p, n) => {
if (chart.type === 'table-info') {
xAxis
.filter(axis => [2, 3, 4].includes(axis.deType))
.forEach(axis => {
p[axis.dataeaseName] =
(parseFloat(n[axis.dataeaseName]) || 0) + (parseFloat(p[axis.dataeaseName]) || 0)
})
} else {
yAxis.forEach(axis => {
p[axis.dataeaseName] =
(parseFloat(n[axis.dataeaseName]) || 0) + (parseFloat(p[axis.dataeaseName]) || 0)
})
const axis = chart.type === 'table-info'
? filter(chart.xAxis, axis => [2, 3, 4].includes(axis.deType))
: chart.yAxis
const summaryObj = {SUMMARY: true}
for (let i = 0; i < axis.length; i++) {
const a = axis[i].dataeaseName
let savedAxis = find(basicStyle.seriesSummary, s => s.field === a)
if (savedAxis) {
if (savedAxis.summary == undefined) {
savedAxis.summary = 'sum'
}
return p
},
{ SUMMARY: true }
)
if (savedAxis.show == undefined) {
savedAxis.show = true
}
} else {
savedAxis = {
field: a,
summary: 'sum',
show: true
}
}
if (!savedAxis.show) {
continue
}
switch (savedAxis.summary) {
case 'sum':
summaryObj[a] = sumBy(newData, d => (parseFloat(d[a]) || 0))
break
case 'avg':
summaryObj[a] = meanBy(newData, d => (parseFloat(d[a]) || 0))
break
case 'max':
summaryObj[a] = maxBy(filter(newData, d => parseFloat(d[a]) !== undefined), d => parseFloat(d[a]))[a]
break
case 'min':
summaryObj[a] = minBy(filter(newData, d => parseFloat(d[a]) !== undefined), d => parseFloat(d[a]))[a]
break
case 'var_pop'://方差
if (newData.length < 2) {
continue
} else {
const mean = meanBy(newData, d => (parseFloat(d[a]) || 0)) // 计算均值
const squaredDeviations = map(newData, d => ((parseFloat(d[a]) || 0) - mean) ** 2); // 计算偏差平方
summaryObj[a] = sum(squaredDeviations) / (size(newData) - 1); // 样本方差分母n-1
}
break
case 'stddev_pop'://标准差
if (newData.length < 2) {
continue
} else {
const mean = meanBy(newData, d => (parseFloat(d[a]) || 0)) // 计算均值
const squaredDeviations = map(newData, d => ((parseFloat(d[a]) || 0) - mean) ** 2); // 计算偏差平方
const sampleVariance = sum(squaredDeviations) / (size(newData) - 1); // 样本方差分母n-1
summaryObj[a] = Math.sqrt(sampleVariance); // 样本标准差
}
break
}
}
newData.push(summaryObj)
s2Options.dataCell = viewMeta => {
// 配置文本自动换行参数
@@ -1948,9 +2009,10 @@ export class SummaryCell extends CustomDataCell {
textStyle.textAlign = this.theme.dataCell.text.textAlign
return textStyle
}
getBackgroundColor() {
const { backgroundColor, backgroundColorOpacity } = this.theme.colCell.cell
return { backgroundColor, backgroundColorOpacity }
const {backgroundColor, backgroundColorOpacity} = this.theme.colCell.cell
return {backgroundColor, backgroundColorOpacity}
}
}
@@ -2026,12 +2088,12 @@ export const getColumns = (fields, cols: Array<ColumnNode>) => {
export function drawImage() {
const img = new Image()
const { x, y, width, height, fieldValue } = this.meta
const {x, y, width, height, fieldValue} = this.meta
img.src = fieldValue as string
img.setAttribute('crossOrigin', 'anonymous')
img.onload = () => {
!this.cfg.children && (this.cfg.children = [])
const { width: imgWidth, height: imgHeight } = img
const {width: imgWidth, height: imgHeight} = img
const ratio = Math.max(imgWidth / width, imgHeight / height)
// 不铺满,部分留白
const imgShowWidth = (imgWidth / ratio) * 0.8