Integrating D3.js in Angular 14 to Visualize Relations/dependencies Between Microservices

Integrating D3.js in Angular 14 to Visualize Relations/dependencies Between Microservices

Introduction: Microservices architecture has gained significant popularity due to its scalability and flexibility. However, as the number of microservices grows, visualizing and understanding their relationships becomes crucial for effective management. In this article, we will explore how to integrate D3.js, a powerful data visualization library, into an Angular application visualize relations between microservices.

Prerequisites: To follow along with the code examples and implementation, you should have a basic understanding of Angular and familiarity with TypeScript.

Step 1: Setting Up the Angular Project: Begin by creating an Angular project using the Angular CLI. Open your command prompt or terminal and execute the following command:

ng new ServiceVisuals        

To install D3.js as a dependency, run the following command:

npm install d3        

This command will install the latest version of D3.js and add it to your project's package.json file. Now add it following en scripts array in angular.json file

  "scripts":[
            "node_modules/d3/dist/d3.js"
            ]        

Step 3: Code Implementation:

Create a component using following command:

ng g c MyD3Component        

open the generated component file, src/app/my-d3-component/my-d3-component.component.ts, and replace the following code. 

import { AfterViewInit, Component, ElementRef, OnInit, TemplateRef, ViewChild } from '@angular/core'
import * as d3 from 'd3';

@Component({
  selector: 'app-my-d3-component',
  templateUrl: './my-d3-component.component.html',
  styleUrls: ['./my-d3-component.component.css']
})
export class MyD3ComponentComponent implements OnInit{
  @ViewChild('chartContainer', { static: true })
  chartContainer!: ElementRef;
 
  ngOnInit() {
    const diameter = 860;
    const radius = diameter / 2;
    const innerRadius = radius - 170;


    const cluster = d3.cluster()
      .size([360, innerRadius]);


    const line = d3.lineRadial<any>()
      .curve(d3.curveBundle.beta(0.85))
      .radius((d) => d.y)
      .angle((d) => d.x / 180 * Math.PI);


    const svg = d3.select(this.chartContainer.nativeElement)
      .append('svg')
      .attr('width', diameter)
      .attr('height', diameter)
      .append('g')
      .attr('transform', `translate(${radius}, ${radius})`);


    let link: d3.Selection<SVGPathElement, any, SVGGElement, unknown>, node: d3.Selection<SVGTextElement, d3.HierarchyNode<any>, SVGGElement, unknown>;

    d3.json<any>('assets/serviceNet1.json').then((classes) => {
      const root = packageHierarchy(classes)
        .sum((d) => d.size);
    
      cluster(root);
    
      link = svg.append('g')
        .selectAll('.link')
        .data(packageImports(root.leaves()))
        .enter().append('path')
        .each((d: any) => { d.source = d[0], d.target = d[d.length - 1]; })
        .attr('class', 'link')
        .attr('d', line);
    
      node = svg.append('g')
        .selectAll('.node')
        .data(root.leaves())
        .enter().append('text')
        .attr('class', 'node')
        .attr('dy', '0.31em')
        .attr('transform', (d: any) => `rotate(${d.x - 90})translate(${d.y + 8},0)${d.x < 180 ? '' : 'rotate(180)'}`)
        .attr('text-anchor', (d: any) => (d.x < 180 ? 'start' : 'end'))
        .text((d: any) => d.data.key)
        .on('mouseover', mouseovered.bind(this))
        .on('mouseout', mouseouted.bind(this));
    });

    function mouseovered(d: any) {
      node.each((n: any) => { n.target = n.source = false; });
    
      link
        .classed('link--target', (l: any) => {
          if (l.target.data === d.target.__data__.data) {
            l.source.source = true;
            return true;
          }
          return false;
        })
        .classed('link--source', (l: any) => {
          if (l.source.data === d.target.__data__.data) {
            l.target.target = true;
            return true;
          }
          return false;
        })
        .filter((l: any) => l.target.data === d || l.source.data === d)
        .raise();
    
      node
        .classed('node--target', (n: any) => n.target)
        .classed('node--source', (n: any) => n.source);
    }
    
    function mouseouted() {
      link
        .classed('link--target', false)
        .classed('link--source', false);
    
      node
        .classed('node--target', false)
        .classed('node--source', false);
    }


    function packageHierarchy(classes: any[]) {
      const map: { [key: string]: any } = {};
    
      function find(name: string, data?: { name: string; children: any[] }) {
        let node = map[name];
        let i: number;
        if (!node) {
          node = map[name] = data || { name: name, children: [] };
          if (name.length) {
            node.parent = find(name.substring(0, i = name.lastIndexOf('.')));
            node.parent.children.push(node);
            node.key = name.substring(i + 1);
          }
        }
        return node;
      }
    
      classes.forEach((d) => {
        find(d.name, d);
      });
    
      return d3.hierarchy(map['']);
    }
  
    function packageImports(nodes: any[]) {
      const map: { [key: string]: any } = {};
      const imports: any[] = [];
    
      // Compute a map from name to node.
      nodes.forEach((d) => {
        map[d.data.name] = d;
      });
    
      // For each import, construct a link from the source to target node.
      nodes.forEach((d) => {
        if (d.data.imports) {
          d.data.imports.forEach((i: string | number) => {
            imports.push(map[d.data.name].path(map[i]));
          });
        }
      });
    
      return imports;
    }
  }
};        

Add some styling in styles.css file


