feat: add g6 ssr

This commit is contained in:
antv 2024-10-29 21:47:16 +08:00
parent dbe0868027
commit 7f1dbd9f3c
15 changed files with 439 additions and 0 deletions

View File

@ -18,6 +18,13 @@ jobs:
with: with:
node-version: 18 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 - uses: pnpm/action-setup@v4
name: Install pnpm name: Install pnpm
with: with:

29
packages/g6-ssr/README.md Normal file
View File

@ -0,0 +1,29 @@
## SSR extension for G6 5.0
This extension package provides SSR support for G6 5.0, which supports canvas rendering in server side.
## Usage
1. Install
```bash
npm install @antv/g6-ssr
```
2. Render
```js
// usage 1
import { createGraph } from '@antv/g6-ssr';
const graph = createGraph({
width: 500,
height: 500,
data: {
// your data
},
// your other options
});
graph.writeToFile('output.png');
```

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -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<R> {
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.writeToFile(join(__dirname, './assets/image'));
graph.destroy();
});
it('file pdf', async () => {
const graph = await fn('pdf');
graph.writeToFile(join(__dirname, '/assets/file'));
graph.destroy();
});
it('file svg', async () => {
const graph = await fn('svg');
expect(graph).toMatchFile('./assets/file.svg');
graph.writeToFile(join(__dirname, './assets/file'));
graph.destroy();
});
});

View File

@ -0,0 +1,11 @@
module.exports = {
transform: {
'^.+\\.[tj]s$': ['@swc/jest'],
},
collectCoverageFrom: ['src/**/*.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
transformIgnorePatterns: [`<rootDir>/node_modules/.pnpm/(?!(d3-*))`],
moduleNameMapper: {
'@antv/g6': '<rootDir>/../g6/src',
},
};

View File

@ -0,0 +1,36 @@
{
"name": "g6-ssr",
"version": "0.0.1",
"description": "",
"keywords": [
"antv",
"g6",
"ssr"
],
"license": "MIT",
"author": "Aarebecca",
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build": "run-p build:*",
"build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib -p tsconfig.build.json",
"build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm -p tsconfig.build.json",
"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": "workspace:*",
"canvas": "^2.11.2"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}

View File

@ -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';
/**
* <zh/>
*
* <en/> create canvas
* @param options <zh/> options | <en/> options canvas configuration
* @returns <zh/> [G6 , NodeCanvas ] | <en/> [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];
}

View File

@ -0,0 +1,54 @@
import { Graph as G6Graph } from '@antv/g6';
import { existsSync, lstatSync, writeFileSync } from 'fs';
import { createCanvas } from './canvas';
import type { Graph, Options } from './types';
/**
* <zh/>
*
* <en/> Get the extension name of the output file
* @param options - <zh/> | <en/>options
* @returns - <zh/> | <en/>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';
}
/**
* <zh/>
*
* <en/> Create a graph and wait for the rendering to complete
* @param options - <zh/> | <en/>Graph options
* @returns - <zh/> | <en/>Extended graph instance
*/
export async function createGraph(options: Options) {
const [g6Canvas, nodeCanvas] = createCanvas(options);
const { outputType, ...restOptions } = options;
const graph = new G6Graph({
...restOptions,
container: g6Canvas,
});
// @ts-expect-error extend Graph
graph.writeToFile = (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;
}

View File

@ -0,0 +1,3 @@
export { createCanvas } from './canvas';
export { createGraph } from './graph';
export type { Graph, Options } from './types';

View File

@ -0,0 +1,19 @@
import type { GraphOptions } from '@antv/g6';
import { Graph as G6Graph } from '@antv/g6';
export interface Options extends Omit<GraphOptions, 'renderer' | 'container'> {
width: number;
height: number;
/**
* <zh/>
*
* <en/> output file type, default export as image
* @defaultValue 'image'
*/
outputType?: 'image' | 'pdf' | 'svg';
}
export interface Graph extends G6Graph {
writeToFile: (file: string) => void;
toBuffer: () => Buffer;
}

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {}
},
"include": ["src/**/*"],
"extends": "./tsconfig.json"
}

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"strict": true,
"outDir": "lib",
"target": "ESNext",
"lib": ["ESNext"],
"paths": {
"@antv/g6": ["../g6/src/index.ts"]
}
},
"extends": "../../tsconfig.json",
"include": ["src/**/*", "__tests__/**/*"]
}

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {}
},
"include": ["src/**/*", "__tests__/**/*"],
"extends": "./tsconfig.json"
}