working rendering using d3-force plugin
This commit is contained in:
@@ -14,7 +14,7 @@ ajax '/ajax/content/device/netmap' => require_login sub {
|
|||||||
template 'ajax/device/netmap.tt', {}, { layout => undef };
|
template 'ajax/device/netmap.tt', {}, { layout => undef };
|
||||||
};
|
};
|
||||||
|
|
||||||
ajax '/ajax/data/device/alldevicelinks' => require_login sub {
|
ajax '/ajax/data/device/netmap' => require_login sub {
|
||||||
my $q = param('q');
|
my $q = param('q');
|
||||||
my %data = ( nodes => [], links => [] );
|
my %data = ( nodes => [], links => [] );
|
||||||
|
|
||||||
|
|||||||
197
share/public/css/d3-force.css
Normal file
197
share/public/css/d3-force.css
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
.net_gobrechts_d3_force,
|
||||||
|
.net_gobrechts_d3_force_customize,
|
||||||
|
.net_gobrechts_d3_force_customize td,
|
||||||
|
.net_gobrechts_d3_force_tooltip {
|
||||||
|
box-sizing: content-box;
|
||||||
|
font-family: Arial, Helvetica, Sans Serif;
|
||||||
|
font-size: 10px;
|
||||||
|
background-color: #fff
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force.border {
|
||||||
|
border: 1px solid silver;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force circle.highlighted {
|
||||||
|
stroke: #555;
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke-opacity: 1.0;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force circle.selected {
|
||||||
|
stroke: #555;
|
||||||
|
stroke-width: 4px;
|
||||||
|
stroke-dasharray: 4 2;
|
||||||
|
stroke-opacity: 1.0;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force text.label,
|
||||||
|
.net_gobrechts_d3_force text.labelCircular {
|
||||||
|
fill: black;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force text.label{
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force text.highlighted {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force text.link {
|
||||||
|
font-size: 12px;
|
||||||
|
fill: blue;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force line.link,
|
||||||
|
.net_gobrechts_d3_force path.link {
|
||||||
|
fill: none;
|
||||||
|
stroke: #bbb;
|
||||||
|
stroke-width: 1.5px;
|
||||||
|
stroke-opacity: 0.8;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force line.dotted,
|
||||||
|
.net_gobrechts_d3_force path.dotted {
|
||||||
|
stroke-dasharray: .01 3;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force line.dashed,
|
||||||
|
.net_gobrechts_d3_force path.dashed {
|
||||||
|
stroke-dasharray: 4 2;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force line.highlighted,
|
||||||
|
.net_gobrechts_d3_force path.highlighted {
|
||||||
|
stroke: #555 !important;
|
||||||
|
stroke-opacity: 1.0;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force marker.normal {
|
||||||
|
stroke: none;
|
||||||
|
fill: #bbb;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force marker.highlighted {
|
||||||
|
stroke: none;
|
||||||
|
fill: #555;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force .graphOverlay,
|
||||||
|
.net_gobrechts_d3_force .graphOverlaySizeHelper {
|
||||||
|
fill: none;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force .lasso path {
|
||||||
|
stroke: #505050;
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force .lasso .drawn {
|
||||||
|
fill-opacity: 0.05 ;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force .lasso .loop_close {
|
||||||
|
fill: none;
|
||||||
|
stroke-dasharray: 4,4;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force .lasso .origin {
|
||||||
|
fill: #3399FF;
|
||||||
|
fill-opacity: 0.5;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force .loading rect {
|
||||||
|
fill: black;
|
||||||
|
fill-opacity: 0.2;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force .loading text {
|
||||||
|
fill: white;
|
||||||
|
font-size: 36px;
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_tooltip {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: silver;
|
||||||
|
opacity: 0.9;
|
||||||
|
width: 150px;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
z-index: 100000;
|
||||||
|
pointer-events: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize {
|
||||||
|
border: 1px solid silver;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
position: absolute;
|
||||||
|
padding: 5px;
|
||||||
|
background-color:white;
|
||||||
|
box-shadow: 1px 1px 6px #666;
|
||||||
|
z-index: 200000;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize .drag {
|
||||||
|
border: 1px dashed silver;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: block;
|
||||||
|
cursor: move;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 24px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize .title {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize .close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
border: none;
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize tr.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize td {
|
||||||
|
padding: 1px;
|
||||||
|
font-size: 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize .label {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize .warning {
|
||||||
|
background-color: orange;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize input,
|
||||||
|
.net_gobrechts_d3_force_customize select,
|
||||||
|
.net_gobrechts_d3_force_customize textarea,
|
||||||
|
.net_gobrechts_d3_force_customize a {
|
||||||
|
border: 1px solid silver;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize a {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: blue;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize input:focus,
|
||||||
|
.net_gobrechts_d3_force_customize select:focus,
|
||||||
|
.net_gobrechts_d3_force_customize textarea:focus,
|
||||||
|
.net_gobrechts_d3_force_customize a:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border: 1px solid blue !important;
|
||||||
|
background-color: yellow !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.net_gobrechts_d3_force_customize textarea {
|
||||||
|
font-size: 10px !important;
|
||||||
|
padding: 2px;
|
||||||
|
width: 160px;
|
||||||
|
height: 85px;
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
4175
share/public/javascripts/d3-force.js
vendored
Normal file
4175
share/public/javascripts/d3-force.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
share/public/javascripts/d3.min.js
vendored
9
share/public/javascripts/d3.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,184 +1,28 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
var winHeight = window.innerHeight;
|
|
||||||
var winWidth = window.innerWidth;
|
|
||||||
|
|
||||||
// links in the initial tree drawing use this generator
|
|
||||||
var treeLink = d3.svg.diagonal.radial()
|
|
||||||
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
|
|
||||||
|
|
||||||
// actual device neighbor links use this generator
|
|
||||||
var neighLink = d3.svg.diagonal.radial();
|
|
||||||
|
|
||||||
// store x,y for all circles on the map
|
|
||||||
var loc = {};
|
|
||||||
// store actual links between all nodes
|
|
||||||
var neighbors_data = {};
|
|
||||||
|
|
||||||
// main SVG background, with support for pan/zoom
|
|
||||||
var svg = d3.select("#netmap_pane").append("svg")
|
|
||||||
.attr("width", winWidth - 50)
|
|
||||||
.attr("height", winHeight - 100)
|
|
||||||
.attr("pointer-events", "all")
|
|
||||||
.append('g')
|
|
||||||
.call(d3.behavior.zoom().on("zoom", redraw))
|
|
||||||
.append("g")
|
|
||||||
.attr("transform", "translate(" + winHeight / 2 + "," + winHeight / 2 + ")");
|
|
||||||
|
|
||||||
// this is the image background
|
|
||||||
// XXX there must be a way to discover the radial tree's size?
|
|
||||||
svg.append('rect')
|
|
||||||
.attr("x", (0 - (winHeight * 2)))
|
|
||||||
.attr('width', "400%")
|
|
||||||
.attr("y", (0 - (winHeight * 2)))
|
|
||||||
.attr('height', "400%")
|
|
||||||
.attr('fill', 'white');
|
|
||||||
|
|
||||||
// handle pan and zoom
|
|
||||||
function redraw() {
|
|
||||||
svg.attr("transform",
|
|
||||||
"translate(" + d3.event.translate + ")"
|
|
||||||
+ "scale(" + d3.event.scale + ")"
|
|
||||||
+ "translate(" + (winHeight / 2) + "," + (winHeight / 2) + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
// save the x,y of an element into the loc dictionary
|
|
||||||
function recordLocation(d,i) {
|
|
||||||
var rect = this.getBoundingClientRect();
|
|
||||||
loc[d.ip] = {
|
|
||||||
'x': (rect.left + ((rect.right - rect.left) / 2))
|
|
||||||
,'y': (rect.top + ((rect.bottom - rect.top) / 2))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert a device ip to a valid CSS class name
|
|
||||||
function to_class(ip) { return 'nd_' + ip.replace(/[:.]/g, "_") }
|
|
||||||
|
|
||||||
// handler for clicking on a circle - redirect to that device's netmap
|
|
||||||
function circleClick(d) {
|
|
||||||
window.location = '[% uri_for('/device') %]?tab=netmap'
|
|
||||||
+ '&q=' + d.ip
|
|
||||||
+ '&depth=[% params.depth | uri %]'
|
|
||||||
+ '&vlan=[% params.vlan | uri %]';
|
|
||||||
}
|
|
||||||
|
|
||||||
// handler for mouseover on a circle - show that device's real neighbors
|
|
||||||
function circleOver(d) {
|
|
||||||
$('.link').hide();
|
|
||||||
$('path.' + to_class(d.ip)).show();
|
|
||||||
$(this).css('cursor', 'pointer');
|
|
||||||
|
|
||||||
$.each(neighbors_data[d.ip], function(idx, target) {
|
|
||||||
if (! (target in loc)) { return true }
|
|
||||||
$('circle.' + to_class(target)).css('fill', '#e96cfa');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// handler for mouseout on a circle - hide real neighbours and show treeLinks
|
|
||||||
function circleOut(d) {
|
|
||||||
$.each(neighbors_data[d.ip], function(idx, target) {
|
|
||||||
if (! (target in loc)) { return true }
|
|
||||||
$('circle.' + to_class(target)).css('fill', '#fff');
|
|
||||||
});
|
|
||||||
|
|
||||||
$(this).css('cursor', 'auto');
|
|
||||||
$('path.' + to_class(d.ip)).hide();
|
|
||||||
$('.link').show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// load all device connections into neighbors_data dictionary
|
// load all device connections into neighbors_data dictionary
|
||||||
$.getJSON('[% uri_for('/ajax/data/device/alldevicelinks') %]', function(data) {
|
$.getJSON('[% uri_for('/ajax/data/device/netmap') %]', function(data) {
|
||||||
neighbors_data = data;
|
function resizeGraphContainer() {
|
||||||
|
setTimeout(function(){
|
||||||
|
var graph = jQuery('#netmap_pane');
|
||||||
|
var toc = jQuery('#dw__toc');
|
||||||
|
netmap_pane.width(
|
||||||
|
parseInt(graph.parent().css('width')) -
|
||||||
|
( toc.css('float') === 'right' && parseInt(toc.css('height')) > 30 ? parseInt(toc.css('width')) : 0 )
|
||||||
|
).resume();
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
// draw the tree
|
jQuery(document).ready(function() {
|
||||||
d3.json("[% uri_for('/ajax/data/device/netmap') %]?"
|
window.netmap_pane = netGobrechtsD3Force('netmap_pane')
|
||||||
+ '&q=[% params.q | uri %]'
|
.debug(true)
|
||||||
+ '&depth=[% params.depth | uri %]'
|
// .lassoMode(true)
|
||||||
+ '&vlan=[% params.vlan | uri %]', function(error, root) {
|
.start(data);
|
||||||
var tree = d3.layout.tree()
|
jQuery(window).on("resize", resizeGraphContainer);
|
||||||
// magic number "8" for scaling (network depth). seems to make things look right for me.
|
// jQuery('#dw__toc h3').on("click", resizeGraphContainer);
|
||||||
.size([360, (winHeight / 8 * (root['scale'] || 0))])
|
$('#nd_waiting').hide();
|
||||||
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
|
resizeGraphContainer();
|
||||||
|
|
||||||
var nodes = tree.nodes(root),
|
|
||||||
links = tree.links(nodes);
|
|
||||||
|
|
||||||
var link = svg.selectAll(".link")
|
|
||||||
.data(links)
|
|
||||||
.enter().append("path")
|
|
||||||
.attr("class", "link")
|
|
||||||
.attr("d", treeLink);
|
|
||||||
|
|
||||||
var node = svg.selectAll(".node")
|
|
||||||
.data(nodes)
|
|
||||||
.enter().append("g")
|
|
||||||
.attr("class", "node")
|
|
||||||
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; });
|
|
||||||
|
|
||||||
// begin to draw...
|
|
||||||
d3.select("#nd_waiting").remove();
|
|
||||||
|
|
||||||
node.append("circle")
|
|
||||||
.attr("r", 4.5)
|
|
||||||
// circle has class name of its device, so we can show/hide it
|
|
||||||
.attr("class", function(d) { return to_class(d.ip) })
|
|
||||||
// store the x,y of every circle we've just drawn
|
|
||||||
.each(recordLocation)
|
|
||||||
// handlers for mouse interaction with the circles
|
|
||||||
.on("click", circleClick)
|
|
||||||
.on("mouseover", circleOver)
|
|
||||||
.on("mouseout", circleOut);
|
|
||||||
|
|
||||||
node.append("text")
|
|
||||||
.attr("dy", ".31em")
|
|
||||||
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
|
|
||||||
.attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; })
|
|
||||||
.text(function(d) { return d.name; });
|
|
||||||
|
|
||||||
// reorient text on the root node
|
|
||||||
svg.select(".node")
|
|
||||||
.attr("transform", function(d) {
|
|
||||||
return d.x < 180 ? "rotate(0)translate(0)" : "rotate(180)translate(0)"; });
|
|
||||||
|
|
||||||
// key (ip) of the root node in our locations store
|
|
||||||
var rootname = svg.select(".node").data()[0].ip;
|
|
||||||
// reformatted neighbors_data for the real neighbor links
|
|
||||||
var neighbors = [];
|
|
||||||
|
|
||||||
// need to build neighbors array only after we have built loc dictionary,
|
|
||||||
// after drawing the circles and storing their x,y
|
|
||||||
$.each(neighbors_data, function(key, val) {
|
|
||||||
if (! (key in loc)) { return true }
|
|
||||||
|
|
||||||
$.each(val, function(idx, ip) {
|
|
||||||
if (! (ip in loc)) { return true }
|
|
||||||
|
|
||||||
neighbors.push({
|
|
||||||
'source': {
|
|
||||||
'ip': key
|
|
||||||
,'x': loc[key]['x']
|
|
||||||
,'y': loc[key]['y']
|
|
||||||
}
|
|
||||||
,'target': {
|
|
||||||
'ip': ip
|
|
||||||
,'x': loc[ip]['x']
|
|
||||||
,'y': loc[ip]['y']
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// insert Netdisco neighbor links below circles but above tree links
|
|
||||||
svg.selectAll(".neighbor")
|
|
||||||
.data(neighbors)
|
|
||||||
.enter().insert("path", ".node")
|
|
||||||
// add class name of source device, so we can show/hide the link
|
|
||||||
// (also "neighbor" class)
|
|
||||||
.attr("class", function(d) { return ("neighbor " + to_class( d.source.ip )) })
|
|
||||||
.attr("d", neighLink)
|
|
||||||
.attr("transform", "translate(-" + loc[rootname]['x'] + ",-" + loc[rootname]['y'] + ")");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}); // jquery getJSON for all connections
|
}); // jquery getJSON for all connections
|
||||||
|
|
||||||
// vim: ft=javascript
|
// vim: ft=javascript
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<script type="text/javascript" src="[% uri_base %]/javascripts/underscore.min.js"></script>
|
<script type="text/javascript" src="[% uri_base %]/javascripts/underscore.min.js"></script>
|
||||||
<script type="text/javascript" src="[% uri_base %]/javascripts/jquery.qtip.min.js"></script>
|
<script type="text/javascript" src="[% uri_base %]/javascripts/jquery.qtip.min.js"></script>
|
||||||
<script type="text/javascript" src="[% uri_base %]/javascripts/d3.min.js"></script>
|
<script type="text/javascript" src="[% uri_base %]/javascripts/d3.min.js"></script>
|
||||||
|
<script type="text/javascript" src="[% uri_base %]/javascripts/d3-force.js"></script>
|
||||||
<script type="text/javascript" src="[% uri_base %]/javascripts/toastr.js"></script>
|
<script type="text/javascript" src="[% uri_base %]/javascripts/toastr.js"></script>
|
||||||
<script type="text/javascript" src="[% uri_base %]/javascripts/jquery.floatThead.js"></script>
|
<script type="text/javascript" src="[% uri_base %]/javascripts/jquery.floatThead.js"></script>
|
||||||
<script type="text/javascript" src="[% uri_base %]/javascripts/daterangepicker.js"></script>
|
<script type="text/javascript" src="[% uri_base %]/javascripts/daterangepicker.js"></script>
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
<link rel="stylesheet" href="[% uri_base %]/css/smoothness/jquery-ui.custom.min.css"/>
|
<link rel="stylesheet" href="[% uri_base %]/css/smoothness/jquery-ui.custom.min.css"/>
|
||||||
<link rel="stylesheet" href="[% uri_base %]/css/font-awesome.min.css"/>
|
<link rel="stylesheet" href="[% uri_base %]/css/font-awesome.min.css"/>
|
||||||
<link rel="stylesheet" href="[% uri_base %]/css/toastr.css"/>
|
<link rel="stylesheet" href="[% uri_base %]/css/toastr.css"/>
|
||||||
|
<link rel="stylesheet" href="[% uri_base %]/css/d3-force.css"/>
|
||||||
<link rel="stylesheet" href="[% uri_base %]/css/netdisco.css"/>
|
<link rel="stylesheet" href="[% uri_base %]/css/netdisco.css"/>
|
||||||
<link rel="stylesheet" href="[% uri_base %]/css/bootstrap-tree.css"/>
|
<link rel="stylesheet" href="[% uri_base %]/css/bootstrap-tree.css"/>
|
||||||
<link rel="stylesheet" href="[% uri_base %]/css/daterangepicker-bs2.css"/>
|
<link rel="stylesheet" href="[% uri_base %]/css/daterangepicker-bs2.css"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user