.node {
    font: 300 15px "Helvetica Neue", Helvetica, Arial, sans-serif;
    fill: #bbb;
  }
  
  .node:hover {
    fill: #000;
  }
  
  .link {
    stroke: steelblue;
    stroke-opacity: 0.4;
    fill: none;
    pointer-events: none;
  }
  
  .node:hover,
  .node--source,
  .node--target {
    font-weight: 700;
  }
  
  .node--source {
    fill: #2ca02c;
  }
  
  .node--target {
    fill: #d62728;
  }
  
  .link--source,
  .link--target {
    stroke-opacity: 1;
    stroke-width: 2px;
  }
  
  .link--source {
    stroke: #d62728;
    stroke-dasharray: 1000;
    stroke-dashoffset: 1000;
    animation: growStroke 1s linear forwards;
  }
  
  .link--target {
    stroke: #2ca02c;
    stroke-dasharray: 1000;
    stroke-dashoffset: 1000;
    animation: growStroke 1s linear forwards;
  }
  
  @keyframes growStroke {
    0% {
      stroke-dashoffset: 1000;
    }
    100% {
      stroke-dashoffset: 0;
    }
  }


  .red {
        background-color: red;
        width: 20px;
  }
      
  .green {
        background-color: green;
        width: 20px;
  }        


Step 4: Data Source: Ensure you have a valid data source in the form of a JSON file, serviceNet.json, containing the necessary data structure representing the microservices and their relationships. Make sure the JSON file is stored in the assets folder of your Angular project.

Here is a sample JSON used in this application

 [
  {
      "imports": [
        "UserManagementService",
        "OrderProcessingService",
        "InventoryManagementService"
      ],
      "name": "PaymentGatewayService",
      "size": 3938
    },
    {
      "imports": [
        "UserManagementService",
        "AnalyticsService"
      ],
      "name": "EmailNotificationService",
      "size": 3938
    },
    {
      "imports": [
        "OrderProcessingService",
        "ShippingService",
        "AnalyticsService"
      ],
      "name": "ProductCatalogService",
      "size": 3950
    },
    {
      "imports": [
        "PaymentGatewayService",
        "EmailNotificationService",
        "LoggingService"
      ],
      "name": "AuthenticationService",
      "size": 3943
    },
    {
      "imports": [
        "MessagingService",
        "LoggingService",
        "SearchService"
      ],
      "name": "CustomerSupportService",
      "size": 3949
    },
    {
      "imports": [
        "ImageProcessingService",
        "SearchService",
        "AuthenticationService"
      ],
      "name": "RecommendationEngineService",
      "size": 3950
    },
    {
      "imports": [
        "UserManagementService",
        "AnalyticsService",
        "LoggingService"
      ],
      "name": "InventoryManagementService",
      "size": 3943
    },
    {
      "imports": [
        "EmailNotificationService",
        "AnalyticsService"
      ],
      "name": "OrderProcessingService",
      "size": 3954
    },
    {
      "imports": [
        "LoggingService",
        "EmailNotificationService",
        "AuthenticationService"
      ],
      "name": "UserManagementService",
      "size": 3939
    },
    {
      "imports": [
        "ImageProcessingService",
        "ProductCatalogService"
      ],
      "name": "SearchService",
      "size": 3940
    },
    {
      "imports": [
        "PaymentGatewayService",
        "EmailNotificationService",
        "InventoryManagementService"
      ],
      "name": "MessagingService",
      "size": 3938
    },
    {
      "imports": [
        "UserManagementService",
        "ShippingService",
        "InventoryManagementService"
      ],
      "name": "LoggingService",
      "size": 3947
    },
    {
      "imports": [
        "SearchService",
        "AuthenticationService",
        "ProductCatalogService",
        "ShippingService",
        "UserManagementService"
      ],
      "name": "AnalyticsService",
      "size": 3940
    },
    {
      "imports": [
        "AuthenticationService",
        "InventoryManagementService"
      ],
      "name": "ImageProcessingService",
      "size": 3959
    },
    {
      "imports": [
        "CustomerSupportService",
        "ProductCatalogService",
        "LoggingService",
        "SearchService"
      ],
      "name": "ShippingService",
      "size": 3938
    },
    {
      "imports": [
        "UserManagementService",
        "AuthenticationService",
        "LoggingService",
        "EmailNotificationService",
        "MessagingService",
        "ProductCatalogService",
        "SearchService",
        "InventoryManagementService"
      ],
      "name": "PaymentGatewayService",
      "size": 3938
    }
  ]        

Step 5: Customization and Scaling: To customize or scale the visualization, modify the provided code based on your specific requirements. You can adjust the dimensions, colors, and styling of the diagram elements. Explore additional D3.js features and layouts to enhance the visualization further.

Step 6: Running the Application:

To run the Angular application, execute the following command:

ng serve        

This command will start the development server, and you can access the application by navigating to http://localhost:4200 in your browser.

this is how final result will look like.

No alt text provided for this image
micro-service relations


here I have sample application hosted on Netlify. https://d3-angular14.netlify.app/

know more about Netlify here

Conclusion: By integrating D3.js into your Angular application, you can effectively maintain and map relations between microservices. The cluster diagram provides a clear visual representation of the connections, enabling easier management and understanding of complex microservice architectures. With the ability to customize and scale the visualization

References :

D3.js: https://observablehq.com/@d3/hierarchical-edge-bundling

Angular14: https://angular.io/guide/update-to-version-14

Netlify: https://www.netlify.com/


It would be nice to have the html part .. when I have added into html <div #chartContainer></div> I got this result : image below After modifying the component encapsulation ( encapsulation: ViewEncapsulation.None ) , the page looks as expected.

  • No alternative text description for this image

Please note, that the file src/app/my-d3-component/my-d3-component.component.html is missing in this article as of today....

To view or add a comment, sign in

Others also viewed

Explore content categories