feat(视图-透视表): 支持透视表

This commit is contained in:
wisonic-s 2024-01-24 18:39:25 +08:00
parent 2342eb240e
commit b8f2f4fad0
6 changed files with 947 additions and 14 deletions

View File

@ -263,7 +263,7 @@ const active = computed(() => {
})
const boardMoveActive = computed(() => {
return ['map', 'table-info', 'table-normal'].includes(element.value.innerType)
return ['map', 'table-info', 'table-normal', 'table-pivot'].includes(element.value.innerType)
})
const dashboardActive = computed(() => {

View File

@ -275,7 +275,7 @@ declare interface TotalConfig {
/**
* 小计维度
*/
subTotalsDimensions: []
subTotalsDimensions: string[]
/**
* 总计汇总设置
*/
@ -297,7 +297,17 @@ declare interface TotalConfig {
* 汇总聚合方式
*/
declare interface CalcTotals {
aggregation: string
aggregation: 'MIN' | 'MAX' | 'AVG' | 'SUM'
cfg: CalcTotalCfg[]
calcFunc?: (...args) => any
}
/**
* 汇总聚合配置
*/
declare interface CalcTotalCfg {
dataeaseName: string
aggregation: 'MIN' | 'MAX' | 'AVG' | 'SUM' | ''
}
/**

View File

@ -82,12 +82,10 @@ watch(
},
{ deep: true }
)
const AXIS_FORMAT_VIEW = ['table-normal', 'table-info', 'table-pivot', 'indicator']
const showValueFormatter = computed<boolean>(() => {
return (
(props.chart.type === 'table-normal' ||
props.chart.type === 'table-info' ||
props.chart.type === 'indicator') &&
AXIS_FORMAT_VIEW.includes(props.chart.type) &&
(props.item.deType === 2 || props.item.deType === 3)
)
})

View File

@ -1,5 +1,582 @@
<script setup lang="ts"></script>
<script lang="ts" setup>
import { computed, onMounted, PropType, reactive, watch } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { DEFAULT_TABLE_TOTAL } from '@/views/chart/components/editor/util/chart'
import { cloneDeep, defaultsDeep } from 'lodash-es'
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>
}
})
watch(
[props.chart.customAttr.tableTotal, props.chart.yAxis],
() => {
init()
},
{ deep: true }
)
const fontSizeList = computed(() => {
const arr = []
for (let i = 10; i <= 40; i = i + 2) {
arr.push({
name: i + '',
value: i
})
}
return arr
})
const aggregations = [
{ name: t('chart.sum'), value: 'SUM' },
{ name: t('chart.avg'), value: 'AVG' },
{ name: t('chart.max'), value: 'MAX' },
{ name: t('chart.min'), value: 'MIN' }
]
const state = reactive({
tableTotalForm: cloneDeep(DEFAULT_TABLE_TOTAL) as ChartTableTotalAttr,
rowSubTotalItem: {
dataeaseName: '',
aggregation: ''
} as CalcTotalCfg,
rowTotalItem: {
dataeaseName: '',
aggregation: ''
} as CalcTotalCfg,
colSubTotalItem: {
dataeaseName: '',
aggregation: ''
} as CalcTotalCfg,
colTotalItem: {
dataeaseName: '',
aggregation: ''
} as CalcTotalCfg
})
const emit = defineEmits(['onTableTotalChange'])
const changeTableTotal = prop => {
emit('onTableTotalChange', state.tableTotalForm, prop)
}
const init = () => {
const tableTotal = props.chart?.customAttr?.tableTotal
if (tableTotal) {
state.tableTotalForm = defaultsDeep(cloneDeep(tableTotal), cloneDeep(DEFAULT_TABLE_TOTAL))
}
const yAxis = props.chart.yAxis
if (yAxis?.length > 0) {
const axisArr = yAxis.map(i => i.dataeaseName)
if (axisArr.indexOf(state.tableTotalForm.row.totalSortField) != -1) {
state.tableTotalForm.row.totalSortField = yAxis[0].dataeaseName
}
state.tableTotalForm.col.totalSortField = yAxis[0].dataeaseName
} else {
state.tableTotalForm.row.totalSortField = ''
state.tableTotalForm.col.totalSortField = ''
}
const totals = [
{ ...state.tableTotalForm.row.calcTotals },
{ ...state.tableTotalForm.row.calcSubTotals },
{ ...state.tableTotalForm.col.calcTotals },
{ ...state.tableTotalForm.col.calcSubTotals }
]
totals.forEach(total => {
setupTotalCfg(total.cfg, yAxis)
})
const totalTupleArr: [CalcTotalCfg, CalcTotalCfg[]][] = [
[state.rowTotalItem, state.tableTotalForm.row.calcTotals.cfg],
[state.rowSubTotalItem, state.tableTotalForm.row.calcSubTotals.cfg],
[state.colTotalItem, state.tableTotalForm.col.calcTotals.cfg],
[state.colSubTotalItem, state.tableTotalForm.col.calcSubTotals.cfg]
]
totalTupleArr.forEach(tuple => {
const [total, totalCfg] = tuple
if (!totalCfg.length) {
total.dataeaseName = ''
total.aggregation = ''
return
}
const totalIndex = totalCfg.findIndex(i => i.dataeaseName === total.dataeaseName)
if (totalIndex !== -1) {
total.aggregation = totalCfg[totalIndex].aggregation
} else {
total.dataeaseName = totalCfg[0].dataeaseName
total.aggregation = totalCfg[0].aggregation
}
})
}
const showProperty = prop => props.propertyInner?.includes(prop)
const changeTotal = (totalItem, totals) => {
for (let i = 0; i < totals.length; i++) {
const item = totals[i]
if (item.dataeaseName === totalItem.dataeaseName) {
totalItem.aggregation = item.aggregation
return
}
}
}
const changeTotalAggr = (totalItem, totals, colOrNum) => {
for (let i = 0; i < totals.length; i++) {
const item = totals[i]
if (item.dataeaseName === totalItem.dataeaseName) {
item.aggregation = totalItem.aggregation
break
}
}
changeTableTotal(colOrNum)
}
const setupTotalCfg = (totalCfg, axis) => {
if (!totalCfg.length) {
axis.forEach(i => {
totalCfg.push({
dataeaseName: i.dataeaseName,
aggregation: 'SUM'
})
})
return
}
if (!axis.length) {
totalCfg.splice(0, totalCfg.length)
return
}
const cfgMap = totalCfg.reduce((p, n) => {
p[n.dataeaseName] = n
return p
}, {})
totalCfg.splice(0, totalCfg.length)
axis.forEach(i => {
totalCfg.push({
dataeaseName: i.dataeaseName,
aggregation: cfgMap[i.dataeaseName] ? cfgMap[i.dataeaseName].aggregation : 'SUM'
})
})
}
onMounted(() => {
init()
})
</script>
<template>
<span>to be implement</span>
<el-form ref="tableTotalForm" :model="state.tableTotalForm" label-position="top">
<el-divider v-if="showProperty('row')" content-position="center" class="divider-style">
{{ t('chart.row_cfg') }}
</el-divider>
<el-form-item
v-show="showProperty('row')"
:label="t('chart.total_show')"
class="form-item"
:class="'form-item-' + themes"
>
<el-checkbox
v-model="state.tableTotalForm.row.showGrandTotals"
@change="changeTableTotal('row.showGrandTotals')"
>
{{ t('chart.show') }}
</el-checkbox>
</el-form-item>
<div v-show="showProperty('row') && state.tableTotalForm.row.showGrandTotals">
<el-form-item
:label="t('chart.total_position')"
class="form-item"
:class="'form-item-' + themes"
>
<el-radio-group
v-model="state.tableTotalForm.row.reverseLayout"
@change="changeTableTotal('row.reverseLayout')"
>
<el-radio :label="true">{{ t('chart.total_pos_top') }}</el-radio>
<el-radio :label="false">{{ t('chart.total_pos_bottom') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
:label="t('chart.total_label')"
class="form-item"
:class="'form-item-' + themes"
>
<el-input
v-model="state.tableTotalForm.row.label"
:placeholder="t('chart.total_label')"
clearable
@change="changeTableTotal('row.label')"
/>
</el-form-item>
<el-form-item
:label="t('chart.aggregation')"
class="form-item"
:class="'form-item-' + themes"
>
<el-col :span="11">
<el-select
v-model="state.rowTotalItem.dataeaseName"
:placeholder="t('chart.aggregation')"
@change="changeTotal(state.rowTotalItem, state.tableTotalForm.row.calcTotals.cfg)"
>
<el-option
v-for="option in chart.yAxis"
:key="option.dataeaseName"
:label="option.name"
:value="option.dataeaseName"
/>
</el-select>
</el-col>
<el-col :span="11" :offset="2">
<el-select
v-model="state.rowTotalItem.aggregation"
:placeholder="t('chart.aggregation')"
@change="
changeTotalAggr(
state.rowTotalItem,
state.tableTotalForm.row.calcTotals.cfg,
'row.calcTotals.cfg'
)
"
>
<el-option
v-for="option in aggregations"
:key="option.value"
:label="option.name"
:value="option.value"
/>
</el-select>
</el-col>
</el-form-item>
<el-form-item
v-if="chart.type === 'table-pivot'"
:label="t('chart.total_sort')"
class="form-item"
:class="'form-item-' + themes"
>
<el-radio-group
v-model="state.tableTotalForm.row.totalSort"
@change="changeTableTotal('row.totalSort')"
>
<el-radio label="none">{{ t('chart.total_sort_none') }}</el-radio>
<el-radio label="asc">{{ t('chart.total_sort_asc') }}</el-radio>
<el-radio label="desc">{{ t('chart.total_sort_desc') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="chart.type === 'table-pivot' && state.tableTotalForm.row.totalSort !== 'none'"
:label="t('chart.total_sort_field')"
class="form-item"
:class="'form-item-' + themes"
>
<el-select
v-model="state.tableTotalForm.row.totalSortField"
class="form-item-select"
:placeholder="t('chart.total_sort_field')"
@change="changeTableTotal('row')"
>
<el-option
v-for="option in chart.yAxis"
:key="option.dataeaseName"
:label="option.name"
:value="option.dataeaseName"
/>
</el-select>
</el-form-item>
</div>
<el-form-item
v-show="showProperty('row')"
:label="t('chart.sub_total_show')"
class="form-item"
:class="'form-item-' + themes"
>
<el-checkbox
v-model="state.tableTotalForm.row.showSubTotals"
:disabled="chart.xAxisExt.length < 2"
@change="changeTableTotal('row')"
>{{ t('chart.show') }}</el-checkbox
>
</el-form-item>
<div v-show="showProperty('row') && state.tableTotalForm.row.showSubTotals">
<el-form-item
:label="t('chart.total_position')"
class="form-item"
:class="'form-item-' + themes"
>
<el-radio-group
v-model="state.tableTotalForm.row.reverseSubLayout"
:disabled="chart.xAxisExt.length < 2"
@change="changeTableTotal('row')"
>
<el-radio :label="true">{{ t('chart.total_pos_top') }}</el-radio>
<el-radio :label="false">{{ t('chart.total_pos_bottom') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
:label="t('chart.total_label')"
class="form-item"
:class="'form-item-' + themes"
>
<el-input
v-model="state.tableTotalForm.row.subLabel"
:disabled="chart.xAxisExt.length < 2"
:placeholder="t('chart.total_label')"
clearable
@change="changeTableTotal"
/>
</el-form-item>
<el-form-item
:label="t('chart.aggregation')"
class="form-item"
:class="'form-item-' + themes"
>
<el-col :span="11">
<el-select
v-model="state.rowSubTotalItem.dataeaseName"
:disabled="chart.xAxisExt.length < 2"
:placeholder="t('chart.aggregation')"
@change="changeTotal(state.rowSubTotalItem, state.tableTotalForm.row.calcSubTotals.cfg)"
>
<el-option
v-for="option in chart.yAxis"
:key="option.dataeaseName"
:label="option.name"
:value="option.dataeaseName"
/>
</el-select>
</el-col>
<el-col :span="11" :offset="2">
<el-select
v-model="state.rowSubTotalItem.aggregation"
:disabled="chart.xAxisExt.length < 2"
:placeholder="t('chart.aggregation')"
@change="
changeTotalAggr(
state.rowSubTotalItem,
state.tableTotalForm.row.calcSubTotals.cfg,
'row'
)
"
>
<el-option
v-for="option in aggregations"
:key="option.value"
:label="option.name"
:value="option.value"
/>
</el-select>
</el-col>
</el-form-item>
</div>
<el-divider v-if="showProperty('col')" content-position="center" class="divider-style">{{
t('chart.col_cfg')
}}</el-divider>
<el-form-item
v-show="showProperty('col')"
:label="t('chart.total_show')"
class="form-item"
:class="'form-item-' + themes"
>
<el-checkbox
v-model="state.tableTotalForm.col.showGrandTotals"
@change="changeTableTotal('col')"
>{{ t('chart.show') }}</el-checkbox
>
</el-form-item>
<div v-show="showProperty('col') && state.tableTotalForm.col.showGrandTotals">
<el-form-item
:label="t('chart.total_position')"
class="form-item"
:class="'form-item-' + themes"
>
<el-radio-group
v-model="state.tableTotalForm.col.reverseLayout"
@change="changeTableTotal('col')"
>
<el-radio :label="true">{{ t('chart.total_pos_left') }}</el-radio>
<el-radio :label="false">{{ t('chart.total_pos_right') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
:label="t('chart.total_label')"
class="form-item"
:class="'form-item-' + themes"
>
<el-input
v-model="state.tableTotalForm.col.label"
:placeholder="t('chart.total_label')"
clearable
@change="changeTableTotal('col')"
/>
</el-form-item>
<el-form-item
:label="t('chart.aggregation')"
class="form-item"
:class="'form-item-' + themes"
>
<el-col :span="11">
<el-select
v-model="state.colTotalItem.dataeaseName"
:placeholder="t('chart.aggregation')"
@change="changeTotal(state.colTotalItem, state.tableTotalForm.col.calcTotals.cfg)"
>
<el-option
v-for="option in chart.yAxis"
:key="option.dataeaseName"
:label="option.name"
:value="option.dataeaseName"
/>
</el-select>
</el-col>
<el-col :span="11" :offset="2">
<el-select
v-model="state.colTotalItem.aggregation"
:placeholder="t('chart.aggregation')"
@change="
changeTotalAggr(
state.colTotalItem,
state.tableTotalForm.col.calcTotals.cfg,
'col.calcTotals.cfg'
)
"
>
<el-option
v-for="option in aggregations"
:key="option.value"
:label="option.name"
:value="option.value"
/>
</el-select>
</el-col>
</el-form-item>
<el-form-item
v-if="chart.type === 'table-pivot'"
:label="t('chart.total_sort')"
class="form-item"
:class="'form-item-' + themes"
>
<el-radio-group
v-model="state.tableTotalForm.col.totalSort"
@change="changeTableTotal('col')"
>
<el-radio label="none">{{ t('chart.total_sort_none') }}</el-radio>
<el-radio label="asc">{{ t('chart.total_sort_asc') }}</el-radio>
<el-radio label="desc">{{ t('chart.total_sort_desc') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-show="
false && chart.type === 'table-pivot' && state.tableTotalForm.col?.totalSort !== 'none'
"
:label="t('chart.total_sort_field')"
class="form-item"
>
<el-select
v-model="state.tableTotalForm.col.totalSortField"
class="form-item-select"
:placeholder="t('chart.total_sort_field')"
@change="changeTableTotal('col')"
>
<el-option
v-for="option in chart.yAxis"
:key="option.dataeaseName"
:label="option.name"
:value="option.dataeaseName"
/>
</el-select>
</el-form-item>
</div>
<el-form-item
v-show="showProperty('col')"
:label="t('chart.sub_total_show')"
class="form-item"
:class="'form-item-' + themes"
>
<el-checkbox
v-model="state.tableTotalForm.col.showSubTotals"
:disabled="chart.xAxis.length < 2"
@change="changeTableTotal('col')"
>{{ t('chart.show') }}</el-checkbox
>
</el-form-item>
<div v-show="showProperty('col') && state.tableTotalForm.col.showSubTotals">
<el-form-item
:label="t('chart.total_position')"
class="form-item"
:class="'form-item-' + themes"
>
<el-radio-group
v-model="state.tableTotalForm.col.reverseSubLayout"
:disabled="chart.xAxis?.length < 2"
@change="changeTableTotal('col')"
>
<el-radio :label="true">{{ t('chart.total_pos_left') }}</el-radio>
<el-radio :label="false">{{ t('chart.total_pos_right') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
:label="t('chart.total_label')"
class="form-item"
:class="'form-item-' + themes"
>
<el-input
v-model="state.tableTotalForm.col.subLabel"
:disabled="chart.xAxis?.length < 2"
:placeholder="t('chart.total_label')"
clearable
@change="changeTableTotal('col')"
/>
</el-form-item>
<el-form-item
:label="t('chart.aggregation')"
class="form-item"
:class="'form-item-' + themes"
>
<el-col :span="11">
<el-select
v-model="state.colSubTotalItem.dataeaseName"
:disabled="chart.xAxis?.length < 2"
:placeholder="t('chart.aggregation')"
@change="changeTotal(state.colSubTotalItem, state.tableTotalForm.col.calcSubTotals.cfg)"
>
<el-option
v-for="option in chart.yAxis"
:key="option.dataeaseName"
:label="option.name"
:value="option.dataeaseName"
/>
</el-select>
</el-col>
<el-col :span="11" :offset="2">
<el-select
v-model="state.colSubTotalItem.aggregation"
:disabled="chart.xAxis?.length < 2"
:placeholder="t('chart.aggregation')"
@change="
changeTotalAggr(
state.colSubTotalItem,
state.tableTotalForm.col.calcSubTotals.cfg,
'col.calcSubTotals.cfg'
)
"
>
<el-option
v-for="option in aggregations"
:key="option.value"
:label="option.name"
:value="option.value"
/>
</el-select>
</el-col>
</el-form-item>
</div>
</el-form>
</template>
<style scoped lang="less"></style>
<style scoped></style>

View File

@ -289,10 +289,12 @@ export const DEFAULT_TABLE_TOTAL: ChartTableTotalAttr = {
subLabel: '小计',
subTotalsDimensions: [],
calcTotals: {
aggregation: 'SUM'
aggregation: 'SUM',
cfg: []
},
calcSubTotals: {
aggregation: 'SUM'
aggregation: 'SUM',
cfg: []
},
totalSort: 'none',
totalSortField: ''
@ -306,10 +308,12 @@ export const DEFAULT_TABLE_TOTAL: ChartTableTotalAttr = {
subLabel: '小计',
subTotalsDimensions: [],
calcTotals: {
aggregation: 'SUM'
aggregation: 'SUM',
cfg: []
},
calcSubTotals: {
aggregation: 'SUM'
aggregation: 'SUM',
cfg: []
},
totalSort: 'none', // asc,desc
totalSortField: ''
@ -1013,6 +1017,13 @@ export const CHART_TYPE_CONFIGS = [
value: 'table-normal',
title: t('chart.chart_table_normal'),
icon: 'table-normal'
},
{
render: 'antv',
category: 'table',
value: 'table-pivot',
title: t('chart.chart_table_pivot'),
icon: 'table-pivot'
}
]
},

View File

@ -0,0 +1,337 @@
import { EXTRA_FIELD, PivotSheet, S2Event, S2Options, TOTAL_VALUE } from '@antv/s2/esm/index'
import { formatterItem, valueFormatter } from '../../../formatter'
import { hexColorToRGBA, parseJson } from '../../../util'
import { S2ChartView, S2DrawOptions } from '../../types/impl/s2'
import { TABLE_EDITOR_PROPERTY_INNER } from './common'
import { useI18n } from '@/hooks/web/useI18n'
import { maxBy, merge, minBy } from 'lodash-es'
import { S2Theme } from '@antv/s2'
const { t } = useI18n()
/**
* 透视表
*/
export class TablePivot extends S2ChartView<PivotSheet> {
properties: EditorProperty[] = [
'background-overall-component',
'basic-style-selector',
'table-header-selector',
'table-cell-selector',
'table-total-selector',
'title-selector',
'function-cfg',
'threshold',
'linkage',
'jump-set'
]
propertyInner = {
...TABLE_EDITOR_PROPERTY_INNER,
'table-header-selector': [
'tableHeaderBgColor',
'tableTitleFontSize',
'tableHeaderFontColor',
'tableTitleHeight',
'tableHeaderAlign'
],
'table-total-selector': ['row', 'col'],
'basic-style-selector': ['tableColumnMode', 'tableBorderColor', 'tableScrollBarColor', 'alpha']
}
axis: AxisType[] = ['xAxis', 'xAxisExt', 'yAxis', 'filter']
axisConfig: AxisConfig = {
xAxis: {
name: `${t('chart.table_pivot_row')} / ${t('chart.dimension')}`,
type: 'd'
},
xAxisExt: {
name: `${t('chart.drag_block_table_data_column')} / ${t('chart.dimension')}`,
type: 'd'
},
yAxis: {
name: `${t('chart.drag_block_table_data_column')} / ${t('chart.quota')}`,
type: 'q'
}
}
public drawChart(drawOption: S2DrawOptions<PivotSheet>): PivotSheet {
const { container, chart, chartObj, action } = drawOption
const containerDom = document.getElementById(container)
const { xAxis: columnFields, xAxisExt: rowFields, yAxis: valueFields } = chart
const [c, r, v] = [columnFields, rowFields, valueFields].map(arr =>
arr.map(i => i.dataeaseName)
)
// fields
const fields = chart.data.fields
if (!fields || fields.length === 0) {
if (chartObj) {
chartObj.destroy()
}
return
}
const columns = []
const meta = []
const valueFieldMap: Record<string, Axis> = chart.yAxis.reduce((p, n) => {
p[n.dataeaseName] = n
return p
}, {})
fields.forEach(ele => {
const f = valueFieldMap[ele.dataeaseName]
columns.push(ele.dataeaseName)
meta.push({
field: ele.dataeaseName,
name: ele.name,
formatter: value => {
if (!f) {
return value
}
if (value === null || value === undefined) {
return value
}
if (f.formatterCfg) {
return valueFormatter(value, f.formatterCfg)
} else {
return valueFormatter(value, formatterItem)
}
}
})
})
// total config
const customAttr = parseJson(chart.customAttr)
const { tableTotal } = customAttr
tableTotal.row.subTotalsDimensions = r
tableTotal.col.subTotalsDimensions = c
// 解析合计小计排序
const sortParams = []
if (
tableTotal.row.totalSort &&
tableTotal.row.totalSort !== 'none' &&
c.length > 0 &&
tableTotal.row.showGrandTotals &&
v.indexOf(tableTotal.row.totalSortField) > -1
) {
const sort = {
sortFieldId: c[0],
sortMethod: tableTotal.row.totalSort.toUpperCase(),
sortByMeasure: TOTAL_VALUE,
query: {
[EXTRA_FIELD]: tableTotal.row.totalSortField
}
}
sortParams.push(sort)
}
if (
tableTotal.col.totalSort &&
tableTotal.col.totalSort !== 'none' &&
r.length > 0 &&
tableTotal.col.showGrandTotals &&
v.indexOf(tableTotal.col.totalSortField) > -1
) {
const sort = {
sortFieldId: r[0],
sortMethod: tableTotal.col.totalSort.toUpperCase(),
sortByMeasure: TOTAL_VALUE,
query: {
[EXTRA_FIELD]: tableTotal.col.totalSortField
}
}
sortParams.push(sort)
}
// 自定义总计小计
const totals = [
tableTotal.row.calcTotals,
tableTotal.row.calcSubTotals,
tableTotal.col.calcTotals,
tableTotal.col.calcSubTotals
]
totals.forEach(total => {
if (total.cfg?.length) {
delete total.aggregation
const totalCfgMap = total.cfg.reduce((p, n) => {
p[n.dataeaseName] = n
return p
}, {})
total.calcFunc = (query, data) => {
return customCalcFunc(query, data, totalCfgMap)
}
}
})
// 空值处理
const newData = this.configEmptyDataStrategy(chart)
// data config
const s2DataConfig = {
fields: {
rows: r,
columns: c,
values: v
},
meta: meta,
data: newData,
sortParams: sortParams
}
// options
const s2Options = {
width: containerDom.offsetWidth,
height: containerDom.offsetHeight,
style: this.configStyle(chart),
totals: tableTotal,
conditions: this.configConditions(chart)
}
// 开始渲染
const s2 = new PivotSheet(containerDom, s2DataConfig, s2Options as unknown as S2Options)
// click
s2.on(S2Event.DATA_CELL_CLICK, ev => this.dataCellClickAction(chart, ev, s2, action))
s2.on(S2Event.ROW_CELL_CLICK, ev => this.headerCellClickAction(chart, ev, s2, action))
s2.on(S2Event.COL_CELL_CLICK, ev => this.headerCellClickAction(chart, ev, s2, action))
// theme
const customTheme = this.configTheme(chart)
s2.setThemeCfg({ theme: customTheme })
return s2
}
private dataCellClickAction(chart: Chart, ev, s2Instance: PivotSheet, callback) {
const cell = s2Instance.getCell(ev.target)
const meta = cell.getMeta()
const nameIdMap = chart.data.fields.reduce((pre, next) => {
pre[next['dataeaseName']] = next['id']
return pre
}, {})
const rowData = { ...meta.rowQuery, ...meta.colQuery }
rowData[meta.valueField] = meta.fieldValue
const dimensionList = []
for (const key in rowData) {
if (nameIdMap[key]) {
dimensionList.push({ id: nameIdMap[key], value: rowData[key] })
}
}
const param = {
x: ev.x,
y: ev.y,
data: {
dimensionList,
name: nameIdMap[meta.valueField],
sourceType: 'table-pivot',
quotaList: []
}
}
callback(param)
}
private headerCellClickAction(chart: Chart, ev, s2Instance: PivotSheet, callback) {
const cell = s2Instance.getCell(ev.target)
const meta = cell.getMeta()
const rowData = meta.query
const nameIdMap = chart.data.fields.reduce((pre, next) => {
pre[next['dataeaseName']] = next['id']
return pre
}, {})
const dimensionList = []
for (const key in rowData) {
if (nameIdMap[key]) {
dimensionList.push({ id: nameIdMap[key], value: rowData[key] })
}
}
const param = {
x: ev.x,
y: ev.y,
data: {
dimensionList,
name: nameIdMap[meta.valueField],
sourceType: 'table-pivot',
quotaList: []
}
}
callback(param)
}
protected configTheme(chart: Chart): S2Theme {
const theme = super.configTheme(chart)
const { basicStyle, tableHeader } = parseJson(chart.customAttr)
const tableHeaderBgColor = hexColorToRGBA(tableHeader.tableHeaderBgColor, basicStyle.alpha)
const tableBorderColor = hexColorToRGBA(basicStyle.tableBorderColor, basicStyle.alpha)
const tableHeaderFontColor = hexColorToRGBA(tableHeader.tableHeaderFontColor, basicStyle.alpha)
const pivotTheme = {
cornerCell: {
cell: {
verticalBorderWidth: 1
}
},
rowCell: {
cell: {
backgroundColor: tableHeaderBgColor,
horizontalBorderColor: tableBorderColor,
verticalBorderColor: tableBorderColor
},
text: {
fill: tableHeaderFontColor,
fontSize: tableHeader.tableTitleFontSize,
textAlign: tableHeader.tableHeaderAlign,
textBaseline: 'top'
},
bolderText: {
fill: tableHeaderFontColor,
fontSize: tableHeader.tableTitleFontSize,
textAlign: tableHeader.tableHeaderAlign
},
measureText: {
fill: tableHeaderFontColor,
fontSize: tableHeader.tableTitleFontSize,
textAlign: tableHeader.tableHeaderAlign
},
seriesText: {
fill: tableHeaderFontColor,
fontSize: tableHeader.tableTitleFontSize,
textAlign: tableHeader.tableHeaderAlign
}
}
}
merge(theme, pivotTheme)
return theme
}
constructor() {
super('table-pivot', [])
}
}
function customCalcFunc(query, data, totalCfgMap) {
if (!data?.length) {
return 0
}
const aggregation = totalCfgMap[query[EXTRA_FIELD]].aggregation
switch (aggregation) {
case 'SUM': {
return data.reduce((p, n) => {
return p + n[n[EXTRA_FIELD]]
}, 0)
}
case 'AVG': {
const sum = data.reduce((p, n) => {
return p + n[n[EXTRA_FIELD]]
}, 0)
return sum / data.length
}
case 'MIN': {
const result = minBy(data, n => {
return n[n[EXTRA_FIELD]]
})
return result[result[EXTRA_FIELD]]
}
case 'MAX': {
const result = maxBy(data, n => {
return n[n[EXTRA_FIELD]]
})
return result[result[EXTRA_FIELD]]
}
default: {
return data.reduce((p, n) => {
return p + n[n[EXTRA_FIELD]]
}, 0)
}
}
}