prasanna t
prasanna t

Reputation: 13

How to sort and align the nodes vertically in the graph using d3.js and angular 13

I have used the d3.js graph structure to draw the flow chart. I have managed to align each node vertically at each level. However, I am facing several issues:

  1. I am unable to sort the nodes based on their position in the JSON.
  2. The chart always starts in the** middle of the canvas**.
  3. There is some unwanted code that needs to be removed.
  4. If more data is added, some nodes overlap with each other.
  5. I need to implement a draggable functionality for each node.
  6. The path should follow the curved line to connect the nodes

I tried the following code to draw the chart.

HTML

<div class="main">
    <div class="main-content">
        <div class="controls">
            <button (click)="zoomIn()">Zoom In</button>
            <button (click)="zoomOut()">Zoom Out</button>
            <button (click)="fitToScreen()">Fit to Screen</button>
        </div>
        <svg class="chart" width="1200" height="450"></svg>
        <svg class="legend"></svg>
    </div>
</div>

.ts file

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

@Component({
  selector: 'app-asset-linking',
  templateUrl: './asset-linking.component.html',
  styleUrls: ['./asset-linking.component.scss']
})
export class AssetLinkingComponent implements OnInit {
  nodes: any;
  links: any;
  zoom: any;
  svg: any;
  g: any;

  constructor() { }

  ngOnInit(): void {
    this.createChart();
    this.fitToScreen();
    this.createLegend();

  }

