From 51943c120d4fbb08b23e7373337b28d62a1f28b2 Mon Sep 17 00:00:00 2001 From: wisonic-s <51065359+wisonic-s@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:51:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=BE=E8=A1=A8):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=9D=A2=E7=A7=AF=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/panel/charts/g2/line/area.ts | 659 ++++++++++++++++++ .../js/panel/charts/g2/line/line.ts | 11 +- 2 files changed, 669 insertions(+), 1 deletion(-) create mode 100644 core/core-frontend/src/views/chart/components/js/panel/charts/g2/line/area.ts diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/g2/line/area.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/g2/line/area.ts new file mode 100644 index 0000000000..98ced1c976 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/g2/line/area.ts @@ -0,0 +1,659 @@ +import { G2ChartView, G2DrawOptions } from '../../../types/impl/g2' +import { + flow, + getLineConditions, + getLineLabelColorByCondition, + hexColorToRGBA, + parseJson, + setUpGroupSeriesColor +} from '@/views/chart/components/js/util' +import { cloneDeep, defaultsDeep, isEmpty } from 'lodash-es' +import { valueFormatter } from '@/views/chart/components/js/formatter' +import { LINE_AXIS_TYPE, LINE_EDITOR_PROPERTY, LINE_EDITOR_PROPERTY_INNER } from './common' +import { useI18n } from '@/hooks/web/useI18n' +import { clearExtremum } from '@/views/chart/components/js/extremumUitl' +import { Chart as G2Chart, G2Spec } from '@antv/g2' +import { DEFAULT_YAXIS_STYLE } from '@/views/chart/components/editor/util/chart' +import { setGradientColor, TOOLTIP_ITEM_TPL, TOOLTIP_TITLE_TPL } from '../../../common/common_antv' + +const { t } = useI18n() +const DEFAULT_DATA = [] +export class Area extends G2ChartView { + properties = LINE_EDITOR_PROPERTY + propertyInner = { + ...LINE_EDITOR_PROPERTY_INNER, + 'basic-style-selector': [ + ...LINE_EDITOR_PROPERTY_INNER['basic-style-selector'], + 'gradient', + 'seriesColor' + ], + 'label-selector': ['seriesLabelVPosition', 'seriesLabelFormatter', 'showExtremum'], + 'tooltip-selector': [ + ...LINE_EDITOR_PROPERTY_INNER['tooltip-selector'], + 'seriesTooltipFormatter', + 'carousel' + ] + } + axis: AxisType[] = [...LINE_AXIS_TYPE] + axisConfig = { + ...this['axisConfig'], + xAxis: { + name: `${t('chart.drag_block_type_axis')} / ${t('chart.dimension')}`, + type: 'd' + }, + yAxis: { + name: `${t('chart.drag_block_value_axis')} / ${t('chart.quota')}`, + type: 'q' + } + } + async drawChart(drawOptions: G2DrawOptions): Promise { + const { chart, action, container } = drawOptions + chart.container = container + if (!chart.data?.data?.length) { + clearExtremum(chart) + return + } + const data = cloneDeep(chart.data.data) + // options + const initOptions: G2Spec = { + type: 'view', + data: { + value: data + }, + autoFit: true, + encode: { + x: 'field', + y: 'value', + color: 'category' + }, + scale: { + x: { + range: [0, 1] + }, + y: { + nice: true + } + }, + children: [ + { + type: 'area', + tooltip: false, + style: { fillOpacity: 0.3 }, + zIndex: 1 + }, + { type: 'line', encode: { series: 'category' }, zIndex: 0 }, + { type: 'point', tooltip: false, zIndex: 2 } + ] + } + const newChart = new G2Chart({ container }) + const options = this.setupOptions(chart, initOptions, { chartObj: newChart }) + // 开始渲染 + newChart.options(options) + newChart.on('point:click', action) + // extremumEvt(newChart, chart, options, container) + // configPlotTooltipEvent(chart, newChart) + return newChart + } + + protected configTheme(chart: Chart, options: G2Spec): G2Spec { + const customAttr = parseJson(chart.customAttr) + const colors: string[] = [] + if (customAttr.basicStyle) { + const basicStyle = customAttr.basicStyle + basicStyle.colors.forEach(ele => { + let color = hexColorToRGBA(ele, basicStyle.alpha) + if (basicStyle.gradient) { + color = setGradientColor(color, true, 270) + } + colors.push(color) + }) + } + const customStyle = parseJson(chart.customStyle) + let bgColor + if (customStyle.background) { + bgColor = hexColorToRGBA(customStyle.background.color, customStyle.background.alpha) + } + const theme = { + color: colors[0], + category10: colors, + category20: colors, + view: { + viewFill: bgColor + } + } + return { ...options, theme } + } + + protected configColor(chart: Chart, options: G2Spec): G2Spec { + const { basicStyle } = parseJson(chart.customAttr) + const { seriesColor } = basicStyle + if (!seriesColor?.length) { + return options + } + const { xAxis, yAxis } = chart + if (!xAxis?.length || !yAxis?.length) { + return options + } + const relations = [] + const colorMap = seriesColor.reduce((pre, next) => { + pre[next.id] = next.color + return pre + }, {}) + yAxis.forEach(item => { + if (colorMap[item.id]) { + let color = hexColorToRGBA(colorMap[item.id], basicStyle.alpha) + if (basicStyle.gradient) { + color = setGradientColor(color, true, 270) + } + relations.push([item.chartShowName ?? item.name, color]) + } + }) + if (relations.length) { + const scaleOptions = { + scale: { + color: { + relations + } + } + } + defaultsDeep(options, scaleOptions) + } + return options + } + + protected configLabel(chart: Chart, options: G2Spec): G2Spec { + const { label: labelAttr } = parseJson(chart.customAttr) + if (!labelAttr.show) { + return options + } + const conditions = getLineConditions(chart) + const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => { + pre[next.id] = next + return pre + }, {}) + const pointMark: G2Spec = options.children[2] + const labelOpt = { + labels: [ + { + text: d => { + if (d.EXTREME) { + return '' + } + if (!labelAttr.seriesLabelFormatter?.length) { + return d.value + } + const labelCfg = formatterMap?.[d.quotaList[0].id] as SeriesFormatter + if (!labelCfg) { + return d.value + } + if (!labelCfg.show) { + return '' + } + return valueFormatter(d.value, labelCfg.formatterCfg) + }, + style: { + opacity: 1, + fontSize: d => { + if (d.EXTREME) { + return 0 + } + if (!labelAttr.seriesLabelFormatter?.length) { + return 12 + } + const labelCfg = formatterMap?.[d.quotaList[0].id] as SeriesFormatter + if (!labelCfg) { + return 12 + } + if (!labelCfg.show) { + return 0 + } + return labelCfg.fontSize + }, + fill: d => { + if (d.EXTREME || !labelAttr.seriesLabelFormatter?.length) { + return 'black' + } + const labelCfg = formatterMap?.[d.quotaList[0].id] as SeriesFormatter + if (!labelCfg?.show) { + return 'black' + } + const color = + getLineLabelColorByCondition(conditions, d.value, d.quotaList[0].id) || + labelCfg.color + return color + }, + position: d => { + if (d.EXTREME || !labelAttr.seriesLabelFormatter?.length) { + return 'top' + } + const labelCfg = formatterMap?.[d.quotaList[0].id] as SeriesFormatter + if (!labelCfg?.show) { + return 'top' + } + return labelCfg.position + } + }, + textBaseline: d => { + if (d.EXTREME || !labelAttr.seriesLabelFormatter?.length) { + return 'bottom' + } + const labelCfg = formatterMap?.[d.quotaList[0].id] as SeriesFormatter + if (!labelCfg?.show) { + return 'bottom' + } + return labelCfg.position === 'top' ? 'bottom' : 'top' + }, + transform: labelAttr.fullDisplay + ? [] + : [{ type: 'overlapHide' }, { type: 'exceedAdjust' }], + fontFamily: chart.fontFamily + } + ] + } + defaultsDeep(pointMark, labelOpt) + return options + } + + protected configBasicStyle(chart: Chart, options: G2Spec, context: Record): G2Spec { + // size + const { basicStyle } = parseJson(chart.customAttr) + const [_, lineMark, pointMark] = options.children + const lineStyleOpt = { + encode: { + shape: basicStyle.lineSmooth ? 'smooth' : 'line', + size: basicStyle.lineWidth + } + } + defaultsDeep(lineMark, lineStyleOpt) + const pointStyleOpt = { + encode: { + shape: basicStyle.lineSymbol, + size: basicStyle.lineSymbolSize + } + } + if (basicStyle.lineSymbolSize === 0) { + pointStyleOpt.encode.shape = 'none' + } + defaultsDeep(pointMark, pointStyleOpt) + return options + } + + protected configXAxis(chart: Chart, options: G2Spec): G2Spec { + const { xAxis } = parseJson(chart.customStyle) + if (!xAxis.show) { + const axisHide = { + axis: { + x: false + } + } + return defaultsDeep(options, axisHide) + } + let lineLineDash = undefined + if (xAxis.axisLine.lineStyle.style === 'dashed') { + lineLineDash = [10, 8] + } + if (xAxis.axisLine.lineStyle.style === 'dotted') { + lineLineDash = [1, 2] + } + let gridLineDash = undefined + if (xAxis.splitLine.lineStyle.style === 'dashed') { + gridLineDash = [10, 8] + } + if (xAxis.splitLine.lineStyle.style === 'dotted') { + gridLineDash = [1, 2] + } + const axisStyle = { + axis: { + x: { + position: xAxis.position, + title: xAxis.nameShow === false ? false : xAxis.name, + titleFontSize: xAxis.fontSize, + titleFill: xAxis.color, + line: xAxis.axisLine.show, + lineStroke: xAxis.axisLine.lineStyle.color, + lineStrokeOpacity: 1, + lineLineWidth: xAxis.axisLine.lineStyle.width, + lineLineDash, + label: xAxis.axisLabel.show, + labelFill: xAxis.axisLabel.color, + labelFillOpacity: 1, + labelFontSize: xAxis.axisLabel.fontSize, + grid: xAxis.splitLine.show, + gridStroke: xAxis.splitLine.lineStyle.color, + gridStrokeOpacity: 1, + gridLineWidth: xAxis.splitLine.lineStyle.width, + gridLineDash, + transform: xAxis.axisLabel.rotate + ? [ + { + type: 'rotate', + optionalAngles: [xAxis.axisLabel.rotate], + recoverWhenFailed: false + } + ] + : [] + } + } + } + return defaultsDeep(options, axisStyle) + } + + protected configYAxis(chart: Chart, options: G2Spec): G2Spec { + const { yAxis } = parseJson(chart.customStyle) + if (!yAxis.show) { + const axisHide = { + axis: { + y: false + } + } + return defaultsDeep(options, axisHide) + } + let lineLineDash = undefined + if (yAxis.axisLine.lineStyle.style === 'dashed') { + lineLineDash = [10, 8] + } + if (yAxis.axisLine.lineStyle.style === 'dotted') { + lineLineDash = [1, 2] + } + let gridLineDash = [0, 0] + if (yAxis.splitLine.lineStyle.style === 'dashed') { + gridLineDash = [10, 8] + } + if (yAxis.splitLine.lineStyle.style === 'dotted') { + gridLineDash = [1, 2] + } + const axisOption = { + axis: { + y: { + position: yAxis.position, + title: yAxis.nameShow === false ? false : yAxis.name, + titleFontSize: yAxis.fontSize, + titleFill: yAxis.color, + line: yAxis.axisLine.show, + lineStroke: yAxis.axisLine.lineStyle.color, + lineStrokeOpacity: 1, + lineLineWidth: yAxis.axisLine.lineStyle.width, + lineLineDash, + label: yAxis.axisLabel.show, + labelFill: yAxis.axisLabel.color, + labelFillOpacity: 1, + labelFontSize: yAxis.axisLabel.fontSize, + grid: yAxis.splitLine.show, + gridStroke: yAxis.splitLine.lineStyle.color, + gridStrokeOpacity: 1, + gridLineWidth: yAxis.splitLine.lineStyle.width, + gridLineDash, + transform: yAxis.axisLabel.rotate + ? [ + { + type: 'rotate', + optionalAngles: [yAxis.axisLabel.rotate], + recoverWhenFailed: false + } + ] + : [], + labelFormatter: d => { + return valueFormatter(d, yAxis.axisLabelFormatter) + } + } + } + } + if (!yAxis.axisValue.auto) { + const scaleOpt = { + scale: { + y: { + domainMin: yAxis.axisValue.min, + domainMax: yAxis.axisValue.max, + tickCount: yAxis.axisValue.splitCount, + tickMethod: (min, max, count) => { + const step = (max - min) / count + const ticks = [] + for (let i = 0; i <= count; i++) { + ticks.push(min + step * i) + } + return ticks + } + } + } + } + defaultsDeep(axisOption, scaleOpt) + } + return defaultsDeep(options, axisOption) + } + + public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] { + return setUpGroupSeriesColor(chart, data) + } + + protected configLegend(chart: Chart, options: G2Spec): G2Spec { + const { legend } = parseJson(chart.customStyle) + if (!legend.show) { + return { ...options, legend: false } + } + const baseLegend = this.getLegend(chart) + const tmpLegend = { + legend: { + color: { + ...baseLegend, + itemMarkerSize: legend.size, + itemMarker: legend.icon + } + } + } + defaultsDeep(options, tmpLegend) + return options + } + + protected configAssistLine(chart: Chart, options: G2Spec): G2Spec { + const { assistLineCfg } = parseJson(chart.senior) + if (!assistLineCfg.enable || !assistLineCfg.assistLine?.length) { + return options + } + const lineData = [] + const { yAxis } = parseJson(chart.customStyle) + const position = yAxis.position === 'left' ? 'left' : 'right' + const axisFormatterCfg = yAxis.axisLabelFormatter ?? DEFAULT_YAXIS_STYLE.axisLabelFormatter + const dynamicFields = [] + assistLineCfg.assistLine?.forEach(item => { + // 固定值 + if (item.field === '0') { + lineData.push(item) + } + // 动态值 + if (item.field === '1') { + dynamicFields.push(item.fieldId) + } + }) + chart.data.dynamicAssistLines?.forEach(item => { + if (dynamicFields.includes(item.fieldId)) { + lineData.push(item) + } + }) + let max, min + options.data.value.forEach(item => { + const value = item.value + if (value === null || value === undefined) { + return + } + if (max === undefined || value > max) { + max = value + } + if (min === undefined || value < min) { + min = value + } + }) + if (lineData.length) { + const assistLineMark: G2Spec = { + type: 'lineY', + encode: { y: 'value' }, + scale: { + y: { + domain: [min, max] + } + }, + data: lineData, + style: { + stroke: d => d.color, + lineDash: d => (d.lineType === 'solid' ? [] : d.lineType === 'dashed' ? [10, 8] : [1, 2]), + opacity: 1 + }, + labels: [ + { + text: d => { + const value = valueFormatter(parseFloat(d.value), axisFormatterCfg) + return d.name ? `${d.name}: ${value}` : value + }, + style: { + fontSize: d => parseInt(d.fontSize), + fill: d => d.color, + fillOpacity: 1 + }, + position: position, + textBaseline: 'bottom', + transform: [{ type: 'overlapHide' }, { type: 'exceedAdjust' }], + fontFamily: chart.fontFamily + } + ] + } + options.children.push(assistLineMark) + } + return options + } + + protected configTooltip(chart: Chart, options: G2Spec): G2Spec { + const customAttr: DeepPartial = 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 + let g2TooltipWrapper = document.getElementById('G2-TOOLTIP-WRAPPER') + if (!g2TooltipWrapper) { + g2TooltipWrapper = document.createElement('div') + g2TooltipWrapper.id = 'G2-TOOLTIP-WRAPPER' + g2TooltipWrapper.style.position = 'absolute' + g2TooltipWrapper.style.pointerEvents = 'none' + g2TooltipWrapper.style.zIndex = '9999' + document.body.appendChild(g2TooltipWrapper) + } + const lineMark = options.children[1] + const tooltipOptions: G2Spec = { + tooltip: d => d, + interaction: { + tooltip: { + crosshairsLineDash: [4, 4], + mount: g2TooltipWrapper, + css: { + '.g2-tooltip': { + background: tooltipAttr.backgroundColor + }, + '.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` + } + }, + render: (e, { 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 => { + if (item.value === null || item.value === undefined) { + return + } + const formatter = formatterMap[item.quotaList[0].id] ?? yAxis[0] + const value = valueFormatter(item.value, formatter.formatterCfg) + result.push({ ...item, name: item.category, 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 = `
    ${itemsHtml}
` + return `${titleHtml}${listHtml}` + } + } + } + } + defaultsDeep(lineMark, tooltipOptions) + return options + } + + protected configSlider(chart: Chart, options: G2Spec): G2Spec { + const { functionCfg } = parseJson(chart.senior) + if (!functionCfg?.sliderShow) { + return options + } + const lineMark = options.children[1] + const sliderOpt = { + slider: { + x: { + values: [functionCfg.sliderRange[0] / 100, functionCfg.sliderRange[1] / 100], + style: { + trackFill: functionCfg.sliderBg, + selectionFill: functionCfg.sliderFillBg, + handleLabelFill: functionCfg.sliderTextColor, + sparklineLineStrokeOpacity: 0 + } + } + } + } + defaultsDeep(lineMark, sliderOpt) + return options + } + + protected setupOptions(chart: Chart, options: G2Spec, context: Record): G2Spec { + return flow( + this.configTheme, + this.configColor, + this.configLabel, + this.configBasicStyle, + this.configLegend, + this.configXAxis, + this.configYAxis, + this.configAssistLine, + this.configTooltip, + this.configSlider + )(chart, options, context, this) + } + + constructor(name = 'area') { + super(name, DEFAULT_DATA) + } +} diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/g2/line/line.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/g2/line/line.ts index 963be231e8..2436336c66 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/charts/g2/line/line.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/g2/line/line.ts @@ -234,7 +234,16 @@ export class Line extends G2ChartView { return labelCfg.position } }, - textBaseline: 'bottom', + textBaseline: d => { + if (d.EXTREME || !labelAttr.seriesLabelFormatter?.length) { + return 'bottom' + } + const labelCfg = formatterMap?.[d.quotaList[0].id] as SeriesFormatter + if (!labelCfg?.show) { + return 'bottom' + } + return labelCfg.position === 'top' ? 'bottom' : 'top' + }, transform: labelAttr.fullDisplay ? [] : [{ type: 'overlapHide' }, { type: 'exceedAdjust' }],