├── .gitignore ├── .DS_Store ├── demo.gif ├── pages ├── index.js ├── component │ ├── index.js │ ├── nodeTooltip.js │ ├── index.css │ ├── edgTooltip.js │ └── contextMenu.js ├── index.html ├── data.js ├── tree │ └── index.js ├── tutorital.jsx ├── registerShape.js └── app.js ├── .babelrc ├── tsconfig.json ├── README.md ├── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | pages/.umi/ 3 | dist -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizn/g6-in-react/HEAD/.DS_Store -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baizn/g6-in-react/HEAD/demo.gif -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './tutorital.jsx'; 4 | 5 | ReactDOM.render(, document.getElementById('container')); 6 | -------------------------------------------------------------------------------- /pages/component/index.js: -------------------------------------------------------------------------------- 1 | export { default as EdgeToolTips } from './edgTooltip' 2 | export { default as NodeTooltips } from './nodeTooltip' 3 | export { default as NodeContextMenu } from './contextMenu' 4 | -------------------------------------------------------------------------------- /pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | G6 & React 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "edge": 17, 8 | "firefox": 60, 9 | "chrome": 67, 10 | "safari": 11.1 11 | }, 12 | "useBuiltIns": "usage" 13 | } 14 | ], 15 | "@babel/preset-react" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "preserve", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["pages/*"] 13 | }, 14 | "allowSyntheticDefaultImports": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 如何在React中使用G6? 2 | 在平时的答疑工作中,经常会遇到「如何在React中使用G6」的问题,为了让G6用户能够在开发中很方便地接入G6,我们提供一个G6在React中使用的Demo,供开发者参考。 3 | 4 | `说明:鉴于目前React hooks比较🔥, 且大多数React开发者已经在使用hooks进行业务开发,因此,我们提供的Demo也是基于hooks的。` 5 | 6 | 更详细的内容请参考[React中使用G6](https://www.yuque.com/antv/g6/zmfur7)文档。 7 | 8 | 9 | 10 | ### 功能 11 | 我们提供的Demo,包括了以下功能: 12 | - 自定义节点; 13 | - 自定义边; 14 | - 节点的tooltip; 15 | - 边的tooltip; 16 | - 节点的ContextMenu; 17 | - tooltip及ContextMenu如何渲染自定义的React组件。 18 | 19 | ### Development 20 | ``` 21 | # clone repo 22 | $ git clone https://github.com/baizn/g6-in-react.git 23 | 24 | # install dependencies 25 | $ npm install 26 | 27 | # start server 28 | $ npm start 29 | 30 | ``` -------------------------------------------------------------------------------- /pages/component/nodeTooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Timeline } from 'antd' 3 | import 'antd/es/timeline/style/css' 4 | import styles from './index.css' 5 | const NodeToolTips = ({ x, y }) => { 6 | return ( 7 |
8 | 9 | Create a services site 2015-09-01 10 | Solve initial network problems 2015-09-01 11 | Technical testing 2015-09-01 12 | Network problems being solved 2015-09-01 13 | 14 |
15 | ) 16 | } 17 | 18 | export default NodeToolTips 19 | -------------------------------------------------------------------------------- /pages/component/index.css: -------------------------------------------------------------------------------- 1 | .edgeTooltips { 2 | position: absolute; 3 | left: -300px; 4 | top: -100px; 5 | } 6 | .edgeTitle { 7 | position: relative; 8 | top: 0; 9 | left: 0; 10 | width: 150px; 11 | height: 70px; 12 | background: #fff; 13 | border: 1px solid #40a9ff; 14 | } 15 | .edgeDetail { 16 | position: relative; 17 | top: 0; 18 | left: 0; 19 | background: #bae7ff; 20 | width: 200px; 21 | } 22 | .tooltipsCommon { 23 | font-size: 16px; 24 | margin: 0; 25 | padding-left: 10px; 26 | } 27 | .tooltipsMoney { 28 | padding-bottom: 5px; 29 | } 30 | .tooltipsDate { 31 | font-size: 12px; 32 | color: #ccc; 33 | } 34 | .detailContent { 35 | margin-bottom: 10px; 36 | } 37 | .edgeCode { 38 | margin: 0; 39 | padding-left: 10px; 40 | font-size: 12px; 41 | } 42 | .edgeValue { 43 | padding-left: 10px; 44 | } 45 | .nodeTooltips { 46 | position: absolute; 47 | left: -300px; 48 | top: -100px; 49 | /* width: 150px; */ 50 | height: 160px; 51 | padding: 10px; 52 | border: 1px solid #36cfc9; 53 | background: #e6fffb; 54 | } -------------------------------------------------------------------------------- /pages/component/edgTooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './index.css' 3 | 4 | const EdgeToolTips = ({ x, y }) => { 5 | return ( 6 |
7 |
8 |

凭证开立

9 |

1000,000,000元

10 |

2019-09-10

11 |
12 |
13 |
14 |

交易编码:

15 | 1000190203455 16 |
17 |
18 |

交易编码:

19 | 1000190203455 20 |
21 |
22 |

交易编码:

23 | 1000190203455 24 |
25 |
26 |
27 | ) 28 | } 29 | 30 | export default EdgeToolTips 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "g6-in-react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config webpack.config.js", 8 | "build": "webpack --config webpack.config.js --mode production", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/baizn/g6-in-react.git" 14 | }, 15 | "keywords": [ 16 | "g6" 17 | ], 18 | "author": "baizn", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/baizn/g6-in-react/issues" 22 | }, 23 | "homepage": "https://github.com/baizn/g6-in-react#readme", 24 | "dependencies": { 25 | "@antv/g6": "^3.4.0", 26 | "antd": "^3.23.5", 27 | "html-webpack-plugin": "^4.2.0", 28 | "react": "^16.13.1", 29 | "react-dom": "^16.13.1", 30 | "webpack": "^4.42.1" 31 | }, 32 | "devDependencies": { 33 | "@babel/preset-env": "^7.9.5", 34 | "@babel/preset-react": "^7.9.4", 35 | "@babel/core": "^7.9.6", 36 | "babel-loader": "^8.1.0", 37 | "css-loader": "^3.5.2", 38 | "postcss-loader": "^3.0.0", 39 | "style-loader": "^1.1.4", 40 | "ts-loader": "^7.0.0", 41 | "uglifyjs-webpack-plugin": "^2.2.0", 42 | "webpack-cli": "^3.3.11", 43 | "webpack-dev-server": "^3.10.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pages/component/contextMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Menu, Icon } from 'antd' 3 | import 'antd/es/menu/style/css' 4 | const { SubMenu } = Menu 5 | 6 | const NodeContextMenu = ({ x = -300, y = 0 }) => { 7 | return 8 | 12 | 13 | Navigation One 14 | 15 | } 16 | > 17 | 18 | Option 1 19 | Option 2 20 | 21 | 22 | Option 3 23 | Option 4 24 | 25 | 26 | 30 | 31 | Navigation Two 32 | 33 | } 34 | > 35 | Option 5 36 | Option 6 37 | 38 | Option 7 39 | Option 8 40 | 41 | 42 | 46 | 47 | Navigation Three 48 | 49 | } 50 | > 51 | Option 9 52 | Option 10 53 | Option 11 54 | Option 12 55 | 56 | 57 | } 58 | 59 | export default NodeContextMenu -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path') 3 | // eslint-disable-next-line prefer-destructuring 4 | const resolve = path.resolve; 5 | const HTMLPlugin = require('html-webpack-plugin'); 6 | const UglifyJSWebpackPlguin = require('uglifyjs-webpack-plugin') 7 | 8 | module.exports = { 9 | entry: { 10 | g6InReact: [ 11 | 'webpack-dev-server/client?http://localhost:8080', 12 | './pages/index.js'], 13 | }, 14 | output: { 15 | filename: '[name].min.js', 16 | library: 'G6', 17 | libraryTarget: 'umd', 18 | libraryExport: 'default', 19 | path: resolve(process.cwd(), 'dist/'), 20 | }, 21 | target: 'web', 22 | resolve: { 23 | // Add `.ts` as a resolvable extension. 24 | extensions: ['.ts', '.js'], 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.(js|jsx)$/, 30 | use: { 31 | loader: 'babel-loader', 32 | options: { 33 | babelrc: true, 34 | }, 35 | }, 36 | }, 37 | { 38 | test: /\.(ts|tsx)$/, 39 | use: { 40 | loader: 'ts-loader', 41 | options: { 42 | transpileOnly: true, 43 | }, 44 | }, 45 | }, 46 | { 47 | test: /\.(css)$/, 48 | use: [ 49 | 'style-loader', 50 | { 51 | loader: 'css-loader', 52 | options: { 53 | modules: true, 54 | }, 55 | }, 56 | ] 57 | } 58 | ], 59 | }, 60 | plugins: [ 61 | new webpack.NoEmitOnErrorsPlugin(), 62 | new webpack.optimize.AggressiveMergingPlugin(), 63 | new HTMLPlugin({ 64 | template: './pages/index.html' 65 | }), 66 | // 更多配置项参考:https://www.webpackjs.com/plugins/uglifyjs-webpack-plugin/ 67 | new UglifyJSWebpackPlguin({ 68 | exclude: /node_modules/, 69 | uglifyOptions: { 70 | ecma: 5, 71 | ie8: true 72 | } 73 | }) 74 | ], 75 | devtool: 'cheap-module-eval-source-map', 76 | devServer: { 77 | // 本地测试服务器加载的页面所在的目录,默认webpack-dev-server会为根文件夹提供本地服务器 78 | // contentBase: "./test-server", 79 | contentBase: './dist', 80 | // 监听的端口,默认为8080 81 | port: 8080, 82 | // 不跳转 83 | historyApiFallback: true 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /pages/data.js: -------------------------------------------------------------------------------- 1 | export const data = { 2 | nodes: [ 3 | { 4 | id: '1', 5 | label: '公司1' 6 | }, 7 | { 8 | id: '2', 9 | label: '公司2' 10 | }, 11 | { 12 | id: '3', 13 | label: '公司3' 14 | }, 15 | { 16 | id: '4', 17 | label: '公司4' 18 | }, 19 | { 20 | id: '5', 21 | label: '公司5' 22 | }, 23 | { 24 | id: '6', 25 | label: '公司6' 26 | }, 27 | { 28 | id: '7', 29 | label: '公司7' 30 | }, 31 | { 32 | id: '8', 33 | label: '公司8' 34 | }, 35 | { 36 | id: '9', 37 | label: '公司9' 38 | } 39 | ], 40 | edges: [ 41 | { 42 | source: '1', 43 | target: '2', 44 | data: { 45 | type: 'name1', 46 | amount: '100,000,000,00 元', 47 | date: '2019-08-03' 48 | } 49 | }, 50 | { 51 | source: '1', 52 | target: '3', 53 | data: { 54 | type: 'name2', 55 | amount: '100,000,000,00 元', 56 | date: '2019-08-03' 57 | } 58 | }, 59 | { 60 | source: '2', 61 | target: '5', 62 | data: { 63 | type: 'name1', 64 | amount: '100,000,000,00 元', 65 | date: '2019-08-03' 66 | } 67 | }, 68 | { 69 | source: '5', 70 | target: '6', 71 | data: { 72 | type: 'name2', 73 | amount: '100,000,000,00 元', 74 | date: '2019-08-03' 75 | } 76 | }, 77 | { 78 | source: '3', 79 | target: '4', 80 | data: { 81 | type: 'name3', 82 | amount: '100,000,000,00 元', 83 | date: '2019-08-03' 84 | } 85 | }, 86 | { 87 | source: '4', 88 | target: '7', 89 | data: { 90 | type: 'name2', 91 | amount: '100,000,000,00 元', 92 | date: '2019-08-03' 93 | } 94 | }, 95 | { 96 | source: '1', 97 | target: '8', 98 | data: { 99 | type: 'name2', 100 | amount: '100,000,000,00 元', 101 | date: '2019-08-03' 102 | } 103 | }, 104 | { 105 | source: '1', 106 | target: '9', 107 | data: { 108 | type: 'name3', 109 | amount: '100,000,000,00 元', 110 | date: '2019-08-03' 111 | } 112 | } 113 | ] 114 | }; -------------------------------------------------------------------------------- /pages/tree/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Button } from 'antd' 4 | import G6 from '@antv/g6'; 5 | 6 | const treeData = { 7 | id: 'root', 8 | label: 'Root', 9 | children: [ 10 | { 11 | id: 'SubTreeNode1', 12 | label: 'subroot1', 13 | children: [ 14 | { 15 | id: 'SubTreeNode1.1', 16 | label: 'subroot1.1', 17 | } 18 | ] 19 | }, 20 | { 21 | id: 'SubTreeNode2', 22 | label: 'subroot2', 23 | children: [ 24 | { 25 | id: 'SubTreeNode2.1', 26 | label: 'subroot2.1', 27 | }, 28 | { 29 | id: 'SubTreeNode2.2', 30 | label: 'subroot2.2', 31 | } 32 | ] 33 | } 34 | ] 35 | }; 36 | 37 | const TreeGraphReact = () => { 38 | const ref = React.useRef(null) 39 | let graph = null 40 | 41 | useEffect(() => { 42 | if(!graph) { 43 | 44 | graph = new G6.TreeGraph({ 45 | container: ref.current, 46 | width: 500, 47 | height: 500, 48 | modes: { 49 | default: ['drag-canvas'] 50 | }, 51 | defaultEdge: { 52 | shape: 'cubic-horizontal', 53 | style: { 54 | stroke: '#A3B1BF' 55 | } 56 | }, 57 | defaultNode: { 58 | shape: 'rect', 59 | labelCfg: { 60 | style: { 61 | fill: '#000000A6', 62 | fontSize: 10 63 | } 64 | }, 65 | style: { 66 | stroke: '#72CC4A', 67 | width: 150 68 | } 69 | }, 70 | layout: { 71 | type: 'dendrogram', // 布局类型 72 | direction: 'LR', // 自左至右布局,可选的有 H / V / LR / RL / TB / BT 73 | nodeSep: 50, // 节点之间间距 74 | rankSep: 200 // 每个层级之间的间距 75 | } 76 | }) 77 | } 78 | graph.data(treeData) 79 | graph.render() 80 | }, []) 81 | 82 | const handleChangeData = () => { 83 | const node = graph.findById('SubTreeNode1') 84 | graph.updateItem(node, { 85 | label: 'xxx', 86 | style: { 87 | fill: 'red' 88 | } 89 | }) 90 | } 91 | 92 | return ( 93 |
94 | 95 |
96 | ); 97 | } 98 | 99 | export default TreeGraphReact -------------------------------------------------------------------------------- /pages/tutorital.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import G6 from '@antv/g6'; 3 | 4 | const data = { 5 | nodes: [ 6 | { id: 'node0', size: 50 }, 7 | { id: 'node1', size: 30 }, 8 | { id: 'node2', size: 30 }, 9 | { id: 'node3', size: 30 }, 10 | { id: 'node4', size: 30 }, 11 | { id: 'node5', size: 30 }, 12 | { id: 'node6', size: 15 }, 13 | { id: 'node7', size: 15 }, 14 | { id: 'node8', size: 15 }, 15 | { id: 'node9', size: 15 }, 16 | { id: 'node10', size: 15 }, 17 | { id: 'node11', size: 15 }, 18 | { id: 'node12', size: 15 }, 19 | { id: 'node13', size: 15 }, 20 | { id: 'node14', size: 15 }, 21 | { id: 'node15', size: 15 }, 22 | { id: 'node16', size: 15 }, 23 | ], 24 | edges: [ 25 | { source: 'node0', target: 'node1' }, 26 | { source: 'node0', target: 'node2' }, 27 | { source: 'node0', target: 'node3' }, 28 | { source: 'node0', target: 'node4' }, 29 | { source: 'node0', target: 'node5' }, 30 | { source: 'node1', target: 'node6' }, 31 | { source: 'node1', target: 'node7' }, 32 | { source: 'node2', target: 'node8' }, 33 | { source: 'node2', target: 'node9' }, 34 | { source: 'node2', target: 'node10' }, 35 | { source: 'node2', target: 'node11' }, 36 | { source: 'node2', target: 'node12' }, 37 | { source: 'node2', target: 'node13' }, 38 | { source: 'node3', target: 'node14' }, 39 | { source: 'node3', target: 'node15' }, 40 | { source: 'node3', target: 'node16' }, 41 | ], 42 | }; 43 | 44 | const Tutorital = () => { 45 | const ref = React.useRef(null) 46 | let graph = null 47 | 48 | useEffect(() => { 49 | if(!graph) { 50 | // 实例化 Minimap 51 | const minimap = new G6.Minimap() 52 | 53 | // 实例化 Graph 54 | graph = new G6.Graph({ 55 | container: ref.current, 56 | width: 600, 57 | height: 400, 58 | plugins: [minimap], 59 | modes: { 60 | default: ['drag-canvas', 'zoom-canvas'] 61 | }, 62 | defaultNode: { 63 | type: 'circle', 64 | labelCfg: { 65 | style: { 66 | fill: '#000000A6', 67 | fontSize: 10 68 | } 69 | }, 70 | style: { 71 | stroke: '#72CC4A', 72 | width: 150 73 | } 74 | }, 75 | defaultEdge: { 76 | type: 'line' 77 | }, 78 | layout: { 79 | type: 'force', 80 | preventOverlap: true, 81 | linkDistance: d => { 82 | if (d.source.id === 'node0') { 83 | return 100; 84 | } 85 | return 30; 86 | }, 87 | }, 88 | nodeStateStyles: { 89 | hover: { 90 | stroke: 'red', 91 | lineWidth: 3 92 | } 93 | }, 94 | edgeStateStyles: { 95 | hover: { 96 | stroke: 'blue', 97 | lineWidth: 3 98 | } 99 | } 100 | }) 101 | } 102 | 103 | graph.data(data) 104 | 105 | graph.render() 106 | 107 | graph.on('node:mouseenter', evt => { 108 | graph.setItemState(evt.item, 'hover', true) 109 | }) 110 | 111 | graph.on('node:mouseleave', evt => { 112 | graph.setItemState(evt.item, 'hover', false) 113 | }) 114 | 115 | graph.on('edge:mouseenter', evt => { 116 | graph.setItemState(evt.item, 'hover', true) 117 | }) 118 | 119 | graph.on('edge:mouseleave', evt => { 120 | graph.setItemState(evt.item, 'hover', false) 121 | }) 122 | 123 | }, []) 124 | 125 | return
126 | } 127 | 128 | export default Tutorital 129 | -------------------------------------------------------------------------------- /pages/registerShape.js: -------------------------------------------------------------------------------- 1 | import G6 from '@antv/g6' 2 | const colorMap = { 3 | 'name1': '#72CC4A', 4 | 'name2': '#1A91FF', 5 | 'name3': '#FFAA15' 6 | } 7 | 8 | G6.registerNode('node', { 9 | drawShape: function drawShape(cfg, group) { 10 | const width = cfg.style.width; 11 | const stroke = cfg.style.stroke; 12 | const rect = group.addShape('rect', { 13 | attrs: { 14 | x: -width / 2, 15 | y: -15, 16 | width, 17 | height: 30, 18 | radius: 15, 19 | stroke, 20 | lineWidth: 0.6, 21 | fillOpacity: 1, 22 | fill: '#fff' 23 | } 24 | }); 25 | const point1 = group.addShape('circle', { 26 | attrs: { 27 | x: -width / 2, 28 | y: 0, 29 | r: 3, 30 | fill: stroke 31 | } 32 | }); 33 | const point2 = group.addShape('circle', { 34 | attrs: { 35 | x: width / 2, 36 | y: 0, 37 | r: 3, 38 | fill: stroke 39 | } 40 | }); 41 | return rect; 42 | }, 43 | getAnchorPoints: function getAnchorPoints() { 44 | return [[0, 0.5], [1, 0.5]]; 45 | }, 46 | update: function (cfg, item) { 47 | const group = item.getContainer() 48 | const children = group.get('children') 49 | const node = children[0] 50 | const circleLeft = children[1] 51 | const circleRight = children[2] 52 | 53 | const {style: {stroke}, labelStyle} = cfg 54 | 55 | if (stroke) { 56 | node.attr('stroke', stroke) 57 | circleLeft.attr('fill', stroke) 58 | circleRight.attr('fill', stroke) 59 | } 60 | } 61 | }, 62 | 'single-shape' 63 | ); 64 | 65 | G6.registerEdge('polyline', { 66 | itemType: 'edge', 67 | draw: function draw(cfg, group) { 68 | const startPoint = cfg.startPoint; 69 | const endPoint = cfg.endPoint; 70 | 71 | const Ydiff = endPoint.y - startPoint.y; 72 | 73 | const slope = Ydiff !== 0 ? 500 / Math.abs(Ydiff) : 0; 74 | 75 | const cpOffset = 16; 76 | const offset = Ydiff < 0 ? cpOffset : -cpOffset; 77 | 78 | const line1EndPoint = { 79 | x: startPoint.x + slope, 80 | y: endPoint.y + offset 81 | }; 82 | const line2StartPoint = { 83 | x: line1EndPoint.x + cpOffset, 84 | y: endPoint.y 85 | }; 86 | 87 | // 控制点坐标 88 | const controlPoint = { 89 | x: 90 | ((line1EndPoint.x - startPoint.x) * (endPoint.y - startPoint.y)) / 91 | (line1EndPoint.y - startPoint.y) + 92 | startPoint.x, 93 | y: endPoint.y 94 | }; 95 | 96 | let path = [ 97 | ['M', startPoint.x, startPoint.y], 98 | ['L', line1EndPoint.x, line1EndPoint.y], 99 | [ 100 | 'Q', 101 | controlPoint.x, 102 | controlPoint.y, 103 | line2StartPoint.x, 104 | line2StartPoint.y 105 | ], 106 | ['L', endPoint.x, endPoint.y] 107 | ]; 108 | 109 | if (Ydiff === 0) { 110 | path = [ 111 | ['M', startPoint.x, startPoint.y], 112 | ['L', endPoint.x, endPoint.y] 113 | ]; 114 | } 115 | 116 | const line = group.addShape('path', { 117 | attrs: { 118 | path, 119 | stroke: colorMap[cfg.data.type], 120 | lineWidth: 1.2, 121 | endArrow: false 122 | } 123 | }); 124 | 125 | const labelLeftOffset = 8; 126 | const labelTopOffset = 8; 127 | // amount 128 | const amount = group.addShape('text', { 129 | attrs: { 130 | text: cfg.data.amount, 131 | x: line2StartPoint.x + labelLeftOffset, 132 | y: endPoint.y - labelTopOffset - 2, 133 | fontSize: 14, 134 | textAlign: 'left', 135 | textBaseline: 'middle', 136 | fill: '#000000D9' 137 | } 138 | }); 139 | // type 140 | const type = group.addShape('text', { 141 | attrs: { 142 | text: cfg.data.type, 143 | x: line2StartPoint.x + labelLeftOffset, 144 | y: endPoint.y - labelTopOffset - amount.getBBox().height - 2, 145 | fontSize: 10, 146 | textAlign: 'left', 147 | textBaseline: 'middle', 148 | fill: '#000000D9' 149 | } 150 | }); 151 | // date 152 | const date = group.addShape('text', { 153 | attrs: { 154 | text: cfg.data.date, 155 | x: line2StartPoint.x + labelLeftOffset, 156 | y: endPoint.y + labelTopOffset + 4, 157 | fontSize: 12, 158 | fontWeight: 300, 159 | textAlign: 'left', 160 | textBaseline: 'middle', 161 | fill: '#000000D9' 162 | } 163 | }); 164 | return line; 165 | } 166 | }); -------------------------------------------------------------------------------- /pages/app.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { data } from './data'; 3 | import G6 from '@antv/g6'; 4 | import { NodeTooltips, EdgeToolTips, NodeContextMenu } from './component' 5 | import './registerShape'; 6 | 7 | export default function() { 8 | const ref = React.useRef(null) 9 | let graph = null 10 | 11 | // 边tooltip坐标 12 | const [showEdgeTooltip, setShowEdgeTooltip] = useState(false) 13 | const [edgeTooltipX, setEdgeTooltipX] = useState(0) 14 | const [edgeTooltipY, setEdgeTooltipY] = useState(0) 15 | 16 | // 节点tooltip坐标 17 | const [showNodeTooltip, setShowNodeTooltip] = useState(false) 18 | const [nodeTooltipX, setNodeToolTipX] = useState(0) 19 | const [nodeTooltipY, setNodeToolTipY] = useState(0) 20 | 21 | // 节点ContextMenu坐标 22 | const [showNodeContextMenu, setShowNodeContextMenu] = useState(false) 23 | const [nodeContextMenuX, setNodeContextMenuX] = useState(0) 24 | const [nodeContextMenuY, setNodeContextMenuY] = useState(0) 25 | const bindEvents = () => { 26 | // 监听edge上面mouse事件 27 | graph.on('edge:mouseenter', evt => { 28 | const { item, target } = evt 29 | debugger 30 | const type = target.get('type') 31 | if(type !== 'text') { 32 | return 33 | } 34 | const model = item.getModel() 35 | const { endPoint } = model 36 | // y=endPoint.y - height / 2,在同一水平线上,x值=endPoint.x - width - 10 37 | const y = endPoint.y - 35 38 | const x = endPoint.x - 150 - 10 39 | const point = graph.getCanvasByPoint(x, y) 40 | setEdgeTooltipX(point.x) 41 | setEdgeTooltipY(point.y) 42 | setShowEdgeTooltip(true) 43 | }) 44 | 45 | graph.on('edge:mouseleave', () => { 46 | setShowEdgeTooltip(false) 47 | }) 48 | 49 | // 监听node上面mouse事件 50 | graph.on('node:mouseenter', evt => { 51 | const { item } = evt 52 | const model = item.getModel() 53 | const { x, y } = model 54 | const point = graph.getCanvasByPoint(x, y) 55 | 56 | setNodeToolTipX(point.x - 75) 57 | setNodeToolTipY(point.y + 15) 58 | setShowNodeTooltip(true) 59 | }) 60 | 61 | // 节点上面触发mouseleave事件后隐藏tooltip和ContextMenu 62 | graph.on('node:mouseleave', () => { 63 | setShowNodeTooltip(false) 64 | setShowNodeContextMenu(false) 65 | }) 66 | 67 | // 监听节点上面右键菜单事件 68 | graph.on('node:contextmenu', evt => { 69 | const { item } = evt 70 | const model = item.getModel() 71 | const { x, y } = model 72 | const point = graph.getCanvasByPoint(x, y) 73 | setNodeContextMenuX(point.x) 74 | setNodeContextMenuY(point.y) 75 | setShowNodeContextMenu(true) 76 | }) 77 | } 78 | 79 | useEffect(() => { 80 | if(!graph) { 81 | const miniMap = new G6.Minimap() 82 | graph = new G6.Graph({ 83 | container: ref.current, 84 | width: 1200, 85 | height: 800, 86 | modes: { 87 | default: ['drag-canvas', 'drag-node'] 88 | }, 89 | defaultNode: { 90 | shape: 'node', 91 | // 节点文本样式 92 | labelCfg: { 93 | style: { 94 | fill: '#000000A6', 95 | fontSize: 10 96 | } 97 | }, 98 | // 节点默认样式 99 | style: { 100 | stroke: '#72CC4A', 101 | width: 150 102 | } 103 | }, 104 | defaultEdge: { 105 | shape: 'polyline' 106 | }, 107 | // 节点交互状态配置 108 | nodeStateStyles: { 109 | hover: { 110 | stroke: 'red', 111 | lineWidth: 3 112 | } 113 | }, 114 | edgeStateStyles: { 115 | hover: { 116 | stroke: 'blue', 117 | lineWidth: 3 118 | } 119 | }, 120 | layout: { 121 | type: 'dagre', 122 | rankdir: 'LR', 123 | nodesep: 30, 124 | ranksep: 100 125 | }, 126 | plugins: [miniMap] 127 | }) 128 | } 129 | 130 | graph.data(data) 131 | 132 | graph.render() 133 | 134 | const edges = graph.getEdges() 135 | edges.forEach(edge => { 136 | const line = edge.getKeyShape() 137 | const stroke = line.attr('stroke') 138 | const targetNode = edge.getTarget() 139 | targetNode.update({ 140 | style: { stroke } 141 | }) 142 | }) 143 | graph.paint() 144 | 145 | bindEvents() 146 | }, []) 147 | 148 | return ( 149 |
150 | { showEdgeTooltip && } 151 | { showNodeTooltip && } 152 | { showNodeContextMenu && } 153 |
154 | ); 155 | } 156 | --------------------------------------------------------------------------------