Skip to content

Commit 10eb732

Browse files
Fixed computation of the optimal set of feedback edges that must be removed to eliminate all cycles in a family structure.
2 parents 0269cc9 + 868a71e commit 10eb732

File tree

45 files changed

+399
-219
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+399
-219
lines changed

apireference/algorithms.md

Lines changed: 117 additions & 92 deletions
Large diffs are not rendered by default.

apireference/functions.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,14 @@ Callback function to iterate over pairs of crossing rectangles
111111

112112
## <a name="getFamilyLoops" id="getFamilyLoops">getFamilyLoops</a>
113113

114-
This function finds [optimal collection of feedback edges](https://en.wikipedia.org/wiki/Feedback_arc_set) needed to be cut in order to eliminate loops in family structure.
114+
Computes the optimal set of feedback edges that must be removed to eliminate all cycles in a family structure. This corresponds to [finding a minimum feedback arc set in the directed graph](https://en.wikipedia.org/wiki/Feedback_arc_set) formed by the family relationships. The function analyzes the directed dependencies inside the family, detects all cycles, and returns the smallest collection of edges whose removal makes the structure acyclic.
115115

116-
Returns: `Edge[]` - returns optimal collection of feedback loops
116+
Returns: `Edge[]` - the minimal set of edges whose removal breaks all cycles.
117117

118118
| Param | Type | Default | Description |
119119
| --- | --- | --- | --- |
120-
| `family` | Family | `` | Family structure |
120+
| `family` | Family | `` | - The family structure represented as a directed graph. |
121+
| `debug` | boolean | `false` | - If true, enables diagnostic output. |
121122

122123
## <a name="getGreen" id="getGreen">getGreen</a>
123124

changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#### Version 6.6.2
2+
* Fixed computation of the optimal set of feedback edges that must be removed to eliminate all cycles in a family structure.
13
#### Version 6.6.0
24
* Added templated end points to connector annotations. Added `showFromEndpoint`, `showToEndpoint`, `context` properties to to `ConnectorAnnotationConfig`. Added `onEndPointRender`, `showEndPoints`, `endPointSize`, `endPointCornerRadius`, `endPointOpacity` properties to `OrgConfig`. Added drag and drop sample creating and editing connector annotations.
35
* Changed CSS styles for connector annotations, so when they are rendered over diagram nodes, they should be transparent for mouse events.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "basicprimitives",
33
"sideEffects": false,
44
"homepage": "https://www.basicprimitives.com/",
5-
"version": "6.6.1",
5+
"version": "6.6.2",
66
"author": "Basic Primitives Inc. <support@basicprimitives.com> (https://www.basicprimitives.com)",
77
"description": "Basic Primitives Diagrams for JavaScript - data visualization components library that implements organizational chart and multi-parent dependency diagrams, contains implementations of JavaScript Controls and PDF rendering plugins.",
88
"repository": {

src/algorithms/Graph.js

Lines changed: 114 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import Tree from './Tree';
22
import FibonacciHeap from './FibonacciHeap';
3+
34
/**
4-
* Creates graph structure
5+
* Creates an undirected graph structure backed by adjacency lists.
6+
* Each edge stores a context object provided by the caller.
7+
*
58
* @class Graph
6-
*
7-
* @returns {Graph} Returns graph object
9+
* @returns {Graph} A new graph instance
810
*/
911
export default function Graph() {
1012
var _edges = {},
1113
MAXIMUMTOTALWEIGHT = 1,
1214
MINIMUMWEIGHT = 2;
1315

1416
/**
15-
* Adds edge to the graph
16-
* @param {string} from The id of the start node
17-
* @param {string} to The id of the end node
18-
* @param {object} edge The edge contextual object
17+
* Adds an undirected edge between two nodes.
18+
* If the edge already exists, it will not be replaced.
19+
*
20+
* @param {string} from The starting node id
21+
* @param {string} to The ending node id
22+
* @param {object} edge The edge context object
1923
*/
2024
function addEdge(from, to, edge) {
2125
if ((_edges[from] == null || _edges[from][to] == null) && edge != null) {
@@ -33,11 +37,11 @@ export default function Graph() {
3337
}
3438

3539
/**
36-
* Returns edge context object
37-
*
38-
* @param {string} from The edge's from node id
39-
* @param {string} to The edge's to node id
40-
* @returns {object} The edge's context object
40+
* Retrieves the stored edge context object for a given pair of nodes.
41+
*
42+
* @param {string} from The source node id
43+
* @param {string} to The target node id
44+
* @returns {object|null} The edge's context object, or null if none exists
4145
*/
4246
function edge(from, to) {
4347
var result = null;
@@ -48,29 +52,54 @@ export default function Graph() {
4852
}
4953

5054
/**
51-
* Returns true if node exists in the graph
52-
*
55+
* Checks whether a node exists in the graph.
56+
*
5357
* @param {string} from The node id
54-
* @returns {boolean} Returns true if node exists
58+
* @returns {boolean} True if the node is present in the graph
5559
*/
5660
function hasNode(from) {
5761
return _edges.hasOwnProperty(from);
5862
}
5963

6064
/**
61-
* Callback for iterating edges of the graph's node
62-
*
65+
* Callback invoked for each edge during iteration.
66+
*
6367
* @callback onEdgeCallback
64-
* @param {string} to The neighboring node id
65-
* @param {Object} edge The edge's context object
68+
* @param {string} from The start node id
69+
* @param {string} to The end node id
70+
* @param {Object} edge The edge context object
6671
*/
6772

6873
/**
69-
* Loop edges of the node
70-
*
71-
* @param {object} thisArg The callback function invocation context
72-
* @param {string} itemid The node id
73-
* @param {onEdgeCallback} onEdge A callback function to call for every edge of the node
74+
* Iterates over all edges in the graph.
75+
*
76+
* @param {object} thisArg Execution context for the callback
77+
* @param {onEdgeCallback} onEdge Callback invoked for each edge
78+
*/
79+
function loopEdges(thisArg, onEdge) {
80+
var neighbours, fromKey, toKey;
81+
if (onEdge != null) {
82+
for (fromKey in _edges) {
83+
if (_edges.hasOwnProperty(fromKey)) {
84+
neighbours = _edges[fromKey];
85+
if (neighbours != null) {
86+
for (toKey in neighbours) {
87+
if (neighbours.hasOwnProperty(toKey)) {
88+
onEdge.call(thisArg, fromKey, toKey, neighbours[toKey]);
89+
}
90+
}
91+
}
92+
}
93+
}
94+
}
95+
}
96+
97+
/**
98+
* Iterates over all edges connected to a specific node.
99+
*
100+
* @param {object} thisArg Execution context for the callback
101+
* @param {string} itemid The node id whose edges to iterate
102+
* @param {onEdgeCallback} onEdge Callback invoked for each connected edge
74103
*/
75104
function loopNodeEdges(thisArg, itemid, onEdge) {
76105
var neighbours, neighbourKey;
@@ -87,20 +116,20 @@ export default function Graph() {
87116
}
88117

89118
/**
90-
* Callback function for iterating graphs nodes
91-
*
119+
* Callback for node iteration functions.
120+
*
92121
* @callback onNodeCallback
93-
* @param {string} to The next neighboring node id
94-
* @returns {boolean} Returns true to break loop
122+
* @param {string} to The node id visited
123+
* @returns {boolean} Return true to stop traversal early
95124
*/
96125

97126
/**
98-
* Loop nodes of the graph
99-
*
100-
* @param {object} thisArg The callback function invocation context
101-
* @param {string} [itemid=undefined] The optional start node id. If start node is undefined,
102-
* function loops graphs node starting from first available node
103-
* @param {onNodeCallback} onItem A callback function to be called for every neighboring node
127+
* Traverses all connected nodes starting from the given node.
128+
* If no start node is provided, traversal begins with the first available node.
129+
*
130+
* @param {object} thisArg Execution context for the callback
131+
* @param {string} [startNode] Optional starting node id
132+
* @param {onNodeCallback} onItem Callback invoked for each visited node
104133
*/
105134
function loopNodes(thisArg, startNode, onItem) {
106135
var processed = {};
@@ -152,21 +181,22 @@ export default function Graph() {
152181
}
153182

154183
/**
155-
* Callback for finding edge weight
156-
*
184+
* Returns edge weight used in certain algorithms.
185+
*
157186
* @callback getGraphEdgeWeightCallback
158187
* @param {object} edge The edge context object
159-
* @param {string} fromItem The edge's start node id
160-
* @param {string} toItem The edge's end node id
161-
* @returns {number} Returns weight of the edge
188+
* @param {string} fromItem The start node id
189+
* @param {string} toItem The end node id
190+
* @returns {number} The weight of the edge
162191
*/
163192

164193
/**
165-
* Get maximum spanning tree. Graph may have disconnected sub graphs, so start node is necessary.
166-
*
167-
* @param {string} startNode The node to start searching for maximum spanning tree. Graph is not necessary connected
168-
* @param {getGraphEdgeWeightCallback} getWeightFunc Callback function to get weight of an edge.
169-
* @returns {tree} Returns tree structure containing maximum spanning tree of the graph
194+
* Computes a maximum spanning tree using a priority queue.
195+
* The graph may be disconnected; a start node is required.
196+
*
197+
* @param {string} startNode Node to begin spanning tree search
198+
* @param {getGraphEdgeWeightCallback} getWeightFunc Function returning edge weight
199+
* @returns {tree} A Tree structure containing the maximum spanning tree
170200
*/
171201
function getSpanningTree(startNode, getWeightFunc) {
172202
var result = Tree(),
@@ -362,22 +392,21 @@ export default function Graph() {
362392
}
363393

364394
/**
365-
* Callback for returning optimal connection path for every end node.
366-
*
395+
* Callback invoked when a full path has been reconstructed.
396+
*
367397
* @callback onPathFoundCallback
368-
* @param {string[]} path An array of connection path node ids.
369-
* @param {string} to The end node id, the connection path is found for.
398+
* @param {string[]} path The node sequence forming the path
399+
* @param {string} to The end node id
370400
*/
371401

372402
/**
373-
* Get shortest path between two nodes in graph. The start and the end nodes are supposed to have connection path.
374-
*
375-
* @param {object} thisArg The callback function invocation context
376-
* @param {string} startNode The start node id
377-
* @param {string[]} endNodes The array of end node ids.
378-
* @param {getGraphEdgeWeightCallback} getWeightFunc Callback function to get weight of an edge.
379-
* @param {onPathFoundCallback} onPathFound A callback function to be called for every end node
380-
* with the optimal connection path
403+
* Computes the shortest paths from a start node to one or more target nodes.
404+
*
405+
* @param {object} thisArg Execution context for callbacks
406+
* @param {string} startNode Starting node id
407+
* @param {string[]} endNodes Target node ids
408+
* @param {getGraphEdgeWeightCallback} getWeightFunc Optional function returning edge weight
409+
* @param {onPathFoundCallback} onPathFound Callback invoked when a target path is found
381410
*/
382411
function getShortestPath(thisArg, startNode, endNodes, getWeightFunc, onPathFound) {
383412
var margin = FibonacciHeap(false),
@@ -457,22 +486,23 @@ export default function Graph() {
457486
}
458487

459488
/**
460-
* Callback for iterating path edges
461-
*
489+
* Callback used for filtering usable edges during DFS path search.
490+
*
462491
* @callback onPathEdgeCallback
463-
* @param {string} from The from node id
464-
* @param {string} to The to node id
465-
* @param {Object} edge The edge's context object
466-
* @returns {boolean} Returns true if edge is usable
492+
* @param {string} from The start node id
493+
* @param {string} to The end node id
494+
* @param {Object} edge The edge context object
495+
* @returns {boolean} True if the edge may be used in traversal
467496
*/
468497

469498
/**
470-
* Search any path from node to node using depth first search
471-
*
472-
* @param {object} thisArg The callback function invocation context
473-
* @param {string} startNode The start node id
474-
* @param {string} endNode The end node id.
475-
* @param {onPathEdgeCallback} onEdge A callback function to call for every edge of the node
499+
* Finds any path between two nodes using depth-first search.
500+
*
501+
* @param {object} thisArg Execution context for callbacks
502+
* @param {string} startNode The start node id
503+
* @param {string} endNode The end node id
504+
* @param {onPathEdgeCallback} onEdge Callback deciding whether an edge is usable
505+
* @returns {string[]} Array of node ids forming the found path
476506
*/
477507
function dfsPath(thisArg, startNode, endNode, onEdge) {
478508
var margin = [],
@@ -522,11 +552,13 @@ export default function Graph() {
522552
}
523553

524554
/**
525-
* Get Level Graph starting with `startNode`
526-
*
527-
* @param {object} thisArg The callback function invocation context
528-
* @param {string} startNode The start node id
529-
* @param {onPathEdgeCallback} onEdge A callback function to call for every edge of the graph
555+
* Computes a level graph starting from a given node.
556+
* Levels are assigned via BFS using only edges allowed by the callback.
557+
*
558+
* @param {object} thisArg Execution context
559+
* @param {string} startNode The start node id
560+
* @param {onPathEdgeCallback} onEdge Callback deciding if an edge is valid to traverse
561+
* @returns {Graph} A new graph representing the level structure
530562
*/
531563
function getLevelGraph(thisArg, startNode, onEdge) {
532564
var level = {},
@@ -560,7 +592,7 @@ export default function Graph() {
560592
}
561593

562594
// Create level graph, copy existing edges to the new graph
563-
var levelGraph = Graph();
595+
var levelGraph = Graph();
564596
for (currentNode in _edges) {
565597
if (level.hasOwnProperty(currentNode)) {
566598
currentLevel = level[currentNode];
@@ -580,12 +612,13 @@ export default function Graph() {
580612
}
581613

582614
/**
583-
* Depth first search loop
584-
*
585-
* @param {object} thisArg The callback function invocation context
586-
* @param {string} startNode The start node id
587-
* @param {onPathEdgeCallback} onEdge A callback function to call for every edge of the graph
588-
* @param {onNodeCallback} onNode A callback function to be called for every neighboring node
615+
* Performs a depth-first traversal starting at a node.
616+
* Edge usability is determined by the onEdge callback.
617+
*
618+
* @param {object} thisArg Execution context
619+
* @param {string} startNode The start node id
620+
* @param {onPathEdgeCallback} onEdge Callback deciding edge usability
621+
* @param {onNodeCallback} onNode Callback invoked for each newly visited node
589622
*/
590623
function dfsLoop(thisArg, startNode, onEdge, onNode) {
591624
var margin = [],
@@ -625,6 +658,7 @@ export default function Graph() {
625658
edge: edge,
626659
hasNode: hasNode,
627660
loopNodes: loopNodes,
661+
loopEdges: loopEdges,
628662
loopNodeEdges: loopNodeEdges,
629663
getSpanningTree: getSpanningTree,
630664
getTotalWeightGrowthSequence: getTotalWeightGrowthSequence,

src/algorithms/Graph.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,36 @@ test('dfsLoop searches graph nodes using depth first order', () => {
295295

296296
expect(result).toBe(true);
297297
});
298+
299+
test('loopEdges function test', () => {
300+
var items = [
301+
{ from: 'A', to: 'B' }, { from: 'A', to: 'C' }, { from: 'A', to: 'D' },
302+
{ from: 'B', to: 'E' },
303+
{ from: 'C', to: 'E' }, { from: 'C', to: 'D' },
304+
{ from: 'D', to: 'F' }, { from: 'D', to: 'J' },
305+
{ from: 'E', to: 'Z' },
306+
{ from: 'Z', to: 'F' }
307+
];
308+
309+
var graph = Graph();
310+
for (var index = 0; index < items.length; index += 1) {
311+
var item = items[index];
312+
graph.addEdge(item.from, item.to, item);
313+
}
314+
315+
var result = [];
316+
var expected = [];
317+
for(var index = 0; index < items.length; index+=1) {
318+
var item = items[index];
319+
expected.push([item.from, item.to]);
320+
expected.push([item.to, item.from]);
321+
}
322+
323+
graph.loopEdges(this, function (fromNode, toNode, edge) {
324+
result.push([fromNode, toNode]);
325+
});
326+
327+
result.sort((a, b) => (a[0] + a[1]).localeCompare(b[0] + b[1]));
328+
expected.sort((a, b) => (a[0] + a[1]).localeCompare(b[0] + b[1]));
329+
expect(result).toEqual(expected);
330+
});
-1 Bytes
Loading
0 Bytes
Loading
-1 Bytes
Loading
-7 Bytes
Loading

0 commit comments

Comments
 (0)