Skip to content

Commit

Permalink
feat: skylark chart generation
Browse files Browse the repository at this point in the history
  • Loading branch information
da730 committed Dec 18, 2023
1 parent 5932d04 commit 0ff15de
Show file tree
Hide file tree
Showing 22 changed files with 534 additions and 261 deletions.
58 changes: 58 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"dependencies": {
"js-yaml": "^4.1.0"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function ChartPreview(props: IPropsType) {
const [outType, setOutType] = useState<'gif' | 'video' | ''>('');
const [src, setSrc] = useState('');

const vmind = new VMind(import.meta.OPENAI_KEY!);
const vmind = new VMind({});
// const [describe, setDescribe] = useState<string>(mockUserInput6.input);
// const [csv, setCsv] = useState<string>(mockUserInput6.csv);
// const [loading, setLoading] = useState<boolean>(false);
Expand Down
20 changes: 14 additions & 6 deletions packages/vmind/__tests__/browser/src/pages/DataInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
mockUserInput14,
mockUserInput16
} from '../constants/mockData';
import { excel2csv } from '../../../../src/excel';
import VMind from '../../../../src/index';
import { Model } from '../../../../src/typings';

Expand Down Expand Up @@ -68,15 +67,24 @@ export function DataInput(props: IPropsType) {
const [spec, setSpec] = useState<string>('');
const [time, setTime] = useState<number>(1000);
const [loading, setLoading] = useState<boolean>(false);
const vmind = new VMind(import.meta.env.OPENAI_KEY!, {
url: import.meta.env.VITE_OPENAI_URL ?? undefined
//const vmind = new VMind({
// url: import.meta.env.VITE_OPENAI_URL ?? undefined,
// model:Model.GPT3_5
//});

const vmind = new VMind({
url: import.meta.env.VITE_SKYLARK_URL ?? undefined,
model: Model.SKYLARK,
headers: {
'api-key': import.meta.env.VITE_SKYLARK_KEY
}
});

const askGPT = useCallback(async () => {
setLoading(true);
//const {fieldInfo,dataset}=vmind.parseCSVData(csv)
const { fieldInfo, dataset } = await vmind.parseDataWithGPT(csv, describe);
const { spec, time } = await vmind.generateChart(Model.GPT3_5, describe, fieldInfo, dataset);
const { fieldInfo, dataset } = vmind.parseCSVData(csv);
//const { fieldInfo, dataset } = await vmind.parseCSVDataWithLLM(csv, describe);
const { spec, time } = await vmind.generateChart(describe, fieldInfo, dataset);
props.onSpecGenerate(spec, time as any);
setLoading(false);
}, [vmind, csv, describe, props]);
Expand Down
2 changes: 1 addition & 1 deletion packages/vmind/src/common/dataProcess/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DataSet, DataView, csvParser, fold } from '@visactor/vdataset';
import { DataItem, IGPTOptions, SimpleFieldInfo } from '../../typings';
import { DataItem, SimpleFieldInfo } from '../../typings';
import { getFieldInfoFromDataset } from './utils';

export const parseCSVWithVChart = (csvString: string) => {
Expand Down
23 changes: 23 additions & 0 deletions packages/vmind/src/common/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LOCATION, SimpleFieldInfo, VizSchema } from '../typings';

/**
* generate vizSchema from fieldInfo
* @param fieldInfo SimpleFieldInfo
* @returns
*/
export const getSchemaFromFieldInfo = (fieldInfo: SimpleFieldInfo[]): Partial<VizSchema> => {
const schema = {
fields: fieldInfo
//.filter(d => usefulFields.includes(d.fieldName))
.map(d => ({
id: d.fieldName,
alias: d.fieldName,
description: d.description,
visible: true,
type: d.type,
role: d.role,
location: d.role as unknown as LOCATION
}))
};
return schema;
};
2 changes: 1 addition & 1 deletion packages/vmind/src/common/vizDataToSpec/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const oneByOneGroupSize = 10; //one-by-one动画 10个点一组
export const DEFAULT_VIDEO_LENGTH = 2000;
export const DEFAULT_PIE_VIDEO_LENGTH = 5000;
export const DEFAULT_VIDEO_LENGTH_LONG = 10000;
export const CHARTTYP_VIDEO_ELENGTH: Record<string, number> = {
export const VIDEO_LENGTH_BY_CHART_TYPE: Record<string, number> = {
pie: DEFAULT_PIE_VIDEO_LENGTH,
wordCloud: DEFAULT_VIDEO_LENGTH_LONG,
wordcloud: DEFAULT_VIDEO_LENGTH_LONG
Expand Down
2 changes: 1 addition & 1 deletion packages/vmind/src/common/vizDataToSpec/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { vizDataToSpec, patchChartTypeAndCell, checkChartTypeAndCell } from './vizDataToSpec';
export { vizDataToSpec, checkChartTypeAndCell } from './vizDataToSpec';
export { SUPPORTED_CHART_LIST } from './constants';
26 changes: 26 additions & 0 deletions packages/vmind/src/common/vizDataToSpec/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { VIDEO_LENGTH_BY_CHART_TYPE, DEFAULT_VIDEO_LENGTH } from './constants';

export const detectAxesType = (values: any[], field: string) => {
const isNumber = values.every(d => !d[field] || !isNaN(Number(d[field])));
if (isNumber) {
Expand All @@ -17,3 +19,27 @@ export const CARTESIAN_CHART_LIST = [
'Waterfall Chart',
'Box Plot Chart'
];

export const estimateVideoTime = (chartType: string, spec: any, parsedTime?: number) => {
//估算视频长度
if (chartType === 'DYNAMIC BAR CHART') {
const frameNumber = spec.player.specs.length;
const duration = spec.player.interval;
return {
totalTime: parsedTime ?? frameNumber * duration,
frameArr: parsedTime
? Array.from(new Array(frameNumber).keys()).map(n => Number(parsedTime / frameNumber))
: Array.from(new Array(frameNumber).keys()).map(n => duration)
};
}

// chartType不是真实的图表类型,转一次
const map: Record<string, string> = {
'PIE CHART': 'pie',
'WORD CLOUD': 'wordCloud'
};
return {
totalTime: parsedTime ?? VIDEO_LENGTH_BY_CHART_TYPE[map[chartType]] ?? DEFAULT_VIDEO_LENGTH,
frameArr: []
};
};
181 changes: 0 additions & 181 deletions packages/vmind/src/common/vizDataToSpec/vizDataToSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,187 +65,6 @@ export const vizDataToSpec = (
return spec;
};

export const patchChartTypeAndCell = (chartTypeOutter: string, cell: any, dataset: any[]) => {
//对GPT返回结果进行修正
//某些时候由于用户输入的意图不明确,GPT返回的cell中可能缺少字段。
//此时需要根据规则补全
//TODO: 多个y字段时,使用fold

const { x, y } = cell;

let chartType = chartTypeOutter;
// y轴字段有多个时,处理方式:
// 1. 图表类型为: 箱型图, 图表类型不做矫正
// 2. 图表类型为: 柱状图 或 折线图, 图表类型矫正为双轴图
// 3. 其他情况, 图表类型矫正为散点图
if (y && typeof y !== 'string' && y.length > 1) {
if (chartType === 'BOX PLOT CHART') {
return {
chartTypeNew: chartType,
cellNew: cell
};
}
if (chartType === 'BAR CHART' || chartType === 'LINE CHART') {
chartType = 'DUAL AXIS CHART';
} else {
return {
chartTypeNew: 'SCATTER PLOT',
cellNew: {
...cell,
x: y[0],
y: y[1],
color: typeof x === 'string' ? x : x[0]
}
};
}
}
//双轴图 订正yLeft和yRight
if (chartType === 'DUAL AXIS CHART' && cell.yLeft && cell.yRight) {
return {
chartTypeNew: chartType,
cellNew: { ...cell, y: [cell.yLeft, cell.yRight] }
};
}
//饼图 必须有color字段和angle字段
if (chartType === 'PIE CHART') {
const cellNew = { ...cell };
if (!cellNew.color || !cellNew.angle) {
const usedFields = Object.values(cell);
const dataFields = Object.keys(dataset[0]);
const remainedFields = dataFields.filter(f => !usedFields.includes(f));
if (!cellNew.color) {
//没有分配颜色字段,从剩下的字段里选择一个离散字段分配到颜色上
const colorField = remainedFields.find(f => {
const fieldType = detectAxesType(dataset, f);
return fieldType === 'band';
});
if (colorField) {
cellNew.color = colorField;
} else {
cellNew.color = remainedFields[0];
}
}
if (!cellNew.angle) {
//没有分配角度字段,从剩下的字段里选择一个连续字段分配到角度上
const angleField = remainedFields.find(f => {
const fieldType = detectAxesType(dataset, f);
return fieldType === 'linear';
});
if (angleField) {
cellNew.angle = angleField;
} else {
cellNew.angle = remainedFields[0];
}
}
}
return {
chartTypeNew: chartType,
cellNew
};
}
//词云 必须有color字段和size字段
if (chartType === 'WORD CLOUD') {
const cellNew = { ...cell };
if (!cellNew.size || !cellNew.color || cellNew.color === cellNew.size) {
const usedFields = Object.values(cell);
const dataFields = Object.keys(dataset[0]);
const remainedFields = dataFields.filter(f => !usedFields.includes(f));
//首先根据cell中的其他字段选择size和color
//若没有,则从数据的剩余字段中选择
if (!cellNew.size || cellNew.size === cellNew.color) {
const newSize = cellNew.weight ?? cellNew.fontSize;
if (newSize) {
cellNew.size = newSize;
} else {
const sizeField = remainedFields.find(f => {
const fieldType = detectAxesType(dataset, f);
return fieldType === 'linear';
});
if (sizeField) {
cellNew.size = sizeField;
} else {
cellNew.size = remainedFields[0];
}
}
}
if (!cellNew.color) {
const newColor = cellNew.text ?? cellNew.word ?? cellNew.label ?? cellNew.x;
if (newColor) {
cellNew.color = newColor;
} else {
const colorField = remainedFields.find(f => {
const fieldType = detectAxesType(dataset, f);
return fieldType === 'band';
});
if (colorField) {
cellNew.color = colorField;
} else {
cellNew.color = remainedFields[0];
}
}
}
}
return {
chartTypeNew: chartType,
cellNew
};
}
if (chartType === 'DYNAMIC BAR CHART') {
const cellNew = { ...cell };

if (!cell.time || cell.time === '' || cell.time.length === 0) {
const flattenedXField = Array.isArray(cell.x) ? cell.x : [cell.x];
const usedFields = Object.values(cellNew).filter(f => !Array.isArray(f));
usedFields.push(...flattenedXField);
const dataFields = Object.keys(dataset[0]);
const remainedFields = dataFields.filter(f => !usedFields.includes(f));

//动态条形图没有time字段,选择一个离散字段作为time
const timeField = remainedFields.find(f => {
const fieldType = detectAxesType(dataset, f);
return fieldType === 'band';
});
if (timeField) {
cellNew.time = timeField;
} else {
cellNew.time = remainedFields[0];
}
}
return {
chartTypeNew: chartType,
cellNew
};
}
//直角坐标图表 必须有x字段
if (CARTESIAN_CHART_LIST.map(chart => chart.toUpperCase()).includes(chartType)) {
const cellNew = { ...cell };
if (!cellNew.x) {
const usedFields = Object.values(cell);
const dataFields = Object.keys(dataset[0]);
const remainedFields = dataFields.filter(f => !usedFields.includes(f));
//没有分配x字段,从剩下的字段里选择一个离散字段分配到x上
const xField = remainedFields.find(f => {
const fieldType = detectAxesType(dataset, f);
return fieldType === 'band';
});
if (xField) {
cellNew.x = xField;
} else {
cellNew.x = remainedFields[0];
}
}
return {
chartTypeNew: chartType,
cellNew
};
}

return {
chartTypeNew: chartType,
cellNew: cell
};
};

export const checkChartTypeAndCell = (chartType: string, cell: any): boolean => {
switch (chartType) {
case 'BAR CHART':
Expand Down
Loading

0 comments on commit 0ff15de

Please sign in to comment.