UNPKG

@swimlane/ngx-graph

Version:
1,494 lines (1,477 loc) 235 kB
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