Since bmx7's netjsongraph.js contains CSS `translate()` directives, they're treated as translation calls by the i18n source scanner. Mask these directives to avoid false positives. Fixes: #3484 Signed-off-by: Jo-Philipp Wich <jo@mein.io>
568 lines
25 KiB
JavaScript
568 lines
25 KiB
JavaScript
// version 0.1
|
||
(function () {
|
||
/**
|
||
* vanilla JS implementation of jQuery.extend()
|
||
*/
|
||
d3._extend = function(defaults, options) {
|
||
var extended = {},
|
||
prop;
|
||
for(prop in defaults) {
|
||
if(Object.prototype.hasOwnProperty.call(defaults, prop)) {
|
||
extended[prop] = defaults[prop];
|
||
}
|
||
}
|
||
for(prop in options) {
|
||
if(Object.prototype.hasOwnProperty.call(options, prop)) {
|
||
extended[prop] = options[prop];
|
||
}
|
||
}
|
||
return extended;
|
||
};
|
||
|
||
/**
|
||
* @function
|
||
* @name d3._pxToNumber
|
||
* Convert strings like "10px" to 10
|
||
*
|
||
* @param {string} val The value to convert
|
||
* @return {int} The converted integer
|
||
*/
|
||
d3._pxToNumber = function(val) {
|
||
return parseFloat(val.replace('px'));
|
||
};
|
||
|
||
/**
|
||
* @function
|
||
* @name d3._windowHeight
|
||
*
|
||
* Get window height
|
||
*
|
||
* @return {int} The window innerHeight
|
||
*/
|
||
d3._windowHeight = function() {
|
||
return window.innerHeight || document.documentElement.clientHeight || 600;
|
||
};
|
||
|
||
/**
|
||
* @function
|
||
* @name d3._getPosition
|
||
*
|
||
* Get the position of `element` relative to `container`
|
||
*
|
||
* @param {object} element
|
||
* @param {object} container
|
||
*/
|
||
d3._getPosition = function(element, container) {
|
||
var n = element.node(),
|
||
nPos = n.getBoundingClientRect();
|
||
cPos = container.node().getBoundingClientRect();
|
||
return {
|
||
top: nPos.top - cPos.top,
|
||
left: nPos.left - cPos.left,
|
||
width: nPos.width,
|
||
bottom: nPos.bottom - cPos.top,
|
||
height: nPos.height,
|
||
right: nPos.right - cPos.left
|
||
};
|
||
};
|
||
|
||
/**
|
||
* netjsongraph.js main function
|
||
*
|
||
* @constructor
|
||
* @param {string} url The NetJSON file url
|
||
* @param {object} opts The object with parameters to override {@link d3.netJsonGraph.opts}
|
||
*/
|
||
d3.netJsonGraph = function(url, opts) {
|
||
/**
|
||
* Default options
|
||
*
|
||
* @param {string} el "body" The container element el: "body" [description]
|
||
* @param {bool} metadata true Display NetJSON metadata at startup?
|
||
* @param {bool} defaultStyle true Use css style?
|
||
* @param {bool} animationAtStart false Animate nodes or not on load
|
||
* @param {array} scaleExtent [0.25, 5] The zoom scale's allowed range. @see {@link https://github.com/mbostock/d3/wiki/Zoom-Behavior#scaleExtent}
|
||
* @param {int} charge -130 The charge strength to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#charge}
|
||
* @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}
|
||
* @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}
|
||
* @param {float} friction 0.9 The friction coefficient to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#friction}
|
||
* @param {string} chargeDistance Infinity The maximum distance over which charge forces are applied. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#chargeDistance}
|
||
* @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}
|
||
* @param {float} gravity 0.1 The gravitational strength to the specified numerical value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#gravity}
|
||
* @param {int} circleRadius 8 The radius of circles (nodes) in pixel
|
||
* @param {string} labelDx "0" SVG dx (distance on x axis) attribute of node labels in graph
|
||
* @param {string} labelDy "-1.3em" SVG dy (distance on y axis) attribute of node labels in graph
|
||
* @param {function} onInit Callback function executed on initialization
|
||
* @param {function} onLoad Callback function executed after data has been loaded
|
||
* @param {function} onEnd Callback function executed when initial animation is complete
|
||
* @param {function} linkDistanceFunc By default high density areas have longer links
|
||
* @param {function} redraw Called when panning and zooming
|
||
* @param {function} prepareData Used to convert NetJSON NetworkGraph to the javascript data
|
||
* @param {function} onClickNode Called when a node is clicked
|
||
* @param {function} onClickLink Called when a link is clicked
|
||
*/
|
||
opts = d3._extend({
|
||
el: "body",
|
||
metadata: true,
|
||
defaultStyle: true,
|
||
animationAtStart: true,
|
||
scaleExtent: [0.25, 5],
|
||
charge: -130,
|
||
linkDistance: 50,
|
||
linkStrength: 0.2,
|
||
friction: 0.9, // d3 default
|
||
chargeDistance: Infinity, // d3 default
|
||
theta: 0.8, // d3 default
|
||
gravity: 0.1,
|
||
circleRadius: 8,
|
||
labelDx: "0",
|
||
labelDy: "-1.3em",
|
||
nodeClassProperty: null,
|
||
linkClassProperty: null,
|
||
/**
|
||
* @function
|
||
* @name onInit
|
||
*
|
||
* Callback function executed on initialization
|
||
* @param {string|object} url The netJson remote url or object
|
||
* @param {object} opts The object of passed arguments
|
||
* @return {function}
|
||
*/
|
||
onInit: function(url, opts) {},
|
||
/**
|
||
* @function
|
||
* @name onLoad
|
||
*
|
||
* Callback function executed after data has been loaded
|
||
* @param {string|object} url The netJson remote url or object
|
||
* @param {object} opts The object of passed arguments
|
||
* @return {function}
|
||
*/
|
||
onLoad: function(url, opts) {},
|
||
/**
|
||
* @function
|
||
* @name onEnd
|
||
*
|
||
* Callback function executed when initial animation is complete
|
||
* @param {string|object} url The netJson remote url or object
|
||
* @param {object} opts The object of passed arguments
|
||
* @return {function}
|
||
*/
|
||
onEnd: function(url, opts) {},
|
||
/**
|
||
* @function
|
||
* @name linkDistanceFunc
|
||
*
|
||
* By default, high density areas have longer links
|
||
*/
|
||
linkDistanceFunc: function(d){
|
||
var val = opts.linkDistance;
|
||
if(d.source.linkCount >= 4 && d.target.linkCount >= 4) {
|
||
return val * 2;
|
||
}
|
||
return val;
|
||
},
|
||
/**
|
||
* @function
|
||
* @name redraw
|
||
*
|
||
* Called on zoom and pan
|
||
*/
|
||
redraw: function() {
|
||
panner.attr("transform",
|
||
"trans"+"late(" + d3.event.translate + ") " +
|
||
"scale(" + d3.event.scale + ")"
|
||
);
|
||
},
|
||
/**
|
||
* @function
|
||
* @name prepareData
|
||
*
|
||
* Convert NetJSON NetworkGraph to the data structure consumed by d3
|
||
*
|
||
* @param graph {object}
|
||
*/
|
||
prepareData: function(graph) {
|
||
var nodesMap = {},
|
||
nodes = graph.nodes.slice(), // copy
|
||
links = graph.links.slice(), // copy
|
||
nodes_length = graph.nodes.length,
|
||
links_length = graph.links.length;
|
||
|
||
for(var i = 0; i < nodes_length; i++) {
|
||
// count how many links every node has
|
||
nodes[i].linkCount = 0;
|
||
nodesMap[nodes[i].id] = i;
|
||
}
|
||
for(var c = 0; c < links_length; c++) {
|
||
var sourceIndex = nodesMap[links[c].source],
|
||
targetIndex = nodesMap[links[c].target];
|
||
// ensure source and target exist
|
||
if(!nodes[sourceIndex]) { throw("source '" + links[c].source + "' not found"); }
|
||
if(!nodes[targetIndex]) { throw("target '" + links[c].target + "' not found"); }
|
||
links[c].source = nodesMap[links[c].source];
|
||
links[c].target = nodesMap[links[c].target];
|
||
// add link count to both ends
|
||
nodes[sourceIndex].linkCount++;
|
||
nodes[targetIndex].linkCount++;
|
||
}
|
||
return { "nodes": nodes, "links": links };
|
||
},
|
||
/**
|
||
* @function
|
||
* @name onClickNode
|
||
*
|
||
* Called when a node is clicked
|
||
*/
|
||
onClickNode: function(n) {
|
||
var overlay = d3.select(".njg-overlay"),
|
||
overlayInner = d3.select(".njg-overlay > .njg-inner"),
|
||
html = "<p><b>id</b>: " + n.id + "</p>";
|
||
if(n.label) { html += "<p><b>label</b>: " + n.label + "</p>"; }
|
||
if(n.properties) {
|
||
for(var key in n.properties) {
|
||
if(!n.properties.hasOwnProperty(key)) { continue; }
|
||
html += "<p><b>"+key.replace(/_/g, " ")+"</b>: " + n.properties[key] + "</p>";
|
||
}
|
||
}
|
||
if(n.linkCount) { html += "<p><b>links</b>: " + n.linkCount + "</p>"; }
|
||
if(n.local_addresses) {
|
||
html += "<p><b>local addresses</b>:<br>" + n.local_addresses.join('<br>') + "</p>";
|
||
}
|
||
overlayInner.html(html);
|
||
overlay.classed("njg-hidden", false);
|
||
overlay.style("display", "block");
|
||
// set "open" class to current node
|
||
removeOpenClass();
|
||
d3.select(this).classed("njg-open", true);
|
||
},
|
||
/**
|
||
* @function
|
||
* @name onClickLink
|
||
*
|
||
* Called when a node is clicked
|
||
*/
|
||
onClickLink: function(l) {
|
||
var overlay = d3.select(".njg-overlay"),
|
||
overlayInner = d3.select(".njg-overlay > .njg-inner"),
|
||
html = "<p><b>source</b>: " + (l.source.label || l.source.id) + "</p>";
|
||
html += "<p><b>target</b>: " + (l.target.label || l.target.id) + "</p>";
|
||
html += "<p><b>cost</b>: " + l.cost + "</p>";
|
||
if(l.properties) {
|
||
for(var key in l.properties) {
|
||
if(!l.properties.hasOwnProperty(key)) { continue; }
|
||
html += "<p><b>"+ key.replace(/_/g, " ") +"</b>: " + l.properties[key] + "</p>";
|
||
}
|
||
}
|
||
overlayInner.html(html);
|
||
overlay.classed("njg-hidden", false);
|
||
overlay.style("display", "block");
|
||
// set "open" class to current link
|
||
removeOpenClass();
|
||
d3.select(this).classed("njg-open", true);
|
||
}
|
||
}, opts);
|
||
|
||
// init callback
|
||
opts.onInit(url, opts);
|
||
|
||
if(!opts.animationAtStart) {
|
||
opts.linkStrength = 2;
|
||
opts.friction = 0.3;
|
||
opts.gravity = 0;
|
||
}
|
||
if(opts.el == "body") {
|
||
var body = d3.select(opts.el),
|
||
rect = body.node().getBoundingClientRect();
|
||
if (d3._pxToNumber(d3.select("body").style("height")) < 60) {
|
||
body.style("height", d3._windowHeight() - rect.top - rect.bottom + "px");
|
||
}
|
||
}
|
||
var el = d3.select(opts.el).style("position", "relative"),
|
||
width = d3._pxToNumber(el.style('width')),
|
||
height = d3._pxToNumber(el.style('height')),
|
||
force = d3.layout.force()
|
||
.charge(opts.charge)
|
||
.linkStrength(opts.linkStrength)
|
||
.linkDistance(opts.linkDistanceFunc)
|
||
.friction(opts.friction)
|
||
.chargeDistance(opts.chargeDistance)
|
||
.theta(opts.theta)
|
||
.gravity(opts.gravity)
|
||
// width is easy to get, if height is 0 take the height of the body
|
||
.size([width, height]),
|
||
zoom = d3.behavior.zoom().scaleExtent(opts.scaleExtent),
|
||
// panner is the element that allows zooming and panning
|
||
panner = el.append("svg")
|
||
.attr("width", width)
|
||
.attr("height", height)
|
||
.call(zoom.on("zoom", opts.redraw))
|
||
.append("g")
|
||
.style("position", "absolute"),
|
||
svg = d3.select(opts.el + " svg"),
|
||
drag = force.drag(),
|
||
overlay = d3.select(opts.el).append("div").attr("class", "njg-overlay"),
|
||
closeOverlay = overlay.append("a").attr("class", "njg-close"),
|
||
overlayInner = overlay.append("div").attr("class", "njg-inner"),
|
||
metadata = d3.select(opts.el).append("div").attr("class", "njg-metadata"),
|
||
metadataInner = metadata.append("div").attr("class", "njg-inner"),
|
||
closeMetadata = metadata.append("a").attr("class", "njg-close"),
|
||
// container of ungrouped networks
|
||
str = [],
|
||
selected = [],
|
||
/**
|
||
* @function
|
||
* @name removeOpenClass
|
||
*
|
||
* Remove open classes from nodes and links
|
||
*/
|
||
removeOpenClass = function () {
|
||
d3.selectAll("svg .njg-open").classed("njg-open", false);
|
||
};
|
||
processJson = function(graph) {
|
||
/**
|
||
* Init netJsonGraph
|
||
*/
|
||
init = function(url, opts) {
|
||
d3.netJsonGraph(url, opts);
|
||
};
|
||
/**
|
||
* Remove all instances
|
||
*/
|
||
destroy = function() {
|
||
force.stop();
|
||
d3.select("#selectGroup").remove();
|
||
d3.select(".njg-overlay").remove();
|
||
d3.select(".njg-metadata").remove();
|
||
overlay.remove();
|
||
overlayInner.remove();
|
||
metadata.remove();
|
||
svg.remove();
|
||
node.remove();
|
||
link.remove();
|
||
nodes = [];
|
||
links = [];
|
||
};
|
||
/**
|
||
* Destroy and e-init all instances
|
||
* @return {[type]} [description]
|
||
*/
|
||
reInit = function() {
|
||
destroy();
|
||
init(url, opts);
|
||
};
|
||
|
||
var data = opts.prepareData(graph),
|
||
links = data.links,
|
||
nodes = data.nodes;
|
||
|
||
// disable some transitions while dragging
|
||
drag.on('dragstart', function(n){
|
||
d3.event.sourceEvent.stopPropagation();
|
||
zoom.on('zoom', null);
|
||
})
|
||
// re-enable transitions when dragging stops
|
||
.on('dragend', function(n){
|
||
zoom.on('zoom', opts.redraw);
|
||
})
|
||
.on("drag", function(d) {
|
||
// avoid pan & drag conflict
|
||
d3.select(this).attr("x", d.x = d3.event.x).attr("y", d.y = d3.event.y);
|
||
});
|
||
|
||
force.nodes(nodes).links(links).start();
|
||
|
||
var link = panner.selectAll(".link")
|
||
.data(links)
|
||
.enter().append("line")
|
||
.attr("class", function (link) {
|
||
var baseClass = "njg-link",
|
||
addClass = null;
|
||
value = link.properties && link.properties[opts.linkClassProperty];
|
||
if (opts.linkClassProperty && value) {
|
||
// if value is stirng use that as class
|
||
if (typeof(value) === "string") {
|
||
addClass = value;
|
||
}
|
||
else if (typeof(value) === "number") {
|
||
addClass = opts.linkClassProperty + value;
|
||
}
|
||
else if (value === true) {
|
||
addClass = opts.linkClassProperty;
|
||
}
|
||
return baseClass + " " + addClass;
|
||
}
|
||
return baseClass;
|
||
})
|
||
.on("click", opts.onClickLink),
|
||
groups = panner.selectAll(".node")
|
||
.data(nodes)
|
||
.enter()
|
||
.append("g");
|
||
node = groups.append("circle")
|
||
.attr("class", function (node) {
|
||
var baseClass = "njg-node",
|
||
addClass = null;
|
||
value = node.properties && node.properties[opts.nodeClassProperty];
|
||
if (opts.nodeClassProperty && value) {
|
||
// if value is stirng use that as class
|
||
if (typeof(value) === "string") {
|
||
addClass = value;
|
||
}
|
||
else if (typeof(value) === "number") {
|
||
addClass = opts.nodeClassProperty + value;
|
||
}
|
||
else if (value === true) {
|
||
addClass = opts.nodeClassProperty;
|
||
}
|
||
return baseClass + " " + addClass;
|
||
}
|
||
return baseClass;
|
||
})
|
||
.attr("r", opts.circleRadius)
|
||
.on("click", opts.onClickNode)
|
||
.call(drag);
|
||
|
||
var labels = groups.append('text')
|
||
.text(function(n){ return n.label || n.id })
|
||
.attr('dx', opts.labelDx)
|
||
.attr('dy', opts.labelDy)
|
||
.attr('class', 'njg-tooltip');
|
||
|
||
// Close overlay
|
||
closeOverlay.on("click", function() {
|
||
removeOpenClass();
|
||
overlay.classed("njg-hidden", true);
|
||
});
|
||
// Close Metadata panel
|
||
closeMetadata.on("click", function() {
|
||
// Reinitialize the page
|
||
if(graph.type === "NetworkCollection") {
|
||
reInit();
|
||
}
|
||
else {
|
||
removeOpenClass();
|
||
metadata.classed("njg-hidden", true);
|
||
}
|
||
});
|
||
// default style
|
||
// TODO: probably change defaultStyle
|
||
// into something else
|
||
if(opts.defaultStyle) {
|
||
var colors = d3.scale.category20c();
|
||
node.style({
|
||
"fill": function(d){ return colors(d.linkCount); },
|
||
"cursor": "pointer"
|
||
});
|
||
}
|
||
// Metadata style
|
||
if(opts.metadata) {
|
||
metadata.attr("class", "njg-metadata").style("display", "block");
|
||
}
|
||
|
||
var attrs = ["protocol",
|
||
"version",
|
||
"revision",
|
||
"metric",
|
||
"router_id",
|
||
"topology_id"],
|
||
html = "";
|
||
if(graph.label) {
|
||
html += "<h3>" + graph.label + "</h3>";
|
||
}
|
||
for(var i in attrs) {
|
||
var attr = attrs[i];
|
||
if(graph[attr]) {
|
||
html += "<p><b>" + attr + "</b>: <span>" + graph[attr] + "</span></p>";
|
||
}
|
||
}
|
||
// Add nodes and links count
|
||
html += "<p><b>nodes</b>: <span>" + graph.nodes.length + "</span></p>";
|
||
html += "<p><b>links</b>: <span>" + graph.links.length + "</span></p>";
|
||
metadataInner.html(html);
|
||
metadata.classed("njg-hidden", false);
|
||
|
||
// onLoad callback
|
||
opts.onLoad(url, opts);
|
||
|
||
force.on("tick", function() {
|
||
link.attr("x1", function(d) {
|
||
return d.source.x;
|
||
})
|
||
.attr("y1", function(d) {
|
||
return d.source.y;
|
||
})
|
||
.attr("x2", function(d) {
|
||
return d.target.x;
|
||
})
|
||
.attr("y2", function(d) {
|
||
return d.target.y;
|
||
});
|
||
|
||
node.attr("cx", function(d) {
|
||
return d.x;
|
||
})
|
||
.attr("cy", function(d) {
|
||
return d.y;
|
||
});
|
||
|
||
labels.attr("transform", function(d) {
|
||
return "trans"+"late(" + d.x + "," + d.y + ")";
|
||
});
|
||
})
|
||
.on("end", function(){
|
||
force.stop();
|
||
// onEnd callback
|
||
opts.onEnd(url, opts);
|
||
});
|
||
|
||
return force;
|
||
};
|
||
|
||
if(typeof(url) === "object") {
|
||
processJson(url);
|
||
}
|
||
else {
|
||
/**
|
||
* Parse the provided json file
|
||
* and call processJson() function
|
||
*
|
||
* @param {string} url The provided json file
|
||
* @param {function} error
|
||
*/
|
||
d3.json(url, function(error, graph) {
|
||
if(error) { throw error; }
|
||
/**
|
||
* Check if the json contains a NetworkCollection
|
||
*/
|
||
if(graph.type === "NetworkCollection") {
|
||
var selectGroup = body.append("div").attr("id", "njg-select-group"),
|
||
select = selectGroup.append("select")
|
||
.attr("id", "select");
|
||
str = graph;
|
||
select.append("option")
|
||
.attr({
|
||
"value": "",
|
||
"selected": "selected",
|
||
"name": "default",
|
||
"disabled": "disabled"
|
||
})
|
||
.html("Choose the network to display");
|
||
graph.collection.forEach(function(structure) {
|
||
select.append("option").attr("value", structure.type).html(structure.type);
|
||
// Collect each network json structure
|
||
selected[structure.type] = structure;
|
||
});
|
||
select.on("change", function() {
|
||
selectGroup.attr("class", "njg-hidden");
|
||
// Call selected json structure
|
||
processJson(selected[this.options[this.selectedIndex].value]);
|
||
});
|
||
}
|
||
else {
|
||
processJson(graph);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
})();
|