diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5395ca2ca46..04c142eb9df 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,6 +18,13 @@ jobs:
with:
node-version: 18
+ - name: Install Dependencies
+ run: |
+ brew update
+ brew install python3 || : # python doesn't need to be linked
+ brew install pkg-config cairo pango libpng jpeg giflib librsvg
+ pip install setuptools
+
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
diff --git a/package.json b/package.json
index 778405918c8..1c5e92c063a 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"@commitlint/config-conventional": "^18.6.3",
"@playwright/test": "^1.48.2",
"@rollup/plugin-commonjs": "^25.0.8",
+ "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
diff --git a/packages/g6-ssr/README.md b/packages/g6-ssr/README.md
new file mode 100644
index 00000000000..c8b4e7687c9
--- /dev/null
+++ b/packages/g6-ssr/README.md
@@ -0,0 +1,63 @@
+## SSR extension for G6 5.0
+
+This extension package provides SSR support for G6 5.0, which supports canvas rendering in server side.
+
+## Usage
+
+### Install
+
+```bash
+npm install @antv/g6-ssr
+```
+
+### Render in JavaScript API
+
+> For complete options, please refer to [G6 Graph Options](https://g6.antv.antgroup.com/api/graph/option)
+
+```js
+import { createGraph } from '@antv/g6-ssr';
+
+const graph = await createGraph({
+ width: 500,
+ height: 500,
+ data: {
+ // data
+ },
+ // other options
+});
+
+graph.exportToFile('image');
+// -> image.png
+```
+
+### Render in CLI
+
+```bash
+npx g6-ssr export -i [graph-options].json -o ./image
+```
+
+### Export SVG / PDF
+
+When render in JavaScript API, you can pass `outputType` option to export SVG or PDF.
+
+```js
+const graph = await createGraph({
+ width: 500,
+ height: 500,
+ data: {
+ // data
+ },
+ outputType: 'svg', // or 'pdf'
+ // other options
+});
+```
+
+When render in CLI, you can pass `-t` or `--type` option to export SVG or PDF.
+
+```bash
+npx g6-ssr export -i [graph-options].json -o ./file -t pdf
+```
+
+## License
+
+MIT
diff --git a/packages/g6-ssr/__tests__/assets/file.pdf b/packages/g6-ssr/__tests__/assets/file.pdf
new file mode 100644
index 00000000000..64d3c2f1b19
Binary files /dev/null and b/packages/g6-ssr/__tests__/assets/file.pdf differ
diff --git a/packages/g6-ssr/__tests__/assets/file.svg b/packages/g6-ssr/__tests__/assets/file.svg
new file mode 100644
index 00000000000..67972aa3d4d
--- /dev/null
+++ b/packages/g6-ssr/__tests__/assets/file.svg
@@ -0,0 +1,132 @@
+
+
diff --git a/packages/g6-ssr/__tests__/assets/image.png b/packages/g6-ssr/__tests__/assets/image.png
new file mode 100644
index 00000000000..797b51982a8
Binary files /dev/null and b/packages/g6-ssr/__tests__/assets/image.png differ
diff --git a/packages/g6-ssr/__tests__/graph-options.json b/packages/g6-ssr/__tests__/graph-options.json
new file mode 100644
index 00000000000..b747588dc0d
--- /dev/null
+++ b/packages/g6-ssr/__tests__/graph-options.json
@@ -0,0 +1,113 @@
+{
+ "width": 500,
+ "height": 500,
+ "autoFit": "view",
+ "background": "rgba(100, 80, 180, 0.4)",
+ "data": {
+ "nodes": [
+ { "id": "0" },
+ { "id": "1" },
+ { "id": "2" },
+ { "id": "3" },
+ { "id": "4" },
+ { "id": "5" },
+ { "id": "6" },
+ { "id": "7" },
+ { "id": "8" },
+ { "id": "9" },
+ { "id": "10" },
+ { "id": "11" },
+ { "id": "12" },
+ { "id": "13" },
+ { "id": "14" },
+ { "id": "15" },
+ { "id": "16" },
+ { "id": "17" },
+ { "id": "18" },
+ { "id": "19" },
+ { "id": "20" },
+ { "id": "21" },
+ { "id": "22" },
+ { "id": "23" },
+ { "id": "24" },
+ { "id": "25" },
+ { "id": "26" },
+ { "id": "27" },
+ { "id": "28" },
+ { "id": "29" },
+ { "id": "30" },
+ { "id": "31" },
+ { "id": "32" },
+ { "id": "33" }
+ ],
+ "edges": [
+ { "source": "0", "target": "1" },
+ { "source": "0", "target": "2" },
+ { "source": "0", "target": "3" },
+ { "source": "0", "target": "4" },
+ { "source": "0", "target": "5" },
+ { "source": "0", "target": "7" },
+ { "source": "0", "target": "8" },
+ { "source": "0", "target": "9" },
+ { "source": "0", "target": "10" },
+ { "source": "0", "target": "11" },
+ { "source": "0", "target": "13" },
+ { "source": "0", "target": "14" },
+ { "source": "0", "target": "15" },
+ { "source": "0", "target": "16" },
+ { "source": "2", "target": "3" },
+ { "source": "4", "target": "5" },
+ { "source": "4", "target": "6" },
+ { "source": "5", "target": "6" },
+ { "source": "7", "target": "13" },
+ { "source": "8", "target": "14" },
+ { "source": "9", "target": "10" },
+ { "source": "10", "target": "22" },
+ { "source": "10", "target": "14" },
+ { "source": "10", "target": "12" },
+ { "source": "10", "target": "24" },
+ { "source": "10", "target": "21" },
+ { "source": "10", "target": "20" },
+ { "source": "11", "target": "24" },
+ { "source": "11", "target": "22" },
+ { "source": "11", "target": "14" },
+ { "source": "12", "target": "13" },
+ { "source": "16", "target": "17" },
+ { "source": "16", "target": "18" },
+ { "source": "16", "target": "21" },
+ { "source": "16", "target": "22" },
+ { "source": "17", "target": "18" },
+ { "source": "17", "target": "20" },
+ { "source": "18", "target": "19" },
+ { "source": "19", "target": "20" },
+ { "source": "19", "target": "33" },
+ { "source": "19", "target": "22" },
+ { "source": "19", "target": "23" },
+ { "source": "20", "target": "21" },
+ { "source": "21", "target": "22" },
+ { "source": "22", "target": "24" },
+ { "source": "22", "target": "25" },
+ { "source": "22", "target": "26" },
+ { "source": "22", "target": "23" },
+ { "source": "22", "target": "28" },
+ { "source": "22", "target": "30" },
+ { "source": "22", "target": "31" },
+ { "source": "22", "target": "32" },
+ { "source": "22", "target": "33" },
+ { "source": "23", "target": "28" },
+ { "source": "23", "target": "27" },
+ { "source": "23", "target": "29" },
+ { "source": "23", "target": "30" },
+ { "source": "23", "target": "31" },
+ { "source": "23", "target": "33" },
+ { "source": "32", "target": "33" }
+ ]
+ },
+ "node": {
+ "style": {
+ "labelFill": "#fff",
+ "labelPlacement": "center"
+ }
+ },
+ "layout": { "type": "circular" }
+}
diff --git a/packages/g6-ssr/__tests__/test.spec.ts b/packages/g6-ssr/__tests__/test.spec.ts
new file mode 100644
index 00000000000..ae56bf7349d
--- /dev/null
+++ b/packages/g6-ssr/__tests__/test.spec.ts
@@ -0,0 +1,83 @@
+import { existsSync, readFileSync } from 'fs';
+import { join } from 'path';
+import type { Graph } from '../src';
+import { createGraph } from '../src';
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace jest {
+ interface Matchers {
+ toMatchFile(path: string): R;
+ }
+ }
+}
+
+describe('createGraph', () => {
+ const fn = async (outputType?: any) => {
+ const data = (await fetch('https://assets.antv.antgroup.com/g6/circular.json').then((res) => res.json())) as any;
+
+ return await createGraph({
+ width: 500,
+ height: 500,
+ outputType,
+ autoFit: 'view',
+ background: 'rgba(100, 80, 180, 0.4)',
+ data,
+ node: {
+ style: {
+ labelText: (d) => d.id,
+ labelFill: '#fff',
+ labelPlacement: 'center',
+ },
+ },
+ layout: {
+ type: 'circular',
+ },
+ });
+ };
+
+ expect.extend({
+ toMatchFile: (received: Graph, path: string) => {
+ const pass = existsSync(path) ? received.toBuffer().equals(readFileSync(path)) : true;
+ if (pass) {
+ return {
+ message: () => 'passed',
+ pass: true,
+ };
+ } else {
+ return {
+ message: () => 'expected files are equal',
+ pass: false,
+ };
+ }
+ },
+ });
+
+ it('image image', async () => {
+ const graph = await fn();
+
+ expect(graph).toMatchFile('./assets/image.png');
+
+ graph.exportToFile(join(__dirname, './assets/image'));
+
+ graph.destroy();
+ });
+
+ it('file pdf', async () => {
+ const graph = await fn('pdf');
+
+ graph.exportToFile(join(__dirname, '/assets/file'));
+
+ graph.destroy();
+ });
+
+ it('file svg', async () => {
+ const graph = await fn('svg');
+
+ expect(graph).toMatchFile('./assets/file.svg');
+
+ graph.exportToFile(join(__dirname, './assets/file'));
+
+ graph.destroy();
+ });
+});
diff --git a/packages/g6-ssr/bin/g6-ssr.js b/packages/g6-ssr/bin/g6-ssr.js
new file mode 100755
index 00000000000..3b0e023f8bc
--- /dev/null
+++ b/packages/g6-ssr/bin/g6-ssr.js
@@ -0,0 +1,62 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+/* eslint-disable no-console */
+const cac = require('cac');
+const fs = require('fs');
+const { createGraph } = require('../dist/g6-ssr.cjs');
+const { version } = require('../package.json');
+
+const cli = cac();
+
+cli.version(version);
+
+cli.command('version', 'Show version').action(() => {
+ console.log(version);
+});
+
+cli
+ .command('export', 'Export G6 Options to Image, PDF or SVG')
+ .option('-i, --input ', 'Path to the G6 options file')
+ .option('-o, --output ', 'Path to the export file')
+ .option('-t, --type [type]', 'File type, default is image')
+ .action(async (options) => {
+ const { input, output, type } = options;
+
+ if (!input) {
+ console.log('\x1b[31m%s\x1b[0m', 'Please provide a path to the G6 options file');
+ process.exit(1);
+ }
+
+ if (!fs.existsSync(input)) {
+ console.log('\x1b[31m%s\x1b[0m', 'File does not exist: ', input);
+ process.exit(1);
+ }
+
+ let graphOptions;
+
+ try {
+ graphOptions = JSON.parse(fs.readFileSync(input, 'utf-8'));
+ } catch (e) {
+ console.log('\x1b[31m%s\x1b[0m', 'Invalid JSON file');
+ process.exit(1);
+ }
+
+ if (!graphOptions.outputType) {
+ if (type === 'svg' || type === 'pdf') {
+ graphOptions.outputType = type;
+ }
+ }
+
+ console.log(`Exporting to ${type || 'image'}...`);
+
+ const graph = await createGraph(graphOptions);
+
+ graph.exportToFile(output, type);
+
+ console.log('\x1b[32m%s\x1b[0m', 'Exported successfully!');
+
+ process.exit(0);
+ });
+
+cli.help();
+
+cli.parse();
diff --git a/packages/g6-ssr/jest.config.js b/packages/g6-ssr/jest.config.js
new file mode 100644
index 00000000000..be420483aa6
--- /dev/null
+++ b/packages/g6-ssr/jest.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ transform: {
+ '^.+\\.[tj]s$': ['@swc/jest'],
+ },
+ collectCoverageFrom: ['src/**/*.ts'],
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
+ transformIgnorePatterns: [`/node_modules/.pnpm/(?!(d3-*))`],
+ moduleNameMapper: {
+ '@antv/g6': '/../g6/src',
+ },
+};
diff --git a/packages/g6-ssr/package.json b/packages/g6-ssr/package.json
new file mode 100644
index 00000000000..7490e97b30f
--- /dev/null
+++ b/packages/g6-ssr/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@antv/g6-ssr",
+ "version": "0.0.1",
+ "description": "",
+ "keywords": [
+ "antv",
+ "g6",
+ "ssr"
+ ],
+ "license": "MIT",
+ "author": "Aarebecca",
+ "main": "./dist/g6-ssr.cjs",
+ "types": "./dist/lib/index.d.ts",
+ "bin": "./bin/g6-ssr.cjs",
+ "files": [
+ "dist",
+ "bin"
+ ],
+ "scripts": {
+ "build": "rimraf ./dist && rollup -c",
+ "ci": "run-s lint type-check build test",
+ "dev": "tsx ./src/index.ts",
+ "lint": "eslint ./src __tests__ --quiet && prettier ./src __tests__ --check",
+ "prepublishOnly": "npm run ci",
+ "test": "jest",
+ "type-check": "tsc --noEmit -p tsconfig.test.json"
+ },
+ "dependencies": {
+ "@antv/g": "^6.1.3",
+ "@antv/g-canvas": "^2.0.12",
+ "@antv/g6": "5.0.28-alpha.1",
+ "cac": "^6.7.14",
+ "canvas": "^2.11.2",
+ "webpack-cli": "^5.1.4"
+ },
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org/"
+ },
+ "type11": "module"
+}
diff --git a/packages/g6-ssr/rollup.config.mjs b/packages/g6-ssr/rollup.config.mjs
new file mode 100644
index 00000000000..caa46baec6e
--- /dev/null
+++ b/packages/g6-ssr/rollup.config.mjs
@@ -0,0 +1,31 @@
+import commonjs from '@rollup/plugin-commonjs';
+import json from '@rollup/plugin-json';
+import resolve from '@rollup/plugin-node-resolve';
+import terser from '@rollup/plugin-terser';
+import typescript from '@rollup/plugin-typescript';
+import nodePolyfills from 'rollup-plugin-polyfill-node';
+
+export default [
+ {
+ input: 'src/index.ts',
+ output: {
+ file: 'dist/g6-ssr.cjs',
+ format: 'cjs',
+ exports: 'named',
+ // format: 'umd',
+ // name: 'G6SSR',
+ sourcemap: true,
+ },
+ plugins: [
+ nodePolyfills(),
+ resolve(),
+ commonjs(),
+ json(),
+ typescript({
+ tsconfig: 'tsconfig.build.json',
+ }),
+ terser(),
+ ],
+ external: ['fs', 'path', 'canvas'],
+ },
+];
diff --git a/packages/g6-ssr/src/canvas.ts b/packages/g6-ssr/src/canvas.ts
new file mode 100644
index 00000000000..eb213c363da
--- /dev/null
+++ b/packages/g6-ssr/src/canvas.ts
@@ -0,0 +1,38 @@
+import { Renderer } from '@antv/g-canvas';
+import { Canvas as G6Canvas } from '@antv/g6';
+import type { Canvas as NodeCanvas } from 'canvas';
+import { createCanvas as createNodeCanvas } from 'canvas';
+import type { Options } from './types';
+
+/**
+ * 创建画布
+ *
+ * create canvas
+ * @param options options 画布配置 | options canvas configuration
+ * @returns [G6 画布, NodeCanvas 画布] | [G6Canvas, NodeCanvas]
+ */
+export function createCanvas(options: Options): [G6Canvas, NodeCanvas] {
+ const { width, height, background, outputType } = options;
+ const nodeCanvas = createNodeCanvas(width, height, outputType as any);
+ const offscreenNodeCanvas = createNodeCanvas(1, 1);
+
+ const g6Canvas = new G6Canvas({
+ width,
+ height,
+ background,
+ // @ts-expect-error missing types
+ canvas: nodeCanvas as any,
+ offscreenCanvas: offscreenNodeCanvas as any,
+ enableMultiLayer: false,
+ renderer: () => {
+ const renderer = new Renderer();
+ const htmlRendererPlugin = renderer.getPlugin('html-renderer');
+ const domInteractionPlugin = renderer.getPlugin('dom-interaction');
+ renderer.unregisterPlugin(htmlRendererPlugin);
+ renderer.unregisterPlugin(domInteractionPlugin);
+ return renderer;
+ },
+ });
+
+ return [g6Canvas, nodeCanvas];
+}
diff --git a/packages/g6-ssr/src/graph.ts b/packages/g6-ssr/src/graph.ts
new file mode 100644
index 00000000000..8466b257ea3
--- /dev/null
+++ b/packages/g6-ssr/src/graph.ts
@@ -0,0 +1,55 @@
+import { Graph as G6Graph } from '@antv/g6';
+import { existsSync, lstatSync, writeFileSync } from 'fs';
+import { createCanvas } from './canvas';
+import type { Graph, Options } from './types';
+
+/**
+ * 获取输出文件的扩展名
+ *
+ * Get the extension name of the output file
+ * @param options - 配置项 | options
+ * @returns 输出文件的扩展名 | The extension name of the output file
+ */
+function getExtendNameOf(options: Options) {
+ const { outputType } = options;
+ if (outputType === 'pdf') return '.pdf';
+ if (outputType === 'svg') return '.svg';
+ return '.png';
+}
+
+/**
+ * 创建图并等待渲染完成
+ *
+ * Create a graph and wait for the rendering to complete
+ * @param options - 图配置项 | Graph options
+ * @returns 扩展图实例 | Extended graph instance
+ */
+export async function createGraph(options: Options) {
+ const [g6Canvas, nodeCanvas] = createCanvas(options);
+
+ const { outputType, ...restOptions } = options;
+ const graph = new G6Graph({
+ animation: false,
+ ...restOptions,
+ container: g6Canvas,
+ });
+
+ // @ts-expect-error extend Graph
+ graph.exportToFile = (file: string) => {
+ const extendName = getExtendNameOf(options);
+ if (!file.endsWith(extendName)) {
+ if (!existsSync(file)) file += extendName;
+ else if (lstatSync(file).isDirectory()) file = `${file}/image${extendName}`;
+ else file += extendName;
+ }
+
+ writeFileSync(file, nodeCanvas.toBuffer());
+ };
+
+ // @ts-expect-error extend Graph
+ graph.toBuffer = () => nodeCanvas.toBuffer();
+
+ await graph.render();
+
+ return graph as Graph;
+}
diff --git a/packages/g6-ssr/src/index.ts b/packages/g6-ssr/src/index.ts
new file mode 100644
index 00000000000..7cbe2d27f1b
--- /dev/null
+++ b/packages/g6-ssr/src/index.ts
@@ -0,0 +1,3 @@
+export { createCanvas } from './canvas';
+export { createGraph } from './graph';
+export type { Graph, Options } from './types';
diff --git a/packages/g6-ssr/src/types.ts b/packages/g6-ssr/src/types.ts
new file mode 100644
index 00000000000..647d40c4843
--- /dev/null
+++ b/packages/g6-ssr/src/types.ts
@@ -0,0 +1,19 @@
+import type { GraphOptions } from '@antv/g6';
+import { Graph as G6Graph } from '@antv/g6';
+
+export interface Options extends Omit {
+ width: number;
+ height: number;
+ /**
+ * 输出文件类型,默认导出为图片
+ *
+ * output file type, default export as image
+ * @defaultValue 'image'
+ */
+ outputType?: 'image' | 'pdf' | 'svg';
+}
+
+export interface Graph extends G6Graph {
+ exportToFile: (file: string) => void;
+ toBuffer: () => Buffer;
+}
diff --git a/packages/g6-ssr/tsconfig.build.json b/packages/g6-ssr/tsconfig.build.json
new file mode 100644
index 00000000000..5790ea55085
--- /dev/null
+++ b/packages/g6-ssr/tsconfig.build.json
@@ -0,0 +1,7 @@
+{
+ "compilerOptions": {
+ "paths": {}
+ },
+ "include": ["src/**/*"],
+ "extends": "./tsconfig.json"
+}
diff --git a/packages/g6-ssr/tsconfig.json b/packages/g6-ssr/tsconfig.json
new file mode 100644
index 00000000000..62743c6198e
--- /dev/null
+++ b/packages/g6-ssr/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "outDir": "lib",
+ "target": "ESNext",
+ "module": "CommonJS",
+ "lib": ["ESNext"],
+ "paths": {
+ "@antv/g6": ["../g6/src/index.ts"]
+ }
+ },
+ "extends": "../../tsconfig.json",
+ "include": ["src/**/*", "__tests__/**/*"]
+}
diff --git a/packages/g6-ssr/tsconfig.test.json b/packages/g6-ssr/tsconfig.test.json
new file mode 100644
index 00000000000..74e04566a17
--- /dev/null
+++ b/packages/g6-ssr/tsconfig.test.json
@@ -0,0 +1,7 @@
+{
+ "compilerOptions": {
+ "paths": {}
+ },
+ "include": ["src/**/*", "__tests__/**/*"],
+ "extends": "./tsconfig.json"
+}
diff --git a/packages/g6/__tests__/main.ts b/packages/g6/__tests__/main.ts
index 66340d7e670..896b1082be0 100644
--- a/packages/g6/__tests__/main.ts
+++ b/packages/g6/__tests__/main.ts
@@ -10,6 +10,7 @@ type Options = {
Renderer: string;
Theme: string;
Animation: boolean;
+ MultiLayers: boolean;
[keys: string]: any;
};
@@ -20,6 +21,7 @@ const options: Options = {
GridLine: true,
Theme: 'light',
Animation: true,
+ MultiLayers: true,
interval: 0,
Reload: () => {},
forms: [],
@@ -48,8 +50,9 @@ function initPanel() {
applyGridLine();
});
const Animation = panel.add(options, 'Animation').onChange(render);
+ const MultiLayers = panel.add(options, 'MultiLayers').onChange(render);
const reload = panel.add(options, 'Reload').onChange(render);
- return { panel, Demo, Search, Renderer, GridLine, Theme, Animation, reload };
+ return { panel, Demo, Search, Renderer, GridLine, Theme, Animation, MultiLayers, reload };
}
async function render() {
@@ -62,13 +65,21 @@ async function render() {
applyGridLine();
// render
- const { Renderer, Demo, Animation, Theme } = options;
- const canvas = createGraphCanvas($container, 500, 500, Renderer);
+ const { Renderer, Demo, Animation, Theme, MultiLayers } = options;
+
+ const canvasOptions = { enableMultiLayer: MultiLayers };
+
+ const canvas = createGraphCanvas($container, 500, 500, Renderer, canvasOptions);
await canvas.ready;
const testCase = demos[Demo as keyof typeof demos];
if (!testCase) return;
- const graph = await testCase({ container: canvas, animation: Animation, theme: Theme });
+ const graph = await testCase({
+ container: canvas,
+ animation: Animation,
+ theme: Theme,
+ canvas: canvasOptions,
+ });
Object.assign(window, { graph, __g_instances__: Object.values(graph.getCanvas().getLayers()) });
diff --git a/packages/g6/__tests__/unit/runtime/canvas.spec.ts b/packages/g6/__tests__/unit/runtime/canvas.spec.ts
index fffe7ba6274..665a811c4f7 100644
--- a/packages/g6/__tests__/unit/runtime/canvas.spec.ts
+++ b/packages/g6/__tests__/unit/runtime/canvas.spec.ts
@@ -98,4 +98,14 @@ describe('Canvas', () => {
expect(graph.getCanvas().getConfig().cursor).toEqual('progress');
});
+
+ it('layers', () => {
+ const singleLayerCanvas = createGraphCanvas(document.getElementById('container'), 500, 500, 'svg', {
+ enableMultiLayer: false,
+ });
+ expect(Object.keys(singleLayerCanvas.getLayers())).toEqual(['main']);
+
+ const multiLayerCanvas = createGraphCanvas(document.getElementById('container'), 500, 500, 'svg');
+ expect(Object.keys(multiLayerCanvas.getLayers())).toEqual(['background', 'main', 'label', 'transient']);
+ });
});
diff --git a/packages/g6/__tests__/utils/create.ts b/packages/g6/__tests__/utils/create.ts
index 9235bae63a8..386623dd1f3 100644
--- a/packages/g6/__tests__/utils/create.ts
+++ b/packages/g6/__tests__/utils/create.ts
@@ -1,12 +1,9 @@
-import type { GraphOptions } from '@/src';
-import { Graph } from '@/src';
-import { Circle } from '@/src/elements';
-import { Canvas } from '@/src/runtime/canvas';
-import type { Node, Point } from '@/src/types';
import { resetEntityCounter } from '@antv/g';
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
import { Renderer as SVGRenderer } from '@antv/g-svg';
import { Renderer as WebGLRenderer } from '@antv/g-webgl';
+import type { CanvasConfig, GraphOptions, Node, Point } from '@antv/g6';
+import { Canvas, Circle, Graph } from '@antv/g6';
import { OffscreenCanvasContext } from './offscreen-canvas-context';
function getRenderer(renderer: string) {
@@ -28,6 +25,7 @@ function getRenderer(renderer: string) {
* @param width - width
* @param height - height
* @param renderer - render
+ * @param options - options
* @returns instance
*/
export function createGraphCanvas(
@@ -35,6 +33,7 @@ export function createGraphCanvas(
width: number = 500,
height: number = 500,
renderer: string = 'svg',
+ options?: Partial,
) {
const container = dom || document.createElement('div');
container.style.width = `${width}px`;
@@ -42,7 +41,9 @@ export function createGraphCanvas(
resetEntityCounter();
- let extraOptions = {};
+ const extraOptions: Record = {
+ ...options,
+ };
if (globalThis.process) {
const offscreenNodeCanvas = {
@@ -50,10 +51,10 @@ export function createGraphCanvas(
} as unknown as HTMLCanvasElement;
const context = new OffscreenCanvasContext(offscreenNodeCanvas);
// 下列参数仅在 node 环境下需要传入 / These parameters only need to be passed in the node environment
- extraOptions = {
+ Object.assign(extraOptions, {
document: container.ownerDocument,
offscreenCanvas: offscreenNodeCanvas,
- };
+ });
}
const offscreenNodeCanvas = {
diff --git a/packages/g6/src/exports.ts b/packages/g6/src/exports.ts
index 5658e080eaf..5e90cb767af 100644
--- a/packages/g6/src/exports.ts
+++ b/packages/g6/src/exports.ts
@@ -95,6 +95,7 @@ export {
} from './plugins';
export { getExtension, getExtensions } from './registry/get';
export { register } from './registry/register';
+export { Canvas } from './runtime/canvas';
export { Graph } from './runtime/graph';
export { BaseTransform, MapNodeSize, PlaceRadialLabels, ProcessParallelEdges } from './transforms';
export { isCollapsed } from './utils/collapsibility';
@@ -200,7 +201,7 @@ export type {
TooltipOptions,
WatermarkOptions,
} from './plugins';
-export type { DataURLOptions } from './runtime/canvas';
+export type { CanvasConfig, DataURLOptions } from './runtime/canvas';
export type { RuntimeContext } from './runtime/types';
export type {
BehaviorOptions,
diff --git a/packages/g6/src/plugins/fullscreen/index.ts b/packages/g6/src/plugins/fullscreen/index.ts
index 3b7078b96b2..d3dee049ee0 100644
--- a/packages/g6/src/plugins/fullscreen/index.ts
+++ b/packages/g6/src/plugins/fullscreen/index.ts
@@ -105,8 +105,8 @@ export class Fullscreen extends BasePlugin {
private setGraphSize(fullScreen = true) {
let width, height;
if (fullScreen) {
- width = window.screen.width;
- height = window.screen.height;
+ width = globalThis.screen?.width || 0;
+ height = globalThis.screen?.height || 0;
this.graphSize = this.context.graph.getSize();
} else {
[width, height] = this.graphSize;
diff --git a/packages/g6/src/runtime/canvas.ts b/packages/g6/src/runtime/canvas.ts
index 6def2386783..c4c4b1bbd5b 100644
--- a/packages/g6/src/runtime/canvas.ts
+++ b/packages/g6/src/runtime/canvas.ts
@@ -10,7 +10,23 @@ import { parsePoint, toPointObject } from '../utils/point';
export interface CanvasConfig
extends Pick {
+ /**
+ * 渲染器
+ *
+ * renderer
+ */
renderer?: CanvasOptions['renderer'];
+ /**
+ * 是否启用多图层
+ *
+ * Whether to enable multiple layers
+ * @defaultValue true
+ * @remarks
+ * 非动态参数,仅在初始化时生效
+ *
+ * Non-dynamic parameters, only take effect during initialization
+ */
+ enableMultiLayer?: boolean;
}
export interface DataURLOptions {
@@ -39,7 +55,19 @@ export interface DataURLOptions {
encoderOptions: number;
}
-const layersName: CanvasLayer[] = ['background', 'main', 'label', 'transient'];
+const SINGLE_LAYER_NAME: CanvasLayer[] = ['main'];
+const MULTI_LAYER_NAME: CanvasLayer[] = ['background', 'main', 'label', 'transient'];
+
+/**
+ * 获取主画布图层
+ *
+ * Get the main canvas layer
+ * @param layers - 画布图层 | Canvas layer
+ * @returns 主画布图层 | Main canvas layer
+ */
+function getMainLayerOf(layers: Record) {
+ return layers.main;
+}
export class Canvas {
private extends: {
@@ -49,14 +77,16 @@ export class Canvas {
layers: Record;
};
- private config: CanvasConfig;
+ private config: CanvasConfig = {
+ enableMultiLayer: true,
+ };
public getConfig() {
return this.config;
}
public getLayer(layer: CanvasLayer = 'main') {
- return this.extends.layers[layer];
+ return this.extends.layers[layer] || getMainLayerOf(this.getLayers());
}
/**
@@ -113,18 +143,18 @@ export class Canvas {
}
constructor(config: CanvasConfig) {
- this.config = config;
-
- const { renderer, background, cursor, ...restConfig } = config;
- const renderers = createRenderers(renderer);
+ Object.assign(this.config, config);
+ const { renderer, background, cursor, enableMultiLayer, ...restConfig } = this.config;
+ const layersName = enableMultiLayer ? MULTI_LAYER_NAME : SINGLE_LAYER_NAME;
+ const renderers = createRenderers(renderer, layersName);
const layers = Object.fromEntries(
layersName.map((layer) => {
const canvas = new GCanvas({
...restConfig,
- supportsMutipleCanvasesInOneContainer: true,
+ supportsMutipleCanvasesInOneContainer: enableMultiLayer,
renderer: renderers[layer],
- background: layer === 'background' ? background : undefined,
+ background: enableMultiLayer ? (layer === 'background' ? background : undefined) : background,
});
return [layer, canvas];
@@ -134,7 +164,7 @@ export class Canvas {
configCanvasDom(layers);
this.extends = {
- config,
+ config: this.config,
renderer,
renderers,
layers,
@@ -207,7 +237,7 @@ export class Canvas {
public setRenderer(renderer: CanvasOptions['renderer']) {
if (renderer === this.extends.renderer) return;
- const renderers = createRenderers(renderer);
+ const renderers = createRenderers(renderer, this.config.enableMultiLayer ? MULTI_LAYER_NAME : SINGLE_LAYER_NAME);
this.extends.renderers = renderers;
Object.entries(renderers).forEach(([layer, instance]) => this.getLayer(layer as CanvasLayer).setRenderer(instance));
configCanvasDom(this.getLayers());
@@ -240,7 +270,7 @@ export class Canvas {
}
public async toDataURL(options: Partial = {}) {
- const devicePixelRatio = window.devicePixelRatio || 1;
+ const devicePixelRatio = globalThis.devicePixelRatio || 1;
const { mode = 'viewport', ...restOptions } = options;
let [startX, startY, width, height] = [0, 0, 0, 0];
@@ -322,9 +352,10 @@ export class Canvas {
*
* Create renderers
* @param renderer - 渲染器创建器 Renderer creator
+ * @param layersName - 图层名称 Layer name
* @returns 渲染器 Renderer
*/
-function createRenderers(renderer: CanvasConfig['renderer']) {
+function createRenderers(renderer: CanvasConfig['renderer'], layersName: CanvasLayer[]) {
return Object.fromEntries(
layersName.map((layer) => {
const instance = renderer?.(layer) || new CanvasRenderer();
@@ -357,10 +388,14 @@ function configCanvasDom(layers: Record) {
Object.entries(layers).forEach(([layer, canvas]) => {
const domElement = canvas.getContextService().getDomElement() as unknown as HTMLElement;
- domElement.style.position = 'absolute';
- domElement.style.outline = 'none';
- domElement.tabIndex = 1;
+ // 浏览器环境下,设置画布样式
+ // Set canvas style in browser environment
+ if (domElement?.style) {
+ domElement.style.position = 'absolute';
+ domElement.style.outline = 'none';
+ domElement.tabIndex = 1;
- if (layer !== 'main') domElement.style.pointerEvents = 'none';
+ if (layer !== 'main') domElement.style.pointerEvents = 'none';
+ }
});
}
diff --git a/packages/g6/src/runtime/graph.ts b/packages/g6/src/runtime/graph.ts
index 4f77867a603..36df5904ae0 100644
--- a/packages/g6/src/runtime/graph.ts
+++ b/packages/g6/src/runtime/graph.ts
@@ -91,8 +91,8 @@ export class Graph extends EventEmitter {
this._setOptions(this.options, true);
this.context.graph = this;
- // Listening window.resize to autoResize.
- this.options.autoResize && window.addEventListener('resize', this.onResize);
+ // Listening resize to autoResize.
+ this.options.autoResize && globalThis.addEventListener?.('resize', this.onResize);
}
/**
@@ -1031,7 +1031,8 @@ export class Graph extends EventEmitter {
renderer,
cursor,
background,
- devicePixelRatio = window.devicePixelRatio ?? 1,
+ canvas: canvasOptions,
+ devicePixelRatio = globalThis.devicePixelRatio ?? 1,
} = this.options;
if (container instanceof Canvas) {
this.context.canvas = container;
@@ -1043,8 +1044,8 @@ export class Graph extends EventEmitter {
const containerSize = sizeOf($container!);
this.emit(GraphEvent.BEFORE_CANVAS_INIT, { container: $container, width, height });
-
- const canvas = new Canvas({
+ const options = {
+ ...canvasOptions,
container: $container!,
width: width || containerSize[0],
height: height || containerSize[1],
@@ -1052,7 +1053,9 @@ export class Graph extends EventEmitter {
renderer,
cursor,
devicePixelRatio,
- });
+ };
+
+ const canvas = new Canvas(options);
this.context.canvas = canvas;
await canvas.ready;
@@ -1204,7 +1207,7 @@ export class Graph extends EventEmitter {
this.context = {};
this.off();
- window.removeEventListener('resize', this.onResize);
+ globalThis.removeEventListener?.('resize', this.onResize);
this.destroyed = true;
diff --git a/packages/g6/src/spec/canvas.ts b/packages/g6/src/spec/canvas.ts
index 30acd8972c0..95c8e154c17 100644
--- a/packages/g6/src/spec/canvas.ts
+++ b/packages/g6/src/spec/canvas.ts
@@ -1,5 +1,5 @@
import type { Cursor, IRenderer } from '@antv/g';
-import type { Canvas } from '../runtime/canvas';
+import type { Canvas, CanvasConfig } from '../runtime/canvas';
/**
* 画布配置项
@@ -57,4 +57,14 @@ export interface CanvasOptions {
* cursor style
*/
cursor?: Cursor;
+ /**
+ * 画布配置
+ *
+ * canvas config
+ * @remarks
+ * GraphOptions 下相关配置项为快捷配置项,会被转换为 canvas 配置项
+ *
+ * The related configuration items under GraphOptions are shortcut configuration items, which will be converted to canvas configuration items
+ */
+ canvas?: CanvasConfig;
}
diff --git a/packages/g6/src/utils/shortcut.ts b/packages/g6/src/utils/shortcut.ts
index f7dbe24713f..c19c998d548 100644
--- a/packages/g6/src/utils/shortcut.ts
+++ b/packages/g6/src/utils/shortcut.ts
@@ -57,7 +57,7 @@ export class Shortcut {
// 窗口重新获得焦点后清空按键,避免按键状态异常
// Clear the keys when the window regains focus to avoid abnormal key states
- window.addEventListener('focus', this.onFocus);
+ globalThis.addEventListener?.('focus', this.onFocus);
}
private onKeyDown = (event: KeyboardEvent) => {
@@ -117,6 +117,6 @@ export class Shortcut {
this.emitter.off(CommonEvent.KEY_UP, this.onKeyUp);
this.emitter.off(CommonEvent.WHEEL, this.onWheel);
this.emitter.off(CommonEvent.DRAG, this.onDrag);
- window.removeEventListener('blur', this.onFocus);
+ globalThis.removeEventListener?.('blur', this.onFocus);
}
}