UNPKG

basicprimitives

Version:

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.

638 lines (570 loc) 21.3 kB
import Tree from './Tree'; import FibonacciHeap from './FibonacciHeap'; /** * Creates graph structure * @class Graph * * @returns {Graph} Returns graph object */ export default function Graph() { var _edges = {}, MAXIMUMTOTALWEIGHT = 1, MINIMUMWEIGHT = 2; /** * Adds edge to the graph * @param {string} from The id of the start node * @param {string} to The id of the end node * @param {object} edge The edge contextual object */ function addEdge(from, to, edge) { if ((_edges[from] == null || _edges[from][to] == null) && edge != null) { if (_edges[from] == null) { _edges[from] = {}; } _edges[from][to] = edge; if (_edges[to] == null) { _edges[to] = {}; } _edges[to][from] = edge; } } /** * Returns edge context object * * @param {string} from The edge's from node id * @param {string} to The edge's to node id * @returns {object} The edge's context object */ function edge(from, to) { var result = null; if (_edges[from] != null && _edges[from][to]) { result = _edges[from][to]; } return result; } /** * Returns true if node exists in the graph * * @param {string} from The node id * @returns {boolean} Returns true if node exists */ function hasNode(from) { return _edges.hasOwnProperty(from); } /** * Callback for iterating edges of the graph's node * * @callback onEdgeCallback * @param {string} to The neighboring node id * @param {Object} edge The edge's context object */ /** * Loop edges of the node * * @param {object} thisArg The callback function invocation context * @param {string} itemid The node id * @param {onEdgeCallback} onEdge A callback function to call for every edge of the node */ function loopNodeEdges(thisArg, itemid, onEdge) { var neighbours, neighbourKey; if (onEdge != null) { neighbours = _edges[itemid]; if (neighbours != null) { for (neighbourKey in neighbours) { if (neighbours.hasOwnProperty(neighbourKey)) { onEdge.call(thisArg, neighbourKey, neighbours[neighbourKey]); } } } } } /** * Callback function for iterating graphs nodes * * @callback onNodeCallback * @param {string} to The next neighboring node id * @returns {boolean} Returns true to break loop */ /** * Loop nodes of the graph * * @param {object} thisArg The callback function invocation context * @param {string} [itemid=undefined] The optional start node id. If start node is undefined, * function loops graphs node starting from first available node * @param {onNodeCallback} onItem A callback function to be called for every neighboring node */ function loopNodes(thisArg, startNode, onItem) { var processed = {}; if (startNode == null) { for (startNode in _edges) { if (_edges.hasOwnProperty(startNode)) { if (!processed.hasOwnProperty[startNode]) { _loopNodes(thisArg, startNode, processed, onItem); } } } } else { _loopNodes(thisArg, startNode, processed, onItem); } } function _loopNodes(thisArg, startNode, processed, onItem) { var margin = [], marginKey, newMargin, index, len, neighbours, neighbourKey; margin.push(startNode); processed[startNode] = true; if (onItem != null) { while (margin.length > 0) { newMargin = []; /* iterate neighbours of every node on margin */ for (index = 0, len = margin.length; index < len; index += 1) { marginKey = margin[index]; if (onItem.call(thisArg, marginKey)) { return; } neighbours = _edges[marginKey]; for (neighbourKey in neighbours) { if (neighbours.hasOwnProperty(neighbourKey) && !processed.hasOwnProperty(neighbourKey)) { processed[neighbourKey] = true; newMargin.push(neighbourKey); } } } margin = newMargin; } } } /** * Callback for finding edge weight * * @callback getGraphEdgeWeightCallback * @param {object} edge The edge context object * @param {string} fromItem The edge's start node id * @param {string} toItem The edge's end node id * @returns {number} Returns weight of the edge */ /** * Get maximum spanning tree. Graph may have disconnected sub graphs, so start node is necessary. * * @param {string} startNode The node to start searching for maximum spanning tree. Graph is not necessary connected * @param {getGraphEdgeWeightCallback} getWeightFunc Callback function to get weight of an edge. * @returns {tree} Returns tree structure containing maximum spanning tree of the graph */ function getSpanningTree(startNode, getWeightFunc) { var result = Tree(), margin = FibonacciHeap(true), marginNode, parents = {}, /* if parent for item is set then it was laready visited */ neighbours, neighbourKey, neighbourWeight, currentWeight; /* add start node to margin */ margin.add(startNode, 0, null /*parent of root node is null*/); parents[startNode] = null; /* search graph */ while ((marginNode = margin.extractRoot()) != null) { /* iterate neighbours of every node on margin */ neighbours = _edges[marginNode.key]; for (neighbourKey in neighbours) { if (neighbours.hasOwnProperty(neighbourKey) && !result.node(neighbourKey)) { neighbourWeight = getWeightFunc != null ? getWeightFunc(neighbours[neighbourKey]) : neighbours[neighbourKey]; currentWeight = margin.getPriority(neighbourKey); if (currentWeight == null) { margin.add(neighbourKey, neighbourWeight, null); parents[neighbourKey] = marginNode.key.toString(); } else { if (currentWeight <= neighbourWeight) { /* improve node distance */ margin.setPriority(neighbourKey, neighbourWeight); parents[neighbourKey] = marginNode.key.toString(); } } } } /* add next margin item to resul tree */ result.add(parents[marginNode.key], marginNode.key.toString(), {}); } return result; } function _findStartNode(thisArg, onEdgeWeight) { var result = null, fromItem, toItems, toItem, weight = 0, maxWeight = null; for (fromItem in _edges) { if (_edges.hasOwnProperty(fromItem)) { toItems = _edges[fromItem]; weight = 0; for (toItem in toItems) { if (toItems.hasOwnProperty(toItem)) { weight += onEdgeWeight.call(thisArg, toItems[toItem], fromItem, toItem); } } if (weight > maxWeight || maxWeight == null) { result = fromItem; maxWeight = weight; } } } return result; } /** * Get graph growth sequence. The sequence of graph traversing order. * * @param {object} thisArg The callback function invocation context * @param {getGraphEdgeWeightCallback} getWeightFunc Callback function to get weight of an edge. * @param {onNodeCallback} onItem A callback function to be called for every node of the growth sequence */ function getTotalWeightGrowthSequence(thisArg, onEdgeWeight, onItem) { var startNode = _findStartNode(thisArg, onEdgeWeight); _getGrowthSequence(thisArg, startNode, onEdgeWeight, onItem, MAXIMUMTOTALWEIGHT); } /** * Get minimum weight graph growth sequence. The sequence of the traversing order of the graph nodes. * * @param {object} thisArg The callback function invocation context * @param {string} [startNode=undefined] The optional start node id * @param {getGraphEdgeWeightCallback} onEdgeWeight Callback function to get weight of an edge. * @param {onNodeCallback} onItem A callback function to be called for every node of the growth sequence */ function getMinimumWeightGrowthSequence(thisArg, startNode, onEdgeWeight, onItem) { _getGrowthSequence(thisArg, startNode, onEdgeWeight, onItem, MINIMUMWEIGHT); } function _getGrowthSequence(thisArg, startNode, onEdgeWeight, onItem, growsMode) { var margin = {}, marginKey, itemsToRemove = [], /* if margin item has no neighbours to expand we remove it from margin*/ hasNeighbours, processed = {}, /* if item is set then it was already visited */ marginLength = 0, /* curent margin length */ nextMarginKey, nextMarginWeight, bestWeight, neighbours, neighbourKey, neighbourWeight, index, len; if (onEdgeWeight != null && onItem != null) { if (startNode == null) { startNode = _findStartNode(thisArg, onEdgeWeight); } if (startNode != null) { onItem.call(thisArg, startNode); /* add start node to margin */ margin[startNode] = true; marginLength += 1; /* add startNode to result tree */ processed[startNode] = null; /* search graph */ while (marginLength > 0) { itemsToRemove = []; nextMarginKey = null; nextMarginWeight = null; bestWeight = {}; /* iterate neighbours of every node on margin */ for (marginKey in margin) { if (margin.hasOwnProperty(marginKey)) { neighbours = _edges[marginKey]; hasNeighbours = false; for (neighbourKey in neighbours) { if (neighbours.hasOwnProperty(neighbourKey) && !processed.hasOwnProperty(neighbourKey)) { neighbourWeight = onEdgeWeight.call(thisArg, neighbours[neighbourKey], marginKey, neighbourKey); hasNeighbours = true; switch (growsMode) { case MAXIMUMTOTALWEIGHT: if (bestWeight[neighbourKey] == null) { bestWeight[neighbourKey] = 0; } bestWeight[neighbourKey] += neighbourWeight; if (!nextMarginWeight || bestWeight[neighbourKey] > nextMarginWeight) { nextMarginKey = neighbourKey; nextMarginWeight = bestWeight[neighbourKey]; } break; case MINIMUMWEIGHT: if (bestWeight[neighbourKey] == null) { bestWeight[neighbourKey] = neighbourWeight; } else { bestWeight[neighbourKey] = Math.min(bestWeight[neighbourKey], neighbourWeight); } if (!nextMarginWeight || bestWeight[neighbourKey] < nextMarginWeight) { nextMarginKey = neighbourKey; nextMarginWeight = bestWeight[neighbourKey]; } break; } } } if (!hasNeighbours) { itemsToRemove.push(marginKey); } } } if (nextMarginKey == null) { /* no items to expand to exit*/ break; } else { margin[nextMarginKey] = true; marginLength += 1; processed[nextMarginKey] = true; /* add next margin item to result sequence */ onItem.call(thisArg, nextMarginKey); } for (index = 0, len = itemsToRemove.length; index < len; index += 1) { /* delete visited node from margin */ delete margin[itemsToRemove[index]]; marginLength -= 1; } } } } } /** * Callback for returning optimal connection path for every end node. * * @callback onPathFoundCallback * @param {string[]} path An array of connection path node ids. * @param {string} to The end node id, the connection path is found for. */ /** * Get shortest path between two nodes in graph. The start and the end nodes are supposed to have connection path. * * @param {object} thisArg The callback function invocation context * @param {string} startNode The start node id * @param {string[]} endNodes The array of end node ids. * @param {getGraphEdgeWeightCallback} getWeightFunc Callback function to get weight of an edge. * @param {onPathFoundCallback} onPathFound A callback function to be called for every end node * with the optimal connection path */ function getShortestPath(thisArg, startNode, endNodes, getWeightFunc, onPathFound) { var margin = FibonacciHeap(false), distance = {}, breadcramps = {}, bestNodeOnMargin, key, children, newDistance, weight, path, currentNode, endNodesHash = {}, index, len, endsCount = 0, endsFound = 0; /* create hash table of end nodes to find */ for (index = 0, len = endNodes.length; index < len; index += 1) { key = endNodes[index]; if (!endNodesHash.hasOwnProperty(key)) { endsCount += 1; endNodesHash[key] = true; } } /* add start node to margin */ margin.add(startNode, 0, null); breadcramps[startNode] = null; /* search graph */ while ((bestNodeOnMargin = margin.extractRoot()) != null) { /* iterate neighbours of selected node on margin */ children = _edges[bestNodeOnMargin.key]; for (key in children) { if (children.hasOwnProperty(key)) { weight = 1; if (getWeightFunc != null) { weight = getWeightFunc.call(thisArg, children[key], bestNodeOnMargin, key); newDistance = bestNodeOnMargin.priority + weight; } else { newDistance = bestNodeOnMargin.priority + 1; } if (weight >= 0) { distance = margin.getPriority(key); if (distance != null) { if (distance > newDistance) { margin.setPriority(key, newDistance); breadcramps[key] = bestNodeOnMargin.key; } } else { if (!breadcramps.hasOwnProperty(key)) { margin.add(key, newDistance, null); breadcramps[key] = bestNodeOnMargin.key; } } } } } if (endNodesHash.hasOwnProperty(bestNodeOnMargin.key)) { /* trace path */ path = []; currentNode = bestNodeOnMargin.key; while (currentNode != null) { path.push(currentNode); currentNode = breadcramps[currentNode]; } onPathFound.call(thisArg, path, bestNodeOnMargin.key); endsFound += 1; if (endsFound >= endsCount) { break; } } } } /** * Callback for iterating path edges * * @callback onPathEdgeCallback * @param {string} from The from node id * @param {string} to The to node id * @param {Object} edge The edge's context object * @returns {boolean} Returns true if edge is usable */ /** * Search any path from node to node using depth first search * * @param {object} thisArg The callback function invocation context * @param {string} startNode The start node id * @param {string} endNode The end node id. * @param {onPathEdgeCallback} onEdge A callback function to call for every edge of the node */ function dfsPath(thisArg, startNode, endNode, onEdge) { var margin = [], backtrace = {}; margin.push(startNode); backtrace[startNode] = null; if (startNode != endNode) { /* search graph */ while (margin.length > 0 && !backtrace.hasOwnProperty(endNode)) { // Remove last node out of margin var currentNode = margin[margin.length - 1]; margin.length -= 1; // search its neighbours and add them to margin var neighbours = _edges[currentNode]; for (var neighbour in neighbours) { if (neighbours.hasOwnProperty(neighbour)) { if (!backtrace.hasOwnProperty(neighbour)) { // node is not passed yet, check edge capacity and add new neighbour to the margin if (onEdge.call(thisArg, currentNode, neighbour, neighbours[neighbour])) { margin.push(neighbour); backtrace[neighbour] = currentNode; if (neighbour == endNode) { break; } } } } } } } currentNode = endNode; var path = []; while (backtrace.hasOwnProperty(currentNode)) { path.push(currentNode); currentNode = backtrace[currentNode]; } var result = []; if (path.length > 0) { for (var index = path.length - 1; index >= 0; index -= 1) { result.push(path[index]); } } return result; } /** * Get Level Graph starting with `startNode` * * @param {object} thisArg The callback function invocation context * @param {string} startNode The start node id * @param {onPathEdgeCallback} onEdge A callback function to call for every edge of the graph */ function getLevelGraph(thisArg, startNode, onEdge) { var level = {}, margin = [], currentNode, currentLevel, neighbours; margin.push(startNode); level[startNode] = 1; /* search graph level by level */ while (margin.length > 0) { var newMargin = []; for (var index = 0, len = margin.length; index < len; index += 1) { currentNode = margin[index]; currentLevel = level[currentNode]; neighbours = _edges[currentNode]; for (var neighbour in neighbours) { if (neighbours.hasOwnProperty(neighbour)) { if (!level.hasOwnProperty(neighbour)) { if (onEdge.call(thisArg, currentNode, neighbour, neighbours[neighbour])) { newMargin.push(neighbour); level[neighbour] = currentLevel + 1; } } } } } margin = newMargin; } // Create level graph, copy existing edges to the new graph var levelGraph = Graph(); for (currentNode in _edges) { if (level.hasOwnProperty(currentNode)) { currentLevel = level[currentNode]; neighbours = _edges[currentNode]; for (neighbour in neighbours) { if (level.hasOwnProperty(neighbour)) { var neighbourLevel = level[neighbour]; if (currentLevel + 1 == neighbourLevel) { levelGraph.addEdge(currentNode, neighbour, neighbours[neighbour]); } } } } } return levelGraph; } /** * Depth first search loop * * @param {object} thisArg The callback function invocation context * @param {string} startNode The start node id * @param {onPathEdgeCallback} onEdge A callback function to call for every edge of the graph * @param {onNodeCallback} onNode A callback function to be called for every neighboring node */ function dfsLoop(thisArg, startNode, onEdge, onNode) { var margin = [], visited = {}, currentNode; margin.push(startNode); visited[startNode] = true; /* search graph */ while (margin.length > 0) { // Remove last node out of margin currentNode = margin[margin.length - 1]; margin.length -= 1; // search its neighbours and add them to margin var neighbours = _edges[currentNode]; for (var neighbour in neighbours) { if (neighbours.hasOwnProperty(neighbour)) { if (!visited.hasOwnProperty(neighbour)) { // node is not passed yet, check edge capacity and add new neighbour to the margin if (onEdge.call(thisArg, currentNode, neighbour, neighbours[neighbour])) { margin.push(neighbour); visited[neighbour] = true; if (onNode.call(thisArg, neighbour)) { return; } } } } } } } return { addEdge: addEdge, edge: edge, hasNode: hasNode, loopNodes: loopNodes, loopNodeEdges: loopNodeEdges, getSpanningTree: getSpanningTree, getTotalWeightGrowthSequence: getTotalWeightGrowthSequence, getMinimumWeightGrowthSequence: getMinimumWeightGrowthSequence, getShortestPath: getShortestPath, dfsPath: dfsPath, getLevelGraph: getLevelGraph, dfsLoop: dfsLoop }; };