164 lines
8.2 KiB
Text
164 lines
8.2 KiB
Text
#!/usr/bin/env nu
|
|
|
|
# Project the ontology (core.ncl) into a SELF-CONTAINED, navigable Cytoscape graph
|
|
# page. This is the interactive "prize" the About graph block links to (ADR-057
|
|
# hook-static / prize-live) — but daemon-free: the data is embedded and the page
|
|
# uses the cytoscape libs already shipped under /js/. The site's own /graph
|
|
# (Leptos GraphView) renders an empty canvas under htmx-ssr (no WASM hydration),
|
|
# so we ship our own page instead.
|
|
#
|
|
# Served as a static file at /images/ontoref-graph.html (only /images, /js, /styles
|
|
# are static-served; bare paths are intercepted by the content router).
|
|
#
|
|
# Usage (from outreach/site):
|
|
# nu scripts/build/gen-graph-page.nu --root ../.. --out site/public/images/ontoref-graph.html
|
|
|
|
const TEMPLATE = r##'<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>ontoref · ontology graph</title>
|
|
<style>
|
|
:root{--amber:#E8A838;--silver:#C0CCD8;--dark:#0F1319;--surface:#141c24;--border:#1e2a38;--gray:#8090A4}
|
|
*{box-sizing:border-box}
|
|
html,body{margin:0;height:100%;background:var(--dark);color:var(--silver);font-family:Inter,system-ui,sans-serif}
|
|
#bar{position:fixed;top:0;left:0;right:0;height:52px;display:flex;align-items:center;gap:14px;padding:0 18px;border-bottom:1px solid var(--border);background:rgba(15,19,25,.86);backdrop-filter:blur(8px);z-index:5}
|
|
#bar .brand{font-family:ui-monospace,monospace;font-weight:700;color:var(--amber);font-size:1.05rem;text-decoration:none}
|
|
#bar .brand:hover{opacity:.82}
|
|
#bar .meta{color:var(--gray);font-size:.85rem}
|
|
#bar .btn{margin-left:auto;color:var(--silver);text-decoration:none;border:1px solid var(--border);border-radius:.4rem;padding:.3rem .75rem;font-size:.85rem}
|
|
#bar .btn:hover{border-color:var(--amber);color:var(--amber)}
|
|
#cy{position:fixed;top:52px;left:0;right:0;bottom:0}
|
|
.panel{position:fixed;background:rgba(20,28,36,.92);border:1px solid var(--border);border-radius:.5rem;z-index:5}
|
|
#legend{left:18px;bottom:18px;padding:.6rem .85rem;font-size:.8rem}
|
|
#legend .row{display:flex;align-items:center;gap:.5rem;margin:.28rem 0}
|
|
#legend .dot{width:11px;height:11px;border-radius:50%;flex:0 0 auto}
|
|
#hint{right:18px;bottom:18px;padding:.45rem .7rem;color:var(--gray);font-size:.78rem}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="bar">
|
|
<a href="/" class="brand" title="ontoref home">ontoref</a>
|
|
<span class="meta">ontology graph · __NCOUNT__ nodes · __ECOUNT__ edges</span>
|
|
<a href="/about" class="btn">← About</a>
|
|
</div>
|
|
<div id="cy"></div>
|
|
<div id="legend" class="panel">
|
|
<div class="row"><span class="dot" style="background:#E8A838"></span>Axioms · __NAX__</div>
|
|
<div class="row"><span class="dot" style="background:#C0CCD8"></span>Tensions · __NTN__</div>
|
|
<div class="row"><span class="dot" style="background:#7fc8e0"></span>Practices · __NPR__</div>
|
|
</div>
|
|
<div id="hint" class="panel">drag to pan · scroll to zoom · click a node to focus · click empty space to reset</div>
|
|
<script type="application/json" id="graph-data">/*__ELEMENTS__*/</script>
|
|
<script src="/js/cytoscape.min.js"></script>
|
|
<script src="/js/layout-base.js"></script>
|
|
<script src="/js/cose-base.js"></script>
|
|
<script src="/js/cytoscape-fcose.js"></script>
|
|
<script>
|
|
(function(){
|
|
var COL = { Axiom:"#E8A838", Tension:"#C0CCD8", Practice:"#7fc8e0" };
|
|
var data = JSON.parse(document.getElementById("graph-data").textContent);
|
|
var cy = cytoscape({
|
|
container: document.getElementById("cy"),
|
|
elements: data,
|
|
wheelSensitivity: 0.2,
|
|
style: [
|
|
{ selector:"node", style:{
|
|
"background-color": function(ele){ return COL[ele.data("level")] || "#7fc8e0"; },
|
|
"width": function(ele){ return ele.data("level")==="Axiom" ? 30 : (ele.data("level")==="Tension" ? 26 : 20); },
|
|
"height": function(ele){ return ele.data("level")==="Axiom" ? 30 : (ele.data("level")==="Tension" ? 26 : 20); },
|
|
"label":"data(title)", "color":"#C0CCD8", "font-size":"9px",
|
|
"text-valign":"bottom", "text-halign":"center", "text-margin-y":3,
|
|
"min-zoomed-font-size":7, "border-width":1, "border-color":"#0F1319"
|
|
} },
|
|
{ selector:"edge", style:{ "width":1, "line-color":"#46607c", "opacity":0.34, "curve-style":"haystack" } },
|
|
{ selector:".faded", style:{ "opacity":0.07, "text-opacity":0.07 } },
|
|
{ selector:"node.hi", style:{ "border-width":2, "border-color":"#E8A838", "opacity":1, "text-opacity":1 } },
|
|
{ selector:"edge.hi", style:{ "line-color":"#E8A838", "opacity":0.85, "width":1.6 } }
|
|
]
|
|
});
|
|
cy.minZoom(0.15); cy.maxZoom(3);
|
|
if (window.cytoscapeFcose) { try { cytoscape.use(window.cytoscapeFcose); } catch(e){} }
|
|
// fcose with packComponents:true PACKS the 5 edgeless orphan nodes right next to
|
|
// the main component (no scattered corners, no two groups); nodeDimensionsInclude-
|
|
// Labels spaces by label box (kills overlap); fit + post-fit fills the viewport.
|
|
var COMMON = { animate:false, fit:true, padding:50, nodeDimensionsIncludeLabels:true };
|
|
var layoutOpts = Object.assign({
|
|
name:"fcose", quality:"proof", randomize:true, packComponents:true,
|
|
idealEdgeLength:80, nodeSeparation:95, gravity:0.25, numIter:2500
|
|
}, COMMON);
|
|
var l;
|
|
try { l = cy.layout(layoutOpts); }
|
|
catch(e){ l = cy.layout(Object.assign({ name:"cose", componentSpacing:90, nodeRepulsion:500000 }, COMMON)); }
|
|
l.one("layoutstop", function(){ cy.fit(undefined, 50); });
|
|
l.run();
|
|
cy.on("tap","node",function(ev){
|
|
var n = ev.target;
|
|
var nb = n.closedNeighborhood();
|
|
cy.elements().addClass("faded");
|
|
nb.removeClass("faded").addClass("hi");
|
|
});
|
|
cy.on("tap", function(ev){ if (ev.target === cy){ cy.elements().removeClass("faded hi"); } });
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
'##
|
|
|
|
def main [
|
|
--root: string = "../.."
|
|
--out: string = "site/public/images/ontoref-graph.html"
|
|
# Gate mode: reproduce from core.ncl and refuse if what is served differs. This page is the
|
|
# LIVE PRIZE the About routes to (ADR-057 hook-static / prize-live) — a prize that silently
|
|
# stops tracking the ontology is worse than no prize, because the reveal still promises it.
|
|
# Same shape as gen-architecture.nu --check.
|
|
--check
|
|
] {
|
|
let ip = $"($root)/code/ontology"
|
|
let core = $"($root)/.ontoref/ontology/core.ncl"
|
|
if not ($core | path exists) { error make { msg: $"core.ncl not found: ($core)" } }
|
|
let graph = (nickel export --format json --import-path $ip $core | from json)
|
|
let nodes = ($graph | get nodes)
|
|
let edges = ($graph.edges? | default [])
|
|
|
|
let ids = ($nodes | each {|n| $n.id? | default "" } | where {|x| $x | is-not-empty })
|
|
let node_els = ($nodes | each {|n| { data: {
|
|
id: ($n.id? | default ""),
|
|
title: ($n.name? | default ($n.id? | default "")),
|
|
level: ($n.level? | default "Practice"),
|
|
} } })
|
|
let edge_els = ($edges | where {|e| (($e.from? | default "") in $ids) and (($e.to? | default "") in $ids) } | each {|e| { data: {
|
|
id: $"($e.from)__($e.to)__($e.kind? | default 'rel')",
|
|
source: ($e.from? | default ""),
|
|
target: ($e.to? | default ""),
|
|
kind: ($e.kind? | default ""),
|
|
} } })
|
|
let elements = ($node_els | append $edge_els | to json --raw)
|
|
|
|
let nax = ($nodes | where {|n| ($n.level? | default "") == "Axiom" } | length)
|
|
let ntn = ($nodes | where {|n| ($n.level? | default "") == "Tension" } | length)
|
|
let npr = ($nodes | where {|n| ($n.level? | default "") == "Practice" } | length)
|
|
|
|
let html = ($TEMPLATE
|
|
| str replace --all '__NCOUNT__' ($nodes | length | into string)
|
|
| str replace --all '__ECOUNT__' ($edge_els | length | into string)
|
|
| str replace --all '__NAX__' ($nax | into string)
|
|
| str replace --all '__NTN__' ($ntn | into string)
|
|
| str replace --all '__NPR__' ($npr | into string)
|
|
| str replace '/*__ELEMENTS__*/' $elements)
|
|
|
|
if $check {
|
|
if (not ($out | path exists)) or ((open --raw $out) != $html) {
|
|
print $"(ansi red)DRIFT: ($out) does not reproduce from core.ncl \(run: just diagram\)(ansi reset)"
|
|
exit 1
|
|
}
|
|
print $"(ansi green)gen-graph-page: up to date(ansi reset) · ($nodes | length) nodes · ($edge_els | length) edges"
|
|
return
|
|
}
|
|
|
|
let parent = ($out | path dirname)
|
|
if ($parent | is-not-empty) { mkdir $parent }
|
|
$html | save -f $out
|
|
print $"gen-graph-page: ($nodes | length) nodes · ($edge_els | length) edges → ($out)"
|
|
}
|