UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

585 lines (518 loc) 23.2 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." /> <meta itemprop="description" content="Interactive diagram showing all distances from a node, and highlighting all paths between two nodes." /> <meta property="og:description" content="Interactive diagram showing all distances from a node, and highlighting all paths between two nodes." /> <meta name="twitter:description" content="Interactive diagram showing all distances from a node, and highlighting all paths between two nodes." /> <link rel="preconnect" href="https://rsms.me/"> <link rel="stylesheet" href="../assets/css/style.css"> <!-- Copyright 1998-2025 by Northwoods Software Corporation. --> <meta itemprop="name" content="Finding and Highlighting Node-Link Paths in Graphs" /> <meta property="og:title" content="Finding and Highlighting Node-Link Paths in Graphs" /> <meta name="twitter:title" content="Finding and Highlighting Node-Link Paths in Graphs" /> <meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/distances.png" /> <meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/distances.png" /> <meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/distances.png" /> <meta property="og:url" content="https://gojs.net/latest/samples/distances.html" /> <meta property="twitter:url" content="https://gojs.net/latest/samples/distances.html" /> <meta name="twitter:card" content="summary_large_image" /> <meta property="og:type" content="website" /> <meta property="twitter:domain" content="gojs.net" /> <title> Finding and Highlighting Node-Link Paths in Graphs | GoJS Diagramming Library </title> </head> <body> <!-- This top nav is not part of the sample code --> <nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200"> <div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4"> <a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo" href="../"> GoJS </a> <div class="relative"> <button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation"> <svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button> <div id="topnavList" class="hidden md:block"> <div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit"> <a href="../learn/">Learn</a> <a href="../samples/">Samples</a> <a href="../intro/">Intro</a> <a href="../api/">API</a> <a href="../download.html">Download</a> <a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a> <a id="tc" href="https://nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a> <a id="tb" href="https://nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a> </div> </div> </div> </div> </nav> <script> window.addEventListener("DOMContentLoaded", function () { // topnav var topButton = document.getElementById("topnavButton"); var topnavList = document.getElementById("topnavList"); if (topButton && topnavList) { topButton.addEventListener("click", function (e) { topnavList .classList .toggle("hidden"); e.stopPropagation(); }); document.addEventListener("click", function (e) { // if the clicked element isn't the list, close the list if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) { topButton.click(); } }); // set active <a> element var url = window .location .href .toLowerCase(); var aTags = topnavList.getElementsByTagName('a'); for (var i = 0; i < aTags.length; i++) { var lowerhref = aTags[i] .href .toLowerCase(); if (lowerhref.endsWith('.html')) lowerhref = lowerhref.slice(0, -5); if (url.startsWith(lowerhref)) { aTags[i] .classList .add('active'); break; } } } }); </script> <div class="flex flex-col prose"> <div class="w-full max-w-screen-xl mx-auto"> <!-- * * * * * * * * * * * * * --> <!-- Start of GoJS sample code --> <script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script> <div id="allSampleContent" class="p-4 w-full"> <script id="code"> function init() { myDiagram = new go.Diagram('myDiagramDiv', { initialAutoScale: go.AutoScale.Uniform, contentAlignment: go.Spot.Center, layout: new go.ForceDirectedLayout(), maxSelectionCount: 2 }); // define the Node template myDiagram.nodeTemplate = new go.Node('Horizontal', { locationSpot: go.Spot.Center, // Node.location is the center of the Shape locationObjectName: 'SHAPE', selectionAdorned: false, selectionChanged: nodeSelectionChanged // defined below }) .add( new go.Panel('Spot') .add( new 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 }) .bindObject('fill', 'isSelected', (s, obj) => s ? 'red' : obj.part.data.color), new go.TextBlock() .bind('text', 'distance', d => d === Infinity ? 'INF' : d | 0) ), new go.TextBlock() .bind('text') ); // define the Link template myDiagram.linkTemplate = new go.Link({ selectable: false, // links cannot be selected by the user curve: go.Curve.Bezier, layerName: 'Background' // don't cross in front of any nodes }) .add( new go.Shape({ // this shape only shows when it isHighlighted isPanelMain: true, stroke: null, strokeWidth: 5 }) .bindObject('stroke', 'isHighlighted', h => h ? 'red' : null), new go.Shape({ // mark each Shape to get the link geometry with isPanelMain: true isPanelMain: true, stroke: 'black', strokeWidth: 1 }) .bind('stroke', 'color'), new 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; const e = diagram.lastInput; const count = diagram.selection.count; const curobj = diagram.findPartAt(e.documentPoint, false); if (curobj !== null) { if (count < 2) { // add the part to the selection if (!curobj.isSelected) { const part = curobj; if (part !== null) part.isSelected = true; } } else { if (!curobj.isSelected) { const 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() { const names = [ 'Joshua', 'Kathryn', 'Robert', 'Jason', 'Scott', 'Betsy', 'John', 'Walter', 'Gabriel', 'Simon', 'Emily', 'Tina', 'Elena', 'Samuel', 'Jacob', 'Michael', 'Juliana', 'Natalie', 'Grace', 'Ashley', 'Dylan' ]; const nodeDataArray = []; for (let i = 0; i < names.length; i++) { nodeDataArray.push({ key: i, text: names[i], color: go.Brush.randomColor(128, 240) }); } const linkDataArray = []; const num = nodeDataArray.length; for (let i = 0; i < num * 2; i++) { const a = Math.floor(i / 2); const 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(); const num = myDiagram.model.nodeDataArray.length; let node1 = null; let node2 = null; for (let i = Math.floor(Math.random() * num); i < num * 2; i++) { node1 = myDiagram.findNodeForKey(i % num); const distances = findDistances(node1); for (let j = Math.floor(Math.random() * num); j < num * 2; j++) { node2 = myDiagram.findNodeForKey(j % num); const 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) { const 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 const sel = document.getElementById('myPaths'); sel.innerHTML = ''; // show the distance for each node from the selected node const begin = diagram.selection.first(); showDistances(begin); if (diagram.selection.count === 2) { const 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 const it = distances.iterator; while (it.next()) { const n = it.key; const 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 const sel = document.getElementById('myPaths'); sel.innerHTML = ''; // clear out any old Option elements paths.each(p => { const 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) { let s = path.length + ': '; for (let 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() { const sel = document.getElementById('myPaths'); highlightPath(paths.get(sel.selectedIndex)); } // Highlight a particular path, a List of Nodes. function highlightPath(path) { myDiagram.clearHighlighteds(); for (let i = 0; i < path.count - 1; i++) { const f = path.get(i); const 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) { const diagram = source.diagram; // keep track of distances from the source node const distances = new go.Map(/*go.Node, "number"*/); // all nodes start with distance Infinity const nit = diagram.nodes; while (nit.next()) { const 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 const 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 const finished = new go.Set(/*go.Node*/); while (seen.count > 0) { // look at the unfinished node with the shortest distance so far const least = leastNode(seen, distances); const 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 const it = least.findLinksOutOf(); while (it.next()) { const link = it.value; const neighbor = link.getOtherNode(least); // skip nodes that we have finished if (finished.has(neighbor)) continue; const neighbordist = distances.get(neighbor); // assume "distance" along a link is unitary, but could be any non-negative number. const 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) { let bestdist = Infinity; let bestnode = null; const it = coll.iterator; while (it.next()) { const n = it.value; const 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 const path = new go.List(); path.add(end); while (end !== null) { let 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) { const stack = new go.List(/*go.Node*/); const 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 const 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> <div id="allTagDescriptions" class="p-4 w-full max-w-screen-xl mx-auto"> <hr/> <h3 class="text-xl">GoJS Features in this sample</h3> <!-- blacklist tags that do not correspond to a specific GoJS feature --> <h4>Collections</h4> <p> <b>GoJS</b> provides its own collection classes: <a href="../api/symbols/List.html" target="api">List</a>, <a href="../api/symbols/Set.html" target="api">Set</a>, and <a href="../api/symbols/Map.html" target="api">Map</a>. You can iterate over a collection by using an <a href="../api/symbols/Iterator.html" target="api">Iterator</a>. More information can be found in the <a href="../intro/collections.html">GoJS Intro</a>. </p> <p> <a href="../samples/index.html#collections">Related samples</a> </p> <hr> <!-- blacklist tags that do not correspond to a specific GoJS feature --> <h4>Force Directed Layout</h4> <p> This predefined layout treats the graph as if it were a system of physical bodies with forces acting on and between them. The layout iteratively moves nodes and links to minimize the total sum of forces on each node. The resulting layout will normally not contain overlapping Nodes, excluding cases where the graph is densely interconnected. More information can be found in the <a href="../intro/layouts.html#ForceDirectedLayout">GoJS Intro</a>. </p> <p> <a href="../samples/index.html#force-directed">Related samples</a> </p> <hr> <!-- blacklist tags that do not correspond to a specific GoJS feature --> <h4>HTML Interaction</h4> <p> GoJS Diagrams can be used alongside other HTML elements in a webapp. For custom Text Editors, Context Menus, and ToolTips, which are invoked and hidden via GoJS tool operations, it is best to use the <a href="../api/symbols/HTMLInfo.html" target="api">HTMLInfo</a> class. </p> <p> More information can be found in the <a href="../intro/HTMLinteraction.html">GoJS Intro</a>. </p> <p> <a href="../samples/index.html#html">Related samples</a> </p> <hr> </div> </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>