@swimlane/ngx-graph
Version:
Graph visualization for angular
1,494 lines (1,477 loc) • 235 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, Output, Directive, output, HostListener, Injectable, inject, Injector, input, model, contentChild, viewChildren, effect, afterNextRender, isDevMode, ChangeDetectionStrategy, ViewEncapsulation, Component, NgModule } from '@angular/core';
import { NgTemplateOutlet, CommonModule } from '@angular/common';
import { __decorate } from 'tslib';
import { select } from 'd3-selection';
import * as shape from 'd3-shape';
import { Subject, Subscription, Observable, of } 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 d3ease from 'd3-ease';
import * as dagre from 'dagre';
import * as d3Force from 'd3-force';
import { forceLink, forceSimulation, forceManyBody, forceCollide } from 'd3-force';
import { d3adaptor } from 'webcola';
import * as d3Dispatch from 'd3-dispatch';
import * as d3Timer from 'd3-timer';
/**
* Default inter-layer node spacing (px) used when seeding provisional node positions for layered layouts.
* Align with `elk.layered.spacing.nodeNodeBetweenLayers` when using ELK-style `layoutSettings.properties`.
*/
const LAYERED_NODE_NODE_BETWEEN_LAYERS_PX = 72;
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["LowerLeft"] = "LowerLeft";
MiniMapPosition["LowerRight"] = "LowerRight";
})(MiniMapPosition || (MiniMapPosition = {}));
const DefaultMiniMapMargin = {
top: 0,
right: 0,
bottom: 0,
left: 0
};
/**
* 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: "21.1.1", ngImport: i0, type: VisibilityObserver, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.1", type: VisibilityObserver, isStandalone: true, selector: "visibility-observer", outputs: { visible: "visible" }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: VisibilityObserver, decorators: [{
type: Directive,
args: [{
// tslint:disable-next-line:directive-selector
selector: 'visibility-observer'
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.NgZone }], propDecorators: { visible: [{
type: Output
}] } });
/**
* 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 = output();
mouseWheelDown = output();
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: "21.1.1", ngImport: i0, type: MouseWheelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.1", type: MouseWheelDirective, isStandalone: true, 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: "21.1.1", ngImport: i0, type: MouseWheelDirective, decorators: [{
type: Directive,
args: [{
selector: '[mouseWheel]'
}]
}], propDecorators: { mouseWheelUp: [{ type: i0.Output, args: ["mouseWheelUp"] }], mouseWheelDown: [{ type: i0.Output, args: ["mouseWheelDown"] }], onMouseWheelChrome: [{
type: HostListener,
args: ['mousewheel', ['$event']]
}], onMouseWheelFirefox: [{
type: HostListener,
args: ['DOMMouseScroll', ['$event']]
}], onWheel: [{
type: HostListener,
args: ['wheel', ['$event']]
}], onMouseWheelIE: [{
type: HostListener,
args: ['onmousewheel', ['$event']]
}] } });
const DEFAULT_LAYOUT_MORPH_CAPTURE = {
previousSource: 'model-transform',
degenerateEpsilon: 1e-3,
modelResolutionOrder: ['compound', 'cluster', 'node'],
syncTargetsFromPositionAfterTick: false,
snapAddedNodeIds: false
};
function mergeLayoutMorphCapture(partial) {
return { ...DEFAULT_LAYOUT_MORPH_CAPTURE, ...partial };
}
const DEFAULT_GRAPH_LAYOUT_TRANSITION = {
mode: 'instant',
scope: 'full',
durationMs: 500,
easing: 'cubicInOut',
morphCapture: mergeLayoutMorphCapture(undefined)
};
const DEFAULT_VIEWPORT_TRANSLATION_TRANSITION = {
enabled: false,
durationMs: 280,
easing: 'cubicOut'
};
const DEFAULT_LAYOUT_TRANSITION_EFFECT = {
kind: 'none',
peakDegrees: 12
};
function resolveGraphTransitionEasing(easing) {
if (typeof easing === 'function') {
return easing;
}
switch (easing) {
case 'linear':
return d3ease.easeLinear;
case 'cubicIn':
return d3ease.easeCubicIn;
case 'cubicOut':
return d3ease.easeCubicOut;
case 'quadInOut':
return d3ease.easeQuadInOut;
case 'elasticOut':
return d3ease.easeElasticOut;
case 'cubicInOut':
default:
return d3ease.easeCubicInOut;
}
}
/**
* Resolves final layout transition from the graph `transitionAfterChanges` input merged with defaults.
* When unset or empty, returns {@link DEFAULT_GRAPH_LAYOUT_TRANSITION} (`mode: 'instant'`).
*/
function mergeGraphLayoutTransition(explicit) {
const baseMorph = mergeLayoutMorphCapture(DEFAULT_GRAPH_LAYOUT_TRANSITION.morphCapture);
const base = { ...DEFAULT_GRAPH_LAYOUT_TRANSITION, morphCapture: baseMorph };
if (explicit != null) {
const defined = Object.fromEntries(Object.entries(explicit).filter(([, v]) => v !== undefined));
if (Object.keys(defined).length > 0) {
const { morphCapture: morphPartial, ...rest } = defined;
return {
...base,
...rest,
morphCapture: mergeLayoutMorphCapture(morphPartial ?? base.morphCapture)
};
}
}
return base;
}
function mergeViewportTransition(explicit) {
if (explicit != null) {
const defined = Object.fromEntries(Object.entries(explicit).filter(([, v]) => v !== undefined));
if (Object.keys(defined).length > 0) {
return { ...DEFAULT_VIEWPORT_TRANSLATION_TRANSITION, ...defined };
}
}
return { ...DEFAULT_VIEWPORT_TRANSLATION_TRANSITION };
}
function mergeLayoutEffect(explicit) {
if (explicit != null) {
const defined = Object.fromEntries(Object.entries(explicit).filter(([, v]) => v !== undefined));
if (Object.keys(defined).length > 0) {
return { ...DEFAULT_LAYOUT_TRANSITION_EFFECT, ...defined };
}
}
return { ...DEFAULT_LAYOUT_TRANSITION_EFFECT };
}
const AXIS_EPS = 1e-3;
/**
* Maps Dagre `rankdir` to rank (layer) vs order (within layer) axes, matching
* {@link DagreNodesOnlyLayout} / graphlib conventions.
*/
function rankOrderAxesFromDagreRankdir(rankdir) {
const r = rankdir ?? 'LR';
const rankAxis = r === 'BT' || r === 'TB' ? 'y' : 'x';
const orderAxis = rankAxis === 'y' ? 'x' : 'y';
return { rankAxis, orderAxis };
}
/**
* Maps ELK layered `elk.direction` to rank vs order axes for drag-time edge ports.
*/
function rankOrderAxesFromElkDirection(direction) {
const u = (direction ?? 'DOWN').toUpperCase();
if (u === 'LEFT' || u === 'RIGHT') {
return { rankAxis: 'x', orderAxis: 'y' };
}
return { rankAxis: 'y', orderAxis: 'x' };
}
function withRankOrder(rankAxis, orderAxis, rank, order) {
return rankAxis === 'x' ? { x: rank, y: order } : { x: order, y: rank };
}
/**
* Rank-facing attachment points (centers projected to box faces toward the other node),
* matching the endpoints used by {@link edgePointsAfterDrag}.
*/
function rankFacePortPoints(source, target, axes) {
const { rankAxis, orderAxis } = axes;
const sp = source.position ?? { x: 0, y: 0 };
const tp = target.position ?? { x: 0, y: 0 };
const sw = source.dimension?.width ?? 0;
const sh = source.dimension?.height ?? 0;
const tw = target.dimension?.width ?? 0;
const th = target.dimension?.height ?? 0;
const halfRankS = (rankAxis === 'x' ? sw : sh) / 2;
const halfRankT = (rankAxis === 'x' ? tw : th) / 2;
const dir = sp[rankAxis] <= tp[rankAxis] ? -1 : 1;
const sRank = sp[rankAxis] - dir * halfRankS;
const tRank = tp[rankAxis] + dir * halfRankT;
const sOrd = sp[orderAxis];
const tOrd = tp[orderAxis];
return [withRankOrder(rankAxis, orderAxis, sRank, sOrd), withRankOrder(rankAxis, orderAxis, tRank, tOrd)];
}
/** Axis-aligned Manhattan path (one bend) between two ports. */
function orthogonalManhattanPolyline(p0, p1) {
if (Math.abs(p0.x - p1.x) < AXIS_EPS || Math.abs(p0.y - p1.y) < AXIS_EPS) {
return [p0, p1];
}
return [p0, { x: p1.x, y: p0.y }, p1];
}
/** Normalize ELK `elk.edgeRouting` for branching. */
function normalizeElkEdgeRouting(value) {
const u = (value ?? '').toUpperCase();
if (u.includes('SPLINE') || u === 'SPLINES') {
return 'SPLINES';
}
if (u === 'ORTHOGONAL') {
return 'ORTHOGONAL';
}
if (u === 'POLYLINE') {
return 'POLYLINE';
}
return 'UNKNOWN';
}
function segmentAxisAligned(a, b) {
return Math.abs(a.x - b.x) < AXIS_EPS || Math.abs(a.y - b.y) < AXIS_EPS;
}
/**
* Classify last laid-out polyline for `dragEdgeStyle: 'auto'`.
*/
function inferDragStyleFromPoints(points) {
if (!points || points.length < 2) {
return 'smooth';
}
if (points.length === 2) {
return 'straight';
}
let allOrtho = true;
for (let i = 0; i < points.length - 1; i++) {
if (!segmentAxisAligned(points[i], points[i + 1])) {
allOrtho = false;
break;
}
}
if (allOrtho) {
return 'orthogonal';
}
if (points.length >= 3) {
return 'smooth';
}
return 'straight';
}
/**
* Resolves `auto` using prior `edge.points`, otherwise returns the explicit style.
*/
function resolveDagreDragEdgeStyle(dragEdgeStyle, priorPoints) {
const mode = dragEdgeStyle ?? 'auto';
if (mode === 'auto') {
return inferDragStyleFromPoints(priorPoints);
}
return mode;
}
/**
* Builds drag polyline for Dagre / DagreCluster / DagreNodesOnly from resolved style.
* `smooth` uses {@link edgePointsAfterDrag} (rank-offset interior points) for curved spline-style strokes;
* `orthogonal` uses {@link orthogonalManhattanPolyline}; `straight` uses two port points only.
*/
function dagreDragPolyline(source, target, axes, curveDistance, style) {
if (style === 'straight') {
const [p0, p1] = rankFacePortPoints(source, target, axes);
return [p0, p1];
}
if (style === 'orthogonal') {
const [p0, p1] = rankFacePortPoints(source, target, axes);
return orthogonalManhattanPolyline(p0, p1);
}
return edgePointsAfterDrag(source, target, { curveDistance, ...axes });
}
/**
* Drag-time polyline for ElkLayout from normalized `elk.edgeRouting`.
* `UNKNOWN` defaults to smooth spline-style segments (four-point rank path).
*/
function elkDragPolyline(source, target, axes, curveDistance, routing) {
const mode = routing === 'UNKNOWN' ? 'SPLINES' : routing;
const [p0, p1] = rankFacePortPoints(source, target, axes);
if (mode === 'ORTHOGONAL' || mode === 'POLYLINE') {
return orthogonalManhattanPolyline(p0, p1);
}
return edgePointsAfterDrag(source, target, { curveDistance, ...axes });
}
/**
* Builds a 4-point polyline for interactive drag updates: ports on the rank-facing
* sides of each node box (using node centers + dimensions), plus two interior
* control points so d3 curve generators (e.g. `curveBasis`) have knots to bend.
*
* Aligns with {@link DagreNodesOnlyLayout.updateEdge}, with explicit rank/order axes
* so LR/RL/TB/BT and ELK directions attach on the correct face.
*/
function edgePointsAfterDrag(source, target, options) {
const { rankAxis, orderAxis, curveDistance } = options;
const sp = source.position ?? { x: 0, y: 0 };
const tp = target.position ?? { x: 0, y: 0 };
const dir = sp[rankAxis] <= tp[rankAxis] ? -1 : 1;
const cd = Math.max(0, curveDistance);
const axes = { rankAxis, orderAxis };
const [startingPoint, endingPoint] = rankFacePortPoints(source, target, axes);
return [
startingPoint,
withRankOrder(rankAxis, orderAxis, startingPoint[rankAxis] - dir * cd, startingPoint[orderAxis]),
withRankOrder(rankAxis, orderAxis, endingPoint[rankAxis] + dir * cd, endingPoint[orderAxis]),
endingPoint
];
}
/**
* When `style` is `linear`, returns only the two port points (straight segment).
* Otherwise returns the full 4-point drag polyline.
*/
function edgePointsForDragMode(source, target, options) {
const pts = edgePointsAfterDrag(source, target, options);
if (options.style === 'linear') {
return [pts[0], pts[pts.length - 1]];
}
return pts;
}
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,
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
};
}
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);
if (!sourceNode?.position || !targetNode?.position) {
return graph;
}
const settings = Object.assign({}, this.defaultSettings, this.settings);
const axes = rankOrderAxesFromDagreRankdir(settings.orientation);
const curveDistance = settings.curveDistance ?? 20;
const resolved = resolveDagreDragEdgeStyle(settings.dragEdgeStyle ?? 'auto', edge.points);
edge.points = dagreDragPolyline(sourceNode, targetNode, axes, curveDistance, resolved);
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,
curveDistance: 20,
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);
if (!sourceNode?.position || !targetNode?.position) {
return graph;
}
const settings = Object.assign({}, this.defaultSettings, this.settings);
const axes = rankOrderAxesFromDagreRankdir(settings.orientation);
const curveDistance = settings.curveDistance ?? 20;
const resolved = resolveDagreDragEdgeStyle(settings.dragEdgeStyle ?? 'auto', edge.points);
edge.points = dagreDragPolyline(sourceNode, targetNode, axes, curveDistance, resolved);
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);
if (!sourceNode?.position || !targetNode?.position) {
return graph;
}
const settings = Object.assign({}, this.defaultSettings, this.settings);
const axes = rankOrderAxesFromDagreRankdir(settings.orientation);
const curveDistance = settings.curveDistance ?? this.defaultSettings.curveDistance ?? 20;
const resolved = resolveDagreDragEdgeStyle(settings.dragEdgeStyle ?? 'auto', edge.points);
edge.points = dagreDragPolyline(sourceNode, targetNode, axes, curveDistance, resolved);
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);
const p0 = source.bounds.rayIntersection(target.bounds.cx(), target.bounds.cy());
const p1 = target.bounds.rayIntersection(source.bounds.cx(), source.bounds.cy());
return {
...edge,
source: source.id,
target: target.id,
points: [p0, p1]
};
})
.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);
const p0 = source.bounds.rayIntersection(target.bounds.cx(), target.bounds.cy());
const p1 = target.bounds.rayIntersection(source.bounds.cx(), source.bounds.cy());
return {
...groupLink,
source: source.id,
target: target.id,
points: [p0, p1]
};
}));
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 n