Repository: antvis/GPT-Vis
Pull request #83: feat: add gpt-vis SSR
Author: login: XinyueDu
Labels: name: pr(feature)
Created: 2025-04-01T12:13:42Z | Merged: 2025-04-23T13:16:59Z

## 介绍

在大模型输出的时候，为了兼容不同平台，端的渲染环境，使用静态图是最为快捷简单的方式，所以为 GPT-Vis 提供服务端出图的库。

## API 设计

设计上沿用 gpt-vis 包的 API，配置上保持完全一致。

```ts
import { render } from '@antv/gpt-vis/ssr';

const buffer = render({
  type: 'pie',
  data: [],
});
```

## 具体方案

1. 在 src 目录新开一个 ssr 目录，共享类型定义。
2. 在 package.json 中 export 定义 ssr 目录，让 `@antv/gpt-vis/ssr` 就是 nodejs 代码
3. 单测

- fixed #1

Linked issue #1: Server rendering support?
Thanks to this nice work.
I am used to ECharts for rendering both on server and client. Similarly, would GPT-Vis be able to render on server? This is important that the text2any agent serves as a submodule for a bigger app.
Linked issue comments: user: login: csunny
created_at: 2024-04-15T03:21:20Z
body: @MurrayC7  Not yet, we mainly deal with client-side rendering and front-end visualization.
author_association: CONTRIBUTOR
user: login: hustcc
created_at: 2025-04-11T02:41:03Z
body: - related: https://github.com/antvis/GPT-Vis/pull/83
author_association: MEMBER

Diff:
diff --git a/.changeset/pre.json b/.changeset/pre.json
new file mode 100644
index 00000000..947294b8
--- /dev/null
+++ b/.changeset/pre.json
@@ -0,0 +1,8 @@
+{
+  "mode": "pre",
+  "tag": "beta",
+  "initialVersions": {
+    "@antv/gpt-vis": "0.4.4"
+  },
+  "changesets": []
+}
diff --git a/.changeset/salty-berries-like.md b/.changeset/salty-berries-like.md
new file mode 100644
index 00000000..371d2e36
--- /dev/null
+++ b/.changeset/salty-berries-like.md
@@ -0,0 +1,5 @@
+---
+'@antv/gpt-vis': minor
+---
+
+gpt-vis support ssr
diff --git a/.gitignore b/.gitignore
index 5075a9a6..284954f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,3 +80,6 @@ dist
 docs-dist
 server
 .dumi
