summaryrefslogtreecommitdiff
path: root/graph/web/groups.js
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-09-23 16:35:50 -0700
committerCliff Click <cliffc@acm.org>2026-09-23 16:35:50 -0700
commit0f32001b466def0cee753557ad6e296cf15e2504 (patch)
tree9e16038c574407bbd3f5c21f469c2df71e6835f8 /graph/web/groups.js
parent695429df954b084d2226fe2a6c1a47e1861bcc01 (diff)
downloadSimple-0f32001b466def0cee753557ad6e296cf15e2504.tar.gz
Simple-0f32001b466def0cee753557ad6e296cf15e2504.zip
Keyboard shortcuts
Fast forward/reverse, pick-a-frame. Shorter type names.
Diffstat (limited to 'graph/web/groups.js')
-rw-r--r--graph/web/groups.js46
1 files changed, 46 insertions, 0 deletions
diff --git a/graph/web/groups.js b/graph/web/groups.js
new file mode 100644
index 00000000..6ee4e091
--- /dev/null
+++ b/graph/web/groups.js
@@ -0,0 +1,46 @@
+// A display projection of a snapshot. Original nodes and edge slots stay intact.
+class GraphGroups {
+ static view(snap, folded) {
+ const raw = new Map(snap.nodes.map(n => [n.id, n]));
+ const groups = new Map((snap.groups || []).map(g => [g.id, {...g, members: new Set(g.nodes)}]));
+ const home = new Map(), cover = new Map();
+ for (const g of groups.values()) for (const id of g.nodes) home.set(id, g.id);
+ for (const g of groups.values()) {
+ for (let p = groups.get(g.par); p; p = groups.get(p.par))
+ for (const id of g.nodes) p.members.add(id);
+ }
+ for (const n of snap.nodes) {
+ let id = n.id;
+ for (let g = groups.get(home.get(id)); g; g = groups.get(g.par))
+ if (folded.has(g.id)) id = g.id;
+ cover.set(n.id, id);
+ }
+ const nodes = [], byId = new Map();
+ for (const n of snap.nodes) {
+ if (cover.get(n.id) !== n.id) continue;
+ const fold = groups.has(n.id) && folded.has(n.id);
+ const v = {...n, edges: [], fold: fold ? groups.get(n.id) : null};
+ if (fold) {
+ v.proj = null;
+ v.type = `${v.fold.members.size} nodes folded`;
+ }
+ nodes.push(v); byId.set(n.id, v);
+ }
+ for (const n of snap.nodes) for (const e of n.edges) {
+ const use = byId.get(cover.get(n.id)), def = cover.get(e.def) || 0;
+ if (use.fold && (!def || def === use.id)) continue;
+ use.edges.push({...e, idx: use.fold ? use.edges.length : e.idx, def,
+ orig: {use: n.id, def: e.def, idx: e.idx}});
+ }
+ // Expanded groups only; a folded group is represented by its header box.
+ const open = [...groups.values()].filter(g => !folded.has(g.id) && cover.get(g.id) === g.id);
+ const depth = g => {
+ let n = 0;
+ for (; g.par; g = groups.get(g.par)) n++;
+ return n;
+ };
+ // Paint outer containers before inner ones, even after headers are cloned.
+ open.sort((a,b) => depth(a) - depth(b) || a.id - b.id);
+ return {snap: {...snap, nodes}, raw, home, cover, open, groups};
+ }
+}