The network below has six nodes and several edges, each labeled with the time at which it is active. Pick a source and a target, then click Find path to search for a time-respecting route — one where each hop departs no earlier than the previous hop arrived.
<!-- Temporal network path finder — edges carry timestamps; a path is valid only if timestamps are non-decreasing. -->
<p class="hint">Each edge is labeled with the time it is active. A valid temporal path must use edges in non-decreasing time order. Choose a source and target, then click <b>Find path</b>.</p>
<div class="controls">
<label>Source: <select id="src"></select></label>
<label>Target: <select id="tgt"></select></label>
<button id="find" type="button">Find path</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="460" height="260"></canvas>
<div class="status" id="status">Choose source and target, then find a path.</div>
/* Layout and visual styling for the temporal network demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .5rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .3rem; }
select { font-size: .88rem; border: 1px solid #aaa; border-radius: 6px; padding: .2rem .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 10px;
background: #f7f9fb; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin-top: .5rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.idle { color: #555; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Temporal graph definition: nodes with fixed positions, edges with timestamps.
// Node positions on the 460x260 canvas (id, x, y).
const NODES = [
{ id: 'A', x: 60, y: 130 },
{ id: 'B', x: 170, y: 55 },
{ id: 'C', x: 170, y: 205 },
{ id: 'D', x: 290, y: 55 },
{ id: 'E', x: 290, y: 205 },
{ id: 'F', x: 400, y: 130 }
];
// Directed edges with timestamps. Each edge is traversable at time t or later.
// Format: { u: source, v: target, t: active time }
const EDGES = [
{ u: 'A', v: 'B', t: 1 },
{ u: 'A', v: 'C', t: 3 },
{ u: 'B', v: 'D', t: 2 },
{ u: 'C', v: 'E', t: 5 },
{ u: 'D', v: 'F', t: 4 },
{ u: 'E', v: 'F', t: 6 },
{ u: 'B', v: 'E', t: 7 },
{ u: 'C', v: 'D', t: 1 }
];
const nodeMap = {};
NODES.forEach(n => { nodeMap[n.id] = n; });
// BFS over temporal edges: only use edge (u,v,t) if t >= current arrival time.
function findTemporalPath(src, tgt) {
// Start at source at time 0.
const queue = [{ node: src, time: 0, path: [src] }];
const visited = {};
visited[src + ':0'] = true;
while (queue.length) {
const { node, time, path } = queue.shift();
if (node === tgt) return path;
// Expand neighbors reachable via edges active at or after current time.
for (const e of EDGES) {
let next = null;
if (e.u === node && e.t >= time) next = e.v;
else if (e.v === node && e.t >= time) next = e.u;
if (!next) continue;
const key = next + ':' + e.t;
if (!visited[key]) {
visited[key] = true;
queue.push({ node: next, time: e.t + 1, path: [...path, next] });
}
}
}
return null;
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Draw the network on the canvas, highlighting the found path if any.
function draw(highlightPath, highlightEdges) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw edges first so nodes appear on top.
for (const e of EDGES) {
const a = nodeMap[e.u], b = nodeMap[e.v];
const onPath = highlightEdges && highlightEdges.some(
he => (he.u === e.u && he.v === e.v && he.t === e.t) ||
(he.u === e.v && he.v === e.u && he.t === e.t)
);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = onPath ? '#0a7d33' : '#b0bec5';
ctx.lineWidth = onPath ? 3 : 1.5;
ctx.stroke();
// Draw timestamp label at midpoint of each edge.
const mx = (a.x + b.x) / 2;
const my = (a.y + b.y) / 2;
ctx.font = 'bold 11px system-ui';
ctx.fillStyle = onPath ? '#0a7d33' : '#607d8b';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('t=' + e.t, mx, my - 8);
}
// Draw node circles with labels.
for (const n of NODES) {
const onPath = highlightPath && highlightPath.includes(n.id);
ctx.beginPath();
ctx.arc(n.x, n.y, 18, 0, Math.PI * 2);
ctx.fillStyle = onPath ? '#1d3557' : '#e8eef3';
ctx.strokeStyle = onPath ? '#1d3557' : '#90a4ae';
ctx.lineWidth = 2;
ctx.fill();
ctx.stroke();
ctx.font = 'bold 14px system-ui';
ctx.fillStyle = onPath ? '#fff' : '#1d3557';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(n.id, n.x, n.y);
}
}
// Populate source/target dropdowns with node ids.
const srcEl = document.getElementById('src');
const tgtEl = document.getElementById('tgt');
NODES.forEach(n => {
const a = document.createElement('option'); a.value = n.id; a.textContent = n.id; srcEl.appendChild(a);
const b = document.createElement('option'); b.value = n.id; b.textContent = n.id; tgtEl.appendChild(b);
});
tgtEl.value = 'F';
function setStatus(t, cls) {
const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls;
}
document.getElementById('find').addEventListener('click', () => {
const src = srcEl.value, tgt = tgtEl.value;
if (src === tgt) { setStatus('Source and target are the same node.', 'idle'); draw(null, null); return; }
const path = findTemporalPath(src, tgt);
if (!path) {
setStatus(i18nFmt('No temporal path from %src% to %tgt% — the timestamps do not line up.', { src: src, tgt: tgt }), 'bad');
draw(null, null);
} else {
// Collect the earliest valid edge for each hop in the path.
const hEdges = [];
for (let i = 0; i < path.length - 1; i++) {
const u = path[i], v = path[i + 1];
const candidates = EDGES.filter(e =>
(e.u === u && e.v === v) || (e.u === v && e.v === u)
);
candidates.sort((a, b) => a.t - b.t);
if (candidates.length) hEdges.push(candidates[0]);
}
draw(path, hEdges);
setStatus(i18nFmt('Path: %path%', { path: path.join(' → ') }), 'ok');
}
});
document.getElementById('reset').addEventListener('click', () => {
srcEl.value = 'A'; tgtEl.value = 'F';
setStatus('Choose source and target, then find a path.', 'idle');
draw(null, null);
});
draw(null, null);
setStatus('Choose source and target, then find a path.', 'idle');
<!-- Temporal network path finder — edges carry timestamps; a path is valid only if timestamps are non-decreasing. -->
<p class="hint">Each edge is labeled with the time it is active. A valid temporal path must use edges in non-decreasing time order. Choose a source and target, then click <b>Find path</b>.</p>
<div class="controls">
<label>Source: <select id="src"></select></label>
<label>Target: <select id="tgt"></select></label>
<button id="find" type="button">Find path</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="460" height="260"></canvas>
<div class="status" id="status">Choose source and target, then find a path.</div>
/* Layout and visual styling for the temporal network demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .5rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .3rem; }
select { font-size: .88rem; border: 1px solid #aaa; border-radius: 6px; padding: .2rem .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 10px;
background: #f7f9fb; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin-top: .5rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.idle { color: #555; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Temporal graph definition: nodes with fixed positions, edges with timestamps.
// Node positions on the 460x260 canvas (id, x, y).
const NODES = [
{ id: 'A', x: 60, y: 130 },
{ id: 'B', x: 170, y: 55 },
{ id: 'C', x: 170, y: 205 },
{ id: 'D', x: 290, y: 55 },
{ id: 'E', x: 290, y: 205 },
{ id: 'F', x: 400, y: 130 }
];
// Directed edges with timestamps. Each edge is traversable at time t or later.
// Format: { u: source, v: target, t: active time }
const EDGES = [
{ u: 'A', v: 'B', t: 1 },
{ u: 'A', v: 'C', t: 3 },
{ u: 'B', v: 'D', t: 2 },
{ u: 'C', v: 'E', t: 5 },
{ u: 'D', v: 'F', t: 4 },
{ u: 'E', v: 'F', t: 6 },
{ u: 'B', v: 'E', t: 7 },
{ u: 'C', v: 'D', t: 1 }
];
const nodeMap = {};
NODES.forEach(n => { nodeMap[n.id] = n; });
// BFS over temporal edges: only use edge (u,v,t) if t >= current arrival time.
function findTemporalPath(src, tgt) {
// Start at source at time 0.
const queue = [{ node: src, time: 0, path: [src] }];
const visited = {};
visited[src + ':0'] = true;
while (queue.length) {
const { node, time, path } = queue.shift();
if (node === tgt) return path;
// Expand neighbors reachable via edges active at or after current time.
for (const e of EDGES) {
let next = null;
if (e.u === node && e.t >= time) next = e.v;
else if (e.v === node && e.t >= time) next = e.u;
if (!next) continue;
const key = next + ':' + e.t;
if (!visited[key]) {
visited[key] = true;
queue.push({ node: next, time: e.t + 1, path: [...path, next] });
}
}
}
return null;
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Draw the network on the canvas, highlighting the found path if any.
function draw(highlightPath, highlightEdges) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw edges first so nodes appear on top.
for (const e of EDGES) {
const a = nodeMap[e.u], b = nodeMap[e.v];
const onPath = highlightEdges && highlightEdges.some(
he => (he.u === e.u && he.v === e.v && he.t === e.t) ||
(he.u === e.v && he.v === e.u && he.t === e.t)
);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = onPath ? '#0a7d33' : '#b0bec5';
ctx.lineWidth = onPath ? 3 : 1.5;
ctx.stroke();
// Draw timestamp label at midpoint of each edge.
const mx = (a.x + b.x) / 2;
const my = (a.y + b.y) / 2;
ctx.font = 'bold 11px system-ui';
ctx.fillStyle = onPath ? '#0a7d33' : '#607d8b';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('t=' + e.t, mx, my - 8);
}
// Draw node circles with labels.
for (const n of NODES) {
const onPath = highlightPath && highlightPath.includes(n.id);
ctx.beginPath();
ctx.arc(n.x, n.y, 18, 0, Math.PI * 2);
ctx.fillStyle = onPath ? '#1d3557' : '#e8eef3';
ctx.strokeStyle = onPath ? '#1d3557' : '#90a4ae';
ctx.lineWidth = 2;
ctx.fill();
ctx.stroke();
ctx.font = 'bold 14px system-ui';
ctx.fillStyle = onPath ? '#fff' : '#1d3557';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(n.id, n.x, n.y);
}
}
// Populate source/target dropdowns with node ids.
const srcEl = document.getElementById('src');
const tgtEl = document.getElementById('tgt');
NODES.forEach(n => {
const a = document.createElement('option'); a.value = n.id; a.textContent = n.id; srcEl.appendChild(a);
const b = document.createElement('option'); b.value = n.id; b.textContent = n.id; tgtEl.appendChild(b);
});
tgtEl.value = 'F';
function setStatus(t, cls) {
const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls;
}
document.getElementById('find').addEventListener('click', () => {
const src = srcEl.value, tgt = tgtEl.value;
if (src === tgt) { setStatus('Source and target are the same node.', 'idle'); draw(null, null); return; }
const path = findTemporalPath(src, tgt);
if (!path) {
setStatus(i18nFmt('No temporal path from %src% to %tgt% — the timestamps do not line up.', { src: src, tgt: tgt }), 'bad');
draw(null, null);
} else {
// Collect the earliest valid edge for each hop in the path.
const hEdges = [];
for (let i = 0; i < path.length - 1; i++) {
const u = path[i], v = path[i + 1];
const candidates = EDGES.filter(e =>
(e.u === u && e.v === v) || (e.u === v && e.v === u)
);
candidates.sort((a, b) => a.t - b.t);
if (candidates.length) hEdges.push(candidates[0]);
}
draw(path, hEdges);
setStatus(i18nFmt('Path: %path%', { path: path.join(' → ') }), 'ok');
}
});
document.getElementById('reset').addEventListener('click', () => {
srcEl.value = 'A'; tgtEl.value = 'F';
setStatus('Choose source and target, then find a path.', 'idle');
draw(null, null);
});
draw(null, null);
setStatus('Choose source and target, then find a path.', 'idle');
Notice what happens when you choose pairs where the timestamps run "backwards." The nodes may look connected on a static map, yet no valid temporal path exists because every route requires traveling an earlier edge after a later one — and time does not allow that.
Comments
Loading comments...