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
+
+
+
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