import { Injectable } from '@angular/core'; import * as d3 from 'd3'; import { NetworkGraph, NetworkGraphOptions } from '../components/visualizations/network-graph/network-graph.model'; import { Node, Link } from '@creedinteractive/onguard-models' @Injectable({ providedIn: 'root', }) export class D3Service { /** This service will provide methods to enable user interaction with elements * while maintaining the d3 simulations physics */ constructor() {} /** A method to bind a pan and zoom behaviour to an svg element */ applyZoomableBehaviour(svgElement, containerElement): void { const svg = d3.select(svgElement); const container = d3.select(containerElement); const zoomed = (event) => { const transform = event.transform; container.attr( 'transform', 'translate(' + transform.x + ',' + transform.y + ') scale(' + transform.k + ')', ); }; const zoom = d3.zoom().on('zoom', zoomed); svg.call(zoom); } /** A method to bind a draggable behaviour to an svg element */ applyDraggableBehaviour(element, node: Node, graph: NetworkGraph): void { const d3element = d3.select(element); function started(startEvent: any): void { /** Preventing propagation of dragstart to parent elements */ startEvent.sourceEvent.stopPropagation(); if (!startEvent.active) { graph.simulation.alphaTarget(0.3).restart(); } startEvent.on('drag', dragged).on('end', ended); function dragged(dragEvent: any): void { node.fx = dragEvent.x; node.fy = dragEvent.y; } function ended(endEvent: any): void { if (!endEvent.active) { graph.simulation.alphaTarget(0); } node.fx = null; node.fy = null; } } d3element.call(d3.drag().on('start', started)); } addGradientDef( element: SVGElement, x1: number, x2: number, y1: number, y2: number, points: GradientPoint[], id = 'gradient', ) { // Find or create defs for const svg = d3.select(element); if (svg.select('defs').empty()) { svg.append('defs'); } const defs = svg.select('defs'); console.log(defs); // Define const gradient = defs .append('linearGradient') .attr('id', id) .attr('x1', `${x1}%`) .attr('y1', `${y1}%`) .attr('x2', `${x2}%`) .attr('y2', `${y2}%`); points.forEach((point) => { gradient .append('stop') .style('stop-color', point.color) .style('stop-opacity', point.opacity) .attr('offset', `${point.offset}%`); }); console.log('added gradient url'); } // https://stackoverflow.com/questions/17883655/svg-shadow-cut-off addDropShadowFilter( element: SVGElement, dx = 1, dy = 1, stdDeviation = 4, feGaussianBlurIn = FeGaussianBlurIn.SourceAlpha, ): void { const svg = d3.select(element); if (svg.select('defs').empty()) { svg.append('defs'); } const defs = svg.select('defs'); console.log(defs); const filter = defs .append('filter') .attr('id', 'dropshadow') .attr('y', '-90%') // This positions the filter so that it starts before the element. Preventing cut-off lines .attr('x', '-90%') .attr('height', '300%') .attr('width', '300%'); filter .append('feGaussianBlur') .attr('in', feGaussianBlurIn) .attr('stdDeviation', stdDeviation) .attr('result', 'blur'); filter .append('feOffset') .attr('in', 'blur') .attr('dx', dx) .attr('dy', dy) .attr('result', 'offsetBlur'); const feMerge = filter.append('feMerge'); feMerge.append('feMergeNode').attr('in', 'offsetBlur'); feMerge.append('feMergeNode').attr('in', 'SourceGraphic'); } download(element: SVGElement): string { var html = (d3 .select(element) .attr('title', 'svg_title') .attr('version', 1.1) .attr('xmlns', 'http://www.w3.org/2000/svg') .node().parentNode as HTMLElement).innerHTML; return `data:image/svg+xml;base64,\n ${btoa(html)}`; } /** The interactable graph we will simulate in this article * This method does not interact with the document, purely physical calculations with d3 */ getNetworkGraph(nodes: Node[], links: Link[] , options: NetworkGraphOptions): NetworkGraph { return new NetworkGraph(nodes, links, options); } raise(element): void { d3.select(element).raise(); } wrapAll(svgs, radius, yStart = 0, maxLines = 2, delimiters = /(\s+|_|-|,)/): void { const nodes = svgs.nodes(); let currentYPosition = null; for (let i = 0; i < nodes.length; i++) { let currentLines = 0; const nodeText = d3.select(nodes[i]); const fontSize = parseFloat(nodeText.attr('font-size')); const words = nodeText.text().trim().split(delimiters); const y = currentYPosition ? currentYPosition : yStart; const x = 0; const anchor = nodeText.attr('text-anchor'); let line = []; let tspan = nodeText .text(null) .append('tspan') .attr('x', x) .attr('y', y) .attr('text-anchor', anchor); for (const word of words) { line.push(word); tspan.text(line.join('')); // Calculates width of rectangle, formed by radius of node and height of text const effectiveWidth = Math.sqrt( Math.pow(radius, 2) - Math.pow(y + fontSize * (currentLines + 1), 2), ) * 2; if ( tspan.node().getBBox().width > effectiveWidth && (currentLines + 1 >= maxLines || line.length === 1) ) { if (line.length !== 1) { line.pop(); } tspan.text(line.join('')); this.dotting(tspan, line, effectiveWidth); break; } else if (tspan.node().getBBox().width > effectiveWidth) { currentLines++; line.pop(); tspan.text(line.join('')); line = [word]; currentYPosition = y + currentLines * fontSize + 0.5; tspan = nodeText .append('tspan') .attr('x', x) .attr('y', currentYPosition) .attr('anchor', anchor) .text(word); } } currentYPosition = y + (currentLines + 1) * fontSize + 0.5; } } dotting(svg, words, effectiveWidth: number) { const allWords = words; const ellipsis = svg.text('').append('tspan').attr('class', 'elip').text('...'); const width = effectiveWidth - ellipsis.node().getComputedTextLength(); const tspan = svg.insert('tspan', ':first-child').text(words.join('')); // Try the whole line // While it's too long, and we have words left, keep removing words or letters if (allWords.length === 1) { let word = allWords[0]; while (tspan.node().getComputedTextLength() > width) { word = word.slice(0, -1); tspan.text(word); } } else { while (tspan.node().getComputedTextLength() > width && allWords.length) { allWords.pop(); tspan.text(allWords.join('')); } } } } export enum FeGaussianBlurIn { SourceGraphic = 'SourceGraphic', SourceAlpha = 'SourceAlpha', BackgroundImage = 'BackgroundImage', BackgroundAlpha = 'BackgroundAlpha', FillPaint = 'FillPaint', StrokePaint = 'StrokePaint', } export interface GradientPoint { color: string; opacity: number; offset: number; }