0.9.0: model graph with queries #27

Merged
root-at-skic merged 10 commits from feat/model-graph into main 2026-10-21 13:08:36 -07:00
Showing only changes of commit 91fcde9fa5 - Show all commits
+18 -7
View File
@@ -156,12 +156,22 @@ class GraphPage {
private elements() {
const nodes = this.graph.nodes.map((n) => ({ group: 'nodes' as const, data: { id: n.id, label: n.label, type: n.type ?? '', kind: n.kind, shared: n.shared ? 1 : 0, color: nodeColor(n) } }));
// Labels of connections that leave the same element are staggered along the line so they do not pile up.
const leaving = new Map<string, number>();
// In the layered layout a label sits at the end of the connection where fewer connections meet, so a
// fan-out labels at its targets and a fan-in at its sources; what still shares an end is staggered.
const outCount = new Map<string, number>();
const inCount = new Map<string, number>();
for (const e of this.graph.edges) {
if (e.kind !== 'connection') continue;
outCount.set(e.from, (outCount.get(e.from) ?? 0) + 1);
inCount.set(e.to, (inCount.get(e.to) ?? 0) + 1);
}
const used = new Map<string, number>();
const edges = this.graph.edges.map((e) => {
const index = e.kind === 'connection' ? leaving.get(e.from) ?? 0 : 0;
if (e.kind === 'connection') leaving.set(e.from, index + 1);
return { group: 'edges' as const, data: { id: e.id, source: e.from, target: e.to, kind: e.kind, label: e.labels.join(' / '), offset: 44 + (index % 4) * 30 } };
const at = e.kind === 'connection' && (outCount.get(e.from) ?? 0) > (inCount.get(e.to) ?? 0) ? 'target' : 'source';
const key = `${at}:${at === 'source' ? e.from : e.to}`;
const index = used.get(key) ?? 0;
used.set(key, index + 1);
return { group: 'edges' as const, data: { id: e.id, source: e.from, target: e.to, kind: e.kind, label: e.labels.join(' / '), at, offset: 40 + (index % 3) * 34 } };
});
return [...nodes, ...edges];
}
@@ -381,13 +391,14 @@ class GraphPage {
{ selector: 'node[kind = "registry"]', style: { shape: 'cut-rectangle', height: 32, 'font-size': 11, 'font-weight': 500, 'font-family': 'ui-monospace, Menlo, monospace', 'border-width': 1, 'border-color': p.muted, 'background-color': mix(dark ? '#94a3b8' : '#64748b', bg, 0.08), width: (n: NodeSingular) => Math.min(220, Math.max(110, String(n.data('label')).length * 6.8 + 36)) } },
{ selector: 'node.hover', style: { 'underlay-opacity': dark ? 0.22 : 0.16, 'border-width': 2, 'background-color': fill(dark ? 0.28 : 0.18) } },
{ selector: 'edge', style: { width: 1.4, 'line-color': p.edge, 'line-opacity': 0.85, 'target-arrow-color': p.edge, 'target-arrow-shape': 'triangle', 'arrow-scale': 0.8, 'curve-style': 'bezier', label: '', 'font-size': 10, 'font-family': p.font, color: p.muted, 'text-background-color': p.bg, 'text-background-opacity': 1, 'text-background-padding': '3px', 'text-background-shape': 'round-rectangle', 'text-rotation': 'autorotate', 'transition-property': 'width, line-color', 'transition-duration': 120 } },
{ selector: 'edge.ortho', style: { 'curve-style': 'taxi', 'taxi-direction': 'auto', 'taxi-turn': '45%', 'taxi-turn-min-distance': 24, 'text-rotation': 'none', 'source-text-offset': 'data(offset)', 'source-text-rotation': 'none', 'source-text-background-color': p.bg, 'source-text-background-opacity': 1, 'source-text-background-padding': '3px', 'source-text-background-shape': 'round-rectangle' } },
{ selector: 'edge.ortho', style: { 'curve-style': 'taxi', 'taxi-direction': 'auto', 'taxi-turn': '45%', 'taxi-turn-min-distance': 24, 'text-rotation': 'none', 'source-text-offset': 'data(offset)', 'source-text-rotation': 'none', 'source-text-background-color': p.bg, 'source-text-background-opacity': 1, 'source-text-background-padding': '3px', 'source-text-background-shape': 'round-rectangle', 'target-text-offset': 'data(offset)', 'target-text-rotation': 'none', 'target-text-background-color': p.bg, 'target-text-background-opacity': 1, 'target-text-background-padding': '3px', 'target-text-background-shape': 'round-rectangle' } },
{ selector: 'edge.edge-hover', style: { width: 2.4, 'line-color': p.fg, 'target-arrow-color': p.fg, 'line-opacity': 1, color: p.fg } },
{ selector: 'edge[kind != "connection"]', style: { display: 'none', width: 1, 'curve-style': 'bezier', 'line-style': 'dotted', 'target-arrow-shape': 'none', 'line-color': p.muted, 'line-opacity': 0.55 } },
{ selector: 'edge[kind = "supersedes"]', style: { 'line-style': 'dashed', 'line-color': TYPE_COLORS.decision, 'target-arrow-shape': 'triangle', 'target-arrow-color': TYPE_COLORS.decision } },
{ selector: 'edge[kind != "connection"].shown', style: { display: 'element' } },
{ selector: 'edge.labelled[kind = "connection"], edge.labelled[kind = "supersedes"]', style: { label: 'data(label)' } },
{ selector: 'edge.labelled.ortho[kind = "connection"]', style: { label: '', 'source-label': 'data(label)' } },
{ selector: 'edge.labelled.ortho[kind = "connection"][at = "source"]', style: { label: '', 'source-label': 'data(label)' } },
{ selector: 'edge.labelled.ortho[kind = "connection"][at = "target"]', style: { label: '', 'target-label': 'data(label)' } },
{ selector: '.dim', style: { opacity: 0.12, events: 'no' } },
{ selector: 'node.hit', style: { 'border-width': 2.5 } },
{ selector: 'edge.path', style: { width: 3, 'line-color': p.accent, 'target-arrow-color': p.accent, color: p.fg, 'line-opacity': 1 } },