Click on two nodes consecutively in the chart to see the shortest path between them.
You need to import the shortestPaths method from the ReGraph analysis module.
See also
import React, { useState } from "react";
import { createRoot } from "react-dom/client";
import cloneDeep from "lodash/cloneDeep";
import has from "lodash/has";
import isUndefined from "lodash/isUndefined";
import mapValues from "lodash/mapValues";
import merge from "lodash/merge";
import { Chart } from "regraph";
import { shortestPaths } from "regraph/analysis";
import { data, defaultPositions } from "./data";
import { style } from "@ci/theme/rg/js/storyStyles";
import "@ci/theme/rg/css/layout.css";
function isNode(item) {
return !has(item, "id1");
}
const pathLink = {
...style.selectMain,
width: 6,
label: { ...style.primaryLabel1, border: { ...style.selectMain, width: 4, radius: 20 } },
};
const endNode = {
border: { ...style.selectMain, width: 4 },
};
const pathNode = {
...endNode,
label: { ...style.darkLabel },
color: "transparent",
};
function weightValToString(weightVal, num) {
if (weightVal === "time") {
const min = num % 60;
const hr = (num - min) / 60;
const minVal = min < 10 ? `0${min}` : min;
return `${hr}:${minVal} hr`;
}
if (weightVal === "distance") {
return `${num} miles`;
}
return "";
}
export const Demo = () => <PathFinding items={data()} positions={ } />;
function PathFinding(props) {
const { items, positions } = props;
const [state, setState] = useState({
from: undefined,
to: undefined,
weightVal: "time",
path: undefined,
});
const updatePath = async ({ from, to, weightVal }) => {
let updatedPath;
if (from && to) {
const shortestPath = await shortestPaths(items, from, to, {
direction: "any",
value: weightVal,
});
updatedPath = { onePath: shortestPath.onePath, distance: shortestPath.distance };
} else {
updatedPath = undefined;
}
setState({
from,
to,
weightVal,
path: updatedPath,
});
};
const itemPressHandler = (id, itemType) => {
const { from, to, weightVal } = state;
// if pressing the background or pressing on a link
if (itemType === null || itemType === "link") {
// then we clear the ends
updatePath({ from: undefined, to: undefined, weightVal });
} else if (itemType === "node") {
// this was probably a node click but be careful because
// the navigation control ids also will arrive here if
// clicked - hence the if statement above
// only change anything if the id is different
if (id !== from && id !== to) {
if (!from) {
updatePath({ from: id, to, weightVal });
} else if (!to) {
updatePath({ to: id, from, weightVal });
} else {
updatePath({ from: to, to: id, weightVal });
}
}
}
};
const clickHandler = ({ preventDefault, id, itemType }) => {
itemPressHandler(id, itemType);
preventDefault();
};
// stop nodes being dragged
const dragHandler = ({ preventDefault, type }) => {
if (type === "node") {
preventDefault();
}
};
const setWeightVal = (weightVal) => {
updatePath({ ...state, weightVal });
};
const setLinkLabels = (itemsToUpdate, weightVal) =>
mapValues(itemsToUpdate, (item) => {
const updatedItem = { ...item };
if (!isNode(item)) {
updatedItem.label = {
text: weightValToString(weightVal, item.data[weightVal]),
color: style.colors.charcoal,
backgroundColor: "white",
border: { color: style.colors.charcoal, radius: 20, width: 1 },
padding: "4 6 3 6",
...item.label,
};
}
return updatedItem;
});
const styledItems = () => {
const { from, to, weightVal, path } = state;
const styled = { ...items };
if (from) {
styled[from] = merge(cloneDeep(items[from]), endNode);
}
if (to) {
styled[to] = merge(cloneDeep(items[to]), endNode);
}
if (path) {
const { onePath } = path;
// style items along the path
for (let i = 1; i < onePath.length - 1; i += 1) {
const id = onePath[i];
const item = items[id];
const pathStyle = isNode(item) ? pathNode : pathLink;
styled[id] = merge(cloneDeep(items[id]), pathStyle);
}
}
const itemsToReturn = { ...items, ...styled };
return setLinkLabels(itemsToReturn, weightVal);
};
return (
<div className="story">
<div className="options">
<button
type="button"
className={state.weightVal === "time" ? "active" : ""}
onClick={() => setWeightVal("time")}
>
Time
</button>
<button
type="button"
className={state.weightVal === "distance" ? "active" : ""}
onClick={() => setWeightVal("distance")}
>
Distance
</button>
</div>
<div className="chart-wrapper">
<Chart
items={styledItems()}
positions={positions}
onClick={clickHandler}
onDragStart={dragHandler}
animation={{ animate: false }}
options={{
links: { inlineLabels: true },
navigation: false,
overview: false,
selection: { color: "rgba(0,0,0,0)", labelColor: "black" },
}}
/>
</div>
{isUndefined(state.path) ? (
<pre className="properties empty">Select two nodes to calculate the shortest path</pre>
) : (
<pre className="properties">
<p>
{state.weightVal === "distance" ? "Distance" : "Time"}:{" "}
{weightValToString(state.weightVal, state.path.distance)}
</p>
<p>Links: {(state.path.onePath.length - 1) / 2}</p>
</pre>
)}
</div>
);
}
const root = createRoot(document.getElementById("regraph"));
root.render(<Demo />); import { style } from "@ci/theme/rg/js/storyStyles";
function data() {
const cities = [
"Atlantic City",
"Baltimore",
"Buffalo",
"Charleston",
"Cincinnati",
"Cleveland",
"Columbus",
"Detroit",
"Harrisburg",
"London",
"New York",
"Norfolk",
"Philadelphia",
"Pittsburgh",
"Richmond",
"Scranton",
"Toledo",
"Toronto",
"Washington",
"Youngstown",
];
const connections = {
"Atlantic City-Baltimore": {
id1: "Atlantic City",
id2: "Baltimore",
data: {
distance: 152,
time: 158,
},
},
"Atlantic City-Norfolk": {
id1: "Atlantic City",
id2: "Norfolk",
data: {
distance: 322,
time: 466,
},
},
"Atlantic City-Philadelphia": {
id1: "Atlantic City",
id2: "Philadelphia",
data: {
distance: 62,
time: 64,
},
},
"Buffalo-Harrisburg": {
id1: "Buffalo",
id2: "Harrisburg",
data: {
distance: 289,
time: 340,
},
},
"Buffalo-Scranton": {
id1: "Buffalo",
id2: "Scranton",
data: {
distance: 287,
time: 296,
},
},
"Charleston-Pittsburgh": {
id1: "Charleston",
id2: "Pittsburgh",
data: {
distance: 229,
time: 229,
},
},
"Charleston-Richmond": {
id1: "Charleston",
id2: "Richmond",
data: {
distance: 316,
time: 316,
},
},
"Charleston-Washington": {
id1: "Charleston",
id2: "Washington",
data: {
distance: 366,
time: 366,
},
},
"Cincinnati-Columbus": {
id1: "Cincinnati",
id2: "Columbus",
data: {
distance: 111,
time: 115,
},
},
"Cleveland-Charleston": {
id1: "Cleveland",
id2: "Charleston",
data: {
distance: 250,
time: 256,
},
},
"Cleveland-Youngstown": {
id1: "Cleveland",
id2: "Youngstown",
data: {
distance: 76,
time: 80,
},
},
"Columbus-Charleston": {
id1: "Columbus",
id2: "Charleston",
data: {
distance: 167,
time: 207,
},
},
"Columbus-Cleveland": {
id1: "Columbus",
id2: "Cleveland",
data: {
distance: 142,
time: 147,
},
},
"Columbus-Washington": {
id1: "Columbus",
id2: "Washington",
data: {
distance: 417,
time: 428,
},
},
"Detroit-Toledo": {
id1: "Detroit",
id2: "Toledo",
data: {
distance: 62,
time: 66,
},
},
"Harrisburg-Baltimore": {
id1: "Harrisburg",
id2: "Baltimore",
data: {
distance: 88,
time: 92,
},
},
"Harrisburg-Philadelphia": {
id1: "Harrisburg",
id2: "Philadelphia",
data: {
distance: 114,
time: 118,
},
},
"Harrisburg-Scranton": {
id1: "Harrisburg",
id2: "Scranton",
data: {
distance: 120,
time: 123,
},
},
"Harrisburg-Washington": {
id1: "Harrisburg",
id2: "Washington",
data: {
distance: 126,
time: 138,
},
},
"London-Buffalo": {
id1: "London",
id2: "Buffalo",
data: {
distance: 157,
time: 180,
},
},
"London-Detroit": {
id1: "London",
id2: "Detroit",
data: {
distance: 127,
time: 127,
},
},
"London-Toronto": {
id1: "London",
id2: "Toronto",
data: {
distance: 125,
time: 125,
},
},
"New York-Atlantic City": {
id1: "New York",
id2: "Atlantic City",
data: {
distance: 134,
time: 151,
},
},
"New York-Harrisburg": {
id1: "New York",
id2: "Harrisburg",
data: {
distance: 178,
time: 198,
},
},
"New York-Philadelphia": {
id1: "New York",
id2: "Philadelphia",
data: {
distance: 99,
time: 120,
},
},
"New York-Scranton": {
id1: "New York",
id2: "Scranton",
data: {
distance: 131,
time: 146,
},
},
"New York-Youngstown": {
id1: "New York",
id2: "Youngstown",
data: {
distance: 410,
time: 467,
},
},
"Philadelphia-Baltimore": {
id1: "Philadelphia",
id2: "Baltimore",
data: {
distance: 102,
time: 102,
},
},
"Philadelphia-Norfolk": {
id1: "Philadelphia",
id2: "Norfolk",
data: {
distance: 276,
time: 348,
},
},
"Pittsburgh-Buffalo": {
id1: "Pittsburgh",
id2: "Buffalo",
data: {
distance: 216,
time: 219,
},
},
"Pittsburgh-Harrisburg": {
id1: "Pittsburgh",
id2: "Harrisburg",
data: {
distance: 204,
time: 208,
},
},
"Pittsburgh-Washington": {
id1: "Pittsburgh",
id2: "Washington",
data: {
distance: 252,
time: 242,
},
},
"Richmond-Norfolk": {
id1: "Richmond",
id2: "Norfolk",
data: {
distance: 93,
time: 97,
},
},
"Richmond-Washington": {
id1: "Richmond",
id2: "Washington",
data: {
distance: 106,
time: 110,
},
},
"Scranton-Philadelphia": {
id1: "Scranton",
id2: "Philadelphia",
data: {
distance: 126,
time: 131,
},
},
"Toledo-Cincinnati": {
id1: "Toledo",
id2: "Cincinnati",
data: {
distance: 198,
time: 203,
},
},
"Toledo-Cleveland": {
id1: "Toledo",
id2: "Cleveland",
data: {
distance: 115,
time: 115,
},
},
"Toledo-Columbus": {
id1: "Toledo",
id2: "Columbus",
data: {
distance: 142,
time: 164,
},
},
"Toronto-Buffalo": {
id1: "Toronto",
id2: "Buffalo",
data: {
distance: 107,
time: 107,
},
},
"Youngstown-Harrisburg": {
id1: "Youngstown",
id2: "Harrisburg",
data: {
distance: 267,
time: 272,
},
},
"Youngstown-Pittsburgh": {
id1: "Youngstown",
id2: "Pittsburgh",
data: {
distance: 70,
time: 73,
},
},
};
const nodes = {};
for (const city of cities) {
nodes[city] = {
...style.primary1,
shape: "box",
border: { radius: 30 },
label: {
...style.primaryLabel1,
text: city,
fontSize: "auto",
maxHeight: 16,
backgroundColor: style.colors.transparent,
},
};
}
return { ...nodes, ...connections };
}
function defaultPositions() {
return {
"Atlantic City": {
x: 2045,
y: 779,
},
Baltimore: {
x: 1836,
y: 848,
},
Buffalo: {
x: 1322,
y: 473,
},
Charleston: {
x: 1235,
y: 1211,
},
Cincinnati: {
x: 804,
y: 1241,
},
Cleveland: {
x: 1067,
y: 779,
},
Columbus: {
x: 972,
y: 1062,
},
Detroit: {
x: 828,
y: 702,
},
Harrisburg: {
x: 1703,
y: 738,
},
London: {
x: 1040,
y: 527,
},
"New York": {
x: 2027,
y: 537,
},
Norfolk: {
x: 2031,
y: 1221,
},
Philadelphia: {
x: 1910,
y: 716,
},
Pittsburgh: {
x: 1323,
y: 855,
},
Richmond: {
x: 1817,
y: 1178,
},
Scranton: {
x: 1770,
y: 525,
},
Toledo: {
x: 830,
y: 816,
},
Toronto: {
x: 1208,
y: 392,
},
Washington: {
x: 1772,
y: 956,
},
Youngstown: {
x: 1215,
y: 809,
},
};
}
export { data, defaultPositions }; <!doctype html>
<html>
<body>
<div id="regraph" style="height: 100vh"></div>
<script type="module" src="./code.jsx"></script>
</body>
</html>