diff options
| author | MtBntChvn <[email protected]> | 2026-02-21 08:11:41 +0000 |
|---|---|---|
| committer | MtBntChvn <[email protected]> | 2026-02-21 08:11:41 +0000 |
| commit | e30933f55b7c9ea8eb6f30bd3d6c0d55a51f7971 (patch) | |
| tree | f58bb6a140123317361608fbb9adef9afd6d853e /src | |
| parent | focus view on expanded node and fit children after expansion (diff) | |
| download | zen-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.js | 151 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/util/graphengine.js | 58 |
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) |