diff --git a/src/webui/service/templates/js/topology.js b/src/webui/service/templates/js/topology.js index dcbbc51be4fe53f000206401f4c77ef18b53b9c4..d1266909193c2286a21f77454e3f9d5f73ddd545 100644 --- a/src/webui/service/templates/js/topology.js +++ b/src/webui/service/templates/js/topology.js @@ -31,24 +31,39 @@ const margin = {top: 5, right: 5, bottom: 5, left: 5}; const icon_width = 40; const icon_height = 40; -width = 1400 - margin.left - margin.right; -height = 800 - margin.top - margin.bottom; - -//function handleZoom(e) { -// console.dir(e); -// d3.select('svg g').attr('transform', e.transform); -//} -//let zoom = d3.zoom().scaleExtent([0.01, 10]).translateExtent([[0, 0], [width, height]]).on('zoom', handleZoom); - -// append the svg object to the body of the page -const svg = d3.select('#topology') - .append('svg') +const topologyContainer = d3.select('#topology'); +const svg = topologyContainer.append('svg'); +const zoomLayer = svg.append('g'); +const graphLayer = zoomLayer.append('g') + .attr('transform', `translate(${margin.left}, ${margin.top})`); + +let width = 0; +let height = 0; +let graphInitialized = false; + +function updateCanvasSize() { + const bounds = topologyContainer.node().getBoundingClientRect(); + width = Math.max(bounds.width - margin.left - margin.right, 200); + height = Math.max(bounds.height - margin.top - margin.bottom, 200); + svg .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - //.call(zoom) - .append('g') - .attr('transform', `translate(${margin.left}, ${margin.top})`) - ; + .attr('height', height + margin.top + margin.bottom); +} +updateCanvasSize(); + +function handleZoom() { + zoomLayer.attr('transform', d3.event.transform); +} + +const zoom = d3.zoom() + .scaleExtent([0.2, 8]) + .on('zoom', handleZoom); + +svg.call(zoom); + +const ZOOM_DURATION_MS = 250; +const ZOOM_SCALE_STEP = 1.2; +const PAN_OFFSET = 80; // svg objects var link, node, optical_link, labels; @@ -67,10 +82,21 @@ forceProperties = { var simulation = d3.forceSimulation(); +function applyDirectionalForces() { + simulation + .force("center", d3.forceCenter(width * forceProperties.center.x, height * forceProperties.center.y)) + .force("forceX", d3.forceX() + .strength(forceProperties.forceX.strength * forceProperties.forceX.enabled) + .x(width * forceProperties.forceX.x)) + .force("forceY", d3.forceY() + .strength(forceProperties.forceY.strength * forceProperties.forceY.enabled) + .y(height * forceProperties.forceY.y)); +} + // load the data d3.json("{{ url_for('main.topology') }}", function(data) { // set the data and properties of link lines and node circles - link = svg.append("g").attr("class", "links")//.style('stroke', '#aaa') + link = graphLayer.append("g").attr("class", "links")//.style('stroke', '#aaa') .selectAll("line") .data(data.links) .enter() @@ -86,7 +112,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { return l.name.toLowerCase().includes('mgmt') ? "5,5" : "0"; }); - optical_link = svg.append("g").attr("class", "links")//.style('stroke', '#aaa') + optical_link = graphLayer.append("g").attr("class", "links")//.style('stroke', '#aaa') .selectAll("line") .data(data.optical_links) .enter() @@ -102,7 +128,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { return l.name.toLowerCase().includes('mgmt') ? "5,5" : "0"; }); - node = svg.append("g").attr("class", "devices").attr('r', 20).style('fill', '#69b3a2') + node = graphLayer.append("g").attr("class", "devices").attr('r', 20).style('fill', '#69b3a2') .selectAll("circle") .data(data.devices) .enter() @@ -117,7 +143,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { // node tooltip //node.append("title").text(function(n) { return n.name; }); // persistent node labels - labels = svg.append("g").attr("class", "labels") + labels = graphLayer.append("g").attr("class", "labels") .selectAll("text") .data(data.devices) .enter() @@ -142,7 +168,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { // add forces, associate each with a name, and set their properties // Experimental : Optical link part - all_links = data.links.concat(data.optical_links) + const all_links = data.links.concat(data.optical_links); simulation .force("link", d3.forceLink() .id(function(d) {return d.id;}) @@ -162,19 +188,13 @@ d3.json("{{ url_for('main.topology') }}", function(data) { .force("collide", d3.forceCollide() .strength(forceProperties.collide.strength * forceProperties.collide.enabled) .radius(forceProperties.collide.radius) - .iterations(forceProperties.collide.iterations)) - .force("center", d3.forceCenter() - .x(width * forceProperties.center.x) - .y(height * forceProperties.center.y)) - .force("forceX", d3.forceX() - .strength(forceProperties.forceX.strength * forceProperties.forceX.enabled) - .x(width * forceProperties.forceX.x)) - .force("forceY", d3.forceY() - .strength(forceProperties.forceY.strength * forceProperties.forceY.enabled) - .y(height * forceProperties.forceY.y)); + .iterations(forceProperties.collide.iterations)); + + applyDirectionalForces(); // after each simulation tick, update the display positions simulation.on("tick", ticked); + graphInitialized = true; }); // update the display positions @@ -221,30 +241,40 @@ function dragended(d) { } // update size-related forces -d3.select(window).on("resize", function(){ - width = +svg.node().getBoundingClientRect().width; - height = +svg.node().getBoundingClientRect().height; - simulation.alpha(1).restart(); -}); +function handleResize() { + updateCanvasSize(); + if (graphInitialized) { + applyDirectionalForces(); + simulation.alpha(0.3).restart(); + } +} -///******************** UI ACTIONS *******************/ -// -//function resetZoom() { -// d3.select('svg').transition().call(zoom.scaleTo, 1.0); -//} -//function zoomIn() { -// d3.select('svg').transition().call(zoom.scaleBy, 2.0); -//} -//function zoomOut() { -// d3.select('svg').transition().call(zoom.scaleBy, 0.5); -//} -// -//function center() { -// d3.select('svg').transition().call(zoom.translateTo, 0.5 * width, 0.5 * height); -//} -//function panLeft() { -// d3.select('svg').transition().call(zoom.translateBy, -50, 0); -//} -//function panRight() { -// d3.select('svg').transition().call(zoom.translateBy, 50, 0); -//} +d3.select(window).on("resize", handleResize); + +/******************** UI ACTIONS *******************/ + +function resetZoom() { + svg.transition().duration(ZOOM_DURATION_MS).call(zoom.transform, d3.zoomIdentity); +} + +function zoomIn() { + svg.transition().duration(ZOOM_DURATION_MS).call(zoom.scaleBy, ZOOM_SCALE_STEP); +} + +function zoomOut() { + svg.transition().duration(ZOOM_DURATION_MS).call(zoom.scaleBy, 1 / ZOOM_SCALE_STEP); +} + +function center() { + const centerX = margin.left + width / 2; + const centerY = margin.top + height / 2; + svg.transition().duration(ZOOM_DURATION_MS).call(zoom.translateTo, centerX, centerY); +} + +function panLeft() { + svg.transition().duration(ZOOM_DURATION_MS).call(zoom.translateBy, -PAN_OFFSET, 0); +} + +function panRight() { + svg.transition().duration(ZOOM_DURATION_MS).call(zoom.translateBy, PAN_OFFSET, 0); +} diff --git a/src/webui/service/templates/main/home.html b/src/webui/service/templates/main/home.html index 5a5c439e1cce66433a984e3ab11ba01544a7f461..4b66463ca15052f85575b573b0cd4e8244f6e516 100644 --- a/src/webui/service/templates/main/home.html +++ b/src/webui/service/templates/main/home.html @@ -18,6 +18,17 @@ {% block content %}