├── .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
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 |
--------------------------------------------------------------------------------