Skip to content

Commit 52df1b4

Browse files
authored
Fix/graph unnecessary calls to graph forces config (#114)
* Add deprecation note on componentWillReceiveProps * Add antiPick method to utils * Initialize links in initializeGraphState * Add check:light task in package.json * Implement checkForGraphElementsChanges in graph.helper * e2e test collapse improvements * Small check on number of links graph e2e * Proper link initialization by mapping input to d3 link
1 parent b66e6e4 commit 52df1b4

File tree

8 files changed

+210
-43
lines changed

8 files changed

+210
-43
lines changed

cypress/integration/graph.e2e.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ describe('[rd3g-graph] graph tests', function() {
6565
it('nodes props modifications should be reflected in the graph', function() {
6666
cy.get('text').should('have.length', 14);
6767
cy.get('path[class="link"]').should('be.visible');
68+
cy.get('path[class="link"]').should('have.length', 23);
6869

6970
this.sandboxPO.addNode();
7071
this.sandboxPO.addNode();
@@ -73,6 +74,9 @@ describe('[rd3g-graph] graph tests', function() {
7374

7475
cy.get('text').should('have.length', 18);
7576

77+
// should now have more than 23 links
78+
cy.get('path[class="link"]').should('not.have.length', 23);
79+
7680
// click (+) add prop to 1st node
7781
this.sandboxPO.addJsonTreeFirstNodeProp();
7882
// prop name be color
@@ -125,6 +129,8 @@ describe('[rd3g-graph] graph tests', function() {
125129

126130
this.node1PO = new NodePO(1);
127131
this.node2PO = new NodePO(2);
132+
this.node3PO = new NodePO(3);
133+
this.node4PO = new NodePO(4);
128134
this.link12PO = new LinkPO(0);
129135
});
130136

@@ -141,6 +147,19 @@ describe('[rd3g-graph] graph tests', function() {
141147
// Check the leaf node & link is no longer visible
142148
this.node2PO.getPath().should('not.be.visible');
143149
line.should('not.be.visible');
150+
151+
// Check if other nodes and links are still visible
152+
this.node1PO.getPath().should('be.visible');
153+
this.node3PO.getPath().should('be.visible');
154+
this.node4PO.getPath().should('be.visible');
155+
156+
const link13PO = new LinkPO(1);
157+
const link14PO = new LinkPO(2);
158+
const link34PO = new LinkPO(3);
159+
160+
link13PO.getLine().should('be.visible');
161+
link14PO.getLine().should('be.visible');
162+
link34PO.getLine().should('be.visible');
144163
});
145164
});
146165
});

cypress/integration/link.e2e.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const NodePO = require('../page-objects/node.po');
66
const SandboxPO = require('../page-objects/sandbox.po');
77
let nodes = require('./../../sandbox/data/small/small.data').nodes.map(({ id }) => id);
88

9-
describe('[rd3g-graph] link tests', function() {
9+
describe('[rd3g-link] link tests', function() {
1010
before(function() {
1111
this.sandboxPO = new SandboxPO();
1212
// visit sandbox

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"license": "MIT",
77
"scripts": {
88
"check": "npm run docs:lint && npm run lint && npm run test && npm run functional",
9+
"check:light": "npm run lint && npm run test",
910
"dev": "NODE_ENV=dev webpack-dev-server --mode=development --content-base sandbox --config webpack.config.js --inline --hot --port 3002",
1011
"dist:rd3g": "rm -rf dist/ && webpack --config webpack.config.dist.js -p --display-modules --optimize-minimize",
1112
"dist:sandbox": "webpack --config webpack.config.js -p",

src/components/graph/Graph.jsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -295,16 +295,22 @@ export default class Graph extends React.Component {
295295
this.state = graphHelper.initializeGraphState(this.props, this.state);
296296
}
297297

298+
/**
299+
* @deprecated
300+
* `componentWillReceiveProps` has a replacement method in react v16.3 onwards.
301+
* that is getDerivedStateFromProps.
302+
* But one needs to be aware that if an anti pattern of `componentWillReceiveProps` is
303+
* in place for this implementation the migration might not be that easy.
304+
* See {@link https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html}.
305+
* @param {Object} nextProps - props.
306+
* @returns {undefined}
307+
*/
298308
componentWillReceiveProps(nextProps) {
299-
const newGraphElements =
300-
nextProps.data.nodes.length !== this.state.nodesInputSnapshot.length ||
301-
nextProps.data.links.length !== this.state.linksInputSnapshot.length ||
302-
!utils.isDeepEqual(nextProps.data, {
303-
nodes: this.state.nodesInputSnapshot,
304-
links: this.state.linksInputSnapshot
305-
});
306-
const state = newGraphElements ? graphHelper.initializeGraphState(nextProps, this.state) : this.state;
307-
309+
const { graphElementsUpdated, newGraphElements } = graphHelper.checkForGraphElementsChanges(
310+
nextProps,
311+
this.state
312+
);
313+
const state = graphElementsUpdated ? graphHelper.initializeGraphState(nextProps, this.state) : this.state;
308314
const newConfig = nextProps.config || {};
309315
const configUpdated =
310316
newConfig && !utils.isObjectEmpty(newConfig) && !utils.isDeepEqual(newConfig, this.state.config);
@@ -318,8 +324,8 @@ export default class Graph extends React.Component {
318324
this.setState({
319325
...state,
320326
config,
321-
newGraphElements,
322327
configUpdated,
328+
newGraphElements,
323329
transform
324330
});
325331
}

src/components/graph/graph.helper.js

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/*eslint-disable max-lines*/
12
/**
23
* @module Graph/helper
34
* @description
@@ -148,6 +149,39 @@ function _initializeNodes(graphNodes) {
148149
return nodes;
149150
}
150151

152+
/**
153+
* Maps an input link (with format `{ source: 'sourceId', target: 'targetId' }`) to a d3Link
154+
* (with format `{ source: { id: 'sourceId' }, target: { id: 'targetId' } }`). If d3Link with
155+
* given index exists already that same d3Link is returned.
156+
* @param {Object} link - input link.
157+
* @param {number} index - index of the input link.
158+
* @param {Array.<Object>} d3Links - all d3Links.
159+
* @returns {Object} a d3Link.
160+
*/
161+
function _mapDataLinkToD3Link(link, index, d3Links = []) {
162+
const d3Link = d3Links[index];
163+
164+
if (d3Link) {
165+
return d3Link;
166+
}
167+
168+
const highlighted = false;
169+
const source = {
170+
id: link.source,
171+
highlighted
172+
};
173+
const target = {
174+
id: link.target,
175+
highlighted
176+
};
177+
178+
return {
179+
index,
180+
source,
181+
target
182+
};
183+
}
184+
151185
/**
152186
* Some integrity validations on links and nodes structure. If some validation fails the function will
153187
* throw an error.
@@ -322,6 +356,42 @@ function buildNodeProps(node, config, nodeCallbacks = {}, highlightedNode, highl
322356
};
323357
}
324358

359+
// list of properties that are of no interest when it comes to nodes and links comparison
360+
const NODE_PROPERTIES_DISCARD_TO_COMPARE = ['x', 'y', 'vx', 'vy', 'index'];
361+
362+
/**
363+
* This function checks for graph elements (nodes and links) changes, in two different
364+
* levels of significance, updated elements (whether some property has changed in some
365+
* node or link) and new elements (whether some new elements or added/removed from the graph).
366+
* @param {Object} nextProps - nextProps that graph will receive.
367+
* @param {Object} currentState - the current state of the graph.
368+
* @returns {Object.<string, boolean>} returns object containing update check flags:
369+
* - newGraphElements - flag that indicates whether new graph elements were added.
370+
* - graphElementsUpdated - flag that indicates whether some graph elements have
371+
* updated (some property that is not in NODE_PROPERTIES_DISCARD_TO_COMPARE was added to
372+
* some node or link or was updated).
373+
*/
374+
function checkForGraphElementsChanges(nextProps, currentState) {
375+
const nextNodes = nextProps.data.nodes.map(n => utils.antiPick(n, NODE_PROPERTIES_DISCARD_TO_COMPARE));
376+
const nextLinks = nextProps.data.links;
377+
const stateD3Nodes = currentState.d3Nodes.map(n => utils.antiPick(n, NODE_PROPERTIES_DISCARD_TO_COMPARE));
378+
const stateD3Links = currentState.d3Links.map(l => ({
379+
// FIXME: solve this source data inconsistency later
380+
source: l.source.id || l.source,
381+
target: l.target.id || l.target
382+
}));
383+
const graphElementsUpdated = !(
384+
utils.isDeepEqual(nextNodes, stateD3Nodes) && utils.isDeepEqual(nextLinks, stateD3Links)
385+
);
386+
const newGraphElements =
387+
nextNodes.length !== stateD3Nodes.length ||
388+
nextLinks.length !== stateD3Links.length ||
389+
!utils.isDeepEqual(nextNodes.map(({ id }) => ({ id })), stateD3Nodes.map(({ id }) => ({ id }))) ||
390+
!utils.isDeepEqual(nextLinks, stateD3Links.map(({ source, target }) => ({ source, target })));
391+
392+
return { graphElementsUpdated, newGraphElements };
393+
}
394+
325395
/**
326396
* Encapsulates common procedures to initialize graph.
327397
* @param {Object} props - Graph component props, object that holds data, id and config.
@@ -333,33 +403,29 @@ function buildNodeProps(node, config, nodeCallbacks = {}, highlightedNode, highl
333403
* @memberof Graph/helper
334404
*/
335405
function initializeGraphState({ data, id, config }, state) {
336-
let graph;
337-
338406
_validateGraphData(data);
339407

408+
let graph;
340409
const nodesInputSnapshot = data.nodes.map(n => Object.assign({}, n));
341410
const linksInputSnapshot = data.links.map(l => Object.assign({}, l));
342411

343-
if (state && state.nodes && state.links) {
344-
// absorb existent positioning
412+
if (state && state.nodes) {
345413
graph = {
346414
nodes: data.nodes.map(
347415
n =>
348416
state.nodes[n.id]
349417
? Object.assign({}, n, utils.pick(state.nodes[n.id], NODE_PROPS_WHITELIST))
350418
: Object.assign({}, n)
351419
),
352-
links: {}
420+
links: data.links.map((l, index) => _mapDataLinkToD3Link(l, index, state && state.d3Links))
353421
};
354422
} else {
355423
graph = {
356424
nodes: data.nodes.map(n => Object.assign({}, n)),
357-
links: {}
425+
links: data.links.map(l => Object.assign({}, l))
358426
};
359427
}
360428

361-
graph.links = data.links.map(l => Object.assign({}, l));
362-
363429
let newConfig = Object.assign({}, utils.merge(DEFAULT_CONFIG, config || {}));
364430
let nodes = _initializeNodes(graph.nodes);
365431
let links = _initializeLinks(graph.links); // matrix of graph connections
@@ -513,8 +579,10 @@ function getNodeCardinality(nodeId, linksMatrix) {
513579
}
514580

515581
export {
582+
NODE_PROPS_WHITELIST,
516583
buildLinkProps,
517584
buildNodeProps,
585+
checkForGraphElementsChanges,
518586
disconnectLeafNodeConnections,
519587
getLeafNodeConnections,
520588
getNodeCardinality,

src/utils.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ function pick(o, props = []) {
127127
}, {});
128128
}
129129

130+
/**
131+
* Picks all props except the ones passed in the props array.
132+
* @param {Object} o - the object to pick props from.
133+
* @param {Array.<string>} props - list of props that we DON'T want to pick from o.
134+
* @returns {Object} the object resultant from the anti picking operation.
135+
*/
136+
function antiPick(o, props = []) {
137+
const wanted = Object.keys(o).filter(k => !props.includes(k));
138+
139+
return pick(o, wanted);
140+
}
141+
130142
/**
131143
* Helper function for customized error logging.
132144
* @param {string} component - the name of the component where the error is to be thrown.
@@ -145,5 +157,6 @@ export default {
145157
isObjectEmpty,
146158
merge,
147159
pick,
160+
antiPick,
148161
throwErr
149162
};

test/component/graph/graph.helper.test.js

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ describe('Graph Helper', () => {
235235
});
236236

237237
describe('and received state was already initialized', () => {
238-
test('should create graph structure absorbing stored nodes behavior in state obj', () => {
238+
test('should create graph structure absorbing stored nodes and links behavior', () => {
239239
const data = {
240240
nodes: [{ id: 'A' }, { id: 'B' }, { id: 'C' }],
241241
links: [{ source: 'A', target: 'B' }, { source: 'C', target: 'A' }]
@@ -245,7 +245,7 @@ describe('Graph Helper', () => {
245245
A: { x: 20, y: 40 },
246246
B: { x: 40, y: 60 }
247247
},
248-
links: 'links',
248+
links: [],
249249
nodeIndexMapping: 'nodeIndexMapping'
250250
};
251251

@@ -273,12 +273,26 @@ describe('Graph Helper', () => {
273273
]);
274274
expect(newState.d3Links).toEqual([
275275
{
276-
source: 'A',
277-
target: 'B'
276+
index: 0,
277+
source: {
278+
highlighted: false,
279+
id: 'A'
280+
},
281+
target: {
282+
highlighted: false,
283+
id: 'B'
284+
}
278285
},
279286
{
280-
source: 'C',
281-
target: 'A'
287+
index: 1,
288+
source: {
289+
highlighted: false,
290+
id: 'C'
291+
},
292+
target: {
293+
highlighted: false,
294+
id: 'A'
295+
}
282296
}
283297
]);
284298
});
@@ -364,12 +378,26 @@ describe('Graph Helper', () => {
364378
configUpdated: false,
365379
d3Links: [
366380
{
367-
source: 'A',
368-
target: 'B'
381+
index: 0,
382+
source: {
383+
highlighted: false,
384+
id: 'A'
385+
},
386+
target: {
387+
highlighted: false,
388+
id: 'B'
389+
}
369390
},
370391
{
371-
source: 'C',
372-
target: 'A'
392+
index: 1,
393+
source: {
394+
highlighted: false,
395+
id: 'C'
396+
},
397+
target: {
398+
highlighted: false,
399+
id: 'A'
400+
}
373401
}
374402
],
375403
d3Nodes: [
@@ -406,6 +434,16 @@ describe('Graph Helper', () => {
406434
A: 1
407435
}
408436
},
437+
linksInputSnapshot: [
438+
{
439+
source: 'A',
440+
target: 'B'
441+
},
442+
{
443+
source: 'C',
444+
target: 'A'
445+
}
446+
],
409447
newGraphElements: false,
410448
nodes: {
411449
A: {
@@ -427,10 +465,6 @@ describe('Graph Helper', () => {
427465
y: 0
428466
}
429467
},
430-
simulation: {
431-
force: forceStub
432-
},
433-
transform: 1,
434468
nodesInputSnapshot: [
435469
{
436470
id: 'A'
@@ -442,16 +476,10 @@ describe('Graph Helper', () => {
442476
id: 'C'
443477
}
444478
],
445-
linksInputSnapshot: [
446-
{
447-
source: 'A',
448-
target: 'B'
449-
},
450-
{
451-
source: 'C',
452-
target: 'A'
453-
}
454-
]
479+
simulation: {
480+
force: forceStub
481+
},
482+
transform: 1
455483
});
456484
});
457485
});

0 commit comments

Comments
 (0)