feat: support multiple metrics in line, column, and bar charts

This commit is contained in:
ulleo
2026-01-08 16:27:12 +08:00
committed by ulleo
parent a0544fae83
commit 1cd7f743f2
18 changed files with 906 additions and 486 deletions
+45 -6
View File
@@ -836,10 +836,27 @@ class LLMService:
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'):
muti_quota = chart['axis']['multi-quota']
if muti_quota.get('value'):
if isinstance(muti_quota['value'], list):
# 将数组中的每个值转换为小写
muti_quota['value'] = [v.lower() if v else v for v in muti_quota['value']]
elif isinstance(muti_quota['value'], str):
# 如果是字符串,也转换为小写
muti_quota['value'] = muti_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
muti_quota_fields = []
muti_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'):
muti_quota_fields = axis_data.get('multi-quota').get('value', [])
muti_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 muti_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 muti_quota_name:
axis.append({'name': muti_quota_name, 'value': muti_quota_name, 'type': 'other-info'})
request_obj = {
"path": os.path.join(settings.MCP_IMAGE_PATH, file_name),
+11 -1
View File
@@ -89,7 +89,17 @@ class DataFormat:
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')
+62 -15
View File
@@ -138,9 +138,6 @@ template:
<rule>
若涉及多表查询,则生成的SQL内,不论查询的表字段是否有重名,表字段前必须加上对应的表名
</rule>
<rule>
我们目前的情况适用于单指标、多分类的场景(展示table除外)
</rule>
<rule>
是否生成对话标题在<change-title>内,如果为True需要生成,否则不需要生成,生成的对话标题要求在20字以内
</rule>
@@ -323,24 +320,59 @@ template:
必须从 SQL 查询列中提取“columns”
</rule>
<rule>
如果需要柱状图,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"。
<strong>字段类型定义:</strong>
<strong>- 分类字段(series):用于分组数据的离散值字段,如国家、产品类别、用户类型等(非时间、非数值的离散字段)</strong>
<strong>- 指标字段(数值字段/y轴):需要计算或展示的数值字段,通常是数值类型</strong>
<strong>- 维度字段(维度轴/x轴):用于X轴的分类或时间字段,如日期、产品名称、地区等</strong>
</rule>
<rule>
如果需要条形图,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"。
<strong>图表配置决策流程:</strong>
<strong>1. 先判断SQL查询结果中是否存在分类字段(非时间、非数值的离散字段)</strong>
<strong>2. 如果存在分类字段 → 必须使用series配置,此时y轴只能有一个指标字段</strong>
<strong>3. 如果不存在分类字段,但存在多个指标字段 → 必须使用multi-quota配置</strong>
<strong>4. 如果只有一个指标字段且无分类字段 → 直接配置y轴,不使用series和multi-quota</strong>
</rule>
<rule>
如果需要折线图,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格式应为<strong>(series为可选字段,仅当有分类字段时使用)</strong>:
{{"type":"column", "title": "标题", "axis": {{"x": {{"name":"维度轴的{lang}名称", "value": "SQL 查询维度轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "y": [{{"name":"数值轴的{lang}名称","value": "SQL 查询数值轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}], "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}}
<strong>柱状图配置说明:</strong>
<strong>1. x轴:维度轴,通常放置分类或时间字段(如日期、产品类别)</strong>
<strong>2. y轴:数值轴,放置需要展示的数值指标</strong>
<strong>3. series:当需要对数据进一步分组时使用(如不同产品系列在不同日期的销售额)</strong>
柱状图使用一个分类字段(series),一个<strong>维度轴</strong>字段(x)和一个<strong>数值轴</strong>字段(y),其中必须从SQL查询列中提取"x"、"y"与"series"。
<strong>如果SQL中没有分类列,那么JSON内的series字段不需要出现</strong>
</rule>
<rule>
如果需要条形图,JSON格式应为<strong>(series为可选字段,仅当有分类字段时使用)</strong>:
<strong>⚠️ 重要:条形图是柱状图的视觉旋转,但数据映射逻辑保持不变!</strong>
<strong>必须遵循:x轴 = 维度轴(分类),y轴 = 数值轴(指标)</strong>
<strong>不要将条形图的横向展示误解为x轴是数值!</strong>
{{"type":"bar", "title": "标题", "axis": {{"x": {{"name":"维度轴的{lang}名称", "value": "SQL 查询维度轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "y": [{{"name":"数值轴的{lang}名称","value": "SQL 查询数值轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}], "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}}
<strong>条形图配置原则:</strong>
<strong>1. 条形图只是视觉展示不同,数据逻辑与柱状图相同</strong>
<strong>2. x轴必须是维度字段(分类、时间等)</strong>
<strong>3. y轴必须是数值字段(指标、度量等)</strong>
<strong>4. 如果存在分类字段(如不同产品系列),使用series分组</strong>
条形图使用一个分类字段(series),一个<strong>维度轴</strong>字段(x)和一个<strong>数值轴</strong>字段(y),其中必须从SQL查询列中提取"x"和"y"与"series"。
<strong>如果SQL中没有分类列,那么JSON内的series字段不需要出现</strong>
</rule>
<rule>
如果需要折线图,JSON格式应为<strong>(series为可选字段,仅当有分类字段时使用)</strong>:
{{"type":"line", "title": "标题", "axis": {{"x": {{"name":"维度轴的{lang}名称","value": "SQL 查询维度轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}, "y": [{{"name":"数值轴的{lang}名称","value": "SQL 查询数值轴的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}], "series": {{"name":"分类的{lang}名称","value":"SQL 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}}
<strong>折线图配置说明:</strong>
<strong>1. x轴:维度轴,通常放置时间字段(如日期、月份)</strong>
<strong>2. y轴:数值轴,放置需要展示趋势的数值指标</strong>
<strong>3. series:当需要对比多个分类的趋势时使用(如不同产品的销售趋势)</strong>
折线图使用一个分类字段(series),一个<strong>维度轴</strong>字段(x)和一个<strong>数值轴</strong>字段(y),其中必须从SQL查询列中提取"x"、"y"与"series"。
<strong>如果SQL中没有分类列,那么JSON内的series字段不需要出现</strong>
</rule>
<rule>
如果需要饼图,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 查询分类的列(有别名用别名,去掉外层的反引号、双引号、方括号)"}}}}}}
<strong>饼图配置说明:</strong>
<strong>1. y轴:数值字段,表示各部分的大小</strong>
<strong>2. series:分类字段,表示各部分的名称</strong>
饼图使用一个分类字段(series)和一个<strong>数值</strong>字段(y),其中必须从SQL查询列中提取"y"与"series"。
</rule>
<rule>
如果SQL中没有分类列,那么JSON内的series字段不需要出现
@@ -349,7 +381,11 @@ template:
如果SQL查询结果中存在可用于数据分类的字段(如国家、产品类型等),则必须提供series配置。如果不存在,则无需在JSON中包含series字段。
</rule>
<rule>
我们目前的情况适用于单指标、多分类的场景(展示table除外),若SQL中包含多指标列,请选择一个最符合提问情况的指标作为值轴
对于柱状图/条形图/折线图:
1. 如果SQL查询中存在多个指标字段(如"收入"、"支出"、"利润"等数值字段)且不存在分类字段,则必须提供multi-quota配置,形如:"multi-quota":{{"name":"指标类型","value":["指标字段1","指标字段2",...]}}
2. 如果SQL查询中存在多个指标字段且同时存在分类字段,则以分类字段为主,选取多指标字段中的其中一个作为指标即可,不需要multi-quota配置
3. 如果只有一个指标字段,无论是否存在分类字段,都不需要multi-quota配置
<strong>重要提醒:multi-quota和series是互斥的配置,一个图表配置中只能使用其中之一,不能同时存在</strong>
</rule>
<rule>
如果你无法根据提供的内容生成合适的JSON配置,则返回:{{"type":"error", "reason": "抱歉,我无法生成合适的图表配置"}}
@@ -381,6 +417,17 @@ template:
{{"type":"pie","title":"组织人数统计","axis":{{"y":{{"name":"人数","value":"user_count"}},"series":{{"name":"组织名称","value":"org_name"}}}}}}
</output>
</example>
<example>
<input>
<sql>SELECT `s`.`date` AS `date`, `s`.`income` AS `income`, `s`.`expense` AS `expense` FROM `financial_data` `s` ORDER BY `date` ASC LIMIT 1000</sql>
<user-question>展示每月的收入与支出</user-question>
<chart-type> line </chart-type>
</input>
<output>
// 无分类字段,但有多个指标字段的情况
{{"type":"line","title":"财务指标趋势","axis":{{"x":{{"name":"日期","value":"date"}},"y":[{{"name":"收入","value":"income"}}, {{"name":"支出","value":"expense"}}], "multi-quota":{{"name":"财务指标","value":["income","expense"]}}}}}}
</output>
</example>
</chat-examples>
<example>
@@ -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 {
@@ -13,6 +13,7 @@ const params = withDefaults(
x?: Array<ChartAxis>
y?: Array<ChartAxis>
series?: Array<ChartAxis>
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() {
@@ -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<string>
}
}
columns: Array<{ name: string; value: string }>
}>(() => {
@@ -32,22 +36,40 @@ 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 []
}
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(() => {
const axis = chartObject.value?.axis
if (axis?.series) {
return [axis.series]
}
return []
})
const series = computed(() => {
if (chartObject.value?.axis?.series) {
return [chartObject.value.axis.series]
}
return []
const multiQuotaName = computed(() => {
return chartObject.value?.axis?.['multi-quota']?.name
})
const chartRef = ref()
@@ -94,6 +116,7 @@ defineExpose({
:y="yAxis"
:series="series"
:data="data"
:multi-quota-name="multiQuotaName"
/>
<el-empty v-else :description="loadingData ? t('chat.loading_data') : t('chat.no_data')" />
</div>
+32 -30
View File
@@ -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<ChartAxis>, data: Array<ChartData>) {
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) {
@@ -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<ChartAxis>, data: Array<ChartData>) {
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: {
@@ -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<ChartAxis>, data: Array<ChartData>) {
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: {
@@ -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<ChartAxis>, data: Array<ChartData>) {
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(),
+108 -21
View File
@@ -6,35 +6,122 @@ interface CheckedData {
data: Array<ChartData>
}
export function checkIsPercent(valueAxis: ChartAxis, data: Array<ChartData>): 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<ChartAxis>,
y: Array<ChartAxis>,
multiQuota: Array<string>,
multiQuotaName: string = 'sqlbot_auto_series',
data: Array<ChartData>
) {
const _list: Array<ChartData> = []
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<ChartAxis>, data: Array<ChartData>): 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
@@ -166,6 +166,7 @@ defineExpose({
:y="viewInfo.chart?.yAxis"
:series="viewInfo.chart?.series"
:data="viewInfo.data?.data"
:multi-quota-name="viewInfo.chart?.multiQuotaName"
/>
</div>
<el-dialog
@@ -146,13 +146,32 @@ function adaptorChartInfoList(chatInfo: ChatInfo) {
const recordeInfo = { id: chatInfo.id + '_' + record.id, data: data, chart: {} }
const chartBaseInfo = JSON.parse(record.chart)
if (chartBaseInfo) {
let yAxis = []
const axis = chartBaseInfo?.axis
if (!axis?.y) {
yAxis = []
} else {
const y = axis.y
const multiQuotaValues = axis['multi-quota']?.value || []
// 统一处理为数组
const yArray = Array.isArray(y) ? [...y] : [{ ...y }]
// 标记 multi-quota
yAxis = yArray.map((item) => ({
...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)
}
+122 -105
View File
@@ -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,
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,
},
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
},
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 ? '%' : ''}`
},
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},
position: (data) => {
if (data[y[0].value] < 0) {
return 'bottom'
}
return 'top'
},
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 ? '%' : ''}`
},
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}
module.exports = { getBarOptions }
+122 -105
View File
@@ -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,
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,
},
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
},
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 ? '%' : ''}`
},
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},
position: (data) => {
if (data[y[0].value] < 0) {
return 'bottom'
}
return 'top'
},
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 ? '%' : ''}`
},
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}
module.exports = { getColumnOptions }
+104 -87
View File
@@ -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,
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,
},
labelAutoRotate: false,
labelAutoWrap: true,
labelAutoEllipsis: true,
},
y: { title: y[0].name },
},
scale: {
x: {
nice: true,
},
y: {
nice: true,
type: 'linear',
},
},
children: [
{
type: 'line',
encode: {
x: x[0].value,
y: y[0].value,
color: series.length > 0 ? series[0].value : undefined,
shape: 'smooth',
},
axis: {
x: {
title: x[0].name,
labelFontSize: 12,
labelAutoHide: {
type: 'hide',
keepHeader: true,
keepTail: true,
},
labelAutoRotate: false,
labelAutoWrap: true,
labelAutoEllipsis: true,
labels: [
{
text: (data) => {
const value = data[y[0].value]
if (value === undefined || value === null) {
return ''
}
return `${value}${_data.isPercent ? '%' : ''}`
},
y: {title: y[0].name},
},
scale: {
x: {
nice: true,
},
y: {
nice: true,
type: 'linear',
},
},
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 ? '%' : ''}`}
}
},
},
{
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 }
+46 -47
View File
@@ -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 }
+111 -32
View File
@@ -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,
}
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++) {
const v = data[i]
const _v = {...v}
if (result.isPercent) {
const formatValue = replace(v[valueAxis.value], '%', '')
_v[valueAxis.value] = Number(formatValue)
}
result.data.push(_v)
}
// 分组
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
})
return result
// 应用过滤规则
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
}
module.exports = {checkIsPercent}
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] !== '' &&
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 // 找到一个百分比轴就结束检查
}
}
}
// 如果发现任何百分比轴,处理所有轴的所有百分比数据
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
}
}
}
}
}
return result
}