UNPKG

@swimlane/ngx-graph

Version:
1 lines 365 kB
{"version":3,"file":"swimlane-ngx-graph.mjs","sources":["../../../projects/swimlane/ngx-graph/src/lib/graph/layouts/layout-layered-constants.ts","../../../projects/swimlane/ngx-graph/src/lib/utils/id.ts","../../../projects/swimlane/ngx-graph/src/lib/enums/panning.enum.ts","../../../projects/swimlane/ngx-graph/src/lib/enums/mini-map-position.enum.ts","../../../projects/swimlane/ngx-graph/src/lib/utils/throttle.ts","../../../projects/swimlane/ngx-graph/src/lib/utils/color-sets.ts","../../../projects/swimlane/ngx-graph/src/lib/utils/color.helper.ts","../../../projects/swimlane/ngx-graph/src/lib/utils/view-dimensions.helper.ts","../../../projects/swimlane/ngx-graph/src/lib/utils/visibility-observer.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/mouse-wheel.directive.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/transition.model.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/layouts/edge-geometry.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/layouts/dagre.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/layouts/dagreCluster.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/layouts/dagreNodesOnly.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/layouts/d3ForceDirected.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/layouts/colaForceDirected.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/layouts/layout.service.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/graph.component.ts","../../../projects/swimlane/ngx-graph/src/lib/graph/graph.component.html","../../../projects/swimlane/ngx-graph/src/lib/graph/graph.module.ts","../../../projects/swimlane/ngx-graph/src/lib/ngx-graph.module.ts","../../../projects/swimlane/ngx-graph/src/public_api.ts","../../../projects/swimlane/ngx-graph/src/swimlane-ngx-graph.ts"],"sourcesContent":["/**\n * Default inter-layer node spacing (px) used when seeding provisional node positions for layered layouts.\n * Align with `elk.layered.spacing.nodeNodeBetweenLayers` when using ELK-style `layoutSettings.properties`.\n */\nexport const LAYERED_NODE_NODE_BETWEEN_LAYERS_PX = 72;\n","const cache = {};\n\n/**\n * Generates a short id.\n *\n */\nexport function id(): string {\n let newId = ('0000' + ((Math.random() * Math.pow(36, 4)) << 0).toString(36)).slice(-4);\n\n newId = `a${newId}`;\n\n // ensure not already used\n if (!cache[newId]) {\n cache[newId] = true;\n return newId;\n }\n\n return id();\n}\n","export enum PanningAxis {\n Both = 'both',\n Horizontal = 'horizontal',\n Vertical = 'vertical'\n}\n","export enum MiniMapPosition {\n UpperLeft = 'UpperLeft',\n UpperRight = 'UpperRight',\n LowerLeft = 'LowerLeft',\n LowerRight = 'LowerRight'\n}\n\nexport interface MiniMapMargin {\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\n\nexport const DefaultMiniMapMargin = {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n};\n","/**\n * Throttle a function\n *\n * @export\n * @param {*} func\n * @param {number} wait\n * @param {*} [options]\n * @returns\n */\nexport function throttle(context: any, func: any, wait: number, options?: any) {\n options = options || {};\n let args: any;\n let result: any;\n let timeout = null;\n let previous = 0;\n\n function later() {\n previous = options.leading === false ? 0 : +new Date();\n timeout = null;\n result = func.apply(context, args);\n }\n\n return function (..._arguments: any[]) {\n const now = +new Date();\n\n if (!previous && options.leading === false) {\n previous = now;\n }\n\n const remaining = wait - (now - previous);\n args = _arguments;\n\n if (remaining <= 0) {\n clearTimeout(timeout);\n timeout = null;\n previous = now;\n result = func.apply(context, args);\n } else if (!timeout && options.trailing !== false) {\n timeout = setTimeout(later, remaining);\n }\n\n return result;\n };\n}\n\n/**\n * Throttle decorator\n *\n * class MyClass {\n * throttleable(10)\n * myFn() { ... }\n * }\n *\n * @export\n * @param {number} duration\n * @param {*} [options]\n * @returns\n */\nexport function throttleable(duration: number, options?: any) {\n return function innerDecorator(target, key, descriptor) {\n return {\n configurable: true,\n enumerable: descriptor.enumerable,\n get: function getter() {\n Object.defineProperty(this, key, {\n configurable: true,\n enumerable: descriptor.enumerable,\n value: throttle(this, descriptor.value, duration, options)\n });\n\n return this[key];\n }\n };\n };\n}\n","export const colorSets = [\n {\n name: 'vivid',\n selectable: true,\n group: 'Ordinal',\n domain: [\n '#647c8a',\n '#3f51b5',\n '#2196f3',\n '#00b862',\n '#afdf0a',\n '#a7b61a',\n '#f3e562',\n '#ff9800',\n '#ff5722',\n '#ff4514'\n ]\n },\n {\n name: 'natural',\n selectable: true,\n group: 'Ordinal',\n domain: [\n '#bf9d76',\n '#e99450',\n '#d89f59',\n '#f2dfa7',\n '#a5d7c6',\n '#7794b1',\n '#afafaf',\n '#707160',\n '#ba9383',\n '#d9d5c3'\n ]\n },\n {\n name: 'cool',\n selectable: true,\n group: 'Ordinal',\n domain: [\n '#a8385d',\n '#7aa3e5',\n '#a27ea8',\n '#aae3f5',\n '#adcded',\n '#a95963',\n '#8796c0',\n '#7ed3ed',\n '#50abcc',\n '#ad6886'\n ]\n },\n {\n name: 'fire',\n selectable: true,\n group: 'Ordinal',\n domain: ['#ff3d00', '#bf360c', '#ff8f00', '#ff6f00', '#ff5722', '#e65100', '#ffca28', '#ffab00']\n },\n {\n name: 'solar',\n selectable: true,\n group: 'Continuous',\n domain: [\n '#fff8e1',\n '#ffecb3',\n '#ffe082',\n '#ffd54f',\n '#ffca28',\n '#ffc107',\n '#ffb300',\n '#ffa000',\n '#ff8f00',\n '#ff6f00'\n ]\n },\n {\n name: 'air',\n selectable: true,\n group: 'Continuous',\n domain: [\n '#e1f5fe',\n '#b3e5fc',\n '#81d4fa',\n '#4fc3f7',\n '#29b6f6',\n '#03a9f4',\n '#039be5',\n '#0288d1',\n '#0277bd',\n '#01579b'\n ]\n },\n {\n name: 'aqua',\n selectable: true,\n group: 'Continuous',\n domain: [\n '#e0f7fa',\n '#b2ebf2',\n '#80deea',\n '#4dd0e1',\n '#26c6da',\n '#00bcd4',\n '#00acc1',\n '#0097a7',\n '#00838f',\n '#006064'\n ]\n },\n {\n name: 'flame',\n selectable: false,\n group: 'Ordinal',\n domain: [\n '#A10A28',\n '#D3342D',\n '#EF6D49',\n '#FAAD67',\n '#FDDE90',\n '#DBED91',\n '#A9D770',\n '#6CBA67',\n '#2C9653',\n '#146738'\n ]\n },\n {\n name: 'ocean',\n selectable: false,\n group: 'Ordinal',\n domain: [\n '#1D68FB',\n '#33C0FC',\n '#4AFFFE',\n '#AFFFFF',\n '#FFFC63',\n '#FDBD2D',\n '#FC8A25',\n '#FA4F1E',\n '#FA141B',\n '#BA38D1'\n ]\n },\n {\n name: 'forest',\n selectable: false,\n group: 'Ordinal',\n domain: [\n '#55C22D',\n '#C1F33D',\n '#3CC099',\n '#AFFFFF',\n '#8CFC9D',\n '#76CFFA',\n '#BA60FB',\n '#EE6490',\n '#C42A1C',\n '#FC9F32'\n ]\n },\n {\n name: 'horizon',\n selectable: false,\n group: 'Ordinal',\n domain: [\n '#2597FB',\n '#65EBFD',\n '#99FDD0',\n '#FCEE4B',\n '#FEFCFA',\n '#FDD6E3',\n '#FCB1A8',\n '#EF6F7B',\n '#CB96E8',\n '#EFDEE0'\n ]\n },\n {\n name: 'neons',\n selectable: false,\n group: 'Ordinal',\n domain: [\n '#FF3333',\n '#FF33FF',\n '#CC33FF',\n '#0000FF',\n '#33CCFF',\n '#33FFFF',\n '#33FF66',\n '#CCFF33',\n '#FFCC00',\n '#FF6600'\n ]\n },\n {\n name: 'picnic',\n selectable: false,\n group: 'Ordinal',\n domain: [\n '#FAC51D',\n '#66BD6D',\n '#FAA026',\n '#29BB9C',\n '#E96B56',\n '#55ACD2',\n '#B7332F',\n '#2C83C9',\n '#9166B8',\n '#92E7E8'\n ]\n },\n {\n name: 'night',\n selectable: false,\n group: 'Ordinal',\n domain: [\n '#2B1B5A',\n '#501356',\n '#183356',\n '#28203F',\n '#391B3C',\n '#1E2B3C',\n '#120634',\n '#2D0432',\n '#051932',\n '#453080',\n '#75267D',\n '#2C507D',\n '#4B3880',\n '#752F7D',\n '#35547D'\n ]\n },\n {\n name: 'nightLights',\n selectable: false,\n group: 'Ordinal',\n domain: [\n '#4e31a5',\n '#9c25a7',\n '#3065ab',\n '#57468b',\n '#904497',\n '#46648b',\n '#32118d',\n '#a00fb3',\n '#1052a2',\n '#6e51bd',\n '#b63cc3',\n '#6c97cb',\n '#8671c1',\n '#b455be',\n '#7496c3'\n ]\n }\n];\n","import { range } from 'd3-array';\nimport { scaleBand, scaleLinear, scaleOrdinal, scaleQuantile } from 'd3-scale';\n\nimport { colorSets } from './color-sets';\n\nexport class ColorHelper {\n scale: any;\n colorDomain: any[];\n domain: any;\n customColors: any;\n\n constructor(scheme, domain, customColors?) {\n if (typeof scheme === 'string') {\n scheme = colorSets.find(cs => {\n return cs.name === scheme;\n });\n }\n this.colorDomain = scheme.domain;\n this.domain = domain;\n this.customColors = customColors;\n\n this.scale = this.generateColorScheme(scheme, this.domain);\n }\n\n generateColorScheme(scheme, domain) {\n if (typeof scheme === 'string') {\n scheme = colorSets.find(cs => {\n return cs.name === scheme;\n });\n }\n return scaleOrdinal().range(scheme.domain).domain(domain);\n }\n\n getColor(value) {\n if (value === undefined || value === null) {\n throw new Error('Value can not be null');\n }\n\n if (typeof this.customColors === 'function') {\n return this.customColors(value);\n }\n\n const formattedValue = value.toString();\n let found: any; // todo type customColors\n if (this.customColors && this.customColors.length > 0) {\n found = this.customColors.find(mapping => {\n return mapping.name.toLowerCase() === formattedValue.toLowerCase();\n });\n }\n\n if (found) {\n return found.value;\n } else {\n return this.scale(value);\n }\n }\n}\n","export interface ViewDimensions {\n width: number;\n height: number;\n}\n\nexport function calculateViewDimensions({ width, height }): ViewDimensions {\n let chartWidth = width;\n let chartHeight = height;\n\n chartWidth = Math.max(0, chartWidth);\n chartHeight = Math.max(0, chartHeight);\n\n return {\n width: Math.floor(chartWidth),\n height: Math.floor(chartHeight)\n };\n}\n","import { Output, EventEmitter, NgZone, Directive, ElementRef } from '@angular/core';\n\n/**\n * Visibility Observer\n */\n@Directive({\n // tslint:disable-next-line:directive-selector\n selector: 'visibility-observer'\n})\nexport class VisibilityObserver {\n @Output() visible: EventEmitter<any> = new EventEmitter();\n\n timeout: any;\n isVisible: boolean = false;\n\n constructor(\n private element: ElementRef,\n private zone: NgZone\n ) {\n this.runCheck();\n }\n\n destroy(): void {\n clearTimeout(this.timeout);\n }\n\n onVisibilityChange(): void {\n // trigger zone recalc for columns\n this.zone.run(() => {\n this.isVisible = true;\n this.visible.emit(true);\n });\n }\n\n runCheck(): void {\n const check = () => {\n if (!this.element) {\n return;\n }\n\n // https://davidwalsh.name/offsetheight-visibility\n const { offsetHeight, offsetWidth } = this.element.nativeElement;\n\n if (offsetHeight && offsetWidth) {\n clearTimeout(this.timeout);\n this.onVisibilityChange();\n } else {\n clearTimeout(this.timeout);\n this.zone.runOutsideAngular(() => {\n this.timeout = setTimeout(() => check(), 100);\n });\n }\n };\n\n this.zone.runOutsideAngular(() => {\n this.timeout = setTimeout(() => check());\n });\n }\n}\n","import { Directive, HostListener, output } from '@angular/core';\n\n/**\n * Mousewheel directive\n * https://github.com/SodhanaLibrary/angular2-examples/blob/master/app/mouseWheelDirective/mousewheel.directive.ts\n *\n * @export\n */\n// tslint:disable-next-line: directive-selector\n@Directive({\n selector: '[mouseWheel]'\n})\nexport class MouseWheelDirective {\n readonly mouseWheelUp = output<WheelEvent>();\n readonly mouseWheelDown = output<WheelEvent>();\n\n @HostListener('mousewheel', ['$event'])\n onMouseWheelChrome(event: any): void {\n this.mouseWheelFunc(event);\n }\n\n @HostListener('DOMMouseScroll', ['$event'])\n onMouseWheelFirefox(event: any): void {\n this.mouseWheelFunc(event);\n }\n\n @HostListener('wheel', ['$event'])\n onWheel(event: any): void {\n this.mouseWheelFunc(event);\n }\n\n @HostListener('onmousewheel', ['$event'])\n onMouseWheelIE(event: any): void {\n this.mouseWheelFunc(event);\n }\n\n mouseWheelFunc(event: any): void {\n if (window.event) {\n event = window.event;\n }\n\n const delta: number = Math.max(-1, Math.min(1, event.wheelDelta || -event.detail || event.deltaY || event.deltaX));\n // Firefox don't have native support for wheel event, as a result delta values are reverse\n const isWheelMouseUp: boolean = event.wheelDelta ? delta > 0 : delta < 0;\n const isWheelMouseDown: boolean = event.wheelDelta ? delta < 0 : delta > 0;\n if (isWheelMouseUp) {\n this.mouseWheelUp.emit(event);\n } else if (isWheelMouseDown) {\n this.mouseWheelDown.emit(event);\n }\n\n // for IE\n event.returnValue = false;\n\n // for Chrome and Firefox\n if (event.preventDefault) {\n event.preventDefault();\n }\n }\n}\n","import * as d3ease from 'd3-ease';\n\n/** Named easings mapped to d3-ease factories (same as layout morph animations). */\nexport type GraphTransitionEasingName = 'linear' | 'cubicIn' | 'cubicOut' | 'cubicInOut' | 'quadInOut' | 'elasticOut';\n\nexport type GraphLayoutTransitionMode = 'none' | 'instant' | 'tween';\n\n/**\n * Where **previous** `translate(tx,ty)` values come from before the graph model is replaced (layout morph).\n * Applies to **nodes, clusters, and compound nodes** equally (`g.node-group[id]` under `.graph.chart`).\n * Does **not** affect cluster/compound **size** morph: prior width/height for that tween always come from the\n * graph model at capture time, not from the DOM.\n */\nexport type LayoutMorphPreviousSource =\n /** Read `transform` on the graph model only (backward compatible). */\n | 'model-transform'\n /** Read `g.node-group[id]` under `.graph.chart` (excludes minimap). */\n | 'dom-svg'\n /** DOM first; if translate is near zero, use model `position` / `transform` (see `degenerateEpsilon`). */\n | 'dom-with-model-fallback';\n\n/**\n * Options for capturing prior node-group translates when `mode: 'tween'`.\n * With `scope: 'full'`, the same unified rAF tween drives **nodes, clusters, and compounds** (translate lerp plus,\n * for clusters/compounds only, optional width/height lerp from model snapshots).\n */\nexport interface LayoutMorphCapture {\n /**\n * Default `model-transform` (backward compatible).\n * DOM modes read the same `g.node-group` elements used for regular nodes; clusters and compounds use the same\n * `class=\"node-group\"` + `id` wiring in the graph template.\n */\n previousSource?: LayoutMorphPreviousSource;\n /** Used with `dom-with-model-fallback`. Default `1e-3`. */\n degenerateEpsilon?: number;\n /**\n * Order when resolving a node id on the **model** graph for fallback math.\n * Default `['compound', 'cluster', 'node']`.\n */\n modelResolutionOrder?: Array<'compound' | 'cluster' | 'node'>;\n /**\n * After `tick()`, for `scope: 'full'` only, recompute `layoutAnimationTargets` from layout `position` (and\n * `dimension` via `centerNodesOnPositionChange`) for every id already in the target map — **including clusters and\n * compounds**. Runs before transforms are reset to “previous” for the tween, so endpoints match the new layout.\n * Default false.\n */\n syncTargetsFromPositionAfterTick?: boolean;\n /**\n * When true: new **node** ids also get `previousLayoutTransforms` snapped to the current target so they do not tween\n * from a missing or bogus origin; plus degenerate-stable snap for clusters/compounds (see `degenerateEpsilon`).\n * Role changes (same id moving between `nodes` / `clusters` / `compoundNodes`) and **new** cluster/compound ids snap\n * even when this flag is false. Default false.\n */\n snapAddedNodeIds?: boolean;\n}\n\nexport const DEFAULT_LAYOUT_MORPH_CAPTURE: LayoutMorphCapture = {\n previousSource: 'model-transform',\n degenerateEpsilon: 1e-3,\n modelResolutionOrder: ['compound', 'cluster', 'node'],\n syncTargetsFromPositionAfterTick: false,\n snapAddedNodeIds: false\n};\n\nexport function mergeLayoutMorphCapture(partial: LayoutMorphCapture | null | undefined): LayoutMorphCapture {\n return { ...DEFAULT_LAYOUT_MORPH_CAPTURE, ...partial };\n}\n\n/**\n * Layout transition after graph model / layout output changes.\n * - `none` / `instant`: keep prior snapshot for continuity, then snap to final layout in one frame (no rAF morph).\n * - `tween`: interpolate node-group translates and edge paths over `durationMs` with `easing`. Full scope tweens\n * **nodes, clusters, and compounds** together; cluster/compound rects can also tween width/height from prior model\n * dimensions to the new layout’s dimensions (`scope: 'additive'` skips positional and size tweens on groups).\n */\nexport interface GraphLayoutTransition {\n mode: GraphLayoutTransitionMode;\n /** When `tween`, only new ids interpolate in additive mode; stable nodes/edges snap. */\n scope: 'full' | 'additive';\n durationMs: number;\n easing: GraphTransitionEasingName | ((t: number) => number);\n /** When `mode: 'tween'`, how **prior translates** are captured; see {@link LayoutMorphPreviousSource}. */\n morphCapture?: LayoutMorphCapture;\n}\n\nexport const DEFAULT_GRAPH_LAYOUT_TRANSITION: GraphLayoutTransition = {\n mode: 'instant',\n scope: 'full',\n durationMs: 500,\n easing: 'cubicInOut',\n morphCapture: mergeLayoutMorphCapture(undefined)\n};\n\n/** Programmatic viewport pan (panTo, center, minimap, zoomToFit autoCenter): optional eased translation only; zoom scale unchanged. */\nexport interface ViewportTranslationTransition {\n enabled: boolean;\n durationMs: number;\n easing: GraphTransitionEasingName | ((t: number) => number);\n}\n\nexport const DEFAULT_VIEWPORT_TRANSLATION_TRANSITION: ViewportTranslationTransition = {\n enabled: false,\n durationMs: 280,\n easing: 'cubicOut'\n};\n\n/** Optional flair during layout tween only (default off). */\nexport interface LayoutTransitionEffect {\n kind: 'none' | 'perspectiveFlip' | 'rotate';\n /** Max rotation in degrees (perspective flip uses rotateX). */\n peakDegrees?: number;\n /** For `rotate`: pivot in graph coordinates. */\n rotatePivot?: 'graphCenter' | 'viewportCenter' | { nodeId: string };\n}\n\nexport const DEFAULT_LAYOUT_TRANSITION_EFFECT: LayoutTransitionEffect = {\n kind: 'none',\n peakDegrees: 12\n};\n\nexport function resolveGraphTransitionEasing(\n easing: GraphLayoutTransition['easing'] | ViewportTranslationTransition['easing']\n): (t: number) => number {\n if (typeof easing === 'function') {\n return easing;\n }\n switch (easing) {\n case 'linear':\n return d3ease.easeLinear;\n case 'cubicIn':\n return d3ease.easeCubicIn;\n case 'cubicOut':\n return d3ease.easeCubicOut;\n case 'quadInOut':\n return d3ease.easeQuadInOut;\n case 'elasticOut':\n return d3ease.easeElasticOut;\n case 'cubicInOut':\n default:\n return d3ease.easeCubicInOut;\n }\n}\n\n/**\n * Resolves final layout transition from the graph `transitionAfterChanges` input merged with defaults.\n * When unset or empty, returns {@link DEFAULT_GRAPH_LAYOUT_TRANSITION} (`mode: 'instant'`).\n */\nexport function mergeGraphLayoutTransition(\n explicit: Partial<GraphLayoutTransition> | null | undefined\n): GraphLayoutTransition {\n const baseMorph = mergeLayoutMorphCapture(DEFAULT_GRAPH_LAYOUT_TRANSITION.morphCapture);\n const base: GraphLayoutTransition = { ...DEFAULT_GRAPH_LAYOUT_TRANSITION, morphCapture: baseMorph };\n if (explicit != null) {\n const defined = Object.fromEntries(\n Object.entries(explicit).filter(([, v]) => v !== undefined)\n ) as Partial<GraphLayoutTransition>;\n if (Object.keys(defined).length > 0) {\n const { morphCapture: morphPartial, ...rest } = defined;\n return {\n ...base,\n ...rest,\n morphCapture: mergeLayoutMorphCapture(morphPartial ?? base.morphCapture)\n };\n }\n }\n return base;\n}\n\nexport function mergeViewportTransition(\n explicit: Partial<ViewportTranslationTransition> | null | undefined\n): ViewportTranslationTransition {\n if (explicit != null) {\n const defined = Object.fromEntries(\n Object.entries(explicit).filter(([, v]) => v !== undefined)\n ) as Partial<ViewportTranslationTransition>;\n if (Object.keys(defined).length > 0) {\n return { ...DEFAULT_VIEWPORT_TRANSLATION_TRANSITION, ...defined };\n }\n }\n return { ...DEFAULT_VIEWPORT_TRANSLATION_TRANSITION };\n}\n\nexport function mergeLayoutEffect(\n explicit: Partial<LayoutTransitionEffect> | null | undefined\n): LayoutTransitionEffect {\n if (explicit != null) {\n const defined = Object.fromEntries(\n Object.entries(explicit).filter(([, v]) => v !== undefined)\n ) as Partial<LayoutTransitionEffect>;\n if (Object.keys(defined).length > 0) {\n return { ...DEFAULT_LAYOUT_TRANSITION_EFFECT, ...defined };\n }\n }\n return { ...DEFAULT_LAYOUT_TRANSITION_EFFECT };\n}\n","import type { Node } from '../../models/node.model';\n\nconst AXIS_EPS = 1e-3;\n\n/** Dagre `rankdir` string (e.g. LR, TB). */\nexport type DagreRankdir = string | undefined;\n\n/** ELK `elk.direction` value (e.g. RIGHT, DOWN). */\nexport type ElkDirection = string | undefined;\n\nexport interface RankOrderAxes {\n rankAxis: 'x' | 'y';\n orderAxis: 'x' | 'y';\n}\n\n/**\n * Maps Dagre `rankdir` to rank (layer) vs order (within layer) axes, matching\n * {@link DagreNodesOnlyLayout} / graphlib conventions.\n */\nexport function rankOrderAxesFromDagreRankdir(rankdir: DagreRankdir): RankOrderAxes {\n const r = rankdir ?? 'LR';\n const rankAxis: 'x' | 'y' = r === 'BT' || r === 'TB' ? 'y' : 'x';\n const orderAxis: 'x' | 'y' = rankAxis === 'y' ? 'x' : 'y';\n return { rankAxis, orderAxis };\n}\n\n/**\n * Maps ELK layered `elk.direction` to rank vs order axes for drag-time edge ports.\n */\nexport function rankOrderAxesFromElkDirection(direction: ElkDirection): RankOrderAxes {\n const u = (direction ?? 'DOWN').toUpperCase();\n if (u === 'LEFT' || u === 'RIGHT') {\n return { rankAxis: 'x', orderAxis: 'y' };\n }\n return { rankAxis: 'y', orderAxis: 'x' };\n}\n\nexport interface DragEdgePointsOptions {\n curveDistance: number;\n rankAxis: 'x' | 'y';\n orderAxis: 'x' | 'y';\n}\n\n/** Drag-time edge style for Dagre-family layouts (`layoutSettings` on `ngx-graph`). */\nexport type DagreDragEdgeStyle = 'auto' | 'orthogonal' | 'smooth' | 'straight';\n\n/** Resolved style after `auto` heuristic. */\nexport type ResolvedDagreDragStyle = 'orthogonal' | 'smooth' | 'straight';\n\nfunction withRankOrder(\n rankAxis: 'x' | 'y',\n orderAxis: 'x' | 'y',\n rank: number,\n order: number\n): { x: number; y: number } {\n return rankAxis === 'x' ? { x: rank, y: order } : { x: order, y: rank };\n}\n\n/**\n * Rank-facing attachment points (centers projected to box faces toward the other node),\n * matching the endpoints used by {@link edgePointsAfterDrag}.\n */\nexport function rankFacePortPoints(\n source: Node,\n target: Node,\n axes: RankOrderAxes\n): [{ x: number; y: number }, { x: number; y: number }] {\n const { rankAxis, orderAxis } = axes;\n const sp = source.position ?? { x: 0, y: 0 };\n const tp = target.position ?? { x: 0, y: 0 };\n const sw = source.dimension?.width ?? 0;\n const sh = source.dimension?.height ?? 0;\n const tw = target.dimension?.width ?? 0;\n const th = target.dimension?.height ?? 0;\n const halfRankS = (rankAxis === 'x' ? sw : sh) / 2;\n const halfRankT = (rankAxis === 'x' ? tw : th) / 2;\n const dir = sp[rankAxis] <= tp[rankAxis] ? -1 : 1;\n const sRank = sp[rankAxis] - dir * halfRankS;\n const tRank = tp[rankAxis] + dir * halfRankT;\n const sOrd = sp[orderAxis];\n const tOrd = tp[orderAxis];\n return [withRankOrder(rankAxis, orderAxis, sRank, sOrd), withRankOrder(rankAxis, orderAxis, tRank, tOrd)];\n}\n\n/** Axis-aligned Manhattan path (one bend) between two ports. */\nexport function orthogonalManhattanPolyline(\n p0: { x: number; y: number },\n p1: { x: number; y: number }\n): Array<{ x: number; y: number }> {\n if (Math.abs(p0.x - p1.x) < AXIS_EPS || Math.abs(p0.y - p1.y) < AXIS_EPS) {\n return [p0, p1];\n }\n return [p0, { x: p1.x, y: p0.y }, p1];\n}\n\n/** Normalize ELK `elk.edgeRouting` for branching. */\nexport function normalizeElkEdgeRouting(value: string | undefined): 'SPLINES' | 'ORTHOGONAL' | 'POLYLINE' | 'UNKNOWN' {\n const u = (value ?? '').toUpperCase();\n if (u.includes('SPLINE') || u === 'SPLINES') {\n return 'SPLINES';\n }\n if (u === 'ORTHOGONAL') {\n return 'ORTHOGONAL';\n }\n if (u === 'POLYLINE') {\n return 'POLYLINE';\n }\n return 'UNKNOWN';\n}\n\nfunction segmentAxisAligned(a: { x: number; y: number }, b: { x: number; y: number }): boolean {\n return Math.abs(a.x - b.x) < AXIS_EPS || Math.abs(a.y - b.y) < AXIS_EPS;\n}\n\n/**\n * Classify last laid-out polyline for `dragEdgeStyle: 'auto'`.\n */\nexport function inferDragStyleFromPoints(\n points: Array<{ x: number; y: number }> | undefined | null\n): ResolvedDagreDragStyle {\n if (!points || points.length < 2) {\n return 'smooth';\n }\n if (points.length === 2) {\n return 'straight';\n }\n let allOrtho = true;\n for (let i = 0; i < points.length - 1; i++) {\n if (!segmentAxisAligned(points[i], points[i + 1])) {\n allOrtho = false;\n break;\n }\n }\n if (allOrtho) {\n return 'orthogonal';\n }\n if (points.length >= 3) {\n return 'smooth';\n }\n return 'straight';\n}\n\n/**\n * Resolves `auto` using prior `edge.points`, otherwise returns the explicit style.\n */\nexport function resolveDagreDragEdgeStyle(\n dragEdgeStyle: DagreDragEdgeStyle | undefined,\n priorPoints: Array<{ x: number; y: number }> | undefined\n): ResolvedDagreDragStyle {\n const mode = dragEdgeStyle ?? 'auto';\n if (mode === 'auto') {\n return inferDragStyleFromPoints(priorPoints);\n }\n return mode;\n}\n\n/**\n * Builds drag polyline for Dagre / DagreCluster / DagreNodesOnly from resolved style.\n * `smooth` uses {@link edgePointsAfterDrag} (rank-offset interior points) for curved spline-style strokes;\n * `orthogonal` uses {@link orthogonalManhattanPolyline}; `straight` uses two port points only.\n */\nexport function dagreDragPolyline(\n source: Node,\n target: Node,\n axes: RankOrderAxes,\n curveDistance: number,\n style: ResolvedDagreDragStyle\n): Array<{ x: number; y: number }> {\n if (style === 'straight') {\n const [p0, p1] = rankFacePortPoints(source, target, axes);\n return [p0, p1];\n }\n if (style === 'orthogonal') {\n const [p0, p1] = rankFacePortPoints(source, target, axes);\n return orthogonalManhattanPolyline(p0, p1);\n }\n return edgePointsAfterDrag(source, target, { curveDistance, ...axes });\n}\n\n/**\n * Drag-time polyline for ElkLayout from normalized `elk.edgeRouting`.\n * `UNKNOWN` defaults to smooth spline-style segments (four-point rank path).\n */\nexport function elkDragPolyline(\n source: Node,\n target: Node,\n axes: RankOrderAxes,\n curveDistance: number,\n routing: ReturnType<typeof normalizeElkEdgeRouting>\n): Array<{ x: number; y: number }> {\n const mode = routing === 'UNKNOWN' ? 'SPLINES' : routing;\n const [p0, p1] = rankFacePortPoints(source, target, axes);\n if (mode === 'ORTHOGONAL' || mode === 'POLYLINE') {\n return orthogonalManhattanPolyline(p0, p1);\n }\n return edgePointsAfterDrag(source, target, { curveDistance, ...axes });\n}\n\n/**\n * Builds a 4-point polyline for interactive drag updates: ports on the rank-facing\n * sides of each node box (using node centers + dimensions), plus two interior\n * control points so d3 curve generators (e.g. `curveBasis`) have knots to bend.\n *\n * Aligns with {@link DagreNodesOnlyLayout.updateEdge}, with explicit rank/order axes\n * so LR/RL/TB/BT and ELK directions attach on the correct face.\n */\nexport function edgePointsAfterDrag(\n source: Node,\n target: Node,\n options: DragEdgePointsOptions\n): Array<{ x: number; y: number }> {\n const { rankAxis, orderAxis, curveDistance } = options;\n const sp = source.position ?? { x: 0, y: 0 };\n const tp = target.position ?? { x: 0, y: 0 };\n const dir = sp[rankAxis] <= tp[rankAxis] ? -1 : 1;\n const cd = Math.max(0, curveDistance);\n const axes = { rankAxis, orderAxis };\n const [startingPoint, endingPoint] = rankFacePortPoints(source, target, axes);\n\n return [\n startingPoint,\n withRankOrder(rankAxis, orderAxis, startingPoint[rankAxis] - dir * cd, startingPoint[orderAxis]),\n withRankOrder(rankAxis, orderAxis, endingPoint[rankAxis] + dir * cd, endingPoint[orderAxis]),\n endingPoint\n ];\n}\n\n/**\n * When `style` is `linear`, returns only the two port points (straight segment).\n * Otherwise returns the full 4-point drag polyline.\n */\nexport function edgePointsForDragMode(\n source: Node,\n target: Node,\n options: DragEdgePointsOptions & { style?: 'linear' | 'smooth' }\n): Array<{ x: number; y: number }> {\n const pts = edgePointsAfterDrag(source, target, options);\n if (options.style === 'linear') {\n return [pts[0], pts[pts.length - 1]];\n }\n return pts;\n}\n","import { Layout } from '../../models/layout.model';\nimport { Graph } from '../../models/graph.model';\nimport { id } from '../../utils/id';\nimport * as dagre from 'dagre';\nimport { Edge } from '../../models/edge.model';\nimport {\n dagreDragPolyline,\n rankOrderAxesFromDagreRankdir,\n resolveDagreDragEdgeStyle,\n type DagreDragEdgeStyle\n} from './edge-geometry';\n\nexport enum Orientation {\n LEFT_TO_RIGHT = 'LR',\n RIGHT_TO_LEFT = 'RL',\n TOP_TO_BOTTOM = 'TB',\n BOTTOM_TO_TOM = 'BT'\n}\nexport enum Alignment {\n CENTER = 'C',\n UP_LEFT = 'UL',\n UP_RIGHT = 'UR',\n DOWN_LEFT = 'DL',\n DOWN_RIGHT = 'DR'\n}\n\nexport interface DagreSettings {\n orientation?: Orientation;\n marginX?: number;\n marginY?: number;\n edgePadding?: number;\n rankPadding?: number;\n nodePadding?: number;\n align?: Alignment;\n acyclicer?: 'greedy' | undefined;\n ranker?: 'network-simplex' | 'tight-tree' | 'longest-path';\n multigraph?: boolean;\n compound?: boolean;\n /** Offset along rank axis for drag-time edge control points (default 20). */\n curveDistance?: number;\n /**\n * How to rebuild edge polylines while dragging: `auto` infers from the last laid-out `edge.points`,\n * or set explicitly (`orthogonal` / `smooth` / `straight`).\n */\n dragEdgeStyle?: DagreDragEdgeStyle;\n}\n\nexport class DagreLayout implements Layout {\n defaultSettings: DagreSettings = {\n orientation: Orientation.LEFT_TO_RIGHT,\n marginX: 20,\n marginY: 20,\n edgePadding: 100,\n rankPadding: 100,\n nodePadding: 50,\n curveDistance: 20,\n multigraph: true,\n compound: true\n };\n settings: DagreSettings = {};\n\n dagreGraph: any;\n dagreNodes: any;\n dagreEdges: any;\n\n run(graph: Graph): Graph {\n this.createDagreGraph(graph);\n dagre.layout(this.dagreGraph);\n\n graph.edgeLabels = this.dagreGraph._edgeLabels;\n\n for (const dagreNodeId in this.dagreGraph._nodes) {\n const dagreNode = this.dagreGraph._nodes[dagreNodeId];\n const node = graph.nodes.find(n => n.id === dagreNode.id);\n node.position = {\n x: dagreNode.x,\n y: dagreNode.y\n };\n node.dimension = {\n width: dagreNode.width,\n height: dagreNode.height\n };\n }\n\n return graph;\n }\n\n updateEdge(graph: Graph, edge: Edge): Graph {\n const sourceNode = graph.nodes.find(n => n.id === edge.source);\n const targetNode = graph.nodes.find(n => n.id === edge.target);\n if (!sourceNode?.position || !targetNode?.position) {\n return graph;\n }\n const settings = Object.assign({}, this.defaultSettings, this.settings);\n const axes = rankOrderAxesFromDagreRankdir(settings.orientation);\n const curveDistance = settings.curveDistance ?? 20;\n const resolved = resolveDagreDragEdgeStyle(settings.dragEdgeStyle ?? 'auto', edge.points);\n edge.points = dagreDragPolyline(sourceNode, targetNode, axes, curveDistance, resolved);\n return graph;\n }\n\n createDagreGraph(graph: Graph): any {\n const settings = Object.assign({}, this.defaultSettings, this.settings);\n this.dagreGraph = new dagre.graphlib.Graph({ compound: settings.compound, multigraph: settings.multigraph });\n\n this.dagreGraph.setGraph({\n rankdir: settings.orientation,\n marginx: settings.marginX,\n marginy: settings.marginY,\n edgesep: settings.edgePadding,\n ranksep: settings.rankPadding,\n nodesep: settings.nodePadding,\n align: settings.align,\n acyclicer: settings.acyclicer,\n ranker: settings.ranker,\n multigraph: settings.multigraph,\n compound: settings.compound\n });\n\n // Default to assigning a new object as a label for each new edge.\n this.dagreGraph.setDefaultEdgeLabel(() => {\n return {\n /* empty */\n };\n });\n\n this.dagreNodes = graph.nodes.map(n => {\n const node: any = Object.assign({}, n);\n node.width = n.dimension.width;\n node.height = n.dimension.height;\n node.x = n.position.x;\n node.y = n.position.y;\n return node;\n });\n\n this.dagreEdges = graph.edges.map(l => {\n const newLink: any = Object.assign({}, l);\n if (!newLink.id) {\n newLink.id = id();\n }\n return newLink;\n });\n\n for (const node of this.dagreNodes) {\n if (!node.width) {\n node.width = 20;\n }\n if (!node.height) {\n node.height = 30;\n }\n\n // update dagre\n this.dagreGraph.setNode(node.id, node);\n }\n\n // update dagre\n for (const edge of this.dagreEdges) {\n if (settings.multigraph) {\n this.dagreGraph.setEdge(edge.source, edge.target, edge, edge.id);\n } else {\n this.dagreGraph.setEdge(edge.source, edge.target);\n }\n }\n\n return this.dagreGraph;\n }\n}\n","import { Layout } from '../../models/layout.model';\nimport { Graph } from '../../models/graph.model';\nimport { id } from '../../utils/id';\nimport * as dagre from 'dagre';\nimport { Edge } from '../../models/edge.model';\nimport { Node, ClusterNode } from '../../models/node.model';\nimport { DagreSettings, Orientation } from './dagre';\nimport { dagreDragPolyline, rankOrderAxesFromDagreRankdir, resolveDagreDragEdgeStyle } from './edge-geometry';\n\nexport class DagreClusterLayout implements Layout {\n defaultSettings: DagreSettings = {\n orientation: Orientation.LEFT_TO_RIGHT,\n marginX: 20,\n marginY: 20,\n edgePadding: 100,\n rankPadding: 100,\n nodePadding: 50,\n curveDistance: 20,\n multigraph: true,\n compound: true\n };\n settings: DagreSettings = {};\n\n dagreGraph: any;\n dagreNodes: Node[];\n dagreClusters: ClusterNode[];\n dagreEdges: any;\n\n run(graph: Graph): Graph {\n this.createDagreGraph(graph);\n dagre.layout(this.dagreGraph);\n\n graph.edgeLabels = this.dagreGraph._edgeLabels;\n\n const dagreToOutput = node => {\n const dagreNode = this.dagreGraph._nodes[node.id];\n return {\n ...node,\n position: {\n x: dagreNode.x,\n y: dagreNode.y\n },\n dimension: {\n width: dagreNode.width,\n height: dagreNode.height\n }\n };\n };\n\n graph.clusters = (graph.clusters || []).map(dagreToOutput);\n graph.nodes = graph.nodes.map(dagreToOutput);\n\n return graph;\n }\n\n updateEdge(graph: Graph, edge: Edge): Graph {\n const sourceNode = graph.nodes.find(n => n.id === edge.source);\n const targetNode = graph.nodes.find(n => n.id === edge.target);\n if (!sourceNode?.position || !targetNode?.position) {\n return graph;\n }\n const settings = Object.assign({}, this.defaultSettings, this.settings);\n const axes = rankOrderAxesFromDagreRankdir(settings.orientation);\n const curveDistance = settings.curveDistance ?? 20;\n const resolved = resolveDagreDragEdgeStyle(settings.dragEdgeStyle ?? 'auto', edge.points);\n edge.points = dagreDragPolyline(sourceNode, targetNode, axes, curveDistance, resolved);\n return graph;\n }\n\n createDagreGraph(graph: Graph): any {\n const settings = Object.assign({}, this.defaultSettings, this.settings);\n this.dagreGraph = new dagre.graphlib.Graph({ compound: settings.compound, multigraph: settings.multigraph });\n this.dagreGraph.setGraph({\n rankdir: settings.orientation,\n marginx: settings.marginX,\n marginy: settings.marginY,\n edgesep: settings.edgePadding,\n ranksep: settings.rankPadding,\n nodesep: settings.nodePadding,\n align: settings.align,\n acyclicer: settings.acyclicer,\n ranker: settings.ranker,\n multigraph: settings.multigraph,\n compound: settings.compound\n });\n\n // Default to assigning a new object as a label for each new edge.\n this.dagreGraph.setDefaultEdgeLabel(() => {\n return {\n /* empty */\n };\n });\n\n this.dagreNodes = graph.nodes.map((n: Node) => {\n const node: any = Object.assign({}, n);\n node.width = n.dimension.width;\n node.height = n.dimension.height;\n node.x = n.position.x;\n node.y = n.position.y;\n return node;\n });\n\n this.dagreClusters = graph.clusters || [];\n\n this.dagreEdges = graph.edges.map(l => {\n const newLink: any = Object.assign({}, l);\n if (!newLink.id) {\n newLink.id = id();\n }\n return newLink;\n });\n\n for (const node of this.dagreNodes) {\n this.dagreGraph.setNode(node.id, node);\n }\n\n for (const cluster of this.dagreClusters) {\n this.dagreGraph.setNode(cluster.id, cluster);\n cluster.childNodeIds.forEach(childNodeId => {\n this.dagreGraph.setParent(childNodeId, cluster.id);\n });\n }\n\n // update dagre\n for (const edge of this.dagreEdges) {\n if (settings.multigraph) {\n this.dagreGraph.setEdge(edge.source, edge.target, edge, edge.id);\n } else {\n this.dagreGraph.setEdge(edge.source, edge.target);\n }\n }\n\n return this.dagreGraph;\n }\n}\n","import { Layout } from '../../models/layout.model';\nimport { Graph } from '../../models/graph.model';\nimport { id } from '../../utils/id';\nimport * as dagre from 'dagre';\nimport { Edge } from '../../models/edge.model';\nimport { DagreSettings, Orientation } from './dagre';\nimport { dagreDragPolyline, rankOrderAxesFromDagreRankdir, resolveDagreDragEdgeStyle } from './edge-geometry';\n\nexport interface DagreNodesOnlySettings extends DagreSettings {\n curveDistance?: number;\n}\n\nconst DEFAULT_EDGE_NAME = '\\x00';\nconst GRAPH_NODE = '\\x00';\nconst EDGE_KEY_DELIM = '\\x01';\n\nexport class DagreNodesOnlyLayout implements Layout {\n defaultSettings: DagreNodesOnlySettings = {\n orientation: Orientation.LEFT_TO_RIGHT,\n marginX: 20,\n marginY: 20,\n edgePadding: 100,\n rankPadding: 100,\n nodePadding: 50,\n curveDistance: 20,\n multigraph: true,\n compound: true\n };\n settings: DagreNodesOnlySettings = {};\n\n dagreGraph: any;\n dagreNodes: any;\n dagreEdges: any;\n\n run(graph: Graph): Graph {\n this.createDagreGraph(graph);\n dagre.layout(this.dagreGraph);\n\n graph.edgeLabels = this.dagreGraph._edgeLabels;\n\n for (const dagreNodeId in this.dagreGraph._nodes) {\n const dagreNode = this.dagreGraph._nodes[dagreNodeId];\n const node = graph.nodes.find(n => n.id === dagreNode.id);\n node.position = {\n x: dagreNode.x,\n y: dagreNode.y\n };\n node.dimension = {\n width: dagreNode.width,\n height: dagreNode.height\n };\n }\n for (const edge of graph.edges) {\n this.updateEdge(graph, edge);\n }\n\n return graph;\n }\n\n updateEdge(graph: Graph, edge: Edge): Graph {\n const sourceNode = graph.nodes.find(n => n.id === edge.source);\n const targetNode = graph.nodes.find(n => n.id === edge.target);\n if (!sourceNode?.position || !targetNode?.position) {\n return graph;\n }\n const settings = Object.assign({}, this.defaultSettings, this.settings);\n const axes = rankOrderAxesFromDagreRankdir(settings.orientation);\n const curveDistance = settings.curveDistance ?? this.defaultSettings.curveDistance ?? 20;\n const resolved = resolveDagreDragEdgeStyle(settings.dragEdgeStyle ?? 'auto', edge.points);\n edge.points = dagreDragPolyline(sourceNode, targetNode, axes, curveDistance, resolved);\n const edgeLabelId = `${edge.source}${EDGE_KEY_DELIM}${edge.target}${EDGE_KEY_DELIM}${DEFAULT_EDGE_NAME}`;\n const matchingEdgeLabel = graph.edgeLabels[edgeLabelId];\n if (matchingEdgeLabel) {\n matchingEdgeLabel.points = edge.points;\n }\n return graph;\n }\n\n createDagreGraph(graph: Graph): any {\n const settings = Object.assign({}, this.defaultSettings, this.settings);\n this.dagreGraph = new dagre.graphlib.Graph({ compound: settings.compound, multigraph: settings.multigraph });\n this.dagreGraph.setGraph({\n rankdir: settings.orientation,\n marginx: settings.marginX,\n marginy: settings.marginY,\n edgesep: settings.edgePadding,\n ranksep: settings.rankPadding,\n nodesep: settings.nodePadding,\n align: settings.align,\n acyclicer: settings.acyclicer,\n ranker: settings.ranker,\n multigraph: settings.multigraph,\n compound: settings.compound\n });\n\n // Default to assigning a new object as a label for each new edge.\n this.dagreGraph.setDefaultEdgeLabel(() => {\n return {\n /* empty */\n };\n });\n\n this.dagreNodes = graph.nodes.map(n => {\n const node: any = Object.assign({}, n);\n node.width = n.dimension.width;\n node.height = n.dimension.height;\n node.x = n.position.x;\n node.y = n.position.y;\n return node;\n });\n\n this.dagreEdges = graph.edges.map(l => {\n const newLink: any = Object.assign({}, l);\n if (!newLink.id) {\n newLink.id = id();\n }\n return newLink;\n });\n\n for (const node of this.dagreNodes) {\n if (!node.width) {\n node.width = 20;\n }\n if (!node.height) {\n node.height = 30;\n }\n\n // update dagre\n this.dagreGraph.setNode(node.id, node);\n }\n\n // update dagre\n for (const edge of this.dagreEdges) {\n if (settings.multigraph) {\n this.dagreGraph.setEdge(edge.source, edge.target, edge, edge.id);\n } else {\n this.dagreGraph.setEdge(edge.source, edge.target);\n }\n }\n\n return this.dagreGraph;\n }\n}\n","import { Layout } from '../../models/layout.model';\nimport { Graph } from '../../models/graph.model';\nimport { Node } from '../../models/node.model';\nimport { id } from '../../utils/id';\nimport { forceCollide, forceLink, forceManyBody, forceSimulation } from 'd3-force';\nimport { Edge } from '../../models/edge.model';\nimport { Observable, Subject } from 'rxjs';\nimport { NodePosition } from '../../models';\n\nexport interface D3ForceDirectedSettings {\n force?: any;\n forceLink?: any;\n}\nexport interface D3Node {\n id?: string;\n x: number;\n y: number;\n width?: number;\n height?: number;\n fx?: number;\n fy?: number;\n}\nexport interface D3Edge {\n source: string | D3Node;\n target: string | D3Node;\n midPoint: NodePosition;\n}\nexport interface D3Graph {\n nodes: D3Node[];\n edges: D3Edge[];\n}\nexport interface MergedNode extends D3Node, Node {\n id: string;\n}\n\nexport function toD3Node(maybeNode: string | D3Node): D3Node {\n if (typeof maybeNode === 'string') {\n return {\n id: maybeNode,\n x: 0,\n y: 0\n };\n }\n return maybeNode;\n}\n\nexport class D3ForceDirectedLayout implements Layout {\n defaultSettings: D3ForceDirectedSettings = {\n force: forceSimulation<any>().force('charge', forceManyBody().strength(-150)).force('collide', forceCollide(5)),\n forceLink: forceLink<any, any>()\n .id(node => node.id)\n .distance(() => 100)\n };\n settings: D3ForceDirectedSettings = {};\n\n inputGraph: Graph;\n outputGraph: Graph;\n d3Graph: D3Graph;\n outputGraph$: Subject<Graph> = new Subject();\n\n draggingStart: { x: number; y: number };\n\n run(graph: Graph): Observable<Graph> {\n this.inputGraph = graph;\n this.d3Graph = {\n nodes: [...this.inputGraph.nodes.map(n => ({ ...n }))] as any,\n edges: [...this.inputGraph.edges.map(e => ({ ...e }))] as any\n };\n this.outputGraph = {\n nodes: [],\n edges: [],\n edgeLabels: []\n };\n this.outputGraph$.next(this.outputGraph);\n this.settings = Object.assign({}, this.defaultSettings, this.settings);\n if (this.settings.force) {\n this.settings.force\n .nodes(this.d3Graph.nodes)\n .force('link', this.settings.forceLink.links(this.d3Graph.edges))\n .alpha(0.5)\n .restart()\n .on('tick', () => {\n this.outputGraph$.next(this.d3GraphToOutputGraph(this.d3Graph));\n });\n }\n\n return this.outputGraph$.asObservable();\n }\n\n updateEdge(graph: Graph, edge: Edge): Observable<Graph> {\n const settings = Object.assign({}, this.defaultSettings, this.settings);\n if (settings.force) {\n settings.force\n .nodes(this.d3Graph.nodes)\n .force('link', settings.forceLink.links(this.d3Graph.edges))\n .alpha(0.5)\n .restart()\n .on('tick', () => {\n this.outputGraph$.next(this.d3GraphToOutputGraph(this.d3Graph));\n });\n }\n\n return this.outputGraph$.asObservable();\n }\n\n d3GraphToOutputGraph(d3Graph: D3Graph): Graph {\n this.outputGraph.nodes = this.d3Graph.nodes.map((node: MergedNode) => ({\n ...node,\n id: node.id || id(),\n position: {\n x: node.x,\n y: node.y\n },\n dimension: {\n width: (node.dimension && node.dimension.width) || 20,\n height: (node.dimension && node.dimension.height) || 20\n },\n transform: `translate(${node.x - ((node.dimension && node.dimension.width) || 20) / 2 || 0}, ${\n node.y - ((node.dimension && node.dimension.height) || 20) / 2 || 0\n })`\n }));\n\n this.outputGraph.edges = this.d3Graph.edges.map(edge => ({\n ...edge,\n source: toD3Node(edge.source).id,\n target: toD3Node(edge.target).id,\n points: [\n {\n x: toD3Node(edge.source).x,\n y: toD3Node(edge.source).y\n },\n {\n x: toD3Node(edge.target).x,\n y: toD3Node(edge.target).y\n }\n ]\n }));\n\n this.outputGraph.edgeLabels = this.outputGraph.edges;\n return this.outputGraph;\n }\n\n onDragStart(draggingNode: Node, $event: MouseEvent): void {\n this.settings.force.alphaTarget(0.3).restart();\n const node = this.d3Graph.nodes.find(d3Node => d3Node.id === draggingNode.id);\n if (!node) {\n return;\n }\n this.draggingStart = { x: $event.x - node.x, y: $event.y - node.y };\n node.fx = $event.x - this.draggingStart.x;\n node.fy = $event.y - this.draggingStart.y;\n }\n\n onDrag(draggingNode: Node, $event: MouseEvent): void {\n if (!draggingNode) {\n return;\n }\n const node = this.d3Graph.nodes.find(d3Node => d3Node.id === draggingNode.id);\n if (!node) {\n return;\n }\n node.fx = $event.x - this.draggingStart.x;\n node.fy = $event.y - this.draggingStart.y;\n }\n\n onDragEnd(draggingNode: Node, $event: MouseEvent): void {\n if (!draggingNode) {\n return;\n }\n const node = this.d3Graph.nodes.find(d3Node => d3Node.id === draggingNode.id);\n if (!node) {\n return;\n }\n\n this.settings.force.alphaTarget(0);\n node.fx = undefined;\n node.fy = undefined;\n }\n}\n","import { Layout } from '../../models/layout.model';\nimport { Graph } from '../../models/graph.model';\nimport { Node, ClusterNode } from '../../models/node.model';\nimport { id } from '../../utils/id';\nimport { d3adaptor, ID3StyleLayoutAdaptor, Layout as ColaLayout, Group, InputNode, Link, Rectangle } from 'webcola';\nimport * as d3Dispatch from 'd3-dispatch';\nimport * as d3Force from 'd3-force';\nimport * as d3Timer from 'd3-timer';\nimport { Edge } from '../../models/edge.model';\nimport { Observable, Subject } from 'rxjs';\nimport { ViewDimensions } from '../../utils/view-dimensions.helper';\n\nexport interface ColaForceDirectedSettings {\n force?: ColaLayout & ID3StyleLayoutAdaptor;\n forceModifierFn?: (force: ColaLayout & ID3StyleLayoutAdaptor) => ColaLayout & ID3StyleLayoutAdaptor;\n onTickListener?: (internalGraph: ColaGraph) => void;\n viewDimensions?: ViewDimensions;\n}\nexport interface ColaGraph {\n groups: Group[];\n nodes