@swimlane/ngx-graph
Version:
Graph visualization for angular
1,575 lines (1,559 loc) • 109 kB
JavaScript
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