UNPKG

@swimlane/ngx-graph

Version:
1,575 lines (1,559 loc) 109 kB
import * as i0 from '@angular/core'; import { EventEmitter, Directive, Output, Injectable, HostListener, Component, ViewEncapsulation, ChangeDetectionStrategy, Input, ContentChild, ViewChildren, NgModule } from '@angular/core'; import * as i2 from '@angular/common'; import { CommonModule } from '@angular/common'; import { __decorate } from 'tslib'; import { trigger, transition, style, animate } from '@angular/animations'; import { select } from 'd3-selection'; import * as shape from 'd3-shape'; import * as ease from 'd3-ease'; import 'd3-transition'; import { Subject, Subscription, Observable, of, fromEvent } from 'rxjs'; import { takeUntil, debounceTime } from 'rxjs/operators'; import { identity, transform, translate, scale, toSVG, smoothMatrix } from 'transformation-matrix'; import { scaleOrdinal } from 'd3-scale'; import * as dagre from 'dagre'; import * as d3Force from 'd3-force'; import { forceSimulation, forceManyBody, forceCollide, forceLink } from 'd3-force'; import { d3adaptor } from 'webcola'; import * as d3Dispatch from 'd3-dispatch'; import * as d3Timer from 'd3-timer'; const cache = {}; /** * Generates a short id. * */ function id() { let newId = ('0000' + ((Math.random() * Math.pow(36, 4)) << 0).toString(36)).slice(-4); newId = `a${newId}`; // ensure not already used if (!cache[newId]) { cache[newId] = true; return newId; } return id(); } var PanningAxis; (function (PanningAxis) { PanningAxis["Both"] = "both"; PanningAxis["Horizontal"] = "horizontal"; PanningAxis["Vertical"] = "vertical"; })(PanningAxis || (PanningAxis = {})); var MiniMapPosition; (function (MiniMapPosition) { MiniMapPosition["UpperLeft"] = "UpperLeft"; MiniMapPosition["UpperRight"] = "UpperRight"; })(MiniMapPosition || (MiniMapPosition = {})); /** * Throttle a function * * @export * @param {*} func * @param {number} wait * @param {*} [options] * @returns */ function throttle(context, func, wait, options) { options = options || {}; let args; let result; let timeout = null; let previous = 0; function later() { previous = options.leading === false ? 0 : +new Date(); timeout = null; result = func.apply(context, args); } return function (..._arguments) { const now = +new Date(); if (!previous && options.leading === false) { previous = now; } const remaining = wait - (now - previous); args = _arguments; if (remaining <= 0) { clearTimeout(timeout); timeout = null; previous = now; result = func.apply(context, args); } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; } /** * Throttle decorator * * class MyClass { * throttleable(10) * myFn() { ... } * } * * @export * @param {number} duration * @param {*} [options] * @returns */ function throttleable(duration, options) { return function innerDecorator(target, key, descriptor) { return { configurable: true, enumerable: descriptor.enumerable, get: function getter() { Object.defineProperty(this, key, { configurable: true, enumerable: descriptor.enumerable, value: throttle(this, descriptor.value, duration, options) }); return this[key]; } }; }; } const colorSets = [ { name: 'vivid', selectable: true, group: 'Ordinal', domain: [ '#647c8a', '#3f51b5', '#2196f3', '#00b862', '#afdf0a', '#a7b61a', '#f3e562', '#ff9800', '#ff5722', '#ff4514' ] }, { name: 'natural', selectable: true, group: 'Ordinal', domain: [ '#bf9d76', '#e99450', '#d89f59', '#f2dfa7', '#a5d7c6', '#7794b1', '#afafaf', '#707160', '#ba9383', '#d9d5c3' ] }, { name: 'cool', selectable: true, group: 'Ordinal', domain: [ '#a8385d', '#7aa3e5', '#a27ea8', '#aae3f5', '#adcded', '#a95963', '#8796c0', '#7ed3ed', '#50abcc', '#ad6886' ] }, { name: 'fire', selectable: true, group: 'Ordinal', domain: ['#ff3d00', '#bf360c', '#ff8f00', '#ff6f00', '#ff5722', '#e65100', '#ffca28', '#ffab00'] }, { name: 'solar', selectable: true, group: 'Continuous', domain: [ '#fff8e1', '#ffecb3', '#ffe082', '#ffd54f', '#ffca28', '#ffc107', '#ffb300', '#ffa000', '#ff8f00', '#ff6f00' ] }, { name: 'air', selectable: true, group: 'Continuous', domain: [ '#e1f5fe', '#b3e5fc', '#81d4fa', '#4fc3f7', '#29b6f6', '#03a9f4', '#039be5', '#0288d1', '#0277bd', '#01579b' ] }, { name: 'aqua', selectable: true, group: 'Continuous', domain: [ '#e0f7fa', '#b2ebf2', '#80deea', '#4dd0e1', '#26c6da', '#00bcd4', '#00acc1', '#0097a7', '#00838f', '#006064' ] }, { name: 'flame', selectable: false, group: 'Ordinal', domain: [ '#A10A28', '#D3342D', '#EF6D49', '#FAAD67', '#FDDE90', '#DBED91', '#A9D770', '#6CBA67', '#2C9653', '#146738' ] }, { name: 'ocean', selectable: false, group: 'Ordinal', domain: [ '#1D68FB', '#33C0FC', '#4AFFFE', '#AFFFFF', '#FFFC63', '#FDBD2D', '#FC8A25', '#FA4F1E', '#FA141B', '#BA38D1' ] }, { name: 'forest', selectable: false, group: 'Ordinal', domain: [ '#55C22D', '#C1F33D', '#3CC099', '#AFFFFF', '#8CFC9D', '#76CFFA', '#BA60FB', '#EE6490', '#C42A1C', '#FC9F32' ] }, { name: 'horizon', selectable: false, group: 'Ordinal', domain: [ '#2597FB', '#65EBFD', '#99FDD0', '#FCEE4B', '#FEFCFA', '#FDD6E3', '#FCB1A8', '#EF6F7B', '#CB96E8', '#EFDEE0' ] }, { name: 'neons', selectable: false, group: 'Ordinal', domain: [ '#FF3333', '#FF33FF', '#CC33FF', '#0000FF', '#33CCFF', '#33FFFF', '#33FF66', '#CCFF33', '#FFCC00', '#FF6600' ] }, { name: 'picnic', selectable: false, group: 'Ordinal', domain: [ '#FAC51D', '#66BD6D', '#FAA026', '#29BB9C', '#E96B56', '#55ACD2', '#B7332F', '#2C83C9', '#9166B8', '#92E7E8' ] }, { name: 'night', selectable: false, group: 'Ordinal', domain: [ '#2B1B5A', '#501356', '#183356', '#28203F', '#391B3C', '#1E2B3C', '#120634', '#2D0432', '#051932', '#453080', '#75267D', '#2C507D', '#4B3880', '#752F7D', '#35547D' ] }, { name: 'nightLights', selectable: false, group: 'Ordinal', domain: [ '#4e31a5', '#9c25a7', '#3065ab', '#57468b', '#904497', '#46648b', '#32118d', '#a00fb3', '#1052a2', '#6e51bd', '#b63cc3', '#6c97cb', '#8671c1', '#b455be', '#7496c3' ] } ]; class ColorHelper { scale; colorDomain; domain; customColors; constructor(scheme, domain, customColors) { if (typeof scheme === 'string') { scheme = colorSets.find(cs => { return cs.name === scheme; }); } this.colorDomain = scheme.domain; this.domain = domain; this.customColors = customColors; this.scale = this.generateColorScheme(scheme, this.domain); } generateColorScheme(scheme, domain) { if (typeof scheme === 'string') { scheme = colorSets.find(cs => { return cs.name === scheme; }); } return scaleOrdinal().range(scheme.domain).domain(domain); } getColor(value) { if (value === undefined || value === null) { throw new Error('Value can not be null'); } if (typeof this.customColors === 'function') { return this.customColors(value); } const formattedValue = value.toString(); let found; // todo type customColors if (this.customColors && this.customColors.length > 0) { found = this.customColors.find(mapping => { return mapping.name.toLowerCase() === formattedValue.toLowerCase(); }); } if (found) { return found.value; } else { return this.scale(value); } } } function calculateViewDimensions({ width, height }) { let chartWidth = width; let chartHeight = height; chartWidth = Math.max(0, chartWidth); chartHeight = Math.max(0, chartHeight); return { width: Math.floor(chartWidth), height: Math.floor(chartHeight) }; } /** * Visibility Observer */ class VisibilityObserver { element; zone; visible = new EventEmitter(); timeout; isVisible = false; constructor(element, zone) { this.element = element; this.zone = zone; this.runCheck(); } destroy() { clearTimeout(this.timeout); } onVisibilityChange() { // trigger zone recalc for columns this.zone.run(() => { this.isVisible = true; this.visible.emit(true); }); } runCheck() { const check = () => { if (!this.element) { return; } // https://davidwalsh.name/offsetheight-visibility const { offsetHeight, offsetWidth } = this.element.nativeElement; if (offsetHeight && offsetWidth) { clearTimeout(this.timeout); this.onVisibilityChange(); } else { clearTimeout(this.timeout); this.zone.runOutsideAngular(() => { this.timeout = setTimeout(() => check(), 100); }); } }; this.zone.runOutsideAngular(() => { this.timeout = setTimeout(() => check()); }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: VisibilityObserver, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.4", type: VisibilityObserver, isStandalone: false, selector: "visibility-observer", outputs: { visible: "visible" }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: VisibilityObserver, decorators: [{ type: Directive, args: [{ // tslint:disable-next-line:directive-selector selector: 'visibility-observer', standalone: false }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.NgZone }], propDecorators: { visible: [{ type: Output }] } }); var Orientation; (function (Orientation) { Orientation["LEFT_TO_RIGHT"] = "LR"; Orientation["RIGHT_TO_LEFT"] = "RL"; Orientation["TOP_TO_BOTTOM"] = "TB"; Orientation["BOTTOM_TO_TOM"] = "BT"; })(Orientation || (Orientation = {})); var Alignment; (function (Alignment) { Alignment["CENTER"] = "C"; Alignment["UP_LEFT"] = "UL"; Alignment["UP_RIGHT"] = "UR"; Alignment["DOWN_LEFT"] = "DL"; Alignment["DOWN_RIGHT"] = "DR"; })(Alignment || (Alignment = {})); class DagreLayout { defaultSettings = { orientation: Orientation.LEFT_TO_RIGHT, marginX: 20, marginY: 20, edgePadding: 100, rankPadding: 100, nodePadding: 50, multigraph: true, compound: true }; settings = {}; dagreGraph; dagreNodes; dagreEdges; run(graph) { this.createDagreGraph(graph); dagre.layout(this.dagreGraph); graph.edgeLabels = this.dagreGraph._edgeLabels; for (const dagreNodeId in this.dagreGraph._nodes) { const dagreNode = this.dagreGraph._nodes[dagreNodeId]; const node = graph.nodes.find(n => n.id === dagreNode.id); node.position = { x: dagreNode.x, y: dagreNode.y }; node.dimension = { width: dagreNode.width, height: dagreNode.height }; } return graph; } updateEdge(graph, edge) { const sourceNode = graph.nodes.find(n => n.id === edge.source); const targetNode = graph.nodes.find(n => n.id === edge.target); // determine new arrow position const dir = sourceNode.position.y <= targetNode.position.y ? -1 : 1; const startingPoint = { x: sourceNode.position.x, y: sourceNode.position.y - dir * (sourceNode.dimension.height / 2) }; const endingPoint = { x: targetNode.position.x, y: targetNode.position.y + dir * (targetNode.dimension.height / 2) }; // generate new points edge.points = [startingPoint, endingPoint]; return graph; } createDagreGraph(graph) { const settings = Object.assign({}, this.defaultSettings, this.settings); this.dagreGraph = new dagre.graphlib.Graph({ compound: settings.compound, multigraph: settings.multigraph }); this.dagreGraph.setGraph({ rankdir: settings.orientation, marginx: settings.marginX, marginy: settings.marginY, edgesep: settings.edgePadding, ranksep: settings.rankPadding, nodesep: settings.nodePadding, align: settings.align, acyclicer: settings.acyclicer, ranker: settings.ranker, multigraph: settings.multigraph, compound: settings.compound }); // Default to assigning a new object as a label for each new edge. this.dagreGraph.setDefaultEdgeLabel(() => { return { /* empty */ }; }); this.dagreNodes = graph.nodes.map(n => { const node = Object.assign({}, n); node.width = n.dimension.width; node.height = n.dimension.height; node.x = n.position.x; node.y = n.position.y; return node; }); this.dagreEdges = graph.edges.map(l => { const newLink = Object.assign({}, l); if (!newLink.id) { newLink.id = id(); } return newLink; }); for (const node of this.dagreNodes) { if (!node.width) { node.width = 20; } if (!node.height) { node.height = 30; } // update dagre this.dagreGraph.setNode(node.id, node); } // update dagre for (const edge of this.dagreEdges) { if (settings.multigraph) { this.dagreGraph.setEdge(edge.source, edge.target, edge, edge.id); } else { this.dagreGraph.setEdge(edge.source, edge.target); } } return this.dagreGraph; } } class DagreClusterLayout { defaultSettings = { orientation: Orientation.LEFT_TO_RIGHT, marginX: 20, marginY: 20, edgePadding: 100, rankPadding: 100, nodePadding: 50, multigraph: true, compound: true }; settings = {}; dagreGraph; dagreNodes; dagreClusters; dagreEdges; run(graph) { this.createDagreGraph(graph); dagre.layout(this.dagreGraph); graph.edgeLabels = this.dagreGraph._edgeLabels; const dagreToOutput = node => { const dagreNode = this.dagreGraph._nodes[node.id]; return { ...node, position: { x: dagreNode.x, y: dagreNode.y }, dimension: { width: dagreNode.width, height: dagreNode.height } }; }; graph.clusters = (graph.clusters || []).map(dagreToOutput); graph.nodes = graph.nodes.map(dagreToOutput); return graph; } updateEdge(graph, edge) { const sourceNode = graph.nodes.find(n => n.id === edge.source); const targetNode = graph.nodes.find(n => n.id === edge.target); // determine new arrow position const dir = sourceNode.position.y <= targetNode.position.y ? -1 : 1; const startingPoint = { x: sourceNode.position.x, y: sourceNode.position.y - dir * (sourceNode.dimension.height / 2) }; const endingPoint = { x: targetNode.position.x, y: targetNode.position.y + dir * (targetNode.dimension.height / 2) }; // generate new points edge.points = [startingPoint, endingPoint]; return graph; } createDagreGraph(graph) { const settings = Object.assign({}, this.defaultSettings, this.settings); this.dagreGraph = new dagre.graphlib.Graph({ compound: settings.compound, multigraph: settings.multigraph }); this.dagreGraph.setGraph({ rankdir: settings.orientation, marginx: settings.marginX, marginy: settings.marginY, edgesep: settings.edgePadding, ranksep: settings.rankPadding, nodesep: settings.nodePadding, align: settings.align, acyclicer: settings.acyclicer, ranker: settings.ranker, multigraph: settings.multigraph, compound: settings.compound }); // Default to assigning a new object as a label for each new edge. this.dagreGraph.setDefaultEdgeLabel(() => { return { /* empty */ }; }); this.dagreNodes = graph.nodes.map((n) => { const node = Object.assign({}, n); node.width = n.dimension.width; node.height = n.dimension.height; node.x = n.position.x; node.y = n.position.y; return node; }); this.dagreClusters = graph.clusters || []; this.dagreEdges = graph.edges.map(l => { const newLink = Object.assign({}, l); if (!newLink.id) { newLink.id = id(); } return newLink; }); for (const node of this.dagreNodes) { this.dagreGraph.setNode(node.id, node); } for (const cluster of this.dagreClusters) { this.dagreGraph.setNode(cluster.id, cluster); cluster.childNodeIds.forEach(childNodeId => { this.dagreGraph.setParent(childNodeId, cluster.id); }); } // update dagre for (const edge of this.dagreEdges) { if (settings.multigraph) { this.dagreGraph.setEdge(edge.source, edge.target, edge, edge.id); } else { this.dagreGraph.setEdge(edge.source, edge.target); } } return this.dagreGraph; } } const DEFAULT_EDGE_NAME = '\x00'; const GRAPH_NODE = '\x00'; const EDGE_KEY_DELIM = '\x01'; class DagreNodesOnlyLayout { defaultSettings = { orientation: Orientation.LEFT_TO_RIGHT, marginX: 20, marginY: 20, edgePadding: 100, rankPadding: 100, nodePadding: 50, curveDistance: 20, multigraph: true, compound: true }; settings = {}; dagreGraph; dagreNodes; dagreEdges; run(graph) { this.createDagreGraph(graph); dagre.layout(this.dagreGraph); graph.edgeLabels = this.dagreGraph._edgeLabels; for (const dagreNodeId in this.dagreGraph._nodes) { const dagreNode = this.dagreGraph._nodes[dagreNodeId]; const node = graph.nodes.find(n => n.id === dagreNode.id); node.position = { x: dagreNode.x, y: dagreNode.y }; node.dimension = { width: dagreNode.width, height: dagreNode.height }; } for (const edge of graph.edges) { this.updateEdge(graph, edge); } return graph; } updateEdge(graph, edge) { const sourceNode = graph.nodes.find(n => n.id === edge.source); const targetNode = graph.nodes.find(n => n.id === edge.target); const rankAxis = this.settings.orientation === 'BT' || this.settings.orientation === 'TB' ? 'y' : 'x'; const orderAxis = rankAxis === 'y' ? 'x' : 'y'; const rankDimension = rankAxis === 'y' ? 'height' : 'width'; // determine new arrow position const dir = sourceNode.position[rankAxis] <= targetNode.position[rankAxis] ? -1 : 1; const startingPoint = { [orderAxis]: sourceNode.position[orderAxis], [rankAxis]: sourceNode.position[rankAxis] - dir * (sourceNode.dimension[rankDimension] / 2) }; const endingPoint = { [orderAxis]: targetNode.position[orderAxis], [rankAxis]: targetNode.position[rankAxis] + dir * (targetNode.dimension[rankDimension] / 2) }; const curveDistance = this.settings.curveDistance || this.defaultSettings.curveDistance; // generate new points edge.points = [ startingPoint, { [orderAxis]: startingPoint[orderAxis], [rankAxis]: startingPoint[rankAxis] - dir * curveDistance }, { [orderAxis]: endingPoint[orderAxis], [rankAxis]: endingPoint[rankAxis] + dir * curveDistance }, endingPoint ]; const edgeLabelId = `${edge.source}${EDGE_KEY_DELIM}${edge.target}${EDGE_KEY_DELIM}${DEFAULT_EDGE_NAME}`; const matchingEdgeLabel = graph.edgeLabels[edgeLabelId]; if (matchingEdgeLabel) { matchingEdgeLabel.points = edge.points; } return graph; } createDagreGraph(graph) { const settings = Object.assign({}, this.defaultSettings, this.settings); this.dagreGraph = new dagre.graphlib.Graph({ compound: settings.compound, multigraph: settings.multigraph }); this.dagreGraph.setGraph({ rankdir: settings.orientation, marginx: settings.marginX, marginy: settings.marginY, edgesep: settings.edgePadding, ranksep: settings.rankPadding, nodesep: settings.nodePadding, align: settings.align, acyclicer: settings.acyclicer, ranker: settings.ranker, multigraph: settings.multigraph, compound: settings.compound }); // Default to assigning a new object as a label for each new edge. this.dagreGraph.setDefaultEdgeLabel(() => { return { /* empty */ }; }); this.dagreNodes = graph.nodes.map(n => { const node = Object.assign({}, n); node.width = n.dimension.width; node.height = n.dimension.height; node.x = n.position.x; node.y = n.position.y; return node; }); this.dagreEdges = graph.edges.map(l => { const newLink = Object.assign({}, l); if (!newLink.id) { newLink.id = id(); } return newLink; }); for (const node of this.dagreNodes) { if (!node.width) { node.width = 20; } if (!node.height) { node.height = 30; } // update dagre this.dagreGraph.setNode(node.id, node); } // update dagre for (const edge of this.dagreEdges) { if (settings.multigraph) { this.dagreGraph.setEdge(edge.source, edge.target, edge, edge.id); } else { this.dagreGraph.setEdge(edge.source, edge.target); } } return this.dagreGraph; } } function toD3Node(maybeNode) { if (typeof maybeNode === 'string') { return { id: maybeNode, x: 0, y: 0 }; } return maybeNode; } class D3ForceDirectedLayout { defaultSettings = { force: forceSimulation().force('charge', forceManyBody().strength(-150)).force('collide', forceCollide(5)), forceLink: forceLink() .id(node => node.id) .distance(() => 100) }; settings = {}; inputGraph; outputGraph; d3Graph; outputGraph$ = new Subject(); draggingStart; run(graph) { this.inputGraph = graph; this.d3Graph = { nodes: [...this.inputGraph.nodes.map(n => ({ ...n }))], edges: [...this.inputGraph.edges.map(e => ({ ...e }))] }; this.outputGraph = { nodes: [], edges: [], edgeLabels: [] }; this.outputGraph$.next(this.outputGraph); this.settings = Object.assign({}, this.defaultSettings, this.settings); if (this.settings.force) { this.settings.force .nodes(this.d3Graph.nodes) .force('link', this.settings.forceLink.links(this.d3Graph.edges)) .alpha(0.5) .restart() .on('tick', () => { this.outputGraph$.next(this.d3GraphToOutputGraph(this.d3Graph)); }); } return this.outputGraph$.asObservable(); } updateEdge(graph, edge) { const settings = Object.assign({}, this.defaultSettings, this.settings); if (settings.force) { settings.force .nodes(this.d3Graph.nodes) .force('link', settings.forceLink.links(this.d3Graph.edges)) .alpha(0.5) .restart() .on('tick', () => { this.outputGraph$.next(this.d3GraphToOutputGraph(this.d3Graph)); }); } return this.outputGraph$.asObservable(); } d3GraphToOutputGraph(d3Graph) { this.outputGraph.nodes = this.d3Graph.nodes.map((node) => ({ ...node, id: node.id || id(), position: { x: node.x, y: node.y }, dimension: { width: (node.dimension && node.dimension.width) || 20, height: (node.dimension && node.dimension.height) || 20 }, transform: `translate(${node.x - ((node.dimension && node.dimension.width) || 20) / 2 || 0}, ${node.y - ((node.dimension && node.dimension.height) || 20) / 2 || 0})` })); this.outputGraph.edges = this.d3Graph.edges.map(edge => ({ ...edge, source: toD3Node(edge.source).id, target: toD3Node(edge.target).id, points: [ { x: toD3Node(edge.source).x, y: toD3Node(edge.source).y }, { x: toD3Node(edge.target).x, y: toD3Node(edge.target).y } ] })); this.outputGraph.edgeLabels = this.outputGraph.edges; return this.outputGraph; } onDragStart(draggingNode, $event) { this.settings.force.alphaTarget(0.3).restart(); const node = this.d3Graph.nodes.find(d3Node => d3Node.id === draggingNode.id); if (!node) { return; } this.draggingStart = { x: $event.x - node.x, y: $event.y - node.y }; node.fx = $event.x - this.draggingStart.x; node.fy = $event.y - this.draggingStart.y; } onDrag(draggingNode, $event) { if (!draggingNode) { return; } const node = this.d3Graph.nodes.find(d3Node => d3Node.id === draggingNode.id); if (!node) { return; } node.fx = $event.x - this.draggingStart.x; node.fy = $event.y - this.draggingStart.y; } onDragEnd(draggingNode, $event) { if (!draggingNode) { return; } const node = this.d3Graph.nodes.find(d3Node => d3Node.id === draggingNode.id); if (!node) { return; } this.settings.force.alphaTarget(0); node.fx = undefined; node.fy = undefined; } } function toNode(nodes, nodeRef) { if (typeof nodeRef === 'number') { return nodes[nodeRef]; } return nodeRef; } class ColaForceDirectedLayout { defaultSettings = { force: d3adaptor({ ...d3Dispatch, ...d3Force, ...d3Timer }) .linkDistance(150) .avoidOverlaps(true), viewDimensions: { width: 600, height: 600 } }; settings = {}; inputGraph; outputGraph; internalGraph; outputGraph$ = new Subject(); draggingStart; run(graph) { this.inputGraph = graph; if (!this.inputGraph.clusters) { this.inputGraph.clusters = []; } this.internalGraph = { nodes: [ ...this.inputGraph.nodes.map(n => ({ ...n, width: n.dimension ? n.dimension.width : 20, height: n.dimension ? n.dimension.height : 20 })) ], groups: [ ...this.inputGraph.clusters.map((cluster) => ({ padding: 5, groups: cluster.childNodeIds .map(nodeId => this.inputGraph.clusters.findIndex(node => node.id === nodeId)) .filter(x => x >= 0), leaves: cluster.childNodeIds .map(nodeId => this.inputGraph.nodes.findIndex(node => node.id === nodeId)) .filter(x => x >= 0) })) ], links: [ ...this.inputGraph.edges .map(e => { const sourceNodeIndex = this.inputGraph.nodes.findIndex(node => e.source === node.id); const targetNodeIndex = this.inputGraph.nodes.findIndex(node => e.target === node.id); if (sourceNodeIndex === -1 || targetNodeIndex === -1) { return undefined; } return { ...e, source: sourceNodeIndex, target: targetNodeIndex }; }) .filter(x => !!x) ], groupLinks: [ ...this.inputGraph.edges .map(e => { const sourceNodeIndex = this.inputGraph.nodes.findIndex(node => e.source === node.id); const targetNodeIndex = this.inputGraph.nodes.findIndex(node => e.target === node.id); if (sourceNodeIndex >= 0 && targetNodeIndex >= 0) { return undefined; } return e; }) .filter(x => !!x) ] }; this.outputGraph = { nodes: [], clusters: [], edges: [], edgeLabels: [] }; this.outputGraph$.next(this.outputGraph); this.settings = Object.assign({}, this.defaultSettings, this.settings); if (this.settings.force) { this.settings.force = this.settings.force .nodes(this.internalGraph.nodes) .groups(this.internalGraph.groups) .links(this.internalGraph.links) .alpha(0.5) .on('tick', () => { if (this.settings.onTickListener) { this.settings.onTickListener(this.internalGraph); } this.outputGraph$.next(this.internalGraphToOutputGraph(this.internalGraph)); }); if (this.settings.viewDimensions) { this.settings.force = this.settings.force.size([ this.settings.viewDimensions.width, this.settings.viewDimensions.height ]); } if (this.settings.forceModifierFn) { this.settings.force = this.settings.forceModifierFn(this.settings.force); } this.settings.force.start(); } return this.outputGraph$.asObservable(); } updateEdge(graph, edge) { const settings = Object.assign({}, this.defaultSettings, this.settings); if (settings.force) { settings.force.start(); } return this.outputGraph$.asObservable(); } internalGraphToOutputGraph(internalGraph) { this.outputGraph.nodes = internalGraph.nodes.map(node => ({ ...node, id: node.id || id(), position: { x: node.x, y: node.y }, dimension: { width: (node.dimension && node.dimension.width) || 20, height: (node.dimension && node.dimension.height) || 20 }, transform: `translate(${node.x - ((node.dimension && node.dimension.width) || 20) / 2 || 0}, ${node.y - ((node.dimension && node.dimension.height) || 20) / 2 || 0})` })); this.outputGraph.edges = internalGraph.links .map(edge => { const source = toNode(internalGraph.nodes, edge.source); const target = toNode(internalGraph.nodes, edge.target); return { ...edge, source: source.id, target: target.id, points: [ source.bounds.rayIntersection(target.bounds.cx(), target.bounds.cy()), target.bounds.rayIntersection(source.bounds.cx(), source.bounds.cy()) ] }; }) .concat(internalGraph.groupLinks.map(groupLink => { const sourceNode = internalGraph.nodes.find(foundNode => foundNode.id === groupLink.source); const targetNode = internalGraph.nodes.find(foundNode => foundNode.id === groupLink.target); const source = sourceNode || internalGraph.groups.find(foundGroup => foundGroup.id === groupLink.source); const target = targetNode || internalGraph.groups.find(foundGroup => foundGroup.id === groupLink.target); return { ...groupLink, source: source.id, target: target.id, points: [ source.bounds.rayIntersection(target.bounds.cx(), target.bounds.cy()), target.bounds.rayIntersection(source.bounds.cx(), source.bounds.cy()) ] }; })); this.outputGraph.clusters = internalGraph.groups.map((group, index) => { const inputGroup = this.inputGraph.clusters[index]; return { ...inputGroup, dimension: { width: group.bounds ? group.bounds.width() : 20, height: group.bounds ? group.bounds.height() : 20 }, position: { x: group.bounds ? group.bounds.x + group.bounds.width() / 2 : 0, y: group.bounds ? group.bounds.y + group.bounds.height() / 2 : 0 } }; }); this.outputGraph.edgeLabels = this.outputGraph.edges; return this.outputGraph; } onDragStart(draggingNode, $event) { const nodeIndex = this.outputGraph.nodes.findIndex(foundNode => foundNode.id === draggingNode.id); const node = this.internalGraph.nodes[nodeIndex]; if (!node) { return; } this.draggingStart = { x: node.x - $event.x, y: node.y - $event.y }; node.fixed = 1; this.settings.force.start(); } onDrag(draggingNode, $event) { if (!draggingNode) { return; } const nodeIndex = this.outputGraph.nodes.findIndex(foundNode => foundNode.id === draggingNode.id); const node = this.internalGraph.nodes[nodeIndex]; if (!node) { return; } node.x = this.draggingStart.x + $event.x; node.y = this.draggingStart.y + $event.y; } onDragEnd(draggingNode, $event) { if (!draggingNode) { return; } const nodeIndex = this.outputGraph.nodes.findIndex(foundNode => foundNode.id === draggingNode.id); const node = this.internalGraph.nodes[nodeIndex]; if (!node) { return; } node.fixed = 0; } } const layouts = { dagre: DagreLayout, dagreCluster: DagreClusterLayout, dagreNodesOnly: DagreNodesOnlyLayout, d3ForceDirected: D3ForceDirectedLayout, colaForceDirected: ColaForceDirectedLayout }; class LayoutService { getLayout(name) { if (layouts[name]) { return new layouts[name](); } else { throw new Error(`Unknown layout type '${name}'`); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: LayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: LayoutService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: LayoutService, decorators: [{ type: Injectable }] }); /** * Mousewheel directive * https://github.com/SodhanaLibrary/angular2-examples/blob/master/app/mouseWheelDirective/mousewheel.directive.ts * * @export */ // tslint:disable-next-line: directive-selector class MouseWheelDirective { mouseWheelUp = new EventEmitter(); mouseWheelDown = new EventEmitter(); onMouseWheelChrome(event) { this.mouseWheelFunc(event); } onMouseWheelFirefox(event) { this.mouseWheelFunc(event); } onWheel(event) { this.mouseWheelFunc(event); } onMouseWheelIE(event) { this.mouseWheelFunc(event); } mouseWheelFunc(event) { if (window.event) { event = window.event; } const delta = Math.max(-1, Math.min(1, event.wheelDelta || -event.detail || event.deltaY || event.deltaX)); // Firefox don't have native support for wheel event, as a result delta values are reverse const isWheelMouseUp = event.wheelDelta ? delta > 0 : delta < 0; const isWheelMouseDown = event.wheelDelta ? delta < 0 : delta > 0; if (isWheelMouseUp) { this.mouseWheelUp.emit(event); } else if (isWheelMouseDown) { this.mouseWheelDown.emit(event); } // for IE event.returnValue = false; // for Chrome and Firefox if (event.preventDefault) { event.preventDefault(); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: MouseWheelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.4", type: MouseWheelDirective, isStandalone: false, selector: "[mouseWheel]", outputs: { mouseWheelUp: "mouseWheelUp", mouseWheelDown: "mouseWheelDown" }, host: { listeners: { "mousewheel": "onMouseWheelChrome($event)", "DOMMouseScroll": "onMouseWheelFirefox($event)", "wheel": "onWheel($event)", "onmousewheel": "onMouseWheelIE($event)" } }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: MouseWheelDirective, decorators: [{ type: Directive, args: [{ selector: '[mouseWheel]', standalone: false }] }], propDecorators: { mouseWheelUp: [{ type: Output }], mouseWheelDown: [{ type: Output }], onMouseWheelChrome: [{ type: HostListener, args: ['mousewheel', ['$event']] }], onMouseWheelFirefox: [{ type: HostListener, args: ['DOMMouseScroll', ['$event']] }], onWheel: [{ type: HostListener, args: ['wheel', ['$event']] }], onMouseWheelIE: [{ type: HostListener, args: ['onmousewheel', ['$event']] }] } }); var NgxGraphStates; (function (NgxGraphStates) { NgxGraphStates["Init"] = "init"; NgxGraphStates["Subscribe"] = "subscribe"; NgxGraphStates["Transform"] = "transform"; /* eslint-disable @typescript-eslint/no-shadow */ NgxGraphStates["Output"] = "output"; })(NgxGraphStates || (NgxGraphStates = {})); class GraphComponent { el; zone; cd; layoutService; nodes = []; clusters = []; compoundNodes = []; links = []; activeEntries = []; curve; draggingEnabled = true; nodeHeight; nodeMaxHeight; nodeMinHeight; nodeWidth; nodeMinWidth; nodeMaxWidth; panningEnabled = true; panningAxis = PanningAxis.Both; enableZoom = true; zoomSpeed = 0.1; minZoomLevel = 0.1; maxZoomLevel = 4.0; autoZoom = false; panOnZoom = true; animate = false; autoCenter = false; update$; center$; zoomToFit$; panToNode$; layout; layoutSettings; enableTrackpadSupport = false; showMiniMap = false; miniMapMaxWidth = 100; miniMapMaxHeight; miniMapPosition = MiniMapPosition.UpperRight; view; scheme = 'cool'; customColors; deferDisplayUntilPosition = false; centerNodesOnPositionChange = true; enablePreUpdateTransform = true; select = new EventEmitter(); activate = new EventEmitter(); deactivate = new EventEmitter(); zoomChange = new EventEmitter(); clickHandler = new EventEmitter(); stateChange = new EventEmitter(); linkTemplate; nodeTemplate; clusterTemplate; defsTemplate; miniMapNodeTemplate; nodeElements; linkElements; chartWidth; isMouseMoveCalled = false; graphSubscription = new Subscription(); colors; dims; seriesDomain; transform; isPanning = false; isDragging = false; draggingNode; initialized = false; graph; graphDims = { width: 0, height: 0 }; _oldLinks = []; oldNodes = new Set(); oldClusters = new Set(); oldCompoundNodes = new Set(); transformationMatrix = identity(); _touchLastX = null; _touchLastY = null; minimapScaleCoefficient = 3; minimapTransform; minimapOffsetX = 0; minimapOffsetY = 0; isMinimapPanning = false; minimapClipPathId; width; height; resizeSubscription; visibilityObserver; destroy$ = new Subject(); constructor(el, zone, cd, layoutService) { this.el = el; this.zone = zone; this.cd = cd; this.layoutService = layoutService; } groupResultsBy = node => node.label; /** * Get the current zoom level */ get zoomLevel() { return this.transformationMatrix.a; } /** * Set the current zoom level */ set zoomLevel(level) { this.zoomTo(Number(level)); } /** * Get the current `x` position of the graph */ get panOffsetX() { return this.transformationMatrix.e; } /** * Set the current `x` position of the graph */ set panOffsetX(x) { this.panTo(Number(x), null); } /** * Get the current `y` position of the graph */ get panOffsetY() { return this.transformationMatrix.f; } /** * Set the current `y` position of the graph */ set panOffsetY(y) { this.panTo(null, Number(y)); } /** * Angular lifecycle event * * * @memberOf GraphComponent */ ngOnInit() { if (this.update$) { this.update$.pipe(takeUntil(this.destroy$)).subscribe(() => { this.update(); }); } if (this.center$) { this.center$.pipe(takeUntil(this.destroy$)).subscribe(() => { this.center(); }); } if (this.zoomToFit$) { this.zoomToFit$.pipe(takeUntil(this.destroy$)).subscribe(options => { this.zoomToFit(options ? options : {}); }); } if (this.panToNode$) { this.panToNode$.pipe(takeUntil(this.destroy$)).subscribe((nodeId) => { this.panToNodeId(nodeId); }); } this.minimapClipPathId = `minimapClip${id()}`; this.stateChange.emit({ state: NgxGraphStates.Subscribe }); } ngOnChanges(changes) { this.basicUpdate(); const { layoutSettings } = changes; this.setLayout(this.layout); if (layoutSettings) { this.setLayoutSettings(this.layoutSettings); } if (this.layout && this.nodes.length && this.links.length) { this.update(); } } setLayout(layout) { this.initialized = false; if (!layout) { layout = 'dagre'; } if (typeof layout === 'string') { this.layout = this.layoutService.getLayout(layout); this.setLayoutSettings(this.layoutSettings); } } setLayoutSettings(settings) { if (this.layout && typeof this.layout !== 'string') { this.layout.settings = settings; } } /** * Angular lifecycle event * * * @memberOf GraphComponent */ ngOnDestroy() { this.unbindEvents(); if (this.visibilityObserver) { this.visibilityObserver.visible.unsubscribe(); this.visibilityObserver.destroy(); } this.destroy$.next(); this.destroy$.complete(); } /** * Angular lifecycle event * * * @memberOf GraphComponent */ ngAfterViewInit() { this.bindWindowResizeEvent(); // listen for visibility of the element for hidden by default scenario this.visibilityObserver = new VisibilityObserver(this.el, this.zone); this.visibilityObserver.visible.subscribe(this.update.bind(this)); setTimeout(() => this.update()); } /** * Base class update implementation for the dag graph * * @memberOf GraphComponent */ update() { this.basicUpdate(); if (!this.curve) { this.curve = shape.curveBundle.beta(1); } this.zone.run(() => { this.dims = calculateViewDimensions({ width: this.width, height: this.height }); this.seriesDomain = this.getSeriesDomain(); this.setColors(); this.createGraph(); this.updateTransform(); if (!this.initialized) { this.stateChange.emit({ state: NgxGraphStates.Init }); } this.initialized = true; }); } /** * Creates the dagre graph engine * * @memberOf GraphComponent */ createGraph() { this.graphSubscription.unsubscribe(); this.graphSubscription = new Subscription(); const initializeNode = (n) => { if (!n.meta) { n.meta = {}; } if (!n.id) { n.id = id(); } if (!n.dimension) { n.dimension = { width: this.nodeWidth ? this.nodeWidth : 30, height: this.nodeHeight ? this.n