@@ -1792,6 +1893,7 @@ const onRefreshChange = val => {
@onTooltipChange="onTooltipChange"
@onChangeXAxisForm="onChangeXAxisForm"
@onChangeYAxisForm="onChangeYAxisForm"
+ @onChangeYAxisExtForm="onChangeYAxisExtForm"
@onTextChange="onTextChange"
@onIndicatorChange="onIndicatorChange"
@onIndicatorNameChange="onIndicatorNameChange"
diff --git a/core/core-frontend/src/views/chart/components/editor/util/chart.ts b/core/core-frontend/src/views/chart/components/editor/util/chart.ts
index 589a241cfb..954c6fec97 100644
--- a/core/core-frontend/src/views/chart/components/editor/util/chart.ts
+++ b/core/core-frontend/src/views/chart/components/editor/util/chart.ts
@@ -568,7 +568,7 @@ export const DEFAULT_YAXIS_EXT_STYLE: ChartAxisStyle = {
}
},
splitLine: {
- show: true,
+ show: false,
lineStyle: {
color: '#cccccc',
width: 1,
@@ -577,10 +577,10 @@ export const DEFAULT_YAXIS_EXT_STYLE: ChartAxisStyle = {
},
axisValue: {
auto: true,
- min: null,
- max: null,
- split: null,
- splitCount: null
+ min: 10,
+ max: 100,
+ split: 10,
+ splitCount: 10
},
axisLabelFormatter: {
type: 'auto',
@@ -1297,6 +1297,20 @@ export const CHART_TYPE_CONFIGS = [
}
]
},
+ {
+ category: 'dual_axes',
+ title: t('chart.chart_type_dual_axes'),
+ display: 'show',
+ details: [
+ {
+ render: 'antv',
+ category: 'dual_axes',
+ value: 'chart-mix',
+ title: t('chart.chart_mix'),
+ icon: 'chart-mix'
+ }
+ ]
+ },
{
category: 'other',
title: '富文本',
diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix-common.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix-common.ts
new file mode 100644
index 0000000000..c7bdb86726
--- /dev/null
+++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix-common.ts
@@ -0,0 +1,69 @@
+export const CHART_MIX_EDITOR_PROPERTY: EditorProperty[] = [
+ 'background-overall-component',
+ 'basic-style-selector',
+ 'x-axis-selector',
+ 'dual-y-axis-selector',
+ 'title-selector',
+ 'legend-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'assist-line',
+ 'function-cfg',
+ 'jump-set',
+ 'linkage'
+]
+export const CHART_MIX_EDITOR_PROPERTY_INNER: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'label-selector': ['fontSize', 'color'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor'],
+ 'basic-style-selector': [
+ 'colors',
+ 'alpha',
+ 'lineWidth',
+ 'lineSymbol',
+ 'lineSymbolSize',
+ 'lineSmooth'
+ ],
+ 'x-axis-selector': [
+ 'name',
+ 'color',
+ 'fontSize',
+ 'position',
+ 'axisLabel',
+ 'axisLine',
+ 'splitLine'
+ ],
+ 'y-axis-selector': [
+ 'name',
+ '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': ['slider', 'emptyDataStrategy']
+}
+
+export const CHART_MIX_AXIS_TYPE: AxisType[] = [
+ 'xAxis',
+ 'yAxis',
+ 'drill',
+ 'filter',
+ 'extLabel',
+ 'extTooltip'
+]
diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts
new file mode 100644
index 0000000000..7f011112f4
--- /dev/null
+++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/chart-mix.ts
@@ -0,0 +1,389 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/views/chart/components/js/panel/types/impl/g2plot'
+import { DualAxes, DualAxesOptions } from '@antv/g2plot/esm/plots/dual-axes'
+import { getLabel, getPadding, getYAxis, getYAxisExt } from '../../common/common_antv'
+import { flow, hexColorToRGBA, parseJson } from '@/views/chart/components/js/util'
+import { cloneDeep, isEmpty, defaultTo, map } from 'lodash-es'
+import { valueFormatter } from '@/views/chart/components/js/formatter'
+import {
+ CHART_MIX_AXIS_TYPE,
+ CHART_MIX_EDITOR_PROPERTY,
+ CHART_MIX_EDITOR_PROPERTY_INNER
+} from './chart-mix-common'
+import { Datum } from '@antv/g2plot/esm/types/common'
+import { useI18n } from '@/hooks/web/useI18n'
+import { DEFAULT_LABEL } from '@/views/chart/components/editor/util/chart'
+
+const { t } = useI18n()
+const DEFAULT_DATA = []
+
+/**
+ * 柱线混合图
+ */
+export class ColumnLineMix extends G2PlotChartView {
+ properties = CHART_MIX_EDITOR_PROPERTY
+ propertyInner = {
+ ...CHART_MIX_EDITOR_PROPERTY_INNER,
+ 'label-selector': ['vPosition', 'seriesLabelFormatter'],
+ 'tooltip-selector': [
+ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'],
+ 'seriesTooltipFormatter'
+ ]
+ }
+ axis: AxisType[] = [...CHART_MIX_AXIS_TYPE, 'yAxisExt']
+ axisConfig = {
+ ...this['axisConfig'],
+ yAxis: {
+ name: `${t('chart.drag_block_value_axis_left')} / ${t('chart.quota')}`,
+ type: 'q'
+ },
+ yAxisExt: {
+ name: `${t('chart.drag_block_value_axis_right')} / ${t('chart.quota')}`,
+ type: 'q'
+ }
+ }
+ drawChart(drawOptions: G2PlotDrawOptions): DualAxes {
+ const { chart, action, container } = drawOptions
+ if (!chart.data.data?.length) {
+ return
+ }
+ const data = cloneDeep(chart.data.data)
+
+ const data1Type = data[0]?.type === 'bar' ? 'column' : data[0]?.type
+ const data2Type = data[1]?.type === 'bar' ? 'column' : data[1]?.type
+
+ const data1 = defaultTo(data[0]?.data, [])
+ const data2 = map(defaultTo(data[1]?.data, []), d => {
+ return {
+ ...d,
+ valueExt: d.value
+ }
+ })
+ // custom color
+ const customAttr = parseJson(chart.customAttr)
+ const color = customAttr.basicStyle.colors
+ // options
+ const initOptions: DualAxesOptions = {
+ data: [data1, data2],
+ xField: 'field',
+ yField: ['value', 'valueExt'], //这里不能设置成一样的
+ appendPadding: getPadding(chart),
+ color,
+ geometryOptions: [
+ {
+ geometry: data1Type
+ },
+ {
+ geometry: data2Type
+ }
+ ],
+ interactions: [
+ {
+ type: 'legend-active',
+ cfg: {
+ start: [{ trigger: 'legend-item:mouseenter', action: ['element-active:reset'] }],
+ end: [{ trigger: 'legend-item:mouseleave', action: ['element-active:reset'] }]
+ }
+ },
+ {
+ type: 'legend-filter',
+ cfg: {
+ start: [
+ {
+ trigger: 'legend-item:click',
+ action: [
+ 'list-unchecked:toggle',
+ 'data-filter:filter',
+ 'element-active:reset',
+ 'element-highlight:reset'
+ ]
+ }
+ ]
+ }
+ },
+ {
+ type: 'tooltip',
+ cfg: {
+ start: [{ trigger: 'point:mousemove', action: 'tooltip:show' }],
+ end: [{ trigger: 'point:mouseleave', action: 'tooltip:hide' }]
+ }
+ },
+ {
+ type: 'active-region',
+ cfg: {
+ start: [{ trigger: 'element:mousemove', action: 'active-region:show' }],
+ end: [{ trigger: 'element:mouseleave', action: 'active-region:hide' }]
+ }
+ }
+ ]
+ }
+ const options = this.setupOptions(chart, initOptions)
+ // 开始渲染
+ const newChart = new DualAxes(container, options)
+
+ newChart.on('point:click', action)
+
+ return newChart
+ }
+
+ protected configLabel(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const tempLabel = getLabel(chart)
+ const tmpOption = { ...options }
+ if (!tempLabel) {
+ if (tmpOption.geometryOptions) {
+ tmpOption.geometryOptions[0].label = false
+ tmpOption.geometryOptions[1].label = false
+ }
+ return tmpOption
+ }
+
+ const labelAttr = parseJson(chart.customAttr).label
+ const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {})
+ tempLabel.style.fill = DEFAULT_LABEL.color
+ const label = {
+ fields: [],
+ ...tempLabel,
+ offsetY: -8,
+ formatter: (data: Datum) => {
+ if (!labelAttr.seriesLabelFormatter?.length) {
+ return data.value
+ }
+ const labelCfg = formatterMap?.[data.quotaList[0].id] as SeriesFormatter
+ if (!labelCfg) {
+ return data.value
+ }
+ if (!labelCfg.show) {
+ return
+ }
+ const value = valueFormatter(data.value, labelCfg.formatterCfg)
+ const group = new G2PlotChartView.engine.Group({})
+ group.addShape({
+ type: 'text',
+ attrs: {
+ x: 0,
+ y: 0,
+ text: value,
+ textAlign: 'start',
+ textBaseline: 'top',
+ fontSize: labelCfg.fontSize,
+ fill: labelCfg.color
+ }
+ })
+ return group
+ }
+ }
+ if (tmpOption.geometryOptions) {
+ tmpOption.geometryOptions[0].label = label
+ tmpOption.geometryOptions[1].label = label
+ }
+ return tmpOption
+ }
+
+ protected configBasicStyle(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ // size
+ const customAttr: DeepPartial = parseJson(chart.customAttr)
+ const s = JSON.parse(JSON.stringify(customAttr.basicStyle))
+ const smooth = s.lineSmooth
+ const point = {
+ size: s.lineSymbolSize,
+ shape: s.lineSymbol
+ }
+ const lineStyle = {
+ lineWidth: s.lineWidth
+ }
+ const tempOption = {
+ ...options,
+ smooth,
+ point,
+ lineStyle
+ }
+ if (tempOption.geometryOptions) {
+ tempOption.geometryOptions[0].smooth = smooth
+ tempOption.geometryOptions[0].point = point
+ tempOption.geometryOptions[0].lineStyle = lineStyle
+
+ tempOption.geometryOptions[1].smooth = smooth
+ tempOption.geometryOptions[1].point = point
+ tempOption.geometryOptions[1].lineStyle = lineStyle
+ }
+ return tempOption
+ }
+
+ protected configCustomColors(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const basicStyle = parseJson(chart.customAttr).basicStyle
+ const color = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha))
+ return {
+ ...options,
+ color
+ }
+ }
+
+ protected configYAxis(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const yAxis = getYAxis(chart)
+ const yAxisExt = getYAxisExt(chart)
+
+ const tempOption = {
+ ...options
+ }
+
+ if (!yAxis) {
+ //左右轴都要隐藏
+ tempOption.yAxis = {
+ value: false,
+ valueExt: false
+ }
+ } else {
+ tempOption.yAxis = {
+ value: undefined,
+ valueExt: undefined
+ }
+ }
+
+ 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
+ }
+
+ protected configTooltip(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const customAttr: DeepPartial = parseJson(chart.customAttr)
+ const tooltipAttr = customAttr.tooltip
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: false
+ }
+ }
+ const xAxisExt = chart.xAxisExt
+ const formatterMap = tooltipAttr.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {}) as Record
+ const tooltip: DualAxesOptions['tooltip'] = {
+ shared: true,
+ showTitle: true,
+ customItems(originalItems) {
+ if (!tooltipAttr.seriesTooltipFormatter?.length) {
+ return originalItems
+ }
+ const head = originalItems[0]
+ // 非原始数据
+ if (!head.data.quotaList) {
+ return originalItems
+ }
+ const result = []
+ originalItems
+ .filter(item => formatterMap[item.data.quotaList[0].id])
+ .forEach(item => {
+ const formatter = formatterMap[item.data.quotaList[0].id]
+ const value = valueFormatter(parseFloat(item.value as string), formatter.formatterCfg)
+ let name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ if (xAxisExt?.length > 0) {
+ name = item.data.category
+ }
+ result.push({ ...item, name, value })
+ })
+ head.data.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 })
+ }
+ })
+ return result
+ }
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+
+ protected configLegend(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const o = super.configLegend(chart, options)
+ if (o.legend) {
+ const data = chart.data.data
+ o.legend.itemName = {
+ formatter: (text: string, item: any, index: number) => {
+ let name = undefined
+ if (index === 0 && text === 'value') {
+ name = data[0]?.name
+ } else if (index === 1 && text === 'valueExt') {
+ name = data[1]?.name
+ }
+ if (name === undefined) {
+ return text
+ } else {
+ return name
+ }
+ }
+ }
+ }
+ return o
+ }
+
+ protected setupOptions(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ return flow(
+ this.configTheme,
+ this.configLabel,
+ this.configTooltip,
+ this.configBasicStyle,
+ this.configCustomColors,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyse,
+ this.configEmptyDataStrategy
+ )(chart, options)
+ }
+
+ constructor(name = 'chart-mix') {
+ super(name, DEFAULT_DATA)
+ }
+}
diff --git a/core/core-frontend/src/views/chart/components/js/panel/common/common_antv.ts b/core/core-frontend/src/views/chart/components/js/panel/common/common_antv.ts
index 22d6887aaf..cd9f18bf51 100644
--- a/core/core-frontend/src/views/chart/components/js/panel/common/common_antv.ts
+++ b/core/core-frontend/src/views/chart/components/js/panel/common/common_antv.ts
@@ -506,6 +506,98 @@ export function getYAxis(chart: Chart) {
return axis
}
+export function getYAxisExt(chart: Chart) {
+ let axis: Record | boolean = {}
+ const yAxis = parseJson(chart.customStyle).yAxisExt
+ if (!yAxis.show) {
+ return false
+ }
+ const title =
+ yAxis.name && yAxis.name !== ''
+ ? {
+ text: yAxis.name,
+ style: {
+ fill: yAxis.color,
+ fontSize: yAxis.fontSize
+ },
+ spacing: 8
+ }
+ : null
+ const grid = yAxis.splitLine.show
+ ? {
+ line: {
+ style: {
+ stroke: yAxis.splitLine.lineStyle.color,
+ lineWidth: yAxis.splitLine.lineStyle.width
+ }
+ }
+ }
+ : null
+ const axisCfg = yAxis.axisLine ? yAxis.axisLine : DEFAULT_YAXIS_STYLE.axisLine
+ const line = axisCfg.show
+ ? {
+ style: {
+ stroke: axisCfg.lineStyle.color,
+ lineWidth: axisCfg.lineStyle.width
+ }
+ }
+ : null
+ const tickLine = axisCfg.show
+ ? {
+ style: {
+ stroke: axisCfg.lineStyle.color
+ }
+ }
+ : null
+ const rotate = yAxis.axisLabel.rotate
+ let textAlign = 'end'
+ let textBaseline = 'middle'
+ if (yAxis.position === 'right') {
+ textAlign = 'start'
+ if (Math.abs(rotate) > 75) {
+ textAlign = 'center'
+ }
+ if (rotate > 75) {
+ textBaseline = 'bottom'
+ }
+ if (rotate < -75) {
+ textBaseline = 'top'
+ }
+ }
+ if (yAxis.position === 'left') {
+ if (Math.abs(rotate) > 75) {
+ textAlign = 'center'
+ }
+ if (rotate > 75) {
+ textBaseline = 'top'
+ }
+ if (rotate < -75) {
+ textBaseline = 'bottom'
+ }
+ }
+ const label = yAxis.axisLabel.show
+ ? {
+ rotate: (rotate * Math.PI) / 180,
+ style: {
+ fill: yAxis.axisLabel.color,
+ fontSize: yAxis.axisLabel.fontSize,
+ textBaseline,
+ textAlign
+ }
+ }
+ : null
+
+ axis = {
+ position: yAxis.position,
+ title,
+ grid,
+ label,
+ line,
+ tickLine
+ }
+ return axis
+}
+
export function getSlider(chart: Chart) {
let cfg
const senior = parseJson(chart.senior)