Reputation: 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:
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)
);
}
}
Upvotes: 0
Views: 38