2026 02 25_doi_fisheye_plan - mark-ik/graphshell GitHub Wiki
Status: Deferred (blocked) — awaiting basic LOD/culling baseline from 2026-02-24_performance_tuning_plan.md Phases 1–2
Blocking Prerequisites: Viewport culling (Phase 1) and Node + Edge LOD (Phase 2) from 2026-02-24_performance_tuning_plan.md must be in place. The canonical zoom-adaptive LOD tiers (Point / Compact / Expanded) and their camera.scale thresholds are defined in graph_node_edge_interaction_spec.md §4.8; DOI rendering must build on top of those tiers, not redefine them.
Context: Post-LOD readability improvement for dense graphs; preserves mental map while surfacing relevance.
Relates to: 2026-02-24_performance_tuning_plan.md (LOD/culling primitives this plan builds on), graph_node_edge_interaction_spec.md §4.8 (canonical LOD tier thresholds and hysteresis — DOI is an overlay, not a replacement), 2026-02-18_graph_ux_research_report.md §§13.2, 14.8, 14.9 (research basis), multi_view_pane_spec.md (per-pane isolation rule)
Introduce DOI-driven rendering and semantic fisheye as a concrete post-LOD roadmap item for graph UX. Focus+context without geometric distortion: keep the graph topology undistorted while scaling visual emphasis by relevance.
Not a replacement for LOD/culling. DOI and semantic fisheye are rendering-emphasis layers that build on top of stable viewport culling and zoom-adaptive LOD primitives. They do not duplicate or replace those systems.
DOI(n) = α·Recency(n) + β·Frequency(n) + γ·ExplicitInterest(n) - δ·DistanceFromFocus(n)
| Component | Formula | Range |
|---|---|---|
Recency(n) |
1.0 / (1.0 + time_since_last_visit_secs) |
[0.0, 1.0] |
Frequency(n) |
log(1 + visit_count) / log(1 + max_visit_count) |
[0.0, 1.0] |
ExplicitInterest(n) |
Pinned = 1.0, Bookmarked = 0.8, Tagged = 0.5, None = 0.0 | {0.0, 0.5, 0.8, 1.0} |
DistanceFromFocus(n) |
BFS hop distance from selected/hovered node, normalized to [0.0, 1.0] over max_hops
|
[0.0, 1.0] |
Default weights: α = 0.30, β = 0.20, γ = 0.30, δ = 0.20. All weights must sum to 1.0. Weights are configurable via CanvasRegistry policy; the defaults are the starting point, not a fixed contract.
| Tier | Range | Rendering Behavior |
|---|---|---|
High |
>= 0.65 |
Full node size, full label, full opacity, full color |
Medium |
0.30 – 0.64 |
Normal node size, domain-only label, full opacity |
Low |
0.10 – 0.29 |
Dot-only, muted color, no label |
Ghost |
< 0.10 |
Very faint dot; structural context preserved, visual noise minimized |
Ghost tier nodes are never hidden entirely — they provide the structural context that makes semantic fisheye meaningful. Hiding Ghost nodes is the responsibility of the search/filter system, not DOI.
DOI scores are cached in MetadataFrame per graph pane alongside the existing LOD zoom oracle:
pub struct DoiScore {
pub value: f32, // [0.0, 1.0]
pub tier: DoiTier, // High / Medium / Low / Ghost
pub computed_at: Instant, // for staleness check
}
pub enum DoiTier {
High,
Medium,
Low,
Ghost,
}The cache (HashMap<NodeIndex, DoiScore>) is write-owned by the DOI calculator worker and read-only from the render path. Reads require no lock (snapshot swap pattern) to avoid stalling the render thread.
DOI scores are recomputed on a background worker, not per-frame. The cadence targets:
| Trigger | Cadence |
|---|---|
| No focus change, no structural change | 100 ms (time-based tick) |
| Focus change (node selected or hovered) |
Immediate — enqueue next tick ≤ 16 ms; DistanceFromFocus component changes |
| Structural change (node or edge added/removed) |
Immediate — enqueue next tick; Recency and Frequency may change |
| Graph idle (no interaction for > 5 s) | Suspend — stop ticking; resume on next interaction event |
The worker holds a read-only snapshot of node metadata. It does not hold the graph write lock during calculation.
| Guardrail | Target |
|---|---|
| Per-recalculation wall time | ≤ 2 ms for graphs up to 1 000 nodes |
| Cache read cost per frame |
O(1) lookup by NodeIndex key |
BFS max depth (max_hops) |
5 hops — nodes beyond 5 hops get DistanceFromFocus = 1.0 without traversal |
| Background thread CPU budget | ≤ 5 % single-core at 100 ms cadence on reference hardware |
| Staleness threshold | 500 ms — if cache entry is older than 500 ms and no recompute has run, the render path falls back to zoom-adaptive LOD-only mode (no DOI tier applied) |
| Fisheye per-frame cost | O(visible_nodes) — one multiply per culled-visible node; no BFS or cache required |
Key design rule: DOI drives rendering emphasis only. It does not hide, remove, or filter nodes from the graph, and it does not redefine the canonical zoom-adaptive LOD tiers (Point / Compact / Expanded) defined in graph_node_edge_interaction_spec.md §4.8. DOI emphasis operates as an overlay: it may promote a node's label or size upward from the zoom-LOD baseline (e.g., show a full label at Compact LOD when DOI is High), but it must never suppress interactions that the active LOD tier makes available. Node visibility for filtering is governed by the separate search/filter system. These concerns must not be entangled.
| Concern | Owner | DOI's Role |
|---|---|---|
| Node visible / hidden (filter) | Search/filter system | None — DOI does not control visibility |
| Node size | DOI render layer | Scales radius by tier (see table below) |
| Node opacity | DOI render layer | Fades toward Ghost tier threshold |
| Label LOD (full / domain / none) | DOI render layer | DOI High may promote a node to full label even at zoom levels where zoom-adaptive LOD alone would suppress it; DOI never demotes below the zoom-LOD floor |
| Node color / brightness | DOI render layer | Mutes color toward Low / Ghost tier |
| Z-order in render pass | DOI render layer | High-DOI nodes render on top of low-DOI nodes within the same frame |
rendered_radius = base_radius * size_scale(tier)
| Tier | size_scale |
|---|---|
High |
1.5 |
Medium |
1.0 |
Low |
0.6 |
Ghost |
0.3 |
| Tier | Alpha |
|---|---|
High |
1.0 |
Medium |
0.85 |
Low |
0.50 |
Ghost |
0.15 |
Semantic fisheye applies cursor-distance scaling on top of DOI tier rendering. It is a separate, independently toggleable pass.
Invariant: node (x, y) positions are never modified. Only draw-size changes. Graph topology remains undistorted.
For each visible node in the current frame:
dist = length(mouse_pos - node_pos)
scale = max(1.0, 3.0 * (1.0 - dist / fisheye_radius))
where fisheye_radius is a configurable canvas-space radius (default: 300.0 canvas units, stored in CanvasRegistry).
Apply the fisheye scale on top of the DOI-tier size:
final_radius = doi_radius * scale
Render pass z-order: sort visible nodes by final_radius ascending (paint smallest first; largest — most focused — on top).
| Guardrail | Target |
|---|---|
| Per-frame cost | O(visible_nodes) — one distance + multiply per visible node |
| Input source | Reuse existing egui hover/mouse state; no additional polling |
| Disable path |
semantic_fisheye_enabled = false in CanvasRegistry; render path skips scale pass entirely |
-
CanvasRegistry: policy toggles and parameters:doi_rendering_enabled: boolsemantic_fisheye_enabled: bool-
doi_update_cadence_ms: u32(default: 100) -
doi_max_hops: u8(default: 5) -
fisheye_radius: f32(default: 300.0) -
doi_weights: DoiWeights(α, β, γ, δ; must sum to 1.0)
-
MetadataFrame: DOI score cache (HashMap<NodeIndex, DoiScore>) stored alongside the LOD zoom oracle; snapshot-swap updated by worker, read-only to render. -
DOI calculator worker: background task owned by graph-pane context; reads graph snapshot + node visit metadata; writes snapshot to
MetadataFramecache. -
Render path (
render/graph_node_shape.rsor equivalent): reads cachedDoiScoreper node; applies size/opacity/LOD tier overrides; applies fisheye scale whensemantic_fisheye_enabled. -
Multi-pane rule: DOI scores and fisheye calculations are computed per graph pane, consistent with existing culling/LOD per-pane isolation (
multi_view_pane_spec.md).
The following must be in place before implementation begins:
-
Viewport culling (
2026-02-24_performance_tuning_plan.mdPhase 1) — DOI rendering should only iterate over the culled visible node set to stay within the 2 ms per-recalculation budget. -
Node Label LOD (
2026-02-24_performance_tuning_plan.mdPhase 2.1) — DOI LOD tier overrides zoom-adaptive LOD upward only; the zoom-adaptive LOD path must be stable before DOI can layer on top of it. -
MetadataFrameavailable as a stable per-pane cache oracle — DOI cache fields are added here; the frame struct must be settled before new fields are introduced.
- DOI score recomputation completes in ≤ 2 ms for a 1 000-node graph on reference hardware.
- DOI cache hit rate ≥ 99 % during interactive hover/selection at 100 ms cadence.
-
doi_rendering_enabled = falseproduces rendering identical to pre-DOI baseline (no visual regression). -
semantic_fisheye_enabled = falsedoes not affect DOI tier-based size/opacity/LOD behavior. - Node
(x, y)positions are unchanged by the fisheye pass — verified by snapshot comparison of node position map before and after. - High-DOI nodes render on top of low-DOI nodes within the same render pass.
-
Ghosttier nodes remain visible as faint dots; they are not hidden by the DOI layer. - All DOI + fisheye policy toggles are governed by
CanvasRegistryand not hardcoded in render callsites. - DOI rendering does not mutate graph state; it is a read-only render pass over cached scores.