create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
585 lines (518 loc) • 23.2 kB
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>