import { Component, OnInit, ChangeDetectorRef, ChangeDetectionStrategy, Input, AfterViewInit, HostListener, ViewChild, ElementRef, ViewContainerRef, } from '@angular/core'; import { Node, LinkToNode, Link } from '@creedinteractive/onguard-models'; import { D3Service, FeGaussianBlurIn } from '../../../services/d3.service'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import {NetworkGraph, NetworkGraphOptions} from './network-graph.model'; @Component({ selector: 'onguard-network-graph', templateUrl: './network-graph.component.html', styleUrls: ['./network-graph.component.scss'], changeDetection: ChangeDetectionStrategy.Default, }) export class NetworkGraphComponent implements OnInit, AfterViewInit { @Input() networkGraphModel: NetworkGraph; // Selection View Options @Input() selectedColor = 'url(#greenGradient)'; @Input() selectedNeighborColor = 'url(#greenGradient)'; @Input() selectedLinkThickness = 2; // Path View Options @Input() pathColor = 'url(#purpleGradient)'; @Input() pathLinkThickness = 4; nodes: Node[]; links: Link[]; // Force and Dimension Options options: NetworkGraphOptions = { width: 800, height: 600, forces: { link: 0, collision: 0, charge: 0 }, }; @ViewChild('svgContainer') svgContainer: ElementRef; @ViewChild('svg') svg: ElementRef; @ViewChild('tooltip') tooltip; currentElement: any; mouseOver = false; // Keep reference for more efficient reverting selectedNodes: Node[] = []; selectedLinks: Link[] = []; showTooltip = false; currentPosition; hoveredNode: Node; graph: NetworkGraph; loading = false; processingDownload = false; downloadLink: SafeResourceUrl; private pathLinkColor = '#9665F6'; private nodeLinkColor = '#67C9C5'; constructor( private changeDetectorRef: ChangeDetectorRef, private d3Service: D3Service, private sanitizer: DomSanitizer, public viewContainerRef: ViewContainerRef, ) {} @HostListener('window:resize', ['$event']) onResize(event: any): void { if (this.setBounds()) { this.graph.initSimulation(this.options); this.changeDetectorRef.markForCheck(); } } ngOnInit(): void { /** Receiving an initialized simulated graph from our custom d3 service */ this.loading = true; const { nodes, links, options } = this.networkGraphModel; this.nodes = nodes; this.links = links; this.options = options; this.changeDetectorRef.markForCheck(); this.graph = this.d3Service.getNetworkGraph(this.nodes, this.links, this.options); // Subscribe to graph simulation events. this.graph.ticker.subscribe((d) => { this.changeDetectorRef.markForCheck(); }); this.graph.ended.subscribe((d) => { this.loading = false; this.changeDetectorRef.markForCheck(); }); } setBounds(): boolean { console.log('setting bounds') if (!this.svgContainer && !this.svgContainer.nativeElement.getBoundingClientRect()) { return false; } const bounds = this.svgContainer.nativeElement.getBoundingClientRect(); this.options.width = bounds.width || this.options.width; this.options.height = bounds.height || this.options.height; return true; } ngAfterViewInit(): void { setTimeout(() => { // Set up gradient definitions this.d3Service.addGradientDef( this.svg.nativeElement, 0, 100, 0, 100, [ { offset: 0, opacity: 1, color: '#077BDB', }, { offset: 100, opacity: 1, color: '#014EC9', }, ], 'blueGradient', ); this.d3Service.addGradientDef( this.svg.nativeElement, 0, 100, 0, 100, [ { offset: 0, opacity: 1, color: '#22C2A5', }, { offset: 100, opacity: 1, color: '#0293A4', }, ], 'greenGradient', ); this.d3Service.addGradientDef( this.svg.nativeElement, 0, 100, 0, 100, [ { offset: 0, opacity: 1, color: '#835DF1', }, { offset: 100, opacity: 1, color: '#7527CF', }, ], 'purpleGradient', ); this.d3Service.addDropShadowFilter( this.svg.nativeElement, 2, 2, 3.5, FeGaussianBlurIn.SourceGraphic, ); // Initialize simulation if (this.setBounds()) { this.graph.initSimulation(this.options); } }, 20); } clickNode(node: Node): void { // if displaying path clear if (this.selectedNodes.length === 2) { this.selectedNodes = []; this.graph.reset(); } // If node is unselected, select it if (!this.selectedNodes.includes(node)) { node.color = this.selectedColor; node.selected = true; this.selectedNodes.push(node); // If Path Selection if (this.selectedNodes.length === 2) { const potentialPath = this.graph.getPath( this.selectedNodes[0], this.selectedNodes[1], ); // If Path is found, draw it. if (potentialPath) { this.graph.reset(); this.drawPath(potentialPath); } // Handle showing single node } else { this.showLinkedNodes(node); this.graph.refresh(); } // Reset if node was already selected } else { node.reset(); const index = this.selectedNodes.indexOf(node); this.selectedNodes.splice(index); this.graph.resetLinksToNode(node); this.graph.refresh(); } } clickBackground(event: Event): void { // Don't do anything if clicked not on background if (event.target !== event.currentTarget) { return; } this.selectedNodes = []; this.graph.reset(); } drawPath(path: LinkToNode[]): void { for (const nl of path) { nl.node.color = this.pathColor; nl.node.coSelected = true; if (nl.link) { nl.link.thickness = this.pathLinkThickness; nl.link.color = this.pathLinkColor; } } this.graph.initLinks(); } showLinkedNodes(node: Node): void { const nodeLinks = this.graph.getLinkedNodes(node); for (const nodeLink of nodeLinks) { if (nodeLink.node === node) { continue; } nodeLink.link.selected = true; nodeLink.link.thickness = 3; nodeLink.link.color = this.nodeLinkColor; nodeLink.node.coSelected = true; nodeLink.node.color = this.selectedNeighborColor; } } prepareDownload(sanitized = false): SafeResourceUrl { this.processingDownload = true; if (sanitized) { const url = this.sanitizer.bypassSecurityTrustResourceUrl( this.d3Service.download(this.svg.nativeElement), ); this.downloadLink = url; this.processingDownload = false; return url; } return this.d3Service.download(this.svg.nativeElement); } onSkillMouseEnter(event, node: Node): void { if (node.selected || node.coSelected) { this.currentElement = event.target; this.currentPosition = { top: event.clientY, left: event.clientX }; this.hoveredNode = node; this.showTooltip = true; } } @HostListener('mouseover', ['$event']) onSkillMouseOver(event): void { let hoverComponent = event.target; let inside = false; do { if (this.tooltip) { if ( hoverComponent === this.tooltip._element.nativeElement || hoverComponent === this.currentElement ) { inside = true; } } hoverComponent = hoverComponent.parentNode; } while (hoverComponent); if (!inside) { this.showTooltip = false; } } }