+
+# test
+__tests__/assets/*.png
diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts
new file mode 100644
index 00000000..5bbcec04
--- /dev/null
+++ b/__tests__/index.test.ts
@@ -0,0 +1,34 @@
+// @ts-nocheck
+import { join } from 'path';
+import { render } from '../src/ssr';
+
+// 本地测试
+describe('temp', () => {
+  // 异步测试
+  it('draw', async () => {
+    const graph = await render({
+      width: 600,
+      height: 400,
+      type: 'fishbone-diagram',
+      data: {
+        name: '生产效率低',
+        children: [
+          {
+            name: '设备问题',
+            children: [{ name: '设备老化' }, { name: '维护不及时' }],
+          },
+          {
+            name: '员工问题',
+            children: [{ name: '技能不足' }, { name: '工作态度差' }],
+          },
+          {
+            name: '流程问题',
+            children: [{ name: '流程繁琐' }, { name: '缺乏标准化' }],
+          },
+        ],
+      },
+    });
+    // @ts-ignore
+    graph.exportToFile(join(__dirname, './assets/fishbone-diagram'));
+  });
+});
diff --git a/package.json b/package.json
index 4717ead7..e9389dd6 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,27 @@
   "typings": "dist/esm/index.d.ts",
   "jsdelivr": "dist/umd/index.min.js",
   "unpkg": "dist/umd/index.min.js",
+  "exports": {
+    ".": {
+      "types": "./dist/lib/index.d.ts",
+      "import": "./dist/esm/index.js",
+      "require": "./dist/lib/index.js"
+    },
+    "./lib/*": {
+      "types": "./dist/lib/*.d.ts",
+      "require": "./dist/lib/*.js"
+    },
+    "./esm/*": {
+      "types": "./dist/lib/*.d.ts",
+      "import": "./dist/esm/*.js"
+    },
+    "ssr": {
+      "types": "./dist/lib/ssr/index.d.ts",
+      "import": "./dist/esm/ssr/index.js",
+      "require": "./dist/lib/ssr/index.js"
+    },
+    "./package.json": "./package.json"
+  },
   "browser": {
     "./src/export-map.ts": "./src/export-map.browser.ts",
     "./dist/esm/export-map.js": "./dist/esm/export-map.browser.js"
@@ -53,56 +74,62 @@
     "publish-package": "pnpm build && changeset publish"
   },
   "dependencies": {
-    "@ant-design/graphs": "^2.0.2",
-    "@ant-design/icons": "^5.5.2",
-    "@ant-design/plots": "^2.3.2",
+    "@ant-design/graphs": "^2.0.5",
+    "@ant-design/icons": "^5.6.1",
+    "@ant-design/plots": "^2.3.3",
     "@antv/event-emitter": "^0.1.3",
-    "@antv/l7": "^2.22.3",
+    "@antv/g": "^6.1.23",
+    "@antv/g2-ssr": "^0.0.8",
+    "@antv/g6": "^5.0.45",
+    "@antv/g6-ssr": "^0.0.16",
+    "@antv/l7": "^2.22.5",
     "@antv/larkmap": "^1.5.1",
-    "@babel/runtime": "^7.26.0",
+    "@babel/runtime": "^7.27.0",
     "lodash": "^4.17.21",
-    "react-markdown": "^9.0.1",
+    "react-markdown": "^9.1.0",
     "rehype-raw": "^7.0.0",
-    "remark-gfm": "^4.0.0",
-    "styled-components": "^6.1.13"
+    "remark-gfm": "^4.0.1",
+    "styled-components": "^6.1.17"
   },
   "devDependencies": {
-    "@ant-design/x": "^1.0.2",
-    "@changesets/changelog-github": "^0.5.0",
-    "@changesets/cli": "^2.27.10",
-    "@commitlint/cli": "^19.6.0",
-    "@commitlint/config-conventional": "^19.6.0",
+    "@ant-design/x": "^1.1.0",
+    "@changesets/changelog-github": "^0.5.1",
+    "@changesets/cli": "^2.28.1",
+    "@commitlint/cli": "^19.8.0",
+    "@commitlint/config-conventional": "^19.8.0",
     "@types/jest": "^29.5.14",
-    "@types/lodash": "^4.17.13",
-    "@types/react": "^18.3.16",
+    "@types/lodash": "^4.17.16",
+    "@types/react": "^18.3.20",
     "@types/webpack-bundle-analyzer": "^4.7.0",
-    "antd": "^5.22.4",
-    "dumi": "^2.4.16",
-    "eslint": "^9.16.0",
+    "antd": "^5.24.6",
+    "dumi": "^2.4.20",
+    "eslint": "^9.24.0",
     "eslint-config-prettier": "^9.1.0",
-    "eslint-plugin-jest": "^28.9.0",
+    "eslint-plugin-jest": "^28.11.0",
     "eslint-plugin-jsx-a11y": "^6.10.2",
-    "eslint-plugin-react": "^7.37.2",
-    "eslint-plugin-react-hooks": "^5.1.0",
-    "father": "^4.5.1",
-    "gh-pages": "^6.2.0",
-    "globals": "^15.13.0",
+    "eslint-plugin-react": "^7.37.5",
+    "eslint-plugin-react-hooks": "^5.2.0",
+    "father": "^4.5.2",
+    "gh-pages": "^6.3.0",
+    "globals": "^15.15.0",
     "husky": "^9.1.7",
     "jest": "^29.7.0",
     "jest-environment-jsdom": "^29.7.0",
     "limit-size": "^0.1.4",
-    "lint-staged": "^15.2.11",
+    "lint-staged": "^15.5.0",
     "npm-run-all": "^4.1.5",
-    "prettier": "^3.4.2",
+    "prettier": "^3.5.3",
     "prettier-plugin-organize-imports": "^4.1.0",
     "react": "^18.3.1",
-    "ts-jest": "^29.2.5",
+    "ts-jest": "^29.3.1",
     "ts-node": "^10.9.2",
-    "typescript": "^5.7.2",
-    "typescript-eslint": "^8.18.0",
-    "webpack-bundle-analyzer": "^4.10.2"
+    "typescript": "^5.8.3",
+    "typescript-eslint": "^8.29.1",
+    "webpack-bundle-analyzer": "^4.10.2",
+    "canvas": "^3.1.0"
   },
   "peerDependencies": {
+    "canvas": "^3.1.0",
     "antd": "^5.0.0",
     "react": ">=16.9.0"
   },
diff --git a/src/ssr/area.ts b/src/ssr/area.ts
new file mode 100644
index 00000000..8a0129e0
--- /dev/null
+++ b/src/ssr/area.ts
@@ -0,0 +1,42 @@
+import { createChart } from '@antv/g2-ssr';
+import { type AreaProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type AreaOptions = BaseChartConfig & { type: 'area' } & AreaProps;
+export async function Area(options: AreaOptions) {
+  const { data, title, width, height, stack, axisYTitle, axisXTitle } = options;
+
+  let encode = {};
+  let transform: any = [];
+  if (stack) {
+    encode = { x: 'time', y: 'value', color: 'group' };
+    transform = [{ type: 'stackY' }];
+  } else {
+    encode = { x: 'time', y: 'value' };
+  }
+
+  return await createChart({
+    type: 'view',
+    title,
+    data,
+    width,
+    height,
+    encode,
+    transform,
+    style: { minHeight: 1 },
+    axis: {
+      y: {
+        title: axisYTitle || false,
+      },
+      x: {
+        title: axisXTitle || false,
+      },
+    },
+    children: [
+      {
+        type: 'area',
+        style: { opacity: 0.7 },
+      },
+    ],
+  });
+}
diff --git a/src/ssr/bar.ts b/src/ssr/bar.ts
new file mode 100644
index 00000000..7074cf9e
--- /dev/null
+++ b/src/ssr/bar.ts
@@ -0,0 +1,60 @@
+import { createChart } from '@antv/g2-ssr';
+import { type BarProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type BarOptions = BaseChartConfig & { type: 'bar' } & BarProps;
+export async function Bar(options: BarOptions) {
+  const { data, title, width, height, stack, group, axisYTitle, axisXTitle } = options;
+  const hasGroupField = (data || [])[0]?.group !== undefined;
+
+  let transforms: any = [];
+
+  if (group) {
+    transforms = [
+      {
+        type: 'dodgeX',
+      },
+    ];
+  }
+
+  if (stack) {
+    transforms = [
+      {
+        type: 'stackY',
+      },
+    ];
+  }
+
+  return await createChart({
+    width,
+    height,
+    title,
+    type: 'interval',
+    data,
+    encode: hasGroupField
+      ? {
+          x: 'category',
+          y: 'value',
+          color: 'group',
+        }
+      : {
+          x: 'category',
+          y: 'value',
+        },
+    transform: transforms,
+    coordinate: { transform: [{ type: 'transpose' }] },
+    style: {
+      // 圆角样式
+      radiusTopLeft: 10,
+      radiusTopRight: 10,
+    },
+    axis: {
+      x: {
+        title: axisXTitle,
+      },
+      y: {
+        title: axisYTitle,
+      },
+    },
+  });
+}
diff --git a/src/ssr/column.ts b/src/ssr/column.ts
new file mode 100644
index 00000000..73293e8c
--- /dev/null
+++ b/src/ssr/column.ts
@@ -0,0 +1,57 @@
+import { createChart } from '@antv/g2-ssr';
+import { type ColumnProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type ColumnOptions = BaseChartConfig & { type: 'column' } & ColumnProps;
+
+export async function Column(options: ColumnOptions) {
+  const { data, title, width, height, axisYTitle, axisXTitle, group, stack } = options;
+  const hasGroupField = (data || [])[0]?.group !== undefined;
+
+  let transforms: any = [];
+
+  if (group) {
+    transforms = [
+      {
+        type: 'dodgeX',
+      },
+    ];
+  }
+  if (stack) {
+    transforms = [
+      {
+        type: 'stackY',
+      },
+    ];
+  }
+  return await createChart({
+    width,
+    height,
+    title,
+    data,
+    type: 'interval',
+    encode: hasGroupField
+      ? {
+          x: 'category',
+          y: 'value',
+          color: 'group',
+        }
+      : {
+          x: 'category',
+          y: 'value',
+        },
+    transform: transforms,
+    style: {
+      radiusTopLeft: 10,
+      radiusTopRight: 10,
+    },
+    axis: {
+      x: {
+        title: axisXTitle,
+      },
+      y: {
+        title: axisYTitle,
+      },
+    },
+  });
+}
diff --git a/src/ssr/dual-axes.ts b/src/ssr/dual-axes.ts
new file mode 100644
index 00000000..4e169083
--- /dev/null
+++ b/src/ssr/dual-axes.ts
@@ -0,0 +1,92 @@
+import { createChart } from '@antv/g2-ssr';
+import { type DualAxesProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type DualAxespOptions = BaseChartConfig & { type: 'dual-axes' } & DualAxesProps;
+
+export async function DualAxes(options: DualAxespOptions) {
+  const { series, categories, title, width, height } = options;
+  type DualAxesSeriesItem = {
+    type: string;
+    data: number[];
+    axisYTitle?: string;
+  };
+
+  enum ChartType {
+    Column = 'column',
+    Line = 'line',
+  }
+
+  function transform(series: DualAxesSeriesItem[], categories: string[]) {
+    const newChildren = series.map((item: any) => {
+      const { type, axisYTitle, ...others } = item;
+
+      const baseConfig = {
+        ...others,
+        axis: { y: { title: axisYTitle } },
+        encode: { x: 'category', y: axisYTitle, color: () => axisYTitle },
+        legend: {
+          color: {
+            itemMarker: (v: any) => {
+              if (v === axisYTitle) return 'smooth';
+              return 'rect';
+            },
+          },
+        },
+        data: undefined,
+      };
+
+      if (type === ChartType.Column) {
+        return { ...baseConfig, type: 'interval' };
+      }
+
+      if (type === ChartType.Line) {
+        return {
+          ...baseConfig,
+          type,
+          axis: { y: { position: 'right', title: axisYTitle } },
+          encode: { x: 'category', y: axisYTitle, shape: 'smooth', color: () => axisYTitle },
+          style: { lineWidth: 2, stroke: '#5AD8A6' },
+          scale: { y: { independent: true } },
+        };
+      }
+
+      return baseConfig;
+    });
+
+    const newData = categories.map((item: string, index: number) => {
+      const temp = {
+        category: item,
+      } as {
+        category: string;
+        [key: string]: any;
+      };
+      series.forEach((s: DualAxesSeriesItem, i: number) => {
+        const defaultYField = s.axisYTitle || `value_${i + 1}`;
+        temp[defaultYField] = s.data[index];
+      });
+      return temp;
+    });
+
+    const legendTypeList = series.map((item: any) => {
+      return item.type === ChartType.Line ? 'smooth' : 'rect';
+    });
+
+    return {
+      children: newChildren,
+      data: newData,
+      legendTypeList,
+    };
+  }
+
+  const config = transform(series, categories);
+
+  return await createChart({
+    type: 'view',
+    autoFit: true,
+    title,
+    width,
+    height,
+    ...config,
+  });
+}
diff --git a/src/ssr/fishbone-diagram.ts b/src/ssr/fishbone-diagram.ts
new file mode 100644
index 00000000..d71c7214
--- /dev/null
+++ b/src/ssr/fishbone-diagram.ts
@@ -0,0 +1,191 @@
+import { BaseTransform, ExtensionCategory, treeToGraphData } from '@antv/g6';
+import { createGraph, register } from '@antv/g6-ssr';
+import type { CanvasRenderingContext2D } from 'canvas';
+import { createCanvas } from 'canvas';
+import { type FishboneDiagramProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type FishboneDiagramOptions = BaseChartConfig & {
+  type: 'fishbone-diagram';
+} & FishboneDiagramProps;
+
+let canvas: ReturnType<typeof createCanvas> | null = null;
+let ctx: CanvasRenderingContext2D | null = null;
+
+type TextStyle = {
+  text: string;
+  fontSize?: number;
+  fontFamily?: string;
+  fontWeight?: string;
+  fontStyle?: string;
+};
+
+const measureText = (style: TextStyle): number => {
+  if (!canvas) {
+    canvas = createCanvas(0, 0);
+    ctx = canvas.getContext('2d');
+  }
+
+  const font = [
+    style.fontStyle || 'normal',
+    style.fontWeight || 'normal',
+    `${style.fontSize || 12}px`,
+    style.fontFamily || 'sans-serif',
+  ].join(' ');
+
+  ctx!.font = font;
+  return ctx!.measureText(style.text).width;
+};
+
+class AssignColorByBranch extends BaseTransform {
+  static defaultOptions = {
+    colors: [
+      '#1783FF',
+      '#F08F56',
+      '#D580FF',
+      '#00C9C9',
+      '#7863FF',
+      '#DB9D0D',
+      '#60C42D',
+      '#FF80CA',
+      '#2491B3',
+      '#17C76F',
+    ],
+  };
+
+  constructor(context: any, options: any) {
+    super(context, Object.assign({}, AssignColorByBranch.defaultOptions, options));
+  }
+
+  beforeDraw(input: any) {
+    const nodes = this.context.model.getNodeData();
+
+    if (nodes.length === 0) return input;
+
+    let colorIndex = 0;
+    const dfs = (nodeId: string, color: any) => {
+      const node = nodes.find((datum) => datum.id == nodeId);
+      if (!node) return;
+
+      node.style ||= {};
+      node.style.color = color || this.options.colors[colorIndex++ % this.options.colors.length];
+      node.children?.forEach((childId) => dfs(childId, node.style?.color));
+    };
+    // @ts-ignore
+    nodes.filter((node) => node.depth === 1).forEach((rootNode) => dfs(rootNode.id));
+
+    return input;
+  }
+}
+
+register(ExtensionCategory.TRANSFORM, 'assign-color-by-branch', AssignColorByBranch);
+
+const getNodeSize = (id: any, depth: any) => {
+  const FONT_FAMILY = 'system-ui, sans-serif';
+  return depth === 0
+    ? [
+        measureText({ text: id, fontSize: 24, fontWeight: 'bold', fontFamily: FONT_FAMILY }) + 80,
+        70,
+      ]
+    : depth === 1
+      ? [measureText({ text: id, fontSize: 18, fontFamily: FONT_FAMILY }) + 50, 42]
+      : [2, 30];
+};
+
+function visTreeData2GraphData(data: any) {
+  return treeToGraphData(data, {
+    getNodeData: (datum: any, depth: any) => {
+      datum.id = datum.name;
+      datum.depth = depth;
+      if (!datum.children) return datum;
+      const { children, ...restDatum } = datum;
+      return {
+        ...restDatum,
+        children: children.map((child: any) => child.name),
+      };
+    },
+    getEdgeData: (source, target) => ({
+      source: source.name,
+      target: target.name,
+    }),
+  });
+}
+
+export async function FishboneDiagram(options: FishboneDiagramOptions) {
+  const { data, width = 600, height = 400 } = options;
+
+  const dataParse = visTreeData2GraphData(data);
+
+  return await createGraph({
+    autoFit: {
+      type: 'view',
+      options: {
+        when: 'overflow',
+        direction: 'x',
+      },
+    },
+    width,
+    height,
+    data: dataParse,
+    padding: 20,
+    node: {
+      type: 'rect',
+      // @ts-ignore
+      style: (d) => {
+        const style = {
+          radius: 8,
+          size: getNodeSize(d.id, d.depth),
+          labelText: d.id,
+          labelPlacement: 'left',
+          labelFontFamily: 'Gill Sans',
+        };
+
+        if (d.depth === 0) {
+          Object.assign(style, {
+            fill: '#EFF0F0',
+            labelFill: '#262626',
+            labelFontWeight: 'bold',
+            labelFontSize: 24,
+            labelOffsetY: 4,
+            labelPlacement: 'center',
+            labelLineHeight: 24,
+          });
+        } else if (d.depth === 1) {
+          Object.assign(style, {
+            labelFontSize: 18,
+            labelFill: '#fff',
+            labelFillOpacity: 0.9,
+            labelOffsetY: 5,
+            labelPlacement: 'center',
+            fill: d.style?.color,
+          });
+        } else {
+          Object.assign(style, {
+            fill: 'transparent',
+            labelFontSize: 16,
+            labeFill: '#262626',
+          });
+        }
+        return style;
+      },
+    },
+    edge: {
+      type: 'polyline',
+      style: {
+        lineWidth: 3,
+        // @ts-ignore
+        stroke: function (data) {
+          // @ts-ignore
+          return this.getNodeData(data.target).style.color || '#99ADD1';
+        },
+      },
+    },
+    transforms: ['assign-color-by-branch'],
+    layout: {
+      type: 'fishbone',
+      direction: 'RL',
+      hGap: 40,
+      vGap: 60,
+    },
+  });
+}
diff --git a/src/ssr/flow-diagram.ts b/src/ssr/flow-diagram.ts
new file mode 100644
index 00000000..ab56951e
--- /dev/null
+++ b/src/ssr/flow-diagram.ts
@@ -0,0 +1,61 @@
+import { createGraph } from '@antv/g6-ssr';
+import { type FlowDiagramProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type FlowDiagramOptions = BaseChartConfig & { type: 'flow-diagram' } & FlowDiagramProps;
+
+export async function FlowDiagram(options: FlowDiagramOptions) {
+  const { data, width = 600, height = 400 } = options;
+
+  const graphData = {
+    nodes: data.nodes.map((node) => ({ ...node, id: node.name })),
+    edges: data.edges.map((edge) => ({ ...edge, id: `${edge.source}-${edge.target}` })),
+  };
+
+  return await createGraph({
+    data: graphData,
+    width,
+    height,
+    devicePixelRatio: 3,
+    autoFit: 'view',
+    padding: 20,
+    animation: false,
+    node: {
+      type: 'rect',
+      style: {
+        size: (d: any) => [d.name.length * 15 + 30, 35],
+        radius: 6,
+        // @ts-ignore
+        iconText: (d) => d.name,
+        iconFontSize: 12,
+        iconFontWeight: 800,
+      },
+    },
+    edge: {
+      type: 'polyline',
+      style: {
+        lineWidth: 2,
+        radius: 10,
+        stroke: '#99ADD1',
+        endArrow: true,
+        // @ts-ignore
+        labelText: (d) => d.name,
+        labelFill: '#555555',
+        labelFontWeight: 800,
+        labelBackground: true,
+        labelBackgroundFill: 'rgba(255,255,255,0.6)',
+        labelBackgroundOpacity: 1,
+        labelBackgroundLineWidth: 2,
+        labelPadding: [2, 5],
+        labelBackgroundRadius: 2,
+        router: {
+          type: 'orth',
+        },
+      },
+    },
+    layout: {
+      type: 'dagre',
+      rankdir: 'LR',
+    },
+  });
+}
diff --git a/src/ssr/histogram.ts b/src/ssr/histogram.ts
new file mode 100644
index 00000000..6174cd84
--- /dev/null
+++ b/src/ssr/histogram.ts
@@ -0,0 +1,28 @@
+import { createChart } from '@antv/g2-ssr';
+import { type HistogramProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type HistogramOptions = BaseChartConfig & { type: 'histogram' } & HistogramProps;
+
+export async function Histogram(options: HistogramOptions) {
+  const { data, title, width, height, axisYTitle, axisXTitle, binNumber } = options;
+  // TODO 有问题
+  return await createChart({
+    type: 'interval',
+    width,
+    height,
+    data,
+    title,
+    encode: {
+      x: (d: any) => d,
+      y: 'count',
+    },
+    transform: [{ type: 'binX', y: 'count', thresholds: binNumber }],
+    style: { minHeight: 1, columnWidthRatio: 1, inset: 0.5 },
+    axis: {
+      x: { title: axisXTitle },
+      y: { title: axisYTitle },
+    },
+    animate: false,
+  });
+}
diff --git a/src/ssr/index.ts b/src/ssr/index.ts
new file mode 100644
index 00000000..8d05edf1
--- /dev/null
+++ b/src/ssr/index.ts
@@ -0,0 +1,53 @@
+import { Area, type AreaOptions } from './area';
+import { Bar, type BarOptions } from './bar';
+import { Column, type ColumnOptions } from './column';
+import { DualAxes, type DualAxespOptions } from './dual-axes';
+import { FishboneDiagram, type FishboneDiagramOptions } from './fishbone-diagram';
+import { FlowDiagram, type FlowDiagramOptions } from './flow-diagram';
+import { Histogram, type HistogramOptions } from './histogram';
+import { Line, type LineOptions } from './line';
+import { MindMap, type MindMapOptions } from './mind-map';
+import { NetworkGraph, type NetworkGraphOptions } from './network-graph';
+import { Pie, type PieOptions } from './pie';
+import { Radar, type RadarOptions } from './radar';
+import { Scatter, type ScatterOptions } from './scatter';
+import { Treemap, type TreemapOptions } from './treemap';
+import type { Options } from './type';
+import { WordCloud, type WordCloudOptions } from './word-cloud';
+
+export async function render(options: Options) {
+  switch (options.type) {
+    case 'line':
+      return Line(options as LineOptions);
+    case 'column':
+      return Column(options as ColumnOptions);
+    case 'pie':
+      return Pie(options as PieOptions);
+    case 'area':
+      return Area(options as AreaOptions);
+    case 'bar':
+      return Bar(options as BarOptions);
+    case 'histogram':
+      return Histogram(options as HistogramOptions);
+    case 'scatter':
+      return Scatter(options as ScatterOptions);
+    case 'word-cloud':
+      return WordCloud(options as WordCloudOptions);
+    case 'treemap':
+      return Treemap(options as TreemapOptions);
+    case 'dual-axes':
+      return DualAxes(options as DualAxespOptions);
+    case 'radar':
+      return Radar(options as RadarOptions);
+    case 'mind-map':
+      return MindMap(options as MindMapOptions);
+    case 'network-graph':
+      return NetworkGraph(options as NetworkGraphOptions);
+    case 'flow-diagram':
+      return FlowDiagram(options as FlowDiagramOptions);
+    case 'fishbone-diagram':
+      return FishboneDiagram(options as FishboneDiagramOptions);
+    default:
+      throw new Error(`Unknown chart type: ${options.type}`);
+  }
+}
diff --git a/src/ssr/line.ts b/src/ssr/line.ts
new file mode 100644
index 00000000..3a9002bb
--- /dev/null
+++ b/src/ssr/line.ts
@@ -0,0 +1,40 @@
+import { createChart } from '@antv/g2-ssr';
+import { type LineProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type LineOptions = BaseChartConfig & { type: 'line' } & LineProps;
+
+export async function Line(options: LineOptions) {
+  const { data, title, width = 600, height = 400, axisYTitle, axisXTitle } = options;
+  const hasGroupField = (data || [])[0]?.group !== undefined;
+
+  let encode = {};
+  if (hasGroupField) {
+    encode = { x: 'time', y: 'value', color: 'group' };
+  } else {
+    encode = { x: 'time', y: 'value' };
+  }
+
+  return await createChart({
+    type: 'view',
+    title,
+    data,
+    width,
+    height,
+    encode: encode,
+    style: { minHeight: 1 },
+    axis: {
+      y: {
+        title: axisYTitle || false,
+      },
+      x: {
+        title: axisXTitle || false,
+      },
+    },
+    children: [
+      {
+        type: 'line',
+      },
+    ],
+  });
+}
diff --git a/src/ssr/mind-map.ts b/src/ssr/mind-map.ts
new file mode 100644
index 00000000..c74d6241
--- /dev/null
+++ b/src/ssr/mind-map.ts
@@ -0,0 +1,348 @@
+import { Rect } from '@antv/g';
+import { createGraph, G6 } from '@antv/g6-ssr';
+import type { CanvasRenderingContext2D } from 'canvas';
+import { createCanvas } from 'canvas';
+import { type MindMapProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type MindMapOptions = BaseChartConfig & { type: 'mind-map' } & MindMapProps;
+
+const { register, BaseNode, BaseTransform, ExtensionCategory, idOf, positionOf, treeToGraphData } =
+  G6;
+
+let canvas: ReturnType<typeof createCanvas> | null = null;
+let ctx: CanvasRenderingContext2D | null = null;
+
+type TextStyle = {
+  text: string;
+  fontSize?: number;
+  fontFamily?: string;
+  fontWeight?: string;
+  fontStyle?: string;
+};
+
+const measureText = (style: TextStyle): number => {
+  if (!canvas) {
+    canvas = createCanvas(0, 0);
+    ctx = canvas.getContext('2d');
+  }
+
+  const font = [
+    style.fontStyle || 'normal',
+    style.fontWeight || 'normal',
+    `${style.fontSize || 12}px`,
+    style.fontFamily || 'sans-serif',
+  ].join(' ');
+
+  ctx!.font = font;
+  return ctx!.measureText(style.text).width;
+};
+
+const RootNodeStyle = {
+  fill: '#EFF0F0',
+  labelFill: '#262626',
+  labelFontSize: 24,
+  labelFontWeight: 600,
+  labelOffsetY: 8,
+  labelPlacement: 'center',
+  ports: [{ placement: 'right' }, { placement: 'left' }],
+  radius: 8,
+};
+
+const NodeStyle = {
+  fill: 'transparent',
+  labelPlacement: 'center',
+  labelFontSize: 16,
+  ports: [{ placement: 'right-bottom' }, { placement: 'left-bottom' }],
+};
+
+const getNodeWidth = (nodeId: any, isRoot: boolean) => {
+  const padding = isRoot ? 40 : 30;
+  const nodeStyle = isRoot ? RootNodeStyle : NodeStyle;
+  return (
+    measureText({ text: nodeId, fontSize: nodeStyle.labelFontSize, fontFamily: 'Gill Sans' }) +
+    padding
+  );
+};
+
+const getNodeSize = (nodeId: string, isRoot: boolean) => {
+  const width = getNodeWidth(nodeId, isRoot);
+  const height = isRoot ? 36 : 32;
+  return [width, height] as const;
+};
+
+class MindmapNode extends BaseNode {
+  static defaultStyleProps: object = {
+    ...BaseNode.defaultStyleProps,
+    showIcon: false,
+  };
+
+  get childrenData() {
+    return this.context.model.getChildrenData(this.id);
+  }
+
+  get rootId() {
+    return idOf(this.context.model.getRootsData()[0]);
+  }
+
+  isShowCollapse(attributes: { collapsed: any; showIcon: any }) {
+    const { collapsed, showIcon } = attributes;
+    return !collapsed && showIcon && this.childrenData.length > 0;
+  }
+
+  getCollapseStyle(attributes: any) {
+    const { showIcon, color, direction } = attributes;
+    if (!this.isShowCollapse(attributes)) return false;
+    const [width, height] = this.getSize(attributes);
+
+    return {
+      backgroundFill: color,
+      backgroundHeight: 12,
+      backgroundWidth: 12,
+      cursor: 'pointer',
+      fill: '#fff',
+      fontFamily: 'iconfont',
+      fontSize: 8,
+      text: '\ue6e4',
+      textAlign: 'center',
+      transform: direction === 'left' ? [['rotate', 90]] : [['rotate', -90]],
+      visibility: showIcon ? 'visible' : 'hidden',
+      x: direction === 'left' ? -6 : width + 6,
+      y: height,
+    };
+  }
+
+  drawCollapseShape() {}
+
+  getCountStyle(attributes: any) {
+    const { collapsed, color, direction } = attributes;
+    const count = this.context.model.getDescendantsData(this.id).length;
+    if (!collapsed || count === 0) return false;
+    const [width, height] = this.getSize(attributes);
+    return {
+      backgroundFill: color,
+      backgroundHeight: 12,
+      backgroundWidth: 12,
+      cursor: 'pointer',
+      fill: '#fff',
+      fontSize: 8,
+      text: count.toString(),
+      textAlign: 'center',
+      x: direction === 'left' ? -6 : width + 6,
+      y: height,
+    };
+  }
+
+  drawCountShape() {}
+
+  getAddStyle(attributes: any) {
+    const { collapsed, showIcon, direction } = attributes;
+    if (collapsed || !showIcon) return false;
+    const [width, height] = this.getSize(attributes);
+    const color = '#ddd';
+
+    const offsetX = this.isShowCollapse(attributes) ? 24 : 12;
+    const isRoot = this.id === this.rootId;
+
+    return {
+      backgroundFill: '#fff',
+      backgroundHeight: 12,
+      backgroundLineWidth: 1,
+      backgroundStroke: color,
+      backgroundWidth: 12,
+      cursor: 'pointer',
+      fill: color,
+      fontFamily: 'iconfont',
+      fontSize: 8,
+      text: '\ue664',
+      textAlign: 'center',
+      x: isRoot ? width + 12 : direction === 'left' ? -offsetX : width + offsetX,
+      y: isRoot ? height / 2 : height,
+    };
+  }
+
+  getAddBarStyle(attributes: any) {
+    const { collapsed, showIcon, direction, color = '#1783FF' } = attributes;
+    if (collapsed || !showIcon) return false;
+    const [width, height] = this.getSize(attributes);
+
+    const offsetX = this.isShowCollapse(attributes) ? 12 : 0;
+    const isRoot = this.id === this.rootId;
+
+    const HEIGHT = 2;
+    const WIDTH = 6;
+
+    return {
+      cursor: 'pointer',
+      fill:
+        direction === 'left'
+          ? `linear-gradient(180deg, #fff 20%, ${color})`
+          : `linear-gradient(0deg, #fff 20%, ${color})`,
+      height: HEIGHT,
+      width: WIDTH,
+      x: isRoot ? width : direction === 'left' ? -offsetX - WIDTH : width + offsetX,
+      y: isRoot ? height / 2 - HEIGHT / 2 : height - HEIGHT / 2,
+      zIndex: -1,
+    };
+  }
+
+  drawAddShape() {}
+
+  forwardEvent() {}
+
+  getKeyStyle(attributes: any) {
+    const [width, height] = this.getSize(attributes);
+    const keyShape = super.getKeyStyle(attributes);
+    return { width, height, ...keyShape };
+  }
+
+  drawKeyShape(attributes: any, container: any) {
+    const keyStyle = this.getKeyStyle(attributes);
+    // @ts-ignore
+    return this.upsert('key', Rect, keyStyle, container);
+  }
+
+  render(attributes = this.parsedAttributes, container = this) {
+    super.render(attributes, container);
+  }
+}
+
+class AssignColorByBranch extends BaseTransform {
+  static defaultOptions = {
+    colors: [
+      '#1783FF',
+      '#F08F56',
+      '#D580FF',
+      '#00C9C9',
+      '#7863FF',
+      '#DB9D0D',
+      '#60C42D',
+      '#FF80CA',
+      '#2491B3',
+      '#17C76F',
+    ],
+  };
+
+  constructor(context: G6.RuntimeContext, options: any) {
+    super(context, Object.assign({}, AssignColorByBranch.defaultOptions, options));
+  }
+
+  beforeDraw(input: any) {
+    const nodes = this.context.model.getNodeData();
+
+    if (nodes.length === 0) return input;
+
+    let colorIndex = 0;
+    const dfs = (nodeId: string, color: any) => {
+      const node = nodes.find((datum) => datum.id == nodeId);
+      if (!node) return;
+
+      node.style ||= {};
+      node.style.color = color || this.options.colors[colorIndex++ % this.options.colors.length];
+      node.children?.forEach((childId) => dfs(childId, node.style?.color));
+    };
+    // @ts-ignore
+    nodes.filter((node) => node.depth === 1).forEach((rootNode) => dfs(rootNode.id));
+
+    return input;
+  }
+}
+
+register(ExtensionCategory.NODE, 'mindmap', MindmapNode);
+register(ExtensionCategory.TRANSFORM, 'assign-color-by-branch', AssignColorByBranch);
+
+const getNodeSide = (nodeData: any, parentData: any) => {
+  if (!parentData) return 'center';
+
+  const nodePositionX = positionOf(nodeData)[0];
+  const parentPositionX = positionOf(parentData)[0];
+  return parentPositionX > nodePositionX ? 'left' : 'right';
+};
+
+function visTreeData2GraphData(data: any) {
+  return treeToGraphData(data, {
+    getNodeData: (datum: any, depth: any) => {
+      datum.id = datum.name;
+      datum.depth = depth;
+      if (!datum.children) return datum;
+      const { children, ...restDatum } = datum;
+      return {
+        ...restDatum,
+        children: children.map((child: any) => child.name),
+      };
+    },
+    getEdgeData: (source, target) => ({
+      source: source.name,
+      target: target.name,
+    }),
+  });
+}
+
+export async function MindMap(options: MindMapOptions) {
+  const { data, width, height } = options;
+  const dataParse = visTreeData2GraphData(data);
+
+  const rootId = data.name;
+
+  return await createGraph({
+    waitForRender: 300,
+    width,
+    height,
+    data: dataParse,
+    padding: 24,
+    autoFit: {
+      type: 'view',
+    },
+    node: {
+      type: 'mindmap',
+      // @ts-ignore
+      style: function (d) {
+        const direction = getNodeSide(d, this.getParentData(idOf(d), 'tree'));
+        const isRoot = idOf(d) === rootId;
+        const depth = d.depth;
+
+        return {
+          direction,
+          labelText: idOf(d),
+          size: getNodeSize(idOf(d), isRoot),
+          labelFontFamily: 'Gill Sans',
+          lineWidth: 2,
+          radius: 8,
+          stroke: depth === 0 ? '#f1f4f5' : d.style?.color,
+          labelBackground: true,
+          labelBackgroundFill: 'transparent',
+          labelPadding: direction === 'left' ? [2, 0, 10, 40] : [2, 40, 10, 0],
+          ...(isRoot ? RootNodeStyle : NodeStyle),
+          fill: depth === 0 ? '#f1f4f5' : depth === 1 ? d.style?.color : 'transparent',
+          labelFill: depth === 0 ? '#262626' : depth === 1 ? '#FFF' : d.style?.color,
+          ports: [{ placement: 'left' }, { placement: 'right' }],
+        };
+      },
+    },
+    edge: {
+      type: 'cubic-horizontal',
+      style: {
+        lineWidth: 1.5,
+        // @ts-ignore
+        stroke: function (data: any) {
+          // @ts-ignore
+          return this.getNodeData(data.target).depth > 1
+            ? // @ts-ignore
+              this.getNodeData(data.target).style.color
+            : '#99ADD1';
+        },
+      },
+    },
+    layout: {
+      type: 'mindmap',
+      direction: 'H',
+      getHeight: () => 30,
+      getWidth: (node: any) => getNodeWidth(node.id, node.id === rootId),
+      getVGap: () => 14,
+      getHGap: () => 64,
+      animation: false,
+    },
+    transforms: ['assign-color-by-branch'],
+    animation: false,
+  });
+}
diff --git a/src/ssr/network-graph.ts b/src/ssr/network-graph.ts
new file mode 100644
index 00000000..f20815c1
--- /dev/null
+++ b/src/ssr/network-graph.ts
@@ -0,0 +1,51 @@
+import { createGraph } from '@antv/g6-ssr';
+import { type NetworkGraphProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type NetworkGraphOptions = BaseChartConfig & { type: 'network-graph' } & NetworkGraphProps;
+
+export async function NetworkGraph(options: NetworkGraphOptions) {
+  const { data, width = 600, height = 400 } = options;
+
+  const graphData = {
+    nodes: data.nodes.map((node) => ({ ...node, id: node.name })),
+    edges: data.edges.map((edge) => ({ ...edge, id: `${edge.source}-${edge.target}` })),
+  };
+
+  return await createGraph({
+    data: graphData,
+    width: width,
+    height: height,
+    devicePixelRatio: 3,
+    autoFit: 'view',
+    padding: 20,
+    animation: false,
+    node: {
+      type: 'circle',
+      style: {
+        size: 20,
+        // @ts-ignore
+        labelText: (d) => d.name,
+        labelSize: 10,
+        labelFontSize: 10,
+        labelBackground: true,
+      },
+    },
+    edge: {
+      style: {
+        // @ts-ignore
+        labelText: (d) => d.name,
+        labelFontSize: 10,
+        labelBackground: true,
+        endArrow: true,
+      },
+      animation: { enter: false },
+    },
+    layout: {
+      type: 'force-atlas2',
+      preventOverlap: true,
+      kr: 600,
+    },
+    transforms: ['process-parallel-edges'],
+  });
+}
diff --git a/src/ssr/pie.ts b/src/ssr/pie.ts
new file mode 100644
index 00000000..8d8858a3
--- /dev/null
+++ b/src/ssr/pie.ts
@@ -0,0 +1,42 @@
+import { createChart } from '@antv/g2-ssr';
+import { type PieProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type PieOptions = BaseChartConfig & { type: 'pie' } & PieProps;
+
+export async function Pie(options: PieOptions) {
+  const { data, title, width, height, innerRadius } = options;
+
+  return await createChart({
+    type: 'interval',
+    title,
+    width,
+    height,
+    data,
+    encode: { y: 'value', color: 'category' },
+    transform: [{ type: 'stackY' }],
+    coordinate: {
+      type: 'theta',
+      outerRadius: 0.95,
+      innerRadius,
+    },
+    style: {
+      radius: 4,
+      stroke: '#fff',
+      lineWidth: 1,
+    },
+    labels: [
+      {
+        text: (data: any) => `${data.category}: ${data.value}`,
+        position: 'outside',
+        radius: 0.85,
+        fontSize: 12,
+      },
+    ],
+    legend: {
+      color: { position: 'bottom', layout: { justifyContent: 'center' } },
+    },
+    animate: false,
+    axis: false,
+  });
+}
diff --git a/src/ssr/radar.ts b/src/ssr/radar.ts
new file mode 100644
index 00000000..99102766
--- /dev/null
+++ b/src/ssr/radar.ts
@@ -0,0 +1,70 @@
+import { createChart } from '@antv/g2-ssr';
+import { type RadarProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type RadarOptions = BaseChartConfig & { type: 'radar' } & RadarProps;
+
+export async function Radar(options: RadarOptions) {
+  const { data, title, width, height } = options;
+  const hasGroupField = (data || [])[0]?.group !== undefined;
+
+  let encode = {};
+  if (hasGroupField) {
+    encode = { x: 'name', y: 'value', color: 'group' };
+  } else {
+    encode = { x: 'name', y: 'value' };
+  }
+
+  return await createChart({
+    title,
+    type: 'view',
+    width,
+    data,
+    height,
+    children: [
+      {
+        axis: {
+          x: {
+            grid: true,
+            line: true,
+          },
+          y: {
+            zIndex: 1,
+            title: false,
+            line: true,
+            nice: true,
+          },
+        },
+        // @ts-ignore
+        coordinateType: 'polar',
+        scale: {
+          x: {
+            padding: 0.5,
+            align: 0,
+          },
+          y: {
+            nice: true,
+          },
+        },
+        coordinate: {
+          type: 'polar',
+        },
+        encode: encode,
+        type: 'line',
+      },
+      {
+        style: {
+          fillOpacity: 0.5,
+        },
+        encode: encode,
+        scale: {
+          x: {
+            padding: 0.5,
+            align: 0,
+          },
+        },
+        type: 'area',
+      },
+    ],
+  });
+}
diff --git a/src/ssr/scatter.ts b/src/ssr/scatter.ts
new file mode 100644
index 00000000..470d480c
--- /dev/null
+++ b/src/ssr/scatter.ts
@@ -0,0 +1,33 @@
+import { createChart } from '@antv/g2-ssr';
+import { type ScatterProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type ScatterOptions = BaseChartConfig & { type: 'scatter' } & ScatterProps;
+
+export async function Scatter(options: ScatterOptions) {
+  const { data, title, width, height, axisYTitle, axisXTitle } = options;
+  return await createChart({
+    type: 'point',
+    data,
+    width,
+    height,
+    title,
+    encode: {
+      x: 'x',
+      y: 'y',
+      // shape: 'point',
+    },
+    axis: {
+      x: {
+        title: axisXTitle,
+      },
+      y: {
+        title: axisYTitle,
+      },
+    },
+    style: { lineWidth: 1 },
+    legend: { size: false },
+    animate: false,
+    tooltip: false,
+  });
+}
diff --git a/src/ssr/treemap.ts b/src/ssr/treemap.ts
new file mode 100644
index 00000000..94b534f3
--- /dev/null
+++ b/src/ssr/treemap.ts
@@ -0,0 +1,34 @@
+import { createChart } from '@antv/g2-ssr';
+import { type TreemapProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type TreemapOptions = BaseChartConfig & { type: 'treemap' } & TreemapProps;
+
+export async function Treemap(options: TreemapOptions) {
+  const { data, title, width, height } = options;
+  return await createChart({
+    type: 'treemap',
+    width,
+    height,
+    title,
+    data: {
+      type: 'inline',
+      value: {
+        name: 'root',
+        children: data,
+      },
+    },
+    layout: {
+      tile: 'treemapBinary',
+      paddingInner: 2,
+    },
+    encode: { value: 'value' },
+    style: {
+      fillOpacity: 0.8,
+      labelFontSize: 10,
+    },
+    tooltip: false,
+    legend: false,
+    animate: false,
+  });
+}
diff --git a/src/ssr/type.ts b/src/ssr/type.ts
new file mode 100644
index 00000000..11ddc8a1
--- /dev/null
+++ b/src/ssr/type.ts
@@ -0,0 +1,47 @@
+import {
+  type AreaProps,
+  type BarProps,
+  type ColumnProps,
+  type DualAxesProps,
+  type FishboneDiagramProps,
+  type FlowDiagramProps,
+  type HistogramProps,
+  type LineProps,
+  type MindMapProps,
+  type NetworkGraphProps,
+  type PieProps,
+  type RadarProps,
+  type ScatterProps,
+  type TreemapProps,
+  type WordCloudProps,
+} from '../export';
+
+type ChartConfigMap = {
+  line: LineProps;
+  column: ColumnProps;
+  pie: PieProps;
+  area: AreaProps;
+  bar: BarProps;
+  histogram: HistogramProps;
+  scatter: ScatterProps;
+  treemap: TreemapProps;
+  radar: RadarProps;
+  'dual-axes': DualAxesProps;
+  'word-cloud': WordCloudProps;
+  'mind-map': MindMapProps;
+  'network-graph': NetworkGraphProps;
+  'flow-diagram': FlowDiagramProps;
+  'fishbone-diagram': FishboneDiagramProps;
+};
+
+export type ChartType = keyof ChartConfigMap;
+
+export interface BaseChartConfig {
+  type: ChartType;
+  width: number;
+  height: number;
+}
+
+export type Options = {
+  [K in ChartType]: BaseChartConfig & { type: K } & ChartConfigMap[K];
+}[ChartType];
diff --git a/src/ssr/word-cloud.ts b/src/ssr/word-cloud.ts
new file mode 100644
index 00000000..1b81f523
--- /dev/null
+++ b/src/ssr/word-cloud.ts
@@ -0,0 +1,25 @@
+import { createChart } from '@antv/g2-ssr';
+import { type WordCloudProps } from '../export';
+import { type BaseChartConfig } from './type';
+
+export type WordCloudOptions = BaseChartConfig & { type: 'word-cloud' } & WordCloudProps;
+
+export async function WordCloud(options: WordCloudOptions) {
+  const { data, title, width, height } = options;
+  return await createChart({
+    type: 'wordCloud',
+    layout: {
+      fontSize: [8, 42],
+    },
+    title,
+    data,
+    width,
+    height,
+    encode: {
+      text: 'text',
+      color: 'text',
+      value: 'value',
+    },
+    legend: false,
+  });
+}


Reviews:
user: login: lvisei
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T12:57:12Z
user: login: lvisei
body: 还缺一个 README.md 文件
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T12:58:51Z
user: login: lvisei
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T13:00:23Z
user: login: hustcc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T02:17:24Z
user: login: hustcc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T12:48:25Z
user: login: hustcc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T12:49:33Z
user: login: hustcc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T12:51:48Z
user: login: lvisei
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T13:10:40Z
user: login: lvisei
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T13:14:18Z
user: login: hustcc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T12:48:20Z
user: login: lvisei
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T12:49:01Z
user: login: hustcc
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T13:14:14Z

Review comments:
diff_hunk: @@ -0,0 +1,31 @@
+import { join } from 'path';
+import { generateChart } from '../src';
+
+describe('Math Utilities', () => {
path: bindings/gpt-vis-ssr/__test__/index.test.ts
user: login: lvisei
body: Math Utilities 名字不对？
created_at: 2025-04-01T12:54:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,31 @@
+import { join } from 'path';
+import { generateChart } from '../src';
+
+describe('Math Utilities', () => {
path: bindings/gpt-vis-ssr/__test__/index.test.ts
user: login: lvisei
body: 这个测试主要干啥的，目前只是本地验证吗，就不需要提交 assets 文件。用于单元测试的话，需要加上 CI。
created_at: 2025-04-01T12:55:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,35 @@
+{
+  "name": "@antv/gpt-vis-ssr",
+  "version": "0.0.1",
+  "main": "dist/index.js",
+  "scripts": {
+    "build": "tsc",
+    "start": "node dist/index.js",
+    "dev": "ts-node-dev src/index.ts",
+    "test": "jest",
+    "test:watch": "jest --watchAll",
+    "test:coverage": "jest --coverage"
+  },
+  "keywords": [
+    "antv",
+    "GPTs",
+    "ssr"
+  ],
+  "author": "antvis",
+  "license": "ISC",
+  "description": "",
path: bindings/gpt-vis-ssr/package.json
user: login: lvisei
body: 加一段吧
created_at: 2025-04-01T12:56:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,35 @@
+{
+  "name": "@antv/gpt-vis-ssr",
+  "version": "0.0.1",
+  "main": "dist/index.js",
+  "scripts": {
+    "build": "tsc",
+    "start": "node dist/index.js",
+    "dev": "ts-node-dev src/index.ts",
+    "test": "jest",
+    "test:watch": "jest --watchAll",
+    "test:coverage": "jest --coverage"
+  },
+  "keywords": [
+    "antv",
+    "GPTs",
+    "ssr"
+  ],
+  "author": "antvis",
+  "license": "ISC",
path: bindings/gpt-vis-ssr/package.json
user: login: lvisei
body: MIT
created_at: 2025-04-01T12:56:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,35 @@
+{
+  "name": "@antv/gpt-vis-ssr",
+  "version": "0.0.1",
+  "main": "dist/index.js",
+  "scripts": {
+    "build": "tsc",
+    "start": "node dist/index.js",
+    "dev": "ts-node-dev src/index.ts",
+    "test": "jest",
+    "test:watch": "jest --watchAll",
+    "test:coverage": "jest --coverage"
+  },
+  "keywords": [
+    "antv",
+    "GPTs",
+    "ssr"
+  ],
+  "author": "antvis",
path: bindings/gpt-vis-ssr/package.json
user: login: lvisei
body: 发包还需要这个配置

```json
"author": "antvis",
"files": [
    "dist",
    "README.md"
  ],
```
created_at: 2025-04-01T13:00:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,460 @@
+// @ts-nocheck
path: bindings/gpt-vis-ssr/src/index.ts
user: login: hustcc
body: 拆成多个文件组织，变成一个可用的包。
created_at: 2025-04-09T02:17:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,40 @@
+import { createChart } from '@antv/g2-ssr';
path: src/ssr/Area/index.ts
user: login: hustcc
body: 每个组件一个文件吧，简单一些，就不加目录了。
created_at: 2025-04-22T12:48:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,40 @@
+import { createChart } from '@antv/g2-ssr';
path: src/ssr/Area/index.ts
user: login: hustcc
body: 文件名小写，和 type 名字一致。
created_at: 2025-04-22T12:49:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,65 @@
+import {
+  type AreaProps,
+  type BarProps,
+  type ColumnProps,
+  type DualAxesProps,
+  type FishboneDiagramProps,
+  type FlowDiagramProps,
+  type HistogramProps,
+  type LineProps,
+  type MindMapProps,
+  type NetworkGraphProps,
+  type PieProps,
+  type RadarProps,
+  type ScatterProps,
+  type TreemapProps,
+  type WordCloudProps,
+} from '../export';
+
+type ChartConfigMap = {
+  line: LineProps;
+  column: ColumnProps;
+  pie: PieProps;
+  area: AreaProps;
+  bar: BarProps;
+  histogram: HistogramProps;
+  scatter: ScatterProps;
+  treemap: TreemapProps;
+  radar: RadarProps;
+  'dual-axes': DualAxesProps;
+  'word-cloud': WordCloudProps;
+  'mind-map': MindMapProps;
+  'network-graph': NetworkGraphProps;
+  'flow-diagram': FlowDiagramProps;
+  'fishbone-diagram': FishboneDiagramProps;
+};
+
+export type ChartType = keyof ChartConfigMap;
+
+export interface BaseChartConfig {
+  type: ChartType;
+  width: number;
+  height: number;
+}
+
+export type Options = {
+  [K in ChartType]: BaseChartConfig & { type: K } & ChartConfigMap[K];
+}[ChartType];
+
+export type LineOptions = BaseChartConfig & { type: 'line' } & LineProps;
+export type ColumnOptions = BaseChartConfig & { type: 'column' } & ColumnProps;
+export type PieOptions = BaseChartConfig & { type: 'pie' } & PieProps;
+export type AreaOptions = BaseChartConfig & { type: 'area' } & AreaProps;
+export type BarOptions = BaseChartConfig & { type: 'bar' } & BarProps;
+export type HistogramOptions = BaseChartConfig & { type: 'histogram' } & HistogramProps;
+export type ScatterOptions = BaseChartConfig & { type: 'scatter' } & ScatterProps;
+export type TreemapOptions = BaseChartConfig & { type: 'treemap' } & TreemapProps;
+export type RadarOptions = BaseChartConfig & { type: 'radar' } & RadarProps;
+export type DualAxespOptions = BaseChartConfig & { type: 'dual-axes' } & DualAxesProps;
+export type WordCloudOptions = BaseChartConfig & { type: 'word-cloud' } & WordCloudProps;
+export type MindMapOptions = BaseChartConfig & { type: 'mind-map' } & MindMapProps;
+export type NetworkGraphOptions = BaseChartConfig & { type: 'network-graph' } & NetworkGraphProps;
+export type FlowDiagramOptions = BaseChartConfig & { type: 'flow-diagram' } & FlowDiagramProps;
path: src/ssr/type.ts
user: login: hustcc
body: 这个要么直接都写到每一个组件的文件中去？
created_at: 2025-04-22T12:51:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,134 @@
+interface BaseChartConfig {
path: bindings/gpt-vis-ssr/src/types.ts
user: login: lvisei
body: 这个文件要删掉吧
created_at: 2025-04-22T13:03:57Z
author_association: MEMBER
diff_hunk: @@ -23,6 +23,13 @@
   "typings": "dist/esm/index.d.ts",
   "jsdelivr": "dist/umd/index.min.js",
   "unpkg": "dist/umd/index.min.js",
+  "exports": {
+    "ssr": {
+      "types": "./lib/ssr/index.d.ts",
+      "import": "./esm/ssr/index.js",
+      "require": "./lib/ssr/index.js"
+    }
+  },
path: package.json
user: login: lvisei
body: 这样写了之前的导出方式也需要加上了，参考 G2

![image](https://github.com/user-attachments/assets/c3089a83-0dc3-450d-aac2-655cb233c0a3)

created_at: 2025-04-22T13:09:59Z
author_association: MEMBER
diff_hunk: @@ -53,53 +60,58 @@
     "publish-package": "pnpm build && changeset publish"
   },
   "dependencies": {
-    "@ant-design/graphs": "^2.0.2",
-    "@ant-design/icons": "^5.5.2",
-    "@ant-design/plots": "^2.3.2",
+    "@ant-design/graphs": "^2.0.5",
+    "@ant-design/icons": "^5.6.1",
+    "@ant-design/plots": "^2.3.3",
     "@antv/event-emitter": "^0.1.3",
-    "@antv/l7": "^2.22.3",
+    "@antv/g": "^6.1.23",
+    "@antv/g2-ssr": "^0.0.8",
+    "@antv/g6": "^5.0.45",
+    "@antv/g6-ssr": "^0.0.16",
+    "@antv/l7": "^2.22.5",
     "@antv/larkmap": "^1.5.1",
-    "@babel/runtime": "^7.26.0",
+    "@babel/runtime": "^7.27.0",
+    "canvas": "^3.1.0",
path: package.json
user: login: lvisei
body: canvas 放 peerDependencies 里面吧，只用 GPT-Vis 不需要这个依赖，而且很难装。用 SSR 的文档上需要单独装 canvas 这个包
created_at: 2025-04-22T13:14:14Z
author_association: MEMBER
diff_hunk: @@ -23,6 +23,27 @@
   "typings": "dist/esm/index.d.ts",
   "jsdelivr": "dist/umd/index.min.js",
   "unpkg": "dist/umd/index.min.js",
+  "exports": {
+    ".": {
+      "types": "./lib/index.d.ts",
+      "import": "./esm/index.js",
+      "require": "./lib/index.js"
+    },
+    "./lib/*": {
path: package.json
user: login: lvisei
body: 咋们的产物在 dist 下面，需要改下路径

![image](https://github.com/user-attachments/assets/d12a356f-6d9d-4273-9c6d-7bd785a44e7e)

created_at: 2025-04-23T12:47:50Z
author_association: MEMBER
diff_hunk: @@ -53,56 +74,61 @@
     "publish-package": "pnpm build && changeset publish"
   },
   "dependencies": {
-    "@ant-design/graphs": "^2.0.2",
-    "@ant-design/icons": "^5.5.2",
-    "@ant-design/plots": "^2.3.2",
+    "@ant-design/graphs": "^2.0.5",
+    "@ant-design/icons": "^5.6.1",
+    "@ant-design/plots": "^2.3.3",
     "@antv/event-emitter": "^0.1.3",
-    "@antv/l7": "^2.22.3",
+    "@antv/g": "^6.1.23",
+    "@antv/g2-ssr": "^0.0.8",
+    "@antv/g6": "^5.0.45",
+    "@antv/g6-ssr": "^0.0.16",
+    "@antv/l7": "^2.22.5",
     "@antv/larkmap": "^1.5.1",
-    "@babel/runtime": "^7.26.0",
+    "@babel/runtime": "^7.27.0",
     "lodash": "^4.17.21",
-    "react-markdown": "^9.0.1",
+    "react-markdown": "^9.1.0",
     "rehype-raw": "^7.0.0",
-    "remark-gfm": "^4.0.0",
-    "styled-components": "^6.1.13"
+    "remark-gfm": "^4.0.1",
+    "styled-components": "^6.1.17"
   },
   "devDependencies": {
-    "@ant-design/x": "^1.0.2",
-    "@changesets/changelog-github": "^0.5.0",
-    "@changesets/cli": "^2.27.10",
-    "@commitlint/cli": "^19.6.0",
-    "@commitlint/config-conventional": "^19.6.0",
+    "@ant-design/x": "^1.1.0",
+    "@changesets/changelog-github": "^0.5.1",
+    "@changesets/cli": "^2.28.1",
+    "@commitlint/cli": "^19.8.0",
+    "@commitlint/config-conventional": "^19.8.0",
     "@types/jest": "^29.5.14",
-    "@types/lodash": "^4.17.13",
-    "@types/react": "^18.3.16",
+    "@types/lodash": "^4.17.16",
+    "@types/react": "^18.3.20",
     "@types/webpack-bundle-analyzer": "^4.7.0",
-    "antd": "^5.22.4",
-    "dumi": "^2.4.16",
-    "eslint": "^9.16.0",
+    "antd": "^5.24.6",
+    "dumi": "^2.4.20",
+    "eslint": "^9.24.0",
     "eslint-config-prettier": "^9.1.0",
-    "eslint-plugin-jest": "^28.9.0",
+    "eslint-plugin-jest": "^28.11.0",
     "eslint-plugin-jsx-a11y": "^6.10.2",
-    "eslint-plugin-react": "^7.37.2",
-    "eslint-plugin-react-hooks": "^5.1.0",
-    "father": "^4.5.1",
-    "gh-pages": "^6.2.0",
-    "globals": "^15.13.0",
+    "eslint-plugin-react": "^7.37.5",
+    "eslint-plugin-react-hooks": "^5.2.0",
+    "father": "^4.5.2",
+    "gh-pages": "^6.3.0",
+    "globals": "^15.15.0",
     "husky": "^9.1.7",
     "jest": "^29.7.0",
     "jest-environment-jsdom": "^29.7.0",
     "limit-size": "^0.1.4",
-    "lint-staged": "^15.2.11",
+    "lint-staged": "^15.5.0",
     "npm-run-all": "^4.1.5",
-    "prettier": "^3.4.2",
+    "prettier": "^3.5.3",
     "prettier-plugin-organize-imports": "^4.1.0",
     "react": "^18.3.1",
-    "ts-jest": "^29.2.5",
+    "ts-jest": "^29.3.1",
     "ts-node": "^10.9.2",
-    "typescript": "^5.7.2",
-    "typescript-eslint": "^8.18.0",
+    "typescript": "^5.8.3",
+    "typescript-eslint": "^8.29.1",
path: package.json
user: login: hustcc
body: canvas 改到 peer 之后，也需要加到 dev 中。
created_at: 2025-04-23T12:48:20Z
author_association: MEMBER
diff_hunk: @@ -80,3 +80,7 @@ dist
 docs-dist
 server
 .dumi
+
+# test
+assets
+.png
path: .gitignore
user: login: lvisei
body: 忽略到 assets 下的 png 吧
assets/*.png
created_at: 2025-04-23T12:48:57Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-01T12:13:45Z
body: ###  🦋  Changeset detected

Latest commit: 619b77351c36148c15d0f0e5fa577ca58e3485ed

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name          | Type  |
| ------------- | ----- |
| @antv/gpt-vis | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/antvis/GPT-Vis/new/add_chart_com?filename=.changeset/sour-beers-judge.md&value=---%0A%22%40antv%2Fgpt-vis%22%3A%20patch%0A---%0A%0Afeat%3A%20add%20gpt-vis%20SSR%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-01T12:14:21Z
body: 
🎊 PR Preview 619b77351c36148c15d0f0e5fa577ca58e3485ed has been successfully built and deployed to https://antvis-GPT-Vis-preview-pr-83.surge.sh

🕐 Build time: **15.225s**

<a href="https://github.com/antvis/GPT-Vis/actions/runs/14619046911"><img width="300" src="https://user-images.githubusercontent.com/507615/90250366-88233900-de6e-11ea-95a5-84f0762ffd39.png"></a>

<sub>🤖 By [surge-preview](https://github.com/afc163/surge-preview)</sub>
    
<!-- Sticky Pull Request Comment: Surge Preview preview' -->
author_association: CONTRIBUTOR
user: login: hustcc
created_at: 2025-04-01T14:21:06Z
body: 是不是可以多文件组织代码？然后在 oneclip 中用这个包即可？
author_association: MEMBER
user: login: hustcc
created_at: 2025-04-09T02:27:30Z
body: 我在描述中增加加了 API 设计，可以按照这个来。
author_association: MEMBER