| import path from "path"; |
| import fs from "fs"; |
| import puppeteer from "puppeteer"; |
| import VMind, { ChartType, DataTable } from "@visactor/vmind"; |
| import { isString } from "@visactor/vutils"; |
|
|
| enum AlgorithmType { |
| OverallTrending = "overallTrend", |
| AbnormalTrend = "abnormalTrend", |
| PearsonCorrelation = "pearsonCorrelation", |
| SpearmanCorrelation = "spearmanCorrelation", |
| ExtremeValue = "extremeValue", |
| MajorityValue = "majorityValue", |
| StatisticsAbnormal = "statisticsAbnormal", |
| StatisticsBase = "statisticsBase", |
| DbscanOutlier = "dbscanOutlier", |
| LOFOutlier = "lofOutlier", |
| TurningPoint = "turningPoint", |
| PageHinkley = "pageHinkley", |
| DifferenceOutlier = "differenceOutlier", |
| Volatility = "volatility", |
| } |
|
|
| const getBase64 = async (spec: any, width?: number, height?: number) => { |
| spec.animation = false; |
| width && (spec.width = width); |
| height && (spec.height = height); |
| const browser = await puppeteer.launch(); |
| const page = await browser.newPage(); |
| await page.setContent(getHtmlVChart(spec, width, height)); |
|
|
| const dataUrl = await page.evaluate(() => { |
| const canvas: any = document |
| .getElementById("chart-container") |
| ?.querySelector("canvas"); |
| return canvas?.toDataURL("image/png"); |
| }); |
|
|
| const base64Data = dataUrl.replace(/^data:image\/png;base64,/, ""); |
| await browser.close(); |
| return Buffer.from(base64Data, "base64"); |
| }; |
|
|
| const serializeSpec = (spec: any) => { |
| return JSON.stringify(spec, (key, value) => { |
| if (typeof value === "function") { |
| const funcStr = value |
| .toString() |
| .replace(/(\r\n|\n|\r)/gm, "") |
| .replace(/\s+/g, " "); |
|
|
| return `__FUNCTION__${funcStr}`; |
| } |
| return value; |
| }); |
| }; |
|
|
| function getHtmlVChart(spec: any, width?: number, height?: number) { |
| return `<!DOCTYPE html> |
| <html> |
| <head> |
| <title>VChart Demo</title> |
| <script src="https://unpkg.com/@visactor/vchart/build/index.min.js"></script> |
| </head> |
| <body> |
| <div id="chart-container" style="width: ${ |
| width ? `${width}px` : "100%" |
| }; height: ${height ? `${height}px` : "100%"};"></div> |
| <script> |
| // parse spec with function |
| function parseSpec(stringSpec) { |
| return JSON.parse(stringSpec, (k, v) => { |
| if (typeof v === 'string' && v.startsWith('__FUNCTION__')) { |
| const funcBody = v.slice(12); // 移除标记 |
| try { |
| return new Function('return (' + funcBody + ')')(); |
| } catch(e) { |
| console.error('函数解析失败:', e); |
| return () => {}; |
| } |
| } |
| return v; |
| }); |
| } |
| const spec = parseSpec(\`${serializeSpec(spec)}\`); |
| const chart = new VChart.VChart(spec, { |
| dom: 'chart-container' |
| }); |
| chart.renderSync(); |
| </script> |
| </body> |
| </html> |
| `; |
| } |
|
|
| |
| |
| |
| |
| function getSavedPathName( |
| directory: string, |
| fileName: string, |
| outputType: "html" | "png" | "json" | "md", |
| isUpdate: boolean = false |
| ) { |
| let newFileName = fileName; |
| while ( |
| !isUpdate && |
| fs.existsSync( |
| path.join(directory, "visualization", `${newFileName}.${outputType}`) |
| ) |
| ) { |
| newFileName += "_new"; |
| } |
| return path.join(directory, "visualization", `${newFileName}.${outputType}`); |
| } |
|
|
| const readStdin = (): Promise<string> => { |
| return new Promise((resolve) => { |
| let input = ""; |
| process.stdin.setEncoding("utf-8"); |
| process.stdin.on("data", (chunk) => (input += chunk)); |
| process.stdin.on("end", () => resolve(input)); |
| }); |
| }; |
|
|
| |
| const setInsightTemplate = ( |
| path: string, |
| title: string, |
| insights: string[] |
| ) => { |
| let res = ""; |
| if (insights.length) { |
| res += `## ${title} Insights`; |
| insights.forEach((insight, index) => { |
| res += `\n${index + 1}. ${insight}`; |
| }); |
| } |
| if (res) { |
| fs.writeFileSync(path, res, "utf-8"); |
| return { insight_path: path, insight_md: res }; |
| } |
| return {}; |
| }; |
|
|
| |
| async function saveChartRes(options: { |
| spec: any; |
| directory: string; |
| outputType: "png" | "html"; |
| fileName: string; |
| width?: number; |
| height?: number; |
| isUpdate?: boolean; |
| }) { |
| const { directory, fileName, spec, outputType, width, height, isUpdate } = |
| options; |
| const specPath = getSavedPathName(directory, fileName, "json", isUpdate); |
| fs.writeFileSync(specPath, JSON.stringify(spec, null, 2)); |
| const savedPath = getSavedPathName(directory, fileName, outputType, isUpdate); |
| if (outputType === "png") { |
| const base64 = await getBase64(spec, width, height); |
| fs.writeFileSync(savedPath, base64); |
| } else { |
| const html = getHtmlVChart(spec, width, height); |
| fs.writeFileSync(savedPath, html, "utf-8"); |
| } |
| return savedPath; |
| } |
|
|
| async function generateChart( |
| vmind: VMind, |
| options: { |
| dataset: string | DataTable; |
| userPrompt: string; |
| directory: string; |
| outputType: "png" | "html"; |
| fileName: string; |
| width?: number; |
| height?: number; |
| language?: "en" | "zh"; |
| } |
| ) { |
| let res: { |
| chart_path?: string; |
| error?: string; |
| insight_path?: string; |
| insight_md?: string; |
| } = {}; |
| const { |
| dataset, |
| userPrompt, |
| directory, |
| width, |
| height, |
| outputType, |
| fileName, |
| language, |
| } = options; |
| try { |
| |
| const jsonDataset = isString(dataset) ? JSON.parse(dataset) : dataset; |
| const { spec, error, chartType } = await vmind.generateChart( |
| userPrompt, |
| undefined, |
| jsonDataset, |
| { |
| enableDataQuery: false, |
| theme: "light", |
| } |
| ); |
| if (error || !spec) { |
| return { |
| error: error || "Spec of Chart was Empty!", |
| }; |
| } |
|
|
| spec.title = { |
| text: userPrompt, |
| }; |
| if (!fs.existsSync(path.join(directory, "visualization"))) { |
| fs.mkdirSync(path.join(directory, "visualization")); |
| } |
| const specPath = getSavedPathName(directory, fileName, "json"); |
| res.chart_path = await saveChartRes({ |
| directory, |
| spec, |
| width, |
| height, |
| fileName, |
| outputType, |
| }); |
|
|
| |
| const insights = []; |
| if ( |
| chartType && |
| [ |
| ChartType.BarChart, |
| ChartType.LineChart, |
| ChartType.AreaChart, |
| ChartType.ScatterPlot, |
| ChartType.DualAxisChart, |
| ].includes(chartType) |
| ) { |
| const { insights: vmindInsights } = await vmind.getInsights(spec, { |
| maxNum: 6, |
| algorithms: [ |
| AlgorithmType.OverallTrending, |
| AlgorithmType.AbnormalTrend, |
| AlgorithmType.PearsonCorrelation, |
| AlgorithmType.SpearmanCorrelation, |
| AlgorithmType.StatisticsAbnormal, |
| AlgorithmType.LOFOutlier, |
| AlgorithmType.DbscanOutlier, |
| AlgorithmType.MajorityValue, |
| AlgorithmType.PageHinkley, |
| AlgorithmType.TurningPoint, |
| AlgorithmType.StatisticsBase, |
| AlgorithmType.Volatility, |
| ], |
| usePolish: false, |
| language: language === "en" ? "english" : "chinese", |
| }); |
| insights.push(...vmindInsights); |
| } |
| const insightsText = insights |
| .map((insight) => insight.textContent?.plainText) |
| .filter((insight) => !!insight) as string[]; |
| spec.insights = insights; |
| fs.writeFileSync(specPath, JSON.stringify(spec, null, 2)); |
| res = { |
| ...res, |
| ...setInsightTemplate( |
| getSavedPathName(directory, fileName, "md"), |
| userPrompt, |
| insightsText |
| ), |
| }; |
| } catch (error: any) { |
| res.error = error.toString(); |
| } finally { |
| return res; |
| } |
| } |
|
|
| async function updateChartWithInsight( |
| vmind: VMind, |
| options: { |
| directory: string; |
| outputType: "png" | "html"; |
| fileName: string; |
| insightsId: number[]; |
| } |
| ) { |
| const { directory, outputType, fileName, insightsId } = options; |
| let res: { error?: string; chart_path?: string } = {}; |
| try { |
| const specPath = getSavedPathName(directory, fileName, "json", true); |
| const spec = JSON.parse(fs.readFileSync(specPath, "utf8")); |
| |
| const insights = (spec.insights || []).filter( |
| (_insight: any, index: number) => insightsId.includes(index + 1) |
| ); |
| const { newSpec, error } = await vmind.updateSpecByInsights(spec, insights); |
| if (error) { |
| throw error; |
| } |
| res.chart_path = await saveChartRes({ |
| spec: newSpec, |
| directory, |
| outputType, |
| fileName, |
| isUpdate: true, |
| }); |
| } catch (error: any) { |
| res.error = error.toString(); |
| } finally { |
| return res; |
| } |
| } |
|
|
| async function executeVMind() { |
| const input = await readStdin(); |
| const inputData = JSON.parse(input); |
| let res; |
| const { |
| llm_config, |
| width, |
| dataset = [], |
| height, |
| directory, |
| user_prompt: userPrompt, |
| output_type: outputType = "png", |
| file_name: fileName, |
| task_type: taskType = "visualization", |
| insights_id: insightsId = [], |
| language = "en", |
| } = inputData; |
| const { base_url: baseUrl, model, api_key: apiKey } = llm_config; |
| const vmind = new VMind({ |
| url: `${baseUrl}/chat/completions`, |
| model, |
| headers: { |
| "api-key": apiKey, |
| Authorization: `Bearer ${apiKey}`, |
| }, |
| }); |
| if (taskType === "visualization") { |
| res = await generateChart(vmind, { |
| dataset, |
| userPrompt, |
| directory, |
| outputType, |
| fileName, |
| width, |
| height, |
| language, |
| }); |
| } else if (taskType === "insight" && insightsId.length) { |
| res = await updateChartWithInsight(vmind, { |
| directory, |
| fileName, |
| outputType, |
| insightsId, |
| }); |
| } |
| console.log(JSON.stringify(res)); |
| } |
|
|
| executeVMind(); |
|
|