Search code examples
htmlangularsvggraphtensorboard

How to properly measure svg group dimensions in Angular?


I am trying to build a tool with the help of Angular to visualize graphs similar to TensorBoard. My intention is to visualize the dependency graph that the build tool bazel outputs. In order to highlight the graph I want to draw a rect as a background. My current approach is to draw the nodes and edges within one group, measure the dimensions of the group and set the width and height of the rect accordingly. If I change the graph structure by expanding or minimizing a node the background rect does not update properly. I am guessing that this is because no change was made with respect to the components data and therefore the view won't get updated by Angular.

You will find the source code here: https://github.com/dprogm/graph-explorer

The template of the graph looks like this:

<svg:rect
  attr.width="{{_graph.width}}"
  attr.height="{{_graph.height}}" />

<svg:g #graphRef>
  <svg:path  *ngFor="let edge of _graph.edges"
    class="edge"
    [attr.d]="buildPath(edge)" />

  <svg:g app-graph-node *ngFor="let node of _graph.nodes"
    [node]="node"
    (nodeChange)="onNodeChanged($event)"></svg:g>
</svg:g>

The #graphRef is an ElementRef which is used for querying the bounding box:

if(this.graphRef !== undefined) {
  let graphBBox = this.graphRef.nativeElement.getBBox();
  this._graph.width = graphBBox.width;
  this._graph.height = graphBBox.height;
}

The main question is: Is there any better approach of measuring the size of the graph and if not how can I achieve that I get the right dimensions?

Before minimizing the subgraph node

After minimizing the subgraph node

You can reproduce the issue here: https://stackblitz.com/edit/angular-ivy-mkmkm6?file=src/app/app.component.html If you click the circle it changes the size. I want that the black rect also changes its size to fit the circle.


Solution

  • One way is to use requestAnimationFrame():

    import { Component, ElementRef, ViewChild, AfterViewInit, VERSION } from '@angular/core';
    import { Observable } from 'rxjs';
    
    @Component({
      selector: 'my-app',
      templateUrl: './app.component.html',
      styleUrls: [ './app.component.css' ]
    })
    export class AppComponent implements AfterViewInit  {
    
      // Background width
      bgWidth = 0;
      // Background height
      bgHeight = 0;
      // Radius of the circle
      r = 100;
      // Stroke width of the circle
      strokeWidth = 5;
      // Whether to draw a large or small circle
      largeMode: Boolean = true;
    
      @ViewChild('groupRef')
      groupRef : ElementRef;
    
      ngAfterViewInit(): void {
        requestAnimationFrame(()=> this.setRectSize());
      }
    
      setRectSize(): void {
        let groupBBox = this.groupRef.nativeElement.getBBox();
        this.bgWidth = groupBBox.width;
        this.bgHeight = groupBBox.height;
      }
    
      changeSize() : void {
        this.largeMode = !this.largeMode;
        if(this.largeMode) {
          this.r = 100;
        } else {
          this.r = 50;
        }
        requestAnimationFrame(()=> this.setRectSize());
      }
    }