diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index c5badacc..8187f5b4 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -836,10 +836,27 @@ def check_save_chart(self, session: Session, res: str) -> Dict[str, Any]: if chart.get('axis'): if chart.get('axis').get('x'): chart.get('axis').get('x')['value'] = chart.get('axis').get('x').get('value').lower() - if chart.get('axis').get('y'): - chart.get('axis').get('y')['value'] = chart.get('axis').get('y').get('value').lower() + y_axis = chart.get('axis').get('y') + if y_axis: + if isinstance(y_axis, list): + # 数组格式: y: [{name, value}, ...] + for item in y_axis: + if item.get('value'): + item['value'] = item['value'].lower() + elif isinstance(y_axis, dict) and y_axis.get('value'): + # 旧格式: y: {name, value} + y_axis['value'] = y_axis['value'].lower() if chart.get('axis').get('series'): chart.get('axis').get('series')['value'] = chart.get('axis').get('series').get('value').lower() + if chart.get('axis') and chart['axis'].get('multi-quota'): + multi_quota = chart['axis']['multi-quota'] + if multi_quota.get('value'): + if isinstance(multi_quota['value'], list): + # 将数组中的每个值转换为小写 + multi_quota['value'] = [v.lower() if v else v for v in multi_quota['value']] + elif isinstance(multi_quota['value'], str): + # 如果是字符串,也转换为小写 + multi_quota['value'] = multi_quota['value'].lower() elif data['type'] == 'error': message = data['reason'] error = True @@ -1451,10 +1468,18 @@ def request_picture(chat_id: int, record_id: int, chart: dict, data: dict): x = None y = None series = None + multi_quota_fields = [] + multi_quota_name =None + if chart.get('axis'): - x = chart.get('axis').get('x') - y = chart.get('axis').get('y') - series = chart.get('axis').get('series') + axis_data = chart.get('axis') + x = axis_data.get('x') + y = axis_data.get('y') + series = axis_data.get('series') + # 获取multi-quota字段列表 + if axis_data.get('multi-quota') and 'value' in axis_data.get('multi-quota'): + multi_quota_fields = axis_data.get('multi-quota').get('value', []) + multi_quota_name = axis_data.get('multi-quota').get('name') axis = [] for v in columns: @@ -1462,9 +1487,23 @@ def request_picture(chat_id: int, record_id: int, chart: dict, data: dict): if x: axis.append({'name': x.get('name'), 'value': x.get('value'), 'type': 'x'}) if y: - axis.append({'name': y.get('name'), 'value': y.get('value'), 'type': 'y'}) + y_list = y if isinstance(y, list) else [y] + + for y_item in y_list: + if isinstance(y_item, dict) and 'value' in y_item: + y_obj = { + 'name': y_item.get('name'), + 'value': y_item.get('value'), + 'type': 'y' + } + # 如果是multi-quota字段,添加标志 + if y_item.get('value') in multi_quota_fields: + y_obj['multi-quota'] = True + axis.append(y_obj) if series: axis.append({'name': series.get('name'), 'value': series.get('value'), 'type': 'series'}) + if multi_quota_name: + axis.append({'name': multi_quota_name, 'value': multi_quota_name, 'type': 'other-info'}) request_obj = { "path": os.path.join(settings.MCP_IMAGE_PATH, file_name), diff --git a/backend/common/utils/data_format.py b/backend/common/utils/data_format.py index 366e1d7b..1991fb3e 100644 --- a/backend/common/utils/data_format.py +++ b/backend/common/utils/data_format.py @@ -89,7 +89,17 @@ def convert_data_fields_for_pandas(chart: dict, fields: list, data: list): if chart.get('axis').get('x'): _fields[chart.get('axis').get('x').get('value')] = chart.get('axis').get('x').get('name') if chart.get('axis').get('y'): - _fields[chart.get('axis').get('y').get('value')] = chart.get('axis').get('y').get('name') + # _fields[chart.get('axis').get('y').get('value')] = chart.get('axis').get('y').get('name') + y_axis = chart.get('axis').get('y') + if isinstance(y_axis, list): + # y轴是数组的情况(多指标字段) + for y_item in y_axis: + if isinstance(y_item, dict) and 'value' in y_item and 'name' in y_item: + _fields[y_item.get('value')] = y_item.get('name') + elif isinstance(y_axis, dict): + # y轴是对象的情况(单指标字段) + if 'value' in y_axis and 'name' in y_axis: + _fields[y_axis.get('value')] = y_axis.get('name') if chart.get('axis').get('series'): _fields[chart.get('axis').get('series').get('value')] = chart.get('axis').get('series').get( 'name') diff --git a/backend/templates/template.yaml b/backend/templates/template.yaml index a6f50da1..fffa9f90 100644 --- a/backend/templates/template.yaml +++ b/backend/templates/template.yaml @@ -138,9 +138,6 @@ template: 若涉及多表查询,则生成的SQL内,不论查询的表字段是否有重名,表字段前必须加上对应的表名 - - 我们目前的情况适用于单指标、多分类的场景(展示table除外) - 是否生成对话标题在内,如果为True需要生成,否则不需要生成,生成的对话标题要求在20字以内 @@ -323,24 +320,59 @@ template: 必须从 SQL 查询列中提取“columns” - 如果需要柱状图,JSON格式应为(如果有分类则在JSON中返回series): - {{"type":"column", "title": "标题", "axis": {{"x": {{"name":"x轴的{lang}名称", "value": "SQL 查询 x 轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "y": {{"name":"y轴的{lang}名称","value": "SQL 查询 y 轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}} - 柱状图使用一个分类字段(series),一个X轴字段(x)和一个Y轴数值字段(y),其中必须从SQL查询列中提取"x"、"y"与"series"。 + 字段类型定义: + - 分类字段(series):用于分组数据的离散值字段,如国家、产品类别、用户类型等(非时间、非数值的离散字段) + - 指标字段(数值字段/y轴):需要计算或展示的数值字段,通常是数值类型 + - 维度字段(维度轴/x轴):用于X轴的分类或时间字段,如日期、产品名称、地区等 + + + 图表配置决策流程: + 1. 先判断SQL查询结果中是否存在分类字段(非时间、非数值的离散字段) + 2. 如果存在分类字段 → 必须使用series配置,此时y轴只能有一个指标字段 + 3. 如果不存在分类字段,但存在多个指标字段 → 必须使用multi-quota配置 + 4. 如果只有一个指标字段且无分类字段 → 直接配置y轴,不使用series和multi-quota + + + 如果需要柱状图,JSON格式应为(series为可选字段,仅当有分类字段时使用): + {{"type":"column", "title": "标题", "axis": {{"x": {{"name":"维度轴的{lang}名称", "value": "SQL 查询维度轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "y": [{{"name":"数值轴的{lang}名称","value": "SQL 查询数值轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}], "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}} + 柱状图配置说明: + 1. x轴:维度轴,通常放置分类或时间字段(如日期、产品类别) + 2. y轴:数值轴,放置需要展示的数值指标 + 3. series:当需要对数据进一步分组时使用(如不同产品系列在不同日期的销售额) + 柱状图使用一个分类字段(series),一个维度轴字段(x)和一个数值轴字段(y),其中必须从SQL查询列中提取"x"、"y"与"series"。 + 如果SQL中没有分类列,那么JSON内的series字段不需要出现 - 如果需要条形图,JSON格式应为(如果有分类则在JSON中返回series),条形图相当于是旋转后的柱状图,因此 x 轴仍为维度轴,y 轴仍为指标轴: - {{"type":"bar", "title": "标题", "axis": {{"x": {{"name":"x轴的{lang}名称", "value": "SQL 查询 x 轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "y": {{"name":"y轴的{lang}名称","value": "SQL 查询 y 轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}} - 条形图使用一个分类字段(series),一个X轴字段(x)和一个Y轴数值字段(y),其中必须从SQL查询列中提取"x"和"y"与"series"。 + 如果需要条形图,JSON格式应为(series为可选字段,仅当有分类字段时使用): + ⚠️ 重要:条形图是柱状图的视觉旋转,但数据映射逻辑保持不变! + 必须遵循:x轴 = 维度轴(分类),y轴 = 数值轴(指标) + 不要将条形图的横向展示误解为x轴是数值! + {{"type":"bar", "title": "标题", "axis": {{"x": {{"name":"维度轴的{lang}名称", "value": "SQL 查询维度轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "y": [{{"name":"数值轴的{lang}名称","value": "SQL 查询数值轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}], "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}} + 条形图配置原则: + 1. 条形图只是视觉展示不同,数据逻辑与柱状图相同 + 2. x轴必须是维度字段(分类、时间等) + 3. y轴必须是数值字段(指标、度量等) + 4. 如果存在分类字段(如不同产品系列),使用series分组 + 条形图使用一个分类字段(series),一个维度轴字段(x)和一个数值轴字段(y),其中必须从SQL查询列中提取"x"和"y"与"series"。 + 如果SQL中没有分类列,那么JSON内的series字段不需要出现 - 如果需要折线图,JSON格式应为(如果有分类则在JSON中返回series): - {{"type":"line", "title": "标题", "axis": {{"x": {{"name":"x轴的{lang}名称","value": "SQL 查询 x 轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "y": {{"name":"y轴的{lang}名称","value": "SQL 查询 y 轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}} - 折线图使用一个分类字段(series),一个X轴字段(x)和一个Y轴数值字段(y),其中必须从SQL查询列中提取"x"、"y"与"series"。 + 如果需要折线图,JSON格式应为(series为可选字段,仅当有分类字段时使用): + {{"type":"line", "title": "标题", "axis": {{"x": {{"name":"维度轴的{lang}名称","value": "SQL 查询维度轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "y": [{{"name":"数值轴的{lang}名称","value": "SQL 查询数值轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}], "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}} + 折线图配置说明: + 1. x轴:维度轴,通常放置时间字段(如日期、月份) + 2. y轴:数值轴,放置需要展示趋势的数值指标 + 3. series:当需要对比多个分类的趋势时使用(如不同产品的销售趋势) + 折线图使用一个分类字段(series),一个维度轴字段(x)和一个数值轴字段(y),其中必须从SQL查询列中提取"x"、"y"与"series"。 + 如果SQL中没有分类列,那么JSON内的series字段不需要出现 如果需要饼图,JSON格式应为: - {{"type":"pie", "title": "标题", "axis": {{"y": {{"name":"值轴的{lang}名称","value":"SQL 查询数值的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}} - 饼图使用一个分类字段(series)和一个数值字段(y),其中必须从SQL查询列中提取"y"与"series"。 + {{"type":"pie", "title": "标题", "axis": {{"y": {{"name":"数值轴的{lang}名称","value":"SQL 查询数值的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}} + 饼图配置说明: + 1. y轴:数值字段,表示各部分的大小 + 2. series:分类字段,表示各部分的名称 + 饼图使用一个分类字段(series)和一个数值字段(y),其中必须从SQL查询列中提取"y"与"series"。 如果SQL中没有分类列,那么JSON内的series字段不需要出现 @@ -349,7 +381,11 @@ template: 如果SQL查询结果中存在可用于数据分类的字段(如国家、产品类型等),则必须提供series配置。如果不存在,则无需在JSON中包含series字段。 - 我们目前的情况适用于单指标、多分类的场景(展示table除外),若SQL中包含多指标列,请选择一个最符合提问情况的指标作为值轴 + 对于柱状图/条形图/折线图: + 1. 如果SQL查询中存在多个指标字段(如"收入"、"支出"、"利润"等数值字段)且不存在分类字段,则必须提供multi-quota配置,形如:"multi-quota":{{"name":"指标类型","value":["指标字段1","指标字段2",...]}} + 2. 如果SQL查询中存在多个指标字段且同时存在分类字段,则以分类字段为主,选取多指标字段中的其中一个作为指标即可,不需要multi-quota配置 + 3. 如果只有一个指标字段,无论是否存在分类字段,都不需要multi-quota配置 + 重要提醒:multi-quota和series是互斥的配置,一个图表配置中只能使用其中之一,不能同时存在 如果你无法根据提供的内容生成合适的JSON配置,则返回:{{"type":"error", "reason": "抱歉,我无法生成合适的图表配置"}} @@ -381,6 +417,17 @@ template: {{"type":"pie","title":"组织人数统计","axis":{{"y":{{"name":"人数","value":"user_count"}},"series":{{"name":"组织名称","value":"org_name"}}}}}} + + + SELECT `s`.`date` AS `date`, `s`.`income` AS `income`, `s`.`expense` AS `expense` FROM `financial_data` `s` ORDER BY `date` ASC LIMIT 1000 + 展示每月的收入与支出 + line + + + // 无分类字段,但有多个指标字段的情况 + {{"type":"line","title":"财务指标趋势","axis":{{"x":{{"name":"日期","value":"date"}},"y":[{{"name":"收入","value":"income"}}, {{"name":"支出","value":"expense"}}], "multi-quota":{{"name":"财务指标","value":["income","expense"]}}}}}} + + diff --git a/frontend/src/views/chat/component/BaseChart.ts b/frontend/src/views/chat/component/BaseChart.ts index 4a24d126..e497d9e1 100644 --- a/frontend/src/views/chat/component/BaseChart.ts +++ b/frontend/src/views/chat/component/BaseChart.ts @@ -1,7 +1,8 @@ export interface ChartAxis { name: string value: string - type?: 'x' | 'y' | 'series' + type?: 'x' | 'y' | 'series' | 'other-info' + 'multi-quota'?: boolean } export interface ChartData { diff --git a/frontend/src/views/chat/component/ChartComponent.vue b/frontend/src/views/chat/component/ChartComponent.vue index 5aea94b5..dc53d99d 100644 --- a/frontend/src/views/chat/component/ChartComponent.vue +++ b/frontend/src/views/chat/component/ChartComponent.vue @@ -13,6 +13,7 @@ const params = withDefaults( x?: Array y?: Array series?: Array + multiQuotaName?: string | undefined }>(), { data: () => [], @@ -20,6 +21,7 @@ const params = withDefaults( x: () => [], y: () => [], series: () => [], + multiQuotaName: undefined, } ) @@ -36,11 +38,19 @@ const axis = computed(() => { _list.push({ name: column.name, value: column.value, type: 'x' }) }) params.y.forEach((column) => { - _list.push({ name: column.name, value: column.value, type: 'y' }) + _list.push({ + name: column.name, + value: column.value, + type: 'y', + 'multi-quota': column['multi-quota'], + }) }) params.series.forEach((column) => { _list.push({ name: column.name, value: column.value, type: 'series' }) }) + if (params.multiQuotaName) { + _list.push({ name: params.multiQuotaName, value: params.multiQuotaName, type: 'other-info' }) + } return _list }) @@ -52,7 +62,6 @@ function renderChart() { chartInstance.init(axis.value, params.data) chartInstance.render() } - console.debug(chartInstance) } function destroyChart() { diff --git a/frontend/src/views/chat/component/DisplayChartBlock.vue b/frontend/src/views/chat/component/DisplayChartBlock.vue index 619cb6f7..4f16a96e 100644 --- a/frontend/src/views/chat/component/DisplayChartBlock.vue +++ b/frontend/src/views/chat/component/DisplayChartBlock.vue @@ -20,8 +20,12 @@ const chartObject = computed<{ title: string axis: { x: { name: string; value: string } - y: { name: string; value: string } + y: { name: string; value: string } | Array<{ name: string; value: string }> series: { name: string; value: string } + 'multi-quota': { + name: string + value: Array + } } columns: Array<{ name: string; value: string }> }>(() => { @@ -32,24 +36,42 @@ const chartObject = computed<{ }) const xAxis = computed(() => { - if (chartObject.value?.axis?.x) { - return [chartObject.value.axis.x] + const axis = chartObject.value?.axis + if (axis?.x) { + return [axis.x] } return [] }) const yAxis = computed(() => { - if (chartObject.value?.axis?.y) { - return [chartObject.value.axis.y] + const axis = chartObject.value?.axis + if (!axis?.y) { + return [] } - return [] + + const y = axis.y + const multiQuotaValues = axis['multi-quota']?.value || [] + + // 统一处理为数组 + const yArray = Array.isArray(y) ? [...y] : [{ ...y }] + + // 标记 multi-quota + return yArray.map((item) => ({ + ...item, + 'multi-quota': multiQuotaValues.includes(item.value), + })) }) const series = computed(() => { - if (chartObject.value?.axis?.series) { - return [chartObject.value.axis.series] + const axis = chartObject.value?.axis + if (axis?.series) { + return [axis.series] } return [] }) +const multiQuotaName = computed(() => { + return chartObject.value?.axis?.['multi-quota']?.name +}) + const chartRef = ref() function onTypeChange() { @@ -94,6 +116,7 @@ defineExpose({ :y="yAxis" :series="series" :data="data" + :multi-quota-name="multiQuotaName" /> diff --git a/frontend/src/views/chat/component/charts/Bar.ts b/frontend/src/views/chat/component/charts/Bar.ts index 7c6f458b..adbee12b 100644 --- a/frontend/src/views/chat/component/charts/Bar.ts +++ b/frontend/src/views/chat/component/charts/Bar.ts @@ -1,7 +1,11 @@ import { BaseG2Chart } from '@/views/chat/component/BaseG2Chart.ts' import type { ChartAxis, ChartData } from '@/views/chat/component/BaseChart.ts' import type { G2Spec } from '@antv/g2' -import { checkIsPercent } from '@/views/chat/component/charts/utils.ts' +import { + checkIsPercent, + getAxesWithFilter, + processMultiQuotaData, +} from '@/views/chat/component/charts/utils.ts' export class Bar extends BaseG2Chart { constructor(id: string) { @@ -11,15 +15,35 @@ export class Bar extends BaseG2Chart { init(axis: Array, data: Array) { super.init(axis, data) - const x = this.axis.filter((item) => item.type === 'x') - const y = this.axis.filter((item) => item.type === 'y') - const series = this.axis.filter((item) => item.type === 'series') + const axes = getAxesWithFilter(this.axis) - if (x.length == 0 || y.length == 0) { + if (axes.x.length == 0 || axes.y.length == 0) { + console.debug({ instance: this }) return } - const _data = checkIsPercent(y[0], data) + let config = { + data: data, + y: axes.y, + series: axes.series, + } + if (axes.multiQuota.length > 0) { + config = processMultiQuotaData( + axes.x, + config.y, + axes.multiQuota, + axes.multiQuotaName, + config.data + ) + } + + const x = axes.x + const y = config.y + const series = config.series + + const _data = checkIsPercent(y, config.data) + + console.debug({ 'render-info': { x: x, y: y, series: series, data: _data }, instance: this }) const options: G2Spec = { ...this.chart.options(), @@ -59,7 +83,7 @@ export class Bar extends BaseG2Chart { }, axis: { x: { - title: x[0].name, + title: false, // x[0].name, labelFontSize: 12, labelAutoHide: { type: 'hide', @@ -71,7 +95,7 @@ export class Bar extends BaseG2Chart { labelAutoEllipsis: true, }, y: { - title: y[0].name, + title: false, // y[0].name, labelFontSize: 12, labelAutoHide: { type: 'hide', @@ -105,28 +129,6 @@ export class Bar extends BaseG2Chart { return { name: y[0].name, value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}` } } }, - // labels: [ - // { - // text: (data: any) => { - // const value = data[y[0].value] - // if (value === undefined || value === null) { - // return '' - // } - // return `${value}${_data.isPercent ? '%' : ''}` - // }, - // position: (data: any) => { - // if (data[y[0].value] < 0) { - // return 'left' - // } - // return 'right' - // }, - // transform: [ - // { type: 'contrastReverse' }, - // { type: 'exceedAdjust' }, - // { type: 'overlapHide' }, - // ], - // }, - // ], } as G2Spec if (series.length > 0) { diff --git a/frontend/src/views/chat/component/charts/Column.ts b/frontend/src/views/chat/component/charts/Column.ts index 2bcc38e3..629037d4 100644 --- a/frontend/src/views/chat/component/charts/Column.ts +++ b/frontend/src/views/chat/component/charts/Column.ts @@ -1,7 +1,11 @@ import { BaseG2Chart } from '@/views/chat/component/BaseG2Chart.ts' import type { ChartAxis, ChartData } from '@/views/chat/component/BaseChart.ts' import type { G2Spec } from '@antv/g2' -import { checkIsPercent } from '@/views/chat/component/charts/utils.ts' +import { + checkIsPercent, + getAxesWithFilter, + processMultiQuotaData, +} from '@/views/chat/component/charts/utils.ts' export class Column extends BaseG2Chart { constructor(id: string) { @@ -11,15 +15,35 @@ export class Column extends BaseG2Chart { init(axis: Array, data: Array) { super.init(axis, data) - const x = this.axis.filter((item) => item.type === 'x') - const y = this.axis.filter((item) => item.type === 'y') - const series = this.axis.filter((item) => item.type === 'series') + const axes = getAxesWithFilter(this.axis) - if (x.length == 0 || y.length == 0) { + if (axes.x.length == 0 || axes.y.length == 0) { + console.debug({ instance: this }) return } - const _data = checkIsPercent(y[0], data) + let config = { + data: data, + y: axes.y, + series: axes.series, + } + if (axes.multiQuota.length > 0) { + config = processMultiQuotaData( + axes.x, + config.y, + axes.multiQuota, + axes.multiQuotaName, + config.data + ) + } + + const x = axes.x + const y = config.y + const series = config.series + + const _data = checkIsPercent(y, config.data) + + console.debug({ 'render-info': { x: x, y: y, series: series, data: _data }, instance: this }) const options: G2Spec = { ...this.chart.options(), @@ -58,7 +82,7 @@ export class Column extends BaseG2Chart { }, axis: { x: { - title: x[0].name, + title: false, // x[0].name, labelFontSize: 12, labelAutoHide: { type: 'hide', @@ -69,7 +93,9 @@ export class Column extends BaseG2Chart { labelAutoWrap: true, labelAutoEllipsis: true, }, - y: { title: y[0].name }, + y: { + title: false, // y[0].name, + }, }, scale: { x: { diff --git a/frontend/src/views/chat/component/charts/Line.ts b/frontend/src/views/chat/component/charts/Line.ts index 01b4de16..62c80c34 100644 --- a/frontend/src/views/chat/component/charts/Line.ts +++ b/frontend/src/views/chat/component/charts/Line.ts @@ -1,7 +1,11 @@ import { BaseG2Chart } from '@/views/chat/component/BaseG2Chart.ts' import type { ChartAxis, ChartData } from '@/views/chat/component/BaseChart.ts' import type { G2Spec } from '@antv/g2' -import { checkIsPercent } from '@/views/chat/component/charts/utils.ts' +import { + checkIsPercent, + getAxesWithFilter, + processMultiQuotaData, +} from '@/views/chat/component/charts/utils.ts' export class Line extends BaseG2Chart { constructor(id: string) { @@ -11,15 +15,35 @@ export class Line extends BaseG2Chart { init(axis: Array, data: Array) { super.init(axis, data) - const x = this.axis.filter((item) => item.type === 'x') - const y = this.axis.filter((item) => item.type === 'y') - const series = this.axis.filter((item) => item.type === 'series') + const axes = getAxesWithFilter(this.axis) - if (x.length == 0 || y.length == 0) { + if (axes.x.length == 0 || axes.y.length == 0) { + console.debug({ instance: this }) return } - const _data = checkIsPercent(y[0], data) + let config = { + data: data, + y: axes.y, + series: axes.series, + } + if (axes.multiQuota.length > 0) { + config = processMultiQuotaData( + axes.x, + config.y, + axes.multiQuota, + axes.multiQuotaName, + config.data + ) + } + + const x = axes.x + const y = config.y + const series = config.series + + const _data = checkIsPercent(y, config.data) + + console.debug({ 'render-info': { x: x, y: y, series: series, data: _data }, instance: this }) const options: G2Spec = { ...this.chart.options(), @@ -32,7 +56,7 @@ export class Line extends BaseG2Chart { }, axis: { x: { - title: x[0].name, + title: false, // x[0].name, labelFontSize: 12, labelAutoHide: { type: 'hide', @@ -43,7 +67,9 @@ export class Line extends BaseG2Chart { labelAutoWrap: true, labelAutoEllipsis: true, }, - y: { title: y[0].name }, + y: { + title: false, // y[0].name, + }, }, scale: { x: { diff --git a/frontend/src/views/chat/component/charts/Pie.ts b/frontend/src/views/chat/component/charts/Pie.ts index baaf707c..fb65ce5e 100644 --- a/frontend/src/views/chat/component/charts/Pie.ts +++ b/frontend/src/views/chat/component/charts/Pie.ts @@ -1,7 +1,7 @@ import { BaseG2Chart } from '@/views/chat/component/BaseG2Chart.ts' import type { ChartAxis, ChartData } from '@/views/chat/component/BaseChart.ts' import type { G2Spec } from '@antv/g2' -import { checkIsPercent } from '@/views/chat/component/charts/utils.ts' +import { checkIsPercent, getAxesWithFilter } from '@/views/chat/component/charts/utils.ts' export class Pie extends BaseG2Chart { constructor(id: string) { @@ -10,15 +10,16 @@ export class Pie extends BaseG2Chart { init(axis: Array, data: Array) { super.init(axis, data) - const y = this.axis.filter((item) => item.type === 'y') - const series = this.axis.filter((item) => item.type === 'series') + const { y, series } = getAxesWithFilter(this.axis) if (series.length == 0 || y.length == 0) { + console.debug({ instance: this }) return } - // % - const _data = checkIsPercent(y[0], data) + const _data = checkIsPercent(y, data) + + console.debug({ 'render-info': { y: y, series: series, data: _data }, instance: this }) const options: G2Spec = { ...this.chart.options(), diff --git a/frontend/src/views/chat/component/charts/utils.ts b/frontend/src/views/chat/component/charts/utils.ts index 2e1fa1f2..6a4c70ff 100644 --- a/frontend/src/views/chat/component/charts/utils.ts +++ b/frontend/src/views/chat/component/charts/utils.ts @@ -6,35 +6,122 @@ interface CheckedData { data: Array } -export function checkIsPercent(valueAxis: ChartAxis, data: Array): CheckedData { +export function getAxesWithFilter(axes: ChartAxis[]): { + x: ChartAxis[] + y: ChartAxis[] // 过滤后的 y + series: ChartAxis[] + multiQuota: string[] // series 为空时返回 multi-quota 为 true 的 y 轴 value 列表 + multiQuotaName?: string +} { + const groups = { + x: [] as ChartAxis[], + y: [] as ChartAxis[], + series: [] as ChartAxis[], + multiQuota: [] as string[], + multiQuotaName: undefined as string | undefined, + } + + // 分组 + axes.forEach((axis) => { + if (axis.type === 'x') groups.x.push(axis) + else if (axis.type === 'y') groups.y.push(axis) + else if (axis.type === 'series') groups.series.push(axis) + else if (axis.type === 'other-info') groups.multiQuotaName = axis.value + }) + + // 应用过滤规则 + if (groups.series.length > 0) { + groups.y = groups.y.slice(0, 1) + } else { + const multiQuotaY = groups.y.filter((item) => item['multi-quota'] === true) + groups.multiQuota = multiQuotaY.map((item) => item.value) + if (multiQuotaY.length > 0) { + groups.y = multiQuotaY + } + } + + return groups +} + +export function processMultiQuotaData( + x: Array, + y: Array, + multiQuota: Array, + multiQuotaName: string = 'sqlbot_auto_series', + data: Array +) { + const _list: Array = [] + const _map: { [propName: string]: string } = {} + y.forEach((axis) => { + _map[axis.value] = axis.name + }) + for (const datum of data) { + multiQuota.forEach((quota) => { + const _data: { [propName: string]: any } = {} + for (const xAxis of x) { + _data[xAxis.value] = datum[xAxis.value] + } + _data['sqlbot_auto_quota'] = datum[quota] + _data['sqlbot_auto_series'] = _map[quota] + _list.push(_data) + }) + } + + return { + data: _list, + y: [{ name: 'sqlbot_auto_quota', value: 'sqlbot_auto_quota', type: 'y' } as ChartAxis], + series: [{ name: multiQuotaName, value: 'sqlbot_auto_series', type: 'series' } as ChartAxis], + } +} + +export function checkIsPercent(valueAxes: Array, data: Array): CheckedData { const result: CheckedData = { isPercent: false, data: [], } - const notEmptyData = filter( - data, - (d) => - d && - d[valueAxis.value] !== null && - d[valueAxis.value] !== undefined && - d[valueAxis.value] !== 0 && - d[valueAxis.value] !== '0' - ) - if (notEmptyData.length > 0) { - const v = notEmptyData[0][valueAxis.value] + '' - if (endsWith(v.trim(), '%')) { - result.isPercent = true + // 深拷贝原始数据 + for (let i = 0; i < data.length; i++) { + result.data.push({ ...data[i] }) + } + + // 检查是否有任何一个轴包含百分比数据 + for (const valueAxis of valueAxes) { + const notEmptyData = filter( + data, + (d) => + d && + d[valueAxis.value] !== null && + d[valueAxis.value] !== undefined && + d[valueAxis.value] !== '' && + d[valueAxis.value] !== 0 && + d[valueAxis.value] !== '0' + ) + + if (notEmptyData.length > 0) { + const v = notEmptyData[0][valueAxis.value] + '' + if (endsWith(v.trim(), '%')) { + result.isPercent = true + break // 找到一个百分比轴就结束检查 + } } } - for (let i = 0; i < data.length; i++) { - const v = data[i] - const _v = { ...v } as ChartData - if (result.isPercent) { - const formatValue = replace(v[valueAxis.value], '%', '') - _v[valueAxis.value] = Number(formatValue) + + // 如果发现任何百分比轴,处理所有轴的所有百分比数据 + if (result.isPercent) { + for (let i = 0; i < data.length; i++) { + for (const valueAxis of valueAxes) { + const value = data[i][valueAxis.value] + if (value !== null && value !== undefined && value !== '') { + const strValue = String(value).trim() + if (endsWith(strValue, '%')) { + const formatValue = replace(strValue, '%', '') + const numValue = Number(formatValue) + result.data[i][valueAxis.value] = isNaN(numValue) ? 0 : numValue + } + } + } } - result.data.push(_v) } return result diff --git a/frontend/src/views/dashboard/components/sq-view/index.vue b/frontend/src/views/dashboard/components/sq-view/index.vue index 51f86e60..22355d61 100644 --- a/frontend/src/views/dashboard/components/sq-view/index.vue +++ b/frontend/src/views/dashboard/components/sq-view/index.vue @@ -166,6 +166,7 @@ defineExpose({ :y="viewInfo.chart?.yAxis" :series="viewInfo.chart?.series" :data="viewInfo.data?.data" + :multi-quota-name="viewInfo.chart?.multiQuotaName" /> ({ + ...item, + 'multi-quota': multiQuotaValues.includes(item.value), + })) + } + recordeInfo['chart'] = { type: chartBaseInfo?.type, title: chartBaseInfo?.title, columns: chartBaseInfo?.columns, - xAxis: chartBaseInfo?.axis?.x ? [chartBaseInfo?.axis?.x] : [], - yAxis: chartBaseInfo?.axis?.y ? [chartBaseInfo?.axis.y] : [], - series: chartBaseInfo?.axis?.series ? [chartBaseInfo?.axis?.series] : [], + xAxis: axis?.x ? [axis?.x] : [], + yAxis: yAxis, + series: axis?.series ? [axis?.series] : [], + multiQuotaName: axis?.['multi-quota']?.name, } chartInfoList.value.push(recordeInfo) } diff --git a/g2-ssr/charts/bar.js b/g2-ssr/charts/bar.js index a6eb37d7..7023fbd5 100644 --- a/g2-ssr/charts/bar.js +++ b/g2-ssr/charts/bar.js @@ -1,119 +1,136 @@ -const {checkIsPercent} = require("./utils"); +const { checkIsPercent, getAxesWithFilter, processMultiQuotaData } = require('./utils') function getBarOptions(baseOptions, axis, data) { - const x = axis.filter((item) => item.type === 'x') - const y = axis.filter((item) => item.type === 'y') - const series = axis.filter((item) => item.type === 'series') + const axes = getAxesWithFilter(this.axis) - if (x.length === 0 || y.length === 0) { - return - } + if (axes.x.length === 0 || axes?.y?.length === 0) { + return + } - const _data = checkIsPercent(y[0], data) + let config = { + data: data, + y: axes.y, + series: axes.series, + } + if (axes.multiQuota.length > 0) { + config = processMultiQuotaData( + axes.x, + config.y, + axes.multiQuota, + axes.multiQuotaName, + config.data, + ) + } - const options = { - ...baseOptions, - type: 'interval', - data: _data.data, - coordinate: {transform: [{type: 'transpose'}]}, - encode: { - x: x[0].value, - y: y[0].value, - color: series.length > 0 ? series[0].value : undefined, - }, - style: { - radiusTopLeft: (d) => { - if (d[y[0].value] && d[y[0].value] > 0) { - return 4 - } - return 0 - }, - radiusTopRight: (d) => { - if (d[y[0].value] && d[y[0].value] > 0) { - return 4 - } - return 0 - }, - radiusBottomLeft: (d) => { - if (d[y[0].value] && d[y[0].value] < 0) { - return 4 - } - return 0 - }, - radiusBottomRight: (d) => { - if (d[y[0].value] && d[y[0].value] < 0) { - return 4 - } - return 0 - }, - }, - axis: { - x: { - title: x[0].name, - labelFontSize: 12, - labelAutoHide: { - type: 'hide', - keepHeader: true, - keepTail: true, - }, - labelAutoRotate: false, - labelAutoWrap: true, - labelAutoEllipsis: true, - }, - y: {title: y[0].name}, - }, - scale: { - x: { - nice: true, - }, - y: { - nice: true, - type: 'linear', - }, + const x = axes.x + const y = config.y + const series = config.series + + const _data = checkIsPercent(y, config.data) + + const options = { + ...baseOptions, + type: 'interval', + data: _data.data, + coordinate: { transform: [{ type: 'transpose' }] }, + encode: { + x: x[0].value, + y: y[0].value, + color: series.length > 0 ? series[0].value : undefined, + }, + style: { + radiusTopLeft: (d) => { + if (d[y[0].value] && d[y[0].value] > 0) { + return 4 + } + return 0 + }, + radiusTopRight: (d) => { + if (d[y[0].value] && d[y[0].value] > 0) { + return 4 + } + return 0 + }, + radiusBottomLeft: (d) => { + if (d[y[0].value] && d[y[0].value] < 0) { + return 4 + } + return 0 + }, + radiusBottomRight: (d) => { + if (d[y[0].value] && d[y[0].value] < 0) { + return 4 + } + return 0 + }, + }, + axis: { + x: { + title: false, + labelFontSize: 12, + labelAutoHide: { + type: 'hide', + keepHeader: true, + keepTail: true, }, - interaction: { - elementHighlight: {background: true}, + labelAutoRotate: false, + labelAutoWrap: true, + labelAutoEllipsis: true, + }, + y: { title: false }, + }, + scale: { + x: { + nice: true, + }, + y: { + nice: true, + type: 'linear', + }, + }, + interaction: { + elementHighlight: { background: true }, + }, + tooltip: (data) => { + if (series.length > 0) { + return { + name: data[series[0].value], + value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`, + } + } else { + return { name: y[0].name, value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}` } + } + }, + labels: [ + { + text: (data) => { + const value = data[y[0].value] + if (value === undefined || value === null) { + return '' + } + return `${value}${_data.isPercent ? '%' : ''}` }, - tooltip: (data) => { - if (series.length > 0) { - return { - name: data[series[0].value], - value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`, - } - } else { - return {name: y[0].name, value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`} - } + position: (data) => { + if (data[y[0].value] < 0) { + return 'bottom' + } + return 'top' }, - labels: [ - { - text: (data) => { - const value = data[y[0].value] - if (value === undefined || value === null) { - return '' - } - return `${value}${_data.isPercent ? '%' : ''}` - }, - position: (data) => { - if (data[y[0].value] < 0) { - return 'bottom' - } - return 'top' - }, - transform: [ - {type: 'contrastReverse'}, - {type: 'exceedAdjust'}, - {type: 'overlapHide'}, - ], - }, + transform: [ + { type: 'contrastReverse' }, + { type: 'exceedAdjust' }, + { type: 'overlapHide' }, ], - } + }, + ], + } - if (series.length > 0) { - options.transform = [{type: 'stackY'}] - } + if (series.length > 0) { + options.transform = [{ type: 'stackY' }] + } - return options + return options } -module.exports = {getBarOptions} \ No newline at end of file +module.exports = { getBarOptions } \ No newline at end of file diff --git a/g2-ssr/charts/column.js b/g2-ssr/charts/column.js index 0db0f186..fb577916 100644 --- a/g2-ssr/charts/column.js +++ b/g2-ssr/charts/column.js @@ -1,119 +1,136 @@ -const {checkIsPercent} = require("./utils"); +const { checkIsPercent, getAxesWithFilter, processMultiQuotaData } = require('./utils') function getColumnOptions(baseOptions, axis, data) { - const x = axis.filter((item) => item.type === 'x') - const y = axis.filter((item) => item.type === 'y') - const series = axis.filter((item) => item.type === 'series') + const axes = getAxesWithFilter(this.axis) - if (x.length === 0 || y.length === 0) { - return - } + if (axes.x.length === 0 || axes?.y?.length === 0) { + return + } - const _data = checkIsPercent(y[0], data) + let config = { + data: data, + y: axes.y, + series: axes.series, + } + if (axes.multiQuota.length > 0) { + config = processMultiQuotaData( + axes.x, + config.y, + axes.multiQuota, + axes.multiQuotaName, + config.data, + ) + } - const options = { - ...baseOptions, - type: 'interval', - data: _data.data, - encode: { - x: x[0].value, - y: y[0].value, - color: series.length > 0 ? series[0].value : undefined, - }, - style: { - radiusTopLeft: (d) => { - if (d[y[0].value] && d[y[0].value] > 0) { - return 4 - } - return 0 - }, - radiusTopRight: (d) => { - if (d[y[0].value] && d[y[0].value] > 0) { - return 4 - } - return 0 - }, - radiusBottomLeft: (d) => { - if (d[y[0].value] && d[y[0].value] < 0) { - return 4 - } - return 0 - }, - radiusBottomRight: (d) => { - if (d[y[0].value] && d[y[0].value] < 0) { - return 4 - } - return 0 - }, - }, - axis: { - x: { - title: x[0].name, - labelFontSize: 12, - labelAutoHide: { - type: 'hide', - keepHeader: true, - keepTail: true, - }, - labelAutoRotate: false, - labelAutoWrap: true, - labelAutoEllipsis: true, - }, - y: {title: y[0].name}, - }, - scale: { - x: { - nice: true, - }, - y: { - nice: true, - type: 'linear', - }, + const x = axes.x + const y = config.y + const series = config.series + + const _data = checkIsPercent(y, config.data) + + const options = { + ...baseOptions, + type: 'interval', + data: _data.data, + encode: { + x: x[0].value, + y: y[0].value, + color: series.length > 0 ? series[0].value : undefined, + }, + style: { + radiusTopLeft: (d) => { + if (d[y[0].value] && d[y[0].value] > 0) { + return 4 + } + return 0 + }, + radiusTopRight: (d) => { + if (d[y[0].value] && d[y[0].value] > 0) { + return 4 + } + return 0 + }, + radiusBottomLeft: (d) => { + if (d[y[0].value] && d[y[0].value] < 0) { + return 4 + } + return 0 + }, + radiusBottomRight: (d) => { + if (d[y[0].value] && d[y[0].value] < 0) { + return 4 + } + return 0 + }, + }, + axis: { + x: { + title: false, + labelFontSize: 12, + labelAutoHide: { + type: 'hide', + keepHeader: true, + keepTail: true, }, - interaction: { - elementHighlight: {background: true}, + labelAutoRotate: false, + labelAutoWrap: true, + labelAutoEllipsis: true, + }, + y: { title: false }, + }, + scale: { + x: { + nice: true, + }, + y: { + nice: true, + type: 'linear', + }, + }, + interaction: { + elementHighlight: { background: true }, + }, + tooltip: (data) => { + if (series.length > 0) { + return { + name: data[series[0].value], + value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`, + } + } else { + return { name: y[0].name, value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}` } + } + }, + labels: [ + { + text: (data) => { + const value = data[y[0].value] + if (value === undefined || value === null) { + return '' + } + return `${value}${_data.isPercent ? '%' : ''}` }, - tooltip: (data) => { - if (series.length > 0) { - return { - name: data[series[0].value], - value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`, - } - } else { - return {name: y[0].name, value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`} - } + position: (data) => { + if (data[y[0].value] < 0) { + return 'bottom' + } + return 'top' }, - labels: [ - { - text: (data) => { - const value = data[y[0].value] - if (value === undefined || value === null) { - return '' - } - return `${value}${_data.isPercent ? '%' : ''}` - }, - position: (data) => { - if (data[y[0].value] < 0) { - return 'bottom' - } - return 'top' - }, - dy: -25, - transform: [ - {type: 'contrastReverse'}, - {type: 'exceedAdjust'}, - {type: 'overlapHide'}, - ], - }, + dy: -25, + transform: [ + { type: 'contrastReverse' }, + { type: 'exceedAdjust' }, + { type: 'overlapHide' }, ], - } + }, + ], + } - if (series.length > 0) { - options.transform = [{type: 'stackY'}] - } + if (series.length > 0) { + options.transform = [{ type: 'stackY' }] + } - return options + return options } -module.exports = {getColumnOptions} \ No newline at end of file +module.exports = { getColumnOptions } \ No newline at end of file diff --git a/g2-ssr/charts/line.js b/g2-ssr/charts/line.js index 73ac4e84..d3b33840 100644 --- a/g2-ssr/charts/line.js +++ b/g2-ssr/charts/line.js @@ -1,101 +1,118 @@ -const {checkIsPercent} = require("./utils"); +const { checkIsPercent, getAxesWithFilter, processMultiQuotaData } = require('./utils') function getLineOptions(baseOptions, axis, data) { - const x = axis.filter((item) => item.type === 'x') - const y = axis.filter((item) => item.type === 'y') - const series = axis.filter((item) => item.type === 'series') + const axes = getAxesWithFilter(this.axis) - if (x.length === 0 || y.length === 0) { - return - } + if (axes.x.length === 0 || axes?.y?.length === 0) { + return + } - const _data = checkIsPercent(y[0], data) + let config = { + data: data, + y: axes.y, + series: axes.series, + } + if (axes.multiQuota.length > 0) { + config = processMultiQuotaData( + axes.x, + config.y, + axes.multiQuota, + axes.multiQuotaName, + config.data, + ) + } - const options = { - ...baseOptions, - type: 'view', - data: _data.data, - encode: { - x: x[0].value, - y: y[0].value, - color: series.length > 0 ? series[0].value : undefined, - }, - axis: { - x: { - title: x[0].name, - labelFontSize: 12, - labelAutoHide: { - type: 'hide', - keepHeader: true, - keepTail: true, - }, - labelAutoRotate: false, - labelAutoWrap: true, - labelAutoEllipsis: true, - }, - y: {title: y[0].name}, + const x = axes.x + const y = config.y + const series = config.series + + const _data = checkIsPercent(y, config.data) + + const options = { + ...baseOptions, + type: 'view', + data: _data.data, + encode: { + x: x[0].value, + y: y[0].value, + color: series.length > 0 ? series[0].value : undefined, + }, + axis: { + x: { + title: x[0].name, + labelFontSize: 12, + labelAutoHide: { + type: 'hide', + keepHeader: true, + keepTail: true, }, - scale: { - x: { - nice: true, - }, - y: { - nice: true, - type: 'linear', - }, + labelAutoRotate: false, + labelAutoWrap: true, + labelAutoEllipsis: true, + }, + y: { title: y[0].name }, + }, + scale: { + x: { + nice: true, + }, + y: { + nice: true, + type: 'linear', + }, + }, + children: [ + { + type: 'line', + encode: { + shape: 'smooth', }, - children: [ - { - type: 'line', - encode: { - shape: 'smooth', - }, - labels: [ - { - text: (data) => { - const value = data[y[0].value] - if (value === undefined || value === null) { - return '' - } - return `${value}${_data.isPercent ? '%' : ''}` - }, - style: { - dx: -10, - dy: -12, - }, - transform: [ - {type: 'contrastReverse'}, - {type: 'exceedAdjust'}, - {type: 'overlapHide'}, - ], - }, - ], - tooltip: (data) => { - if (series.length > 0) { - return { - name: data[series[0].value], - value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`, - } - } else { - return {name: y[0].name, value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`} - } - }, + labels: [ + { + text: (data) => { + const value = data[y[0].value] + if (value === undefined || value === null) { + return '' + } + return `${value}${_data.isPercent ? '%' : ''}` }, - { - type: 'point', - style: { - fill: 'white', - }, - encode: { - size: 1.5, - }, - tooltip: false, + style: { + dx: -10, + dy: -12, }, + transform: [ + { type: 'contrastReverse' }, + { type: 'exceedAdjust' }, + { type: 'overlapHide' }, + ], + }, ], - } + tooltip: (data) => { + if (series.length > 0) { + return { + name: data[series[0].value], + value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`, + } + } else { + return { name: y[0].name, value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}` } + } + }, + }, + { + type: 'point', + style: { + fill: 'white', + }, + encode: { + size: 1.5, + }, + tooltip: false, + }, + ], + } - return options + return options } -module.exports = {getLineOptions} +module.exports = { getLineOptions } diff --git a/g2-ssr/charts/pie.js b/g2-ssr/charts/pie.js index 3495b476..adea74a1 100644 --- a/g2-ssr/charts/pie.js +++ b/g2-ssr/charts/pie.js @@ -1,57 +1,56 @@ -const {checkIsPercent} = require("./utils"); +const { checkIsPercent, getAxesWithFilter } = require('./utils') function getPieOptions(baseOptions, axis, data) { - const y = axis.filter((item) => item.type === 'y') - const series = axis.filter((item) => item.type === 'series') + const { y, series } = getAxesWithFilter(this.axis) - if (series.length === 0 || y.length === 0) { - return - } + if (series.length === 0 || y.length === 0) { + return + } - const _data = checkIsPercent(y[0], data) + const _data = checkIsPercent(y, data) - return { - ...baseOptions, - type: 'interval', - coordinate: {type: 'theta', outerRadius: 0.8}, - transform: [{type: 'stackY'}], - data: _data.data, - encode: { - y: y[0].value, - color: series[0].value, + return { + ...baseOptions, + type: 'interval', + coordinate: { type: 'theta', outerRadius: 0.8 }, + transform: [{ type: 'stackY' }], + data: _data.data, + encode: { + y: y[0].value, + color: series[0].value, + }, + scale: { + x: { + nice: true, + }, + y: { + type: 'linear', + }, + }, + legend: { + color: { position: 'bottom', layout: { justifyContent: 'center' } }, + }, + labels: [ + { + position: 'spider', + text: (data) => + `${data[series[0].value]}: ${data[y[0].value]}${_data.isPercent ? '%' : ''}`, + }, + ], + tooltip: { + title: (data) => data[series[0].value], + items: [ + (data) => { + return { + name: y[0].name, + value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`, + } }, - scale: { - x: { - nice: true, - }, - y: { - type: 'linear', - }, - }, - legend: { - color: {position: 'bottom', layout: {justifyContent: 'center'}}, - }, - labels: [ - { - position: 'spider', - text: (data) => - `${data[series[0].value]}: ${data[y[0].value]}${_data.isPercent ? '%' : ''}`, - }, - ], - tooltip: { - title: (data) => data[series[0].value], - items: [ - (data) => { - return { - name: y[0].name, - value: `${data[y[0].value]}${_data.isPercent ? '%' : ''}`, - } - }, - ], - }, - } + ], + }, + } } -module.exports = {getPieOptions} +module.exports = { getPieOptions } diff --git a/g2-ssr/charts/utils.js b/g2-ssr/charts/utils.js index 5cea935a..727ca088 100644 --- a/g2-ssr/charts/utils.js +++ b/g2-ssr/charts/utils.js @@ -1,37 +1,116 @@ -const {filter, endsWith, replace} = require("lodash"); +const { endsWith, filter, replace } = require('lodash') -function checkIsPercent(valueAxis, data) { - const result = { - isPercent: false, - data: [], +export function getAxesWithFilter(axes) { + const groups = { + x: [], + y: [], + series: [], + multiQuota: [], + multiQuotaName: undefined, + } + + // 分组 + axes.forEach((axis) => { + if (axis.type === 'x') groups.x.push(axis) + else if (axis.type === 'y') groups.y.push(axis) + else if (axis.type === 'series') groups.series.push(axis) + else if (axis.type === 'other-info') groups.multiQuotaName = axis.value + }) + + // 应用过滤规则 + if (groups.series.length > 0) { + groups.y = groups.y.slice(0, 1) + } else { + const multiQuotaY = groups.y.filter((item) => item['multi-quota'] === true) + groups.multiQuota = multiQuotaY.map((item) => item.value) + if (multiQuotaY.length > 0) { + groups.y = multiQuotaY } + } + + return groups +} + +export function processMultiQuotaData( + x, + y, + multiQuota, + multiQuotaName = 'sqlbot_auto_series', + data, +) { + const _list = [] + const _map = {} + y.forEach((axis) => { + _map[axis.value] = axis.name + }) + for (const datum of data) { + multiQuota.forEach((quota) => { + const _data = {} + for (const xAxis of x) { + _data[xAxis.value] = datum[xAxis.value] + } + _data['sqlbot_auto_quota'] = datum[quota] + _data['sqlbot_auto_series'] = _map[quota] + _list.push(_data) + }) + } + + return { + data: _list, + y: [{ name: 'sqlbot_auto_quota', value: 'sqlbot_auto_quota', type: 'y' }], + series: [{ name: multiQuotaName, value: 'sqlbot_auto_series', type: 'series' }], + } +} +export function checkIsPercent(valueAxes, data) { + const result = { + isPercent: false, + data: [], + } + + // 深拷贝原始数据 + for (let i = 0; i < data.length; i++) { + result.data.push({ ...data[i] }) + } + + // 检查是否有任何一个轴包含百分比数据 + for (const valueAxis of valueAxes) { const notEmptyData = filter( - data, - (d) => - d && - d[valueAxis.value] !== null && - d[valueAxis.value] !== undefined && - d[valueAxis.value] !== 0 && - d[valueAxis.value] !== '0' + data, + (d) => + d && + d[valueAxis.value] !== null && + d[valueAxis.value] !== undefined && + d[valueAxis.value] !== '' && + d[valueAxis.value] !== 0 && + d[valueAxis.value] !== '0', ) + if (notEmptyData.length > 0) { - const v = notEmptyData[0][valueAxis.value] + '' - if (endsWith(v.trim(), '%')) { - result.isPercent = true - } + const v = notEmptyData[0][valueAxis.value] + '' + if (endsWith(v.trim(), '%')) { + result.isPercent = true + break // 找到一个百分比轴就结束检查 + } } + } + + // 如果发现任何百分比轴,处理所有轴的所有百分比数据 + if (result.isPercent) { for (let i = 0; i < data.length; i++) { - const v = data[i] - const _v = {...v} - if (result.isPercent) { - const formatValue = replace(v[valueAxis.value], '%', '') - _v[valueAxis.value] = Number(formatValue) + for (const valueAxis of valueAxes) { + const value = data[i][valueAxis.value] + if (value !== null && value !== undefined && value !== '') { + const strValue = String(value).trim() + if (endsWith(strValue, '%')) { + const formatValue = replace(strValue, '%', '') + const numValue = Number(formatValue) + result.data[i][valueAxis.value] = isNaN(numValue) ? 0 : numValue + } } - result.data.push(_v) + } } + } - return result + return result } - -module.exports = {checkIsPercent} \ No newline at end of file