cytoscape = {
if (window.cytoscape) return window.cytoscape;
const src = "https://cdn.jsdelivr.net/npm/cytoscape@3.30.3/dist/cytoscape.min.js";
await new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = src;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error("Failed to load Cytoscape"));
document.head.appendChild(script);
});
return window.cytoscape;
}Results Network Graph
Interactive, progressive network built with Observable JS and Cytoscape.js.
This page follows the 2025-12-16 plan for the Results Network Graph. It uses Observable JS inside Quarto to turn the long-format results file into a progressive, filterable network.
How to use
- Filters narrow the rows used to build the network (Core Responsibility, Program, and keyword search across labels and evidence).
- The initial view shows only Organization and Core Responsibility nodes. Click a node to expand its children; use Expand visible -> children to grow all visible nodes one step; use Reset view to return to the starting view.
- Click any node to open the details panel. Indicator / Output nodes list evidence snippets and contributing programs.
Data columns
Actual_Results_OfficialSchema_Long.csv columns used here:
- Organization
- Core_Responsibility_or_Section
- Departmental_Result
- Program
- Program_Result_or_Initiative
- Indicator_or_Output
- Program_Evidence
// Load and lightly clean the long-format data.
rawRows = (await FileAttachment("results-network-data/Actual_Results_OfficialSchema_Long.csv")
.csv({ typed: false }))
.map((row) => Object.fromEntries(Object.entries(row).map(([k, v]) => [k, (v ?? "").trim()])));typeMeta = ({
Organization: { label: "Organization", color: "#1f4b7a", level: 0 },
Core_Responsibility_or_Section: { label: "Core Responsibility", color: "#2f7ea0", level: 1 },
Departmental_Result: { label: "Departmental Result", color: "#3b8db2", level: 2 },
Program: { label: "Program", color: "#4b9ec6", level: 3 },
Program_Result_or_Initiative: { label: "Program Result / Initiative", color: "#69b0d0", level: 4 },
Indicator_or_Output: { label: "Indicator / Output", color: "#93c2de", level: 5 }
});// Filter controls
viewof filters = {
const coreOptions = ["All", ...new Set(rawRows.map((d) => d.Core_Responsibility_or_Section).filter(Boolean))].sort();
const programOptions = ["All", ...new Set(rawRows.map((d) => d.Program).filter(Boolean))].sort();
const form = html`<form class="results-graph-filters">
<label class="results-graph-filter">
<span>Core Responsibility</span>
<select name="core" class="form-select form-select-sm"></select>
</label>
<label class="results-graph-filter">
<span>Program</span>
<select name="program" class="form-select form-select-sm"></select>
</label>
<label class="results-graph-filter results-graph-filter-search">
<span>Search labels or evidence</span>
<input
name="search"
type="text"
class="form-control form-control-sm"
placeholder="Keywords (case-insensitive)"
/>
</label>
</form>`;
const coreSelect = form.querySelector('select[name="core"]');
const programSelect = form.querySelector('select[name="program"]');
const searchInput = form.querySelector('input[name="search"]');
function fillOptions(selectEl, options) {
selectEl.innerHTML = "";
for (const option of options) {
const opt = document.createElement("option");
opt.value = option;
opt.textContent = option;
selectEl.appendChild(opt);
}
}
function updateValue() {
form.value = {
core: coreSelect.value,
program: programSelect.value,
search: searchInput.value
};
}
function emit() {
updateValue();
form.dispatchEvent(new Event("input", { bubbles: true }));
}
fillOptions(coreSelect, coreOptions);
fillOptions(programSelect, programOptions);
updateValue();
coreSelect.addEventListener("change", emit);
programSelect.addEventListener("change", emit);
searchInput.addEventListener("input", emit);
return form;
}// Apply filters and prepare rows for graphing.
filteredRows = {
const term = (filters.search || "").toLowerCase().trim();
return rawRows.filter((row) => {
if (filters.core !== "All" && row.Core_Responsibility_or_Section !== filters.core) return false;
if (filters.program !== "All" && row.Program !== filters.program) return false;
if (!term) return true;
const haystack = [
row.Organization,
row.Core_Responsibility_or_Section,
row.Departmental_Result,
row.Program,
row.Program_Result_or_Initiative,
row.Indicator_or_Output,
row.Program_Evidence
]
.join(" ")
.toLowerCase();
return haystack.includes(term);
});
}// Helper utilities
levelByType = Object.fromEntries(Object.keys(typeMeta).map((k) => [k, typeMeta[k].level]));slugify = (text) => {
const base = text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return base || "item";
};prepareGraph = (rows) => {
const nodes = new Map();
const edges = new Map();
const children = new Map();
const evidence = new Map();
function ensureNode(type, label) {
if (!label) return null;
const id = `${type}:${slugify(label)}`;
if (!nodes.has(id)) {
nodes.set(id, { id, label, type, level: levelByType[type], color: typeMeta[type].color });
}
return nodes.get(id);
}
function connect(source, target) {
if (!source || !target) return;
const id = `${source.id}->${target.id}`;
if (!edges.has(id)) edges.set(id, { id, source: source.id, target: target.id });
}
for (const row of rows) {
const org = ensureNode("Organization", row.Organization);
const core = ensureNode("Core_Responsibility_or_Section", row.Core_Responsibility_or_Section);
const dr = ensureNode("Departmental_Result", row.Departmental_Result);
const program = ensureNode("Program", row.Program);
const pri = ensureNode("Program_Result_or_Initiative", row.Program_Result_or_Initiative);
const indicator = ensureNode("Indicator_or_Output", row.Indicator_or_Output);
connect(org, core);
connect(core, dr);
connect(dr, program);
connect(program, pri);
connect(pri, indicator);
if (indicator) {
const bucket =
evidence.get(indicator.id) ?? { programs: new Set(), results: new Set(), snippets: new Set() };
if (row.Program) bucket.programs.add(row.Program);
if (row.Program_Result_or_Initiative) bucket.results.add(row.Program_Result_or_Initiative);
if (row.Program_Evidence) bucket.snippets.add(row.Program_Evidence);
evidence.set(indicator.id, bucket);
}
}
for (const edge of edges.values()) {
if (!children.has(edge.source)) children.set(edge.source, new Set());
children.get(edge.source).add(edge.target);
}
const nodesList = [...nodes.values()];
const edgesList = [...edges.values()];
const initialVisible = nodesList.filter((n) => n.level <= 1).map((n) => n.id);
const roots = nodesList.filter((n) => n.level === 0).map((n) => n.id);
const nodeById = new Map(nodesList.map((n) => [n.id, n]));
const evidenceMap = new Map(
[...evidence.entries()].map(([id, data]) => [
id,
{
programs: [...data.programs].sort(),
results: [...data.results].sort(),
snippets: [...data.snippets].sort()
}
])
);
const childrenMap = new Map([...children.entries()].map(([id, set]) => [id, [...set]]));
return { nodes: nodesList, nodeById, edges: edgesList, roots, initialVisible, evidence: evidenceMap, children: childrenMap };
};// Render the network graph with progressive expansion.
viewof networkGraph = {
if (!graphData.nodes.length) {
return html`<div class="results-graph-empty">No rows match the current filters.</div>`;
}
const shell = html`<div class="results-graph-shell">
<div class="results-graph-panel">
<div class="results-graph-toolbar">
<button class="btn btn-primary btn-sm graph-reset">Reset view</button>
<button class="btn btn-outline-primary btn-sm graph-expand">Expand visible -> children</button>
<span class="text-muted graph-counts"></span>
</div>
<div class="results-graph-canvas"></div>
</div>
<div class="results-graph-detail">
<div class="meta">Click a node to see its details.</div>
</div>
</div>`;
const canvas = shell.querySelector(".results-graph-canvas");
const detail = shell.querySelector(".results-graph-detail");
const counts = shell.querySelector(".graph-counts");
const resetBtn = shell.querySelector(".graph-reset");
const expandBtn = shell.querySelector(".graph-expand");
let visible = new Set(graphData.initialVisible);
let cy;
const baseStyle = [
{
selector: "node",
style: {
"background-color": "data(color)",
label: "data(label)",
color: "#0b1d2a",
"font-size": 11,
"text-wrap": "wrap",
"text-max-width": 160,
"text-valign": "center",
"text-halign": "center",
padding: "6px",
"border-color": "#0e2437",
"border-width": 1.25
}
},
{
selector: "edge",
style: {
width: 1.6,
"curve-style": "bezier",
"line-color": "#b4c7d6",
"target-arrow-color": "#b4c7d6",
"target-arrow-shape": "triangle"
}
},
{
selector: "node:selected",
style: {
"border-color": "#d9731e",
"border-width": 3
}
}
];
const layoutOptions = () => ({
name: "breadthfirst",
directed: true,
padding: 30,
spacingFactor: 1.15
});
const elementsFromVisible = () => {
const nodes = graphData.nodes
.filter((n) => visible.has(n.id))
.map((n) => ({ data: { ...n, typeLabel: typeMeta[n.type].label } }));
const edges = graphData.edges
.filter((e) => visible.has(e.source) && visible.has(e.target))
.map((e) => ({ data: e }));
return [...nodes, ...edges];
};
function runLayout() {
if (!cy || !cy.nodes().length) return;
try {
cy.layout(layoutOptions()).run();
} catch (error) {
cy.layout({ name: "grid", padding: 30 }).run();
}
}
function refresh(runLayoutFlag = true) {
if (!cy) return;
cy.elements().remove();
cy.add(elementsFromVisible());
if (runLayoutFlag) runLayout();
updateCounts();
}
function updateCounts() {
const edgeCount = graphData.edges.filter((e) => visible.has(e.source) && visible.has(e.target)).length;
counts.textContent = `${visible.size} nodes / ${edgeCount} edges shown`;
}
function addChildren(nodeId) {
const kids = graphData.children.get(nodeId) || [];
for (const kid of kids) visible.add(kid);
}
function addChildrenForVisible() {
for (const nodeId of [...visible]) addChildren(nodeId);
}
function resetView() {
visible = new Set(graphData.initialVisible);
refresh(true);
renderDetails(null);
}
function renderDetails(nodeId) {
detail.innerHTML = "";
if (!nodeId) {
detail.appendChild(html`<div class="meta">Click a node to see its details.</div>`);
return;
}
const node = graphData.nodeById.get(nodeId);
if (!node) return;
const header = html`<div>
<h4>${node.label}</h4>
<div class="meta">${typeMeta[node.type].label}</div>
</div>`;
const childList = graphData.children.get(nodeId) || [];
const childInfo =
childList.length > 0
? html`<div class="meta">${childList.length} immediate child${childList.length === 1 ? "" : "ren"}.</div>`
: html`<div class="meta">No children in the current filtered data.</div>`;
detail.append(header, childInfo);
const evidence = graphData.evidence.get(nodeId);
if (evidence && evidence.snippets.length) {
detail.appendChild(html`<div class="results-graph-evidence-title">Evidence</div>`);
const list = html`<ul>${evidence.snippets.map((s) => html`<li>${s}</li>`)}</ul>`;
detail.appendChild(list);
if (evidence.programs.length) {
detail.appendChild(
html`<div class="meta"><strong>Contributing programs:</strong> ${evidence.programs.join(", ")}</div>`
);
}
if (evidence.results.length) {
detail.appendChild(
html`<div class="meta"><strong>Program results / initiatives:</strong> ${evidence.results.join("; ")}</div>`
);
}
}
}
resetBtn.onclick = () => resetView();
expandBtn.onclick = () => {
addChildrenForVisible();
refresh(true);
};
function initGraph() {
if (!canvas.isConnected) {
requestAnimationFrame(initGraph);
return;
}
cy = cytoscape({
container: canvas,
elements: elementsFromVisible(),
style: baseStyle,
wheelSensitivity: 0.2
});
runLayout();
cy.on("tap", "node", (evt) => {
const id = evt.target.id();
addChildren(id);
refresh(true);
renderDetails(id);
});
updateCounts();
}
requestAnimationFrame(initGraph);
invalidation.then(() => {
if (cy) cy.destroy();
});
return shell;
}