mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-02 01:27:14 +00:00
fix(web): fix inconsistency between model recommendation charts and display charts (#1615)
Co-authored-by: hzh97 <2976151305@qq.com> Co-authored-by: aries_ckt <916701291@qq.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Advisor, CkbConfig } from '@antv/ava';
|
||||
import type { Advice, AdviseParams, AdvisorConfig, ChartKnowledgeBase } from '@antv/ava';
|
||||
import { Advisor, CkbConfig, DataFrame } from '@antv/ava';
|
||||
import type { Advice, AdviseParams, AdvisorConfig, ChartKnowledgeBase, Datum, FieldInfo } from '@antv/ava';
|
||||
import type { CustomAdvisorConfig, RuleConfig, Specification } from '../types';
|
||||
import { size } from 'lodash';
|
||||
|
||||
export type CustomRecommendConfig = {
|
||||
customCKB?: Partial<AdvisorConfig['ckbCfg']>;
|
||||
@@ -64,7 +65,7 @@ export const customizeAdvisor = (props: CustomAdvisorConfig): Advisor => {
|
||||
};
|
||||
|
||||
/** 主推荐流程 */
|
||||
export const getVisAdvices = (props: any): Advice[] => {
|
||||
export const getVisAdvices = (props: { data: Datum[]; myChartAdvisor: Advisor; dataMetaMap?: Record<string, FieldInfo> }): Advice[] => {
|
||||
const { data, dataMetaMap, myChartAdvisor } = props;
|
||||
/**
|
||||
* 若输入中有信息能够获取列的类型( Interval, Nominal, Time ),则将这个 信息传给 Advisor
|
||||
@@ -75,9 +76,26 @@ export const getVisAdvices = (props: any): Advice[] => {
|
||||
return { name: item, ...dataMetaMap[item] };
|
||||
})
|
||||
: null;
|
||||
|
||||
// 可根据需要选择是否使用全部 fields 进行推荐
|
||||
const useAllFields = false;
|
||||
// 挑选出维值不只有一个的字段
|
||||
const allFieldsInfo = new DataFrame(data).info();
|
||||
const selectedFields =
|
||||
size(allFieldsInfo) > 2
|
||||
? allFieldsInfo?.filter((field) => {
|
||||
if (field.recommendation === 'string' || field.recommendation === 'date') {
|
||||
return field.distinct && field.distinct > 1;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
: allFieldsInfo;
|
||||
|
||||
const allAdvices = myChartAdvisor?.adviseWithLog({
|
||||
data,
|
||||
dataProps: customDataProps as AdviseParams['dataProps'],
|
||||
// 不传 fields 参数,内部默认使用全部 fields,否则使用业务选择的字段
|
||||
fields: useAllFields ? undefined : selectedFields?.map((field) => field.name),
|
||||
});
|
||||
return allAdvices?.advices ?? [];
|
||||
};
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { hasSubset, intersects } from '../advisor/utils';
|
||||
import { processDateEncode } from './util';
|
||||
import { findOrdinalField, processDateEncode, findNominalField, isUniqueXValue, getLineSize } from './util';
|
||||
import type { ChartKnowledge, CustomChart, GetChartConfigProps, Specification } from '../types';
|
||||
import type { Datum } from '@antv/ava';
|
||||
|
||||
const getChartSpec = (data: GetChartConfigProps['data'], dataProps: GetChartConfigProps['dataProps']) => {
|
||||
const field4X = dataProps.find((field) =>
|
||||
// @ts-ignore
|
||||
intersects(field.levelOfMeasurements, ['Time', 'Ordinal']),
|
||||
);
|
||||
// @ts-ignore
|
||||
const field4Y = dataProps.filter((field) => hasSubset(field.levelOfMeasurements, ['Interval']));
|
||||
const field4Nominal = dataProps.find((field) =>
|
||||
// @ts-ignore
|
||||
hasSubset(field.levelOfMeasurements, ['Nominal']),
|
||||
const ordinalField = findOrdinalField(dataProps);
|
||||
const nominalField = findNominalField(dataProps);
|
||||
// 放宽折线图的 x 轴条件,优先选择 time, ordinal 类型,没有的话使用 nominal 类型
|
||||
const field4X = ordinalField ?? nominalField;
|
||||
|
||||
const field4Y = dataProps.filter((field) => field.levelOfMeasurements && hasSubset(field.levelOfMeasurements, ['Interval']));
|
||||
const field4Nominal = dataProps.find(
|
||||
(field) => field.name !== field4X?.name && field.levelOfMeasurements && hasSubset(field.levelOfMeasurements, ['Nominal']),
|
||||
);
|
||||
if (!field4X || !field4Y) return null;
|
||||
|
||||
@@ -28,6 +28,10 @@ const getChartSpec = (data: GetChartConfigProps['data'], dataProps: GetChartConf
|
||||
encode: {
|
||||
x: processDateEncode(field4X.name as string, dataProps),
|
||||
y: field.name,
|
||||
size: (datum: Datum) => getLineSize(datum, data, { field4Split: field4Nominal, field4X }),
|
||||
},
|
||||
legend: {
|
||||
size: false,
|
||||
},
|
||||
};
|
||||
if (field4Nominal) {
|
||||
|
@@ -1,16 +1,16 @@
|
||||
import { hasSubset } from '../advisor/utils';
|
||||
|
||||
import type { ChartKnowledge, CustomChart, GetChartConfigProps, Specification } from '../types';
|
||||
import { findNominalField, findOrdinalField } from './util';
|
||||
|
||||
const getChartSpec = (data: GetChartConfigProps['data'], dataProps: GetChartConfigProps['dataProps']) => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const field4Y = dataProps?.filter((field) => hasSubset(field.levelOfMeasurements, ['Interval']));
|
||||
const field4Nominal = dataProps?.find((field) =>
|
||||
// @ts-ignore
|
||||
hasSubset(field.levelOfMeasurements, ['Nominal']),
|
||||
);
|
||||
if (!field4Nominal || !field4Y) return null;
|
||||
const nominalField = findNominalField(dataProps);
|
||||
const ordinalField = findOrdinalField(dataProps);
|
||||
const field4X = nominalField ?? ordinalField;
|
||||
if (!field4X || !field4Y) return null;
|
||||
|
||||
const spec: Specification = {
|
||||
type: 'view',
|
||||
@@ -22,7 +22,7 @@ const getChartSpec = (data: GetChartConfigProps['data'], dataProps: GetChartConf
|
||||
const singleLine: Specification = {
|
||||
type: 'interval',
|
||||
encode: {
|
||||
x: field4Nominal.name,
|
||||
x: field4X.name,
|
||||
y: field.name,
|
||||
color: () => field.name,
|
||||
series: () => field.name,
|
||||
|
@@ -1,15 +1,13 @@
|
||||
import { hasSubset, intersects } from '../advisor/utils';
|
||||
import { processDateEncode } from './util';
|
||||
import { hasSubset } from '../advisor/utils';
|
||||
import { findNominalField, findOrdinalField, getLineSize, processDateEncode } from './util';
|
||||
import type { ChartKnowledge, CustomChart, GetChartConfigProps, Specification } from '../types';
|
||||
import { Datum } from '@antv/ava';
|
||||
|
||||
const getChartSpec = (data: GetChartConfigProps['data'], dataProps: GetChartConfigProps['dataProps']) => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const field4Y = dataProps?.filter((field) => hasSubset(field.levelOfMeasurements, ['Interval']));
|
||||
const field4Nominal = dataProps?.find((field) =>
|
||||
// @ts-ignore
|
||||
hasSubset(field.levelOfMeasurements, ['Nominal']),
|
||||
);
|
||||
const field4Nominal = findNominalField(dataProps) ?? findOrdinalField(dataProps);
|
||||
if (!field4Nominal || !field4Y) return null;
|
||||
|
||||
const spec: Specification = {
|
||||
@@ -26,6 +24,10 @@ const getChartSpec = (data: GetChartConfigProps['data'], dataProps: GetChartConf
|
||||
y: field.name,
|
||||
color: () => field.name,
|
||||
series: () => field.name,
|
||||
size: (datum: Datum) => getLineSize(datum, data, { field4X: field4Nominal }),
|
||||
},
|
||||
legend: {
|
||||
size: false,
|
||||
},
|
||||
};
|
||||
spec.children.push(singleLine);
|
||||
|
@@ -1,3 +1,7 @@
|
||||
import { Datum, FieldInfo } from '@antv/ava';
|
||||
import { hasSubset, intersects } from '../advisor/utils';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
type BasicDataPropertyForAdvice = any;
|
||||
|
||||
/**
|
||||
@@ -14,3 +18,35 @@ export function processDateEncode(field: string, dataProps: BasicDataPropertyFor
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
export function findOrdinalField(fields: FieldInfo[]) {
|
||||
return fields.find((field) => field.levelOfMeasurements && intersects(field.levelOfMeasurements, ['Time', 'Ordinal']));
|
||||
}
|
||||
|
||||
export function findNominalField(fields: FieldInfo[]) {
|
||||
return fields.find((field) => field.levelOfMeasurements && hasSubset(field.levelOfMeasurements, ['Nominal']));
|
||||
}
|
||||
|
||||
// 识别 x 轴是否只有一条数据(绘制的折线图是否只有一个点)
|
||||
export const isUniqueXValue = ({ data, xField }: { xField: string; data: Datum[] }): boolean => {
|
||||
const uniqXValues = uniq(data.map((datum) => datum[xField]));
|
||||
return uniqXValues.length <= 1;
|
||||
};
|
||||
|
||||
/** 获取线宽:当只有一条数据时,折线图需要特殊设置线宽,否则仅绘制 1px,看不见 */
|
||||
export const getLineSize = (
|
||||
datum: Datum,
|
||||
allData: Datum[],
|
||||
fields: {
|
||||
field4Split?: FieldInfo;
|
||||
field4X?: FieldInfo;
|
||||
},
|
||||
) => {
|
||||
const { field4Split, field4X } = fields;
|
||||
if (field4Split?.name && field4X?.name) {
|
||||
const seriesValue = datum[field4Split.name];
|
||||
const splitData = allData.filter((item) => field4Split.name && item[field4Split.name] === seriesValue);
|
||||
return isUniqueXValue({ data: splitData, xField: field4X.name }) ? 5 : undefined;
|
||||
}
|
||||
return field4X?.name && isUniqueXValue({ data: allData, xField: field4X.name }) ? 5 : undefined;
|
||||
};
|
||||
|
@@ -8,6 +8,7 @@ import { defaultAdvicesFilter } from './advisor/utils';
|
||||
import { AutoChartProps, ChartType, CustomAdvisorConfig, CustomChart, Specification } from './types';
|
||||
import { customCharts } from './charts';
|
||||
import { ChatContext } from '@/app/chat-context';
|
||||
import { compact, concat, uniq } from 'lodash';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -24,29 +25,61 @@ export const AutoChart = (props: AutoChartProps) => {
|
||||
const input_charts: CustomChart[] = customCharts;
|
||||
const advisorConfig: CustomAdvisorConfig = {
|
||||
charts: input_charts,
|
||||
scopeOfCharts: undefined,
|
||||
scopeOfCharts: {
|
||||
// 排除面积图
|
||||
exclude: ['area_chart', 'stacked_area_chart', 'percent_stacked_area_chart'],
|
||||
},
|
||||
ruleConfig,
|
||||
};
|
||||
setAdvisor(customizeAdvisor(advisorConfig));
|
||||
}, [ruleConfig, scopeOfCharts]);
|
||||
|
||||
/** 将 AVA 得到的图表推荐结果和模型的合并 */
|
||||
const getMergedAdvices = (avaAdvices: Advice[]) => {
|
||||
if (!advisor) return [];
|
||||
const filteredAdvices = defaultAdvicesFilter({
|
||||
advices: avaAdvices,
|
||||
});
|
||||
const allChartTypes = uniq(
|
||||
compact(
|
||||
concat(
|
||||
chartType,
|
||||
avaAdvices.map((item) => item.type),
|
||||
),
|
||||
),
|
||||
);
|
||||
const allAdvices = allChartTypes
|
||||
.map((chartTypeItem) => {
|
||||
const avaAdvice = filteredAdvices.find((item) => item.type === chartTypeItem);
|
||||
// 如果在 AVA 推荐列表中,直接采用推荐列表中的结果
|
||||
if (avaAdvice) {
|
||||
return avaAdvice;
|
||||
}
|
||||
// 如果不在,则单独为其生成图表 spec
|
||||
const dataAnalyzerOutput = advisor.dataAnalyzer.execute({ data });
|
||||
if ('data' in dataAnalyzerOutput) {
|
||||
const specGeneratorOutput = advisor.specGenerator.execute({
|
||||
data: dataAnalyzerOutput.data,
|
||||
dataProps: dataAnalyzerOutput.dataProps,
|
||||
chartTypeRecommendations: [{ chartType: chartTypeItem, score: 1 }],
|
||||
});
|
||||
if ('advices' in specGeneratorOutput) return specGeneratorOutput.advices?.[0];
|
||||
}
|
||||
})
|
||||
.filter((advice) => advice?.spec) as Advice[];
|
||||
return allAdvices;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data && advisor) {
|
||||
const avaAdvices = getVisAdvices({
|
||||
data,
|
||||
myChartAdvisor: advisor,
|
||||
});
|
||||
const filteredAdvices = defaultAdvicesFilter({
|
||||
advices: avaAdvices,
|
||||
});
|
||||
|
||||
filteredAdvices.sort((a, b) => {
|
||||
return chartType.indexOf(b.type) - chartType?.indexOf(a.type);
|
||||
});
|
||||
|
||||
setAdvices(filteredAdvices);
|
||||
|
||||
setRenderChartType(filteredAdvices[0]?.type as ChartType);
|
||||
// 合并模型推荐的图表类型和 ava 推荐的图表类型
|
||||
const allAdvices = getMergedAdvices(avaAdvices);
|
||||
setAdvices(allAdvices);
|
||||
setRenderChartType(allAdvices[0]?.type as ChartType);
|
||||
}
|
||||
}, [data, advisor, chartType]);
|
||||
|
||||
|
Reference in New Issue
Block a user