mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
feat: support multiple metrics in line, column, and bar charts
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user