  private createChart(): void {
    // Node and link data
    this.nodes = nodes: [
        // LEVEL 1
        { id: '1', label: 'DNO', icon: 'fas fa-bolt', bgColor: 'gray', equipmentType: 'LLU/ff0', statusColor: 'green', level: 1 },
        // LEVEL 2
        { id: '2', label: 'Generator', icon: 'fas fa-cog', bgColor: 'purple', equipmentType: 'P10000', statusColor: 'green', level: 2, position: 1 },
        { id: '3', label: 'Mobile Generator', icon: 'fas fa-cog', bgColor: 'green', equipmentType: 'P10000', statusColor: 'green', level: 2, position: 2 },
        { id: '4', label: 'LLU', icon: 'fas fa-plug', bgColor: 'blue', equipmentType: 'EF80', statusColor: 'orange', level: 2, position: 3 },
        { id: '5', label: 'Air Cooling', icon: 'fas fa-plug', bgColor: 'blue', equipmentType: 'AIR COOLING', statusColor: 'orange', level: 2, position: 4 },
        // LEVEL 3
        { id: '6', label: 'HV Circuit', icon: 'fas fa-plug', bgColor: 'blue', equipmentType: 'HV CIRCUIT', statusColor: 'green', level: 3, position: 1 },
        { id: '7', label: 'EE HV Circuit', icon: 'fas fa-plug', bgColor: 'blue', equipmentType: 'HV CIRCUIT', statusColor: 'green', level: 3, position: 2 },
        { id: '8', label: 'HVDC', icon: 'fas fa-plug', bgColor: 'blue', equipmentType: 'HVDC', statusColor: 'green', level: 3, position: 3 },
        { id: '9', label: 'DC SYSTEM', icon: 'fas fa-plug', bgColor: 'blue', equipmentType: 'DC SYSTEM', statusColor: 'green', level: 3, position: 4 },
        { id: '10', label: 'DC SYSTEM', icon: 'fas fa-plug', bgColor: 'blue', equipmentType: 'DC SYSTEM', statusColor: 'green', level: 3, position: 5 },
        
        { id: '11', label: 'DC DC Converter', icon: 'fas fa-plug', bgColor: 'blue', equipmentType: 'DC DC CONVERTER', statusColor: 'green', level: 4, position: 1 },
        { id: '12', label: 'DC SYSTEM', icon: 'fas fa-plug', bgColor: 'blue', equipmentType: 'DC SYSTEM', statusColor: 'green', level: 4, position: 2 },
         { id: '13', label: 'Generator', icon: 'fas fa-plug', bgColor: 'purple', equipmentType: 'Generator', statusColor: 'green', level: 4, position: 3 },
         { id: '14', label: 'UPS', icon: 'fas fa-plug', bgColor: 'red', equipmentType: 'UPS', statusColor: 'green', level: 4, position: 4 },
        { id: '15', label: 'DC Distribution', icon: 'fas fa-plug', bgColor: 'grey', equipmentType: 'DC Distribution', statusColor: 'orange', level: 4, position: 5 },
        { id: '16', label: 'DC Distribution', icon: 'fas fa-plug', bgColor: 'grey', equipmentType: 'DC Distribution', statusColor: 'orange', level: 4, position: 6 },
        { id: '17', label: 'DC Distribution', icon: 'fas fa-plug', bgColor: 'grey', equipmentType: 'DC Distribution', statusColor: 'orange', level: 4, position: 7 },
        { id: '18', label: 'DC Distribution', icon: 'fas fa-plug', bgColor: 'grey', equipmentType: 'DC Distribution', statusColor: 'orange', level: 4, position: 8 },
        { id: '19', label: 'DC Distribution', icon: 'fas fa-plug', bgColor: 'grey', equipmentType: 'DC Distribution', statusColor: 'orange', level: 4, position: 9 },
        { id: '20', label: 'DC Distribution', icon: 'fas fa-plug', bgColor: 'grey', equipmentType: 'DC Distribution', statusColor: 'orange', level: 4, position: 10 },
        { id: '21', label: 'DC Distribution', icon: 'fas fa-plug', bgColor: 'grey', equipmentType: 'DC Distribution', statusColor: 'green', level: 4, position: 11 },
        { id: '22', label: 'DC Distribution', icon: 'fas fa-plug', bgColor: 'grey', equipmentType: 'DC Distribution', statusColor: 'orange', level: 4, position: 12 },
    ],

    this.links = // LEVEL 1
        { source: '1', target: '2' },
        { source: '1', target: '3' },
        { source: '1', target: '4' },
        { source: '1', target: '5' },
        // LEVEL 2
        { source: '2', target: '6' },
        { source: '2', target: '10' },
        { source: '3', target: '7' },
        { source: '4', target: '8' },
        { source: '4', target: '9' },
        // LEVEL 3
        { source: '6', target: '11' },
        { source: '6', target: '12' },
        { source: '7', target: '13' },
        { source: '8', target: '15' },
         { source: '8', target: '14' },
        { source: '9', target: '15' },
        { source: '9', target: '16' },
        { source: '9', target: '17' },
        { source: '9', target: '18' },
        { source: '9', target: '19' },
        { source: '9', target: '20' },
        { source: '9', target: '21' },
        { source: '9', target: '22' },
        { source: '10', target: '15' },
        { source: '10', target: '16' },
        { source: '10', target: '17' },
        { source: '10', target: '18' },
        { source: '10', target: '19' },
        { source: '10', target: '20' },
        { source: '10', target: '21' },
        { source: '10', target: '22' },

    // Set up the SVG element and zoom behavior
    this.svg = d3.select('svg.chart');
    const width = +this.svg.attr('width');
    const height = +this.svg.attr('height');

    this.zoom = d3.zoom()
      .scaleExtent([0.5, 4])
      .on('zoom', (event) => {
        this.g.attr('transform', event.transform);
      });

    this.svg.call(this.zoom);

    // Define the arrowhead marker
    this.svg.append('defs').append('marker')
      .attr('id', 'arrowhead')
      .attr('viewBox', '-0 -5 10 10')
      .attr('refX', 9)
      .attr('refY', 0)
      .attr('orient', 'auto')
      .attr('markerWidth', 6)
      .attr('markerHeight', 6)
      .attr('xoverflow', 'visible')
      .append('svg:path')
      .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
      .attr('fill', '#000')
      .style('stroke', 'none');

    // this.svg.append('line')
    //   .attr('x1', 0)
    //   .attr('y1', 0)
    //   .attr('x2', 0)
    //   .attr('y2', 200)
    //   .attr('stroke-width', 10)
    //   .style("stroke", "lightgreen");


    // Append a group element to the SVG
    this.g = this.svg.append('g');

    const simulation = d3.forceSimulation(this.nodes)
      .force('link', d3.forceLink(this.links).id(d => d.id)
        .distance(d => this.calculateLinkDistance(d.source.level, d.target.level)))
      .force('x', d3.forceX().strength(1).x(d => this.calculateXPosition(d.level)))
      // .force('y', d3.forceY(height / 2).strength(0.1))
      .force('collision', d3.forceCollide().radius(60));

    //a custom force named align is added to the simulation.
    //This force aligns nodes vertically based on their level by calculating the average y - 
    //position of nodes at each level 
    //and setting each node's y-position to this average.
    simulation.force('align', () => {
      const levels = d3.group(this.nodes, d => d.level);
      levels.forEach(nodes => {
        const avgX = d3.mean(nodes, d => d.x);
        nodes.forEach(d => d.x = avgX);
      });
    });

    const link = this.g.append('g')
      .attr('class', 'links')
      .selectAll('line')
      .data(this.links)
      .enter().append('line')
      .attr('stroke-width', 2)
      .attr('stroke', '#000')
      .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
      .attr('marker-end', 'url(#arrowhead)');

    const node = this.g.append('g')
      .attr('class', 'nodes')
      .selectAll('g')
      .data(this.nodes)
      .enter().append('g')
      .on('click', (event, d) => {
        alert(`Node Info:\nLabel: ${d.label}\nEquipment Type: ${d.equipmentType}\nStatus Color: ${d.statusColor}\nPirmId: ${d.id}`);
      });

    node.append('rect')
      .attr('width', 150)
      .attr('height', 60)
      .attr('rx', 0)
      .attr('ry', 0)
      .attr('fill', 'transparent')
      .style('cursor', 'pointer');



    node.append('foreignObject')
      .attr('x', 0)
      .attr('y', 0)
      .attr('width', 30)
      .attr('height', 60)
      .append('xhtml:div')
      .style('background-color', d => d.bgColor)
      .style('width', '100%')
      .style('height', '100%')
      .style('display', 'flex')
      .style('align-items', 'center')
      .style('justify-content', 'center')
      .style('cursor', 'pointer')
      .html(d => `<i class="${d.icon}" style="color: white;"></i>`);

    node.append('foreignObject')
      .attr('x', 30)
      .attr('y', 0)
      .attr('width', 70)
      .attr('height', 60)
      .append('xhtml:div')
      .style('width', '100%')
      .style('height', '100%')
      .style('display', 'flex')
      .style('align-items', 'center')
      .style('justify-content', 'center')
      .style('flex-direction', 'column')
      .style('cursor', 'pointer')
      .html(d => `<div>${d.label}</div><div>${d.equipmentType}</div><div>Pirm ID: ${d.id}</div>`);

    node.append('circle')
      .attr('cx', 140)
      .attr('cy', 10)
      .attr('r', 5)
      .style('fill', d => d.statusColor);

    simulation
      .nodes(this.nodes)
      .on('tick', ticked);

    const that = this;
    function ticked() {
      link
        .attr('x1', d => d.source.x)
        .attr('y1', d => d.source.y)
        .attr('x2', d => d.target.x - 150)
        .attr('y2', d => d.target.y);

      node
        .attr('transform', d => `translate(${d.x - 150},${d.y - 30})`);

      // Update the fit to screen
      that.fitToScreen();
    }
  }

