UNPKG

trassel

Version:
394 lines (357 loc) 14.7 kB
import Layout from "./layout" import DataManager from "./datamanager" import louvain from "./community/louvain" /** * Main API for using the graph engine. */ export default class Graph { /** * @param {import("./model/ibasicnode").IBasicNode[]=} nodes - Initial nodes * @param {import("./model/ibasicedge").IBasicEdge[]=} edges - Initial edges * @param {import("./model/ioptions").IOptions} options - options */ constructor(nodes = [], edges = [], options = {}) { /** @private */ this.nodes = nodes /** @private */ this.edges = edges /** @private */ this.options = options /** @private */ this.layout = new Layout(nodes, edges, options?.layout) /** @private */ this.dataManager = new DataManager(nodes, edges) } /** * Sets the current alpha value in the layout. * @param {number} alpha - Alpha value * @returns {Graph} */ setLayoutAlpha(alpha) { this.layout.alpha = alpha return this } /** * Sets the minimum allowed alpha value in the layout. When this value is reached the loop will end. * @param {number} alphaMin - Minimum alpha value * @returns {Graph} */ setLayoutAlphaMin(alphaMin) { this.layout.alphaMin = alphaMin return this } /** * Sets the alpha decay rate in the layout. This determines how much the alpha value decreases on each update. * @param {number} alphaDecay - Alpha decay rate * @returns {Graph} */ setLayoutAlphaDecay(alphaDecay) { this.layout.alphaDecay = alphaDecay return this } /** * Sets the alpha target in the layout. This sets what alpha value the layout is trying to reach. * @param {number} alphaTarget - Target alpha value * @returns {Graph} */ setLayoutAlphaTarget(alphaTarget) { this.layout.alphaTarget = alphaTarget return this } /** * Sets the velocity decay rate in the layout. * @param {number} velocityDecay - Velocity decay rate * @returns {Graph} */ setLayoutVelocityDecay(velocityDecay) { this.layout.velocityDecay = velocityDecay return this } /** * Sets the update cap (per second) for the layout loop * @param {number} newCap - new cap */ setLayoutUpdateCap(newCap) { this.layout.setUpdateCap(newCap) } /** * Computes communities (groups) based on nodes and edges in the graph * Returns an array of communities, containing nodes grouped by belonging * @returns {{communities: import("../model/nodeid").NodeID[][], communityTable: {[key: string]: any}}} */ computeCommunities() { return this.louvain() } /** * Computes communities (groups) based on nodes and edges in the graph * Returns an array of communities, containing nodes grouped by belonging * @returns {{communities: import("../model/nodeid").NodeID[][], communityTable: {[key: string]: any}}} */ louvain() { return louvain(this.nodes, this.edges) } /** * Updates the nodes and edges in the graph * @param {import("./model/ibasicnode").IBasicNode[]} nodes * @param {import("./model/ibasicedge").IBasicEdge[]} edges * @returns {Graph} */ updateNodesAndEdges(nodes, edges) { this.nodes = nodes this.edges = edges this.layout.updateNodesAndEdges(nodes, edges) this.dataManager.updateNodesAndEdges(nodes, edges) return this } /** * Starts the layout engine's loop * @returns {Graph} */ startLayoutLoop() { this.layout.start() return this } /** * Stops the layout engine * @returns {Graph} */ stopLayoutLoop() { this.layout.stop() return this } /** * Executes one update in the layout engine * @param {boolean} sendEvent - Should an update event be fired? * @returns {Graph} */ updateLayout(sendEvent = true) { this.layout.update(sendEvent) return this } /** * Registers an event listener * @param {(import("./model/events").EEvents} name - Event name to listen for * @param {(() => any)} fn - Callback on event * @returns {Graph} */ on(name, fn) { this.layout.on(name, fn) return this } /** * Animates nodes from source positions to target positions within a duration provided. * This function can be used to transition the graph between states or layouts. * Once triggered the animation cannot be stopped. All other updates and components will be frozen until the animation completes. * There should *never* be more than one animation running simultaneously. * @param {import("./model/itargetnodestate").ITargetNodeState[]} targetNodeStates * @param {number} duration - Animation duration in milliseconds * @param {boolean} shouldFixateOnEnd - If true then the graph will fixate the nodes when the animation ends */ animateLayoutState(targetNodeStates = [], duration = 300, shouldFixateOnEnd = false) { this.layout.animateState(targetNodeStates, duration, shouldFixateOnEnd) } /** * Adds a component to the layout engine * @param {string} id - Unique identifier for the component * @param {import("./model/ilayoutcomponent").ILayoutComponent} component - A layout component compatible class instance * @param {(node: import("./model/igraphnode").IGraphNode) => boolean=} nodeBindings - Function that computes if a node should be affected by the component. Blank means true for all. * @param {(edge: import("./model/igraphedge").IGraphEdge) => boolean=} edgeBindings - Function that computes if an edge should be affected by the component. Blank means true for all. * @returns {Graph} - this */ addLayoutComponent(id, component, nodeBindings = null, edgeBindings = null) { this.layout.addLayoutComponent(id, component, nodeBindings, edgeBindings) return this } /** * Removes a component with the specified ID * @param {string} id * @returns {Graph} */ removeLayoutComponent(id) { this.layout.removeComponent(id) return this } /** * Finds the node closest to the provided coordinates in the graph * @param {number} x * @param {number} y * @returns {import("./model/igraphnode").IGraphNode} - The node */ findClosestNodeByCoordinates(x, y) { return this.layout.findClosestNodeByCoordinates(x, y) } /** * Computes online nodes * @returns {import("./model/ibasicnode").IBasicNode[]} node */ getOnlineNodes() { return this.dataManager.getOnlineNodes() } /** * Computes offline nodes * @returns {import("./model/ibasicnode").IBasicNode[]} node */ getOfflineNodes() { return this.dataManager.getOfflineNodes() } /** * Computes online edges * @returns {import("./model/ibasicedge").IBasicEdge[]} edge */ getOnlineEdges() { this.dataManager.getOnlineEdges() } /** * Computes offline edges * @returns {import("./model/ibasicedge").IBasicEdge[]} edge */ getOfflineEdges() { this.dataManager.getOfflineEdges() } /** * Checks if an edge is online * @param {import("./model/ibasicnode").IBasicEdge} edge * @returns {boolean} */ isEdgeOnline(edge) { return this.dataManager.isEdgeOnline(edge) } /** * Checks if a node is online * @param {string} nodeID * @returns {boolean} */ isNodeOnline(nodeID) { return this.dataManager.isNodeOnline(nodeID) } /** * Brings the list of node IDs offline * @param {import("./model/nodeid").NodeID[]} nodeIDs */ bringNodesOffline(nodeIDs) { this.dataManager.bringNodesOffline(nodeIDs) this.nodes = this.dataManager.getOnlineNodes() this.edges = this.dataManager.getOnlineEdges() this.layout.updateNodesAndEdges(this.nodes, this.edges) } /** * Brings the list of node IDs online * @param {import("./model/nodeid").NodeID[]} nodeIDs */ bringNodesOnline(nodeIDs) { this.dataManager.bringNodesOnline(nodeIDs) this.nodes = this.dataManager.getOnlineNodes() this.edges = this.dataManager.getOnlineEdges() this.layout.updateNodesAndEdges(this.nodes, this.edges) } bringAllNodesOffline() { this.dataManager.bringAllNodesOffline() this.nodes = [] this.edges = [] this.layout.updateNodesAndEdges(this.nodes, this.edges) } bringAllNodesOnline() { this.dataManager.bringAllNodesOnline() this.nodes = this.dataManager.getOnlineNodes() this.edges = this.dataManager.getOnlineEdges() this.layout.updateNodesAndEdges(this.nodes, this.edges) } /** * Retrieves all neighbors for a given nodeID * @param {import("./model/nodeid").NodeID} nodeID - ID od the node neighbors should be retrieved for * @param {boolean} isDirected - Only traverse edges where the input node is the sourceNode * @param {boolean} useOnlyOnline - Only traverse neighbors that are online * @param {boolean} ignoreInternalEdges - Ignore self-edges * @returns {import("./model/nodeid").NodeID[]} */ getNeighbors(nodeID, isDirected = false, useOnlyOnline = true, ignoreInternalEdges = true) { return this.dataManager.getNeighbors(nodeID, isDirected, useOnlyOnline, ignoreInternalEdges) } /** * Computes collateral nodes in implodes or explode operations from a root node (I.e. bringing connected neighbors online/offline) * This function will not apply any changes, but return an array with affected nodes * The function exists specifically to help applications that implement implode/explode functionality in graphs * and need to compute what nodes should be brough online/offline. * @param {import("./model/nodeid").NodeID} nodeID * @param {boolean} isBringOnline - If true neighbors will be brought online otherwise offline * @param {boolean} isDirected - If true then operation will be directed * @param {"single"|"recursive"|"leafs"} mode - Single means all neighbors are affected, leafs means only neighbors with no other neighbors are affected, recursive means neighbors recursively are affected. * @returns {import("./model/nodeid").NodeID[]} - Affected nodes */ computeImplodeOrExplodeNode(nodeID, isBringOnline = false, isDirected = true, mode = "single") { return this.dataManager.computeImplodeOrExplodeNode(nodeID, isBringOnline, isDirected, mode) } /** * Specifically meant to support renderers in determining optimal target positions for nodes that are being brough online. * Accepts an array of node IDs and origin coordinates where the nodes should be animated from. * Returns an array of vertices with optimal positions based on other neighbors present in the graph, or in the case of leafs a circle around the origin. * Note(!) that this function expects all nodes and edges to have been initialized into GraphNodes and GraphEdges in order to compute this information. * @param {import("./model/nodeid").NodeID[]} nodeIDs - Array of node IDs * @param {number} distance - Default distance from origin position to put nodes (for non-average values only!) * @param {number} originX - Start position for the transition * @param {number} originY - Start position for the transition * @returns {{id: import("./model/nodeid").NodeID, x: number, y: numer}[]} - Target coordinates */ stageNodePositions(nodeIDs = [], distance = 300, originX = 0, originY = 0) { return this.dataManager.stageNodePositions(nodeIDs, distance, originX, originY) } /** * Computes the shortest path from one node to another. Returns an array with the nodeIDs, or null if there is no path. * @param {import("./model/nodeid").NodeID} startNode - Node ID where the road starts * @param {import("./model/nodeid").NodeID} endNode - Node ID where the road ends * @param {boolean} useOnlyOnline - If true the shortest path will only be computed for live nodes * @param {boolean} isDirected - If true then operation will be directed * @return {import("./model/nodeid").NodeID[]} - Array of node IDs from startnode to endnode containing the (a) shortest path */ findShortestPathUnweighted(startNode, endNode, useOnlyOnline = true, isDirected = true) { return this.dataManager.findShortestPathUnweighted(startNode, endNode, useOnlyOnline, isDirected) } /** * Computes the shortest path from one node to another. Returns an array with the nodeIDs, or null if there is no path. * This is basically Dijkstra's algorithm: * https://en.wikipedia.org/wiki/Dijkstra's_algorithm * @param {import("./model/nodeid").NodeID} startNode - Node ID where the road starts * @param {import("./model/nodeid").NodeID} endNode - Node ID where the road ends * @param {boolean} useOnlyOnline - If true the shortest path will only be computed for live nodes * @param {boolean} isDirected - If true then operation will be directed * @param {boolean} aggregateEdgeWeights - If true then weights for all edges between a set of nodes are aggregated and treated as a single edge * @return {{id: import("./model/nodeid").NodeID, cost: number}[]} - Array of nodes and costs from startnode to endnode containing the (a) cheapest path */ findShortestPathWeighted(startNode, endNode, useOnlyOnline = true, isDirected = true, aggregateEdgeWeights = false) { return this.dataManager.findShortestPathWeighted(startNode, endNode, useOnlyOnline, isDirected, aggregateEdgeWeights) } /** * Computes strongly connected components in the graph. * Basically an implementation of Kosoraju's algorithm. * https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm * @param {boolean} useOnlyOnline - If true the shortest path will only be computed for live nodes * @return {("./model/nodeid").NodeID[][]} - Strongly connected components. */ computeStronglyConnectedComponents(useOnlyOnline = true) { return this.dataManager.computeStronglyConnectedComponents(useOnlyOnline) } /** * Executes a breadth-first search in the graph given a start node. * Each node encountered will be handed off to a callback function provided, * If the callback function returns true then that branch will be terminated. * @param {import("./model/nodeid").NodeID} startNode * @param {(import("./model/nodeid").NodeID) => void|true} callback * @param {boolean} useOnlyOnline - If true only online nodes will be processed * @param {boolean} isDirected - If true then traversal will be directed */ BFS(startNode, callback, useOnlyOnline = true, isDirected = true) { this.dataManager.BFS(startNode, callback, useOnlyOnline, isDirected) } /** * Executes a depth-first search in the graph given a start node. * Each node encountered will be handed off to a callback function provided, * If the callback function returns true then that branch will be terminated. * @param {import("./model/nodeid").NodeID} startNode * @param {(import("./model/nodeid").NodeID) => void|true} callback * @param {boolean} useOnlyOnline - If true only online nodes will be processed * @param {boolean} isDirected - If true then traversal will be directed */ DFS(startNode, callback, useOnlyOnline = true, isDirected = true) { this.dataManager.DFS(startNode, callback, useOnlyOnline, isDirected) } }