luci/applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/js/netjsongraph.js
Jo-Philipp Wich 96b8cbdc16 luci-app-bmx7: mask CSS translate directives to avoid i18n confusion
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>
2020-01-09 11:19:39 +01:00

568 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 BarnesHut 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);
}
});
}
};
})();