umanggarg Claude Sonnet 4.6 commited on
Commit
ae949da
Β·
1 Parent(s): bd43189

Cap Explore layout at 3 nodes per column to prevent tall stacking

Browse files

When the LLM produces a shallow dependency graph (many nodes at depth 0),
all root concepts piled into a single column making the layout very tall
and vertical. Now each depth level overflows to a new column after 3 items,
keeping the graph readable regardless of how the LLM structures depends_on.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. ui/src/components/ExploreView.jsx +47 -10
ui/src/components/ExploreView.jsx CHANGED
@@ -90,8 +90,15 @@ function expansionOffsets(selectedId, concepts, basePositions) {
90
  return offsets;
91
  }
92
 
93
- // ── Layout: topological column assignment ─────────────────────────────────────
94
  // Returns { [conceptId]: { x, y } } in canvas coordinates.
 
 
 
 
 
 
 
95
  function computeLayout(concepts) {
96
  if (!concepts.length) return {};
97
 
@@ -106,22 +113,52 @@ function computeLayout(concepts) {
106
  }
107
  concepts.forEach(c => depth(c.id));
108
 
109
- // Step 2: group by column, sort within column by reading_order
110
- const cols = {};
111
  concepts.forEach(c => {
112
  const col = depthCache[c.id] ?? 0;
113
- if (!cols[col]) cols[col] = [];
114
- cols[col].push(c);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  });
116
- Object.values(cols).forEach(arr =>
117
  arr.sort((a, b) => (a.reading_order ?? 99) - (b.reading_order ?? 99))
118
  );
119
 
120
- // Step 3: assign pixel positions β€” center each column vertically
121
- const maxColH = Math.max(...Object.values(cols).map(a => a.length)) * (CARD_H + ROW_GAP);
122
  const positions = {};
123
- Object.entries(cols).forEach(([col, nodes]) => {
124
- const x = Number(col) * (CARD_W + COL_GAP) + 48;
125
  const colH = nodes.length * (CARD_H + ROW_GAP) - ROW_GAP;
126
  const startY = (maxColH - colH) / 2 + 48;
127
  nodes.forEach((node, row) => {
 
90
  return offsets;
91
  }
92
 
93
+ // ── Layout: topological column assignment with overflow wrapping ───────────────
94
  // Returns { [conceptId]: { x, y } } in canvas coordinates.
95
+ //
96
+ // The LLM sometimes produces shallow dependency graphs (many nodes at depth 0),
97
+ // which naively stacks all root concepts into one tall column. We cap each
98
+ // column at MAX_PER_COL items and push overflow into the next available column,
99
+ // keeping the visual width reasonable and the graph readable.
100
+ const MAX_PER_COL = 3;
101
+
102
  function computeLayout(concepts) {
103
  if (!concepts.length) return {};
104
 
 
113
  }
114
  concepts.forEach(c => depth(c.id));
115
 
116
+ // Step 2: group by depth-column, sort within column by reading_order
117
+ const depthCols = {};
118
  concepts.forEach(c => {
119
  const col = depthCache[c.id] ?? 0;
120
+ if (!depthCols[col]) depthCols[col] = [];
121
+ depthCols[col].push(c);
122
+ });
123
+ Object.values(depthCols).forEach(arr =>
124
+ arr.sort((a, b) => (a.reading_order ?? 99) - (b.reading_order ?? 99))
125
+ );
126
+
127
+ // Step 3: assign visual columns, capping at MAX_PER_COL items per column.
128
+ // Each depth level starts in its own column. If a depth has more than MAX_PER_COL
129
+ // nodes, overflow spills into the next column. The following depth level then
130
+ // starts in the column after the last one used by the previous depth.
131
+ const colAssign = {};
132
+ let nextCol = 0;
133
+
134
+ const sortedDepths = Object.keys(depthCols).map(Number).sort((a, b) => a - b);
135
+ sortedDepths.forEach(d => {
136
+ const nodes = depthCols[d];
137
+ let col = nextCol;
138
+ let count = 0;
139
+ nodes.forEach(node => {
140
+ if (count >= MAX_PER_COL) { col++; count = 0; }
141
+ colAssign[node.id] = col;
142
+ count++;
143
+ });
144
+ nextCol = col + 1; // next depth starts after the last column used here
145
+ });
146
+
147
+ // Step 4: assign pixel positions β€” group by visual column, center each vertically
148
+ const visualCols = {};
149
+ concepts.forEach(c => {
150
+ const vc = colAssign[c.id] ?? 0;
151
+ if (!visualCols[vc]) visualCols[vc] = [];
152
+ visualCols[vc].push(c);
153
  });
154
+ Object.values(visualCols).forEach(arr =>
155
  arr.sort((a, b) => (a.reading_order ?? 99) - (b.reading_order ?? 99))
156
  );
157
 
158
+ const maxColH = Math.max(...Object.values(visualCols).map(a => a.length)) * (CARD_H + ROW_GAP);
 
159
  const positions = {};
160
+ Object.entries(visualCols).forEach(([vc, nodes]) => {
161
+ const x = Number(vc) * (CARD_W + COL_GAP) + 48;
162
  const colH = nodes.length * (CARD_H + ROW_GAP) - ROW_GAP;
163
  const startY = (maxColH - colH) / 2 + 48;
164
  nodes.forEach((node, row) => {