forked from github/dataease
feat(视图-透视表): 支持透视表
This commit is contained in:
parent
2342eb240e
commit
b8f2f4fad0
@ -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(() => {
|
||||
|
@ -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' | ''
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
)
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user