459 lines
14 KiB
HTML
Raw Normal View History

2026-03-13 00:18:14 +00:00
{% extends "base.html" %}
{% block title %}Ontology Graph — Ontoref{% endblock title %}
{% block nav_graph %}active{% endblock nav_graph %}
{% block head %}
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.30.2/dist/cytoscape.min.js"></script>
<style>
#graph-root {
display: flex;
height: calc(100vh - 148px);
min-height: 400px;
gap: 0;
user-select: none;
}
#cy-wrapper {
flex: 1 1 auto;
min-width: 220px;
overflow: hidden;
border-radius: 0.5rem;
}
#cy { width: 100%; height: 100%; }
#resize-handle {
flex: 0 0 6px;
cursor: col-resize;
background: transparent;
position: relative;
z-index: 20;
transition: background 0.15s;
}
#resize-handle:hover,
#resize-handle.dragging { background: oklch(var(--p) / 0.5); }
#resize-handle::after {
content: "";
position: absolute;
left: 2px;
top: 50%;
transform: translateY(-50%);
width: 2px;
height: 40px;
border-radius: 99px;
background: oklch(var(--bc) / 0.25);
}
#detail-panel {
flex: 0 0 300px;
min-width: 160px;
max-width: 60%;
overflow-y: auto;
overflow-x: hidden;
border-radius: 0.5rem;
}
</style>
{% endblock head %}
{% block content %}
<!-- Toolbar -->
<div class="mb-2 flex flex-wrap items-center justify-between gap-2 text-sm">
<h1 class="text-xl font-bold">Ontology Graph</h1>
<div class="flex flex-wrap gap-1 items-center">
<!-- Level filters (toggle = hide that level) -->
<button class="filter-btn btn btn-xs btn-warning" data-level="Axiom">◆ Axiom</button>
<button class="filter-btn btn btn-xs btn-error" data-level="Tension">● Tension</button>
<button class="filter-btn btn btn-xs btn-success" data-level="Practice">▪ Practice</button>
<div class="w-px h-4 bg-base-content/20 mx-1"></div>
<!-- Pole filters -->
<button class="filter-btn btn btn-xs" style="background:#f59e0b;color:#111;border-color:#f59e0b" data-pole="Yang">Yang</button>
<button class="filter-btn btn btn-xs" style="background:#3b82f6;color:#fff;border-color:#3b82f6" data-pole="Yin">Yin</button>
<button class="filter-btn btn btn-xs" style="background:#8b5cf6;color:#fff;border-color:#8b5cf6" data-pole="Spiral">Spiral</button>
<div class="w-px h-4 bg-base-content/20 mx-1"></div>
<!-- Layout -->
<div class="join">
<button id="btn-bfs" class="join-item btn btn-xs btn-primary">Hierarchy</button>
<button id="btn-cose" class="join-item btn btn-xs btn-ghost">Force</button>
</div>
<button id="btn-reset" class="btn btn-xs btn-ghost">Reset</button>
</div>
</div>
<!-- Split: graph | drag handle | detail -->
<div id="graph-root">
<div id="cy-wrapper" class="bg-base-200">
<div id="cy"></div>
</div>
<div id="resize-handle"></div>
<div id="detail-panel" class="bg-base-200 p-4 hidden">
<div class="flex justify-between items-start mb-2">
<h3 class="font-bold text-base leading-tight" id="d-name"></h3>
<button id="btn-close-panel" class="btn btn-xs btn-ghost ml-2 flex-shrink-0"></button>
</div>
<div class="flex flex-wrap gap-1 mb-2" id="d-badges"></div>
<p class="text-xs text-base-content/70 mb-3 leading-relaxed" id="d-description"></p>
<div id="d-artifacts" class="hidden mb-3">
<p class="text-xs font-semibold text-base-content/40 uppercase tracking-wider mb-1">Artifacts</p>
<ul id="d-artifact-list" class="text-xs font-mono text-base-content/60 space-y-1 break-all"></ul>
</div>
<div id="d-edges" class="hidden">
<p class="text-xs font-semibold text-base-content/40 uppercase tracking-wider mb-1">Connections</p>
<ul id="d-edge-list" class="text-xs text-base-content/60 space-y-1"></ul>
</div>
</div>
</div>
{% endblock content %}
{% block scripts %}
<script>
const GRAPH = {{ graph_json | safe }};
const POLE_COLOR = { Yang: "#f59e0b", Yin: "#3b82f6", Spiral: "#8b5cf6" };
const LEVEL_SHAPE = {
Axiom: "diamond",
Tension: "ellipse",
Practice: "round-rectangle",
Project: "hexagon",
Moment: "triangle",
};
const EDGE_STYLE = {
ManifestsIn: { color: "#6b7280" },
DependsOn: { color: "#ef4444" },
Resolves: { color: "#22c55e" },
Implies: { color: "#f59e0b" },
Complements: { color: "#3b82f6" },
Contradicts: { color: "#ec4899", dashed: true },
TensionWith: { color: "#f97316", dashed: true },
Contains: { color: "#a3a3a3" },
FlowsTo: { color: "#06b6d4" },
SpiralsWith: { color: "#8b5cf6" },
LimitedBy: { color: "#f43f5e" },
ValidatedBy: { color: "#84cc16" },
};
const nodes = (GRAPH.nodes || []).map(n => ({
data: {
id: n.id,
label: n.name || n.id,
pole: n.pole || "",
level: n.level || "",
description: n.description || "",
invariant: !!n.invariant,
artifact_paths: n.artifact_paths || [],
color: POLE_COLOR[n.pole] || "#6b7280",
shape: LEVEL_SHAPE[n.level] || "ellipse",
}
}));
const edges = (GRAPH.edges || []).map(e => {
const s = EDGE_STYLE[e.kind] || {};
return {
data: {
id: `${e.from}__${e.to}__${e.kind}`,
source: e.from,
target: e.to,
kind: e.kind || "",
note: e.note || "",
color: s.color || "#6b7280",
dashed: !!s.dashed,
}
};
});
// ── Cytoscape ────────────────────────────────────────────────
const cy = cytoscape({
container: document.getElementById("cy"),
elements: { nodes, edges },
style: [
{
selector: "node",
style: {
"background-color": "data(color)",
"shape": "data(shape)",
"width": 32,
"height": 32,
"border-width": 2,
"border-color": "#111827",
"label": "data(label)",
"color": "#f9fafb",
"font-size": "10px",
"font-family": "ui-sans-serif, system-ui, sans-serif",
"font-weight": 500,
"text-valign": "bottom",
"text-halign": "center",
"text-margin-y": 6,
"text-wrap": "wrap",
"text-max-width": "100px",
"text-background-color": "#0f172a",
"text-background-opacity": 0.85,
"text-background-shape": "round-rectangle",
"text-background-padding": "3px",
"text-outline-width": 0,
}
},
{
selector: "node[?invariant]",
style: { "border-color": "#f59e0b", "border-width": 3 }
},
{
selector: "node:selected",
style: { "border-color": "#ffffff", "border-width": 3 }
},
{
selector: "node.faded",
style: { "opacity": 0.2, "text-opacity": 0.2 }
},
{
selector: "edge",
style: {
"line-color": "data(color)",
"target-arrow-color": "data(color)",
"target-arrow-shape": "triangle",
"curve-style": "bezier",
"width": 1.5,
"arrow-scale": 0.8,
"opacity": 0.6,
}
},
{
selector: "edge[?dashed]",
style: { "line-style": "dashed", "line-dash-pattern": [6, 4] }
},
{
selector: "edge.faded",
style: { "opacity": 0.05 }
},
{
selector: "edge.highlighted",
style: {
"opacity": 1,
"width": 2.5,
"label": "data(kind)",
"font-size": "9px",
"color": "#e5e7eb",
"text-background-color": "#0f172a",
"text-background-opacity": 0.85,
"text-background-padding": "2px",
"text-background-shape": "round-rectangle",
}
},
],
layout: buildBfsLayout(),
wheelSensitivity: 0.3,
boxSelectionEnabled: false,
minZoom: 0.1,
maxZoom: 4,
});
function buildBfsLayout() {
return {
name: "breadthfirst",
directed: true,
animate: false,
spacingFactor: 2.2,
padding: 48,
fit: true,
avoidOverlap: true,
nodeDimensionsIncludeLabels: true,
};
}
function buildCoseLayout() {
return {
name: "cose",
animate: false,
randomize: true,
componentSpacing: 120,
nodeRepulsion: 800000,
nodeOverlap: 80,
idealEdgeLength: 220,
edgeElasticity: 20,
nestingFactor: 1.0,
gravity: 2,
numIter: 3000,
initialTemp: 2000,
coolingFactor: 0.99,
minTemp: 1.0,
fit: true,
padding: 60,
};
}
// ── Filters ──────────────────────────────────────────────────
// Track which levels/poles are hidden
const hiddenLevels = new Set();
const hiddenPoles = new Set();
function applyFilters() {
cy.nodes().forEach(n => {
const hide = hiddenLevels.has(n.data("level")) || hiddenPoles.has(n.data("pole"));
if (hide) n.hide(); else n.show();
});
cy.edges().forEach(e => {
if (e.source().hidden() || e.target().hidden()) e.hide();
else e.show();
});
}
document.querySelectorAll(".filter-btn[data-level]").forEach(btn => {
btn.addEventListener("click", () => {
const level = btn.dataset.level;
if (hiddenLevels.has(level)) {
hiddenLevels.delete(level);
btn.style.opacity = "1";
btn.style.textDecoration = "";
} else {
hiddenLevels.add(level);
btn.style.opacity = "0.4";
btn.style.textDecoration = "line-through";
}
applyFilters();
});
});
document.querySelectorAll(".filter-btn[data-pole]").forEach(btn => {
btn.addEventListener("click", () => {
const pole = btn.dataset.pole;
if (hiddenPoles.has(pole)) {
hiddenPoles.delete(pole);
btn.style.opacity = "1";
btn.style.textDecoration = "";
} else {
hiddenPoles.add(pole);
btn.style.opacity = "0.4";
btn.style.textDecoration = "line-through";
}
applyFilters();
});
});
// ── Layout buttons ───────────────────────────────────────────
const btnBfs = document.getElementById("btn-bfs");
const btnCose = document.getElementById("btn-cose");
btnBfs.addEventListener("click", () => {
cy.layout(buildBfsLayout()).run();
btnBfs.classList.add("btn-primary"); btnBfs.classList.remove("btn-ghost");
btnCose.classList.add("btn-ghost"); btnCose.classList.remove("btn-primary");
});
btnCose.addEventListener("click", () => {
cy.layout(buildCoseLayout()).run();
btnCose.classList.add("btn-primary"); btnCose.classList.remove("btn-ghost");
btnBfs.classList.add("btn-ghost"); btnBfs.classList.remove("btn-primary");
});
document.getElementById("btn-reset").addEventListener("click", () => {
cy.elements().removeClass("faded highlighted");
cy.fit(undefined, 48);
closePanel();
});
// ── Node detail panel ────────────────────────────────────────
const panel = document.getElementById("detail-panel");
const dName = document.getElementById("d-name");
const dBadges = document.getElementById("d-badges");
const dDesc = document.getElementById("d-description");
const dArtifacts = document.getElementById("d-artifacts");
const dList = document.getElementById("d-artifact-list");
const dEdges = document.getElementById("d-edges");
const dEdgeList = document.getElementById("d-edge-list");
function closePanel() {
panel.classList.add("hidden");
cy.elements().removeClass("faded highlighted");
}
cy.on("tap", "node", evt => {
const d = evt.target.data();
panel.classList.remove("hidden");
dName.textContent = d.label;
dBadges.innerHTML =
`<span class="badge badge-xs badge-outline">${d.level}</span>` +
`<span class="badge badge-xs" style="background:${d.color};color:#111;border:none">${d.pole}</span>` +
(d.invariant ? `<span class="badge badge-xs badge-warning">invariant</span>` : "");
dDesc.textContent = d.description;
if (d.artifact_paths.length) {
dArtifacts.classList.remove("hidden");
dList.innerHTML = d.artifact_paths.map(p =>
`<li class="break-all"><code>${p}</code></li>`
).join("");
} else {
dArtifacts.classList.add("hidden");
}
const conn = evt.target.connectedEdges();
if (conn.length) {
dEdges.classList.remove("hidden");
dEdgeList.innerHTML = conn.map(e => {
const isSrc = e.data("source") === d.id;
const other = isSrc
? cy.getElementById(e.data("target")).data("label")
: cy.getElementById(e.data("source")).data("label");
const arrow = isSrc ? "→" : "←";
return `<li class="flex gap-1"><span class="opacity-40 flex-shrink-0">${arrow}</span>` +
`<span class="text-base-content/80 flex-shrink-0">${e.data("kind")}</span>` +
`<span class="opacity-60 break-all">${other}</span></li>`;
}).join("");
} else {
dEdges.classList.add("hidden");
}
// Dim non-neighbours
cy.elements().addClass("faded").removeClass("highlighted");
evt.target.closedNeighborhood().removeClass("faded").addClass("highlighted");
});
cy.on("tap", evt => {
if (evt.target === cy) closePanel();
});
document.getElementById("btn-close-panel").addEventListener("click", closePanel);
// ── Resizable split ───────────────────────────────────────────
const handle = document.getElementById("resize-handle");
const cyWrapper = document.getElementById("cy-wrapper");
const graphRoot = document.getElementById("graph-root");
let resizing = false;
handle.addEventListener("mousedown", e => {
resizing = true;
handle.classList.add("dragging");
document.body.style.cursor = "col-resize";
e.preventDefault();
});
document.addEventListener("mousemove", e => {
if (!resizing) return;
const rect = graphRoot.getBoundingClientRect();
const hW = handle.offsetWidth;
const cyW = Math.max(220, e.clientX - rect.left);
const panW = Math.max(160, rect.width - cyW - hW);
if (cyW + panW + hW <= rect.width + 2) {
cyWrapper.style.flex = `0 0 ${cyW}px`;
if (!panel.classList.contains("hidden")) {
panel.style.flex = `0 0 ${panW}px`;
}
cy.resize();
}
});
document.addEventListener("mouseup", () => {
if (!resizing) return;
resizing = false;
handle.classList.remove("dragging");
document.body.style.cursor = "";
});
</script>
{% endblock scripts %}