UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

452 lines (404 loc) 19.6 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/> <meta name="description" content="Interactive diagram showing all distances from a node, and highlighting all paths between two nodes."/> <link rel="stylesheet" href="../assets/css/style.css"/> <!-- Copyright 1998-2023 by Northwoods Software Corporation. --> <title>Graph Distances and Paths</title> </head> <body> <!-- This top nav is not part of the sample code --> <nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary"> <div class="w-full container max-w-screen-lg mx-auto flex flex-wrap sm:flex-nowrap items-center justify-between mt-0 py-2"> <div class="md:pl-4"> <a class="text-white hover:text-white no-underline hover:no-underline font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../"> <h1 class="my-0 p-1 ">GoJS</h1> </a> </div> <button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation"> <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6"> <path id="topnavOpen" fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z" clip-rule="evenodd"></path> <path id="topnavClosed" class="hidden" fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path> </svg> </button> <div id="topnavList" class="hidden sm:block items-center w-auto mt-0 text-white p-0 z-20"> <ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0"> <li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li> </ul> </div> </div> <hr class="border-b border-gray-600 opacity-50 my-0 py-0" /> </nav> <div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto"> <div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div> <!-- * * * * * * * * * * * * * --> <!-- Start of GoJS sample code --> <script src="../release/go.js"></script> <div id="allSampleContent" class="p-4 w-full"> <script id="code"> function init() { // Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make // For details, see https://gojs.net/latest/intro/buildingObjects.html const $ = go.GraphObject.make; // for conciseness in defining templates myDiagram = new go.Diagram("myDiagramDiv", // must be the ID or a reference to a DIV { initialAutoScale: go.Diagram.Uniform, contentAlignment: go.Spot.Center, layout: $(go.ForceDirectedLayout, { defaultSpringLength: 10, maxIterations: 300 }), maxSelectionCount: 2 }); // define the Node template myDiagram.nodeTemplate = $(go.Node, "Horizontal", { locationSpot: go.Spot.Center, // Node.location is the center of the Shape locationObjectName: "SHAPE", selectionAdorned: false, selectionChanged: nodeSelectionChanged // defined below }, $(go.Panel, "Spot", $(go.Shape, "Circle", { name: "SHAPE", fill: "lightgray", // default value, but also data-bound strokeWidth: 0, desiredSize: new go.Size(30, 30), portId: "" // so links will go to the shape, not the whole node }, new go.Binding("fill", "isSelected", (s, obj) => s ? "red" : obj.part.data.color).ofObject()), $(go.TextBlock, new go.Binding("text", "distance", d => (d === Infinity) ? "INF" : d | 0))), $(go.TextBlock, new go.Binding("text")) ); // define the Link template myDiagram.linkTemplate = $(go.Link, { selectable: false, // links cannot be selected by the user curve: go.Link.Bezier, layerName: "Background" // don't cross in front of any nodes }, $(go.Shape, // this shape only shows when it isHighlighted { isPanelMain: true, stroke: null, strokeWidth: 5 }, new go.Binding("stroke", "isHighlighted", h => h ? "red" : null).ofObject()), $(go.Shape, // mark each Shape to get the link geometry with isPanelMain: true { isPanelMain: true, stroke: "black", strokeWidth: 1 }, new go.Binding("stroke", "color")), $(go.Shape, { toArrow: "Standard" }) ); // Override the clickSelectingTool's standardMouseSelect // If less than 2 nodes are selected, always add to the selection myDiagram.toolManager.clickSelectingTool.standardMouseSelect = function() { // method override must be function, not => const diagram = this.diagram; if (diagram === null || !diagram.allowSelect) return; var e = diagram.lastInput; var count = diagram.selection.count; var curobj = diagram.findPartAt(e.documentPoint, false); if (curobj !== null) { if (count < 2) { // add the part to the selection if (!curobj.isSelected) { var part = curobj; if (part !== null) part.isSelected = true; } } else { if (!curobj.isSelected) { var part = curobj; if (part !== null) diagram.select(part); } } } else if (e.left && !(e.control || e.meta) && !e.shift) { // left click on background with no modifier: clear selection diagram.clearSelection(); } } generateGraph(); chooseTwoNodes(); } // Create an assign a model that has a bunch of nodes with a bunch of random links between them. function generateGraph() { var names = [ "Joshua", "Kathryn", "Robert", "Jason", "Scott", "Betsy", "John", "Walter", "Gabriel", "Simon", "Emily", "Tina", "Elena", "Samuel", "Jacob", "Michael", "Juliana", "Natalie", "Grace", "Ashley", "Dylan" ]; var nodeDataArray = []; for (var i = 0; i < names.length; i++) { nodeDataArray.push({ key: i, text: names[i], color: go.Brush.randomColor(128, 240) }); } var linkDataArray = []; var num = nodeDataArray.length; for (var i = 0; i < num * 2; i++) { var a = Math.floor(i/2); var b = Math.floor(Math.random() * num / 4) + 1; linkDataArray.push({ from: a, to: (a + b) % num, color: go.Brush.randomColor(0, 127) }); } myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray); } // Select two nodes at random for which there is a path that connects from the first one to the second one. function chooseTwoNodes() { myDiagram.clearSelection(); var num = myDiagram.model.nodeDataArray.length; var node1 = null; var node2 = null; for (var i = Math.floor(Math.random()*num); i < num*2; i++) { node1 = myDiagram.findNodeForKey(i%num); var distances = findDistances(node1); for (var j = Math.floor(Math.random()*num); j < num*2; j++) { node2 = myDiagram.findNodeForKey(j%num); var dist = distances.get(node2); if (dist > 1 && dist < Infinity) { node1.isSelected = true; node2.isSelected = true; break; } } if (myDiagram.selection.count > 0) break; } } // This event handler is declared in the node template and is called when a node's // Node.isSelected property changes value. // When a node is selected show distances from the first selected node. // When a second node is selected, highlight the shortest path between two selected nodes. // If a node is deselected, clear all highlights. function nodeSelectionChanged(node) { var diagram = node.diagram; if (diagram === null) return; diagram.clearHighlighteds(); if (node.isSelected) { // when there is a selection made, always clear out the list of all paths var sel = document.getElementById("myPaths"); sel.innerHTML = ""; // show the distance for each node from the selected node var begin = diagram.selection.first(); showDistances(begin); if (diagram.selection.count === 2) { var end = node; // just became selected // highlight the shortest path highlightShortestPath(begin, end); // list all paths listAllPaths(begin, end); } } } // Have each node show how far it is from the BEGIN node. // This sets the "distance" property on each node.data. function showDistances(begin) { // compute and remember the distance of each node from the BEGIN node distances = findDistances(begin); // show the distance on each node var it = distances.iterator; while (it.next()) { var n = it.key; var dist = it.value; myDiagram.model.setDataProperty(n.data, "distance", dist); } } // Highlight links along one of the shortest paths between the BEGIN and the END nodes. // Assume links are directional. function highlightShortestPath(begin, end) { highlightPath(findShortestPath(begin, end)); } // A collection of all of the paths between a pair of nodes, a List of Lists of Nodes var paths = null; // List all paths from BEGIN to END function listAllPaths(begin, end) { // compute and remember all paths from BEGIN to END: Lists of Nodes paths = collectAllPaths(begin, end); // update the Selection element with a bunch of Option elements, one per path var sel = document.getElementById("myPaths"); sel.innerHTML = ""; // clear out any old Option elements paths.each(p => { var opt = document.createElement("option"); opt.text = pathToString(p); sel.add(opt, null); }); sel.onchange = highlightSelectedPath; } // Return a string representation of a path for humans to read. function pathToString(path) { var s = path.length + ": "; for (var i = 0; i < path.length; i++) { if (i > 0) s += " -- "; s += path.get(i).data.text; } return s; } // This is only used for listing all paths for the selection onchange event. // When the selected item changes in the Selection element, // highlight the corresponding path of nodes. function highlightSelectedPath() { var sel = document.getElementById("myPaths"); highlightPath(paths.get(sel.selectedIndex)); } // Highlight a particular path, a List of Nodes. function highlightPath(path) { myDiagram.clearHighlighteds(); for (var i = 0; i < path.count - 1; i++) { var f = path.get(i); var t = path.get(i + 1); f.findLinksTo(t).each(l => l.isHighlighted = true); } } // There are three bits of functionality here: // 1: findDistances(Node) computes the distance of each Node from the given Node. // This function is used by showDistances to update the model data. // 2: findShortestPath(Node, Node) finds a shortest path from one Node to another. // This uses findDistances. This is used by highlightShortestPath. // 3: collectAllPaths(Node, Node) produces a collection of all paths from one Node to another. // This is used by listAllPaths. The result is remembered in a global variable // which is used by highlightSelectedPath. This does not depend on findDistances. // Returns a Map of Nodes with distance values from the given source Node. // Assumes all links are directional. function findDistances(source) { var diagram = source.diagram; // keep track of distances from the source node var distances = new go.Map(/*go.Node, "number"*/); // all nodes start with distance Infinity var nit = diagram.nodes; while (nit.next()) { var n = nit.value; distances.set(n, Infinity); } // the source node starts with distance 0 distances.set(source, 0); // keep track of nodes for which we have set a non-Infinity distance, // but which we have not yet finished examining var seen = new go.Set(/*go.Node*/); seen.add(source); // keep track of nodes we have finished examining; // this avoids unnecessary traversals and helps keep the SEEN collection small var finished = new go.Set(/*go.Node*/); while (seen.count > 0) { // look at the unfinished node with the shortest distance so far var least = leastNode(seen, distances); var leastdist = distances.get(least); // by the end of this loop we will have finished examining this LEAST node seen.delete(least); finished.add(least); // look at all Links connected with this node var it = least.findLinksOutOf(); while (it.next()) { var link = it.value; var neighbor = link.getOtherNode(least); // skip nodes that we have finished if (finished.has(neighbor)) continue; var neighbordist = distances.get(neighbor); // assume "distance" along a link is unitary, but could be any non-negative number. var dist = leastdist + 1; //Math.sqrt(least.location.distanceSquaredPoint(neighbor.location)); if (dist < neighbordist) { // if haven't seen that node before, add it to the SEEN collection if (neighbordist === Infinity) { seen.add(neighbor); } // record the new best distance so far to that node distances.set(neighbor, dist); } } } return distances; } // This helper function finds a Node in the given collection that has the smallest distance. function leastNode(coll, distances) { var bestdist = Infinity; var bestnode = null; var it = coll.iterator; while (it.next()) { var n = it.value; var dist = distances.get(n); if (dist < bestdist) { bestdist = dist; bestnode = n; } } return bestnode; } // Find a path that is shortest from the BEGIN node to the END node. // (There might be more than one, and there might be none.) function findShortestPath(begin, end) { // compute and remember the distance of each node from the BEGIN node distances = findDistances(begin); // now find a path from END to BEGIN, always choosing the adjacent Node with the lowest distance var path = new go.List(); path.add(end); while (end !== null) { var next = leastNode(end.findNodesInto(), distances); if (next !== null) { if (distances.get(next) < distances.get(end)) { path.add(next); // making progress towards the beginning } else { next = null; // nothing better found -- stop looking } } end = next; } // reverse the list to start at the node closest to BEGIN that is on the path to END // NOTE: if there's no path from BEGIN to END, the first node won't be BEGIN! path.reverse(); return path; } // Recursively walk the graph starting from the BEGIN node; // when reaching the END node remember the list of nodes along the current path. // Finally return the collection of paths, which may be empty. // This assumes all links are directional. function collectAllPaths(begin, end) { var stack = new go.List(/*go.Node*/); var coll = new go.List(/*go.List*/); function find(source, end) { source.findNodesOutOf().each(n => { if (n === source) return; // ignore reflexive links if (n === end) { // success var path = stack.copy(); path.add(end); // finish the path at the end node coll.add(path); // remember the whole path } else if (!stack.has(n)) { // inefficient way to check having visited stack.add(n); // remember that we've been here for this path (but not forever) find(n, end); stack.removeAt(stack.count - 1); } // else might be a cycle }); } stack.add(begin); // start the path at the begin node find(begin, end); return coll; } window.addEventListener('DOMContentLoaded', init); </script> <div id="sample"> <div id="myDiagramDiv" style="border: solid 1px black; background: white; width: 100%; height: 700px"></div> Click on a node to show distances from that node to each other node. Click on a second node to show a shortest path from the first node to the second node. (Note that there might not be any path between the nodes.) Clicking on a third node will de-select the first two. <p> <button onclick="chooseTwoNodes()">Choose another two nodes at random</button> </p> <p> Here is a list of all paths between the first and second selected nodes. Select a path to highlight it in the diagram. </p> <select id="myPaths" style="min-width:100px" size="10"></select> </div> </div> <!-- * * * * * * * * * * * * * --> <!-- End of GoJS sample code --> </div> </body> <!-- This script is part of the gojs.net website, and is not needed to run the sample --> <script src="../assets/js/goSamples.js"></script> </html>