UNPKG

ngx-relationship-visualiser

Version:

A D3 force-directed-graph, implemented in Typescript for Angular, generates a visualisation graph with customisable link lengths and multiple labels between nodes. The graph can handle new data that will update lines, nodes, links, and path labels. Whenev

1 lines 82.9 kB
{"version":3,"file":"ngx-relationship-visualiser.mjs","sources":["../../projects/ngx-relationship-visualiser-lib/lib/db/graphDatabase.ts","../../projects/ngx-relationship-visualiser-lib/lib/visualiser/services/visualiser-graph.service.ts","../../projects/ngx-relationship-visualiser-lib/lib/visualiser/services/dagre-layout.service.ts","../../projects/ngx-relationship-visualiser-lib/lib/visualiser/visualiser-graph/visualiser-graph.component.ts","../../projects/ngx-relationship-visualiser-lib/lib/visualiser/visualiser-graph/visualiser-graph.component.html","../../projects/ngx-relationship-visualiser-lib/lib/ngx-relationship-visualiser.module.ts","../../ngx-relationship-visualiser.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\nimport Dexie from 'dexie';\nimport { Data } from '../models/data.interface';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class DexieService extends Dexie {\n public graphData: Dexie.Table<Data, string>;\n\n constructor() {\n super('GraphDatabase');\n this.version(1).stores({\n graphData: 'dataId'\n });\n this.graphData = this.table('graphData');\n }\n\n async saveGraphData(data: Data): Promise<void> {\n await this.graphData.put(data);\n }\n\n async getGraphData(dataId: string): Promise<Data> {\n return await this.graphData.get(dataId);\n }\n\n async deleteGraphData(dataId: string): Promise<void> {\n await this.graphData.delete(dataId);\n }\n}\n","import { Injectable } from '@angular/core';\nimport * as d3 from 'd3';\nimport { ReplaySubject } from 'rxjs';\nimport { DexieService } from '../../db/graphDatabase';\n\n@Injectable({\n providedIn: 'root',\n})\nexport class VisualiserGraphService {\n constructor(private dexieService: DexieService) { }\n public links = [];\n public nodes = [];\n public gBrush = null;\n public brushMode = false;\n public brushing = false;\n public shiftKey;\n public extent = null;\n public zoom = false;\n public zoomToFit = false;\n public saveGraphData = new ReplaySubject();\n\n public update(data, element, zoom, zoomToFit) {\n const svg = d3.select(element);\n this.zoom = zoom;\n this.zoomToFit = zoomToFit;\n return this._update(d3, svg, data);\n }\n\n private ticked(link, node, edgepaths) {\n link.each(function (d, i, n) {\n // Total difference in x and y from source to target\n let diffX = d.target.x - d.source.x;\n let diffY = d.target.y - d.source.y;\n\n // Length of path from center of source node to center of target node\n let pathLength = Math.sqrt(diffX * diffX + diffY * diffY);\n\n // x and y distances from center to outside edge of target node\n let offsetX = (diffX * 40) / pathLength;\n let offsetY = (diffY * 40) / pathLength;\n\n d3.select(n[i])\n .attr('x1', d.source.x + offsetX)\n .attr('y1', d.source.y + offsetY)\n .attr('x2', d.target.x - offsetX)\n .attr('y2', d.target.y - offsetY);\n });\n\n node.attr('transform', function (d) {\n return `translate(${d.x}, ${d.y + 50})`;\n });\n\n // Sets a boundry for the nodes\n // node.attr('cx', function (d) {\n // return (d.x = Math.max(40, Math.min(900 - 15, d.x)));\n // });\n // node.attr('cy', function (d) {\n // return (d.y = Math.max(50, Math.min(600 - 40, d.y)));\n // });\n\n edgepaths.attr('d', function (d) {\n return `M ${d.source.x} ${d.source.y} L ${d.target.x} ${d.target.y}`;\n });\n\n edgepaths.attr('transform', function (d) {\n if (d.target.x < d.source.x) {\n const bbox = this.getBBox();\n const rx = bbox.x + bbox.width / 2;\n const ry = bbox.y + bbox.height / 2;\n return `rotate(180 ${rx} ${ry})`;\n } else {\n return 'rotate(0)';\n }\n });\n }\n\n private initDefinitions(svg) {\n const defs = svg.append('defs');\n\n function createMarker(id, refX, path) {\n defs\n .append('marker')\n .attr('id', id)\n .attr('viewBox', '-0 -5 10 10')\n .attr('refX', refX)\n .attr('refY', 0)\n .attr('orient', 'auto')\n .attr('markerWidth', 8)\n .attr('markerHeight', 8)\n .attr('xoverflow', 'visible')\n .append('svg:path')\n .attr('d', path)\n .attr('fill', '#b4b4b4')\n .style('stroke', 'none');\n }\n\n createMarker('arrowheadSource', 0, 'M 0,-5 L 10 ,0 L 0,5');\n createMarker('arrowheadTarget', 2, 'M 10 -5 L 0 0 L 10 5');\n\n return svg;\n }\n\n private forceSimulation(_d3, { width, height }) {\n return _d3\n .forceSimulation()\n .velocityDecay(0.1)\n .force(\n 'link',\n _d3\n .forceLink()\n .id(function (d) {\n return d.id;\n })\n .distance(500)\n .strength(1)\n )\n .force('charge', _d3.forceManyBody().strength(0.1))\n .force('center', _d3.forceCenter(width / 2, height / 2))\n .force('collision', _d3.forceCollide().radius(15));\n }\n\n private compareAndMarkNodesNew(nodes, old_nodes) {\n // Create a map of ids to node objects for the old_nodes array\n const oldMap = old_nodes.reduce((map, node) => {\n map[node.id] = node;\n return map;\n }, {});\n\n // Check each node in the nodes array to see if it's new or not\n nodes.forEach((node) => {\n if (!oldMap[node.id]) {\n // Node is new, mark it with the newItem property\n node.newItem = true;\n // Remove the dagre coordinates from new nodes so we can set a random one in view\n node.fx = null;\n node.fy = null;\n }\n });\n\n return nodes;\n }\n\n private removeNewItem(nodes) {\n for (const node of nodes) {\n if (node.hasOwnProperty('newItem')) {\n delete node.newItem;\n }\n }\n return nodes;\n }\n\n private randomiseNodePositions(nodeData, width, height) {\n let minDistance = 100;\n const availableSpace = width * height;\n let adjustedRequiredSpace = nodeData.length * minDistance * minDistance;\n\n if (adjustedRequiredSpace > availableSpace) {\n while (adjustedRequiredSpace > availableSpace && minDistance > 0) {\n minDistance -= 10;\n adjustedRequiredSpace = nodeData.length * minDistance * minDistance;\n }\n\n if (adjustedRequiredSpace > availableSpace) {\n throw new Error(\n 'Not enough space to accommodate all nodes without a fixed position.'\n );\n }\n }\n\n nodeData.forEach((node) => {\n if (node.fx === null && node.fy === null) {\n let currentMinDistance = minDistance;\n let canPlaceNode = false;\n\n while (!canPlaceNode && currentMinDistance > 0) {\n node.fx = crypto.getRandomValues(new Uint32Array(1))[0] % width;\n node.fy = crypto.getRandomValues(new Uint32Array(1))[0] % height;\n\n canPlaceNode = !nodeData.some((otherNode) => {\n if (\n otherNode.fx === null ||\n otherNode.fy === null ||\n otherNode === node\n ) {\n return false;\n }\n\n const dx = otherNode.fx - node.fx;\n const dy = otherNode.fy - node.fy;\n return Math.sqrt(dx * dx + dy * dy) < currentMinDistance;\n });\n\n if (!canPlaceNode) {\n currentMinDistance--;\n }\n }\n\n if (!canPlaceNode) {\n throw new Error(\n 'Not enough space to accommodate all nodes without a fixed position.'\n );\n }\n }\n });\n\n return nodeData;\n }\n\n public async _update(_d3, svg, data) {\n const { nodes, links } = data;\n this.nodes = nodes || [];\n this.links = links || [];\n\n // Disable the reset btn\n\n let saveBtn = document.getElementById('save_graph');\n saveBtn.setAttribute('disabled', 'true');\n // Width/Height of canvas\n const parentWidth = _d3.select('svg').node().parentNode.clientWidth;\n const parentHeight = _d3.select('svg').node().parentNode.clientHeight;\n\n // Check to see if nodes are in Dexie\n const oldData = await this.dexieService.getGraphData('nodes');\n const oldNodes = oldData ? oldData.nodes : [];\n if (Array.isArray(oldNodes)) {\n // Compare and set property for new nodes\n this.nodes = this.compareAndMarkNodesNew(nodes, oldNodes);\n // Remove old nodes from Dexie\n await this.dexieService.deleteGraphData('nodes');\n // Add new nodes to Dexie\n await this.dexieService.saveGraphData({ dataId: 'nodes', nodes: data.nodes, links: data.links });\n } else {\n // Add first set of nodes to Dexie\n await this.dexieService.saveGraphData({ dataId: 'nodes', nodes: data.nodes, links: data.links });\n }\n\n // If nodes don't have a fx/fy coordinate we generate a random one - dagre nodes without links and new nodes added to canvas have null coordinates by design\n this.nodes = this.randomiseNodePositions(\n this.nodes,\n parentWidth,\n parentHeight\n );\n\n // Getting parents lineStyle and adding it to child objects\n const relationshipsArray = this.links.map(\n ({ lineStyle, targetArrow, sourceArrow, relationships }) =>\n relationships.map((r) => ({\n parentLineStyle: lineStyle,\n parentSourceArrow: sourceArrow,\n parentTargetArrow: targetArrow,\n ...r,\n }))\n );\n\n // Adding dy value based on link number and position in parent\n relationshipsArray.map((linkRelationship) => {\n linkRelationship.map((linkObject, i) => {\n // dy increments of 15px\n linkObject['dy'] = 20 + i * 15;\n });\n });\n\n // IE11 does not like .flat\n this.links = relationshipsArray.reduce((acc, val) => acc.concat(val), []);\n\n d3.select('svg').append('g');\n\n // Zoom Start\n const zoomContainer = _d3.select('svg g');\n let currentZoom = d3.zoomTransform(d3.select('svg').node());\n const updateZoomLevel = () => {\n const currentScale = currentZoom.k;\n const maxScale = zoom.scaleExtent()[1];\n const zoomPercentage = ((currentScale - 0.5) / (maxScale - 0.5)) * 200;\n const zoomLevelDisplay = document.getElementById('zoom_level');\n const zoomLevelText = `Zoom: ${zoomPercentage.toFixed(0)}%`;\n const zoomInBtn = document.getElementById('zoom_in');\n const zoomOutBtn = document.getElementById('zoom_out');\n const zoomResetBtn = document.getElementById('zoom_reset');\n\n // It might not exist depending on the this.zoom boolean\n if (zoomResetBtn) {\n zoomResetBtn.setAttribute('disabled', 'true');\n }\n // Check if the zoom level has changed before updating the display / allows for panning without showing the zoom percentage\n if (zoomLevelDisplay && zoomLevelDisplay.innerHTML !== zoomLevelText) {\n zoomLevelDisplay.innerHTML = zoomLevelText;\n zoomLevelDisplay.style.opacity = '1';\n setTimeout(() => {\n if (zoomLevelDisplay) {\n zoomLevelDisplay.style.opacity = '0';\n }\n }, 2000);\n }\n // Disable the zoomInBtn if the zoom level is at 200%\n if (zoomInBtn) {\n if (zoomPercentage === 200) {\n zoomInBtn.setAttribute('disabled', 'true');\n } else {\n zoomInBtn.removeAttribute('disabled');\n }\n }\n // Disable the zoomOutBtn if the zoom level is at 0%\n if (zoomOutBtn) {\n if (zoomPercentage === 0) {\n zoomOutBtn.setAttribute('disabled', 'true');\n } else {\n zoomOutBtn.removeAttribute('disabled');\n }\n }\n // Disable the zoomResetBtn if the zoom level is at 100%\n if (zoomResetBtn) {\n if (zoomPercentage === 100) {\n zoomResetBtn.setAttribute('disabled', 'true');\n } else {\n zoomResetBtn.removeAttribute('disabled');\n }\n }\n };\n let zoomedInit;\n const zoomed = () => {\n const transform = d3.event.transform;\n zoomContainer.attr(\n 'transform',\n `translate(${transform.x}, ${transform.y}) scale(${transform.k})`\n );\n currentZoom = transform;\n zoomedInit = true;\n updateZoomLevel();\n };\n\n const zoom = d3\n .zoom()\n .scaleExtent([0.5, 1.5])\n .on('start', function () {\n d3.select(this).style('cursor', this.zoom ? null : 'grabbing');\n })\n .on('zoom', this.zoom ? zoomed : null)\n .on('end', function () {\n d3.select(this).style('cursor', 'grab');\n });\n svg\n .call(zoom)\n .style('cursor', 'grab')\n .on(this.zoom ? null : 'wheel.zoom', null)\n .on('dblclick.zoom', null);\n zoom.filter(() => !d3.event.shiftKey);\n\n // Zoom button controls\n d3.select('#zoom_in').on('click', function () {\n zoom.scaleBy(svg.transition().duration(750), 1.2);\n updateZoomLevel();\n });\n d3.select('#zoom_out').on('click', function () {\n zoom.scaleBy(svg.transition().duration(750), 0.8);\n updateZoomLevel();\n });\n d3.select('#zoom_reset').on('click', function () {\n zoom.scaleTo(svg.transition().duration(750), 1);\n updateZoomLevel();\n });\n // Zoom to fit function and Button\n const handleZoomToFit = () => {\n const nodeBBox = zoomContainer.node().getBBox();\n\n // Calculate scale and translate values to fit all nodes\n const padding = 30;\n const scaleX = (parentWidth - padding * 2) / nodeBBox.width;\n const scaleY = (parentHeight - padding * 2) / nodeBBox.height;\n const scale = Math.min(scaleX, scaleY, 1.0); // Restrict scale to a maximum of 1.0\n\n const translateX =\n -nodeBBox.x * scale + (parentWidth - nodeBBox.width * scale) / 2;\n const translateY =\n -nodeBBox.y * scale + (parentHeight - nodeBBox.height * scale) / 2;\n\n // Get the bounding box of all nodes\n const allNodes = zoomContainer.selectAll('.node-wrapper');\n const allNodesBBox = allNodes.nodes().reduce(\n (acc, node) => {\n const nodeBBox = node.getBBox();\n acc.x = Math.min(acc.x, nodeBBox.x);\n acc.y = Math.min(acc.y, nodeBBox.y);\n acc.width = Math.max(acc.width, nodeBBox.x + nodeBBox.width);\n acc.height = Math.max(acc.height, nodeBBox.y + nodeBBox.height);\n return acc;\n },\n { x: Infinity, y: Infinity, width: -Infinity, height: -Infinity }\n );\n\n // Check if all nodes are within the viewable container\n if (\n allNodesBBox.x * scale >= 0 &&\n allNodesBBox.y * scale >= 0 &&\n allNodesBBox.width * scale <= parentWidth &&\n allNodesBBox.height * scale <= parentHeight\n ) {\n // All nodes are within the viewable container, no need to apply zoom transform\n return;\n }\n\n // Manually reset the zoom transform\n zoomContainer\n .transition()\n .duration(750)\n .attr('transform', 'translate(0, 0) scale(1)');\n\n // Apply zoom transform to zoomContainer\n zoomContainer\n .transition()\n .duration(750)\n .attr(\n 'transform',\n `translate(${translateX}, ${translateY}) scale(${scale})`\n );\n\n // Update the currentZoom variable with the new transform\n // zoomedInit - created because if zoomToFit is called before anything else it screws up the base transform - e.g. showCurrentMatch\n if (zoomedInit) {\n currentZoom.x = translateX;\n currentZoom.y = translateY;\n currentZoom.k = scale;\n }\n updateZoomLevel();\n };\n\n d3.select('#zoom_to_fit').on('click', handleZoomToFit);\n\n // Check if zoom level is at 0% or 100% before allowing mousewheel zoom - this stabilises the canvas when the limit is reached\n svg.on('wheel', () => {\n const currentScale = currentZoom.k;\n const maxScale = zoom.scaleExtent()[1];\n const minScale = zoom.scaleExtent()[0];\n if (currentScale === maxScale || currentScale === minScale) {\n d3.event.preventDefault();\n }\n });\n\n // Zoom End\n\n // For arrows\n this.initDefinitions(svg);\n\n const simulation = this.forceSimulation(_d3, {\n width: +svg.attr('width'),\n height: +svg.attr('height'),\n });\n\n // Brush Start\n let gBrushHolder = svg.append('g');\n\n let brush = d3\n .brush()\n .on('start', () => {\n this.brushing = true;\n\n nodeEnter.each((d) => {\n d.previouslySelected = this.shiftKey && d.selected;\n });\n\n _d3\n .selectAll('.edgelabel')\n .style('font-weight', 400)\n .style('fill', '#212529');\n\n // Apply styling for the rect.selection\n d3.selectAll('rect.selection')\n .style('stroke', 'none')\n .style('fill', 'steelblue')\n .style('fill-opacity', 0.4);\n })\n .on('brush', () => {\n this.extent = d3.event.selection;\n if (!d3.event.sourceEvent || !this.extent || !this.brushMode) return;\n if (!currentZoom) return;\n\n nodeEnter\n .classed('selected', (d) => {\n return (d.selected =\n d.previouslySelected ^\n (<any>(\n (d3.event.selection[0][0] <=\n d.x * currentZoom.k + currentZoom.x &&\n d.x * currentZoom.k + currentZoom.x <\n d3.event.selection[1][0] &&\n d3.event.selection[0][1] <=\n d.y * currentZoom.k + currentZoom.y &&\n d.y * currentZoom.k + currentZoom.y <\n d3.event.selection[1][1])\n )));\n })\n .select('.nodeText')\n .classed('selected', (d) => d.selected)\n .style('fill', (d) => (d.selected ? 'blue' : '#999'))\n .style('font-weight', (d) => (d.selected ? 700 : 400));\n\n this.extent = d3.event.selection;\n })\n .on('end', () => {\n if (!d3.event.sourceEvent || !this.extent || !this.gBrush) return;\n\n this.gBrush.call(brush.move, null);\n if (!this.brushMode) {\n // the shift key has been release before we ended our brushing\n this.gBrush.remove();\n this.gBrush = null;\n }\n this.brushing = false;\n\n nodeEnter\n .select('.nodeText')\n .filter(function () {\n return !d3.select(this.parentNode).classed('selected');\n })\n .style('fill', '#212529')\n .style('font-weight', 400);\n });\n\n let keyup = () => {\n this.shiftKey = false;\n this.brushMode = false;\n if (this.gBrush && !this.brushing) {\n // only remove the brush if we're not actively brushing\n // otherwise it'll be removed when the brushing ends\n this.gBrush.remove();\n this.gBrush = null;\n }\n };\n\n let keydown = () => {\n // Allows us to turn off default listeners for keyModifiers(shift)\n brush.filter(() => d3.event.shiftKey);\n brush.keyModifiers(false);\n // holding shift key\n if (d3.event.keyCode === 16) {\n this.shiftKey = true;\n\n if (!this.gBrush) {\n this.brushMode = true;\n this.gBrush = gBrushHolder.append('g').attr('class', 'brush');\n this.gBrush.call(brush);\n }\n }\n };\n\n d3.select('body').on('keydown', keydown).on('keyup', keyup);\n // Brush End\n\n const filteredLine = this.links.filter(\n ({ source, target }, index, linksArray) => {\n // Filter out any objects that have matching source and target property values\n // To display only one line (parentLineStyle) - removes html bloat and a darkened line\n return (\n index ===\n linksArray.findIndex(\n (obj) => obj.source === source && obj.target === target\n )\n );\n }\n );\n\n const link = zoomContainer.selectAll().data(filteredLine, function (d) {\n return d.id;\n });\n\n zoomContainer.selectAll('line').data(link).exit().remove();\n\n const linkEnter = link\n .join('line')\n .style('stroke', function (d) {\n if (d.parentLineStyle === 'Solid') {\n return '#777';\n } else {\n return '#b4b4b4';\n }\n })\n .style('stroke-opacity', '.6')\n .style('stroke-dasharray', function (d) {\n if (d.parentLineStyle === 'Dotted') {\n return '8,5';\n }\n return null;\n })\n .style('stroke-width', '2px')\n .attr('class', 'link')\n .attr('id', function (d) {\n const suffix = '_line';\n const source = d.source ? d.source : '';\n const target = d.target ? d.target : '';\n return `${source}_${target}${suffix}`;\n })\n .attr('marker-end', function (d) {\n if (d.parentTargetArrow === true) {\n return 'url(#arrowheadSource)';\n }\n return null;\n })\n .attr('marker-start', function (d) {\n if (d.parentSourceArrow === true) {\n return 'url(#arrowheadTarget)';\n }\n return null;\n });\n\n link.append('title').text(function (d) {\n return d.label;\n });\n\n const edgepaths = zoomContainer.selectAll().data(this.links, function (d) {\n return d.id;\n });\n\n zoomContainer.selectAll('path').data(edgepaths).exit().remove();\n\n const edgepathsEnter = edgepaths\n .join('svg:path')\n .attr('class', 'edgepath')\n .attr('fill-opacity', 0)\n .attr('stroke-opacity', 0)\n .attr('id', function (d, i) {\n return 'edgepath' + i;\n });\n\n const edgelabels = zoomContainer.selectAll().data(this.links, function (d) {\n return d.id;\n });\n\n zoomContainer.selectAll('text').data(edgelabels).exit().remove();\n\n const edgelabelsEnter = edgelabels\n .enter()\n .append('text')\n .attr('class', 'edgelabel')\n .attr('id', function (d) {\n const suffix = '_text';\n const source = d.source ? d.source : '';\n const target = d.target ? d.target : '';\n return `${source}_${target}${suffix}`;\n })\n .style('text-anchor', 'middle')\n .attr('font-size', 14)\n .attr('dy', function (d) {\n return d.dy;\n });\n\n\n edgelabelsEnter\n .append('textPath')\n .attr('xlink:href', function (d, i) {\n return '#edgepath' + i;\n })\n .style('cursor', 'pointer')\n .attr('dominant-baseline', 'bottom')\n .attr('startOffset', '50%')\n .text(function (d) {\n return d.label;\n });\n\n edgelabelsEnter\n .selectAll('textPath')\n .filter(function (d) {\n return d.linkIcon;\n })\n .append('tspan')\n .style('fill', '#856404')\n .style('font-weight', '700')\n .attr('class', 'fa')\n .text(' \\uf0c1');\n // on normal label link click - highlight labels\n svg.selectAll('.edgelabel').on('click', function (d) {\n _d3.event.stopPropagation();\n nodeEnter.each(function (d) {\n d.selected = false;\n d.previouslySelected = false;\n });\n node.classed('selected', false);\n _d3\n .selectAll('.nodeText')\n .style('fill', '#212529')\n .style('font-weight', 400);\n _d3\n .selectAll('.edgelabel')\n .style('font-weight', 400)\n .style('fill', '#212529');\n _d3.select(this).style('fill', 'blue').style('font-weight', 700);\n });\n\n const node = zoomContainer.selectAll().data(this.nodes, function (d) {\n return d.id;\n });\n\n zoomContainer.selectAll('g').data(node).exit().remove();\n\n const nodeEnter = node\n .join('g')\n .call(\n _d3\n .drag()\n .on('start', function dragstarted(d) {\n // Enable the save & reset btn\n document.getElementById('save_graph').removeAttribute('disabled');\n\n if (!_d3.event.active) simulation.alphaTarget(0.9).restart();\n\n if (!d.selected && !this.shiftKey) {\n // if this node isn't selected, then we have to unselect every other node\n nodeEnter.classed('selected', function (p) {\n return (p.selected = p.previouslySelected = false);\n });\n // remove the selected styling on other nodes and labels when we drag a non-selected node\n _d3\n .selectAll('.edgelabel')\n .style('fill', '#212529')\n .style('font-weight', 400);\n _d3\n .selectAll('.nodeText')\n .style('font-weight', 400)\n .style('fill', '#212529');\n }\n\n _d3.select(this).classed('selected', function (p) {\n d.previouslySelected = d.selected;\n return (d.selected = true);\n });\n\n nodeEnter\n .filter(function (d) {\n return d.selected;\n })\n .each(function (d) {\n d.fx = d.x;\n d.fy = d.y;\n });\n })\n .on('drag', function dragged(d) {\n nodeEnter\n .filter(function (d) {\n return d.selected;\n })\n .each(function (d) {\n d.fx += _d3.event.dx;\n d.fy += _d3.event.dy;\n });\n })\n .on('end', function dragended(d) {\n if (!_d3.event.active) {\n simulation.alphaTarget(0.3).restart();\n }\n d.fx = d.x;\n d.fy = d.y;\n // Subscribes to updated graph positions for save\n self.saveGraphData.next(data);\n })\n )\n .attr('class', 'node-wrapper')\n .attr('id', function (d) {\n return d.id;\n });\n\n // no collision - already using this in statement\n const self = this;\n\n // node click and ctrl + click\n svg.selectAll('.node-wrapper').on('click', function (d) {\n // so we don't activate the canvas .click event\n _d3.event.stopPropagation();\n\n // setting the select attribute to the object on single select so we can drag them\n d.selected = true;\n\n svg.selectAll('.node-wrapper').classed('selected', false);\n d3.select(this).classed('selected', true);\n nodeEnter.each(function (d) {\n d.selected = false;\n d.previouslySelected = false;\n });\n\n // remove style from selected node before the class is removed\n _d3\n .selectAll('.selected')\n .selectAll('.nodeText')\n .style('fill', '#212529');\n // Remove styles from all other nodes and labels on single left click\n _d3.selectAll('.edgelabel').style('fill', '#212529');\n _d3\n .selectAll('.nodeText')\n .style('fill', '#212529')\n .style('font-weight', 400);\n // Add style on single left click\n _d3\n .select(this)\n .select('.nodeText')\n .style('fill', 'blue')\n .style('font-weight', 700);\n\n return null;\n });\n\n //click on canvas to remove selected nodes\n _d3.select('svg').on('click', () => {\n nodeEnter.each(function (d) {\n d.selected = false;\n d.previouslySelected = false;\n });\n node.classed('selected', false);\n _d3\n .selectAll('.selected')\n .selectAll('.nodeText')\n .style('fill', '#212529')\n .style('font-weight', 400);\n _d3.selectAll('.selected').classed('selected', false);\n _d3\n .selectAll('.edgelabel')\n .style('fill', '#212529')\n .style('font-weight', 400);\n });\n\n nodeEnter\n .filter(function (d) {\n if (!d.imageUrl || d.imageUrl === '') {\n return null;\n }\n return true;\n })\n .append('image')\n .attr('xlink:href', function (d) {\n return d.imageUrl;\n })\n .attr('x', -15)\n .attr('y', -60)\n .attr('class', function (d) {\n const suffix = 'image';\n const id = d.id ? d.id : '';\n return `${id}_${suffix}`;\n })\n .attr('id', function (d) {\n const id = d.id ? d.id : '';\n return `${id}`;\n })\n .attr('width', 32)\n .attr('height', 32)\n .attr('class', 'image')\n .style('cursor', 'pointer');\n\n nodeEnter\n .filter(function (d) {\n if (!d.icon || d.icon === '') {\n return null;\n }\n return true;\n })\n .append('text')\n .text(function (d) {\n return d.icon;\n })\n .attr('x', -18)\n .attr('y', -30)\n .attr('class', function (d) {\n const suffix = 'icon';\n const id = d.id ? d.id : '';\n return `${id}_${suffix} fa`;\n })\n .attr('id', function (d) {\n const id = d.id ? d.id : '';\n return `${id}`;\n })\n .style('font-size', '35px')\n .style('cursor', 'pointer');\n\n const nodeText = nodeEnter\n .append('text')\n .attr('id', 'nodeText')\n .attr('class', 'nodeText')\n .style('text-anchor', 'middle')\n .style('cursor', 'pointer')\n .attr('dy', -3)\n .attr('y', -25)\n .attr('testhook', function (d) {\n const suffix = 'text';\n const id = d.id ? d.id : '';\n return `${id}_${suffix}`;\n });\n\n nodeText\n .selectAll('tspan.text')\n .data((d) => d.label)\n .enter()\n .append('tspan')\n .attr('class', 'nodeTextTspan')\n .text((d) => d)\n .style('font-size', '14px')\n .attr('x', -10)\n .attr('dx', 10)\n .attr('dy', 15);\n\n nodeEnter\n .filter(function (d) {\n if (!d.additionalIcon) {\n return null;\n }\n return true;\n })\n .append('text')\n .attr('id', function (d) {\n const suffix = 'additionalIcon';\n const id = d.id ? d.id : '';\n return `${id}_${suffix}`;\n })\n .attr('width', 100)\n .attr('height', 25)\n .attr('x', 30)\n .attr('y', -50)\n .attr('class', 'fa')\n .style('fill', '#856404')\n .text(function (d) {\n return d.additionalIcon;\n });\n\n // transition effects for new pulsating nodes\n nodeEnter\n .filter(function (d) {\n if (!d.newItem) {\n return null;\n }\n return true;\n })\n .select('text')\n .transition()\n .duration(1000)\n .attr('fill', 'blue')\n .attr('font-weight', 700)\n .attr('fill-opacity', 1)\n .transition()\n .duration(1000)\n .attr('fill', 'blue')\n .attr('font-weight', 700)\n .attr('fill-opacity', 0.1)\n .transition()\n .duration(1000)\n .attr('fill', 'blue')\n .attr('font-weight', 700)\n .attr('fill-opacity', 1)\n .transition()\n .duration(1000)\n .attr('fill', 'blue')\n .attr('font-weight', 700)\n .attr('fill-opacity', 0.1)\n .transition()\n .duration(1000)\n .attr('fill', 'blue')\n .attr('font-weight', 700)\n .attr('fill-opacity', 1)\n .transition()\n .duration(1000)\n .attr('fill', '#212529')\n .attr('font-weight', 400)\n .attr('fill-opacity', 1)\n .on('end', function () {\n return d3.select(this).call(_d3.transition);\n });\n\n nodeEnter\n .filter(function (d) {\n if (!d.newItem) {\n return null;\n }\n return true;\n })\n .select('image')\n .transition()\n .duration(1000)\n .attr('width', 45)\n .attr('height', 45)\n .transition()\n .duration(1000)\n .attr('width', 32)\n .attr('height', 32)\n .transition()\n .duration(1000)\n .attr('width', 45)\n .attr('height', 45)\n .transition()\n .duration(1000)\n .attr('width', 32)\n .attr('height', 32)\n .transition()\n .duration(1000)\n .attr('width', 45)\n .attr('height', 45)\n .transition()\n .duration(1000)\n .attr('width', 32)\n .attr('height', 32)\n .on('end', function () {\n return d3.select(this).call(d3.transition);\n });\n\n // Remove the newClass so they don't animate next time\n this.nodes = this.removeNewItem(this.nodes);\n\n nodeEnter.append('title').text(function (d) {\n return d.label;\n });\n\n const maxTicks = 30;\n let tickCount = 0;\n let zoomToFitCalled = false;\n\n simulation.nodes(this.nodes).on('tick', () => {\n if (this.zoomToFit && tickCount >= maxTicks && !zoomToFitCalled) {\n simulation.stop();\n handleZoomToFit();\n zoomToFitCalled = true;\n } else {\n this.ticked(linkEnter, nodeEnter, edgepathsEnter);\n tickCount++;\n }\n });\n\n simulation.force('link').links(this.links);\n self.saveGraphData.next(data);\n }\n\n public resetGraph(initialData, element, zoom, zoomToFit) {\n // Reset the data to its initial state\n this.nodes = [];\n this.links = [];\n // Call the update method again to re-simulate the graph with the new data\n this.update(initialData, element, zoom, zoomToFit);\n }\n}\n","import * as dagre from '@dagrejs/dagre';\r\nimport { Injectable } from '@angular/core';\r\n\r\nexport enum Orientation {\r\n LEFT_TO_RIGHT = 'LR',\r\n RIGHT_TO_LEFT = 'RL',\r\n TOP_TO_BOTTOM = 'TB',\r\n BOTTOM_TO_TOP = 'BT',\r\n}\r\n\r\nexport enum Alignment {\r\n CENTER = 'C',\r\n UP_LEFT = 'UL',\r\n UP_RIGHT = 'UR',\r\n DOWN_LEFT = 'DL',\r\n DOWN_RIGHT = 'DR',\r\n}\r\n\r\nexport interface DagreSettings {\r\n orientation?: Orientation;\r\n marginX?: number;\r\n marginY?: number;\r\n edgePadding?: number;\r\n rankPadding?: number;\r\n nodePadding?: number;\r\n align?: Alignment;\r\n acyclicer?: 'greedy';\r\n ranker?: 'network-simplex' | 'tight-tree' | 'longest-path';\r\n multigraph?: boolean;\r\n compound?: boolean;\r\n}\r\n\r\n@Injectable({\r\n providedIn: 'root',\r\n})\r\nexport class DagreNodesOnlyLayout {\r\n defaultSettings = {\r\n orientation: Orientation.LEFT_TO_RIGHT,\r\n marginX: 70,\r\n marginY: 70,\r\n edgePadding: 200,\r\n rankPadding: 300,\r\n nodePadding: 100,\r\n curveDistance: 20,\r\n multigraph: true,\r\n compound: true,\r\n align: Alignment.UP_RIGHT,\r\n acyclicer: undefined,\r\n ranker: 'network-simplex',\r\n };\r\n\r\n dagreGraph: any;\r\n dagreNodes: any;\r\n dagreEdges: any;\r\n\r\n public renderLayout(graph) {\r\n this.createDagreGraph(graph);\r\n dagre.layout(this.dagreGraph);\r\n\r\n for (const dagreNodeId in this.dagreGraph._nodes) {\r\n const dagreNode = this.dagreGraph._nodes[dagreNodeId];\r\n const node = graph.nodes.find((n) => n.id === dagreNode.id);\r\n\r\n if (node.fx === null && node.fy === null) {\r\n // Check if the node has any associated edges\r\n const hasAssociatedEdges = graph.links.some(\r\n (link) => link.source === dagreNode.id || link.target === dagreNode.id\r\n );\r\n if (hasAssociatedEdges) {\r\n node.fx = dagreNode.x;\r\n node.fy = dagreNode.y;\r\n }\r\n }\r\n\r\n node.dimension = {\r\n width: dagreNode.width,\r\n height: dagreNode.height,\r\n };\r\n }\r\n\r\n return graph;\r\n }\r\n\r\n public initRenderLayout(graph) {\r\n this.createDagreGraph(graph);\r\n dagre.layout(this.dagreGraph);\r\n let minFy = Infinity;\r\n for (const dagreNodeId in this.dagreGraph._nodes) {\r\n const dagreNode = this.dagreGraph._nodes[dagreNodeId];\r\n const node = graph.nodes.find((n) => n.id === dagreNode.id);\r\n\r\n // Check if the node has any associated edges\r\n const hasAssociatedEdges = graph.links.some(\r\n (link) => link.source === dagreNode.id || link.target === dagreNode.id\r\n );\r\n\r\n if (hasAssociatedEdges) {\r\n node.fx = dagreNode.x;\r\n node.fy = dagreNode.y;\r\n minFy = Math.min(minFy, dagreNode.y - this.defaultSettings.marginY);\r\n } else {\r\n // Give them a null value to later random position them only when the layout button is pressed so they come into view\r\n node.fx = null;\r\n node.fy = null;\r\n }\r\n\r\n node.dimension = {\r\n width: dagreNode.width,\r\n height: dagreNode.height,\r\n };\r\n }\r\n\r\n // Adjust the fy values to start from 0 if there are associated edges\r\n if (minFy !== Infinity) {\r\n for (const node of graph.nodes) {\r\n if (node.fy !== null) {\r\n node.fy -= minFy;\r\n }\r\n }\r\n }\r\n\r\n return graph;\r\n }\r\n\r\n public createDagreGraph(graph): any {\r\n const settings = Object.assign({}, this.defaultSettings);\r\n this.dagreGraph = new dagre.graphlib.Graph({\r\n compound: settings.compound,\r\n multigraph: settings.multigraph,\r\n });\r\n this.dagreGraph.setGraph({\r\n rankdir: settings.orientation,\r\n marginx: settings.marginX,\r\n marginy: settings.marginY,\r\n edgesep: settings.edgePadding,\r\n ranksep: settings.rankPadding,\r\n nodesep: settings.nodePadding,\r\n align: settings.align,\r\n acyclicer: settings.acyclicer,\r\n ranker: settings.ranker,\r\n multigraph: settings.multigraph,\r\n compound: settings.compound,\r\n });\r\n\r\n this.dagreNodes = graph.nodes.map((n) => {\r\n const node: any = Object.assign({}, n);\r\n node.width = 20;\r\n node.height = 20;\r\n node.x = n.fx;\r\n node.y = n.fy;\r\n return node;\r\n });\r\n\r\n this.dagreEdges = graph.links.map((l) => {\r\n const newLink: any = Object.assign({}, l);\r\n return newLink;\r\n });\r\n\r\n for (const node of this.dagreNodes) {\r\n if (!node.width) {\r\n node.width = 20;\r\n }\r\n if (!node.height) {\r\n node.height = 30;\r\n }\r\n\r\n // update dagre\r\n this.dagreGraph.setNode(node.id, node);\r\n }\r\n\r\n // update dagre\r\n for (const edge of this.dagreEdges) {\r\n if (settings.multigraph) {\r\n this.dagreGraph.setEdge(edge.source, edge.target, edge, edge.linkId);\r\n } else {\r\n this.dagreGraph.setEdge(edge.source, edge.target);\r\n }\r\n }\r\n\r\n return this.dagreGraph;\r\n }\r\n}\r\n","import {\n Component,\n ViewChild,\n ElementRef,\n Input,\n Output,\n EventEmitter,\n OnInit\n} from '@angular/core';\nimport { VisualiserGraphService } from '../services/visualiser-graph.service';\nimport { DagreNodesOnlyLayout } from '../services/dagre-layout.service';\nimport { Data } from '../../models/data.interface';\nimport { DexieService } from '../../db/graphDatabase';\nimport Swal from 'sweetalert2'\n\n\n@Component({\n selector: 'visualiser-graph',\n templateUrl: \"./visualiser-graph.component.html\",\n styleUrls: [\"./visualiser-graph.component.scss\"],\n})\nexport class VisualiserGraphComponent implements OnInit {\n @ViewChild('svgId') graphElement: ElementRef;\n @Output() saveGraphDataEvent = new EventEmitter<any>();\n public saveGraphData;\n\n public width;\n public savedGraphData: Data;\n public buttonBarRightPosition: string;\n\n @Input() zoom: boolean = true;\n @Input() zoomToFit: boolean = false;\n constructor(\n readonly visualiserGraphService: VisualiserGraphService,\n readonly dagreNodesOnlyLayout: DagreNodesOnlyLayout,\n private dexieService: DexieService\n ) { }\n\n @Input()\n set data(data: Data) {\n if (!data || !data.dataId) {\n console.error('Invalid data input');\n return;\n }\n\n this.savedGraphData = data;\n\n // Timeout: The input arrives before the svg is rendered, therefore the nativeElement does not exist\n setTimeout(async () => {\n this.dagreNodesOnlyLayout.renderLayout(data);\n // Take a copy of input for reset\n await this.dexieService.saveGraphData(data);\n\n this.visualiserGraphService.update(\n data,\n this.graphElement.nativeElement,\n this.zoom,\n this.zoomToFit\n );\n }, 500);\n }\n\n public ngOnInit() {\n this.updateWidth();\n this.buttonBarRightPosition = '0';\n // Initialize with default empty data if no data is provided\n if (!this.savedGraphData) {\n console.warn('No data provided, using empty data set');\n this.data = {\n dataId: '1',\n nodes: [],\n links: [],\n };\n }\n }\n\n public onResize(event): void {\n this.updateWidth();\n }\n\n public updateWidth(): void {\n this.width = document.getElementById('pageId').offsetWidth;\n }\n\n public async onConfirmSave(): Promise<void> {\n if (!this.savedGraphData) {\n console.error('savedGraphData is not set');\n return;\n }\n\n Swal.fire({\n title: \"Save Graph\",\n text: \"You won't be able to revert this!\",\n icon: \"warning\",\n showCancelButton: true,\n confirmButtonColor: \"#3085d6\",\n cancelButtonColor: \"#d33\",\n confirmButtonText: \"Save\"\n }).then((result) => {\n if (result.isConfirmed) {\n this.visualiserGraphService.saveGraphData.subscribe((saveGraphData) => {\n this.saveGraphData = saveGraphData;\n });\n\n this.saveGraphDataEvent.emit(this.saveGraphData);\n\n this.disableButtons(true);\n this.data = this.saveGraphData;\n\n Swal.fire({\n title: \"Saved!\",\n text: \"Your graph has been saved.\",\n icon: \"success\"\n });\n }\n });\n }\n private disableButtons(disabled: boolean): void {\n document.querySelectorAll('#save_graph, #reset_graph').forEach(btn => {\n btn.setAttribute('disabled', String(disabled));\n });\n }\n}","<div class=\"page\" id=\"pageId\" (window:resize)=\"onResize($event)\">\n <div class=\"buttonBar\" [style.right]=\"buttonBarRightPosition\">\n <div class=\"d-flex justify-content-end\">\n <div class=\"btn-group\" role=\"group\" aria-label=\"Controls\">\n <button type=\"button\" id=\"save_graph\" class=\"m-3 btn btn-secondary\" data-toggle=\"tooltip\" data-placement=\"top\"\n title=\"Save data\" (click)=\"onConfirmSave()\">\n <i class=\"bi bi-save\"></i>\n </button>\n </div>\n </div>\n </div>\n <!-- Zoom indicator-->\n <div *ngIf=\"zoom\" class=\"zoomIndicator\">\n <span id=\"zoom_level\"></span>\n </div>\n <svg #svgId [attr.width]=\"width\" height=\"780\"></svg>\n</div>","import { NgModule } from '@angular/core';\nimport { BrowserModule } from '@angular/platform-browser';\nimport { VisualiserGraphComponent } from './visualiser/visualiser-graph/visualiser-graph.component';\n\n@NgModule({\n declarations: [\n VisualiserGraphComponent\n ],\n imports: [\n BrowserModule\n ],\n exports: [VisualiserGraphComponent]\n})\n\nexport class NgxRelationshipVisualiserModule { }\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public_api';\n"],"names":["i1.DexieService","i1.VisualiserGraphService","i2.DagreNodesOnlyLayout","i3.DexieService"],"mappings":"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