import {
|
G2PlotChartView,
|
G2PlotDrawOptions
|
} from '@/views/chart/components/js/panel/types/impl/g2plot'
|
import { cloneDeep, defaults, defaultTo, isEmpty, map } from 'lodash-es'
|
import {
|
configAxisLabelLengthLimit,
|
configPlotTooltipEvent,
|
configRoundAngle,
|
getPadding,
|
getTooltipContainer,
|
getTooltipItemConditionColor,
|
getYAxis,
|
getYAxisExt,
|
setGradientColor,
|
TOOLTIP_TPL
|
} from '@/views/chart/components/js/panel/common/common_antv'
|
import type {
|
BidirectionalBar as G2BidirectionalBar,
|
BidirectionalBarOptions
|
} from '@antv/g2plot/esm/plots/bidirectional-bar'
|
import { flow, hexColorToRGBA, parseJson } from '@/views/chart/components/js/util'
|
import { useI18n } from '@/hooks/web/useI18n'
|
import { valueFormatter } from '@/views/chart/components/js/formatter'
|
import type { Options } from '@antv/g2plot/esm'
|
import { Group } from '@antv/g-canvas'
|
|
const { t } = useI18n()
|
/**
|
* 对称柱状图
|
*/
|
|
export class BidirectionalHorizontalBar extends G2PlotChartView<
|
BidirectionalBarOptions,
|
G2BidirectionalBar
|
> {
|
axisConfig = {
|
...this['axisConfig'],
|
xAxis: {
|
name: `${t('chart.drag_block_type_axis')} / ${t('chart.dimension')}`,
|
type: 'd',
|
limit: 1
|
},
|
yAxis: {
|
name: `${t('chart.drag_block_value_axis')} / ${t('chart.quota')}`,
|
type: 'q',
|
limit: 1
|
},
|
yAxisExt: {
|
name: `${t('chart.drag_block_value_axis_ext')} / ${t('chart.quota')}`,
|
type: 'q',
|
limit: 1
|
}
|
}
|
axis: AxisType[] = ['xAxis', 'yAxis', 'yAxisExt', 'filter', 'drill', 'extLabel', 'extTooltip']
|
properties: EditorProperty[] = [
|
'background-overall-component',
|
'border-style',
|
'basic-style-selector',
|
'x-axis-selector',
|
'dual-y-axis-selector',
|
'title-selector',
|
'legend-selector',
|
'label-selector',
|
'tooltip-selector',
|
'function-cfg',
|
'jump-set',
|
'linkage',
|
'threshold'
|
]
|
propertyInner = {
|
'background-overall-component': ['all'],
|
'border-style': ['all'],
|
'basic-style-selector': ['colors', 'alpha', 'gradient', 'layout', 'radiusColumnBar'],
|
'x-axis-selector': ['position', 'axisLabel', 'axisLine', 'splitLine'],
|
'dual-y-axis-selector': [
|
'name',
|
'position',
|
'color',
|
'fontSize',
|
'axisLabel',
|
'axisLine',
|
'splitLine',
|
'axisValue',
|
'axisLabelFormatter'
|
],
|
'title-selector': [
|
'title',
|
'fontSize',
|
'color',
|
'hPosition',
|
'isItalic',
|
'isBolder',
|
'remarkShow',
|
'fontFamily',
|
'letterSpace',
|
'fontShadow'
|
],
|
'legend-selector': ['icon', 'orient', 'fontSize', 'color', 'hPosition', 'vPosition'],
|
'function-cfg': ['emptyDataStrategy'],
|
'label-selector': ['hPosition', 'vPosition', 'seriesLabelFormatter'],
|
'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
|
threshold: ['lineThreshold']
|
}
|
|
selectorSpec: EditorSelectorSpec = {
|
...this['selectorSpec'],
|
'dual-y-axis-selector': {
|
title: `${t('chart.xAxis')}`
|
},
|
'x-axis-selector': {
|
title: `${t('chart.yAxis')}`
|
}
|
}
|
|
async drawChart(drawOptions: G2PlotDrawOptions<G2BidirectionalBar>): Promise<G2BidirectionalBar> {
|
const { chart, container, action } = drawOptions
|
if (!chart.data?.data?.length) {
|
return
|
}
|
// data
|
const data = cloneDeep(chart.data.data)
|
const data1 = defaultTo(data[0]?.data, [])
|
const data2 = map(defaultTo(data[1]?.data, []), d => {
|
return {
|
...d,
|
category: d.field,
|
value: data1.find(item => item.field === d.field)?.value,
|
valueExt: d.value
|
}
|
})
|
// options
|
const initOptions: BidirectionalBarOptions = {
|
xField: 'field',
|
data: data2,
|
xAxis: {
|
label: {
|
style: {}
|
},
|
position: 'bottom'
|
},
|
yField: ['value', 'valueExt'],
|
appendPadding: getPadding(chart),
|
meta: {
|
field: {
|
type: 'cat'
|
}
|
}
|
}
|
const customOptions = this.setupOptions(chart, initOptions)
|
const options = {
|
...customOptions
|
}
|
const xAxis = chart.xAxis
|
if (xAxis?.length === 1 && xAxis[0].deType === 1) {
|
const values = data2.map(item => item.field)
|
options.meta = {
|
field: {
|
type: 'cat',
|
values: values.reverse()
|
}
|
}
|
}
|
const { BidirectionalBar: BidirectionalBarClass } = await import(
|
'@antv/g2plot/esm/plots/bidirectional-bar'
|
)
|
// 开始渲染
|
const newChart = new BidirectionalBarClass(container, options)
|
|
newChart.on('interval:click', action)
|
newChart.on('element:click', ev => {
|
const sourceData = newChart.options.data.filter(
|
item =>
|
item.field === ev.data.data.field &&
|
item[ev.data.data['series-field-key']] === ev.data.data[ev.data.data['series-field-key']]
|
)
|
ev.data.data = {
|
...ev.data.data,
|
...sourceData[0]
|
}
|
})
|
configPlotTooltipEvent(chart, newChart)
|
configAxisLabelLengthLimit(chart, newChart)
|
return newChart
|
}
|
|
protected configBasicStyle(
|
chart: Chart,
|
options: BidirectionalBarOptions
|
): BidirectionalBarOptions {
|
const basicStyle = parseJson(chart.customAttr).basicStyle
|
if (basicStyle.gradient) {
|
const color = basicStyle.colors?.map((ele, index) => {
|
const tmp = hexColorToRGBA(ele, basicStyle.alpha)
|
let angle = 180 - index * 180
|
// 垂直固定角度
|
if (basicStyle.layout === 'vertical') {
|
if (index === 0) {
|
angle = 280
|
}
|
if (index === 1) {
|
angle = 90
|
}
|
}
|
return setGradientColor(tmp, true, angle)
|
})
|
options = {
|
...options,
|
color
|
}
|
}
|
options = {
|
...options,
|
layout: basicStyle.layout
|
}
|
options = {
|
...options,
|
...configRoundAngle(chart, 'barStyle')
|
}
|
return options
|
}
|
|
protected configXAxis(chart: Chart, options: BidirectionalBarOptions): BidirectionalBarOptions {
|
const tmpOptions = super.configXAxis(chart, options)
|
if (!tmpOptions.xAxis) {
|
return tmpOptions
|
}
|
if (tmpOptions.xAxis.label) {
|
delete tmpOptions.xAxis.label.style.textAlign
|
const { lengthLimit } = parseJson(chart.customStyle).xAxis.axisLabel
|
defaults(tmpOptions.xAxis.label, {
|
formatter: value => {
|
return value?.length > lengthLimit ? value.substring(0, lengthLimit) + '...' : value
|
}
|
})
|
}
|
return tmpOptions
|
}
|
|
protected configTooltip(chart: Chart, options: BidirectionalBarOptions): BidirectionalBarOptions {
|
const customAttr: DeepPartial<ChartAttr> = parseJson(chart.customAttr)
|
const tooltipAttr = customAttr.tooltip
|
if (!tooltipAttr.show) {
|
return {
|
...options,
|
tooltip: false
|
}
|
}
|
const yAxis = cloneDeep(chart.yAxis)
|
const yAxisExt = cloneDeep(chart.yAxisExt)
|
const formatterMap = tooltipAttr.seriesTooltipFormatter
|
?.filter(i => i.show)
|
.reduce((pre, next) => {
|
pre[next.seriesId] = next
|
return pre
|
}, {}) as Record<string, SeriesFormatter>
|
const optionsData = cloneDeep(options.data)
|
const yaxisObj = item => {
|
const param = item.data
|
let yaxis = yAxis[0]
|
let axisType = 'yAxis'
|
if (param['series-field-key'] === 'valueExt') {
|
yaxis = yAxisExt[0]
|
axisType = 'yAxisExt'
|
}
|
return {
|
id: yaxis.id,
|
name: yaxis.name,
|
axisType: axisType,
|
value: param[param['series-field-key']]
|
}
|
}
|
const tooltip: BidirectionalBarOptions['tooltip'] = {
|
shared: true,
|
showTitle: true,
|
customItems(originalItems) {
|
if (!tooltipAttr.seriesTooltipFormatter?.length) {
|
return originalItems
|
}
|
const result = []
|
originalItems
|
.filter(item => {
|
const obj = yaxisObj(item)
|
return formatterMap[obj.id + '-' + obj.axisType]
|
})
|
.forEach(item => {
|
const obj = yaxisObj(item)
|
const formatter = formatterMap[obj.id + '-' + obj.axisType]
|
const value = valueFormatter(parseFloat(item.value as string), formatter.formatterCfg)
|
const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
|
const color = getTooltipItemConditionColor(item)
|
result.push({ ...item, name, value, color })
|
})
|
const dynamicTooltipValue = optionsData.find(
|
d => d.field === originalItems[0]['title']
|
)?.dynamicTooltipValue
|
if (dynamicTooltipValue.length > 0) {
|
dynamicTooltipValue.forEach(dy => {
|
const q = tooltipAttr.seriesTooltipFormatter.filter(i => i.id === dy.fieldId)
|
if (q && q.length > 0) {
|
const value = valueFormatter(parseFloat(dy.value as string), q[0].formatterCfg)
|
const name = isEmpty(q[0].chartShowName) ? q[0].name : q[0].chartShowName
|
result.push({ color: 'grey', name, value })
|
}
|
})
|
}
|
return result
|
},
|
container: getTooltipContainer(`tooltip-${chart.id}`),
|
itemTpl: TOOLTIP_TPL,
|
enterable: true
|
}
|
return {
|
...options,
|
tooltip
|
}
|
}
|
|
protected configLegend(chart: Chart, options: BidirectionalBarOptions): BidirectionalBarOptions {
|
const o = super.configLegend(chart, options)
|
if (o.legend) {
|
o.legend.itemName.formatter = (_text: string, _item: any, index: number) => {
|
const yaxis = chart.yAxis[0]
|
const yaxisExt = chart.yAxisExt[0]
|
if (index === 0) {
|
return yaxis.chartShowName ? yaxis.chartShowName : yaxis.name
|
}
|
return yaxisExt.chartShowName ? yaxisExt.chartShowName : yaxisExt.name
|
}
|
}
|
return o
|
}
|
|
protected configYAxis(chart: Chart, options: BidirectionalBarOptions): BidirectionalBarOptions {
|
const yAxis = getYAxis(chart)
|
let yAxisExt = getYAxisExt(chart)
|
|
const tempOption = {
|
...options
|
}
|
|
if (!yAxis) {
|
//左右轴都要隐藏
|
yAxisExt = false
|
tempOption['yAxis'] = {
|
value: false,
|
valueExt: false
|
}
|
} else {
|
tempOption['yAxis'] = {
|
value: undefined,
|
valueExt: undefined
|
}
|
}
|
const layoutHorizontal = options.layout === 'horizontal'
|
// 处理横轴标题方向不对
|
if (yAxis && yAxis['title'] && layoutHorizontal) {
|
yAxis['title'].autoRotate = false
|
}
|
const yAxisTmp = parseJson(chart.customStyle).yAxis
|
if (yAxis['label']) {
|
yAxis['label'].formatter = value => {
|
return valueFormatter(value, yAxisTmp.axisLabelFormatter)
|
}
|
}
|
const axisValue = yAxisTmp.axisValue
|
if (!axisValue?.auto) {
|
tempOption.yAxis.value = {
|
...yAxis,
|
min: axisValue.min,
|
max: axisValue.max,
|
minLimit: axisValue.min,
|
maxLimit: axisValue.max,
|
tickCount: axisValue.splitCount
|
}
|
} else {
|
tempOption.yAxis.value = yAxis
|
}
|
|
const yAxisExtTmp = parseJson(chart.customStyle).yAxisExt
|
if (yAxisExt['label']) {
|
yAxisExt['label'].formatter = value => {
|
return valueFormatter(value, yAxisExtTmp.axisLabelFormatter)
|
}
|
}
|
const axisExtValue = yAxisExtTmp.axisValue
|
if (!axisExtValue?.auto) {
|
tempOption.yAxis.valueExt = {
|
...yAxisExt,
|
min: axisExtValue.min,
|
max: axisExtValue.max,
|
minLimit: axisExtValue.min,
|
maxLimit: axisExtValue.max,
|
tickCount: axisExtValue.splitCount
|
}
|
} else {
|
tempOption.yAxis.valueExt = yAxisExt
|
}
|
|
return tempOption
|
}
|
|
setupDefaultOptions(chart: ChartObj): ChartObj {
|
chart.customStyle.yAxis = {
|
...chart.customStyle.yAxis,
|
position: 'left'
|
}
|
chart.customStyle.yAxisExt = {
|
...chart.customStyle.yAxisExt,
|
position: 'left',
|
splitLine: chart.customStyle.yAxis.splitLine
|
}
|
chart.customAttr.label = {
|
...chart.customAttr.label,
|
position: 'right'
|
}
|
chart.customAttr.basicStyle.layout = 'horizontal'
|
return chart
|
}
|
|
protected configLabel(chart: Chart, options: BidirectionalBarOptions): BidirectionalBarOptions {
|
let label
|
const yAxis = chart.yAxis
|
const yAxisExt = chart.yAxisExt
|
const labelAttr = parseJson(chart.customAttr).label
|
const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
|
pre[next.id] = next
|
return pre
|
}, {})
|
let customAttr: DeepPartial<ChartAttr>
|
const layoutHorizontal = options.layout === 'horizontal'
|
if (chart.customAttr) {
|
customAttr = parseJson(chart.customAttr)
|
// label
|
if (customAttr.label) {
|
const l = customAttr.label
|
if (l.show) {
|
const layout = []
|
if (!labelAttr.fullDisplay) {
|
const tmpOptions = super.configLabel(chart, options)
|
layout.push(...tmpOptions.label.layout)
|
}
|
label = {
|
position: l.position,
|
layout,
|
style: {
|
fill: l.color,
|
fontSize: l.fontSize,
|
fontFamily: chart.fontFamily
|
},
|
formatter: param => {
|
let yaxis = yAxis[0]
|
let res = param.value
|
if (param['series-field-key'] === 'valueExt') {
|
yaxis = yAxisExt[0]
|
}
|
const value = param[param['series-field-key']]
|
const labelCfg = formatterMap?.[yaxis.id] as SeriesFormatter
|
if (yaxis.formatterCfg) {
|
res = valueFormatter(value, yaxis.formatterCfg)
|
}
|
if (!labelCfg) {
|
return res
|
}
|
if (!labelCfg.show) {
|
return
|
}
|
if (labelCfg) {
|
res = valueFormatter(value, labelCfg.formatterCfg)
|
} else {
|
res = valueFormatter(value, l.labelFormatter)
|
}
|
const group = new Group({})
|
const isValue = param['series-field-key'] === 'value'
|
const textAlign = isValue && layoutHorizontal ? 'end' : 'start'
|
const isMiddle = label.position === 'middle'
|
group.addShape({
|
type: 'text',
|
attrs: {
|
x:
|
isValue && layoutHorizontal && !isMiddle
|
? -6
|
: !isValue && layoutHorizontal && !isMiddle
|
? 6
|
: 0,
|
y:
|
isValue && !layoutHorizontal && !isMiddle
|
? -8
|
: !isValue && !layoutHorizontal && !isMiddle
|
? 8
|
: 0,
|
text: res,
|
textAlign: label.position === 'middle' ? 'start' : textAlign,
|
textBaseline: 'top',
|
fontSize: labelCfg.fontSize,
|
fontFamily: chart.fontFamily,
|
fill: labelCfg.color
|
}
|
})
|
return group
|
}
|
}
|
} else {
|
label = false
|
}
|
}
|
}
|
if (!layoutHorizontal) {
|
if (label.position === 'left') {
|
label.position = 'bottom'
|
}
|
if (label.position === 'right') {
|
label.position = 'top'
|
}
|
}
|
return { ...options, label }
|
}
|
|
protected configEmptyDataStrategy(
|
chart: Chart,
|
options: BidirectionalBarOptions
|
): BidirectionalBarOptions {
|
const { data } = options as unknown as Options
|
if (!data?.length) {
|
return options
|
}
|
const strategy = parseJson(chart.senior).functionCfg.emptyDataStrategy
|
if (strategy === 'ignoreData') {
|
const emptyFields = data
|
.filter(obj => obj['value'] === null || obj['valueExt'] === null)
|
.map(obj => obj['field'])
|
return {
|
...options,
|
data: data.filter(obj => {
|
if (emptyFields.includes(obj['field'])) {
|
return false
|
}
|
return true
|
})
|
}
|
}
|
const updateValues = (strategy: 'breakLine' | 'setZero', data: any[]) => {
|
data.forEach(obj => {
|
if (obj['value'] === null) {
|
obj['value'] = strategy === 'breakLine' ? null : 0
|
}
|
if (obj['valueExt'] === null) {
|
obj['valueExt'] = strategy === 'breakLine' ? null : 0
|
}
|
})
|
}
|
if (strategy === 'breakLine' || strategy === 'setZero') {
|
updateValues(strategy, data)
|
}
|
return options
|
}
|
|
protected setupOptions(chart: Chart, options: BidirectionalBarOptions) {
|
return flow(
|
this.addConditionsStyleColorToData,
|
this.configTheme,
|
this.configBasicStyle,
|
this.configLabel,
|
this.configTooltip,
|
this.configLegend,
|
this.configXAxis,
|
this.configYAxis,
|
this.configAnalyse,
|
this.configSlider,
|
this.configEmptyDataStrategy,
|
this.configBarConditions
|
)(chart, options)
|
}
|
|
constructor() {
|
super('bidirectional-bar', [])
|
}
|
}
|