feat: add react-node for v5 (#5079)

* chore: remove old react-node package

* feat: upsertShape support create html element

* feat(react-node): init react-node package

* chore: update global config and ant-design/icons version

* style: fix lint errors

* refactor(react-node): adpapt to lastest react-g

* fix: unregister dom-interaction plugin in transient & background canvases

* chore: update demos

* feat: support jsx node drag

* docs: add demo

* fix: fix type errors

* refactor: add site demos

* chore: update pnpm-lock

* refactor: adapt to upsertShape adjust

* chore: update latest g versions

---------

Co-authored-by: yuqi.pyq <yuqi.pyq@antgroup.com>
This commit is contained in:
Aaron 2023-12-11 22:27:31 +08:00 committed by GitHub
parent 4d5c037005
commit 0690995a0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
107 changed files with 3243 additions and 4211 deletions

View File

@ -11,8 +11,8 @@
"build:site": "cd ./packages/site && npm run site:build"
},
"devDependencies": {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^17.8.1",
"husky": "^8.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.8"

View File

@ -17,6 +17,7 @@
"build:umd": "rimraf ./dist && rollup -c && npm run size",
"build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib",
"build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm",
"build:esm:watch": "rimraf ./esm && tsc --module ESNext --outDir esm --watch",
"build": "run-p build:*",
"bundle-vis": "cross-env BUNDLE_VIS=1 npm run build:umd",
"size": "limit-size",
@ -53,11 +54,11 @@
"@ant-design/colors": "^7.0.0",
"@antv/algorithm": "^0.1.26",
"@antv/event-emitter": "latest",
"@antv/g": "^5.18.17",
"@antv/g-canvas": "^1.11.19",
"@antv/g": "^5.18.23",
"@antv/g-canvas": "^1.11.25",
"@antv/g-plugin-3d": "^1.9.26",
"@antv/g-plugin-control": "^1.9.17",
"@antv/g-plugin-dragndrop": "^1.8.15",
"@antv/g-plugin-dragndrop": "^1.8.17",
"@antv/g-svg": "^1.10.21",
"@antv/g-webgl": "^1.9.29",
"@antv/graphlib": "^2.0.2",

View File

@ -167,6 +167,7 @@ export default abstract class Item implements IItem {
enableBalanceShape,
device: this.device,
zoom: this.zoom,
graph: this.graph,
});
}
@ -273,6 +274,7 @@ export default abstract class Item implements IItem {
lodLevels: this.lodLevels,
device: this.device,
zoom: this.zoom,
graph: this.graph,
});
} else {
this.renderExt.themeStyles = this.themeStyles.default;

View File

@ -219,6 +219,9 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
width ?? this.container.scrollWidth,
height ?? this.container.scrollHeight,
pixelRatio,
undefined,
// enable dom interaction only for main canvas
name === 'canvas' ? [] : ['dom-interaction'],
);
});

View File

