diff --git a/core/core-backend/src/main/java/io/dataease/chart/manage/ChartDataManage.java b/core/core-backend/src/main/java/io/dataease/chart/manage/ChartDataManage.java index 87a7361254..97b9343ee9 100644 --- a/core/core-backend/src/main/java/io/dataease/chart/manage/ChartDataManage.java +++ b/core/core-backend/src/main/java/io/dataease/chart/manage/ChartDataManage.java @@ -115,7 +115,9 @@ public class ChartDataManage { if (StringUtils.equalsIgnoreCase(view.getType(), "table-pivot") || StringUtils.containsIgnoreCase(view.getType(), "group") || ("antv".equalsIgnoreCase(view.getRender()) && "line".equalsIgnoreCase(view.getType())) - || StringUtils.equalsIgnoreCase(view.getType(), "flow-map")) { + || StringUtils.equalsIgnoreCase(view.getType(), "flow-map") + || StringUtils.equalsIgnoreCase(view.getType(), "sankey") + ) { xAxis.addAll(xAxisExt); } List yAxis = new ArrayList<>(view.getYAxis()); diff --git a/core/core-frontend/src/assets/svg/sankey.svg b/core/core-frontend/src/assets/svg/sankey.svg new file mode 100644 index 0000000000..5645f5ded6 --- /dev/null +++ b/core/core-frontend/src/assets/svg/sankey.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/core/core-frontend/src/locales/zh-CN.ts b/core/core-frontend/src/locales/zh-CN.ts index 902b0cf0d1..c6e4baa205 100644 --- a/core/core-frontend/src/locales/zh-CN.ts +++ b/core/core-frontend/src/locales/zh-CN.ts @@ -692,6 +692,7 @@ export default { chart_pie_rose: '玫瑰图', chart_pie_donut_rose: '玫瑰环形图', chart_funnel: '漏斗图', + chart_sankey: '桑基图', chart_radar: '雷达图', chart_gauge: '仪表盘', chart_map: '地图', @@ -766,6 +767,8 @@ export default { chart_data: '数据', chart_style: '样式', drag_block_type_axis: '类别轴', + drag_block_type_axis_start: '源', + drag_block_type_axis_end: '目的', drag_block_value_axis: '值轴', drag_block_value_start: '开始值', drag_block_value_end: '结束值', diff --git a/core/core-frontend/src/views/chart/components/editor/util/chart.ts b/core/core-frontend/src/views/chart/components/editor/util/chart.ts index ac8affe189..35516e7c32 100644 --- a/core/core-frontend/src/views/chart/components/editor/util/chart.ts +++ b/core/core-frontend/src/views/chart/components/editor/util/chart.ts @@ -1336,6 +1336,13 @@ export const CHART_TYPE_CONFIGS = [ value: 'funnel', title: t('chart.chart_funnel'), icon: 'funnel' + }, + { + render: 'antv', + category: 'relation', + value: 'sankey', + title: t('chart.chart_sankey'), + icon: 'sankey' } ] }, diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/sankey-common.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/sankey-common.ts new file mode 100644 index 0000000000..80d63ff2e9 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/sankey-common.ts @@ -0,0 +1,39 @@ +export const SANKEY_EDITOR_PROPERTY: EditorProperty[] = [ + 'background-overall-component', + 'basic-style-selector', + 'label-selector', + 'tooltip-selector', + 'title-selector', + 'function-cfg', + 'jump-set', + 'linkage' +] + +export const SANKEY_EDITOR_PROPERTY_INNER: EditorPropertyInner = { + 'background-overall-component': ['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/core/core-frontend/src/views/chart/components/js/panel/charts/others/sankey.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/sankey.ts new file mode 100644 index 0000000000..0d8d9fe0c9 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/sankey.ts @@ -0,0 +1,280 @@ +import { + G2PlotChartView, + G2PlotDrawOptions +} from '@/views/chart/components/js/panel/types/impl/g2plot' +import { Sankey, SankeyOptions } from '@antv/g2plot/esm/plots/sankey' +import { getPadding, setGradientColor } from '@/views/chart/components/js/panel/common/common_antv' +import { cloneDeep, get } from 'lodash-es' +import { flow, hexColorToRGBA, parseJson } from '@/views/chart/components/js/util' +import { valueFormatter } from '@/views/chart/components/js/formatter' + +import { Datum } from '@antv/g2plot/esm/types/common' +import { useI18n } from '@/hooks/web/useI18n' +import { + SANKEY_AXIS_TYPE, + SANKEY_EDITOR_PROPERTY, + SANKEY_EDITOR_PROPERTY_INNER +} from '@/views/chart/components/js/panel/charts/others/sankey-common' + +const { t } = useI18n() +const DEFAULT_DATA = [] + +/** + * 区间条形图 + */ +export class RangeBar extends G2PlotChartView { + axisConfig = { + ...this['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' + }, + 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'] + } + 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' }] + } + } + ] + } + + drawChart(drawOptions: G2PlotDrawOptions): Sankey { + 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 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 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' + }, + offsetX: isLast ? -8 : 8 + } + }, + layout: [{ type: 'hide-overlap' }, { type: 'limit-in-canvas' }] + } + 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) + } +}