//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//! Find the shortest path between two nodes.
import KeyLines from "keylines";
import { data, colours, styles } from "./findpath-data.js";
let chart;
const infoFields = document.getElementsByClassName("info");
let pathEnds = [null, null];
let pathWeighting = document.querySelector('input[name="option"]:checked')
.value;
let clearAnimation = [];
function formatDuration(number) {
const minsAsNumber = number % 60;
const hours = (number - minsAsNumber) / 60;
const minsAsTwoDigits = minsAsNumber < 10 ? `0${minsAsNumber}` : minsAsNumber;
return `${hours}:${minsAsTwoDigits}`;
}
// update the sidebar to show info about the path
function updateSideInfo(pathLength, pathDistance) {
infoFields[0].innerHTML = pathEnds[0];
infoFields[1].innerHTML = pathEnds[1];
infoFields[2].innerHTML = " ";
// user clicked a link (undefined) or 2 nodes that have no path between them (-0.5)
if (pathLength < 0 || pathLength === undefined) {
if (pathLength < 0) infoFields[2].innerHTML = "No available path";
infoFields[3].innerHTML = "N/A";
infoFields[4].innerHTML = "N/A";
} else if (pathWeighting === "distance") {
infoFields[3].innerHTML = pathLength;
infoFields[4].innerHTML = `${pathDistance}km`;
} else if (pathWeighting === "time") {
infoFields[3].innerHTML = pathLength;
infoFields[4].innerHTML = `${formatDuration(pathDistance)}hrs`;
} else if (pathWeighting === "equal") {
infoFields[3].innerHTML = pathLength;
infoFields[4].innerHTML = `${pathDistance} links`;
} else {
Array.from(infoFields).forEach((element) => {
element.innerHTML = "";
});
}
}
async function animateShortestPath(path, weighting) {
for (const [index, item] of path.onePath.entries()) {
if (index % 2 === 0) {
// item is a node, so update border & enlargement
await chart.animateProperties(
{ id: item, b: colours[weighting], ...styles.pathNode },
{ time: 30 }
);
clearAnimation.push({ id: item, ...styles.node });
} else {
// item is a link, so update colour and width
await chart.animateProperties(
{
id: item,
c: colours[weighting],
border: { colour: colours[weighting], width: 2, radius: 3 },
...styles.pathLink,
},
{ time: 30 }
);
clearAnimation.push({ id: item, ...styles.link });
}
}
}
function selectionChanged(node1, node2) {
chart.animateProperties(clearAnimation, { time: 30 });
clearAnimation = [];
// if we have 2 path ends, calculate the path and animate it
if (node1 && node2) {
const options = { direction: "any" };
if (pathWeighting !== "equal") options.value = pathWeighting;
const path = chart.graph().shortestPaths(node1, node2, options);
animateShortestPath(path, pathWeighting);
updateSideInfo((path.onePath.length - 1) / 2, path.distance);
}
}
function selectNode({ id }) {
function resetSelection() {
chart.selection({});
pathEnds = [null, null];
selectionChanged();
updateSideInfo();
}
const validatedItem = chart.getItem(id);
if (validatedItem && validatedItem.type === "node") {
// if user clicked a node, store it as a path end
pathEnds = [pathEnds[1], null];
pathEnds[1] = id;
// if we have 2 ends, give them to the selection change handler
if (pathEnds[0] !== null) selectionChanged(pathEnds[0], pathEnds[1]);
} else if (id === null || validatedItem) {
// user clicked the background or a link
resetSelection();
}
}
function initialiseInteractions() {
chart.on("click", selectNode);
// prevent users selecting more than one node
chart.on("selection-change", () => {
if (chart.selection()[1]) chart.selection([]);
});
const radioButtons = document.querySelectorAll('input[name="option"]');
Array.from(radioButtons).forEach((button) => {
button.addEventListener("click", () => {
pathWeighting = button.value;
selectionChanged(pathEnds[0], pathEnds[1]);
});
});
}
async function startKeyLines() {
const chartOptions = {
logo: { u: "/images/Logo.png" },
linkStyle: { inline: true },
drag: { links: false },
handMode: true,
overview: false,
minZoom: 0.45,
selectedLink: {
border: {
radius: 3,
colour: colours.themeDark,
width: 1,
},
fbc: colours.themeBase,
fc: "white",
c: colours.themeDark,
},
selectedNode: {
bw: 4,
fc: "white",
},
zoom: {
adaptiveStyling: false,
},
};
chart = await KeyLines.create({
container: "klchart",
options: chartOptions,
});
chart.load(data());
chart.zoom("fit", { animate: false });
initialiseInteractions();
}
window.addEventListener("DOMContentLoaded", startKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Find Shortest Paths</title>
<link rel="stylesheet" type="text/css" href="/css/keylines.css">
<link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
<link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
<link rel="stylesheet" type="text/css" href="/css/demo.css">
<script src="/vendor/jquery.js" defer type="text/javascript"></script>
<script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
<script src="/findpath.js" crossorigin="use-credentials" type="module"></script>
</head>
<body>
<div class="chart-wrapper demo-cols">
<div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
<div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
<div class="klchart" id="klchart">
</div>
</div>
</div>
<div class="rhs citext closed" id="demorhscontent">
<div class="title-bar">
<svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
<text x="0" y="38" style="width: 100%;">Find Shortest Paths</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<div class="cicontent">
<p>Click on two nodes to find the shortest path between them.</p>
<div class="form-stacked pull-left text-left" style="margin-left:15px;margin-top:20px;">
<fieldset>
<legend>Select Weight</legend>
<label class="radio">
<input type="radio" name="option" value="time" checked="checked" metric="Hr">Time
</label>
<label class="radio">
<input type="radio" name="option" value="distance" metric="km">Distance
</label>
<label class="radio">
<input type="radio" name="option" value="equal" metric="">None
</label>
</fieldset>
</div>
<table class="table">
<thead>
<tr>
<th colspan="2" id="title">Path Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>From:</td>
<td><strong class="info" id="startNode"></strong></td>
</tr>
<tr>
<td>To:</td>
<td><strong class="info" id="endNode"></strong></td>
</tr>
</tbody>
<thead>
<tr>
<th class="info" colspan="2" style="color: red;" id="pathInfoTitle"> </th>
</tr>
</thead>
<tbody>
<tr class="pathData">
<td>Number of Links:</td>
<td><strong class="info" id="links"></strong></td>
</tr>
<tr class="pathData">
<td>Weight Value:</td>
<td><strong class="info" id="weight"></strong></td>
</tr>
</tbody>
</table>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
Loading source
