Skip to content

Commit e49e000

Browse files
authored
Refactor/separation of concerns render + logic (#49)
* Separate graph.helper into helper.js and renderer.jsx * Fix jsdoc format in graph.helper.js * Update tests to fit new architecture. Simplify d3 force mocks * Specify some tests for graph helper buildNodeProps method * Add trello board badge to README * Some improvements in graph.helper jsdoc * Add tests for buildNodeProps in graph helper * Enforce valid jsdoc via eslint
1 parent 932b2c6 commit e49e000

File tree

10 files changed

+643
-517
lines changed

10 files changed

+643
-517
lines changed

.eslintrc.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ module.exports = {
2929
"newline-after-var": ["error", "always"],
3030
"no-nested-ternary": "error",
3131
"no-useless-constructor": "error",
32-
"semi": "error"
32+
"semi": "error",
33+
"require-jsdoc": "error",
34+
"valid-jsdoc": "error"
3335
}
3436
};

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,5 @@ coverage
7070
sandbox/rd3g.sandbox.bundle.js.map
7171
gen-docs
7272
.DS_Store
73-
.vscode/
73+
**/.DS_Store
74+
.vscode

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# react-d3-graph · [![Build Status](https://travis-ci.org/danielcaldas/react-d3-graph.svg?branch=master)](https://travis-ci.org/danielcaldas/react-d3-graph) [![npm version](https://img.shields.io/badge/npm-v1.0.0-blue.svg)](https://www.npmjs.com/package/react-d3-graph) [![npm stats](https://img.shields.io/badge/downloads-1.5k-brightgreen.svg)](https://npm-stat.com/charts.html?package=react-d3-graph&from=2017-04-25&to=2017-12-26) [![probot enabled](https://img.shields.io/badge/probot:stale-enabled-yellow.svg)](https://probot.github.io/)
1+
# react-d3-graph · [![Build Status](https://travis-ci.org/danielcaldas/react-d3-graph.svg?branch=master)](https://travis-ci.org/danielcaldas/react-d3-graph) [![npm version](https://img.shields.io/badge/npm-v1.0.0-blue.svg)](https://www.npmjs.com/package/react-d3-graph) [![npm stats](https://img.shields.io/badge/downloads-1.5k-brightgreen.svg)](https://npm-stat.com/charts.html?package=react-d3-graph&from=2017-04-25&to=2017-12-26) [![probot enabled](https://img.shields.io/badge/probot:stale-enabled-yellow.svg)](https://probot.github.io/) [![trello](https://img.shields.io/badge/trello-board-blue.svg)](https://trello.com/b/KrnmFXha/react-d3-graph)
22
:book: [1.0.0](https://danielcaldas.github.io/react-d3-graph/docs/index.html) | [0.4.0](https://danielcaldas.github.io/react-d3-graph/docs/0.4.0.html) | [0.3.0](https://danielcaldas.github.io/react-d3-graph/docs/0.3.0.html)
33

44
### *Interactive and configurable graphs with react and d3 effortlessly*
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
/**
2+
* @module Graph/helper
3+
* @description
4+
* Offers a series of methods that isolate logic of Graph component and also from Graph rendering methods.
5+
*/
6+
/**
7+
* @typedef {Object} Link
8+
* @property {string} source - the node id of the source in the link.
9+
* @property {string} target - the node id of the target in the link.
10+
* @memberof Graph/helper
11+
*/
12+
/**
13+
* @typedef {Object} Node
14+
* @property {string} id - the id of the node.
15+
* @property {string} [color] - color of the node (optional).
16+
* @property {string} [size] - size of the node (optional).
17+
* @property {string} [symbolType] - symbol type of the node (optional).
18+
* @memberof Graph/helper
19+
*/
20+
import {
21+
forceX as d3ForceX,
22+
forceY as d3ForceY,
23+
forceSimulation as d3ForceSimulation,
24+
forceManyBody as d3ForceManyBody
25+
} from 'd3-force';
26+
27+
import CONST from './const';
28+
import DEFAULT_CONFIG from './config';
29+
import ERRORS from '../../err';
30+
31+
import utils from '../../utils';
32+
33+
/**
34+
* Create d3 forceSimulation to be applied on the graph.<br/>
35+
* {@link https://github.com/d3/d3-force#forceSimulation|d3-force#forceSimulation}<br/>
36+
* {@link https://github.com/d3/d3-force#simulation_force|d3-force#simulation_force}<br/>
37+
* Wtf is a force? {@link https://github.com/d3/d3-force#forces| here}
38+
* @param {number} width - the width of the container area of the graph.
39+
* @param {number} height - the height of the container area of the graph.
40+
* @returns {Object} returns the simulation instance to be consumed.
41+
* @memberof Graph/helper
42+
*/
43+
function _createForceSimulation(width, height) {
44+
const frx = d3ForceX(width / 2).strength(CONST.FORCE_X);
45+
const fry = d3ForceY(height / 2).strength(CONST.FORCE_Y);
46+
47+
return d3ForceSimulation()
48+
.force('charge', d3ForceManyBody().strength(CONST.FORCE_IDEAL_STRENGTH))
49+
.force('x', frx)
50+
.force('y', fry);
51+
}
52+
53+
/**
54+
* Get the correct node opacity in order to properly make decisions based on context such as currently highlighted node.
55+
* @param {Object} node - the node object for whom we will generate properties.
56+
* @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}.
57+
* @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}.
58+
* @param {Object} config - same as {@link #buildGraph|config in buildGraph}.
59+
* @returns {number} the opacity value for the given node.
60+
* @memberof Graph/helper
61+
*/
62+
function _getNodeOpacity(node, highlightedNode, highlightedLink, config) {
63+
const highlight = node.highlighted
64+
|| node.id === (highlightedLink && highlightedLink.source)
65+
|| node.id === (highlightedLink && highlightedLink.target);
66+
const someNodeHighlighted = !!(highlightedNode
67+
|| highlightedLink && highlightedLink.source && highlightedLink.target);
68+
let opacity;
69+
70+
if (someNodeHighlighted && config.highlightDegree === 0) {
71+
opacity = highlight ? config.node.opacity : config.highlightOpacity;
72+
} else if (someNodeHighlighted) {
73+
opacity = highlight ? config.node.opacity : config.highlightOpacity;
74+
} else {
75+
opacity = config.node.opacity;
76+
}
77+
78+
return opacity;
79+
}
80+
81+
/**
82+
* Receives a matrix of the graph with the links source and target as concrete node instances and it transforms it
83+
* in a lightweight matrix containing only links with source and target being strings representative of some node id
84+
* and the respective link value (if non existent will default to 1).
85+
* @param {Array.<Link>} graphLinks - an array of all graph links.
86+
* @returns {Object.<string, Object>} an object containing a matrix of connections of the graph, for each nodeId,
87+
* there is an object that maps adjacent nodes ids (string) and their values (number).
88+
* @memberof Graph/helper
89+
*/
90+
function _initializeLinks(graphLinks) {
91+
return graphLinks.reduce((links, l) => {
92+
const source = l.source.id || l.source;
93+
const target = l.target.id || l.target;
94+
95+
if (!links[source]) {
96+
links[source] = {};
97+
}
98+
99+
if (!links[target]) {
100+
links[target] = {};
101+
}
102+
103+
// @TODO: If the graph is directed this should be adapted
104+
links[source][target] = links[target][source] = l.value || 1;
105+
106+
return links;
107+
}, {});
108+
}
109+
110+
/**
111+
* Method that initialize graph nodes provided by rd3g consumer and adds additional default mandatory properties
112+
* that are optional for the user. Also it generates an index mapping, this maps nodes ids the their index in the array
113+
* of nodes. This is needed because d3 callbacks such as node click and link click return the index of the node.
114+
* @param {Array.<Node>} graphNodes - the array of nodes provided by the rd3g consumer.
115+
* @returns {Object.<string, Object>} returns the nodes ready to be used within rd3g with additional properties such as x, y
116+
* and highlighted values.
117+
* @memberof Graph/helper
118+
*/
119+
function _initializeNodes(graphNodes) {
120+
let nodes = {};
121+
const n = graphNodes.length;
122+
123+
for (let i=0; i < n; i++) {
124+
const node = graphNodes[i];
125+
126+
node.highlighted = false;
127+
128+
if (!node.hasOwnProperty('x')) { node['x'] = 0; }
129+
if (!node.hasOwnProperty('y')) { node['y'] = 0; }
130+
131+
nodes[node.id.toString()] = node;
132+
}
133+
134+
return nodes;
135+
}
136+
137+
/**
138+
* Some integrity validations on links and nodes structure. If some validation fails the function will
139+
* throw an error.
140+
* @param {Object} data - Same as {@link #initializeGraphState|data in initializeGraphState}.
141+
* @memberof Graph/helper
142+
* @throws can throw the following error msg:
143+
* INSUFFICIENT_DATA - msg if no nodes are provided
144+
* INVALID_LINKS - if links point to nonexistent nodes
145+
* @returns {undefined}
146+
*/
147+
function _validateGraphData(data) {
148+
if (!data.nodes || !data.nodes.length) {
149+
utils.throwErr('Graph', ERRORS.INSUFFICIENT_DATA);
150+
}
151+
152+
const n = data.links.length;
153+
154+
for (let i=0; i < n; i++) {
155+
const l = data.links[i];
156+
157+
if (!data.nodes.find(n => n.id === l.source)) {
158+
utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - "${l.source}" is not a valid source node id`);
159+
}
160+
if (!data.nodes.find(n => n.id === l.target)) {
161+
utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - "${l.target}" is not a valid target node id`);
162+
}
163+
}
164+
}
165+
166+
/**
167+
* Build some Link properties based on given parameters.
168+
* @param {string} source - the id of the source node (from).
169+
* @param {string} target - the id of the target node (to).
170+
* @param {Object.<string, Object>} nodes - same as {@link #buildGraph|nodes in buildGraph}.
171+
* @param {Object.<string, Object>} links - same as {@link #buildGraph|links in buildGraph}.
172+
* @param {Object} config - same as {@link #buildGraph|config in buildGraph}.
173+
* @param {Function[]} linkCallbacks - same as {@link #buildGraph|linkCallbacks in buildGraph}.
174+
* @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}.
175+
* @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}.
176+
* @param {number} transform - value that indicates the amount of zoom transformation.
177+
* @returns {Object} returns an object that aggregates all props for creating respective Link component instance.
178+
* @memberof Graph/helper
179+
*/
180+
function buildLinkProps(source, target, nodes, links, config, linkCallbacks, highlightedNode,
181+
highlightedLink, transform) {
182+
const x1 = nodes[source] && nodes[source].x || 0;
183+
const y1 = nodes[source] && nodes[source].y || 0;
184+
const x2 = nodes[target] && nodes[target].x || 0;
185+
const y2 = nodes[target] && nodes[target].y || 0;
186+
187+
let mainNodeParticipates = false;
188+
189+
switch (config.highlightDegree) {
190+
case 0:
191+
break;
192+
case 2:
193+
mainNodeParticipates = true;
194+
break;
195+
default: // 1st degree is the fallback behavior
196+
mainNodeParticipates = source === highlightedNode || target === highlightedNode;
197+
break;
198+
}
199+
200+
const reasonNode = mainNodeParticipates && nodes[source].highlighted && nodes[target].highlighted;
201+
const reasonLink = source === (highlightedLink && highlightedLink.source)
202+
&& target === (highlightedLink && highlightedLink.target);
203+
const highlight = reasonNode || reasonLink;
204+
205+
let opacity = config.link.opacity;
206+
207+
if (highlightedNode || (highlightedLink && highlightedLink.source)) {
208+
opacity = highlight ? config.link.opacity : config.highlightOpacity;
209+
}
210+
211+
let stroke = config.link.color;
212+
213+
if (highlight) {
214+
stroke = config.link.highlightColor === CONST.KEYWORDS.SAME ? config.link.color
215+
: config.link.highlightColor;
216+
}
217+
218+
let strokeWidth = config.link.strokeWidth * (1 / transform);
219+
220+
if (config.link.semanticStrokeWidth) {
221+
const linkValue = links[source][target] || links[target][source] || 1;
222+
223+
strokeWidth += (linkValue * strokeWidth) / 10;
224+
}
225+
226+
return {
227+
source,
228+
target,
229+
x1,
230+
y1,
231+
x2,
232+
y2,
233+
strokeWidth,
234+
stroke,
235+
className: CONST.LINK_CLASS_NAME,
236+
opacity,
237+
onClickLink: linkCallbacks.onClickLink,
238+
onMouseOverLink: linkCallbacks.onMouseOverLink,
239+
onMouseOutLink: linkCallbacks.onMouseOutLink
240+
};
241+
}
242+
243+
/**
244+
* Build some Node properties based on given parameters.
245+
* @param {Object} node - the node object for whom we will generate properties.
246+
* @param {Object} config - same as {@link #buildGraph|config in buildGraph}.
247+
* @param {Function[]} nodeCallbacks - same as {@link #buildGraph|nodeCallbacks in buildGraph}.
248+
* @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}.
249+
* @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}.
250+
* @param {number} transform - value that indicates the amount of zoom transformation.
251+
* @returns {Object} returns object that contain Link props ready to be feeded to the Link component.
252+
* @memberof Graph/helper
253+
*/
254+
function buildNodeProps(node, config, nodeCallbacks={}, highlightedNode, highlightedLink, transform) {
255+
const highlight = node.highlighted
256+
|| (node.id === (highlightedLink && highlightedLink.source)
257+
|| node.id === (highlightedLink && highlightedLink.target));
258+
const opacity = _getNodeOpacity(node, highlightedNode, highlightedLink, config);
259+
let fill = node.color || config.node.color;
260+
261+
if (highlight && config.node.highlightColor !== CONST.KEYWORDS.SAME) {
262+
fill = config.node.highlightColor;
263+
}
264+
265+
let stroke = config.node.strokeColor;
266+
267+
if (highlight && config.node.highlightStrokeColor !== CONST.KEYWORDS.SAME) {
268+
stroke = config.node.highlightStrokeColor;
269+
}
270+
271+
const t = 1 / transform;
272+
const nodeSize = node.size || config.node.size;
273+
const fontSize = highlight ? config.node.highlightFontSize : config.node.fontSize;
274+
const dx = (fontSize * t) + (nodeSize / 100) + 1.5;
275+
const strokeWidth = highlight ? config.node.highlightStrokeWidth : config.node.strokeWidth;
276+
277+
return {
278+
className: CONST.NODE_CLASS_NAME,
279+
cursor: config.node.mouseCursor,
280+
cx: node && node.x || '0',
281+
cy: node && node.y || '0',
282+
fill,
283+
fontSize: fontSize * t,
284+
dx,
285+
fontWeight: highlight ? config.node.highlightFontWeight : config.node.fontWeight,
286+
id: node.id,
287+
label: node[config.node.labelProperty] || node.id,
288+
onClickNode: nodeCallbacks.onClickNode,
289+
onMouseOverNode: nodeCallbacks.onMouseOverNode,
290+
onMouseOut: nodeCallbacks.onMouseOut,
291+
opacity,
292+
renderLabel: config.node.renderLabel,
293+
size: nodeSize * t,
294+
stroke,
295+
strokeWidth: strokeWidth * t,
296+
type: node.symbolType || config.node.symbolType
297+
};
298+
}
299+
300+
/**
301+
* Encapsulates common procedures to initialize graph.
302+
* @param {Object} props - Graph component props, object that holds data, id and config.
303+
* @param {Object} props.data - Data object holds links (array of **Link**) and nodes (array of **Node**).
304+
* @param {string} props.id - the graph id.
305+
* @param {Object} props.config - same as {@link #buildGraph|config in buildGraph}.
306+
* @param {Object} state - Graph component current state (same format as returned object on this function).
307+
* @returns {Object} a fully (re)initialized graph state object.
308+
* @memberof Graph/helper
309+
*/
310+
function initializeGraphState({data, id, config}, state) {
311+
let graph;
312+
313+
_validateGraphData(data);
314+
315+
if (state && state.nodes && state.links) {
316+
// absorb existent positioning
317+
graph = {
318+
nodes: data.nodes.map(n => Object.assign({}, n, state.nodes[n.id])),
319+
links: {}
320+
};
321+
} else {
322+
graph = {
323+
nodes: data.nodes.map(n => Object.assign({}, n)),
324+
links: {}
325+
};
326+
}
327+
328+
graph.links = data.links.map(l => Object.assign({}, l));
329+
330+
let newConfig = Object.assign({}, utils.merge(DEFAULT_CONFIG, config || {}));
331+
let nodes = _initializeNodes(graph.nodes);
332+
let links = _initializeLinks(graph.links); // matrix of graph connections
333+
const {nodes: d3Nodes, links: d3Links} = graph;
334+
const formatedId = id.replace(/ /g, '_');
335+
const simulation = _createForceSimulation(newConfig.width, newConfig.height);
336+
337+
return {
338+
id: formatedId,
339+
config: newConfig,
340+
links,
341+
d3Links,
342+
nodes,
343+
d3Nodes,
344+
highlightedNode: '',
345+
simulation,
346+
newGraphElements: false,
347+
configUpdated: false,
348+
transform: 1
349+
};
350+
}
351+
352+
export {
353+
buildLinkProps,
354+
buildNodeProps,
355+
initializeGraphState
356+
};

0 commit comments

Comments
 (0)