import { EventEmitter } from '@angular/core'; import * as d3 from 'd3'; import { Link, Node, LinkToNode } from '@creedinteractive/onguard-models'; export interface NetworkGraphOptions { width: number; height: number; forces: { link: number; collision: number; charge: number; }; bounded?: boolean; title?: string; } export class NetworkGraph { public ticker: EventEmitter> = new EventEmitter(); public ended: EventEmitter> = new EventEmitter(); public simulation: d3.Simulation; public nodes: Node[] = []; public links: Link[] = []; public options: NetworkGraphOptions; constructor(nodes: Node[], links: Link[], options: NetworkGraphOptions = null) { this.nodes = nodes; this.links = links; this.options = options || { width: 800, height: 600, forces: { link: 0.75, collision: 1, charge: -20 }, }; if (this.options.width && this.options.height) { this.initSimulation(this.options); } } connectNodes(source, target): void { if (!this.nodes[source] || !this.nodes[target]) { throw new Error('One of the nodes does not exist'); } const link = new Link(source, target); this.simulation.stop(); this.links.push(link); this.simulation.alphaTarget(0.3).restart(); // this.initLinks(FORCES.LINKS); } initNodes(): void { if (!this.simulation) { throw new Error('simulation was not initialized yet'); } if (this.options.forces.charge) { this.simulation.force( 'charge', d3 .forceManyBody() .strength( (d) => this.options.forces.charge * d.chargeFactor * (d.selected ? 0 : 1) ), ); } if (this.options.forces.collision) { this.simulation.force( 'collide', d3 .forceCollide() .strength(this.options.forces.collision) .radius((d) => d.r + 5) .iterations(1), ); } this.simulation.nodes(this.nodes); } initLinks(): void { if (!this.simulation) { throw new Error('simulation was not initialized yet'); } if (this.options.forces.link !== null) { this.simulation.force( 'links', d3 .forceLink(this.links) .id((d) => d.id) .strength((d) => this.options.forces.link * d.forceFactor), ); } } initSimulation(options: NetworkGraphOptions): void { if (!options || !options.width || !options.height) { throw new Error('missing options when initializing simulation'); } /** Creating the simulation */ if (!this.simulation) { const ticker = this.ticker; const ended = this.ended; this.simulation = d3.forceSimulation(); // Connecting the d3 ticker to an angular event emitter this.simulation.on('tick', function (): void { ticker.emit(this); }); this.simulation.on('end', function (): void { ended.emit(this); }); this.initNodes(); this.initLinks(); } /** Updating the central force of the simulation */ // this.simulation.force('circle', d3.forceRadial(options.height / 2, options.width / 2, options.height / 2)); const xCenter = options.width / 2; const yCenter = options.height / 2; if (options.bounded) { const getXBound = (d: Node, i: number, data: Node[]): number => { // if (d.selected) { // return xCenter; // } if (d.x < 0) { return 0; } if (d.x > options.width) { return options.width; } return d.x; }; const getYBound = (d: Node, i: number, data: Node[]): number => { // if (d.selected) { // return yCenter; // } if (d.y < 0) { return 0; } if (d.y > options.height) { return options.height; } return d.y; }; this.simulation.force('xBoundry', d3.forceX(getXBound)); this.simulation.force('yBoundry', d3.forceY(getYBound)); } this.simulation.force('centers', d3.forceCenter(xCenter, yCenter)); /** Restarting the simulation internal timer */ this.simulation.restart(); } refresh(): void { this.initLinks(); this.initNodes(); } reset(): void { for (const node of this.nodes) { node.reset(); } for (const link of this.links) { link.reset(); } } resetLinksToNode(node: Node): void { for (const link of this.links) { if (link.source === node) { link.target.reset(); link.reset(); } if (link.target === node) { link.source.reset(); link.reset(); } } } getLinkedNodes(node: Node): LinkToNode[] { const links = this.links.filter((link) => link.source === node || link.target === node); const linkedNodes = links.map((link) => { if (link.target !== node) { return { link, node: link.target }; } return { link, node: link.source }; }); return linkedNodes; } getPath(origin: Node, destination: Node, path?: LinkToNode[]): LinkToNode[] | false { if (!path) { path = [{ link: null, node: origin }]; } const linkedNodes = this.getLinkedNodes(origin); // base case const linkedDestination = linkedNodes.find((nl) => nl.node === destination); if (linkedDestination) { path.push(linkedDestination); return path; } if (path.length > 100) { return false; } for (const ln of linkedNodes) { if (path.filter((pathItem) => ln.node === pathItem.node).length) { continue; } path.push(ln); const potentialPath = this.getPath(ln.node, destination, path); if (potentialPath) { return potentialPath; } path.pop(); } return false; } }