  // Helper function to calculate the x position based on node level
  private calculateXPosition(level) {

    switch (level) {
      case 1:
        return 100;
      case 2:
        return 400;
      case 3:
        return 700;
      case 4:
        return 1000;
      case 5:
        return 1300;
      case 6:
        return 1600;
      case 7:
        return 1900;
      default:
        return 0;
    }

  }
  private calculateLinkDistance(sourceLevel, targetLevel) {

    const baseDistance = 300; // Base distance between nodes
    const levelDifference = Math.abs(sourceLevel - targetLevel);
    return baseDistance + levelDifference * 50; // Increase distance based on level difference
  }





  private createLegend(): void {
    const uniqueStatusColors = Array.from(new Set(this.nodes.map(node => node.statusColor)));
    const legendContainer = d3.select('svg.legend')
      .attr('width', 1000)
      .attr('height', uniqueStatusColors.length * 20 + 20);

    const legend = legendContainer.selectAll('.legend-item')
      .data(uniqueStatusColors)
      .enter().append('g')
      .attr('class', 'legend-item')
      .attr('transform', (d, i) => `translate(0, ${i * 20})`);

    legend.append('rect')
      .attr('x', 10)
      .attr('y', 10)
      .attr('width', 18)
      .attr('height', 18)
      .style('fill', d => d)
      .on('click', (event, color) => this.toggleNodesAndLinks(color));

    legend.append('text')
      .attr('x', 35)
      .attr('y', 19)
      .attr('dy', '0.35em')
      .text(d => d);
  }

  private toggleNodesAndLinks(color: string): void {
    const nodes = d3.selectAll('.nodes g');
    const links = d3.selectAll('.links line');

    const isVisible = nodes.filter(d => d.statusColor === color).style('display') !== 'none';

    nodes.filter(d => d.statusColor === color).style('display', isVisible ? 'none' : 'inline');
    links.filter(d => d.source.statusColor === color || d.target.statusColor === color).style('display', isVisible ? 'none' : 'inline');
  }

  zoomIn(): void {
    this.zoom.scaleBy(d3.select('svg.chart').transition().duration(500), 1.2);
  }

  zoomOut(): void {
    this.zoom.scaleBy(d3.select('svg.chart').transition().duration(500), 0.8);
  }

  fitToScreen(): void {
    const bounds = this.g.node().getBBox();
    const fullWidth = this.svg.attr('width');
    const fullHeight = this.svg.attr('height');
    const width = bounds.width;
    const height = bounds.height;
    const midX = bounds.x + width / 2;
    const midY = bounds.y + height / 2;

    if (width === 0 || height === 0) return;

    const scale = 0.95 / Math.max(width / fullWidth, height / fullHeight);
    const translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];

    this.svg.transition().duration(50).call(
      this.zoom.transform,
      d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale)
    );
  }
}

Sreen-shot: enter image description here

Upvotes: 0

Views: 38

Answers (0)

Related Questions