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 8d4c738b13..6a254d68e6 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 @@ -527,6 +527,20 @@ public class ChartDataManage { String limit = ((pageInfo.getGoPage() != null && pageInfo.getPageSize() != null) ? " LIMIT " + pageInfo.getPageSize() + " OFFSET " + (pageInfo.getGoPage() - 1) * pageInfo.getPageSize() : ""); querySql = originSql + limit; totalPageSql = "SELECT COUNT(*) FROM (" + originSql + ") COUNT_TEMP"; + } else if (StringUtils.containsIgnoreCase(view.getType(), "quadrant")) { + Dimension2SQLObj.dimension2sqlObj(sqlMeta, xAxis, transFields(allFields)); + yAxis.addAll(extBubble); + Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, transFields(allFields)); + querySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view); + if (containDetailField(view) && ObjectUtils.isNotEmpty(viewFields)) { + detailFieldList.addAll(xAxis); + detailFieldList.addAll(viewFields); + + Dimension2SQLObj.dimension2sqlObj(sqlMeta, detailFieldList, transFields(allFields)); + String originSql = SQLProvider.createQuerySQL(sqlMeta, false, needOrder, view); + String limit = ((pageInfo.getGoPage() != null && pageInfo.getPageSize() != null) ? " LIMIT " + pageInfo.getPageSize() + " OFFSET " + (pageInfo.getGoPage() - 1) * pageInfo.getPageSize() : ""); + detailFieldSql = originSql + limit; + } } else { Dimension2SQLObj.dimension2sqlObj(sqlMeta, xAxis, transFields(allFields)); Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, transFields(allFields)); @@ -733,6 +747,8 @@ public class ChartDataManage { mapChart = ChartDataBuild.transMixChartDataAntV(xAxis, yAxis, view, data, isDrill); } else if (StringUtils.containsIgnoreCase(view.getType(), "label")) { mapChart = ChartDataBuild.transLabelChartData(xAxis, yAxis, view, data, isDrill); + } else if (StringUtils.containsIgnoreCase(view.getType(), "quadrant")) { + mapChart = ChartDataBuild.transQuadrantDataAntV(xAxis, yAxis, view, data, extBubble, isDrill); } else { mapChart = ChartDataBuild.transChartDataAntV(xAxis, yAxis, view, data, isDrill); } diff --git a/core/core-backend/src/main/java/io/dataease/chart/utils/ChartDataBuild.java b/core/core-backend/src/main/java/io/dataease/chart/utils/ChartDataBuild.java index 0e6f826ecc..ff6a09546a 100644 --- a/core/core-backend/src/main/java/io/dataease/chart/utils/ChartDataBuild.java +++ b/core/core-backend/src/main/java/io/dataease/chart/utils/ChartDataBuild.java @@ -1263,4 +1263,64 @@ public class ChartDataBuild { axisChartDataDTO.setDynamicLabelValue(dynamicLabelValue); axisChartDataDTO.setDynamicTooltipValue(dynamicTooltipValue); } + + //AntV quadrant + public static Map transQuadrantDataAntV(List xAxis, List yAxis, ChartViewDTO view, List data, List extBubble, boolean isDrill) { + Map map = new HashMap<>(); + + List dataList = new ArrayList<>(); + for (int i1 = 0; i1 < data.size(); i1++) { + String[] row = data.get(i1); + + StringBuilder a = new StringBuilder(); + if (isDrill) { + a.append(row[xAxis.size() - 1]); + } else { + for (int i = 0; i < xAxis.size(); i++) { + if (i == xAxis.size() - 1) { + a.append(row[i]); + } else { + a.append(row[i]).append("\n"); + } + } + } + for (int i = 0; i < xAxis.size() + yAxis.size(); i++) { + AxisChartDataAntVDTO axisChartDataDTO = new AxisChartDataAntVDTO(); + axisChartDataDTO.setField(a.toString()); + axisChartDataDTO.setName(a.toString()); + + List dimensionList = new ArrayList<>(); + List quotaList = new ArrayList<>(); + + + for (int j = 0; j < xAxis.size(); j++) { + ChartDimensionDTO chartDimensionDTO = new ChartDimensionDTO(); + chartDimensionDTO.setId(xAxis.get(j).getId()); + chartDimensionDTO.setValue(row[j]); + dimensionList.add(chartDimensionDTO); + } + axisChartDataDTO.setDimensionList(dimensionList); + + int j = i - xAxis.size(); + if (j > -1) { + ChartQuotaDTO chartQuotaDTO = new ChartQuotaDTO(); + chartQuotaDTO.setId(yAxis.get(j).getId()); + quotaList.add(chartQuotaDTO); + axisChartDataDTO.setQuotaList(quotaList); + try { + axisChartDataDTO.setValue(StringUtils.isEmpty(row[i]) ? null : new BigDecimal(row[i])); + axisChartDataDTO.setField(yAxis.get(j).getOriginName()); + axisChartDataDTO.setName(yAxis.get(j).getName()); + } catch (Exception e) { + axisChartDataDTO.setValue(new BigDecimal(0)); + } + axisChartDataDTO.setCategory(StringUtils.defaultIfBlank(yAxis.get(j).getChartShowName(), yAxis.get(j).getName())); + } + dataList.add(axisChartDataDTO); + } + } + map.put("data", dataList); + return map; + } + } diff --git a/core/core-frontend/src/custom-component/de-time-clock/CustomAttr.vue b/core/core-frontend/src/custom-component/de-time-clock/CustomAttr.vue index 22230f46b1..c66d0e3fb6 100644 --- a/core/core-frontend/src/custom-component/de-time-clock/CustomAttr.vue +++ b/core/core-frontend/src/custom-component/de-time-clock/CustomAttr.vue @@ -3,26 +3,20 @@ import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain' import { snapshotStoreWithOut } from '@/store/modules/data-visualization/snapshot' import { storeToRefs } from 'pinia' -import {onMounted, reactive} from "vue"; +import { onMounted, reactive } from 'vue' - - -const state = reactive({ - -}) +const state = reactive({}) const dvMainStore = dvMainStoreWithOut() const snapshotStore = snapshotStoreWithOut() const { curComponent } = storeToRefs(dvMainStore) - diff --git a/core/core-frontend/src/views/chart/components/editor/index.vue b/core/core-frontend/src/views/chart/components/editor/index.vue index 175bf39016..6434e2c98b 100644 --- a/core/core-frontend/src/views/chart/components/editor/index.vue +++ b/core/core-frontend/src/views/chart/components/editor/index.vue @@ -803,6 +803,10 @@ const onTableColumnWidthChange = val => { const onExtTooltipChange = val => { view.value.extTooltip = val } +const onChangeQuadrantForm = val => { + view.value.customAttr.quadrant = val + renderChart(view.value) +} const showRename = val => { recordSnapshotInfo('render') @@ -1799,6 +1803,7 @@ const onRefreshChange = val => { @onTableTotalChange="onTableTotalChange" @onChangeMiscStyleForm="onChangeMiscStyleForm" @onExtTooltipChange="onExtTooltipChange" + @onChangeQuadrantForm="onChangeQuadrantForm" /> 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 67adf97127..589a241cfb 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 @@ -665,6 +665,66 @@ export const DEFAULT_SCROLL: ScrollCfg = { step: 50 } +export const DEFAULT_QUADRANT_STYLE: QuadrantAttr = { + lineStyle: { + stroke: '#aaa', + lineWidth: 1, + opacity: 0.5 + }, + regionStyle: [ + { + fill: '#fdfcfc', + fillOpacity: 0.5 + }, + { + fill: '#fafdfa', + fillOpacity: 0.5 + }, + { + fill: '#fdfcfc', + fillOpacity: 0.5 + }, + { + fill: '#fafdfa', + fillOpacity: 0.5 + } + ], + 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', @@ -1227,6 +1287,13 @@ export const CHART_TYPE_CONFIGS = [ value: 'funnel', title: t('chart.chart_funnel'), icon: 'funnel' + }, + { + render: 'antv', + category: 'distribute', + value: 'quadrant', + title: t('chart.chart_quadrant'), + icon: 'scatter' } ] }, diff --git a/core/core-frontend/src/views/chart/components/js/panel/charts/others/quadrant.ts b/core/core-frontend/src/views/chart/components/js/panel/charts/others/quadrant.ts new file mode 100644 index 0000000000..87f77935e1 --- /dev/null +++ b/core/core-frontend/src/views/chart/components/js/panel/charts/others/quadrant.ts @@ -0,0 +1,373 @@ +import { + G2PlotChartView, + G2PlotDrawOptions +} from '@/views/chart/components/js/panel/types/impl/g2plot' +import { ScatterOptions, Scatter as G2Scatter } from '@antv/g2plot/esm/plots/scatter' +import { flow, parseJson } from '../../../util' +import { valueFormatter } from '../../../formatter' +import { useI18n } from '@/hooks/web/useI18n' +import { isEmpty } from 'lodash-es' +import { Datum } from '@antv/g2plot/esm/types/common' +import { DEFAULT_QUADRANT_STYLE } from '@/views/chart/components/editor/util/chart' + +const { t } = useI18n() + +/** + * 象限图 + */ +export class Quadrant extends G2PlotChartView { + properties: EditorProperty[] = [ + 'background-overall-component', + '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'], + 'label-selector': ['fontSize', 'color', 'labelFormatter'], + 'tooltip-selector': ['fontSize', 'color', 'backgroundColor', 'seriesTooltipFormatter'], + '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', 'extBubble', 'filter', 'drill', 'extLabel', 'extTooltip'] + axisConfig: AxisConfig = { + ...this['axisConfig'], + extBubble: { + name: `${t('chart.bubble_size')} / ${t('chart.quota')}`, + type: 'q', + limit: 1 + }, + xAxis: { + name: `${t('chart.drag_block_table_data_column')} / ${t('chart.dimension')}`, + type: 'd', + limit: 1 + }, + yAxis: { + name: `${t('chart.drag_block_table_data_column')} / ${t('chart.quota')}`, + type: 'q', + limit: 2 + } + } + + 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.yAxis[1]?.id, name: chart.yAxis[1]?.['originName'] } + return { colorFieldObj, sizeFieldObj, xFieldObj, yFieldObj } + } + public getUniqueObjects(arr: T[]): T[] { + return [...new Set(arr.map(JSON.stringify))].map(JSON.parse) as T[] + } + + public drawChart(drawOptions: G2PlotDrawOptions) { + const { chart, container, action } = drawOptions + if (!chart.data?.data) { + return + } + const { colorFieldObj, sizeFieldObj, xFieldObj, yFieldObj } = this.getFieldObject(chart) + if (!xFieldObj.id || !yFieldObj.id) { + return + } + const data: any[] = [] + // 根据指标字段对数据列表进行分组 + const groupedData = chart.data?.data + ?.filter(item => item['category'] != null) + .reduce((result, item) => { + ;(result[item['field']] = result[item['field']] || []).push(item) + return result + }, {}) + // 维度字段数据分组 + chart.data?.data + ?.filter(item => item['category'] === null) + .forEach(item => { + ;(groupedData[colorFieldObj.name] = groupedData[colorFieldObj.name] || []).push( + item['field'] + ) + }) + // 去掉groupedData每个key中集合的对象重复项 + Object.keys(groupedData).forEach(key => { + groupedData[key] = Array.from(this.getUniqueObjects(groupedData[key])) + }) + // 一个指标字段的数据长度,视为数据长度,也就是有多少数据 + const dataLength = chart.data?.data.length / chart.data?.fields.length + for (let index = 0; index < dataLength; index++) { + const tmpData = { + [xFieldObj.name]: groupedData[xFieldObj.name][index].value + } + if (groupedData[yFieldObj.name]) { + tmpData[yFieldObj.name] = groupedData[yFieldObj.name][index].value + } + if ( + groupedData[sizeFieldObj.name] && + sizeFieldObj.name !== yFieldObj.name && + sizeFieldObj.name !== xFieldObj.name + ) { + tmpData[sizeFieldObj.name] = groupedData[sizeFieldObj.name]?.[index].value + } + if (groupedData[colorFieldObj.name]) { + tmpData[colorFieldObj.name] = groupedData[colorFieldObj.name][index] + } + data.push(tmpData) + } + chart.customAttr['quadrant'].xBaseline = ( + data.reduce((valueSoFar, currentItem) => { + return valueSoFar + currentItem[xFieldObj.name] + }, 0) / data.length + ).toFixed() + chart.customAttr['quadrant'].yBaseline = ( + data.reduce((valueSoFar, currentItem) => { + return valueSoFar + currentItem[yFieldObj.name] + }, 0) / data.length + ).toFixed() + const colorField = colorFieldObj.name ? { colorField: colorFieldObj.name } : {} + const quadrant = chart.customAttr['quadrant'] ? { quadrant: chart.customAttr['quadrant'] } : {} + const baseOptions: ScatterOptions = { + ...colorField, + ...quadrant, + data: data, + xField: xFieldObj.name, + yField: yFieldObj.name, + appendPadding: 30, + pointStyle: { + fillOpacity: 0.8, + stroke: '#bbb' + } + } + + const options = this.setupOptions(chart, baseOptions) + const newChart = new G2Scatter(container, options) + newChart.on('point:click', action) + return newChart + } + + protected configBasicStyle(chart: Chart, options: ScatterOptions): ScatterOptions { + const customAttr = parseJson(chart.customAttr) + const basicStyle = customAttr.basicStyle + const extBubbleObj = { id: chart.extBubble[0]?.id, name: chart.extBubble[0]?.['originName'] } + if (chart.extBubble?.length) { + return { + ...options, + size: [5, 30], + sizeField: extBubbleObj.name, + 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) { + label = { + position: l.position, + offsetY: 5, + style: { + fill: l.color, + fontSize: l.fontSize + }, + formatter: function (param: Datum, item) { + const text = String(param[chart.xAxis[0]?.['originName']]) + const radius = item.size + const textWidth = text.length * 10 + return textWidth > 2 * radius ? '' : param[chart.xAxis[0]?.['originName']] + } + } + } 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] + if (!tooltipAttr.show || (!xAxisTitle && !yAxisTitle)) { + return { + ...options, + tooltip: false + } + } + xAxisTitle['show'] = true + yAxisTitle['show'] = true + tooltipAttr.seriesTooltipFormatter?.push(xAxisTitle) + tooltipAttr.seriesTooltipFormatter?.push(yAxisTitle) + const formatterMap = tooltipAttr.seriesTooltipFormatter + ?.filter(i => i.show) + .reduce((pre, next) => { + pre[next['originName']] = next + return pre + }, {}) as Record + const tooltip: ScatterOptions['tooltip'] = { + showTitle: false, + customItems(originalItems) { + if (!tooltipAttr.seriesTooltipFormatter?.length) { + return originalItems + } + const result = [] + originalItems?.forEach(item => { + const formatter = formatterMap[item.name] + 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 }) + } + }) + return result + } + } + return { + ...options, + tooltip + } + } + + setupDefaultOptions(chart: ChartObj): ChartObj { + chart.customStyle.yAxis.splitLine = { + ...chart.customStyle.yAxis.splitLine, + show: false + } + chart.customStyle.yAxis.axisLine = { + ...chart.customStyle.yAxis.axisLine, + show: true + } + chart.customAttr.quadrant = { + ...DEFAULT_QUADRANT_STYLE + } + return chart + } + + protected setupOptions(chart: Chart, options: ScatterOptions) { + return flow( + this.configTheme, + this.configLabel, + this.configTooltip, + this.configLegend, + this.configXAxis, + this.configYAxis, + this.configAnalyse, + this.configSlider, + this.configBasicStyle + )(chart, options) + } + + constructor() { + super('quadrant', []) + } +}