mirror of
https://github.com/dataease/dataease.git
synced 2026-06-17 13:01:44 +08:00
feat(图表): 支持百分比堆叠柱状图
This commit is contained in:
committed by
jianneng-fit2cloud
parent
04330f4d9f
commit
9dcb1f5e3c
@@ -7,15 +7,26 @@ import {
|
||||
} from '@/views/chart/components/js/panel/charts/g2/bar/common'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { flow, hexColorToRGBA, hexToRgba, parseJson } from '@/views/chart/components/js/util'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { cloneDeep, defaultsDeep, isEmpty } from 'lodash-es'
|
||||
import { valueFormatter } from '@/views/chart/components/js/formatter'
|
||||
import { getLineDash, setGradientColor } from '@/views/chart/components/js/panel/common/common_antv'
|
||||
import {
|
||||
getLineDash,
|
||||
setGradientColor,
|
||||
TOOLTIP_ITEM_TPL,
|
||||
TOOLTIP_TITLE_TPL
|
||||
} from '@/views/chart/components/js/panel/common/common_antv'
|
||||
import {
|
||||
DEFAULT_YAXIS_EXT_STYLE,
|
||||
DEFAULT_YAXIS_STYLE
|
||||
} from '@/views/chart/components/editor/util/chart'
|
||||
import _ from 'lodash'
|
||||
import { Transform, ViewSpec } from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
|
||||
import {
|
||||
configTooltip,
|
||||
createTooltipWrapper,
|
||||
tooltipCss,
|
||||
Transform,
|
||||
ViewSpec
|
||||
} from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const DEFAULT_DATA: any[] = []
|
||||
@@ -99,6 +110,7 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
|
||||
const newChart = new G2Column({ container, autoFit: true })
|
||||
newChart.options(options)
|
||||
newChart.on('interval:click', action)
|
||||
configTooltip(newChart, chart)
|
||||
return newChart
|
||||
}
|
||||
|
||||
@@ -121,10 +133,6 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
|
||||
dy: l.position === 'top' ? -10 : 0,
|
||||
dx: 0
|
||||
}
|
||||
// contrastReverse 标签颜色在图形背景上对比度低的情况下,从指定色板选择一个对比度最优的颜色
|
||||
// overlapDodgeY 对位置碰撞的标签在 y 方向上进行调整,防止标签重叠
|
||||
// exceedAdjust 自动对标签做溢出检测和矫正,即当标签超出视图区域时,会对标签自动做反方向的位移
|
||||
// overlapHide 对位置碰撞的标签进行隐藏,默认保留前一个,隐藏后一个
|
||||
const transform = {
|
||||
transform: [{ type: 'exceedAdjust' }, { type: 'overlapHide' }]
|
||||
}
|
||||
@@ -177,7 +185,77 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
|
||||
}
|
||||
}
|
||||
|
||||
protected configTooltip(_chart: Chart, options: ViewSpec): ViewSpec {
|
||||
protected configTooltip(chart: Chart, options: ViewSpec): ViewSpec {
|
||||
const customAttr: DeepPartial<ChartAttr> = parseJson(chart.customAttr)
|
||||
const tooltipAttr = customAttr.tooltip
|
||||
const yAxis = chart.yAxis
|
||||
if (!tooltipAttr.show) {
|
||||
return {
|
||||
...options,
|
||||
tooltip: false
|
||||
}
|
||||
}
|
||||
const formatterMap = tooltipAttr.seriesTooltipFormatter
|
||||
?.filter(i => i.show)
|
||||
.reduce((pre, next) => {
|
||||
pre[next.id] = next
|
||||
return pre
|
||||
}, {}) as Record<string, SeriesFormatter>
|
||||
const tooltipOptions: ViewSpec = {
|
||||
tooltip: d => d,
|
||||
interaction: {
|
||||
tooltip: {
|
||||
mount: createTooltipWrapper(chart),
|
||||
css: tooltipCss(tooltipAttr),
|
||||
enterable: true,
|
||||
bounding: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
position: 'top-right',
|
||||
render: (_, { title, items: originalItems }) => {
|
||||
const titleHtml = TOOLTIP_TITLE_TPL.replace('{title}', title)
|
||||
let tooltipItems = originalItems
|
||||
if (tooltipAttr.seriesTooltipFormatter?.length) {
|
||||
tooltipItems = originalItems.filter(item => formatterMap[item.quotaList[0].id])
|
||||
}
|
||||
const result = []
|
||||
const head = originalItems[0]
|
||||
tooltipItems.forEach(item => {
|
||||
const formatter = formatterMap[item.quotaList[0].id] ?? yAxis[0]
|
||||
const value = valueFormatter(item.value, formatter.formatterCfg)
|
||||
const name = isEmpty(formatter.chartShowName)
|
||||
? formatter.name
|
||||
: formatter.chartShowName
|
||||
result.push({ ...item, name, value })
|
||||
})
|
||||
head.dynamicTooltipValue?.forEach(item => {
|
||||
const formatter = formatterMap[item.fieldId]
|
||||
if (formatter) {
|
||||
const value = valueFormatter(parseFloat(item.value), formatter.formatterCfg)
|
||||
const name = isEmpty(formatter.chartShowName)
|
||||
? formatter.name
|
||||
: formatter.chartShowName
|
||||
result.push({ color: 'grey', name, value })
|
||||
}
|
||||
})
|
||||
const itemsHtml = result
|
||||
.map(item => {
|
||||
const marker = item.color
|
||||
const label = item.name
|
||||
const value = item.value
|
||||
return TOOLTIP_ITEM_TPL.replace('{marker}', marker)
|
||||
.replace('{label}', label)
|
||||
.replace('{value}', value)
|
||||
})
|
||||
.join('')
|
||||
const listHtml = `<ul class="g2-tooltip-list" style="margin: 0px; list-style-type: none; padding: 0px;">${itemsHtml}</ul>`
|
||||
return `${titleHtml}${listHtml}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultsDeep(options.children[0], tooltipOptions)
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -300,7 +378,9 @@ export class Bar extends G2ChartView<ViewSpec, G2Column> {
|
||||
navPageNumFill: legendColor,
|
||||
navButtonSize: legendSize,
|
||||
navOrientation:
|
||||
position === 'left' || position === 'right' ? 'vertical' : 'horizontal'
|
||||
position === 'left' || position === 'right' ? 'vertical' : 'horizontal',
|
||||
maxRows: 1,
|
||||
navControllerSpacing: 20
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parseJson } from '@/views/chart/components/js/util'
|
||||
import { G2Spec } from '@antv/g2'
|
||||
import { Chart as G2Chart, G2Spec } from '@antv/g2'
|
||||
|
||||
export type ViewSpec = { children?: G2Spec[]; [key: string]: any } & G2Spec
|
||||
export type Transform = {
|
||||
@@ -182,3 +182,67 @@ function handleIgnoreData(data: Record<string, any>[]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function tooltipWrapperId(container: string) {
|
||||
return 'G2-TOOLTIP-WRAPPER-' + container
|
||||
}
|
||||
|
||||
export function createTooltipWrapper(chart: Chart) {
|
||||
const wrapperId = tooltipWrapperId(chart.container)
|
||||
let g2TooltipWrapper = document.getElementById(wrapperId)
|
||||
if (!g2TooltipWrapper) {
|
||||
g2TooltipWrapper = document.createElement('div')
|
||||
g2TooltipWrapper.id = wrapperId
|
||||
g2TooltipWrapper.style.position = 'absolute'
|
||||
g2TooltipWrapper.style.pointerEvents = 'none'
|
||||
g2TooltipWrapper.style.zIndex = '9999'
|
||||
g2TooltipWrapper.style.top = '0px'
|
||||
document.body.appendChild(g2TooltipWrapper)
|
||||
}
|
||||
return g2TooltipWrapper
|
||||
}
|
||||
|
||||
export function tooltipCss(tooltipAttr: DeepPartial<ChartTooltipAttr>) {
|
||||
return {
|
||||
'.g2-tooltip': {
|
||||
background: tooltipAttr.backgroundColor,
|
||||
'max-height': '50vh',
|
||||
'overflow-y': 'auto',
|
||||
position: 'fixed',
|
||||
top: '0px'
|
||||
},
|
||||
'.g2-tooltip-title': {
|
||||
color: tooltipAttr.color,
|
||||
'font-size': `${tooltipAttr.fontSize}px`
|
||||
},
|
||||
'.g2-tooltip-list-item-name-label': {
|
||||
color: tooltipAttr.color,
|
||||
'font-size': `${tooltipAttr.fontSize}px`
|
||||
},
|
||||
'.g2-tooltip-list-item-value': {
|
||||
color: tooltipAttr.color,
|
||||
'font-size': `${tooltipAttr.fontSize}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function configTooltip(newChart: G2Chart, chart: Chart) {
|
||||
newChart.on('tooltip:show', event => {
|
||||
const tooltipWrapper = document.getElementById(tooltipWrapperId(chart.container))
|
||||
const allTooltips = tooltipWrapper?.querySelectorAll('.g2-tooltip')
|
||||
if (!allTooltips) return
|
||||
allTooltips.forEach(item => {
|
||||
const tooltip = item as HTMLElement
|
||||
const tooltipMouseleave = () => {
|
||||
tooltip.style.visibility = 'hidden'
|
||||
}
|
||||
tooltip.removeEventListener('mouseleave', tooltipMouseleave)
|
||||
tooltip.addEventListener('mouseleave', tooltipMouseleave)
|
||||
if (event.client.y < tooltip.getBoundingClientRect().height) {
|
||||
tooltip.style.top = '0px'
|
||||
} else {
|
||||
tooltip.style.top = `${event.client.y - tooltip.getBoundingClientRect().height}px`
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,8 +4,18 @@ import {
|
||||
} from '@/views/chart/components/js/panel/charts/g2/bar/common'
|
||||
import { flow, parseJson } from '@/views/chart/components/js/util'
|
||||
import { StackBar } from '@/views/chart/components/js/panel/charts/g2/bar/stack-bar'
|
||||
import { Transform, ViewSpec } from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
|
||||
import {
|
||||
createTooltipWrapper,
|
||||
tooltipCss,
|
||||
Transform,
|
||||
ViewSpec
|
||||
} from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
|
||||
import { valueFormatter } from '@/views/chart/components/js/formatter'
|
||||
import {
|
||||
TOOLTIP_ITEM_TPL,
|
||||
TOOLTIP_TITLE_TPL
|
||||
} from '@/views/chart/components/js/panel/common/common_antv'
|
||||
import { defaultsDeep, isEmpty } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* 分组堆叠柱状图
|
||||
@@ -52,6 +62,61 @@ export class GroupStackBar extends StackBar {
|
||||
]
|
||||
}
|
||||
}
|
||||
protected configTooltip(chart: Chart, options: ViewSpec): ViewSpec {
|
||||
const { tooltip } = parseJson(chart.customAttr)
|
||||
if (!tooltip.show) {
|
||||
return {
|
||||
...options,
|
||||
tooltip: false
|
||||
}
|
||||
}
|
||||
const tooltipMap = function (a) {
|
||||
return a
|
||||
}
|
||||
tooltipMap.title = undefined
|
||||
|
||||
const tooltipOptions: ViewSpec = {
|
||||
tooltip: tooltipMap,
|
||||
interaction: {
|
||||
tooltip: {
|
||||
mount: createTooltipWrapper(chart),
|
||||
css: tooltipCss(tooltip),
|
||||
enterable: true,
|
||||
bounding: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
position: 'top-right',
|
||||
render: (_, { title, items: originalItems }) => {
|
||||
const titleHtml = TOOLTIP_TITLE_TPL.replace('{title}', title)
|
||||
const tooltipItems = originalItems
|
||||
const result = []
|
||||
tooltipItems.forEach(item => {
|
||||
const value = valueFormatter(item.value, tooltip.tooltipFormatter)
|
||||
const name = `${isEmpty(item.category) ? item.field : item.category}${
|
||||
item.group ? '-' + item.group : ''
|
||||
}`
|
||||
result.push({ ...item, name, value })
|
||||
})
|
||||
const itemsHtml = result
|
||||
.map(item => {
|
||||
const marker = item.color
|
||||
const label = item.name
|
||||
const value = item.value
|
||||
return TOOLTIP_ITEM_TPL.replace('{marker}', marker)
|
||||
.replace('{label}', label)
|
||||
.replace('{value}', value)
|
||||
})
|
||||
.join('')
|
||||
const listHtml = `<ul class="g2-tooltip-list" style="margin: 0px; list-style-type: none; padding: 0px;">${itemsHtml}</ul>`
|
||||
return `${titleHtml}${listHtml}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultsDeep(options.children[0], tooltipOptions)
|
||||
return options
|
||||
}
|
||||
|
||||
protected setupOptions(chart: Chart, options: ViewSpec): ViewSpec {
|
||||
return flow(
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { BAR_AXIS_TYPE } from '@/views/chart/components/js/panel/charts/g2/bar/common'
|
||||
import { flow, parseJson } from '@/views/chart/components/js/util'
|
||||
import {
|
||||
createTooltipWrapper,
|
||||
tooltipCss,
|
||||
ViewSpec
|
||||
} from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
|
||||
import { GroupStackBar } from '@/views/chart/components/js/panel/charts/g2/bar/group-stack-bar'
|
||||
import {
|
||||
TOOLTIP_ITEM_TPL,
|
||||
TOOLTIP_TITLE_TPL
|
||||
} from '@/views/chart/components/js/panel/common/common_antv'
|
||||
import { defaultsDeep, isEmpty } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* 百分比堆叠柱状图
|
||||
*/
|
||||
export class PercentageStackBar extends GroupStackBar {
|
||||
propertyInner = {
|
||||
...this['propertyInner'],
|
||||
'label-selector': ['color', 'fontSize', 'vPosition', 'reserveDecimalCount'],
|
||||
'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'show', 'carousel']
|
||||
}
|
||||
|
||||
protected configLabel(chart: Chart, options: ViewSpec): ViewSpec {
|
||||
const customAttr = parseJson(chart.customAttr)
|
||||
const { label: labelAttr } = customAttr
|
||||
if (!labelAttr || !labelAttr.show) return options
|
||||
|
||||
const { children } = options
|
||||
const position = {
|
||||
position: labelAttr.position === 'middle' ? 'inside' : labelAttr.position,
|
||||
textAlign: 'center',
|
||||
dy: labelAttr.position === 'top' ? -10 : 0,
|
||||
dx: 0
|
||||
}
|
||||
const transform = labelAttr.fullDisplay
|
||||
? {}
|
||||
: { transform: [{ type: 'exceedAdjust' }, { type: 'overlapHide' }] }
|
||||
|
||||
const label = {
|
||||
text: 'value',
|
||||
fillOpacity: 1,
|
||||
fill: labelAttr.color,
|
||||
fontSize: labelAttr.fontSize,
|
||||
...position,
|
||||
formatter: (value, _data, _, o) => {
|
||||
// 计算与当前数据相同 field 的 value 总和
|
||||
const sum =
|
||||
o?.reduce(
|
||||
(acc, item) => (item.field === _data.field ? acc + (item.value || 0) : acc),
|
||||
0
|
||||
) || 1
|
||||
// 返回百分比格式化结果
|
||||
return `${((value / sum) * 100).toFixed(labelAttr.reserveDecimalCount)}%`
|
||||
},
|
||||
...transform
|
||||
}
|
||||
return {
|
||||
...options,
|
||||
children: [
|
||||
{
|
||||
...children[0],
|
||||
labels: [label]
|
||||
},
|
||||
...children.slice(1)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected configTooltip(chart: Chart, options: ViewSpec): ViewSpec {
|
||||
const { tooltip } = parseJson(chart.customAttr)
|
||||
if (!tooltip.show) {
|
||||
return {
|
||||
...options,
|
||||
tooltip: false
|
||||
}
|
||||
}
|
||||
const tooltipMap = function (a) {
|
||||
return a
|
||||
}
|
||||
tooltipMap.title = undefined
|
||||
|
||||
const tooltipOptions: ViewSpec = {
|
||||
tooltip: tooltipMap,
|
||||
interaction: {
|
||||
tooltip: {
|
||||
mount: createTooltipWrapper(chart),
|
||||
css: tooltipCss(tooltip),
|
||||
enterable: true,
|
||||
bounding: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
position: 'top-right',
|
||||
render: (_, { title, items: originalItems }) => {
|
||||
const titleHtml = TOOLTIP_TITLE_TPL.replace('{title}', title)
|
||||
const tooltipItems = originalItems
|
||||
const sum = tooltipItems?.reduce(
|
||||
(acc, { value = 0 }: { value: number }) => acc + value,
|
||||
0
|
||||
)
|
||||
const result = []
|
||||
tooltipItems.forEach(item => {
|
||||
const itemValue = item.value ? (item.value as number) : 0
|
||||
const value = `${((itemValue / sum) * 100).toFixed(
|
||||
tooltip.tooltipFormatter.decimalCount
|
||||
)}%`
|
||||
const name = `${isEmpty(item.category) ? item.field : item.category}${
|
||||
item.group ? '-' + item.group : ''
|
||||
}`
|
||||
result.push({ ...item, name, value })
|
||||
})
|
||||
const itemsHtml = result
|
||||
.map(item => {
|
||||
const marker = item.color
|
||||
const label = item.name
|
||||
const value = item.value
|
||||
return TOOLTIP_ITEM_TPL.replace('{marker}', marker)
|
||||
.replace('{label}', label)
|
||||
.replace('{value}', value)
|
||||
})
|
||||
.join('')
|
||||
const listHtml = `<ul class="g2-tooltip-list" style="margin: 0px; list-style-type: none; padding: 0px;">${itemsHtml}</ul>`
|
||||
return `${titleHtml}${listHtml}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultsDeep(options.children[0], tooltipOptions)
|
||||
return options
|
||||
}
|
||||
|
||||
protected setupOptions(chart: Chart, options: ViewSpec): ViewSpec {
|
||||
return flow(
|
||||
this.configTheme,
|
||||
this.configEmptyDataStrategy,
|
||||
this.configColor,
|
||||
this.configBasicStyle,
|
||||
this.configLabel,
|
||||
this.configTooltip,
|
||||
this.configLegend,
|
||||
this.configXAxis,
|
||||
this.configYAxis,
|
||||
this.configAnalyse,
|
||||
this.configBarConditions
|
||||
)(chart, options, {}, this)
|
||||
}
|
||||
|
||||
constructor(name = 'percentage-bar-stack') {
|
||||
super(name)
|
||||
this.intervalOptions.encode = {
|
||||
...this.intervalOptions.encode,
|
||||
series: d => d.group
|
||||
}
|
||||
this.intervalOptions.transform = [{ type: 'stackY' }, { type: 'normalizeY' }]
|
||||
this.axis = [...BAR_AXIS_TYPE, 'extStack']
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,16 @@ import { Bar } from '@/views/chart/components/js/panel/charts/g2/bar/bar'
|
||||
import { formatterItem, valueFormatter } from '@/views/chart/components/js/formatter'
|
||||
import { groupBy } from 'lodash'
|
||||
import {
|
||||
createTooltipWrapper,
|
||||
handleEmptyDataStrategy,
|
||||
tooltipCss,
|
||||
ViewSpec
|
||||
} from '@/views/chart/components/js/panel/charts/g2/bar/barUtil'
|
||||
import {
|
||||
TOOLTIP_ITEM_TPL,
|
||||
TOOLTIP_TITLE_TPL
|
||||
} from '@/views/chart/components/js/panel/common/common_antv'
|
||||
import { defaultsDeep, isEmpty } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* 堆叠柱状图
|
||||
@@ -98,7 +105,57 @@ export class StackBar extends Bar {
|
||||
}
|
||||
}
|
||||
|
||||
protected configTooltip(_chart: Chart, options: ViewSpec): ViewSpec {
|
||||
protected configTooltip(chart: Chart, options: ViewSpec): ViewSpec {
|
||||
const { tooltip } = parseJson(chart.customAttr)
|
||||
if (!tooltip.show) {
|
||||
return {
|
||||
...options,
|
||||
tooltip: false
|
||||
}
|
||||
}
|
||||
const tooltipMap = function (a) {
|
||||
return a
|
||||
}
|
||||
tooltipMap.title = undefined
|
||||
|
||||
const tooltipOptions: ViewSpec = {
|
||||
tooltip: tooltipMap,
|
||||
interaction: {
|
||||
tooltip: {
|
||||
mount: createTooltipWrapper(chart),
|
||||
css: tooltipCss(tooltip),
|
||||
enterable: true,
|
||||
bounding: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
position: 'top-right',
|
||||
render: (_, { title, items: originalItems }) => {
|
||||
const titleHtml = TOOLTIP_TITLE_TPL.replace('{title}', title)
|
||||
const tooltipItems = originalItems
|
||||
const result = []
|
||||
tooltipItems.forEach(item => {
|
||||
const value = valueFormatter(item.value, tooltip.tooltipFormatter)
|
||||
const name = isEmpty(item.category) ? item.field : item.category
|
||||
result.push({ ...item, name, value })
|
||||
})
|
||||
const itemsHtml = result
|
||||
.map(item => {
|
||||
const marker = item.color
|
||||
const label = item.name
|
||||
const value = item.value
|
||||
return TOOLTIP_ITEM_TPL.replace('{marker}', marker)
|
||||
.replace('{label}', label)
|
||||
.replace('{value}', value)
|
||||
})
|
||||
.join('')
|
||||
const listHtml = `<ul class="g2-tooltip-list" style="margin: 0px; list-style-type: none; padding: 0px;">${itemsHtml}</ul>`
|
||||
return `${titleHtml}${listHtml}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultsDeep(options.children[0], tooltipOptions)
|
||||
return options
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user