├── .gitignore ├── README.md ├── dist ├── index.html ├── main.js └── main.js.map ├── index.html ├── package.json ├── src ├── D3ReactForce │ ├── default.js │ ├── index.js │ ├── layout │ │ ├── archimeddeanSpiral.js │ │ ├── base.js │ │ ├── circle.js │ │ ├── dagre.js │ │ ├── grid.js │ │ ├── index.js │ │ └── is.js │ ├── link.js │ ├── node.js │ └── simulation.js ├── app.js ├── index.js └── mock │ └── data.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | .ipr 4 | .iws 5 | *~ 6 | ~* 7 | *.diff 8 | *.patch 9 | *.bak 10 | .DS_Store 11 | Thumbs.db 12 | .project 13 | .*proj 14 | .svn/ 15 | *.swp 16 | *.swo 17 | *.log 18 | *.log.* 19 | *.json.gzip 20 | node_modules/ 21 | .buildpath 22 | .settings 23 | npm-debug.log 24 | nohup.out 25 | _site 26 | _data 27 | /lib 28 | /es 29 | elasticsearch-* 30 | config/base.yaml 31 | /.vscode/ 32 | /coverage 33 | yarn.lock 34 | /.history 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d3-react-force 2 | 3 | react版d3-force封装,简化d3-force配置。 4 | 5 | ## 安装 6 | 7 | ``` 8 | npm install d3-react-force 9 | // yarn add d3-react-force 10 | ``` 11 | 12 | ## 在线演示 13 | 14 | [https://yacan8.github.io/d3-react-force/](https://yacan8.github.io/d3-react-force/) 15 | 16 | ## props参数 17 | 18 | | 参数 | 说明 | 类型 | 默认值 | 19 | -----|-----|-----|------ 20 | | width | 容器宽度 | number | 800 | 21 | | height | 容器高度 | number | 800 | 22 | | nodeIdKey | 节点表示键值 | string | 'id' | 23 | | velocityDecay | 节点速度衰减系数,可理解为摩擦力,0~1之间 | number | 0.1 | 24 | | linkDistance | 连线长度 | number或(link) => number | 0.1 | 25 | | collideRadius | 节点碰撞半径 | number或(node) => number | 0 | 26 | | collideStrength| 节点碰撞强度,0~1之间 | number | 0.5 | 27 | | chargeStrength | 节点之间作用力,整数为引力,负数为斥力 | number | -10 | 28 | | staticLayout | 是否为静态布局(需要事先使用/simulation.js计算节点位置)| boolean | false | 29 | | XYCenter | 是否添加x、y作用力,居中效果,避免不连通图游离 | boolean或Object | {x: 0, y: 0} | 30 | | tick | 动画回调,每一帧 | function(alpah) | noop | 31 | | end | tick结束回调 | function | noop | 32 | | NodeElement | 节点 | React.Element或(node)=> React.Element | circle | 33 | | LinkElement | 边 | (link, addRef) => React.Element或object | link | 34 | | nodeClick | 节点点击事件 | function(node, d3.event) | noop | 35 | | nodeDbClick | 节点双击事件 | function(node, d3.event) | noop | 36 | | nodeMouseover | 节点mouseover事件 | function(node, d3.event) | noop | 37 | | nodeMouseout | 节点mouseout事件 | function(node, d3.event) | noop | 38 | | linkClick | 边点击事件 | function(link, d3.event) | noop | 39 | | linkMouseover | 边mouseover事件 | function(link, d3.event) | noop | 40 | | linkMouseout | 边mouseout事件 | function(link, d3.event) | noop | 41 | | dragEvent | 节点拖拽事件,start、isDrag、drag、end四个事件函数 isDrag判断是否拖拽,返回boolean | Object | {} | 42 | | zoomEvent | 缩放事件,start、isZoom、zoom、end四个事件函数,isZoom判断是否缩放,返回boolean | Object | {} | 43 | 44 | ## API 45 | 46 | 通过ref方式获取组件示例,使用下列API: 47 | 48 | ### adaption(animate) 49 | 50 | 视图居中,animate表示是否动画移动。 51 | 52 | ### transform(translate, scale, animate) 53 | 54 | 缩放平移,translate为数组,数组第一个值为x偏移量,第二个值为y偏移量,scale为缩放比例,animate表示是否动画,默认不使用动画。如果不传任何参数,则返回偏移量与缩放比例。 55 | 56 | ### forceEndTick() 57 | 58 | 强制停止tick动画动画。 59 | 60 | ### addLayout(layout, options) 61 | 62 | 添加布局,layout分为圆形布局circle、阿基米德螺旋布局archimeddeanSpiral、栅格布局grid、分层布局dagre,options为布局参数,返回包含执行布局函数对象。如component.addLayout('circle').execute(),或者使用component.executeLayout('circle',{beforeExecute:() =>{}}) 63 | 64 | ### free() 65 | 66 | 布局释放,布局layout后节点x、y固定,使用free方法释放节点,变成力导向布局。 67 | 68 | ### execute() 69 | 70 | 同步执行里导向布局至静止,注意:不会更新视图,需要手动执行tick更新视图。 71 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | d3-react-force 6 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | d3-react-force 6 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-react-force", 3 | "version": "1.0.15", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "export NODE_ENV=development && webpack-serve --config ./webpack.config.js", 8 | "clean": "rimraf lib", 9 | "build": "npm run clean && echo Using Babel && babel --version && babel src/D3ReactForce --out-dir lib -s", 10 | "pub": "npm run build && npm publish", 11 | "release": "export NODE_ENV=production && webpack" 12 | }, 13 | "babel": { 14 | "presets": [ 15 | "es2015", 16 | "stage-2", 17 | "react" 18 | ], 19 | "plugins": [ 20 | "babel-plugin-transform-class-properties" 21 | ] 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/yacan8/d3-react-force.git" 26 | }, 27 | "keywords": [ 28 | "d3", 29 | "react" 30 | ], 31 | "author": "can.yang", 32 | "license": "ISC", 33 | "homepage": "https://github.com/yacan8/d3-react-force#readme", 34 | "devDependencies": { 35 | "babel-cli": "^6.26.0", 36 | "babel-loader": "^7.1.4", 37 | "babel-plugin-transform-class-properties": "^6.24.1", 38 | "babel-preset-react": "^6.24.1", 39 | "babel-preset-react-hmre": "^1.1.1", 40 | "babel-preset-stage-2": "^6.24.1", 41 | "css-loader": "^0.28.11", 42 | "html-webpack-plugin": "^3.2.0", 43 | "less-loader": "^4.1.0", 44 | "style-loader": "^0.20.3", 45 | "webpack": "^4.6.0", 46 | "webpack-cli": "^2.0.14", 47 | "webpack-serve": "^2.0.2" 48 | }, 49 | "peerDependencies": { 50 | "react": "^16.4.2", 51 | "react-dom": "^16.4.2" 52 | }, 53 | "dependencies": { 54 | "d3-drag": "^1.2.3", 55 | "d3-force": "^1.1.2", 56 | "d3-selection": "^1.3.2", 57 | "d3-zoom": "^1.7.3", 58 | "dagre": "^0.8.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/D3ReactForce/default.js: -------------------------------------------------------------------------------- 1 | 2 | export const WIDTH = 800; 3 | export const HEIGHT = 800; 4 | export const NODE_ID_KEY = 'id'; 5 | export const VELOCITY_DECAY = 0.1; 6 | export const LINK_DISTANCE = 50; 7 | export const COLLIDE_RADIUS = 0; 8 | export const COLLIDE_STRENGTH = 0.5; 9 | export const CHARGE_STRENGTH = -10; 10 | export const XY_CENTER = {x: 0, y: 0}; 11 | 12 | export function noop() {} 13 | -------------------------------------------------------------------------------- /src/D3ReactForce/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { select as d3_select } from 'd3-selection'; 3 | import { zoomIdentity } from 'd3-zoom'; 4 | import Simulation from './simulation'; 5 | import Node from './node'; 6 | import Link from './link'; 7 | import Layout from './layout'; 8 | import _minBy from 'lodash/minBy'; 9 | import _maxBy from 'lodash/maxBy'; 10 | import { 11 | WIDTH, 12 | HEIGHT, 13 | NODE_ID_KEY, 14 | VELOCITY_DECAY, 15 | LINK_DISTANCE, 16 | COLLIDE_RADIUS, 17 | COLLIDE_STRENGTH, 18 | CHARGE_STRENGTH, 19 | XY_CENTER, 20 | noop 21 | } from './default'; 22 | 23 | 24 | class D3ReactForce extends React.Component { 25 | static defaultProps = { 26 | nodes: [], 27 | links: [], 28 | width: WIDTH, 29 | height: HEIGHT, 30 | nodeIdKey: NODE_ID_KEY, 31 | velocityDecay: VELOCITY_DECAY, 32 | linkDistance: LINK_DISTANCE, 33 | collideRadius: COLLIDE_RADIUS, 34 | collideStrength: COLLIDE_STRENGTH, 35 | chargeStrength: CHARGE_STRENGTH, 36 | staticLayout: false, 37 | XYCenter: XY_CENTER, 38 | nodeClick: noop, 39 | nodeDbClick: noop, 40 | nodeMouseover: noop, 41 | nodeMouseout: noop, 42 | linkClick: noop, 43 | linkMouseover: noop, 44 | linkMouseout: noop, 45 | tick: noop, 46 | end: noop, 47 | duration: 500, 48 | hasHoverLink: true 49 | } 50 | 51 | nodesDom = {}; // 保存节点的dom 52 | linksDom = {}; // 保存边的dom 53 | hoverLinksDom = {}; // 保存hover边的dom 54 | mainDom = {}; // 保存节点的包裹节点的dom,直接存this下的话react可能有bug,取不到 55 | state = { 56 | init: false 57 | } 58 | constructor(props) { 59 | super(props); 60 | const { nodes, links, simulation } = props; 61 | this.force = new Simulation(props, simulation); 62 | this.force.setNodesLinks(nodes, links); 63 | } 64 | 65 | tick = (alpha) => { 66 | const { tick, staticLayout } = this.props; 67 | const { nodes, links } = this.force; 68 | if (tick) { 69 | tick(alpha, { 70 | nodes, 71 | links, 72 | nodesDom: this.nodesDom, 73 | linksDom: this.linksDom 74 | }) 75 | } 76 | if (!staticLayout) { 77 | nodes.forEach(node => this.nodeTick(node)) 78 | links.forEach(link => this.linkTick(link)) 79 | } 80 | } 81 | 82 | linkTick = (link, animation = false) => { 83 | const { force, props } = this; 84 | const { nodeIdKey, hasHoverLink, duration } = props; 85 | let linkDom = d3_select(this.linksDom[`${link.source[nodeIdKey]}_${link.target[nodeIdKey]}`]); 86 | if (animation) linkDom = linkDom.transition().duration(duration); 87 | linkDom.attr('x1', () => force.nodesMap[link.source[nodeIdKey]].x) 88 | .attr('y1', () => force.nodesMap[link.source[nodeIdKey]].y) 89 | .attr('x2', () => force.nodesMap[link.target[nodeIdKey]].x) 90 | .attr('y2', () => force.nodesMap[link.target[nodeIdKey]].y); 91 | if (hasHoverLink) { 92 | let hoverLinkDom = d3_select(this.hoverLinksDom[`${link.source[nodeIdKey]}_${link.target[nodeIdKey]}`]) 93 | if (animation) hoverLinkDom = hoverLinkDom.transition().duration(duration); 94 | hoverLinkDom.attr('x1', () => force.nodesMap[link.source[nodeIdKey]].x) 95 | .attr('y1', () => force.nodesMap[link.source[nodeIdKey]].y) 96 | .attr('x2', () => force.nodesMap[link.target[nodeIdKey]].x) 97 | .attr('y2', () => force.nodesMap[link.target[nodeIdKey]].y); 98 | } 99 | } 100 | 101 | nodeTick = (node, animation = false) => { 102 | const { nodeIdKey, duration } = this.props; 103 | let nodeDom = d3_select(this.nodesDom[node[nodeIdKey]]) 104 | if (animation) nodeDom = nodeDom.transition().duration(duration); 105 | nodeDom.attr('transform', () => `translate(${node.x},${node.y})`); 106 | } 107 | 108 | componentDidMount() { 109 | if (!this.props.staticLayout) { 110 | const { dragEvent = {}, zoomEvent = {}, scaleExtent } = this.props; 111 | this.force.initZoom({ 112 | start: zoomEvent.start, 113 | isZoom: zoomEvent.isZoom, 114 | zoom: (transform) => { 115 | d3_select(this.mainDom.outg).attr('transform', `translate(${transform.translate})scale(${transform.scale})`) 116 | zoomEvent.zoom && zoomEvent.zoom(transform); 117 | }, 118 | end: zoomEvent.end 119 | }, scaleExtent); 120 | d3_select(this.mainDom.svg).call(this.force.zoom).on('dblclick.zoom', null); 121 | this.force.initDrag(dragEvent); 122 | } 123 | this.force.tick({ 124 | tick: this.tick, 125 | end: this.props.end 126 | }); 127 | this.setState({ init: true }); // 解决初始化有nodes时 node先渲染,取不到this.drap问题 128 | } 129 | 130 | free = () => { 131 | this.force.nodes.forEach(node => node.fx = node.fy = null) 132 | this.force.simulation.alpha(1).restart(); 133 | } 134 | 135 | addLayout = (layout, _options = {}) => { 136 | const _Layout = Layout[layout]; 137 | if (_Layout) { 138 | const { width, height } = this.props; 139 | const options = Object.assign({ width, height }, _options); 140 | this[`${layout}Layout`] = new _Layout(options, this.force); 141 | return { 142 | execute: this.executeLayout.bind(this, layout) 143 | } 144 | } else { 145 | throw new Error(`Can not find the layout of ${layout}`); 146 | } 147 | } 148 | 149 | executeLayout = (layout, event = {}) => { 150 | const _layout = this[`${layout}Layout`]; 151 | if (_layout) { 152 | this.forceEndTick(); 153 | setTimeout(() => { // 解决tick直接更新dom的x、y问题 154 | _layout.execute(event); 155 | this.transformPosition(); 156 | this.adaption(true); 157 | }, 100) 158 | } 159 | } 160 | 161 | // 居中 162 | adaption = (animation = false) => { 163 | const padding = 20; 164 | const { width, height, duration } = this.props; 165 | const { nodes } = this.force; 166 | let minX = 0, minY = 0, maxX = 0, maxY = 0; 167 | if (nodes.length) { 168 | minX = _minBy(nodes, 'x').x 169 | minY = _minBy(nodes, 'y').y 170 | maxX = _maxBy(nodes, 'x').x 171 | maxY = _maxBy(nodes, 'y').y 172 | } 173 | const offset = { 174 | width: maxX - minX, 175 | height: maxY - minY, 176 | x: minX, 177 | y: minY 178 | }; 179 | let factor = 1, translateX = 0, translateY = 0; 180 | if (offset.width > (width - padding) || offset.height > (height - padding)) { 181 | if (offset.width / width > offset.height / height) { 182 | factor = (width - padding) / offset.width; 183 | } else { 184 | factor = (height - padding) / offset.height; 185 | } 186 | } 187 | translateX = width / 2 - minX * factor - offset.width / 2 * factor; 188 | translateY = height / 2 - minY * factor - offset.height / 2 * factor; 189 | if (animation) { 190 | d3_select(this.mainDom.outg).transition().duration(duration).attr('transform', `translate(${[translateX, translateY]})scale(${factor})`) 191 | setTimeout(() => { 192 | this.force.zoom.transform(d3_select(this.mainDom.svg), zoomIdentity.translate(translateX, translateY).scale(factor)); 193 | }, duration); 194 | } else { 195 | this.force.zoom.transform(d3_select(this.mainDom.svg), zoomIdentity.translate(translateX, translateY).scale(factor)); 196 | } 197 | } 198 | 199 | // 执行里导向布局,直至静止 200 | execute = () => this.force.execute(); 201 | 202 | originTransform = (dom, transform) => { 203 | this.force.zoom.transform(dom, transform) 204 | } 205 | 206 | transform = (translate, scale, animation) => { 207 | const { duration } = this.props; 208 | if (!translate && !scale) { 209 | return { 210 | translate: this.force.translate, 211 | scale: this.force.scale 212 | } 213 | } else { 214 | const _zoomIdentity = zoomIdentity.translate(...translate).scale(scale); 215 | if (animation) { 216 | d3_select(this.mainDom.outg).transition().duration(duration).attr('transform', `translate(${translate})scale(${scale})`) 217 | setTimeout(() => { 218 | this.originTransform(d3_select(this.mainDom.svg), _zoomIdentity); 219 | }, duration); 220 | } else { 221 | this.originTransform(d3_select(this.mainDom.svg), _zoomIdentity); 222 | } 223 | } 224 | } 225 | 226 | zoom = (_scale) => { 227 | const { translate, scale } = this.force; 228 | const _zoomIdentity = zoomIdentity.translate(...translate).scale(_scale * scale); 229 | this.originTransform(d3_select(this.mainDom.svg), _zoomIdentity); 230 | } 231 | 232 | transformPosition = () => { 233 | const { nodes, links } = this.force; 234 | nodes.forEach(node => this.nodeTick(node, true)); 235 | links.forEach(link => this.linkTick(link, true)); 236 | } 237 | 238 | forceEndTick = () => { 239 | const alphaTarget = this.force.simulation.alphaTarget(); 240 | this.force.simulation.alphaTarget(alphaTarget).alpha(alphaTarget); 241 | } 242 | 243 | componentWillReceiveProps(nextProps) { 244 | const { nodes, links } = nextProps; 245 | this.force.setSimulationLayout(nextProps); 246 | this.force.setNodesLinks(nodes, links); 247 | } 248 | 249 | getStaticLayoutTransform = () => { 250 | const { nodes, width, padding } = this.props; 251 | const nodesX = nodes.map(node => node.x); 252 | const nodesY = nodes.map(node => node.y); 253 | const minX = Math.min(...nodesX), minY = Math.min(...nodesY), maxX = Math.max(...nodesX), maxY = Math.max(...nodesY); 254 | const graphWidth = maxX - minX, graphHeight = maxY - minY; 255 | const scale = width > graphWidth ? 1 : width / graphWidth; 256 | const translateX = scale * minX, translateY = scale * minY; 257 | return { 258 | width: graphWidth * scale + 2 * padding, 259 | height: graphHeight * scale + 2 * padding, 260 | translate: [-translateX + padding, -translateY + padding], 261 | scale: scale, 262 | graphWidth, 263 | graphHeight 264 | } 265 | } 266 | 267 | render() { 268 | let { width, height, nodeIdKey, staticLayout, svgProps, outgProps } = this.props; 269 | let { translate, scale, nodes, links } = this.force; 270 | if (staticLayout) { 271 | const getStaticLayoutTransform = this.getStaticLayoutTransform(); 272 | width = getStaticLayoutTransform.width; 273 | height = getStaticLayoutTransform.height; 274 | translate = getStaticLayoutTransform.translate; 275 | scale = getStaticLayoutTransform.scale; 276 | } 277 | return this.mainDom.svg = svg} width={width} height={height} {...svgProps}> 278 | this.mainDom.outg = outg} transform={`translate(${translate})scale(${scale})`} {...outgProps}> 279 | { 280 | this.state.init || staticLayout ? [ 281 | 282 | { 283 | links.map((link, i) => { 284 | const key = `${link.source[nodeIdKey]}_${link.target[nodeIdKey]}` 285 | return { 286 | this.linksDom[key] = c; 287 | }} addHoverRef={c => { 288 | this.hoverLinksDom[key] = c; 289 | }} link={link} /> 290 | }) 291 | } 292 | , 293 | 294 | { 295 | nodes.map((node, i) => { 296 | const key = node[nodeIdKey]; 297 | return { 298 | this.nodesDom[key] = c 299 | }} node={node} /> 300 | }) 301 | } 302 | 303 | ] : null 304 | } 305 | {this.props.children} 306 | 307 | 308 | } 309 | } 310 | 311 | D3ReactForce.Simulation = Simulation; 312 | D3ReactForce.Node = Node; 313 | D3ReactForce.Link = Link; 314 | export default D3ReactForce; 315 | -------------------------------------------------------------------------------- /src/D3ReactForce/layout/archimeddeanSpiral.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @fileOverview 阿基米德螺线布局 4 | * https://zh.wikipedia.org/wiki/%E9%98%BF%E5%9F%BA%E7%B1%B3%E5%BE%B7%E8%9E%BA%E7%BA%BF 5 | * @author can.yang 6 | */ 7 | import Base from './base'; 8 | 9 | class ArchimeddeanSpiral extends Base { 10 | constructor(options, simulation) { 11 | super(options, simulation); 12 | Object.assign(this, { 13 | /** 14 | * 宽 15 | * @type {number} 16 | */ 17 | width: null, 18 | 19 | /** 20 | * 高 21 | * @type {number} 22 | */ 23 | height: null, 24 | 25 | /** 26 | * 图中心 27 | * @type {object} 28 | */ 29 | center: null, 30 | 31 | /** 32 | * 参数 a 33 | * @type {number} 34 | */ 35 | a: 20, 36 | 37 | /** 38 | * 参数 b 39 | * @type {number} 40 | */ 41 | b: 15, 42 | 43 | /** 44 | * 最大角度 45 | * @type {number} 46 | */ 47 | maxAngle: 12 * Math.PI 48 | }, options); 49 | } 50 | // 执行布局 51 | execute({ 52 | beforeExecute = () => {}, 53 | }) { 54 | const { nodes } = this.simulation; 55 | const { a, b, maxAngle } = this; 56 | const width = this.width; 57 | const height = this.height; 58 | const center = this.center ? this.center : { 59 | x: width / 2, 60 | y: height / 2 61 | }; 62 | const l = nodes.length; 63 | const angleStep = maxAngle / l; 64 | const getAngle = i => { 65 | return i * angleStep; 66 | }; 67 | const getRadius = angle => { 68 | return a + b * angle; 69 | }; 70 | this.sort && nodes.sort(this.sort); 71 | this.simulation.nodes = nodes; 72 | beforeExecute && beforeExecute(nodes); 73 | nodes.forEach((node, i) => { 74 | const angle = getAngle(i); 75 | const radius = getRadius(angle); 76 | node.fx = node.x = center.x + radius * Math.cos(angle); 77 | node.fy = node.y = center.y + radius * Math.sin(angle); 78 | }); 79 | } 80 | } 81 | export default ArchimeddeanSpiral; -------------------------------------------------------------------------------- /src/D3ReactForce/layout/base.js: -------------------------------------------------------------------------------- 1 | class Base { 2 | /** 3 | * Simulation ../simulation.js 4 | * @type {Simulation} 5 | */ 6 | simulation = null; 7 | 8 | /** 9 | * 宽度 10 | * @type {number} 11 | */ 12 | width = null; 13 | /** 14 | * 高度 15 | * @type {number} 16 | */ 17 | height = null; 18 | 19 | constructor(options, simulation) { 20 | this.simulation = simulation; 21 | if (options.width) { 22 | this.width = options.width; 23 | } 24 | if (options.height) { 25 | this.height = options.height; 26 | } 27 | } 28 | } 29 | export default Base; -------------------------------------------------------------------------------- /src/D3ReactForce/layout/circle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 圆形布局 3 | * @author can.yang 4 | */ 5 | import Base from './base'; 6 | import is from './is'; 7 | class Circle extends Base { 8 | /** 9 | * 是否避免重叠 10 | * @type {boolean} 11 | */ 12 | clockwise = true; 13 | /** 14 | * 起始角度 15 | * @type {boolean} 16 | */ 17 | startAngle = 3 / 2 * Math.PI; 18 | 19 | constructor(options, simulation) { 20 | super(options, simulation); 21 | if (!is.nil(options.clockwise)) { 22 | this.clockwise = options.clockwise; 23 | } 24 | if (!is.nil(options.startAngle)) { 25 | this.startAngle = options.startAngle; 26 | } 27 | if (!is.nil(options.sort)) { 28 | this.sort = options.sort; 29 | } 30 | if (!is.nil(options.center)) { 31 | this.center = options.center; 32 | } 33 | } 34 | 35 | execute({beforeExecute = () => {}}) { 36 | const { nodes, links } = this.simulation; 37 | let radius; 38 | if (nodes.length <= 1) { 39 | radius = 0; 40 | } else { 41 | radius = Math.min(this.width, this.height) / 2; 42 | } 43 | const center = this.center ? this.center : { 44 | x: this.width / 2, 45 | y: this.height / 2 46 | }; 47 | const angleStep = 2 * Math.PI / nodes.length; 48 | this.sort && nodes.sort(this.sort); 49 | beforeExecute && beforeExecute(nodes); 50 | nodes.forEach((node, i) => { 51 | const theta = this.startAngle + i * angleStep * (this.clockwise ? 1 : -1); 52 | const rx = radius * Math.cos(theta); 53 | const ry = radius * Math.sin(theta); 54 | node.fx = node.x = center.x + rx; 55 | node.fy = node.y = center.y + ry; 56 | }) 57 | } 58 | 59 | } 60 | 61 | export default Circle; -------------------------------------------------------------------------------- /src/D3ReactForce/layout/dagre.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 分层布局 3 | * https://github.com/dagrejs/dagre/wiki 4 | * @author can.yang 5 | */ 6 | import Base from './base'; 7 | import dagre from 'dagre'; 8 | import is from './is'; 9 | 10 | class Dagre extends Base { 11 | constructor(options, simulation) { 12 | super(options, simulation); 13 | Object.assign(this, { 14 | rankdir: 'TB', 15 | align: undefined, 16 | nodesep: 50, 17 | edgesep: 10, 18 | ranksep: 50, 19 | marginx: 0, 20 | marginy: 0, 21 | acyclicer: undefined, 22 | useEdgeControlPoint: true, 23 | ranker: 'network-simplex', 24 | callback: null, 25 | nodeWidth: 20, 26 | nodeHeight: 20 27 | }, options); 28 | } 29 | getValue(name) { 30 | const value = this[name]; 31 | if (is.Function(value)) { 32 | return value(); 33 | } 34 | return value; 35 | } 36 | // 执行布局 37 | execute() { 38 | const { nodes, links, nodeIdKey, sourceKey, targetKey, nodesMap } = this.simulation; 39 | const nodeMap = {}; 40 | const g = new dagre.graphlib.Graph(); 41 | const useEdgeControlPoint = this.useEdgeControlPoint; 42 | g.setGraph({ 43 | rankdir: this.getValue('rankdir'), 44 | align: this.getValue('align'), 45 | nodesep: this.getValue('nodesep'), 46 | edgesep: this.getValue('edgesep'), 47 | ranksep: this.getValue('ranksep'), 48 | marginx: this.getValue('marginx'), 49 | marginy: this.getValue('marginy'), 50 | acyclicer: this.getValue('acyclicer'), 51 | ranker: this.getValue('ranker') 52 | }); 53 | g.setDefaultEdgeLabel(function() { return {}; }); 54 | nodes.forEach(node => { 55 | g.setNode(node[nodeIdKey], { width: this.nodeWidth, height: this.nodeHeight }); 56 | nodeMap[node[nodeIdKey]] = node; 57 | }); 58 | links.forEach(link => { 59 | let source = link.source[nodeIdKey], target = link.target[nodeIdKey]; 60 | g.setEdge(source, target); 61 | }); 62 | dagre.layout(g); 63 | g.nodes().forEach(v => { 64 | const node = g.node(v); 65 | nodeMap[v].fx = nodeMap[v].x = node.x; 66 | nodeMap[v].fy = nodeMap[v].y = node.y; 67 | }); 68 | g.edges().forEach((e, i) => { 69 | const edge = g.edge(e); 70 | if (useEdgeControlPoint) { 71 | links[i].controlPoints = edge.points.slice(1, edge.points.length - 1); 72 | } 73 | }); 74 | } 75 | } 76 | 77 | export default Dagre; -------------------------------------------------------------------------------- /src/D3ReactForce/layout/grid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 栅格布局 3 | * @author can.yang 4 | */ 5 | import Base from './base'; 6 | 7 | class Grid extends Base { 8 | constructor(options, simulation) { 9 | super(options, simulation); 10 | Object.assign(this, { 11 | row: 10, 12 | col: 10, 13 | width: null, 14 | height: null 15 | }, options); 16 | } 17 | // 执行布局 18 | execute() { 19 | const { nodes } = this.simulation; 20 | const width = this.width; 21 | const height = this.height 22 | const center = this.center ? this.center : { 23 | x: width / 2, 24 | y: height / 2 25 | }; 26 | const row = this.row; 27 | const col = this.col; 28 | this.sort && nodes.sort(this.sort); 29 | for (let i = 0; i < nodes.length; i++) { 30 | const node = nodes[i]; 31 | node.fx = node.x = (center.x - width / 2) + i % row / row * width; 32 | node.fy = node.y = (center.y - height / 2) + parseInt(i / col) / col * height; 33 | } 34 | } 35 | } 36 | 37 | export default Grid; -------------------------------------------------------------------------------- /src/D3ReactForce/layout/index.js: -------------------------------------------------------------------------------- 1 | import circle from './circle'; 2 | import dagre from './dagre'; 3 | import archimeddeanSpiral from './archimeddeanSpiral'; 4 | import grid from './grid'; 5 | 6 | export default { 7 | circle, 8 | dagre, 9 | archimeddeanSpiral, 10 | grid 11 | } -------------------------------------------------------------------------------- /src/D3ReactForce/layout/is.js: -------------------------------------------------------------------------------- 1 | function type(obj) { 2 | return Object.prototype.toString.call(obj) 3 | } 4 | 5 | const _String = obj => type(obj) === '[object String]' 6 | const _Array = obj => type(obj) === '[object Array]' 7 | const _Object = obj => type(obj) === '[object Object]' 8 | const _Boolean = obj => type(obj) === '[object Boolean]' 9 | const _Function = obj => type(obj) === '[object Function]' 10 | const _nil = obj => obj === null || obj === undefined 11 | const _valid = obj => !!obj 12 | 13 | export default { 14 | String: _String, 15 | Array: _Array, 16 | Object: _Object, 17 | Boolean: _Boolean, 18 | Function: _Function, 19 | nil: _nil, 20 | valid: _valid 21 | } -------------------------------------------------------------------------------- /src/D3ReactForce/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { select as d3_select, event as d3_event } from 'd3-selection'; 3 | 4 | export default class Link extends React.Component { 5 | 6 | componentWillReceiveProps(nextProps) { 7 | this._link.__data__ = nextProps.link; 8 | } 9 | 10 | componentDidMount = () => { 11 | let { link, parentComponent } = this.props; 12 | this._link.__data__ = link; 13 | const { linkClick, linkMouseover, linkMouseout, hasHoverLink } = parentComponent.props; 14 | d3_select(hasHoverLink ? this._hover_link : this._link).on('click', () => { 15 | linkClick.call(this, this.props.link, d3_event) 16 | }).on('mouseover', () => { 17 | linkMouseover.call(this, this.props.link, d3_event) 18 | }).on('mouseout', () => { 19 | linkMouseout.call(this, this.props.link, d3_event) 20 | }) 21 | } 22 | 23 | 24 | saveRef = child => { 25 | const { addRef } = this.props 26 | addRef(child); 27 | this._link = child; 28 | } 29 | 30 | saveHoverLinkRef = child => { 31 | const { addHoverRef } = this.props; 32 | this._hover_link = child; 33 | addHoverRef(child) 34 | } 35 | 36 | getBaseProps = () => { 37 | const { link } = this.props; 38 | const baseProps = { 39 | link: link, 40 | x1: link.source.x, 41 | y1: link.source.y, 42 | x2: link.target.x, 43 | y2: link.target.y, 44 | }; 45 | return baseProps; 46 | } 47 | 48 | getObjectProps = object => { 49 | const { link } = this.props; 50 | const arrts = {}; 51 | Object.keys(object || {}).forEach(attr => { 52 | arrts[attr] = typeof object[attr] === 'function' ? object[attr](link) : object[attr]; 53 | }); 54 | return arrts; 55 | } 56 | 57 | getLinkJsx = () => { 58 | const { addRef, parentComponent } = this.props 59 | const { linkElement } = parentComponent.props; 60 | const baseProps = this.getBaseProps(); 61 | const linkProps = { 62 | ref: this.saveRef, 63 | ...baseProps 64 | } 65 | if (typeof linkElement === 'function') { 66 | return React.cloneElement(linkElement(link), linkProps); 67 | } else if (React.isValidElement(linkElement)){ 68 | const { ref, ...nestProps } = linkProps; 69 | return React.cloneElement(linkElement, {...nestProps, addRef: this.saveRef}) 70 | } else if (typeof linkElement === 'object' || !linkElement) { 71 | const linkAttrs = this.getObjectProps(linkElement); 72 | return 78 | } else { 79 | throw new Error('prop linkElement isValid'); 80 | } 81 | } 82 | 83 | 84 | getHoverLink = () => { 85 | const { parentComponent, addHoverRef } = this.props; 86 | const baseProps = this.getBaseProps(); 87 | const { hoverLink = {} } = parentComponent.props; 88 | const hoverLinkAttrs = this.getObjectProps(hoverLink); 89 | return 97 | } 98 | 99 | render() { 100 | const { hasHoverLink } = this.props; 101 | return 102 | {this.getLinkJsx()} 103 | { 104 | !!hasHoverLink && this.getHoverLink() 105 | } 106 | 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/D3ReactForce/node.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { select as d3_select, event as d3_event } from 'd3-selection'; 3 | 4 | export default class Node extends React.Component { 5 | 6 | componentWillReceiveProps(nextProps) { 7 | this._node.__data__ = nextProps.node; // 解决导入操作记录时候因为图上节点已存在引用变化的问题 8 | } 9 | 10 | initEvent = props => { 11 | const { node, parentComponent } = props; 12 | this._node.__data__ = node; 13 | d3_select(this._node) 14 | .on('click', d => { 15 | const event = d3_event; 16 | event.stopPropagation(); 17 | const { nodeClick, nodeDbClick } = parentComponent.props; 18 | if (d._clickid) { 19 | clearTimeout(d._clickid); 20 | d._clickid = null; 21 | nodeClick.call(this, d, event); 22 | nodeDbClick.call(this, d, event); 23 | } else { 24 | d._clickid = setTimeout(() => { 25 | nodeClick(d, event); 26 | d._clickid = null; 27 | }, 300); 28 | } 29 | }) 30 | .on('mouseover', node => { 31 | const { nodeMouseover } = parentComponent.props; 32 | nodeMouseover.call(this, node, d3_event); 33 | }) 34 | .on('mouseout', node => { 35 | const { nodeMouseout } = parentComponent.props; 36 | nodeMouseout.call(this, node, d3_event); 37 | }) 38 | .call(parentComponent.force.drag) 39 | .on('mouseover.force', null) 40 | .on('mouseout.force', null); 41 | } 42 | 43 | componentDidMount() { 44 | this.initEvent(this.props); 45 | } 46 | 47 | getNode = () => { 48 | const { node, parentComponent } = this.props; 49 | const { nodeElement } = parentComponent.props; 50 | if (nodeElement) { 51 | if (typeof nodeElement === 'function') { 52 | return nodeElement(node) 53 | } else if (React.isValidElement(nodeElement)) { 54 | return React.cloneElement(nodeElement, {node: node}); 55 | } else { 56 | throw new Error('prop nodeElement isValid'); 57 | } 58 | } 59 | return 60 | } 61 | 62 | saveRef = child => { 63 | this._node = child; 64 | this.props.addRef(child); 65 | } 66 | 67 | render() { 68 | const { node, parentComponent } = this.props; 69 | const { nodeIdKey, width, height, nodeProps } = parentComponent.props; 70 | return ( 71 | 75 | 76 | {this.getNode()} 77 | 78 | 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/D3ReactForce/simulation.js: -------------------------------------------------------------------------------- 1 | import { forceCenter, forceLink, forceCollide, forceX, forceY, forceManyBody, forceSimulation } from 'd3-force'; 2 | import * as d3Zoom from 'd3-zoom'; 3 | import * as d3Drag from 'd3-drag'; 4 | import { event as d3_event } from 'd3-selection'; 5 | import { WIDTH, HEIGHT, NODE_ID_KEY, noop } from './default'; 6 | 7 | export default class Simulation { 8 | sourceKey = 'source'; 9 | targetKey = 'target'; 10 | width = WIDTH; 11 | height = HEIGHT; 12 | nodeIdKey = NODE_ID_KEY; 13 | nodes = []; 14 | links = []; 15 | nodesMap = {}; 16 | translate = [0, 0]; 17 | scale = 1; 18 | 19 | constructor(options, simulation) { 20 | const _simulation = simulation || forceSimulation(); 21 | this.simulation = _simulation; 22 | this.setSimulationLayout(options); 23 | } 24 | 25 | setSimulationLayout({velocityDecay, nodeIdKey, linkDistance, collideRadius, collideStrength, chargeStrength, alphaDecay, alphaMin, XYCenter, width, height, sourceKey, targetKey}) { 26 | this.width = width; 27 | this.height = height; 28 | this.nodeIdKey = nodeIdKey; 29 | if (sourceKey) { 30 | this.sourceKey = sourceKey; 31 | } 32 | if (targetKey) { 33 | this.targetKey = targetKey; 34 | } 35 | if (velocityDecay) { 36 | this.simulation.velocityDecay(velocityDecay) 37 | } 38 | if (alphaMin) { 39 | this.simulation.alphaMin(alphaMin); 40 | } 41 | if (alphaDecay) { 42 | this.simulation.alphaDecay(alphaDecay); 43 | } 44 | const _forceLink = forceLink(); 45 | if (nodeIdKey) { 46 | _forceLink.id(d => d[nodeIdKey]); 47 | } 48 | if (linkDistance) { 49 | _forceLink.distance(linkDistance); 50 | } 51 | this.simulation.force('link', _forceLink); 52 | // this.simulation.force('center', forceCenter()); 53 | 54 | if (collideRadius || collideStrength) { 55 | const _forceCollide = forceCollide(); 56 | if (collideRadius) { 57 | _forceCollide.radius(typeof collideRadius === 'function' ? (d) => { 58 | return collideRadius(d); 59 | } : collideRadius); 60 | } 61 | if (collideStrength) { 62 | _forceCollide.strength(collideStrength); 63 | } 64 | this.simulation.force('collide', _forceCollide); 65 | } else { 66 | this.simulation.force('collide', null); 67 | } 68 | 69 | if (chargeStrength) { 70 | this.simulation.force("charge", forceManyBody().strength(chargeStrength)); 71 | } 72 | if (XYCenter) { 73 | this.simulation.force('x', forceX(XYCenter && XYCenter.x || undefined)) 74 | this.simulation.force('y', forceY(XYCenter && XYCenter.y || undefined)) 75 | } else { 76 | this.simulation.force('x', null); 77 | this.simulation.force('y', null); 78 | } 79 | } 80 | 81 | tick = (event) => { 82 | this.simulation.on('tick', () => { 83 | event.tick && event.tick(this.simulation.alpha()) 84 | }) 85 | .on('end', () => { 86 | event.end && event.end(this.simulation.alpha()); 87 | }) 88 | } 89 | 90 | initNodes(nodes) { 91 | const { nodeIdKey, width, height } = this; 92 | const nodesMap = {}; 93 | const originNodes = []; 94 | nodes.forEach(node => { 95 | node.x = node.x || width / 2; 96 | node.y = node.y || height / 2; 97 | nodesMap[node[nodeIdKey]] = node; 98 | originNodes.push(node); 99 | }) 100 | this.nodesMap = nodesMap; 101 | this.nodes = originNodes; 102 | } 103 | 104 | initLinks(links) { 105 | const { nodeIdKey, sourceKey, targetKey } = this; 106 | const newLinks = []; 107 | links.forEach(link => { 108 | let source = link[sourceKey], target = link[targetKey]; 109 | if (typeof source === 'object') { 110 | source = source[nodeIdKey]; 111 | } 112 | if (typeof target === 'object') { 113 | target = target[nodeIdKey]; 114 | } 115 | const sourcePush = this.nodesMap[source]; 116 | const targetPush = this.nodesMap[target]; 117 | if (sourcePush && targetPush) { 118 | link.source = sourcePush; 119 | link.target = targetPush; 120 | newLinks.push(link); 121 | } 122 | }) 123 | this.links = newLinks; 124 | } 125 | 126 | 127 | setNodesLinks(nodes, links, alpha) { 128 | this.initNodes(nodes); 129 | this.initLinks(links); 130 | this.simulation.nodes(this.nodes).force('link').links(this.links); 131 | if (alpha) { 132 | this.start(alpha); 133 | } 134 | } 135 | 136 | start(alpha) { 137 | const _alpha = this.simulation.alpha() + alpha; 138 | this.simulation.alpha(_alpha > 1 ? 1 : alpha).restart(); 139 | } 140 | 141 | initDrag(event = {}) { 142 | const drag = d3Drag.drag() 143 | .on('start', (d) => { 144 | if (event.isDrag && event.isDrag(d) || !event.isDrag) { 145 | if (!d3_event.active) { 146 | this.simulation.alphaTarget(0.5).restart(); 147 | } 148 | event.start && event.start(d); 149 | } 150 | }) 151 | .on('drag', d => { 152 | if (event.isDrag && event.isDrag(d) || !event.isDrag) { 153 | d.fx = d.x = d3_event.x; 154 | d.fy = d.y = d3_event.y; 155 | event.drag && event.drag(d); 156 | } 157 | }) 158 | .on('end', d => { 159 | if (event.isDrag && event.isDrag(d) || !event.isDrag) { 160 | if (!d3_event.active) { 161 | this.simulation.alphaTarget(0); 162 | } 163 | d.fx = null; 164 | d.fy = null; 165 | event.end && event.end(d); 166 | } 167 | }); 168 | this.drag = drag; 169 | } 170 | 171 | setTransform(transform, scale) { 172 | this.translate = transform; 173 | this.scale = scale; 174 | } 175 | 176 | initZoom(event, scaleExtent) { 177 | const isZoom = event.isZoom || noop; 178 | const zoom = d3Zoom.zoom() 179 | .on('start', (d) => { 180 | event.start && event.start(d); 181 | }) 182 | .on('zoom', () => { 183 | if (isZoom(d3_event) !== false) { 184 | const transform = d3_event.transform; 185 | const translate = [transform.x, transform.y], scale = transform.k; 186 | this.setTransform(translate, scale); 187 | event.zoom && event.zoom({translate, scale}); 188 | } 189 | }) 190 | .on('end', (d) => { 191 | event.end && event.end(d); 192 | }) 193 | if (scaleExtent) { 194 | zoom.scaleExtent(scaleExtent) 195 | } 196 | this.zoom = zoom 197 | } 198 | 199 | 200 | execute = () => { 201 | const { simulation, nodes } = this; 202 | simulation.stop(); 203 | for (var i = 0, n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())); i < n; ++i) { 204 | simulation.tick(); 205 | } 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import D3ReactForce from './D3ReactForce'; 3 | import data from './mock/data.json'; 4 | const { nodes, links } = data; 5 | 6 | let index = 1; 7 | export default class App extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | window.app = this; 11 | this.state = { 12 | nodes: nodes, 13 | links: links, 14 | width: document.documentElement.clientWidth, 15 | height: document.documentElement.clientHeight, 16 | svg: '', 17 | img: '' 18 | } 19 | this.center = true; 20 | this.velocityDecay = 0.2; 21 | this.linkDistance = 70; 22 | this.collideRadius = 10; 23 | this.collideStrength = 0.05; 24 | this.chargeStrength = -1500; 25 | this.count = 400; 26 | this.alphaDecay = 0.005; 27 | } 28 | 29 | addNode = () => { 30 | const { nodes } = this.state; 31 | nodes.push({ 32 | nodeId: `index${index++}` 33 | }) 34 | this.setState({ 35 | nodes: nodes.slice(0) 36 | }) 37 | } 38 | 39 | render() { 40 | const { nodes, links, width, height } = this.state; 41 | const { 42 | velocityDecay, 43 | linkDistance, 44 | collideRadius, 45 | collideStrength, 46 | chargeStrength, 47 | center, 48 | count, 49 | alphaDecay, 50 | alphaMin 51 | } = this; 52 | return (
53 |
62 | 65 | 68 | 81 | 94 | 95 | 100 | 101 | 110 | 111 | 116 |
117 | 摩擦系数: this.velocityDecay = e.target.value}/>
118 | 连线长度: this.linkDistance = e.target.value}/>
119 | 碰撞半径: this.collideRadius = e.target.value}/>
120 | 碰撞强度: this.collideStrength = e.target.value}/>
121 | alpha衰减系数: this.alphaDecay = e.target.value}/>
122 | alpha静止值: this.alphaMin = e.target.value}/>
123 | 作用力: this.chargeStrength = e.target.value}/>
124 | 居中: this.center = e.target.checked}/>
125 |
126 | 节点数: this.count = Number(e.target.value)}/>
127 |
134 | 节点数:{nodes.length}
135 | 边数量:{links.length}
136 |
137 | { this.state.img && } 138 |
139 |
140 | this.D3ReactForce = c} 142 | nodes={nodes} 143 | links={links} 144 | nodeIdKey="nodeId" 145 | width={width} 146 | height={height} 147 | velocityDecay={velocityDecay} 148 | linkDistance={linkDistance} 149 | collideRadius={collideRadius} 150 | collideStrength={collideStrength} 151 | chargeStrength={chargeStrength} 152 | alphaDecay={alphaDecay} 153 | alphaMin={alphaMin} 154 | XYCenter={center} 155 | nodeClick={(d) => { 156 | console.log(d); 157 | }} 158 | tick={(alpha) => { 159 | // // this.D3ReactForce.adaption(); 160 | }} 161 | end={() => { 162 | console.log('结束'); 163 | }} 164 | /> 165 |
166 | 167 |
); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | const MOUNT_NODE = document.getElementById('app'); 5 | 6 | 7 | let render = () => { 8 | ReactDOM.render( 9 | 10 | , MOUNT_NODE); 11 | } 12 | 13 | try { 14 | render() 15 | } catch (e) { 16 | console.error(e); 17 | } 18 | 19 | // if (module.hot) { 20 | // module.hot.accept(['./app'], () => { 21 | // setTimeout(() => { 22 | // ReactDOM.unmountComponentAtNode(MOUNT_NODE); 23 | // render(); 24 | // }); 25 | // }); 26 | // } 27 | -------------------------------------------------------------------------------- /src/mock/data.json: -------------------------------------------------------------------------------- 1 | {"nodes":[{"nodeId":"1396******8482","subGroupId":1},{"nodeId":"1826******8817","subGroupId":1},{"nodeId":"1505******4521","subGroupId":1},{"nodeId":"6212******8332","subGroupId":1},{"nodeId":"1396******6117","subGroupId":1},{"nodeId":"1526******5094","subGroupId":1},{"nodeId":"6212******7189","subGroupId":1},{"nodeId":"1396******3812","subGroupId":1},{"nodeId":"3307******3558","subGroupId":1},{"nodeId":"1585******8214","subGroupId":1},{"nodeId":"1585******3012","subGroupId":1},{"nodeId":"6212******1879","subGroupId":1},{"nodeId":"1585******5768","subGroupId":1},{"nodeId":"6236******5842","subGroupId":1},{"nodeId":"1596******2623","subGroupId":1},{"nodeId":"1596******6815","subGroupId":1},{"nodeId":"3307******822X","subGroupId":1},{"nodeId":"1396******4920","subGroupId":1},{"nodeId":"6228******5412","subGroupId":1},{"nodeId":"1585******7576","subGroupId":1},{"nodeId":"2714******673b","subGroupId":1},{"nodeId":"d75a******d70c","subGroupId":1},{"nodeId":"3307******8228","subGroupId":1},{"nodeId":"bf04******62ed","subGroupId":1},{"nodeId":"a6d9******3ec1","subGroupId":1},{"nodeId":"e46e******8271","subGroupId":1},{"nodeId":"1b19******4a5f","subGroupId":1},{"nodeId":"fa01******30b2","subGroupId":1},{"nodeId":"4680******2b13","subGroupId":1},{"nodeId":"d53f******9c74","subGroupId":1},{"nodeId":"4522******0634","subGroupId":1},{"nodeId":"a0ec******41ca","subGroupId":1},{"nodeId":"5002******1424","subGroupId":1},{"nodeId":"1876******3325","subGroupId":1},{"nodeId":"6217******9647","subGroupId":1},{"nodeId":"1585******5452","subGroupId":1},{"nodeId":"6228******1678","subGroupId":1},{"nodeId":"6228******9710","subGroupId":1},{"nodeId":"1585******5000","subGroupId":1},{"nodeId":"1396******3946","subGroupId":1},{"nodeId":"1396******3307","subGroupId":1},{"nodeId":"4367******3943","subGroupId":1},{"nodeId":"3307******7612","subGroupId":1},{"nodeId":"5379******c177","subGroupId":1},{"nodeId":"6deb******dc43","subGroupId":1},{"nodeId":"D3D1******2888","subGroupId":1},{"nodeId":"1B16******BF15","subGroupId":1},{"nodeId":"ba0a******8547","subGroupId":1},{"nodeId":"2d49******9392","subGroupId":1},{"nodeId":"bfed******97fb","subGroupId":1},{"nodeId":"307f******2d79","subGroupId":1},{"nodeId":"E4C7******66F5","subGroupId":1},{"nodeId":"4127******1532","subGroupId":1},{"nodeId":"9532******71e4","subGroupId":1},{"nodeId":"4116******1512","subGroupId":1},{"nodeId":"3606******4236","subGroupId":1},{"nodeId":"D50F******A9B3","subGroupId":1},{"nodeId":"5DB5******99C6","subGroupId":1},{"nodeId":"3AC9******0248","subGroupId":1},{"nodeId":"3E16******B58A","subGroupId":1},{"nodeId":"4C3E******6D64","subGroupId":1},{"nodeId":"4EE0******75E3","subGroupId":1},{"nodeId":"6641******3814","subGroupId":1},{"nodeId":"6D5B******A34D","subGroupId":1},{"nodeId":"6ce9******8158","subGroupId":1},{"nodeId":"AA86******67C3","subGroupId":1},{"nodeId":"1516******7942","subGroupId":1},{"nodeId":"1585******2839","subGroupId":1},{"nodeId":"1587******4457","subGroupId":1},{"nodeId":"e381******4880","subGroupId":1},{"nodeId":"50b4******ad3c","subGroupId":1},{"nodeId":"3326******4111","subGroupId":1},{"nodeId":"02FC******9ACD","subGroupId":1},{"nodeId":"87C5******C2D3","subGroupId":1},{"nodeId":"96cd******d84b","subGroupId":1},{"nodeId":"9AC2******FE99","subGroupId":1},{"nodeId":"EE5C******14C9","subGroupId":1},{"nodeId":"F5B2******A964","subGroupId":1},{"nodeId":"302E******A97B","subGroupId":1},{"nodeId":"1386******8746","subGroupId":1},{"nodeId":"1386******8748","subGroupId":1},{"nodeId":"52e3******d401","subGroupId":1},{"nodeId":"a154******b109","subGroupId":1},{"nodeId":"1827******7259","subGroupId":1},{"nodeId":"fe36******700f","subGroupId":1},{"nodeId":"18cb******cc79","subGroupId":1},{"nodeId":"2304******D973","subGroupId":1},{"nodeId":"2405******d322","subGroupId":1},{"nodeId":"2533******e37b","subGroupId":1},{"nodeId":"4760******a79a","subGroupId":1},{"nodeId":"47f6******b941","subGroupId":1},{"nodeId":"ba98******7286","subGroupId":1},{"nodeId":"d5e3******628a","subGroupId":1},{"nodeId":"94bc******5edb","subGroupId":1},{"nodeId":"1375******2228","subGroupId":1},{"nodeId":"1381******9320","subGroupId":1},{"nodeId":"1877******4586","subGroupId":1},{"nodeId":"1885******9217","subGroupId":1},{"nodeId":"0F6F******A3C4","subGroupId":1},{"nodeId":"2069******457e","subGroupId":1},{"nodeId":"228c******e74c","subGroupId":1},{"nodeId":"ccc6******b1fb","subGroupId":1},{"nodeId":"ce04******08b4","subGroupId":1},{"nodeId":"f816******5225","subGroupId":1}],"links":[{"target":"3606******4236","source":"1396******8482"},{"target":"3606******4236","source":"1826******8817"},{"target":"3606******4236","source":"1505******4521"},{"target":"4522******0634","source":"6212******8332"},{"target":"4522******0634","source":"1396******6117"},{"target":"4522******0634","source":"1526******5094"},{"target":"D3D1******2888","source":"6212******7189"},{"target":"D3D1******2888","source":"1396******3812"},{"target":"D3D1******2888","source":"3307******3558"},{"target":"D50F******A9B3","source":"1396******3812"},{"target":"D50F******A9B3","source":"3307******3558"},{"target":"E4C7******66F5","source":"3307******3558"},{"target":"E4C7******66F5","source":"1396******3812"},{"target":"E4C7******66F5","source":"6212******7189"},{"target":"5DB5******99C6","source":"3307******3558"},{"target":"5DB5******99C6","source":"1396******3812"},{"target":"3AC9******0248","source":"1396******3812"},{"target":"3AC9******0248","source":"3307******3558"},{"target":"3E16******B58A","source":"6212******7189"},{"target":"3E16******B58A","source":"3307******3558"},{"target":"3E16******B58A","source":"1396******3812"},{"target":"6212******1879","source":"1585******8214"},{"target":"4C3E******6D64","source":"1396******3812"},{"target":"4C3E******6D64","source":"3307******3558"},{"target":"4EE0******75E3","source":"6212******7189"},{"target":"4EE0******75E3","source":"1396******3812"},{"target":"4EE0******75E3","source":"3307******3558"},{"target":"4116******1512","source":"1585******3012"},{"target":"4116******1512","source":"6212******1879"},{"target":"4116******1512","source":"1585******5768"},{"target":"4116******1512","source":"1585******8214"},{"target":"4127******1532","source":"6236******5842"},{"target":"4127******1532","source":"1596******2623"},{"target":"4127******1532","source":"1585******8214"},{"target":"4127******1532","source":"1596******6815"},{"target":"6641******3814","source":"3307******822X"},{"target":"6641******3814","source":"1396******4920"},{"target":"a6d9******3ec1","source":"3307******822X"},{"target":"a6d9******3ec1","source":"1396******4920"},{"target":"6D5B******A34D","source":"6228******5412"},{"target":"6D5B******A34D","source":"1396******4920"},{"target":"6D5B******A34D","source":"3307******822X"},{"target":"6ce9******8158","source":"1396******4920"},{"target":"6ce9******8158","source":"3307******822X"},{"target":"AA86******67C3","source":"3307******3558"},{"target":"AA86******67C3","source":"1396******3812"},{"target":"1516******7942","source":"3307******822X"},{"target":"1516******7942","source":"1396******4920"},{"target":"2714******673b","source":"3307******822X"},{"target":"2714******673b","source":"1396******4920"},{"target":"1585******5000","source":"1585******7576"},{"target":"1585******5000","source":"2714******673b"},{"target":"1585******5000","source":"d75a******d70c"},{"target":"1585******5000","source":"3307******8228"},{"target":"1585******5000","source":"1396******4920"},{"target":"1585******5000","source":"bf04******62ed"},{"target":"1585******5000","source":"3307******822X"},{"target":"1585******2839","source":"1396******4920"},{"target":"1585******2839","source":"a6d9******3ec1"},{"target":"1585******2839","source":"e46e******8271"},{"target":"1585******2839","source":"1b19******4a5f"},{"target":"1585******2839","source":"fa01******30b2"},{"target":"1585******2839","source":"4680******2b13"},{"target":"1585******2839","source":"3307******822X"},{"target":"1587******4457","source":"6212******8332"},{"target":"1587******4457","source":"d53f******9c74"},{"target":"1587******4457","source":"1396******6117"},{"target":"1587******4457","source":"4522******0634"},{"target":"1587******4457","source":"a0ec******41ca"},{"target":"e381******4880","source":"5002******1424"},{"target":"e381******4880","source":"1876******3325"},{"target":"50b4******ad3c","source":"1396******3812"},{"target":"50b4******ad3c","source":"6212******7189"},{"target":"50b4******ad3c","source":"3307******3558"},{"target":"3307******7612","source":"6228******9710"},{"target":"3307******7612","source":"1585******5000"},{"target":"3326******4111","source":"1596******2623"},{"target":"5379******c177","source":"1396******3812"},{"target":"5379******c177","source":"3307******3558"},{"target":"02FC******9ACD","source":"3307******8228"},{"target":"02FC******9ACD","source":"6228******1678"},{"target":"02FC******9ACD","source":"1585******7576"},{"target":"6217******9647","source":"1876******3325"},{"target":"87C5******C2D3","source":"1876******3325"},{"target":"96cd******d84b","source":"1396******6117"},{"target":"96cd******d84b","source":"4522******0634"},{"target":"9AC2******FE99","source":"3307******7612"},{"target":"9AC2******FE99","source":"6228******9710"},{"target":"9AC2******FE99","source":"1585******5000"},{"target":"EE5C******14C9","source":"1396******3812"},{"target":"EE5C******14C9","source":"3307******3558"},{"target":"F5B2******A964","source":"3307******3558"},{"target":"F5B2******A964","source":"6212******7189"},{"target":"F5B2******A964","source":"1396******3812"},{"target":"6deb******dc43","source":"3307******3558"},{"target":"6deb******dc43","source":"1396******3812"},{"target":"2d49******9392","source":"3307******3558"},{"target":"2d49******9392","source":"1396******3812"},{"target":"302E******A97B","source":"3307******3558"},{"target":"302E******A97B","source":"1396******3812"},{"target":"307f******2d79","source":"3307******3558"},{"target":"307f******2d79","source":"1396******3812"},{"target":"4367******3943","source":"1396******3812"},{"target":"1386******8746","source":"3307******3558"},{"target":"1386******8746","source":"4367******3943"},{"target":"1386******8746","source":"5379******c177"},{"target":"1386******8746","source":"6deb******dc43"},{"target":"1386******8746","source":"6212******7189"},{"target":"1386******8746","source":"D3D1******2888"},{"target":"1386******8746","source":"1B16******BF15"},{"target":"1386******8746","source":"1396******3812"},{"target":"1386******8746","source":"ba0a******8547"},{"target":"1386******8746","source":"2d49******9392"},{"target":"1386******8746","source":"bfed******97fb"},{"target":"1386******8746","source":"307f******2d79"},{"target":"1386******8748","source":"3307******3558"},{"target":"1386******8748","source":"1396******3812"},{"target":"1386******8748","source":"E4C7******66F5"},{"target":"52e3******d401","source":"3307******822X"},{"target":"52e3******d401","source":"1396******4920"},{"target":"52e3******d401","source":"6228******5412"},{"target":"a154******b109","source":"1396******4920"},{"target":"a154******b109","source":"3307******822X"},{"target":"1827******7259","source":"1396******6117"},{"target":"1827******7259","source":"6212******8332"},{"target":"1827******7259","source":"4522******0634"},{"target":"fe36******700f","source":"4522******0634"},{"target":"fe36******700f","source":"6212******8332"},{"target":"fe36******700f","source":"1396******6117"},{"target":"18cb******cc79","source":"3307******8228"},{"target":"18cb******cc79","source":"1585******7576"},{"target":"2304******D973","source":"1396******3812"},{"target":"2405******d322","source":"3307******3558"},{"target":"2405******d322","source":"6212******7189"},{"target":"2405******d322","source":"1396******3812"},{"target":"2533******e37b","source":"1396******4920"},{"target":"2533******e37b","source":"3307******822X"},{"target":"4760******a79a","source":"1585******7576"},{"target":"4760******a79a","source":"3307******8228"},{"target":"47f6******b941","source":"3307******822X"},{"target":"47f6******b941","source":"1396******4920"},{"target":"ba98******7286","source":"1396******3812"},{"target":"ba98******7286","source":"3307******3558"},{"target":"d5e3******628a","source":"1396******4920"},{"target":"d5e3******628a","source":"3307******822X"},{"target":"94bc******5edb","source":"1396******4920"},{"target":"94bc******5edb","source":"3307******822X"},{"target":"9532******71e4","source":"1396******6117"},{"target":"9532******71e4","source":"1876******3325"},{"target":"9532******71e4","source":"6212******8332"},{"target":"9532******71e4","source":"4522******0634"},{"target":"9532******71e4","source":"6217******9647"},{"target":"9532******71e4","source":"5002******1424"},{"target":"1375******2228","source":"E4C7******66F5"},{"target":"1375******2228","source":"1396******3812"},{"target":"1375******2228","source":"3307******3558"},{"target":"1381******9320","source":"6236******5842"},{"target":"1381******9320","source":"4127******1532"},{"target":"1381******9320","source":"1596******6815"},{"target":"1877******4586","source":"6212******8332"},{"target":"1877******4586","source":"1396******6117"},{"target":"1877******4586","source":"4522******0634"},{"target":"1885******9217","source":"1396******3812"},{"target":"1885******9217","source":"3307******3558"},{"target":"0F6F******A3C4","source":"3307******3558"},{"target":"0F6F******A3C4","source":"1396******3812"},{"target":"2069******457e","source":"1585******8214"},{"target":"2069******457e","source":"6212******1879"},{"target":"2069******457e","source":"4116******1512"},{"target":"228c******e74c","source":"4522******0634"},{"target":"228c******e74c","source":"1396******6117"},{"target":"ccc6******b1fb","source":"3606******4236"},{"target":"ce04******08b4","source":"4127******1532"},{"target":"ce04******08b4","source":"1596******6815"},{"target":"f816******5225","source":"3307******8228"},{"target":"f816******5225","source":"1585******7576"}]} 2 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const isDebug = process.env.NODE_ENV === 'development'; 4 | 5 | const config = { 6 | entry: './src/index', 7 | mode: isDebug ? 'development' : 'production', 8 | devtool:'eval-source-map', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.jsx?$/, 13 | loader: 'babel-loader', 14 | query: { 15 | cacheDirectory: true, 16 | babelrc: false, 17 | presets: [ 18 | 'react', 19 | 'stage-2' 20 | ], 21 | plugins: [ 22 | 'transform-class-properties', 23 | ] 24 | }, 25 | include: path.join(__dirname, 'src') 26 | }, 27 | {test: /\.css$/, loaders: ['style-loader', 'css-loader']}, 28 | {test: /\.less/, loaders: ['style-loader', 'css-loader', 'less-loader']} 29 | ] 30 | }, 31 | plugins: [ 32 | // new webpack.HotModuleReplacementPlugin(), 33 | new HtmlWebpackPlugin({ 34 | template: './index.html', 35 | hash: false, 36 | title: 'd3-react-force', 37 | filename: 'index.html', 38 | inject: 'body' 39 | }) 40 | ] 41 | } 42 | 43 | 44 | if (isDebug) { 45 | config.module.rules[0].query.presets.unshift('react-hmre') 46 | } 47 | 48 | module.exports = config; 49 | --------------------------------------------------------------------------------