├── src
├── D3ReactForce
│ ├── layout
│ │ ├── index.js
│ │ ├── base.js
│ │ ├── is.js
│ │ ├── grid.js
│ │ ├── circle.js
│ │ ├── archimeddeanSpiral.js
│ │ └── dagre.js
│ ├── default.js
│ ├── node.js
│ ├── link.js
│ ├── simulation.js
│ └── index.js
├── index.js
├── app.js
└── mock
│ └── data.json
├── index.html
├── dist
└── index.html
├── .gitignore
├── webpack.config.js
├── package.json
└── README.md
/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 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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/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/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