@@ -58153,7 +58152,7 @@ var o1 = { exports: {} };
Y.push(we);
}
}), ue.length) {
- R.customAlert(P("\u89C6\u9891\u9A8C\u8BC1\u672A\u901A\u8FC7") + `:
+ R.customAlert(P("\u89C6\u9891\u9A8C\u8BC1\u672A\u901A\u8FC7") + `:
` + ue.join(`
`), "warning");
return;
diff --git a/frontend/src/data-public/Dashboard.vue b/frontend/src/data-public/Dashboard.vue
index d832f33..cd11279 100644
--- a/frontend/src/data-public/Dashboard.vue
+++ b/frontend/src/data-public/Dashboard.vue
@@ -85,13 +85,24 @@
快捷操作
-
+
- FormCreat 编辑面板
+ FormCreat
+
+
+
+
+
+ GIS大屏
+
+
+
+
+
+ 数据看板
-
@@ -128,7 +139,6 @@ const stats = reactive({
dictCount: 0
})
-// 最近活动
const activities = ref([
{
id: 1,
diff --git a/frontend/src/data-public/FormCreateDesigner.vue b/frontend/src/data-public/FormCreateDesigner.vue
index 74a5a47..bb708cc 100644
--- a/frontend/src/data-public/FormCreateDesigner.vue
+++ b/frontend/src/data-public/FormCreateDesigner.vue
@@ -14,10 +14,13 @@ import { onMounted, ref } from 'vue';
import { ElMessage } from 'element-plus-secondary'
import formCreate from '@/data-collect/render/element-plus/form-create.es.js'
+
import { useRoute } from 'vue-router'
+// import { moduleById,moduleUpdate } from '@/api/application/module'
+// const basePath = import.meta.env.VITE_API_BASEPATH
+
const route = useRoute()
const designerForm = formCreate.factory();
-debugger
const appId:any = ref(route.query.appId)
if(route.query.appId == null){
appId.value = '00'
diff --git a/frontend/src/data-visualization/DvPreview.vue b/frontend/src/data-visualization/DvPreview.vue
new file mode 100644
index 0000000..c6f6cb1
--- /dev/null
+++ b/frontend/src/data-visualization/DvPreview.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/data-visualization/canvas/DeCanvas.vue b/frontend/src/data-visualization/canvas/DeCanvas.vue
new file mode 100644
index 0000000..9603124
--- /dev/null
+++ b/frontend/src/data-visualization/canvas/DeCanvas.vue
@@ -0,0 +1,365 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/data-visualization/chart/components/editor/common/ChartTemplateInfo.vue b/frontend/src/data-visualization/chart/components/editor/common/ChartTemplateInfo.vue
new file mode 100644
index 0000000..8774c12
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/editor/common/ChartTemplateInfo.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+ {{
+ t('visualization.template_view_tips')
+ }}
+
+
+
+
+
+
diff --git a/frontend/src/data-visualization/chart/components/editor/common/TableTooltip.vue b/frontend/src/data-visualization/chart/components/editor/common/TableTooltip.vue
new file mode 100644
index 0000000..727a46f
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/editor/common/TableTooltip.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+ sort('asc')">{{ t('chart.asc') }}
+
+
+
+ sort('desc')">{{ t('chart.desc') }}
+
+
+
+ sort()">{{ t('chart.default') }}
+
+
+
+
diff --git a/frontend/src/data-visualization/chart/components/editor/util/StringUtils.ts b/frontend/src/data-visualization/chart/components/editor/util/StringUtils.ts
new file mode 100644
index 0000000..9883ed8
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/editor/util/StringUtils.ts
@@ -0,0 +1,56 @@
+// 替换所有 标准模板格式 为 $panelName$
+export function pdfTemplateReplaceAll(content, source, target) {
+ const pattern = '\\$' + source + '\\$'
+ content = content.replace(new RegExp(pattern, 'gm'), target)
+ return content
+}
+
+export function randomRange(min, max) {
+ let returnStr = ''
+ const range = max ? Math.round(Math.random() * (max - min)) + min : min
+ const charStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+
+ for (let i = 0; i < range; i++) {
+ const index = Math.round(Math.random() * (charStr.length - 1))
+ returnStr += charStr.substring(index, index + 1)
+ }
+ return returnStr
+}
+
+export function equalsAny(target, ...sources) {
+ for (let i = 0; i < sources.length; i++) {
+ if (target === sources[i]) {
+ return true
+ }
+ }
+ return false
+}
+
+export function includesAny(target, ...sources) {
+ if (!target || !sources) {
+ return false
+ }
+ for (let i = 0; i < sources.length; i++) {
+ if (target.includes(sources[i])) {
+ return true
+ }
+ }
+ return false
+}
+
+// 替换字符串中的国际化内容, 格式为$t('xxx')
+export function replaceInlineI18n(rawString) {
+ const res = []
+ const reg = /\$t\('([\w.]+)'\)/gm
+ let tmp
+ if (!rawString) {
+ return res
+ }
+ while ((tmp = reg.exec(rawString)) !== null) {
+ res.push(tmp)
+ }
+ res.forEach(tmp => {
+ rawString = rawString.replaceAll(tmp[0], tmp[1])
+ })
+ return rawString
+}
diff --git a/frontend/src/data-visualization/chart/components/editor/util/chart.ts b/frontend/src/data-visualization/chart/components/editor/util/chart.ts
new file mode 100644
index 0000000..22aa597
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/editor/util/chart.ts
@@ -0,0 +1,1759 @@
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { deepCopy } from '@/data-visualization/utils/utils'
+import { formatterItem } from '@/data-visualization/chart/components/js/formatter'
+const { t } = useI18n()
+
+export const DEFAULT_COLOR_CASE: DeepPartial
= {
+ basicStyle: {
+ colorScheme: 'default',
+ colors: [
+ '#1E90FF',
+ '#90EE90',
+ '#00CED1',
+ '#E2BD84',
+ '#7A90E0',
+ '#3BA272',
+ '#2BE7FF',
+ '#0A8ADA',
+ '#FFD700'
+ ],
+ alpha: 100,
+ gradient: false,
+ mapStyle: 'normal',
+ areaBaseColor: '#FFFFFF',
+ areaBorderColor: '#303133',
+ gaugeStyle: 'default',
+ tableBorderColor: '#E6E7E4',
+ tableScrollBarColor: 'rgba(0, 0, 0, 0.15)',
+ zoomButtonColor: '#aaa',
+ zoomBackground: '#fff'
+ },
+ misc: {
+ flowMapConfig: {
+ lineConfig: {
+ mapLineAnimate: true,
+ mapLineGradient: false,
+ mapLineSourceColor: '#1E90FF',
+ mapLineTargetColor: '#90EE90'
+ }
+ },
+ nameFontColor: '#000000',
+ valueFontColor: '#5470c6'
+ },
+ tableHeader: {
+ tableHeaderBgColor: '#1E90FF',
+ tableHeaderCornerBgColor: '#1E90FF',
+ tableHeaderColBgColor: '#1E90FF',
+ tableHeaderFontColor: '#000000',
+ tableHeaderCornerFontColor: '#000000',
+ tableHeaderColFontColor: '#000000'
+ },
+ tableCell: {
+ tableItemBgColor: '#FFFFFF',
+ tableFontColor: '#000000',
+ tableItemSubBgColor: '#EEEEEE'
+ }
+}
+
+export const DEFAULT_COLOR_CASE_LIGHT: DeepPartial = {
+ basicStyle: {
+ colorScheme: 'default',
+ colors: [
+ '#1E90FF',
+ '#90EE90',
+ '#00CED1',
+ '#E2BD84',
+ '#7A90E0',
+ '#3BA272',
+ '#2BE7FF',
+ '#0A8ADA',
+ '#FFD700'
+ ],
+ alpha: 100,
+ gradient: false,
+ mapStyle: 'normal',
+ areaBaseColor: '#FFFFFF',
+ areaBorderColor: '#303133',
+ gaugeStyle: 'default',
+ tableBorderColor: '#E6E7E4',
+ tableScrollBarColor: 'rgba(0, 0, 0, 0.15)',
+ zoomButtonColor: '#aaa',
+ zoomBackground: '#fff'
+ },
+ misc: {
+ flowMapConfig: {
+ lineConfig: {
+ mapLineAnimate: true,
+ mapLineGradient: false,
+ mapLineSourceColor: '#146C94',
+ mapLineTargetColor: '#576CBC'
+ }
+ },
+ nameFontColor: '#000000',
+ valueFontColor: '#5470c6'
+ },
+ tableHeader: {
+ tableHeaderBgColor: '#1E90FF',
+ tableHeaderCornerBgColor: '#1E90FF',
+ tableHeaderColBgColor: '#1E90FF',
+ tableHeaderFontColor: '#000000',
+ tableHeaderCornerFontColor: '#000000',
+ tableHeaderColFontColor: '#000000'
+ },
+ tableCell: {
+ tableItemBgColor: '#FFFFFF',
+ tableFontColor: '#000000',
+ tableItemSubBgColor: '#EEEEEE'
+ }
+}
+
+export const DEFAULT_COLOR_CASE_DARK: DeepPartial = {
+ basicStyle: {
+ colorScheme: 'default',
+ colors: [
+ '#1E90FF',
+ '#90EE90',
+ '#00CED1',
+ '#E2BD84',
+ '#7A90E0',
+ '#3BA272',
+ '#2BE7FF',
+ '#0A8ADA',
+ '#FFD700'
+ ],
+ alpha: 100,
+ gradient: false,
+ mapStyle: 'darkblue',
+ areaBaseColor: '#5470C6',
+ areaBorderColor: '#EBEEF5',
+ gaugeStyle: 'default',
+ tableBorderColor: '#CCCCCC',
+ tableScrollBarColor: 'rgba(255, 255, 255, 0.5)',
+ zoomButtonColor: '#fff',
+ zoomBackground: '#000'
+ },
+ misc: {
+ flowMapConfig: {
+ lineConfig: {
+ mapLineGradient: false,
+ mapLineSourceColor: '#146C94',
+ mapLineTargetColor: '#576CBC'
+ }
+ },
+ nameFontColor: '#ffffff',
+ valueFontColor: '#5470c6'
+ },
+ tableHeader: {
+ tableHeaderBgColor: '#1E90FF',
+ tableHeaderCornerBgColor: '#1E90FF',
+ tableHeaderColBgColor: '#1E90FF',
+ tableHeaderFontColor: '#FFFFFF',
+ tableHeaderCornerFontColor: '#FFFFFF',
+ tableHeaderColFontColor: '#FFFFFF'
+ },
+ tableCell: {
+ tableItemBgColor: '#131E42',
+ tableFontColor: '#ffffff',
+ tableItemSubBgColor: '#EEEEEE'
+ }
+}
+
+export const TAB_COMMON_STYLE_BASE = {
+ headPosition: 'left'
+}
+export const TAB_COMMON_STYLE_LIGHT = {
+ ...TAB_COMMON_STYLE_BASE,
+ headFontColor: '#000000',
+ headFontActiveColor: '#000000',
+ headBorderColor: '#ffffff',
+ headBorderActiveColor: '#ffffff'
+}
+export const TAB_COMMON_STYLE_DARK = {
+ ...TAB_COMMON_STYLE_BASE,
+ headFontColor: '#ffffff',
+ headFontActiveColor: '#ffffff',
+ headBorderColor: '#000000',
+ headBorderActiveColor: '#000000'
+}
+
+export const SENIOR_STYLE_SETTING_LIGHT = {
+ linkageIconColor: '#a6a6a6',
+ drillLayerColor: '#a6a6a6',
+ pagerColor: '#a6a6a6'
+}
+
+export const SENIOR_STYLE_SETTING_DARK = {
+ linkageIconColor: '#ffffff',
+ drillLayerColor: '#ffffff',
+ pagerColor: '#ffffff'
+}
+
+export const FILTER_COMMON_STYLE_BASE = {
+ layout: 'horizontal',
+ titleLayout: 'left'
+}
+
+export const FILTER_COMMON_STYLE_LIGHT = {
+ ...FILTER_COMMON_STYLE_BASE,
+ labelColor: '#1f2329',
+ titleColor: '#1f2329',
+ color: '#1f2329',
+ borderColor: '#bbbfc4',
+ text: '#1f2329',
+ bgColor: '#FFFFFF'
+}
+
+export const FILTER_COMMON_STYLE_DARK = {
+ ...FILTER_COMMON_STYLE_BASE,
+ labelColor: '#ffffff',
+ titleColor: '#ffffff',
+ color: '#FFFFFF',
+ borderColor: '#484747',
+ text: '#AFAFAF',
+ bgColor: '#131C42'
+}
+
+export const DEFAULT_TAB_COLOR_CASE_BASE = {
+ headPosition: 'left'
+}
+
+export const DEFAULT_TAB_COLOR_CASE_DARK = {
+ ...DEFAULT_TAB_COLOR_CASE_BASE,
+ headFontColor: '#FFFFFF',
+ headFontActiveColor: '#FFFFFF',
+ headBorderColor: '#131E42',
+ headBorderActiveColor: '#131E42'
+}
+
+export const DEFAULT_TAB_COLOR_CASE_LIGHT = {
+ ...DEFAULT_TAB_COLOR_CASE_BASE,
+ headFontColor: '#OOOOOO',
+ headFontActiveColor: '#OOOOOO',
+ headBorderColor: '#OOOOOO',
+ headBorderActiveColor: '#OOOOOO'
+}
+
+export const DEFAULT_MISC: ChartMiscAttr = {
+ pieInnerRadius: 0,
+ pieOuterRadius: 80,
+ radarShape: 'polygon',
+ radarSize: 80,
+ gaugeMinType: 'fix',
+ gaugeMinField: {
+ id: '',
+ summary: ''
+ },
+ gaugeMin: 0,
+ gaugeMaxType: 'dynamic',
+ gaugeMaxField: {
+ id: '',
+ summary: ''
+ },
+ gaugeMax: undefined,
+ gaugeStartAngle: 225,
+ gaugeEndAngle: -45,
+ nameFontSize: 18,
+ valueFontSize: 18,
+ nameValueSpace: 10,
+ valueFontColor: '#5470c6',
+ valueFontFamily: 'Microsoft YaHei',
+ valueFontIsBolder: false,
+ valueFontIsItalic: false,
+ valueLetterSpace: 0,
+ valueFontShadow: false,
+ showName: true,
+ nameFontColor: '#000000',
+ nameFontFamily: 'Microsoft YaHei',
+ nameFontIsBolder: false,
+ nameFontIsItalic: false,
+ nameLetterSpace: '0',
+ nameFontShadow: false,
+ treemapWidth: 80,
+ treemapHeight: 80,
+ liquidMax: undefined,
+ liquidMaxType: 'dynamic',
+ liquidMaxField: {
+ id: '',
+ summary: ''
+ },
+ liquidSize: 80,
+ liquidShape: 'circle',
+ hPosition: 'center',
+ vPosition: 'center',
+ mapPitch: 0,
+ wordSizeRange: [8, 32],
+ wordSpacing: 6,
+ mapAutoLegend: true,
+ mapLegendMax: 0,
+ mapLegendMin: 0,
+ mapLegendNumber: 9,
+ mapLegendRangeType: 'quantize',
+ mapLegendCustomRange: [],
+ flowMapConfig: {
+ lineConfig: {
+ mapLineAnimate: true,
+ mapLineType: 'arc',
+ mapLineWidth: 1,
+ mapLineAnimateDuration: 3,
+ mapLineGradient: false,
+ mapLineSourceColor: '#1E90FF',
+ mapLineTargetColor: '#90EE90',
+ alpha: 100
+ },
+ pointConfig: {
+ text: {
+ color: '#146C94',
+ fontSize: 10
+ },
+ point: {
+ color: '#146C94',
+ size: 4,
+ animate: false,
+ speed: 0.01
+ }
+ }
+ },
+ wordCloudAxisValueRange: {
+ auto: true,
+ min: 0,
+ max: 0,
+ fieldId: undefined
+ }
+}
+
+export const DEFAULT_MARK = {
+ fieldId: '',
+ conditions: []
+}
+export const DEFAULT_LABEL: ChartLabelAttr = {
+ show: false,
+ childrenShow: true,
+ position: 'top',
+ color: '#909399',
+ fontSize: 12,
+ formatter: '',
+ labelLine: {
+ show: true
+ },
+ labelFormatter: formatterItem,
+ reserveDecimalCount: 2,
+ labelShadow: false,
+ labelBgColor: '',
+ labelShadowColor: '',
+ quotaLabelFormatter: formatterItem,
+ showDimension: true,
+ showQuota: false,
+ showProportion: true,
+ seriesLabelFormatter: [],
+ conversionTag: {
+ show: false,
+ precision: 2,
+ text: t('chart.conversion_rate')
+ },
+ showTotal: false,
+ totalFontSize: 12,
+ totalColor: '#FFF',
+ totalFormatter: formatterItem,
+ showStackQuota: false,
+ fullDisplay: false,
+ proportionSeriesFormatter: {
+ show: false,
+ color: '#000',
+ fontSize: 12,
+ formatterCfg: {
+ decimalCount: 2
+ }
+ }
+}
+export const DEFAULT_TOOLTIP: ChartTooltipAttr = {
+ show: true,
+ trigger: 'item',
+ confine: true,
+ fontSize: 12,
+ color: '#909399',
+ tooltipFormatter: formatterItem,
+ backgroundColor: '#ffffff',
+ seriesTooltipFormatter: [],
+ carousel: {
+ enable: false,
+ stayTime: 3,
+ intervalTime: 0
+ }
+}
+export const DEFAULT_TABLE_TOTAL: ChartTableTotalAttr = {
+ row: {
+ showGrandTotals: true,
+ showSubTotals: true,
+ reverseLayout: false,
+ reverseSubLayout: false,
+ label: t('chart.total_show'),
+ subLabel: t('chart.sub_total_show'),
+ subTotalsDimensions: [],
+ subTotalsDimensionsNew: true,
+ calcTotals: {
+ aggregation: 'SUM',
+ cfg: []
+ },
+ calcSubTotals: {
+ aggregation: 'SUM',
+ cfg: []
+ },
+ totalSort: 'none',
+ totalSortField: ''
+ },
+ col: {
+ showGrandTotals: true,
+ showSubTotals: true,
+ reverseLayout: false,
+ reverseSubLayout: false,
+ label: t('chart.total_show'),
+ subLabel: t('chart.sub_total_show'),
+ subTotalsDimensions: [],
+ calcTotals: {
+ aggregation: 'SUM',
+ cfg: []
+ },
+ calcSubTotals: {
+ aggregation: 'SUM',
+ cfg: []
+ },
+ totalSort: 'none', // asc,desc
+ totalSortField: ''
+ }
+}
+export const DEFAULT_TABLE_HEADER: ChartTableHeaderAttr = {
+ indexLabel: t('relation.index'),
+ showIndex: false,
+ tableHeaderAlign: 'left',
+ tableHeaderCornerAlign: 'left',
+ tableHeaderColAlign: 'left',
+ tableHeaderBgColor: '#1E90FF',
+ tableHeaderCornerBgColor: '#1E90FF',
+ tableHeaderColBgColor: '#1E90FF',
+ tableHeaderFontColor: '#000000',
+ tableHeaderCornerFontColor: '#000000',
+ tableHeaderColFontColor: '#000000',
+ tableTitleFontSize: 12,
+ tableTitleCornerFontSize: 12,
+ tableTitleColFontSize: 12,
+ tableTitleHeight: 36,
+ tableHeaderSort: false,
+ showColTooltip: false,
+ showRowTooltip: false,
+ showTableHeader: true,
+ showHorizonBorder: true,
+ showVerticalBorder: true,
+ isItalic: false,
+ isCornerItalic: false,
+ isColItalic: false,
+ isBolder: true,
+ isCornerBolder: true,
+ isColBolder: true,
+ headerGroup: false,
+ headerGroupConfig: {
+ columns: [],
+ meta: []
+ }
+}
+export const DEFAULT_TABLE_CELL: ChartTableCellAttr = {
+ tableFontColor: '#000000',
+ tableItemAlign: 'right',
+ tableItemBgColor: '#FFFFFF',
+ tableItemFontSize: 12,
+ tableItemHeight: 36,
+ enableTableCrossBG: false,
+ tableItemSubBgColor: '#EEEEEE',
+ showTooltip: false,
+ showHorizonBorder: true,
+ showVerticalBorder: true,
+ isItalic: false,
+ isBolder: false,
+ tableFreeze: false,
+ tableColumnFreezeHead: 0,
+ tableRowFreezeHead: 0,
+ mergeCells: true
+}
+export const DEFAULT_TITLE_STYLE: ChartTextStyle = {
+ show: true,
+ fontSize: 16,
+ color: '#ffffff',
+ hPosition: 'left',
+ vPosition: 'top',
+ isItalic: false,
+ isBolder: true,
+ remarkShow: false,
+ remark: '',
+ remarkBackgroundColor: '#ffffff',
+ fontFamily: '',
+ letterSpace: '0',
+ fontShadow: false
+}
+
+export const DEFAULT_INDICATOR_STYLE: ChartIndicatorStyle = {
+ show: true,
+ fontSize: 20,
+ color: '#5470C6ff',
+ hPosition: 'center',
+ vPosition: 'center',
+ isItalic: false,
+ isBolder: true,
+ fontFamily: 'Microsoft YaHei',
+ letterSpace: 0,
+ fontShadow: false,
+ backgroundColor: '',
+
+ suffixEnable: true,
+ suffix: '',
+ suffixFontSize: 14,
+ suffixColor: '#5470C6ff',
+ suffixIsItalic: false,
+ suffixIsBolder: true,
+ suffixFontFamily: 'Microsoft YaHei',
+ suffixLetterSpace: 0,
+ suffixFontShadow: false
+}
+export const DEFAULT_INDICATOR_NAME_STYLE: ChartIndicatorNameStyle = {
+ show: true,
+ fontSize: 18,
+ color: '#ffffffff',
+ isItalic: false,
+ isBolder: true,
+ fontFamily: 'Microsoft YaHei',
+ letterSpace: 0,
+ fontShadow: false,
+ nameValueSpacing: 0
+}
+
+export const DEFAULT_TITLE_STYLE_BASE: ChartTextStyle = {
+ show: true,
+ fontSize: 16,
+ hPosition: 'left',
+ vPosition: 'top',
+ isItalic: false,
+ isBolder: true,
+ remarkShow: false,
+ remark: '',
+ fontFamily: '',
+ letterSpace: '0',
+ fontShadow: false,
+ color: '',
+ remarkBackgroundColor: ''
+}
+
+export const DEFAULT_TITLE_STYLE_LIGHT = {
+ ...DEFAULT_TITLE_STYLE_BASE,
+ color: '#000000',
+ remarkBackgroundColor: '#ffffff'
+}
+
+export const DEFAULT_TITLE_STYLE_DARK = {
+ ...DEFAULT_TITLE_STYLE_BASE,
+ color: '#FFFFFF',
+ remarkBackgroundColor: '#5A5C62'
+}
+
+export const DEFAULT_LEGEND_STYLE: ChartLegendStyle = {
+ show: true,
+ hPosition: 'center',
+ vPosition: 'bottom',
+ orient: 'horizontal',
+ icon: 'circle',
+ color: '#333333',
+ fontSize: 12,
+ size: 4
+}
+
+export const DEFAULT_LEGEND_STYLE_BASE: ChartLegendStyle = {
+ show: true,
+ hPosition: 'center',
+ vPosition: 'bottom',
+ orient: 'horizontal',
+ icon: 'circle',
+ color: '#333333',
+ fontSize: 12,
+ size: 4
+}
+
+export const DEFAULT_LEGEND_STYLE_LIGHT: ChartLegendStyle = {
+ ...DEFAULT_LEGEND_STYLE_BASE,
+ color: '#333333',
+ fontSize: 12
+}
+
+export const DEFAULT_LEGEND_STYLE_DARK: ChartLegendStyle = {
+ ...DEFAULT_LEGEND_STYLE_BASE,
+ color: '#ffffff',
+ fontSize: 12
+}
+
+export const DEFAULT_MARGIN_STYLE = {
+ marginModel: 'auto',
+ marginTop: 40,
+ marginBottom: 44,
+ marginLeft: 15,
+ marginRight: 10
+}
+
+export const DEFAULT_XAXIS_STYLE: ChartAxisStyle = {
+ show: true,
+ position: 'bottom',
+ nameShow: false,
+ name: '',
+ color: '#333333',
+ fontSize: 12,
+ axisLabel: {
+ show: true,
+ color: '#333333',
+ fontSize: 12,
+ rotate: 0,
+ formatter: '{value}',
+ lengthLimit: 10
+ },
+ axisLine: {
+ show: true,
+ lineStyle: {
+ color: '#cccccc',
+ width: 1,
+ style: 'solid'
+ }
+ },
+ splitLine: {
+ show: false,
+ lineStyle: {
+ color: '#cccccc',
+ width: 1,
+ style: 'solid'
+ }
+ },
+ axisValue: {
+ auto: true,
+ min: 10,
+ max: 100,
+ split: 10,
+ splitCount: 10
+ },
+ axisLabelFormatter: {
+ type: 'auto',
+ unit: 1,
+ suffix: '',
+ decimalCount: 2,
+ thousandSeparator: true
+ }
+}
+export const DEFAULT_YAXIS_STYLE: ChartAxisStyle = {
+ show: true,
+ position: 'left',
+ nameShow: false,
+ name: '',
+ color: '#333333',
+ fontSize: 12,
+ axisLabel: {
+ show: true,
+ color: '#333333',
+ fontSize: 12,
+ rotate: 0,
+ formatter: '{value}',
+ lengthLimit: 10
+ },
+ axisLine: {
+ show: false,
+ lineStyle: {
+ color: '#cccccc',
+ width: 1,
+ style: 'solid'
+ }
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#cccccc',
+ width: 1,
+ style: 'solid'
+ }
+ },
+ axisValue: {
+ auto: true,
+ min: 10,
+ max: 100,
+ split: 10,
+ splitCount: 10
+ },
+ axisLabelFormatter: {
+ type: 'auto',
+ unit: 1,
+ suffix: '',
+ decimalCount: 2,
+ thousandSeparator: true
+ }
+}
+export const DEFAULT_YAXIS_EXT_STYLE: ChartAxisStyle = {
+ show: true,
+ position: 'right',
+ name: '',
+ color: '#333333',
+ fontSize: 12,
+ axisLabel: {
+ show: true,
+ color: '#333333',
+ fontSize: 12,
+ rotate: 0,
+ formatter: '{value}'
+ },
+ axisLine: {
+ show: false,
+ lineStyle: {
+ color: '#cccccc',
+ width: 1,
+ style: 'solid'
+ }
+ },
+ splitLine: {
+ show: false,
+ lineStyle: {
+ color: '#cccccc',
+ width: 1,
+ style: 'solid'
+ }
+ },
+ axisValue: {
+ auto: true,
+ min: 10,
+ max: 100,
+ split: 10,
+ splitCount: 10
+ },
+ axisLabelFormatter: {
+ type: 'auto',
+ unit: 1,
+ suffix: '',
+ decimalCount: 2,
+ thousandSeparator: true
+ }
+}
+export const DEFAULT_BACKGROUND_COLOR = {
+ color: '#ffffff',
+ alpha: 0,
+ borderRadius: 0
+}
+export const DEFAULT_MISC_STYLE: ChartMiscStyle = {
+ showName: false,
+ color: '#999',
+ fontSize: 12,
+ axisColor: '#999',
+ splitNumber: 5,
+ axisLine: {
+ show: true,
+ lineStyle: {
+ color: '#999999',
+ width: 1,
+ type: 'solid'
+ }
+ },
+ axisTick: {
+ show: false,
+ length: 5,
+ lineStyle: {
+ color: '#999999',
+ width: 1,
+ type: 'solid'
+ }
+ },
+ axisLabel: {
+ show: false,
+ rotate: 0,
+ margin: 8,
+ color: '#999999',
+ fontSize: '12',
+ formatter: '{value}'
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#999999',
+ width: 1,
+ type: 'solid'
+ }
+ },
+ splitArea: {
+ show: true
+ },
+ axisValue: {
+ auto: true,
+ min: 10,
+ max: 100,
+ split: 10,
+ splitCount: 10
+ }
+}
+export const DEFAULT_FUNCTION_CFG: ChartFunctionCfg = {
+ sliderShow: false,
+ sliderRange: [0, 10],
+ sliderBg: '#FFFFFF',
+ sliderFillBg: '#BCD6F1',
+ sliderTextColor: '#999999',
+ emptyDataStrategy: 'breakLine',
+ emptyDataCustomValue: '',
+ emptyDataFieldCtrl: []
+}
+export const DEFAULT_ASSIST_LINE_CFG: ChartAssistLineCfg = {
+ enable: false,
+ assistLine: []
+}
+export const DEFAULT_THRESHOLD: ChartThreshold = {
+ enable: false,
+ gaugeThreshold: '',
+ liquidThreshold: '',
+ labelThreshold: [],
+ tableThreshold: [],
+ textLabelThreshold: [],
+ lineLabelThreshold: []
+}
+export const DEFAULT_SCROLL: ScrollCfg = {
+ open: false,
+ row: 1,
+ interval: 2000,
+ step: 50
+}
+
+export const DEFAULT_BUBBLE_ANIMATE: BubbleCfg = {
+ enable: false,
+ speed: 1,
+ rings: 1,
+ type: 'wave'
+}
+
+export const DEFAULT_QUADRANT_STYLE: QuadrantAttr = {
+ lineStyle: {
+ stroke: '#aaa',
+ lineWidth: 1,
+ opacity: 0.5
+ },
+ regionStyle: [
+ {
+ fill: '#fdfcfc',
+ fillOpacity: 0
+ },
+ {
+ fill: '#fafdfa',
+ fillOpacity: 0
+ },
+ {
+ fill: '#fdfcfc',
+ fillOpacity: 0
+ },
+ {
+ fill: '#fafdfa',
+ fillOpacity: 0
+ }
+ ],
+ labels: [
+ {
+ content: '',
+ style: {
+ fill: '#000000',
+ fillOpacity: 0.5,
+ fontSize: 14
+ }
+ },
+ {
+ content: '',
+ style: {
+ fill: '#000000',
+ fillOpacity: 0.5,
+ fontSize: 14
+ }
+ },
+ {
+ content: '',
+ style: {
+ fill: '#000000',
+ fillOpacity: 0.5,
+ fontSize: 14
+ }
+ },
+ {
+ content: '',
+ style: {
+ fill: '#000000',
+ fillOpacity: 0.5,
+ fontSize: 14
+ }
+ }
+ ]
+}
+
+export const COLOR_PANEL = [
+ '#FF4500',
+ '#FF8C00',
+ '#FFD700',
+ '#71AE46',
+ '#00CED1',
+ '#1E90FF',
+ '#C71585',
+ '#999999',
+ '#000000',
+ '#FFFFFF'
+]
+
+export const COLOR_CASES = [
+ {
+ name: t('chart.color_default'),
+ value: 'default',
+ colors: [
+ '#1E90FF',
+ '#90EE90',
+ '#00CED1',
+ '#E2BD84',
+ '#7A90E0',
+ '#3BA272',
+ '#2BE7FF',
+ '#0A8ADA',
+ '#FFD700'
+ ]
+ },
+ {
+ name: t('chart.color_retro'),
+ value: 'retro',
+ colors: [
+ '#0780cf',
+ '#765005',
+ '#fa6d1d',
+ '#0e2c82',
+ '#b6b51f',
+ '#da1f18',
+ '#701866',
+ '#f47a75',
+ '#009db2'
+ ]
+ },
+ {
+ name: t('chart.color_elegant'),
+ value: 'elegant',
+ colors: [
+ '#95a2ff',
+ '#fa8080',
+ '#ffc076',
+ '#fae768',
+ '#87e885',
+ '#3cb9fc',
+ '#73abf5',
+ '#cb9bff',
+ '#434348'
+ ]
+ },
+ {
+ name: t('chart.color_future'),
+ value: 'future',
+ colors: [
+ '#63b2ee',
+ '#76da91',
+ '#f8cb7f',
+ '#f89588',
+ '#7cd6cf',
+ '#9192ab',
+ '#7898e1',
+ '#efa666',
+ '#eddd86'
+ ]
+ },
+ {
+ name: t('chart.color_gradual'),
+ value: 'gradual',
+ colors: [
+ '#71ae46',
+ '#96b744',
+ '#c4cc38',
+ '#ebe12a',
+ '#eab026',
+ '#e3852b',
+ '#d85d2a',
+ '#ce2626',
+ '#ac2026'
+ ]
+ },
+ {
+ name: t('chart.color_simple'),
+ value: 'simple',
+ colors: [
+ '#929fff',
+ '#9de0ff',
+ '#ffa897',
+ '#af87fe',
+ '#7dc3fe',
+ '#bb60b2',
+ '#433e7c',
+ '#f47a75',
+ '#009db2'
+ ]
+ },
+ {
+ name: t('chart.color_business'),
+ value: 'business',
+ colors: [
+ '#194f97',
+ '#555555',
+ '#bd6b08',
+ '#00686b',
+ '#c82d31',
+ '#625ba1',
+ '#898989',
+ '#9c9800',
+ '#007f54'
+ ]
+ },
+ {
+ name: t('chart.color_gentle'),
+ value: 'gentle',
+ colors: [
+ '#5b9bd5',
+ '#ed7d31',
+ '#70ad47',
+ '#ffc000',
+ '#4472c4',
+ '#91d024',
+ '#b235e6',
+ '#02ae75',
+ '#5b9bd5'
+ ]
+ },
+ {
+ name: t('chart.color_technology'),
+ value: 'technology',
+ colors: [
+ '#05f8d6',
+ '#0082fc',
+ '#fdd845',
+ '#22ed7c',
+ '#09b0d3',
+ '#1d27c9',
+ '#f9e264',
+ '#f47a75',
+ '#009db2'
+ ]
+ },
+ {
+ name: t('chart.color_light'),
+ value: 'light',
+ colors: [
+ '#884898',
+ '#808080',
+ '#82ae46',
+ '#00a3af',
+ '#ef8b07',
+ '#007bbb',
+ '#9d775f',
+ '#fae800',
+ '#5f9b3c'
+ ]
+ },
+ {
+ name: t('chart.color_classical'),
+ value: 'classical',
+ colors: [
+ '#007bbb',
+ '#ffdb4f',
+ '#dd4b4b',
+ '#2ca9e1',
+ '#ef8b07',
+ '#4a488e',
+ '#82ae46',
+ '#dd4b4b',
+ '#bb9581'
+ ]
+ },
+ {
+ name: t('chart.color_fresh'),
+ value: 'fresh',
+ colors: [
+ '#5f9b3c',
+ '#75c24b',
+ '#83d65f',
+ '#aacf53',
+ '#c7dc68',
+ '#d8e698',
+ '#e0ebaf',
+ '#bbc8e6',
+ '#e5e5e5'
+ ]
+ },
+ {
+ name: t('chart.color_energy'),
+ value: 'energy',
+ colors: [
+ '#ef8b07',
+ '#2a83a2',
+ '#f07474',
+ '#c55784',
+ '#274a78',
+ '#7058a3',
+ '#0095d9',
+ '#75c24b',
+ '#808080'
+ ]
+ },
+ {
+ name: t('chart.color_red'),
+ value: 'red',
+ colors: [
+ '#ff0000',
+ '#ef8b07',
+ '#4c6cb3',
+ '#f8e944',
+ '#69821b',
+ '#9c5ec3',
+ '#00ccdf',
+ '#f07474',
+ '#bb9581'
+ ]
+ },
+ {
+ name: t('chart.color_fast'),
+ value: 'fast',
+ colors: [
+ '#fae800',
+ '#00c039',
+ '#0482dc',
+ '#bb9581',
+ '#ff7701',
+ '#9c5ec3',
+ '#00ccdf',
+ '#00c039',
+ '#ff7701'
+ ]
+ },
+ {
+ name: t('chart.color_spiritual'),
+ value: 'spiritual',
+ colors: [
+ '#00a3af',
+ '#4da798',
+ '#57baaa',
+ '#62d0bd',
+ '#6ee4d0',
+ '#86e7d6',
+ '#aeede1',
+ '#bde1e6',
+ '#e5e5e5'
+ ]
+ }
+]
+
+export const BASE_ECHARTS_SELECT = {
+ itemStyle: {
+ shadowBlur: 2
+ }
+}
+
+export const CHART_FONT_FAMILY_ORIGIN = [
+ { name: t('chart.font_family_ya_hei'), value: 'Microsoft YaHei' },
+ { name: t('chart.font_family_song_ti'), value: 'SimSun, "Songti SC", STSong' },
+ { name: t('chart.font_family_hei_ti'), value: 'SimHei, Helvetica' },
+ { name: t('chart.font_family_kai_ti'), value: 'KaiTi, "Kaiti SC", STKaiti' }
+]
+
+export const CHART_FONT_FAMILY_MAP_TRANS = {
+ 'Microsoft YaHei': 'Microsoft YaHei',
+ 'SimSun, "Songti SC", STSong': 'SimSun',
+ 'SimHei, Helvetica': 'SimHei',
+ 'KaiTi, "Kaiti SC", STKaiti': 'KaiTi'
+}
+
+export const CHART_FONT_FAMILY = [
+ { name: t('chart.font_family_ya_hei'), value: 'Microsoft YaHei' },
+ { name: t('chart.font_family_song_ti'), value: 'SimSun' },
+ { name: t('chart.font_family_hei_ti'), value: 'SimHei' },
+ { name: t('chart.font_family_kai_ti'), value: 'KaiTi' }
+]
+
+export const CHART_FONT_FAMILY_MAP:any = {
+ 'Microsoft YaHei': 'Microsoft YaHei',
+ SimSun: 'SimSun, "Songti SC", STSong',
+ SimHei: 'SimHei, Helvetica',
+ KaiTi: 'KaiTi, "Kaiti SC", STKaiti'
+}
+
+export const CHART_FONT_LETTER_SPACE = [
+ { name: '0px', value: 0 },
+ { name: '1px', value: 1 },
+ { name: '2px', value: 2 },
+ { name: '3px', value: 3 },
+ { name: '4px', value: 4 },
+ { name: '5px', value: 5 },
+ { name: '6px', value: 6 },
+ { name: '7px', value: 7 },
+ { name: '8px', value: 8 },
+ { name: '9px', value: 9 },
+ { name: '10px', value: 10 }
+]
+
+export const NOT_SUPPORT_PAGE_DATASET = [
+ 'kylin',
+ 'sqlServer',
+ 'es',
+ 'presto',
+ 'ds_doris',
+ 'StarRocks',
+ 'impala'
+]
+
+export const SUPPORT_Y_M = ['y', 'y_M', 'y_M_d']
+
+export const DEFAULT_MAP = {
+ mapPitch: 0,
+ lineType: 'line',
+ lineWidth: 1,
+ lineAnimate: true,
+ lineAnimateDuration: 4,
+ lineAnimateInterval: 0.5,
+ lineAnimateTrailLength: 0.1
+}
+
+export const CHART_TYPE_CONFIGS = [
+ {
+ category: 'quota',
+ title: t('chart.chart_type_quota'),
+ display: 'show',
+ details: [
+ {
+ render: 'antv',
+ category: 'quota',
+ value: 'gauge',
+ title: t('chart.chart_gauge'),
+ icon: 'gauge'
+ },
+ {
+ render: 'antv',
+ category: 'quota',
+ value: 'liquid',
+ title: t('chart.chart_liquid'),
+ icon: 'liquid'
+ },
+ {
+ render: 'custom',
+ category: 'quota',
+ value: 'indicator',
+ title: t('chart.chart_indicator'),
+ icon: 'indicator'
+ }
+ ]
+ },
+ {
+ category: 'table',
+ title: t('chart.chart_type_table'),
+ display: 'show',
+ details: [
+ {
+ render: 'antv',
+ category: 'table',
+ value: 'table-info',
+ title: t('chart.chart_table_info'),
+ icon: 'table-info'
+ },
+ {
+ render: 'antv',
+ category: 'table',
+ 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'
+ },
+ {
+ render: 'antv',
+ category: 'table',
+ value: 't-heatmap',
+ title: t('chart.chart_table_heatmap'),
+ icon: 't-heatmap'
+ }
+ ]
+ },
+ {
+ category: 'trend',
+ title: t('chart.chart_type_trend'),
+ display: 'show',
+ details: [
+ {
+ render: 'antv',
+ category: 'trend',
+ value: 'line',
+ title: t('chart.chart_line'),
+ icon: 'line'
+ },
+ {
+ render: 'antv',
+ category: 'trend',
+ value: 'area',
+ title: t('chart.chart_area'),
+ icon: 'area'
+ },
+ {
+ render: 'antv',
+ category: 'trend',
+ value: 'area-stack',
+ title: t('chart.chart_area_stack'),
+ icon: 'area-stack'
+ }
+ ]
+ },
+ {
+ category: 'compare',
+ title: t('chart.chart_type_compare'),
+ display: 'show',
+ details: [
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'bar',
+ title: t('chart.chart_bar'),
+ icon: 'bar'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'bar-stack',
+ title: t('chart.chart_bar_stack'),
+ icon: 'bar-stack'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'percentage-bar-stack',
+ title: t('chart.chart_percentage_bar_stack'),
+ icon: 'percentage-bar-stack'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'bar-group',
+ title: t('chart.chart_bar_group'),
+ icon: 'bar-group'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'bar-group-stack',
+ title: t('chart.chart_bar_group_stack'),
+ icon: 'bar-group-stack'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'waterfall',
+ title: t('chart.chart_waterfall'),
+ icon: 'waterfall'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'bar-horizontal',
+ title: t('chart.chart_bar_horizontal'),
+ icon: 'bar-horizontal'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'bar-stack-horizontal',
+ title: t('chart.chart_bar_stack_horizontal'),
+ icon: 'bar-stack-horizontal'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'percentage-bar-stack-horizontal',
+ title: t('chart.chart_percentage_bar_stack_horizontal'),
+ icon: 'percentage-bar-stack-horizontal'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'bar-range',
+ title: t('chart.chart_bar_range'),
+ icon: 'bar-range'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'bidirectional-bar',
+ title: t('chart.chart_bidirectional_bar'),
+ icon: 'bidirectional-bar'
+ },
+ {
+ render: 'antv',
+ category: 'compare',
+ value: 'progress-bar',
+ title: t('chart.chart_progress_bar'),
+ icon: 'progress-bar'
+ },
+ {
+ render: 'antv',
+ category: 'trend',
+ value: 'stock-line',
+ title: t('chart.chart_stock_line'),
+ icon: 'stock-line'
+ }
+ ]
+ },
+ {
+ category: 'distribute',
+ title: t('chart.chart_type_distribute'),
+ display: 'show',
+ details: [
+ {
+ render: 'antv',
+ category: 'distribute',
+ value: 'pie',
+ title: t('chart.chart_pie'),
+ icon: 'pie'
+ },
+ {
+ render: 'antv',
+ category: 'distribute',
+ value: 'pie-donut',
+ title: t('chart.chart_pie_donut'),
+ icon: 'pie-donut'
+ },
+ {
+ render: 'antv',
+ category: 'distribute',
+ value: 'pie-rose',
+ title: t('chart.chart_pie_rose'),
+ icon: 'pie-rose'
+ },
+ {
+ render: 'antv',
+ category: 'distribute',
+ value: 'pie-donut-rose',
+ title: t('chart.chart_pie_donut_rose'),
+ icon: 'pie-donut-rose'
+ },
+ {
+ render: 'antv',
+ category: 'chart.chart_type_distribute',
+ value: 'radar',
+ title: t('chart.chart_radar'),
+ icon: 'radar'
+ },
+ {
+ render: 'antv',
+ category: 'distribute',
+ value: 'treemap',
+ title: t('chart.chart_treemap'),
+ icon: 'treemap'
+ },
+ {
+ render: 'antv',
+ category: 'distribute',
+ value: 'word-cloud',
+ title: t('chart.chart_word_cloud'),
+ icon: 'word-cloud'
+ }
+ ]
+ },
+ {
+ category: 'map',
+ title: t('chart.chart_type_space'),
+ display: 'show',
+ details: [
+ {
+ render: 'antv',
+ category: 'map',
+ value: 'map',
+ title: t('chart.chart_map'),
+ icon: 'map'
+ },
+ {
+ render: 'antv',
+ category: 'map',
+ value: 'bubble-map',
+ title: t('chart.chart_bubble_map'),
+ icon: 'bubble-map'
+ },
+ {
+ render: 'antv',
+ category: 'map',
+ value: 'flow-map',
+ title: t('chart.chart_flow_map'),
+ icon: 'flow-map'
+ },
+ {
+ render: 'antv',
+ category: 'map',
+ value: 'heat-map',
+ title: t('chart.chart_heat_map'),
+ icon: 'heat-map'
+ },
+ {
+ render: 'antv',
+ category: 'map',
+ value: 'symbolic-map',
+ title: t('chart.chart_symbolic_map'),
+ icon: 'symbolic-map'
+ }
+ ]
+ },
+ {
+ category: 'relation',
+ title: t('chart.chart_type_relation'),
+ display: 'show',
+ details: [
+ {
+ render: 'antv',
+ category: 'distribute',
+ value: 'scatter',
+ title: t('chart.chart_scatter'),
+ icon: 'scatter'
+ },
+ {
+ render: 'antv',
+ category: 'distribute',
+ value: 'quadrant',
+ title: t('chart.chart_quadrant'),
+ icon: 'quadrant'
+ },
+ {
+ render: 'antv',
+ category: 'distribute',
+ value: 'funnel',
+ title: t('chart.chart_funnel'),
+ icon: 'funnel'
+ },
+ {
+ render: 'antv',
+ category: 'relation',
+ value: 'sankey',
+ title: t('chart.chart_sankey'),
+ icon: 'sankey'
+ },
+ {
+ render: 'antv',
+ category: 'distribute',
+ value: 'circle-packing',
+ title: t('chart.chart_circle_packing'),
+ icon: 'circle-packing'
+ }
+ ]
+ },
+ {
+ 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'
+ },
+ {
+ render: 'antv',
+ category: 'dual_axes',
+ value: 'chart-mix-group',
+ title: t('chart.chart_mix_group_column'),
+ icon: 'chart-mix-group'
+ },
+ {
+ render: 'antv',
+ category: 'dual_axes',
+ value: 'chart-mix-stack',
+ title: t('chart.chart_mix_stack_column'),
+ icon: 'chart-mix-stack'
+ },
+ {
+ render: 'antv',
+ category: 'dual_axes',
+ value: 'chart-mix-dual-line',
+ title: t('chart.chart_mix_dual_line'),
+ icon: 'chart-mix-dual-line'
+ }
+ ]
+ },
+ {
+ category: 'other',
+ title: t('datasource.other'),
+ display: 'hidden',
+ details: [
+ {
+ render: 'custom',
+ category: 'quota',
+ value: 'rich-text',
+ title: t('visualization.rich_text'),
+ icon: 'rich-text'
+ },
+ {
+ render: 'custom',
+ category: 'quota',
+ value: 'picture-group',
+ title: t('visualization.picture_group'),
+ icon: 'picture-group'
+ }
+ ]
+ }
+]
+
+export const DEFAULT_BASIC_STYLE: ChartBasicStyle = {
+ alpha: 100,
+ tableBorderColor: '#CCCCCC',
+ tableScrollBarColor: '#1f23294d',
+ tableColumnMode: 'adapt',
+ tableColumnWidth: 100,
+ tableFieldWidth: [],
+ tablePageMode: 'page',
+ tablePageStyle: 'simple',
+ tablePageSize: 20,
+ gaugeStyle: 'default',
+ colorScheme: 'default',
+ colors: [
+ '#5470c6',
+ '#91cc75',
+ '#fac858',
+ '#ee6666',
+ '#73c0de',
+ '#3ba272',
+ '#fc8452',
+ '#9a60b4',
+ '#ea7ccc'
+ ],
+ mapVendor: 'amap',
+ gradient: false,
+ lineWidth: 2,
+ lineSymbol: 'circle',
+ lineSymbolSize: 4,
+ lineSmooth: true,
+ barDefault: true,
+ radiusColumnBar: 'rightAngle',
+ columnBarRightAngleRadius: 20,
+ columnWidthRatio: 60,
+ barWidth: 40,
+ barGap: 0.4,
+ lineType: 'solid',
+ scatterSymbol: 'circle',
+ scatterSymbolSize: 8,
+ radarShape: 'polygon',
+ mapStyle: 'normal',
+ heatMapType: 'heatmap',
+ heatMapIntensity: 2,
+ heatMapRadius: 20,
+ areaBorderColor: '#EBEEF5',
+ areaBaseColor: '#ffffff',
+ mapSymbolOpacity: 0.7,
+ mapSymbolStrokeWidth: 2,
+ mapSymbol: 'circle',
+ mapSymbolSize: 6,
+ radius: 80,
+ innerRadius: 60,
+ showZoom: true,
+ zoomButtonColor: '#aaa',
+ zoomBackground: '#fff',
+ tableLayoutMode: 'grid',
+ calcTopN: false,
+ topN: 5,
+ topNLabel: t('datasource.other'),
+ gaugeAxisLine: true,
+ gaugePercentLabel: true,
+ showSummary: false,
+ summaryLabel: t('chart.total_show'),
+ seriesColor: [],
+ layout: 'horizontal',
+ mapSymbolSizeMin: 4,
+ mapSymbolSizeMax: 30,
+ showLabel: true,
+ mapStyleUrl: '',
+ autoFit: true,
+ mapCenter: {
+ longitude: 117.232,
+ latitude: 39.354
+ },
+ zoomLevel: 7,
+ customIcon: '',
+ showHoverStyle: true,
+ autoWrap: false,
+ maxLines: 3,
+ radarShowPoint: true,
+ radarPointSize: 4,
+ radarAreaColor: true,
+ circleBorderColor: '#fff',
+ circleBorderWidth: 0,
+ circlePadding: 0
+}
+
+export const BASE_VIEW_CONFIG = {
+ id: '', // 图表id
+ title: t('data_set.view'),
+ sceneId: 0, // 仪表板id
+ tableId: '', // 数据集id
+ type: 'bar',
+ render: 'antv',
+ resultCount: 1000,
+ resultMode: 'custom',
+ refreshViewEnable: false,
+ refreshTime: 5,
+ refreshUnit: 'minute',
+ xAxis: [],
+ xAxisExt: [],
+ yAxis: [],
+ yAxisExt: [],
+ extStack: [],
+ drillFields: [],
+ viewFields: [],
+ extBubble: [],
+ extLabel: [],
+ extTooltip: [],
+ customFilter: {},
+ sortPriority: [],
+ customAttr: {
+ basicStyle: DEFAULT_BASIC_STYLE,
+ misc: DEFAULT_MISC,
+ label: DEFAULT_LABEL,
+ tooltip: DEFAULT_TOOLTIP,
+ tableTotal: DEFAULT_TABLE_TOTAL,
+ tableHeader: DEFAULT_TABLE_HEADER,
+ tableCell: DEFAULT_TABLE_CELL,
+ indicator: DEFAULT_INDICATOR_STYLE,
+ indicatorName: DEFAULT_INDICATOR_NAME_STYLE,
+ map: {
+ id: '',
+ level: 'world'
+ }
+ },
+ customStyle: {
+ text: DEFAULT_TITLE_STYLE,
+ legend: DEFAULT_LEGEND_STYLE,
+ xAxis: DEFAULT_XAXIS_STYLE,
+ yAxis: DEFAULT_YAXIS_STYLE,
+ yAxisExt: DEFAULT_YAXIS_EXT_STYLE,
+ misc: DEFAULT_MISC_STYLE
+ },
+ senior: {
+ functionCfg: DEFAULT_FUNCTION_CFG,
+ assistLineCfg: DEFAULT_ASSIST_LINE_CFG,
+ threshold: DEFAULT_THRESHOLD,
+ scrollCfg: DEFAULT_SCROLL,
+ areaMapping: {},
+ bubbleCfg: DEFAULT_BUBBLE_ANIMATE
+ },
+ flowMapStartName: [],
+ flowMapEndName: []
+}
+
+export function getScaleValue(propValue, scale) {
+ const propValueTemp = Math.round(propValue * scale)
+ return propValueTemp > 1 ? propValueTemp : 1
+}
+
+export function getViewConfig(name) {
+ let viewConfigResult = null
+ CHART_TYPE_CONFIGS.forEach(category => {
+ category.details.forEach(viewConfig => {
+ if (viewConfig.value === name) {
+ viewConfigResult = deepCopy(viewConfig)
+ }
+ })
+ })
+ return viewConfigResult
+}
diff --git a/frontend/src/data-visualization/chart/components/editor/util/dataVisualization.ts b/frontend/src/data-visualization/chart/components/editor/util/dataVisualization.ts
new file mode 100644
index 0000000..93b6919
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/editor/util/dataVisualization.ts
@@ -0,0 +1,142 @@
+export const VIEW_DETAILS_BASH_STYLE =
+ '{"id":"view-dialog-details-001","title":"图表明细","sceneId":0,"tableId":"1692381412250939392","type":"table-info","render":"antv","resultCount":1000,"resultMode":"all","refreshViewEnable":false,"refreshTime":5,"refreshUnit":"minute","xAxis":[{"id":"1692330126490","datasourceId":"1691734038709071872","datasetTableId":"7098147058204282880","datasetGroupId":"1692381412250939392","chartId":null,"originName":"月","name":"月","dbFieldName":null,"description":null,"dataeaseName":"f_dd62e53a9192cdf4","groupType":"d","type":"ANY","precision":null,"scale":null,"deType":0,"deExtractType":0,"extField":0,"checked":true,"columnIndex":null,"lastSyncTime":null,"dateFormat":null,"dateFormatType":null,"fieldShortName":"f_dd62e53a9192cdf4","summary":null,"sort":"none","dateStyle":"y_M_d","datePattern":"date_sub","chartType":null,"compareCalc":null,"logic":null,"filterType":null,"index":null,"formatterCfg":null,"chartShowName":null,"filter":[],"customSort":null,"busiType":null},{"id":"1692330126489","datasourceId":"1691734038709071872","datasetTableId":"7098147058204282880","datasetGroupId":"1692381412250939392","chartId":null,"originName":"年份","name":"年份","dbFieldName":null,"description":null,"dataeaseName":"f_190480c43bdda8df","groupType":"q","type":"BIGINT","precision":null,"scale":null,"deType":2,"deExtractType":2,"extField":0,"checked":true,"columnIndex":null,"lastSyncTime":null,"dateFormat":null,"dateFormatType":null,"fieldShortName":"f_190480c43bdda8df","summary":"sum","sort":"none","dateStyle":"y_M_d","datePattern":"date_sub","chartType":"bar","compareCalc":{"type":"none","resultData":"percent","field":null,"custom":null},"logic":null,"filterType":null,"index":null,"formatterCfg":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"chartShowName":null,"filter":[],"customSort":null,"busiType":null}],"xAxisExt":[],"yAxis":[],"yAxisExt":[],"extStack":[],"drillFields":[],"viewFields":[],"extBubble":[],"extLabel":[],"extTooltip":[],"customFilter":{},"customAttr":{"basicStyle":{"alpha":100,"tableBorderColor":"#E6E7E4","tableScrollBarColor":"#00000024","tableColumnMode":"adapt","tableColumnWidth":100,"tablePageMode":"pull","tablePageSize":20,"gaugeStyle":"default","colorScheme":"default","colors":["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],"mapVendor":"amap","gradient":false,"lineWidth":2,"lineSymbol":"circle","lineSymbolSize":4,"lineSmooth":true,"barDefault":true,"barWidth":40,"barGap":0.4,"lineType":"solid","scatterSymbol":"circle","scatterSymbolSize":8,"radarShape":"polygon","mapStyle":"normal","areaBorderColor":"#303133","suspension":true,"areaBaseColor":"#FFFFFF","mapSymbolOpacity":0.7,"mapSymbolStrokeWidth":2,"mapSymbol":"circle","mapSymbolSize":20,"radius":100,"innerRadius":60},"misc":{"pieInnerRadius":0,"pieOuterRadius":80,"radarShape":"polygon","radarSize":80,"gaugeMinType":"fix","gaugeMinField":{"id":"","summary":""},"gaugeMin":0,"gaugeMaxType":"fix","gaugeMaxField":{"id":"","summary":""},"gaugeMax":100,"gaugeStartAngle":225,"gaugeEndAngle":-45,"nameFontSize":18,"valueFontSize":18,"nameValueSpace":10,"valueFontColor":"#5470c6","valueFontFamily":"Microsoft YaHei","valueFontIsBolder":false,"valueFontIsItalic":false,"valueLetterSpace":0,"valueFontShadow":false,"showName":true,"nameFontColor":"#000000","nameFontFamily":"Microsoft YaHei","nameFontIsBolder":false,"nameFontIsItalic":false,"nameLetterSpace":"0","nameFontShadow":false,"treemapWidth":80,"treemapHeight":80,"liquidMax":100,"liquidMaxType":"fix","liquidMaxField":{"id":"","summary":""},"liquidSize":80,"liquidShape":"circle","hPosition":"center","vPosition":"center","mapPitch":0,"mapLineType":"arc","mapLineWidth":1,"mapLineAnimateDuration":3,"mapLineGradient":false,"mapLineSourceColor":"#146C94","mapLineTargetColor":"#576CBC"},"label":{"show":false,"position":"top","color":"#000000","fontSize":10,"formatter":"","labelLine":{"show":true},"labelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"reserveDecimalCount":2,"labelShadow":false,"labelBgColor":"","labelShadowColor":"","quotaLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"showDimension":true,"showQuota":false,"showProportion":true,"seriesLabelFormatter":[]},"tooltip":{"show":true,"trigger":"item","confine":true,"fontSize":10,"color":"#000000","tooltipFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true},"backgroundColor":"#FFFFFF","seriesTooltipFormatter":[]},"tableTotal":{"row":{"showGrandTotals":true,"showSubTotals":true,"reverseLayout":false,"reverseSubLayout":false,"label":"总计","subLabel":"小计","subTotalsDimensions":[],"calcTotals":{"aggregation":"SUM"},"calcSubTotals":{"aggregation":"SUM"},"totalSort":"none","totalSortField":""},"col":{"showGrandTotals":true,"showSubTotals":true,"reverseLayout":false,"reverseSubLayout":false,"label":"总计","subLabel":"小计","subTotalsDimensions":[],"calcTotals":{"aggregation":"SUM"},"calcSubTotals":{"aggregation":"SUM"},"totalSort":"none","totalSortField":""}},"tableHeader":{"indexLabel":"序号","showIndex":false,"tableHeaderAlign":"left","tableHeaderBgColor":"#F5F6F7","tableHeaderFontColor":"#646A73","tableTitleFontSize":14,"tableTitleHeight":36},"tableCell":{"tableFontColor":"#1F2329","tableItemAlign":"right","tableItemBgColor":"#FFFFFF","tableItemFontSize":14,"tableItemHeight":36},"map":{"id":"","level":"world"}},"customStyle":{"text":{"show":false,"fontSize":"18","hPosition":"left","vPosition":"top","isItalic":false,"isBolder":true,"remarkShow":false,"remark":"","fontFamily":"Microsoft YaHei","letterSpace":"0","fontShadow":false,"color":"#000000","remarkBackgroundColor":"#ffffff"},"legend":{"show":true,"hPosition":"center","vPosition":"bottom","orient":"horizontal","icon":"circle","color":"#000000","fontSize":12},"xAxis":{"show":true,"position":"bottom","name":"","color":"#000000","fontSize":12,"axisLabel":{"show":true,"color":"#000000","fontSize":12,"rotate":0,"formatter":"{value}"},"axisLine":{"show":true,"lineStyle":{"color":"#cccccc","width":1,"style":"solid"}},"splitLine":{"show":false,"lineStyle":{"color":"#CCCCCC","width":1,"style":"solid"}},"axisValue":{"auto":true,"min":10,"max":100,"split":10,"splitCount":10},"axisLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true}},"yAxis":{"show":true,"position":"left","name":"","color":"#000000","fontSize":12,"axisLabel":{"show":true,"color":"#000000","fontSize":12,"rotate":0,"formatter":"{value}"},"axisLine":{"show":false,"lineStyle":{"color":"#cccccc","width":1,"style":"solid"}},"splitLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"style":"solid"}},"axisValue":{"auto":true,"min":10,"max":100,"split":10,"splitCount":10},"axisLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true}},"yAxisExt":{"show":true,"position":"right","name":"","color":"#000000","fontSize":12,"axisLabel":{"show":true,"color":"#000000","fontSize":12,"rotate":0,"formatter":"{value}"},"axisLine":{"show":false,"lineStyle":{"color":"#cccccc","width":1,"style":"solid"}},"splitLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"style":"solid"}},"axisValue":{"auto":true,"min":null,"max":null,"split":null,"splitCount":null},"axisLabelFormatter":{"type":"auto","unit":1,"suffix":"","decimalCount":2,"thousandSeparator":true}},"misc":{"showName":false,"color":"#000000","fontSize":12,"axisColor":"#999","splitNumber":5,"axisLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"type":"solid"}},"axisTick":{"show":false,"length":5,"lineStyle":{"color":"#000000","width":1,"type":"solid"}},"axisLabel":{"show":false,"rotate":0,"margin":8,"color":"#000000","fontSize":"12","formatter":"{value}"},"splitLine":{"show":true,"lineStyle":{"color":"#CCCCCC","width":1,"type":"solid"}},"splitArea":{"show":true}}},"senior":{"functionCfg":{"sliderShow":false,"sliderRange":[0,10],"sliderBg":"#FFFFFF","sliderFillBg":"#BCD6F1","sliderTextColor":"#999999","emptyDataStrategy":"breakLine","emptyDataFieldCtrl":[]},"assistLine":[],"threshold":{"gaugeThreshold":"","labelThreshold":[],"tableThreshold":[],"textLabelThreshold":[]},"scrollCfg":{"open":false,"row":1,"interval":2000,"step":50}},"chartExtRequest":{"user":"1","filter":[],"drill":[],"resultCount":1000,"resultMode":"all"}}'
+
+import {
+ DEFAULT_COLOR_CASE_DARK,
+ DEFAULT_COLOR_CASE_LIGHT,
+ DEFAULT_TITLE_STYLE_DARK,
+ DEFAULT_TITLE_STYLE_LIGHT,
+ FILTER_COMMON_STYLE_DARK,
+ FILTER_COMMON_STYLE_LIGHT,
+ SENIOR_STYLE_SETTING_DARK,
+ SENIOR_STYLE_SETTING_LIGHT,
+ TAB_COMMON_STYLE_DARK,
+ TAB_COMMON_STYLE_LIGHT
+} from '@/data-visualization/chart/components/editor/util/chart'
+import {
+ COMMON_COMPONENT_BACKGROUND_DARK,
+ COMMON_COMPONENT_BACKGROUND_LIGHT,
+ COMMON_COMPONENT_BACKGROUND_SCREEN_DARK
+} from '@/data-visualization/custom-component/component-list'
+
+export const PANEL_CHART_INFO_LIGHT = {
+ chartTitle: DEFAULT_TITLE_STYLE_LIGHT,
+ chartColor: DEFAULT_COLOR_CASE_LIGHT,
+ chartCommonStyle: COMMON_COMPONENT_BACKGROUND_LIGHT,
+ filterStyle: FILTER_COMMON_STYLE_LIGHT,
+ tabStyle: TAB_COMMON_STYLE_LIGHT,
+ seniorStyleSetting: SENIOR_STYLE_SETTING_LIGHT
+}
+
+export const PANEL_CHART_INFO_DARK = {
+ chartTitle: DEFAULT_TITLE_STYLE_DARK,
+ chartColor: DEFAULT_COLOR_CASE_DARK,
+ chartCommonStyle: COMMON_COMPONENT_BACKGROUND_DARK,
+ filterStyle: FILTER_COMMON_STYLE_DARK,
+ tabStyle: TAB_COMMON_STYLE_DARK,
+ seniorStyleSetting: SENIOR_STYLE_SETTING_DARK
+}
+
+export const PANEL_CHART_INFO_SCREEN_DARK = {
+ ...PANEL_CHART_INFO_DARK,
+ chartCommonStyle: COMMON_COMPONENT_BACKGROUND_SCREEN_DARK
+}
+
+export const MOBILE_SETTING_BASE = {
+ customSetting: false,
+ imageUrl: null,
+ backgroundType: 'image'
+}
+
+export const MOBILE_SETTING_LIGHT = {
+ ...MOBILE_SETTING_BASE,
+ color: '#000'
+}
+
+export const MOBILE_SETTING_DARK = {
+ ...MOBILE_SETTING_BASE,
+ color: '#fff'
+}
+
+export const DEFAULT_DASHBOARD_STYLE_BASE = {
+ gap: 'yes',
+ gapSize: 5,
+ showGrid: false,
+ matrixBase: 4, // 当前matrix的基数 (是pcMatrixCount的几倍)
+ resultMode: 'all', // 图表结果显示模式 all 图表 custom 仪表板自定义
+ resultCount: 1000 // 图表结果显示条数
+}
+
+export const DEFAULT_DASHBOARD_STYLE_LIGHT = {
+ ...DEFAULT_DASHBOARD_STYLE_BASE,
+ themeColor: 'light',
+ mobileSetting: MOBILE_SETTING_LIGHT
+}
+
+export const DEFAULT_DASHBOARD_STYLE_DARK = {
+ ...DEFAULT_DASHBOARD_STYLE_BASE,
+ themeColor: 'dark',
+ mobileSetting: MOBILE_SETTING_DARK
+}
+
+export const DEFAULT_CANVAS_STYLE_DATA_BASE = {
+ width: 1920,
+ height: 1080,
+ refreshBrowserEnable: false, // 开启浏览器刷新(默认关闭)
+ refreshBrowserUnit: 'minute', // 仪表板刷新时间带外 默认 分钟
+ refreshBrowserTime: 5, // 仪表板刷新时间 默认5分钟
+ refreshViewEnable: false, // 开启图表刷新(默认关闭)
+ refreshViewLoading: true, // 仪表板图表loading提示
+ refreshUnit: 'minute', // 仪表板刷新时间带外 默认 分钟
+ refreshTime: 5, // 仪表板刷新时间 默认5分钟
+ popupAvailable: true, // 弹窗区域是否可用 默认为true
+ popupButtonAvailable: true, // 弹框区域显示按钮是否可用 默认为true
+ suspensionButtonAvailable: false, // 悬浮按钮是否可用 默认false
+ screenAdaptor: 'widthFirst', // 屏幕适配方式 widthFirst=宽度优先 heightFirst=高度优先 full=铺满全屏 keepSize=不缩放
+ dashboardAdaptor: 'keepHeightAndWidth', //仪表板预览展示适配方式 keepHeightAndWidth=高度宽度独立缩放(默认模式),withWidth=跟随宽度
+ scale: 60,
+ scaleWidth: 60,
+ scaleHeight: 60,
+ backgroundColorSelect: true,
+ backgroundImageEnable: false,
+ backgroundType: 'backgroundColor', // 废弃
+ background: '',
+ openCommonStyle: true,
+ opacity: 1, // 废弃
+ fontSize: 14,
+ fontFamily: 'PingFang' //字体设置 默认PingFang
+}
+
+// 基础亮色主题
+export const DEFAULT_CANVAS_STYLE_DATA_LIGHT = {
+ ...DEFAULT_CANVAS_STYLE_DATA_BASE,
+ // 页面全局数据
+ themeId: '10001',
+ color: '#000000',
+ backgroundColor: '#f5f6f7',
+ dashboard: DEFAULT_DASHBOARD_STYLE_LIGHT,
+ component: PANEL_CHART_INFO_LIGHT
+}
+
+// 基础暗色主题
+export const DEFAULT_CANVAS_STYLE_DATA_DARK = {
+ ...DEFAULT_CANVAS_STYLE_DATA_BASE,
+ // 页面全局数据
+ themeId: '10002',
+ color: '#fff',
+ backgroundColor: '#020408',
+ dashboard: DEFAULT_DASHBOARD_STYLE_DARK,
+ component: PANEL_CHART_INFO_DARK
+}
+
+// 大屏基础暗色主题
+export const DEFAULT_CANVAS_STYLE_DATA_SCREEN_DARK = {
+ ...DEFAULT_CANVAS_STYLE_DATA_DARK,
+ component: PANEL_CHART_INFO_SCREEN_DARK
+}
+
+// 基础主题
+export const BASE_THEMES = {
+ light: DEFAULT_CANVAS_STYLE_DATA_LIGHT,
+ dark: DEFAULT_CANVAS_STYLE_DATA_DARK
+}
diff --git a/frontend/src/data-visualization/chart/components/js/extremumUitl.ts b/frontend/src/data-visualization/chart/components/js/extremumUitl.ts
new file mode 100644
index 0000000..6413f62
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/extremumUitl.ts
@@ -0,0 +1,392 @@
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { hexToRgba, parseJson } from '@/data-visualization/chart/components/js/util'
+import { isEmpty } from 'lodash-es'
+
+export const clearExtremum = chart => {
+ // 清除图表标注
+ const pointElement = document.getElementById(chartPointParentId(chart))
+ if (pointElement) {
+ pointElement.remove()
+ pointElement.parentNode?.removeChild(pointElement)
+ }
+}
+
+/**
+ * 判断给定的RGBA字符串表示的颜色是亮色还是暗色
+ * 通过计算RGB颜色值的加权平均值(灰度值),判断颜色的明暗
+ * 如果给定的字符串不包含有效的RGBA值,则原样返回该字符串
+ *
+ * @param rgbaString 一个RGBA颜色字符串,例如 "rgba(255, 255, 255, 1)"
+ * @param greyValue 灰度值默认128
+ * @returns 如果计算出的灰度值大于等于128,则返回true,表示亮色;否则返回false,表示暗色。
+ * 如果rgbaString不包含有效的RGBA值,则返回原字符串
+ */
+const isColorLight = (rgbaString: string, greyValue = 128) => {
+ const lastRGBA = getRgbaColorLastRgba(rgbaString)
+ if (!isEmpty(lastRGBA)) {
+ // 计算灰度值的公式
+ const grayLevel = lastRGBA.r * 0.299 + lastRGBA.g * 0.587 + lastRGBA.b * 0.114
+ return grayLevel >= greyValue
+ } else {
+ return false
+ }
+}
+
+/**
+ * 从给定的rgba颜色字符串中提取最后一个rgba值
+ * @param rgbaString 包含一个或多个rgba颜色值的字符串
+ * @returns 返回最后一个解析出的rgba对象,如果未找到rgba值,则返回null
+ */
+const getRgbaColorLastRgba = (rgbaString: string) => {
+ const rgbaPattern = /rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/g
+ let match: string[]
+ let lastRGBA = null
+ while ((match = rgbaPattern.exec(rgbaString)) !== null) {
+ const r = parseInt(match[1])
+ const g = parseInt(match[2])
+ const b = parseInt(match[3])
+ const a = parseFloat(match[4])
+ lastRGBA = { r, g, b, a }
+ }
+ return lastRGBA
+}
+
+function createExtremumDiv(id, value, formatterCfg, chart) {
+ // 空值不处理
+ if (!value && value != 0) {
+ return
+ }
+ // 装标注的div
+ const parentElement = document.getElementById(chartPointParentId(chart))
+ if (parentElement) {
+ // 标注div
+ const oldElement = document.getElementById(id)
+ if (oldElement) {
+ oldElement.remove()
+ oldElement.parentNode?.removeChild(oldElement)
+ }
+ const div = document.createElement('div')
+ div.id = id
+ div.className = 'child'
+ div.setAttribute(
+ 'style',
+ `width: auto;
+ height: auto;
+ border-radius: 2px;
+ position: relative;
+ padding: 4px 5px 4px 5px;
+ display:none;
+ transform: translateX(-50%);
+ opacity: 1;
+ transition: opacity 0.2s ease-in-out;
+ white-space:nowrap;`
+ )
+ div.textContent = valueFormatter(value, formatterCfg)
+ const span = document.createElement('span')
+ span.setAttribute(
+ 'style',
+ `width: 0px;
+ height: 0px;
+ border: 4px solid transparent;
+ border-top-color: red;
+ position: absolute;
+ left: calc(50% - 4px);
+ margin-top:4px;`
+ )
+ div.appendChild(span)
+ parentElement.appendChild(div)
+ }
+}
+/**
+ * 没有子类别字段的图表
+ * @param chart
+ */
+const noChildrenFieldChart = chart => {
+ return ['area', 'bar'].includes(chart.type)
+}
+
+/**
+ * 支持最值图表的折线图,面积图,柱状图,分组柱状图
+ * @param chart
+ */
+const supportExtremumChartType = chart => {
+ return ['line', 'area', 'bar', 'bar-group'].includes(chart.type)
+}
+
+const chartContainerId = chart => {
+ return chart.container + '_'
+}
+
+const chartPointParentId = chart => {
+ return chart.container + '_point_' + chart.id + '_'
+}
+
+function removeDivsWithPrefix(parentDivId, prefix) {
+ const parentDiv = document.getElementById(parentDivId)
+ if (!parentDiv) {
+ console.error('Parent div not found')
+ return
+ }
+ const childDivs = parentDiv.getElementsByTagName('div')
+ for (let i = childDivs.length - 1; i >= 0; i--) {
+ const div = childDivs[i]
+ if (div.id && div.id.startsWith(prefix)) {
+ div.parentNode.removeChild(div)
+ }
+ }
+}
+
+export const extremumEvt = (newChart, chart, _options, container) => {
+ chart.container = container
+ if (!supportExtremumChartType(chart)) {
+ clearExtremum(chart)
+ return
+ }
+ const { label: labelAttr } = parseJson(chart.customAttr)
+ const { yAxis } = parseJson(chart)
+ newChart.once('beforerender', ev => {
+ ev.view.on('beforepaint', () => {
+ newChart.chart.geometries[0]?.beforeMappingData.forEach(i => {
+ i.forEach(item => {
+ delete item._origin.EXTREME
+ })
+ const { minItem, maxItem } = findMinMax(i.filter(item => item._origin.value))
+ if (!minItem || !maxItem) {
+ return
+ }
+ let showExtremum = false
+ if (noChildrenFieldChart(chart) || yAxis.length > 1) {
+ const seriesLabelFormatter = labelAttr.seriesLabelFormatter.find(
+ d =>
+ (d.chartShowName ? d.chartShowName : d.name === minItem._origin.category) ||
+ (d.chartShowName ? d.chartShowName : d.name === maxItem._origin.category)
+ )
+ showExtremum = seriesLabelFormatter?.showExtremum
+ } else {
+ if (['bar-group'].includes(chart.type)) {
+ showExtremum = labelAttr.showExtremum
+ } else {
+ showExtremum = labelAttr.seriesLabelFormatter[0]?.showExtremum
+ }
+ }
+ if (showExtremum) {
+ minItem._origin.EXTREME = true
+ maxItem._origin.EXTREME = true
+ }
+ })
+ })
+ newChart.chart.geometries[0].on('afteranimate', () => {
+ createExtremumPoint(chart, ev)
+ })
+ })
+ newChart.on('legend-item:click', ev => {
+ const legendHideData = ev.view
+ .getController('legend')
+ .components[0].component.cfg.items.filter(l => l.unchecked)
+ if (legendHideData.length > 0) {
+ legendHideData.forEach(l => {
+ const seriesKey = chartContainerId(chart) + chartPointParentId(chart) + l.id
+ removeDivsWithPrefix(chartPointParentId(chart), seriesKey)
+ })
+ }
+ })
+}
+
+const findMinMax = (data): { minItem; maxItem } => {
+ return data.reduce(
+ ({ minItem, maxItem }, currentItem) => {
+ if (minItem === undefined || currentItem._origin.value < minItem._origin.value) {
+ minItem = currentItem
+ }
+ if (maxItem === undefined || currentItem._origin.value > maxItem._origin.value) {
+ maxItem = currentItem
+ }
+ delete minItem?._origin.EXTREME
+ delete maxItem?._origin.EXTREME
+ return { minItem, maxItem }
+ },
+ { minItem: undefined, maxItem: undefined }
+ )
+}
+export const createExtremumPoint = (chart, ev) => {
+ // 获取标注样式
+ const { label: labelAttr, basicStyle } = parseJson(chart.customAttr)
+ const pointSize = basicStyle.lineSymbolSize
+ const { yAxis } = parseJson(chart)
+ clearExtremum(chart)
+ // 创建标注父元素
+ const divParentElement = document.getElementById(chartPointParentId(chart))
+ if (!divParentElement) {
+ const divParent = document.createElement('div')
+ divParent.id = chartPointParentId(chart)
+ divParent.style.position = 'fixed'
+ divParent.style.zIndex = '1'
+ divParent.style.opacity = '0'
+ divParent.style.transition = 'opacity 0.2s ease-in-out'
+ // 将父标注加入到图表中
+ const containerElement = document.getElementById(chart.container)
+ containerElement.insertBefore(divParent, containerElement.firstChild)
+ // 处理最值闪烁的问题
+ let opacity = 0
+ const animate = () => {
+ // 增加不透明度
+ opacity += 0.19
+ if (opacity >= 1) {
+ cancelAnimationFrame(animationFrameId)
+ return
+ }
+ divParent.style.opacity = opacity + ''
+ animationFrameId = requestAnimationFrame(animate)
+ }
+ let animationFrameId = requestAnimationFrame(animate)
+ }
+ let geometriesDataArray = []
+ // 获取数据点
+ const intervalPoint = ev.view
+ .getGeometries()
+ .find((intervalItem: { type: string }) => intervalItem.type === 'interval')
+ if (intervalPoint) {
+ geometriesDataArray = intervalPoint.dataArray
+ }
+ const pointPoint = ev.view
+ .getGeometries()
+ .find((pointItem: { type: string }) => pointItem.type === 'point')
+ if (pointPoint) {
+ geometriesDataArray = pointPoint.dataArray
+ }
+ performChunk(geometriesDataArray, pointObjList => {
+ if (pointObjList && pointObjList.length > 0) {
+ const pointObj = pointObjList[0]
+ const [minItem, maxItem] = pointObjList.filter(i => i._origin.EXTREME)
+ let attr
+ let showExtremum = false
+ if (noChildrenFieldChart(chart) || yAxis.length > 1) {
+ const seriesLabelFormatter = labelAttr.seriesLabelFormatter.find(d =>
+ d.chartShowName
+ ? d.chartShowName === pointObj._origin.category
+ : d.name === pointObj._origin.category
+ )
+ showExtremum = seriesLabelFormatter?.showExtremum
+ attr = seriesLabelFormatter
+ } else {
+ if (['bar-group'].includes(chart.type)) {
+ showExtremum = labelAttr.showExtremum
+ } else {
+ showExtremum = labelAttr.seriesLabelFormatter[0]?.showExtremum
+ attr = labelAttr.seriesLabelFormatter[0]
+ }
+ }
+ const fontSize = attr ? attr.fontSize : labelAttr.fontSize
+ if (!minItem) {
+ return
+ }
+ const maxKey =
+ chartContainerId(chart) +
+ chartPointParentId(chart) +
+ pointObj._origin.category +
+ '_' +
+ (maxItem ? maxItem._origin.value : minItem._origin.value)
+ const minKey =
+ chartContainerId(chart) +
+ chartPointParentId(chart) +
+ pointObj._origin.category +
+ '_' +
+ minItem._origin.value
+ // 最值标注
+ if (showExtremum && labelAttr.show) {
+ if (maxItem) {
+ createExtremumDiv(
+ maxKey,
+ maxItem._origin.value,
+ attr ? attr.formatterCfg : labelAttr.labelFormatter,
+ chart
+ )
+ }
+ createExtremumDiv(
+ minKey,
+ minItem._origin.value,
+ attr ? attr.formatterCfg : labelAttr.labelFormatter,
+ chart
+ )
+ pointObjList.forEach(point => {
+ const pointElement = document.getElementById(
+ chartContainerId(chart) +
+ chartPointParentId(chart) +
+ point._origin.category +
+ '_' +
+ point._origin.value
+ )
+ if (pointElement && point._origin.EXTREME) {
+ pointElement.style.position = 'absolute'
+ const top =
+ (point.y[1] ? point.y[1] : point.y) - (fontSize + (pointSize ? pointSize : 0) + 12)
+ pointElement.style.top = top + 'px'
+ pointElement.style.left = point.x + 'px'
+ pointElement.style.zIndex = '10'
+ pointElement.style.fontSize = fontSize + 'px'
+ pointElement.style.lineHeight = fontSize + 'px'
+ // 渐变颜色时需要获取最后一个rgba的值作为背景
+ const color = point.color.startsWith('#')
+ ? hexToRgba(point.color, basicStyle.alpha / 100)
+ : getRgbaColorLastRgba(point.color)
+ const { r, b, g, a } = color
+ pointElement.style.backgroundColor = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'
+ pointElement.style.color = isColorLight(point.color) ? '#000' : '#fff'
+ pointElement.children[0]['style'].borderTopColor =
+ 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'
+ pointElement.style.display = 'table'
+ // 显示箭头
+ const childNode = pointElement.childNodes[1]
+ // 最值在数据点下方显示
+ const translateYValue = Math.ceil(point.y + Math.abs(Math.floor(top)) + 6)
+ // 最值dom高度超过50%时,最值dom向下
+ if (top < 0 && (Math.abs(top) / point.y) * 100 >= 50) {
+ pointElement.style.transform = `translateX(-50%) translateY(${translateYValue}px)`
+ childNode.style.marginTop = '-12px'
+ childNode.style.transform = 'rotate(180deg)'
+ } else {
+ childNode.style.display = 'block'
+ }
+ }
+ })
+ } else {
+ removeDivElement(maxKey)
+ removeDivElement(minKey)
+ }
+ }
+ })
+}
+
+function removeDivElement(key) {
+ const element = document.getElementById(key)
+ if (element) {
+ element.remove()
+ element.parentNode?.removeChild(element)
+ }
+}
+
+/**
+ * 用于分批处理数据,利用requestIdleCallback在浏览器空闲期间执行任务,避免阻塞主线程
+ * @param dataList
+ * @param taskHandler
+ */
+function performChunk(dataList, taskHandler) {
+ if (typeof dataList === 'number') {
+ dataList = { length: dataList }
+ }
+ if (dataList.length === 0) return
+ let i = 0
+ function _run() {
+ if (i >= dataList.length) return
+ // 请求浏览器空闲期间执行的回调函数
+ requestIdleCallback(idle => {
+ // 在当前空闲期间内尽可能多地处理任务,直到时间耗尽或所有任务处理完毕
+ while (idle.timeRemaining() > 0 && i < dataList.length) {
+ taskHandler(dataList[i], i)
+ i++
+ }
+ _run()
+ })
+ }
+ _run()
+}
diff --git a/frontend/src/data-visualization/chart/components/js/formatter.ts b/frontend/src/data-visualization/chart/components/js/formatter.ts
new file mode 100644
index 0000000..a297fbb
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/formatter.ts
@@ -0,0 +1,107 @@
+import { Datum } from '@antv/g2plot'
+
+export const formatterItem = {
+ type: 'auto', // auto,value,percent
+ unit: 1, // 换算单位
+ suffix: '', // 单位后缀
+ decimalCount: 2, // 小数位数
+ thousandSeparator: true // 千分符
+}
+
+// 单位list
+export const unitType = [
+ { name: 'unit_none', value: 1 },
+ { name: 'unit_thousand', value: 1000 },
+ { name: 'unit_ten_thousand', value: 10000 },
+ { name: 'unit_million', value: 1000000 },
+ { name: 'unit_hundred_million', value: 100000000 }
+]
+
+// 格式化方式
+export const formatterType = [
+ { name: 'value_formatter_auto', value: 'auto' },
+ { name: 'value_formatter_value', value: 'value' },
+ { name: 'value_formatter_percent', value: 'percent' }
+]
+
+export function valueFormatter(value, formatter) {
+ if (value === null || value === undefined) {
+ return null
+ }
+ // 1.unit 2.decimal 3.thousand separator and suffix
+ let result
+ if (formatter.type === 'auto') {
+ result = transSeparatorAndSuffix(transUnit(value, formatter), formatter)
+ } else if (formatter.type === 'value') {
+ result = transSeparatorAndSuffix(
+ transDecimal(transUnit(value, formatter), formatter),
+ formatter
+ )
+ } else if (formatter.type === 'percent') {
+ value = value * 100
+ result = transSeparatorAndSuffix(transDecimal(value, formatter), formatter)
+ } else {
+ result = value
+ }
+ return result
+}
+
+function transUnit(value, formatter) {
+ return value / formatter.unit
+}
+
+function transDecimal(value, formatter) {
+ const resultV = value.toFixed(formatter.decimalCount)
+ if (Object.is(parseFloat(resultV), -0)) {
+ return resultV.slice(1)
+ }
+ return resultV
+}
+
+function transSeparatorAndSuffix(value, formatter) {
+ let str = value + ''
+ if (str.match(/^(\d)(\.\d)?e-(\d)/)) {
+ str = value.toFixed(18).replace(/\.?0+$/, '')
+ }
+ if (formatter.thousandSeparator) {
+ const thousandsReg = /(\d)(?=(\d{3})+$)/g
+ const numArr = str.split('.')
+ numArr[0] = numArr[0].replace(thousandsReg, '$1,')
+ str = numArr.join('.')
+ }
+ if (formatter.type === 'percent') {
+ str += '%'
+ //百分比没有后缀,直接返回
+ return str
+ } else {
+ if (formatter.unit === 1000) {
+ str += '千'
+ } else if (formatter.unit === 10000) {
+ str += '万'
+ } else if (formatter.unit === 1000000) {
+ str += '百万'
+ } else if (formatter.unit === 100000000) {
+ str += '亿'
+ }
+ }
+ return str + formatter.suffix.replace(/(^\s*)|(\s*$)/g, '')
+}
+
+export function singleDimensionTooltipFormatter(param: Datum, chart: Chart, prop = 'category') {
+ let res
+ const yAxis = chart.yAxis
+ const obj = { name: param[prop], value: param.value }
+ for (let i = 0; i < yAxis.length; i++) {
+ const f = yAxis[i]
+ if (f.name === param[prop]) {
+ if (f.formatterCfg) {
+ res = valueFormatter(param.value, f.formatterCfg)
+ } else {
+ res = valueFormatter(param.value, formatterItem)
+ }
+ break
+ }
+ }
+ obj.value = res ?? ''
+ return obj
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bar.ts
new file mode 100644
index 0000000..3d25530
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bar.ts
@@ -0,0 +1,691 @@
+import type { Column, ColumnOptions } from '@antv/g2plot/esm/plots/column'
+import { cloneDeep, each, groupBy, isEmpty } from 'lodash-es'
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import {
+ flow,
+ hexColorToRGBA,
+ hexToRgba,
+ parseJson,
+ setUpGroupSeriesColor,
+ setUpStackSeriesColor
+} from '@/data-visualization/chart/components/js/util'
+import type { Datum } from '@antv/g2plot'
+import { formatterItem, valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import {
+ BAR_AXIS_TYPE,
+ BAR_EDITOR_PROPERTY,
+ BAR_EDITOR_PROPERTY_INNER
+} from '@/data-visualization/chart/components/js/panel/charts/bar/common'
+import {
+ configPlotTooltipEvent,
+ getLabel,
+ getPadding,
+ getTooltipContainer,
+ setGradientColor,
+ TOOLTIP_TPL
+} from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { DEFAULT_BASIC_STYLE, DEFAULT_LABEL } from '@/data-visualization/chart/components/editor/util/chart'
+import { clearExtremum, extremumEvt } from '@/data-visualization/chart/components/js/extremumUitl'
+import { Group } from '@antv/g-canvas'
+
+const { t } = useI18n()
+const DEFAULT_DATA: any[] = []
+/**
+ * 柱状图
+ */
+export class Bar extends G2PlotChartView {
+ properties = BAR_EDITOR_PROPERTY
+ propertyInner = {
+ ...BAR_EDITOR_PROPERTY_INNER,
+ 'basic-style-selector': [...BAR_EDITOR_PROPERTY_INNER['basic-style-selector'], 'seriesColor'],
+ 'label-selector': ['vPosition', 'seriesLabelFormatter', 'showExtremum'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
+ 'y-axis-selector': [...BAR_EDITOR_PROPERTY_INNER['y-axis-selector'], 'axisLabelFormatter']
+ }
+ protected baseOptions: ColumnOptions = {
+ xField: 'field',
+ yField: 'value',
+ seriesField: 'category',
+ isGroup: true,
+ data: []
+ }
+
+ axis: AxisType[] = [...BAR_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: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart?.data?.data?.length) {
+ chart.container = container
+ clearExtremum(chart)
+ return
+ }
+ const data = cloneDeep(drawOptions.chart.data?.data)
+ const initOptions: ColumnOptions = {
+ ...this.baseOptions,
+ appendPadding: getPadding(chart),
+ data
+ }
+ const options: ColumnOptions = this.setupOptions(chart, initOptions)
+ let newChart = null
+ const { Column: ColumnClass } = await import('@antv/g2plot/esm/plots/column')
+ newChart = new ColumnClass(container, options)
+ newChart.on('interval:click', action)
+ extremumEvt(newChart, chart, options, container)
+ configPlotTooltipEvent(chart, newChart)
+ return newChart
+ }
+
+ protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const tmpOptions = super.configLabel(chart, options)
+ if (!tmpOptions.label) {
+ return {
+ ...tmpOptions,
+ label: false
+ }
+ }
+ const { label: labelAttr } = parseJson(chart.customAttr)
+ const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {})
+ // 默认是灰色
+ tmpOptions.label.style.fill = DEFAULT_LABEL.color
+ const label = {
+ fields: [],
+ ...tmpOptions.label,
+ formatter: (data: Datum, _point) => {
+ if (data.EXTREME) {
+ return ''
+ }
+ 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 Group({})
+ group.addShape({
+ type: 'text',
+ attrs: {
+ x: 0,
+ y: 0,
+ text: value,
+ textAlign: 'start',
+ textBaseline: 'top',
+ fontSize: labelCfg.fontSize,
+ fontFamily: chart.fontFamily,
+ fill: labelCfg.color
+ }
+ })
+ return group
+ },
+ position: data => {
+ if (data.value < 0) {
+ if (tmpOptions.label?.position === 'top') {
+ return 'bottom'
+ }
+ if (tmpOptions.label?.position === 'bottom') {
+ return 'top'
+ }
+ }
+ return tmpOptions.label?.position
+ }
+ }
+ return {
+ ...tmpOptions,
+ label
+ }
+ }
+
+ protected configTooltip(chart: Chart, options: ColumnOptions): ColumnOptions {
+ return super.configMultiSeriesTooltip(chart, options)
+ }
+
+ protected configBasicStyle(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const basicStyle = parseJson(chart.customAttr).basicStyle
+ if (basicStyle.gradient) {
+ let color = basicStyle.colors
+ color = color.map(ele => {
+ const tmp = hexColorToRGBA(ele, basicStyle.alpha)
+ return setGradientColor(tmp, true, 270)
+ })
+ options = {
+ ...options,
+ color
+ }
+ }
+ if (basicStyle.radiusColumnBar === 'roundAngle') {
+ const columnStyle = {
+ radius: [
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius
+ ]
+ }
+ options = {
+ ...options,
+ columnStyle
+ }
+ }
+ let columnWidthRatio
+ const _v = basicStyle.columnWidthRatio ?? DEFAULT_BASIC_STYLE.columnWidthRatio
+ if (_v >= 1 && _v <= 100) {
+ columnWidthRatio = _v / 100.0
+ } else if (_v < 1) {
+ columnWidthRatio = 1 / 100.0
+ } else if (_v > 100) {
+ columnWidthRatio = 1
+ }
+ if (columnWidthRatio) {
+ options.columnWidthRatio = columnWidthRatio
+ }
+
+ return options
+ }
+
+ protected configYAxis(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const tmpOptions = super.configYAxis(chart, options)
+ if (!tmpOptions.yAxis) {
+ return tmpOptions
+ }
+ const yAxis = parseJson(chart.customStyle).yAxis
+ const axisValue = yAxis.axisValue
+ if (tmpOptions.yAxis.label) {
+ tmpOptions.yAxis.label.formatter = value => {
+ return valueFormatter(value, yAxis.axisLabelFormatter)
+ }
+ }
+ if (!axisValue?.auto) {
+ const axis = {
+ yAxis: {
+ ...tmpOptions.yAxis,
+ min: axisValue.min,
+ max: axisValue.max,
+ minLimit: axisValue.min,
+ maxLimit: axisValue.max,
+ tickCount: axisValue.splitCount
+ }
+ }
+ return { ...tmpOptions, ...axis }
+ }
+ return tmpOptions
+ }
+
+ protected setupOptions(chart: Chart, options: ColumnOptions): ColumnOptions {
+ return flow(
+ this.addConditionsStyleColorToData,
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configColor,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyse,
+ this.configBarConditions
+ )(chart, options, {}, this)
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.senior.functionCfg.emptyDataStrategy = 'ignoreData'
+ return chart
+ }
+
+ constructor(name = 'bar', defaultData = DEFAULT_DATA) {
+ super(name, defaultData)
+ }
+}
+
+/**
+ * 堆叠柱状图
+ */
+export class StackBar extends Bar {
+ properties = BAR_EDITOR_PROPERTY.filter(ele => ele !== 'threshold')
+ propertyInner = {
+ ...this['propertyInner'],
+ 'label-selector': [
+ ...BAR_EDITOR_PROPERTY_INNER['label-selector'],
+ 'vPosition',
+ 'showTotal',
+ 'totalColor',
+ 'totalFontSize',
+ 'totalFormatter',
+ 'showStackQuota'
+ ],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'tooltipFormatter', 'show']
+ }
+ protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions {
+ let label = getLabel(chart)
+ if (!label) {
+ return options
+ }
+ options = { ...options, label }
+ const { label: labelAttr } = parseJson(chart.customAttr)
+ if (labelAttr.showStackQuota || labelAttr.showStackQuota === undefined) {
+ label.style.fill = labelAttr.color
+ label = {
+ ...label,
+ formatter: function (param: Datum) {
+ return valueFormatter(param.value, labelAttr.labelFormatter)
+ }
+ }
+ } else {
+ label = false
+ }
+ if (labelAttr.showTotal) {
+ const formatterCfg = labelAttr.labelFormatter ?? formatterItem
+ each(groupBy(options.data, 'field'), (values, key) => {
+ const total = values.reduce((a, b) => a + b.value, 0)
+ const value = valueFormatter(total, formatterCfg)
+ if (!options.annotations) {
+ options = {
+ ...options,
+ annotations: []
+ }
+ }
+ options.annotations.push({
+ type: 'text',
+ position: [key, total],
+ content: `${value}`,
+ style: {
+ textAlign: 'center',
+ fontSize: labelAttr.fontSize,
+ fill: labelAttr.color
+ },
+ offsetY: -(parseInt(labelAttr.fontSize as unknown as string) / 2)
+ })
+ })
+ }
+ return {
+ ...options,
+ label
+ }
+ }
+
+ protected configTooltip(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const tooltipAttr = parseJson(chart.customAttr).tooltip
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: false
+ }
+ }
+ const tooltip = {
+ formatter: (param: Datum) => {
+ const name = isEmpty(param.category) ? param.field : param.category
+ const obj = { name, value: param.value }
+ const res = valueFormatter(param.value, tooltipAttr.tooltipFormatter)
+ obj.value = res ?? ''
+ return obj
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+
+ protected configColor(chart: Chart, options: ColumnOptions): ColumnOptions {
+ return this.configStackColor(chart, options)
+ }
+
+ protected configData(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const { xAxis, extStack, yAxis } = chart
+ const mainSort = xAxis.some(axis => axis.sort !== 'none')
+ const subSort = extStack.some(axis => axis.sort !== 'none')
+ if (mainSort || subSort) {
+ return options
+ }
+ const quotaSort = yAxis?.[0].sort !== 'none'
+ if (!quotaSort || !extStack.length || !yAxis.length) {
+ return options
+ }
+ const { data } = options
+ const mainAxisValueMap = data.reduce((p, n) => {
+ p[n.field] = p[n.field] ? p[n.field] + n.value : n.value || 0
+ return p
+ }, {})
+ const sort = yAxis[0].sort
+ data.sort((p, n) => {
+ if (sort === 'asc') {
+ return mainAxisValueMap[p.field] - mainAxisValueMap[n.field]
+ } else {
+ return mainAxisValueMap[n.field] - mainAxisValueMap[p.field]
+ }
+ })
+ return options
+ }
+
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ return setUpStackSeriesColor(chart, data)
+ }
+
+ protected setupOptions(chart: Chart, options: ColumnOptions): ColumnOptions {
+ return flow(
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configData,
+ this.configColor,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyse
+ )(chart, options, {}, this)
+ }
+
+ constructor(name = 'bar-stack') {
+ super(name)
+ this.baseOptions = {
+ ...this.baseOptions,
+ isStack: true,
+ isGroup: false,
+ meta: {
+ category: {
+ type: 'cat'
+ }
+ }
+ }
+ this.axis = [...this.axis, 'extStack']
+ }
+}
+
+/**
+ * 分组柱状图
+ */
+export class GroupBar extends StackBar {
+ properties = BAR_EDITOR_PROPERTY
+ propertyInner = {
+ ...this['propertyInner'],
+ 'label-selector': [...BAR_EDITOR_PROPERTY_INNER['label-selector'], 'vPosition', 'showExtremum']
+ }
+ axisConfig = {
+ ...this['axisConfig'],
+ yAxis: {
+ name: `${t('chart.drag_block_value_axis')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+
+ protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const tmpLabel = getLabel(chart)
+ if (!tmpLabel) {
+ return options
+ }
+ const baseOptions = { ...options, label: tmpLabel }
+ const { label: labelAttr } = parseJson(chart.customAttr)
+ baseOptions.label.style.fill = labelAttr.color
+ const label = {
+ ...baseOptions.label,
+ formatter: function (param: Datum, _point) {
+ if (param.EXTREME) {
+ return ''
+ }
+ const value = valueFormatter(param.value, labelAttr.labelFormatter)
+ return labelAttr.childrenShow ? value : null
+ }
+ }
+ return {
+ ...baseOptions,
+ label
+ }
+ }
+
+ protected configColor(chart: Chart, options: ColumnOptions): ColumnOptions {
+ return this.configGroupColor(chart, options)
+ }
+
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ return setUpGroupSeriesColor(chart, data)
+ }
+
+ protected setupOptions(chart: Chart, options: ColumnOptions): ColumnOptions {
+ return flow(
+ this.addConditionsStyleColorToData,
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configColor,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyse,
+ this.configBarConditions
+ )(chart, options, {}, this)
+ }
+
+ constructor(name = 'bar-group') {
+ super(name)
+ this.baseOptions = {
+ ...this.baseOptions,
+ isGroup: true,
+ isStack: false,
+ meta: {
+ category: {
+ type: 'cat'
+ }
+ }
+ }
+ this.axis = [...BAR_AXIS_TYPE, 'xAxisExt']
+ }
+}
+
+/**
+ * 分组堆叠柱状图
+ */
+export class GroupStackBar extends StackBar {
+ propertyInner = {
+ ...this['propertyInner'],
+ 'label-selector': [...BAR_EDITOR_PROPERTY_INNER['label-selector'], 'vPosition']
+ }
+ protected configTheme(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const baseOptions = super.configTheme(chart, options)
+ const baseTheme = baseOptions.theme as object
+ const theme = {
+ ...baseTheme,
+ innerLabels: {
+ offset: 0
+ }
+ }
+ return {
+ ...options,
+ theme
+ }
+ }
+
+ protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const tmpLabel = getLabel(chart)
+ if (!tmpLabel) {
+ return options
+ }
+ const baseOptions = { ...options, label: tmpLabel }
+ const { label: labelAttr } = parseJson(chart.customAttr)
+ baseOptions.label.style.fill = labelAttr.color
+ const label = {
+ ...baseOptions.label,
+ formatter: function (param: Datum) {
+ return valueFormatter(param.value, labelAttr.labelFormatter)
+ }
+ }
+ return {
+ ...baseOptions,
+ label
+ }
+ }
+
+ protected configTooltip(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const tooltipAttr = parseJson(chart.customAttr).tooltip
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: false
+ }
+ }
+ const tooltip = {
+ fields: [],
+ formatter: (param: Datum) => {
+ const obj = { name: `${param.category} - ${param.group}`, value: param.value }
+ obj.value = valueFormatter(param.value, tooltipAttr.tooltipFormatter)
+ return obj
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+
+ protected setupOptions(chart: Chart, options: ColumnOptions): ColumnOptions {
+ return flow(
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configColor,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyse
+ )(chart, options, {}, this)
+ }
+
+ constructor(name = 'bar-group-stack') {
+ super(name)
+ this.baseOptions = {
+ ...this.baseOptions,
+ isGroup: true,
+ groupField: 'group'
+ }
+ this.axis = [...this.axis, 'xAxisExt', 'extStack']
+ }
+}
+
+/**
+ * 百分比堆叠柱状图
+ */
+export class PercentageStackBar extends GroupStackBar {
+ propertyInner = {
+ ...this['propertyInner'],
+ 'label-selector': ['color', 'fontSize', 'vPosition', 'reserveDecimalCount'],
+ 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'show']
+ }
+ protected configLabel(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const baseOptions = super.configLabel(chart, options)
+ if (!baseOptions.label) {
+ return baseOptions
+ }
+ const { customAttr } = chart
+ const l = parseJson(customAttr).label
+ const label = {
+ ...baseOptions.label,
+ formatter: function (param: Datum) {
+ if (!param.value) {
+ return '0%'
+ }
+ return (Math.round(param.value * 10000) / 100).toFixed(l.reserveDecimalCount) + '%'
+ }
+ }
+ return {
+ ...baseOptions,
+ label
+ }
+ }
+
+ protected configTooltip(chart: Chart, options: ColumnOptions): ColumnOptions {
+ const tooltipAttr = parseJson(chart.customAttr).tooltip
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: {
+ showContent: false
+ }
+ }
+ }
+ const { customAttr } = chart
+ const l = parseJson(customAttr).label
+ const tooltip = {
+ formatter: (param: Datum) => {
+ const obj = { name: param.category, value: param.value }
+ obj.value = (Math.round(param.value * 10000) / 100).toFixed(l.reserveDecimalCount) + '%'
+ return obj
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+ protected setupOptions(chart: Chart, options: ColumnOptions): ColumnOptions {
+ return flow(
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configColor,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyse
+ )(chart, options, {}, this)
+ }
+ constructor() {
+ super('percentage-bar-stack')
+ this.baseOptions = {
+ ...this.baseOptions,
+ isStack: true,
+ isPercent: true,
+ isGroup: false,
+ groupField: undefined,
+ meta: {
+ category: {
+ type: 'cat'
+ }
+ }
+ }
+ this.axis = [...BAR_AXIS_TYPE, 'extStack']
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bidirectional-bar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bidirectional-bar.ts
new file mode 100644
index 0000000..41456d0
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/bidirectional-bar.ts
@@ -0,0 +1,582 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import { cloneDeep, defaultTo, isEmpty, map } from 'lodash-es'
+import {
+ configAxisLabelLengthLimit,
+ configPlotTooltipEvent,
+ getPadding,
+ getTooltipContainer,
+ getTooltipItemConditionColor,
+ getYAxis,
+ getYAxisExt,
+ setGradientColor,
+ TOOLTIP_TPL,
+ addConditionsStyleColorToData
+} from '@/data-visualization/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 '@/data-visualization/chart/components/js/util'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { valueFormatter } from '@/data-visualization/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): Promise {
+ 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
+ }
+ if (basicStyle.radiusColumnBar === 'roundAngle') {
+ const barStyle = {
+ radius: [
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius
+ ]
+ }
+ options = {
+ ...options,
+ 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
+ }
+ return tmpOptions
+ }
+
+ protected configTooltip(chart: Chart, options: BidirectionalBarOptions): BidirectionalBarOptions {
+ const customAttr: DeepPartial = 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
+ 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
+ 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', [])
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/common.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/common.ts
new file mode 100644
index 0000000..6421d18
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/common.ts
@@ -0,0 +1,84 @@
+export const BAR_EDITOR_PROPERTY: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'x-axis-selector',
+ 'y-axis-selector',
+ 'title-selector',
+ 'legend-selector',
+ 'function-cfg',
+ 'assist-line',
+ 'jump-set',
+ 'linkage',
+ 'threshold'
+]
+export const BAR_RANGE_EDITOR_PROPERTY: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'x-axis-selector',
+ 'y-axis-selector',
+ 'title-selector',
+ 'legend-selector',
+ 'function-cfg',
+ 'jump-set',
+ 'linkage',
+ 'threshold'
+]
+
+export const BAR_EDITOR_PROPERTY_INNER: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha', 'gradient', 'radiusColumnBar', 'columnWidthRatio'],
+ 'label-selector': ['fontSize', 'color', 'labelFormatter'],
+ 'tooltip-selector': ['fontSize', 'color', 'tooltipFormatter', 'show'],
+ 'x-axis-selector': [
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisLine',
+ 'splitLine',
+ 'axisForm',
+ 'axisLabel',
+ 'position'
+ ],
+ 'y-axis-selector': [
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisValue',
+ 'axisLine',
+ 'splitLine',
+ 'axisForm',
+ 'axisLabel',
+ 'position'
+ ],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'legend-selector': ['icon', 'orient', 'fontSize', 'color', 'hPosition', 'vPosition'],
+ 'function-cfg': ['slider', 'emptyDataStrategy'],
+ threshold: ['lineThreshold']
+}
+
+export const BAR_AXIS_TYPE: AxisType[] = [
+ 'xAxis',
+ 'yAxis',
+ 'filter',
+ 'drill',
+ 'extLabel',
+ 'extTooltip'
+]
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/horizontal-bar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/horizontal-bar.ts
new file mode 100644
index 0000000..12e8ec9
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/horizontal-bar.ts
@@ -0,0 +1,501 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { Bar, BarOptions } from '@antv/g2plot/esm/plots/bar'
+import {
+ configAxisLabelLengthLimit,
+ configPlotTooltipEvent,
+ getPadding,
+ getTooltipContainer,
+ setGradientColor,
+ TOOLTIP_TPL
+} from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { cloneDeep } from 'lodash-es'
+import {
+ flow,
+ hexColorToRGBA,
+ parseJson,
+ setUpStackSeriesColor
+} from '@/data-visualization/chart/components/js/util'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import {
+ BAR_AXIS_TYPE,
+ BAR_EDITOR_PROPERTY,
+ BAR_EDITOR_PROPERTY_INNER
+} from '@/data-visualization/chart/components/js/panel/charts/bar/common'
+import type { Datum } from '@antv/g2plot/esm/types/common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { DEFAULT_BASIC_STYLE, DEFAULT_LABEL } from '@/data-visualization/chart/components/editor/util/chart'
+import { Group } from '@antv/g-canvas'
+
+const { t } = useI18n()
+const DEFAULT_DATA = []
+
+/**
+ * 条形图
+ */
+export class HorizontalBar extends G2PlotChartView {
+ 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'
+ }
+ }
+ properties = BAR_EDITOR_PROPERTY
+ propertyInner = {
+ ...BAR_EDITOR_PROPERTY_INNER,
+ 'basic-style-selector': [...BAR_EDITOR_PROPERTY_INNER['basic-style-selector'], 'seriesColor'],
+ 'label-selector': ['hPosition', 'seriesLabelFormatter'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
+ 'x-axis-selector': [
+ ...BAR_EDITOR_PROPERTY_INNER['x-axis-selector'],
+ 'axisLabelFormatter',
+ 'axisValue'
+ ],
+ 'y-axis-selector': [
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisLine',
+ 'splitLine',
+ 'axisForm',
+ 'axisLabel',
+ 'position',
+ 'showLengthLimit'
+ ]
+ }
+ axis: AxisType[] = [...BAR_AXIS_TYPE]
+ protected baseOptions: BarOptions = {
+ data: [],
+ xField: 'value',
+ yField: 'field',
+ seriesField: 'category',
+ isGroup: true
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data?.length) {
+ return
+ }
+ // data
+ const data = cloneDeep(chart.data.data)
+
+ // options
+ const initOptions: BarOptions = {
+ ...this.baseOptions,
+ appendPadding: getPadding(chart),
+ data
+ }
+
+ const options = this.setupOptions(chart, initOptions)
+
+ const { Bar } = await import('@antv/g2plot/esm/plots/bar')
+ // 开始渲染
+ const newChart = new Bar(container, options)
+
+ newChart.on('interval:click', action)
+ configPlotTooltipEvent(chart, newChart)
+ configAxisLabelLengthLimit(chart, newChart)
+ return newChart
+ }
+
+ protected configXAxis(chart: Chart, options: BarOptions): BarOptions {
+ const tmpOptions = super.configXAxis(chart, options)
+ if (!tmpOptions.xAxis) {
+ return tmpOptions
+ }
+ const xAxis = parseJson(chart.customStyle).xAxis
+ const axisValue = xAxis.axisValue
+ if (tmpOptions.xAxis.label) {
+ tmpOptions.xAxis.label.formatter = value => {
+ return valueFormatter(value, xAxis.axisLabelFormatter)
+ }
+ }
+ if (tmpOptions.xAxis.position === 'top') {
+ tmpOptions.xAxis.position = 'left'
+ }
+ if (tmpOptions.xAxis.position === 'bottom') {
+ tmpOptions.xAxis.position = 'right'
+ }
+ if (!axisValue?.auto) {
+ const axis = {
+ xAxis: {
+ ...tmpOptions.xAxis,
+ min: axisValue.min,
+ max: axisValue.max,
+ minLimit: axisValue.min,
+ maxLimit: axisValue.max,
+ tickCount: axisValue.splitCount
+ }
+ }
+ return { ...tmpOptions, ...axis }
+ }
+ return tmpOptions
+ }
+
+ protected configTooltip(chart: Chart, options: BarOptions): BarOptions {
+ return super.configMultiSeriesTooltip(chart, options)
+ }
+
+ protected configBasicStyle(chart: Chart, options: BarOptions): BarOptions {
+ const basicStyle = parseJson(chart.customAttr).basicStyle
+ if (basicStyle.gradient) {
+ let color = basicStyle.colors
+ color = color.map(ele => {
+ const tmp = hexColorToRGBA(ele, basicStyle.alpha)
+ return setGradientColor(tmp, true)
+ })
+ options = {
+ ...options,
+ color
+ }
+ }
+ if (basicStyle.radiusColumnBar === 'roundAngle') {
+ const barStyle = {
+ radius: [
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius
+ ]
+ }
+ options = {
+ ...options,
+ barStyle
+ }
+ }
+
+ let barWidthRatio
+ const _v = basicStyle.columnWidthRatio ?? DEFAULT_BASIC_STYLE.columnWidthRatio
+ if (_v >= 1 && _v <= 100) {
+ barWidthRatio = _v / 100.0
+ } else if (_v < 1) {
+ barWidthRatio = 1 / 100.0
+ } else if (_v > 100) {
+ barWidthRatio = 1
+ }
+ if (barWidthRatio) {
+ options.barWidthRatio = barWidthRatio
+ }
+
+ return options
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const { customAttr, senior } = chart
+ const { label } = customAttr
+ if (!['left', 'middle', 'right'].includes(label.position)) {
+ label.position = 'middle'
+ }
+ senior.functionCfg.emptyDataStrategy = 'ignoreData'
+ return chart
+ }
+
+ protected configLabel(chart: Chart, options: BarOptions): BarOptions {
+ const tmpOptions = super.configLabel(chart, options)
+ if (!tmpOptions.label) {
+ return {
+ ...tmpOptions,
+ label: false
+ }
+ }
+ const labelAttr = parseJson(chart.customAttr).label
+ const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {})
+ // 默认灰色
+ tmpOptions.label.style.fill = DEFAULT_LABEL.color
+ const label = {
+ fields: [],
+ ...tmpOptions.label,
+ 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 Group({})
+ group.addShape({
+ type: 'text',
+ attrs: {
+ x: 0,
+ y: 0,
+ text: value,
+ textAlign: 'start',
+ textBaseline: 'top',
+ fontSize: labelCfg.fontSize,
+ fontFamily: chart.fontFamily,
+ fill: labelCfg.color
+ }
+ })
+ return group
+ }
+ }
+ return {
+ ...tmpOptions,
+ label
+ }
+ }
+
+ protected configYAxis(chart: Chart, options: BarOptions): BarOptions {
+ const tmpOptions = super.configYAxis(chart, options)
+ if (!tmpOptions.yAxis) {
+ return tmpOptions
+ }
+ if (tmpOptions.yAxis.position === 'left') {
+ tmpOptions.yAxis.position = 'bottom'
+ }
+ if (tmpOptions.yAxis.position === 'right') {
+ tmpOptions.yAxis.position = 'top'
+ }
+ return tmpOptions
+ }
+
+ protected setupOptions(chart: Chart, options: BarOptions): BarOptions {
+ return flow(
+ this.addConditionsStyleColorToData,
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configColor,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyseHorizontal,
+ this.configBarConditions
+ )(chart, options, {}, this)
+ }
+
+ constructor(name = 'bar-horizontal') {
+ super(name, DEFAULT_DATA)
+ }
+}
+
+/**
+ * 堆叠条形图
+ */
+export class HorizontalStackBar extends HorizontalBar {
+ properties = BAR_EDITOR_PROPERTY.filter(ele => ele !== 'threshold')
+ axisConfig = {
+ ...this['axisConfig'],
+ extStack: {
+ name: `${t('chart.stack_item')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1,
+ allowEmpty: true
+ }
+ }
+ propertyInner = {
+ ...this['propertyInner'],
+ 'label-selector': ['color', 'fontSize', 'hPosition', 'labelFormatter'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'tooltipFormatter', 'show']
+ }
+ protected configLabel(chart: Chart, options: BarOptions): BarOptions {
+ const baseOptions = super.configLabel(chart, options)
+ if (!baseOptions.label) {
+ return baseOptions
+ }
+ const { label: labelAttr } = parseJson(chart.customAttr)
+ baseOptions.label.style.fill = labelAttr.color
+ const label = {
+ ...baseOptions.label,
+ formatter: function (param: Datum) {
+ return valueFormatter(param.value, labelAttr.labelFormatter)
+ }
+ }
+ return {
+ ...baseOptions,
+ label
+ }
+ }
+
+ protected configTooltip(chart: Chart, options: BarOptions): BarOptions {
+ const tooltipAttr = parseJson(chart.customAttr).tooltip
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: false
+ }
+ }
+ const tooltip = {
+ formatter: (param: Datum) => {
+ const obj = { name: param.category, value: param.value }
+ const res = valueFormatter(param.value, tooltipAttr.tooltipFormatter)
+ obj.value = res ?? ''
+ return obj
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+ protected configColor(chart: Chart, options: BarOptions): BarOptions {
+ return this.configStackColor(chart, options)
+ }
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ return setUpStackSeriesColor(chart, data)
+ }
+
+ protected configData(chart: Chart, options: BarOptions): BarOptions {
+ const { xAxis, extStack, yAxis } = chart
+ const mainSort = xAxis.some(axis => axis.sort !== 'none')
+ const subSort = extStack.some(axis => axis.sort !== 'none')
+ if (mainSort || subSort) {
+ return options
+ }
+ const quotaSort = yAxis?.[0]?.sort !== 'none'
+ if (!quotaSort || !extStack.length || !yAxis.length) {
+ return options
+ }
+ const { data } = options
+ const mainAxisValueMap = data.reduce((p, n) => {
+ p[n.field] = p[n.field] ? p[n.field] + n.value : n.value || 0
+ return p
+ }, {})
+ const sort = yAxis[0].sort
+ data.sort((p, n) => {
+ if (sort === 'asc') {
+ return mainAxisValueMap[p.field] - mainAxisValueMap[n.field]
+ } else {
+ return mainAxisValueMap[n.field] - mainAxisValueMap[p.field]
+ }
+ })
+ return options
+ }
+
+ protected setupOptions(chart: Chart, options: BarOptions): BarOptions {
+ return flow(
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configData,
+ this.configColor,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyseHorizontal
+ )(chart, options, {}, this)
+ }
+
+ constructor(name = 'bar-stack-horizontal') {
+ super(name)
+ this.baseOptions = {
+ ...this.baseOptions,
+ isGroup: false,
+ isStack: true,
+ meta: {
+ category: {
+ type: 'cat'
+ }
+ }
+ }
+ this.axis = [...this.axis, 'extStack']
+ }
+}
+
+/**
+ * 百分比堆叠条形图
+ */
+export class HorizontalPercentageStackBar extends HorizontalStackBar {
+ propertyInner = {
+ ...this['propertyInner'],
+ 'label-selector': ['color', 'fontSize', 'hPosition', 'reserveDecimalCount'],
+ 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'show']
+ }
+ protected configLabel(chart: Chart, options: BarOptions): BarOptions {
+ const baseOptions = super.configLabel(chart, options)
+ if (!baseOptions.label) {
+ return baseOptions
+ }
+ const { customAttr } = chart
+ const l = parseJson(customAttr).label
+ const label = {
+ ...baseOptions.label,
+ formatter: function (param: Datum) {
+ if (!param.value) {
+ return '0%'
+ }
+ return (Math.round(param.value * 10000) / 100).toFixed(l.reserveDecimalCount) + '%'
+ }
+ }
+ return {
+ ...baseOptions,
+ label
+ }
+ }
+
+ protected configTooltip(chart: Chart, options: BarOptions): BarOptions {
+ const tooltipAttr = parseJson(chart.customAttr).tooltip
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: {
+ showContent: false
+ }
+ }
+ }
+ const { customAttr } = chart
+ const l = parseJson(customAttr).label
+ const tooltip = {
+ formatter: (param: Datum) => {
+ const obj = { name: param.category, value: param.value }
+ obj.value = (Math.round(param.value * 10000) / 100).toFixed(l.reserveDecimalCount) + '%'
+ return obj
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+ protected setupOptions(chart: Chart, options: BarOptions): BarOptions {
+ return flow(
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configColor,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyseHorizontal
+ )(chart, options, {}, this)
+ }
+
+ constructor() {
+ super('percentage-bar-stack-horizontal')
+ this.baseOptions = {
+ ...this.baseOptions,
+ isPercent: true
+ }
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/progress-bar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/progress-bar.ts
new file mode 100644
index 0000000..b9399cf
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/progress-bar.ts
@@ -0,0 +1,389 @@
+import { G2PlotChartView, G2PlotDrawOptions } from '../../types/impl/g2plot'
+import { flow, hexColorToRGBA, parseJson } from '../../../util'
+import {
+ configAxisLabelLengthLimit,
+ configPlotTooltipEvent,
+ getTooltipContainer,
+ getTooltipItemConditionColor,
+ setGradientColor,
+ TOOLTIP_TPL
+} from '../../common/common_antv'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import type { Bar as G2Progress, BarOptions } from '@antv/g2plot/esm/plots/bar'
+import {
+ BAR_AXIS_TYPE,
+ BAR_EDITOR_PROPERTY_INNER
+} from '@/data-visualization/chart/components/js/panel/charts/bar/common'
+import { cloneDeep, defaultTo } from 'lodash-es'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { Options } from '@antv/g2plot/esm'
+import { DEFAULT_BASIC_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+
+const { t } = useI18n()
+
+export class ProgressBar extends G2PlotChartView {
+ axisConfig = {
+ xAxis: {
+ name: `${t('chart.form_type')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1
+ },
+ yAxis: {
+ name: `${t('chart.progress_target')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ },
+ yAxisExt: {
+ name: `${t('chart.progress_current')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'y-axis-selector',
+ 'title-selector',
+ 'function-cfg',
+ 'jump-set',
+ 'linkage',
+ 'threshold'
+ ]
+ propertyInner = {
+ ...BAR_EDITOR_PROPERTY_INNER,
+ 'legend-selector': null,
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha', 'gradient', 'radiusColumnBar', 'columnWidthRatio'],
+ 'label-selector': ['hPosition', 'color', 'fontSize', 'showQuota', 'showProportion'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'tooltipFormatter', 'show'],
+ 'y-axis-selector': [
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisForm',
+ 'axisLabel',
+ 'position',
+ 'showLengthLimit'
+ ],
+ 'function-cfg': ['emptyDataStrategy'],
+ threshold: ['lineThreshold']
+ }
+ axis: AxisType[] = [...BAR_AXIS_TYPE, 'yAxisExt']
+ protected baseOptions: BarOptions = {
+ data: [],
+ xField: 'progress',
+ yField: 'title',
+ seriesField: 'type',
+ isGroup: false,
+ isPercent: true,
+ isStack: true,
+ xAxis: false,
+ appendPadding: [0, 0, 10, 0]
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data?.length) {
+ return
+ }
+ const getCompletionRate = (target: number, current: number) => {
+ if (target === 0) {
+ return 100
+ }
+ // 目标为正 当前为负
+ if (target > 0 && current < 0) {
+ return 0
+ }
+ // 目标为负 当前为正 正向
+ if ((target < 0 && current > 0) || (target < 0 && current === 0)) {
+ return (2 - current / target) * 100
+ }
+ // 目标与当前都为正
+ if (target > 0 && current > 0) {
+ return (current / target) * 100
+ }
+ // 目标与当前都为负 负向小于0为0
+ if (target < 0 && current < 0) {
+ const completionRate = (2 - current / target) * 100
+ return Number(Math.max(completionRate, 0).toFixed(2))
+ }
+ return 0
+ }
+ // data
+ const sourceData: Array = cloneDeep(chart.data.data)
+ const data1 = defaultTo(sourceData[0]?.data, [])
+ const data2 = defaultTo(sourceData[1]?.data, [])
+ const currentData = data2.map(item => {
+ const progress = getCompletionRate(data1.find(i => i.field === item.field)?.value, item.value)
+ return {
+ ...item,
+ type: 'current',
+ title: item.field,
+ id: item.quotaList[0].id,
+ originalValue: item.value,
+ originalProgress: progress,
+ progress: progress >= 100 ? 100 : progress
+ }
+ })
+ const targetData = data1.map(item => {
+ const progress = 100 - currentData.find(i => i.title === item.field)?.progress
+ return {
+ ...item,
+ type: 'target',
+ title: item.field,
+ id: item.quotaList[0].id,
+ originalValue: item.value,
+ progress: progress
+ }
+ })
+ // options
+ const initOptions: BarOptions = {
+ ...this.baseOptions,
+ data: currentData.concat(targetData).flat()
+ }
+ const options = this.setupOptions(chart, initOptions)
+
+ const { Bar: G2Progress } = await import('@antv/g2plot/esm/plots/bar')
+ // 开始渲染
+ const newChart = new G2Progress(container, options)
+
+ newChart.on('interval:click', action)
+ configPlotTooltipEvent(chart, newChart)
+ configAxisLabelLengthLimit(chart, newChart)
+ return newChart
+ }
+ protected configBasicStyle(chart: Chart, options: BarOptions): BarOptions {
+ const basicStyle = parseJson(chart.customAttr).basicStyle
+ let color1 = basicStyle.colors?.map((ele, index) => {
+ if (index === 1) {
+ return hexColorToRGBA(ele, basicStyle.alpha > 10 ? 10 : basicStyle.alpha)
+ } else {
+ return hexColorToRGBA(ele, basicStyle.alpha)
+ }
+ })
+ if (basicStyle.gradient) {
+ color1 = color1.map((ele, _index) => {
+ return setGradientColor(ele, true, 0)
+ })
+ }
+ options = {
+ ...options,
+ color: datum => {
+ if (datum.type === 'target') {
+ return 'rgba(0, 0, 0, 0)'
+ }
+ return color1[0]
+ },
+ barBackground: {
+ style: {
+ fill: color1[1]
+ }
+ }
+ }
+ if (basicStyle.radiusColumnBar === 'roundAngle') {
+ const barStyle = {
+ radius: [
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius
+ ]
+ }
+ options = {
+ ...options,
+ barStyle
+ }
+ }
+
+ let barWidthRatio
+ const _v = basicStyle.columnWidthRatio ?? DEFAULT_BASIC_STYLE.columnWidthRatio
+ if (_v >= 1 && _v <= 100) {
+ barWidthRatio = _v / 100.0
+ } else if (_v < 1) {
+ barWidthRatio = 1 / 100.0
+ } else if (_v > 100) {
+ barWidthRatio = 1
+ }
+ if (barWidthRatio) {
+ options.barWidthRatio = barWidthRatio
+ }
+
+ return options
+ }
+ protected configTooltip(chart: Chart, options: BarOptions): BarOptions {
+ const tooltipAttr = parseJson(chart.customAttr).tooltip
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: {
+ showContent: false
+ }
+ }
+ }
+ const yAxis = cloneDeep(chart.yAxis)[0]
+ const yAxisExt = cloneDeep(chart.yAxisExt)[0]
+ return {
+ ...options,
+ tooltip: {
+ showContent: true,
+ domStyles: {
+ 'g2-tooltip-marker': null
+ },
+ customItems(originalItems) {
+ const result = []
+ originalItems.forEach(item => {
+ if (item.data) {
+ const value = valueFormatter(item.data.value, tooltipAttr.tooltipFormatter)
+ if (item.data.id === yAxis.id) {
+ result.push({
+ ...item,
+ marker: false,
+ name: yAxis.chartShowName ? yAxis.chartShowName : yAxis.name,
+ value: value
+ })
+ }
+ if (item.data.id === yAxisExt.id) {
+ result.push({
+ ...item,
+ marker: false,
+ name: yAxisExt.chartShowName ? yAxisExt.chartShowName : yAxisExt.name,
+ value: value
+ })
+ }
+ }
+ })
+ return result.length == 0 ? originalItems : result
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ }
+ }
+
+ protected configLabel(chart: Chart, options: BarOptions): BarOptions {
+ const baseOptions = super.configLabel(chart, options)
+ if (!baseOptions.label) return baseOptions
+ if (!baseOptions.label.layout?.[0]) {
+ baseOptions.label.layout = [{ type: 'limit-in-canvas' }]
+ }
+ const { label: labelAttr } = parseJson(chart.customAttr)
+ baseOptions.label.style.fill = labelAttr.color
+ const label = {
+ ...baseOptions.label,
+ content: item => {
+ if (item.type === 'target') return ''
+ let text = ''
+ if (labelAttr.showQuota) text += valueFormatter(item.value, labelAttr.quotaLabelFormatter)
+ if (labelAttr.showProportion) {
+ let proportion = item.originalProgress.toFixed(labelAttr.reserveDecimalCount) + '%'
+ if (labelAttr.showQuota) {
+ proportion = ` (${proportion}) `
+ }
+ text += proportion
+ }
+ return text
+ }
+ }
+ if (label.position === 'top') label.position = 'right'
+ return { ...baseOptions, label }
+ }
+ protected configYAxis(chart: Chart, options: BarOptions): BarOptions {
+ const baseOption = super.configYAxis(chart, options)
+ if (!baseOption.yAxis) {
+ return baseOption
+ }
+ if (baseOption.yAxis.position === 'left') {
+ baseOption.yAxis.position = 'bottom'
+ }
+ if (baseOption.yAxis.position === 'right') {
+ baseOption.yAxis.position = 'top'
+ }
+ return baseOption
+ }
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.customStyle.yAxis = {
+ ...chart.customStyle.yAxis,
+ position: 'left',
+ axisLine: {
+ show: false,
+ lineStyle: chart.customStyle.yAxis.axisLine.lineStyle
+ },
+ splitLine: {
+ show: false,
+ lineStyle: chart.customStyle.yAxis.axisLine.lineStyle
+ }
+ }
+ chart.customStyle.legend.show = false
+ chart.customAttr.label.show = true
+ chart.customAttr.label.position = 'right'
+ chart.customAttr.label.showQuota = false
+ chart.customAttr.label.showProportion = true
+ return chart
+ }
+
+ protected configLegend(chart: Chart, options: BarOptions): BarOptions {
+ const o = super.configLegend(chart, options)
+ return {
+ ...o,
+ legend: false
+ }
+ }
+
+ protected configEmptyDataStrategy(chart: Chart, options: BarOptions): BarOptions {
+ 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).map(obj => obj['field'])
+ return {
+ ...options,
+ data: data.filter(obj => {
+ if (emptyFields.includes(obj['field'])) {
+ return false
+ }
+ return true
+ })
+ }
+ }
+ if (strategy === 'breakLine') {
+ data.forEach(obj => {
+ if (obj['value'] === null) {
+ obj['value'] = null
+ }
+ })
+ }
+ if (strategy === 'setZero') {
+ data.forEach(obj => {
+ if (obj['value'] === null) {
+ obj['value'] = 0
+ }
+ })
+ }
+ return options
+ }
+
+ protected setupOptions(chart: Chart, options: BarOptions): BarOptions {
+ return flow(
+ this.addConditionsStyleColorToData,
+ this.configTheme,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configYAxis,
+ this.configEmptyDataStrategy,
+ this.configBarConditions
+ )(chart, options)
+ }
+
+ constructor() {
+ super('progress-bar', [])
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/range-bar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/range-bar.ts
new file mode 100644
index 0000000..5db5326
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/range-bar.ts
@@ -0,0 +1,434 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { Bar, BarOptions } from '@antv/g2plot/esm/plots/bar'
+import {
+ configAxisLabelLengthLimit,
+ configPlotTooltipEvent,
+ getPadding,
+ getTooltipContainer,
+ setGradientColor,
+ TOOLTIP_TPL
+} from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { cloneDeep, find } from 'lodash-es'
+import { flow, hexColorToRGBA, parseJson } from '@/data-visualization/chart/components/js/util'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import {
+ BAR_AXIS_TYPE,
+ BAR_RANGE_EDITOR_PROPERTY,
+ BAR_EDITOR_PROPERTY_INNER
+} from '@/data-visualization/chart/components/js/panel/charts/bar/common'
+import { Datum } from '@antv/g2plot/esm/types/common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { DEFAULT_BASIC_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+
+const { t } = useI18n()
+const DEFAULT_DATA = []
+
+/**
+ * 区间条形图
+ */
+export class RangeBar extends G2PlotChartView {
+ axisConfig = {
+ xAxis: {
+ name: `${t('chart.drag_block_type_axis')} / ${t('chart.dimension')}`,
+ type: 'd'
+ },
+ yAxis: {
+ name: `${t('chart.drag_block_value_start')} / ${t('chart.time_dimension_or_quota')}`,
+ limit: 1,
+ type: 'q'
+ },
+ yAxisExt: {
+ name: `${t('chart.drag_block_value_end')} / ${t('chart.time_dimension_or_quota')}`,
+ limit: 1,
+ type: 'q'
+ }
+ }
+ properties = BAR_RANGE_EDITOR_PROPERTY.filter(p => p !== 'threshold')
+ propertyInner = {
+ ...BAR_EDITOR_PROPERTY_INNER,
+ 'label-selector': ['hPosition', 'color', 'fontSize', 'labelFormatter', 'showGap'],
+ 'tooltip-selector': [
+ 'fontSize',
+ 'color',
+ 'backgroundColor',
+ 'tooltipFormatter',
+ 'showGap',
+ 'show'
+ ],
+ 'x-axis-selector': [...BAR_EDITOR_PROPERTY_INNER['x-axis-selector'], 'axisLabelFormatter'],
+ 'y-axis-selector': [
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisLine',
+ 'splitLine',
+ 'axisForm',
+ 'axisLabel',
+ 'position',
+ 'showLengthLimit'
+ ]
+ }
+ axis: AxisType[] = [...BAR_AXIS_TYPE, 'yAxisExt']
+ protected baseOptions: BarOptions = {
+ data: [],
+ xField: 'values',
+ yField: 'field',
+ colorField: 'category',
+ isGroup: true
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data?.length) {
+ return
+ }
+ // data
+ const data: Array = cloneDeep(chart.data.data)
+
+ data.forEach(d => {
+ d.tempId = (Math.random() * 10000000).toString()
+ })
+
+ const ifAggregate = !!chart.aggregate
+
+ const isDate = !!chart.data.isDate
+
+ const axis = chart.yAxis ?? chart.yAxisExt ?? []
+ let dateFormat: string
+ const dateSplit = axis[0]?.datePattern === 'date_split' ? '/' : '-'
+ switch (axis[0]?.dateStyle) {
+ case 'y':
+ dateFormat = 'YYYY'
+ break
+ case 'y_M':
+ dateFormat = 'YYYY' + dateSplit + 'MM'
+ break
+ case 'y_M_d':
+ dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD'
+ break
+ // case 'H_m_s':
+ // dateFormat = 'HH:mm:ss'
+ // break
+ case 'y_M_d_H':
+ dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH'
+ break
+ case 'y_M_d_H_m':
+ dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH:mm'
+ break
+ case 'y_M_d_H_m_s':
+ dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH:mm:ss'
+ break
+ default:
+ dateFormat = 'YYYY-MM-dd HH:mm:ss'
+ }
+
+ const minTime = chart.data.minTime
+ const maxTime = chart.data.maxTime
+
+ const minNumber = chart.data.min
+ const maxNumber = chart.data.max
+
+ // options
+ const initOptions: BarOptions = {
+ ...this.baseOptions,
+ appendPadding: getPadding(chart),
+ data,
+ seriesField: isDate ? (ifAggregate ? 'category' : undefined) : 'category',
+ isGroup: isDate ? !ifAggregate : false,
+ isStack: isDate ? !ifAggregate : false,
+ meta: isDate
+ ? {
+ values: {
+ type: 'time',
+ min: minTime,
+ max: maxTime,
+ mask: dateFormat
+ },
+ tempId: {
+ key: true
+ }
+ }
+ : {
+ values: {
+ min: minNumber,
+ max: maxNumber,
+ mask: dateFormat
+ },
+ tempId: {
+ key: true
+ }
+ }
+ }
+
+ const options = this.setupOptions(chart, initOptions)
+
+ const { Bar: BarClass } = await import('@antv/g2plot/esm/plots/bar')
+ // 开始渲染
+ const newChart = new BarClass(container, options)
+
+ newChart.on('interval:click', action)
+ configPlotTooltipEvent(chart, newChart)
+ configAxisLabelLengthLimit(chart, newChart)
+ return newChart
+ }
+
+ protected configXAxis(chart: Chart, options: BarOptions): BarOptions {
+ const tmpOptions = super.configXAxis(chart, options)
+ if (!tmpOptions.xAxis) {
+ return tmpOptions
+ }
+ const xAxis = parseJson(chart.customStyle).xAxis
+ const axisValue = xAxis.axisValue
+ const isDate = !!chart.data.isDate
+ if (tmpOptions.xAxis.label) {
+ tmpOptions.xAxis.label.formatter = value => {
+ if (isDate) {
+ return value
+ }
+ return valueFormatter(value, xAxis.axisLabelFormatter)
+ }
+ }
+ if (tmpOptions.xAxis.position === 'top') {
+ tmpOptions.xAxis.position = 'left'
+ }
+ if (tmpOptions.xAxis.position === 'bottom') {
+ tmpOptions.xAxis.position = 'right'
+ }
+ if (!axisValue?.auto) {
+ const axis = {
+ xAxis: {
+ ...tmpOptions.xAxis,
+ min: axisValue.min,
+ max: axisValue.max,
+ minLimit: axisValue.min,
+ maxLimit: axisValue.max,
+ tickCount: axisValue.splitCount
+ }
+ }
+ return { ...tmpOptions, ...axis }
+ }
+ return tmpOptions
+ }
+
+ protected configTooltip(chart: Chart, options: BarOptions): BarOptions {
+ const isDate = !!chart.data.isDate
+ let tooltip
+ let customAttr: DeepPartial
+ if (chart.customAttr) {
+ customAttr = parseJson(chart.customAttr)
+ // tooltip
+ if (customAttr.tooltip) {
+ const t = JSON.parse(JSON.stringify(customAttr.tooltip))
+ if (t.show) {
+ tooltip = {
+ fields: ['values', 'field', 'gap'],
+ formatter: function (param: Datum) {
+ let res
+ if (isDate) {
+ res = param.values[0] + ' ~ ' + param.values[1]
+ if (t.showGap) {
+ res = res + ' (' + param.gap + ')'
+ }
+ } else {
+ res =
+ valueFormatter(param.values[0], t.tooltipFormatter) +
+ ' ~ ' +
+ valueFormatter(param.values[1], t.tooltipFormatter)
+ if (t.showGap) {
+ res = res + ' (' + valueFormatter(param.gap, t.tooltipFormatter) + ')'
+ }
+ }
+ return { value: res, values: param.values, name: param.field }
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ } else {
+ tooltip = false
+ }
+ }
+ }
+ return { ...options, tooltip }
+ }
+
+ protected configBasicStyle(chart: Chart, options: BarOptions): BarOptions {
+ const isDate = !!chart.data.isDate
+ const ifAggregate = !!chart.aggregate
+ const basicStyle = parseJson(chart.customAttr).basicStyle
+
+ if (isDate && !ifAggregate) {
+ const customColors = []
+ const groups = []
+ for (let i = 0; i < chart.data.data.length; i++) {
+ const name = chart.data.data[i].field
+ if (groups.indexOf(name) < 0) {
+ groups.push(name)
+ }
+ }
+ for (let i = 0; i < groups.length; i++) {
+ const s = groups[i]
+ customColors.push({
+ name: s,
+ color: basicStyle.colors[i % basicStyle.colors.length],
+ isCustom: false
+ })
+ }
+ const color = obj => {
+ const colorObj = find(customColors, o => {
+ return o.name === obj.field
+ })
+ if (colorObj === undefined) {
+ return undefined
+ }
+ const color = hexColorToRGBA(colorObj.color, basicStyle.alpha)
+ if (basicStyle.gradient) {
+ return setGradientColor(color, true)
+ } else {
+ return color
+ }
+ }
+
+ options = {
+ ...options,
+ color
+ }
+ } else {
+ if (basicStyle.gradient) {
+ let color = basicStyle.colors
+ color = color.map(ele => {
+ const tmp = hexColorToRGBA(ele, basicStyle.alpha)
+ return setGradientColor(tmp, true)
+ })
+ options = {
+ ...options,
+ color
+ }
+ }
+ }
+ if (basicStyle.radiusColumnBar === 'roundAngle') {
+ const barStyle = {
+ radius: [
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius,
+ basicStyle.columnBarRightAngleRadius
+ ]
+ }
+ options = {
+ ...options,
+ barStyle
+ }
+ }
+ let barWidthRatio
+ const _v = basicStyle.columnWidthRatio ?? DEFAULT_BASIC_STYLE.columnWidthRatio
+ if (_v >= 1 && _v <= 100) {
+ barWidthRatio = _v / 100.0
+ } else if (_v < 1) {
+ barWidthRatio = 1 / 100.0
+ } else if (_v > 100) {
+ barWidthRatio = 1
+ }
+ if (barWidthRatio) {
+ options.barWidthRatio = barWidthRatio
+ }
+
+ return options
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const { customAttr, senior } = chart
+ const { label } = customAttr
+ if (!['left', 'middle', 'right'].includes(label.position)) {
+ label.position = 'middle'
+ }
+ senior.functionCfg.emptyDataStrategy = 'ignoreData'
+ return chart
+ }
+
+ protected configLabel(chart: Chart, options: BarOptions): BarOptions {
+ const isDate = !!chart.data.isDate
+ const ifAggregate = !!chart.aggregate
+
+ const tmpOptions = super.configLabel(chart, options)
+ if (!tmpOptions.label) {
+ return {
+ ...tmpOptions,
+ label: false
+ }
+ }
+ const labelAttr = parseJson(chart.customAttr).label
+
+ if (isDate && !ifAggregate) {
+ if (!tmpOptions.label.layout) {
+ tmpOptions.label.layout = []
+ }
+ tmpOptions.label.layout.push({ type: 'interval-hide-overlap' })
+ tmpOptions.label.layout.push({ type: 'limit-in-plot', cfg: { action: 'hide' } })
+ }
+
+ const label = {
+ fields: [],
+ ...tmpOptions.label,
+ formatter: (param: Datum) => {
+ let res
+ if (isDate) {
+ if (labelAttr.showGap) {
+ res = param.gap
+ } else {
+ res = param.values[0] + ' ~ ' + param.values[1]
+ }
+ } else {
+ if (labelAttr.showGap) {
+ res = valueFormatter(param.gap, labelAttr.labelFormatter)
+ } else {
+ res =
+ valueFormatter(param.values[0], labelAttr.labelFormatter) +
+ ' ~ ' +
+ valueFormatter(param.values[1], labelAttr.labelFormatter)
+ }
+ }
+ return res
+ }
+ }
+ return {
+ ...tmpOptions,
+ label
+ }
+ }
+
+ protected configYAxis(chart: Chart, options: BarOptions): BarOptions {
+ const tmpOptions = super.configYAxis(chart, options)
+ if (!tmpOptions.yAxis) {
+ return tmpOptions
+ }
+ if (tmpOptions.yAxis.position === 'left') {
+ tmpOptions.yAxis.position = 'bottom'
+ }
+ if (tmpOptions.yAxis.position === 'right') {
+ tmpOptions.yAxis.position = 'top'
+ }
+ return tmpOptions
+ }
+
+ protected setupOptions(chart: Chart, options: BarOptions): BarOptions {
+ return flow(
+ this.configTheme,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configEmptyDataStrategy
+ )(chart, options)
+ }
+
+ constructor(name = 'bar-range') {
+ super(name, DEFAULT_DATA)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/bar/waterfall.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/waterfall.ts
new file mode 100644
index 0000000..f827924
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/bar/waterfall.ts
@@ -0,0 +1,337 @@
+import type { WaterfallOptions, Waterfall as G2Waterfall } from '@antv/g2plot/esm/plots/waterfall'
+import { G2PlotChartView, G2PlotDrawOptions } from '../../types/impl/g2plot'
+import { flow, hexColorToRGBA, parseJson } from '../../../util'
+import { valueFormatter } from '../../../formatter'
+import {
+ configAxisLabelLengthLimit,
+ configPlotTooltipEvent,
+ getPadding,
+ getTooltipContainer,
+ getTooltipItemConditionColor,
+ getTooltipSeriesTotalMap,
+ setGradientColor,
+ TOOLTIP_TPL
+} from '../../common/common_antv'
+import { isEmpty } from 'lodash-es'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { DEFAULT_BASIC_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+const { t } = useI18n()
+
+/**
+ * 瀑布图
+ */
+export class Waterfall extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'title-selector',
+ 'legend-selector',
+ 'x-axis-selector',
+ 'y-axis-selector',
+ 'threshold'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha', 'gradient', 'columnWidthRatio'],
+ 'label-selector': ['fontSize', 'color', 'vPosition', 'labelFormatter'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'legend-selector': ['icon', 'orient', 'fontSize', 'color', 'hPosition', 'vPosition'],
+ 'x-axis-selector': [
+ 'position',
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisLine',
+ 'splitLine',
+ 'axisForm',
+ 'axisLabel'
+ ],
+ 'y-axis-selector': [
+ 'position',
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisValue',
+ 'splitLine',
+ 'axisForm',
+ 'axisLabel',
+ 'axisLabelFormatter',
+ 'showLengthLimit'
+ ],
+ threshold: ['lineThreshold']
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'filter', 'drill', 'extLabel', 'extTooltip']
+ 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',
+ limit: 1
+ }
+ }
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data) {
+ return
+ }
+ const data = chart.data.data
+ const baseOptions = {
+ data,
+ xField: 'field',
+ yField: 'value',
+ seriesField: 'category',
+ appendPadding: getPadding(chart)
+ }
+ const options = this.setupOptions(chart, baseOptions)
+ const { Waterfall: G2Waterfall } = await import('@antv/g2plot/esm/plots/waterfall')
+ const newChart = new G2Waterfall(container, options)
+ newChart.on('interval:click', action)
+ configPlotTooltipEvent(chart, newChart)
+ configAxisLabelLengthLimit(chart, newChart)
+ return newChart
+ }
+
+ protected configMeta(chart: Chart, options: WaterfallOptions): WaterfallOptions {
+ const yAxis = chart.yAxis
+ const meta: WaterfallOptions['meta'] = {
+ field: {
+ type: 'cat'
+ }
+ }
+ if (!yAxis?.length) {
+ return {
+ ...options,
+ meta
+ }
+ }
+ const f = yAxis[0]
+ const yAxisStyle = parseJson(chart.customStyle).yAxis
+ meta.value = {
+ alias: f.name,
+ formatter: (value: number) => {
+ return valueFormatter(value, yAxisStyle.axisLabelFormatter)
+ }
+ }
+ return {
+ ...options,
+ meta
+ }
+ }
+
+ protected configBasicStyle(chart: Chart, options: WaterfallOptions): WaterfallOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const { colors, gradient, alpha } = customAttr.basicStyle
+ const [risingColorRgba, fallingColorRgba, totalColorRgba] = colors
+
+ let columnWidthRatio
+ const _v = customAttr.basicStyle.columnWidthRatio ?? DEFAULT_BASIC_STYLE.columnWidthRatio
+ if (_v >= 1 && _v <= 100) {
+ columnWidthRatio = _v / 100.0
+ } else if (_v < 1) {
+ columnWidthRatio = 1 / 100.0
+ } else if (_v > 100) {
+ columnWidthRatio = 1
+ }
+ if (columnWidthRatio) {
+ options.columnWidthRatio = columnWidthRatio
+ }
+
+ return {
+ ...options,
+ total: {
+ label: t('chart.waterfall_total'),
+ style: {
+ fill: setGradientColor(hexColorToRGBA(totalColorRgba, alpha), gradient, 270)
+ }
+ },
+ risingFill: setGradientColor(hexColorToRGBA(risingColorRgba, alpha), gradient, 270),
+ fallingFill: setGradientColor(hexColorToRGBA(fallingColorRgba, alpha), gradient, 270)
+ }
+ }
+
+ protected configYAxis(chart: Chart, options: WaterfallOptions): WaterfallOptions {
+ const tmpOptions = super.configYAxis(chart, options)
+ if (!tmpOptions.yAxis) {
+ return tmpOptions
+ }
+ const yAxis = parseJson(chart.customStyle).yAxis
+ const axisValue = yAxis.axisValue
+ if (!axisValue?.auto) {
+ const axis = {
+ yAxis: {
+ ...tmpOptions.yAxis,
+ min: axisValue.min,
+ max: axisValue.max,
+ minLimit: axisValue.min,
+ maxLimit: axisValue.max,
+ tickCount: axisValue.splitCount
+ }
+ }
+ return { ...tmpOptions, ...axis }
+ }
+ return tmpOptions
+ }
+
+ protected configTooltip(chart: Chart, options: WaterfallOptions): WaterfallOptions {
+ 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
+ const totalMap = getTooltipSeriesTotalMap(options.data)
+ const tooltip: WaterfallOptions['tooltip'] = {
+ showTitle: true,
+ customItems(originalItems) {
+ if (!tooltipAttr.seriesTooltipFormatter?.length) {
+ return originalItems
+ }
+ const result = []
+ const head = originalItems[0]
+ // 汇总
+ if (!head.data.quotaList) {
+ Object.keys(formatterMap).forEach(id => {
+ const formatter = formatterMap[id]
+ let tmpValue = totalMap[id]
+ let color = 'grey'
+ if (id === yAxis[0].id) {
+ tmpValue = head.data.value
+ color = head.color
+ }
+ const value = valueFormatter(tmpValue, formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ if (id === yAxis[0].id) {
+ result.unshift({ color, name, value })
+ return
+ }
+ result.push({ color, name, value })
+ })
+ return result
+ }
+ originalItems
+ .filter(item => formatterMap[item.data.quotaList[0].id])
+ .forEach(item => {
+ const formatter = formatterMap[item.data.quotaList[0].id]
+ const itemValue = (item.value + '').replace(/,/g, '')
+ formatter.formatterCfg.type = 'value'
+ const value = valueFormatter(parseFloat(itemValue), formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ ...item, name, value })
+ })
+ head.data.dynamicTooltipValue?.forEach(item => {
+ const formatter = formatterMap[item.fieldId]
+ if (formatter) {
+ const itemValue = (item.value + '').replace(/,/g, '')
+ const value = valueFormatter(parseFloat(itemValue), formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ color: 'grey', name, value })
+ }
+ })
+ result.forEach(item => {
+ const color = getTooltipItemConditionColor(item)
+ item.color = color
+ })
+ return result
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+
+ protected configLegend(chart: Chart, options: WaterfallOptions): WaterfallOptions {
+ const tmp = super.configLegend(chart, options)
+ if (!tmp.legend) {
+ return tmp
+ }
+ const customAttr = parseJson(chart.customAttr)
+ const { colors, gradient, alpha } = customAttr.basicStyle
+ const [risingColorRgba, fallingColorRgba, totalColorRgba] = colors
+ return {
+ ...tmp,
+ legend: {
+ ...tmp.legend,
+ items: [
+ {
+ name: t('chart.increase'),
+ value: '',
+ marker: {
+ style: {
+ fill: setGradientColor(hexColorToRGBA(risingColorRgba, alpha), gradient, 270)
+ }
+ }
+ },
+ {
+ name: t('chart.decrease'),
+ value: '',
+ marker: {
+ style: {
+ fill: setGradientColor(hexColorToRGBA(fallingColorRgba, alpha), gradient, 270)
+ }
+ }
+ },
+ {
+ name: t('chart.waterfall_total'),
+ value: '',
+ marker: {
+ style: {
+ fill: setGradientColor(hexColorToRGBA(totalColorRgba, alpha), gradient, 270)
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+
+ protected setupOptions(chart: Chart, options: WaterfallOptions): WaterfallOptions {
+ return flow(
+ this.addConditionsStyleColorToData,
+ this.configTheme,
+ this.configLegend,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configXAxis,
+ this.configYAxis,
+ this.configMeta,
+ this.configBarConditions
+ )(chart, options)
+ }
+
+ constructor() {
+ super('waterfall', [])
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/line/area.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/line/area.ts
new file mode 100644
index 0000000..28a3fc6
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/line/area.ts
@@ -0,0 +1,396 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { Area as G2Area, AreaOptions } from '@antv/g2plot/esm/plots/area'
+import {
+ configPlotTooltipEvent,
+ getPadding,
+ getTooltipContainer,
+ setGradientColor,
+ TOOLTIP_TPL
+} from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { cloneDeep } from 'lodash-es'
+import {
+ flow,
+ getLineConditions,
+ getLineLabelColorByCondition,
+ hexColorToRGBA,
+ parseJson,
+ setUpStackSeriesColor
+} from '@/data-visualization/chart/components/js/util'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import {
+ LINE_AXIS_TYPE,
+ LINE_EDITOR_PROPERTY,
+ LINE_EDITOR_PROPERTY_INNER
+} from '@/data-visualization/chart/components/js/panel/charts/line/common'
+import { Label } from '@antv/g2plot/lib/types/label'
+import { Datum } from '@antv/g2plot/esm/types/common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { DEFAULT_LABEL } from '@/data-visualization/chart/components/editor/util/chart'
+import { clearExtremum, extremumEvt } from '@/data-visualization/chart/components/js/extremumUitl'
+import { Group } from '@antv/g-canvas'
+
+const { t } = useI18n()
+const DEFAULT_DATA = []
+export class Area extends G2PlotChartView {
+ 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'
+ ]
+ }
+ 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'
+ }
+ }
+ baseOptions: AreaOptions = {
+ data: [],
+ xField: 'field',
+ yField: 'value',
+ seriesField: 'category',
+ isStack: false,
+ 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: 'active-region',
+ cfg: {
+ start: [{ trigger: 'element:mousemove', action: 'active-region:show' }],
+ end: [{ trigger: 'element:mouseleave', action: 'active-region:hide' }]
+ }
+ }
+ ]
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data?.length) {
+ chart.container = container
+ clearExtremum(chart)
+ return
+ }
+ // data
+ const data = cloneDeep(chart.data.data)
+
+ const initOptions: AreaOptions = {
+ ...this.baseOptions,
+ data,
+ appendPadding: getPadding(chart)
+ }
+ // options
+ const options = this.setupOptions(chart, initOptions)
+ const { Area: G2Area } = await import('@antv/g2plot/esm/plots/area')
+ // 开始渲染
+ const newChart = new G2Area(container, options)
+
+ newChart.on('point:click', action)
+ extremumEvt(newChart, chart, options, container)
+ configPlotTooltipEvent(chart, newChart)
+ return newChart
+ }
+
+ protected configLabel(chart: Chart, options: AreaOptions): AreaOptions {
+ const tmpOptions = super.configLabel(chart, options)
+ if (!tmpOptions.label) {
+ return {
+ ...tmpOptions,
+ label: false
+ }
+ }
+ const { label: labelAttr, basicStyle } = parseJson(chart.customAttr)
+ const conditions = getLineConditions(chart)
+ const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {})
+ tmpOptions.label.style.fill = DEFAULT_LABEL.color
+ const label = {
+ fields: [],
+ ...tmpOptions.label,
+ layout: labelAttr.fullDisplay ? [{ type: 'limit-in-plot' }] : tmpOptions.label.layout,
+ formatter: (data: Datum, _point) => {
+ if (data.EXTREME) {
+ return ''
+ }
+ 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 position =
+ labelCfg.position === 'top'
+ ? -2 - basicStyle.lineSymbolSize
+ : 10 + basicStyle.lineSymbolSize
+ const value = valueFormatter(data.value, labelCfg.formatterCfg)
+ const color =
+ getLineLabelColorByCondition(conditions, data.value, data.quotaList[0].id) ||
+ labelCfg.color
+ const group = new Group({})
+ group.addShape({
+ type: 'text',
+ attrs: {
+ x: 0,
+ y: position,
+ text: value,
+ textAlign: 'start',
+ textBaseline: 'top',
+ fontSize: labelCfg.fontSize,
+ fontFamily: chart.fontFamily,
+ fill: color
+ }
+ })
+ return group
+ }
+ }
+ return {
+ ...tmpOptions,
+ label
+ }
+ }
+
+ protected configBasicStyle(chart: Chart, options: AreaOptions): AreaOptions {
+ // size
+ const customAttr: DeepPartial = parseJson(chart.customAttr)
+ const s: DeepPartial = JSON.parse(JSON.stringify(customAttr.basicStyle))
+ const smooth = s.lineSmooth
+ const point = {
+ size: s.lineSymbolSize,
+ shape: s.lineSymbol
+ }
+ const line = {
+ style: {
+ lineWidth: s.lineWidth
+ }
+ }
+ // custom color
+ const { colors, alpha } = customAttr.basicStyle
+ const areaColors = [...colors, ...colors]
+ let areaStyle
+ if (customAttr.basicStyle.gradient) {
+ const colorMap = new Map()
+ const yAxis = parseJson(chart.customStyle).yAxis
+ const axisValue = yAxis.axisValue
+ const start =
+ !axisValue?.auto && axisValue.min && axisValue.max ? axisValue.min / axisValue.max : 0
+ areaStyle = item => {
+ let ele: string
+ const key = `${item.field}-${item.category}`
+ if (colorMap.has(key)) {
+ ele = colorMap.get(key)
+ } else {
+ ele = areaColors.shift()
+ colorMap.set(key, ele)
+ }
+ if (ele) {
+ return {
+ fill: setGradientColor(hexColorToRGBA(ele, alpha), true, 270, start)
+ }
+ }
+ return {
+ fill: 'rgba(255,255,255,0)'
+ }
+ }
+ }
+ return {
+ ...options,
+ smooth,
+ line,
+ point,
+ areaStyle
+ }
+ }
+
+ protected configYAxis(chart: Chart, options: AreaOptions): AreaOptions {
+ const tmpOptions = super.configYAxis(chart, options)
+ if (!tmpOptions.yAxis) {
+ return tmpOptions
+ }
+ const yAxis = parseJson(chart.customStyle).yAxis
+ if (tmpOptions.yAxis.label) {
+ tmpOptions.yAxis.label.formatter = value => {
+ return valueFormatter(value, yAxis.axisLabelFormatter)
+ }
+ }
+ const axisValue = yAxis.axisValue
+ if (!axisValue?.auto) {
+ const axis = {
+ yAxis: {
+ ...tmpOptions.yAxis,
+ min: axisValue.min,
+ max: axisValue.max,
+ minLimit: axisValue.min,
+ maxLimit: axisValue.max,
+ tickCount: axisValue.splitCount
+ }
+ }
+ return { ...tmpOptions, ...axis }
+ }
+ return tmpOptions
+ }
+
+ protected configTooltip(chart: Chart, options: AreaOptions): AreaOptions {
+ return super.configMultiSeriesTooltip(chart, options)
+ }
+
+ protected setupOptions(chart: Chart, options: AreaOptions): AreaOptions {
+ return flow(
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configColor,
+ this.configLabel,
+ this.configTooltip,
+ this.configBasicStyle,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyse,
+ this.configConditions
+ )(chart, options, {}, this)
+ }
+
+ constructor(name = 'area') {
+ super(name, DEFAULT_DATA)
+ }
+}
+
+/**
+ * 堆叠面积图
+ */
+export class StackArea extends Area {
+ propertyInner = {
+ ...this['propertyInner'],
+ 'label-selector': ['vPosition', 'fontSize', 'color', 'labelFormatter'],
+ 'tooltip-selector': ['fontSize', 'color', 'tooltipFormatter', 'show']
+ }
+ axisConfig = {
+ ...this['axisConfig'],
+ extStack: {
+ name: `${t('chart.stack_item')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1,
+ allowEmpty: true
+ }
+ }
+ protected configLabel(chart: Chart, options: AreaOptions): AreaOptions {
+ const { label: labelAttr, basicStyle } = parseJson(chart.customAttr)
+ if (!labelAttr?.show) {
+ return {
+ ...options,
+ label: false
+ }
+ }
+ const layout = []
+ if (!labelAttr.fullDisplay) {
+ const tmpOptions = super.configLabel(chart, options)
+ layout.push(...tmpOptions.label.layout)
+ } else {
+ layout.push({ type: 'limit-in-plot' })
+ }
+ const position =
+ labelAttr.position === 'top' ? -2 - basicStyle.lineSymbolSize : 8 + basicStyle.lineSymbolSize
+ const label: Label = {
+ position: labelAttr.position as any,
+ offsetY: position,
+ layout,
+ style: {
+ fill: labelAttr.color,
+ fontSize: labelAttr.fontSize
+ },
+ formatter: function (param: Datum) {
+ return valueFormatter(param.value, labelAttr.labelFormatter)
+ }
+ }
+ return { ...options, label }
+ }
+
+ public setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.senior.functionCfg.emptyDataStrategy = 'ignoreData'
+ return chart
+ }
+
+ protected configColor(chart: Chart, options: AreaOptions): AreaOptions {
+ return this.configStackColor(chart, options)
+ }
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ return setUpStackSeriesColor(chart, data)
+ }
+ protected configTooltip(chart: Chart, options: AreaOptions): AreaOptions {
+ const customAttr: DeepPartial = parseJson(chart.customAttr)
+ const tooltipAttr = customAttr.tooltip
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: false
+ }
+ }
+ const tooltip = {
+ formatter: function (param: Datum) {
+ const obj = {
+ name: param.category,
+ value: valueFormatter(param.value, tooltipAttr.tooltipFormatter)
+ }
+ return obj
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return { ...options, tooltip }
+ }
+
+ constructor() {
+ super('area-stack')
+ this.baseOptions = {
+ ...this.baseOptions,
+ isStack: true
+ }
+ delete this.propertyInner.threshold
+ this.properties = this.properties.filter(item => item !== 'threshold')
+ this.axis.push('extStack')
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/line/common.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/line/common.ts
new file mode 100644
index 0000000..ffc4241
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/line/common.ts
@@ -0,0 +1,74 @@
+export const LINE_EDITOR_PROPERTY: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'x-axis-selector',
+ 'y-axis-selector',
+ 'title-selector',
+ 'legend-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'assist-line',
+ 'function-cfg',
+ 'jump-set',
+ 'linkage',
+ 'threshold'
+]
+export const LINE_EDITOR_PROPERTY_INNER: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'label-selector': ['fontSize', 'color'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'show'],
+ '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',
+ 'position',
+ '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'],
+ threshold: ['lineThreshold']
+}
+
+export const LINE_AXIS_TYPE: AxisType[] = [
+ 'xAxis',
+ 'yAxis',
+ 'drill',
+ 'filter',
+ 'extLabel',
+ 'extTooltip'
+]
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/line/line.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/line/line.ts
new file mode 100644
index 0000000..d128c6c
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/line/line.ts
@@ -0,0 +1,377 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { Line as G2Line, LineOptions } from '@antv/g2plot/esm/plots/line'
+import {
+ configPlotTooltipEvent,
+ getPadding,
+ getTooltipContainer,
+ TOOLTIP_TPL
+} from '../../common/common_antv'
+import {
+ flow,
+ getLineConditions,
+ getLineLabelColorByCondition,
+ hexColorToRGBA,
+ parseJson,
+ setUpGroupSeriesColor
+} from '@/data-visualization/chart/components/js/util'
+import { cloneDeep, defaults, isEmpty } from 'lodash-es'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import {
+ LINE_AXIS_TYPE,
+ LINE_EDITOR_PROPERTY,
+ LINE_EDITOR_PROPERTY_INNER
+} from '@/data-visualization/chart/components/js/panel/charts/line/common'
+import type { Datum } from '@antv/g2plot/esm/types/common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { DEFAULT_LABEL, DEFAULT_LEGEND_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+import { clearExtremum, extremumEvt } from '@/data-visualization/chart/components/js/extremumUitl'
+import { Group } from '@antv/g-canvas'
+
+const { t } = useI18n()
+const DEFAULT_DATA = []
+/**
+ * 折线图
+ */
+export class Line extends G2PlotChartView {
+ properties = LINE_EDITOR_PROPERTY
+ propertyInner = {
+ ...LINE_EDITOR_PROPERTY_INNER,
+ 'basic-style-selector': [...LINE_EDITOR_PROPERTY_INNER['basic-style-selector'], 'seriesColor'],
+ 'label-selector': ['seriesLabelVPosition', 'seriesLabelFormatter', 'showExtremum'],
+ 'tooltip-selector': [
+ ...LINE_EDITOR_PROPERTY_INNER['tooltip-selector'],
+ 'seriesTooltipFormatter'
+ ]
+ }
+ axis: AxisType[] = [...LINE_AXIS_TYPE, 'xAxisExt']
+ axisConfig = {
+ ...this['axisConfig'],
+ xAxis: {
+ name: `${t('chart.drag_block_type_axis')} / ${t('chart.dimension')}`,
+ type: 'd'
+ },
+ xAxisExt: {
+ name: `${t('chart.chart_group')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1,
+ allowEmpty: true
+ },
+ yAxis: {
+ name: `${t('chart.drag_block_value_axis')} / ${t('chart.quota')}`,
+ type: 'q'
+ }
+ }
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, action, container } = drawOptions
+ if (!chart.data?.data?.length) {
+ chart.container = container
+ clearExtremum(chart)
+ return
+ }
+ const data = cloneDeep(chart.data.data)
+ // custom color
+ const customAttr = parseJson(chart.customAttr)
+ const color = customAttr.basicStyle.colors
+ // options
+ const initOptions: LineOptions = {
+ data,
+ xField: 'field',
+ yField: 'value',
+ seriesField: 'category',
+ appendPadding: getPadding(chart),
+ color,
+ 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: '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 { Line: G2Line } = await import('@antv/g2plot/esm/plots/line')
+ // 开始渲染
+ const newChart = new G2Line(container, options)
+
+ newChart.on('point:click', action)
+ extremumEvt(newChart, chart, options, container)
+ configPlotTooltipEvent(chart, newChart)
+ return newChart
+ }
+
+ protected configLabel(chart: Chart, options: LineOptions): LineOptions {
+ const tmpOptions = super.configLabel(chart, options)
+ if (!tmpOptions.label) {
+ return {
+ ...tmpOptions,
+ label: false
+ }
+ }
+ const { label: labelAttr, basicStyle } = parseJson(chart.customAttr)
+ const conditions = getLineConditions(chart)
+ const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {})
+ tmpOptions.label.style.fill = DEFAULT_LABEL.color
+ const label = {
+ fields: [],
+ ...tmpOptions.label,
+ layout: labelAttr.fullDisplay ? [{ type: 'limit-in-plot' }] : tmpOptions.label.layout,
+ formatter: (data: Datum, _point) => {
+ if (data.EXTREME) {
+ return ''
+ }
+ 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 position =
+ labelCfg.position === 'top'
+ ? -2 - basicStyle.lineSymbolSize
+ : 10 + basicStyle.lineSymbolSize
+ const value = valueFormatter(data.value, labelCfg.formatterCfg)
+ const color =
+ getLineLabelColorByCondition(conditions, data.value, data.quotaList[0].id) ||
+ labelCfg.color
+ const group = new Group({})
+ group.addShape({
+ type: 'text',
+ attrs: {
+ x: 0,
+ y: position,
+ text: value,
+ textAlign: 'start',
+ textBaseline: 'top',
+ fontSize: labelCfg.fontSize,
+ fontFamily: chart.fontFamily,
+ fill: color
+ }
+ })
+ return group
+ }
+ }
+ return {
+ ...tmpOptions,
+ label
+ }
+ }
+
+ protected configBasicStyle(chart: Chart, options: LineOptions): LineOptions {
+ // 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
+ }
+ return {
+ ...options,
+ smooth,
+ point,
+ lineStyle
+ }
+ }
+
+ protected configCustomColors(chart: Chart, options: LineOptions): LineOptions {
+ const basicStyle = parseJson(chart.customAttr).basicStyle
+ const color = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha))
+ return {
+ ...options,
+ color
+ }
+ }
+
+ protected configYAxis(chart: Chart, options: LineOptions): LineOptions {
+ const tmpOptions = super.configYAxis(chart, options)
+ if (!tmpOptions.yAxis) {
+ return tmpOptions
+ }
+ const yAxis = parseJson(chart.customStyle).yAxis
+ if (tmpOptions.yAxis.label) {
+ tmpOptions.yAxis.label.formatter = value => {
+ return valueFormatter(value, yAxis.axisLabelFormatter)
+ }
+ }
+ const axisValue = yAxis.axisValue
+ if (!axisValue?.auto) {
+ const axis = {
+ yAxis: {
+ ...tmpOptions.yAxis,
+ min: axisValue.min,
+ max: axisValue.max,
+ minLimit: axisValue.min,
+ maxLimit: axisValue.max,
+ tickCount: axisValue.splitCount
+ }
+ }
+ return { ...tmpOptions, ...axis }
+ }
+ return tmpOptions
+ }
+
+ protected configTooltip(chart: Chart, options: LineOptions): LineOptions {
+ 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: LineOptions['tooltip'] = {
+ 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
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ return setUpGroupSeriesColor(chart, data)
+ }
+ protected configLegend(chart: Chart, options: LineOptions): LineOptions {
+ const optionTmp = super.configLegend(chart, options)
+ if (!optionTmp.legend) {
+ return optionTmp
+ }
+ const xAxisExt = chart.xAxisExt[0]
+ if (xAxisExt?.customSort?.length > 0) {
+ // 图例自定义排序
+ const sort = xAxisExt.customSort ?? []
+ if (sort?.length) {
+ // 用值域限定排序,有可能出现新数据但是未出现在图表上,所以这边要遍历一下子维度,加到后面,让新数据显示出来
+ const data = optionTmp.data
+ data?.forEach(d => {
+ const cat = d['category']
+ if (cat && !sort.includes(cat)) {
+ sort.push(cat)
+ }
+ })
+ optionTmp.meta = {
+ ...optionTmp.meta,
+ category: {
+ type: 'cat',
+ values: sort
+ }
+ }
+ }
+ }
+
+ const customStyle = parseJson(chart.customStyle)
+ let size
+ if (customStyle && customStyle.legend) {
+ size = defaults(JSON.parse(JSON.stringify(customStyle.legend)), DEFAULT_LEGEND_STYLE).size
+ } else {
+ size = DEFAULT_LEGEND_STYLE.size
+ }
+
+ optionTmp.legend.marker.style = style => {
+ return {
+ r: size,
+ fill: style.stroke
+ }
+ }
+ return optionTmp
+ }
+ protected setupOptions(chart: Chart, options: LineOptions): LineOptions {
+ return flow(
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configGroupColor,
+ this.configLabel,
+ this.configTooltip,
+ this.configBasicStyle,
+ this.configCustomColors,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configSlider,
+ this.configAnalyse,
+ this.configConditions
+ )(chart, options)
+ }
+
+ constructor(name = 'line') {
+ super(name, DEFAULT_DATA)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/line/stock-line.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/line/stock-line.ts
new file mode 100644
index 0000000..625ad65
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/line/stock-line.ts
@@ -0,0 +1,718 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { Mix, MixOptions } from '@antv/g2plot/esm/plots/mix'
+import { flow, hexColorToRGBA, parseJson } from '@/data-visualization/chart/components/js/util'
+import { LINE_EDITOR_PROPERTY_INNER } from '@/data-visualization/chart/components/js/panel/charts/line/common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import type { Options } from '@antv/g2plot/esm'
+import { MixOptions } from '@antv/g2plot'
+
+const { t } = useI18n()
+const DEFAULT_DATA = []
+/**
+ * K线图
+ */
+export class StockLine extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'legend-selector',
+ 'x-axis-selector',
+ 'y-axis-selector',
+ 'title-selector',
+ 'tooltip-selector',
+ 'function-cfg',
+ 'jump-set',
+ 'linkage'
+ ]
+ propertyInner = {
+ ...LINE_EDITOR_PROPERTY_INNER,
+ 'function-cfg': ['emptyDataStrategy'],
+ 'y-axis-selector': [
+ 'name',
+ 'color',
+ 'fontSize',
+ 'position',
+ 'axisLabel',
+ 'axisLine',
+ 'splitLine',
+ 'axisLabelFormatter'
+ ],
+ 'legend-selector': ['fontSize', 'color', 'show']
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'filter', 'extLabel', 'extTooltip']
+ axisConfig = {
+ xAxis: {
+ name: `${t('common.component.date')} / ${t('chart.dimension')}`,
+ limit: 1,
+ type: 'd'
+ },
+ yAxis: {
+ name: `${t('chart.k_line_yaxis_tip')} / ${t('chart.quota')}`,
+ limit: 4,
+ type: 'q'
+ }
+ }
+
+ /**
+ * 计算收盘价平均值
+ * @param data
+ * @param dayCount
+ * @param chart
+ */
+ calculateMovingAverage = (data, dayCount, chart) => {
+ const xAxis = chart.xAxis
+ const yAxis = chart.yAxis
+ // 时间字段
+ const xAxisDataeaseName = xAxis[0].dataeaseName
+ // 收盘价字段
+ const yAxisDataeaseName = yAxis[1].dataeaseName
+ const result = []
+ for (let i = 0; i < data.length; i++) {
+ if (i < dayCount) {
+ result.push({
+ [xAxisDataeaseName]: data[i][xAxisDataeaseName],
+ value: null
+ })
+ } else {
+ const sum = data
+ .slice(i - dayCount + 1, i + 1)
+ .reduce((sum, item) => sum + item[yAxisDataeaseName], 0)
+ result.push({
+ [xAxisDataeaseName]: data[i][xAxisDataeaseName],
+ value: parseFloat((sum / dayCount).toFixed(3))
+ })
+ }
+ }
+ return result
+ }
+
+ /**
+ * 获取数据集合中对象属性值的最大最小值
+ * @param data
+ */
+ calculateMinMax = data => {
+ return data.reduce(
+ (acc, current) => {
+ // 获取 current 对象的所有属性值
+ const values = Object.values(current)
+ // 过滤出数字值
+ const numericValues: any[] = values.filter(value => typeof value === 'number')
+ // 找到 current 对象的数字属性值中的最大值和最小值
+ // 如果存在数字值,则计算当前对象的最大值和最小值
+ if (numericValues.length > 0) {
+ const currentMax = Math.max(...numericValues)
+ const currentMin = Math.min(...numericValues)
+ // 更新全局最大值和最小值
+ acc.maxValue = Math.max(acc.maxValue, currentMax)
+ acc.minValue = Math.min(acc.minValue, currentMin)
+ }
+ return acc
+ },
+ { maxValue: Number.NEGATIVE_INFINITY, minValue: Number.POSITIVE_INFINITY }
+ )
+ }
+
+ /**
+ * 注册图表事件
+ * @param data
+ * @param plot
+ * @param averagesLineData
+ */
+ registerEvent = (data, plot, averagesLineData) => {
+ // 监听图例点击事件,显示隐藏
+ let risingVisible = true
+ plot.on('legend-item:click', evt => {
+ const { value } = evt.target.get('delegateObject').item
+ if (value === 'k') {
+ risingVisible = !risingVisible
+ plot.chart.geometries.forEach(geom => {
+ if (geom.type === 'schema') {
+ geom.changeVisible(risingVisible)
+ }
+ })
+ } else {
+ const lines = plot.chart.geometries.filter(item => item.type === 'line')
+ const points = plot.chart.geometries.filter(item => item.type === 'point')
+ let lineIndex = 0
+ for (const key of averagesLineData.keys()) {
+ lineIndex++
+ if (key === value) {
+ lines[lineIndex - 1].changeVisible(!lines[lineIndex - 1].visible)
+ points[lineIndex - 1].changeVisible(!points[lineIndex - 1].visible)
+ }
+ }
+ }
+ })
+ // 监听图表渲染事件
+ plot.on('afterrender', e => {
+ let first = false
+ if (plot.chart.options.slider.start === 0.5 && plot.chart.options.slider.end === 1) {
+ first = true
+ }
+ if (e.view?.options?.scales) {
+ const startIndex = Math.floor(0.5 * data.length)
+ const endIndex = Math.ceil(1 * data.length)
+ const filteredData = data.slice(startIndex, endIndex)
+ const { maxValue, minValue } = this.calculateMinMax(
+ first ? filteredData : e.view.filteredData
+ )
+ const a = e.view.options.scales
+ Object.keys(a).forEach(item => {
+ if (a[item].max) {
+ a[item].max = maxValue
+ }
+ if (a[item].min) {
+ a[item].min = minValue
+ }
+ })
+ }
+ })
+ // 监听图例组点击事件,设置缩放
+ plot.on('legend-item-group:click', e => {
+ if (e.view?.options?.scales) {
+ const { maxValue, minValue } = this.calculateMinMax(e.view.filteredData)
+ const a = e.view.options.scales
+ Object.keys(a).forEach(item => {
+ if (a[item].max) {
+ a[item].max = maxValue
+ }
+ if (a[item].min) {
+ a[item].min = minValue
+ }
+ })
+ }
+ })
+ // 监听滑块事件,设置缩放
+ plot.on('slider:valuechanged', e => {
+ const start = e.gEvent.currentTarget.cfg.component.cfg.start
+ const end = e.gEvent.currentTarget.cfg.component.cfg.end
+ plot.chart.options.slider.start = start
+ plot.chart.options.slider.end = end
+ const startIndex = Math.floor(start * data.length)
+ const endIndex = Math.ceil(end * data.length)
+ const filteredData = data.slice(startIndex, endIndex)
+ const { maxValue, minValue } = this.calculateMinMax(filteredData)
+ const a = e.view.options.scales
+ Object.keys(a).forEach(item => {
+ if (a[item].max) {
+ a[item].max = maxValue
+ }
+ if (a[item].min) {
+ a[item].min = minValue
+ }
+ })
+ })
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, action, container } = drawOptions
+ if (!chart.data?.data?.length) {
+ return
+ }
+ const xAxis = chart.xAxis
+ const yAxis = chart.yAxis
+ if (yAxis.length != 4) {
+ return
+ }
+ const basicStyle = parseJson(chart.customAttr).basicStyle
+ const colors = []
+ const alpha = basicStyle.alpha
+ basicStyle.colors.forEach(ele => {
+ colors.push(hexColorToRGBA(ele, alpha))
+ })
+ const data = parseJson(chart.data?.tableRow)
+
+ // 时间字段
+ const xAxisDataeaseName = xAxis[0].dataeaseName
+ const averages = [5, 10, 20, 60, 120, 180]
+ const legendItems: any[] = [
+ {
+ name: '日K',
+ value: 'k',
+ marker: {
+ symbol: (x, y, r) => {
+ const width = r * 1
+ const height = r
+ return [
+ // 矩形框
+ ['M', x - width - 1 / 2, y - height / 2],
+ ['L', x + width + 1 / 2, y - height / 2],
+ ['L', x + width + 1 / 2, y + height / 2],
+ ['L', x - width - 1 / 2, y + height / 2],
+ ['Z'],
+ // 中线
+ ['M', x, y + 10 / 2],
+ ['L', x, y - 10 / 2]
+ ]
+ },
+ style: { fill: 'red', stroke: 'red', lineWidth: 2 }
+ }
+ }
+ ]
+ // 计算均线数据
+ const averagesLineData = new Map()
+ averages.forEach(item => {
+ averagesLineData.set('ma' + item, this.calculateMovingAverage(data, item, chart))
+ })
+
+ // 将均线数据设置到主数据中
+ data.forEach((item: any) => {
+ const date = item[xAxisDataeaseName]
+ for (const [key, value] of averagesLineData) {
+ item[key] = value.find(m => m[xAxisDataeaseName] === date)?.value
+ }
+ })
+
+ const averageLines: any[] = []
+ let index = 0
+ const start = 0.5
+ const end = 1
+ const startIndex = Math.floor(start * data.length)
+ const endIndex = Math.ceil(end * data.length)
+ const filteredData = data.slice(startIndex, endIndex)
+ const { maxValue, minValue } = this.calculateMinMax(filteredData)
+ for (const key of averagesLineData.keys()) {
+ index++
+ averageLines.push({
+ type: 'line',
+ top: true,
+ options: {
+ smooth: false,
+ xField: xAxisDataeaseName,
+ yField: key,
+ color: colors[index - 1],
+ xAxis: null,
+ yAxis: {
+ label: false,
+ min: minValue,
+ max: maxValue,
+ grid: null,
+ line: null
+ },
+ lineStyle: {
+ lineWidth: 2
+ }
+ }
+ })
+ legendItems.push({
+ name: key.toUpperCase(),
+ value: key,
+ marker: { symbol: 'hyphen', style: { stroke: colors[index - 1], lineWidth: 2 } }
+ })
+ }
+ const axis = chart.xAxis ?? []
+ let dateFormat: string
+ const dateSplit = axis[0]?.datePattern === 'date_split' ? '/' : '-'
+ switch (axis[0]?.dateStyle) {
+ case 'y':
+ dateFormat = 'YYYY'
+ break
+ case 'y_M':
+ dateFormat = 'YYYY' + dateSplit + 'MM'
+ break
+ case 'y_M_d':
+ dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD'
+ break
+ // case 'H_m_s':
+ // dateFormat = 'HH:mm:ss'
+ // break
+ case 'y_M_d_H':
+ dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH'
+ break
+ case 'y_M_d_H_m':
+ dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH:mm'
+ break
+ case 'y_M_d_H_m_s':
+ dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH:mm:ss'
+ break
+ default:
+ dateFormat = 'YYYY-MM-dd HH:mm:ss'
+ }
+ const option = this.setupOptions(chart, {
+ data,
+ slider: {
+ start: 0.5,
+ end: 1,
+ textStyle: {
+ fontFamily: chart.fontFamily
+ }
+ },
+ plots: [
+ {
+ type: 'stock',
+ top: true,
+
+ options: {
+ meta: {
+ [xAxisDataeaseName]: {
+ mask: dateFormat
+ }
+ },
+ stockStyle: {
+ stroke: 'black',
+ lineWidth: 0.5
+ },
+ yAxis: {
+ label: {},
+ position: 'left',
+ min: minValue,
+ max: maxValue
+ },
+ xField: xAxisDataeaseName,
+ yField: [
+ yAxis[0].dataeaseName,
+ yAxis[1].dataeaseName,
+ yAxis[2].dataeaseName,
+ yAxis[3].dataeaseName
+ ],
+ legend: {
+ position: 'top',
+ custom: true,
+ items: legendItems
+ }
+ }
+ },
+ ...averageLines
+ ]
+ })
+ const { Mix: MixClass } = await import('@antv/g2plot/esm/plots/mix')
+ const plot = new MixClass(container, option)
+ this.registerEvent(data, plot, averagesLineData)
+ plot.on('schema:click', evt => {
+ const selectSchema = evt.data.data[xAxisDataeaseName]
+ const paramData = parseJson(chart.data?.data)
+ const selectData = paramData.filter(item => item.field === selectSchema)
+ const quotaList = []
+ selectData.forEach(item => {
+ quotaList.push({ ...item.quotaList[0], value: item.value })
+ })
+ if (selectData.length) {
+ const param = {
+ x: evt.x,
+ y: evt.y,
+ data: {
+ data: {
+ ...evt.data.data,
+ value: quotaList[0].value,
+ name: selectSchema,
+ dimensionList: selectData[0].dimensionList,
+ quotaList: quotaList
+ }
+ }
+ }
+ action(param)
+ }
+ })
+ return plot
+ }
+
+ protected configBasicStyle(chart: Chart, options: MixOptions): MixOptions {
+ // 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 plots = []
+ options.plots.forEach(item => {
+ if (item.type === 'stock') {
+ plots.push({ ...item })
+ }
+ if (item.type === 'line') {
+ plots.push({ ...item, options: { ...item.options, smooth, point, lineStyle } })
+ }
+ })
+ return {
+ ...options,
+ plots
+ }
+ }
+
+ protected configTooltip(chart: Chart, options: MixOptions): MixOptions {
+ const tooltipAttr = parseJson(chart.customAttr).tooltip
+ const xAxis = chart.xAxis
+ const newPlots = []
+ const linePlotList = options.plots.filter(item => item.type === 'line')
+ linePlotList.forEach(item => {
+ newPlots.push(item)
+ })
+ const stockPlot = options.plots.filter(item => item.type === 'stock')[0]
+ if (!tooltipAttr.show) {
+ const stockOption = {
+ ...stockPlot.options,
+ tooltip: {
+ showContent: false
+ }
+ }
+ newPlots.push({ ...stockPlot, options: stockOption })
+ return {
+ ...options,
+ plots: newPlots
+ }
+ }
+
+ const showFiled = chart.data.fields
+ const customTooltipItems = originalItems => {
+ const formattedItems = originalItems.map(item => {
+ const fieldObj = showFiled.find(q => q.dataeaseName === item.name)
+ const displayName = fieldObj?.chartShowName || fieldObj?.name || item.name
+ const formattedName = displayName.startsWith('ma') ? displayName.toUpperCase() : displayName
+ tooltipAttr.tooltipFormatter.decimalCount = 3
+ const formattedValue = valueFormatter(item.value, tooltipAttr.tooltipFormatter)
+
+ return {
+ ...item,
+ name: formattedName,
+ value: formattedValue,
+ color: item.color
+ }
+ })
+
+ const hasKLine = formattedItems.some(item => !item.name.startsWith('MA'))
+ const kLines = formattedItems.filter(item => !item.name.startsWith('MA'))
+ return hasKLine
+ ? [
+ { name: '日K', value: '', marker: true, color: kLines[0]?.color },
+ ...kLines,
+ ...formattedItems.filter(item => item.name.startsWith('MA'))
+ ]
+ : formattedItems
+ }
+ const formatTooltipItem = (item: any) => {
+ const size = item.name.startsWith('MA') || !item.value ? 10 : 5
+ const markerMarginRight = item.name.startsWith('MA') || !item.value ? 5 : 9
+ const markerMarginLeft = item.name.startsWith('MA') || !item.value ? 0 : 2
+ return `
+
+
+
+
+
+ ${item.name}
+ ${item.name.startsWith('MA') && item.value === '0' ? '-' : item.value}
+
+
+ `
+ }
+ const generateCustomTooltipContent = (title: string, items: Array) => {
+ return `
+
+
${title}
+
+ ${items.map(formatTooltipItem).join('')}
+
+
+ `
+ }
+ const stockOption = {
+ ...stockPlot.options,
+ tooltip: {
+ showMarkers: true,
+ showCrosshairs: true,
+ showNil: true,
+ crosshairs: {
+ follow: true,
+ text: (axisType, value, data) => {
+ if (axisType === 'y') {
+ return { content: value ? value.toFixed(0) : value }
+ }
+ return { content: data[0].title, position: 'end' }
+ }
+ },
+ showContent: true,
+ customItems: customTooltipItems,
+ customContent: generateCustomTooltipContent
+ }
+ }
+ newPlots.push({ ...stockPlot, options: stockOption })
+ return {
+ ...options,
+ plots: newPlots
+ }
+ }
+
+ protected configXAxis(chart: Chart, options: MixOptions): MixOptions {
+ const xAxisOptions = super.configXAxis(chart, options)
+ if (!xAxisOptions) {
+ return options
+ }
+ const newPlots = []
+ const linePlotList = options.plots.filter(item => item.type === 'line')
+
+ const stockPlot = options.plots.filter(item => item.type === 'stock')[0]
+ const newStockPlot = {
+ ...stockPlot,
+ options: {
+ ...stockPlot.options,
+ xAxis: xAxisOptions['xAxis']
+ ? {
+ ...stockPlot.options['xAxis'],
+ ...xAxisOptions['xAxis']
+ }
+ : {
+ label: false,
+ line: null
+ }
+ }
+ }
+ newPlots.push(newStockPlot)
+ linePlotList.forEach(item => {
+ newPlots.push(item)
+ })
+ return {
+ ...options,
+ plots: newPlots
+ }
+ }
+ protected configYAxis(chart: Chart, options: MixOptions): MixOptions {
+ const yAxisOptions = super.configYAxis(chart, options)
+ if (!yAxisOptions) {
+ return options
+ }
+ const yAxis = parseJson(chart.customStyle).yAxis
+ const newPlots = []
+ const linePlotList = options.plots.filter(item => item.type === 'line')
+
+ const stockPlot = options.plots.filter(item => item.type === 'stock')[0]
+ let label = false
+ if (yAxisOptions['yAxis'].label) {
+ label = {
+ ...yAxisOptions['yAxis'].label,
+ formatter: value => {
+ return valueFormatter(value, yAxis.axisLabelFormatter)
+ }
+ }
+ }
+ const newStockPlot = {
+ ...stockPlot,
+ options: {
+ ...stockPlot.options,
+ yAxis: label
+ ? {
+ ...stockPlot.options['yAxis'],
+ ...yAxisOptions['yAxis'],
+ label
+ }
+ : {
+ ...yAxisOptions['yAxis'],
+ label,
+ grid: null,
+ line: null
+ }
+ }
+ }
+ newPlots.push(newStockPlot)
+ linePlotList.forEach(item => {
+ newPlots.push(item)
+ })
+ return {
+ ...options,
+ plots: newPlots
+ }
+ }
+
+ protected customConfigEmptyDataStrategy(chart: Chart, options: MixOptions): MixOptions {
+ const { data } = options as unknown as Options
+ if (!data?.length) {
+ return options
+ }
+ const strategy = parseJson(chart.senior).functionCfg.emptyDataStrategy
+ if (strategy === 'ignoreData') {
+ for (let i = data.length - 1; i >= 0; i--) {
+ const item = data[i]
+ Object.keys(item).forEach(key => {
+ if (key.startsWith('f_') && item[key] === null) {
+ data.splice(i, 1)
+ }
+ })
+ }
+ }
+ const updateValues = (strategy: 'breakLine' | 'setZero', data: any[]) => {
+ data.forEach(obj => {
+ Object.keys(obj).forEach(key => {
+ if (key.startsWith('f_') && obj[key] === null) {
+ obj[key] = strategy === 'breakLine' ? null : 0
+ }
+ })
+ })
+ }
+ if (strategy === 'breakLine' || strategy === 'setZero') {
+ updateValues(strategy, data)
+ }
+ return options
+ }
+ protected configLegend(chart: Chart, options: MixOptions): MixOptions {
+ let legend = {}
+ let customStyle: CustomStyle
+ const stockPlot = options.plots.filter(item => item.type === 'stock')[0]
+ if (chart.customStyle) {
+ customStyle = parseJson(chart.customStyle)
+ // legend
+ if (customStyle.legend) {
+ const l = JSON.parse(JSON.stringify(customStyle.legend))
+ if (l.show) {
+ legend = {
+ ...stockPlot.options.legend,
+ itemName: {
+ style: {
+ fill: l.color,
+ fontSize: l.fontSize
+ }
+ }
+ }
+ } else {
+ legend = false
+ }
+ }
+ }
+ const newPlots = []
+ const stockOption = {
+ ...stockPlot.options,
+ legend: legend
+ }
+ const linePlotList = options.plots.filter(item => item.type === 'line')
+ linePlotList.forEach(item => {
+ newPlots.push(item)
+ })
+ newPlots.push({ ...stockPlot, options: stockOption })
+ return {
+ ...options,
+ plots: newPlots
+ }
+ }
+
+ protected setupOptions(chart: Chart, options: MixOptions): MixOptions {
+ return flow(
+ this.configTheme,
+ this.configBasicStyle,
+ this.configXAxis,
+ this.configYAxis,
+ this.configTooltip,
+ this.configLegend,
+ this.customConfigEmptyDataStrategy
+ )(chart, options)
+ }
+
+ constructor(name = 'stock-line') {
+ super(name, DEFAULT_DATA)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/liquid/liquid.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/liquid/liquid.ts
new file mode 100644
index 0000000..9ca812c
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/liquid/liquid.ts
@@ -0,0 +1,221 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { Liquid as G2Liquid, LiquidOptions } from '@antv/g2plot/esm/plots/liquid'
+import { flow, hexColorToRGBA, parseJson } from '@/data-visualization/chart/components/js/util'
+import { DEFAULT_MISC } from '@/data-visualization/chart/components/editor/util/chart'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+
+const { t } = useI18n()
+const DEFAULT_LIQUID_DATA = []
+/**
+ * 水波图
+ */
+export class Liquid extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'label-selector',
+ 'misc-selector',
+ 'title-selector',
+ 'threshold'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha'],
+ 'label-selector': ['fontSize', 'color', 'labelFormatter'],
+ 'misc-selector': ['liquidShape', 'liquidSize', 'liquidMaxType', 'liquidMaxField'],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ threshold: ['liquidThreshold']
+ }
+ axis: AxisType[] = ['yAxis', 'filter']
+ axisConfig: AxisConfig = {
+ yAxis: {
+ name: `${t('chart.drag_block_progress')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.series || !chart.yAxis.length) {
+ return
+ }
+ const initOptions: LiquidOptions = {
+ percent: 0
+ }
+ const options = this.setupOptions(chart, initOptions)
+ const { Liquid: G2Liquid } = await import('@antv/g2plot/esm/plots/liquid')
+ const newChart = new G2Liquid(container, options)
+ newChart.on('afterrender', () => {
+ action({
+ from: 'liquid',
+ data: {
+ type: 'liquid',
+ max: chart.data?.series[0]?.data[0]
+ }
+ })
+ })
+ // 处理空数据, 只要有一个指标是空数据,就不显示图表
+ const hasNoneData = chart.data?.series.some(s => !s.data?.[0])
+ this.configEmptyDataStyle(newChart, hasNoneData ? [] : [1], container)
+ if (hasNoneData) {
+ return
+ }
+ return newChart
+ }
+
+ protected configTheme(chart: Chart, options: LiquidOptions): LiquidOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const colors: string[] = []
+ if (customAttr.basicStyle) {
+ const basicStyle = customAttr.basicStyle
+ basicStyle.colors.forEach(ele => {
+ colors.push(hexColorToRGBA(ele, basicStyle.alpha))
+ })
+ }
+ const customStyle = parseJson(chart.customStyle)
+ let bgColor
+ if (customStyle.background) {
+ bgColor = hexColorToRGBA(customStyle.background.color, customStyle.background.alpha)
+ }
+ const theme = {
+ styleSheet: {
+ brandColor: colors[0],
+ paletteQualitative10: colors,
+ paletteQualitative20: colors,
+ backgroundColor: bgColor
+ }
+ }
+ return { ...options, theme }
+ }
+
+ protected configMisc(chart: Chart, options: LiquidOptions): LiquidOptions {
+ const customAttr = parseJson(chart.customAttr)
+ let value = 0
+ if (chart.data.series.length > 0) {
+ value = chart.data.series[0].data[0]
+ }
+ let max, radius, shape
+ if (customAttr.misc) {
+ const misc = customAttr.misc
+ const defaultLiquidMax = chart.data?.series[chart.data?.series.length - 1]?.data[0]
+ if (misc.liquidMaxType === 'dynamic') {
+ max = defaultLiquidMax
+ } else {
+ max = misc.liquidMax ? misc.liquidMax : defaultLiquidMax
+ }
+ radius = (misc.liquidSize ? misc.liquidSize : DEFAULT_MISC.liquidSize) / 100
+ shape = misc.liquidShape ?? DEFAULT_MISC.liquidShape
+ }
+ const size: LiquidOptions = {
+ percent: value / max,
+ radius: radius,
+ shape: shape
+ }
+ return { ...options, ...size }
+ }
+
+ protected configLabel(chart: Chart, options: LiquidOptions): LiquidOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const originVal = options.percent
+ // 数值过大图表会异常,大于 1 无意义
+ if (originVal > 1) {
+ options = {
+ ...options,
+ percent: 1
+ }
+ }
+ if (!customAttr.label?.show) {
+ return {
+ ...options,
+ statistic: {
+ content: false
+ }
+ }
+ }
+ const label = customAttr.label
+ const labelFormatter = label.labelFormatter
+ return {
+ ...options,
+ statistic: {
+ content: {
+ style: {
+ fontSize: label.fontSize.toString() + 'px',
+ color: label.color
+ },
+ formatter: () => {
+ return valueFormatter(originVal, labelFormatter)
+ }
+ }
+ }
+ }
+ }
+
+ protected configThreshold(chart: Chart, options: LiquidOptions): LiquidOptions {
+ const senior = parseJson(chart.senior)
+ if (senior?.threshold?.enable) {
+ const { liquidThreshold } = senior?.threshold
+ if (liquidThreshold) {
+ const { paletteQualitative10: colors } = (options.theme as any).styleSheet
+ const liquidStyle = () => {
+ const thresholdArr = liquidThreshold.split(',')
+ let index = 0
+ thresholdArr.forEach((v, i) => {
+ if (options.percent > parseFloat(v) / 100) {
+ index = i + 1
+ }
+ })
+ return {
+ fill: colors[index % colors.length],
+ stroke: colors[index % colors.length]
+ }
+ }
+ return { ...options, liquidStyle }
+ }
+ }
+ return options
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.customAttr.label = {
+ ...chart.customAttr.label,
+ fontSize: 12,
+ show: true,
+ labelFormatter: {
+ type: 'percent',
+ thousandSeparator: true,
+ decimalCount: 2
+ }
+ }
+ return chart
+ }
+
+ protected setupOptions(chart: Chart, options: LiquidOptions): LiquidOptions {
+ return flow(
+ this.configTheme,
+ this.configMisc,
+ this.configLabel,
+ this.configThreshold
+ )(chart, options)
+ }
+ constructor() {
+ super('liquid', DEFAULT_LIQUID_DATA)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/bubble-map.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/bubble-map.ts
new file mode 100644
index 0000000..d9554e9
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/bubble-map.ts
@@ -0,0 +1,514 @@
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import {
+ L7PlotChartView,
+ L7PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/l7plot'
+import { Choropleth, ChoroplethOptions } from '@antv/l7plot/dist/esm/plots/choropleth'
+import { Dot, DotOptions, IPlotLayer } from '@antv/l7plot'
+import {
+ MAP_AXIS_TYPE,
+ MAP_EDITOR_PROPERTY,
+ MAP_EDITOR_PROPERTY_INNER,
+ MapMouseEvent
+} from '@/data-visualization/chart/components/js/panel/charts/map/common'
+import { flow, getGeoJsonFile, hexColorToRGBA, parseJson } from '@/data-visualization/chart/components/js/util'
+import { cloneDeep, isEmpty } from 'lodash-es'
+import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types'
+import {
+ handleGeoJson,
+ mapRendered,
+ mapRendering
+} from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { deepCopy } from '@/data-visualization/utils/utils'
+import { configCarouselTooltip } from '@/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel'
+import { getCustomGeoArea } from '@/api/data-visualization/map'
+import { TextLayer } from '@antv/l7plot/dist/esm'
+import { centroid } from '@turf/centroid'
+
+const { t } = useI18n()
+
+/**
+ * 气泡地图
+ */
+export class BubbleMap extends L7PlotChartView {
+ properties: EditorProperty[] = [...MAP_EDITOR_PROPERTY, 'bubble-animate']
+ propertyInner = {
+ ...MAP_EDITOR_PROPERTY_INNER,
+ 'tooltip-selector': [...MAP_EDITOR_PROPERTY_INNER['tooltip-selector'], 'carousel'],
+ 'basic-style-selector': [...MAP_EDITOR_PROPERTY_INNER['basic-style-selector'], 'areaBaseColor']
+ }
+ axis = MAP_AXIS_TYPE
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.area')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1
+ },
+ yAxis: {
+ name: `${t('chart.bubble_size')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+ constructor() {
+ super('bubble-map')
+ }
+
+ async drawChart(drawOption: L7PlotDrawOptions): Promise {
+ const { chart, level, areaId, container, action, scope } = drawOption
+ if (!areaId) {
+ return
+ }
+ chart.container = container
+ let geoJson = {} as FeatureCollection
+ let customSubArea: CustomGeoSubArea[] = []
+ let data = chart.data?.data
+ if (areaId.startsWith('custom_')) {
+ customSubArea = (await getCustomGeoArea(areaId)).data || []
+ customSubArea.forEach(a => (a.scopeArr = a.scope?.split(',') || []))
+ geoJson = cloneDeep(await getGeoJsonFile('156'))
+ const areaNameMap = geoJson.features.reduce((p, n) => {
+ p['156' + n.properties.adcode] = n.properties.name
+ return p
+ }, {})
+ const { areaMapping } = parseJson(chart.senior)
+ const areaMap = customSubArea.reduce((p, n) => {
+ const mappedName = areaMapping?.[areaId]?.[n.name]
+ if (mappedName) {
+ n.name = mappedName
+ }
+ p[n.name] = n
+ n.scopeArr = n.scope?.split(',') || []
+ return p
+ }, {})
+ const fakeData = []
+ data?.forEach(d => {
+ const area = areaMap[d.name]
+ if (area) {
+ area.scopeArr.forEach(adcode => {
+ fakeData.push({
+ ...d,
+ name: areaNameMap[adcode],
+ field: areaNameMap[adcode],
+ scope: area.scopeArr,
+ areaName: d.name
+ })
+ })
+ }
+ })
+ data = fakeData
+ } else {
+ if (scope) {
+ geoJson = cloneDeep(await getGeoJsonFile('156'))
+ geoJson.features = geoJson.features.filter(f => scope.includes('156' + f.properties.adcode))
+ } else {
+ geoJson = cloneDeep(await getGeoJsonFile(areaId))
+ }
+ }
+ let options: ChoroplethOptions = {
+ preserveDrawingBuffer: true,
+ map: {
+ type: 'mapbox',
+ style: 'blank'
+ },
+ geoArea: {
+ type: 'geojson'
+ },
+ source: {
+ data: data || [],
+ joinBy: {
+ sourceField: 'name',
+ geoField: 'name',
+ geoData: geoJson
+ }
+ },
+ viewLevel: {
+ level,
+ adcode: 'all'
+ },
+ autoFit: true,
+ chinaBorder: false,
+ color: {
+ field: 'value'
+ },
+ style: {
+ opacity: 1,
+ lineWidth: 0.6,
+ lineOpacity: 1
+ },
+ label: {
+ field: '_DE_LABEL_',
+ style: {
+ textAnchor: 'center'
+ }
+ },
+ tooltip: {},
+ legend: false,
+ // 禁用线上地图数据
+ customFetchGeoData: () => null
+ }
+ const context: Record = { drawOption, geoJson, customSubArea }
+ options = this.setupOptions(chart, options, context)
+
+ const tooltip = deepCopy(options.tooltip)
+ options = { ...options, tooltip: { ...tooltip, showComponent: false } }
+ const view = new Choropleth(container, options)
+ const dotLayer = this.getDotLayer(chart, geoJson, drawOption, customSubArea)
+ if (!areaId.startsWith('custom_')) {
+ dotLayer.options = { ...dotLayer.options, tooltip }
+ }
+ this.configZoomButton(chart, view)
+ mapRendering(container)
+ view.once('loaded', () => {
+ // 修改地图鼠标样式为默认
+ view.scene.map._canvasContainer.lastElementChild.style.cursor = 'default'
+ const { layers } = context
+ if (layers) {
+ layers.forEach(l => {
+ view.addLayer(l)
+ })
+ }
+ dotLayer.addToScene(view.scene)
+ dotLayer.once('add', () => {
+ mapRendered(container)
+ })
+ view.scene.map['keyboard'].disable()
+ dotLayer.on('dotLayer:click', (ev: MapMouseEvent) => {
+ const data = ev.feature.properties
+ let adcode, scope
+ if (areaId.startsWith('custom_')) {
+ adcode = '156'
+ const area = customSubArea.find(a => a.name === data.name)
+ scope = area?.scopeArr
+ } else {
+ adcode = view.currentDistrictData.features.find(
+ i => i.properties.name === ev.feature.properties.name
+ )?.properties.adcode
+ }
+ action({
+ x: ev.x,
+ y: ev.y,
+ data: {
+ data,
+ extra: { adcode, scope }
+ }
+ })
+ })
+ dotLayer.once('loaded', () => {
+ chart.container = container
+ configCarouselTooltip(chart, view, data || [], null, customSubArea, drawOption)
+ })
+ })
+ return view
+ }
+
+ private getDotLayer(
+ chart: Chart,
+ geoJson: FeatureCollection,
+ drawOption: L7PlotDrawOptions,
+ customSubArea: CustomGeoSubArea[]
+ ): IPlotLayer {
+ const { areaId } = drawOption
+ const { basicStyle, tooltip } = parseJson(chart.customAttr)
+ const { bubbleCfg } = parseJson(chart.senior)
+ const { offsetHeight, offsetWidth } = document.getElementById(drawOption.container)
+ const dotData = []
+ const options: DotOptions = {
+ source: {
+ data: dotData,
+ parser: {
+ type: 'json',
+ x: 'x',
+ y: 'y'
+ }
+ },
+ shape: 'circle',
+ size: {
+ field: 'size',
+ value: [5, Math.min(offsetHeight, offsetWidth) / 20]
+ },
+ visible: true,
+ zIndex: 0.05,
+ color: hexColorToRGBA(basicStyle.colors[0], basicStyle.alpha),
+ name: 'bubbleLayer',
+ style: {
+ opacity: 1
+ },
+ state: {
+ active: { color: 'rgba(30,90,255,1)' }
+ },
+ tooltip: {
+ showComponent: tooltip.show
+ }
+ }
+ if (areaId.startsWith('custom_')) {
+ const geoJsonMap = geoJson.features.reduce((p, n) => {
+ if (n.properties['adcode']) {
+ p['156' + n.properties['adcode']] = n
+ }
+ return p
+ }, {})
+ const { areaMapping } = parseJson(chart.senior)
+ const customAreaMap = customSubArea.reduce((p, n) => {
+ const mappedName = areaMapping?.[areaId]?.[n.name]
+ if (mappedName) {
+ n.name = mappedName
+ }
+ p[n.name] = n
+ return p
+ }, {})
+ chart.data?.data?.forEach(d => {
+ const area = customAreaMap[d.name]
+ if (area) {
+ const areaJsonArr = []
+ area.scopeArr?.forEach(adcode => {
+ const json = geoJsonMap[adcode]
+ json && areaJsonArr.push(json)
+ })
+ if (areaJsonArr.length) {
+ const areaJson: FeatureCollection = {
+ type: 'FeatureCollection',
+ features: areaJsonArr
+ }
+ const center = centroid(areaJson)
+ // 轮播用
+ area.centroid = [center.geometry.coordinates[0], center.geometry.coordinates[1]]
+ dotData.push({
+ name: area.name,
+ size: d.value,
+ properties: d,
+ x: center.geometry.coordinates[0],
+ y: center.geometry.coordinates[1]
+ })
+ }
+ }
+ })
+ if (options.tooltip && options.tooltip.showComponent) {
+ options.tooltip.items = ['name', 'adcode', 'value']
+ options.tooltip.customTitle = ({ name }) => {
+ return name
+ }
+ const formatterMap = tooltip.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {}) as Record
+ options.tooltip.customItems = originalItem => {
+ const result = []
+ if (isEmpty(formatterMap)) {
+ return result
+ }
+ const head = originalItem.properties
+ const formatter = formatterMap[head.quotaList?.[0]?.id]
+ if (!isEmpty(formatter)) {
+ const originValue = parseFloat(head.value as string)
+ const value = valueFormatter(originValue, formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ ...head, name, value: `${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: `${value ?? ''}` })
+ }
+ })
+ return result
+ }
+ options.tooltip.domStyles = {
+ 'l7plot-tooltip': {
+ 'background-color': tooltip.backgroundColor,
+ 'font-size': `${tooltip.fontSize}px`,
+ 'line-height': 1.6
+ },
+ 'l7plot-tooltip__name': {
+ color: tooltip.color
+ },
+ 'l7plot-tooltip__value': {
+ color: tooltip.color
+ },
+ 'l7plot-tooltip__title': {
+ color: tooltip.color
+ }
+ }
+ }
+ } else {
+ const areaMap = chart.data?.data?.reduce((obj, value) => {
+ obj[value['field']] = { value: value.value, data: value }
+ return obj
+ }, {})
+ geoJson.features.forEach(item => {
+ const name = item.properties['name']
+ if (areaMap?.[name]?.value) {
+ dotData.push({
+ x: item.properties['centroid'][0],
+ y: item.properties['centroid'][1],
+ size: areaMap[name].value,
+ properties: areaMap[name].data,
+ name: name
+ })
+ }
+ })
+ }
+ if (bubbleCfg && bubbleCfg.enable) {
+ return new Dot({
+ ...options,
+ size: {
+ field: 'size',
+ value: [10, Math.min(offsetHeight, offsetWidth) / 10]
+ },
+ animate: {
+ enable: true,
+ speed: bubbleCfg.speed,
+ rings: bubbleCfg.rings
+ }
+ })
+ }
+ return new Dot(options)
+ }
+
+ private configBasicStyle(
+ chart: Chart,
+ options: ChoroplethOptions,
+ context: Record
+ ): ChoroplethOptions {
+ const { areaId }: L7PlotDrawOptions = context.drawOption
+ const geoJson: FeatureCollection = context.geoJson
+ const { basicStyle, label } = parseJson(chart.customAttr)
+ const senior = parseJson(chart.senior)
+ const curAreaNameMapping = senior.areaMapping?.[areaId]
+ handleGeoJson(geoJson, curAreaNameMapping)
+ options.color = basicStyle.areaBaseColor
+ if (!chart.data?.data?.length || !geoJson?.features?.length) {
+ options.label && (options.label.field = 'name')
+ return options
+ }
+ const data = chart.data.data
+ const areaMap = data.reduce((obj, value) => {
+ obj[value['field']] = value.value
+ return obj
+ }, {})
+ geoJson.features.forEach(item => {
+ const name = item.properties['name']
+ // trick, maybe move to configLabel, here for perf
+ if (label.show) {
+ const content = []
+ if (label.showDimension) {
+ content.push(name)
+ }
+ if (label.showQuota) {
+ areaMap[name] && content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter))
+ }
+ item.properties['_DE_LABEL_'] = content.join('\n\n')
+ }
+ })
+ return options
+ }
+
+ protected configCustomArea(
+ chart: Chart,
+ options: ChoroplethOptions,
+ context: Record
+ ): ChoroplethOptions {
+ const { drawOption, customSubArea, geoJson } = context
+ if (!drawOption.areaId.startsWith('custom_')) {
+ return options
+ }
+ const customAttr = parseJson(chart.customAttr)
+ const { label } = customAttr
+ const data = chart.data?.data
+ const areaMap = data?.reduce((obj, value) => {
+ obj[value['field']] = value
+ return obj
+ }, {})
+ //处理label
+ options.label = {
+ visible: false
+ }
+ if (label.show) {
+ const geoJsonMap = geoJson.features.reduce((p, n) => {
+ if (n.properties['adcode']) {
+ p['156' + n.properties['adcode']] = n
+ }
+ return p
+ }, {})
+ const { areaMapping } = parseJson(chart.senior)
+ const labelLocation = []
+ customSubArea.forEach(area => {
+ const areaJsonArr = []
+ area.scopeArr?.forEach(adcode => {
+ const json = geoJsonMap[adcode]
+ json && areaJsonArr.push(json)
+ })
+ if (areaJsonArr.length) {
+ const areaJson: FeatureCollection = {
+ type: 'FeatureCollection',
+ features: areaJsonArr
+ }
+ const content = []
+ if (label.showDimension) {
+ const mappedName = areaMapping?.[drawOption.areaId]?.[area.name]
+ if (mappedName) {
+ area.name = mappedName
+ }
+ content.push(area.name)
+ }
+ if (label.showQuota) {
+ areaMap[area.name] &&
+ content.push(valueFormatter(areaMap[area.name].value, label.quotaLabelFormatter))
+ }
+ const center = centroid(areaJson)
+ labelLocation.push({
+ name: content.join('\n\n'),
+ x: center.geometry.coordinates[0],
+ y: center.geometry.coordinates[1]
+ })
+ }
+ })
+ const areaLabelLayer = new TextLayer({
+ name: 'areaLabelLayer',
+ source: {
+ data: labelLocation,
+ parser: {
+ type: 'json',
+ x: 'x',
+ y: 'y'
+ }
+ },
+ field: 'name',
+ zIndex: 0.06,
+ style: {
+ fill: label.color,
+ fontSize: label.fontSize,
+ opacity: 1,
+ fontWeight: 'bold',
+ textAnchor: 'center',
+ textAllowOverlap: label.fullDisplay,
+ padding: !label.fullDisplay ? [2, 2] : undefined
+ }
+ })
+ context.layers = [areaLabelLayer]
+ }
+ return options
+ }
+
+ protected setupOptions(
+ chart: Chart,
+ options: ChoroplethOptions,
+ context: Record
+ ): ChoroplethOptions {
+ return flow(
+ this.configEmptyDataStrategy,
+ this.configLabel,
+ this.configStyle,
+ this.configTooltip,
+ this.configBasicStyle,
+ this.configCustomArea
+ )(chart, options, context, this)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/common.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/common.ts
new file mode 100644
index 0000000..7589f4d
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/common.ts
@@ -0,0 +1,56 @@
+export const MAP_EDITOR_PROPERTY: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'title-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'function-cfg',
+ 'map-mapping',
+ 'jump-set',
+ 'linkage'
+]
+
+export const MAP_EDITOR_PROPERTY_INNER: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha', 'areaBorderColor', 'zoom'],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'label-selector': [
+ 'color',
+ 'fontSize',
+ 'labelBgColor',
+ 'labelShadow',
+ 'labelShadowColor',
+ 'showDimension',
+ 'showQuota'
+ ],
+ 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
+ 'function-cfg': ['emptyDataStrategy'],
+ 'map-mapping': ['']
+}
+
+export const MAP_AXIS_TYPE: AxisType[] = [
+ 'xAxis',
+ 'yAxis',
+ 'area',
+ 'drill',
+ 'filter',
+ 'extLabel',
+ 'extTooltip'
+]
+
+export declare type MapMouseEvent = MouseEvent & {
+ feature: GeoJSON.Feature
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/flow-map.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/flow-map.ts
new file mode 100644
index 0000000..2d11189
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/flow-map.ts
@@ -0,0 +1,411 @@
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import {
+ L7ChartView,
+ L7Config,
+ L7DrawConfig,
+ L7Wrapper
+} from '@/data-visualization/chart/components/js/panel/types/impl/l7'
+import { MAP_EDITOR_PROPERTY_INNER } from '@/data-visualization/chart/components/js/panel/charts/map/common'
+import { hexColorToRGBA, parseJson } from '@/data-visualization/chart/components/js/util'
+import { deepCopy } from '@/data-visualization/utils/utils'
+import { GaodeMap } from '@antv/l7-maps'
+import { Scene } from '@antv/l7-scene'
+import { LineLayer } from '@antv/l7-layers'
+import { PointLayer } from '@antv/l7-layers'
+import { mapRendered, mapRendering } from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { DEFAULT_BASIC_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+const { t } = useI18n()
+
+/**
+ * 流向地图
+ */
+export class FlowMap extends L7ChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'title-selector',
+ 'flow-map-line-selector',
+ 'flow-map-point-selector',
+ 'bubble-animate'
+ ]
+ propertyInner: EditorPropertyInner = {
+ ...MAP_EDITOR_PROPERTY_INNER,
+ 'basic-style-selector': [
+ 'mapBaseStyle',
+ 'mapLineStyle',
+ 'zoom',
+ 'showLabel',
+ 'autoFit',
+ 'mapCenter',
+ 'zoomLevel'
+ ]
+ }
+ axis: AxisType[] = ['xAxis', 'xAxisExt', 'filter', 'flowMapStartName', 'flowMapEndName', 'yAxis']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.start_coordinates')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 2
+ },
+ xAxisExt: {
+ name: `${t('chart.end_coordinates')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 2
+ },
+ flowMapStartName: {
+ name: `${t('chart.start_name')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1,
+ allowEmpty: true
+ },
+ flowMapEndName: {
+ name: `${t('chart.end_name')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1,
+ allowEmpty: true
+ },
+ yAxis: {
+ name: `${t('chart.flow_map_line_width')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1,
+ tooltip: t('chart.flow_map_line_width_tip'),
+ allowEmpty: true
+ }
+ }
+ constructor() {
+ super('flow-map', [])
+ }
+
+ async drawChart(drawOption: L7DrawConfig) {
+ const { chart, container } = drawOption
+ const containerDom = document.getElementById(container)
+ const rect = containerDom?.getBoundingClientRect()
+ if (rect?.height <= 0) {
+ return new L7Wrapper(drawOption.chartObj?.getScene(), [])
+ }
+ const xAxis = deepCopy(chart.xAxis)
+ const xAxisExt = deepCopy(chart.xAxisExt)
+ const { basicStyle, misc } = deepCopy(parseJson(chart.customAttr))
+
+ let center: [number, number] = [
+ DEFAULT_BASIC_STYLE.mapCenter.longitude,
+ DEFAULT_BASIC_STYLE.mapCenter.latitude
+ ]
+ if (basicStyle.autoFit === false) {
+ center = [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude]
+ }
+ let mapStyle = basicStyle.mapStyleUrl
+ if (basicStyle.mapStyle !== 'custom') {
+ mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}`
+ }
+ const mapKey = await this.getMapKey()
+ // 底层
+ const chartObj = drawOption.chartObj as unknown as L7Wrapper
+ let scene = chartObj?.getScene()
+ if(scene){
+ if (scene.getLayers()?.length) {
+ await scene.removeAllLayer()
+ scene.setPitch(misc.mapPitch)
+ }
+ }
+ if (mapStyle.indexOf('Satellite') == -1) {
+ scene = new Scene({
+ id: container,
+ logoVisible: false,
+ map: new GaodeMap({
+ token: mapKey?.key ?? undefined,
+ style: mapStyle,
+ pitch: misc.mapPitch,
+ center,
+ zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined,
+ showLabel: !(basicStyle.showLabel === false),
+ WebGLParams: {
+ preserveDrawingBuffer: true
+ }
+ })
+ })
+ }else{
+ scene = new Scene({
+ id: container,
+ logoVisible: false,
+ map: new GaodeMap({
+ token: mapKey?.key ?? undefined,
+ style: mapStyle,
+ features: ['bg', 'road'], // 必须开启路网层
+ plugin: ['AMap.TileLayer.Satellite'], // 显式声明卫星图层
+ WebGLParams: {
+ preserveDrawingBuffer: true
+ }
+ })
+ })
+ }
+ // if (!scene) {
+ // scene = new Scene({
+ // id: container,
+ // logoVisible: false,
+ // map: new GaodeMap({
+ // token: mapKey?.key ?? undefined,
+ // style: mapStyle,
+ // pitch: misc.mapPitch,
+ // center: basicStyle.autoFit === false ? center : undefined,
+ // zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined,
+ // showLabel: !(basicStyle.showLabel === false),
+ // WebGLParams: {
+ // preserveDrawingBuffer: true
+ // }
+ // })
+ // })
+ // } else {
+ // if (scene.getLayers()?.length) {
+ // await scene.removeAllLayer()
+ // scene.setPitch(misc.mapPitch)
+ // scene.setMapStyle(mapStyle)
+ // scene.map.showLabel = !(basicStyle.showLabel === false)
+ // }
+ // if (basicStyle.autoFit === false) {
+ // scene.setZoomAndCenter(basicStyle.zoomLevel, center)
+ // }
+ // }
+ mapRendering(container)
+ if (mapStyle.indexOf('Satellite') == -1) {
+ scene.once('loaded', () => {
+ mapRendered(container)
+ })
+ } else {
+ scene.once('loaded', () => {
+ // 创建卫星图层实例
+ const satelliteLayer = new AMap.TileLayer.Satellite()
+ // 与矢量图层叠加显示
+ satelliteLayer.setMap(scene.map)
+ mapRendered(container)
+ })
+ }
+ // scene.once('loaded', () => {
+ // mapRendered(container)
+ // })
+ this.configZoomButton(chart, scene)
+ if (xAxis?.length < 2 || xAxisExt?.length < 2) {
+ return new L7Wrapper(scene, undefined)
+ }
+ const configList = []
+ configList.push(this.lineConfig(chart, xAxis, xAxisExt, basicStyle, misc))
+ this.startAndEndNameConfig(chart, xAxis, xAxisExt, misc, configList)
+ this.pointConfig(chart, xAxis, xAxisExt, misc, configList)
+ configList[0].once('inited', () => {
+ mapRendered(container)
+ })
+ return new L7Wrapper(scene, configList)
+ }
+
+ lineConfig = (chart, xAxis, xAxisExt, basicStyle, misc) => {
+ const flowLineStyle = {
+ type: misc.flowMapConfig.lineConfig.mapLineType,
+ size:
+ misc.flowMapConfig.lineConfig.mapLineType === 'line'
+ ? misc.flowMapConfig.lineConfig.mapLineWidth / 2
+ : misc.flowMapConfig.lineConfig.mapLineWidth,
+ animate: misc.flowMapConfig.lineConfig.mapLineAnimate,
+ animateDuration: misc.flowMapConfig.lineConfig.mapLineAnimateDuration,
+ gradient: misc.flowMapConfig.lineConfig.mapLineGradient,
+ sourceColor: misc.flowMapConfig.lineConfig.mapLineSourceColor,
+ targetColor: misc.flowMapConfig.lineConfig.mapLineTargetColor,
+ alpha: misc.flowMapConfig.lineConfig.alpha
+ }
+ const colorsWithAlpha = basicStyle.colors.map(color =>
+ hexColorToRGBA(color, misc.flowMapConfig.lineConfig.alpha)
+ )
+ flowLineStyle.sourceColor = colorsWithAlpha[0]
+ flowLineStyle.targetColor = colorsWithAlpha[1]
+ // 线条粗细
+ let lineWidthField = null
+ const yAxis = deepCopy(chart.yAxis)
+ if (yAxis.length > 0) {
+ lineWidthField = yAxis[0].dataeaseName
+ }
+ // 线条颜色
+ let lineColorField = null
+ const yAxisExt = deepCopy(chart.yAxisExt)
+ if (yAxisExt.length > 0) {
+ lineColorField = yAxisExt[0].dataeaseName
+ }
+ const asteriskField = '*'
+ const data = []
+ chart.data?.tableRow.forEach(item => {
+ const newKey = 'f_record'
+ const newObj = Object.keys(item).reduce((acc, key) => {
+ if (key === asteriskField) {
+ acc[newKey] = item[key]
+ } else {
+ acc[key] = item[key]
+ }
+ return acc
+ }, {})
+ data.push(newObj)
+ })
+ const config: L7Config = new LineLayer({
+ name: 'line',
+ blend: 'normal',
+ autoFit: !(basicStyle.autoFit === false)
+ })
+ .source(data, {
+ parser: {
+ type: 'json',
+ x: xAxis[0].dataeaseName,
+ y: xAxis[1].dataeaseName,
+ x1: xAxisExt[0].dataeaseName,
+ y1: xAxisExt[1].dataeaseName
+ }
+ })
+ .size(flowLineStyle.size)
+ .shape(flowLineStyle.type)
+ .animate({
+ enable: flowLineStyle.animate,
+ duration: flowLineStyle.animateDuration,
+ interval: 1,
+ trailLength: 1
+ })
+
+ if (lineWidthField) {
+ config.size(lineWidthField === asteriskField ? 'f_record' : lineWidthField, [1, 10])
+ }
+ if (lineColorField) {
+ config.style({
+ opacity: flowLineStyle.alpha / 100
+ })
+ config.color(lineColorField)
+ } else {
+ if (flowLineStyle.gradient) {
+ config.style({
+ sourceColor: flowLineStyle.sourceColor,
+ targetColor: flowLineStyle.targetColor,
+ opacity: flowLineStyle.alpha / 100
+ })
+ } else {
+ config
+ .style({
+ opacity: flowLineStyle.alpha / 100
+ })
+ .color(flowLineStyle.sourceColor)
+ }
+ }
+
+ return config
+ }
+
+ startAndEndNameConfig = (chart, xAxis, xAxisExt, misc, configList) => {
+ const flowMapStartName = deepCopy(chart.flowMapStartName)
+ const flowMapEndName = deepCopy(chart.flowMapEndName)
+ const textColor = misc.flowMapConfig.pointConfig.text.color
+ const textFontSize = misc.flowMapConfig.pointConfig.text.fontSize
+ const has = new Map()
+ if (flowMapStartName?.length > 0) {
+ const startTextLayer = new PointLayer()
+ .source(chart.data?.tableRow, {
+ parser: {
+ type: 'json',
+ x: xAxis[0].dataeaseName,
+ y: xAxis[1].dataeaseName
+ }
+ })
+ .shape(flowMapStartName[0].dataeaseName, args => {
+ if (has.has('from-' + args)) {
+ return ''
+ }
+ has.set('from-' + args, args)
+ return args
+ })
+ .size(textFontSize)
+ .color(textColor)
+ .style({
+ textAnchor: 'top', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
+ textOffset: [0, 0], // 文本相对锚点的偏移量 [水平, 垂直]
+ spacing: 2, // 字符间距
+ padding: [1, 1], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
+ textAllowOverlap: true,
+ fontFamily: chart.fontFamily ? chart.fontFamily : undefined
+ })
+ configList.push(startTextLayer)
+ }
+ if (flowMapEndName?.length > 0) {
+ const endTextLayer = new PointLayer()
+ .source(chart.data?.tableRow, {
+ parser: {
+ type: 'json',
+ x: xAxisExt[0].dataeaseName,
+ y: xAxisExt[1].dataeaseName
+ }
+ })
+ .shape(flowMapEndName[0].dataeaseName, args => {
+ if (has.has('from-' + args) || has.has('to-' + args)) {
+ return ''
+ }
+ has.set('to-' + args, args)
+ return args
+ })
+ .size(textFontSize)
+ .color(textColor)
+ .style({
+ textAnchor: 'top', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
+ textOffset: [0, 0], // 文本相对锚点的偏移量 [水平, 垂直]
+ spacing: 2, // 字符间距
+ padding: [1, 1], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
+ textAllowOverlap: true,
+ fontFamily: chart.fontFamily ? chart.fontFamily : undefined
+ })
+ configList.push(endTextLayer)
+ }
+ }
+
+ pointConfig = (chart, xAxis, xAxisExt, misc, configList) => {
+ const color = misc.flowMapConfig.pointConfig.point.color
+ const size = misc.flowMapConfig.pointConfig.point.size
+ const { bubbleCfg } = parseJson(chart.senior)
+ const fromDefaultPointLayer = new PointLayer({ zIndex: -1 })
+ .source(chart.data?.tableRow, {
+ parser: {
+ type: 'json',
+ x: xAxis[0].dataeaseName,
+ y: xAxis[1].dataeaseName
+ }
+ })
+ .shape('circle')
+ .size(size)
+ .color(color)
+ .style({
+ blur: 0.6
+ })
+ const toDefaultPointLayer = new PointLayer({ zIndex: -1 })
+ .source(chart.data?.tableRow, {
+ parser: {
+ type: 'json',
+ x: xAxisExt[0].dataeaseName,
+ y: xAxisExt[1].dataeaseName
+ }
+ })
+ .shape('circle')
+ .size(size)
+ .color(color)
+ .style({
+ blur: 0.6
+ })
+ if (bubbleCfg && bubbleCfg.enable) {
+ const animate = {
+ enable: true,
+ speed: bubbleCfg.speed,
+ rings: bubbleCfg.rings
+ }
+ fromDefaultPointLayer.size(size * 2)
+ fromDefaultPointLayer.animate(animate)
+ toDefaultPointLayer.size(size * 2)
+ toDefaultPointLayer.animate(animate)
+ }
+ configList.push(fromDefaultPointLayer)
+ configList.push(toDefaultPointLayer)
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.customAttr.misc.flowMapConfig.lineConfig.mapLineAnimate = true
+ return chart
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/heat-map.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/heat-map.ts
new file mode 100644
index 0000000..3701bbd
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/heat-map.ts
@@ -0,0 +1,186 @@
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import {
+ L7ChartView,
+ L7Config,
+ L7DrawConfig,
+ L7Wrapper
+} from '@/data-visualization/chart/components/js/panel/types/impl/l7'
+import { MAP_EDITOR_PROPERTY_INNER } from '@/data-visualization/chart/components/js/panel/charts/map/common'
+import { flow, parseJson } from '@/data-visualization/chart/components/js/util'
+import { deepCopy } from '@/data-visualization/utils/utils'
+import { GaodeMap } from '@antv/l7-maps'
+import { Scene } from '@antv/l7-scene'
+import { HeatmapLayer } from '@antv/l7-layers'
+import { DEFAULT_BASIC_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+import { mapRendered, mapRendering } from '@/data-visualization/chart/components/js/panel/common/common_antv'
+const { t } = useI18n()
+
+/**
+ * 流向地图
+ */
+export class HeatMap extends L7ChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'title-selector'
+ ]
+ propertyInner: EditorPropertyInner = {
+ ...MAP_EDITOR_PROPERTY_INNER,
+ 'basic-style-selector': [
+ 'colors',
+ 'heatMapStyle',
+ 'zoom',
+ 'showLabel',
+ 'autoFit',
+ 'mapCenter',
+ 'zoomLevel'
+ ]
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'filter']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.longitude_and_latitude')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 2
+ },
+ yAxis: {
+ name: `${t('chart.chart_data')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+ constructor() {
+ super('heat-map', [])
+ }
+
+ async drawChart(drawOption: L7DrawConfig) {
+ const { chart, container } = drawOption
+ const containerDom = document.getElementById(container)
+ const rect = containerDom?.getBoundingClientRect()
+ if (rect?.height <= 0) {
+ return new L7Wrapper(drawOption.chartObj?.getScene(), [])
+ }
+ const xAxis = deepCopy(chart.xAxis)
+ const yAxis = deepCopy(chart.yAxis)
+ let basicStyle: DeepPartial
+ let miscStyle: DeepPartial
+ if (chart.customAttr) {
+ basicStyle = parseJson(chart.customAttr).basicStyle
+ miscStyle = parseJson(chart.customAttr).misc
+ }
+ let center: [number, number] = [
+ DEFAULT_BASIC_STYLE.mapCenter.longitude,
+ DEFAULT_BASIC_STYLE.mapCenter.latitude
+ ]
+ if (basicStyle.autoFit === false) {
+ center = [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude]
+ }
+ let mapStyle = basicStyle.mapStyleUrl
+ if (basicStyle.mapStyle !== 'custom') {
+ mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}`
+ }
+ const mapKey = await this.getMapKey()
+ // 底层
+ const chartObj = drawOption.chartObj as unknown as L7Wrapper
+ let scene = chartObj?.getScene()
+ if(scene){
+ if (scene.getLayers()?.length) {
+ await scene.removeAllLayer()
+ scene.setPitch(miscStyle.mapPitch)
+ }
+ }
+
+ if (mapStyle.indexOf('Satellite') == -1) {
+ scene = new Scene({
+ id: container,
+ logoVisible: false,
+ map: new GaodeMap({
+ token: mapKey?.key ?? undefined,
+ style: mapStyle,
+ pitch: miscStyle.mapPitch,
+ center,
+ zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined,
+ showLabel: !(basicStyle.showLabel === false),
+ WebGLParams: {
+ preserveDrawingBuffer: true
+ }
+ })
+ })
+ } else {
+ scene = new Scene({
+ id: container,
+ logoVisible: false,
+ map: new GaodeMap({
+ token: mapKey?.key ?? undefined,
+ style: mapStyle,
+ features: ['bg', 'road'], // 必须开启路网层
+ plugin: ['AMap.TileLayer.Satellite'], // 显式声明卫星图层
+ WebGLParams: {
+ preserveDrawingBuffer: true
+ }
+ })
+ })
+ }
+
+ // } else {
+ // // if (scene.getLayers()?.length) {
+ // await scene.removeAllLayer()
+ // scene.setPitch(miscStyle.mapPitch)
+ // scene.setMapStyle(mapStyle)
+ // scene.map.showLabel = !(basicStyle.showLabel === false)
+ // if (basicStyle.autoFit === false) {
+ // scene.setZoomAndCenter(basicStyle.zoomLevel, center)
+ // }
+ // // }
+ // }
+ mapRendering(container)
+ if (mapStyle.indexOf('Satellite') == -1) {
+ scene.once('loaded', () => {
+ mapRendered(container)
+ })
+ } else {
+ scene.once('loaded', () => {
+ // 创建卫星图层实例
+ const satelliteLayer = new AMap.TileLayer.Satellite()
+ // 与矢量图层叠加显示
+ satelliteLayer.setMap(scene.map)
+ mapRendered(container)
+ })
+ }
+
+ this.configZoomButton(chart, scene)
+ if (xAxis?.length < 2 || yAxis?.length < 1) {
+ return new L7Wrapper(scene, undefined)
+ }
+ const config: L7Config = new HeatmapLayer({
+ name: 'line',
+ blend: 'normal',
+ autoFit: !(basicStyle.autoFit === false)
+ })
+ .source(chart.data?.data, {
+ parser: {
+ type: 'json',
+ x: 'x',
+ y: 'y'
+ }
+ })
+ .size('value', [0, 1.0]) // weight映射通道
+ .shape(basicStyle.heatMapType ?? DEFAULT_BASIC_STYLE.heatMapType)
+
+ config.style({
+ intensity: basicStyle.heatMapIntensity ?? DEFAULT_BASIC_STYLE.heatMapIntensity,
+ radius: basicStyle.heatMapRadius ?? DEFAULT_BASIC_STYLE.heatMapRadius,
+ rampColors: {
+ colors: basicStyle.colors.reverse(),
+ positions: [0, 0.11, 0.22, 0.33, 0.44, 0.55, 0.66, 0.77, 0.88, 1.0]
+ }
+ })
+
+ return new L7Wrapper(scene, config)
+ }
+
+ protected setupOptions(chart: Chart, config: L7Config): L7Config {
+ return flow(this.configEmptyDataStrategy)(chart, config)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/map.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/map.ts
new file mode 100644
index 0000000..51ceea3
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/map.ts
@@ -0,0 +1,617 @@
+import {
+ L7PlotChartView,
+ L7PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/l7plot'
+import type { Choropleth, ChoroplethOptions } from '@antv/l7plot/dist/esm/plots/choropleth'
+import {
+ filterChartDataByRange,
+ flow,
+ getDynamicColorScale,
+ getGeoJsonFile,
+ hexColorToRGBA,
+ parseJson,
+ getMaxAndMinValueByData,
+ filterEmptyMinValue
+} from '@/data-visualization/chart/components/js/util'
+import {
+ handleGeoJson,
+ mapRendered,
+ mapRendering
+} from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import type { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types'
+import { cloneDeep, defaultsDeep, isEmpty } from 'lodash-es'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { valueFormatter } from '../../../formatter'
+import {
+ MAP_AXIS_TYPE,
+ MAP_EDITOR_PROPERTY,
+ MAP_EDITOR_PROPERTY_INNER,
+ MapMouseEvent
+} from '@/data-visualization/chart/components/js/panel/charts/map/common'
+import type { CategoryLegendListItem } from '@antv/l7plot-component/dist/lib/types/legend'
+import createDom from '@antv/dom-util/esm/create-dom'
+import {
+ CONTAINER_TPL,
+ ITEM_TPL,
+ LIST_CLASS
+} from '@antv/l7plot-component/dist/esm/legend/category/constants'
+import substitute from '@antv/util/esm/substitute'
+import { configCarouselTooltip } from '@/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel'
+import { getCustomGeoArea } from '@/api/data-visualization/map'
+import { centroid } from '@turf/centroid'
+import { TextLayer } from '@antv/l7plot/dist/esm'
+
+const { t } = useI18n()
+
+/**
+ * 地图
+ */
+export class Map extends L7PlotChartView {
+ properties: EditorProperty[] = [...MAP_EDITOR_PROPERTY, 'legend-selector']
+ propertyInner: EditorPropertyInner = {
+ ...MAP_EDITOR_PROPERTY_INNER,
+ 'basic-style-selector': [
+ 'colors',
+ 'alpha',
+ 'areaBorderColor',
+ 'areaBaseColor',
+ 'zoom',
+ 'gradient-color'
+ ],
+ 'legend-selector': ['icon', 'fontSize', 'color'],
+ 'tooltip-selector': [...MAP_EDITOR_PROPERTY_INNER['tooltip-selector'], 'carousel']
+ }
+ axis = MAP_AXIS_TYPE
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.area')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1
+ },
+ yAxis: {
+ name: `${t('chart.chart_data')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+
+ constructor() {
+ super('map', [])
+ }
+
+ async drawChart(drawOption: L7PlotDrawOptions): Promise {
+ const { chart, level, container, action, scope } = drawOption
+ const { areaId } = drawOption
+ if (!areaId) {
+ return
+ }
+ chart.container = container
+ let sourceData = JSON.parse(JSON.stringify(chart.data?.data || []))
+ const { misc } = parseJson(chart.customAttr)
+ const { legend } = parseJson(chart.customStyle)
+ let geoJson = {} as FeatureCollection
+ // 自定义区域,去除非区域数据,优先级最高
+ let customSubArea: CustomGeoSubArea[] = []
+ if (areaId.startsWith('custom_')) {
+ customSubArea = (await getCustomGeoArea(areaId)).data || []
+ geoJson = cloneDeep(await getGeoJsonFile('156'))
+ const areaNameMap = geoJson.features.reduce((p, n) => {
+ p['156' + n.properties.adcode] = n.properties.name
+ return p
+ }, {})
+ const { areaMapping } = parseJson(chart.senior)
+ const areaMap = customSubArea.reduce((p, n) => {
+ const mappedName = areaMapping?.[areaId]?.[n.name]
+ if (mappedName) {
+ n.name = mappedName
+ }
+ p[n.name] = n
+ n.scopeArr = n.scope?.split(',') || []
+ return p
+ }, {})
+ const fakeData = []
+ sourceData.forEach(d => {
+ const area = areaMap[d.name]
+ if (area) {
+ area.scopeArr.forEach(adcode => {
+ fakeData.push({
+ ...d,
+ name: areaNameMap[adcode],
+ field: areaNameMap[adcode],
+ scope: area.scopeArr,
+ areaName: d.name
+ })
+ })
+ }
+ })
+ sourceData = fakeData
+ } else {
+ if (scope) {
+ geoJson = cloneDeep(await getGeoJsonFile('156'))
+ geoJson.features = geoJson.features.filter(f => scope.includes('156' + f.properties.adcode))
+ } else {
+ geoJson = cloneDeep(await getGeoJsonFile(areaId))
+ }
+ }
+ let data = []
+ // 自定义图例
+ if (!misc.mapAutoLegend && legend.show) {
+ let minValue = misc.mapLegendMin
+ let maxValue = misc.mapLegendMax
+ let legendNumber = 9
+ if (misc.mapLegendRangeType === 'custom') {
+ maxValue = 0
+ minValue = 0
+ legendNumber = misc.mapLegendNumber
+ }
+ getMaxAndMinValueByData(sourceData, 'value', maxValue, minValue, (max, min) => {
+ maxValue = max
+ minValue = min
+ action({
+ from: 'map',
+ data: {
+ max: maxValue,
+ min: minValue ?? filterEmptyMinValue(sourceData, 'value'),
+ legendNumber: legendNumber
+ }
+ })
+ })
+ data = filterChartDataByRange(sourceData, maxValue, minValue)
+ } else {
+ data = sourceData
+ }
+ let options: ChoroplethOptions = {
+ preserveDrawingBuffer: true,
+ map: {
+ type: 'mapbox',
+ style: 'blank'
+ },
+ geoArea: {
+ type: 'geojson'
+ },
+ source: {
+ data: data,
+ joinBy: {
+ sourceField: 'name',
+ geoField: 'name',
+ geoData: geoJson
+ }
+ },
+ viewLevel: {
+ level,
+ adcode: 'all'
+ },
+ autoFit: true,
+ chinaBorder: false,
+ color: {
+ field: 'value'
+ },
+ style: {
+ opacity: 1,
+ lineWidth: 0.6,
+ lineOpacity: 1
+ },
+ label: {
+ field: '_DE_LABEL_',
+ style: {
+ textAnchor: 'center'
+ }
+ },
+ state: {
+ active: { stroke: 'green', lineWidth: 1 }
+ },
+ tooltip: {},
+ // 禁用线上地图数据
+ customFetchGeoData: () => null
+ }
+ const context: Record = { drawOption, geoJson, customSubArea }
+ options = this.setupOptions(chart, options, context)
+ const { Choropleth } = await import('@antv/l7plot/dist/esm/plots/choropleth')
+ const view = new Choropleth(container, options)
+ this.configZoomButton(chart, view)
+ mapRendering(container)
+ view.once('loaded', () => {
+ mapRendered(container)
+ const { layers } = context
+ if (layers) {
+ layers.forEach(l => {
+ view.addLayer(l)
+ })
+ }
+ view.scene.map['keyboard'].disable()
+ view.on('fillAreaLayer:click', (ev: MapMouseEvent) => {
+ const data = ev.feature.properties
+ if (areaId.startsWith('custom_')) {
+ data.name = data.areaName
+ data.adcode = '156'
+ }
+ action({
+ x: ev.x,
+ y: ev.y,
+ data: {
+ data,
+ extra: { adcode: data.adcode, scope: data.scope }
+ }
+ })
+ })
+ chart.container = container
+ configCarouselTooltip(chart, view, data, null, customSubArea, drawOption)
+ })
+ return view
+ }
+
+ private configBasicStyle(
+ chart: Chart,
+ options: ChoroplethOptions,
+ context: Record
+ ): ChoroplethOptions {
+ const { areaId }: L7PlotDrawOptions = context.drawOption
+ const geoJson: FeatureCollection = context.geoJson
+ const { basicStyle, label, misc } = parseJson(chart.customAttr)
+ const senior = parseJson(chart.senior)
+ const curAreaNameMapping = senior.areaMapping?.[areaId]
+ handleGeoJson(geoJson, curAreaNameMapping)
+ options.color = {
+ field: 'value',
+ value: [basicStyle.colors[0]],
+ scale: {
+ type: 'quantize',
+ unknown: basicStyle.areaBaseColor
+ }
+ }
+ if (!chart.data?.data?.length || !geoJson?.features?.length) {
+ options.label && (options.label.field = 'name')
+ return options
+ }
+ const sourceData = options.source.data
+ const colors = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha))
+ const { legend } = parseJson(chart.customStyle)
+ let data = sourceData
+ let colorScale = []
+ let minValue = misc.mapLegendMin
+ let maxValue = misc.mapLegendMax
+ let mapLegendNumber = misc.mapLegendNumber
+ if (legend.show) {
+ getMaxAndMinValueByData(sourceData, 'value', maxValue, minValue, (max, min) => {
+ maxValue = max
+ minValue = min
+ mapLegendNumber = 9
+ })
+ // 非自动,过滤数据
+ if (!misc.mapAutoLegend) {
+ data = filterChartDataByRange(sourceData, maxValue, minValue)
+ } else {
+ mapLegendNumber = 9
+ }
+ // 定义最大值、最小值、区间数量和对应的颜色
+ colorScale = getDynamicColorScale(minValue, maxValue, mapLegendNumber, colors)
+ } else {
+ colorScale = colors
+ }
+ const areaMap = data.reduce((obj, value) => {
+ obj[value['field']] = value.value
+ return obj
+ }, {})
+ geoJson.features.forEach(item => {
+ const name = item.properties['name']
+ // trick, maybe move to configLabel, here for perf
+ if (label.show) {
+ const content = []
+ if (label.showDimension) {
+ content.push(name)
+ }
+ if (label.showQuota) {
+ areaMap[name] && content.push(valueFormatter(areaMap[name], label.quotaLabelFormatter))
+ }
+ item.properties['_DE_LABEL_'] = content.join('\n\n')
+ }
+ })
+ if (colorScale.length) {
+ options.color['value'] = colorScale.map(item => (item.color ? item.color : item))
+ if (colorScale[0].value && !misc.mapAutoLegend) {
+ options.color['scale']['domain'] = [
+ minValue ?? filterEmptyMinValue(sourceData, 'value'),
+ maxValue
+ ]
+ }
+ }
+ return options
+ }
+
+ // 内部函数 创建自定义图例的内容
+ private createLegendCustomContent = showItems => {
+ const containerDom = createDom(CONTAINER_TPL) as HTMLElement
+ const listDom = containerDom.getElementsByClassName(LIST_CLASS)[0] as HTMLElement
+ showItems.forEach(item => {
+ let value = '-'
+ if (item.value !== '') {
+ if (Array.isArray(item.value)) {
+ item.value.forEach((v, i) => {
+ item.value[i] = Number.isNaN(v) || v === 'NaN' ? 'NaN' : parseFloat(v).toFixed(0)
+ })
+ value = item.value.join('-')
+ } else {
+ const tmp = item.value as string
+ value = Number.isNaN(tmp) || tmp === 'NaN' ? 'NaN' : parseFloat(tmp).toFixed(0)
+ }
+ }
+ const substituteObj = { ...item, value }
+
+ const domStr = substitute(ITEM_TPL, substituteObj)
+ const itemDom = createDom(domStr)
+ // 给 legend 形状用的
+ itemDom.style.setProperty('--bgColor', item.color)
+ listDom.appendChild(itemDom)
+ })
+ return listDom
+ }
+
+ private customConfigLegend(
+ chart: Chart,
+ options: ChoroplethOptions,
+ context: Record
+ ): ChoroplethOptions {
+ const { basicStyle, misc } = parseJson(chart.customAttr)
+ const colors = basicStyle.colors.map(item => hexColorToRGBA(item, basicStyle.alpha))
+ if (basicStyle.suspension === false && basicStyle.showZoom === undefined) {
+ return options
+ }
+ const { legend } = parseJson(chart.customStyle)
+ if (!legend.show) {
+ return options
+ }
+ const LEGEND_SHAPE_STYLE_MAP = {
+ circle: {
+ borderRadius: '50%'
+ },
+ square: {},
+ triangle: {
+ border: 'unset',
+ borderLeft: '5px solid transparent',
+ borderRight: '5px solid transparent',
+ borderBottom: '10px solid var(--bgColor)',
+ background: 'unset'
+ },
+ diamond: {
+ transform: 'rotate(45deg)'
+ }
+ }
+ const customLegend = {
+ position: 'bottomleft',
+ domStyles: {
+ 'l7plot-legend__category-value': {
+ fontSize: legend.fontSize + 'px',
+ color: legend.color,
+ 'font-family': chart.fontFamily ? chart.fontFamily : undefined
+ },
+ 'l7plot-legend__category-marker': {
+ ...LEGEND_SHAPE_STYLE_MAP[legend.icon],
+ width: legend.size + 'px',
+ height: legend.size + 'px',
+ ...(legend.icon === 'triangle'
+ ? {
+ ...LEGEND_SHAPE_STYLE_MAP[legend.icon]['triangle'],
+ borderLeft: `${legend.size / 2}px solid transparent`,
+ borderRight: `${legend.size / 2}px solid transparent`,
+ borderBottom: `${legend.size}px solid var(--bgColor)`
+ }
+ : { border: '0.01px solid #f4f4f4' }),
+ ...(legend.icon === 'diamond'
+ ? {
+ transform: 'rotate(45deg)',
+ marginBottom: `${legend.size / 4}px`
+ }
+ : {})
+ }
+ }
+ }
+ // 不是自动图例、自定义图例区间、不是下钻时
+ if (!misc.mapAutoLegend && misc.mapLegendRangeType === 'custom' && !chart.drill) {
+ // 获取图例区间数据
+ const items = []
+ // 区间数组
+ const ranges = misc.mapLegendCustomRange
+ .slice(0, -1)
+ .map((item, index) => [item, misc.mapLegendCustomRange[index + 1]])
+ ranges.forEach((range, index) => {
+ const tmpRange = [range[0], range[1]]
+ const colorIndex = index % colors.length
+ // 当区间第一个值小于最小值时,颜色取地图底色
+ const isLessThanMin = range[0] < ranges[0][0] && range[1] < ranges[0][0]
+ let rangeColor = colors[colorIndex]
+ if (isLessThanMin) {
+ rangeColor = hexColorToRGBA(basicStyle.areaBaseColor, basicStyle.alpha)
+ }
+ items.push({
+ value: tmpRange,
+ color: rangeColor
+ })
+ })
+ customLegend['customContent'] = (_: string, _items: CategoryLegendListItem[]) => {
+ if (items?.length) {
+ return this.createLegendCustomContent(items)
+ }
+ return ''
+ }
+ options.color['value'] = ({ value }) => {
+ const item = items.find(item => value >= item.value[0] && value <= item.value[1])
+ return item ? item.color : hexColorToRGBA(basicStyle.areaBaseColor, basicStyle.alpha)
+ }
+ options.color.scale.domain = [ranges[0][0], ranges[ranges.length - 1][1]]
+ } else {
+ customLegend['customContent'] = (_: string, items: CategoryLegendListItem[]) => {
+ const showItems = items?.length > 30 ? items.slice(0, 30) : items
+ if (showItems?.length) {
+ return this.createLegendCustomContent(showItems)
+ }
+ return ''
+ }
+ }
+ // 下钻时按照数据值计算图例
+ if (chart.drill) {
+ getMaxAndMinValueByData(options.source.data, 'value', 0, 0, (max, min) => {
+ options.color.scale.domain = [min, max]
+ })
+ }
+ defaultsDeep(options, { legend: customLegend })
+ return options
+ }
+
+ protected configCustomArea(
+ chart: Chart,
+ options: ChoroplethOptions,
+ context: Record
+ ): ChoroplethOptions {
+ const { drawOption, customSubArea, geoJson } = context
+ if (!drawOption.areaId.startsWith('custom_')) {
+ return options
+ }
+ const customAttr = parseJson(chart.customAttr)
+ const { label } = customAttr
+ const data = chart.data.data
+ const areaMap = data?.reduce((obj, value) => {
+ obj[value['field']] = value
+ return obj
+ }, {})
+ const geoJsonMap = geoJson.features.reduce((p, n) => {
+ if (n.properties['adcode']) {
+ p['156' + n.properties['adcode']] = n
+ }
+ return p
+ }, {})
+ customSubArea.forEach(area => {
+ const areaJsonArr = []
+ area.scopeArr?.forEach(adcode => {
+ const json = geoJsonMap[adcode]
+ json && areaJsonArr.push(json)
+ })
+ if (areaJsonArr.length) {
+ const areaJson: FeatureCollection = {
+ type: 'FeatureCollection',
+ features: areaJsonArr
+ }
+ const center = centroid(areaJson)
+ // 轮播用
+ area.centroid = [center.geometry.coordinates[0], center.geometry.coordinates[1]]
+ }
+ })
+ //处理label
+ options.label = {
+ visible: false
+ }
+ if (label.show) {
+ const labelLocation = []
+ customSubArea.forEach(area => {
+ if (area.centroid) {
+ const content = []
+ if (label.showDimension) {
+ content.push(area.name)
+ }
+ if (label.showQuota) {
+ areaMap[area.name] &&
+ content.push(valueFormatter(areaMap[area.name].value, label.quotaLabelFormatter))
+ }
+ labelLocation.push({
+ name: content.join('\n\n'),
+ x: area.centroid[0],
+ y: area.centroid[1]
+ })
+ }
+ })
+ const areaLabelLayer = new TextLayer({
+ name: 'areaLabelLayer',
+ source: {
+ data: labelLocation,
+ parser: {
+ type: 'json',
+ x: 'x',
+ y: 'y'
+ }
+ },
+ field: 'name',
+ style: {
+ fill: label.color,
+ fontSize: label.fontSize,
+ opacity: 1,
+ fontWeight: 'bold',
+ textAnchor: 'center',
+ textAllowOverlap: label.fullDisplay,
+ padding: !label.fullDisplay ? [2, 2] : undefined
+ }
+ })
+ context.layers = [areaLabelLayer]
+ }
+ // 处理tooltip
+ const subAreaMap = customSubArea.reduce((p, n) => {
+ n.scopeArr.forEach(a => {
+ p[a] = n.name
+ })
+ return p
+ }, {})
+ if (options.tooltip && options.tooltip.showComponent) {
+ options.tooltip.items = ['name', 'adcode', 'value']
+ options.tooltip.customTitle = ({ name, adcode }) => {
+ adcode = '156' + adcode
+ return subAreaMap[adcode] ?? name
+ }
+ const tooltip = customAttr.tooltip
+ const formatterMap = tooltip.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {}) as Record
+ options.tooltip.customItems = originalItem => {
+ const result = []
+ if (isEmpty(formatterMap)) {
+ return result
+ }
+ const head = originalItem.properties
+ const { adcode } = head
+ const areaName = subAreaMap['156' + adcode]
+ const valItem = areaMap[areaName]
+ if (!valItem) {
+ return result
+ }
+ const formatter = formatterMap[valItem.quotaList?.[0]?.id]
+ if (!isEmpty(formatter)) {
+ const originValue = parseFloat(valItem.value as string)
+ const value = valueFormatter(originValue, formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ ...valItem, name, value: `${value ?? ''}` })
+ }
+ valItem.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: `${value ?? ''}` })
+ }
+ })
+ return result
+ }
+ }
+ return options
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.customAttr.basicStyle.areaBaseColor = '#f4f4f4'
+ return chart
+ }
+
+ protected setupOptions(
+ chart: Chart,
+ options: ChoroplethOptions,
+ context: Record
+ ): ChoroplethOptions {
+ return flow(
+ this.configEmptyDataStrategy,
+ this.configLabel,
+ this.configStyle,
+ this.configTooltip,
+ this.configBasicStyle,
+ this.customConfigLegend,
+ this.configCustomArea
+ )(chart, options, context, this)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/symbolic-map.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/symbolic-map.ts
new file mode 100644
index 0000000..8e16a7a
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/symbolic-map.ts
@@ -0,0 +1,645 @@
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import {
+ L7ChartView,
+ L7Config,
+ L7DrawConfig,
+ L7Wrapper
+} from '@/data-visualization/chart/components/js/panel/types/impl/l7'
+import { MAP_EDITOR_PROPERTY_INNER } from '@/data-visualization/chart/components/js/panel/charts/map/common'
+import {
+ getColorFormAlphaColor,
+ hexColorToRGBA,
+ parseJson,
+ svgStrToUrl
+} from '@/data-visualization/chart/components/js/util'
+import { deepCopy } from '@/data-visualization/utils/utils'
+import { GaodeMap } from '@antv/l7-maps'
+import { Scene } from '@antv/l7-scene'
+import { PointLayer } from '@antv/l7-layers'
+import { LayerPopup, Popup } from '@antv/l7'
+import { mapRendered, mapRendering } from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { configCarouselTooltip } from '@/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel'
+import { DEFAULT_BASIC_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+import { filter } from 'lodash-es'
+const { t } = useI18n()
+
+/**
+ * 符号地图
+ */
+export class SymbolicMap extends L7ChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'symbolic-style-selector',
+ 'title-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'threshold'
+ ]
+ propertyInner: EditorPropertyInner = {
+ ...MAP_EDITOR_PROPERTY_INNER,
+ 'basic-style-selector': [
+ 'colors',
+ 'alpha',
+ 'mapBaseStyle',
+ 'zoom',
+ 'showLabel',
+ 'autoFit',
+ 'mapCenter',
+ 'zoomLevel'
+ ],
+ 'symbolic-style-selector': ['symbolicMapStyle'],
+ 'label-selector': ['color', 'fontSize', 'showFields', 'customContent'],
+ 'tooltip-selector': [
+ 'color',
+ 'fontSize',
+ 'showFields',
+ 'customContent',
+ 'show',
+ 'backgroundColor',
+ 'carousel'
+ ],
+ threshold: ['lineThreshold']
+ }
+ axis: AxisType[] = ['xAxis', 'xAxisExt', 'extBubble', 'filter', 'extLabel', 'extTooltip']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.symbolic_map_coordinates')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 2
+ },
+ xAxisExt: {
+ name: `${t('chart.color')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1,
+ allowEmpty: true
+ },
+ extBubble: {
+ name: `${t('chart.bubble_size')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1,
+ tooltip: t('chart.symbolic_map_bubble_size_tip'),
+ allowEmpty: true
+ }
+ }
+ constructor() {
+ super('symbolic-map', [])
+ }
+
+ async drawChart(drawOption: L7DrawConfig) {
+ const { chart, container, action } = drawOption
+ const containerDom = document.getElementById(container)
+ const rect = containerDom?.getBoundingClientRect()
+ if (rect?.height <= 0) {
+ return new L7Wrapper(drawOption.chartObj?.getScene(), [])
+ }
+ const xAxis = deepCopy(chart.xAxis)
+ let basicStyle
+ let miscStyle
+ if (chart.customAttr) {
+ basicStyle = parseJson(chart.customAttr).basicStyle
+ miscStyle = parseJson(chart.customAttr).misc
+ }
+
+ let mapStyle = basicStyle.mapStyleUrl
+ if (basicStyle.mapStyle !== 'custom') {
+ mapStyle = `amap://styles/${basicStyle.mapStyle ? basicStyle.mapStyle : 'normal'}`
+ }
+ const mapKey = await this.getMapKey()
+ let center: [number, number] = [
+ DEFAULT_BASIC_STYLE.mapCenter.longitude,
+ DEFAULT_BASIC_STYLE.mapCenter.latitude
+ ]
+ if (basicStyle.autoFit === false) {
+ center = [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude]
+ }
+ // 联动时,聚焦到数据点,多个取第一个
+ if (
+ chart.chartExtRequest?.linkageFilters?.length &&
+ xAxis?.length === 2 &&
+ chart.data?.tableRow.length
+ ) {
+ // 经度
+ const lng = chart.data?.tableRow?.[0][chart.xAxis[0].dataeaseName]
+ // 纬度
+ const lat = chart.data?.tableRow?.[0][chart.xAxis[1].dataeaseName]
+ center = [lng, lat]
+ }
+ const chartObj = drawOption.chartObj as unknown as L7Wrapper
+ let scene = chartObj?.getScene()
+ if (!scene) {
+ scene = new Scene({
+ id: container,
+ logoVisible: false,
+ map: new GaodeMap({
+ token: mapKey?.key ?? undefined,
+ style: mapStyle,
+ pitch: miscStyle.mapPitch,
+ center,
+ zoom: basicStyle.autoFit === false ? basicStyle.zoomLevel : undefined,
+ showLabel: !(basicStyle.showLabel === false),
+ WebGLParams: {
+ preserveDrawingBuffer: true
+ }
+ })
+ })
+ } else {
+ if (scene.getLayers()?.length) {
+ await scene.removeAllLayer()
+ scene.setPitch(miscStyle.mapPitch)
+ scene.setMapStyle(mapStyle)
+ scene.map.showLabel = !(basicStyle.showLabel === false)
+ }
+ if (basicStyle.autoFit === false) {
+ scene.setZoomAndCenter(basicStyle.zoomLevel, center)
+ }
+ }
+ mapRendering(container)
+ scene.once('loaded', () => {
+ mapRendered(container)
+ })
+ this.configZoomButton(chart, scene)
+ if (xAxis?.length < 2) {
+ return new L7Wrapper(scene, undefined)
+ }
+ const configList: L7Config[] = []
+ const symbolicLayer = await this.buildSymbolicLayer(chart, scene)
+ configList.push(symbolicLayer)
+ const tooltipLayer = this.buildTooltip(chart, container, symbolicLayer, scene)
+ if (tooltipLayer) {
+ scene.addPopup(tooltipLayer)
+ }
+ this.buildLabel(chart, configList)
+ symbolicLayer.on('inited', () => {
+ chart.container = container
+ configCarouselTooltip(chart, symbolicLayer, symbolicLayer.sourceOption.data, scene)
+ })
+ symbolicLayer.on('click', ev => {
+ const data = ev.feature
+ const dimensionList = []
+ const quotaList = []
+ chart.data.fields.forEach((item, index) => {
+ Object.keys(data).forEach(key => {
+ if (key.startsWith('f_') && item.dataeaseName === key) {
+ if (index === 0) {
+ dimensionList.push({
+ id: item.id,
+ dataeaseName: item.dataeaseName,
+ value: data[key]
+ })
+ } else {
+ quotaList.push({
+ id: item.id,
+ dataeaseName: item.dataeaseName,
+ value: data[key]
+ })
+ }
+ }
+ })
+ })
+ action({
+ x: ev.x,
+ y: ev.y,
+ data: {
+ data: {
+ ...data,
+ value: quotaList[0].value,
+ name: dimensionList[0].id,
+ dimensionList: dimensionList,
+ quotaList: quotaList
+ }
+ }
+ })
+ })
+
+ return new L7Wrapper(scene, configList)
+ }
+
+ /**
+ * 构建符号图层
+ * @param chart
+ */
+ buildSymbolicLayer = async (chart, scene: Scene) => {
+ const { basicStyle } = parseJson(chart.customAttr) as ChartAttr
+ const xAxis = deepCopy(chart.xAxis)
+ const xAxisExt = deepCopy(chart.xAxisExt)
+ const extBubble = deepCopy(chart.extBubble)
+ const {
+ mapSymbolOpacity,
+ mapSymbolSize,
+ mapSymbol,
+ mapSymbolStrokeWidth,
+ colors,
+ alpha,
+ mapSymbolSizeMin,
+ mapSymbolSizeMax
+ } = deepCopy(basicStyle)
+ const colorsWithAlpha = colors.map(color => hexColorToRGBA(color, alpha))
+ let colorIndex = 0
+ // 存储已分配的颜色
+ const colorAssignments = new Map()
+ const sizeKey = extBubble.length > 0 ? extBubble[0].dataeaseName : ''
+
+ //条件颜色
+ const { threshold } = parseJson(chart.senior)
+ let conditions = []
+ if (threshold.enable) {
+ conditions = threshold.lineThreshold ?? []
+ }
+ const extBubbleIds = chart.extBubble.map(i => i.id)
+ conditions = filter(conditions, c => extBubbleIds.includes(c.fieldId))
+
+ const baseColor = colorsWithAlpha[0]
+ const baseColorList = []
+
+ const data = chart.data?.tableRow
+ ? chart.data.tableRow.map((item, index) => {
+ item['_index'] = '_index' + index
+ // 颜色标识
+ const identifier = item[xAxisExt[0]?.dataeaseName]
+ // 检查该标识是否已有颜色分配,如果没有则分配
+ let color = colorAssignments.get(identifier)
+ if (!color) {
+ color = colorsWithAlpha[colorIndex++ % colorsWithAlpha.length]
+ // 记录分配的颜色
+ colorAssignments.set(identifier, color)
+ }
+
+ baseColorList[index] = color
+
+ if (conditions.length > 0) {
+ for (let i = 0; i < conditions.length; i++) {
+ const c = conditions[i]
+ const value = item[c.field.dataeaseName]
+ for (const t of c.conditions) {
+ const v = t.value
+
+ //保存一下颜色到map
+ const _color = getColorFormAlphaColor(t.color)
+
+ if (t.term === 'between') {
+ const start = parseFloat(t.min)
+ const end = parseFloat(t.max)
+ if (start <= value && value <= end) {
+ color = hexColorToRGBA(_color, alpha)
+ baseColorList[index] = color
+ }
+ } else if ('lt' === t.term) {
+ if (value < v) {
+ color = hexColorToRGBA(_color, alpha)
+ baseColorList[index] = color
+ }
+ } else if ('le' === t.term) {
+ if (value <= v) {
+ color = hexColorToRGBA(_color, alpha)
+ baseColorList[index] = color
+ }
+ } else if ('gt' === t.term) {
+ if (value > v) {
+ color = hexColorToRGBA(_color, alpha)
+ baseColorList[index] = color
+ }
+ } else if ('ge' === t.term) {
+ if (value >= v) {
+ color = hexColorToRGBA(_color, alpha)
+ baseColorList[index] = color
+ }
+ } else if ('eq' === t.term) {
+ if (value === v) {
+ color = hexColorToRGBA(_color, alpha)
+ baseColorList[index] = color
+ }
+ } else if ('not_eq' === t.term) {
+ if (value !== v) {
+ color = hexColorToRGBA(_color, alpha)
+ baseColorList[index] = color
+ }
+ }
+ }
+ }
+ }
+
+ return {
+ ...item,
+ color,
+ size: parseInt(item[sizeKey]) ?? mapSymbolSize,
+ name: identifier
+ }
+ })
+ : []
+ const pointLayer = new PointLayer({ autoFit: !(basicStyle.autoFit === false) })
+ .source(data, {
+ parser: {
+ type: 'json',
+ x: xAxis[0].dataeaseName,
+ y: xAxis[1].dataeaseName
+ }
+ })
+ .active(true)
+ if (xAxisExt[0]?.dataeaseName) {
+ if (basicStyle.mapSymbol === 'custom' && basicStyle.customIcon) {
+ // 图片无法改色
+ if (basicStyle.customIcon.startsWith('data')) {
+ scene.removeImage('customIcon')
+ await scene.addImage('customIcon', basicStyle.customIcon)
+ pointLayer.shape('customIcon')
+ } else {
+ const parser = new DOMParser()
+ for (let index = 0; index < Math.min(baseColorList.length, colorIndex + 1); index++) {
+ const color = baseColorList[index]
+ const fillRegex = /(fill="[^"]*")/g
+ const svgStr = basicStyle.customIcon.replace(fillRegex, '')
+ const doc = parser.parseFromString(svgStr, 'image/svg+xml')
+ const svgEle = doc.documentElement
+ svgEle.setAttribute('fill', color)
+ scene.removeImage(`icon-${color}`)
+ await scene.addImage(`icon-${color}`, svgStrToUrl(svgEle.outerHTML))
+ }
+ pointLayer.shape('color', c => {
+ return `icon-${c}`
+ })
+ }
+ } else {
+ pointLayer.shape(mapSymbol).color('_index', baseColorList)
+ pointLayer.style({
+ stroke: {
+ field: 'color'
+ },
+ strokeWidth: mapSymbolStrokeWidth,
+ opacity: mapSymbolOpacity / 10
+ })
+ }
+ } else {
+ if (basicStyle.mapSymbol === 'custom' && basicStyle.customIcon) {
+ scene.removeImage('customIcon')
+ if (basicStyle.customIcon.startsWith('data')) {
+ await scene.addImage('customIcon', basicStyle.customIcon)
+ pointLayer.shape('customIcon')
+ } else {
+ const parser = new DOMParser()
+ const color = baseColor
+ const fillRegex = /(fill="[^"]*")/g
+ const svgStr = basicStyle.customIcon.replace(fillRegex, '')
+ const doc = parser.parseFromString(svgStr, 'image/svg+xml')
+ const svgEle = doc.documentElement
+ svgEle.setAttribute('fill', color)
+ await scene.addImage(`customIcon`, svgStrToUrl(svgEle.outerHTML))
+ pointLayer.shape('customIcon')
+ }
+ } else {
+ pointLayer
+ .shape(mapSymbol)
+ .color('_index', baseColorList)
+ .style({
+ stroke: {
+ field: 'color'
+ },
+ strokeWidth: mapSymbolStrokeWidth,
+ opacity: mapSymbolOpacity / 10
+ })
+ }
+ }
+ if (sizeKey) {
+ pointLayer.size('size', [mapSymbolSizeMin, mapSymbolSizeMax])
+ } else {
+ pointLayer.size(mapSymbolSize)
+ }
+ return pointLayer
+ }
+
+ /**
+ * 合并详情到 map
+ * @param details
+ * @returns {Map}
+ */
+ mergeDetailsToMap = details => {
+ const resultMap = new Map()
+ details.forEach(item => {
+ Object.entries(item).forEach(([key, value]) => {
+ if (resultMap.has(key)) {
+ const existingValue = resultMap.get(key)
+ if (existingValue !== value) {
+ resultMap.set(key, `${existingValue}, ${value}`)
+ }
+ } else {
+ resultMap.set(key, value)
+ }
+ })
+ })
+ return resultMap
+ }
+
+ /**
+ * 清除 popup
+ * @param container
+ */
+ clearPopup = container => {
+ const containerElement = document.getElementById(container)
+ containerElement?.querySelectorAll('.l7-popup').forEach((element: Element) => element.remove())
+ }
+
+ /**
+ * 构建 tooltip
+ * @param chart
+ * @param pointLayer
+ */
+ buildTooltip = (chart, container, pointLayer, scene) => {
+ const customAttr = chart.customAttr ? parseJson(chart.customAttr) : null
+ this.clearPopup(container)
+ if (customAttr?.tooltip?.show) {
+ const { tooltip } = deepCopy(customAttr)
+ let showFields = tooltip.showFields || []
+ if (!tooltip.showFields || tooltip.showFields.length === 0) {
+ showFields = [
+ ...chart.xAxisExt.map(i => `${i.dataeaseName}@${i.name}`),
+ ...chart.xAxis.map(i => `${i.dataeaseName}@${i.name}`)
+ ]
+ }
+ // 修改背景色
+ const styleId = 'tooltip-' + container
+ const styleElement = document.getElementById(styleId)
+ if (styleElement) {
+ styleElement.remove()
+ styleElement.parentNode?.removeChild(styleElement)
+ }
+ const style = document.createElement('style')
+ style.id = styleId
+ style.innerHTML = `
+ #${container} .l7-popup-content {
+ background-color: ${tooltip.backgroundColor} !important;
+ padding: 6px 10px 6px;
+ line-height: 1.6;
+ border-top-left-radius: 3px;
+ }
+ #${container} .l7-popup-tip {
+ border-top-color: ${tooltip.backgroundColor} !important;
+ }
+ `
+ document.head.appendChild(style)
+ const htmlPrefix = ``
+ const htmlSuffix = '
'
+ const containerElement = document.getElementById(container)
+ if (containerElement) {
+ containerElement.addEventListener('mousemove', event => {
+ const rect = containerElement.getBoundingClientRect()
+ const mouseX = event.clientX - rect.left
+ const mouseY = event.clientY - rect.top
+ const tooltipElement = containerElement.getElementsByClassName('l7-popup')
+ for (let i = 0; i < tooltipElement?.length; i++) {
+ const element = tooltipElement[i] as HTMLElement
+ element.firstElementChild.style.display = 'none'
+ element.style.transform = 'translate(15px, 12px)'
+ const isNearRightEdge =
+ containerElement.clientWidth - mouseX <= element.clientWidth + 10
+ const isNearBottomEdge = containerElement.clientHeight - mouseY <= element.clientHeight
+ let transform = ''
+ if (isNearRightEdge) {
+ transform += 'translateX(-120%) translateY(15%) '
+ }
+ if (isNearBottomEdge) {
+ transform += 'translateX(15%) translateY(-80%) '
+ }
+ if (transform) {
+ element.style.transform = transform.trim()
+ }
+ }
+ })
+ }
+ pointLayer.on('touchend', e => {
+ if (e.lngLat) {
+ const fieldData = {
+ ...e.feature,
+ ...Object.fromEntries(this.mergeDetailsToMap(e.feature.details ?? []))
+ }
+ const content = this.buildTooltipContent(tooltip, fieldData, showFields)
+ const popup = new Popup({
+ lngLat: e.lngLat,
+ title: '',
+ closeButton: false,
+ closeOnClick: true,
+ html: `${htmlPrefix}${content}${htmlSuffix}`
+ })
+ scene.addPopup(popup)
+ }
+ })
+ return new LayerPopup({
+ anchor: 'top-left',
+ className: 'l7-popup-' + container,
+ items: [
+ {
+ layer: pointLayer,
+ customContent: item => {
+ const fieldData = {
+ ...item,
+ ...Object.fromEntries(this.mergeDetailsToMap(item.details))
+ }
+ const content = this.buildTooltipContent(tooltip, fieldData, showFields)
+ return `${htmlPrefix}${content}${htmlSuffix}`
+ }
+ }
+ ],
+ trigger: 'hover'
+ })
+ }
+ return undefined
+ }
+
+ /**
+ * 构建 tooltip 内容
+ * @param tooltip
+ * @param fieldData
+ * @param showFields
+ * @returns {string}
+ */
+ buildTooltipContent = (tooltip, fieldData, showFields) => {
+ let content = ``
+ if (tooltip.customContent) {
+ content = tooltip.customContent
+ showFields.forEach(field => {
+ content = content.replace(`\${${field.split('@')[1]}}`, fieldData[field.split('@')[0]])
+ })
+ } else {
+ showFields.forEach(field => {
+ content += `${field.split('@')[1]}: ${
+ fieldData[field.split('@')[0]]
+ }
`
+ })
+ }
+ return content.replace(/\n/g, '
')
+ }
+
+ /**
+ * 构建 label
+ * @param chart
+ * @param configList
+ */
+ buildLabel = (chart, configList) => {
+ const xAxis = deepCopy(chart.xAxis)
+
+ const customAttr = chart.customAttr ? parseJson(chart.customAttr) : null
+ if (customAttr?.label?.show) {
+ const { label } = customAttr
+ const data = chart.data?.tableRow || []
+ let showFields = label.showFields || []
+ if (!label.showFields || label.showFields.length === 0) {
+ showFields = [
+ ...chart.xAxisExt.map(i => `${i.dataeaseName}@${i.name}`),
+ ...chart.xAxis.map(i => `${i.dataeaseName}@${i.name}`)
+ ]
+ }
+ data.forEach(item => {
+ const fieldData = {
+ ...item,
+ ...Object.fromEntries(this.mergeDetailsToMap(item.details))
+ }
+ let content = label.customContent || ''
+
+ if (content) {
+ showFields.forEach(field => {
+ const [fieldKey, fieldName] = field.split('@')
+ content = content.replace(`\${${fieldName}}`, fieldData[fieldKey])
+ })
+ } else {
+ content = showFields.map(field => fieldData[field.split('@')[0]]).join(',')
+ }
+
+ content = content.replace(/\n/g, '')
+ item.textLayerContent = content
+ })
+
+ configList.push(
+ new PointLayer()
+ .source(data, {
+ parser: {
+ type: 'json',
+ x: xAxis[0].dataeaseName,
+ y: xAxis[1].dataeaseName
+ }
+ })
+ .shape('textLayerContent', 'text')
+ .color(label.color)
+ .size(label.fontSize)
+ .style({
+ textAllowOverlap: label.fullDisplay,
+ textAnchor: 'center',
+ textOffset: [0, 0],
+ fontFamily: chart.fontFamily ? chart.fontFamily : undefined
+ })
+ )
+ }
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.customAttr.label = {
+ ...chart.customAttr.label,
+ show: false
+ }
+ chart.customAttr.basicStyle = {
+ ...chart.customAttr.basicStyle,
+ mapSymbolOpacity: 5,
+ mapStyle: 'normal'
+ }
+ return chart
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel.ts
new file mode 100644
index 0000000..31616a8
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/map/tooltip-carousel.ts
@@ -0,0 +1,657 @@
+import { Popup } from '@antv/l7'
+import { Plot } from '@antv/l7plot/dist/lib/core/plot'
+import isEmpty from 'lodash-es/isEmpty'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { parseJson } from '@/data-visualization/chart/components/js/util'
+import { Scene } from '@antv/l7-scene'
+import { deepCopy } from '@/data-visualization/utils/utils'
+
+export const configCarouselTooltip = (chart, view, data, scene, customSubArea?, drawOption?) => {
+ if (['bubble-map', 'map'].includes(chart.type)) {
+ data = view.source.data.dataArray
+ ?.filter(i => i.dimensionList?.length > 0)
+ .reduce((acc, current) => {
+ const existingItem = acc.find(obj => {
+ if (drawOption?.areaId?.startsWith('custom_')) {
+ return obj.areaName === current.areaName
+ } else {
+ return obj.name === current.name || (obj.adcode && obj.adcode === current.adcode)
+ }
+ })
+ if (!existingItem) {
+ acc.push(current)
+ }
+ return acc
+ }, [])
+ }
+ if (carouselManagerInstances[chart.container]) {
+ const instances = carouselManagerInstances[chart.container]
+ instances.update(scene, chart, view, data, customSubArea, drawOption)
+ } else {
+ new CarouselManager(scene, chart, view, data, customSubArea, drawOption)
+ }
+}
+export const carouselManagerInstances: { [key: string]: CarouselManager } = {}
+
+/**
+ * 轮播管理类
+ */
+export class CarouselManager {
+ /**
+ * 停留时长定时器
+ * @private
+ */
+ private popupTimeoutId: number | null = null
+ /**
+ * 轮播间隔定时器
+ * @private
+ */
+ private popupIntervalId: number | null = null
+ /**
+ * 是否暂停轮播
+ * @private
+ */
+ private isPaused = false
+ /**
+ * 当前显示的数据索引
+ * @private
+ */
+ private currentIndex = 0
+ /**
+ * 地图实例,气泡地图用
+ * @private
+ */
+ private scene: Scene
+ private chart: Chart
+ /**
+ * 轮播弹窗的位置数据
+ * @private
+ */
+ private view: Plot
+ private data: any[]
+ /**
+ * 停留时长
+ * @private
+ */
+ private stayTime: number
+ /**
+ * 轮播间隔
+ * @private
+ */
+ private intervalTime: number
+ /**
+ * 轮播弹窗
+ * @private
+ */
+ private popup: Popup
+
+ /**
+ * 自定义区域列表
+ * @private
+ */
+ private customSubArea: CustomGeoSubArea[]
+
+ /**
+ * 渲染参数
+ * @private
+ */
+ private drawOption: L7PlotDrawOptions
+
+ // 保存事件监听函数的引用
+ private onMouseEnterHandler: () => void
+ private onMouseLeaveHandler: () => void
+ private onVisibilityChangeHandler: () => void
+
+ constructor(scene, chart, view, data: any[], customSubArea, drawOption?) {
+ // 绑定事件处理函数
+ this.onMouseEnterHandler = this.pauseCarouselPopups.bind(this)
+ this.onMouseLeaveHandler = this.resumeCarouselPopups.bind(this)
+ this.onVisibilityChangeHandler = this.handleVisibilityChange.bind(this)
+ this.clearExistingTimers = this.clearExistingTimers.bind(this)
+ this.init(scene, chart, view, data, customSubArea, drawOption)
+ }
+
+ /**
+ * 更新轮播弹窗对象内容
+ * @param scene
+ * @param chart
+ * @param view
+ * @param data
+ * @param customSubArea
+ */
+ public update(scene, chart, view, data: any[], customSubArea, drawOption?) {
+ this.init(scene, chart, view, data, customSubArea, drawOption)
+ }
+
+ /**
+ * 初始化轮播弹窗
+ * @param scene
+ * @param chart
+ * @param view
+ * @param data
+ * @private
+ */
+ private init(scene, chart, view, data: any[], customSubArea, drawOption?) {
+ this.view = view
+ this.chart = chart
+ this.scene = scene
+ this.data = data
+ this.popup = null
+ this.currentIndex = 0
+ this.customSubArea = customSubArea
+ this.drawOption = drawOption
+ this.clearPreviousInstance(this.chart.container)
+ if (
+ this.chart.customAttr?.tooltip?.show &&
+ this.chart.customAttr?.tooltip?.carousel?.enable &&
+ this.data.length > 0
+ ) {
+ this.popup = new Popup({ closeButton: false, maxWidth: 600 })
+ const carousel = this.chart.customAttr?.tooltip?.carousel
+ this.stayTime = carousel.stayTime * 1000
+ this.intervalTime = carousel.intervalTime * 1000
+ this.startCarouselPopups()
+ const divElement = document.getElementById(this.chart.container)
+ divElement.addEventListener('mouseenter', this.pauseCarouselPopups)
+ divElement.addEventListener('mouseleave', this.resumeCarouselPopups)
+ // 移动端符号地图不支持mouseenter和mouseleave事件,这里特殊处理一下
+ if (this.chart.type === 'symbolic-map') {
+ // 监听符号触摸事件, 暂停轮播
+ scene?.getLayers()?.[0]?.addListener('touchend', () => {
+ this.pauseCarouselPopups()
+ })
+ // 地图空白区域触摸事件, 启动轮播
+ scene?.getMapCanvasContainer()?.addEventListener('touchend', () => {
+ this.resumeCarouselPopups()
+ })
+ }
+ // 监听页面可见性变化
+ document.addEventListener('visibilitychange', this.handleVisibilityChange)
+ carouselManagerInstances[this.chart.container] = this
+ }
+ }
+
+ private handleVisibilityChange = (): void => {
+ if (document.hidden) {
+ this.clearPreviousInstance(this.chart.container)
+ } else {
+ this.startCarouselPopups()
+ }
+ }
+
+ /**
+ * 清除之前的实例数据
+ * @param containerId
+ * @private
+ */
+ private clearPreviousInstance(containerId: string): void {
+ if (carouselManagerInstances[containerId]) {
+ const instance = carouselManagerInstances[containerId]
+ this.clearExistingTimers()
+ instance.popup?.remove()
+ instance.removeStyle()
+ }
+ }
+
+ /**
+ * 开始轮播
+ * @private
+ */
+ private startCarouselPopups(): void {
+ this.clearExistingTimers()
+ this.carouselPopups()
+ }
+
+ /**
+ * 鼠标移入暂停轮播
+ */
+ private pauseCarouselPopups = (): void => {
+ if (this.popup) {
+ this.popup?.remove()
+ }
+ this.removeStyle()
+ this.isPaused = true
+ this.clearExistingTimers()
+ }
+
+ /**
+ * 鼠标移出开始轮播
+ */
+ private resumeCarouselPopups = (): void => {
+ if (this.isPaused) {
+ this.isPaused = false
+ this.startCarouselPopups()
+ }
+ }
+
+ /**
+ * 管理轮播弹窗的显示
+ *
+ * 此方法用于处理轮播弹窗的显示逻辑它会根据当前的索引显示对应的弹窗,
+ * 并在一定时间后自动移除当前弹窗并显示下一个弹窗
+ *
+ * @private
+ */
+ private carouselPopups(): void {
+ const showPopup = (index: number): void => {
+ this.removeStyle()
+ const containerElement = document.getElementById(this.chart.container)
+ if (containerElement) {
+ if (this.chart.type === 'symbolic-map') {
+ // 轮播进行时,隐藏隐藏鼠标悬浮的tooltip
+ const mouseTooltip = containerElement.getElementsByClassName(
+ 'l7-popup-' + this.chart.container
+ )
+ for (const tooltip of Array.from(mouseTooltip)) {
+ const tooltipElement = tooltip as HTMLElement
+ tooltipElement.classList.add('l7-popup-hide')
+ }
+ this.createSymbolicMapPopup(index)
+ } else {
+ if (this.chart.type === 'map') {
+ // 轮播进行时,隐藏隐藏鼠标悬浮的tooltip
+ const mouseTooltip = containerElement.getElementsByClassName('l7plot-tooltip-container')
+ for (const tooltip of Array.from(mouseTooltip)) {
+ const tooltipElement = tooltip as HTMLElement
+ tooltipElement.style.display = 'none'
+ }
+ }
+ this.createPopup(index)
+ }
+ this.clearExistingTimers()
+ this.popupTimeoutId = window.setTimeout(() => {
+ this.currentIndex++
+ this.popup?.remove()
+ this.cancelHighlightLayer(index)
+ if (this.currentIndex >= this.data.length) {
+ this.currentIndex = 0
+ }
+ this.popupIntervalId = window.setTimeout(() => {
+ showPopup(this.currentIndex)
+ }, this.intervalTime)
+ }, this.stayTime)
+ } else {
+ this.clearExistingTimers()
+ }
+ }
+
+ showPopup(this.currentIndex)
+ }
+
+ /**
+ * 清除定时器
+ * @private
+ */
+ private readonly clearExistingTimers = (): void => {
+ if (this.popupTimeoutId !== null) {
+ clearTimeout(this.popupTimeoutId)
+ this.popupTimeoutId = 0
+ }
+ if (this.popupIntervalId !== null) {
+ clearInterval(this.popupIntervalId)
+ this.popupIntervalId = 0
+ }
+ }
+
+ /**
+ * 移除样式
+ * 每次创建弹窗前移除之前的样式
+ * @private
+ */
+ private removeStyle(): void {
+ const styleToRemove = document.getElementById('style-' + this.chart.container)
+ if (styleToRemove) {
+ styleToRemove.remove()
+ styleToRemove.parentNode?.removeChild(styleToRemove)
+ }
+ }
+
+ /**
+ * 创建弹窗信息
+ * @param index
+ * @private
+ */
+ private createPopup(index: number): void {
+ const tooltipStyle = this.view.tooltip.options.domStyles
+ const tooltipBackgroundColor = tooltipStyle['l7plot-tooltip']['background-color']
+ const tooltipFontSize = tooltipStyle['l7plot-tooltip']['font-size']
+ const style = document.createElement('style')
+ style.id = 'style-' + this.chart.container
+ style.innerHTML = `
+ #${this.chart.container} .l7-popup-content {
+ background-color: ${tooltipBackgroundColor} !important;
+ font-size: ${tooltipFontSize};
+ padding: 10px 10px 6px;
+ line-height: 1.6;
+ }
+ #${this.chart.container} .l7-popup-tip {
+ border-top-color: ${tooltipBackgroundColor} !important;
+ }
+ `
+ document.head.appendChild(style)
+
+ const popupData = this.getPopupData(index)
+ if (popupData.data) {
+ let tooltipItem = ''
+ this.getTooltipItems(popupData.data).forEach(fieldData => {
+ tooltipItem += `
+
+ ${fieldData.name}
+ ${fieldData.value}
+ `
+ })
+
+ const html = `
+
+
${popupData.data.name}
+
+
+ `
+
+ this.popup.setLngLat({ lng: popupData.centroid[0], lat: popupData.centroid[1] })
+ this.popup.setHTML(html)
+ this.popup.closeButton = false
+ this.view.addLayer(this.popup)
+ // 地图层高亮
+ this.view.scene
+ .getLayers()
+ ?.find(i => i.name === 'highlightLayer')
+ ?.setData(this.getActiveData(index))
+ if (this.chart.type === 'bubble-map') {
+ // 气泡地图高亮
+ const { _id } = this.view.scene
+ .getLayers()
+ ?.find(i => i.name === 'bubbleLayer')
+ ?.layerSource.data.dataArray.find(i => i.name === this.data[index].name)
+ this.view.scene
+ .getLayers()
+ ?.find(i => i.name === 'bubbleLayer' && i.coordCenter)
+ ?.setActive(_id, { color: 'rgba(30,90,255,1)' })
+ }
+ }
+ }
+
+ private getActiveData(index): any {
+ if (this.drawOption?.areaId?.startsWith('custom_')) {
+ const result = {
+ type: 'FeatureCollection',
+ features: []
+ }
+ const area = this.customSubArea.find(a => a.name === this.data[index].areaName)
+ const areaMap = this.view.currentDistrictData.features.reduce((p, n) => {
+ p['156' + n.properties.adcode] = n
+ return p
+ }, {})
+ area?.scopeArr?.forEach(s => {
+ if (areaMap[s]) {
+ result.features.push(areaMap[s])
+ }
+ })
+ return result
+ }
+ return {
+ type: 'FeatureCollection',
+ features: [
+ this.view.currentDistrictData.features.find(
+ i => i.properties.name === this.data[index].name
+ )
+ ]
+ }
+ }
+
+ /**
+ * 获取弹窗信息,包括原始数据及位置信息
+ * @param index
+ * @private
+ */
+ private getPopupData(index: number): any {
+ if (this.drawOption?.areaId?.startsWith('custom_')) {
+ const data = this.data[index]
+ const area = this.customSubArea?.find(a => a.name === data.areaName)
+ data.name = data.areaName
+ return {
+ data,
+ centroid: area.centroid
+ }
+ } else {
+ return {
+ data: this.data[index],
+ centroid: this.view.currentDistrictData.features.find(
+ i => i.properties.name === this.data[index].name
+ )?.properties.centroid
+ }
+ }
+ }
+
+ /**
+ * 将对象转换为 CSS 属性
+ * @param obj
+ * @private
+ */
+ private objectToSemicolonSeparated(obj: any): string {
+ let result = ''
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ result += `${this.convertToSnakeCase(key)}:${obj[key]};`
+ }
+ }
+ return result
+ }
+
+ private cancelHighlightLayer(index?: number): void {
+ this.view.scene
+ ?.getLayers()
+ ?.find(i => i.name === 'highlightLayer')
+ ?.setData({ type: 'FeatureCollection', features: [] })
+ if (this.chart.type === 'bubble-map') {
+ const { _id } = this.view.scene
+ ?.getLayers()
+ ?.find(i => i.name === 'bubbleLayer')
+ ?.layerSource.data.dataArray.find(i => i.name === this.data[index].name)
+ this.view.scene
+ .getLayers()
+ ?.find(i => i.name === 'bubbleLayer' && i.coordCenter)
+ ?.setActive(_id, {
+ color: this.view.scene
+ .getLayers()
+ .find(i => i.name === 'bubbleLayer')
+ .styleAttributeService.getLayerStyleAttribute('color').scale.field
+ })
+ }
+ if (this.chart.type === 'symbolic-map') {
+ const lngField = this.chart.xAxis[0].dataeaseName
+ const latField = this.chart.xAxis[1].dataeaseName
+ const { _id } = this.scene
+ ?.getLayers()
+ ?.find(i => i.type === 'PointLayer')
+ ?.layerSource.data.dataArray.find(i => {
+ const targetLng = this.data[index][lngField]
+ const targetLat = this.data[index][latField]
+ return i[lngField] === targetLng && i[latField] === targetLat
+ })
+ this.scene
+ .getLayers()
+ ?.find(i => i.type === 'PointLayer' && i.coordCenter)
+ ?.setActive(_id, {
+ color: this.scene
+ .getLayers()
+ .find(i => i.type === 'PointLayer')
+ .styleAttributeService.getLayerStyleAttribute('color').scale.field
+ })
+ }
+ }
+
+ /**
+ * 将驼峰式命名转换为蛇形命名
+ * @param str
+ * @private
+ */
+ private convertToSnakeCase(str: string): string {
+ return str.replace(/([A-Z])/g, match => '-' + match.toLowerCase())
+ }
+
+ /**
+ * 获取弹窗字段信息
+ * 与tooltip要显示的内容一致
+ * @param data
+ * @private
+ */
+ private getTooltipItems(data) {
+ const result = []
+ const customAttr = parseJson(this.chart.customAttr)
+ const tooltip = customAttr.tooltip
+ const formatterMap = tooltip.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {}) as Record
+ if (isEmpty(formatterMap)) {
+ return result
+ }
+ const head = data
+ const formatter = formatterMap[head.quotaList?.[0]?.id]
+ if (!isEmpty(formatter)) {
+ const originValue = parseFloat(head.value as string)
+ const value = valueFormatter(originValue, formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ ...head, name, value: `${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: `${value ?? ''}` })
+ }
+ })
+ return result
+ }
+
+ /**
+ * 符号地图特殊处理,tooltip的配置可自定义显示内容
+ * @param index
+ * @private
+ */
+ private createSymbolicMapPopup(index): void {
+ const buildTooltip = () => {
+ const customAttr = this.chart.customAttr ? parseJson(this.chart.customAttr) : null
+ if (customAttr?.tooltip?.show) {
+ if (!this.popup) {
+ return undefined
+ }
+ const { tooltip } = deepCopy(customAttr)
+ let showFields = tooltip.showFields || []
+ if (!tooltip.showFields || tooltip.showFields.length === 0) {
+ showFields = [
+ ...this.chart.xAxisExt.map(i => `${i.dataeaseName}@${i.name}`),
+ ...this.chart.xAxis.map(i => `${i.dataeaseName}@${i.name}`)
+ ]
+ }
+ const style = document.createElement('style')
+ style.id = 'style-' + this.chart.container
+ style.innerHTML = `
+ #${this.chart.container} .l7-popup-content {
+ background-color: ${tooltip.backgroundColor} !important;
+ padding: 6px 10px 6px;
+ line-height: 1.6;
+ }
+ #${this.chart.container} .l7-popup-tip {
+ border-top-color: ${tooltip.backgroundColor} !important;
+ }
+ `
+ document.head.appendChild(style)
+ const lngField = this.chart.xAxis[0].dataeaseName
+ const latField = this.chart.xAxis[1].dataeaseName
+ const htmlPrefix = ``
+ const htmlSuffix = '
'
+ const data = this.view.sourceOption.data[index]
+ if (data && data.details?.length) {
+ const fieldData = {
+ ...data,
+ ...Object.fromEntries(mergeDetailsToMap(data.details))
+ }
+ const content = buildTooltipContent(tooltip, fieldData, showFields)
+ const html = `${htmlPrefix}${content}${htmlSuffix}`
+ this.popup.setLngLat({
+ lng: data[lngField],
+ lat: data[latField]
+ })
+ this.popup.setHTML(html)
+ this.popup.closeButton = false
+ this.scene.addPopup(this.popup)
+ this.popup.addTo(this.scene)
+ const { _id } = this.scene
+ .getLayers()
+ ?.find(i => i.type === 'PointLayer')
+ ?.layerSource.data.dataArray.find(i => {
+ const targetLng = this.data[index][lngField]
+ const targetLat = this.data[index][latField]
+ return i[lngField] === targetLng && i[latField] === targetLat
+ })
+ this.scene
+ .getLayers()
+ ?.find(i => i.type === 'PointLayer' && i.coordCenter)
+ ?.setActive(_id, { color: 'rgba(30,90,255,1)' })
+ }
+ }
+ return undefined
+ }
+
+ /**
+ * 构建 tooltip 内容
+ * @param tooltip
+ * @param fieldData
+ * @param showFields
+ * @returns {string}
+ */
+ const buildTooltipContent = (tooltip, fieldData, showFields) => {
+ let content = ''
+ if (tooltip.customContent) {
+ content = tooltip.customContent
+ showFields.forEach(field => {
+ content = content.replace(`\${${field.split('@')[1]}}`, fieldData[field.split('@')[0]])
+ })
+ } else {
+ showFields.forEach(field => {
+ content += `${field.split('@')[1]}: ${
+ fieldData[field.split('@')[0]]
+ }
`
+ })
+ }
+ return content.replace(/\n/g, '
')
+ }
+ /**
+ * 合并详情到 map
+ * @param details
+ * @returns {Map}
+ */
+ const mergeDetailsToMap = details => {
+ const resultMap = new Map()
+ details.forEach(item => {
+ Object.entries(item).forEach(([key, value]) => {
+ if (resultMap.has(key)) {
+ const existingValue = resultMap.get(key)
+ if (existingValue !== value) {
+ resultMap.set(key, `${existingValue}, ${value}`)
+ }
+ } else {
+ resultMap.set(key, value)
+ }
+ })
+ })
+ return resultMap
+ }
+ buildTooltip()
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/chart-mix-common.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/chart-mix-common.ts
new file mode 100644
index 0000000..403f47c
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/chart-mix-common.ts
@@ -0,0 +1,119 @@
+import { DEFAULT_BASIC_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+
+export const CHART_MIX_EDITOR_PROPERTY: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'dual-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'],
+ 'border-style': ['all'],
+ 'label-selector': ['fontSize', 'color'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'show'],
+ 'dual-basic-style-selector': [
+ 'colors',
+ 'alpha',
+ 'gradient',
+ 'lineWidth',
+ 'lineSymbol',
+ 'lineSymbolSize',
+ 'lineSmooth',
+ 'radiusColumnBar',
+ 'subSeriesColor',
+ 'seriesColor',
+ 'columnWidthRatio'
+ ],
+ 'x-axis-selector': [
+ 'name',
+ 'color',
+ 'fontSize',
+ 'position',
+ 'axisLabel',
+ 'axisLine',
+ 'splitLine'
+ ],
+ 'dual-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': ['emptyDataStrategy']
+}
+
+export const CHART_MIX_AXIS_TYPE: AxisType[] = [
+ 'xAxis',
+ 'yAxis',
+ 'drill',
+ 'filter',
+ 'extLabel',
+ 'extTooltip'
+]
+
+export const CHART_MIX_DEFAULT_BASIC_STYLE = {
+ ...DEFAULT_BASIC_STYLE,
+ subAlpha: 100,
+ subColorScheme: 'fast',
+ subSeriesColor: [],
+ subColors: [
+ '#fae800',
+ '#00c039',
+ '#0482dc',
+ '#bb9581',
+ '#ff7701',
+ '#9c5ec3',
+ '#00ccdf',
+ '#00c039',
+ '#ff7701'
+ ],
+ leftLineWidth: 2,
+ leftLineSymbol: 'circle',
+ leftLineSymbolSize: 4,
+ leftLineSmooth: true
+}
+
+export interface MixChartBasicStyle extends ChartBasicStyle {
+ subAlpha: number
+ subColors: string[]
+ subSeriesColor: {
+ /**
+ * 序列识别id,多指标就是轴id,分组或者堆叠就是类别值
+ */
+ id: string
+ /**
+ * 显示名称
+ */
+ name: string
+ /**
+ * 序列颜色
+ */
+ color: string
+ }[]
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/chart-mix.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/chart-mix.ts
new file mode 100644
index 0000000..0d2627a
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/chart-mix.ts
@@ -0,0 +1,1006 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { DualAxes, DualAxesOptions } from '@antv/g2plot/esm/plots/dual-axes'
+import {
+ configPlotTooltipEvent,
+ getAnalyse,
+ getLabel,
+ getPadding,
+ getTooltipContainer,
+ getYAxis,
+ getYAxisExt,
+ setGradientColor,
+ TOOLTIP_TPL
+} from '../../common/common_antv'
+import { flow, hexColorToRGBA, parseJson } from '@/data-visualization/chart/components/js/util'
+import {
+ cloneDeep,
+ isEmpty,
+ defaultTo,
+ map,
+ filter,
+ union,
+ defaultsDeep,
+ defaults
+} from 'lodash-es'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import {
+ CHART_MIX_AXIS_TYPE,
+ CHART_MIX_DEFAULT_BASIC_STYLE,
+ CHART_MIX_EDITOR_PROPERTY,
+ CHART_MIX_EDITOR_PROPERTY_INNER,
+ MixChartBasicStyle
+} from './chart-mix-common'
+import type { Datum } from '@antv/g2plot/esm/types/common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import {
+ DEFAULT_BASIC_STYLE,
+ DEFAULT_LABEL,
+ DEFAULT_LEGEND_STYLE
+} from '@/data-visualization/chart/components/editor/util/chart'
+import type { Options } from '@antv/g2plot/esm'
+import { Group } from '@antv/g-canvas'
+
+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, 'xAxisExtRight', 'yAxisExt']
+ axisConfig = {
+ xAxis: {
+ name: `${t('chart.drag_block_type_axis')} / ${t('chart.dimension')}`,
+ type: 'd'
+ },
+ yAxis: {
+ name: `${t('chart.drag_block_value_axis_left')} / ${t('chart.column_quota')}`,
+ limit: 1,
+ type: 'q'
+ },
+ extBubble: {
+ //用这个字段存放右轴分类
+ name: `${t('chart.drag_block_type_axis_right')} / ${t('chart.dimension')}`,
+ limit: 1,
+ type: 'd',
+ allowEmpty: true
+ },
+ yAxisExt: {
+ name: `${t('chart.drag_block_value_axis_right')} / ${t('chart.line_quota')}`,
+ limit: 1,
+ type: 'q',
+ allowEmpty: true
+ }
+ }
+
+ protected getLeftType(): string {
+ return 'column'
+ }
+ protected getRightType(): string {
+ return 'line'
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, action, container } = drawOptions
+ if (!chart.data?.left?.data?.length && !chart.data?.right?.data?.length) {
+ return
+ }
+ const left = cloneDeep(chart.data?.left?.data)
+ const right = cloneDeep(chart.data?.right?.data)
+
+ // const data1Type = (left[0]?.type === 'bar' ? 'column' : left[0]?.type) ?? 'column'
+ // const data2Type = (right[0]?.type === 'bar' ? 'column' : right[0]?.type) ?? 'column'
+ const data1Type = this.getLeftType()
+ const data2Type = this.getRightType()
+
+ const isGroup = this.name === 'chart-mix-group' && chart.xAxisExt?.length > 0
+ const isStack = this.name === 'chart-mix-stack' && chart.extStack?.length > 0
+ const seriesField = 'category'
+ const seriesField2 = 'category'
+
+ const data1 = defaultTo(left[0]?.data, [])
+ const data2 = map(defaultTo(right[0]?.data, []), d => {
+ return {
+ ...d,
+ valueExt: d.value
+ }
+ })
+
+ // options
+ const initOptions: DualAxesOptions = {
+ data: [data1, data2],
+ xField: 'field',
+ yField: ['value', 'valueExt'], //这里不能设置成一样的
+ appendPadding: getPadding(chart),
+ geometryOptions: [
+ {
+ geometry: data1Type,
+ color: [],
+ isGroup: isGroup,
+ isStack: isStack,
+ seriesField: seriesField
+ },
+ {
+ geometry: data2Type,
+ color: [],
+ seriesField: seriesField2
+ }
+ ],
+ 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: 'active-region'
+ }
+ ]
+ }
+ const options = this.setupOptions(chart, initOptions)
+ const { DualAxes } = await import('@antv/g2plot/esm/plots/dual-axes')
+ // 开始渲染
+ const newChart = new DualAxes(container, options)
+
+ newChart.on('point:click', action)
+ newChart.on('interval:click', action)
+ configPlotTooltipEvent(chart, newChart)
+ 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 axisFormatterMap = {}
+ labelAttr.seriesLabelFormatter?.forEach(attr => {
+ if (!axisFormatterMap[attr.axisType]) {
+ axisFormatterMap[attr.axisType] = []
+ }
+ axisFormatterMap[attr.axisType].push(attr)
+ })
+ const axisTypes = ['yAxis', 'yAxisExt']
+ axisTypes.forEach(axisType => {
+ const formatterMap = axisFormatterMap[axisType]?.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 Group({})
+ group.addShape({
+ type: 'text',
+ attrs: {
+ x: 0,
+ y: 0,
+ text: value,
+ textAlign: 'start',
+ textBaseline: 'top',
+ fontSize: labelCfg.fontSize,
+ fontFamily: chart.fontFamily,
+ fill: labelCfg.color
+ }
+ })
+ return group
+ }
+ }
+ if (tmpOption.geometryOptions) {
+ if (axisType === 'yAxis') {
+ tmpOption.geometryOptions[0].label = label
+ } else if (axisType === 'yAxisExt') {
+ tmpOption.geometryOptions[1].label = label
+ }
+ }
+ })
+
+ return tmpOption
+ }
+
+ protected configBasicStyle(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ // size
+ const customAttr: DeepPartial = parseJson(chart.customAttr)
+ const s = defaultsDeep(
+ JSON.parse(JSON.stringify(customAttr.basicStyle)),
+ CHART_MIX_DEFAULT_BASIC_STYLE
+ )
+ const smooth = s.lineSmooth
+ const point = {
+ size: s.lineSymbolSize,
+ shape: s.lineSymbol,
+ style: {
+ stroke: hexColorToRGBA('#FFFFFF', s.subAlpha)
+ }
+ }
+ const lineStyle = {
+ lineWidth: s.lineWidth
+ }
+ const leftSmooth = s.leftLineSmooth
+ const leftPoint = {
+ size: s.leftLineSymbolSize,
+ shape: s.leftLineSymbol,
+ style: {
+ stroke: hexColorToRGBA('#FFFFFF', s.alpha)
+ }
+ }
+ const leftLineStyle = {
+ lineWidth: s.leftLineWidth
+ }
+ const tempOption = {
+ ...options,
+ smooth,
+ point,
+ lineStyle
+ }
+ if (tempOption.geometryOptions) {
+ tempOption.geometryOptions[0].smooth = leftSmooth
+ tempOption.geometryOptions[0].point = leftPoint
+ tempOption.geometryOptions[0].lineStyle = leftLineStyle
+
+ tempOption.geometryOptions[1].smooth = smooth
+ tempOption.geometryOptions[1].point = point
+ tempOption.geometryOptions[1].lineStyle = lineStyle
+
+ if (s.radiusColumnBar === 'roundAngle') {
+ const columnStyle = {
+ radius: [
+ s.columnBarRightAngleRadius,
+ s.columnBarRightAngleRadius,
+ s.columnBarRightAngleRadius,
+ s.columnBarRightAngleRadius
+ ]
+ }
+ tempOption.geometryOptions[0].columnStyle = columnStyle
+ tempOption.geometryOptions[1].columnStyle = columnStyle
+ }
+ }
+
+ let columnWidthRatio
+ const _v = s.columnWidthRatio ?? DEFAULT_BASIC_STYLE.columnWidthRatio
+ if (_v >= 1 && _v <= 100) {
+ columnWidthRatio = _v / 100.0
+ } else if (_v < 1) {
+ columnWidthRatio = 1 / 100.0
+ } else if (_v > 100) {
+ columnWidthRatio = 1
+ }
+ if (columnWidthRatio) {
+ tempOption.geometryOptions[0].columnWidthRatio = columnWidthRatio
+ }
+
+ if (super.name !== 'chart-mix-dual-line') {
+ tempOption.geometryOptions[0].appendPadding = getPadding(chart)
+ }
+
+ return tempOption
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const { customAttr, senior } = chart
+ if (
+ senior.functionCfg.emptyDataStrategy == undefined ||
+ senior.functionCfg.emptyDataStrategy === 'ignoreData'
+ ) {
+ senior.functionCfg.emptyDataStrategy = 'breakLine'
+ }
+ return chart
+ }
+
+ protected configCustomColors(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const tempOption = {
+ ...options
+ }
+ const basicStyle = parseJson(chart.customAttr).basicStyle as MixChartBasicStyle
+
+ const { seriesColor } = basicStyle
+ if (seriesColor?.length) {
+ const seriesMap = seriesColor.reduce((p, n) => {
+ p[n.id] = n
+ return p
+ }, {})
+ const { yAxis } = chart
+ yAxis?.forEach((axis, index) => {
+ const curAxisColor = seriesMap[axis.id]
+ if (curAxisColor) {
+ if (index + 1 > basicStyle.colors.length) {
+ basicStyle.colors.push(curAxisColor.color)
+ } else {
+ basicStyle.colors[index] = curAxisColor.color
+ }
+ }
+ })
+ }
+ //左轴
+ const color = basicStyle.colors.map(ele => {
+ const tmp = hexColorToRGBA(ele, basicStyle.alpha)
+ if (basicStyle.gradient) {
+ return setGradientColor(tmp, true, 270)
+ } else {
+ return tmp
+ }
+ })
+ tempOption.geometryOptions[0].color = color
+
+ return tempOption
+ }
+
+ protected configSubCustomColors(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const tempOption = {
+ ...options
+ }
+ const basicStyle = defaultsDeep(
+ parseJson(chart.customAttr).basicStyle as MixChartBasicStyle,
+ cloneDeep(CHART_MIX_DEFAULT_BASIC_STYLE)
+ )
+ //右轴
+ const { subSeriesColor } = basicStyle
+ if (subSeriesColor?.length) {
+ const { yAxisExt, extBubble } = chart
+ const seriesMap = subSeriesColor.reduce((p, n) => {
+ p[n.id] = n
+ return p
+ }, {})
+ const { data } = options as unknown as Options
+ if (extBubble?.length) {
+ const seriesSet = new Set()
+ data[1]?.forEach(d => d.category !== null && seriesSet.add(d.category))
+ const tmp = [...seriesSet]
+ tmp.forEach((c, i) => {
+ const curAxisColor = seriesMap[c as string]
+ if (curAxisColor) {
+ if (i + 1 > basicStyle.subColors.length) {
+ basicStyle.subColors.push(curAxisColor.color)
+ } else {
+ basicStyle.subColors[i] = curAxisColor.color
+ }
+ }
+ })
+ } else {
+ yAxisExt?.forEach((axis, index) => {
+ const curAxisColor = seriesMap[axis.id]
+ if (curAxisColor) {
+ if (index + 1 > basicStyle.subColors.length) {
+ basicStyle.subColors.push(curAxisColor.color)
+ } else {
+ basicStyle.subColors[index] = curAxisColor.color
+ }
+ }
+ })
+ }
+ }
+ const subColor = basicStyle.subColors.map(c => {
+ const cc = hexColorToRGBA(c, basicStyle.subAlpha)
+ return cc
+ })
+ tempOption.geometryOptions[1].color = subColor
+
+ return tempOption
+ }
+
+ public setupSubSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ const result: ChartBasicStyle['seriesColor'] = []
+ const seriesSet = new Set()
+ const colors = chart.customAttr.basicStyle.subColors ?? CHART_MIX_DEFAULT_BASIC_STYLE.subColors
+ const { yAxisExt, extBubble } = chart
+ if (extBubble?.length) {
+ data?.forEach(d => {
+ if (d.value === null || d.category === null || seriesSet.has(d.category)) {
+ return
+ }
+ seriesSet.add(d.category)
+ result.push({
+ id: d.category,
+ name: d.category,
+ color: colors[(seriesSet.size - 1) % colors.length]
+ })
+ })
+ } else {
+ yAxisExt?.forEach(axis => {
+ if (seriesSet.has(axis.id)) {
+ return
+ }
+ seriesSet.add(axis.id)
+ result.push({
+ id: axis.id,
+ name: axis.chartShowName ?? axis.name,
+ color: colors[(seriesSet.size - 1) % colors.length]
+ })
+ })
+ }
+ return result
+ }
+
+ protected configYAxis(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const yAxis = getYAxis(chart)
+ const yAxisExt = getYAxisExt(chart)
+
+ const tempOption = {
+ ...options
+ }
+
+ tempOption.yAxis = {}
+ if (!yAxis) {
+ //左右轴都要隐藏
+ tempOption.yAxis.value = false
+ } else {
+ tempOption.yAxis.value = undefined
+ yAxis.position = 'left'
+
+ const yAxisTmp = parseJson(chart.customStyle).yAxis
+ if (yAxis.label) {
+ yAxis.label.style.textAlign = 'end'
+ 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
+ }
+ }
+
+ if (!yAxisExt) {
+ //左右轴都要隐藏
+ tempOption.yAxis.valueExt = false
+ } else {
+ tempOption.yAxis.valueExt = undefined
+ yAxisExt.position = 'right'
+
+ const yAxisExtTmp = parseJson(chart.customStyle).yAxisExt
+ if (yAxisExt.label) {
+ yAxisExt.label.style.textAlign = 'start'
+ 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 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)
+ const 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
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+
+ protected configLegend(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const o = super.configLegend(chart, options)
+ if (o.legend) {
+ const left = cloneDeep(chart.data?.left?.data)
+ const right = cloneDeep(chart.data?.right?.data)
+
+ o.legend.itemName.formatter = (text: string, item: any, index: number) => {
+ let name = undefined
+ if (item.viewId === 'left-axes-view' && text === 'value') {
+ name = left[0]?.categories[0]
+ } else if (item.viewId === 'right-axes-view' && text === 'valueExt') {
+ name = right[0]?.categories[0]
+ }
+ item.id = item.id + '__' + index //防止重复的图例出现问题,但是左右轴如果有相同的怎么办
+ if (name === undefined) {
+ return text
+ } else {
+ return name
+ }
+ }
+
+ const customStyle = parseJson(chart.customStyle)
+ let size
+ if (customStyle && customStyle.legend) {
+ size = defaults(JSON.parse(JSON.stringify(customStyle.legend)), DEFAULT_LEGEND_STYLE).size
+ } else {
+ size = DEFAULT_LEGEND_STYLE.size
+ }
+
+ o.legend.marker.style = style => {
+ const fill = style.fill ?? style.stroke
+ return {
+ r: size,
+ fill
+ }
+ }
+ }
+ return o
+ }
+
+ protected configAnalyse(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ chart.data.dynamicAssistLines = union(
+ defaultTo(chart.data?.left?.dynamicAssistLines, []),
+ defaultTo(chart.data?.right?.dynamicAssistLines, [])
+ )
+ const list = getAnalyse(chart)
+ const annotations = {
+ value: filter(list, l => l.yAxisType === 'left'),
+ valueExt: filter(list, l => l.yAxisType === 'right')
+ }
+ return { ...options, annotations }
+ }
+
+ protected setupOptions(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ return flow(
+ this.configTheme,
+ this.configLabel,
+ this.configTooltip,
+ this.configBasicStyle,
+ this.configCustomColors,
+ this.configSubCustomColors,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configAnalyse,
+ this.configEmptyDataStrategy
+ )(chart, options)
+ }
+
+ constructor(name = 'chart-mix') {
+ super(name, DEFAULT_DATA)
+ }
+}
+
+export class GroupColumnLineMix extends ColumnLineMix {
+ axis: AxisType[] = [...this['axis'], 'xAxisExt']
+ propertyInner = {
+ ...CHART_MIX_EDITOR_PROPERTY_INNER,
+ 'label-selector': ['vPosition', 'seriesLabelFormatter'],
+ 'tooltip-selector': [
+ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'],
+ 'seriesTooltipFormatter'
+ ]
+ }
+ axisConfig = {
+ ...this['axisConfig'],
+ xAxisExt: {
+ name: `${t('chart.chart_group')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1,
+ allowEmpty: true
+ }
+ }
+
+ protected configCustomColors(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const tempOption = {
+ ...options
+ }
+ const basicStyle = parseJson(chart.customAttr).basicStyle as MixChartBasicStyle
+
+ const { seriesColor } = basicStyle
+ if (seriesColor?.length) {
+ const seriesMap = seriesColor.reduce((p, n) => {
+ p[n.id] = n
+ return p
+ }, {})
+ const { yAxis, xAxisExt } = chart
+ const { data } = options as unknown as Options
+ if (xAxisExt?.length) {
+ const seriesSet = new Set()
+ data[0]?.forEach(d => d.category !== null && seriesSet.add(d.category))
+ const tmp = [...seriesSet]
+ tmp.forEach((c, i) => {
+ const curAxisColor = seriesMap[c as string]
+ if (curAxisColor) {
+ if (i + 1 > basicStyle.colors.length) {
+ basicStyle.colors.push(curAxisColor.color)
+ } else {
+ basicStyle.colors[i] = curAxisColor.color
+ }
+ }
+ })
+ } else {
+ yAxis?.forEach((axis, index) => {
+ const curAxisColor = seriesMap[axis.id]
+ if (curAxisColor) {
+ if (index + 1 > basicStyle.colors.length) {
+ basicStyle.colors.push(curAxisColor.color)
+ } else {
+ basicStyle.colors[index] = curAxisColor.color
+ }
+ }
+ })
+ }
+ }
+ //左轴
+ const color = basicStyle.colors.map(ele => {
+ const tmp = hexColorToRGBA(ele, basicStyle.alpha)
+ if (basicStyle.gradient) {
+ return setGradientColor(tmp, true, 270)
+ } else {
+ return tmp
+ }
+ })
+ tempOption.geometryOptions[0].color = color
+
+ return tempOption
+ }
+
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ const result: ChartBasicStyle['seriesColor'] = []
+ const seriesSet = new Set()
+ const colors = chart.customAttr.basicStyle.colors
+ const { yAxis, xAxisExt } = chart
+ if (xAxisExt?.length) {
+ data?.forEach(d => {
+ if (d.value === null || d.category === null || seriesSet.has(d.category)) {
+ return
+ }
+ seriesSet.add(d.category)
+ result.push({
+ id: d.category,
+ name: d.category,
+ color: colors[(seriesSet.size - 1) % colors.length]
+ })
+ })
+ } else {
+ yAxis?.forEach(axis => {
+ if (seriesSet.has(axis.id)) {
+ return
+ }
+ seriesSet.add(axis.id)
+ result.push({
+ id: axis.id,
+ name: axis.chartShowName ?? axis.name,
+ color: colors[(seriesSet.size - 1) % colors.length]
+ })
+ })
+ }
+ return result
+ }
+
+ constructor(name = 'chart-mix-group') {
+ super(name)
+ }
+}
+export class StackColumnLineMix extends ColumnLineMix {
+ axis: AxisType[] = [...this['axis'], 'extStack']
+ propertyInner = {
+ ...CHART_MIX_EDITOR_PROPERTY_INNER,
+ 'label-selector': ['vPosition', 'seriesLabelFormatter'],
+ 'tooltip-selector': [
+ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'],
+ 'seriesTooltipFormatter'
+ ]
+ }
+ axisConfig = {
+ ...this['axisConfig'],
+ extStack: {
+ name: `${t('chart.stack_item')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1,
+ allowEmpty: true
+ }
+ }
+
+ protected configCustomColors(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const tempOption = {
+ ...options
+ }
+ const basicStyle = parseJson(chart.customAttr).basicStyle as MixChartBasicStyle
+
+ const { seriesColor } = basicStyle
+ if (seriesColor?.length) {
+ const seriesMap = seriesColor.reduce((p, n) => {
+ p[n.id] = n
+ return p
+ }, {})
+ const { yAxis, extStack } = chart
+ const { data } = options as unknown as Options
+ if (extStack?.length) {
+ const seriesSet = new Set()
+ data[0]?.forEach(d => d.category !== null && seriesSet.add(d.category))
+ const tmp = [...seriesSet]
+ tmp.forEach((c, i) => {
+ const curAxisColor = seriesMap[c as string]
+ if (curAxisColor) {
+ if (i + 1 > basicStyle.colors.length) {
+ basicStyle.colors.push(curAxisColor.color)
+ } else {
+ basicStyle.colors[i] = curAxisColor.color
+ }
+ }
+ })
+ } else {
+ yAxis?.forEach((axis, index) => {
+ const curAxisColor = seriesMap[axis.id]
+ if (curAxisColor) {
+ if (index + 1 > basicStyle.colors.length) {
+ basicStyle.colors.push(curAxisColor.color)
+ } else {
+ basicStyle.colors[index] = curAxisColor.color
+ }
+ }
+ })
+ }
+ }
+ //左轴
+ const color = basicStyle.colors.map(ele => {
+ const tmp = hexColorToRGBA(ele, basicStyle.alpha)
+ if (basicStyle.gradient) {
+ return setGradientColor(tmp, true, 270)
+ } else {
+ return tmp
+ }
+ })
+ tempOption.geometryOptions[0].color = color
+
+ return tempOption
+ }
+
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ const result: ChartBasicStyle['seriesColor'] = []
+ const seriesSet = new Set()
+ const colors = chart.customAttr.basicStyle.colors
+ const { yAxis, extStack } = chart
+ if (extStack?.length) {
+ data?.forEach(d => {
+ if (d.value === null || d.category === null || seriesSet.has(d.category)) {
+ return
+ }
+ seriesSet.add(d.category)
+ result.push({
+ id: d.category,
+ name: d.category,
+ color: colors[(seriesSet.size - 1) % colors.length]
+ })
+ })
+ } else {
+ yAxis?.forEach(axis => {
+ if (seriesSet.has(axis.id)) {
+ return
+ }
+ seriesSet.add(axis.id)
+ result.push({
+ id: axis.id,
+ name: axis.chartShowName ?? axis.name,
+ color: colors[(seriesSet.size - 1) % colors.length]
+ })
+ })
+ }
+ return result
+ }
+
+ constructor(name = 'chart-mix-stack') {
+ super(name)
+ }
+}
+
+export class DualLineMix extends ColumnLineMix {
+ axis: AxisType[] = [...this['axis'], 'xAxisExt']
+ propertyInner = {
+ ...CHART_MIX_EDITOR_PROPERTY_INNER,
+ 'label-selector': ['seriesLabelFormatter'],
+ 'tooltip-selector': [
+ ...CHART_MIX_EDITOR_PROPERTY_INNER['tooltip-selector'],
+ 'seriesTooltipFormatter'
+ ]
+ }
+ axisConfig = {
+ ...this['axisConfig'],
+ xAxisExt: {
+ name: `${t('chart.drag_block_type_axis_left')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1,
+ allowEmpty: true
+ }
+ }
+
+ protected getLeftType(): string {
+ return 'line'
+ }
+
+ protected configCustomColors(chart: Chart, options: DualAxesOptions): DualAxesOptions {
+ const tempOption = {
+ ...options
+ }
+ const basicStyle = parseJson(chart.customAttr).basicStyle as MixChartBasicStyle
+
+ const { seriesColor } = basicStyle
+ if (seriesColor?.length) {
+ const seriesMap = seriesColor.reduce((p, n) => {
+ p[n.id] = n
+ return p
+ }, {})
+ const { yAxis, xAxisExt } = chart
+ const { data } = options as unknown as Options
+ if (xAxisExt?.length) {
+ const seriesSet = new Set()
+ data[0]?.forEach(d => d.category !== null && seriesSet.add(d.category))
+ const tmp = [...seriesSet]
+ tmp.forEach((c, i) => {
+ const curAxisColor = seriesMap[c as string]
+ if (curAxisColor) {
+ if (i + 1 > basicStyle.colors.length) {
+ basicStyle.colors.push(curAxisColor.color)
+ } else {
+ basicStyle.colors[i] = curAxisColor.color
+ }
+ }
+ })
+ } else {
+ yAxis?.forEach((axis, index) => {
+ const curAxisColor = seriesMap[axis.id]
+ if (curAxisColor) {
+ if (index + 1 > basicStyle.colors.length) {
+ basicStyle.colors.push(curAxisColor.color)
+ } else {
+ basicStyle.colors[index] = curAxisColor.color
+ }
+ }
+ })
+ }
+ }
+ //左轴
+ const color = basicStyle.colors.map(ele => {
+ const tmp = hexColorToRGBA(ele, basicStyle.alpha)
+ if (basicStyle.gradient) {
+ return setGradientColor(tmp, true, 270)
+ } else {
+ return tmp
+ }
+ })
+ tempOption.geometryOptions[0].color = color
+
+ return tempOption
+ }
+
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ const result: ChartBasicStyle['seriesColor'] = []
+ const seriesSet = new Set()
+ const colors = chart.customAttr.basicStyle.colors
+ const { yAxis, xAxisExt } = chart
+ if (xAxisExt?.length) {
+ data?.forEach(d => {
+ if (d.value === null || d.category === null || seriesSet.has(d.category)) {
+ return
+ }
+ seriesSet.add(d.category)
+ result.push({
+ id: d.category,
+ name: d.category,
+ color: colors[(seriesSet.size - 1) % colors.length]
+ })
+ })
+ } else {
+ yAxis?.forEach(axis => {
+ if (seriesSet.has(axis.id)) {
+ return
+ }
+ seriesSet.add(axis.id)
+ result.push({
+ id: axis.id,
+ name: axis.chartShowName ?? axis.name,
+ color: colors[(seriesSet.size - 1) % colors.length]
+ })
+ })
+ }
+ return result
+ }
+
+ constructor(name = 'chart-mix-dual-line') {
+ super(name)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/circle-packing.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/circle-packing.ts
new file mode 100644
index 0000000..ab6bbad
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/circle-packing.ts
@@ -0,0 +1,279 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type {
+ CirclePacking as G2CirclePacking,
+ CirclePackingOptions
+} from '@antv/g2plot/esm/plots/circle-packing'
+import { flow, parseJson } from '@/data-visualization/chart/components/js/util'
+import { getPadding } from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import type { Datum } from '@antv/g2plot/esm/types/common'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { cloneDeep } from 'lodash-es'
+
+const { t } = useI18n()
+const DEFAULT_DATA = []
+/**
+ * 圆形填充图
+ */
+export class CirclePacking extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'basic-style-selector',
+ 'background-overall-component',
+ 'border-style',
+ 'label-selector',
+ 'legend-selector',
+ 'title-selector',
+ 'tooltip-selector',
+ 'jump-set',
+ 'linkage'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha', 'circleBorderStyle'],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'function-cfg': ['emptyDataStrategy'],
+ 'label-selector': ['color', 'fontSize'],
+ 'legend-selector': ['icon', 'orient', 'fontSize', 'color', 'hPosition', 'vPosition'],
+ 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'tooltipFormatter', 'show']
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'filter', 'drill']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.circle_packing_name')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1
+ },
+ yAxis: {
+ name: `${t('chart.circle_packing_value')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (chart?.data?.data?.length) {
+ // data
+ const data = chart.data.data
+ const { xAxis, yAxis, drillFields } = chart
+ const ySort = yAxis[0]?.sort ?? 'none'
+ const sort = {
+ sort: (a, b) =>
+ ySort === 'asc' ? a.value - b.value : ySort === 'desc' ? b.value - a.value : 0
+ }
+ // 将数据转为圆形填充图数据格式
+ const getCirclePackingData = () => {
+ const result = [{ name: t('commons.all'), children: [] }]
+ const addNode = (nodes, item) => {
+ const node = { ...item, name: item.name, children: [] }
+ nodes.push(node)
+ }
+ data.forEach(item => addNode(result[0].children, item))
+ return result[0]
+ }
+ // options
+ const initOptions: CirclePackingOptions = {
+ data: getCirclePackingData(),
+ appendPadding: getPadding(chart),
+ hierarchyConfig: {
+ ...(ySort === 'none' ? {} : sort)
+ },
+ 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'
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ const options = this.setupOptions(chart, initOptions)
+ const { CirclePacking: G2CirclePacking } = await import(
+ '@antv/g2plot/esm/plots/circle-packing'
+ )
+ const newChart = new G2CirclePacking(container, options)
+ newChart.on('point:click', param => {
+ const pointData = param?.data?.data
+ if (pointData?.name === t('commons.all')) {
+ return
+ }
+ const actionParams = {
+ x: param.x,
+ y: param.y,
+ data: {
+ data: {
+ ...pointData
+ }
+ }
+ }
+ action(actionParams)
+ })
+ return newChart
+ }
+ }
+
+ protected configBasicStyle(chart: Chart, options: CirclePackingOptions): CirclePackingOptions {
+ // size
+ const customAttr: DeepPartial = parseJson(chart.customAttr)
+ const s = JSON.parse(JSON.stringify(customAttr.basicStyle))
+ // 圆形边框样式
+ const pointStyle = {
+ stroke: s.circleBorderColor,
+ lineWidth: s.circleBorderWidth ?? 0
+ }
+ const padding = s.circlePadding
+ return {
+ ...options,
+ hierarchyConfig: {
+ ...options.hierarchyConfig,
+ padding: typeof padding === 'number' && !isNaN(padding) ? padding / 100 : 0
+ },
+ pointStyle
+ }
+ }
+
+ protected configLabel(chart: Chart, options: CirclePackingOptions): CirclePackingOptions {
+ const tmpOptions = super.configLabel(chart, options)
+ if (!tmpOptions.label) {
+ return {
+ ...tmpOptions,
+ label: false
+ }
+ }
+ const { label: labelAttr } = parseJson(chart.customAttr)
+ const label = {
+ ...tmpOptions.label,
+ textAlign: 'center',
+ offsetY: 5,
+ layout: labelAttr.fullDisplay ? [{ type: 'limit-in-plot' }] : tmpOptions.label.layout,
+ formatter: (d: Datum, _point) => {
+ return d.children.length === 0 ? d.name : ''
+ }
+ }
+ return {
+ ...tmpOptions,
+ label
+ }
+ }
+
+ protected configTooltip(chart: Chart, options: CirclePackingOptions): CirclePackingOptions {
+ const temOptions = super.configTooltip(chart, options)
+ if (!temOptions.tooltip) {
+ return temOptions
+ }
+ const tooltipAttr = parseJson(chart.customAttr).tooltip
+ return {
+ ...temOptions,
+ tooltip: {
+ ...temOptions,
+ fields: ['name', 'value'],
+ formatter: d => {
+ let value = d.value
+ if (tooltipAttr.tooltipFormatter) {
+ value = valueFormatter(value, tooltipAttr.tooltipFormatter)
+ }
+ return { name: d.name, value }
+ }
+ }
+ }
+ }
+ configEmptyDataStrategy(chart: Chart, options: CirclePackingOptions): CirclePackingOptions {
+ const { functionCfg } = parseJson(chart.senior)
+ const emptyDataStrategy = functionCfg.emptyDataStrategy
+ const setChildren = children => {
+ if (emptyDataStrategy === 'ignoreData') {
+ for (let i = children.length - 1; i >= 0; i--) {
+ let isNotNullChildren = []
+ if (children[i].children?.length) {
+ isNotNullChildren = children[i].children.filter(item => item.value !== null)
+ }
+ if (children[i].children?.length && isNotNullChildren.length) {
+ setChildren(children[i].children)
+ }
+ if (children[i]?.hasOwnProperty('value') && children[i].value === null) {
+ children.splice(i, 1)
+ }
+ if (!children[i]?.hasOwnProperty('value') && isNotNullChildren.length === 0) {
+ children.splice(i, 1)
+ }
+ }
+ } else {
+ for (let i = children.length - 1; i >= 0; i--) {
+ let isNotNullChildren = []
+ if (children[i].children?.length) {
+ isNotNullChildren = children[i].children.filter(item => item.value !== null)
+ if (!isNotNullChildren.length) {
+ children[i].children = []
+ continue
+ }
+ }
+ setChildren(children[i].children)
+ }
+ }
+ }
+ const data = cloneDeep(options.data.children)
+ setChildren(data)
+ options.data.children = data
+ return options
+ }
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const { customAttr, customStyle, senior } = chart
+ const { label, basicStyle } = customAttr
+ const { legend } = customStyle
+ senior.functionCfg.emptyDataStrategy = 'ignoreData'
+ customAttr.label = {
+ ...label,
+ show: true
+ }
+ legend.show = false
+ basicStyle.circleBorderWidth = 0
+ basicStyle.circleBorderColor = '#fff'
+ basicStyle.circlePadding = 0
+ return chart
+ }
+ protected setupOptions(chart: Chart, options: CirclePackingOptions): CirclePackingOptions {
+ return flow(
+ this.configTheme,
+ this.configEmptyDataStrategy,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend
+ )(chart, options)
+ }
+
+ constructor() {
+ super('circle-packing', DEFAULT_DATA)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/funnel.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/funnel.ts
new file mode 100644
index 0000000..2120b69
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/funnel.ts
@@ -0,0 +1,215 @@
+import type { FunnelOptions, Funnel as G2Funnel } from '@antv/g2plot/esm/plots/funnel'
+import { G2PlotChartView, G2PlotDrawOptions } from '../../types/impl/g2plot'
+import { flow, parseJson, setUpSingleDimensionSeriesColor } from '@/data-visualization/chart/components/js/util'
+import { configPlotTooltipEvent, getPadding } from '../../common/common_antv'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { Datum } from '@antv/g2plot/esm/types/common'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+
+const { t } = useI18n()
+
+/**
+ * 漏斗图
+ */
+export class Funnel extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'title-selector',
+ 'legend-selector',
+ 'jump-set',
+ 'linkage'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha', 'seriesColor'],
+ 'label-selector': ['fontSize', 'color', 'hPosition', 'showQuota', 'conversionTag'],
+ 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
+ 'title-selector': [
+ 'show',
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'legend-selector': ['icon', 'orient', 'color', 'fontSize', 'hPosition', 'vPosition']
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'filter', 'drill', 'extLabel', 'extTooltip']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.drag_block_funnel_split')} / ${t('chart.dimension')}`,
+ type: 'd'
+ },
+ yAxis: {
+ name: `${t('chart.drag_block_funnel_width')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data) {
+ return
+ }
+ const data = chart.data.data
+ const baseOptions: FunnelOptions = {
+ data,
+ xField: 'field',
+ yField: 'value',
+ appendPadding: getPadding(chart),
+ 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: 'interval:mousemove', action: 'tooltip:show' }],
+ end: [{ trigger: 'interval:mouseleave', action: 'tooltip:hide' }]
+ }
+ }
+ ],
+ meta: {
+ field: {
+ type: 'cat'
+ }
+ }
+ }
+ const options = this.setupOptions(chart, baseOptions)
+ const { Funnel: G2Funnel } = await import('@antv/g2plot/esm/plots/funnel')
+ const newChart = new G2Funnel(container, options)
+ newChart.on('interval:click', action)
+ configPlotTooltipEvent(chart, newChart)
+ return newChart
+ }
+
+ protected configLabel(chart: Chart, options: FunnelOptions): FunnelOptions {
+ let label
+ let conversionTag
+ let customAttr: DeepPartial
+ if (chart.customAttr) {
+ customAttr = parseJson(chart.customAttr)
+ const showQuota = customAttr.label.showQuota
+ const l = customAttr.label
+ if (customAttr.label?.show) {
+ // label
+ if (showQuota) {
+ const layout = []
+ if (!l.fullDisplay) {
+ layout.push(...[{ type: 'hide-overlap' }, { type: 'limit-in-plot' }])
+ }
+ label = {
+ position: l.position,
+ layout,
+ style: {
+ fill: l.color,
+ fontSize: l.fontSize
+ },
+ formatter: function (param: Datum) {
+ return valueFormatter(param.value, l.quotaLabelFormatter)
+ }
+ }
+ const position = label.position
+ if (position === 'right') {
+ label.offsetX = -40
+ }
+ }
+ // 转化率
+ const conversionTagAtt = parseJson(chart.customAttr).label.conversionTag
+ if (conversionTagAtt && conversionTagAtt.show) {
+ conversionTag = {
+ style: {
+ fill: l.color,
+ fontSize: l.fontSize
+ },
+ formatter: datum => {
+ if (!datum['$$conversion$$'][0]) {
+ return `${conversionTagAtt.text ?? ''} -`
+ }
+ const rate = (
+ (datum['$$conversion$$'][1] / datum['$$conversion$$'][0]) *
+ 100
+ ).toFixed(conversionTagAtt.precision)
+ return `${conversionTagAtt.text ?? ''} ${rate}%`
+ }
+ }
+ }
+ }
+ return {
+ ...options,
+ label,
+ conversionTag,
+ maxSize: conversionTag ? 0.8 : 1
+ }
+ }
+ return options
+ }
+
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ return setUpSingleDimensionSeriesColor(chart, data)
+ }
+ protected setupOptions(chart: Chart, options: FunnelOptions): FunnelOptions {
+ return flow(
+ this.configTheme,
+ this.configSingleDimensionColor,
+ this.configLabel,
+ this.configMultiSeriesTooltip,
+ this.configLegend
+ )(chart, options)
+ }
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const { customAttr, customStyle } = chart
+ const { label } = customAttr
+ if (!['left', 'middle', 'right'].includes(label.position)) {
+ label.position = 'middle'
+ }
+ customAttr.label = {
+ ...label,
+ show: true,
+ showQuota: true,
+ conversionTag: {
+ show: false,
+ precision: 2,
+ text: t('chart.conversion_rate')
+ }
+ }
+ const { legend } = customStyle
+ legend.show = false
+ return chart
+ }
+
+ constructor() {
+ super('funnel', [])
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/gauge.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/gauge.ts
new file mode 100644
index 0000000..1a98729
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/gauge.ts
@@ -0,0 +1,350 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { Gauge as G2Gauge, GaugeOptions } from '@antv/g2plot/esm/plots/gauge'
+import { flow, parseJson } from '@/data-visualization/chart/components/js/util'
+import {
+ DEFAULT_LABEL,
+ DEFAULT_MISC,
+ DEFAULT_THRESHOLD,
+ getScaleValue
+} from '@/data-visualization/chart/components/editor/util/chart'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { getPadding, setGradientColor } from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { merge } from 'lodash-es'
+
+const { t } = useI18n()
+
+const DEFAULT_DATA = []
+export class Gauge extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'label-selector',
+ 'misc-selector',
+ 'title-selector',
+ 'threshold'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha', 'gradient', 'gaugeAxisLine', 'gaugePercentLabel'],
+ 'label-selector': ['fontSize', 'color', 'labelFormatter'],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'misc-selector': [
+ 'gaugeMinType',
+ 'gaugeMinField',
+ 'gaugeMin',
+ 'gaugeMaxType',
+ 'gaugeMaxField',
+ 'gaugeMax',
+ 'gaugeStartAngle',
+ 'gaugeEndAngle'
+ ],
+ threshold: ['gaugeThreshold']
+ }
+ axis: AxisType[] = ['yAxis', 'filter']
+ axisConfig: AxisConfig = {
+ yAxis: {
+ name: `${t('chart.drag_block_gauge_angel')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, scale, action } = drawOptions
+ if (!chart.data?.series || !chart.yAxis.length) {
+ return
+ }
+ // options
+ const initOptions: GaugeOptions = {
+ percent: 0,
+ appendPadding: getPadding(chart),
+ axis: {
+ tickInterval: 0.2,
+ label: {
+ style: {
+ fontSize: getScaleValue(12, scale) // 刻度值字体大小
+ }
+ },
+ tickLine: {
+ length: getScaleValue(12, scale) * -1, // 刻度线长度
+ style: {
+ lineWidth: getScaleValue(1, scale) // 刻度线宽度
+ }
+ },
+ subTickLine: {
+ count: 4, // 子刻度数
+ length: getScaleValue(6, scale) * -1, // 子刻度线长度
+ style: {
+ lineWidth: getScaleValue(1, scale) // 子刻度线宽度
+ }
+ }
+ }
+ }
+ const options = this.setupOptions(chart, initOptions, { scale })
+ const { Gauge: G2Gauge } = await import('@antv/g2plot/esm/plots/gauge')
+ const newChart = new G2Gauge(container, options)
+ newChart.on('afterrender', () => {
+ action({
+ from: 'gauge',
+ data: {
+ type: 'gauge',
+ max: chart.data?.series[0]?.data[0]
+ }
+ })
+ })
+ const hasNoneData = chart.data?.series.some(s => !s.data?.[0])
+ this.configEmptyDataStyle(newChart, hasNoneData ? [] : [1], container)
+ if (hasNoneData) {
+ return
+ }
+ return newChart
+ }
+
+ protected configMisc(
+ chart: Chart,
+ options: GaugeOptions,
+ context: Record
+ ): GaugeOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const data = chart.data.series[0].data[0]
+ let min, max, startAngle, endAngle
+ if (customAttr.misc) {
+ const misc = customAttr.misc
+ if (misc.gaugeMinType === 'dynamic' && misc.gaugeMaxType === 'dynamic') {
+ min = chart.data?.series[chart.data?.series.length - 2]?.data[0]
+ max = chart.data?.series[chart.data?.series.length - 1]?.data[0]
+ } else if (misc.gaugeMinType !== 'dynamic' && misc.gaugeMaxType === 'dynamic') {
+ min = misc.gaugeMin || misc.gaugeMin === 0 ? misc.gaugeMin : DEFAULT_MISC.gaugeMin
+ max = chart.data?.series[chart.data?.series.length - 1]?.data[0]
+ } else if (misc.gaugeMinType === 'dynamic' && misc.gaugeMaxType !== 'dynamic') {
+ min = chart.data?.series[chart.data?.series.length - 1]?.data[0]
+ max = misc.gaugeMax ? misc.gaugeMax : DEFAULT_MISC.gaugeMax
+ } else {
+ min = misc.gaugeMin || misc.gaugeMin === 0 ? misc.gaugeMin : DEFAULT_MISC.gaugeMin
+ max = misc.gaugeMax
+ ? misc.gaugeMax
+ : chart.data?.series[chart.data?.series.length - 1]?.data[0]
+ }
+ startAngle = (misc.gaugeStartAngle * Math.PI) / 180
+ endAngle = (misc.gaugeEndAngle * Math.PI) / 180
+ context.min = min
+ context.max = max
+ }
+ const percent = (parseFloat(data) - parseFloat(min)) / (parseFloat(max) - parseFloat(min))
+ const tmp = {
+ percent,
+ startAngle,
+ endAngle
+ }
+ return { ...options, ...tmp }
+ }
+
+ private configRange(
+ chart: Chart,
+ options: GaugeOptions,
+ context: Record
+ ): GaugeOptions {
+ const { scale } = context
+ const range = [0]
+ let index = 0
+ let flag = false
+ let hasThreshold = false
+ const theme = options.theme as any
+
+ if (chart.senior) {
+ const senior = parseJson(chart.senior)
+ const threshold = senior.threshold ?? DEFAULT_THRESHOLD
+ if (threshold.enable && threshold.gaugeThreshold) {
+ hasThreshold = true
+ const arr = threshold.gaugeThreshold.split(',')
+ for (let i = 0; i < arr.length; i++) {
+ const ele = arr[i]
+ const p = parseFloat(ele) / 100
+ range.push(p)
+ if (!flag && options.percent <= p) {
+ flag = true
+ index = i
+ }
+ }
+ if (!flag) {
+ index = arr.length
+ }
+ }
+ }
+ range.push(1)
+ let rangOptions
+ if (hasThreshold) {
+ rangOptions = {
+ range: {
+ color: theme.styleSheet.paletteQualitative10,
+ ticks: range
+ },
+ indicator: {
+ pointer: {
+ style: {
+ stroke:
+ theme.styleSheet.paletteQualitative10[
+ index % theme.styleSheet.paletteQualitative10.length
+ ]
+ }
+ },
+ pin: {
+ style: {
+ stroke:
+ theme.styleSheet.paletteQualitative10[
+ index % theme.styleSheet.paletteQualitative10.length
+ ],
+ r: getScaleValue(10, scale)
+ }
+ }
+ }
+ }
+ } else {
+ rangOptions = {
+ indicator: {
+ pin: {
+ style: {
+ r: getScaleValue(10, scale)
+ }
+ }
+ }
+ }
+ }
+ const customAttr = parseJson(chart.customAttr)
+ if (customAttr.basicStyle.gradient) {
+ const colorList = (theme.styleSheet?.paletteQualitative10 || []).map(ele => {
+ return setGradientColor(ele, true)
+ })
+ if (!rangOptions.range) {
+ rangOptions.range = {
+ color: colorList
+ }
+ } else {
+ rangOptions.range.color = colorList
+ }
+ }
+ return { ...options, ...rangOptions }
+ }
+
+ protected configLabel(
+ chart: Chart,
+ options: GaugeOptions,
+ context?: Record
+ ): GaugeOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const data = chart.data.series[0].data[0]
+ let labelTitle: GaugeOptions['statistic']['title'] = false
+ let labelContent: GaugeOptions['statistic']['content'] = false
+ const label = customAttr.label
+ const labelFormatter = label.labelFormatter ?? DEFAULT_LABEL.labelFormatter
+ if (label.show && label.childrenShow) {
+ labelTitle = {
+ style: {
+ fontSize: `${label.fontSize}px`,
+ color: label.color
+ },
+ formatter: function () {
+ let value
+ if (labelFormatter.type === 'percent') {
+ value = options.percent
+ } else {
+ value = data
+ }
+ return valueFormatter(value, labelFormatter)
+ }
+ } as GaugeOptions['statistic']['title']
+ }
+ const { min, max } = context
+ if (label.show && label.proportionSeriesFormatter.show) {
+ const proportionFormatter = label.proportionSeriesFormatter
+ labelContent = {
+ offsetY: proportionFormatter.fontSize + label.fontSize,
+ style: {
+ fontSize: `${proportionFormatter.fontSize}px`,
+ color: proportionFormatter.color
+ },
+ formatter: function () {
+ const proportionValue = ((parseFloat(data) - min) / (max - min)) * 100
+ return (
+ t('chart.proportion') +
+ ': ' +
+ proportionValue.toFixed(proportionFormatter.formatterCfg.decimalCount) +
+ '%'
+ )
+ }
+ } as GaugeOptions['statistic']['content']
+ }
+ const statistic = {
+ title: labelTitle,
+ content: labelContent
+ }
+ const { gaugeAxisLine, gaugePercentLabel } = customAttr.basicStyle
+ const tmp = {
+ axis: {
+ label: {
+ formatter: v => {
+ if (gaugeAxisLine === false) {
+ return ''
+ }
+ if (gaugePercentLabel === false) {
+ const resultV = v === '0' ? min : v === '1' ? max : min + (max - min) * v
+ return labelFormatter.type === 'value'
+ ? valueFormatter(resultV, labelFormatter)
+ : resultV
+ }
+ return v === '0' ? v : v * 100 + '%'
+ }
+ }
+ }
+ }
+ options = merge(options, tmp)
+ return { ...options, statistic }
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.customAttr.label = {
+ ...chart.customAttr.label,
+ show: true,
+ labelFormatter: {
+ type: 'value',
+ thousandSeparator: true,
+ decimalCount: 0,
+ unit: 1
+ }
+ }
+ return chart
+ }
+
+ protected setupOptions(
+ chart: Chart,
+ options: GaugeOptions,
+ context: Record
+ ): GaugeOptions {
+ return flow(
+ this.configTheme,
+ this.configMisc,
+ this.configLabel,
+ this.configRange
+ )(chart, options, context)
+ }
+ constructor() {
+ super('gauge', DEFAULT_DATA)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/indicator.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/indicator.ts
new file mode 100644
index 0000000..c28611c
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/indicator.ts
@@ -0,0 +1,76 @@
+import { AbstractChartView, ChartLibraryType, ChartRenderType } from '../../types'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { COLOR_CASES } from '@/data-visualization/chart/components/editor/util/chart'
+
+const { t } = useI18n()
+/**
+ * 指标卡图表
+ */
+export class IndicatorChartView extends AbstractChartView {
+ selectorSpec: EditorSelectorSpec
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'title-selector',
+ 'indicator-value-selector',
+ 'indicator-name-selector',
+ 'threshold',
+ 'function-cfg'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'indicator-value-selector': [
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'indicator-name-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'function-cfg': ['emptyDataStrategy']
+ }
+ axis: AxisType[] = ['yAxis', 'filter']
+ axisConfig: AxisConfig = {
+ yAxis: {
+ name: `${t('chart.quota')}`,
+ limit: 1
+ }
+ }
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const basicColors = COLOR_CASES[0].colors
+ chart.customAttr.basicStyle.colors = basicColors
+ chart.customAttr.indicator.color = basicColors[0]
+ chart.customAttr.indicatorName.color = basicColors[1]
+ return chart
+ }
+
+ constructor() {
+ super(ChartRenderType.CUSTOM, ChartLibraryType.INDICATOR, 'indicator')
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/picture-group.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/picture-group.ts
new file mode 100644
index 0000000..bd349c6
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/picture-group.ts
@@ -0,0 +1,29 @@
+import { AbstractChartView, ChartLibraryType, ChartRenderType } from '../../types'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+
+const { t } = useI18n()
+/**
+ * 图片组图表
+ */
+export class PictureGroupView extends AbstractChartView {
+ properties: EditorProperty[] = ['background-overall-component', 'border-style', 'threshold']
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ threshold: ['tableThreshold']
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'filter']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.dimension')}`,
+ type: 'd'
+ },
+ yAxis: {
+ name: `${t('chart.quota')}`,
+ type: 'q'
+ }
+ }
+ constructor() {
+ super(ChartRenderType.CUSTOM, ChartLibraryType.PICTURE_GROUP, 'picture-group')
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/quadrant.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/quadrant.ts
new file mode 100644
index 0000000..d9d4e34
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/quadrant.ts
@@ -0,0 +1,488 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { ScatterOptions, Scatter as G2Scatter } from '@antv/g2plot/esm/plots/scatter'
+import { flow, parseJson, setUpSingleDimensionSeriesColor } from '../../../util'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { defaults, isEmpty, map } from 'lodash-es'
+import { cloneDeep, defaultTo } from 'lodash-es'
+import {
+ configAxisLabelLengthLimit,
+ configPlotTooltipEvent,
+ configYaxisTitleLengthLimit,
+ getTooltipContainer,
+ TOOLTIP_TPL
+} from '../../common/common_antv'
+import { DEFAULT_LEGEND_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+
+const { t } = useI18n()
+/**
+ * 象限图
+ */
+export class Quadrant extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'x-axis-selector',
+ 'y-axis-selector',
+ 'title-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'legend-selector',
+ 'jump-set',
+ 'linkage',
+ 'quadrant-selector'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'basic-style-selector': [
+ 'colors',
+ 'alpha',
+ 'scatterSymbol',
+ 'scatterSymbolSize',
+ 'seriesColor'
+ ],
+ 'label-selector': ['fontSize', 'color'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
+ 'x-axis-selector': [
+ 'position',
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisLine',
+ 'axisValue',
+ 'splitLine',
+ 'axisForm',
+ 'axisLabel',
+ 'axisLabelFormatter'
+ ],
+ 'y-axis-selector': [
+ 'position',
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisValue',
+ 'axisLine',
+ 'splitLine',
+ 'axisForm',
+ 'axisLabel',
+ 'axisLabelFormatter'
+ ],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'legend-selector': ['icon', 'orient', 'color', 'fontSize', 'hPosition', 'vPosition'],
+ 'quadrant-selector': ['regionStyle', 'label', 'lineStyle']
+ }
+ axis: AxisType[] = [
+ 'xAxis',
+ 'yAxis',
+ 'yAxisExt',
+ 'extBubble',
+ 'filter',
+ 'drill',
+ 'extLabel',
+ 'extTooltip'
+ ]
+ axisConfig: AxisConfig = {
+ extBubble: {
+ name: `${t('chart.bubble_size')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1,
+ allowEmpty: true
+ },
+ xAxis: {
+ name: `${t('chart.form_type')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1
+ },
+ yAxis: {
+ name: `${t('chart.x_axis')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ },
+ yAxisExt: {
+ name: `${t('chart.y_axis')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+
+ public getFieldObject(chart: Chart) {
+ const colorFieldObj = { id: chart.xAxis[0]?.id, name: chart.xAxis[0]?.['originName'] }
+ const sizeFieldObj = { id: chart.extBubble[0]?.id, name: chart.extBubble[0]?.['originName'] }
+ const xFieldObj = { id: chart.yAxis[0]?.id, name: chart.yAxis[0]?.['originName'] }
+ const yFieldObj = { id: chart.yAxisExt[0]?.id, name: chart.yAxisExt[0]?.['originName'] }
+ return { colorFieldObj, sizeFieldObj, xFieldObj, yFieldObj }
+ }
+ public getUniqueObjects(arr: T[]): T[] {
+ return [...new Set(arr.map(JSON.stringify))].map(JSON.parse) as T[]
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action, quadrantDefaultBaseline } = drawOptions
+ if (!chart.data?.data) {
+ return
+ }
+ // data
+ const sourceData: Array = cloneDeep(chart.data.data)
+ const data1 = defaultTo(sourceData[0]?.data, [])
+ const data2 = defaultTo(sourceData[1]?.data, [])
+ const data3 = defaultTo(sourceData[2]?.data, [])
+ const xData = data1.map(item => {
+ return {
+ ...item,
+ id: item.quotaList[0]?.id,
+ field: item.field,
+ value: item.value
+ }
+ })
+ const yData = data2.map(item => {
+ return {
+ ...item,
+ id: item.quotaList[0]?.id,
+ field: item.field,
+ value: item.value
+ }
+ })
+ const eData = data3.map(item => {
+ return {
+ ...item,
+ id: item.quotaList[0]?.id,
+ field: item.field,
+ value: item.value
+ }
+ })
+ // x轴基准线 默认值
+ const xValues = xData.map(item => item.value)
+ const xBaseline = ((Math.max(...xValues) + Math.min(...xValues)) / 2).toFixed()
+ // y轴基准线 默认值
+ const yValues = yData.map(item => item.value)
+ const yBaseline = ((Math.max(...yValues) + Math.min(...yValues)) / 2).toFixed()
+ const defaultBaselineQuadrant = {
+ ...chart.customAttr['quadrant']
+ }
+ // 新建图表
+ if (defaultBaselineQuadrant.xBaseline === undefined) {
+ // 默认基准线值
+ defaultBaselineQuadrant.xBaseline = xBaseline
+ defaultBaselineQuadrant.yBaseline = yBaseline
+ }
+ const getQuotaList = d => {
+ const eQuotaList = eData.find(item => item.field === d.field)?.quotaList
+ const yQuotaList = yData.find(item => item.field === d.field)?.quotaList
+ if (JSON.stringify(eQuotaList) === JSON.stringify(yQuotaList)) {
+ return yQuotaList
+ }
+ return [...(eQuotaList || []), ...(yQuotaList || [])]
+ }
+ const data = map(defaultTo(xData, []), d => {
+ return {
+ ...d,
+ yAxis: d.value,
+ quotaList: getQuotaList(d),
+ yAxisExt: yData.find(item => item.field === d.field)?.value,
+ extBubble: eData.find(item => item.field === d.field)?.value
+ }
+ })
+ const baseOptions: ScatterOptions = {
+ colorField: 'field',
+ meta: {
+ field: {
+ type: 'cat'
+ }
+ },
+ quadrant: {
+ ...defaultBaselineQuadrant
+ },
+ data: data,
+ xField: 'yAxis',
+ yField: 'yAxisExt',
+ appendPadding: 30,
+ pointStyle: {
+ fillOpacity: 0.8,
+ stroke: '#bbb'
+ }
+ }
+ chart.container = container
+ const options = this.setupOptions(chart, baseOptions)
+ const { Scatter: G2Scatter } = await import('@antv/g2plot/esm/plots/scatter')
+ const newChart = new G2Scatter(container, options)
+ newChart.on('point:click', action)
+ newChart.on('click', () => quadrantDefaultBaseline(defaultBaselineQuadrant))
+ newChart.on('afterrender', () => quadrantDefaultBaseline(defaultBaselineQuadrant))
+ const yAxis = parseJson(chart.customStyle).yAxis
+ if (yAxis?.name) {
+ configYaxisTitleLengthLimit(chart, newChart)
+ configAxisLabelLengthLimit(chart, newChart, 'axis-title')
+ }
+ configPlotTooltipEvent(chart, newChart)
+ return newChart
+ }
+
+ protected configBasicStyle(chart: Chart, options: ScatterOptions): ScatterOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const basicStyle = customAttr.basicStyle
+ if (chart.extBubble?.length) {
+ return {
+ ...options,
+ size: [4, 30],
+ sizeField: 'extBubble',
+ shape: basicStyle.scatterSymbol
+ }
+ }
+ return {
+ ...options,
+ size: basicStyle.scatterSymbolSize,
+ shape: basicStyle.scatterSymbol
+ }
+ }
+
+ protected configXAxis(chart: Chart, options: ScatterOptions): ScatterOptions {
+ const tmpOptions = super.configXAxis(chart, options)
+ if (!tmpOptions.xAxis) {
+ return tmpOptions
+ }
+ const xAxis = parseJson(chart.customStyle).xAxis
+ if (tmpOptions.xAxis.label) {
+ tmpOptions.xAxis.label.formatter = value => {
+ return valueFormatter(value, xAxis.axisLabelFormatter)
+ }
+ }
+ const axisValue = xAxis.axisValue
+ if (!axisValue?.auto) {
+ const axis = {
+ xAxis: {
+ ...tmpOptions.xAxis,
+ min: axisValue.min,
+ max: axisValue.max,
+ minLimit: axisValue.min,
+ maxLimit: axisValue.max,
+ tickCount: axisValue.splitCount
+ }
+ }
+ return { ...tmpOptions, ...axis }
+ }
+ return tmpOptions
+ }
+
+ protected configYAxis(chart: Chart, options: ScatterOptions): ScatterOptions {
+ const tmpOptions = super.configYAxis(chart, options)
+ if (!tmpOptions.yAxis) {
+ return tmpOptions
+ }
+ const yAxis = parseJson(chart.customStyle).yAxis
+ if (tmpOptions.yAxis.label) {
+ tmpOptions.yAxis.label.formatter = value => {
+ return valueFormatter(value, yAxis.axisLabelFormatter)
+ }
+ }
+ const axisValue = yAxis.axisValue
+ if (!axisValue?.auto) {
+ const axis = {
+ yAxis: {
+ ...tmpOptions.yAxis,
+ min: axisValue.min,
+ max: axisValue.max,
+ minLimit: axisValue.min,
+ maxLimit: axisValue.max,
+ tickCount: axisValue.splitCount
+ }
+ }
+ return { ...tmpOptions, ...axis }
+ }
+ return tmpOptions
+ }
+
+ protected configLabel(chart: Chart, options: ScatterOptions): ScatterOptions {
+ let label
+ let customAttr: DeepPartial
+ if (chart.customAttr) {
+ customAttr = parseJson(chart.customAttr)
+ // label
+ if (customAttr.label) {
+ const l = customAttr.label
+ if (l.show) {
+ const layout = []
+ if (!l.fullDisplay) {
+ layout.push({ type: 'hide-overlap' })
+ layout.push({ type: 'limit-in-shape' })
+ }
+ label = {
+ offset: 0,
+ style: {
+ fill: l.color,
+ fontSize: l.fontSize
+ },
+ content: datum => {
+ return datum['name']
+ },
+ layout
+ }
+ } else {
+ label = false
+ }
+ }
+ }
+ return { ...options, label }
+ }
+
+ protected configTooltip(chart: Chart, options: ScatterOptions): ScatterOptions {
+ const customAttr: DeepPartial = parseJson(chart.customAttr)
+ const tooltipAttr = customAttr.tooltip
+ const xAxisTitle = chart.xAxis[0]
+ const yAxisTitle = chart.yAxis[0]
+ const yAxisExtTitle = chart.yAxisExt[0]
+ if (!tooltipAttr.show || (!xAxisTitle && !yAxisTitle && !yAxisExtTitle)) {
+ return {
+ ...options,
+ tooltip: false
+ }
+ }
+ const formatterMap = tooltipAttr.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next['seriesId']] = next
+ return pre
+ }, {}) as Record
+ const optionsData = cloneDeep(options.data)
+ const tooltip: ScatterOptions['tooltip'] = {
+ showTitle: true,
+ title: (_title, datum) => {
+ return datum?.['name']
+ },
+ customItems(originalItems) {
+ if (!tooltipAttr.seriesTooltipFormatter?.length) {
+ return originalItems
+ }
+ const result = []
+ originalItems.forEach(item => {
+ Object.keys(formatterMap).forEach(key => {
+ if (key.endsWith(item.name)) {
+ const formatter = formatterMap[key]
+ if (formatter) {
+ const value =
+ formatter.groupType === 'q'
+ ? valueFormatter(parseFloat(item.value as string), formatter.formatterCfg)
+ : item.value
+ const name = isEmpty(formatter.chartShowName)
+ ? formatter.name
+ : formatter.chartShowName
+ result.push({ color: item.color, name, value })
+ }
+ }
+ })
+ })
+ 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
+ }
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.customStyle.yAxis.splitLine = {
+ ...chart.customStyle.yAxis.splitLine,
+ show: false
+ }
+ chart.customStyle.yAxisExt.splitLine = {
+ ...chart.customStyle.yAxisExt.splitLine,
+ show: false
+ }
+ chart.customStyle.yAxis.axisLine = {
+ ...chart.customStyle.yAxis.axisLine,
+ show: true
+ }
+ chart.customStyle.yAxisExt.axisLine = {
+ ...chart.customStyle.yAxisExt.axisLine,
+ show: true
+ }
+ return chart
+ }
+
+ protected configColor(chart: Chart, options: ScatterOptions): ScatterOptions {
+ const { xAxis, yAxis, yAxisExt } = chart
+ if (!(xAxis?.length && yAxis?.length && yAxisExt?.length)) {
+ return options
+ }
+ return this.configSingleDimensionColor(chart, options)
+ }
+
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ const { xAxis, yAxis, yAxisExt } = chart
+ if (!(xAxis?.length && yAxis?.length && yAxisExt?.length)) {
+ return []
+ }
+ const tmp = data?.[0]?.data
+ return setUpSingleDimensionSeriesColor(chart, tmp)
+ }
+
+ protected configLegend(chart: Chart, options: ScatterOptions): ScatterOptions {
+ const optionTmp = super.configLegend(chart, options)
+ if (!optionTmp.legend) {
+ return optionTmp
+ }
+ const customStyle = parseJson(chart.customStyle)
+ let size
+ if (customStyle && customStyle.legend) {
+ size = defaults(JSON.parse(JSON.stringify(customStyle.legend)), DEFAULT_LEGEND_STYLE).size
+ } else {
+ size = DEFAULT_LEGEND_STYLE.size
+ }
+ optionTmp.legend.marker.style = style => {
+ return {
+ r: size,
+ fill: style.fill
+ }
+ }
+ return optionTmp
+ }
+
+ protected setupOptions(chart: Chart, options: ScatterOptions) {
+ return flow(
+ this.configTheme,
+ this.configColor,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configAnalyse,
+ this.configSlider,
+ this.configBasicStyle
+ )(chart, options, {}, this)
+ }
+
+ constructor() {
+ super('quadrant', [])
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/radar.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/radar.ts
new file mode 100644
index 0000000..4da7509
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/radar.ts
@@ -0,0 +1,306 @@
+import type { RadarOptions, Radar as G2Radar } from '@antv/g2plot/esm/plots/radar'
+import { G2PlotChartView, G2PlotDrawOptions } from '../../types/impl/g2plot'
+import { flow, parseJson } from '../../../util'
+import { configPlotTooltipEvent } from '../../common/common_antv'
+import { valueFormatter } from '../../../formatter'
+import type { Datum } from '@antv/g2plot/esm/types/common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { DEFAULT_LABEL, DEFAULT_LEGEND_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+import { Group } from '@antv/g-canvas'
+import { defaults } from 'lodash-es'
+
+const { t } = useI18n()
+
+export class Radar extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'title-selector',
+ 'legend-selector',
+ 'misc-style-selector',
+ 'jump-set',
+ 'linkage'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'basic-style-selector': [
+ 'colors',
+ 'alpha',
+ 'radarShape',
+ 'seriesColor',
+ 'radarShowPoint',
+ 'radarPointSize',
+ 'radarAreaColor'
+ ],
+ 'label-selector': ['seriesLabelFormatter'],
+ 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
+ 'misc-style-selector': ['showName', 'color', 'fontSize', 'axisColor', 'axisValue'],
+ 'title-selector': [
+ 'show',
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'legend-selector': ['icon', 'orient', 'color', 'fontSize', 'hPosition', 'vPosition']
+ }
+ selectorSpec: EditorSelectorSpec = {
+ ...this['selectorSpec'],
+ 'misc-style-selector': {
+ title: `${t('chart.tooltip_axis')}`
+ }
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'drill', 'filter', 'extLabel', 'extTooltip']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.drag_block_radar_label')} / ${t('chart.dimension')}`,
+ type: 'd'
+ },
+ yAxis: {
+ name: `${t('chart.drag_block_radar_length')} / ${t('chart.quota')}`,
+ type: 'q'
+ }
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data) {
+ return
+ }
+ const data = chart.data.data
+ const baseOptions: RadarOptions = {
+ data,
+ xField: 'field',
+ yField: 'value',
+ seriesField: 'category',
+ appendPadding: [10, 10, 10, 10],
+ 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: 'active-region',
+ cfg: {
+ start: [{ trigger: 'point:mousemove', action: 'active-region:show' }],
+ end: [{ trigger: 'point:mouseleave', action: 'active-region:hide' }]
+ }
+ }
+ ]
+ }
+ const options = this.setupOptions(chart, baseOptions)
+ const { Radar: G2Radar } = await import('@antv/g2plot/esm/plots/radar')
+ const newChart = new G2Radar(container, options)
+ newChart.on('point:click', action)
+ if (options.label) {
+ newChart.on('label:click', e => {
+ action({
+ x: e.x,
+ y: e.y,
+ data: {
+ data: e.target.attrs.data
+ }
+ })
+ })
+ }
+ configPlotTooltipEvent(chart, newChart)
+ return newChart
+ }
+
+ protected configBasicStyle(chart: Chart, options: RadarOptions): RadarOptions {
+ const { radarShowPoint, radarPointSize, radarAreaColor } = parseJson(
+ chart.customAttr
+ ).basicStyle
+ const tempOptions: RadarOptions = {}
+
+ if (radarShowPoint) {
+ tempOptions['point'] = { shape: 'circle', size: radarPointSize, style: { fill: null } }
+ }
+ if (radarAreaColor) {
+ tempOptions['area'] = {}
+ }
+
+ return { ...options, ...tempOptions }
+ }
+
+ protected configLabel(chart: Chart, options: RadarOptions): RadarOptions {
+ const tmpOptions = super.configLabel(chart, options)
+ if (!tmpOptions.label) {
+ return {
+ ...tmpOptions,
+ label: false
+ }
+ }
+ const labelAttr = parseJson(chart.customAttr).label
+ const formatterMap = labelAttr.seriesLabelFormatter?.reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {})
+ tmpOptions.label.style.fill = DEFAULT_LABEL.color
+ // 自动旋转和标签自定义有冲突
+ const label = {
+ fields: [],
+ ...tmpOptions.label,
+ autoRotate: false,
+ autoHide: true,
+ 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 Group({})
+ group.addShape({
+ type: 'text',
+ attrs: {
+ data,
+ x: 0,
+ y: 0,
+ text: value,
+ textAlign: 'start',
+ textBaseline: 'top',
+ fontSize: labelCfg.fontSize,
+ fill: labelCfg.color
+ }
+ })
+ return group
+ }
+ }
+ return {
+ ...tmpOptions,
+ label
+ }
+ }
+
+ protected configAxis(chart: Chart, options: RadarOptions): RadarOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const customStyle = parseJson(chart.customStyle)
+ const basicStyle = customAttr.basicStyle
+ const misc = customStyle.misc
+ let label: any = {
+ style: {
+ fill: misc.color,
+ fontSize: misc.fontSize
+ }
+ }
+ if (!misc.showName) {
+ label = false
+ }
+ const xAxis = {
+ line: null,
+ tickLine: null,
+ label,
+ grid: {
+ line: {
+ style: {
+ stroke: misc.axisColor
+ }
+ }
+ }
+ }
+ const yAxis = {
+ label: null,
+ line: null,
+ tickLine: null,
+ grid: {
+ line: {
+ type: basicStyle.radarShape,
+ style: {
+ stroke: misc.axisColor
+ }
+ }
+ }
+ }
+ const axisValue = misc.axisValue
+ if (!axisValue?.auto) {
+ const axisYAxis = {
+ ...yAxis,
+ min: axisValue.min,
+ max: axisValue.max,
+ minLimit: axisValue.min,
+ maxLimit: axisValue.max,
+ tickCount: axisValue.splitCount
+ }
+ return {
+ ...options,
+ xAxis,
+ yAxis: axisYAxis
+ }
+ }
+ return {
+ ...options,
+ xAxis,
+ yAxis
+ }
+ }
+
+ protected configLegend(chart: Chart, options: RadarOptions): RadarOptions {
+ const optionTmp = super.configLegend(chart, options)
+ if (!optionTmp.legend) {
+ return optionTmp
+ }
+ const customStyle = parseJson(chart.customStyle)
+ let size
+ if (customStyle && customStyle.legend) {
+ size = defaults(JSON.parse(JSON.stringify(customStyle.legend)), DEFAULT_LEGEND_STYLE).size
+ } else {
+ size = DEFAULT_LEGEND_STYLE.size
+ }
+ optionTmp.legend.marker.style = style => {
+ return {
+ r: size,
+ fill: style.stroke
+ }
+ }
+ return optionTmp
+ }
+
+ protected setupOptions(chart: Chart, options: RadarOptions): RadarOptions {
+ return flow(
+ this.configTheme,
+ this.configColor,
+ this.configLabel,
+ this.configLegend,
+ this.configMultiSeriesTooltip,
+ this.configAxis,
+ this.configBasicStyle
+ )(chart, options)
+ }
+
+ constructor() {
+ super('radar', [])
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/rich-text.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/rich-text.ts
new file mode 100644
index 0000000..e0fa370
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/rich-text.ts
@@ -0,0 +1,37 @@
+import { AbstractChartView, ChartLibraryType, ChartRenderType } from '../../types'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+
+const { t } = useI18n()
+/**
+ * 富文本图表
+ */
+export class RichTextChartView extends AbstractChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'threshold',
+ 'function-cfg'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ threshold: ['tableThreshold'],
+ 'function-cfg': ['emptyDataStrategy']
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'filter']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.dimension')}`,
+ type: 'd',
+ allowEmpty: true
+ },
+ yAxis: {
+ name: `${t('chart.quota')}`,
+ type: 'q',
+ allowEmpty: true
+ }
+ }
+ constructor() {
+ super(ChartRenderType.CUSTOM, ChartLibraryType.RICH_TEXT, 'rich-text')
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/sankey-common.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/sankey-common.ts
new file mode 100644
index 0000000..d080ac6
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/sankey-common.ts
@@ -0,0 +1,40 @@
+export const SANKEY_EDITOR_PROPERTY: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'title-selector',
+ 'jump-set',
+ 'linkage'
+]
+
+export const SANKEY_EDITOR_PROPERTY_INNER: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha', 'gradient'],
+ 'label-selector': ['fontSize', 'color', 'labelFormatter'],
+ 'tooltip-selector': ['fontSize', 'color', 'tooltipFormatter'],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'function-cfg': ['slider', 'emptyDataStrategy']
+}
+
+export const SANKEY_AXIS_TYPE: AxisType[] = [
+ 'xAxis',
+ 'xAxisExt',
+ 'yAxis',
+ 'filter',
+ 'extLabel',
+ 'extTooltip'
+]
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/sankey.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/sankey.ts
new file mode 100644
index 0000000..783efa6
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/sankey.ts
@@ -0,0 +1,285 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { Sankey, SankeyOptions } from '@antv/g2plot/esm/plots/sankey'
+import { getPadding, setGradientColor } from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { cloneDeep, get } from 'lodash-es'
+import { flow, hexColorToRGBA, parseJson } from '@/data-visualization/chart/components/js/util'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+
+import { Datum } from '@antv/g2plot/esm/types/common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import {
+ SANKEY_AXIS_TYPE,
+ SANKEY_EDITOR_PROPERTY,
+ SANKEY_EDITOR_PROPERTY_INNER
+} from '@/data-visualization/chart/components/js/panel/charts/others/sankey-common'
+
+const { t } = useI18n()
+const DEFAULT_DATA = []
+
+/**
+ * 桑基图
+ */
+export class SankeyBar extends G2PlotChartView {
+ axisConfig = {
+ xAxis: {
+ name: `${t('chart.drag_block_type_axis_start')} / ${t('chart.dimension')}`,
+ limit: 1,
+ type: 'd'
+ },
+ xAxisExt: {
+ name: `${t('chart.drag_block_type_axis_end')} / ${t('chart.dimension')}`,
+ limit: 1,
+ type: 'd',
+ allowEmpty: true
+ },
+ yAxis: {
+ name: `${t('chart.chart_data')} / ${t('chart.quota')}`,
+ limit: 1,
+ type: 'q'
+ }
+ }
+ properties = SANKEY_EDITOR_PROPERTY
+ propertyInner = {
+ ...SANKEY_EDITOR_PROPERTY_INNER,
+ 'label-selector': ['color', 'fontSize'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'tooltipFormatter', 'show']
+ }
+ axis: AxisType[] = [...SANKEY_AXIS_TYPE]
+ protected baseOptions: SankeyOptions = {
+ data: [],
+ sourceField: 'source',
+ targetField: 'target',
+ weightField: 'value',
+ rawFields: ['dimensionList', 'quotaList'],
+ 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: 'interval:mousemove', action: 'tooltip:show' }],
+ end: [{ trigger: 'interval:mouseleave', action: 'tooltip:hide' }]
+ }
+ },
+ {
+ type: 'active-region',
+ cfg: {
+ start: [{ trigger: 'interval:mousemove', action: 'active-region:show' }],
+ end: [{ trigger: 'interval:mouseleave', action: 'active-region:hide' }]
+ }
+ }
+ ]
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data?.length) {
+ return
+ }
+ // data
+ const data: Array = cloneDeep(chart.data.data)
+
+ data.forEach(d => {
+ if (d.dimensionList) {
+ if (d.dimensionList[0]) {
+ d.source = d.dimensionList[0].value
+ }
+ if (d.dimensionList[1]) {
+ d.target = d.dimensionList[1].value
+ }
+ }
+ })
+
+ // options
+ const initOptions: SankeyOptions = {
+ ...this.baseOptions,
+ appendPadding: getPadding(chart),
+ data,
+ nodeSort: (a, b) => {
+ // 这里是前端自己排序
+ if (chart.yAxis && chart.yAxis[0]) {
+ if (chart.yAxis[0].sort === 'asc') {
+ return a.value - b.value
+ } else if (chart.yAxis[0].sort === 'desc') {
+ return b.value - a.value
+ }
+ }
+
+ if (chart.xAxis && chart.xAxis[0] && a.sourceLinks.length > 0) {
+ if (chart.xAxis[0].sort === 'custom_sort' && chart.xAxis[0].customSort) {
+ return (
+ chart.xAxis[0].customSort.indexOf(a.name) - chart.xAxis[0].customSort.indexOf(b.name)
+ )
+ } else if (chart.xAxis[0].sort === 'asc') {
+ return a.name.localeCompare(b.name)
+ } else if (chart.xAxis[0].sort === 'desc') {
+ return b.name.localeCompare(a.name)
+ }
+ }
+ if (chart.xAxisExt && chart.xAxisExt[0] && a.targetLinks.length > 0) {
+ if (chart.xAxisExt[0].sort === 'custom_sort' && chart.xAxisExt[0].customSort) {
+ return (
+ chart.xAxisExt[0].customSort.indexOf(a.name) -
+ chart.xAxisExt[0].customSort.indexOf(b.name)
+ )
+ } else if (chart.xAxisExt[0].sort === 'asc') {
+ return a.name.localeCompare(b.name)
+ } else if (chart.xAxisExt[0].sort === 'desc') {
+ return b.name.localeCompare(a.name)
+ }
+ }
+
+ return b.value - a.value
+ }
+ }
+
+ const options = this.setupOptions(chart, initOptions)
+ const { Sankey } = await import('@antv/g2plot/esm/plots/sankey')
+ // 开始渲染
+ const newChart = new Sankey(container, options)
+
+ newChart.on('edge:click', action)
+
+ return newChart
+ }
+
+ protected configTooltip(chart: Chart, options: SankeyOptions): SankeyOptions {
+ let tooltip
+ let customAttr: DeepPartial
+ if (chart.customAttr) {
+ customAttr = parseJson(chart.customAttr)
+ // tooltip
+ if (customAttr.tooltip) {
+ const t = JSON.parse(JSON.stringify(customAttr.tooltip))
+ if (t.show) {
+ tooltip = {
+ showTitle: false,
+ showMarkers: false,
+ shared: false,
+ // 内置:node 不显示 tooltip,edge 显示 tooltip
+ showContent: items => {
+ return !get(items, [0, 'data', 'isNode'])
+ },
+ formatter: (datum: Datum) => {
+ const { source, target, value } = datum
+ return {
+ name: source + ' -> ' + target,
+ value: valueFormatter(value, t.tooltipFormatter)
+ }
+ }
+ }
+ } else {
+ tooltip = false
+ }
+ }
+ }
+ return { ...options, tooltip }
+ }
+
+ protected configBasicStyle(chart: Chart, options: SankeyOptions): SankeyOptions {
+ const basicStyle = parseJson(chart.customAttr).basicStyle
+
+ let color = basicStyle.colors
+ color = color.map(ele => {
+ const tmp = hexColorToRGBA(ele, basicStyle.alpha)
+ if (basicStyle.gradient) {
+ return setGradientColor(tmp, true)
+ } else {
+ return tmp
+ }
+ })
+
+ options = {
+ ...options,
+ color
+ }
+ return options
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const { customAttr, senior } = chart
+ const { label } = customAttr
+ if (!['left', 'middle', 'right'].includes(label.position)) {
+ label.position = 'middle'
+ }
+ senior.functionCfg.emptyDataStrategy = 'ignoreData'
+ return chart
+ }
+
+ protected configLabel(chart: Chart, options: SankeyOptions): SankeyOptions {
+ const labelAttr = parseJson(chart.customAttr).label
+ if (labelAttr.show) {
+ const layout = []
+ if (!labelAttr.fullDisplay) {
+ layout.push(...[{ type: 'hide-overlap' }, { type: 'limit-in-canvas' }])
+ }
+ const label = {
+ //...tmpOptions.label,
+ formatter: ({ name }) => name,
+ callback: (x: number[]) => {
+ const isLast = x[1] === 1 // 最后一列靠边的节点
+ return {
+ style: {
+ fill: labelAttr.color,
+ fontSize: labelAttr.fontSize,
+ textAlign: isLast ? 'end' : 'start',
+ fontFamily: chart.fontFamily
+ },
+ offsetX: isLast ? -8 : 8
+ }
+ },
+ layout
+ }
+ return {
+ ...options,
+ label
+ }
+ } else {
+ return {
+ ...options,
+ label: false
+ }
+ }
+ }
+
+ protected setupOptions(chart: Chart, options: SankeyOptions): SankeyOptions {
+ return flow(
+ this.configTheme,
+ this.configBasicStyle,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configSlider,
+ this.configAnalyseHorizontal,
+ this.configEmptyDataStrategy
+ )(chart, options)
+ }
+
+ constructor(name = 'sankey') {
+ super(name, DEFAULT_DATA)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/scatter.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/scatter.ts
new file mode 100644
index 0000000..a06684e
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/scatter.ts
@@ -0,0 +1,298 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { ScatterOptions, Scatter as G2Scatter } from '@antv/g2plot/esm/plots/scatter'
+import { flow, parseJson } from '../../../util'
+import { valueFormatter } from '../../../formatter'
+import {
+ configPlotTooltipEvent,
+ getPadding,
+ getTooltipContainer,
+ TOOLTIP_TPL
+} from '../../common/common_antv'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { defaults, isEmpty } from 'lodash-es'
+import { DEFAULT_LEGEND_STYLE } from '@/data-visualization/chart/components/editor/util/chart'
+
+const { t } = useI18n()
+/**
+ * 散点图
+ */
+export class Scatter extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'x-axis-selector',
+ 'y-axis-selector',
+ 'title-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'legend-selector',
+ 'jump-set',
+ 'linkage'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'basic-style-selector': [
+ 'colors',
+ 'alpha',
+ 'scatterSymbol',
+ 'scatterSymbolSize',
+ 'seriesColor'
+ ],
+ 'label-selector': ['fontSize', 'color', 'labelFormatter'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
+ 'x-axis-selector': [
+ 'position',
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisLine',
+ 'splitLine',
+ 'axisForm',
+ 'axisLabel'
+ ],
+ 'y-axis-selector': [
+ 'position',
+ 'name',
+ 'color',
+ 'fontSize',
+ 'axisValue',
+ 'axisLine',
+ 'splitLine',
+ 'axisForm',
+ 'axisLabel',
+ 'axisLabelFormatter'
+ ],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'legend-selector': ['icon', 'orient', 'color', 'fontSize', 'hPosition', 'vPosition']
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'extBubble', 'filter', 'drill', 'extLabel', 'extTooltip']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.drag_block_type_axis')} / ${t('chart.dimension')}`,
+ type: 'd'
+ },
+ yAxis: {
+ ...this['axisConfig'].yAxis,
+ limit: undefined,
+ allowEmpty: false
+ },
+ extBubble: {
+ name: `${t('chart.bubble_size')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1,
+ allowEmpty: true
+ }
+ }
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data) {
+ return
+ }
+ const data = chart.data.data
+ const baseOptions: ScatterOptions = {
+ data: data,
+ xField: 'field',
+ yField: 'value',
+ colorField: 'category',
+ meta: {
+ field: {
+ type: 'cat'
+ }
+ },
+ appendPadding: getPadding(chart),
+ 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'
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ const options = this.setupOptions(chart, baseOptions)
+ const { Scatter: G2Scatter } = await import('@antv/g2plot/esm/plots/scatter')
+ const newChart = new G2Scatter(container, options)
+ newChart.on('point:click', action)
+ configPlotTooltipEvent(chart, newChart)
+ return newChart
+ }
+
+ protected configBasicStyle(chart: Chart, options: ScatterOptions): ScatterOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const basicStyle = customAttr.basicStyle
+ if (chart.extBubble?.length) {
+ return {
+ ...options,
+ size: [5, 30],
+ sizeField: 'popSize',
+ shape: basicStyle.scatterSymbol
+ }
+ }
+ return {
+ ...options,
+ size: basicStyle.scatterSymbolSize,
+ shape: basicStyle.scatterSymbol
+ }
+ }
+
+ protected configYAxis(chart: Chart, options: ScatterOptions): ScatterOptions {
+ const tmpOptions = super.configYAxis(chart, options)
+ if (!tmpOptions.yAxis) {
+ return tmpOptions
+ }
+ const yAxis = parseJson(chart.customStyle).yAxis
+ if (tmpOptions.yAxis.label) {
+ tmpOptions.yAxis.label.formatter = value => {
+ return valueFormatter(value, yAxis.axisLabelFormatter)
+ }
+ }
+ const axisValue = yAxis.axisValue
+ if (!axisValue?.auto) {
+ const axis = {
+ yAxis: {
+ ...tmpOptions.yAxis,
+ min: axisValue.min,
+ max: axisValue.max,
+ minLimit: axisValue.min,
+ maxLimit: axisValue.max,
+ tickCount: axisValue.splitCount
+ }
+ }
+ return { ...tmpOptions, ...axis }
+ }
+ return tmpOptions
+ }
+
+ protected configTooltip(chart: Chart, options: ScatterOptions): ScatterOptions {
+ const customAttr: DeepPartial = parseJson(chart.customAttr)
+ const tooltipAttr = customAttr.tooltip
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: false
+ }
+ }
+ const formatterMap = tooltipAttr.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next.seriesId] = next
+ return pre
+ }, {}) as Record
+ const VALID_ITEMS = ['value', 'popSize']
+ const tooltip: ScatterOptions['tooltip'] = {
+ 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 => VALID_ITEMS.includes(item.name))
+ .forEach(item => {
+ let formatter = formatterMap[`${item.data.quotaList[0].id}-yAxis`]
+ if (item.name === 'popSize') {
+ formatter = formatterMap[`${item.data.quotaList[1].id}-extBubble`]
+ }
+ if (!formatter) {
+ return
+ }
+ const value = valueFormatter(parseFloat(item.value as string), formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ 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
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+
+ protected configLegend(chart: Chart, options: ScatterOptions): ScatterOptions {
+ const optionTmp = super.configLegend(chart, options)
+ if (!optionTmp.legend) {
+ return optionTmp
+ }
+ const customStyle = parseJson(chart.customStyle)
+ let size
+ if (customStyle && customStyle.legend) {
+ size = defaults(JSON.parse(JSON.stringify(customStyle.legend)), DEFAULT_LEGEND_STYLE).size
+ } else {
+ size = DEFAULT_LEGEND_STYLE.size
+ }
+ optionTmp.legend.marker.style = style => {
+ return {
+ r: size,
+ fill: style.fill
+ }
+ }
+ return optionTmp
+ }
+
+ protected setupOptions(chart: Chart, options: ScatterOptions) {
+ return flow(
+ this.configTheme,
+ this.configColor,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend,
+ this.configXAxis,
+ this.configYAxis,
+ this.configAnalyse,
+ this.configSlider,
+ this.configBasicStyle
+ )(chart, options)
+ }
+
+ constructor() {
+ super('scatter', [])
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/treemap.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/treemap.ts
new file mode 100644
index 0000000..98f0364
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/treemap.ts
@@ -0,0 +1,246 @@
+import { TreemapOptions, Treemap as G2Treemap } from '@antv/g2plot/esm/plots/treemap'
+import { G2PlotChartView, G2PlotDrawOptions } from '../../types/impl/g2plot'
+import { flow, parseJson, setUpSingleDimensionSeriesColor } from '../../../util'
+import { getPadding, getTooltipSeriesTotalMap } from '../../common/common_antv'
+import { valueFormatter } from '../../../formatter'
+import { Label } from '@antv/g2plot/lib/types/label'
+import { Datum } from '@antv/g2plot/esm/types/common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import isEmpty from 'lodash-es/isEmpty'
+
+const { t } = useI18n()
+
+/**
+ * 矩形树图
+ */
+export class Treemap extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'title-selector',
+ 'legend-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'jump-set',
+ 'linkage'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha', 'seriesColor'],
+ 'label-selector': ['fontSize', 'color', 'showDimension', 'showQuota', 'showProportion'],
+ 'legend-selector': ['icon', 'orient', 'fontSize', 'color', 'hPosition', 'vPosition'],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ]
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'filter', 'drill', 'extLabel', 'extTooltip']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.drag_block_treemap_label')} / ${t('chart.dimension')}`,
+ type: 'd'
+ },
+ yAxis: {
+ name: `${t('chart.drag_block_treemap_size')} / ${t('chart.quota')}`,
+ limit: 1
+ }
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data?.length) {
+ return
+ }
+ const data = chart.data.data
+ const baseOptions = {
+ data: {
+ name: 'root',
+ children: data
+ },
+ colorField: 'name',
+ appendPadding: getPadding(chart),
+ 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: 'element:mousemove', action: 'tooltip:show' }],
+ end: [{ trigger: 'element:mouseleave', action: 'tooltip:hide' }]
+ }
+ }
+ ]
+ }
+ const options = this.setupOptions(chart, baseOptions)
+ const { Treemap: G2Treemap } = await import('@antv/g2plot/esm/plots/treemap')
+ const newChart = new G2Treemap(container, options)
+ newChart.on('polygon:click', action)
+ return newChart
+ }
+ protected configTooltip(chart: Chart, options: TreemapOptions): TreemapOptions {
+ const { tooltip: tooltipAttr, label } = parseJson(chart.customAttr)
+ const { yAxis } = chart
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: false
+ }
+ }
+ const reserveDecimalCount = label.reserveDecimalCount
+ const seriesTotalMap = getTooltipSeriesTotalMap(options.data.children)
+ const formatterMap = tooltipAttr.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {}) as Record
+ const tooltip: TreemapOptions['tooltip'] = {
+ showTitle: true,
+ title: () => undefined,
+ customItems(originalItems) {
+ let tooltipItems = originalItems
+ if (tooltipAttr.seriesTooltipFormatter?.length) {
+ tooltipItems = originalItems.filter(item => formatterMap[item.data.quotaList[0].id])
+ }
+ const result = []
+ const head = originalItems[0]
+ tooltipItems.forEach(item => {
+ const formatter = formatterMap[item.data.quotaList[0].id] ?? yAxis[0]
+ const value = valueFormatter(parseFloat(item.value as string), formatter.formatterCfg)
+ // sync with label
+ const percent = (
+ Math.round((item.data.value / item.data.path[1].value) * 10000) / 100
+ ).toFixed(reserveDecimalCount)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ ...item, name, value: `${value ?? ''} (${percent}%)` })
+ })
+ head.data.dynamicTooltipValue?.forEach(item => {
+ const formatter = formatterMap[item.fieldId]
+ if (formatter) {
+ const total = seriesTotalMap[item.fieldId]
+ // sync with label
+ const percent = (Math.round((item.value / total) * 10000) / 100).toFixed(
+ reserveDecimalCount
+ )
+ const value = valueFormatter(parseFloat(item.value), formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ color: 'grey', name, value: `${value ?? ''} (${percent}%)` })
+ }
+ })
+ return result
+ }
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+ protected configLabel(chart: Chart, options: TreemapOptions): TreemapOptions {
+ const customAttr: DeepPartial = parseJson(chart.customAttr)
+ const labelAttr = customAttr.label
+ if (!labelAttr.show) {
+ return {
+ ...options,
+ label: false
+ }
+ }
+ const label: Label = {
+ style: {
+ fill: labelAttr.color,
+ fontSize: labelAttr.fontSize
+ },
+ formatter: function (param: Datum) {
+ let res = param.value
+ const contentItems = []
+ if (labelAttr.showDimension) {
+ contentItems.push(param.field)
+ }
+ if (labelAttr.showQuota) {
+ contentItems.push(valueFormatter(param.value, labelAttr.quotaLabelFormatter))
+ }
+ if (labelAttr.showProportion) {
+ const percentage = `${(((param.value / param.parent.value) * 10000) / 100).toFixed(
+ labelAttr.reserveDecimalCount
+ )}%`
+ contentItems.push(percentage)
+ }
+ res = contentItems.join('\n')
+ return res
+ }
+ }
+ if (labelAttr.fullDisplay) {
+ label.layout = [{ type: 'limit-in-plot' }]
+ }
+ return { ...options, label }
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const { customAttr, customStyle } = chart
+ const { label } = customAttr
+ customAttr.label = {
+ ...label,
+ show: true,
+ showDimension: true,
+ showProportion: true,
+ reserveDecimalCount: 2
+ }
+ const { legend } = customStyle
+ legend.show = false
+ return chart
+ }
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ data?.sort((a, b) => b.value - a.value)
+ return setUpSingleDimensionSeriesColor(chart, data)
+ }
+ protected configColor(chart: Chart, options: TreemapOptions): TreemapOptions {
+ const data = options.data.children
+ data.sort((a, b) => b.value - a.value)
+ const tmpOptions = this.configSingleDimensionColor(chart, { ...options, data })
+ return { ...options, color: tmpOptions.color }
+ }
+ protected setupOptions(chart: Chart, options: TreemapOptions): TreemapOptions {
+ return flow(
+ this.configTheme,
+ this.configColor,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend
+ )(chart, options, {}, this)
+ }
+
+ constructor() {
+ super('treemap', [])
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/others/word-cloud.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/others/word-cloud.ts
new file mode 100644
index 0000000..0baf181
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/others/word-cloud.ts
@@ -0,0 +1,184 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { WordCloud as G2WordCloud, WordCloudOptions } from '@antv/g2plot/esm/plots/word-cloud'
+import {
+ filterChartDataByRange,
+ flow,
+ getMaxAndMinValueByData,
+ parseJson
+} from '@/data-visualization/chart/components/js/util'
+import { getPadding } from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { isEmpty } from 'lodash-es'
+import { DEFAULT_MISC } from '@/data-visualization/chart/components/editor/util/chart'
+
+const { t } = useI18n()
+const DEFAULT_DATA = []
+/**
+ * 词云图
+ */
+export class WordCloud extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'basic-style-selector',
+ 'background-overall-component',
+ 'border-style',
+ 'title-selector',
+ 'tooltip-selector',
+ 'misc-selector',
+ 'jump-set',
+ 'linkage'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'basic-style-selector': ['colors', 'alpha'],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'misc-selector': ['wordSizeRange', 'wordSpacing', 'wordCloudAxisValueRange'],
+ 'tooltip-selector': ['color', 'fontSize', 'backgroundColor', 'seriesTooltipFormatter', 'show']
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'filter']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.drag_block_word_cloud_label')} / ${t('chart.dimension_or_quota')}`,
+ type: 'd',
+ limit: 1
+ },
+ yAxis: {
+ name: `${t('chart.drag_block_word_cloud_size')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+ setDataRange = (action, maxValue, minValue) => {
+ action({
+ from: 'word-cloud',
+ data: {
+ max: maxValue,
+ min: minValue
+ }
+ })
+ }
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (chart?.data) {
+ // data
+ let data = chart.data.data
+ const { misc } = parseJson(chart.customAttr)
+ let minValue = 0
+ let maxValue = 0
+ if (
+ !misc.wordCloudAxisValueRange?.auto &&
+ misc.wordCloudAxisValueRange?.fieldId === chart.yAxis[0].id
+ ) {
+ minValue = misc.wordCloudAxisValueRange.min
+ maxValue = misc.wordCloudAxisValueRange.max
+ }
+ getMaxAndMinValueByData(data ?? [], 'value', maxValue, minValue, (max, min) => {
+ maxValue = max
+ minValue = min
+ })
+ data = filterChartDataByRange(data ?? [], maxValue, minValue)
+ // options
+ const initOptions: WordCloudOptions = {
+ data: data,
+ wordField: 'field',
+ weightField: 'value',
+ colorField: 'field',
+ wordStyle: {
+ fontFamily: chart.fontFamily ? chart.fontFamily : 'Verdana',
+ fontSize: (misc.wordSizeRange ?? DEFAULT_MISC.wordSizeRange) as [number, number],
+ rotation: [0, 0],
+ padding: misc.wordSpacing ?? DEFAULT_MISC.wordSpacing
+ },
+ random: () => 0.5,
+ appendPadding: getPadding(chart),
+ legend: false,
+ interactions: []
+ }
+ const options = this.setupOptions(chart, initOptions)
+ const { WordCloud: G2WordCloud } = await import('@antv/g2plot/esm/plots/word-cloud')
+ const newChart = new G2WordCloud(container, options)
+ newChart.on('click', () => {
+ this.setDataRange(action, maxValue, minValue)
+ })
+ newChart.on('afterrender', () => {
+ this.setDataRange(action, maxValue, minValue)
+ })
+ newChart.on('point:click', param => {
+ action({ x: param.x, y: param.y, data: { data: param.data.data.datum } })
+ })
+ return newChart
+ }
+ }
+
+ protected configTooltip(chart: Chart, options: WordCloudOptions): WordCloudOptions {
+ 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
+ const tooltip: WordCloudOptions['tooltip'] = {
+ showTitle: true,
+ title: () => undefined,
+ customItems(originalItems) {
+ let tooltipItems = originalItems
+ if (tooltipAttr.seriesTooltipFormatter?.length) {
+ tooltipItems = originalItems.filter(item => formatterMap[item.data.datum.quotaList[0].id])
+ }
+ const result = []
+ const head = originalItems[0]
+ tooltipItems.forEach(item => {
+ const formatter = formatterMap[item.data.datum.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.data.datum.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 setupOptions(chart: Chart, options: WordCloudOptions): WordCloudOptions {
+ return flow(this.configTheme, this.configTooltip)(chart, options)
+ }
+
+ constructor() {
+ super('word-cloud', DEFAULT_DATA)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/pie/common.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/pie/common.ts
new file mode 100644
index 0000000..f8b808d
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/pie/common.ts
@@ -0,0 +1,63 @@
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+
+const { t } = useI18n()
+
+export const PIE_EDITOR_PROPERTY: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'title-selector',
+ 'legend-selector',
+ 'label-selector',
+ 'tooltip-selector',
+ 'jump-set',
+ 'linkage'
+]
+export const PIE_EDITOR_PROPERTY_INNER: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'border-style': ['all'],
+ 'label-selector': [
+ 'fontSize',
+ 'color',
+ 'rPosition',
+ 'showDimension',
+ 'showQuota',
+ 'showProportion'
+ ],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter', 'show'],
+ 'basic-style-selector': ['colors', 'alpha', 'radius', 'seriesColor'],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'legend-selector': ['icon', 'orient', 'fontSize', 'color', 'hPosition', 'vPosition']
+}
+
+export const PIE_AXIS_TYPE: AxisType[] = [
+ 'xAxis',
+ 'yAxis',
+ 'drill',
+ 'filter',
+ 'extLabel',
+ 'extTooltip'
+]
+
+export const PIE_AXIS_CONFIG: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.drag_block_pie_label')} / ${t('chart.dimension')}`,
+ type: 'd'
+ },
+ yAxis: {
+ name: `${t('chart.drag_block_pie_angle')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/pie/pie.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/pie/pie.ts
new file mode 100644
index 0000000..0263080
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/pie/pie.ts
@@ -0,0 +1,356 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { Pie as G2Pie, PieOptions } from '@antv/g2plot/esm/plots/pie'
+import {
+ flow,
+ hexColorToRGBA,
+ parseJson,
+ setUpSingleDimensionSeriesColor
+} from '@/data-visualization/chart/components/js/util'
+import {
+ configPlotTooltipEvent,
+ getPadding,
+ getTooltipContainer,
+ getTooltipSeriesTotalMap,
+ TOOLTIP_TPL
+} from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import {
+ PIE_AXIS_CONFIG,
+ PIE_AXIS_TYPE,
+ PIE_EDITOR_PROPERTY,
+ PIE_EDITOR_PROPERTY_INNER
+} from '@/data-visualization/chart/components/js/panel/charts/pie/common'
+import type { Datum } from '@antv/g2plot/esm/types/common'
+import { add } from 'mathjs'
+import isEmpty from 'lodash-es/isEmpty'
+import { cloneDeep } from 'lodash-es'
+
+const DEFAULT_DATA = []
+export class Pie extends G2PlotChartView {
+ axis: AxisType[] = PIE_AXIS_TYPE
+ properties = PIE_EDITOR_PROPERTY
+ propertyInner: EditorPropertyInner = {
+ ...PIE_EDITOR_PROPERTY_INNER,
+ 'basic-style-selector': ['colors', 'alpha', 'radius', 'topN', 'seriesColor']
+ }
+ axisConfig = PIE_AXIS_CONFIG
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart.data?.data?.length) {
+ return
+ }
+ // data
+ const data = chart.data.data
+ // custom color
+ const customAttr = parseJson(chart.customAttr)
+ const color = customAttr.basicStyle.colors.map(i =>
+ hexColorToRGBA(i, customAttr.basicStyle.alpha)
+ )
+ // options
+ const initOptions: PieOptions = {
+ data: data,
+ angleField: 'value',
+ colorField: 'field',
+ appendPadding: getPadding(chart),
+ color,
+ animation: false,
+ pieStyle: {
+ lineWidth: 0
+ },
+ statistic: {
+ title: false,
+ content: {
+ style: {
+ whiteSpace: 'pre-wrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis'
+ },
+ content: ''
+ }
+ },
+ 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: 'interval:mousemove', action: 'tooltip:show' }],
+ end: [{ trigger: 'interval:mouseleave', action: 'tooltip:hide' }]
+ }
+ },
+ {
+ type: 'active-region',
+ cfg: {
+ start: [{ trigger: 'interval:mousemove', action: 'active-region:show' }],
+ end: [{ trigger: 'interval:mouseleave', action: 'active-region:hide' }]
+ }
+ }
+ ],
+ meta: {
+ field: {
+ type: 'cat'
+ }
+ }
+ }
+ const options = this.setupOptions(chart, initOptions)
+ const { Pie: G2Pie } = await import('@antv/g2plot/esm/plots/pie')
+ const newChart = new G2Pie(container, options)
+ newChart.on('interval:click', action)
+ configPlotTooltipEvent(chart, newChart)
+ return newChart
+ }
+
+ protected configLabel(chart: Chart, options: PieOptions): PieOptions {
+ const { label: labelAttr } = parseJson(chart.customAttr)
+ if (!labelAttr?.show) {
+ return {
+ ...options,
+ label: false
+ }
+ }
+ const layout = []
+ let textAlign = undefined
+ if (labelAttr.position === 'inner') {
+ textAlign = 'center'
+ if (labelAttr.fullDisplay) {
+ layout.push({ type: 'limit-in-plot' })
+ } else {
+ layout.push({ type: 'limit-in-canvas' })
+ layout.push({ type: 'hide-overlap' })
+ }
+ } else {
+ if (!labelAttr.fullDisplay) {
+ layout.push({ type: 'limit-in-plot' })
+ }
+ }
+ let labelType = labelAttr.position === 'outer' ? 'spider' : labelAttr.position
+ if (layout.length === 0) {
+ labelType = 'no'
+ }
+ const label = {
+ type: labelType,
+ textAlign,
+ layout,
+ autoRotate: false,
+ style: {
+ fill: labelAttr.color,
+ fontSize: labelAttr.fontSize
+ },
+ formatter: (param: Datum) => {
+ let res = param.value
+ const contentItems = []
+ if (labelAttr.showDimension) {
+ contentItems.push(param.field)
+ }
+ if (labelAttr.showQuota) {
+ contentItems.push(valueFormatter(param.value, labelAttr.quotaLabelFormatter))
+ }
+ if (labelAttr.showProportion) {
+ const percentage = `${(Math.round(param.percent * 10000) / 100).toFixed(
+ labelAttr.reserveDecimalCount
+ )}%`
+ if (labelAttr.showDimension && labelAttr.showQuota) {
+ contentItems.push(`(${percentage})`)
+ } else {
+ contentItems.push(percentage)
+ }
+ }
+ res = contentItems.join(' ')
+ return res
+ }
+ }
+ return { ...options, label }
+ }
+
+ protected configTooltip(chart: Chart, options: PieOptions): PieOptions {
+ const { tooltip: tooltipAttr, label } = parseJson(chart.customAttr)
+ const { yAxis } = chart
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: false
+ }
+ }
+ const reserveDecimalCount = label.reserveDecimalCount
+ const seriesTotalMap = getTooltipSeriesTotalMap(options.data)
+ // trick, cal total, maybe use scale of chart in plot instance
+ const total = options.data?.reduce((pre, next) => add(pre, next.value ?? 0), 0)
+ const formatterMap = tooltipAttr.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {}) as Record
+ const tooltip: PieOptions['tooltip'] = {
+ showTitle: true,
+ title: () => undefined,
+ customItems(originalItems) {
+ let tooltipItems = originalItems
+ if (tooltipAttr.seriesTooltipFormatter?.length) {
+ tooltipItems = originalItems.filter(item => formatterMap[item.data.quotaList[0].id])
+ }
+ const result = []
+ const head = originalItems[0]
+ tooltipItems.forEach(item => {
+ const formatter = formatterMap[item.data.quotaList[0].id] ?? yAxis[0]
+ const originValue = parseFloat(item.value as string)
+ const value = valueFormatter(originValue, formatter.formatterCfg)
+ // sync with label
+ const percent = (Math.round((originValue / total) * 10000) / 100).toFixed(
+ reserveDecimalCount
+ )
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ ...item, name, value: `${value ?? ''} (${percent}%)` })
+ })
+ head.data.dynamicTooltipValue?.forEach(item => {
+ const formatter = formatterMap[item.fieldId]
+ if (formatter) {
+ const total = seriesTotalMap[item.fieldId]
+ // sync with label
+ const percent = (Math.round((item.value / total) * 10000) / 100).toFixed(
+ reserveDecimalCount
+ )
+ const value = valueFormatter(parseFloat(item.value), formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ color: 'grey', name, value: `${value ?? ''} (${percent}%)` })
+ }
+ })
+ return result
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+
+ protected configBasicStyle(chart: Chart, options: PieOptions): PieOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const { basicStyle } = customAttr
+ const { data } = options
+ if (data?.length && basicStyle.calcTopN && data.length > basicStyle.topN) {
+ data.sort((a, b) => b.value - a.value)
+ const otherItems = data.splice(basicStyle.topN)
+ const initOtherItem = {
+ ...data[0],
+ dynamicTooltipValue: [],
+ field: basicStyle.topNLabel,
+ name: basicStyle.topNLabel,
+ value: 0
+ }
+ const dynamicTotalMap: Record = {}
+ otherItems.reduce((p, n) => {
+ p.value += n.value ?? 0
+ n.dynamicTooltipValue?.forEach(val => {
+ dynamicTotalMap[val.fieldId] = (dynamicTotalMap[val.fieldId] || 0) + val.value
+ })
+ return p
+ }, initOtherItem)
+ for (const key in dynamicTotalMap) {
+ initOtherItem.dynamicTooltipValue.push({
+ fieldId: key,
+ value: dynamicTotalMap[key]
+ })
+ }
+ data.push(initOtherItem)
+ }
+ return {
+ ...options,
+ radius: basicStyle.radius / 100
+ }
+ }
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const { customAttr, customStyle } = chart
+ const { label } = customAttr
+ if (!['inner', 'outer'].includes(label.position)) {
+ label.position = 'outer'
+ }
+ customAttr.label = {
+ ...label,
+ show: true,
+ showDimension: true,
+ showProportion: true,
+ reserveDecimalCount: 2
+ }
+ const { legend } = customStyle
+ legend.show = false
+ return chart
+ }
+
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ data = cloneDeep(data)
+ const { calcTopN, topN, topNLabel } = chart.customAttr.basicStyle
+ if (data?.length && calcTopN && data.length > topN) {
+ data.sort((a, b) => b.value - a.value)
+ data.splice(topN)
+ data.push({
+ field: topNLabel,
+ value: 0
+ })
+ }
+ return setUpSingleDimensionSeriesColor(chart, data)
+ }
+
+ protected setupOptions(chart: Chart, options: PieOptions): PieOptions {
+ return flow(
+ this.configTheme,
+ this.configBasicStyle,
+ this.configSingleDimensionColor,
+ this.configLabel,
+ this.configTooltip,
+ this.configLegend
+ )(chart, options, {}, this)
+ }
+
+ constructor(name = 'pie') {
+ super(name, DEFAULT_DATA)
+ }
+}
+
+export class PieDonut extends Pie {
+ propertyInner: EditorPropertyInner = {
+ ...PIE_EDITOR_PROPERTY_INNER,
+ 'basic-style-selector': ['colors', 'alpha', 'radius', 'innerRadius', 'topN', 'seriesColor']
+ }
+ protected configBasicStyle(chart: Chart, options: PieOptions): PieOptions {
+ const tmp = super.configBasicStyle(chart, options)
+ const { basicStyle } = parseJson(chart.customAttr)
+ return {
+ ...tmp,
+ radius: basicStyle.radius / 100,
+ innerRadius: basicStyle.innerRadius / 100
+ }
+ }
+
+ constructor() {
+ super('pie-donut')
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/pie/rose.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/pie/rose.ts
new file mode 100644
index 0000000..0432ca7
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/pie/rose.ts
@@ -0,0 +1,285 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import { RoseOptions, Rose as G2Rose } from '@antv/g2plot/esm/plots/rose'
+import {
+ PIE_AXIS_CONFIG,
+ PIE_AXIS_TYPE,
+ PIE_EDITOR_PROPERTY,
+ PIE_EDITOR_PROPERTY_INNER
+} from './common'
+import {
+ configPlotTooltipEvent,
+ getPadding,
+ getTooltipContainer,
+ getTooltipSeriesTotalMap,
+ TOOLTIP_TPL
+} from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { parseJson, flow, setUpSingleDimensionSeriesColor } from '@/data-visualization/chart/components/js/util'
+import { Label } from '@antv/g2plot/lib/types/label'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { Datum } from '@antv/g2plot/esm/types/common'
+import { add } from 'mathjs'
+import isEmpty from 'lodash-es/isEmpty'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+const { t } = useI18n()
+
+export class Rose extends G2PlotChartView {
+ axis: AxisType[] = PIE_AXIS_TYPE
+ properties: EditorProperty[] = PIE_EDITOR_PROPERTY
+ propertyInner: EditorPropertyInner = PIE_EDITOR_PROPERTY_INNER
+ axisConfig: AxisConfig = {
+ ...PIE_AXIS_CONFIG,
+ yAxis: {
+ name: `${t('chart.drag_block_pie_radius')} / ${t('chart.quota')}`,
+ type: 'q',
+ limit: 1
+ }
+ }
+
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ if (!chart?.data?.data?.length) {
+ return
+ }
+ // data
+ const data = chart.data.data
+ // options
+ const baseOptions: RoseOptions = {
+ data: data,
+ xField: 'field',
+ yField: 'value',
+ seriesField: 'field',
+ appendPadding: getPadding(chart),
+ 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: 'interval:mousemove', action: 'tooltip:show' }],
+ end: [{ trigger: 'interval:mouseleave', action: 'tooltip:hide' }]
+ }
+ }
+ ],
+ meta: {
+ field: {
+ type: 'cat'
+ }
+ }
+ }
+ const options = this.setupOptions(chart, baseOptions)
+
+ const { Rose: G2Rose } = await import('@antv/g2plot/esm/plots/rose')
+ // 开始渲染
+ const plot = new G2Rose(container, options)
+
+ plot.on('interval:click', action)
+ configPlotTooltipEvent(chart, plot)
+ return plot
+ }
+
+ protected configBasicStyle(chart: Chart, options: RoseOptions): RoseOptions {
+ const { basicStyle } = parseJson(chart.customAttr)
+ return {
+ ...options,
+ radius: basicStyle.radius / 100
+ }
+ }
+
+ protected configLabel(chart: Chart, options: RoseOptions): RoseOptions {
+ const { label: labelAttr } = parseJson(chart.customAttr)
+ if (!labelAttr.show) {
+ return {
+ ...options,
+ label: false
+ }
+ }
+ const total = options.data?.reduce((pre, next) => add(pre, next.value ?? 0), 0)
+ const layout = []
+ if (!labelAttr.fullDisplay) {
+ const tmpOptions = super.configLabel(chart, options)
+ layout.push(...tmpOptions.label.layout)
+ }
+ const labelOptions: Label = {
+ autoRotate: true,
+ layout,
+ style: {
+ fill: labelAttr.color,
+ fontSize: labelAttr.fontSize
+ },
+ formatter: (param: Datum) => {
+ let res = param.value
+ const contentItems = []
+ if (labelAttr.showDimension) {
+ contentItems.push(param.field)
+ }
+ if (labelAttr.showQuota) {
+ contentItems.push(valueFormatter(param.value, labelAttr.quotaLabelFormatter))
+ }
+ if (labelAttr.showProportion) {
+ const percentage = `${(Math.round((param.value / total) * 10000) / 100).toFixed(
+ labelAttr.reserveDecimalCount
+ )}%`
+ if (labelAttr.showDimension && labelAttr.showQuota) {
+ contentItems.push(`(${percentage})`)
+ } else {
+ contentItems.push(percentage)
+ }
+ }
+ res = contentItems.join('\n')
+ return res
+ }
+ }
+ if (labelAttr.position === 'inner') {
+ labelOptions.offset = -10
+ }
+ return {
+ ...options,
+ label: labelOptions
+ }
+ }
+
+ protected configTooltip(chart: Chart, options: RoseOptions): RoseOptions {
+ const { tooltip: tooltipAttr, label } = parseJson(chart.customAttr)
+ const { yAxis } = chart
+ if (!tooltipAttr.show) {
+ return {
+ ...options,
+ tooltip: false
+ }
+ }
+ const reserveDecimalCount = label.reserveDecimalCount
+ const seriesTotalMap = getTooltipSeriesTotalMap(options.data)
+ // trick, cal total, maybe use scale of chart in plot instance
+ const total = options.data?.reduce((pre, next) => add(pre, next.value ?? 0), 0)
+ const formatterMap = tooltipAttr.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {}) as Record
+ const tooltip: RoseOptions['tooltip'] = {
+ showTitle: true,
+ title: () => undefined,
+ customItems(originalItems) {
+ let tooltipItems = originalItems
+ if (tooltipAttr.seriesTooltipFormatter?.length) {
+ tooltipItems = originalItems.filter(item => formatterMap[item.data.quotaList[0].id])
+ }
+ const result = []
+ const head = originalItems[0]
+ tooltipItems.forEach(item => {
+ const formatter = formatterMap[item.data.quotaList[0].id] ?? yAxis[0]
+ const originValue = parseFloat(item.value as string)
+ const value = valueFormatter(originValue, formatter.formatterCfg)
+ // sync with label
+ const percent = (Math.round((originValue / total) * 10000) / 100).toFixed(
+ reserveDecimalCount
+ )
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ ...item, name, value: `${value ?? ''} (${percent}%)` })
+ })
+ head.data.dynamicTooltipValue?.forEach(item => {
+ const formatter = formatterMap[item.fieldId]
+ if (formatter) {
+ const total = seriesTotalMap[item.fieldId]
+ // sync with label
+ const percent = (Math.round((item.value / total) * 10000) / 100).toFixed(
+ reserveDecimalCount
+ )
+ const value = valueFormatter(parseFloat(item.value), formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ color: 'grey', name, value: `${value ?? ''} (${percent}%)` })
+ }
+ })
+ return result
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const { customAttr, customStyle } = chart
+ const { label } = customAttr
+ if (!['inner', 'outer'].includes(label.position)) {
+ label.position = 'outer'
+ }
+ customAttr.label = {
+ ...label,
+ show: true,
+ showDimension: true,
+ showProportion: true,
+ reserveDecimalCount: 2
+ }
+ const { legend } = customStyle
+ legend.show = false
+ return chart
+ }
+
+ public setupSeriesColor(chart: ChartObj, data?: any[]): ChartBasicStyle['seriesColor'] {
+ return setUpSingleDimensionSeriesColor(chart, data)
+ }
+
+ protected setupOptions(chart: Chart, options: RoseOptions): RoseOptions {
+ return flow(
+ this.configBasicStyle,
+ this.configSingleDimensionColor,
+ this.configTheme,
+ this.configLabel,
+ this.configLegend,
+ this.configTooltip
+ )(chart, options)
+ }
+
+ constructor(name = 'pie-rose') {
+ super(name, [])
+ }
+}
+
+export class RoseDonut extends Rose {
+ propertyInner: EditorPropertyInner = {
+ ...PIE_EDITOR_PROPERTY_INNER,
+ 'basic-style-selector': ['colors', 'alpha', 'radius', 'innerRadius', 'seriesColor']
+ }
+ protected configBasicStyle(chart: Chart, options: RoseOptions): RoseOptions {
+ const customAttr = parseJson(chart.customAttr)
+ return {
+ ...options,
+ radius: customAttr.basicStyle.radius / 100,
+ innerRadius: customAttr.basicStyle.innerRadius / 100
+ }
+ }
+
+ constructor() {
+ super('pie-donut-rose')
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/table/common.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/table/common.ts
new file mode 100644
index 0000000..08b146e
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/table/common.ts
@@ -0,0 +1,58 @@
+export const TABLE_EDITOR_PROPERTY: EditorProperty[] = [
+ 'background-overall-component',
+ 'border-style',
+ 'basic-style-selector',
+ 'table-header-selector',
+ 'table-cell-selector',
+ 'title-selector',
+ 'tooltip-selector',
+ 'function-cfg',
+ 'threshold',
+ 'scroll-cfg',
+ 'jump-set',
+ 'linkage'
+]
+export const TABLE_EDITOR_PROPERTY_INNER: EditorPropertyInner = {
+ 'border-style': ['all'],
+ 'background-overall-component': ['all'],
+ 'basic-style-selector': ['tableColumnMode', 'tableBorderColor', 'tableScrollBarColor', 'alpha'],
+ 'table-header-selector': [
+ 'tableHeaderBgColor',
+ 'tableTitleFontSize',
+ 'tableHeaderFontColor',
+ 'tableTitleHeight',
+ 'tableHeaderAlign',
+ 'showIndex',
+ 'indexLabel',
+ 'showColTooltip',
+ 'showHorizonBorder',
+ 'showVerticalBorder'
+ ],
+ 'table-cell-selector': [
+ 'tableItemBgColor',
+ 'tableItemFontSize',
+ 'tableFontColor',
+ 'tableItemAlign',
+ 'tableItemHeight',
+ 'enableTableCrossBG',
+ 'tableItemSubBgColor',
+ 'showTooltip',
+ 'showHorizonBorder',
+ 'showVerticalBorder'
+ ],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'show'],
+ 'function-cfg': ['emptyDataStrategy'],
+ threshold: ['tableThreshold']
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/table/t-heatmap.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/table/t-heatmap.ts
new file mode 100644
index 0000000..8750439
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/table/t-heatmap.ts
@@ -0,0 +1,362 @@
+import {
+ G2PlotChartView,
+ G2PlotDrawOptions
+} from '@/data-visualization/chart/components/js/panel/types/impl/g2plot'
+import type { Heatmap, HeatmapOptions } from '@antv/g2plot/esm/plots/heatmap'
+import { flow, hexColorToRGBA, parseJson } from '@/data-visualization/chart/components/js/util'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { deepCopy } from '@/data-visualization/utils/utils'
+import { cloneDeep } from 'lodash-es'
+import {
+ configAxisLabelLengthLimit,
+ getPadding,
+ getXAxis,
+ getYAxis
+} from '@/data-visualization/chart/components/js/panel/common/common_antv'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+
+const { t } = useI18n()
+const DEFAULT_DATA = []
+/**
+ * 热力图
+ */
+export class TableHeatmap extends G2PlotChartView {
+ properties: EditorProperty[] = [
+ 'basic-style-selector',
+ 'background-overall-component',
+ 'label-selector',
+ 'legend-selector',
+ 'x-axis-selector',
+ 'y-axis-selector',
+ 'title-selector',
+ 'tooltip-selector',
+ 'jump-set',
+ 'linkage',
+ 'border-style'
+ ]
+ propertyInner: EditorPropertyInner = {
+ 'background-overall-component': ['all'],
+ 'basic-style-selector': ['colors'],
+ 'label-selector': ['fontSize', 'color'],
+ 'x-axis-selector': ['name', 'color', 'fontSize', 'position', 'axisLabel', 'axisLine'],
+ 'y-axis-selector': [
+ 'name',
+ 'color',
+ 'fontSize',
+ 'position',
+ 'axisLabel',
+ 'axisLine',
+ 'showLengthLimit'
+ ],
+ 'title-selector': [
+ 'title',
+ 'fontSize',
+ 'color',
+ 'hPosition',
+ 'isItalic',
+ 'isBolder',
+ 'remarkShow',
+ 'fontFamily',
+ 'letterSpace',
+ 'fontShadow'
+ ],
+ 'legend-selector': ['orient', 'color', 'fontSize', 'hPosition', 'vPosition'],
+ 'tooltip-selector': ['show', 'color', 'fontSize', 'backgroundColor'],
+ 'border-style': ['all']
+ }
+ axis: AxisType[] = ['xAxis', 'xAxisExt', 'extColor', 'filter']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.x_axis')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1
+ },
+ xAxisExt: {
+ name: `${t('chart.y_axis')} / ${t('chart.dimension')}`,
+ type: 'd',
+ limit: 1
+ },
+ extColor: {
+ name: `${t('chart.color')} / ${t('chart.dimension_or_quota')}`,
+ limit: 1
+ }
+ }
+ protected getDefaultLength = (chart, l) => {
+ const containerDom = document.getElementById(chart.container)
+ const containerHeight = containerDom?.clientHeight || 100
+ const containerWidth = containerDom?.clientWidth || 100
+ let defaultLength = containerHeight - containerHeight * 0.5
+ if (l.orient !== 'vertical') {
+ defaultLength = containerWidth - containerWidth * 0.5
+ }
+ return defaultLength
+ }
+ protected sortData = (fieldObj, data) => {
+ const { deType, sort, customSort } = fieldObj
+
+ if (sort === 'desc') {
+ if (deType === 0) {
+ return data.sort().reverse()
+ } else {
+ return data.sort((a, b) => b - a)
+ }
+ } else if (sort === 'asc') {
+ if (deType === 0) {
+ return data.sort()
+ } else {
+ return data.sort((a, b) => a - b)
+ }
+ }
+
+ // 如果没有指定排序方式,直接返回原始数据或 customSort
+ return customSort && customSort.length > 0 ? customSort : data
+ }
+ async drawChart(drawOptions: G2PlotDrawOptions): Promise {
+ const { chart, container, action } = drawOptions
+ const xAxis = deepCopy(chart.xAxis)
+ const xAxisExt = deepCopy(chart.xAxisExt)
+ const extColor = deepCopy(chart.extColor)
+ if (!xAxis?.length || !xAxisExt?.length || !extColor?.length) {
+ return
+ }
+ const xField = xAxis[0].dataeaseName
+ const xFieldExt = xAxisExt[0].dataeaseName
+ const extColorField = extColor[0].dataeaseName
+ // data
+ const data = cloneDeep(chart.data.tableRow)
+ data.forEach(i => {
+ Object.keys(i).forEach(key => {
+ if (key === '*') {
+ i['@'] = i[key]
+ }
+ })
+ })
+
+ // options
+ const initOptions: HeatmapOptions = {
+ data: data,
+ xField: xField,
+ yField: xFieldExt,
+ colorField: extColorField === '*' ? '@' : extColorField,
+ appendPadding: getPadding(chart),
+ meta: {
+ [xField]: {
+ type: 'cat',
+ values: this.sortData(xAxis[0], [...new Set(data.map(i => i[[xField]]))])
+ },
+ [xFieldExt]: {
+ type: 'cat',
+ values: this.sortData(xAxisExt[0], [...new Set(data.map(i => i[[xFieldExt]]))]).reverse()
+ }
+ },
+ legend: {
+ layout: 'vertical',
+ position: 'right',
+ slidable: true,
+ label: {
+ align: 'left',
+ spacing: 10
+ }
+ }
+ }
+ chart.container = container
+ const options = this.setupOptions(chart, initOptions)
+ const { Heatmap } = await import('@antv/g2plot/esm/plots/heatmap')
+ const newChart = new Heatmap(container, options)
+ newChart.on('plot:click', param => {
+ if (!param.data?.data) {
+ return
+ }
+ const pointData = param.data.data
+ const dimensionList = []
+ chart.data.fields.forEach(item => {
+ Object.keys(pointData).forEach(key => {
+ if (key.startsWith('f_') && item.dataeaseName === key) {
+ dimensionList.push({
+ id: item.id,
+ dataeaseName: item.dataeaseName,
+ value: pointData[key]
+ })
+ }
+ })
+ })
+ action({
+ x: param.data.x,
+ y: param.data.y,
+ data: {
+ data: {
+ ...param.data.data,
+ value: dimensionList[1].value,
+ name: dimensionList[1].id,
+ dimensionList: dimensionList,
+ quotaList: [dimensionList[1]]
+ }
+ }
+ })
+ })
+ newChart.on('afterrender', ev => {
+ const l = JSON.parse(JSON.stringify(parseJson(chart.customStyle).legend))
+ if (l.show) {
+ const rail = ev.view.getController('legend').option[extColor[0].dataeaseName]?.['rail']
+ if (rail) {
+ rail.defaultLength = this.getDefaultLength(chart, l)
+ }
+ }
+ })
+ configAxisLabelLengthLimit(chart, newChart)
+ return newChart
+ }
+
+ protected configBasicStyle(chart: Chart, options: HeatmapOptions): HeatmapOptions {
+ const basicStyle = parseJson(chart.customAttr).basicStyle
+ const color = basicStyle.colors?.map(ele => {
+ return hexColorToRGBA(ele, basicStyle.alpha)
+ })
+ return {
+ ...options,
+ color
+ }
+ }
+ protected configTooltip(chart: Chart, options: HeatmapOptions): HeatmapOptions {
+ let tooltip
+ let customAttr: DeepPartial
+ if (chart.customAttr) {
+ customAttr = parseJson(chart.customAttr)
+ // tooltip
+ if (customAttr.tooltip) {
+ const extColor = deepCopy(chart.extColor)
+ const xAxisExt = deepCopy(chart.xAxisExt)
+ const tooltipFiledList = [xAxisExt, extColor]
+ const t = JSON.parse(JSON.stringify(customAttr.tooltip))
+ if (t.show) {
+ tooltip = {
+ showTitle: true,
+ customItems(originalItems) {
+ const items = []
+ const createItem = (fieldObj, items, originalItems) => {
+ const name = fieldObj?.chartShowName ? fieldObj?.chartShowName : fieldObj?.name
+ let value = originalItems[0].data[fieldObj.dataeaseName]
+ if (!isNaN(Number(value))) {
+ value = valueFormatter(value, fieldObj?.formatterCfg)
+ }
+ items.push({
+ ...originalItems[0],
+ name: name,
+ value: value
+ })
+ }
+ tooltipFiledList.forEach(field => {
+ createItem(field[0], items, originalItems)
+ })
+ return items
+ }
+ }
+ } else {
+ tooltip = false
+ }
+ }
+ }
+ return {
+ ...options,
+ tooltip
+ }
+ }
+
+ protected configXAxis(chart: Chart, options: HeatmapOptions): HeatmapOptions {
+ const xAxis = getXAxis(chart)
+ return {
+ ...options,
+ xAxis: xAxis ? { ...xAxis, grid: null } : false
+ }
+ }
+
+ protected configYAxis(chart: Chart, options: HeatmapOptions): HeatmapOptions {
+ const yAxis = getYAxis(chart)
+ return {
+ ...options,
+ yAxis: yAxis ? { ...yAxis, grid: null } : false
+ }
+ }
+
+ protected configLegend(chart: Chart, options: HeatmapOptions): HeatmapOptions {
+ const tmpOptions = super.configLegend(chart, options)
+ if (tmpOptions.legend) {
+ const l = JSON.parse(JSON.stringify(parseJson(chart.customStyle).legend))
+ tmpOptions.legend.slidable = true
+ tmpOptions.legend.minHeight = 10
+ tmpOptions.legend.minWidth = 10
+ tmpOptions.legend.maxHeight = 600
+ tmpOptions.legend.maxWidth = 600
+ const containerDom = document.getElementById(chart.container)
+ const containerHeight = containerDom?.clientHeight || 100
+ const containerWidth = containerDom?.clientWidth || 100
+ let defaultLength = containerHeight - containerHeight * 0.5
+ if (l.orient === 'vertical') {
+ tmpOptions.legend.offsetY = -5
+ } else {
+ defaultLength = containerWidth - containerWidth * 0.5
+ }
+ tmpOptions.legend.rail = { defaultLength: defaultLength }
+ tmpOptions.legend.label = {
+ spacing: 10,
+ style: {
+ fill: l.color,
+ fontSize: l.fontSize
+ }
+ }
+ }
+ return tmpOptions
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.customStyle.legend.orient = 'vertical'
+ chart.customStyle.legend.vPosition = 'center'
+ chart.customStyle.legend.hPosition = 'right'
+ chart.customStyle.legend['rail'] = { defaultLength: 100 }
+ return chart
+ }
+
+ protected configLabel(chart: Chart, options: HeatmapOptions): HeatmapOptions {
+ const tmpOptions = super.configLabel(chart, options)
+ if (tmpOptions.label) {
+ const extColor = deepCopy(chart.extColor)
+ const layout = []
+ if (!tmpOptions.label.fullDisplay) {
+ layout.push(...tmpOptions.label.layout)
+ }
+ const label = {
+ ...tmpOptions.label,
+ position: 'middle',
+ layout,
+ formatter: data => {
+ const value = data[extColor[0]?.dataeaseName]
+ if (!isNaN(Number(value))) {
+ return valueFormatter(value, extColor[0]?.formatterCfg)
+ }
+ return value
+ }
+ }
+ return {
+ ...tmpOptions,
+ label
+ }
+ }
+ return tmpOptions
+ }
+
+ protected setupOptions(chart: Chart, options: HeatmapOptions): HeatmapOptions {
+ return flow(
+ this.configTheme,
+ this.configXAxis,
+ this.configYAxis,
+ this.configBasicStyle,
+ this.configLegend,
+ this.configTooltip,
+ this.configLabel
+ )(chart, options)
+ }
+
+ constructor() {
+ super('t-heatmap', DEFAULT_DATA)
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-info.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-info.ts
new file mode 100644
index 0000000..0485bcd
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-info.ts
@@ -0,0 +1,510 @@
+import {
+ type LayoutResult,
+ S2DataConfig,
+ S2Event,
+ S2Options,
+ S2Theme,
+ ScrollbarPositionType,
+ TableColCell,
+ TableSheet,
+ ViewMeta
+} from '@antv/s2'
+import { formatterItem, valueFormatter } from '../../../formatter'
+import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util'
+import { S2ChartView, S2DrawOptions } from '../../types/impl/s2'
+import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { isEqual, isNumber, merge } from 'lodash-es'
+import {
+ copyContent,
+ CustomDataCell,
+ CustomTableColCell,
+ getRowIndex,
+ calculateHeaderHeight,
+ SortTooltip,
+ configSummaryRow,
+ summaryRowStyle,
+ configEmptyDataStyle,
+ getLeafNodes,
+ getColumns
+} from '@/data-visualization/chart/components/js/panel/common/common_table'
+
+const { t } = useI18n()
+class ImageCell extends CustomDataCell {
+ protected drawTextShape(): void {
+ const img = new Image()
+ const { x, y, width, height, fieldValue } = this.meta
+ img.src = fieldValue as string
+ img.setAttribute('crossOrigin', 'anonymous')
+ img.onload = () => {
+ !this.cfg.children && (this.cfg.children = [])
+ const { width: imgWidth, height: imgHeight } = img
+ const ratio = Math.max(imgWidth / width, imgHeight / height)
+ // 不铺满,部分留白
+ const imgShowWidth = (imgWidth / ratio) * 0.8
+ const imgShowHeight = (imgHeight / ratio) * 0.8
+ this.textShape = this.addShape('image', {
+ attrs: {
+ x: x + (imgShowWidth < width ? (width - imgShowWidth) / 2 : 0),
+ y: y + (imgShowHeight < height ? (height - imgShowHeight) / 2 : 0),
+ width: imgShowWidth,
+ height: imgShowHeight,
+ img
+ }
+ })
+ }
+ }
+}
+/**
+ * 明细表
+ */
+export class TableInfo extends S2ChartView {
+ properties = TABLE_EDITOR_PROPERTY
+ propertyInner = {
+ ...TABLE_EDITOR_PROPERTY_INNER,
+ 'table-header-selector': [
+ ...TABLE_EDITOR_PROPERTY_INNER['table-header-selector'],
+ 'tableHeaderSort',
+ 'showTableHeader',
+ 'headerGroup'
+ ],
+ 'basic-style-selector': [
+ 'tableColumnMode',
+ 'tableBorderColor',
+ 'tableScrollBarColor',
+ 'alpha',
+ 'tablePageMode',
+ 'showHoverStyle',
+ 'autoWrap',
+ 'showSummary',
+ 'summaryLabel'
+ ],
+ 'table-cell-selector': [
+ ...TABLE_EDITOR_PROPERTY_INNER['table-cell-selector'],
+ 'tableFreeze',
+ 'tableColumnFreezeHead',
+ 'tableRowFreezeHead',
+ 'mergeCells'
+ ]
+ }
+ axis: AxisType[] = ['xAxis', 'filter', 'drill']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ name: `${t('chart.drag_block_table_data_column')} / ${t('chart.dimension_or_quota')}`
+ }
+ }
+
+ public drawChart(drawOption: S2DrawOptions): TableSheet {
+ const { container, chart, pageInfo, action, resizeAction } = drawOption
+ const containerDom = document.getElementById(container)
+
+ // fields
+ let fields = chart.data?.fields ?? []
+ const columns = []
+ const meta = []
+ const axisMap = chart.xAxis.reduce((pre, cur) => {
+ pre[cur.dataeaseName] = cur
+ return pre
+ }, {})
+ const drillFieldMap = {}
+ if (chart.drill) {
+ // 下钻过滤字段
+ const filterFields = chart.drillFilters.map(i => i.fieldId)
+ // 下钻入口的字段下标
+ const drillFieldId = chart.drillFields[0].id
+ const drillFieldIndex = chart.xAxis.findIndex(ele => ele.id === drillFieldId)
+ // 当前下钻字段
+ const curDrillFieldId = chart.drillFields[filterFields.length].id
+ const curDrillField = fields.find(ele => ele.id === curDrillFieldId)
+ filterFields.push(curDrillFieldId)
+ // 移除下钻字段,把当前下钻字段插入到下钻入口位置
+ fields = fields.filter(ele => {
+ return !filterFields.includes(ele.id)
+ })
+ drillFieldMap[curDrillField.dataeaseName] = chart.drillFields[0].dataeaseName
+ fields.splice(drillFieldIndex, 0, curDrillField)
+ }
+ fields.forEach(ele => {
+ const f = axisMap[ele.dataeaseName]
+ if (f?.hide === true) {
+ return
+ }
+ columns.push(ele.dataeaseName)
+ meta.push({
+ field: ele.dataeaseName,
+ name: ele.chartShowName ?? ele.name,
+ formatter: function (value) {
+ if (!f) {
+ return value
+ }
+ if (value === null || value === undefined) {
+ return value
+ }
+ if (![2, 3].includes(f.deType) || !isNumber(value)) {
+ return value
+ }
+ let formatCfg = f.formatterCfg
+ if (!formatCfg) {
+ formatCfg = formatterItem
+ }
+ return valueFormatter(value, formatCfg)
+ }
+ })
+ })
+ const { basicStyle, tableCell, tableHeader, tooltip } = parseJson(chart.customAttr)
+ // 表头分组
+ const { headerGroup, showTableHeader } = tableHeader
+ if (headerGroup && showTableHeader !== false) {
+ const { headerGroupConfig } = tableHeader
+ if (headerGroupConfig?.columns?.length) {
+ const allKeys = columns.map(c => drillFieldMap[c] || c)
+ const leafNodes = getLeafNodes(headerGroupConfig.columns as ColumnNode[])
+ const leafKeys = leafNodes.map(c => c.key)
+ if (isEqual(leafKeys, allKeys)) {
+ if (Object.keys(drillFieldMap).length) {
+ const originField = Object.values(drillFieldMap)[0]
+ const drillField = Object.keys(drillFieldMap)[0]
+ const [drillCol] = getColumns([originField], headerGroupConfig.columns as ColumnNode[])
+ drillCol.key = drillField
+ }
+ columns.splice(0, columns.length, ...headerGroupConfig.columns)
+ meta.push(...headerGroupConfig.meta)
+ }
+ }
+ }
+ // 空值处理
+ const newData = this.configEmptyDataStrategy(chart)
+ // data config
+ const s2DataConfig: S2DataConfig = {
+ fields: {
+ columns: columns
+ },
+ meta: meta,
+ data: newData
+ }
+
+ // options
+ const s2Options: S2Options = {
+ width: containerDom.getBoundingClientRect().width,
+ height: containerDom.offsetHeight,
+ showSeriesNumber: tableHeader.showIndex,
+ conditions: this.configConditions(chart),
+ tooltip: {
+ getContainer: () => containerDom,
+ renderTooltip: sheet => new SortTooltip(sheet)
+ },
+ interaction: {
+ hoverHighlight: !(basicStyle.showHoverStyle === false),
+ scrollbarPosition: newData.length
+ ? ScrollbarPositionType.CONTENT
+ : ScrollbarPositionType.CANVAS
+ }
+ }
+ s2Options.style = this.configStyle(chart, s2DataConfig)
+ // 自适应列宽模式下,URL 字段的宽度固定为 120
+ if (basicStyle.tableColumnMode === 'adapt') {
+ const urlFields = fields.filter(
+ field => field.deType === 7 && !axisMap[field.dataeaseName]?.hide
+ )
+ s2Options.style.colCfg.widthByFieldValue = urlFields?.reduce((p, n) => {
+ p[n.chartShowName ?? n.name] = 120
+ return p
+ }, {})
+ }
+ if (tableCell.tableFreeze && !tableCell.mergeCells) {
+ s2Options.frozenColCount = tableCell.tableColumnFreezeHead ?? 0
+ s2Options.frozenRowCount = tableCell.tableRowFreezeHead ?? 0
+ }
+ // 开启序号之后,第一列就是序号列,修改 label 即可
+ if (s2Options.showSeriesNumber) {
+ let indexLabel = tableHeader.indexLabel
+ if (!indexLabel) {
+ indexLabel = ''
+ }
+ s2Options.layoutCoordinate = (_, __, col) => {
+ if (col.colIndex === 0 && col.rowIndex === 0) {
+ col.label = indexLabel
+ col.value = indexLabel
+ }
+ }
+ }
+ s2Options.dataCell = viewMeta => {
+ const field = fields.filter(f => f.dataeaseName === viewMeta.valueField)?.[0]
+ if (field?.deType === 7 && chart.showPosition !== 'dialog') {
+ return new ImageCell(viewMeta, viewMeta?.spreadsheet)
+ }
+ if (viewMeta.colIndex === 0 && s2Options.showSeriesNumber) {
+ if (tableCell.mergeCells) {
+ viewMeta.fieldValue = getRowIndex(s2Options.mergedCellsInfo, viewMeta)
+ } else {
+ viewMeta.fieldValue =
+ pageInfo.pageSize * (pageInfo.currentPage - 1) + viewMeta.rowIndex + 1
+ }
+ }
+ // 配置文本自动换行参数
+ viewMeta.autoWrap = tableCell.mergeCells ? false : basicStyle.autoWrap
+ viewMeta.maxLines = basicStyle.maxLines
+ return new CustomDataCell(viewMeta, viewMeta?.spreadsheet)
+ }
+ // tooltip
+ this.configTooltip(chart, s2Options)
+ // 合并单元格
+ this.configMergeCells(chart, s2Options, s2DataConfig)
+ // 隐藏表头,保留顶部的分割线, 禁用表头横向 resize
+ if (tableHeader.showTableHeader === false) {
+ s2Options.style.colCfg.height = 1
+ if (tableCell.showHorizonBorder === false) {
+ s2Options.style.colCfg.height = 0
+ }
+ s2Options.interaction.resize = {
+ colCellVertical: false
+ }
+ s2Options.colCell = (node, sheet, config) => {
+ node.label = ' '
+ return new TableColCell(node, sheet, config)
+ }
+ } else {
+ // header interaction
+ chart.container = container
+ this.configHeaderInteraction(chart, s2Options)
+ s2Options.colCell = (node, sheet, config) => {
+ // 配置文本自动换行参数
+ node.autoWrap = tableCell.mergeCells ? false : basicStyle.autoWrap
+ node.maxLines = basicStyle.maxLines
+ return new CustomTableColCell(node, sheet, config)
+ }
+ }
+ // 总计
+ configSummaryRow(chart, s2Options, newData, tableHeader, basicStyle, basicStyle.showSummary)
+ // 开始渲染
+ const newChart = new TableSheet(containerDom, s2DataConfig, s2Options)
+ // 总计紧贴在单元格后面
+ summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary)
+ // 开启自动换行
+ if (basicStyle.autoWrap && !tableCell.mergeCells) {
+ // 调整表头宽度时,计算表头高度
+ newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, info => {
+ calculateHeaderHeight(info, newChart, tableHeader, basicStyle, null)
+ })
+ newChart.on(S2Event.LAYOUT_AFTER_HEADER_LAYOUT, (ev: LayoutResult) => {
+ const maxHeight = newChart.store.get('autoCalcHeight') as number
+ if (maxHeight) {
+ // 更新列的高度
+ ev.colLeafNodes.forEach(n => (n.height = maxHeight))
+ ev.colsHierarchy.height = maxHeight
+ newChart.store.set('autoCalcHeight', undefined)
+ } else {
+ if (ev.colLeafNodes?.length) {
+ const { value, width } = ev.colLeafNodes[0]
+ calculateHeaderHeight(
+ { info: { meta: { value }, resizedWidth: width } },
+ newChart,
+ tableHeader,
+ basicStyle,
+ ev
+ )
+ }
+ }
+ })
+ }
+ // 自适应铺满
+ if (basicStyle.tableColumnMode === 'adapt') {
+ newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, () => {
+ newChart.store.set('lastLayoutResult', newChart.facet.layoutResult)
+ })
+ newChart.on(S2Event.LAYOUT_AFTER_HEADER_LAYOUT, (ev: LayoutResult) => {
+ const lastLayoutResult = newChart.store.get('lastLayoutResult') as LayoutResult
+ if (lastLayoutResult) {
+ // 拖动表头 resize
+ const widthByFieldValue = newChart.options.style?.colCfg?.widthByFieldValue
+ const lastLayoutWidthMap: Record =
+ lastLayoutResult?.colLeafNodes.reduce((p, n) => {
+ p[n.value] = widthByFieldValue?.[n.value] ?? n.width
+ return p
+ }, {}) || {}
+ const totalWidth = ev.colLeafNodes.reduce((p, n) => {
+ n.width = lastLayoutWidthMap[n.value] || n.width
+ n.x = p
+ return p + n.width
+ }, 0)
+ // 处理分组的单元格,宽度为所有叶子节点之和
+ ev.colNodes.forEach(n => {
+ if (n.colIndex === -1) {
+ n.width = calcTreeWidth(n)
+ n.x = getStartPosition(n)
+ }
+ })
+ ev.colsHierarchy.width = totalWidth
+ newChart.store.set('lastLayoutResult', undefined)
+ return
+ }
+ // 第一次渲染初始化,把图片字段固定为 120 进行计算
+ const urlFields = fields
+ .filter(field => field.deType === 7 && !axisMap[field.dataeaseName]?.hide)
+ .map(f => f.dataeaseName)
+ const totalWidthWithImg = ev.colLeafNodes.reduce((p, n) => {
+ return p + (urlFields.includes(n.field) ? 120 : n.width)
+ }, 0)
+ const containerWidth = containerDom.getBoundingClientRect().width
+ if (containerWidth <= totalWidthWithImg) {
+ // 图库计算的布局宽度已经大于等于容器宽度,不需要再扩大,但是需要处理非整数宽度值,不然会出现透明细线
+ ev.colLeafNodes.reduce((p, n) => {
+ n.width = Math.round(n.width)
+ n.x = p
+ return p + n.width
+ }, 0)
+ return
+ }
+ // 图片字段固定 120, 剩余宽度按比例均摊到其他字段进行扩大
+ const totalWidthWithoutImg = ev.colLeafNodes.reduce((p, n) => {
+ return p + (urlFields.includes(n.field) ? 0 : n.width)
+ }, 0)
+ const restWidth = containerWidth - urlFields.length * 120
+ const scale = restWidth / totalWidthWithoutImg
+ const totalWidth = ev.colLeafNodes.reduce((p, n) => {
+ n.width = urlFields.includes(n.field) ? 120 : Math.round(n.width * scale)
+ n.x = p
+ return p + n.width
+ }, 0)
+ // 处理分组的单元格,宽度为所有叶子节点之和
+ ev.colNodes.forEach(n => {
+ if (n.colIndex === -1) {
+ n.width = calcTreeWidth(n)
+ n.x = getStartPosition(n)
+ }
+ })
+ if (totalWidth > containerWidth) {
+ ev.colLeafNodes[ev.colLeafNodes.length - 1].width -= totalWidth - containerWidth
+ }
+ ev.colsHierarchy.width = containerWidth
+ })
+ }
+ // 空数据时表格样式
+ configEmptyDataStyle(newChart, basicStyle, newData, container)
+ // click
+ newChart.on(S2Event.DATA_CELL_CLICK, ev => {
+ const cell = newChart.getCell(ev.target)
+ const meta = cell.getMeta() as ViewMeta
+ const nameIdMap = fields.reduce((pre, next) => {
+ pre[next['dataeaseName']] = next['id']
+ return pre
+ }, {})
+
+ const rowData = newChart.dataSet.getRowData(meta)
+ 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-info',
+ quotaList: []
+ }
+ }
+ action(param)
+ })
+ // 合并的单元格直接复用数据单元格的事件
+ newChart.on(S2Event.MERGED_CELLS_CLICK, e => newChart.emit(S2Event.DATA_CELL_CLICK, e))
+ // tooltip
+ const { show } = tooltip
+ if (show) {
+ newChart.on(S2Event.COL_CELL_HOVER, event => this.showTooltip(newChart, event, meta))
+ newChart.on(S2Event.DATA_CELL_HOVER, event => this.showTooltip(newChart, event, meta))
+ newChart.on(S2Event.MERGED_CELLS_HOVER, event => this.showTooltip(newChart, event, meta))
+ }
+ // header resize
+ newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, ev => resizeAction(ev))
+ // right click
+ newChart.on(S2Event.GLOBAL_CONTEXT_MENU, event => copyContent(newChart, event, meta))
+ // touch
+ this.configTouchEvent(newChart, drawOption, meta)
+ // theme
+ const customTheme = this.configTheme(chart)
+ newChart.setThemeCfg({ theme: customTheme })
+ return newChart
+ }
+
+ protected configTheme(chart: Chart): S2Theme {
+ const theme = super.configTheme(chart)
+ const { basicStyle, tableCell } = parseJson(chart.customAttr)
+ if (tableCell.mergeCells) {
+ const tableFontColor = hexColorToRGBA(tableCell.tableFontColor, basicStyle.alpha)
+ let tableItemBgColor = tableCell.tableItemBgColor
+ if (!isAlphaColor(tableItemBgColor)) {
+ tableItemBgColor = hexColorToRGBA(tableItemBgColor, basicStyle.alpha)
+ }
+ const { tableBorderColor } = basicStyle
+ const { tableItemAlign, tableItemFontSize } = tableCell
+ const fontStyle = tableCell.isItalic ? 'italic' : 'normal'
+ const fontWeight = tableCell.isBolder === false ? 'normal' : 'bold'
+ const mergeCellTheme: S2Theme = {
+ mergedCell: {
+ cell: {
+ backgroundColor: tableItemBgColor,
+ crossBackgroundColor: tableItemBgColor,
+ horizontalBorderColor: tableBorderColor,
+ verticalBorderColor: tableBorderColor,
+ horizontalBorderWidth: tableCell.showHorizonBorder ? 1 : 0,
+ verticalBorderWidth: tableCell.showVerticalBorder ? 1 : 0
+ },
+ bolderText: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontStyle,
+ fontWeight
+ },
+ text: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontStyle,
+ fontWeight
+ },
+ measureText: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontStyle,
+ fontWeight
+ },
+ seriesText: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontStyle,
+ fontWeight
+ }
+ }
+ }
+ merge(theme, mergeCellTheme)
+ }
+ return theme
+ }
+
+ constructor() {
+ super('table-info', [])
+ }
+}
+
+function calcTreeWidth(node) {
+ if (!node.children?.length) {
+ return node.width
+ }
+ return node.children.reduce((pre, cur) => {
+ return pre + calcTreeWidth(cur)
+ }, 0)
+}
+
+function getStartPosition(node) {
+ if (!node.children?.length) {
+ return node.x
+ }
+ return getStartPosition(node.children[0])
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-normal.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-normal.ts
new file mode 100644
index 0000000..327dd5b
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-normal.ts
@@ -0,0 +1,300 @@
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { formatterItem, valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import {
+ configEmptyDataStyle,
+ configSummaryRow,
+ copyContent,
+ SortTooltip,
+ summaryRowStyle
+} from '@/data-visualization/chart/components/js/panel/common/common_table'
+import { S2ChartView, S2DrawOptions } from '@/data-visualization/chart/components/js/panel/types/impl/s2'
+import { parseJson } from '@/data-visualization/chart/components/js/util'
+import {
+ type LayoutResult,
+ S2DataConfig,
+ S2Event,
+ S2Options,
+ ScrollbarPositionType,
+ TableColCell,
+ TableSheet,
+ ViewMeta
+} from '@antv/s2'
+import { cloneDeep, isNumber } from 'lodash-es'
+import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common'
+
+const { t } = useI18n()
+/**
+ * 汇总表
+ */
+export class TableNormal extends S2ChartView {
+ properties = TABLE_EDITOR_PROPERTY
+ propertyInner: EditorPropertyInner = {
+ ...TABLE_EDITOR_PROPERTY_INNER,
+ 'table-header-selector': [
+ ...TABLE_EDITOR_PROPERTY_INNER['table-header-selector'],
+ 'tableHeaderSort',
+ 'showTableHeader'
+ ],
+ 'basic-style-selector': [
+ ...TABLE_EDITOR_PROPERTY_INNER['basic-style-selector'],
+ 'showSummary',
+ 'summaryLabel',
+ 'showHoverStyle'
+ ],
+ 'table-cell-selector': [
+ ...TABLE_EDITOR_PROPERTY_INNER['table-cell-selector'],
+ 'tableFreeze',
+ 'tableColumnFreezeHead',
+ 'tableRowFreezeHead'
+ ]
+ }
+ axis: AxisType[] = ['xAxis', 'yAxis', 'drill', 'filter']
+ axisConfig: AxisConfig = {
+ xAxis: {
+ 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'
+ }
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ chart.xAxis = []
+ return chart
+ }
+
+ drawChart(drawOption: S2DrawOptions): TableSheet {
+ const { container, chart, action, resizeAction } = drawOption
+ const containerDom = document.getElementById(container)
+ if (!containerDom) return
+
+ // fields
+ let fields = chart.data.fields
+
+ const columns = []
+ const meta = []
+ if (chart.drill) {
+ // 下钻过滤字段
+ const filterFields = chart.drillFilters.map(i => i.fieldId)
+ // 下钻入口的字段下标
+ const drillFieldId = chart.drillFields[0].id
+ const drillFieldIndex = chart.xAxis.findIndex(ele => ele.id === drillFieldId)
+ // 当前下钻字段
+ const curDrillFieldId = chart.drillFields[filterFields.length].id
+ const curDrillField = fields.filter(ele => ele.id === curDrillFieldId)
+ filterFields.push(curDrillFieldId)
+ // 移除下钻字段,把当前下钻字段插入到下钻入口位置
+ fields = fields.filter(ele => {
+ return !filterFields.includes(ele.id)
+ })
+ fields.splice(drillFieldIndex, 0, ...curDrillField)
+ }
+ const axisMap = [...chart.xAxis, ...chart.yAxis].reduce((pre, cur) => {
+ pre[cur.dataeaseName] = cur
+ return pre
+ }, {})
+ // add drill list
+ fields.forEach(ele => {
+ const f = axisMap[ele.dataeaseName]
+ if (f?.hide === true) {
+ return
+ }
+ columns.push(ele.dataeaseName)
+ meta.push({
+ field: ele.dataeaseName,
+ name: ele.chartShowName ?? ele.name,
+ formatter: function (value) {
+ if (!f) {
+ return value
+ }
+ if (value === null || value === undefined) {
+ return value
+ }
+ if (![2, 3].includes(f.deType) || !isNumber(value)) {
+ return value
+ }
+ let formatCfg = f.formatterCfg
+ if (!formatCfg) {
+ formatCfg = formatterItem
+ }
+ return valueFormatter(value, formatCfg)
+ }
+ })
+ })
+
+ // 空值处理
+ const newData = this.configEmptyDataStrategy(chart)
+ // data config
+ const s2DataConfig: S2DataConfig = {
+ fields: {
+ columns: columns
+ },
+ meta: meta,
+ data: newData
+ }
+
+ const { basicStyle, tableCell, tableHeader, tooltip } = parseJson(chart.customAttr)
+ // options
+ const s2Options: S2Options = {
+ width: containerDom.getBoundingClientRect().width,
+ height: containerDom.offsetHeight,
+ showSeriesNumber: tableHeader.showIndex,
+ conditions: this.configConditions(chart),
+ tooltip: {
+ getContainer: () => containerDom,
+ renderTooltip: sheet => new SortTooltip(sheet)
+ },
+ interaction: {
+ hoverHighlight: !(basicStyle.showHoverStyle === false),
+ scrollbarPosition: newData.length
+ ? ScrollbarPositionType.CONTENT
+ : ScrollbarPositionType.CANVAS
+ }
+ }
+ // 列宽设置
+ s2Options.style = this.configStyle(chart, s2DataConfig)
+ // 行列冻结
+ if (tableCell.tableFreeze) {
+ s2Options.frozenColCount = tableCell.tableColumnFreezeHead ?? 0
+ s2Options.frozenRowCount = tableCell.tableRowFreezeHead ?? 0
+ }
+ // 开启序号之后,第一列就是序号列,修改 label 即可
+ if (s2Options.showSeriesNumber) {
+ let indexLabel = tableHeader.indexLabel
+ if (!indexLabel) {
+ indexLabel = ''
+ }
+ s2Options.layoutCoordinate = (_, __, col) => {
+ if (col.colIndex === 0 && col.rowIndex === 0) {
+ col.label = indexLabel
+ col.value = indexLabel
+ }
+ }
+ }
+ // tooltip
+ this.configTooltip(chart, s2Options)
+ // 隐藏表头,保留顶部的分割线, 禁用表头横向 resize
+ if (tableHeader.showTableHeader === false) {
+ s2Options.style.colCfg.height = 1
+ if (tableCell.showHorizonBorder === false) {
+ s2Options.style.colCfg.height = 0
+ }
+ s2Options.interaction.resize = {
+ colCellVertical: false
+ }
+ s2Options.colCell = (node, sheet, config) => {
+ node.label = ' '
+ return new TableColCell(node, sheet, config)
+ }
+ } else {
+ // header interaction
+ chart.container = container
+ this.configHeaderInteraction(chart, s2Options)
+ }
+
+ // 总计
+ configSummaryRow(chart, s2Options, newData, tableHeader, basicStyle, basicStyle.showSummary)
+ // 开始渲染
+ const newChart = new TableSheet(containerDom, s2DataConfig, s2Options)
+ // 总计紧贴在单元格后面
+ summaryRowStyle(newChart, newData, tableCell, tableHeader, basicStyle.showSummary)
+ // 自适应铺满
+ if (basicStyle.tableColumnMode === 'adapt') {
+ newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, () => {
+ newChart.store.set('lastLayoutResult', newChart.facet.layoutResult)
+ })
+ newChart.on(S2Event.LAYOUT_AFTER_HEADER_LAYOUT, (ev: LayoutResult) => {
+ const lastLayoutResult = newChart.store.get('lastLayoutResult') as LayoutResult
+ if (lastLayoutResult) {
+ // 拖动表头 resize
+ const widthByFieldValue = newChart.options.style?.colCfg?.widthByFieldValue
+ const lastLayoutWidthMap: Record =
+ lastLayoutResult?.colLeafNodes.reduce((p, n) => {
+ p[n.value] = widthByFieldValue?.[n.value] ?? n.width
+ return p
+ }, {}) || {}
+ const totalWidth = ev.colLeafNodes.reduce((p, n) => {
+ n.width = lastLayoutWidthMap[n.value] || n.width
+ n.x = p
+ return p + n.width
+ }, 0)
+ ev.colsHierarchy.width = totalWidth
+ newChart.store.set('lastLayoutResult', undefined)
+ return
+ }
+ const containerWidth = containerDom.getBoundingClientRect().width
+ const scale = containerWidth / ev.colsHierarchy.width
+ if (scale <= 1) {
+ // 图库计算的布局宽度已经大于等于容器宽度,不需要再扩大,但是需要处理非整数宽度值,不然会出现透明细线
+ ev.colLeafNodes.reduce((p, n) => {
+ n.width = Math.round(n.width)
+ n.x = p
+ return p + n.width
+ }, 0)
+ return
+ }
+ const totalWidth = ev.colLeafNodes.reduce((p, n) => {
+ n.width = Math.round(n.width * scale)
+ n.x = p
+ return p + n.width
+ }, 0)
+ if (totalWidth > containerWidth) {
+ // 从最后一列减掉
+ ev.colLeafNodes[ev.colLeafNodes.length - 1].width -= totalWidth - containerWidth
+ }
+ ev.colsHierarchy.width = containerWidth
+ })
+ }
+ configEmptyDataStyle(newChart, basicStyle, newData, container)
+ // click
+ newChart.on(S2Event.DATA_CELL_CLICK, ev => {
+ const cell = newChart.getCell(ev.target)
+ const meta = cell.getMeta() as ViewMeta
+ const nameIdMap = fields.reduce((pre, next) => {
+ pre[next['dataeaseName']] = next['id']
+ return pre
+ }, {})
+
+ const rowData = newChart.dataSet.getRowData(meta)
+ 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-normal',
+ quotaList: []
+ }
+ }
+ action(param)
+ })
+ // tooltip
+ const { show } = tooltip
+ if (show) {
+ newChart.on(S2Event.COL_CELL_HOVER, event => this.showTooltip(newChart, event, meta))
+ newChart.on(S2Event.DATA_CELL_HOVER, event => this.showTooltip(newChart, event, meta))
+ }
+ // header resize
+ newChart.on(S2Event.LAYOUT_RESIZE_COL_WIDTH, ev => resizeAction(ev))
+ // right click
+ newChart.on(S2Event.GLOBAL_CONTEXT_MENU, event => copyContent(newChart, event, meta))
+ // touch
+ this.configTouchEvent(newChart, drawOption, meta)
+ // theme
+ const customTheme = this.configTheme(chart)
+ newChart.setThemeCfg({ theme: customTheme })
+
+ return newChart
+ }
+ constructor() {
+ super('table-normal', [])
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-pivot.ts b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-pivot.ts
new file mode 100644
index 0000000..f7f695c
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/charts/table/table-pivot.ts
@@ -0,0 +1,1003 @@
+import {
+ EXTRA_FIELD,
+ PivotSheet,
+ S2Event,
+ S2Options,
+ TOTAL_VALUE,
+ S2Theme,
+ Totals,
+ PivotDataSet,
+ Query,
+ VALUE_FIELD,
+ QueryDataType,
+ TotalStatus,
+ Aggregation,
+ S2DataConfig,
+ MergedCell
+} from '@antv/s2'
+import { formatterItem, valueFormatter } from '../../../formatter'
+import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util'
+import { S2ChartView, S2DrawOptions } from '../../types/impl/s2'
+import { TABLE_EDITOR_PROPERTY_INNER } from './common'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+import { isNumber, keys, maxBy, merge, minBy, some, isEmpty, get } from 'lodash-es'
+import { copyContent, CustomDataCell } from '../../common/common_table'
+import Decimal from 'decimal.js'
+import { DEFAULT_TABLE_HEADER } from '@/data-visualization/chart/components/editor/util/chart'
+
+type DataItem = Record
+
+const { t } = useI18n()
+
+class CustomPivotDataset extends PivotDataSet {
+ getTotalValue(query: Query, totalStatus?: TotalStatus) {
+ const { options } = this.spreadsheet
+ const effectiveStatus = some(totalStatus)
+ const status = effectiveStatus ? totalStatus : this.getTotalStatus(query)
+ const { aggregation, calcFunc } =
+ getAggregationAndCalcFuncByQuery(status, options?.totals) || {}
+
+ // 聚合方式从用户配置的 s2Options.totals 取, 在触发前端兜底计算汇总逻辑时, 如果没有汇总的配置, 默认按 [求和] 计算,避免排序失效.
+ const defaultAggregation =
+ isEmpty(options?.totals) && !this.spreadsheet.isHierarchyTreeType() ? Aggregation.SUM : ''
+ const calcAction = calcActionByType[aggregation || defaultAggregation]
+
+ // 前端计算汇总值
+ if (calcAction || calcFunc) {
+ const data = this.getMultiData(query, {
+ queryType: QueryDataType.DetailOnly
+ })
+ let totalValue: number
+ if (calcFunc) {
+ totalValue = calcFunc(query, data, this.spreadsheet, status)
+ } else if (calcAction) {
+ totalValue = calcAction(data, VALUE_FIELD)
+ }
+
+ return {
+ ...query,
+ [VALUE_FIELD]: totalValue,
+ [query[EXTRA_FIELD]]: totalValue
+ }
+ }
+ }
+}
+/**
+ * 透视表
+ */
+export class TablePivot extends S2ChartView {
+ properties: EditorProperty[] = [
+ 'border-style',
+ 'background-overall-component',
+ 'basic-style-selector',
+ 'table-header-selector',
+ 'table-cell-selector',
+ 'table-total-selector',
+ 'title-selector',
+ 'tooltip-selector',
+ 'function-cfg',
+ 'threshold',
+ 'linkage',
+ 'jump-set'
+ ]
+ propertyInner = {
+ ...TABLE_EDITOR_PROPERTY_INNER,
+ 'table-header-selector': [
+ 'tableHeaderBgColor',
+ 'tableTitleFontSize',
+ 'tableHeaderFontColor',
+ 'tableTitleHeight',
+ 'tableHeaderAlign',
+ 'showColTooltip',
+ 'showRowTooltip',
+ 'showHorizonBorder',
+ 'showVerticalBorder'
+ ],
+ 'table-total-selector': ['row', 'col'],
+ 'basic-style-selector': [
+ 'tableColumnMode',
+ 'tableBorderColor',
+ 'tableScrollBarColor',
+ 'alpha',
+ 'tableLayoutMode',
+ 'showHoverStyle'
+ ]
+ }
+ 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',
+ allowEmpty: true
+ },
+ yAxis: {
+ name: `${t('chart.drag_block_table_data_column')} / ${t('chart.quota')}`,
+ type: 'q'
+ }
+ }
+
+ public drawChart(drawOption: S2DrawOptions): PivotSheet {
+ const { container, chart, chartObj, action } = drawOption
+ const containerDom = document.getElementById(container)
+
+ const { xAxisExt: columnFields, xAxis: rowFields, yAxis: valueFields } = chart
+ const [c, r, v] = [columnFields, rowFields, valueFields].map(arr =>
+ arr.map(i => i.dataeaseName)
+ )
+
+ // fields
+ const { fields, customCalc } = chart.data
+ if (!fields || fields.length === 0) {
+ if (chartObj) {
+ chartObj.destroy()
+ }
+ return
+ }
+
+ const columns = []
+ const meta = []
+
+ const valueFieldMap: Record = [
+ ...chart.xAxis,
+ ...chart.xAxisExt,
+ ...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.chartShowName ?? ele.name,
+ formatter: value => {
+ if (!f) {
+ return value
+ }
+ if (value === null || value === undefined) {
+ return value
+ }
+ if (![2, 3].includes(f.deType) || !isNumber(value)) {
+ return value
+ }
+ if (f.formatterCfg) {
+ return valueFormatter(value, f.formatterCfg)
+ } else {
+ return valueFormatter(value, formatterItem)
+ }
+ }
+ })
+ })
+
+ // total config
+ const { basicStyle, tooltip, tableTotal } = parseJson(chart.customAttr)
+ if (!tableTotal.row.subTotalsDimensionsNew || tableTotal.row.subTotalsDimensions == undefined) {
+ 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)
+ }
+ //列维度为空,行排序按照指标列来排序,取第一个有排序设置的指标
+ if (!columnFields?.length) {
+ const sortField = valueFields?.find(v => !['none', 'custom_sort'].includes(v.sort))
+ if (sortField) {
+ const sort = {
+ sortFieldId: r[0],
+ sortMethod: sortField.sort.toUpperCase(),
+ sortByMeasure: TOTAL_VALUE,
+ query: {
+ [EXTRA_FIELD]: sortField.dataeaseName
+ }
+ }
+ sortParams.push(sort)
+ }
+ }
+ // 自定义总计小计
+ const totals = [
+ tableTotal.row.calcTotals,
+ tableTotal.row.calcSubTotals,
+ tableTotal.col.calcTotals,
+ tableTotal.col.calcSubTotals
+ ]
+ const axisMap = {
+ row: chart.xAxis,
+ col: chart.xAxisExt,
+ quota: chart.yAxis
+ }
+ //树形模式下,列维度为空,行小计会变成列总计,特殊处理下
+ if (basicStyle.tableLayoutMode === 'tree' && !chart.xAxisExt?.length) {
+ tableTotal.col.calcTotals = tableTotal.row.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, _, status) => {
+ return customCalcFunc(query, data, status, chart, totalCfgMap, axisMap, customCalc)
+ }
+ }
+ })
+ // 空值处理
+ const newData = this.configEmptyDataStrategy(chart)
+ // data config
+ const s2DataConfig: S2DataConfig = {
+ fields: {
+ rows: r,
+ columns: c,
+ values: v
+ },
+ meta: meta,
+ data: newData,
+ sortParams: sortParams
+ }
+ const s2Options: S2Options = {
+ width: containerDom.offsetWidth,
+ height: containerDom.offsetHeight,
+ totals: tableTotal as Totals,
+ conditions: this.configConditions(chart),
+ tooltip: {
+ getContainer: () => containerDom
+ },
+ hierarchyType: basicStyle.tableLayoutMode ?? 'grid',
+ dataSet: spreadSheet => new CustomPivotDataset(spreadSheet),
+ interaction: {
+ hoverHighlight: !(basicStyle.showHoverStyle === false)
+ },
+ dataCell: meta => {
+ return new CustomDataCell(meta, meta.spreadsheet)
+ }
+ }
+ // options
+ s2Options.style = this.configStyle(chart, s2DataConfig)
+ s2Options.style.hierarchyCollapse = true
+ // tooltip
+ this.configTooltip(chart, s2Options)
+ // 开始渲染
+ const s2 = new PivotSheet(containerDom, s2DataConfig, s2Options as unknown as S2Options)
+ // tooltip
+ const { show } = tooltip
+ if (show) {
+ s2.on(S2Event.COL_CELL_HOVER, event => this.showTooltip(s2, event, meta))
+ s2.on(S2Event.ROW_CELL_HOVER, event => this.showTooltip(s2, event, meta))
+ s2.on(S2Event.DATA_CELL_HOVER, event => this.showTooltip(s2, event, meta))
+ }
+ // empty data tip
+ configEmptyDataStyle(s2, newData)
+ // 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))
+ // right click
+ s2.on(S2Event.GLOBAL_CONTEXT_MENU, event => copyContent(s2, event, meta))
+ // touch
+ this.configTouchEvent(s2, drawOption, meta)
+ // 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)
+ let tableHeaderBgColor = tableHeader.tableHeaderBgColor
+ if (!isAlphaColor(tableHeaderBgColor)) {
+ tableHeaderBgColor = hexColorToRGBA(tableHeaderBgColor, basicStyle.alpha)
+ }
+ let tableHeaderCornerBgColor =
+ tableHeader.tableHeaderCornerBgColor ?? DEFAULT_TABLE_HEADER.tableHeaderCornerBgColor
+ if (!isAlphaColor(tableHeaderCornerBgColor)) {
+ tableHeaderCornerBgColor = hexColorToRGBA(tableHeaderCornerBgColor, basicStyle.alpha)
+ }
+ let tableHeaderColBgColor =
+ tableHeader.tableHeaderColBgColor ?? DEFAULT_TABLE_HEADER.tableHeaderColBgColor
+ if (!isAlphaColor(tableHeaderColBgColor)) {
+ tableHeaderColBgColor = hexColorToRGBA(tableHeaderColBgColor, basicStyle.alpha)
+ }
+ let tableBorderColor = basicStyle.tableBorderColor
+ if (!isAlphaColor(tableBorderColor)) {
+ tableBorderColor = hexColorToRGBA(tableBorderColor, basicStyle.alpha)
+ }
+ const tableHeaderColFontColor = hexColorToRGBA(
+ tableHeader.tableHeaderColFontColor,
+ basicStyle.alpha
+ )
+ const tableHeaderCornerFontColor = hexColorToRGBA(
+ tableHeader.tableHeaderCornerFontColor,
+ basicStyle.alpha
+ )
+ const colFontStyle = tableHeader.isColItalic ? 'italic' : 'normal'
+ const cornerFontStyle = tableHeader.isCornerItalic ? 'italic' : 'normal'
+ const colFontWeight = tableHeader.isColBolder === false ? 'normal' : 'bold'
+ const cornerFontWeight = tableHeader.isCornerBolder === false ? 'normal' : 'bold'
+ const pivotTheme = {
+ rowCell: {
+ cell: {
+ backgroundColor: tableHeaderColBgColor,
+ horizontalBorderColor: tableBorderColor,
+ verticalBorderColor: tableBorderColor
+ },
+ text: {
+ fill: tableHeaderColFontColor,
+ fontSize: tableHeader.tableTitleColFontSize,
+ textAlign: tableHeader.tableHeaderColAlign,
+ textBaseline: 'top',
+ fontStyle: colFontStyle,
+ fontWeight: colFontWeight
+ },
+ bolderText: {
+ fill: tableHeaderColFontColor,
+ fontSize: tableHeader.tableTitleColFontSize,
+ textAlign: tableHeader.tableHeaderColAlign,
+ fontStyle: colFontStyle,
+ fontWeight: colFontWeight
+ },
+ measureText: {
+ fill: tableHeaderColFontColor,
+ fontSize: tableHeader.tableTitleColFontSize,
+ textAlign: tableHeader.tableHeaderColAlign,
+ fontStyle: colFontStyle,
+ fontWeight: colFontWeight
+ },
+ seriesText: {
+ fill: tableHeaderColFontColor,
+ fontSize: tableHeader.tableTitleColFontSize,
+ textAlign: tableHeader.tableHeaderColAlign,
+ fontStyle: colFontStyle,
+ fontWeight: colFontWeight
+ }
+ },
+ cornerCell: {
+ cell: {
+ backgroundColor: tableHeaderCornerBgColor
+ },
+ text: {
+ fill: tableHeaderCornerFontColor,
+ fontSize: tableHeader.tableTitleCornerFontSize,
+ textAlign: tableHeader.tableHeaderCornerAlign,
+ fontStyle: cornerFontStyle,
+ fontWeight: cornerFontWeight
+ },
+ bolderText: {
+ fill: tableHeaderCornerFontColor,
+ fontSize: tableHeader.tableTitleCornerFontSize,
+ textAlign: tableHeader.tableHeaderCornerAlign,
+ fontStyle: cornerFontStyle,
+ fontWeight: cornerFontWeight
+ },
+ measureText: {
+ fill: tableHeaderCornerFontColor,
+ fontSize: tableHeader.tableTitleCornerFontSize,
+ textAlign: tableHeader.tableHeaderCornerAlign,
+ fontStyle: cornerFontStyle,
+ fontWeight: cornerFontWeight
+ }
+ }
+ }
+ merge(theme, pivotTheme)
+ if (tableHeader.showHorizonBorder === false) {
+ const tmp: S2Theme = {
+ cornerCell: {
+ cell: {
+ horizontalBorderColor: tableHeaderBgColor,
+ horizontalBorderWidth: 0
+ }
+ },
+ rowCell: {
+ cell: {
+ horizontalBorderColor: tableHeaderBgColor,
+ horizontalBorderWidth: 0
+ }
+ }
+ }
+ merge(theme, tmp)
+ }
+ if (tableHeader.showVerticalBorder === false) {
+ const tmp: S2Theme = {
+ cornerCell: {
+ cell: {
+ verticalBorderColor: tableHeaderBgColor,
+ verticalBorderWidth: 0
+ }
+ },
+ rowCell: {
+ cell: {
+ verticalBorderColor: tableHeaderBgColor,
+ verticalBorderWidth: 0
+ }
+ }
+ }
+ merge(theme, tmp)
+ }
+ return theme
+ }
+
+ setupDefaultOptions(chart: ChartObj): ChartObj {
+ const { customAttr } = chart
+ if (customAttr.basicStyle.tableColumnMode === 'field') {
+ customAttr.basicStyle.tableColumnMode = 'custom'
+ }
+ return chart
+ }
+
+ constructor() {
+ super('table-pivot', [])
+ }
+}
+function customCalcFunc(query, data, status, chart, totalCfgMap, axisMap, customCalc) {
+ if (!data?.length || !query[EXTRA_FIELD]) {
+ return 0
+ }
+ const aggregation = totalCfgMap[query[EXTRA_FIELD]]?.aggregation || 'SUM'
+ switch (aggregation) {
+ case 'SUM': {
+ return data.reduce((p, n) => {
+ return p + parseFloat(n[query[EXTRA_FIELD]] ?? 0)
+ }, 0)
+ }
+ case 'AVG': {
+ const sum = data.reduce((p, n) => {
+ return p + parseFloat(n[query[EXTRA_FIELD]] ?? 0)
+ }, 0)
+ return sum / data.length
+ }
+ case 'MIN': {
+ const result = minBy(data, n => {
+ return parseFloat(n[query[EXTRA_FIELD]])
+ })
+ return result?.[query[EXTRA_FIELD]]
+ }
+ case 'MAX': {
+ const result = maxBy(data, n => {
+ return parseFloat(n[query[EXTRA_FIELD]])
+ })
+ return result?.[query[EXTRA_FIELD]]
+ }
+ case 'CUSTOM': {
+ const val = getCustomCalcResult(query, axisMap, chart, status, customCalc || {})
+ if (val === '') {
+ return val
+ }
+ return parseFloat(val)
+ }
+ default: {
+ return data.reduce((p, n) => {
+ return p + parseFloat(n[query[EXTRA_FIELD]] ?? 0)
+ }, 0)
+ }
+ }
+}
+
+function getTreeCustomCalcResult(query, axisMap, status: TotalStatus, customCalc) {
+ const quotaField = query[EXTRA_FIELD]
+ const { row, col } = axisMap
+ // 行列交叉总计
+ if (status.isRowTotal && status.isColTotal) {
+ return customCalc.rowColTotal?.data?.[quotaField]
+ }
+ // 列总计
+ if (status.isColTotal && !status.isRowSubTotal) {
+ const { colTotal, rowSubInColTotal } = customCalc
+ const path = getTreePath(query, row)
+ let val
+ if (path.length) {
+ const subLevel = getSubLevel(query, row)
+ if (subLevel + 1 === row.length && colTotal) {
+ path.push(quotaField)
+ val = get(colTotal.data, path)
+ }
+ if (subLevel + 1 < row.length && rowSubInColTotal) {
+ const data = rowSubInColTotal?.[subLevel]?.data
+ path.push(quotaField)
+ val = get(data, path)
+ }
+ }
+ return val
+ }
+ // 列小计
+ if (status.isColSubTotal && !status.isRowTotal && !status.isRowSubTotal) {
+ const { colSubTotal } = customCalc
+ const subLevel = getSubLevel(query, col)
+ const rowPath = getTreePath(query, row)
+ const colPath = getTreePath(query, col)
+ const path = [...rowPath, ...colPath]
+ const data = colSubTotal?.[subLevel]?.data
+ let val
+ if (path.length && data) {
+ path.push(quotaField)
+ val = get(data, path)
+ }
+ return val
+ }
+ // 行总计
+ if (status.isRowTotal && !status.isColSubTotal) {
+ const { rowTotal } = customCalc
+ const path = getTreePath(query, col)
+ let val
+ if (rowTotal) {
+ if (path.length) {
+ path.push(quotaField)
+ val = get(rowTotal.data, path)
+ }
+ // 列维度为空,行维度不为空
+ if (!col.length && row.length) {
+ val = get(rowTotal.data, quotaField)
+ }
+ }
+ return val
+ }
+ // 行小计
+ if (status.isRowSubTotal) {
+ // 列维度为空,行小计直接当成列总计
+ if (
+ (!status.isColTotal && !status.isColSubTotal) ||
+ (!col.length && status.isColTotal && status.isRowSubTotal)
+ ) {
+ const { rowSubTotal } = customCalc
+ const rowLevel = getSubLevel(query, row)
+ const colPath = getTreePath(query, col)
+ const rowPath = getTreePath(query, row)
+ const path = [...colPath, ...rowPath]
+ const data = rowSubTotal?.[rowLevel]?.data
+ let val
+ if (path.length && rowSubTotal) {
+ path.push(quotaField)
+ val = get(data, path)
+ }
+ return val
+ }
+ }
+ // 行总计里面的列小计
+ if (status.isRowTotal && status.isColSubTotal) {
+ const { colSubInRowTotal } = customCalc
+ const colLevel = getSubLevel(query, col)
+ const { data } = colSubInRowTotal?.[colLevel]
+ const colPath = getTreePath(query, col)
+ let val
+ if (colPath.length && colSubInRowTotal) {
+ colPath.push(quotaField)
+ val = get(data, colPath)
+ }
+ return val
+ }
+ // 列总计里面的行小计
+ if (status.isColTotal && status.isRowSubTotal) {
+ const { rowSubInColTotal } = customCalc
+ const rowSubLevel = getSubLevel(query, row)
+ const data = rowSubInColTotal?.[rowSubLevel]?.data
+ const path = getTreePath(query, row)
+ let val
+ if (path.length && rowSubInColTotal) {
+ path.push(quotaField)
+ val = get(data, path)
+ }
+ return val
+ }
+ // 列小计里面的行小计
+ if (status.isColSubTotal && status.isRowSubTotal) {
+ const { rowSubInColSub } = customCalc
+ const rowSubLevel = getSubLevel(query, row)
+ const colSubLevel = getSubLevel(query, col)
+ const data = rowSubInColSub?.[rowSubLevel]?.[colSubLevel]?.data
+ const rowPath = getTreePath(query, row)
+ const colPath = getTreePath(query, col)
+ const path = [...rowPath, ...colPath]
+ let val
+ if (path.length && rowSubInColSub) {
+ path.push(quotaField)
+ val = get(data, path)
+ }
+ return val
+ }
+ return NaN
+}
+
+function getGridCustomCalcResult(query, axisMap, status: TotalStatus, customCalc) {
+ const quotaField = query[EXTRA_FIELD]
+ const { row, col } = axisMap
+ // 行列交叉总计
+ if (status.isRowTotal && status.isColTotal) {
+ return customCalc.rowColTotal?.data?.[quotaField]
+ }
+ // 列总计
+ if (status.isColTotal && !status.isRowSubTotal) {
+ const { colTotal } = customCalc
+ const path = getTreePath(query, row)
+ let val
+ if (path.length) {
+ if (colTotal) {
+ path.push(quotaField)
+ val = get(colTotal.data, path)
+ }
+ }
+ return val
+ }
+ // 列小计
+ if (status.isColSubTotal && !status.isRowTotal && !status.isRowSubTotal) {
+ const { colSubTotal } = customCalc
+ const subLevel = getSubLevel(query, col)
+ const rowPath = getTreePath(query, row)
+ const colPath = getTreePath(query, col)
+ const path = [...rowPath, ...colPath]
+ const data = colSubTotal?.[subLevel]?.data
+ let val
+ if (path.length && data) {
+ path.push(quotaField)
+ val = get(data, path)
+ }
+ return val
+ }
+ // 行总计
+ if (status.isRowTotal && !status.isColSubTotal) {
+ const { rowTotal } = customCalc
+ const path = getTreePath(query, col)
+ let val
+ if (rowTotal) {
+ if (path.length) {
+ path.push(quotaField)
+ val = get(rowTotal.data, path)
+ }
+ // 列维度为空,行维度不为空
+ if (!col.length && row.length) {
+ val = get(rowTotal.data, quotaField)
+ }
+ }
+ return val
+ }
+ // 行小计
+ if (status.isRowSubTotal && !status.isColTotal && !status.isColSubTotal) {
+ const { rowSubTotal } = customCalc
+ const rowLevel = getSubLevel(query, row)
+ const colPath = getTreePath(query, col)
+ const rowPath = getTreePath(query, row)
+ const path = [...colPath, ...rowPath]
+ const data = rowSubTotal?.[rowLevel]?.data
+ let val
+ if (path.length && rowSubTotal) {
+ path.push(quotaField)
+ val = get(data, path)
+ }
+ return val
+ }
+ // 行总计里面的列小计
+ if (status.isRowTotal && status.isColSubTotal) {
+ const { colSubInRowTotal } = customCalc
+ const colLevel = getSubLevel(query, col)
+ const { data } = colSubInRowTotal?.[colLevel]
+ const colPath = getTreePath(query, col)
+ let val
+ if (colPath.length && colSubInRowTotal) {
+ colPath.push(quotaField)
+ val = get(data, colPath)
+ }
+ return val
+ }
+ // 列总计里面的行小计
+ if (status.isColTotal && status.isRowSubTotal) {
+ const { rowSubInColTotal } = customCalc
+ const rowSubLevel = getSubLevel(query, row)
+ const data = rowSubInColTotal?.[rowSubLevel]?.data
+ const path = getTreePath(query, row)
+ let val
+ if (path.length && rowSubInColTotal) {
+ path.push(quotaField)
+ val = get(data, path)
+ }
+ return val
+ }
+ // 列小计里面的行小计
+ if (status.isColSubTotal && status.isRowSubTotal) {
+ const { rowSubInColSub } = customCalc
+ const rowSubLevel = getSubLevel(query, row)
+ const colSubLevel = getSubLevel(query, col)
+ const data = rowSubInColSub?.[rowSubLevel]?.[colSubLevel]?.data
+ const rowPath = getTreePath(query, row)
+ const colPath = getTreePath(query, col)
+ const path = [...rowPath, ...colPath]
+ let val
+ if (path.length && rowSubInColSub) {
+ path.push(quotaField)
+ val = get(data, path)
+ }
+ return val
+ }
+}
+function getCustomCalcResult(query, axisMap, chart: ChartObj, status: TotalStatus, customCalc) {
+ const { tableLayoutMode } = chart.customAttr.basicStyle
+ if (tableLayoutMode === 'tree') {
+ return getTreeCustomCalcResult(query, axisMap, status, customCalc)
+ }
+ return getGridCustomCalcResult(query, axisMap, status, customCalc)
+}
+
+function getSubLevel(query, axis) {
+ const fields: [] = axis.map(a => a.dataeaseName)
+ let subLevel = -1
+ const queryFields = keys(query)
+ for (let i = fields.length - 1; i >= 0; i--) {
+ const field = fields[i]
+ const index = queryFields.findIndex(f => f === field)
+ if (index !== -1) {
+ subLevel++
+ }
+ }
+ return subLevel
+}
+
+function getTreePath(query, axis) {
+ const path = []
+ const fields = keys(query)
+ axis.forEach(a => {
+ const index = fields.findIndex(f => f === a.dataeaseName)
+ if (index !== -1) {
+ path.push(query[a.dataeaseName])
+ }
+ })
+ return path
+}
+
+function getAggregationAndCalcFuncByQuery(totalsStatus, totalsOptions) {
+ const { isRowTotal, isRowSubTotal, isColTotal, isColSubTotal } = totalsStatus
+ const { row, col } = totalsOptions || {}
+ const { calcTotals: rowCalcTotals = {}, calcSubTotals: rowCalcSubTotals = {} } = row || {}
+ const { calcTotals: colCalcTotals = {}, calcSubTotals: colCalcSubTotals = {} } = col || {}
+
+ const getCalcTotals = (dimensionTotals: CalcTotals, isTotal: boolean) => {
+ if ((dimensionTotals.aggregation || dimensionTotals.calcFunc) && isTotal) {
+ return {
+ aggregation: dimensionTotals.aggregation,
+ calcFunc: dimensionTotals.calcFunc
+ }
+ }
+ }
+
+ // 优先级: 列总计/小计 > 行总计/小计
+ return (
+ getCalcTotals(colCalcTotals, isColTotal) ||
+ getCalcTotals(colCalcSubTotals, isColSubTotal) ||
+ getCalcTotals(rowCalcTotals, isRowTotal) ||
+ getCalcTotals(rowCalcSubTotals, isRowSubTotal)
+ )
+}
+
+export const isNotNumber = (value: unknown) => {
+ if (typeof value === 'number') {
+ return Number.isNaN(value)
+ }
+ if (!value) {
+ return true
+ }
+ if (typeof value === 'string') {
+ return Number.isNaN(Number(value))
+ }
+ return true
+}
+
+const processFieldValues = (data: DataItem[], field: string, filterIllegalValue = false) => {
+ if (!data?.length) {
+ return []
+ }
+
+ return data.reduce>((resultArr, item) => {
+ const fieldValue = get(item, field)
+ const notNumber = isNotNumber(fieldValue)
+
+ if (filterIllegalValue && notNumber) {
+ // 过滤非法值
+ return resultArr
+ }
+
+ const val = notNumber ? 0 : fieldValue
+ resultArr.push(new Decimal(val))
+
+ return resultArr
+ }, [])
+}
+
+export const getDataSumByField = (data: DataItem[], field: string): number => {
+ const fieldValues = processFieldValues(data, field)
+ if (!fieldValues.length) {
+ return 0
+ }
+
+ return Decimal.sum(...fieldValues).toNumber()
+}
+
+export const getDataExtremumByField = (
+ method: 'min' | 'max',
+ data: DataItem[],
+ field: string
+): number => {
+ // 防止预处理时默认值 0 影响极值结果,处理时需过滤非法值
+ const fieldValues = processFieldValues(data, field, true)
+ if (!fieldValues?.length) {
+ return
+ }
+
+ return Decimal[method](...fieldValues).toNumber()
+}
+
+export const getDataAvgByField = (data: DataItem[], field: string): number => {
+ const fieldValues = processFieldValues(data, field)
+ if (!fieldValues?.length) {
+ return 0
+ }
+
+ return Decimal.sum(...fieldValues)
+ .dividedBy(fieldValues.length)
+ .toNumber()
+}
+
+const calcActionByType: {
+ [type in Aggregation]: (data: DataItem[], field: string) => number
+} = {
+ [Aggregation.SUM]: getDataSumByField,
+ [Aggregation.MIN]: (data, field) => getDataExtremumByField('min', data, field),
+ [Aggregation.MAX]: (data, field) => getDataExtremumByField('max', data, field),
+ [Aggregation.AVG]: getDataAvgByField
+}
+
+class EmptyDataCell extends MergedCell {
+ drawTextShape(): void {
+ this.meta.fieldValue = ' '
+ super.drawTextShape()
+ const { rowHeader, columnHeader } = this.spreadsheet.facet
+ const offsetX = columnHeader.getConfig().viewportWidth / 2
+ const offsetY = rowHeader.getConfig().viewportHeight / 2
+ const style = this.getTextStyle()
+ const config = {
+ attrs: {
+ ...style,
+ x: offsetX,
+ y: offsetY,
+ text: t('data_set.no_data'),
+ opacity: 1,
+ textAlign: 'center',
+ textBaseline: 'middle'
+ }
+ }
+ this.addShape('text', config)
+ }
+
+ protected drawBackgroundShape(): void {
+ const cellTheme = this.theme.dataCell.cell
+ cellTheme.backgroundColor = setColorOpacity(cellTheme.backgroundColor, 1)
+ super.drawBackgroundShape()
+ }
+}
+
+export function setColorOpacity(color: string, opacity: number) {
+ if (color.indexOf('rgba') !== -1) {
+ const colorArr = color.split(',')
+ colorArr[3] = `${opacity})`
+ return colorArr.join(',')
+ }
+ if (color.indexOf('rgb') !== -1) {
+ return `${color.replace('rgb', 'rgba').replace(')', `,${opacity})`)}`
+ }
+ if (color.indexOf('#') !== -1) {
+ if (color.length === 7) {
+ return `${color}${Math.round(opacity * 255).toString(16)}`
+ }
+ if (color.length === 9) {
+ return color.slice(0, 7) + Math.round(opacity * 255).toString(16)
+ }
+ }
+ return color
+}
+
+function configEmptyDataStyle(instance: PivotSheet, data: any[]) {
+ if (data?.length) {
+ return
+ }
+ instance.on(S2Event.LAYOUT_AFTER_RENDER, () => {
+ const { colLeafNodes, rowLeafNodes } = instance.facet?.layoutResult || {}
+ if (!colLeafNodes?.length || !rowLeafNodes?.length) {
+ return
+ }
+ const mergedCells = []
+ colLeafNodes.forEach((_, colIndex) => {
+ rowLeafNodes.forEach((__, rowIndex) => {
+ mergedCells.push({ rowIndex, colIndex })
+ })
+ })
+ instance.options.mergedCell = (s, c, m) => new EmptyDataCell(s, c, m)
+ instance.interaction.mergeCells(mergedCells)
+ })
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/common/common_antv.ts b/frontend/src/data-visualization/chart/components/js/panel/common/common_antv.ts
new file mode 100644
index 0000000..8bc7d32
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/common/common_antv.ts
@@ -0,0 +1,1963 @@
+import { hexColorToRGBA, hexToRgba, measureText, parseJson } from '../../util'
+import {
+ DEFAULT_BASIC_STYLE,
+ DEFAULT_LEGEND_STYLE,
+ DEFAULT_XAXIS_STYLE,
+ DEFAULT_YAXIS_EXT_STYLE,
+ DEFAULT_YAXIS_STYLE
+} from '@/data-visualization/chart/components/editor/util/chart'
+import { valueFormatter } from '@/data-visualization/chart/components/js/formatter'
+import { AreaOptions, LabelOptions } from '@antv/l7plot'
+import { TooltipOptions } from '@antv/l7plot/dist/lib/types/tooltip'
+import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types'
+import { Datum } from '@antv/g2plot/esm/types/common'
+import { Tooltip } from '@antv/g2plot/esm'
+import { add } from 'mathjs'
+import isEmpty from 'lodash-es/isEmpty'
+import _ from 'lodash'
+import type { LegendOptions } from '@antv/l7plot/dist/esm/types/legend'
+import { CategoryLegendListItem } from '@antv/l7plot-component/dist/lib/types/legend'
+import createDom from '@antv/dom-util/esm/create-dom'
+import {
+ CONTAINER_TPL,
+ ITEM_TPL,
+ LIST_CLASS
+} from '@antv/l7plot-component/dist/esm/legend/category/constants'
+import substitute from '@antv/util/esm/substitute'
+import type { Plot as L7Plot, PlotOptions } from '@antv/l7plot/dist/esm'
+import { Zoom } from '@antv/l7'
+import { DOM } from '@antv/l7-utils'
+import { Scene } from '@antv/l7-scene'
+import { type IZoomControlOption } from '@antv/l7-component'
+import { PositionType } from '@antv/l7-core'
+import { centroid } from '@turf/centroid'
+import type { Plot } from '@antv/g2plot'
+import type { PickOptions } from '@antv/g2plot/lib/core/plot'
+import { defaults } from 'lodash-es'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+const { t: tI18n } = useI18n()
+import { isMobile } from '@/data-visualization/utils/utils'
+
+export function getPadding(chart: Chart): number[] {
+ if (chart.drill) {
+ return [0, 10, 22, 10]
+ } else {
+ return [0, 10, 10, 10]
+ }
+}
+// color,label,tooltip,axis,legend,background
+export function getTheme(chart: Chart) {
+ const colors = []
+ let bgColor,
+ labelFontsize,
+ labelColor,
+ tooltipColor,
+ tooltipFontsize,
+ tooltipBackgroundColor,
+ legendColor,
+ legendFontsize
+ let customAttr: DeepPartial
+ if (chart.customAttr) {
+ customAttr = parseJson(chart.customAttr)
+ // color
+ if (customAttr.basicStyle) {
+ const b = JSON.parse(JSON.stringify(customAttr.basicStyle))
+ b.colors.forEach(ele => {
+ colors.push(hexColorToRGBA(ele, b.alpha))
+ })
+ }
+ // label
+ if (customAttr.label) {
+ const l = JSON.parse(JSON.stringify(customAttr.label))
+ labelFontsize = l.fontSize
+ labelColor = l.color
+ }
+ // tooltip
+ if (customAttr.tooltip) {
+ const t = JSON.parse(JSON.stringify(customAttr.tooltip))
+ tooltipColor = t.color
+ tooltipFontsize = t.fontSize
+ tooltipBackgroundColor = t.backgroundColor
+ }
+ }
+
+ let customStyle: DeepPartial
+ if (chart.customStyle) {
+ customStyle = parseJson(chart.customStyle)
+ // bg
+ if (customStyle.background) {
+ bgColor = hexColorToRGBA(customStyle.background.color, customStyle.background.alpha)
+ }
+ // legend
+ if (customStyle.legend) {
+ const l = customStyle.legend
+ legendColor = l.color
+ legendFontsize = l.fontSize
+ }
+ }
+
+ const theme = {
+ styleSheet: {
+ brandColor: colors[0],
+ paletteQualitative10: colors,
+ paletteQualitative20: colors,
+ backgroundColor: bgColor
+ },
+ labels: {
+ offset: 4,
+ style: {
+ fill: labelColor,
+ fontSize: labelFontsize
+ }
+ },
+ innerLabels: {
+ offset: 4,
+ style: {
+ fill: labelColor,
+ fontSize: labelFontsize
+ }
+ },
+ pieLabels: {
+ offset: 4,
+ style: {
+ fill: labelColor,
+ fontSize: labelFontsize
+ }
+ },
+ components: {
+ tooltip: {
+ domStyles: {
+ 'g2-tooltip': {
+ color: tooltipColor,
+ fontSize: tooltipFontsize + 'px',
+ background: tooltipBackgroundColor,
+ boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.1)',
+ 'z-index': 2000,
+ position: 'fixed'
+ },
+ 'g2-tooltip-list-item': {
+ display: 'flex',
+ 'align-items': 'center'
+ },
+ 'g2-tooltip-name': {
+ display: 'inline-block',
+ 'line-height': tooltipFontsize + 'px',
+ flex: 1
+ },
+ 'g2-tooltip-marker': {
+ 'min-width': '8px',
+ 'min-height': '8px'
+ }
+ }
+ },
+ legend: {
+ common: {
+ itemName: {
+ style: {
+ fill: legendColor,
+ fontSize: legendFontsize
+ }
+ }
+ }
+ }
+ }
+ }
+ if (chart.fontFamily) {
+ theme.styleSheet.fontFamily = chart.fontFamily
+ }
+ return theme
+}
+// 通用label
+export function getLabel(chart: Chart) {
+ let label
+ let customAttr: DeepPartial
+ if (chart.customAttr) {
+ customAttr = parseJson(chart.customAttr)
+ // label
+ if (customAttr.label) {
+ const l = customAttr.label
+ if (l.show) {
+ const layout = []
+ if (!l.fullDisplay) {
+ if (chart.type === 'bar-stack') {
+ layout.push({ type: 'interval-hide-overlap' })
+ } else if (
+ chart.type.indexOf('-horizontal') > -1 ||
+ [
+ 'bidirectional-bar',
+ 'progress-bar',
+ 'pie-donut',
+ 'radar',
+ 'waterfall',
+ 't-heatmap',
+ 'bar'
+ ].includes(chart.type)
+ ) {
+ layout.push({ type: 'limit-in-canvas' })
+ layout.push({ type: 'hide-overlap' })
+ } else if (chart.type.includes('chart-mix')) {
+ layout.push({ type: 'limit-in-canvas' })
+ layout.push({ type: 'limit-in-plot' })
+ layout.push({ type: 'hide-overlap' })
+ } else {
+ layout.push({ type: 'limit-in-plot' })
+ layout.push({ type: 'hide-overlap' })
+ }
+ }
+ label = {
+ position: l.position,
+ layout,
+ style: {
+ fill: l.color,
+ fontSize: l.fontSize,
+ fontFamily: chart.fontFamily
+ },
+ formatter: function (param: Datum) {
+ return valueFormatter(param.value, l.labelFormatter)
+ }
+ }
+ } else {
+ label = false
+ }
+ }
+ }
+ return label
+}
+// 通用tooltip
+export function getTooltip(chart: Chart) {
+ let tooltip
+ let customAttr: DeepPartial
+ if (chart.customAttr) {
+ customAttr = parseJson(chart.customAttr)
+ // tooltip
+ if (customAttr.tooltip) {
+ const t = JSON.parse(JSON.stringify(customAttr.tooltip))
+ if (t.show) {
+ tooltip = {
+ formatter: function (param: Datum) {
+ const value = valueFormatter(param.value, t.tooltipFormatter)
+ return { name: param.field, value }
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ } else {
+ tooltip = false
+ }
+ }
+ }
+ return tooltip
+}
+
+export function getMultiSeriesTooltip(chart: Chart) {
+ const customAttr: DeepPartial = parseJson(chart.customAttr)
+ const tooltipAttr = customAttr.tooltip
+ if (!tooltipAttr.show) {
+ return false
+ }
+ const formatterMap = tooltipAttr.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {}) as Record
+ const tooltip: Tooltip = {
+ 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)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ const color = getTooltipItemConditionColor(item)
+ result.push({ ...item, name, value, ...(color ? { color } : {}) })
+ })
+ 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
+ },
+ container: getTooltipContainer(`tooltip-${chart.id}`),
+ itemTpl: TOOLTIP_TPL,
+ enterable: true
+ }
+ return tooltip
+}
+// 通用legend
+export function getLegend(chart: Chart) {
+ let legend = {}
+ let customStyle: CustomStyle
+ if (chart.customStyle) {
+ customStyle = parseJson(chart.customStyle)
+ // legend
+ if (customStyle.legend) {
+ const l = defaults(JSON.parse(JSON.stringify(customStyle.legend)), DEFAULT_LEGEND_STYLE)
+ if (l.show) {
+ let offsetX, offsetY, position
+ const orient = l.orient
+ const legendSymbol = l.icon
+ // fix position
+ if (l.hPosition === 'center') {
+ position = l.vPosition === 'center' ? 'top' : l.vPosition
+ } else if (l.vPosition === 'center') {
+ position = l.hPosition === 'center' ? 'left' : l.hPosition
+ } else {
+ if (orient === 'horizontal') {
+ position = l.vPosition + '-' + l.hPosition
+ } else {
+ position = l.hPosition + '-' + l.vPosition
+ }
+ }
+ // fix offset
+ if (orient === 'horizontal') {
+ if (l.hPosition === 'left') {
+ offsetX = 16
+ } else if (l.hPosition === 'right') {
+ offsetX = -16
+ } else {
+ offsetX = 0
+ }
+ if (l.vPosition === 'top') {
+ offsetY = 0
+ } else if (l.vPosition === 'bottom') {
+ if (chart.drill) {
+ offsetY = -12
+ }
+ } else {
+ offsetY = 0
+ }
+ } else {
+ if (l.hPosition === 'left') {
+ offsetX = 10
+ } else if (l.hPosition === 'right') {
+ offsetX = -10
+ } else {
+ offsetX = 0
+ }
+ if (l.vPosition === 'top') {
+ offsetY = 0
+ } else if (l.vPosition === 'bottom') {
+ if (chart.drill) {
+ offsetY = -18
+ } else {
+ offsetY = -10
+ }
+ } else {
+ offsetY = 0
+ }
+ }
+
+ legend = {
+ layout: orient,
+ position: position,
+ offsetX: offsetX,
+ offsetY: offsetY,
+ marker: {
+ symbol: legendSymbol,
+ style: {
+ r: l.size
+ }
+ },
+ itemName: {
+ style: {
+ fill: l.color,
+ fontSize: l.fontSize
+ }
+ },
+ itemHeight: (l.fontSize > l.size * 2 ? l.fontSize : l.size * 2) + 4,
+ radio: false,
+ pageNavigator: {
+ marker: {
+ style: {
+ fill: 'rgba(0,0,0,0.65)',
+ stroke: 'rgba(192,192,192,0.52)',
+ size: l.size * 2
+ }
+ },
+ text: {
+ style: {
+ fill: l.color,
+ fontSize: l.fontSize
+ }
+ }
+ }
+ }
+ } else {
+ legend = false
+ }
+ }
+ }
+ return legend
+}
+// xAxis
+export function getXAxis(chart: Chart) {
+ let axis: Record | boolean = {}
+ let customStyle: CustomStyle
+ if (chart.customStyle) {
+ customStyle = parseJson(chart.customStyle)
+ // legend
+ if (customStyle.xAxis) {
+ const a = JSON.parse(JSON.stringify(customStyle.xAxis))
+ if (a.show) {
+ const title =
+ a.nameShow && a.name && a.name !== ''
+ ? {
+ text: a.name,
+ style: {
+ fill: a.color,
+ fontSize: a.fontSize
+ },
+ spacing: 8
+ }
+ : null
+ const grid = a.splitLine.show
+ ? {
+ line: {
+ style: {
+ stroke: a.splitLine.lineStyle.color,
+ lineWidth: a.splitLine.lineStyle.width,
+ lineDash: getLineDash(a.splitLine.lineStyle.style)
+ }
+ }
+ }
+ : null
+ const axisCfg = a.axisLine ? a.axisLine : DEFAULT_XAXIS_STYLE.axisLine
+ const line = axisCfg.show
+ ? {
+ style: {
+ stroke: axisCfg.lineStyle.color,
+ lineWidth: axisCfg.lineStyle.width,
+ lineDash: getLineDash(axisCfg.lineStyle.style)
+ }
+ }
+ : null
+ const tickLine = axisCfg.show
+ ? {
+ style: {
+ stroke: axisCfg.lineStyle.color,
+ lineWidth: axisCfg.lineStyle.width
+ }
+ }
+ : null
+ let textAlign = 'center'
+ const rotate = a.axisLabel.rotate
+ if (a.position === 'top') {
+ textAlign = rotate > 20 ? 'end' : rotate < -20 ? 'start' : 'center'
+ }
+ if (a.position === 'bottom') {
+ textAlign = rotate > 20 ? 'start' : rotate < -20 ? 'end' : 'center'
+ }
+ const label = a.axisLabel.show
+ ? {
+ rotate: (rotate * Math.PI) / 180,
+ style: {
+ fill: a.axisLabel.color,
+ fontSize: a.axisLabel.fontSize,
+ textAlign: textAlign
+ },
+ formatter: value => {
+ return chart.type === 'bidirectional-bar' && value.length > a.axisLabel.lengthLimit
+ ? value.substring(0, a.axisLabel.lengthLimit) + '...'
+ : value
+ }
+ }
+ : null
+
+ axis = {
+ position: a.position,
+ title,
+ grid,
+ label,
+ line,
+ tickLine
+ }
+ } else {
+ axis = false
+ }
+ }
+ }
+ return axis
+}
+// yAxis
+export function getYAxis(chart: Chart) {
+ let axis: Record | boolean = {}
+ const yAxis = parseJson(chart.customStyle).yAxis
+ if (!yAxis.show) {
+ return false
+ }
+ const title =
+ yAxis.nameShow && 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,
+ lineDash: getLineDash(yAxis.splitLine.lineStyle.style)
+ }
+ }
+ }
+ : null
+ const axisCfg = yAxis.axisLine ? yAxis.axisLine : DEFAULT_YAXIS_STYLE.axisLine
+ const line = axisCfg.show
+ ? {
+ style: {
+ stroke: axisCfg.lineStyle.color,
+ lineWidth: axisCfg.lineStyle.width,
+ lineDash: getLineDash(axisCfg.lineStyle.style)
+ }
+ }
+ : null
+ const tickLine = axisCfg.show
+ ? {
+ style: {
+ stroke: axisCfg.lineStyle.color,
+ lineWidth: axisCfg.lineStyle.width
+ }
+ }
+ : 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
+ },
+ formatter: value => {
+ return value.length > yAxis.axisLabel.lengthLimit
+ ? value.substring(0, yAxis.axisLabel.lengthLimit) + '...'
+ : value
+ }
+ }
+ : null
+
+ axis = {
+ position: yAxis.position,
+ title,
+ grid,
+ label,
+ line,
+ tickLine,
+ nice: true
+ }
+ 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,
+ lineDash: getLineDash(yAxis.splitLine.lineStyle.style)
+ }
+ }
+ }
+ : 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,
+ nice: true
+ }
+ return axis
+}
+
+export function getSlider(chart: Chart) {
+ let cfg
+ const senior = parseJson(chart.senior)
+ if (senior.functionCfg) {
+ if (senior.functionCfg.sliderShow) {
+ cfg = {
+ start: senior.functionCfg.sliderRange[0] / 100,
+ end: senior.functionCfg.sliderRange[1] / 100
+ }
+
+ if (senior.functionCfg.sliderBg) {
+ cfg.backgroundStyle = {
+ fill: senior.functionCfg.sliderBg,
+ stroke: senior.functionCfg.sliderBg,
+ lineWidth: 1,
+ strokeOpacity: 0.5
+ }
+ }
+ if (senior.functionCfg.sliderFillBg) {
+ cfg.foregroundStyle = {
+ fill: senior.functionCfg.sliderFillBg,
+ fillOpacity: 0.5
+ }
+ }
+ if (senior.functionCfg.sliderTextColor) {
+ cfg.textStyle = {
+ fill: senior.functionCfg.sliderTextColor,
+ fontFamily: chart.fontFamily
+ }
+ cfg.handlerStyle = {
+ fill: senior.functionCfg.sliderTextColor,
+ fillOpacity: 0.5,
+ highLightFill: senior.functionCfg.sliderTextColor
+ }
+ }
+ }
+ }
+ return cfg
+}
+
+export function getAnalyse(chart: Chart) {
+ const assistLine = []
+ const senior = parseJson(chart.senior)
+ if (!senior.assistLineCfg?.enable) {
+ return assistLine
+ }
+ const assistLineArr = senior.assistLineCfg.assistLine
+ if (assistLineArr?.length > 0) {
+ const customStyle = parseJson(chart.customStyle)
+ let yAxisPosition, axisFormatterCfg, yAxisExtPosition, axisExtFormatterCfg
+ if (customStyle.yAxis) {
+ const a = JSON.parse(JSON.stringify(customStyle.yAxis))
+ yAxisPosition = a.position
+ axisFormatterCfg = a.axisLabelFormatter
+ ? a.axisLabelFormatter
+ : DEFAULT_YAXIS_STYLE.axisLabelFormatter
+ }
+ if (customStyle.yAxisExt) {
+ const a = JSON.parse(JSON.stringify(customStyle.yAxisExt))
+ yAxisExtPosition = a.position
+ axisExtFormatterCfg = a.axisLabelFormatter
+ ? a.axisLabelFormatter
+ : DEFAULT_YAXIS_EXT_STYLE.axisLabelFormatter
+ }
+
+ const fixedLines = assistLineArr.filter(ele => ele.field === '0')
+ const dynamicLineFields = assistLineArr
+ .filter(ele => ele.field === '1')
+ .map(item => item.fieldId)
+ const quotaFields = _.filter(chart.yAxis, ele => ele.summary !== '' && ele.id !== '-1')
+ const quotaExtFields = _.filter(chart.yAxisExt, ele => ele.summary !== '' && ele.id !== '-1')
+ const dynamicLines = chart.data.dynamicAssistLines?.filter(item => {
+ return (
+ dynamicLineFields?.includes(item.fieldId) &&
+ (!!_.find(quotaFields, d => d.id === item.fieldId) ||
+ (!!_.find(quotaExtFields, d => d.id === item.fieldId) &&
+ chart.type.includes('chart-mix')))
+ )
+ })
+ const lines = fixedLines.concat(dynamicLines || [])
+ lines.forEach(ele => {
+ const value = parseFloat(ele.value)
+ const content =
+ ele.name +
+ ' : ' +
+ valueFormatter(value, ele.yAxisType === 'left' ? axisFormatterCfg : axisExtFormatterCfg)
+ assistLine.push({
+ type: 'line',
+ yAxisType: ele.yAxisType,
+ start: ['start', value],
+ end: ['end', value],
+ style: {
+ stroke: ele.color,
+ lineDash: getLineDash(ele.lineType)
+ }
+ })
+ assistLine.push({
+ type: 'text',
+ yAxisType: ele.yAxisType,
+ position: [
+ (ele.yAxisType === 'left' ? yAxisPosition : yAxisExtPosition) === 'left'
+ ? 'start'
+ : 'end',
+ value
+ ],
+ content: content,
+ offsetY: -2,
+ offsetX:
+ (ele.yAxisType === 'left' ? yAxisPosition : yAxisExtPosition) === 'left'
+ ? 2
+ : -10 * (content.length - 2),
+ style: {
+ textBaseline: 'bottom',
+ fill: ele.color,
+ fontSize: ele.fontSize ? ele.fontSize : 10
+ }
+ })
+ })
+ }
+ return assistLine
+}
+
+export function getAnalyseHorizontal(chart: Chart) {
+ const assistLine = []
+ const senior = parseJson(chart.senior)
+ if (!senior.assistLineCfg?.enable) {
+ return assistLine
+ }
+ const assistLineArr = senior.assistLineCfg.assistLine
+ if (assistLineArr?.length > 0) {
+ const customStyle = parseJson(chart.customStyle)
+ let xAxisPosition, axisFormatterCfg
+ if (customStyle.xAxis) {
+ const a = JSON.parse(JSON.stringify(customStyle.xAxis))
+ xAxisPosition = transAxisPosition(a.position)
+ axisFormatterCfg = a.axisLabelFormatter
+ ? a.axisLabelFormatter
+ : DEFAULT_XAXIS_STYLE.axisLabelFormatter
+ }
+
+ const fixedLines = assistLineArr.filter(ele => ele.field === '0')
+ const dynamicLineFields = assistLineArr
+ .filter(ele => ele.field === '1')
+ .map(item => item.fieldId)
+ const quotaFields = _.filter(chart.yAxis, ele => ele.summary !== '' && ele.id !== '-1')
+ const dynamicLines = chart.data.dynamicAssistLines?.filter(
+ item =>
+ dynamicLineFields?.includes(item.fieldId) &&
+ !!_.find(quotaFields, d => d.id === item.fieldId)
+ )
+ const lines = fixedLines.concat(dynamicLines || [])
+
+ lines.forEach(ele => {
+ const value = parseFloat(ele.value)
+ const content = ele.name + ' : ' + valueFormatter(value, axisFormatterCfg)
+ assistLine.push({
+ type: 'line',
+ start: ['start', value],
+ end: ['end', value],
+ style: {
+ stroke: ele.color,
+ lineDash: getLineDash(ele.lineType)
+ }
+ })
+ assistLine.push({
+ type: 'text',
+ position: ['start', value],
+ content: content,
+ offsetY: 5,
+ offsetX: 2,
+ rotate: Math.PI / 2,
+ style: {
+ textBaseline: 'bottom',
+ fill: ele.color,
+ fontSize: ele.fontSize ? ele.fontSize : 10
+ }
+ })
+ })
+ }
+ return assistLine
+}
+
+export function getLineDash(type) {
+ switch (type) {
+ case 'solid':
+ return [0, 0]
+ case 'dashed':
+ return [10, 8]
+ case 'dotted':
+ return [2, 2]
+ default:
+ return [0, 0]
+ }
+}
+
+/**
+ * 将 RGBA 格式的颜色转换成 ANTV 支持的渐变色格式
+ * @param rawColor 原始 RGBA 颜色
+ * @param show
+ * @param angle 渐变角度
+ * @param start 起始值
+ */
+export function setGradientColor(rawColor: string, show = false, angle = 0, start = 0) {
+ const item = rawColor.split(',')
+ item.splice(3, 1, '0.3)')
+ let color: string
+ if (start == 0) {
+ color = `l(${angle}) 0:${item.join(',')} 1:${rawColor}`
+ } else if (start > 0) {
+ color = `l(${angle}) 0:rgba(255,255,255,0) ${start}:${item.join(',')} 1:${rawColor}`
+ } else {
+ color = `l(${angle}) 0:rgba(255,255,255,0) 0.1:${item.join(',')} 1:${rawColor}`
+ }
+ return show ? color : rawColor
+}
+
+export function transAxisPosition(position: string): string {
+ switch (position) {
+ case 'top':
+ return 'left'
+ case 'bottom':
+ return 'right'
+ case 'left':
+ return 'bottom'
+ case 'right':
+ return 'top'
+ default:
+ return position
+ }
+}
+
+export function configL7Label(chart: Chart): false | LabelOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const label = customAttr.label
+ const style = {
+ fill: label.color,
+ fontSize: label.fontSize,
+ textAllowOverlap: true,
+ fontWeight: 'bold'
+ }
+ if (!label.fullDisplay) {
+ style.textAllowOverlap = false
+ style.padding = [2, 2]
+ }
+ if (chart.fontFamily) {
+ style.fontFamily = chart.fontFamily
+ }
+ return {
+ visible: label.show,
+ style
+ }
+}
+
+export function configL7Style(chart: Chart): AreaOptions['style'] {
+ const customAttr = parseJson(chart.customAttr)
+ return {
+ stroke: customAttr.basicStyle.areaBorderColor
+ }
+}
+
+export function configL7Tooltip(chart: Chart): TooltipOptions {
+ const customAttr = parseJson(chart.customAttr)
+ const tooltip = customAttr.tooltip
+ const formatterMap = tooltip.seriesTooltipFormatter
+ ?.filter(i => i.show)
+ .reduce((pre, next) => {
+ pre[next.id] = next
+ return pre
+ }, {}) as Record
+ const container = document.getElementById(chart.container)
+ if (container) {
+ container.addEventListener('mousemove', event => {
+ const rect = container.getBoundingClientRect()
+ const mouseX = event.clientX - rect.left
+ const mouseY = event.clientY - rect.top
+ const tooltipElement = container.getElementsByClassName('l7plot-tooltip-container')
+ for (let i = 0; i < tooltipElement?.length; i++) {
+ const element = tooltipElement[i] as HTMLElement
+ const isNearRightEdge = container.clientWidth - mouseX <= element.clientWidth
+ const isNearBottomEdge = container.clientHeight - mouseY <= element.clientHeight
+ let transform = ''
+ if (isNearRightEdge) {
+ transform += 'translateX(-120%) '
+ }
+ if (isNearBottomEdge) {
+ transform += 'translateY(-100%) '
+ }
+ if (transform) {
+ element.style.transform = transform.trim()
+ }
+ }
+ })
+ }
+ return {
+ customTitle(data) {
+ return data.name
+ },
+ customItems(originalItem) {
+ const result = []
+ if (isEmpty(formatterMap)) {
+ return result
+ }
+ const head = originalItem.properties
+ const formatter = formatterMap[head.quotaList?.[0]?.id]
+ if (!isEmpty(formatter)) {
+ const originValue = parseFloat(head.value as string)
+ const value = valueFormatter(originValue, formatter.formatterCfg)
+ const name = isEmpty(formatter.chartShowName) ? formatter.name : formatter.chartShowName
+ result.push({ ...head, name, value: `${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: `${value ?? ''}` })
+ }
+ })
+ return result
+ },
+ showComponent: tooltip.show,
+ domStyles: {
+ 'l7plot-tooltip': {
+ 'background-color': tooltip.backgroundColor,
+ 'font-size': `${tooltip.fontSize}px`,
+ 'line-height': 1.6,
+ 'font-family': chart.fontFamily ? chart.fontFamily : undefined
+ },
+ 'l7plot-tooltip__name': {
+ color: tooltip.color
+ },
+ 'l7plot-tooltip__value': {
+ color: tooltip.color
+ },
+ 'l7plot-tooltip__title': {
+ color: tooltip.color
+ }
+ }
+ }
+}
+
+export function handleGeoJson(geoJson: FeatureCollection, nameMapping?: Record) {
+ geoJson.features.forEach(item => {
+ if (!item.properties['centroid']) {
+ if (item.properties['center']) {
+ item.properties['centroid'] = item.properties['center']
+ } else {
+ const tmp = centroid(item.geometry)
+ item.properties['centroid'] = tmp.geometry.coordinates
+ }
+ }
+ let name = item.properties['name']
+ if (nameMapping?.[name]) {
+ name = nameMapping[name]
+ item.properties['name'] = name
+ }
+ })
+}
+
+export function getTooltipSeriesTotalMap(data: any[]): Record {
+ const result = {}
+ data?.forEach(item => {
+ item.dynamicTooltipValue?.forEach(ele => {
+ if (!result[ele.fieldId]) {
+ result[ele.fieldId] = 0
+ }
+ if (ele.value) {
+ result[ele.fieldId] = add(result[ele.fieldId], ele.value)
+ }
+ })
+ })
+ return result
+}
+const LEGEND_SHAPE_STYLE_MAP = {
+ circle: {
+ borderRadius: '50%'
+ },
+ square: {},
+ triangle: {
+ borderLeft: '5px solid transparent',
+ borderRight: '5px solid transparent',
+ borderBottom: '10px solid var(--bgColor)',
+ background: 'unset'
+ },
+ diamond: {
+ transform: 'rotate(45deg)'
+ }
+}
+export function configL7Legend(chart: Chart): LegendOptions | false {
+ const { basicStyle } = parseJson(chart.customAttr)
+ if (basicStyle.suspension === false && basicStyle.showZoom === undefined) {
+ return false
+ }
+ const { legend } = parseJson(chart.customStyle)
+ if (!legend.show) {
+ return false
+ }
+ return {
+ position: 'bottomleft',
+ customContent: (_: string, items: CategoryLegendListItem[]) => {
+ const showItems = items?.length > 30 ? items.slice(0, 30) : items
+ if (showItems?.length) {
+ const containerDom = createDom(CONTAINER_TPL) as HTMLElement
+ const listDom = containerDom.getElementsByClassName(LIST_CLASS)[0] as HTMLElement
+ showItems.forEach(item => {
+ let value = '-'
+ if (item.value !== '') {
+ if (Array.isArray(item.value)) {
+ item.value.forEach((v, i) => {
+ item.value[i] = Number.isNaN(v) || v === 'NaN' ? 'NaN' : parseFloat(v).toFixed(0)
+ })
+ value = item.value.join('-')
+ } else {
+ const tmp = item.value as string
+ value = Number.isNaN(tmp) || tmp === 'NaN' ? 'NaN' : parseFloat(tmp).toFixed(0)
+ }
+ }
+ const substituteObj = { ...item, value }
+
+ const domStr = substitute(ITEM_TPL, substituteObj)
+ const itemDom = createDom(domStr)
+ // 给 legend 形状用的
+ itemDom.style.setProperty('--bgColor', item.color)
+ listDom.appendChild(itemDom)
+ })
+ return listDom
+ }
+ return ''
+ },
+ domStyles: {
+ 'l7plot-legend__category-value': {
+ fontSize: legend.fontSize + 'px',
+ color: legend.color
+ },
+ 'l7plot-legend__category-marker': {
+ ...LEGEND_SHAPE_STYLE_MAP[legend.icon]
+ }
+ }
+ }
+}
+const ZOOM_IN_BTN =
+ ''
+const RESET_BTN =
+ ''
+const ZOOM_OUT_BTN =
+ ''
+export class CustomZoom extends Zoom {
+ resetButtonGroup(container) {
+ DOM.clearChildren(container)
+ this['zoomInButton'] = this['createButton'](
+ this.controlOption.zoomInText,
+ this.controlOption.zoomInTitle,
+ 'l7-button-control',
+ container,
+ this.zoomIn
+ )
+ this['zoomResetButton'] = this['createButton'](
+ this.controlOption['resetText'],
+ 'Reset',
+ 'l7-button-control',
+ container,
+ () => {
+ if (this.controlOption['bounds']) {
+ this.mapsService.fitBounds(this.controlOption['bounds'], { animate: true })
+ } else {
+ this.mapsService.setZoomAndCenter(
+ this.controlOption['initZoom'],
+ this.controlOption['center']
+ )
+ }
+ }
+ )
+ if (this.controlOption.showZoom) {
+ this['zoomNumDiv'] = this['createButton'](
+ '0',
+ '',
+ 'l7-button-control l7-control-zoom__number',
+ container
+ )
+ }
+ this['zoomOutButton'] = this['createButton'](
+ this.controlOption.zoomOutText,
+ this.controlOption.zoomOutTitle,
+ 'l7-button-control',
+ container,
+ this.zoomOut
+ )
+ const { buttonBackground } = this.controlOption as any
+ const elements = [this['zoomResetButton'], this['zoomInButton'], this['zoomOutButton']]
+ if (buttonBackground) {
+ setStyle(elements, 'background', buttonBackground)
+ }
+ setStyle(elements, 'border-bottom', 'none')
+ this['updateDisabled']()
+ }
+ public getDefault(option: Partial) {
+ const { buttonColor } = option as any
+ let zoomInText = ZOOM_IN_BTN
+ let zoomOutText = ZOOM_OUT_BTN
+ let resetText = RESET_BTN
+ if (buttonColor) {
+ zoomInText = zoomInText.replace('${fill}', buttonColor)
+ zoomOutText = zoomOutText.replace('${fill}', buttonColor)
+ resetText = resetText.replace('${fill}', buttonColor)
+ }
+ return {
+ ...option,
+ position: PositionType.BOTTOMRIGHT,
+ name: 'zoom',
+ zoomInText,
+ zoomInTitle: 'Zoom in',
+ zoomOutText,
+ zoomOutTitle: 'Zoom out',
+ resetText,
+ showZoom: false
+ } as IZoomControlOption
+ }
+}
+export function configL7Zoom(chart: Chart, scene: Scene) {
+ const { basicStyle } = parseJson(chart.customAttr)
+ const zoomOption = scene?.getControlByName('zoom')
+ if (zoomOption) {
+ scene.removeControl(zoomOption)
+ }
+ if (shouldHideZoom(basicStyle)) {
+ return
+ }
+ if (!scene?.getControlByName('zoom')) {
+ if (!scene.map) {
+ scene.once('loaded', () => {
+ scene.map.on('complete', () => {
+ const initZoom = basicStyle.autoFit === false ? basicStyle.zoomLevel : scene.getZoom()
+ const center =
+ basicStyle.autoFit === false
+ ? [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude]
+ : [scene.map.getCenter().lng, scene.map.getCenter().lat]
+ const newZoomOptions = {
+ initZoom: initZoom,
+ center: center,
+ buttonColor: basicStyle.zoomButtonColor,
+ buttonBackground: basicStyle.zoomBackground
+ } as any
+ scene.addControl(new CustomZoom(newZoomOptions))
+ })
+ })
+ } else {
+ const newZoomOptions = {
+ buttonColor: basicStyle.zoomButtonColor,
+ buttonBackground: basicStyle.zoomBackground
+ } as any
+ if (basicStyle.autoFit === false) {
+ newZoomOptions.initZoom = basicStyle.zoomLevel
+ newZoomOptions.center = [basicStyle.mapCenter.longitude, basicStyle.mapCenter.latitude]
+ } else {
+ const coordinates: [][] = []
+ if (chart.type === 'flow-map') {
+ const startAxis = chart.xAxis
+ const endAxis = chart.xAxisExt
+ if (startAxis?.length === 2) {
+ chart.data?.tableRow?.forEach(row => {
+ coordinates.push([row[startAxis[0].dataeaseName], row[startAxis[1].dataeaseName]])
+ })
+ }
+ if (endAxis?.length === 2) {
+ chart.data?.tableRow?.forEach(row => {
+ coordinates.push([row[endAxis[0].dataeaseName], row[endAxis[1].dataeaseName]])
+ })
+ }
+ } else {
+ const axis = chart.xAxis
+ if (axis?.length === 2) {
+ chart.data?.tableRow?.forEach(row => {
+ coordinates.push([row[axis[0].dataeaseName], row[axis[1].dataeaseName]])
+ })
+ }
+ }
+ newZoomOptions.bounds = calculateBounds(coordinates)
+ }
+ scene.addControl(new CustomZoom(newZoomOptions))
+ }
+ }
+}
+/**
+ * 计算经纬度数据的边界点
+ * @param coordinates 经纬度数组 [[lng, lat], [lng, lat], ...]
+ * @returns {[[number, number], [number, number]]} 返回东北角和西南角的坐标
+ */
+export function calculateBounds(coordinates: number[][]): {
+ northEast: [number, number]
+ southWest: [number, number]
+} {
+ if (!coordinates || coordinates.length === 0) {
+ return {
+ northEast: [180, 90],
+ southWest: [-180, -90]
+ }
+ }
+
+ let maxLng = -180
+ let minLng = 180
+ let maxLat = -90
+ let minLat = 90
+
+ coordinates.forEach(([lng, lat]) => {
+ maxLng = Math.max(maxLng, lng)
+ minLng = Math.min(minLng, lng)
+ maxLat = Math.max(maxLat, lat)
+ minLat = Math.min(minLat, lat)
+ })
+
+ return [
+ [maxLng, maxLat], // 东北角坐标
+ [minLng, minLat] // 西南角坐标
+ ]
+}
+
+export function configL7PlotZoom(chart: Chart, plot: L7Plot) {
+ const { basicStyle } = parseJson(chart.customAttr)
+ if (shouldHideZoom(basicStyle)) {
+ return
+ }
+ plot.once('loaded', () => {
+ const zoomOptions = {
+ initZoom: plot.scene.getZoom(),
+ center: plot.scene.getCenter(),
+ buttonColor: basicStyle.zoomButtonColor,
+ buttonBackground: basicStyle.zoomBackground
+ } as any
+ plot.scene.addControl(new CustomZoom(zoomOptions))
+ })
+}
+
+function setStyle(elements: HTMLElement[], styleProp: string, value) {
+ elements.forEach(e => {
+ e.style[styleProp] = value
+ })
+}
+
+export function mapRendering(dom: HTMLElement | string) {
+ if (typeof dom === 'string') {
+ dom = document.getElementById(dom)
+ }
+ dom.classList.add('de-map-rendering')
+}
+
+export function mapRendered(dom: HTMLElement | string) {
+ if (typeof dom === 'string') {
+ dom = document.getElementById(dom)
+ }
+ dom.classList.add('de-map-rendered')
+}
+
+/**
+ * 隐藏缩放控件
+ * @param basicStyle
+ */
+function shouldHideZoom(basicStyle: any): boolean {
+ return (
+ (basicStyle.suspension === false && basicStyle.showZoom === undefined) ||
+ basicStyle.showZoom === false
+ )
+}
+
+const G2_TOOLTIP_WRAPPER = 'g2-tooltip-wrapper'
+export function getTooltipContainer(id) {
+ let wrapperDom = document.getElementById(G2_TOOLTIP_WRAPPER)
+ if (!wrapperDom) {
+ wrapperDom = document.createElement('div')
+ wrapperDom.id = G2_TOOLTIP_WRAPPER
+ document.body.appendChild(wrapperDom)
+ }
+ const curDom = document.getElementById(id)
+ if (curDom) {
+ curDom.remove()
+ }
+ const g2Tooltip = document.createElement('div')
+ g2Tooltip.setAttribute('id', id)
+ g2Tooltip.classList.add('g2-tooltip')
+ // 最多半屏,鼠标移入可滚动
+ g2Tooltip.style.maxHeight = '50%'
+ isMobile() ? (g2Tooltip.style.maxWidth = '50%') : (g2Tooltip.style.maxWidth = '25%')
+ g2Tooltip.style.overflowY = 'auto'
+ g2Tooltip.style.display = 'none'
+ g2Tooltip.style.position = 'fixed'
+ g2Tooltip.style.left = '0px'
+ g2Tooltip.style.top = '0px'
+ const g2TooltipTitle = document.createElement('div')
+ g2TooltipTitle.classList.add('g2-tooltip-title')
+ g2Tooltip.appendChild(g2TooltipTitle)
+
+ const g2TooltipList = document.createElement('ul')
+ g2TooltipList.classList.add('g2-tooltip-list')
+ g2Tooltip.appendChild(g2TooltipList)
+ const full = document.getElementsByClassName('fullscreen')
+ if (full.length) {
+ full.item(0).appendChild(g2Tooltip)
+ } else {
+ wrapperDom.appendChild(g2Tooltip)
+ }
+ return g2Tooltip
+}
+export function configPlotTooltipEvent>(
+ chart: Chart,
+ plot: P
+) {
+ const { tooltip } = parseJson(chart.customAttr)
+ if (!tooltip.show) {
+ return
+ }
+ // 鼠标可移入, 移入之后保持显示, 移出之后隐藏
+ plot.options.tooltip.container.addEventListener('mouseenter', e => {
+ e.target.style.visibility = 'visible'
+ e.target.style.display = 'block'
+ })
+ plot.options.tooltip.container.addEventListener('mouseleave', e => {
+ e.target.style.visibility = 'hidden'
+ e.target.style.display = 'none'
+ })
+ // 手动处理 tooltip 的显示和隐藏事件,需配合源码理解
+ // https://github.com/antvis/G2/blob/master/src/chart/controller/tooltip.ts#showTooltip
+ plot.on('tooltip:show', () => {
+ const tooltipCtl = plot.chart.getController('tooltip')
+ if (!tooltipCtl) {
+ return
+ }
+ const event = plot.chart.interactions.tooltip?.context?.event
+ if (tooltipCtl.tooltip) {
+ // 处理视图放大后再关闭 tooltip 的 dom 被清除
+ const container = tooltipCtl.tooltip.cfg.container
+ container.style.display = 'block'
+ const dom = document.getElementById(container.id)
+ if (!dom) {
+ const full = document.getElementsByClassName('fullscreen')
+ if (full.length) {
+ full.item(0).appendChild(container)
+ } else {
+ const wrapperDom = document.getElementById(G2_TOOLTIP_WRAPPER)
+ wrapperDom.appendChild(container)
+ }
+ }
+ }
+ plot.chart.getOptions().tooltip.follow = false
+ tooltipCtl.title = Math.random().toString()
+ plot.chart.getTheme().components.tooltip.x = event.clientX
+ plot.chart.getTheme().components.tooltip.y = event.clientY
+ })
+ // https://github.com/antvis/G2/blob/master/src/chart/controller/tooltip.ts#hideTooltip
+ plot.on('plot:leave', () => {
+ const tooltipCtl = plot.chart.getController('tooltip')
+ if (!tooltipCtl) {
+ return
+ }
+ plot.chart.getOptions().tooltip.follow = true
+ const container = tooltipCtl.tooltip?.cfg?.container
+ if (container) {
+ container.style.display = 'none'
+ }
+ tooltipCtl.hideTooltip()
+ })
+ // 移动端处理,关闭其他图表的提示
+ plot.on('plot:touchstart', () => {
+ const wrapperDom = document.getElementById(G2_TOOLTIP_WRAPPER)
+ if (wrapperDom) {
+ const tooltipCtl = plot.chart.getController('tooltip')
+ if (!tooltipCtl) {
+ return
+ }
+ const container = tooltipCtl.tooltip.cfg.container
+ for (const ele of wrapperDom.children) {
+ if (container.id !== ele.id) {
+ ele.style.display = 'none'
+ }
+ }
+ }
+ })
+}
+
+export const TOOLTIP_TPL =
+ '' +
+ '' +
+ '{name}:' +
+ '{value}' +
+ ''
+
+export function getConditions(chart: Chart) {
+ const { threshold } = parseJson(chart.senior)
+ const annotations = []
+ if (!threshold.enable || chart.type === 'area-stack' || chart.type === 'symbolic-map')
+ return annotations
+ const conditions = threshold.lineThreshold ?? []
+ const yAxisIds = chart.yAxis.map(i => i.id)
+ for (const field of conditions) {
+ if (!yAxisIds.includes(field.fieldId)) {
+ continue
+ }
+ for (const t of field.conditions) {
+ const annotation = {
+ type: 'regionFilter',
+ start: ['start', 'median'],
+ end: ['end', 'min'],
+ color: t.color
+ }
+ // 加中线
+ const annotationLine = {
+ type: 'line',
+ start: ['start', t.value],
+ end: ['end', t.value],
+ style: {
+ stroke: t.color,
+ lineDash: [2, 2]
+ }
+ }
+ if (t.term === 'between') {
+ annotation.start = ['start', parseFloat(t.min)]
+ annotation.end = ['end', parseFloat(t.max)]
+ annotationLine.start = ['start', parseFloat(t.min)]
+ annotationLine.end = ['end', parseFloat(t.min)]
+ annotations.push(JSON.parse(JSON.stringify(annotationLine)))
+ annotationLine.start = ['start', parseFloat(t.max)]
+ annotationLine.end = ['end', parseFloat(t.max)]
+ annotations.push(annotationLine)
+ } else if (['lt', 'le'].includes(t.term)) {
+ annotation.start = ['start', t.value]
+ annotation.end = ['end', 'min']
+ annotations.push(annotationLine)
+ } else if (['gt', 'ge'].includes(t.term)) {
+ annotation.start = ['start', t.value]
+ annotation.end = ['end', 'max']
+ annotations.push(annotationLine)
+ }
+ annotations.push(annotation)
+ }
+ }
+ return annotations
+}
+const AXIS_LABEL_TOOLTIP_STYLE = {
+ transition:
+ 'left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s',
+ backgroundColor: 'rgb(255, 255, 255)',
+ boxShadow: 'rgb(174, 174, 174) 0px 0px 10px',
+ borderRadius: '3px',
+ padding: '8px 12px',
+ opacity: '0.95',
+ position: 'absolute',
+ visibility: 'visible'
+}
+const AXIS_LABEL_TOOLTIP_TPL =
+ ''
+export function configAxisLabelLengthLimit(chart, plot, triggerObjName) {
+ // 设置触发事件的名称,如果未传入,则默认为 'axis-label'
+ const triggerName = triggerObjName || 'axis-label'
+
+ // 判断是否是Y轴标题
+ const isYaxisTitle = triggerName === 'axis-title'
+
+ // 解析图表的自定义样式和属性
+ const { customStyle, customAttr } = parseJson(chart)
+ const { lengthLimit, fontSize, color, show } = customStyle.yAxis.axisLabel
+ const { tooltip } = customAttr
+
+ // 如果不是标题,判断没有设置长度限制、没有显示或Y轴不显示,或图表类型为双向条形图,则不执行后续操作
+ if (
+ !isYaxisTitle &&
+ (!lengthLimit || !show || !customStyle.yAxis.show || chart.type === 'bidirectional-bar')
+ )
+ return
+
+ // 鼠标进入事件
+ plot.on(triggerName + ':mouseenter', e => {
+ const field = e.target.cfg.delegateObject.component.cfg.field
+ const position = e.target.cfg.delegateObject.component.cfg.position
+ const isYaxis = position === 'left' || position === 'right'
+
+ // 如果不是 'field' 或 'title',且不是Y轴,直接返回
+ if (field !== 'field' && field !== 'title' && !isYaxis) return
+
+ // 获取轴标签的实际内容
+ const realContent = e.target.attrs.text
+
+ // 不是标题时,判断标签长度小于限制或已经省略(以'...'结尾),则不显示 tooltip
+ if (
+ isYaxisTitle ? false : realContent.length < lengthLimit || !(realContent.slice(-3) === '...')
+ )
+ return
+
+ // 获取当前鼠标事件的坐标
+ const { x, y } = e
+ const parentNode = e.event.target.parentNode
+
+ // 获取父节点中是否已有 tooltip
+ let labelTooltipDom = parentNode.getElementsByClassName('g2-axis-label-tooltip')[0]
+
+ // 获取轴的标题
+ const title =
+ e.target.cfg.delegateObject.item?.name ||
+ e.target.cfg.delegateObject.axis.cfg.title.originalText
+
+ // 如果没有 tooltip,创建新的 tooltip DOM 元素
+ if (!labelTooltipDom) {
+ const domStr = substitute(AXIS_LABEL_TOOLTIP_TPL, { title })
+ labelTooltipDom = createDom(domStr)
+
+ // 设置 tooltip 的样式
+ AXIS_LABEL_TOOLTIP_STYLE.backgroundColor = tooltip.backgroundColor
+ AXIS_LABEL_TOOLTIP_STYLE.boxShadow = `${tooltip.backgroundColor} 0px 0px 5px`
+ AXIS_LABEL_TOOLTIP_STYLE.maxWidth = '200px'
+ _.assign(labelTooltipDom.style, AXIS_LABEL_TOOLTIP_STYLE)
+
+ // 将 tooltip 添加到父节点
+ parentNode.appendChild(labelTooltipDom)
+ } else {
+ // 如果已有 tooltip,更新其标题并使其可见
+ labelTooltipDom.getElementsByClassName('g2-tooltip-title')[0].innerHTML = title
+ labelTooltipDom.style.visibility = 'visible'
+ }
+
+ // 获取父节点的尺寸和 tooltip 的尺寸
+ const { height, width } = parentNode.getBoundingClientRect()
+ const { offsetHeight, offsetWidth } = labelTooltipDom
+
+ // 如果 tooltip 的尺寸超出了父节点的尺寸,则将其位置重置为 (0, 0)
+ if (offsetHeight > height || offsetWidth > width) {
+ labelTooltipDom.style.left = labelTooltipDom.style.top = '0px'
+ return
+ }
+
+ // 计算 tooltip 的初始位置
+ const initPosition = { left: x + 10, top: y + 15 }
+
+ // 调整位置,避免 tooltip 超出边界
+ if (initPosition.left + offsetWidth > width) initPosition.left = width - offsetWidth - 10
+ if (initPosition.top + offsetHeight > height) initPosition.top -= offsetHeight + 15
+
+ // 设置 tooltip 的位置和样式
+ labelTooltipDom.style.left = `${initPosition.left}px`
+ labelTooltipDom.style.top = `${initPosition.top}px`
+ labelTooltipDom.style.color = color
+ labelTooltipDom.style.fontSize = `${fontSize}px`
+ })
+
+ // 鼠标离开事件
+ plot.on(triggerName + ':mouseleave', e => {
+ const field = e.target.cfg.delegateObject.component.cfg.field
+ const position = e.target.cfg.delegateObject.component.cfg.position
+ const isYaxis = position === 'left' || position === 'right'
+
+ // 如果不是 'field' 或 'title',且不是Y轴,直接返回
+ if (field !== 'field' && field !== 'title' && !isYaxis) return
+
+ // 获取轴标签的实际内容
+ const realContent = e.target.attrs.text
+
+ // 如果标签长度小于限制或已经省略(以'...'结尾),则不显示 tooltip
+ if (
+ isYaxisTitle ? false : realContent.length < lengthLimit || !(realContent.slice(-3) === '...')
+ )
+ return
+
+ // 获取父节点中的 tooltip
+ const parentNode = e.event.target.parentNode
+ const labelTooltipDom = parentNode.getElementsByClassName('g2-axis-label-tooltip')[0]
+
+ // 如果 tooltip 存在,隐藏它
+ if (labelTooltipDom) labelTooltipDom.style.visibility = 'hidden'
+ })
+}
+
+/**
+ * y轴标题截取
+ * @param chart
+ * @param plot
+ */
+export function configYaxisTitleLengthLimit(chart, plot) {
+ // 监听图表渲染前事件
+ plot.on('beforerender', ev => {
+ // 获取图表的Y轴自定义样式
+ const { yAxis } = parseJson(chart.customStyle)
+
+ // 计算最大可用空间高度,80% 为最大高度比
+ const maxHeightRatio =
+ 0.8 * (ev.view.canvas.cfg.height - (ev.view.canvas.cfg.height < 120 ? 60 : 30))
+
+ // 计算Y轴标题的每行高度
+ const titleHeight = measureText(
+ chart,
+ yAxis.name,
+ { fontSize: yAxis.fontSize, fontFamily: chart.fontFamily },
+ 'height'
+ )
+
+ // 用于存储截取后的标题
+ let wrappedTitle = ''
+
+ // 循环截取标题内容,直到超过最大高度
+ for (
+ let charIndex = 0;
+ charIndex < yAxis.name.length && (charIndex + 1) * titleHeight <= maxHeightRatio;
+ charIndex++
+ ) {
+ wrappedTitle += yAxis.name[charIndex]
+ }
+
+ // 如果标题被截断,添加省略号
+ if (yAxis.name.length > wrappedTitle.length) {
+ wrappedTitle =
+ wrappedTitle.length > 2
+ ? wrappedTitle.slice(0, wrappedTitle.length - 2) + '...'
+ : wrappedTitle + '...'
+ }
+
+ // 更新Y轴标题的原始文本和截断后的文本
+ ev.view.options.axes.yAxisExt.title.originalText = yAxis.name
+ ev.view.options.axes.yAxisExt.title.text = wrappedTitle
+ })
+}
+
+/**
+ * 调整原始数据options.data
+ * 添加conditionColor字段,用于保存符合条件的颜色
+ * conditionColor 为数组,多个指标多个颜色,按照指标的顺序
+ * @param chart
+ * @param options
+ */
+export const addConditionsStyleColorToData = (chart: Chart, options) => {
+ const { threshold } = parseJson(chart.senior)
+ if (!threshold.enable) return options
+ options.data.forEach(item => {
+ item['conditionColor'] = []
+ // 条形图的值字段是xField,柱形图的值字段是yField
+ const valueField = chart.type === 'bar-horizontal' ? options.xField : options.yField
+ // 对称条形图区分左右值,value、 valueExt,quotaList只有一个
+ if (chart.type === 'bidirectional-bar') {
+ valueField.forEach(value => {
+ const quotaList = value === 'value' ? chart.yAxis : chart.yAxisExt
+ const conditionColor = getColorByConditions([quotaList[0]?.id], item[value], chart)
+ if (conditionColor) {
+ item[item[options.xField] + '-' + value] = conditionColor
+ }
+ })
+ } else if (item.quotaList?.length) {
+ const quotaList = item.quotaList.map(q => q.id) ?? []
+ quotaList.forEach((q, index) => {
+ // 定义后,在 handleConditionsStyle 函数中使用
+ let currentValue = item[valueField]
+ if (chart.type === 'progress-bar') {
+ currentValue = item['originalValue']
+ }
+ const cColor = getColorByConditions([q], currentValue, chart)
+ if (cColor) {
+ item.conditionColor.push(cColor)
+ } else {
+ item.conditionColor = undefined
+ }
+ })
+ }
+ })
+ return options
+}
+
+/**
+ * 辅助函数:获取颜色, 根据条件以及值计算
+ * @param quotaList 指标列表
+ * @param values 值
+ */
+const getColorByConditions = (quotaList: [], values: number | number[], chart) => {
+ const { threshold } = parseJson(chart.senior)
+ const { basicStyle } = parseJson(chart.customAttr)
+ const currentValue = Array.isArray(values) ? values[1] - values[0] : values
+ if (!currentValue) return undefined
+ // 同样的指标只取最后一个
+ const conditionMap = new Map()
+ for (const condition of threshold.lineThreshold ?? []) {
+ conditionMap.set(condition.fieldId, condition)
+ }
+ for (const condition of conditionMap.values()) {
+ if (chart.type === 'progress-bar' && chart.yAxisExt?.[0]?.id !== quotaList[0]) continue
+ if (!quotaList.includes(condition.fieldId) && chart.type !== 'waterfall') continue
+ for (const tc of condition.conditions) {
+ if (
+ (tc.term === 'between' && currentValue >= tc.min && currentValue <= tc.max) ||
+ (tc.term === 'lt' && currentValue < tc.value) ||
+ (tc.term === 'le' && currentValue <= tc.value) ||
+ (tc.term === 'gt' && currentValue > tc.value) ||
+ (tc.term === 'ge' && currentValue >= tc.value)
+ ) {
+ let tmpColor = hexToRgba(tc.color, basicStyle.alpha)
+ if (basicStyle.gradient) {
+ let vhAngle = ['bar-horizontal', 'progress-bar'].includes(chart.type) ? 0 : 270
+ if (chart.type === 'bidirectional-bar') {
+ const yAxis = chart.yAxis.find(item => item.id === condition.fieldId)
+ vhAngle = getBidirectionalAngle(basicStyle, yAxis ? 0 : 1)
+ }
+ tmpColor = setGradientColor(tmpColor, true, vhAngle)
+ }
+ return tmpColor
+ }
+ }
+ }
+}
+
+/**
+ * 处理柱条图的样式
+ * 柱条的颜色
+ * 提示marker的颜色
+ * 注: 原始options中tooltip已经配置了customItems,这里将会忽略
+ * @param chart
+ * @param options
+ */
+export function handleConditionsStyle(chart: Chart, options: O) {
+ const { threshold } = parseJson(chart.senior)
+ if (!threshold.enable) return options
+ const { basicStyle } = parseJson(chart.customAttr)
+ // 该字段出处 addConditionsStyleColorToData
+ const colorField = 'conditionColor'
+ // 配置条件样式的颜色字段
+ const rawFields = options.rawFields || []
+ rawFields.push(colorField)
+ // 辅助函数:配置柱条样式颜色,条形图为barStyle,柱形图为columnStyle
+ const columnStyle = data => {
+ return {
+ ...options.columnStyle,
+ ...options.barStyle,
+ ...(data[colorField]?.[0] ? { fill: data[colorField][0] } : {})
+ }
+ }
+ let newColor = undefined
+ if (chart.type === 'bidirectional-bar') {
+ rawFields.push(options.xField)
+ newColor = getBidirectionalBarColor(chart, basicStyle, options)
+ } else if (chart.type === 'waterfall') {
+ newColor = getWaterfallColor(basicStyle, chart)
+ }
+ const tmpOption = {
+ ...options,
+ rawFields,
+ columnStyle: columnStyle,
+ barStyle: columnStyle,
+ tooltip: {
+ ...options.tooltip,
+ ...(options.tooltip['customItems']
+ ? {}
+ : {
+ customItems: originalItems => {
+ originalItems.forEach(item => {
+ if (item.data?.[colorField]) {
+ item.color = item.data[colorField][0]
+ }
+ })
+ return originalItems
+ }
+ })
+ },
+ ...(newColor ? { color: newColor } : {})
+ }
+ return tmpOption
+}
+
+/**
+ * 配置瀑布图的color
+ * 瀑布color,这个图表固定为基础样式中颜色的前三个颜色,第一个为增加,第二个为减少,第三个为总计
+ * @param basicStyle
+ * @param chart
+ */
+const getWaterfallColor = (basicStyle, chart) => {
+ const waterfallBasicColors = getBasicColors(chart, basicStyle, 270)
+ return data => {
+ if (data['$$isTotal$$']) return waterfallBasicColors[2]
+ const values = data['$$yField$$']
+ const newColor = getColorByConditions([], values, chart)
+ return newColor ?? (values[1] > values[0] ? waterfallBasicColors[0] : waterfallBasicColors[1])
+ }
+}
+
+/**
+ * 配置对称条形图的color
+ * @param basicStyle
+ * @param options
+ */
+const getBidirectionalBarColor = (chart, basicStyle, options) => {
+ const basicColors = getBasicColors(chart, basicStyle, 270)
+ return ref => {
+ const obj = options.data.find(item => item[ref[options.xField] + '-' + ref['series-field-key']])
+ if (obj) {
+ return obj[ref[options.xField] + '-' + ref['series-field-key']]
+ }
+ return ref['series-field-key'] === 'value' ? basicColors[0] : basicColors[1]
+ }
+}
+
+/**
+ * 获取基础颜色
+ * @param chart
+ * @param basicStyle
+ * @param angle
+ */
+const getBasicColors = (chart, basicStyle, angle) => {
+ const baseColors = []
+ basicStyle.colors?.forEach((color, index) => {
+ if (chart.type === 'bidirectional-bar') {
+ baseColors.push(
+ setGradientColor(
+ hexToRgba(color, basicStyle.alpha),
+ true,
+ getBidirectionalAngle(basicStyle, index)
+ )
+ )
+ } else {
+ baseColors.push(setGradientColor(hexToRgba(color, basicStyle.alpha), true, angle))
+ }
+ })
+ return basicStyle.gradient ? baseColors : basicStyle.colors
+}
+
+/**
+ * 获取对称条形图颜色的渐变角度
+ * @param basicStyle
+ * @param index
+ */
+const getBidirectionalAngle = (basicStyle, index) => {
+ let vhAngle = 180 - index * 180
+ if (basicStyle.layout === 'vertical') {
+ vhAngle = index === 0 ? 280 : 90
+ }
+ return vhAngle
+}
+
+/**
+ * tooltip验证条件样式中的颜色,有就使用,否则使用原始颜色
+ * @param item
+ */
+export const getTooltipItemConditionColor = item => {
+ let color = item.color
+ if (item.data?.['conditionColor']) {
+ color = item.data['conditionColor'][0]
+ }
+ return color
+}
+
+/**
+ * 配置空数据样式
+ * @param newChart
+ * @param newData
+ * @param container
+ */
+export const configEmptyDataStyle = (newChart, newData, container) => {
+ /**
+ * 辅助函数:移除空数据dom
+ */
+ const removeEmptyDom = () => {
+ const emptyElement = document.getElementById(container + '_empty')
+ if (emptyElement) {
+ emptyElement.parentElement.removeChild(emptyElement)
+ }
+ }
+ removeEmptyDom()
+ if (newData.length > 0) return
+ if (!newData.length) {
+ const emptyDom = document.createElement('div')
+ emptyDom.id = container + '_empty'
+ emptyDom.textContent = tI18n('data_set.no_data')
+ emptyDom.setAttribute(
+ 'style',
+ `position: absolute;
+ left: 45%;
+ top: 50%;`
+ )
+ const parent = document.getElementById(container)
+ parent.insertBefore(emptyDom, parent.firstChild)
+ newChart.destroy()
+ }
+}
diff --git a/frontend/src/data-visualization/chart/components/js/panel/common/common_table.ts b/frontend/src/data-visualization/chart/components/js/panel/common/common_table.ts
new file mode 100644
index 0000000..16fb754
--- /dev/null
+++ b/frontend/src/data-visualization/chart/components/js/panel/common/common_table.ts
@@ -0,0 +1,2021 @@
+/* eslint-disable prettier/prettier */
+import {
+ copyString,
+ hexColorToRGBA,
+ isAlphaColor,
+ isTransparent,
+ parseJson,
+ resetRgbOpacity
+} from '../../util'
+import {
+ DEFAULT_BASIC_STYLE,
+ DEFAULT_TABLE_CELL,
+ DEFAULT_TABLE_HEADER
+} from '@/data-visualization/chart/components/editor/util/chart'
+import {
+ BaseTooltip,
+ DataCellBrushSelection,
+ FONT_FAMILY,
+ getAutoAdjustPosition,
+ getEmptyPlaceholder,
+ getPolygonPoints,
+ getTooltipDefaultOptions,
+ InteractionName,
+ InteractionStateName,
+ MergedCell,
+ MergedCellInfo,
+ type Meta,
+ type Node,
+ type PivotSheet,
+ renderPolygon,
+ renderText,
+ S2DataConfig,
+ S2Event,
+ S2Options,
+ S2Theme,
+ SERIES_NUMBER_FIELD,
+ setTooltipContainerStyle,
+ SHAPE_STYLE_MAP,
+ SpreadSheet,
+ Style,
+ TableColCell,
+ TableDataCell,
+ updateShapeAttr,
+ ViewMeta
+} from '@antv/s2'
+import { cloneDeep, filter, find, intersection, keys, merge, repeat } from 'lodash-es'
+import { createVNode, render } from 'vue'
+import TableTooltip from '@/data-visualization/chart/components/editor/common/TableTooltip.vue'
+import Exceljs from 'exceljs'
+import { saveAs } from 'file-saver'
+import { ElMessage } from 'element-plus-secondary'
+import { useI18n } from '@/data-visualization/hooks/web/useI18n'
+const { t: i18nt } = useI18n()
+
+export function getCustomTheme(chart: Chart): S2Theme {
+ const headerColor = hexColorToRGBA(
+ DEFAULT_TABLE_HEADER.tableHeaderBgColor,
+ DEFAULT_BASIC_STYLE.alpha
+ )
+ const headerAlign = DEFAULT_TABLE_HEADER.tableHeaderAlign
+ const itemColor = hexColorToRGBA(DEFAULT_TABLE_CELL.tableItemBgColor, DEFAULT_BASIC_STYLE.alpha)
+ const itemAlign = DEFAULT_TABLE_CELL.tableItemAlign
+ const borderColor = hexColorToRGBA(
+ DEFAULT_BASIC_STYLE.tableBorderColor,
+ DEFAULT_BASIC_STYLE.alpha
+ )
+ const scrollBarColor = DEFAULT_BASIC_STYLE.tableScrollBarColor
+ const scrollBarHoverColor = resetRgbOpacity(scrollBarColor, 3)
+ const textFontFamily =
+ chart.fontFamily && chart.fontFamily !== 'inherit' ? chart.fontFamily : FONT_FAMILY
+ const theme: S2Theme = {
+ background: {
+ color: '#00000000'
+ },
+ splitLine: {
+ horizontalBorderColor: borderColor,
+ horizontalBorderColorOpacity: 1,
+ horizontalBorderWidth: 1,
+ verticalBorderColor: borderColor,
+ verticalBorderColorOpacity: 1,
+ verticalBorderWidth: 1,
+ showShadow: false
+ },
+ cornerCell: {
+ cell: {
+ backgroundColor: headerColor,
+ horizontalBorderColor: borderColor,
+ verticalBorderColor: borderColor
+ },
+ text: {
+ fill: DEFAULT_TABLE_HEADER.tableHeaderFontColor,
+ fontSize: DEFAULT_TABLE_HEADER.tableTitleFontSize,
+ textAlign: headerAlign,
+ fontFamily: textFontFamily
+ },
+ bolderText: {
+ fill: DEFAULT_TABLE_HEADER.tableHeaderFontColor,
+ fontSize: DEFAULT_TABLE_HEADER.tableTitleFontSize,
+ textAlign: headerAlign,
+ fontFamily: textFontFamily
+ },
+ measureText: {
+ fill: DEFAULT_TABLE_HEADER.tableHeaderFontColor,
+ fontSize: DEFAULT_TABLE_HEADER.tableTitleFontSize,
+ textAlign: headerAlign,
+ fontFamily: textFontFamily
+ }
+ },
+ rowCell: {
+ cell: {
+ backgroundColor: headerColor,
+ horizontalBorderColor: borderColor,
+ verticalBorderColor: borderColor
+ },
+ text: {
+ fill: DEFAULT_TABLE_HEADER.tableHeaderFontColor,
+ fontSize: DEFAULT_TABLE_HEADER.tableTitleFontSize,
+ textAlign: headerAlign,
+ textBaseline: 'middle',
+ fontFamily: textFontFamily
+ },
+ bolderText: {
+ fill: DEFAULT_TABLE_HEADER.tableHeaderFontColor,
+ fontSize: DEFAULT_TABLE_HEADER.tableTitleFontSize,
+ textAlign: headerAlign,
+ fontFamily: textFontFamily
+ },
+ measureText: {
+ fill: DEFAULT_TABLE_HEADER.tableHeaderFontColor,
+ fontSize: DEFAULT_TABLE_HEADER.tableTitleFontSize,
+ textAlign: headerAlign,
+ fontFamily: textFontFamily
+ },
+ seriesText: {
+ fill: DEFAULT_TABLE_CELL.tableItemBgColor,
+ fontSize: DEFAULT_TABLE_CELL.tableItemFontSize,
+ textAlign: itemAlign,
+ fontFamily: textFontFamily
+ }
+ },
+ colCell: {
+ cell: {
+ backgroundColor: headerColor,
+ horizontalBorderColor: borderColor,
+ verticalBorderColor: borderColor
+ },
+ text: {
+ fill: DEFAULT_TABLE_HEADER.tableHeaderFontColor,
+ fontSize: DEFAULT_TABLE_HEADER.tableTitleFontSize,
+ textAlign: headerAlign,
+ fontFamily: textFontFamily
+ },
+ bolderText: {
+ fill: DEFAULT_TABLE_HEADER.tableHeaderFontColor,
+ fontSize: DEFAULT_TABLE_HEADER.tableTitleFontSize,
+ textAlign: headerAlign,
+ fontFamily: textFontFamily
+ },
+ measureText: {
+ fill: DEFAULT_TABLE_HEADER.tableHeaderFontColor,
+ fontSize: DEFAULT_TABLE_HEADER.tableTitleFontSize,
+ textAlign: headerAlign,
+ fontFamily: textFontFamily
+ }
+ },
+ dataCell: {
+ cell: {
+ backgroundColor: itemColor,
+ horizontalBorderColor: borderColor,
+ verticalBorderColor: borderColor
+ },
+ text: {
+ fill: DEFAULT_TABLE_CELL.tableFontColor,
+ fontSize: DEFAULT_TABLE_CELL.tableItemFontSize,
+ textAlign: itemAlign,
+ fontFamily: textFontFamily
+ },
+ bolderText: {
+ fill: DEFAULT_TABLE_CELL.tableFontColor,
+ fontSize: DEFAULT_TABLE_CELL.tableItemFontSize,
+ textAlign: itemAlign,
+ fontFamily: textFontFamily
+ },
+ measureText: {
+ fill: DEFAULT_TABLE_CELL.tableFontColor,
+ fontSize: DEFAULT_TABLE_CELL.tableItemFontSize,
+ textAlign: headerAlign,
+ fontFamily: textFontFamily
+ }
+ },
+ scrollBar: {
+ thumbColor: scrollBarColor,
+ thumbHoverColor: scrollBarHoverColor,
+ size: 8,
+ hoverSize: 12
+ }
+ }
+
+ let customAttr: DeepPartial
+ if (chart.customAttr) {
+ customAttr = parseJson(chart.customAttr)
+ const { basicStyle, tableHeader, tableCell } = customAttr
+ // basic
+ if (basicStyle) {
+ const tableBorderColor = basicStyle.tableBorderColor
+ const tableScrollBarColor = basicStyle.tableScrollBarColor
+ const tmpTheme: S2Theme = {
+ splitLine: {
+ horizontalBorderColor: tableBorderColor,
+ verticalBorderColor: tableBorderColor
+ },
+ cornerCell: {
+ cell: {
+ horizontalBorderColor: tableBorderColor,
+ verticalBorderColor: tableBorderColor
+ }
+ },
+ colCell: {
+ cell: {
+ horizontalBorderColor: tableBorderColor,
+ verticalBorderColor: tableBorderColor
+ }
+ },
+ dataCell: {
+ cell: {
+ horizontalBorderColor: tableBorderColor,
+ verticalBorderColor: tableBorderColor,
+ interactionState: {
+ hoverFocus: {
+ borderOpacity: basicStyle.showHoverStyle === false ? 0 : 1
+ }
+ }
+ }
+ },
+ scrollBar: {
+ thumbColor: tableScrollBarColor,
+ thumbHoverColor: resetRgbOpacity(tableScrollBarColor, 1.5)
+ }
+ }
+ merge(theme, tmpTheme)
+ }
+ // header
+ if (tableHeader) {
+ const tableHeaderFontColor = hexColorToRGBA(
+ tableHeader.tableHeaderFontColor,
+ basicStyle.alpha
+ )
+ let tableHeaderBgColor = tableHeader.tableHeaderBgColor
+ if (!isAlphaColor(tableHeaderBgColor)) {
+ tableHeaderBgColor = hexColorToRGBA(tableHeaderBgColor, basicStyle.alpha)
+ }
+ const fontStyle = tableHeader.isItalic ? 'italic' : 'normal'
+ const fontWeight = tableHeader.isBolder === false ? 'normal' : 'bold'
+ const { tableHeaderAlign, tableTitleFontSize } = tableHeader
+ const tmpTheme: S2Theme = {
+ cornerCell: {
+ cell: {
+ backgroundColor: tableHeaderBgColor
+ },
+ bolderText: {
+ fill: tableHeaderFontColor,
+ fontSize: tableTitleFontSize,
+ textAlign: tableHeaderAlign,
+ fontStyle,
+ fontWeight,
+ fontFamily: textFontFamily
+ },
+ text: {
+ fill: tableHeaderFontColor,
+ fontSize: tableTitleFontSize,
+ textAlign: tableHeaderAlign,
+ fontStyle,
+ fontWeight,
+ fontFamily: textFontFamily
+ },
+ measureText: {
+ fill: tableHeaderFontColor,
+ fontSize: tableTitleFontSize,
+ textAlign: tableHeaderAlign,
+ fontStyle,
+ fontWeight,
+ fontFamily: textFontFamily
+ }
+ },
+ colCell: {
+ cell: {
+ backgroundColor: tableHeaderBgColor
+ },
+ bolderText: {
+ fill: tableHeaderFontColor,
+ fontSize: tableTitleFontSize,
+ textAlign: tableHeaderAlign,
+ fontStyle,
+ fontWeight,
+ fontFamily: textFontFamily
+ },
+ text: {
+ fill: tableHeaderFontColor,
+ fontSize: tableTitleFontSize,
+ textAlign: tableHeaderAlign,
+ fontStyle,
+ fontWeight,
+ fontFamily: textFontFamily
+ },
+ measureText: {
+ fill: tableHeaderFontColor,
+ fontSize: tableTitleFontSize,
+ textAlign: tableHeaderAlign,
+ fontStyle,
+ fontWeight,
+ fontFamily: textFontFamily
+ }
+ }
+ }
+ merge(theme, tmpTheme)
+ // 这边设置为 0 的话就会显示表头背景颜色,所以要判断一下表头是否关闭
+ if (tableHeader.showHorizonBorder === false && tableHeader.showTableHeader !== false) {
+ const tmpTheme: S2Theme = {
+ splitLine: {
+ horizontalBorderColor: tableHeaderBgColor,
+ horizontalBorderWidth: 0,
+ horizontalBorderColorOpacity: 0
+ },
+ colCell: {
+ cell: {
+ horizontalBorderColor: tableHeaderBgColor,
+ horizontalBorderWidth: 0
+ }
+ }
+ }
+ merge(theme, tmpTheme)
+ }
+ if (tableHeader.showVerticalBorder === false && tableHeader.showTableHeader !== false) {
+ const tmpTheme: S2Theme = {
+ splitLine: {
+ verticalBorderColor: tableHeaderBgColor,
+ verticalBorderWidth: 0,
+ verticalBorderColorOpacity: 0
+ },
+ colCell: {
+ cell: {
+ verticalBorderColor: tableHeaderBgColor,
+ verticalBorderWidth: 0
+ }
+ },
+ cornerCell: {
+ cell: {
+ verticalBorderColor: tableHeaderBgColor,
+ verticalBorderWidth: 0
+ }
+ }
+ }
+ merge(theme, tmpTheme)
+ }
+ }
+ // cell
+ if (tableCell) {
+ const tableFontColor = hexColorToRGBA(tableCell.tableFontColor, basicStyle.alpha)
+ let tableItemBgColor = tableCell.tableItemBgColor
+ if (!isAlphaColor(tableItemBgColor)) {
+ tableItemBgColor = hexColorToRGBA(tableItemBgColor, basicStyle.alpha)
+ }
+ let tableItemSubBgColor = tableCell.tableItemSubBgColor
+ if (!isAlphaColor(tableItemSubBgColor)) {
+ tableItemSubBgColor = hexColorToRGBA(tableItemSubBgColor, basicStyle.alpha)
+ }
+ const fontStyle = tableCell.isItalic ? 'italic' : 'normal'
+ const fontWeight = tableCell.isBolder === false ? 'normal' : 'bold'
+ const { tableItemAlign, tableItemFontSize, enableTableCrossBG } = tableCell
+ const tmpTheme: S2Theme = {
+ rowCell: {
+ cell: {
+ backgroundColor: tableItemBgColor,
+ horizontalBorderColor: tableItemBgColor,
+ verticalBorderColor: tableItemBgColor
+ },
+ bolderText: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontFamily: textFontFamily
+ },
+ text: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontFamily: textFontFamily
+ },
+ measureText: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontFamily: textFontFamily
+ },
+ seriesText: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontFamily: textFontFamily
+ }
+ },
+ dataCell: {
+ cell: {
+ crossBackgroundColor:
+ enableTableCrossBG && !tableCell.mergeCells ? tableItemSubBgColor : tableItemBgColor,
+ backgroundColor: tableItemBgColor
+ },
+ bolderText: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontStyle,
+ fontWeight,
+ fontFamily: textFontFamily
+ },
+ text: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontStyle,
+ fontWeight,
+ fontFamily: textFontFamily
+ },
+ measureText: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontStyle,
+ fontWeight,
+ fontFamily: textFontFamily
+ },
+ seriesText: {
+ fill: tableFontColor,
+ textAlign: tableItemAlign,
+ fontSize: tableItemFontSize,
+ fontStyle,
+ fontWeight,
+ fontFamily: textFontFamily
+ }
+ }
+ }
+ merge(theme, tmpTheme)
+ if (tableCell.showHorizonBorder === false) {
+ const tmpTheme: S2Theme = {
+ dataCell: {
+ cell: {
+ horizontalBorderColor: tableItemBgColor,
+ horizontalBorderWidth: 0
+ }
+ }
+ }
+ merge(theme, tmpTheme)
+ }
+ if (tableCell.showVerticalBorder === false) {
+ const tmpTheme: S2Theme = {
+ splitLine: {
+ verticalBorderWidth: 0,
+ verticalBorderColorOpacity: 0
+ },
+ dataCell: {
+ cell: {
+ verticalBorderColor: tableItemBgColor,
+ verticalBorderWidth: 0
+ }
+ }
+ }
+ merge(theme, tmpTheme)
+ }
+ }
+ }
+
+ return theme
+}
+
+export function getStyle(chart: Chart, dataConfig: S2DataConfig): Style {
+ const style: Style = {}
+ let customAttr: DeepPartial
+ if (chart.customAttr) {
+ customAttr = parseJson(chart.customAttr)
+ const { basicStyle, tableHeader, tableCell } = customAttr
+ style.colCfg = {
+ height: tableHeader.tableTitleHeight
+ }
+ style.cellCfg = {
+ height: tableCell.tableItemHeight
+ }
+ switch (basicStyle.tableColumnMode) {
+ case 'adapt': {
+ style.layoutWidthType = 'compact'
+ break
+ }
+ case 'field': {
+ delete style.layoutWidthType
+ const fieldMap =
+ basicStyle.tableFieldWidth?.reduce((p, n) => {
+ p[n.fieldId] = n
+ return p
+ }, {}) || {}
+ // 下钻字段使用入口字段的宽度
+ if (chart.drill) {
+ const { xAxis } = parseJson(chart)
+ const curDrillField = chart.drillFields[chart.drillFilters.length]
+ const drillEnterFieldIndex = xAxis.findIndex(
+ item => item.id === chart.drillFilters[0].fieldId
+ )
+ const drillEnterField = xAxis[drillEnterFieldIndex]
+ fieldMap[curDrillField.dataeaseName] = {
+ width: fieldMap[drillEnterField.dataeaseName]?.width
+ }
+ }
+ // 铺满
+ const totalWidthPercent = dataConfig.meta?.reduce((p, n) => {
+ return p + (fieldMap[n.field]?.width ?? 10)
+ }, 0)
+ const fullFilled = parseInt(totalWidthPercent.toFixed(0)) === 100
+ const widthArr = []
+ style.colCfg.width = node => {
+ const width = node.spreadsheet.container.cfg.el.getBoundingClientRect().width
+ if (!basicStyle.tableFieldWidth?.length) {
+ const fieldsSize = chart.data.fields.length
+ const columnCount = tableHeader.showIndex ? fieldsSize + 1 : fieldsSize
+ return width / columnCount
+ }
+ const baseWidth = width / 100
+ const tmpWidth = fieldMap[node.field]
+ ? fieldMap[node.field].width * baseWidth
+ : baseWidth * 10
+ const resultWidth = parseInt(tmpWidth.toFixed(0))
+ if (fullFilled) {
+ if (widthArr.length === dataConfig.meta.length - 1) {
+ const curTotalWidth = widthArr.reduce((p, n) => {
+ return p + n
+ }, 0)
+ const restWidth = width - curTotalWidth
+ widthArr.splice(0)
+ if (restWidth < resultWidth) {
+ return restWidth
+ }
+ } else {
+ widthArr.push(resultWidth)
+ }
+ }
+ return resultWidth
+ }
+ break
+ }
+ case 'custom': {
+ style.colCfg.width = basicStyle.tableColumnWidth
+ break
+ }
+ // 查看详情用,均分铺满
+ default: {
+ delete style.layoutWidthType
+ style.colCfg.width = node => {
+ const width = node.spreadsheet.container.cfg.el.offsetWidth
+ const fieldsSize = node.spreadsheet.dataCfg.meta.length
+ if (!fieldsSize) {
+ return 0
+ }
+ const columnCount = tableHeader.showIndex ? fieldsSize + 1 : fieldsSize
+ const minWidth = width / columnCount
+ return Math.max(minWidth, basicStyle.tableColumnWidth)
+ }
+ }
+ }
+ }
+
+ return style
+}
+
+export function getCurrentField(valueFieldList: Axis[], field: ChartViewField) {
+ let list = []
+ let res = null
+ try {
+ list = parseJson(valueFieldList)
+ } catch (err) {
+ list = JSON.parse(JSON.stringify(valueFieldList))
+ }
+ if (list) {
+ for (let i = 0; i < list.length; i++) {
+ const f = list[i]
+ if (field.dataeaseName === f.dataeaseName) {
+ res = f
+ break
+ }
+ }
+ }
+
+ return res
+}
+
+export function getConditions(chart: Chart) {
+ const { threshold } = parseJson(chart.senior)
+ if (!threshold.enable) {
+ return
+ }
+ const res = {
+ text: [],
+ background: []
+ }
+ const conditions = threshold.tableThreshold ?? []
+
+ const dimFields = [...chart.xAxis, ...chart.xAxisExt].map(i => i.dataeaseName)
+ if (conditions?.length > 0) {
+ const { tableCell, basicStyle, tableHeader } = parseJson(chart.customAttr)
+ // 合并单元格时,班马纹失效
+ const enableTableCrossBG =
+ chart.type === 'table-info'
+ ? tableCell.enableTableCrossBG && !tableCell.mergeCells
+ : tableCell.enableTableCrossBG
+ const valueColor = isAlphaColor(tableCell.tableFontColor)
+ ? tableCell.tableFontColor
+ : hexColorToRGBA(tableCell.tableFontColor, basicStyle.alpha)
+ const valueBgColor = enableTableCrossBG
+ ? null
+ : isAlphaColor(tableCell.tableItemBgColor)
+ ? tableCell.tableItemBgColor
+ : hexColorToRGBA(tableCell.tableItemBgColor, basicStyle.alpha)
+ const headerValueColor = tableHeader.tableHeaderFontColor
+ const headerValueBgColor = isAlphaColor(tableHeader.tableHeaderBgColor)
+ ? tableHeader.tableHeaderBgColor
+ : hexColorToRGBA(tableHeader.tableHeaderBgColor, basicStyle.alpha)
+ const filedValueMap = getFieldValueMap(chart)
+ for (let i = 0; i < conditions.length; i++) {
+ const field = conditions[i]
+ let defaultValueColor = valueColor
+ let defaultBgColor = valueBgColor
+ // 透视表表头颜色配置
+ if (chart.type === 'table-pivot' && dimFields.includes(field.field.dataeaseName)) {
+ defaultValueColor = headerValueColor
+ defaultBgColor = headerValueBgColor
+ }
+ res.text.push({
+ field: field.field.dataeaseName,
+ mapping(value, rowData) {
+ // 总计小计
+ if (rowData?.isTotals) {
+ return null
+ }
+ // 表头
+ if (rowData?.id && rowData?.field === rowData.id) {
+ return null
+ }
+ return {
+ fill: mappingColor(value, defaultValueColor, field, 'color', filedValueMap, rowData)
+ }
+ }
+ })
+ res.background.push({
+ field: field.field.dataeaseName,
+ mapping(value, rowData) {
+ if (rowData?.isTotals) {
+ return null
+ }
+ if (rowData?.id && rowData?.field === rowData.id) {
+ return null
+ }
+ const fill = mappingColor(
+ value,
+ defaultBgColor,
+ field,
+ 'backgroundColor',
+ filedValueMap,
+ rowData
+ )
+ if (isTransparent(fill)) {
+ return null
+ }
+ return { fill }
+ }
+ })
+ }
+ }
+ return res
+}
+
+export function mappingColor(value, defaultColor, field, type, filedValueMap?, rowData?) {
+ let color = null
+ for (let i = 0; i < field.conditions.length; i++) {
+ let flag = false
+ const t = field.conditions[i]
+ let tv, max, min
+ if (t.type === 'dynamic') {
+ if (t.term === 'between') {
+ max = parseFloat(getValue(t.dynamicMaxField, filedValueMap, rowData))
+ min = parseFloat(getValue(t.dynamicMinField, filedValueMap, rowData))
+ } else {
+ tv = getValue(t.dynamicField, filedValueMap, rowData)
+ }
+ } else {
+ if (t.term === 'between') {
+ min = parseFloat(t.min)
+ max = parseFloat(t.max)
+ } else {
+ tv = t.value
+ }
+ }
+ if (field.field.deType === 2 || field.field.deType === 3 || field.field.deType === 4) {
+ tv = parseFloat(tv)
+ if (t.term === 'eq') {
+ if (value === tv) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'not_eq') {
+ if (value !== tv) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'lt') {
+ if (value < tv) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'gt') {
+ if (value > tv) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'le') {
+ if (value <= tv) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'ge') {
+ if (value >= tv) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'between') {
+ if (min <= value && value <= max) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'default') {
+ color = t[type]
+ flag = true
+ }
+ if (flag) {
+ break
+ } else if (i === field.conditions.length - 1) {
+ color = defaultColor
+ }
+ } else if (field.field.deType === 0 || field.field.deType === 5) {
+ if (t.term === 'eq') {
+ if (value === tv) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'not_eq') {
+ if (value !== tv) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'like') {
+ if (value.includes(tv)) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'not like') {
+ if (!value.includes(tv)) {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'null') {
+ if (value === null || value === undefined || value === '') {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'not_null') {
+ if (value !== null && value !== undefined && value !== '') {
+ color = t[type]
+ flag = true
+ }
+ } else if (t.term === 'default') {
+ color = t[type]
+ flag = true
+ }
+ if (flag) {
+ break
+ } else if (i === field.conditions.length - 1) {
+ color = defaultColor
+ }
+ } else {
+ // time
+ const fc = field.conditions[i]
+ tv = new Date(tv.replace(/-/g, '/') + ' GMT+8').getTime()
+ const v = new Date(value.replace(/-/g, '/') + ' GMT+8').getTime()
+ if (fc.term === 'eq') {
+ if (v === tv) {
+ color = fc[type]
+ flag = true
+ }
+ } else if (fc.term === 'not_eq') {
+ if (v !== tv) {
+ color = fc[type]
+ flag = true
+ }
+ } else if (fc.term === 'lt') {
+ if (v < tv) {
+ color = fc[type]
+ flag = true
+ }
+ } else if (fc.term === 'gt') {
+ if (v > tv) {
+ color = fc[type]
+ flag = true
+ }
+ } else if (fc.term === 'le') {
+ if (v <= tv) {
+ color = fc[type]
+ flag = true
+ }
+ } else if (fc.term === 'ge') {
+ if (v >= tv) {
+ color = fc[type]
+ flag = true
+ }
+ } else if (fc.term === 'default') {
+ color = fc[type]
+ flag = true
+ }
+ if (flag) {
+ break
+ } else if (i === field.conditions.length - 1) {
+ color = defaultColor
+ }
+ }
+ }
+ return color
+}
+
+function getFieldValueMap(view) {
+ const fieldValueMap = {}
+ if (view.data && view.data.dynamicAssistLines && view.data.dynamicAssistLines.length > 0) {
+ view.data.dynamicAssistLines.forEach(ele => {
+ fieldValueMap[ele.summary + '-' + ele.fieldId] = ele.value
+ })
+ }
+ return fieldValueMap
+}
+
+function getValue(field, filedValueMap, rowData) {
+ if (field.summary === 'value') {
+ return rowData ? rowData[field.field?.dataeaseName] : undefined
+ } else {
+ return filedValueMap[field.summary + '-' + field.fieldId]
+ }
+}
+
+export function handleTableEmptyStrategy(chart: Chart) {
+ let newData = (chart.data?.tableRow || []) as Record[]
+ let intersectionArr = []
+ const senior = parseJson(chart.senior)
+ let emptyDataStrategy = senior?.functionCfg?.emptyDataStrategy
+ if (!emptyDataStrategy) {
+ emptyDataStrategy = 'breakLine'
+ }
+ const emptyDataFieldCtrl = senior?.functionCfg?.emptyDataFieldCtrl
+ if (emptyDataStrategy !== 'breakLine' && emptyDataFieldCtrl?.length && newData?.length) {
+ const deNames = keys(newData[0])
+ intersectionArr = intersection(deNames, emptyDataFieldCtrl)
+ }
+ if (intersectionArr.length) {
+ newData = cloneDeep(newData)
+ for (let i = newData.length - 1; i >= 0; i--) {
+ for (let j = 0, tmp = intersectionArr.length; j < tmp; j++) {
+ const deName = intersectionArr[j]
+ if (newData[i][deName] === null) {
+ if (emptyDataStrategy === 'setZero') {
+ newData[i][deName] = 0
+ }
+ if (emptyDataStrategy === 'ignoreData') {
+ newData = filter(newData, (_, index) => index !== i)
+ break
+ }
+ }
+ }
+ }
+ }
+ return newData
+}
+export class SortTooltip extends BaseTooltip {
+ show(showOptions) {
+ const { iconName } = showOptions
+ if (iconName) {
+ this.showSortTooltip(showOptions)
+ return
+ }
+ super.show(showOptions)
+ }
+
+ showSortTooltip(showOptions) {
+ const { position, options, meta, event } = showOptions
+ const { enterable } = getTooltipDefaultOptions(options)
+ const { autoAdjustBoundary, adjustPosition } = this.spreadsheet.options.tooltip || {}
+ this.visible = true
+ this.options = showOptions
+ const container = this['getContainer']()
+ // 用 vue 手动 patch
+ const vNode = createVNode(TableTooltip, {
+ table: this.spreadsheet,
+ meta
+ })
+ this.spreadsheet.tooltip.container.innerHTML = ''
+ const childElement = document.createElement('div')
+ this.spreadsheet.tooltip.container.appendChild(childElement)
+ render(vNode, childElement)
+
+ const { x, y } = getAutoAdjustPosition({
+ spreadsheet: this.spreadsheet,
+ position,
+ tooltipContainer: container,
+ autoAdjustBoundary
+ })
+
+ this.position = adjustPosition?.({ position: { x, y }, event }) ?? {
+ x,
+ y
+ }
+
+ setTooltipContainerStyle(container, {
+ style: {
+ left: `${this.position?.x}px`,
+ top: `${this.position?.y}px`,
+ pointerEvents: enterable ? 'all' : 'none',
+ zIndex: 9999,
+ position: 'absolute',
+ color: 'black',
+ background: 'white',
+ fontSize: '16px'
+ },
+ visible: true
+ })
+ }
+}
+const SORT_DEFAULT =
+ ''
+const SORT_UP =
+ ''
+const SORT_DOWN =
+ ''
+
+function svg2Base64(svg) {
+ return `data:image/svg+xml;charset=utf-8;base64,${btoa(svg)}`
+}
+
+export function configHeaderInteraction(chart: Chart, option: S2Options) {
+ const { tableHeaderFontColor, tableHeaderSort } = parseJson(chart.customAttr).tableHeader
+ if (!tableHeaderSort) {
+ return
+ }
+ const iconColor = tableHeaderFontColor ?? '#666'
+ const sortDefault = svg2Base64(SORT_DEFAULT.replace('{fill}', iconColor))
+ const sortUp = svg2Base64(SORT_UP.replace('{fill}', iconColor))
+ const sortDown = svg2Base64(SORT_DOWN.replace('{fill}', iconColor))
+ // 防止缓存
+ const randomSuffix = Math.random()
+ const sortIconMap = {
+ asc: `customSortUp${randomSuffix}`,
+ desc: `customSortDown${randomSuffix}`
+ }
+ option.customSVGIcons = [
+ {
+ name: `customSortDefault${randomSuffix}`,
+ svg: sortDefault
+ },
+ {
+ name: `customSortUp${randomSuffix}`,
+ svg: sortUp
+ },
+ {
+ name: `customSortDown${randomSuffix}`,
+ svg: sortDown
+ }
+ ]
+ option.headerActionIcons = [
+ {
+ iconNames: [
+ `customSortDefault${randomSuffix}`,
+ `customSortUp${randomSuffix}`,
+ `customSortDown${randomSuffix}`
+ ],
+ belongsCell: 'colCell',
+ displayCondition: (meta, iconName) => {
+ if (meta.field === SERIES_NUMBER_FIELD) {
+ return false
+ }
+ // 分组
+ if (meta.colIndex === -1) {
+ return false
+ }
+ const sortMethodMap = meta.spreadsheet.store.get('sortMethodMap')
+ const sortType = sortMethodMap?.[meta.field]
+ if (sortType) {
+ return iconName === sortIconMap[sortType]
+ }
+ return iconName === `customSortDefault${randomSuffix}`
+ },
+ onClick: props => {
+ const { meta, event } = props
+ meta.spreadsheet.showTooltip({
+ position: {
+ x: event.clientX,
+ y: event.clientY
+ },
+ event,
+ ...props
+ })
+ const parent = document.getElementById(chart.container)
+ if (parent?.childNodes?.length) {
+ const child = Array.from(parent.childNodes)
+ .filter(node => node.nodeType === Node.ELEMENT_NODE)
+ .find(node => node.classList.contains('antv-s2-tooltip-container'))
+ if (child) {
+ const left = child.offsetLeft + child.clientWidth
+ if (left > parent.offsetWidth) {
+ const newLeft = parent.offsetWidth - child.clientWidth - 10
+ child.style.left = `${newLeft}px`
+ }
+ }
+ }
+ }
+ }
+ ]
+}
+
+export function configTooltip(chart: Chart, option: S2Options) {
+ const { tooltip } = parseJson(chart.customAttr)
+ const textFontFamily = chart.fontFamily ? chart.fontFamily : FONT_FAMILY
+ option.tooltip = {
+ ...option.tooltip,
+ style: {
+ background: tooltip.backgroundColor,
+ fontSize: tooltip.fontSize + 'px',
+ fontFamily: textFontFamily,
+ color: tooltip.color,
+ boxShadow: 'rgba(0, 0, 0, 0.1) 0px 4px 8px 0px',
+ borderRadius: '3px',
+ padding: '4px 12px',
+ opacity: 0.95,
+ position: 'absolute'
+ },
+ adjustPosition: ({ event }) => {
+ return getTooltipPosition(event)
+ }
+ }
+}
+
+export function copyContent(s2Instance: SpreadSheet, event, fieldMeta) {
+ event.preventDefault()
+ const cell = s2Instance.getCell(event.target)
+ const valueField = cell.getMeta().valueField
+ const cellMeta = cell.getMeta()
+ const selectState = s2Instance.interaction.getState()
+ let content = ''
+ // 多选
+ if (selectState.stateName === InteractionStateName.SELECTED) {
+ const { cells } = selectState
+ if (!cells?.length) {
+ return
+ }
+ if (cells.length === 1) {
+ const curCell = cells[0]
+ if (cell.getMeta().id === curCell.id) {
+ copyString(cellMeta.value + '', true)
+ }
+ s2Instance.interaction.clearState()
+ return
+ }
+ const brushSelection = s2Instance.interaction.interactions.get(
+ InteractionName.BRUSH_SELECTION
+ ) as DataCellBrushSelection
+ const selectedCells: TableDataCell[] = brushSelection.getScrollBrushRangeCells(cells)
+ selectedCells.sort((a, b) => {
+ const aMeta = a.getMeta()
+ const bMeta = b.getMeta()
+ if (aMeta.rowIndex !== bMeta.rowIndex) {
+ return aMeta.rowIndex - bMeta.rowIndex
+ }
+ return aMeta.colIndex - bMeta.colIndex
+ })
+ // 点击已选的就复制,未选的就忽略
+ let validClick = false
+ const matrix = selectedCells.reduce((p, n) => {
+ if (
+ n.getMeta().colIndex === cellMeta.colIndex &&
+ n.getMeta().rowIndex === cellMeta.rowIndex
+ ) {
+ validClick = true
+ }
+ const arr = p[n.getMeta().rowIndex]
+ if (!arr) {
+ p[n.getMeta().rowIndex] = [n]
+ } else {
+ arr.push(n)
+ }
+ return p
+ }, {}) as Record
+ if (validClick) {
+ keys(matrix).forEach(k => {
+ const arr = matrix[k] as TableDataCell[]
+ arr.forEach((cell, index) => {
+ const cellMeta = cell.getMeta()
+ const value = cellMeta.data?.[cellMeta.valueField]
+ const metaObj = find(fieldMeta, m => m.field === cellMeta.valueField)
+ let fieldVal = value?.toString()
+ if (metaObj) {
+ fieldVal = metaObj.formatter(value)
+ }
+ if (fieldVal === undefined || fieldVal === null) {
+ fieldVal = ''
+ }
+ if (index !== arr.length - 1) {
+ fieldVal += '\t'
+ }
+ content += fieldVal
+ })
+ content = content + '\n'
+ })
+ if (content) {
+ copyString(content, true)
+ }
+ }
+ s2Instance.interaction.clearState()
+ return
+ }
+ // 单元格
+ if (cellMeta?.data) {
+ const value = cellMeta.data[valueField]
+ const metaObj = find(fieldMeta, m => m.field === valueField)
+ content = value?.toString()
+ if (metaObj) {
+ content = metaObj.formatter(value)
+ }
+ } else {
+ // 列头&行头
+ const fieldMap = fieldMeta?.reduce((p, n) => {
+ p[n.field] = n.name
+ return p
+ }, {})
+ content = cellMeta.value
+ if (fieldMap?.[content]) {
+ content = fieldMap[content]
+ }
+ }
+ if (content) {
+ copyString(content, true)
+ }
+}
+
+function getTooltipPosition(event) {
+ const s2Instance = event.s2Instance
+ const { x, y } = event
+ const result = { x: x + 15, y }
+ if (!s2Instance) {
+ return result
+ }
+ const { height, width } = s2Instance.getCanvasElement().getBoundingClientRect()
+ const { offsetHeight, offsetWidth } = s2Instance.tooltip.getContainer()
+ if (offsetWidth > width) {
+ result.x = 0
+ }
+ if (offsetHeight > height) {
+ result.y = 0
+ }
+ if (!(result.x || result.y)) {
+ return result
+ }
+ if (result.x && result.x + offsetWidth > width) {
+ result.x -= result.x + offsetWidth - width
+ }
+ if (result.y) {
+ if (result.y > offsetHeight) {
+ if (result.y - offsetHeight >= 15) {
+ result.y -= offsetHeight + 15
+ } else {
+ result.y = 0
+ }
+ } else {
+ result.y += 15
+ }
+ }
+ return result
+}
+
+export async function exportGridPivot(instance: PivotSheet, chart: ChartObj) {
+ const { layoutResult } = instance.facet
+ const { meta, fields } = instance.dataCfg
+ const rowLength = fields?.rows?.length || 0
+ const colLength = fields?.columns?.length || 0
+ const colNums = layoutResult.colLeafNodes.length + rowLength + 1
+ if (colNums > 16384) {
+ ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed'))
+ return
+ }
+ const workbook = new Exceljs.Workbook()
+ const worksheet = workbook.addWorksheet(i18nt('chart.chart_data'))
+ const metaMap: Record = meta?.reduce((p, n) => {
+ if (n.field) {
+ p[n.field] = n
+ }
+ return p
+ }, {})
+ // 角头
+ fields.columns?.forEach((column, index) => {
+ const cell = worksheet.getCell(index + 1, 1)
+ cell.value = metaMap[column]?.name ?? column
+ cell.alignment = { vertical: 'middle', horizontal: 'center' }
+ if (rowLength >= 2) {
+ worksheet.mergeCells(index + 1, 1, index + 1, rowLength)
+ }
+ cell.border = {
+ right: { style: 'thick', color: { argb: '00000000' } }
+ }
+ })
+ fields?.rows?.forEach((row, index) => {
+ const cell = worksheet.getCell(colLength + 1, index + 1)
+ cell.value = metaMap[row]?.name ?? row
+ cell.alignment = { vertical: 'middle', horizontal: 'center' }
+ cell.border = {
+ bottom: { style: 'thick', color: { argb: '00000000' } }
+ }
+ if (index === fields.rows.length - 1) {
+ cell.border.right = { style: 'thick', color: { argb: '00000000' } }
+ }
+ })
+ // 行头
+ const { rowLeafNodes, rowsHierarchy, rowNodes } = layoutResult
+ const maxColIndex = rowsHierarchy.maxLevel + 1
+ const notLeafNodeHeightMap: Record = {}
+ rowLeafNodes.forEach(node => {
+ // 行头的高度由子节点相加决定,也就是行头子节点中包含的叶子节点数量
+ let curNode = node.parent
+ while (curNode) {
+ const height = notLeafNodeHeightMap[curNode.id] ?? 0
+ notLeafNodeHeightMap[curNode.id] = height + 1
+ curNode = curNode.parent
+ }
+ const { rowIndex } = node
+ const writeRowIndex = rowIndex + 1 + colLength + 1
+ const writeColIndex = node.level + 1
+ const cell = worksheet.getCell(writeRowIndex, writeColIndex)
+ cell.value = node.label
+ cell.alignment = { vertical: 'middle', horizontal: 'center' }
+ if (writeColIndex < maxColIndex) {
+ worksheet.mergeCells(writeRowIndex, writeColIndex, writeRowIndex, maxColIndex)
+ }
+ cell.border = {
+ right: { style: 'thick', color: { argb: '00000000' } }
+ }
+ })
+
+ const getNodeStartRowIndex = (node: Node) => {
+ if (!node.children?.length) {
+ return node.rowIndex + 1
+ } else {
+ return getNodeStartRowIndex(node.children[0])
+ }
+ }
+ rowNodes?.forEach(node => {
+ if (node.isLeaf) {
+ return
+ }
+ const rowIndex = getNodeStartRowIndex(node)
+ const height = notLeafNodeHeightMap[node.id]
+ const writeRowIndex = rowIndex + colLength + 1
+ const mergeColCount = node.children[0].level - node.level
+ const value = node.label
+ const cell = worksheet.getCell(writeRowIndex, node.level + 1)
+ cell.value = value
+ cell.alignment = { vertical: 'middle', horizontal: 'center' }
+ if (mergeColCount > 1 || height > 1) {
+ worksheet.mergeCells(
+ writeRowIndex,
+ node.level + 1,
+ writeRowIndex + height - 1,
+ node.level + mergeColCount
+ )
+ }
+ })
+
+ // 列头
+ const { colLeafNodes, colNodes, colsHierarchy } = layoutResult
+ const maxColHeight = colsHierarchy.maxLevel + 1
+ const notLeafNodeWidthMap: Record = {}
+ colLeafNodes.forEach(node => {
+ // 列头的宽度由子节点相加决定,也就是列头子节点中包含的叶子节点数量
+ let curNode = node.parent
+ while (curNode) {
+ const width = notLeafNodeWidthMap[curNode.id] ?? 0
+ notLeafNodeWidthMap[curNode.id] = width + 1
+ curNode = curNode.parent
+ }
+ const { colIndex } = node
+ const writeRowIndex = node.level + 1
+ const writeColIndex = colIndex + 1 + rowLength
+ const cell = worksheet.getCell(writeRowIndex, writeColIndex)
+ let value = node.label
+ if (node.field === '$$extra$$' && metaMap[value]?.name) {
+ value = metaMap[value].name
+ }
+ cell.value = value
+ cell.alignment = { vertical: 'middle', horizontal: 'center' }
+ if (writeRowIndex < maxColHeight) {
+ worksheet.mergeCells(writeRowIndex, writeColIndex, maxColHeight, writeColIndex)
+ }
+ cell.border = {
+ bottom: { style: 'thick', color: { argb: '00000000' } }
+ }
+ })
+ const getNodeStartColIndex = (node: Node) => {
+ if (!node.children?.length) {
+ return node.colIndex + 1
+ } else {
+ return getNodeStartColIndex(node.children[0])
+ }
+ }
+ colNodes.forEach(node => {
+ if (node.isLeaf) {
+ return
+ }
+ const colIndex = getNodeStartColIndex(node)
+ const width = notLeafNodeWidthMap[node.id]
+ const writeRowIndex = node.level + 1
+ const mergeRowCount = node.children[0].level - node.level
+ const value = node.label
+ const writeColIndex = colIndex + rowLength
+ const cell = worksheet.getCell(writeRowIndex, writeColIndex)
+ cell.value = value
+ cell.alignment = { vertical: 'middle', horizontal: 'center' }
+ if (mergeRowCount > 1 || width > 1) {
+ worksheet.mergeCells(
+ writeRowIndex,
+ writeColIndex,
+ writeRowIndex + mergeRowCount - 1,
+ writeColIndex + width - 1
+ )
+ }
+ })
+ // 单元格数据
+ for (let rowIndex = 0; rowIndex < rowLeafNodes.length; rowIndex++) {
+ for (let colIndex = 0; colIndex < colLeafNodes.length; colIndex++) {
+ const dataCellMeta = layoutResult.getCellMeta(rowIndex, colIndex)
+ const { fieldValue } = dataCellMeta
+ if (fieldValue === 0 || fieldValue) {
+ const meta = metaMap[dataCellMeta.valueField]
+ const cell = worksheet.getCell(rowIndex + maxColHeight + 1, rowLength + colIndex + 1)
+ const value = meta?.formatter?.(fieldValue) || fieldValue.toString()
+ cell.alignment = { vertical: 'middle', horizontal: 'center' }
+ cell.value = value
+ }
+ }
+ }
+ const buffer = await workbook.xlsx.writeBuffer()
+ const dataBlob = new Blob([buffer], {
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8'
+ })
+ saveAs(dataBlob, `${chart.title ?? '透视表'}.xlsx`)
+}
+
+export async function exportTreePivot(instance: PivotSheet, chart: ChartObj) {
+ const layoutResult = instance.facet.layoutResult
+ if (layoutResult.colLeafNodes.length + 2 > 16384) {
+ ElMessage.warning(i18nt('chart.pivot_export_invalid_col_exceed'))
+ return
+ }
+ const { meta, fields } = instance.dataCfg
+ const colLength = fields?.columns?.length || 0
+ const workbook = new Exceljs.Workbook()
+ const worksheet = workbook.addWorksheet(i18nt('chart.chart_data'))
+ const metaMap: Record = meta?.reduce((p, n) => {
+ if (n.field) {
+ p[n.field] = n
+ }
+ return p
+ }, {})
+
+ // 角头
+ fields.columns?.forEach((column, index) => {
+ const cell = worksheet.getCell(index + 1, 1)
+ cell.value = metaMap[column]?.name ?? column
+ cell.alignment = { vertical: 'middle', horizontal: 'center' }
+ cell.border = {
+ right: { style: 'thick', color: { argb: '00000000' } }
+ }
+ })
+ const maxColHeight = layoutResult.colsHierarchy.maxLevel + 1
+ const rowName = fields?.rows?.map(row => metaMap[row]?.name ?? row).join('/')
+ const cell = worksheet.getCell(colLength + 1, 1)
+ cell.value = rowName
+ cell.alignment = { vertical: 'middle', horizontal: 'center' }
+ cell.border = {
+ right: { style: 'thick', color: { argb: '00000000' } },
+ bottom: { style: 'thick', color: { argb: '00000000' } }
+ }
+ //行头
+ const { rowLeafNodes } = layoutResult
+ rowLeafNodes.forEach((node, index) => {
+ const cell = worksheet.getCell(maxColHeight + index + 1, 1)
+ cell.value = repeat(' ', node.level) + node.label
+ cell.alignment = { vertical: 'middle', horizontal: 'left' }
+ cell.border = {
+ right: { style: 'thick', color: { argb: '00000000' } }
+ }
+ })
+ // 列头
+ const notLeafNodeWidthMap: Record