[YUNIKORN-2347] Navigate to the center of the queue SVG (#186)

Closes: #186

Signed-off-by: Yu-Lin Chen <chenyulin0719@apache.org>
diff --git a/src/app/components/queue-v2/queues-v2.component.html b/src/app/components/queue-v2/queues-v2.component.html
index a3c137e..c3e5153 100644
--- a/src/app/components/queue-v2/queues-v2.component.html
+++ b/src/app/components/queue-v2/queues-v2.component.html
@@ -21,6 +21,12 @@
       <div class="header">
         <div class="title-group">
           <div>Partition</div>
+          <button id="fitButton" class="fit-to-screen-button">
+            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-6 h-6">
+              <path d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />          
+            </svg>
+            <span id="tooltip" class="tooltip" role="tooltip">fit to screen</span>
+          </button>
         </div>
       </div>
       <div class="body">
diff --git a/src/app/components/queue-v2/queues-v2.component.scss b/src/app/components/queue-v2/queues-v2.component.scss
index f9a55f1..fd3fd4a 100644
--- a/src/app/components/queue-v2/queues-v2.component.scss
+++ b/src/app/components/queue-v2/queues-v2.component.scss
@@ -23,28 +23,64 @@
     background-color: white;
     padding: 20px; /* Adjust padding to your preference */
     box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
-    overflow: auto; /* Add scroll for overflow content */
+    overflow: show; /* Add scroll for overflow content */
   }
   
   .header .title-group {
     display: flex;
     align-items: center;
-    justify-content: center;
+    justify-content: space-between;
     padding-bottom: 1rem;
   }
-  
-  .title-group .icon {
-    height: 2rem;
-    width: 2rem;
-    margin-right: 10px;
-  }
-  
+   
   .title-group div {
+    flex-grow: 1; 
+    text-align: center;
     font-size: 1.25rem; 
     font-weight: 600; 
-    color: #010407; 
+    color: #010407;
   } 
   
+  .fit-to-screen-button {
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    padding: 6px 6px;
+    background-color: #e5ecf6;
+    border: 1px solid #132030;
+    border-radius: 5px;
+    cursor: pointer;
+    overflow: show;
+  }
+
+  .tooltip {
+      width: 100px;
+      position: absolute;
+      bottom: 90%;
+      left: 50%;
+      transform: translateX(-50%);
+      background-color: rgb(225, 228, 241);
+      color: rgb(6, 7, 6);
+      text-align: center;
+      padding: 5px 5px;
+      border-radius: 6px;
+      visibility: hidden;
+      opacity: 1;
+      transition: opacity 0.3s, visibility 0.3s;
+  }
+
+  .fit-to-screen-button:hover .tooltip {
+      margin-bottom: 10px;
+      visibility: visible;
+      opacity: 1;
+  }
+
+
+  .fit-to-screen-button:hover {
+      background-color: #8090a5;
+  }
+
   .visualize-area {
     display: flex;
     flex-direction: column;
diff --git a/src/app/components/queue-v2/queues-v2.component.ts b/src/app/components/queue-v2/queues-v2.component.ts
index 823a8db..58f8da2 100644
--- a/src/app/components/queue-v2/queues-v2.component.ts
+++ b/src/app/components/queue-v2/queues-v2.component.ts
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit} from '@angular/core';
 import { Router } from '@angular/router';
 import { NgxSpinnerService } from 'ngx-spinner';
 import { QueueInfo } from '@app/models/queue-info.model';
@@ -69,23 +69,77 @@
         if (data && data.rootQueue) {
           this.rootQueue = data.rootQueue;
           queueVisualization(this.rootQueue as QueueInfo)
+          setTimeout(() => this.adjustToScreen(),1000) // since the ngAfterViewInit hook is not working, we used setTimeout instead
         }
       });
   }
+
+  adjustToScreen() {
+    const fitButton = document.getElementById('fitButton');
+    fitButton?.click(); 
+  }
 }
 
 function queueVisualization(rawData : QueueInfo){
+  let numberOfNode = 0;
+  const duration = 750;
+
   const svg = select('.visualize-area').append('svg')
                .attr('width', '100%')
                .attr('height', '100%')
-              
-    const svgWidth = 1150;
-    const svgHeight = 600;
+                  
+    function fitGraphScale(){
+      const baseSvgElem = svg.node() as SVGGElement;
+      const bounds = baseSvgElem.getBBox();
+      const parent = baseSvgElem.parentElement as HTMLElement;
+      const fullWidth = parent.clientWidth;
+      const fullHeight = parent.clientHeight;
+      
+      const xfactor: number = fullWidth / bounds.width;
+      const yfactor: number = fullHeight / bounds.height;
+      let scaleFactor: number = Math.min(xfactor, yfactor);
 
-    // Center the group
+       // Add some padding so that the graph is not touching the edges
+       const paddingPercent = 0.9;
+       scaleFactor = scaleFactor * paddingPercent;
+       return scaleFactor
+    }
+
+    function centerGraph() {
+        const bbox = (svgGroup.node() as SVGGElement).getBBox();
+        const cx = bbox.x + bbox.width / 2;
+        const cy = bbox.y + bbox.height / 2;
+        return {cx, cy};
+    }
+
+    function adjustVisulizeArea(duration : number = 0){
+      const scaleFactor = fitGraphScale();
+      const {cx, cy} = centerGraph();
+      // make the total duration to be 1 second
+      svg.transition().duration(duration/1.5).call(zoom.translateTo, cx, cy)
+      .on("end", function() {
+        svg.transition().duration(duration/1.5).call(zoom.scaleBy, scaleFactor)
+      })
+    } 
+
+    // Append a svg group which holds all nodes and which is for the d3 zoom
     const svgGroup = svg.append("g")
-      .attr("transform", `translate(${svgWidth / 3}, ${svgHeight / 10})`); 
-  
+
+    const fitButton = select(".fit-to-screen-button")
+    .on("click", function() {
+      adjustVisulizeArea(duration)
+    })
+    .on('mouseenter', function() {
+      select(this).select('.tooltip')
+            .style('visibility', 'visible')
+            .style('opacity', 1);
+    })
+    .on('mouseleave', function() {
+      select(this).select('.tooltip')
+            .style('visibility', 'hidden')
+            .style('opacity', 0);
+    });
+    
     const treelayout = d3flextree
       .flextree<QueueInfo>({})
       .nodeSize((d) => {
@@ -98,15 +152,11 @@
     .zoom<SVGSVGElement, unknown>()
     .scaleExtent([0.1, 5]) 
     .on("zoom", (event) => {
-      const initialTransform = d3zoom.zoomIdentity.translate(svgWidth / 3, svgHeight / 10);
-      svgGroup.attr("transform", event.transform.toString() + initialTransform.toString());
+      svgGroup.attr("transform", event.transform)
     });
     svg.call(zoom);
 
-    let numberOfNode = 0;
-    const duration = 750;
     const root = d3hierarchy.hierarchy(rawData);
-
     update(root);
 
     function update(source: any){