Skip to content

Commit bab2c93

Browse files
authored
Fix/not call graph configs inside component did update (#10)
* Add fix me note. Handle this as a 🐛 * Update README * Amazing fix ⭐. Pause simulation to update graph * Improve condition for zoom config updates * Rename utils method isEqual to isDeepEqual * Separate node update logic from config update logic * Handle static graph data updates * Absorve node/links positioning when receiving new props * Error detection for invalid links. Count nodes and links in sandbox * Update sandbox
1 parent 8d6a837 commit bab2c93

File tree

6 files changed

+93
-56
lines changed

6 files changed

+93
-56
lines changed

README.md

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,6 @@ const onClickLink = function(source, target) {
8383
onMouseOutNode={onMouseOutNode} />
8484
```
8585

86-
## TODOs
87-
This consists in a list of ideas for further developments:
88-
- Expose a graph property **background-color** that is applied to the svg graph container;
89-
- Expose d3-force values as configurable such as **alphaTarget** simulation value;
90-
- Improve opacity/highlightBehavior strategy maybe use a global *background: rgba(...)* value and then set a higher
91-
value on selected nodes;
92-
- At the moment highlightBehavior is highlighting the mouse hovered node, its 1st degree connections and their 1st
93-
degree connections. Make **highlightBehaviorDegree** which consists in a *step value* on the depth that we wish to highlight;
94-
- Semantic node size. Have a property value in each node that then is used along side config.nodeSize property
95-
to calculate effective node size in run time;
96-
- Improve semanticStrokeWidth calculation;
97-
- On Graph instantiation do a check on all config properties. If there is a "bad property" (name or value) throw
98-
a custom error (property error checking);
99-
- Path highlight - highlight a certain set of links and nodes (use case: highlight shortest path between two given nodes);
100-
- Link mouseover with highlight behavior highlights the intervenient nodes.
101-
10286
## Contributions
10387
Contributions are welcome fell free to submit new features or simply grab something from
10488
the above TODO list.

sandbox/Sandbox.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,14 @@ export default class Sandbox extends React.Component {
8686
});
8787
} else {
8888
// 1st node
89-
this.setState({
89+
const data = {
9090
nodes: [
9191
{id: 'Node 1'}
92-
]
93-
});
92+
],
93+
links: []
94+
};
95+
96+
this.setState({ data });
9497
}
9598
}
9699

@@ -100,13 +103,11 @@ export default class Sandbox extends React.Component {
100103
onClickRemoveNode = () => {
101104
if (this.state.data.nodes && this.state.data.nodes.length) {
102105
const id = this.state.data.nodes[0].id;
103-
const nodes = this.state.data.nodes.splice(0, 1);
106+
this.state.data.nodes.splice(0, 1);
104107
const links = this.state.data.links.filter(l => l.source !== id && l.target !== id);
108+
const data = { nodes: this.state.data.nodes, links };
105109

106-
this.setState({
107-
nodes,
108-
links
109-
});
110+
this.setState({ data });
110111
} else {
111112
alert('No more nodes to remove!');
112113
}
@@ -199,6 +200,7 @@ export default class Sandbox extends React.Component {
199200
<button onClick={this.resetNodesPositions} className='btn btn-default' style={btnStyle} disabled={this.state.config.staticGraph}>Unstick nodes</button>
200201
<button onClick={this.onClickAddNode} className='btn btn-default' style={btnStyle}>+</button>
201202
<button onClick={this.onClickRemoveNode} className='btn btn-default' style={btnStyle}>-</button>
203+
<br/><b>Nodes: </b> {this.state.data.nodes.length}, <b>Links: </b> {this.state.data.links.length}
202204
<Graph ref='graph' {...graphProps}/>
203205
</div>
204206
<div className='container__form'>
@@ -228,7 +230,7 @@ export default class Sandbox extends React.Component {
228230

229231
class JSONContainer extends React.Component {
230232
shouldComponentUpdate(nextProps, nextState) {
231-
return !this.props.staticData && !ReactD3GraphUtils.isEqual(nextProps.data, this.props.data);
233+
return !this.props.staticData && !ReactD3GraphUtils.isDeepEqual(nextProps.data, this.props.data);
232234
}
233235

234236
render() {

src/components/Graph/index.js

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,21 @@ export default class Graph extends React.Component {
201201
*/
202202
restartSimulation = () => !this.state.config.staticGraph && this.state.simulation.restart();
203203

204+
/**
205+
* Some integraty validations on links and nodes structure.
206+
* @param {Object} data
207+
*/
208+
_validateGraphData(data) {
209+
data.links.forEach(l => {
210+
if (!data.nodes.find(n => n.id === l.source)) {
211+
Utils.throwErr(this.constructor.name, `${ERRORS.INVALID_LINKS} - ${l.source} is not a valid node id`);
212+
}
213+
if (!data.nodes.find(n => n.id === l.target)) {
214+
Utils.throwErr(this.constructor.name, `${ERRORS.INVALID_LINKS} - ${l.target} is not a valid node id`);
215+
}
216+
});
217+
}
218+
204219
/**
205220
* Incapsulates common procedures to initialize graph.
206221
* @param {Object} data
@@ -209,14 +224,29 @@ export default class Graph extends React.Component {
209224
* @returns {Object}
210225
*/
211226
_initializeGraphState(data) {
212-
let graph = data || {};
227+
this._validateGraphData(data);
228+
229+
let graph;
230+
231+
if (this.state && this.state.nodes && this.state.links && this.state.nodeIndexMapping) {
232+
// absorve existent positining
233+
graph = {
234+
nodes: data.nodes.map(n => Object.assign({}, n, this.state.nodes[n.id])),
235+
links: {}
236+
};
237+
} else {
238+
graph = {
239+
nodes: data.nodes.map(n => Object.assign({}, n)),
240+
links: {}
241+
};
242+
}
213243

214-
let config = Utils.merge(DEFAULT_CONFIG, this.props.config || {});
244+
graph.links = data.links.map(l => Object.assign({}, l));
215245

246+
let config = Object.assign({}, Utils.merge(DEFAULT_CONFIG, this.props.config || {}));
216247
let {nodes, nodeIndexMapping} = GraphHelper.initializeNodes(graph.nodes);
217248
let links = GraphHelper.initializeLinks(graph.links); // Matrix of graph connections
218249
const {nodes: d3Nodes, links: d3Links} = graph;
219-
220250
const id = this.props.id.replace(/ /g, '_');
221251
const simulation = GraphHelper.createForceSimulation(config.width, config.height);
222252

@@ -230,7 +260,8 @@ export default class Graph extends React.Component {
230260
d3Nodes,
231261
nodeHighlighted: false,
232262
simulation,
233-
graphDataChanged: false
263+
newGraphElements: false,
264+
configUpdated: false
234265
};
235266
}
236267

@@ -259,33 +290,48 @@ export default class Graph extends React.Component {
259290
super(props);
260291

261292
if (!this.props.id) {
262-
throw Utils.throwErr(this.constructor.name, ERRORS.GRAPH_NO_ID_PROP);
293+
Utils.throwErr(this.constructor.name, ERRORS.GRAPH_NO_ID_PROP);
263294
}
264295

265296
this.state = this._initializeGraphState(this.props.data);
266297
}
267298

268299
componentWillReceiveProps(nextProps) {
269-
const state = this._initializeGraphState(nextProps.data);
300+
const newGraphElements = nextProps.data.nodes.length !== this.state.d3Nodes.length
301+
|| nextProps.data.links.length !== this.state.d3Links.length;
302+
303+
if (newGraphElements && nextProps.config.staticGraph) {
304+
Utils.throwErr(this.constructor.name, ERRORS.STATIC_GRAPH_DATA_UPDATE);
305+
}
306+
307+
const configUpdated = Utils.isDeepEqual(nextProps.config, this.state.config);
308+
const state = newGraphElements ? this._initializeGraphState(nextProps.data) : this.state;
270309
const config = Utils.merge(DEFAULT_CONFIG, nextProps.config || {});
271310

311+
// In order to properly update graph data we need to pause eventual d3 ongoing animations
312+
newGraphElements && this.pauseSimulation();
313+
272314
this.setState({
273315
...state,
274316
config,
275-
graphDataChanged: true
317+
newGraphElements,
318+
configUpdated
276319
});
277320
}
278321

279322
componentDidUpdate() {
280-
// If some zoom config changed we want to apply possible new values for maxZoom and minZoom
281-
this._zoomConfig();
282-
283323
// If the property staticGraph was activated we want to stop possible ongoing simulation
284324
this.state.config.staticGraph && this.state.simulation.stop();
285325

286-
if (!this.state.config.staticGraph && this.state.graphDataChanged) {
326+
if (!this.state.config.staticGraph && this.state.newGraphElements) {
287327
this._graphForcesConfig();
288-
this.state.graphDataChanged = false;
328+
this.restartSimulation();
329+
this.state.newGraphElements = false;
330+
}
331+
332+
if (this.state.configUpdated) {
333+
this._zoomConfig();
334+
this.state.configUpdated = false;
289335
}
290336
}
291337

src/err.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export default {
2-
GRAPH_NO_ID_PROP: 'id prop not defined! id property is mandatory and it should be unique.'
2+
GRAPH_NO_ID_PROP: "id prop not defined! id property is mandatory and it should be unique.",
3+
STATIC_GRAPH_DATA_UPDATE: "a static graph cannot receive new data (nodes or links).\
4+
Make sure config.staticGraph is set to true if you want to update graph data",
5+
INVALID_LINKS: "you provided a invalid links data structure.\
6+
Links source and target attributes must point to an existent node"
37
};

src/utils.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* that are common across rd3g such as error logging.
66
*/
77

8-
// This variable assures that recursive methods such as merge and isEqual do not fall on
8+
// This variable assures that recursive methods such as merge and isDeepEqual do not fall on
99
// circular JSON structure evaluation.
1010
const MAX_DEPTH = 5;
1111

@@ -28,7 +28,7 @@ function _isPropertyNestedObject(o, k) {
2828
* @memberof utils
2929
* @return {boolean} returns true if o1 and o2 have exactly the same content, or are exactly the same object reference.
3030
*/
31-
function isEqual(o1, o2, _depth=0) {
31+
function isDeepEqual(o1, o2, _depth=0) {
3232
let diffs = [];
3333

3434
if (_depth === 0 && o1 === o2) {
@@ -43,7 +43,7 @@ function isEqual(o1, o2, _depth=0) {
4343
const nestedO = _isPropertyNestedObject(o1, k) && _isPropertyNestedObject(o2, k);
4444

4545
if (nestedO && _depth < MAX_DEPTH) {
46-
diffs.push(isEqual(o1[k], o2[k], _depth + 1));
46+
diffs.push(isDeepEqual(o1[k], o2[k], _depth + 1));
4747
} else {
4848
const r = isObjectEmpty(o1[k]) && isObjectEmpty(o2[k]) || o2.hasOwnProperty(k) && o2[k] === o1[k];
4949

@@ -70,6 +70,7 @@ function isObjectEmpty(o) {
7070
}
7171

7272
/**
73+
* @TODO: Support for arrays?
7374
* This function merges two objects o1 and o2, where o2 properties override existent o1 properties, and
7475
* if o2 doesn't posses some o1 property the function will fallback to the o1 property.
7576
* @param {Object} o1 - object.
@@ -109,7 +110,7 @@ function throwErr(component, msg) {
109110
}
110111

111112
export default {
112-
isEqual,
113+
isDeepEqual,
113114
isObjectEmpty,
114115
merge,
115116
throwErr

test/utils.test.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('Utils', () => {
4646
expect(Utils.isObjectEmpty({})).toEqual(true);
4747
});
4848

49-
describe('isEqual', () => {
49+
describe('isDeepEqual', () => {
5050
let that = {};
5151

5252
beforeEach(() => {
@@ -90,63 +90,63 @@ describe('Utils', () => {
9090
});
9191

9292
test('should return true if no modifications are performed', () => {
93-
expect(Utils.isEqual(that.o1, that.o2)).toEqual(true);
93+
expect(Utils.isDeepEqual(that.o1, that.o2)).toEqual(true);
9494
});
9595

9696
test('should return false when o2 is an empty object but o1 is not', () => {
97-
expect(Utils.isEqual(that.o1, {})).toEqual(false);
97+
expect(Utils.isDeepEqual(that.o1, {})).toEqual(false);
9898
});
9999

100100
test('should return false when o1 is an empty object but o2 is not', () => {
101-
expect(Utils.isEqual({}, that.o2)).toEqual(false);
101+
expect(Utils.isDeepEqual({}, that.o2)).toEqual(false);
102102
});
103103

104104
test('should return true when both objects are empty', () => {
105-
expect(Utils.isEqual({}, {})).toEqual(true);
105+
expect(Utils.isDeepEqual({}, {})).toEqual(true);
106106
});
107107

108108
test('should return false when: o2.b.j.k = undefined', () => {
109109
that.o2.b.j.k = undefined;
110-
expect(Utils.isEqual(that.o1, that.o2)).toEqual(false);
110+
expect(Utils.isDeepEqual(that.o1, that.o2)).toEqual(false);
111111
});
112112

113113
test('should return false when: o2.b.j.m.n.o = "1"', () => {
114114
that.o2.b.j.m.n.o = '1';
115-
expect(Utils.isEqual(that.o1, that.o2)).toEqual(false);
115+
expect(Utils.isDeepEqual(that.o1, that.o2)).toEqual(false);
116116
});
117117

118118
test('should return false when: o2.b.c.e = "tests"', () => {
119119
that.o2.b.c.e = 'tests';
120-
expect(Utils.isEqual(that.o1, that.o2)).toEqual(false);
120+
expect(Utils.isDeepEqual(that.o1, that.o2)).toEqual(false);
121121
});
122122

123123
test('should return false when: o1.b.i = false', () => {
124124
that.o1.b.i = false;
125-
expect(Utils.isEqual(that.o1, that.o2)).toEqual(false);
125+
expect(Utils.isDeepEqual(that.o1, that.o2)).toEqual(false);
126126
});
127127

128128
test('should return false when: o1.a = false', () => {
129129
that.o1.a = false;
130-
expect(Utils.isEqual(that.o1, that.o2)).toEqual(false);
130+
expect(Utils.isDeepEqual(that.o1, that.o2)).toEqual(false);
131131
});
132132

133133
test('should return false when: o2.b.g = "tests"', () => {
134134
that.o2.b.g = 'tests';
135-
expect(Utils.isEqual(that.o1, that.o2)).toEqual(false);
135+
expect(Utils.isDeepEqual(that.o1, that.o2)).toEqual(false);
136136
});
137137

138138
test('should return true when: o1.b.g = o2.b and o2.b.g = o2.b (circular structure)', () => {
139-
// isEqual will evaluate until certain depth
139+
// isDeepEqual will evaluate until certain depth
140140
// but since objects still the same we expect the result to be true
141141
that.o1.b.g = that.o2.b;
142142
that.o2.b.g = that.o2.b;
143-
expect(Utils.isEqual(that.o1, that.o2)).toEqual(true);
143+
expect(Utils.isDeepEqual(that.o1, that.o2)).toEqual(true);
144144
});
145145

146146
test('should return true when: o1.b.g = o2.b and o2.b.g = o2 (circular structure)', () => {
147147
that.o1.b.g = that.o2.b;
148148
that.o2.b.g = that.o2;
149-
expect(Utils.isEqual(that.o1, that.o2)).toEqual(false);
149+
expect(Utils.isDeepEqual(that.o1, that.o2)).toEqual(false);
150150
});
151151
});
152152
});

0 commit comments

Comments
 (0)