@ -49,7 +49,7 @@ export abstract class BaseEdge {
labelBackgroundShapeTransform?: string;
};
// cache the zoom level infomations
private zoomCache: {
#zoomCache: {
// last responsed zoom ratio.
zoom: number;
// wordWrapWidth of labelShape according to the maxWidth
@ -63,7 +63,7 @@ export abstract class BaseEdge {
if (themeStyles) this.themeStyles = themeStyles;
this.lodLevels = lodLevels;
this.transformCache = {};
this.zoomCache.zoom = zoom;
this.#zoomCache.zoom = zoom;
}
/**
@ -120,16 +120,16 @@ export abstract class BaseEdge {
}
});
const { zoom } = this.zoomCache;
const { zoom } = this.#zoomCache;
const { maxWidth = '60%' } = this.mergedStyles.labelShape || {};
this.zoomCache.wordWrapWidth = getWordWrapWidthByEnds(
this.#zoomCache.wordWrapWidth = getWordWrapWidthByEnds(
[this.sourcePoint, this.targetPoint],
maxWidth,
1,
);
this.zoomCache.zoom = 1;
this.#zoomCache.zoom = 1;
if (zoom !== 1) this.onZoom(shapeMap, zoom);
}
@ -288,9 +288,9 @@ export abstract class BaseEdge {
const wordWrapWidth = getWordWrapWidthByEnds(
[this.sourcePoint, this.targetPoint],
maxWidth,
this.zoomCache.zoom,
this.#zoomCache.zoom,
);
this.zoomCache.wordWrapWidth = wordWrapWidth;
this.#zoomCache.wordWrapWidth = wordWrapWidth;
const style = {
...this.defaultStyles.labelShape,
textAlign: positionPreset.textAlign,

View File

@ -28,11 +28,13 @@ import {
mergeStyles,
upsertShape,
} from '../../../util/shape';
import type { IGraph } from '../../../types/graph';
import { getWordWrapWidthByBox } from '../../../util/text';
import { convertToNumber } from '../../../util/type';
export abstract class BaseNode {
type: string;
graph: IGraph;
defaultStyles: NodeShapeStyles | ComboShapeStyles;
themeStyles: NodeShapeStyles | ComboShapeStyles;
mergedStyles: NodeShapeStyles | ComboShapeStyles;
@ -40,13 +42,8 @@ export abstract class BaseNode {
enableBalanceShape?: boolean;
//vertex coordinate
/**
* Cache the scale transform calculated by balancing size, for restoring.
*/
protected scaleTransformCache = '';
// cache the zoom level infomations
protected zoomCache: {
// cache the zoom level information
#zoomCache: {
// last response zoom ratio.
zoom: number;
// wordWrapWidth of labelShape according to the maxWidth
@ -57,11 +54,13 @@ export abstract class BaseNode {
};
constructor(props) {
const { themeStyles, lodLevels, enableBalanceShape, zoom } = props;
const { graph, themeStyles, lodLevels, enableBalanceShape, zoom } = props;
if (themeStyles) this.themeStyles = themeStyles;
this.graph = graph;
this.lodLevels = lodLevels;
this.enableBalanceShape = enableBalanceShape;
this.zoomCache.zoom = zoom;
this.#zoomCache.zoom = zoom;
}
/**
@ -133,7 +132,7 @@ export abstract class BaseNode {
public updateCache(shapeMap) {
if (shapeMap.labelShape) {
const { maxWidth = '200%' } = this.mergedStyles.labelShape || {};
this.zoomCache.wordWrapWidth = getWordWrapWidthByBox(
this.#zoomCache.wordWrapWidth = getWordWrapWidthByBox(
shapeMap.keyShape.getLocalBounds(),
maxWidth,
1,
@ -255,7 +254,7 @@ export abstract class BaseNode {
const wordWrapWidth = getWordWrapWidthByBox(
keyShapeBox as AABB,
maxWidth,
this.zoomCache.zoom,
this.#zoomCache.zoom,
this.enableBalanceShape,
);
@ -294,7 +293,8 @@ export abstract class BaseNode {
positionPreset.textBaseline = 'middle';
positionPreset.offsetX = 4;
break;
default: // at bottom by default
default:
// at bottom by default
positionPreset.offsetY = 2;
break;
}
@ -503,7 +503,7 @@ export abstract class BaseNode {
const anchorPositionMap = this.calculateAnchorPosition(keyShapeStyle);
individualConfigs.forEach((config, i) => {
const { position, fill = keyShapeStyle.fill, ...style } = config;
const [cx, cy] = this.getAnchorPosition(
const [cx, cy] = this.#getAnchorPosition(
position,
anchorPositionMap,
shapeMap,
@ -530,7 +530,7 @@ export abstract class BaseNode {
return shapes;
}
private getAnchorPosition(
#getAnchorPosition(
position: string | [number, number],
anchorPositionMap: IAnchorPositionMap,
shapeMap: NodeShapeMap | ComboShapeMap,

View File

@ -1,26 +1,25 @@
import { ID } from '@antv/graphlib';
import {
AABB,
CircleStyleProps,
RectStyleProps,
DisplayObject,
EllipseStyleProps,
PolygonStyleProps,
Group,
HTMLStyleProps,
IAnimation,
ImageStyleProps,
LineStyleProps,
PathStyleProps,
PolygonStyleProps,
PolylineStyleProps,
RectStyleProps,
TextStyleProps,
ImageStyleProps,
Group,
DisplayObject,
IAnimation,
Shape,
} from '@antv/g';
import {
CubeGeometryProps,
PlaneGeometryProps,
SphereGeometryProps,
TorusGeometryProps,
} from '@antv/g-plugin-3d';
import { ID } from '@antv/graphlib';
import { AnimateCfg, IAnimates } from './animate';
import {
ComboDisplayModel,
@ -58,6 +57,7 @@ export type GShapeStyle = CircleStyleProps &
PathStyleProps &
SphereGeometryProps &
CubeGeometryProps &
HTMLStyleProps &
PlaneGeometryProps & {
interactive?: boolean;
};
@ -114,7 +114,8 @@ export type SHAPE_TYPE =
| 'line'
| 'path'
| 'text'
| 'group';
| 'group'
| 'html';
export type SHAPE_TYPE_3D = 'sphere' | 'cube' | 'plane';

View File

@ -22,6 +22,7 @@ export const createCanvas = (
height: number,
pixelRatio?: number,
canvasConfig: Partial<CanvasConfig> = {},
unregisterPlugins: string[] = [],
): Canvas => {
let renderer: any;
switch (rendererType.toLowerCase()) {
@ -46,6 +47,13 @@ export const createCanvas = (
}),
);
if (unregisterPlugins.length) {
unregisterPlugins.forEach((name) => {
const plugin = renderer.getPlugin(name);
renderer.unregisterPlugin(plugin);
});
}
return new Canvas({
container,
width,

View File

@ -1,32 +1,25 @@
import {
AABB,
Circle,
DisplayObject,
Ellipse,
Group,
HTML,
IElement,
Image,
Line,
Path,
Polygon,
Polyline,
Rect,
Text,
Image,
Path,
AABB,
Tuple3Number,
} from '@antv/g';
import { isArray, isNumber } from '@antv/util';
import { DEFAULT_LABEL_BG_PADDING } from '../constant';
import { Padding, Point, StandardPadding } from '../types/common';
import { EdgeDisplayModel, EdgeModelData, EdgeShapeMap } from '../types/edge';
import {
GShapeStyle,
SHAPE_TYPE,
ItemShapeStyles,
ShapeStyle,
SHAPE_TYPE_3D,
State,
} from '../types/item';
import { NodeDisplayModel, NodeModelData, NodeShapeMap } from '../types/node';
import Combo from '../item/combo';
import Edge from '../item/edge';
import Node from '../item/node';
import {
AnimateTiming,
ComboDisplayModel,
@ -34,14 +27,21 @@ import {
IAnimates,
IGraph,
} from '../types';
import Node from '../item/node';
import Edge from '../item/edge';
import Combo from '../item/combo';
import { Padding, Point, StandardPadding } from '../types/common';
import { EdgeDisplayModel, EdgeModelData, EdgeShapeMap } from '../types/edge';
import {
GShapeStyle,
ItemShapeStyles,
SHAPE_TYPE,
SHAPE_TYPE_3D,
ShapeStyle,
State,
} from '../types/item';
import { NodeDisplayModel, NodeModelData, NodeShapeMap } from '../types/node';
import { getShapeAnimateBeginStyles } from './animate';
import { isArrayOverlap } from './array';
import { isBetween } from './math';
import { cloneJSON } from './data';
import { ComboShapeMap } from 'types/combo';
import { isBetween } from './math';
export const ShapeTagMap = {
circle: Circle,
@ -54,6 +54,7 @@ export const ShapeTagMap = {
image: Image,
path: Path,
group: Group,
html: HTML,
};
const LINE_TYPES = ['line', 'polyline', 'path'];

View File

@ -0,0 +1,9 @@
import { defineConfig } from 'dumi';
export default defineConfig({
outputPath: 'docs-dist',
themeConfig: {
name: '@antv/g6-react-node',
},
mfsu: false,
});

3
packages/react-node/.editorconfig Executable file → Normal file
View File

@ -11,6 +11,3 @@ insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

View File

@ -0,0 +1,3 @@
module.exports = {
extends: require.resolve('@umijs/lint/dist/config/eslint'),
};

View File

@ -1,4 +1,6 @@
export default {
esm: 'rollup',
cjs: 'rollup',
};
import { defineConfig } from 'father';
export default defineConfig({
// more father config: https://github.com/umijs/father/blob/master/docs/config.md
esm: { output: 'dist' },
});

View File

@ -1,22 +1,6 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/npm-debug.log*
/yarn-error.log
/yarn.lock
/package-lock.json
# production
node_modules
/dist
/docs-dist
# misc
.dumi/tmp
.dumi/tmp-test
.dumi/tmp-production
.DS_Store
# umi
.umi
.umi-production
.umi-test
.env.local
.dumi/tmp

View File

@ -1,3 +0,0 @@
docs
src/.umi
docs-dist

View File

@ -1,7 +1,2 @@
**/*.svg
**/*.ejs
**/*.html
package.json
.umi
.umi-production
.umi-test
/dist
*.yaml

View File

@ -1,11 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"overrides": [
{
"files": ".prettierrc",
"options": { "parser": "json" }
}
]
}

View File

@ -0,0 +1,19 @@
module.exports = {
pluginSearchDirs: false,
plugins: [
require.resolve('prettier-plugin-organize-imports'),
require.resolve('prettier-plugin-packagejson'),
],
printWidth: 80,
proseWrap: 'never',
singleQuote: true,
trailingComma: 'all',
overrides: [
{
files: '*.md',
options: {
proseWrap: 'preserve',
},
},
],
};

View File

@ -0,0 +1,3 @@
{
"extends": "@umijs/lint/dist/config/stylelint"
}

View File

@ -1,18 +0,0 @@
import { defineConfig } from 'dumi';
export default defineConfig({
title: 'G6 React Node',
favicon:
'https://gw.alipayobjects.com/zos/antfincdn/cfg5jFqgVt/DiceGraph.png',
logo: 'https://gw.alipayobjects.com/zos/antfincdn/cfg5jFqgVt/DiceGraph.png',
outputPath: 'docs',
locales: [
['zh-CN', '中文'],
['en-US', 'English'],
],
resolve: { includes: ['docs', 'src'] },
dynamicImport: {
loading: '@/Loading',
},
// more config: https://d.umijs.org/config
});

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,90 +1,40 @@
# G6 React Node
# @antv/g6-react-node
> Using React Component to custom your g6 node
[![NPM version](https://img.shields.io/npm/v/@antv/g6-react-node.svg?style=flat)](https://npmjs.org/package/@antv/g6-react-node)
[![NPM downloads](http://img.shields.io/npm/dm/@antv/g6-react-node.svg?style=flat)](https://npmjs.org/package/@antv/g6-react-node)
## Docs
Using React Component to Define Your G6 Graph Node
[https://dicegraph.github.io/](https://dicegraph.github.io/g6-react-node)
## Usage
## Example
TODO
```jsx
import {
Group,
Rect,
Text,
Circle,
Image,
createNodeFromReact,
} from '@antv/g6-react-node';
const ReactNode = ({ cfg = {} }) => {
const { description, meta = {}, label = 'label' } = cfg;
return (
<Group>
<Rect>
<Rect
style={{
width: 150,
height: 20,
fill: cfg.color,
radius: [6, 6, 0, 0],
cursor: 'move',
stroke: cfg.color,
}}
draggable
>
<Text
style={{
marginTop: 2,
marginLeft: 75,
textAlign: 'center',
fontWeight: 'bold',
fill: '#fff',
}}
>
{label}
</Text>
</Rect>
<Rect
style={{
width: 150,
height: 55,
stroke: cfg.color,
fill: '#ffffff',
radius: [0, 0, 6, 6],
}}
>
<Text style={{ marginTop: 5, fill: '#333', marginLeft: 4 }}>
Desc: {description}
</Text>
<Text style={{ marginTop: 10, fill: '#333', marginLeft: 4 }}>
Creator: {meta.creatorName}
</Text>
</Rect>
</Rect>
<Circle
style={{
stroke: cfg.color,
r: 10,
fill: '#fff',
marginLeft: 75,
cursor: 'pointer',
}}
name="circle"
>
<Image
style={{
img:
'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
width: 12,
height: 12,
marginLeft: 69,
marginTop: -5,
}}
/>
</Circle>
</Group>
);
};
G6.registerNode('yourNode', createNodeFromReact(ReactNode));
## Options
TODO
## Development
```bash
# install dependencies
$ pnpm install
# develop library by docs demo
$ pnpm start
# build library source code
$ pnpm run build
# build library source code in watch mode
$ pnpm run build:watch
# build docs
$ pnpm run docs:build
# check your project for potential problems
$ pnpm run doctor
```
## LICENSE
MIT

View File

@ -0,0 +1 @@
This is a guide example.

View File

@ -1,174 +0,0 @@
# Register Node Using React
How about building your G6 node using React Component with correct type inference.
```jsx
import React from 'react';
import G6 from '@antv/g6';
import {
Group,
Rect,
Text,
Circle,
Image,
createNodeFromReact,
} from '@antv/g6-react-node';
import { G6MiniDemo } from '../src/ReactNode/demo';
const ReactNode = ({ cfg = {} }) => {
const { description, meta = {}, label = 'label' } = cfg;
return (
<Group>
<Rect>
<Rect
style={{
width: 150,
height: 20,
fill: cfg.color,
radius: [6, 6, 0, 0],
cursor: 'move',
stroke: cfg.color,
}}
draggable
>
<Text
style={{
marginTop: 2,
marginLeft: 75,
textAlign: 'center',
fontWeight: 'bold',
fill: '#fff',
}}
>
{label}
</Text>
</Rect>
<Rect
style={{
width: 150,
height: 55,
stroke: cfg.color,
fill: '#ffffff',
radius: [0, 0, 6, 6],
}}
>
<Text style={{ marginTop: 5, fill: '#333', marginLeft: 4 }}>
Desc: {description}
</Text>
<Text style={{ marginTop: 10, fill: '#333', marginLeft: 4 }}>
Creator: {meta.creatorName}
</Text>
</Rect>
</Rect>
<Circle
style={{
stroke: cfg.color,
r: 10,
fill: '#fff',
marginLeft: 75,
cursor: 'pointer',
}}
name="circle"
>
<Image
style={{
img:
'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
width: 12,
height: 12,
marginLeft: 69,
marginTop: -5,
}}
/>
</Circle>
</Group>
);
};
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
```jsx | pure
import {
Group,
Rect,
Text,
Circle,
Image,
createNodeFromReact,
} from '@antv/g6-react-node';
const ReactNode = ({ cfg = {} }) => {
const { description, meta = {}, label = 'label' } = cfg;
return (
<Group>
<Rect>
<Rect
style={{
width: 150,
height: 20,
fill: cfg.color,
radius: [6, 6, 0, 0],
cursor: 'move',
stroke: cfg.color,
}}
draggable
>
<Text
style={{
marginTop: 2,
marginLeft: 75,
textAlign: 'center',
fontWeight: 'bold',
fill: '#fff',
}}
>
{label}
</Text>
</Rect>
<Rect
style={{
width: 150,
height: 55,
stroke: cfg.color,
fill: '#ffffff',
radius: [0, 0, 6, 6],
}}
>
<Text style={{ marginTop: 5, fill: '#333', marginLeft: 4 }}>
描述: {description}
</Text>
<Text style={{ marginTop: 10, fill: '#333', marginLeft: 4 }}>
创建者: {meta.creatorName}
</Text>
</Rect>
</Rect>
<Circle
style={{
stroke: cfg.color,
r: 10,
fill: '#fff',
marginLeft: 75,
cursor: 'pointer',
}}
name="circle"
>
<Image
style={{
img:
'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
width: 12,
height: 12,
marginLeft: 69,
marginTop: -5,
}}
/>
</Circle>
</Group>
);
};
G6.registerNode('yourNode', createNodeFromReact(ReactNode));
```

View File

@ -1,170 +1,22 @@
# 用 React 定义节点
---
hero:
title: library
description: Using React Component to Define Your G6 Graph Node
actions:
- text: Hello
link: /
- text: World
link: /
features:
- title: Hello
emoji: 💎
description: Put hello description here
- title: World
emoji: 🌈
description: Put world description here
- title: '!'
emoji: 🚀
description: Put ! description here
---
直接用 React 组件定义你的 G6 组件,自带类型提示。
```jsx
import React from 'react';
import G6 from '@antv/g6';
import {
Group,
Rect,
Text,
Circle,
Image,
createNodeFromReact,
} from '@antv/g6-react-node';
import { G6MiniDemo } from '../src/ReactNode/demo';
const ReactNode = ({ cfg = {} }) => {
const { description, meta = {}, label = 'label' } = cfg;
return (
<Group>
<Rect>
<Rect
style={{
width: 150,
height: 20,
fill: cfg.color,
radius: [6, 6, 0, 0],
cursor: 'move',
stroke: cfg.color,
justifyContent: 'center',
}}
draggable
>
<Text
style={{
margin: [4, 5],
fontWeight: 'bold',
fill: '#fff',
}}
>
{label}
</Text>
</Rect>
<Rect
style={{
width: 150,
height: 55,
stroke: cfg.color,
fill: '#ffffff',
radius: [0, 0, 6, 6],
}}
>
<Text style={{ fill: '#333', margin: [8, 4] }}>
描述: {description}
</Text>
<Text style={{ fill: '#333', margin: [6, 4] }}>
创建者: {meta.creatorName}
</Text>
</Rect>
</Rect>
<Circle
style={{
stroke: cfg.color,
r: 10,
fill: '#fff',
cursor: 'pointer',
margin: [0, 'auto'],
}}
name="circle"
>
<Image
style={{
img:
'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
width: 12,
height: 12,
margin: [4, 'auto'],
}}
/>
</Circle>
</Group>
);
};
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={3} />;
```
```jsx | pure
import {
Group,
Rect,
Text,
Circle,
Image,
createNodeFromReact,
} from '@antv/g6-react-node';
const ReactNode = ({ cfg = {} }) => {
const { description, meta = {}, label = 'label' } = cfg;
return (
<Group>
<Rect>
<Rect
style={{
width: 150,
height: 20,
fill: cfg.color,
radius: [6, 6, 0, 0],
cursor: 'move',
stroke: cfg.color,
justifyContent: 'center',
}}
draggable
>
<Text
style={{
margin: [4, 5],
fontWeight: 'bold',
fill: '#fff',
}}
>
{label}
</Text>
</Rect>
<Rect
style={{
width: 150,
height: 55,
stroke: cfg.color,
fill: '#ffffff',
radius: [0, 0, 6, 6],
}}
>
<Text style={{ marginTop: 5, fill: '#333', margin: [8, 4] }}>
描述: {description}
</Text>
<Text style={{ marginTop: 10, fill: '#333', margin: [6, 4] }}>
创建者: {meta.creatorName}
</Text>
</Rect>
</Rect>
<Circle
style={{
stroke: cfg.color,
r: 10,
fill: '#fff',
cursor: 'pointer',
margin: [0, 'auto'],
}}
name="circle"
>
<Image
style={{
img:
'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
width: 12,
height: 12,
margin: [4, 'auto'],
}}
/>
</Circle>
</Group>
);
};
G6.registerNode('yourNode', createNodeFromReact(ReactNode));
```
@antv/g6-react-node

View File

@ -1,47 +1,53 @@
{
"name": "@antv/g6-react-node",
"version": "2.0.0-beta.0",
"description": "Using React Component to Define Your G6 Graph Node",
"version": "1.4.5",
"scripts": {
"start": "dumi dev",
"build": "father-build",
"deploy": "npm run docs:build && npm run docs:deploy",
"release": "npm run build && npm publish",
"prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"",
"docs:build": "dumi build",
"docs:deploy": "gh-pages -d docs-dist",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
"repository": "https://github.com/antvis/G6.git",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"typings": "dist/index.d.ts",
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,less,md,json}": [
"prettier --write"
],
"*.ts?(x)": [
"prettier --parser=typescript --write"
]
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "father build",
"build:watch": "father dev",
"dev": "dumi dev",
"docs:build": "dumi build",
"doctor": "father doctor",
"prepare": "dumi setup",
"prepublishOnly": "father doctor && npm run build",
"start": "npm run dev"
},
"dependencies": {
"@antv/g6-core": "^0.0.7",
"@antv/g-base": "^0.5.1",
"@types/yoga-layout": "^1.9.3",
"react": "^16.12.0",
"yoga-layout-prebuilt": "^1.10.0"
"@antv/react-g": "^1.10.19",
"@antv/util": "^3.3.5"
},
"devDependencies": {
"@umijs/test": "^3.0.5",
"dumi": "^1.1.1",
"father-build": "^1.17.2",
"gh-pages": "^3.0.0",
"lint-staged": "^10.0.7",
"prettier": "^2.8.8",
"yorkie": "^2.0.0"
}
}
"@ant-design/icons": "^5.2.6",
"@antv/g-canvas": "^1.11.22",
"@antv/g-svg": "^1.10.21",
"@antv/g-webgl": "^1.9.29",
"@antv/g6": "workspace:*",
"@types/react": "^18.2.29",
"@types/react-dom": "^18.2.14",
"antd": "^5.10.2",
"dumi": "^2.2.13",
"father": "^4.3.5",
"prettier": "^3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-packagejson": "^2.4.6",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"peerDependencies": {
"@antv/g": ">=5.18.23",
"@antv/g6": "workspace:*",
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"publishConfig": {
"access": "public"
},
"authors": []
}

View File

@ -1,5 +0,0 @@
import { CircleStyle } from '../ReactNode/Shape/Circle';
import React from 'react';
const Circle: React.FC<CircleStyle> = (props) => <div>{props}</div>;
export default Circle;

View File

@ -1,5 +0,0 @@
import { EllipseStyle } from '../ReactNode/Shape/Ellipse';
import React from 'react';
const Ellipse: React.FC<EllipseStyle> = (props) => <div>{props}</div>;
export default Ellipse;

View File

@ -1,5 +0,0 @@
import React from 'react';
import { EventAttrs } from '../Register/event';
const Event: React.FC<EventAttrs> = (props) => <div>{props}</div>;
export default Event;

View File

@ -1,5 +0,0 @@
import { ImageStyle } from '../ReactNode/Shape/Image';
import React from 'react';
const Image: React.FC<ImageStyle> = (props) => <div>{props}</div>;
export default Image;

View File

@ -1,5 +0,0 @@
import { MarkerStyle } from '../ReactNode/Shape/Marker';
import React from 'react';
const Marker: React.FC<MarkerStyle> = (props) => <div>{props}</div>;
export default Marker;

View File

@ -1,5 +0,0 @@
import { PathStyle } from '../ReactNode/Shape/Path';
import React from 'react';
const Path: React.FC<PathStyle> = (props) => <div>{props}</div>;
export default Path;

View File

@ -1,5 +0,0 @@
import { PolygonStyle } from '../ReactNode/Shape/Polygon';
import React from 'react';
const Polygon: React.FC<PolygonStyle> = (props) => <div>{props}</div>;
export default Polygon;

View File

@ -1,5 +0,0 @@
import { RectStyle } from '../ReactNode/Shape/Rect';
import React from 'react';
const Rect: React.FC<RectStyle> = (props) => <div {...props} />;
export default Rect;

View File

@ -1,5 +0,0 @@
import { TextStyle } from '../ReactNode/Shape/Text';
import React from 'react';
const Text: React.FC<TextStyle> = (props) => <div>{props}</div>;
export default Text;

View File

@ -1,28 +0,0 @@
# Circle Style
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Circle, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Circle
style={{
r: 45,
fill: cfg.color,
radius: [6, 6, 0, 0],
cursor: 'move',
stroke: '#eee',
}}
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./CircleStyle.tsx" ></API>

View File

@ -1,29 +0,0 @@
# 圆 (Circle) 样式属性
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Circle, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Circle
style={{
r: 45,
fill: cfg.color,
radius: [6, 6, 0, 0],
cursor: 'move',
stroke: '#eee',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./CircleStyle.tsx" ></API>

View File

@ -1,29 +0,0 @@
# Ellipse Style
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Ellipse, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Ellipse
style={{
rx: 45,
ry: 20,
fill: cfg.color,
cursor: 'move',
stroke: '#888',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./EllipseStyle.tsx" ></API>

View File

@ -1,29 +0,0 @@
# 椭圆 (Ellipse) 样式属性
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Ellipse, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Ellipse
style={{
rx: 45,
ry: 20,
fill: cfg.color,
cursor: 'move',
stroke: '#888',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./EllipseStyle.tsx" ></API>

View File

@ -1,14 +0,0 @@
# Event Props
Every Shape has a event to respond to it, which will not cause propagation.
```typescript
type ShapeEventListner = (
event: IG6GraphEvent,
node: INode | null,
shape: IShape,
graph: Graph,
) => void;
```
<API src="./Event.tsx" ></API>

View File

@ -1,14 +0,0 @@
# 事件Event属性
每一个形状都会有单独的事件响应,他们之间不存在冒泡触发逻辑;
```typescript
type ShapeEventListner = (
event: IG6GraphEvent,
node: INode | null,
shape: IShape,
graph: Graph,
) => void;
```
<API src="./Event.tsx" ></API>

View File

@ -1,28 +0,0 @@
# Image Style
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Image, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Image
style={{
img: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
width: 48,
height: 48,
cursor: 'move',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./ImageStyle.tsx" ></API>

View File

@ -1,28 +0,0 @@
# 图片 (Image) 样式属性
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Image, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Image
style={{
img: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
width: 48,
height: 48,
cursor: 'move',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./ImageStyle.tsx" ></API>

View File

@ -1,5 +0,0 @@
# Group & Common
Every component should be wrapped in a Group component, it's a way to group shape.
<API src="../ReactNode/Group.tsx"></API>

View File

@ -1,5 +0,0 @@
# 图形共有属性 与 Group(组)
我们建议每一个单元的图形,都应该有序的存放在一个 Group 里,一个节点组件,应该被 Group 包裹
<API src="../ReactNode/Group.tsx"></API>

View File

@ -1,31 +0,0 @@
# Marker Style
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Marker, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Marker
style={{
r: 40,
symbol: function(x, y, r) {
return [['M', x, y], ['L', x + r, y + r], ['L', x + r * 2, y], ['Z']];
},
fill: cfg.color,
cursor: 'move',
stroke: '#888',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./MarkerStyle.tsx" ></API>

View File

@ -1,31 +0,0 @@
# 标记 (Marker) 样式属性
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Marker, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Marker
style={{
r: 40,
symbol: function(x, y, r) {
return [['M', x, y], ['L', x + r, y + r], ['L', x + r * 2, y], ['Z']];
},
fill: cfg.color,
cursor: 'move',
stroke: '#888',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./MarkerStyle.tsx" ></API>

View File

@ -1,34 +0,0 @@
# Path Style
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Path, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Path
style={{
path: [
['M', 0, 0],
['L', 20, -7.5],
['L', 13.33, 0],
['L', 20, 7.5],
['Z'],
],
fill: cfg.color,
cursor: 'move',
stroke: '#888',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./PathStyle.tsx" ></API>

View File

@ -1,34 +0,0 @@
# 路径 (Path) 样式属性
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Path, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Path
style={{
path: [
['M', 0, 0],
['L', 20, -7.5],
['L', 13.33, 0],
['L', 20, 7.5],
['Z'],
],
fill: cfg.color,
cursor: 'move',
stroke: '#888',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./PathStyle.tsx" ></API>

View File

@ -1,34 +0,0 @@
# Polygon Style
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Polygon, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Polygon
style={{
points: [
[0, 0],
[10, 30],
[50, 100],
[30, 120],
[-20, 80],
],
fill: cfg.color,
cursor: 'move',
stroke: '#888',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./PolygonStyle.tsx" ></API>

View File

@ -1,34 +0,0 @@
# 多边形 (Polygon) 样式属性
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Polygon, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Polygon
style={{
points: [
[0, 0],
[10, 30],
[50, 100],
[30, 120],
[-20, 80],
],
fill: cfg.color,
cursor: 'move',
stroke: '#888',
}}
draggable
/>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./PolygonStyle.tsx" ></API>

View File

@ -1,45 +0,0 @@
# Rect Style
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Rect, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => {
const { description, meta = {}, label = 'label' } = cfg;
return (
<Group>
<Rect>
<Rect
style={{
width: 150,
height: 20,
fill: cfg.color,
radius: [6, 6, 0, 0],
cursor: 'move',
stroke: cfg.color,
}}
draggable
/>
<Rect
style={{
width: 150,
height: 55,
stroke: cfg.color,
fill: '#ffffff',
radius: [0, 0, 6, 6],
}}
/>
</Rect>
</Group>
);
};
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./RectStyle.tsx" ></API>

View File

@ -1,45 +0,0 @@
# 矩形 (Rect) 样式属性
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Rect, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => {
const { description, meta = {}, label = 'label' } = cfg;
return (
<Group>
<Rect>
<Rect
style={{
width: 150,
height: 20,
fill: cfg.color,
radius: [6, 6, 0, 0],
cursor: 'move',
stroke: cfg.color,
}}
draggable
/>
<Rect
style={{
width: 150,
height: 55,
stroke: cfg.color,
fill: '#ffffff',
radius: [0, 0, 6, 6],
}}
/>
</Rect>
</Group>
);
};
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./RectStyle.tsx" ></API>

View File

@ -1,31 +0,0 @@
# Text Style
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Text, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Text
style={{
fontSize: 25,
fontWeight: 500,
fill: cfg.color,
cursor: 'move',
stroke: '#888',
}}
draggable
>
Text
</Text>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./TextStyle.tsx" ></API>

View File

@ -1,31 +0,0 @@
# 文本 (Text) 样式属性
```jsx
import React from 'react';
import G6 from '@antv/g6';
import { Group, Text, createNodeFromReact } from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const ReactNode = ({ cfg = {} }) => (
<Group>
<Text
style={{
fontSize: 25,
fontWeight: 500,
fill: cfg.color,
cursor: 'move',
stroke: '#888',
}}
draggable
>
Text
</Text>
</Group>
);
G6.registerNode('test', createNodeFromReact(ReactNode));
export default () => <G6MiniDemo nodeType="test" count={2} />;
```
<API src="./TextStyle.tsx" ></API>

View File

@ -1,22 +0,0 @@
import { AbstractShape, AnimateCfg } from '@antv/g-canvas';
import { animations } from './animateFunc';
export type AnimationConfig = AnimateCfg & { animate: keyof typeof animations };
export const animateShapeWithConfig = (
shape: AbstractShape,
config?: Partial<AnimationConfig>,
initMatrix?: number[],
) => {
const animateFunc = config?.animate && animations[config.animate];
if (config && animateFunc) {
const cfg = {
duration: 2000,
...config,
initMatrix,
};
shape.animate(animateFunc, cfg);
} else {
shape.stopAnimate();
}
};

View File

@ -1,158 +0,0 @@
import { Util } from '@antv/g6-core';
type RatioUnit = [number, number] | [number[], number];
const getRatioByArray = (arr: RatioUnit[], ratio: number) => {
let usingArr: [number, number][] = [];
arr.forEach((item) => {
if (item[0] instanceof Array) {
item[0].forEach((subR) => usingArr.push([subR, item[1]]));
} else {
usingArr.push([Number(item[0]), item[1]]);
}
});
usingArr = usingArr.sort((a, b) => a[0] - b[0]);
for (let i = 0; i < usingArr.length; i++) {
const now = usingArr[i];
const next = usingArr[i + 1];
if (!next) {
return now[1];
}
if (ratio > now[0] && ratio <= next[0]) {
const deltaRatio = ratio - now[0];
const allRatio = next[0] - now[0];
const deltaVal = next[1] - now[1];
return now[1] + (deltaVal * deltaRatio) / allRatio;
}
}
};
export const animations = {
spin: (ratio: number) => {
const toMatrix = Util.transform(
[1, 0, 0, 0, 1, 0, 0, 0, 1],
[['r', Math.PI * 2 * ratio]],
);
return {
matrix: toMatrix,
};
},
flash: (ratio: number) => ({ opacity: Math.abs(1 - ratio * 2) }),
pulse: (ratio: number) => {
const uRatio = getRatioByArray(
[
[[0, 1], 1],
[0.5, 1.25],
],
ratio,
);
const toMatrix = Util.transform(
[1, 0, 0, 0, 1, 0, 0, 0, 1],
[['s', uRatio, uRatio]],
);
return {
matrix: toMatrix,
};
},
rubber: (ratio: number) => {
const xratio = getRatioByArray(
[
[0, 1],
[0.3, 1.25],
[0.4, 0.75],
[0.5, 1.15],
[0.65, 0.95],
[0.75, 1.05],
[1, 1],
],
ratio,
);
const yratio = getRatioByArray(
[
[0, 1],
[0.3, 0.75],
[0.4, 1.25],
[0.5, 0.95],
[0.65, 1.05],
[0.75, 0.95],
[1, 1],
],
ratio,
);
const toMatrix = Util.transform(
[1, 0, 0, 0, 1, 0, 0, 0, 1],
[['s', xratio, yratio]],
);
return {
matrix: toMatrix,
};
},
tada: (ratio: number) => {
const scaleRatio = getRatioByArray(
[
[0, 1],
[[0.1, 0.2], 0.9],
[[0.8, 0.9], 1.1],
[1, 1],
],
ratio,
);
const tadaRatio = getRatioByArray(
[
[0, 0],
[[0.3, 0.5, 0.7, 0.9], 1],
[[0.1, 0.2, 0.4, 0.6, 0.8], -1],
[1, 0],
],
ratio,
);
const toMatrix = Util.transform(
[1, 0, 0, 0, 1, 0, 0, 0, 1],
[
['s', scaleRatio, scaleRatio],
['r', ((tadaRatio || 0) * Math.PI) / 60],
],
);
return {
matrix: toMatrix,
};
},
bounce: (ratio: number) => {
const yNum = getRatioByArray(
[
[[0, 1], 0],
[[0.4, 0.43], -30],
[0.7, -15],
[0.8, 10],
[0.9, -3],
],
ratio,
);
const scaleY = getRatioByArray(
[
[[0, 1], 1],
[[0.4, 0.43], 1.1],
[0.7, 1.05],
[0.8, 0.95],
[0.9, 1.02],
],
ratio,
);
const toMatrix = Util.transform(
[1, 0, 0, 0, 1, 0, 0, 0, 1],
[
['t', 0, yNum],
['s', 1, scaleY],
],
);
return {
matrix: toMatrix,
};
},
};

View File

@ -1,92 +0,0 @@
# 动画使用案例
> 这是一个简单的形状事件绑定案例, 点击右上角按钮展开节点
```jsx
import React from 'react';
import G6 from '@antv/g6';
import {
Rect,
Text,
Circle,
Image,
Group,
createNodeFromReact,
} from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const Tag = ({ text, color }) => (
<Rect
style={{
fill: color,
padding: [5, 10],
width: 'auto',
radius: [4],
margin: [0, 8],
}}
>
<Text style={{ fill: '#fff', fontSize: 10 }}>{text}</Text>
</Rect>
);
const Card = ({ cfg }) => {
const { animated = false } = cfg;
return (
<Group draggable>
<Rect
style={{
width: 400,
height: 'auto',
fill: '#fff',
stroke: '#ddd',
shadowColor: '#eee',
shadowBlur: 30,
radius: [8],
justifyContent: 'center',
padding: [18, 0],
}}
draggable
animation={
animated && {
animate: 'rubber',
repeat: true,
duration: 2000,
}
}
>
<Text
style={{
fill: '#000',
margin: [0, 24],
fontSize: 16,
fontWeight: 'bold',
cursor: 'pointer',
}}
onClick={(evt, node, shape, graph) => {
graph.updateItem(node, {
animated: !animated,
});
}}
animation={
animated && {
animate: 'flash',
repeat: true,
duration: 2000,
}
}
>
点我{animated ? '暂停' : '看'}动画
</Text>
<Text style={{ fill: '#ccc', fontSize: 12, margin: [12, 24] }}>
我是一段特别特别特别特别特别特别特别长的描述
</Text>
</Rect>
</Group>
);
};
G6.registerNode('test', createNodeFromReact(Card));
export default () => <G6MiniDemo nodeType="test" count={1} height={400} />;
```

View File

@ -1,95 +0,0 @@
# Simple Card Example
> This is a simple card example
```jsx
import React from 'react';
import G6 from '@antv/g6';
import {
Rect,
Text,
Circle,
Image,
Group,
createNodeFromReact,
} from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const Tag = ({ text, color }) => (
<Rect
style={{
fill: color,
padding: [5, 10],
width: 'auto',
radius: [4],
margin: [0, 8],
}}
>
<Text style={{ fill: '#fff' }}>{text}</Text>
</Rect>
);
const Card = () => {
return (
<Group>
<Rect
style={{
width: 400,
height: 'auto',
fill: '#fff',
stroke: '#ddd',
shadowColor: '#eee',
shadowBlur: 30,
radius: [8],
justifyContent: 'center',
padding: [18, 0],
}}
draggable
>
<Text
style={{
fill: '#000',
margin: [0, 24],
fontSize: 16,
fontWeight: 'bold',
}}
>
This is a card
</Text>
<Text style={{ fill: '#ccc', fontSize: 12, margin: [12, 24] }}>
I'm a very very very very very very very long description.
</Text>
<Image
style={{
img:
'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg',
width: 200,
height: 200,
margin: [24, 'auto'],
}}
/>
<Rect style={{ width: 'auto', flexDirection: 'row', padding: [4, 12] }}>
<Tag color="#66ccff" text="This" />
<Tag color="#66ccff" text="is a card" />
<Tag color="#66ccff" text="definitely" />
<Tag color="#66ccff" text="Tag" />
</Rect>
<Circle
style={{
position: 'absolute',
x: 380,
y: 20,
r: 5,
fill: 'red',
cursor: 'pointer',
}}
/>
</Rect>
</Group>
);
};
G6.registerNode('test', createNodeFromReact(Card));
export default () => <G6MiniDemo nodeType="test" count={1} height={400} />;
```

View File

@ -1,94 +0,0 @@
# 简单卡片案例
> 这是一个简单的卡片案例
```jsx
import React from 'react';
import G6 from '@antv/g6';
import {
Rect,
Text,
Circle,
Image,
Group,
createNodeFromReact,
} from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const Tag = ({ text, color }) => (
<Rect
style={{
fill: color,
padding: [5, 10],
width: 'auto',
radius: [4],
margin: [0, 8],
}}
>
<Text style={{ fill: '#fff' }}>{text}</Text>
</Rect>
);
const Card = () => {
return (
<Group>
<Rect
style={{
width: 400,
height: 'auto',
fill: '#fff',
stroke: '#ddd',
shadowColor: '#eee',
shadowBlur: 30,
radius: [8],
justifyContent: 'center',
padding: [18, 0],
}}
draggable
>
<Text
style={{
fill: '#000',
margin: [0, 24],
fontSize: 16,
fontWeight: 'bold',
}}
>
这是一个卡片
</Text>
<Text style={{ fill: '#ccc', fontSize: 12, margin: [12, 24] }}>
我是一段特别特别特别特别特别特别特别长的描述
</Text>
<Image
style={{
img:
'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg',
width: 200,
height: 200,
margin: [24, 'auto'],
}}
/>
<Rect style={{ width: 'auto', flexDirection: 'row', padding: [4, 12] }}>
{
["我是", "很多个", "很多个的", "标签"].map(e => <Tag color="#66ccff" text={e} />)
}
</Rect>
<Circle
style={{
position: 'absolute',
x: 380,
y: 20,
r: 5,
fill: 'red',
cursor: 'pointer',
}}
/>
</Rect>
</Group>
);
};
G6.registerNode('test', createNodeFromReact(Card));
export default () => <G6MiniDemo nodeType="test" count={1} height={400} />;
```

View File

@ -1,118 +0,0 @@
# Event Usage Example
> This is a simple usage examplr with event
```jsx
import React from 'react';
import G6 from '@antv/g6';
import {
Rect,
Text,
Circle,
Image,
Group,
createNodeFromReact,
} from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const Tag = ({ text, color }) => (
<Rect
style={{
fill: color,
padding: [5, 10],
width: 'auto',
radius: [4],
margin: [0, 8],
}}
>
<Text style={{ fill: '#fff' }}>{text}</Text>
</Rect>
);
const Card = ({ cfg }) => {
const { collapsed = false } = cfg;
return (
<Group draggable>
<Rect
style={{
width: 400,
height: 'auto',
fill: '#fff',
stroke: '#ddd',
shadowColor: '#eee',
shadowBlur: 30,
radius: [8],
justifyContent: 'center',
padding: [18, 0],
}}
draggable
>
<Text
style={{
fill: '#000',
margin: [0, 24],
fontSize: 16,
fontWeight: 'bold',
}}
>
这是一个卡片
</Text>
<Text style={{ fill: '#ccc', fontSize: 12, margin: [12, 24] }}>
我是一段特别特别特别特别特别特别特别长的描述
</Text>
{collapsed && (
<Group>
<Image
style={{
img:
'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg',
width: 200,
height: 200,
margin: [24, 'auto'],
}}
/>
<Rect
style={{ width: 'auto', flexDirection: 'row', padding: [4, 12] }}
>
<Tag color="#66ccff" text="我是" />
<Tag color="#66ccff" text="很多个" />
<Tag color="#66ccff" text="很多个的" />
<Tag color="#66ccff" text="标签" />
</Rect>
</Group>
)}
<Circle
style={{
position: 'absolute',
x: 380,
y: 20,
r: 5,
fill: collapsed ? 'blue' : 'green',
}}
>
<Text
style={{
fill: '#fff',
fontSize: 10,
margin: [-6, -3, 0],
cursor: 'pointer',
}}
onClick={(evt, node, shape, graph) => {
graph.updateItem(node, {
collapsed: !collapsed,
});
}}
>
{collapsed ? '-' : '+'}
</Text>
</Circle>
</Rect>
</Group>
);
};
G6.registerNode('test', createNodeFromReact(Card));
export default () => <G6MiniDemo nodeType="test" count={1} height={400} />;
```

View File

@ -1,118 +0,0 @@
# 事件使用示例
> 这是一个简单的形状事件绑定案例, 点击右上角按钮展开节点
```jsx
import React from 'react';
import G6 from '@antv/g6';
import {
Rect,
Text,
Circle,
Image,
Group,
createNodeFromReact,
} from '@antv/g6-react-node';
import { G6MiniDemo } from '../ReactNode/demo';
const Tag = ({ text, color }) => (
<Rect
style={{
fill: color,
padding: [5, 10],
width: 'auto',
radius: [4],
margin: [0, 8],
}}
>
<Text style={{ fill: '#fff', fontSize: 10 }}>{text}</Text>
</Rect>
);
const Card = ({ cfg }) => {
const { collapsed = false } = cfg;
return (
<Group draggable>
<Rect
style={{
width: 400,
height: 'auto',
fill: '#fff',
stroke: '#ddd',
shadowColor: '#eee',
shadowBlur: 30,
radius: [8],
justifyContent: 'center',
padding: [18, 0],
}}
draggable
>
<Text
style={{
fill: '#000',
margin: [0, 24],
fontSize: 16,
fontWeight: 'bold',
}}
>
这是一个卡片
</Text>
<Text style={{ fill: '#ccc', fontSize: 12, margin: [12, 24] }}>
我是一段特别特别特别特别特别特别特别长的描述
</Text>
{collapsed && (
<Group>
<Image
style={{
img:
'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg',
width: 200,
height: 200,
margin: [24, 'auto'],
}}
/>
<Rect
style={{ width: 'auto', flexDirection: 'row', padding: [4, 12] }}
>
<Tag color="#66ccff" text="我是" />
<Tag color="#66ccff" text="很多个" />
<Tag color="#66ccff" text="很多个的" />
<Tag color="#66ccff" text="标签" />
</Rect>
</Group>
)}
<Circle
style={{
position: 'absolute',
x: 380,
y: 20,
r: 5,
fill: collapsed ? 'blue' : 'green',
}}
>
<Text
style={{
fill: '#fff',
fontSize: 10,
margin: [-6, -3, 0],
cursor: 'pointer',
}}
onClick={(evt, node, shape, graph) => {
graph.updateItem(node, {
collapsed: !collapsed,
});
}}
>
{collapsed ? '-' : '+'}
</Text>
</Circle>
</Rect>
</Group>
);
};
G6.registerNode('test', createNodeFromReact(Card));
export default () => <G6MiniDemo nodeType="test" count={1} height={400} />;
```

View File

@ -0,0 +1,116 @@
# React G Node
基于 @antv/react-g 的 G6 React Node 节点。
```jsx
import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons';
import { Button, Card, Col, Row, Statistic } from 'antd';
import { createReactGNode, Circle, Rect, Text } from '@antv/g6-react-node';
import { Graph, extend, Extensions } from '@antv/g6';
import { useEffect, useRef, useState } from 'react';
import type { ReactNodeProps } from '@antv/g6-react-node';
const Node = ({ model }) => {
const { data } = model;
const { value } = data;
const [showShadow, setShowShadow] = useState(false);
return (
<Rect
width={50}
height={50}
shadowBlur={showShadow ? 10 : 0}
shadowColor="#bebebe"
lineWidth={showShadow}
fill={'rgba(0, 255, 0, 0.5)'}
onMouseenter={() => setShowShadow(true)}
onMouseleave={() => setShowShadow(false)}
>
<Circle cx={25} cy={25} r={20 * value} fill="rgb(255, 255, 0, 0.9)" />
<Text
x={25}
y={25}
fill="red"
text={value.toFixed(2)}
textAlign="center"
textBaseline="middle"
/>
</Rect>
);
};
export default () => {
const ref = useRef();
const graphRef = useRef(null);
useEffect(() => {
if (!ref.current) return;
const container = ref.current;
const width = container.scrollWidth;
const height = container.scrollHeight || 500;
const reactGNode = createReactGNode(Node);
const ExtendGraph = extend(Graph, {
nodes: {
'react-g-node': reactGNode,
},
});
graphRef.current = new ExtendGraph({
container,
width,
height,
modes: {
default: [
{
type: 'drag-node',
enableTransient: false,
},
],
},
data: {
nodes: [
{ id: 'node0', data: { size: [50, 50], value: 0.5 } },
{ id: 'node1', data: { size: [50, 50], value: 0.9 } },
{ id: 'node2', data: { size: [50, 50], value: 0.7 } },
],
edges: [
{ id: 'edge1', source: 'node0', target: 'node1', data: {} },
{ id: 'edge2', source: 'node0', target: 'node2', data: {} },
],
},
node: {
type: 'react-g-node',
otherShapes: {},
},
});
return () => {
graphRef.current.destroy();
};
}, []);
return (
<div style={{ width: '100%', height: 500 }}>
<div>
<Button
onClick={() => {
Array.from({ length: 3 }).forEach((_, i) => {
graphRef.current.updateData('node', {
id: `node${i}`,
data: {
size: [50, 50],
value: Math.random() * 0.5 + 0.5,
},
});
});
}}
>
更新数据
</Button>
</div>
<div ref={ref}></div>
</div>
);
};
```

View File

@ -0,0 +1,3 @@
export * from './layouts';
export { createReactGNode } from './node';
export * from './shapes';

View File

@ -0,0 +1,4 @@
import { register } from './register';
export type { Layout } from './typings';
register();

View File

@ -0,0 +1,11 @@
// import { CSS } from '@antv/g';
// import { Layout as BlockFlowLayout } from '@antv/g-layout-blocklike';
// import { Plugin as PluginYoga } from '@antv/g-plugin-yoga';
import { Renderer } from '../typings';
export const register = (renderer?: Renderer) => {
// CSS.registerLayout('block', BlockFlowLayout);
// if (renderer) {
// renderer.registerPlugin(new PluginYoga({}));
// }
};

View File

@ -0,0 +1,2 @@
export type Layout = 'block' | 'flex';

View File

@ -0,0 +1,97 @@
import type { DisplayObject } from '@antv/g';
import type { ComboModelData, NodeDisplayModel, NodeModelData } from '@antv/g6';
import { Extensions } from '@antv/g6';
import type { State } from '@antv/g6/lib/types/item';
import { NodeShapeMap } from '@antv/g6/lib/types/node';
import { render } from '@antv/react-g';
import React from 'react';
const Ctors = {
rect: Extensions.RectNode,
circle: Extensions.CircleNode,
} as const;
export const createReactGNode = (
Component: (props: {
model?: NodeDisplayModel;
states?: State[];
}) => React.ReactElement,
shape: keyof typeof Ctors = 'rect',
): any => {
const Ctor: any = Ctors[shape];
class GNode extends Ctor {
drawKeyShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?:
| {
previous: NodeModelData | ComboModelData;
current: NodeModelData | ComboModelData;
}
| undefined,
diffState?: { previous: State[]; current: State[] } | undefined,
): DisplayObject<any, any> {
const { data } = model;
const { size: [width, height] = [0, 0] } = data as any;
return this.upsertShape(
shape,
'keyShape',
{
x: -width / 2,
y: -height / 2,
width,
height,
fill: 'opacity',
stroke: 'opacity',
lineWidth: 0,
},
{ shapeMap, model, diffData, diffState },
);
}
drawOtherShapes(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?:
| {
previous: NodeModelData | ComboModelData;
current: NodeModelData | ComboModelData;
}
| undefined,
diffState?: { previous: State[]; current: State[] } | undefined,
): { [id: string]: DisplayObject<any, any> } {
const { id, data } = model;
const {
size: [width, height],
} = data as any;
const groupId = `${id}-group`;
const group = this.upsertShape(
'group',
groupId,
{
x: -width / 2,
y: -height / 2,
width,
height,
},
{ shapeMap, model, diffData, diffState },
);
group.isMutationObserved = true;
group.addEventListener('DOMNodeInsertedIntoDocument', () => {
const content = (
<Component model={model} states={diffState?.current || []} />
);
render(content, group);
});
group.addEventListener('destroy', () => {
group.removeAllEventListeners();
});
return {
[groupId]: group,
};
}
}
return GNode;
};

View File

@ -0,0 +1,29 @@
import {
Arrow,
Circle,
Ellipse,
Group,
HTML,
Image,
Line,
Path,
Polygon,
Polyline,
Rect,
Text,
} from '@antv/react-g';
export {
Arrow,
Circle,
Ellipse,
Group,
HTML,
Image,
Line,
Path,
Polygon,
Polyline,
Rect,
Text,
};

View File

@ -0,0 +1,5 @@
import type { Renderer as CanvasRenderer } from '@antv/g-canvas';
import type { Renderer as SVGRenderer } from '@antv/g-svg';
import type { Renderer as WebGLRenderer } from '@antv/g-webgl';
export type Renderer = CanvasRenderer | SVGRenderer | WebGLRenderer;

View File

@ -1,64 +0,0 @@
import * as Yoga from 'yoga-layout-prebuilt';
export const LayoutAlignMap = {
auto: Yoga.ALIGN_AUTO,
baseline: Yoga.ALIGN_BASELINE,
center: Yoga.ALIGN_CENTER,
'flex-end': Yoga.ALIGN_FLEX_END,
'flex-start': Yoga.ALIGN_FLEX_START,
'space-around': Yoga.ALIGN_SPACE_AROUND,
'space-between': Yoga.ALIGN_SPACE_BETWEEN,
stretch: Yoga.ALIGN_STRETCH,
};
export const DisplayMap = {
none: Yoga.DISPLAY_NONE,
flex: Yoga.DISPLAY_FLEX,
};
export const FlexDirectionMap = {
column: Yoga.FLEX_DIRECTION_COLUMN,
'column-reverse': Yoga.FLEX_DIRECTION_COLUMN_REVERSE,
row: Yoga.FLEX_DIRECTION_ROW,
'row-reverse': Yoga.FLEX_DIRECTION_ROW_REVERSE,
};
export const FlexWrapMap = {
'no-wrap': Yoga.WRAP_NO_WRAP,
wrap: Yoga.WRAP_WRAP,
'wrap-reverse': Yoga.WRAP_WRAP_REVERSE,
};
export const JustifyContentMap = {
center: Yoga.JUSTIFY_CENTER,
'flex-end': Yoga.JUSTIFY_FLEX_END,
'flex-start': Yoga.JUSTIFY_FLEX_START,
'space-around': Yoga.JUSTIFY_SPACE_AROUND,
'space-between': Yoga.JUSTIFY_SPACE_BETWEEN,
'space-evenly': Yoga.JUSTIFY_SPACE_EVENLY,
};
export type NumberOrAuto = number | string | 'auto';
export interface LayoutAttrs {
alignContent: keyof typeof LayoutAlignMap;
alignItems: keyof typeof LayoutAlignMap;
alignSelf: keyof typeof LayoutAlignMap;
display: keyof typeof DisplayMap;
flex: number;
flexBasis: number | string;
flexGrow: number;
flexShrink: number;
flexDirection: keyof typeof FlexDirectionMap;
flexWrap: keyof typeof FlexWrapMap;
height: NumberOrAuto;
width: NumberOrAuto;
justifyContent: keyof typeof JustifyContentMap;
margin: NumberOrAuto | NumberOrAuto[];
padding: number | string | (number | string)[];
maxHeight: number;
maxWidth: number;
minHeight: number;
minWidth: number;
onClick: (e: Event) => void;
}

View File

@ -1,282 +0,0 @@
import Yoga, { Node, YogaNode } from 'yoga-layout-prebuilt';
import { RawNode } from '../Register/getDataFromReactNode';
import getSizeOfShape from './getShapeSize';
import {
DisplayMap,
FlexDirectionMap,
FlexWrapMap,
JustifyContentMap,
LayoutAlignMap,
} from './LayoutEnums';
const getFourFromNumOrArr = (target: string | number | (string | number)[]) => {
if (target instanceof Array) {
switch (target.length) {
case 1:
const m = target[0];
return [m, m, m, m];
case 2:
const [tb, lr] = target;
return [tb, lr, tb, lr];
case 3:
const [t, lar, b] = target;
return [t, lar, b, lar];
case 4:
return target;
default:
return [0, 0, 0, 0];
}
} else {
return [target, target, target, target];
}
};
const constructYogaNode = (node: RawNode) => {
const yogaNode = Node.create();
const style = node.attrs;
if (style.position === 'absolute') {
yogaNode.setWidth(0);
yogaNode.setHeight(0);
return yogaNode;
}
if (style.alignContent) {
yogaNode.setAlignContent(LayoutAlignMap[style.alignContent]);
}
if (style.alignItems) {
yogaNode.setAlignItems(LayoutAlignMap[style.alignItems]);
}
if (style.alignSelf) {
yogaNode.setAlignSelf(LayoutAlignMap[style.alignSelf]);
}
if (style.display) {
yogaNode.setDisplay(DisplayMap[style.display]);
} else {
yogaNode.setDisplay(DisplayMap['flex']);
}
if (style.flex) {
yogaNode.setFlex(style.flex);
}
if (style.flexBasis) {
yogaNode.setFlexBasis(style.flexBasis);
}
if (style.flexGrow) {
yogaNode.setFlexGrow(style.flexGrow);
}
if (style.flexShrink) {
yogaNode.setFlexShrink(style.flexShrink);
}
if (style.flexDirection) {
yogaNode.setFlexDirection(FlexDirectionMap[style.flexDirection]);
}
if (style.flexWrap) {
yogaNode.setFlexWrap(FlexWrapMap[style.flexWrap]);
}
if (style.justifyContent) {
yogaNode.setJustifyContent(JustifyContentMap[style.justifyContent]);
}
if (style.maxHeight) {
yogaNode.setMaxHeight(style.maxHeight);
}
if (style.minHeight) {
yogaNode.setMinHeight(style.minHeight);
}
if (style.maxWidth) {
yogaNode.setMaxWidth(style.maxWidth);
}
if (style.minWidth) {
yogaNode.setMinWidth(style.minWidth);
}
if (style.height) {
if (style.height === 'auto') {
yogaNode.setHeightAuto();
} else {
yogaNode.setHeight(style.height);
}
}
if (style.width) {
if (style.width === 'auto') {
yogaNode.setWidthAuto();
} else {
yogaNode.setWidth(style.width);
}
}
if (style.margin) {
const marginArray = getFourFromNumOrArr(style.margin);
if (marginArray[0] === 'auto') {
yogaNode.setMarginAuto(Yoga.EDGE_TOP);
} else {
yogaNode.setMargin(Yoga.EDGE_TOP, Number(marginArray[0]));
}
if (marginArray[1] === 'auto') {
yogaNode.setMarginAuto(Yoga.EDGE_RIGHT);
} else {
yogaNode.setMargin(Yoga.EDGE_RIGHT, Number(marginArray[1]));
}
if (marginArray[2] === 'auto') {
yogaNode.setMarginAuto(Yoga.EDGE_BOTTOM);
} else {
yogaNode.setMargin(Yoga.EDGE_BOTTOM, Number(marginArray[2]));
}
if (marginArray[3] === 'auto') {
yogaNode.setMarginAuto(Yoga.EDGE_LEFT);
} else {
yogaNode.setMargin(Yoga.EDGE_LEFT, Number(marginArray[3]));
}
}
if (style.padding) {
const paddingArray = getFourFromNumOrArr(style.padding);
yogaNode.setPadding(Yoga.EDGE_TOP, paddingArray[0]);
yogaNode.setPadding(Yoga.EDGE_RIGHT, paddingArray[1]);
yogaNode.setPadding(Yoga.EDGE_BOTTOM, paddingArray[2]);
yogaNode.setPadding(Yoga.EDGE_LEFT, paddingArray[3]);
}
return yogaNode;
};
export interface LayoutedNode extends RawNode {
boundaryBox: {
width: number;
height: number;
x: number;
y: number;
};
children: LayoutedNode[];
}
type ContainerNode = RawNode & {
container: YogaNode;
children: ContainerNode[];
};
const constructNodes = (
root: RawNode,
basicContainer: YogaNode,
): ContainerNode | null => {
const childrenArr = [[root]];
const parentArr: ContainerNode[] = [];
let resultNode: ContainerNode | null = null;
while (childrenArr[0]) {
const children = childrenArr.pop() || [];
const parent = parentArr.pop();
const newChilren: ContainerNode[] = [];
for (let i = 0; i < children?.length; i += 1) {
const node = children[i];
const size = getSizeOfShape(node.type, node.attrs);
if (!node.attrs.width) {
node.attrs.width = size.width || 0;
}
if (!node.attrs.height) {
node.attrs.height = size.height || 0;
}
const containerNode: ContainerNode = {
...node,
container: constructYogaNode(node),
children: [],
};
if (node.children.length) {
parentArr.push(containerNode);
childrenArr.push(node.children);
}
newChilren.push(containerNode);
}
if (!parent) {
resultNode = newChilren[0];
basicContainer.insertChild(newChilren[0].container, 0);
} else {
parent.children = newChilren;
for (let j = 0; j < parent.children.length; j += 1) {
parent.container.insertChild(parent.children[j].container, j);
}
}
}
return resultNode;
};
const caculateNodes = (
node: ContainerNode,
parentBoundaryBox: {
width: number;
height: number;
x: number;
y: number;
},
): LayoutedNode => {
const boundaryBox = {
width: Number(node.attrs.width) || 0,
height: Number(node.attrs.height) || 0,
x: 0,
y: 0,
};
let actualBondary = { ...boundaryBox };
const { container, ...restNode } = node;
if (restNode.attrs.position === 'absolute') {
boundaryBox.x = restNode.attrs.x;
boundaryBox.y = restNode.attrs.y;
actualBondary = boundaryBox;
} else if (container) {
const layout = container.getComputedLayout();
boundaryBox.width = layout.width;
boundaryBox.height = layout.height;
boundaryBox.x = layout.left + parentBoundaryBox.x;
boundaryBox.y = layout.top + parentBoundaryBox.y;
actualBondary = { ...boundaryBox };
if (['circle', 'ellipse'].includes(restNode.type)) {
boundaryBox.x += boundaryBox.width / 2;
boundaryBox.y += boundaryBox.height / 2;
}
if (restNode.type === 'text') {
boundaryBox.y += boundaryBox.height;
}
}
const children: LayoutedNode[] = [];
for (let i = 0; i < restNode.children.length; i += 1) {
children.push(caculateNodes(restNode.children[i], actualBondary));
}
return {
...restNode,
attrs: {
...restNode.attrs,
...boundaryBox,
},
children,
boundaryBox,
};
};
const getPositionUsingYoga = (root: RawNode): LayoutedNode => {
const basicContainer = Node.create();
// init container
basicContainer.setWidthAuto();
basicContainer.setHeightAuto();
const newNodes =
constructNodes(root, basicContainer) ||
({ ...root, container: basicContainer } as ContainerNode);
basicContainer.calculateLayout();
const result = caculateNodes(newNodes, {
width: 0,
height: 0,
x: 0,
y: 0,
});
basicContainer.freeRecursive();
return result;
};
export default getPositionUsingYoga;

View File

@ -1,186 +0,0 @@
import { GPath } from '@/ReactNode/Shape/common';
type SizeOfShape = {
type: string;
width: number;
height: number;
};
const XYPath = ['L', 'M', 'C', 'S', 'Q', 'T', 'V'];
const xyPath = XYPath.map((e) => e.toLocaleLowerCase());
const convertPathToPoints = (path: GPath[]) => {
const points: [number, number][] = [];
path.forEach(function (seg) {
const [command, ...numbers] = seg;
let [lastPoint] = points.slice(-1);
if (!lastPoint) {
lastPoint = [0, 0];
}
if (XYPath.includes(command)) {
const [x, y] = numbers.slice(-2);
points.push([x, y]);
} else if (xyPath.includes(command)) {
const [x, y] = numbers.slice(-2);
points.push([x + lastPoint[0], y + lastPoint[1]]);
} else if (command === 'H') {
const [x] = numbers.slice(-1);
points.push([x, lastPoint[0]]);
} else if (command === 'h') {
const [x] = numbers.slice(-1);
points.push([x + lastPoint[0], lastPoint[1]]);
} else if (command === 'V') {
const [y] = numbers.slice(-1);
points.push([lastPoint[0], y]);
} else if (command === 'v') {
const [y] = numbers.slice(-1);
points.push([lastPoint[0], lastPoint[1] + y]);
}
});
return points;
};
const getPointsSize = (points: [number, number][]) => {
let [xmax, ymax, xmin, ymin] = [0, 0, 0, 0];
points.forEach(([x, y]) => {
if (x > xmax) {
xmax = x;
}
if (y > ymax) {
ymax = y;
}
if (x < xmin) {
xmin = x;
}
if (y < ymin) {
ymin = y;
}
});
return [xmax - xmin, ymax - ymin];
};
const canvasRef: {
context?: CanvasRenderingContext2D;
timeoutSig?: ReturnType<typeof setTimeout>;
canvas?: HTMLCanvasElement;
} = {};
const getCanvasContext = () => {
let context: CanvasRenderingContext2D | null;
if (canvasRef.context) {
context = canvasRef.context;
} else {
const canvas = document?.createElement('canvas');
if (!canvas) {
return null;
}
context = canvas.getContext('2d');
if (context) {
canvasRef.canvas = canvas;
canvasRef.context = context;
}
}
return context;
};
const getTextSize = (
text: string,
attrs: {
fontSize?: number;
fontFamily?: string;
lineHeight?: number;
[key: string]: any;
},
) => {
const textArr = text.split('\n');
const height =
(attrs.fontSize || 12) * textArr.length * (attrs.lineHeight || 1);
// Try to get canvas to measure text
const context = getCanvasContext();
if (context) {
context.font = `${attrs.fontWeight || 'normal'} ${attrs.fontSize || 12}px ${
attrs.fontFamily || ''
}`;
let width = 0;
for (let i = 0; i < textArr.length; i += 1) {
width = Math.max(width, context.measureText(textArr[i]).width);
}
return [(width * (attrs.fontSize || 12)) / 10, height];
}
// fallback solution
return [
Math.max.apply(
Math,
textArr.map((str) => str.length * (attrs.fontSize || 12)),
),
height,
];
};
const getSizeOfShape = (
type: string,
attrs: Partial<{
path: GPath[];
width: number | string;
height: number | string;
r: number;
rx: number;
ry: number;
points: [number, number][];
text: string;
}>,
): SizeOfShape => {
switch (type) {
case 'rect':
case 'image':
return {
type,
width: Number(attrs.width) || 0,
height: Number(attrs.height) || 0,
};
case 'circle':
case 'marker':
return {
type,
width: (attrs.r || 0) * 2,
height: (attrs.r || 0) * 2,
};
case 'ellipse':
return {
type,
width: (attrs.rx || 0) * 2,
height: (attrs.ry || 0) * 2,
};
case 'path':
const pathSize = getPointsSize(convertPathToPoints(attrs.path || []));
return {
type,
width: pathSize[0],
height: pathSize[1],
};
case 'polygon':
const polygonSize = getPointsSize(attrs.points || []);
return {
type,
width: polygonSize[0],
height: polygonSize[1],
};
case 'text':
const textSize = getTextSize(attrs.text || '', attrs);
return {
type,
width: textSize[0],
height: textSize[1],
};
default:
return {
type,
width: 0,
height: 0,
};
}
};
export default getSizeOfShape;

View File

@ -1,23 +0,0 @@
import React from 'react';
export default function Loading() {
return (
<div
style={{
margin: 'auto',
marginTop: 120,
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
}}
>
<div>
<img
style={{ width: 120, height: 'auto' }}
src="https://gw.alipayobjects.com/zos/antfincdn/cfg5jFqgVt/DiceGraph.png"
/>
<h3>L o a d i n g . . . </h3>
</div>
</div>
);
}

View File

@ -1,65 +0,0 @@
import { EventAttrs } from '../Register/event';
import { AnimationConfig } from '../Animation/animate';
import React from 'react';
interface GroupProps {
/**
* @description.en-US The unique id of this group
* @description.zh-CN id
*/
id?: string;
/**
* @description.en-US The name of the shape which can be not unique.
* @description.zh-CN
*/
name?: string;
/**
* @description.en-US Whether the group/shape is visible
* @description.zh-CN /
*/
visible?: boolean;
/**
* @description.en-US Whether the group is capturable
* @description.zh-CN /
*/
capture?: boolean;
/**
* @description.en-US Whether the group is allowed to response dragstart, drag, and dragend events.
* @description.zh-CN /
*/
draggable?: boolean;
/**
* @description.en-US The visual layer index of the group
* @description.zh-CN /
*/
zIndex?: number;
/**
* @description.en-US animation config
* @description.zh-CN
*/
animation?: Partial<AnimationConfig>;
/**
* @description.en-US Nodes wrapped within the component
* @description.zh-CN
*/
children?: React.ReactNode;
}
export type CommonProps = GroupProps & EventAttrs;
const Group: React.FC<GroupProps> = (props) => {
// @ts-ignore
const { children, ...rest } = props;
return (
<div
data-attr={{
...rest,
type: 'group',
}}
>
{children}
</div>
);
};
export default Group;

View File

@ -1,36 +0,0 @@
import React from 'react';
import { CommonProps } from '../Group';
import { CommonShapeProps } from './common';
export interface CircleStyle extends CommonShapeProps {
/**
* @description.en-US The radius of the circle.
* @description.zh-CN
*/
r: number;
}
interface CircleProps extends CommonProps {
/**
* @description.en-US style of shape
*/
style: CircleStyle;
}
const Circle: React.FC<CircleProps> = (props) => {
// @ts-ignore
const { children, ...rest } = props;
return (
<div
data-attr={{
...rest,
type: 'circle',
}}
>
{children}
</div>
);
};
export default Circle;

View File

@ -1,41 +0,0 @@
import React from 'react';
import { CommonProps } from '../Group';
import { CommonShapeProps } from './common';
export interface EllipseStyle extends CommonShapeProps {
/**
* @description.en-US The horizontal raidus of the ellipse.
* @description.zh-CN
*/
rx: number;
/**
* @description.en-US The vertical raidus of the ellipse.
* @description.zh-CN
*/
ry: number;
}
interface EllipseProps extends CommonProps {
/**
* @description.en-US style of shape
*/
style: EllipseStyle;
}
const Ellipse: React.FC<EllipseProps> = (props) => {
// @ts-ignore
const { children, ...rest } = props;
return (
<div
data-attr={{
...rest,
type: 'ellipse',
}}
>
{children}
</div>
);
};
export default Ellipse;

View File

@ -1,46 +0,0 @@
import React from 'react';
import { CommonProps } from '../Group';
import { CommonShapeProps } from './common';
export interface ImageStyle extends CommonShapeProps {
/**
* @description.en-US The width of the image.
* @description.zh-CN
*/
width?: number;
/**
* @description.en-US The height of the image.
* @description.zh-CN
*/
height?: number;
/**
* @description.en-US The img source of the image.
* @description.zh-CN
*/
img: string | ImageData | CanvasImageData;
}
interface ImageProps extends CommonProps {
/**
* @description.en-US style of shape
*/
style: ImageStyle;
}
const Image: React.FC<ImageProps> = (props) => {
// @ts-ignore
const { children, ...rest } = props;
return (
<div
data-attr={{
...rest,
type: 'image',
}}
>
{children}
</div>
);
};
export default Image;

View File

@ -1,47 +0,0 @@
import React from 'react';
import { CommonProps } from '../Group';
import { CommonShapeProps, GPath } from './common';
export interface MarkerStyle extends CommonShapeProps {
/**
* @description.en-US The radius of the marker.
* @description.zh-CN
*/
r: number;
/**
* @description.en-US Built-in shapes or function return path array;
* @description.zh-CN
*/
symbol:
| 'circle'
| 'square'
| 'diamond'
| 'triangle'
| 'triangle-down'
| ((x: number, y: number, r: number) => GPath[]);
}
interface MarkerProps extends CommonProps {
/**
* @description.en-US style of shape
*/
style: MarkerStyle;
}
const Marker: React.FC<MarkerProps> = (props) => {
// @ts-ignore
const { children, ...rest } = props;
return (
<div
data-attr={{
...rest,
type: 'marker',
}}
>
{children}
</div>
);
};
export default Marker;

View File

@ -1,68 +0,0 @@
import React from 'react';
import { CommonProps } from '../Group';
import { Arrow, CommonShapeProps, GPath } from './common';
export interface PathStyle extends CommonShapeProps {
/**
* @description.en-US SVG like Path array.
* @description.zh-CN G使用的SVG路径数组SVG路径
*/
path: GPath[];
/**
* @description.en-US Show the arrow on the start of the path.
* @description.zh-CN SVG路径字符串
*/
startArrow?: Arrow;
/**
* @description.en-US Show the arrow on the end of the path.
* @description.zh-CN SVG路径字符串
*/
endArrow?: Arrow;
/**
* @description.en-US The hitting area of the path. Enlarge the hitting area by enlarging its value.
* @description.zh-CN
*/
lineAppendWidth?: number;
/**
* @description.en-US The style of two ends of the path.
* @description.zh-CN
* @default 'miter'
*/
lineCap?: 'bevel' | 'round' | 'miter';
/**
* @description.en-US The style of the intersection of two path.
* @description.zh-CN
* @default 'miter'
*/
lineJoin?: 'bevel' | 'round' | 'miter';
/**
* @description.en-US The maximum miter length.
* @description.zh-CN
*/
miterLimit?: number;
}
interface PathProps extends CommonProps {
/**
* @description.en-US style of shape
*/
style: PathStyle;
}
const Path: React.FC<PathProps> = (props) => {
// @ts-ignore
const { children, ...rest } = props;
return (
<div
data-attr={{
...rest,
type: 'path',
}}
>
{children}
</div>
);
};
export default Path;

View File

@ -1,36 +0,0 @@
import React from 'react';
import { CommonProps } from '../Group';
import { CommonShapeProps } from './common';
export interface PolygonStyle extends CommonShapeProps {
/**
* @description.en-US The points of the polygon x,y
* @description.zh-CN x, y
*/
points: [number, number][];
}
interface PolygonProps extends CommonProps {
/**
* @description.en-US style of shape
*/
style: PolygonStyle;
}
const Polygon: React.FC<PolygonProps> = (props) => {
// @ts-ignore
const { children, ...rest } = props;
return (
<div
data-attr={{
...rest,
type: 'polygon',
}}
>
{children}
</div>
);
};
export default Polygon;

View File

@ -1,46 +0,0 @@
import React from 'react';
import { CommonProps } from '../Group';
import { CommonShapeProps } from './common';
export interface RectStyle extends CommonShapeProps {
/**
* @description.en-US The radius of the rect corner.
* @description.zh-CN
*/
radius?: number | number[];
/**
* @description.en-US The width of the rect.
* @description.zh-CN
*/
width?: number | 'auto';
/**
* @description.en-US The height of the rect.
* @description.zh-CN
*/
height?: number | 'auto';
}
interface RectProps extends CommonProps {
/**
* @description.en-US style of shape
*/
style?: RectStyle;
}
const Rect: React.FC<RectProps> = (props) => {
// @ts-ignore
const { children, ...rest } = props;
return (
<div
data-attr={{
...rest,
type: 'rect',
}}
>
{children}
</div>
);
};
export default Rect;

View File

@ -1,70 +0,0 @@
import React, { CSSProperties } from 'react';
import { CommonProps } from '../Group';
import { CommonShapeProps } from './common';
export interface TextStyle extends CommonShapeProps {
/**
* @description.en-US text align way, affect relative position of x
* @description.zh-CN x的点
*/
textAlign?: 'center' | 'end' | 'left' | 'right' | 'start';
/**
* @description.en-US text baseline, affect relative position of y
* @description.zh-CN 线线y坐标上
*/
textBaseline?: 'top' | 'middle' | 'bottom' | 'alphabetic' | 'hanging';
/**
* @description.en-US CSS font-style
* @description.zh-CN CSS font-style
*/
fontStyle?: CSSProperties['fontStyle'];
/**
* @description.en-US CSS font-weight
* @description.zh-CN CSS font-weight
*/
fontWeight?: CSSProperties['fontWeight'];
/**
* @description.en-US CSS font-variant
* @description.zh-CN CSS font-variant
*/
fontVariant?: CSSProperties['fontVariant'];
/**
* @description.en-US CSS font-size
* @description.zh-CN CSS font-size
*/ fontSize?: CSSProperties['fontSize'];
/**
* @description.en-US CSS font-family
* @description.zh-CN CSS font-family
*/
fontFamily?: CSSProperties['fontFamily'];
/**
* @description.en-US CSS line-height
* @description.zh-CN CSS line-height
*/
lineHeight?: CSSProperties['lineHeight'];
}
interface TextProps extends CommonProps {
/**
* @description.en-US style of shape
*/
style?: TextStyle;
}
const Text: React.FC<TextProps> = (props) => {
// @ts-ignore
const { children, ...rest } = props;
return (
<div
data-attr={{
...rest,
type: 'text',
}}
>
{children}
</div>
);
};
export default Text;

View File

@ -1,114 +0,0 @@
import { LayoutAttrs } from '../../Layout/LayoutEnums';
import { CSSProperties } from 'react';
export interface GShapeProps extends Partial<LayoutAttrs> {
/**
* @description.en-US the color used to fill the shape, support rgb(a)/hex/gradient
* @description.zh-CN rgb(a)/hex/G渐变色
*/
fill?: string;
/**
* @description.en-US the color used to stroke the border of shape, support rgb(a)/hex/gradient
* @description.zh-CN rgb(a)/hex/G渐变色
*/
stroke?: string;
/**
* @description.en-US width of the stroke line
* @description.zh-CN 线
*/
lineWidth?: string;
/**
* @description.en-US The lineDash of the stroke. If its type is Number[], the elements in the array are the lengths of the lineDash.
* @description.zh-CN 线线线
*/
lineDash?: number | number[];
/**
* @description.en-US The color of the shadow.
* @description.zh-CN
*/
shadowColor?: string;
/**
* @description.en-US The blur level for shadow.
* @description.zh-CN
*/
shadowBlur?: number;
/**
* @description.en-US The horizontal offset of the shadow.
* @description.zh-CN
*/
shadowOffsetX?: number;
/**
* @description.en-US The vertical offset of the shadow.
* @description.zh-CN
*/
shadowOffsetY?: number;
/**
* @description.en-US The filling opacity (alpha value) of the shape. The priority is higher than opacity.
* @description.zh-CN
*/
fillOpacity?: number;
/**
* @description.en-US The stroke opacity (alpha value) of the shape. The priority is higher than opacity.
* @description.zh-CN
*/
strokeOpacity?: number;
/**
* @description.en-US The opacity (alpha value) of the shape.
* @description.zh-CN
*/
opacity?: number;
/**
* @description.en-US Cursor shape when hover on it
* @description.zh-CN
*/
cursor?: CSSProperties['cursor'];
}
export interface CommonShapeProps extends GShapeProps {
/**
* @description.en-US The x of the center of the Shape.
* @description.zh-CN x坐标x绝对计算
*/
x?: number;
/**
* @description.en-US The y of the center of the Shape.
* @description.zh-CN y坐标y绝对计算
*/
y?: number;
/**
* @description.en-US left margin of shape
* @description.zh-CN
*/
marginLeft?: number;
/**
* @description.en-US top margin of shape
* @description.zh-CN
*/
marginTop?: number;
/**
* @description.en-US make next shape follow inline
* @description.zh-CN
*/
next?: 'inline';
}
export type GPath =
| ['Z']
| ['H' | 'h' | 'V' | 'v' | 'T' | 't', number]
| ['M' | 'm' | 'L' | 'l', number, number]
| ['S' | 's' | 'Q' | 'q', number, number, number, number]
| ['C' | 'c', number, number, number, number, number, number]
| ['A' | 'a', number, number, number, number, number, number, number];
export type Arrow =
| boolean
| {
/**
* SVG path string of arrow
*/
path: string;
/**
* @description.en-US offset distance of the arrow
*/
d: number;
};

View File

@ -1,78 +0,0 @@
import React, { useEffect } from 'react';
import G6, { GraphData } from '@antv/g6';
import { appenAutoShapeListener } from '../Register/event';
export const G6MiniDemo = ({
nodeType,
count = 1,
height = 200,
}: {
nodeType: string;
count: number;
height: number;
}) => {
useEffect(() => {
const data = {
nodes: 'e'
.repeat(count)
.split('')
.map((e, i) => ({
description: 'ant_type_name_...',
label: 'Type / ReferType',
color: '#7262fd',
meta: {
creatorName: 'a_creator',
},
id:
'node' +
i +
Math.random()
.toString(16)
.slice(-4),
type: nodeType,
})),
edges: [],
} as GraphData;
if (data && data.nodes && data.nodes.length > 1) {
data.edges!.push({
source: data.nodes[0].id,
target: data.nodes[1].id,
style: {
endArrow: true,
},
});
}
const width = document.getElementById('container')?.clientWidth || 800;
const graph = new G6.Graph({
container: 'container',
width,
height,
fitCenter: true,
modes: {
default: ['drag-node', 'drag-canvas', 'zoom-canvas'],
},
layout: {
type: 'dagre',
rankdir: 'LR',
},
});
graph.data(data);
const time = new Date();
graph.render();
console.log(
`${count} Nodes rendered`,
'Render time:',
(Number(new Date()) - Number(time)) / 1000,
's',
);
appenAutoShapeListener(graph);
return () => {
graph.destroy();
};
}, [count, nodeType]);
return <div id="container"></div>;
};

View File

@ -0,0 +1,105 @@
## Antd React Node
```jsx
import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons';
import { Button, Card, Col, Row, Statistic } from 'antd';
import { createReactNode } from '@antv/g6-react-node';
import { Graph, extend } from '@antv/g6';
import { useEffect, useRef } from 'react';
import type { ReactNodeProps } from '@antv/g6-react-node';
const Statistics: React.FC<ReactNodeProps> = ({ model }) => {
const { value } = model.data;
return (
<Row gutter={16}>
<Col span={24}>
<Card bordered={false}>
<Statistic
title="Active"
value={value}
precision={2}
valueStyle={{ color: value > 0 ? '#3f8600' : '#cf1322' }}
prefix={value > 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
suffix="%"
/>
</Card>
</Col>
</Row>
);
};
export default () => {
const ref = useRef();
const graphRef = useRef(null);
useEffect(() => {
if (!ref.current) return;
const container = ref.current;
const width = container.scrollWidth;
const height = container.scrollHeight || 500;
const reactNode = createReactNode(Statistics);
const ExtendGraph = extend(Graph, {
nodes: {
'react-node': reactNode,
},
});
graphRef.current = new ExtendGraph({
container,
width,
height,
modes: {
default: [
{
type: 'drag-node',
enableTransient: false,
},
],
},
data: {
nodes: [
{ id: 'node0', data: { size: [200, 100], value: 0 } },
{ id: 'node1', data: { size: [200, 100], value: 0.1128 } },
{ id: 'node2', data: { size: [200, 100], value: -0.093 } },
],
edges: [
{ id: 'edge1', source: 'node0', target: 'node1', data: {} },
{ id: 'edge2', source: 'node0', target: 'node2', data: {} },
],
},
node: {
type: 'react-node',
},
});
return () => {
graphRef.current.destroy();
};
}, []);
return (
<div style={{ width: '100%', height: 500 }}>
<div>
<Button
onClick={() => {
Array.from({ length: 3 }).forEach((_, i) => {
graphRef.current.updateData('node', {
id: `node${i}`,
data: {
size: [200, 100],
value: Math.random() * 2 - 1,
},
});
});
}}
>
更新数据
</Button>
</div>
<div ref={ref}></div>
</div>
);
};
```

View File

@ -1,11 +0,0 @@
export { default as Group } from './Group';
export { default as Circle } from './Shape/Circle';
export { default as Ellipse } from './Shape/Ellipse';
export { default as Image } from './Shape/Image';
export { default as Marker } from './Shape/Marker';
export { default as Polygon } from './Shape/Polygon';
export { default as Rect } from './Shape/Rect';
export { default as Path } from './Shape/Path';
export { default as Text } from './Shape/Text';
export { createNodeFromReact } from '../Register/register';
export { appenAutoShapeListener } from '../Register/event';

View File

@ -0,0 +1,2 @@
export { createReactNode } from './node';
export type { ReactNodeProps } from './typings';

View File

@ -0,0 +1,112 @@
import { HTML } from '@antv/g';
import type { NodeDisplayModel } from '@antv/g6';
import { Extensions } from '@antv/g6';
import type { State } from '@antv/g6/lib/types/item';
import type { NodeShapeMap, NodeUserModelData } from '@antv/g6/lib/types/node';
import { throttle } from '@antv/util';
import React from 'react';
import { createRoot } from 'react-dom/client';
const ShapeCollection: Record<string, any> = new Map();
export const createReactNode = (
Component: (props: {
model?: NodeDisplayModel;
states?: State[];
}) => React.ReactElement,
): any => {
class ReactNode extends Extensions.RectNode {
drawKeyShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?:
| { previous: NodeUserModelData; current: NodeUserModelData }
| undefined,
diffState?: { previous: State[]; current: State[] } | undefined,
) {
const { data } = model;
const { size: [width, height] = [200, 50] } = data as any;
const html = this.upsertShape(
'html',
'keyShape',
{
x: -width / 2,
y: -height / 2,
width,
height,
anchor: '0.5 0.5',
pointerEvents: 'auto',
},
{ shapeMap, model, diffData, diffState },
) as HTML;
html.isMutationObserved = true;
html.addEventListener('DOMNodeInserted', () => {
const dom = html.getDomElement();
const content = (
<Component model={model} states={diffState?.current || []} />
);
const root = createRoot(dom);
root.render(content);
// drag logic
dom.addEventListener('pointerdown', (event) => {
const { clientX, clientY } = event;
let baseX = clientX;
let baseY = clientY;
const onMouseMove = throttle(
(event: PointerEvent) => {
dom.style.userSelect = 'none';
const diffX = event.clientX - baseX;
const diffY = event.clientY - baseY;
baseX = event.clientX;
baseY = event.clientY;
const node = this.graph.getNodeData(model.id);
const { x: nodeX = 0, y: nodeY = 0 } = node?.data || {};
this.graph.updateNodePosition(
{
...node,
data: { ...node?.data, x: nodeX + diffX, y: nodeY + diffY },
},
undefined,
true,
);
},
50,
{ leading: true, trailing: true },
) as EventListener;
document.addEventListener('pointermove', onMouseMove);
dom.addEventListener(
'pointerup',
() => {
dom.style.userSelect = 'auto';
document.removeEventListener('pointermove', onMouseMove);
},
{ once: true },
);
});
});
html.addEventListener('destroy', () => {
html.removeAllEventListeners();
html.getDomElement().remove();
});
html.getRenderBounds = html.getBounds;
ShapeCollection.set(model.id, html);
return html;
}
}
return ReactNode;
};

View File

@ -0,0 +1,7 @@
import type { NodeDisplayModel } from '@antv/g6';
import type { State } from '@antv/g6/lib/types/item';
export type ReactNodeProps = {
model: NodeDisplayModel;
states: State[];
};

View File

@ -0,0 +1,147 @@
## Complex React Node
```jsx
import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons';
import { List, Form, Checkbox, Button, Input, Table } from 'antd';
import { createReactNode } from '@antv/g6-react-node';
import { Graph, extend } from '@antv/g6';
import { useEffect, useRef } from 'react';
import type { ReactNodeProps } from '@antv/g6-react-node';
const ComplexNode: React.FC<ReactNodeProps> = ({ model }) => {
const { nodeType } = model.data;
let content = null;
if (nodeType === 'form') {
content = (
<Form
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
initialValues={{ remember: true }}
autoComplete="off"
>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password />
</Form.Item>
<Form.Item
name="remember"
valuePropName="checked"
wrapperCol={{ offset: 8, span: 16 }}
>
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
} else if (nodeType === 'table') {
content = (
<Table
dataSource={[{ key: '1', name: 'Table Item' }]}
columns={[{ title: 'Title', dataIndex: 'name', key: 'name' }]}
/>
);
} else {
content = (
<List
size="small"
header={<div>List</div>}
bordered
dataSource={['List Item']}
renderItem={(item) => <List.Item>{item}</List.Item>}
/>
);
}
return (
<div
style={{
padding: 10,
backgroundColor: 'white',
boxShadow:
'0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)',
}}
>
{content}
</div>
);
};
export default () => {
const ref = useRef();
const graphRef = useRef(null);
useEffect(() => {
if (!ref.current) return;
const container = ref.current;
const width = container.scrollWidth;
const height = container.scrollHeight || 500;
const reactNode = createReactNode(ComplexNode);
const ExtendGraph = extend(Graph, {
nodes: {
'react-node': reactNode,
},
});
graphRef.current = new ExtendGraph({
container,
width,
height,
modes: {
default: [
{
type: 'drag-node',
enableTransient: false,
},
],
},
data: {
nodes: [
{ id: 'node0', data: { size: [200, 100], nodeType: 'list' } },
{ id: 'node1', data: { size: [280, 100], nodeType: 'form' } },
{ id: 'node2', data: { size: [200, 100], nodeType: 'table' } },
],
edges: [
{ id: 'edge1', source: 'node0', target: 'node1', data: {} },
{ id: 'edge2', source: 'node0', target: 'node2', data: {} },
],
},
node: {
type: 'react-node',
},
});
return () => {
graphRef.current.destroy();
};
}, []);
return (
<div style={{ width: '100%', height: 500 }}>
<div ref={ref}></div>
</div>
);
};
```

View File

@ -1,64 +0,0 @@
import { IShape } from '@antv/g-base';
import { IAbstractGraph, IG6GraphEvent, Item } from '@antv/g6-core';
export type ShapeEventListner = (
event: IG6GraphEvent,
node: Item | null,
shape: IShape,
graph: IAbstractGraph,
) => void;
export interface EventAttrs {
onClick?: ShapeEventListner;
onDBClick?: ShapeEventListner;
onMouseEnter?: ShapeEventListner;
onMouseMove?: ShapeEventListner;
onMouseOut?: ShapeEventListner;
onMouseOver?: ShapeEventListner;
onMouseLeave?: ShapeEventListner;
onMouseDown?: ShapeEventListner;
onMouseUp?: ShapeEventListner;
onDragStart?: ShapeEventListner;
onDrag?: ShapeEventListner;
onDragEnd?: ShapeEventListner;
onDragEnter?: ShapeEventListner;
onDragLeave?: ShapeEventListner;
onDragOver?: ShapeEventListner;
onDrop?: ShapeEventListner;
onContextMenu?: ShapeEventListner;
}
const propsToEventMap = {
click: 'onClick',
dblclick: 'onDBClick',
mouseenter: 'onMouseEnter',
mousemove: 'onMouseMove',
mouseout: 'onMouseOut',
mouseover: 'onMouseOver',
mouseleave: 'onMouseLeave',
mousedown: 'onMouseDown',
mouseup: 'onMouseUp',
dragstart: 'onDragStart',
drag: 'onDrag',
dragend: 'onDragEnd',
dragenter: 'onDragEnter',
dragleave: 'onDragLeave',
dragover: 'onDragOver',
drop: 'onDrop',
contextmenu: 'onContextMenu',
};
export function appenAutoShapeListener(graph: IAbstractGraph) {
Object.entries(propsToEventMap).map(([eventName, propName]) => {
graph.on(`node:${eventName}`, (evt) => {
const shape = evt.shape;
const item = evt.item;
const graph = evt.currentTarget as IAbstractGraph;
const func = shape?.get(propName) as ShapeEventListner;
if (func && item) {
func(evt, item, shape, graph);
}
});
});
}

View File

@ -1,54 +0,0 @@
import { ReactElement } from 'react';
import { LayoutAttrs } from '../Layout/LayoutEnums';
export interface RawNode {
type: string;
attrs: { [key: string]: any } & Partial<LayoutAttrs>;
children: RawNode[];
props: { [key: string]: any };
}
const getShapeFromReact = (REl: ReactElement): RawNode => {
if (typeof REl === 'string') {
return REl;
}
if (typeof REl.type === 'string') {
const data = REl.props['data-attr'] || {};
const { style: attrs = {}, type, ...props } = data;
let { children: ochildren } = REl.props;
if (type === 'text') {
attrs.text = ochildren?.join ? ochildren.join('') : ochildren;
return {
type,
attrs,
props,
children: [],
};
}
let children = [];
if (typeof ochildren === 'object' && ochildren?.length) {
children = ochildren
.filter((e: any) => !!e)
.reduce((a: any, b: any) => a.concat(b.concat ? b : [b]), [])
.map((e: ReactElement) => getShapeFromReact(e));
} else if (ochildren) {
children = [getShapeFromReact(ochildren)];
}
return {
type,
attrs,
props,
children,
};
} else {
const Element = REl.type as any;
try {
return getShapeFromReact(new Element({ ...REl.props }));
} catch (e) {
return getShapeFromReact(Element({ ...REl.props }));
}
}
};
export default getShapeFromReact;

View File

@ -1,133 +0,0 @@
import React, { ReactElement } from 'react';
import {
INode,
IEdge,
ICombo,
ModelConfig,
ShapeOptions,
} from '@antv/g6-core/lib';
import { IGroup, IShape } from '@antv/g-base';
import getShapeFromReact from '@/Register/getDataFromReactNode';
import getPositionUsingYoga, {
LayoutedNode,
} from '@/Layout/getPositionsUsingYoga';
import { animateShapeWithConfig } from '@/Animation/animate';
export const registerNodeReact = (el: ReactElement) => {
const result = getShapeFromReact(el);
const target = getPositionUsingYoga(result);
return target;
};
const renderTarget = (target: LayoutedNode, group: any) => {
let g = group;
let keyshape = group;
const { attrs = {}, boundaryBox, type, children, props } = target;
if (target.type !== 'group') {
const shape = group.addShape(target.type, {
attrs,
origin: {
boundaryBox,
type,
children,
},
...props,
});
if (props.keyShape) {
keyshape = shape;
}
animateShapeWithConfig(shape, props.animation);
} else {
g = group.addGroup(props);
if (!keyshape) {
keyshape = g;
}
}
if (target.children) {
const keyshapes = target.children
.map(n => renderTarget(n, g))
.filter(e => e);
keyshape = keyshapes.find(shape => !shape.isGroup()) || keyshape;
}
return keyshape;
};
const getRealStructure = (target: LayoutedNode): LayoutedNode[] => {
const { children } = target;
target.children = [];
let realChildren: LayoutedNode[] = [];
for (let i = 0; i < children.length; i += 1) {
const result = getRealStructure(children[i]);
realChildren = realChildren.concat(result);
}
if (target.type !== 'group') {
return [target, ...realChildren];
} else {
target.children = realChildren;
return [target];
}
};
const diffTarget = (container: IGroup, shapeArr: LayoutedNode[]) => {
const childrenList = [...container.getChildren()];
for (let i = 0; i < childrenList.length; i += 1) {
const lastShape = childrenList[i];
const nowShape = shapeArr[i];
if (!nowShape) {
container.removeChild(lastShape, true);
} else if (!lastShape) {
renderTarget(nowShape, container);
} else if (lastShape.cfg.type !== nowShape.type) {
container.removeChild(lastShape, true);
renderTarget(nowShape, container);
} else {
if (nowShape.props) {
lastShape.cfg = {
...lastShape.cfg,
...nowShape.props,
};
}
if (nowShape.attrs && lastShape.attr) {
lastShape.attr(nowShape.attrs);
}
if (nowShape.type === 'group') {
diffTarget(lastShape as IGroup, nowShape.children);
}
}
}
};
export function createNodeFromReact(
Component: React.FC<{ cfg: ModelConfig }>,
): { [key: string]: any } {
const compileXML = (cfg: ModelConfig) =>
registerNodeReact(<Component cfg={cfg} />);
return {
draw(cfg: ModelConfig | undefined, fatherGroup: IGroup | undefined) {
const resultTarget = compileXML(cfg || {});
const keyshape: IShape = renderTarget(resultTarget, fatherGroup);
return keyshape;
},
update(cfg: ModelConfig, node: INode | IEdge | ICombo | undefined) {
const resultTarget = compileXML(cfg || {});
if (node) {
const nodeGroup = node.getContainer();
const realTarget = getRealStructure(resultTarget);
diffTarget(nodeGroup, realTarget);
}
},
getAnchorPoints() {
return [
[0, 0.5],
[1, 0.5],
[0.5, 1],
[0.5, 0],
];
},
};
}

View File

@ -0,0 +1 @@
export const register = () => {};

View File

@ -1 +1,2 @@
export * from './ReactNode';
export * from './GNode';

View File

@ -1,15 +0,0 @@
import { Group, Rect, Text, Circle } from '@antv/g6-react-node';
const Node = () => {
return (
<Group name="node">
<Circle style={{ r: 40 }}>
<Rect
style={{ width: 20, height: 20, marginTop: 10, marginLeft: 10 }}
/>
</Circle>
<Text style={{ fill: '#000', fontSize: 16 }}>Text</Text>
<Circle style={{ r: 10, x: 60, y: 0 }}></Circle>
</Group>
);
};

View File

@ -1,27 +1,16 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"jsx": "react",
"esModuleInterop": true,
"sourceMap": true,
"baseUrl": "./",
"strict": true,
"declaration": true,
"skipLibCheck": true,
"esModuleInterop": true,
"jsx": "react",
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"@@/*": ["src/.umi/*"]
},
"allowSyntheticDefaultImports": true
"@@/*": [".dumi/tmp/*"],
"@antv/g6-react-node": ["src"],
"@antv/g6-react-node/*": ["src/*", "*"]
}
},
"exclude": [
"node_modules",
"lib",
"es",
"dist",
"typings",
"**/__test__",
"test"
]
"include": [".dumirc.ts", "src/**/*"]
}

View File

@ -1,2 +0,0 @@
declare module '*.css';
declare module '*.less';

View File

@ -1,3 +1,5 @@
// @ts-nocheck
if (window) {
// window.g6 = require('@antv/g6/es'); // import the source for debugging
window.g6 = require('@antv/g6/lib'); // import the source for debugging
@ -13,4 +15,9 @@ if (window) {
window.util = require('@antv/util');
window.stats = require('stats.js');
window.g2 = require('@antv/g2');
window.antd = require('antd');
window.React = require('react');
window.ReactDOM = require('react-dom');
window.g6ReactNode = require('@antv/g6-react-node');
}

Some files were not shown because too many files have changed in this diff Show More