Merge pull request #440 from tohojo/bird-203
[feed/routing.git] / luci-app-bmx7 / files / www / luci-static / resources / bmx7 / js / netjsongraph.js
1 // version 0.1
2 (function () {
3 /**
4 * vanilla JS implementation of jQuery.extend()
5 */
6 d3._extend = function(defaults, options) {
7 var extended = {},
8 prop;
9 for(prop in defaults) {
10 if(Object.prototype.hasOwnProperty.call(defaults, prop)) {
11 extended[prop] = defaults[prop];
12 }
13 }
14 for(prop in options) {
15 if(Object.prototype.hasOwnProperty.call(options, prop)) {
16 extended[prop] = options[prop];
17 }
18 }
19 return extended;
20 };
21
22 /**
23 * @function
24 * @name d3._pxToNumber
25 * Convert strings like "10px" to 10
26 *
27 * @param {string} val The value to convert
28 * @return {int} The converted integer
29 */
30 d3._pxToNumber = function(val) {
31 return parseFloat(val.replace('px'));
32 };
33
34 /**
35 * @function
36 * @name d3._windowHeight
37 *
38 * Get window height
39 *
40 * @return {int} The window innerHeight
41 */
42 d3._windowHeight = function() {
43 return window.innerHeight || document.documentElement.clientHeight || 600;
44 };
45
46 /**
47 * @function
48 * @name d3._getPosition
49 *
50 * Get the position of `element` relative to `container`
51 *
52 * @param {object} element
53 * @param {object} container
54 */
55 d3._getPosition = function(element, container) {
56 var n = element.node(),
57 nPos = n.getBoundingClientRect();
58 cPos = container.node().getBoundingClientRect();
59 return {
60 top: nPos.top - cPos.top,
61 left: nPos.left - cPos.left,
62 width: nPos.width,
63 bottom: nPos.bottom - cPos.top,
64 height: nPos.height,
65 right: nPos.right - cPos.left
66 };
67 };
68
69 /**
70 * netjsongraph.js main function
71 *
72 * @constructor
73 * @param {string} url The NetJSON file url
74 * @param {object} opts The object with parameters to override {@link d3.netJsonGraph.opts}
75 */
76 d3.netJsonGraph = function(url, opts) {
77 /**
78 * Default options
79 *
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
103 */
104 opts = d3._extend({
105 el: "body",
106 metadata: true,
107 defaultStyle: true,
108 animationAtStart: true,
109 scaleExtent: [0.25, 5],
110 charge: -130,
111 linkDistance: 50,
112 linkStrength: 0.2,
113 friction: 0.9, // d3 default
114 chargeDistance: Infinity, // d3 default
115 theta: 0.8, // d3 default
116 gravity: 0.1,
117 circleRadius: 8,
118 labelDx: "0",
119 labelDy: "-1.3em",
120 nodeClassProperty: null,
121 linkClassProperty: null,
122 /**
123 * @function
124 * @name onInit
125 *
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
129 * @return {function}
130 */
131 onInit: function(url, opts) {},
132 /**
133 * @function
134 * @name onLoad
135 *
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
139 * @return {function}
140 */
141 onLoad: function(url, opts) {},
142 /**
143 * @function
144 * @name onEnd
145 *
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
149 * @return {function}
150 */
151 onEnd: function(url, opts) {},
152 /**
153 * @function
154 * @name linkDistanceFunc
155 *
156 * By default, high density areas have longer links
157 */
158 linkDistanceFunc: function(d){
159 var val = opts.linkDistance;
160 if(d.source.linkCount >= 4 && d.target.linkCount >= 4) {
161 return val * 2;
162 }
163 return val;
164 },
165 /**
166 * @function
167 * @name redraw
168 *
169 * Called on zoom and pan
170 */
171 redraw: function() {
172 panner.attr("transform",
173 "translate(" + d3.event.translate + ") " +
174 "scale(" + d3.event.scale + ")"
175 );
176 },
177 /**
178 * @function
179 * @name prepareData
180 *
181 * Convert NetJSON NetworkGraph to the data structure consumed by d3
182 *
183 * @param graph {object}
184 */
185 prepareData: function(graph) {
186 var nodesMap = {},
187 nodes = graph.nodes.slice(), // copy
188 links = graph.links.slice(), // copy
189 nodes_length = graph.nodes.length,
190 links_length = graph.links.length;
191
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;
196 }
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++;
208 }
209 return { "nodes": nodes, "links": links };
210 },
211 /**
212 * @function
213 * @name onClickNode
214 *
215 * Called when a node is clicked
216 */
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>"; }
222 if(n.properties) {
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>";
226 }
227 }
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>";
231 }
232 overlayInner.html(html);
233 overlay.classed("njg-hidden", false);
234 overlay.style("display", "block");
235 // set "open" class to current node
236 removeOpenClass();
237 d3.select(this).classed("njg-open", true);
238 },
239 /**
240 * @function
241 * @name onClickLink
242 *
243 * Called when a node is clicked
244 */
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>";
251 if(l.properties) {
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>";
255 }
256 }
257 overlayInner.html(html);
258 overlay.classed("njg-hidden", false);
259 overlay.style("display", "block");
260 // set "open" class to current link
261 removeOpenClass();
262 d3.select(this).classed("njg-open", true);
263 }
264 }, opts);
265
266 // init callback
267 opts.onInit(url, opts);
268
269 if(!opts.animationAtStart) {
270 opts.linkStrength = 2;
271 opts.friction = 0.3;
272 opts.gravity = 0;
273 }
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");
279 }
280 }
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()
285 .charge(opts.charge)
286 .linkStrength(opts.linkStrength)
287 .linkDistance(opts.linkDistanceFunc)
288 .friction(opts.friction)
289 .chargeDistance(opts.chargeDistance)
290 .theta(opts.theta)
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))
300 .append("g")
301 .style("position", "absolute"),
302 svg = d3.select(opts.el + " svg"),
303 drag = force.drag(),
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
311 str = [],
312 selected = [],
313 /**
314 * @function
315 * @name removeOpenClass
316 *
317 * Remove open classes from nodes and links
318 */
319 removeOpenClass = function () {
320 d3.selectAll("svg .njg-open").classed("njg-open", false);
321 };
322 processJson = function(graph) {
323 /**
324 * Init netJsonGraph
325 */
326 init = function(url, opts) {
327 d3.netJsonGraph(url, opts);
328 };
329 /**
330 * Remove all instances
331 */
332 destroy = function() {
333 force.stop();
334 d3.select("#selectGroup").remove();
335 d3.select(".njg-overlay").remove();
336 d3.select(".njg-metadata").remove();
337 overlay.remove();
338 overlayInner.remove();
339 metadata.remove();
340 svg.remove();
341 node.remove();
342 link.remove();
343 nodes = [];
344 links = [];
345 };
346 /**
347 * Destroy and e-init all instances
348 * @return {[type]} [description]
349 */
350 reInit = function() {
351 destroy();
352 init(url, opts);
353 };
354
355 var data = opts.prepareData(graph),
356 links = data.links,
357 nodes = data.nodes;
358
359 // disable some transitions while dragging
360 drag.on('dragstart', function(n){
361 d3.event.sourceEvent.stopPropagation();
362 zoom.on('zoom', null);
363 })
364 // re-enable transitions when dragging stops
365 .on('dragend', function(n){
366 zoom.on('zoom', opts.redraw);
367 })
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);
371 });
372
373 force.nodes(nodes).links(links).start();
374
375 var link = panner.selectAll(".link")
376 .data(links)
377 .enter().append("line")
378 .attr("class", function (link) {
379 var baseClass = "njg-link",
380 addClass = null;
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") {
385 addClass = value;
386 }
387 else if (typeof(value) === "number") {
388 addClass = opts.linkClassProperty + value;
389 }
390 else if (value === true) {
391 addClass = opts.linkClassProperty;
392 }
393 return baseClass + " " + addClass;
394 }
395 return baseClass;
396 })
397 .on("click", opts.onClickLink),
398 groups = panner.selectAll(".node")
399 .data(nodes)
400 .enter()
401 .append("g");
402 node = groups.append("circle")
403 .attr("class", function (node) {
404 var baseClass = "njg-node",
405 addClass = null;
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") {
410 addClass = value;
411 }
412 else if (typeof(value) === "number") {
413 addClass = opts.nodeClassProperty + value;
414 }
415 else if (value === true) {
416 addClass = opts.nodeClassProperty;
417 }
418 return baseClass + " " + addClass;
419 }
420 return baseClass;
421 })
422 .attr("r", opts.circleRadius)
423 .on("click", opts.onClickNode)
424 .call(drag);
425
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');
431
432 // Close overlay
433 closeOverlay.on("click", function() {
434 removeOpenClass();
435 overlay.classed("njg-hidden", true);
436 });
437 // Close Metadata panel
438 closeMetadata.on("click", function() {
439 // Reinitialize the page
440 if(graph.type === "NetworkCollection") {
441 reInit();
442 }
443 else {
444 removeOpenClass();
445 metadata.classed("njg-hidden", true);
446 }
447 });
448 // default style
449 // TODO: probably change defaultStyle
450 // into something else
451 if(opts.defaultStyle) {
452 var colors = d3.scale.category20c();
453 node.style({
454 "fill": function(d){ return colors(d.linkCount); },
455 "cursor": "pointer"
456 });
457 }
458 // Metadata style
459 if(opts.metadata) {
460 metadata.attr("class", "njg-metadata").style("display", "block");
461 }
462
463 var attrs = ["protocol",
464 "version",
465 "revision",
466 "metric",
467 "router_id",
468 "topology_id"],
469 html = "";
470 if(graph.label) {
471 html += "<h3>" + graph.label + "</h3>";
472 }
473 for(var i in attrs) {
474 var attr = attrs[i];
475 if(graph[attr]) {
476 html += "<p><b>" + attr + "</b>: <span>" + graph[attr] + "</span></p>";
477 }
478 }
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);
484
485 // onLoad callback
486 opts.onLoad(url, opts);
487
488 force.on("tick", function() {
489 link.attr("x1", function(d) {
490 return d.source.x;
491 })
492 .attr("y1", function(d) {
493 return d.source.y;
494 })
495 .attr("x2", function(d) {
496 return d.target.x;
497 })
498 .attr("y2", function(d) {
499 return d.target.y;
500 });
501
502 node.attr("cx", function(d) {
503 return d.x;
504 })
505 .attr("cy", function(d) {
506 return d.y;
507 });
508
509 labels.attr("transform", function(d) {
510 return "translate(" + d.x + "," + d.y + ")";
511 });
512 })
513 .on("end", function(){
514 force.stop();
515 // onEnd callback
516 opts.onEnd(url, opts);
517 });
518
519 return force;
520 };
521
522 if(typeof(url) === "object") {
523 processJson(url);
524 }
525 else {
526 /**
527 * Parse the provided json file
528 * and call processJson() function
529 *
530 * @param {string} url The provided json file
531 * @param {function} error
532 */
533 d3.json(url, function(error, graph) {
534 if(error) { throw error; }
535 /**
536 * Check if the json contains a NetworkCollection
537 */
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");
542 str = graph;
543 select.append("option")
544 .attr({
545 "value": "",
546 "selected": "selected",
547 "name": "default",
548 "disabled": "disabled"
549 })
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;
555 });
556 select.on("change", function() {
557 selectGroup.attr("class", "njg-hidden");
558 // Call selected json structure
559 processJson(selected[this.options[this.selectedIndex].value]);
560 });
561 }
562 else {
563 processJson(graph);
564 }
565 });
566 }
567 };
568 })();