aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMtBntChvn <[email protected]>2026-02-21 08:11:41 +0000
committerMtBntChvn <[email protected]>2026-02-21 08:11:41 +0000
commite30933f55b7c9ea8eb6f30bd3d6c0d55a51f7971 (patch)
treef58bb6a140123317361608fbb9adef9afd6d853e /src
parentfocus view on expanded node and fit children after expansion (diff)
downloadzen-e30933f55b7c9ea8eb6f30bd3d6c0d55a51f7971.tar.xz
zen-e30933f55b7c9ea8eb6f30bd3d6c0d55a51f7971.zip
replace fireworks expansion with ripple (concentric rings) placement
Children are now placed in concentric rings around the expanded node like ripples in water, instead of a single arc at a large radius. Ring placement: - base_radius=70, ring_gap=40, min_node_gap=45 - Each ring fits floor(arc_length / min_gap) nodes - Inner rings fill first, outer rings hold more nodes - Semi-circle for non-root, full circle for root Push-away reduced from 200+4n to a fixed 80px since the tight rings no longer need a large clearance. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/zenserver/frontend/html/pages/graph-debug-playground.js151
-rw-r--r--src/zenserver/frontend/html/util/graphengine.js58
2 files changed, 127 insertions, 82 deletions
diff --git a/src/zenserver/frontend/html/pages/graph-debug-playground.js b/src/zenserver/frontend/html/pages/graph-debug-playground.js
index 26e8f4b52..19c117d1c 100644
--- a/src/zenserver/frontend/html/pages/graph-debug-playground.js
+++ b/src/zenserver/frontend/html/pages/graph-debug-playground.js
@@ -326,19 +326,35 @@ export class Page extends ZenPage
_expand_root_synthetic(node, all_deps, visible)
{
const engine = this._engine;
- const radius = 200 + visible * 4;
+ const base_radius = 70;
+ const ring_gap = 40;
+ const min_node_gap = 45;
+ const arc_span = 2 * Math.PI;
var added = 0;
- for (const dep of all_deps)
+ var ring = 0;
+
+ while (added < all_deps.length && added < MAX_VISIBLE_DEPS)
{
- if (added >= MAX_VISIBLE_DEPS) { node.truncated = true; break; }
- const t = visible > 1 ? added / (visible - 1) : 0.5;
- const angle = t * 2 * Math.PI;
- const r = radius + this._rng() * 40;
- const label = short_name(dep.opkey);
- engine.add_node(dep.opkey, node.x + Math.cos(angle) * r, node.y + Math.sin(angle) * r, false, label);
- engine.add_edge(node, engine.node_map[dep.opkey], dep.dep_name);
- added++;
+ const r = base_radius + ring * ring_gap;
+ const capacity = Math.max(1, Math.floor(arc_span * r / min_node_gap));
+ const remaining = Math.min(all_deps.length, MAX_VISIBLE_DEPS) - added;
+ const batch = Math.min(capacity, remaining);
+
+ for (var j = 0; j < batch; ++j)
+ {
+ const dep = all_deps[added];
+ const t = batch > 1 ? j / (batch - 1) : 0.5;
+ const angle = t * arc_span;
+ const jitter = (this._rng() - 0.5) * 10;
+ const label = short_name(dep.opkey);
+ engine.add_node(dep.opkey, node.x + Math.cos(angle) * (r + jitter), node.y + Math.sin(angle) * (r + jitter), false, label);
+ engine.add_edge(node, engine.node_map[dep.opkey], dep.dep_name);
+ added++;
+ }
+ ring++;
}
+ if (added < all_deps.length)
+ node.truncated = true;
layout_run(engine.nodes, engine.edges, node.x, node.y, 80);
remove_overlaps(engine.nodes, 10, 8);
@@ -352,33 +368,47 @@ export class Page extends ZenPage
? Math.atan2(node.y - parent.y, node.x - parent.x)
: 0;
- // push node away from parent — skip if re-expanding in place
+ // small push to clear immediate area
if (parent && !node._skip_push)
{
- const push_dist = 400 + visible * 6;
- node.x += Math.cos(outward_angle) * push_dist;
- node.y += Math.sin(outward_angle) * push_dist;
+ node.x += Math.cos(outward_angle) * 80;
+ node.y += Math.sin(outward_angle) * 80;
}
node.pinned = true;
const existing = new Set(engine.nodes);
+ // ripple placement: concentric semi-circle rings
const arc_span = Math.PI;
const arc_start = outward_angle - arc_span / 2;
- const radius = 200 + visible * 4;
-
+ const base_radius = 70;
+ const ring_gap = 40;
+ const min_node_gap = 45;
var added = 0;
- for (const dep of all_deps)
+ var ring = 0;
+
+ while (added < all_deps.length && added < MAX_VISIBLE_DEPS)
{
- if (added >= MAX_VISIBLE_DEPS) { node.truncated = true; break; }
- const t = visible > 1 ? added / (visible - 1) : 0.5;
- const angle = arc_start + t * arc_span;
- const r = radius + this._rng() * 40;
- const label = short_name(dep.opkey);
- engine.add_node(dep.opkey, node.x + Math.cos(angle) * r, node.y + Math.sin(angle) * r, false, label);
- engine.add_edge(node, engine.node_map[dep.opkey], dep.dep_name);
- added++;
+ const r = base_radius + ring * ring_gap;
+ const capacity = Math.max(1, Math.floor(arc_span * r / min_node_gap));
+ const remaining = Math.min(all_deps.length, MAX_VISIBLE_DEPS) - added;
+ const batch = Math.min(capacity, remaining);
+
+ for (var j = 0; j < batch; ++j)
+ {
+ const dep = all_deps[added];
+ const t = batch > 1 ? j / (batch - 1) : 0.5;
+ const angle = arc_start + t * arc_span;
+ const jitter = (this._rng() - 0.5) * 10;
+ const label = short_name(dep.opkey);
+ engine.add_node(dep.opkey, node.x + Math.cos(angle) * (r + jitter), node.y + Math.sin(angle) * (r + jitter), false, label);
+ engine.add_edge(node, engine.node_map[dep.opkey], dep.dep_name);
+ added++;
+ }
+ ring++;
}
+ if (added < all_deps.length)
+ node.truncated = true;
for (const n of existing)
{
@@ -581,27 +611,46 @@ export class Page extends ZenPage
const existing = new Set(engine.nodes);
- if (has_sub_groups)
- {
- const sorted = child_keys.sort((a, b) => trie.children[b].count - trie.children[a].count);
- const visible = Math.min(sorted.length, MAX_VISIBLE_DEPS);
- node.dep_count = sorted.length;
- if (sorted.length > MAX_VISIBLE_DEPS)
+ // ripple placement helper for group node children
+ const base_radius = 70;
+ const ring_gap = 40;
+ const min_node_gap = 45;
+ const arc_span = 2 * Math.PI;
+
+ const place_ripple = (items, create_fn) => {
+ var added = 0;
+ var ring = 0;
+ node.dep_count = items.length;
+ if (items.length > MAX_VISIBLE_DEPS)
node.truncated = true;
- const radius = 200 + visible * 4;
- for (var i = 0; i < visible; ++i)
+ while (added < items.length && added < MAX_VISIBLE_DEPS)
{
- const key = sorted[i];
- const child_trie = trie.children[key];
- const has_more = Object.keys(child_trie.children).length > 0 || child_trie.count > 1;
+ const r = base_radius + ring * ring_gap;
+ const capacity = Math.max(1, Math.floor(arc_span * r / min_node_gap));
+ const remaining = Math.min(items.length, MAX_VISIBLE_DEPS) - added;
+ const batch = Math.min(capacity, remaining);
- const t = visible > 1 ? i / (visible - 1) : 0.5;
- const angle = t * 2 * Math.PI;
- const r = radius + Math.random() * 40;
- const x = node.x + Math.cos(angle) * r;
- const y = node.y + Math.sin(angle) * r;
+ for (var j = 0; j < batch; ++j)
+ {
+ const t = batch > 1 ? j / (batch - 1) : 0.5;
+ const angle = t * arc_span;
+ const jitter = (Math.random() - 0.5) * 10;
+ const x = node.x + Math.cos(angle) * (r + jitter);
+ const y = node.y + Math.sin(angle) * (r + jitter);
+ create_fn(items[added], x, y);
+ added++;
+ }
+ ring++;
+ }
+ };
+ if (has_sub_groups)
+ {
+ const sorted = child_keys.sort((a, b) => trie.children[b].count - trie.children[a].count);
+ place_ripple(sorted, (key, x, y) => {
+ const child_trie = trie.children[key];
+ const has_more = Object.keys(child_trie.children).length > 0 || child_trie.count > 1;
const child_path = node.prefix_path + "/" + key;
if (has_more)
@@ -615,7 +664,7 @@ export class Page extends ZenPage
const entry_node = engine.add_node(child_path, x, y, false);
engine.add_edge(node, entry_node, "group");
}
- }
+ });
}
else
{
@@ -624,23 +673,11 @@ export class Page extends ZenPage
const sb = this._size_map[node.prefix_path + "/" + b];
return Number((sb?.raw_size || 0n) - (sa?.raw_size || 0n));
});
- node.dep_count = trie.count;
- if (trie.count > MAX_VISIBLE_DEPS)
- node.truncated = true;
-
- const visible = Math.min(sorted.length, MAX_VISIBLE_DEPS);
- const radius = 200 + visible * 4;
- for (var i = 0; i < visible; ++i)
- {
- const t = visible > 1 ? i / (visible - 1) : 0.5;
- const angle = t * 2 * Math.PI;
- const r = radius + Math.random() * 40;
- const x = node.x + Math.cos(angle) * r;
- const y = node.y + Math.sin(angle) * r;
- const entry_path = node.prefix_path + "/" + sorted[i];
+ place_ripple(sorted, (key, x, y) => {
+ const entry_path = node.prefix_path + "/" + key;
const entry_node = engine.add_node(entry_path, x, y, false);
engine.add_edge(node, entry_node, "group");
- }
+ });
}
// pin existing nodes during layout, then restore
diff --git a/src/zenserver/frontend/html/util/graphengine.js b/src/zenserver/frontend/html/util/graphengine.js
index 009a568c2..4248e29f6 100644
--- a/src/zenserver/frontend/html/util/graphengine.js
+++ b/src/zenserver/frontend/html/util/graphengine.js
@@ -539,19 +539,16 @@ export class GraphEngine
// snapshot existing nodes before adding children
const existing = new Set(this._nodes);
- // determine arc placement
+ // ripple placement: concentric rings around the expanded node
const parent = this._find_parent(node);
const outward_angle = parent
? Math.atan2(node.y - parent.y, node.x - parent.x)
: 0;
- const visible = Math.min(deps.length, MAX_VISIBLE_DEPS);
-
- // push non-root node away from parent to make room for children
- // skip if already pushed once (collapse + re-expand) or re-expanding in place
+ // small push to clear immediate area (only on first expand)
if (parent && !node._skip_push && !node._was_pushed)
{
- const push_dist = 200 + visible * 4;
+ const push_dist = 80;
node.x += Math.cos(outward_angle) * push_dist;
node.y += Math.sin(outward_angle) * push_dist;
node.pinned = true;
@@ -570,30 +567,41 @@ export class GraphEngine
arc_span = Math.PI;
}
- const radius = 120 + visible * 2.5;
-
+ // place children in concentric rings (ripples)
+ const base_radius = 70;
+ const ring_gap = 40;
+ const min_node_gap = 45;
var added = 0;
- for (const dep of deps)
+ var ring = 0;
+
+ while (added < deps.length && added < MAX_VISIBLE_DEPS)
{
- if (added >= MAX_VISIBLE_DEPS)
+ const r = base_radius + ring * ring_gap;
+ const arc_len = arc_span * r;
+ const capacity = Math.max(1, Math.floor(arc_len / min_node_gap));
+ const remaining = Math.min(deps.length, MAX_VISIBLE_DEPS) - added;
+ const batch = Math.min(capacity, remaining);
+
+ for (var j = 0; j < batch; ++j)
{
- node.truncated = true;
- break;
+ const dep = deps[added];
+ const t = batch > 1 ? j / (batch - 1) : 0.5;
+ const angle = arc_start + t * arc_span;
+ const jitter = (Math.random() - 0.5) * 10;
+ const dep_node = this.add_node(
+ dep.opkey,
+ node.x + Math.cos(angle) * (r + jitter),
+ node.y + Math.sin(angle) * (r + jitter),
+ false
+ );
+ dep_node.unresolved = dep.unresolved || false;
+ this.add_edge(node, dep_node, dep.dep_type);
+ added++;
}
-
- const t = visible > 1 ? added / (visible - 1) : 0.5;
- const angle = arc_start + t * arc_span;
- const r = radius + Math.random() * 20;
- const dep_node = this.add_node(
- dep.opkey,
- node.x + Math.cos(angle) * r,
- node.y + Math.sin(angle) * r,
- false
- );
- dep_node.unresolved = dep.unresolved || false;
- this.add_edge(node, dep_node, dep.dep_type);
- added++;
+ ring++;
}
+ if (added < deps.length)
+ node.truncated = true;
// pin existing nodes during layout so only new children move
for (const n of existing)