mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-03 10:05:13 +00:00
refactor: Add frontend code to DB-GPT (#912)
This commit is contained in:
83
web/components/chart/autoChart/advisor/pipeline.ts
Normal file
83
web/components/chart/autoChart/advisor/pipeline.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Advisor, CkbConfig } from '@antv/ava';
|
||||
import type { Advice, AdviseParams, AdvisorConfig, ChartKnowledgeBase } from '@antv/ava';
|
||||
import type { CustomAdvisorConfig, RuleConfig, Specification } from '../types';
|
||||
|
||||
export type CustomRecommendConfig = {
|
||||
customCKB?: Partial<AdvisorConfig['ckbCfg']>;
|
||||
customRule?: Partial<AdvisorConfig['ruleCfg']>;
|
||||
};
|
||||
|
||||
export const customizeAdvisor = (props: CustomAdvisorConfig): Advisor => {
|
||||
const { charts, scopeOfCharts: CKBCfg, ruleConfig: ruleCfg } = props;
|
||||
|
||||
const customCKB: ChartKnowledgeBase = {};
|
||||
charts?.forEach((chart) => {
|
||||
/** 若用户自定义的图表 id 与内置图表 id 相同,内置图表将被覆盖 */
|
||||
if (!chart.chartKnowledge.toSpec) {
|
||||
chart.chartKnowledge.toSpec = (data: any, dataProps: any) => {
|
||||
return { dataProps } as Specification;
|
||||
};
|
||||
} else {
|
||||
const oriFunc = chart.chartKnowledge.toSpec;
|
||||
chart.chartKnowledge.toSpec = (data: any, dataProps: any) => {
|
||||
return {
|
||||
...oriFunc(data, dataProps),
|
||||
dataProps: dataProps,
|
||||
} as Specification;
|
||||
};
|
||||
}
|
||||
customCKB[chart.chartType] = chart.chartKnowledge;
|
||||
});
|
||||
|
||||
// 步骤一:如果有 exclude 项,先从给到的 CKB 中剔除部分选定的图表类型
|
||||
if (CKBCfg?.exclude) {
|
||||
CKBCfg.exclude.forEach((chartType: string) => {
|
||||
if (Object.keys(customCKB).includes(chartType)) {
|
||||
delete customCKB[chartType];
|
||||
}
|
||||
});
|
||||
}
|
||||
// 步骤二:如果有 include 项,则从当前(剔除后的)CKB中,只保留 include 中的图表类型。
|
||||
if (CKBCfg?.include) {
|
||||
const include = CKBCfg.include;
|
||||
Object.keys(customCKB).forEach((chartType: string) => {
|
||||
if (!include.includes(chartType)) {
|
||||
delete customCKB[chartType];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const CKBConfig: CkbConfig = {
|
||||
...CKBCfg,
|
||||
custom: customCKB,
|
||||
};
|
||||
const ruleConfig: RuleConfig = {
|
||||
...ruleCfg,
|
||||
};
|
||||
|
||||
const myAdvisor = new Advisor({
|
||||
ckbCfg: CKBConfig,
|
||||
ruleCfg: ruleConfig,
|
||||
});
|
||||
|
||||
return myAdvisor;
|
||||
};
|
||||
|
||||
/** 主推荐流程 */
|
||||
export const getVisAdvices = (props: any): Advice[] => {
|
||||
const { data, dataMetaMap, myChartAdvisor } = props;
|
||||
/**
|
||||
* 若输入中有信息能够获取列的类型( Interval, Nominal, Time ),则将这个 信息传给 Advisor
|
||||
* 主要是读取 levelOfMeasureMents 这个字段,即 dataMetaMap[item].levelOfMeasurements
|
||||
*/
|
||||
const customDataProps = dataMetaMap
|
||||
? Object.keys(dataMetaMap).map((item) => {
|
||||
return { name: item, ...dataMetaMap[item] };
|
||||
})
|
||||
: null;
|
||||
const allAdvices = myChartAdvisor?.adviseWithLog({
|
||||
data,
|
||||
dataProps: customDataProps as AdviseParams['dataProps'],
|
||||
});
|
||||
return allAdvices?.advices ?? [];
|
||||
};
|
1
web/components/chart/autoChart/advisor/rule.ts
Normal file
1
web/components/chart/autoChart/advisor/rule.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const customRuleCfg = {};
|
46
web/components/chart/autoChart/advisor/utils.ts
Normal file
46
web/components/chart/autoChart/advisor/utils.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { isNull } from 'lodash';
|
||||
import type { Advice } from '@antv/ava';
|
||||
|
||||
export function defaultAdvicesFilter(props: { advices: Advice[] }) {
|
||||
const { advices } = props;
|
||||
return advices;
|
||||
}
|
||||
export const compare = (f1: any, f2: any) => {
|
||||
if (isNull(f1.distinct) || isNull(f2.distinct)) {
|
||||
if (f1.distinct! < f2!.distinct!) {
|
||||
return 1;
|
||||
}
|
||||
if (f1.distinct! > f2.distinct!) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export function hasSubset(array1: any[], array2: any[]): boolean {
|
||||
return array2.every((e) => array1.includes(e));
|
||||
}
|
||||
|
||||
export function intersects(array1: any[], array2: any[]): boolean {
|
||||
return array2.some((e) => array1.includes(e));
|
||||
}
|
||||
|
||||
export function LOM2EncodingType(lom: string) {
|
||||
switch (lom) {
|
||||
case 'Nominal':
|
||||
return 'nominal';
|
||||
case 'Ordinal':
|
||||
return 'ordinal';
|
||||
case 'Interval':
|
||||
return 'quantitative';
|
||||
case 'Time':
|
||||
return 'temporal';
|
||||
case 'Continuous':
|
||||
return 'quantitative';
|
||||
case 'Discrete':
|
||||
return 'nominal';
|
||||
default:
|
||||
return 'nominal';
|
||||
}
|
||||
}
|
8
web/components/chart/autoChart/charts/index.ts
Normal file
8
web/components/chart/autoChart/charts/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import multi_line_chart from './multi-line-chart';
|
||||
import multi_measure_column_chart from './multi-measure-column-chart';
|
||||
import multi_measure_line_chart from './multi-measure-line-chart';
|
||||
import type { CustomChart } from '../types';
|
||||
|
||||
export const customCharts: CustomChart[] = [multi_line_chart, multi_measure_column_chart, multi_measure_line_chart];
|
||||
|
||||
export type CustomChartsType = 'multi_line_chart' | 'multi_measure_column_chart' | 'multi_measure_line_chart';
|
71
web/components/chart/autoChart/charts/multi-line-chart.ts
Normal file
71
web/components/chart/autoChart/charts/multi-line-chart.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { hasSubset, intersects } from '../advisor/utils';
|
||||
import { processDateEncode } from './util';
|
||||
import type { ChartKnowledge, CustomChart, GetChartConfigProps, Specification } from '../types';
|
||||
|
||||
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']),
|
||||
);
|
||||
if (!field4X || !field4Y) return null;
|
||||
|
||||
const spec: Specification = {
|
||||
type: 'view',
|
||||
autoFit: true,
|
||||
data,
|
||||
children: [],
|
||||
};
|
||||
|
||||
field4Y.forEach((field) => {
|
||||
const singleLine: Specification = {
|
||||
type: 'line',
|
||||
encode: {
|
||||
x: processDateEncode(field4X.name as string, dataProps),
|
||||
y: field.name,
|
||||
},
|
||||
};
|
||||
if (field4Nominal) {
|
||||
singleLine.encode.color = field4Nominal.name;
|
||||
}
|
||||
spec.children.push(singleLine);
|
||||
});
|
||||
return spec;
|
||||
};
|
||||
|
||||
const ckb: ChartKnowledge = {
|
||||
id: 'multi_line_chart',
|
||||
name: 'multi_line_chart',
|
||||
alias: ['multi_line_chart'],
|
||||
family: ['LineCharts'],
|
||||
def: 'multi_line_chart uses lines with segments to show changes in data in a ordinal dimension',
|
||||
purpose: ['Comparison', 'Trend'],
|
||||
coord: ['Cartesian2D'],
|
||||
category: ['Statistic'],
|
||||
shape: ['Lines'],
|
||||
dataPres: [
|
||||
{ minQty: 1, maxQty: 1, fieldConditions: ['Time', 'Ordinal'] },
|
||||
{ minQty: 1, maxQty: '*', fieldConditions: ['Interval'] },
|
||||
{ minQty: 0, maxQty: 1, fieldConditions: ['Nominal'] },
|
||||
],
|
||||
channel: ['Color', 'Direction', 'Position'],
|
||||
recRate: 'Recommended',
|
||||
toSpec: getChartSpec,
|
||||
};
|
||||
|
||||
/* 订制一个图表需要的所有参数 */
|
||||
export const multi_line_chart: CustomChart = {
|
||||
/* 图表唯一 Id */
|
||||
chartType: 'multi_line_chart',
|
||||
/* 图表知识 */
|
||||
chartKnowledge: ckb as ChartKnowledge,
|
||||
/** 图表中文名 */
|
||||
chineseName: '折线图',
|
||||
};
|
||||
|
||||
export default multi_line_chart;
|
@@ -0,0 +1,69 @@
|
||||
import { hasSubset } from '../advisor/utils';
|
||||
|
||||
import type { ChartKnowledge, CustomChart, GetChartConfigProps, Specification } from '../types';
|
||||
|
||||
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 spec: Specification = {
|
||||
type: 'view',
|
||||
data,
|
||||
children: [],
|
||||
};
|
||||
|
||||
field4Y?.forEach((field) => {
|
||||
const singleLine: Specification = {
|
||||
type: 'interval',
|
||||
encode: {
|
||||
x: field4Nominal.name,
|
||||
y: field.name,
|
||||
color: () => field.name,
|
||||
series: () => field.name,
|
||||
},
|
||||
};
|
||||
spec.children.push(singleLine);
|
||||
});
|
||||
return spec;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const ckb: ChartKnowledge = {
|
||||
id: 'multi_measure_column_chart',
|
||||
name: 'multi_measure_column_chart',
|
||||
alias: ['multi_measure_column_chart'],
|
||||
family: ['ColumnCharts'],
|
||||
def: 'multi_measure_column_chart uses lines with segments to show changes in data in a ordinal dimension',
|
||||
purpose: ['Comparison', 'Distribution'],
|
||||
coord: ['Cartesian2D'],
|
||||
category: ['Statistic'],
|
||||
shape: ['Lines'],
|
||||
dataPres: [
|
||||
{ minQty: 1, maxQty: '*', fieldConditions: ['Interval'] },
|
||||
{ minQty: 1, maxQty: 1, fieldConditions: ['Nominal'] },
|
||||
],
|
||||
channel: ['Color', 'Direction', 'Position'],
|
||||
recRate: 'Recommended',
|
||||
toSpec: getChartSpec,
|
||||
};
|
||||
|
||||
/* 订制一个图表需要的所有参数 */
|
||||
export const multi_measure_column_chart: CustomChart = {
|
||||
/* 图表唯一 Id */
|
||||
chartType: 'multi_measure_column_chart',
|
||||
/* 图表知识 */
|
||||
chartKnowledge: ckb as ChartKnowledge,
|
||||
/** 图表中文名 */
|
||||
chineseName: '折线图',
|
||||
};
|
||||
|
||||
export default multi_measure_column_chart;
|
@@ -0,0 +1,69 @@
|
||||
import { hasSubset, intersects } from '../advisor/utils';
|
||||
import { processDateEncode } from './util';
|
||||
import type { ChartKnowledge, CustomChart, GetChartConfigProps, Specification } from '../types';
|
||||
|
||||
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 spec: Specification = {
|
||||
type: 'view',
|
||||
data,
|
||||
children: [],
|
||||
};
|
||||
|
||||
field4Y?.forEach((field) => {
|
||||
const singleLine: Specification = {
|
||||
type: 'line',
|
||||
encode: {
|
||||
x: processDateEncode(field4Nominal.name as string, dataProps),
|
||||
y: field.name,
|
||||
color: () => field.name,
|
||||
series: () => field.name,
|
||||
},
|
||||
};
|
||||
spec.children.push(singleLine);
|
||||
});
|
||||
return spec;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const ckb: ChartKnowledge = {
|
||||
id: 'multi_measure_line_chart',
|
||||
name: 'multi_measure_line_chart',
|
||||
alias: ['multi_measure_line_chart'],
|
||||
family: ['LineCharts'],
|
||||
def: 'multi_measure_line_chart uses lines with segments to show changes in data in a ordinal dimension',
|
||||
purpose: ['Comparison', 'Distribution'],
|
||||
coord: ['Cartesian2D'],
|
||||
category: ['Statistic'],
|
||||
shape: ['Lines'],
|
||||
dataPres: [
|
||||
{ minQty: 1, maxQty: '*', fieldConditions: ['Interval'] },
|
||||
{ minQty: 1, maxQty: 1, fieldConditions: ['Nominal'] },
|
||||
],
|
||||
channel: ['Color', 'Direction', 'Position'],
|
||||
recRate: 'Recommended',
|
||||
toSpec: getChartSpec,
|
||||
};
|
||||
|
||||
/* 订制一个图表需要的所有参数 */
|
||||
export const multi_measure_line_chart: CustomChart = {
|
||||
/* 图表唯一 Id */
|
||||
chartType: 'multi_measure_line_chart',
|
||||
/* 图表知识 */
|
||||
chartKnowledge: ckb as ChartKnowledge,
|
||||
/** 图表中文名 */
|
||||
chineseName: '折线图',
|
||||
};
|
||||
|
||||
export default multi_measure_line_chart;
|
16
web/components/chart/autoChart/charts/util.ts
Normal file
16
web/components/chart/autoChart/charts/util.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
type BasicDataPropertyForAdvice = any;
|
||||
|
||||
/**
|
||||
* Process date column to new Date().
|
||||
* @param field
|
||||
* @param dataProps
|
||||
* @returns
|
||||
*/
|
||||
export function processDateEncode(field: string, dataProps: BasicDataPropertyForAdvice[]) {
|
||||
const dp = dataProps.find((dataProp) => dataProp.name === field);
|
||||
|
||||
if (dp?.recommendation === 'date') {
|
||||
return (d: any) => new Date(d[field]);
|
||||
}
|
||||
return field;
|
||||
}
|
35
web/components/chart/autoChart/helpers/index.ts
Normal file
35
web/components/chart/autoChart/helpers/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ChartId } from '@antv/ava';
|
||||
import { CustomChartsType } from '../charts';
|
||||
|
||||
export type BackEndChartType =
|
||||
| 'response_line_chart'
|
||||
| 'response_bar_chart'
|
||||
| 'response_pie_chart'
|
||||
| 'response_scatter_chart'
|
||||
| 'response_area_chart'
|
||||
| 'response_heatmap_chart'
|
||||
| 'response_table';
|
||||
|
||||
type ChartType = ChartId | CustomChartsType;
|
||||
|
||||
export const getChartType = (backendChartType: BackEndChartType): ChartType[] => {
|
||||
if (backendChartType === 'response_line_chart') {
|
||||
return ['multi_line_chart', 'multi_measure_line_chart'];
|
||||
}
|
||||
if (backendChartType === 'response_bar_chart') {
|
||||
return ['multi_measure_column_chart'];
|
||||
}
|
||||
if (backendChartType === 'response_pie_chart') {
|
||||
return ['pie_chart'];
|
||||
}
|
||||
if (backendChartType === 'response_scatter_chart') {
|
||||
return ['scatter_plot'];
|
||||
}
|
||||
if (backendChartType === 'response_area_chart') {
|
||||
return ['area_chart'];
|
||||
}
|
||||
if (backendChartType === 'response_heatmap_chart') {
|
||||
return ['heatmap'];
|
||||
}
|
||||
return [];
|
||||
};
|
100
web/components/chart/autoChart/index.tsx
Normal file
100
web/components/chart/autoChart/index.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Empty, Row, Col, Select, Tooltip } from 'antd';
|
||||
import { Advice, Advisor } from '@antv/ava';
|
||||
import { Chart } from '@berryv/g2-react';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import i18n from '@/app/i18n';
|
||||
import { customizeAdvisor, getVisAdvices } from './advisor/pipeline';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { defaultAdvicesFilter } from './advisor/utils';
|
||||
import { AutoChartProps, ChartType, CustomAdvisorConfig, CustomChart, Specification } from './types';
|
||||
import { customCharts } from './charts';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export const AutoChart = (props: AutoChartProps) => {
|
||||
const { data, chartType, scopeOfCharts, ruleConfig } = props;
|
||||
|
||||
const [advisor, setAdvisor] = useState<Advisor>();
|
||||
const [advices, setAdvices] = useState<Advice[]>([]);
|
||||
const [renderChartType, setRenderChartType] = useState<ChartType>();
|
||||
|
||||
useEffect(() => {
|
||||
const input_charts: CustomChart[] = customCharts;
|
||||
const advisorConfig: CustomAdvisorConfig = {
|
||||
charts: input_charts,
|
||||
scopeOfCharts: undefined,
|
||||
ruleConfig,
|
||||
};
|
||||
setAdvisor(customizeAdvisor(advisorConfig));
|
||||
}, [ruleConfig, scopeOfCharts]);
|
||||
|
||||
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);
|
||||
}
|
||||
}, [data, advisor, chartType]);
|
||||
|
||||
const visComponent = useMemo(() => {
|
||||
/* Advices exist, render the chart. */
|
||||
if (advices?.length > 0) {
|
||||
const chartTypeInput = renderChartType ?? advices[0].type;
|
||||
const spec: Specification = advices?.find((item: Advice) => item.type === chartTypeInput)?.spec ?? undefined;
|
||||
if (spec) {
|
||||
return <Chart key={chartTypeInput} options={spec} />;
|
||||
}
|
||||
}
|
||||
}, [advices, renderChartType]);
|
||||
|
||||
if (renderChartType) {
|
||||
return (
|
||||
<div>
|
||||
<Row justify="start">
|
||||
<Col>{i18n.t('Advices')}</Col>
|
||||
<Col style={{ marginLeft: 24 }}>
|
||||
<Select
|
||||
value={renderChartType}
|
||||
placeholder={'Chart Switcher'}
|
||||
style={{ width: '180px' }}
|
||||
onChange={(value) => setRenderChartType(value)}
|
||||
size={'small'}
|
||||
>
|
||||
{advices?.map((item) => {
|
||||
const name = i18n.t(item.type);
|
||||
|
||||
return (
|
||||
<Option key={item.type} value={item.type}>
|
||||
<Tooltip title={name} placement={'right'}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<DownOutlined />
|
||||
<div style={{ marginLeft: '2px' }}>{name}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="auto-chart-content">{visComponent}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={'暂无合适的可视化视图'} />;
|
||||
};
|
||||
|
||||
export * from './helpers';
|
46
web/components/chart/autoChart/types.ts
Normal file
46
web/components/chart/autoChart/types.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Advice, AdvisorConfig, ChartId, Datum, FieldInfo, PureChartKnowledge } from '@antv/ava';
|
||||
|
||||
export type ChartType = ChartId | string;
|
||||
|
||||
export type Specification = Advice['spec'] | any;
|
||||
export type RuleConfig = AdvisorConfig['ruleCfg'];
|
||||
|
||||
export type AutoChartProps = {
|
||||
data: Datum[];
|
||||
/** Chart type which are suggestted. */
|
||||
chartType: ChartType[];
|
||||
/** Charts exclude or include. */
|
||||
scopeOfCharts?: {
|
||||
exclude?: string[];
|
||||
include?: string[];
|
||||
};
|
||||
/** Customize rules. */
|
||||
ruleConfig?: RuleConfig;
|
||||
};
|
||||
|
||||
export type ChartKnowledge = PureChartKnowledge & { toSpec?: any };
|
||||
|
||||
export type CustomChart = {
|
||||
/** Chart type ID, unique. */
|
||||
chartType: ChartType;
|
||||
/** Chart knowledge. */
|
||||
chartKnowledge: ChartKnowledge;
|
||||
/** Chart name. */
|
||||
chineseName?: string;
|
||||
};
|
||||
|
||||
export type GetChartConfigProps = {
|
||||
data: Datum[];
|
||||
spec: Specification;
|
||||
dataProps: FieldInfo[];
|
||||
chartType?: ChartType;
|
||||
};
|
||||
|
||||
export type CustomAdvisorConfig = {
|
||||
charts?: CustomChart[];
|
||||
scopeOfCharts?: {
|
||||
exclude?: string[];
|
||||
include?: string[];
|
||||
};
|
||||
ruleConfig?: RuleConfig;
|
||||
};
|
36
web/components/chart/bar-chart.tsx
Normal file
36
web/components/chart/bar-chart.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ChartData } from '@/types/chat';
|
||||
import { Chart } from '@berryv/g2-react';
|
||||
import { Card, CardContent, Typography } from '@mui/joy';
|
||||
|
||||
export default function BarChart({ chart }: { key: string; chart: ChartData }) {
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<Card className="h-full" sx={{ background: 'transparent' }}>
|
||||
<CardContent className="h-full">
|
||||
<Typography gutterBottom component="div">
|
||||
{chart.chart_name}
|
||||
</Typography>
|
||||
<Typography gutterBottom level="body3">
|
||||
{chart.chart_desc}
|
||||
</Typography>
|
||||
<div className="h-[300px]">
|
||||
<Chart
|
||||
style={{ height: '100%' }}
|
||||
options={{
|
||||
autoFit: true,
|
||||
type: 'interval',
|
||||
data: chart.values,
|
||||
encode: { x: 'name', y: 'value', color: 'type' },
|
||||
axis: {
|
||||
x: {
|
||||
labelAutoRotate: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
80
web/components/chart/index.tsx
Normal file
80
web/components/chart/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Card, CardContent, Typography } from '@mui/joy';
|
||||
import BarChart from './bar-chart';
|
||||
import LineChart from './line-chart';
|
||||
import TableChart from './table-chart';
|
||||
import { ChartData } from '@/types/chat';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type Props = {
|
||||
chartsData: Array<ChartData>;
|
||||
};
|
||||
|
||||
function Chart({ chartsData }: Props) {
|
||||
const chartRows = useMemo(() => {
|
||||
if (chartsData) {
|
||||
let res = [];
|
||||
// 若是有类型为 IndicatorValue 的,提出去,独占一行
|
||||
const chartCalc = chartsData?.filter((item) => item.chart_type === 'IndicatorValue');
|
||||
if (chartCalc.length > 0) {
|
||||
res.push({
|
||||
charts: chartCalc,
|
||||
type: 'IndicatorValue',
|
||||
});
|
||||
}
|
||||
let otherCharts = chartsData?.filter((item) => item.chart_type !== 'IndicatorValue');
|
||||
let otherLength = otherCharts.length;
|
||||
let curIndex = 0;
|
||||
// charts 数量 3~8个,暂定每行排序
|
||||
let chartLengthMap = [[0], [1], [2], [1, 2], [1, 3], [2, 1, 2], [2, 1, 3], [3, 1, 3], [3, 2, 3]];
|
||||
chartLengthMap[otherLength].forEach((item) => {
|
||||
if (item > 0) {
|
||||
const rowsItem = otherCharts.slice(curIndex, curIndex + item);
|
||||
curIndex = curIndex + item;
|
||||
res.push({
|
||||
charts: rowsItem,
|
||||
});
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
return undefined;
|
||||
}, [chartsData]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{chartRows?.map((chartRow, index) => (
|
||||
<div key={`chart_row_${index}`} className={`${chartRow?.type !== 'IndicatorValue' ? 'flex gap-3' : ''}`}>
|
||||
{chartRow.charts.map((chart) => {
|
||||
if (chart.chart_type === 'IndicatorValue') {
|
||||
return (
|
||||
<div key={chart.chart_uid} className="flex flex-row gap-3">
|
||||
{chart.values.map((item) => (
|
||||
<div key={item.name} className="flex-1">
|
||||
<Card sx={{ background: 'transparent' }}>
|
||||
<CardContent className="justify-around">
|
||||
<Typography gutterBottom component="div">
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography>{item.value}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else if (chart.chart_type === 'LineChart') {
|
||||
return <LineChart key={chart.chart_uid} chart={chart} />;
|
||||
} else if (chart.chart_type === 'BarChart') {
|
||||
return <BarChart key={chart.chart_uid} chart={chart} />;
|
||||
} else if (chart.chart_type === 'Table') {
|
||||
return <TableChart key={chart.chart_uid} chart={chart} />;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export * from './autoChart';
|
||||
export default Chart;
|
59
web/components/chart/line-chart.tsx
Normal file
59
web/components/chart/line-chart.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ChartData } from '@/types/chat';
|
||||
import { Card, CardContent, Typography } from '@mui/joy';
|
||||
import { Chart } from '@berryv/g2-react';
|
||||
|
||||
export default function LineChart({ chart }: { chart: ChartData }) {
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<Card className="h-full" sx={{ background: 'transparent' }}>
|
||||
<CardContent className="h-full">
|
||||
<Typography gutterBottom component="div">
|
||||
{chart.chart_name}
|
||||
</Typography>
|
||||
<Typography gutterBottom level="body3">
|
||||
{chart.chart_desc}
|
||||
</Typography>
|
||||
<div className="h-[300px]">
|
||||
<Chart
|
||||
style={{ height: '100%' }}
|
||||
options={{
|
||||
autoFit: true,
|
||||
type: 'view',
|
||||
data: chart.values,
|
||||
children: [
|
||||
{
|
||||
type: 'line',
|
||||
encode: {
|
||||
x: 'name',
|
||||
y: 'value',
|
||||
color: 'type',
|
||||
shape: 'smooth',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'area',
|
||||
encode: {
|
||||
x: 'name',
|
||||
y: 'value',
|
||||
color: 'type',
|
||||
shape: 'smooth',
|
||||
},
|
||||
legend: false,
|
||||
style: {
|
||||
fillOpacity: 0.15,
|
||||
},
|
||||
},
|
||||
],
|
||||
axis: {
|
||||
x: {
|
||||
labelAutoRotate: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
41
web/components/chart/table-chart.tsx
Normal file
41
web/components/chart/table-chart.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ChartData } from '@/types/chat';
|
||||
import { Card, CardContent, Typography, Table } from '@mui/joy';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
export default function TableChart({ chart }: { key: string; chart: ChartData }) {
|
||||
const data = groupBy(chart.values, 'type');
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<Card className="h-full overflow-auto" sx={{ background: 'transparent' }}>
|
||||
<CardContent className="h-full">
|
||||
<Typography gutterBottom component="div">
|
||||
{chart.chart_name}
|
||||
</Typography>
|
||||
<Typography gutterBottom level="body3">
|
||||
{chart.chart_desc}
|
||||
</Typography>
|
||||
<div className="flex-1">
|
||||
<Table aria-label="basic table" stripe="odd" hoverRow borderAxis="bothBetween">
|
||||
<thead>
|
||||
<tr>
|
||||
{Object.keys(data).map((key) => (
|
||||
<th key={key}>{key}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.values(data)?.[0]?.map((value, i) => (
|
||||
<tr key={i}>
|
||||
{Object.keys(data)?.map((k) => (
|
||||
<td key={k}>{data?.[k]?.[i].value || ''}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user