4 * vanilla JS implementation of jQuery.extend()
6 d3
._extend = function(defaults
, options
) {
9 for(prop
in defaults
) {
10 if(Object
.prototype.hasOwnProperty
.call(defaults
, prop
)) {
11 extended
[prop
] = defaults
[prop
];
14 for(prop
in options
) {
15 if(Object
.prototype.hasOwnProperty
.call(options
, prop
)) {
16 extended
[prop
] = options
[prop
];
24 * @name d3._pxToNumber
25 * Convert strings like "10px" to 10
27 * @param {string} val The value to convert
28 * @return {int} The converted integer
30 d3
._pxToNumber = function(val
) {
31 return parseFloat(val
.replace('px'));
36 * @name d3._windowHeight
40 * @return {int} The window innerHeight
42 d3
._windowHeight = function() {
43 return window
.innerHeight
|| document
.documentElement
.clientHeight
|| 600;
48 * @name d3._getPosition
50 * Get the position of `element` relative to `container`
52 * @param {object} element
53 * @param {object} container
55 d3
._getPosition = function(element
, container
) {
56 var n
= element
.node(),
57 nPos
= n
.getBoundingClientRect();
58 cPos
= container
.node().getBoundingClientRect();
60 top
: nPos
.top
- cPos
.top
,
61 left
: nPos
.left
- cPos
.left
,
63 bottom
: nPos
.bottom
- cPos
.top
,
65 right
: nPos
.right
- cPos
.left
70 * netjsongraph.js main function
73 * @param {string} url The NetJSON file url
74 * @param {object} opts The object with parameters to override {@link d3.netJsonGraph.opts}
76 d3
.netJsonGraph = function(url
, opts
) {
80 * @param {string} el "body" The container element el: "body" [description]
81 * @param {bool} metadata true Display NetJSON metadata at startup?
82 * @param {bool} defaultStyle true Use css style?
83 * @param {bool} animationAtStart false Animate nodes or not on load
84 * @param {array} scaleExtent [0.25, 5] The zoom scale's allowed range. @see {@link https://github.com/mbostock/d3/wiki/Zoom-Behavior#scaleExtent}
85 * @param {int} charge -130 The charge strength to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#charge}
86 * @param {int} linkDistance 50 The target distance between linked nodes to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#linkDistance}
87 * @param {float} linkStrength 0.2 The strength (rigidity) of links to the specified value in the range. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#linkStrength}
88 * @param {float} friction 0.9 The friction coefficient to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#friction}
89 * @param {string} chargeDistance Infinity The maximum distance over which charge forces are applied. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#chargeDistance}
90 * @param {float} theta 0.8 The Barnes–Hut approximation criterion to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#theta}
91 * @param {float} gravity 0.1 The gravitational strength to the specified numerical value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#gravity}
92 * @param {int} circleRadius 8 The radius of circles (nodes) in pixel
93 * @param {string} labelDx "0" SVG dx (distance on x axis) attribute of node labels in graph
94 * @param {string} labelDy "-1.3em" SVG dy (distance on y axis) attribute of node labels in graph
95 * @param {function} onInit Callback function executed on initialization
96 * @param {function} onLoad Callback function executed after data has been loaded
97 * @param {function} onEnd Callback function executed when initial animation is complete
98 * @param {function} linkDistanceFunc By default high density areas have longer links
99 * @param {function} redraw Called when panning and zooming
100 * @param {function} prepareData Used to convert NetJSON NetworkGraph to the javascript data
101 * @param {function} onClickNode Called when a node is clicked
102 * @param {function} onClickLink Called when a link is clicked
108 animationAtStart
: true,
109 scaleExtent
: [0.25, 5],
113 friction
: 0.9, // d3 default
114 chargeDistance
: Infinity
, // d3 default
115 theta
: 0.8, // d3 default
120 nodeClassProperty
: null,
121 linkClassProperty
: null,
126 * Callback function executed on initialization
127 * @param {string|object} url The netJson remote url or object
128 * @param {object} opts The object of passed arguments
131 onInit: function(url
, opts
) {},
136 * Callback function executed after data has been loaded
137 * @param {string|object} url The netJson remote url or object
138 * @param {object} opts The object of passed arguments
141 onLoad: function(url
, opts
) {},
146 * Callback function executed when initial animation is complete
147 * @param {string|object} url The netJson remote url or object
148 * @param {object} opts The object of passed arguments
151 onEnd: function(url
, opts
) {},
154 * @name linkDistanceFunc
156 * By default, high density areas have longer links
158 linkDistanceFunc: function(d
){
159 var val
= opts
.linkDistance
;
160 if(d
.source
.linkCount
>= 4 && d
.target
.linkCount
>= 4) {
169 * Called on zoom and pan
172 panner
.attr("transform",
173 "translate(" + d3
.event
.translate
+ ") " +
174 "scale(" + d3
.event
.scale
+ ")"
181 * Convert NetJSON NetworkGraph to the data structure consumed by d3
183 * @param graph {object}
185 prepareData: function(graph
) {
187 nodes
= graph
.nodes
.slice(), // copy
188 links
= graph
.links
.slice(), // copy
189 nodes_length
= graph
.nodes
.length
,
190 links_length
= graph
.links
.length
;
192 for(var i
= 0; i
< nodes_length
; i
++) {
193 // count how many links every node has
194 nodes
[i
].linkCount
= 0;
195 nodesMap
[nodes
[i
].id
] = i
;
197 for(var c
= 0; c
< links_length
; c
++) {
198 var sourceIndex
= nodesMap
[links
[c
].source
],
199 targetIndex
= nodesMap
[links
[c
].target
];
200 // ensure source and target exist
201 if(!nodes
[sourceIndex
]) { throw("source '" + links
[c
].source
+ "' not found"); }
202 if(!nodes
[targetIndex
]) { throw("target '" + links
[c
].target
+ "' not found"); }
203 links
[c
].source
= nodesMap
[links
[c
].source
];
204 links
[c
].target
= nodesMap
[links
[c
].target
];
205 // add link count to both ends
206 nodes
[sourceIndex
].linkCount
++;
207 nodes
[targetIndex
].linkCount
++;
209 return { "nodes": nodes
, "links": links
};
215 * Called when a node is clicked
217 onClickNode: function(n
) {
218 var overlay
= d3
.select(".njg-overlay"),
219 overlayInner
= d3
.select(".njg-overlay > .njg-inner"),
220 html
= "<p><b>id</b>: " + n
.id
+ "</p>";
221 if(n
.label
) { html
+= "<p><b>label</b>: " + n
.label
+ "</p>"; }
223 for(var key
in n
.properties
) {
224 if(!n
.properties
.hasOwnProperty(key
)) { continue; }
225 html
+= "<p><b>"+key
.replace(/_/g, " ")+"</b
>: " + n.properties[key] + "</p
>";
228 if(n.linkCount) { html += "<p
><b
>links
</b>: " + n.linkCount + "</p>"; }
229 if(n.local_addresses) {
230 html += "<p
><b
>local addresses
</b>:<br>" + n.local_addresses.join('<br>') + "</p>";
232 overlayInner.html(html);
233 overlay.classed("njg
-hidden
", false);
234 overlay.style("display
", "block
");
235 // set "open
" class to current node
237 d3.select(this).classed("njg
-open
", true);
243 * Called when a node is clicked
245 onClickLink: function(l) {
246 var overlay = d3.select(".njg
-overlay
"),
247 overlayInner = d3.select(".njg
-overlay
> .njg
-inner
"),
248 html = "<p
><b
>source
</b>: " + (l.source.label || l.source.id) + "</p>";
249 html += "<p
><b
>target
</b>: " + (l.target.label || l.target.id) + "</p>";
250 html += "<p
><b
>cost
</b>: " + l.cost + "</p>";
252 for(var key in l.properties) {
253 if(!l.properties.hasOwnProperty(key)) { continue; }
254 html += "<p
><b
>"+ key.replace(/_/g, " ") +"</b
>: " + l.properties[key] + "</p
>";
257 overlayInner.html(html);
258 overlay.classed("njg
-hidden
", false);
259 overlay.style("display
", "block
");
260 // set "open
" class to current link
262 d3.select(this).classed("njg
-open
", true);
267 opts.onInit(url, opts);
269 if(!opts.animationAtStart) {
270 opts.linkStrength = 2;
274 if(opts.el == "body
") {
275 var body = d3.select(opts.el),
276 rect = body.node().getBoundingClientRect();
277 if (d3._pxToNumber(d3.select("body
").style("height
")) < 60) {
278 body.style("height
", d3._windowHeight() - rect.top - rect.bottom + "px
");
281 var el = d3.select(opts.el).style("position
", "relative
"),
282 width = d3._pxToNumber(el.style('width')),
283 height = d3._pxToNumber(el.style('height')),
284 force = d3.layout.force()
286 .linkStrength(opts.linkStrength)
287 .linkDistance(opts.linkDistanceFunc)
288 .friction(opts.friction)
289 .chargeDistance(opts.chargeDistance)
291 .gravity(opts.gravity)
292 // width is easy to get, if height is 0 take the height of the body
293 .size([width, height]),
294 zoom = d3.behavior.zoom().scaleExtent(opts.scaleExtent),
295 // panner is the element that allows zooming and panning
296 panner = el.append("svg
")
297 .attr("width
", width)
298 .attr("height
", height)
299 .call(zoom.on("zoom
", opts.redraw))
301 .style("position
", "absolute
"),
302 svg = d3.select(opts.el + " svg
"),
304 overlay = d3.select(opts.el).append("div
").attr("class", "njg
-overlay
"),
305 closeOverlay = overlay.append("a
").attr("class", "njg
-close
"),
306 overlayInner = overlay.append("div
").attr("class", "njg
-inner
"),
307 metadata = d3.select(opts.el).append("div
").attr("class", "njg
-metadata
"),
308 metadataInner = metadata.append("div
").attr("class", "njg
-inner
"),
309 closeMetadata = metadata.append("a
").attr("class", "njg
-close
"),
310 // container of ungrouped networks
315 * @name removeOpenClass
317 * Remove open classes from nodes and links
319 removeOpenClass = function () {
320 d3.selectAll("svg
.njg
-open
").classed("njg
-open
", false);
322 processJson = function(graph) {
326 init = function(url, opts) {
327 d3.netJsonGraph(url, opts);
330 * Remove all instances
332 destroy = function() {
334 d3.select("#selectGroup
").remove();
335 d3.select(".njg
-overlay
").remove();
336 d3.select(".njg
-metadata
").remove();
338 overlayInner.remove();
347 * Destroy and e-init all instances
348 * @return {[type]} [description]
350 reInit = function() {
355 var data = opts.prepareData(graph),
359 // disable some transitions while dragging
360 drag.on('dragstart', function(n){
361 d3.event.sourceEvent.stopPropagation();
362 zoom.on('zoom', null);
364 // re-enable transitions when dragging stops
365 .on('dragend', function(n){
366 zoom.on('zoom', opts.redraw);
368 .on("drag
", function(d) {
369 // avoid pan & drag conflict
370 d3.select(this).attr("x
", d.x = d3.event.x).attr("y
", d.y = d3.event.y);
373 force.nodes(nodes).links(links).start();
375 var link = panner.selectAll(".link
")
377 .enter().append("line
")
378 .attr("class", function (link) {
379 var baseClass = "njg
-link
",
381 value = link.properties && link.properties[opts.linkClassProperty];
382 if (opts.linkClassProperty && value) {
383 // if value is stirng use that as class
384 if (typeof(value) === "string
") {
387 else if (typeof(value) === "number
") {
388 addClass = opts.linkClassProperty + value;
390 else if (value === true) {
391 addClass = opts.linkClassProperty;
393 return baseClass + " " + addClass;
397 .on("click
", opts.onClickLink),
398 groups = panner.selectAll(".node
")
402 node = groups.append("circle
")
403 .attr("class", function (node) {
404 var baseClass = "njg
-node
",
406 value = node.properties && node.properties[opts.nodeClassProperty];
407 if (opts.nodeClassProperty && value) {
408 // if value is stirng use that as class
409 if (typeof(value) === "string
") {
412 else if (typeof(value) === "number
") {
413 addClass = opts.nodeClassProperty + value;
415 else if (value === true) {
416 addClass = opts.nodeClassProperty;
418 return baseClass + " " + addClass;
422 .attr("r
", opts.circleRadius)
423 .on("click
", opts.onClickNode)
426 var labels = groups.append('text')
427 .text(function(n){ return n.label || n.id })
428 .attr('dx', opts.labelDx)
429 .attr('dy', opts.labelDy)
430 .attr('class', 'njg-tooltip');
433 closeOverlay.on("click
", function() {
435 overlay.classed("njg
-hidden
", true);
437 // Close Metadata panel
438 closeMetadata.on("click
", function() {
439 // Reinitialize the page
440 if(graph.type === "NetworkCollection
") {
445 metadata.classed("njg
-hidden
", true);
449 // TODO: probably change defaultStyle
450 // into something else
451 if(opts.defaultStyle) {
452 var colors = d3.scale.category20c();
454 "fill
": function(d){ return colors(d.linkCount); },
460 metadata.attr("class", "njg
-metadata
").style("display
", "block
");
463 var attrs = ["protocol
",
471 html += "<h3
>" + graph.label + "</h3
>";
473 for(var i in attrs) {
476 html += "<p
><b
>" + attr + "</b>: <span>" + graph[attr] + "</span></p
>";
479 // Add nodes and links count
480 html += "<p
><b
>nodes
</b>: <span>" + graph.nodes.length + "</span></p
>";
481 html += "<p
><b
>links
</b>: <span>" + graph.links.length + "</span></p
>";
482 metadataInner.html(html);
483 metadata.classed("njg
-hidden
", false);
486 opts.onLoad(url, opts);
488 force.on("tick
", function() {
489 link.attr("x1
", function(d) {
492 .attr("y1
", function(d) {
495 .attr("x2
", function(d) {
498 .attr("y2
", function(d) {
502 node.attr("cx
", function(d) {
505 .attr("cy
", function(d) {
509 labels.attr("transform
", function(d) {
510 return "translate(" + d.x + "," + d.y + ")";
513 .on("end
", function(){
516 opts.onEnd(url, opts);
522 if(typeof(url) === "object
") {
527 * Parse the provided json file
528 * and call processJson() function
530 * @param {string} url The provided json file
531 * @param {function} error
533 d3.json(url, function(error, graph) {
534 if(error) { throw error; }
536 * Check if the json contains a NetworkCollection
538 if(graph.type === "NetworkCollection
") {
539 var selectGroup = body.append("div
").attr("id
", "njg
-select
-group
"),
540 select = selectGroup.append("select
")
541 .attr("id
", "select
");
543 select.append("option
")
546 "selected
": "selected
",
548 "disabled
": "disabled
"
550 .html("Choose the network to display
");
551 graph.collection.forEach(function(structure) {
552 select.append("option
").attr("value
", structure.type).html(structure.type);
553 // Collect each network json structure
554 selected[structure.type] = structure;
556 select.on("change
", function() {
557 selectGroup.attr("class", "njg
-hidden
");
558 // Call selected json structure
559 processJson(selected[this.options[this.selectedIndex].value]);