2026 03 14_edge_visual_encoding_spec - mark-ik/graphshell GitHub Wiki

Edge Visual Encoding Spec

Date: 2026-03-14 Status: Design โ€” Pre-Implementation Purpose: Define the canonical visual encoding for graph edges by relation family. Establishes the mapping from EdgeKind (and future family variants) to stroke style, color, width, opacity, directionality cue, and interaction affordance. Replaces the ad hoc three-value GraphEdgeVisualStyle enum with a principled, extensible encoding.

Related:

  • 2026-03-14_graph_relation_families.md โ€” family vocabulary, persistence tiers
  • 2026-03-14_canvas_behavior_contract.md โ€” physics scenarios (family force assertions)
  • layout_behaviors_and_physics_spec.md โ€” frame-affinity backdrop rendering (ยง4.6)
  • graph_node_edge_interaction_spec.md โ€” edge interaction model authority
  • ../aspect_render/2026-03-12_compositor_expansion_plan.md โ€” render pass contract

1. Current State and What Changes

The current GraphEdgeVisualStyle enum in model/graph/egui_adapter.rs has three variants with hardcoded colors:

Current variant Color Style Width
Hyperlink Gray (160, 160, 160) Solid 1.4
History Blue (120, 180, 210) Dashed 1.8 + traversal bonus
UserGrouped Amber (236, 171, 64) Solid 3.0

AgentDerived has no visual representation โ€” it falls through to Hyperlink style. The three future family variants (ContainmentRelation, ArrangementRelation, ImportedRelation) have no encoding at all.

What this spec changes:

  1. Extends GraphEdgeVisualStyle to cover all five relation families plus AgentDerived distinctly.
  2. Establishes encoding rules that make family membership readable at a glance without requiring a legend for common cases.
  3. Defines interaction affordances (hover, selection, inspect) per family.
  4. Defines visibility rules โ€” which families render by default and which require a lens to become visible.
  5. Preserves and locks the existing color choices where they are already good (UserGrouped amber, History blue-dashed).

2. Encoding Principles

Five principles govern all family encoding decisions:

P1 โ€” Family readable at a glance. A user looking at two connected nodes must be able to determine why they are connected from the edge appearance alone, without hovering. This means: stroke style (solid/dashed/dotted) carries primary family signal; color carries secondary signal; width carries weight.

P2 โ€” Default canvas is not a visual dump. Families that are not relevant to everyday browsing (Traversal, Containment, Arrangement, Imported) are hidden by default. The default canvas shows only Semantic family edges โ€” the ones the user explicitly created or navigated. A user does not need to see their URL hierarchy drawn as graph edges while browsing.

P3 โ€” Lens activation reveals, not introduces. When a containment or traversal lens activates, edges that were hidden become visible. They do not appear suddenly โ€” they fade in. Deactivating the lens fades them out. The user always knows the edge was always there; the lens just chose to show it.

P4 โ€” Multiplicity is readable. A single pair of nodes can carry multiple edge kinds (e.g., both Hyperlink and TraversalDerived). The encoding must handle multi-kind edges without becoming illegible. The rule: render the highest-priority family's stroke; add a secondary family indicator (a small badge dot or a secondary thin stroke offset) rather than trying to merge the strokes.

P5 โ€” Theme coherence. Family colors are theme-aware tokens, not hardcoded Color32 values. The tokens map to theme palette roles so dark/light mode and custom themes all produce coherent, distinguishable encodings.

P6 โ€” Accessibility is multi-channel. No rendered edge type may rely on hue alone for recognition. Family identity must survive grayscale viewing and common color-vision-deficiency conditions. Pattern is the primary family carrier; width, opacity, endpoint marker, and halo are supporting carriers.


2.1 Accessible Automatic Style Assignment

Edge styling must be assigned by a deterministic registry rather than ad hoc per-callsite colors. The registry owns the family-safe base tokens and produces the concrete render token for each edge kind.

2.1.1 Registry contract

The canonical assignment flow is:

  1. Resolve EdgePayload into a primary visible family/sub-kind per ยง4 priority.
  2. Convert that family/sub-kind into a stable EdgeStyleKey.
  3. Look up the EdgeStyleKey in EdgeStyleRegistry.
  4. Render only the returned EdgeStyleToken; the canvas renderer must not invent per-edge colors or dash styles on its own.

Illustrative shape:

struct EdgeStyleRegistry {
    accessibility_mode: EdgeAccessibilityMode,
}

enum EdgeAccessibilityMode {
    ColorAndPattern,
    Monochrome,
}

struct EdgeStyleToken {
    color: ThemeColorToken,
    width: f32,
    pattern: StrokePattern,
    opacity: f32,
    end_marker: EndpointMarker,
    halo: Option<HaloStyle>,
}

Normative rule:

  • family chooses the primary visual identity,
  • sub-kind chooses only constrained variants within that family,
  • the renderer consumes EdgeStyleToken only.

2.1.2 Family-safe channels

Automatic style assignment must use channels in this order of importance:

  1. stroke pattern
  2. endpoint marker
  3. width
  4. color
  5. halo / emphasis treatment

This ordering is deliberate: if a theme, projector, screenshot, or user vision condition weakens color discrimination, family identity remains readable.

2.1.3 Collision resolution

If two simultaneously visible edge styles are too similar, the registry must resolve the collision in this order:

  1. change dash rhythm
  2. change endpoint marker
  3. increase width delta
  4. add halo or outline treatment
  5. adjust hue/lightness within the same family token range

The registry should preserve learned family color whenever a non-color fix is available.

2.1.4 Minimum distinguishability rules

Two simultaneously rendered edge styles must not collide under:

  • normal theme rendering,
  • grayscale conversion,
  • approximate deuteranopia simulation,
  • approximate protanopia simulation,
  • approximate tritanopia simulation.

The starter implementation may use heuristic checks (luminance distance plus non-color signature comparison) rather than a full medical-grade simulation, but it must still enforce the principle that family identity is not hue-only.

2.1.5 Accessibility mode

The registry must support at least two modes:

  • ColorAndPattern โ€” default; uses family color plus pattern/width/marker.
  • Monochrome โ€” ignores hue differences and preserves only pattern, width, marker, and halo.

Monochrome is not a separate styling system. It is the same registry under a different accessibility projection.


3. Family Encoding Table

3.1 Default Visibility

Family Default visible Visible when
Semantic (Hyperlink, UserGrouped, AgentDerived) Yes Always
Traversal (TraversalDerived) No Traversal lens active
Containment (ContainmentRelation) No Containment lens active
Arrangement (ArrangementRelation) No Arrangement overlay lens active
Imported (ImportedRelation) No Import review mode active

3.2 Stroke Encoding

Sub-type Stroke style Color token Width Opacity Directionality
Hyperlink Solid edge.semantic.hyperlink โ€” neutral gray 1.4 0.85 Arrowhead on hover only
UserGrouped Solid, bold edge.semantic.grouped โ€” amber 3.0 1.0 None (undirected)
AgentDerived Solid, thin edge.semantic.agent โ€” muted violet 1.2 0.55 None; fades with decay
TraversalDerived Dashed edge.traversal โ€” steel blue 1.8 + traversal bonus 0.7 Arrow on dominant direction
ContainmentRelation / url-path Dotted edge.containment.url โ€” teal 1.0 0.6 Arrowhead toward parent
ContainmentRelation / domain Dotted, faint edge.containment.domain โ€” teal, lighter 0.8 0.4 Arrowhead toward parent
ContainmentRelation / user-folder Solid edge.containment.folder โ€” teal, strong 1.6 0.9 Arrowhead toward parent
ContainmentRelation / clip-source Dashed, short edge.containment.clip โ€” light blue 1.2 0.75 Arrowhead toward source
ArrangementRelation / frame-member Double stroke edge.arrangement.frame โ€” indigo 1.0 outer + 0.5 inner gap 0.5 None
ArrangementRelation / tile-group Dotted, tight edge.arrangement.group โ€” indigo, lighter 0.8 0.4 None
ImportedRelation Dashed, long gap edge.imported โ€” warm gray 0.8 0.35 None

Color token defaults (dark theme base; light theme tokens are lighter/darker inversions of the same hue):

Token Dark theme default
edge.semantic.hyperlink rgb(150, 150, 155)
edge.semantic.grouped rgb(236, 171, 64) โ† existing, locked
edge.semantic.agent rgb(180, 140, 220)
edge.traversal rgb(120, 180, 210) โ† existing, locked
edge.containment.url rgb(80, 190, 170)
edge.containment.domain rgb(80, 190, 170) at 60% opacity
edge.containment.folder rgb(60, 200, 160)
edge.containment.clip rgb(140, 200, 240)
edge.arrangement.frame rgb(130, 110, 220)
edge.arrangement.group rgb(130, 110, 220) at 60% opacity
edge.imported rgb(160, 150, 140)

3.3 AgentDerived Decay Opacity

AgentDerived edges have a time-based opacity that maps decay progress to visual fade. The opacity decreases linearly from 0.55 (freshly asserted) to 0.15 (near eviction threshold), then the edge disappears at eviction.

opacity = lerp(0.55, 0.15, decay_progress)
where decay_progress = elapsed_since_last_assertion / decay_window

This makes the provisional nature of agent suggestions visually apparent without requiring a separate UI element.


4. Multi-Kind Edge Rendering

When a single node pair carries multiple edge kinds (e.g., Hyperlink + TraversalDerived, or Hyperlink + UserGrouped), rendering priority determines the primary stroke; secondary kinds add a small indicator:

Priority order (highest renders as primary stroke):

  1. UserGrouped (Semantic/grouped)
  2. ContainmentRelation / user-folder
  3. Hyperlink (Semantic/hyperlink)
  4. AgentDerived (Semantic/agent)
  5. TraversalDerived (Traversal)
  6. ContainmentRelation / other sub-kinds
  7. ArrangementRelation
  8. ImportedRelation

Secondary indicator: a small filled dot (radius 3px) rendered at the edge midpoint in the secondary family's color. At most one secondary indicator per edge โ€” if three or more kinds are present, only the highest-priority secondary is shown; a + superscript on the dot indicates more kinds exist. Hovering the dot opens the edge inspect popover (ยง6).

This avoids stroke layering complexity while keeping multi-kind edges distinguishable from single-kind edges.


5. Interaction Affordances

5.1 Hover

On edge hover:

  • Primary stroke brightens by 20% and width increases by 0.5px
  • Arrowhead appears for directed families (Hyperlink, ContainmentRelation) if not already shown
  • A small tooltip appears after 400ms: family name + sub-kind + key metadata (e.g., "Semantic ยท User grouped ยท Label: research")
  • For TraversalDerived: tooltip includes navigation count and last traversal date

5.2 Selection

Selected edges:

  • Primary stroke uses edge.selected color token (bright accent, theme-defined)
  • Width increases by 1.0px
  • Multi-kind secondary dot becomes brighter

Edge selection is triggered by click on the edge stroke (hit target: 8px wide regardless of actual stroke width โ€” edges are thin and need a wider click area).

5.3 Edge Inspect Popover

Right-click on any edge (or click the secondary-kind dot) opens an edge inspect popover anchored to the click position:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ [node A title]  โ†’  [node B title]    โ”‚
โ”‚                                      โ”‚
โ”‚ Kinds:                               โ”‚
โ”‚  โ— Semantic ยท Hyperlink              โ”‚
โ”‚  โ— Traversal ยท 14 visits ยท last Mon  โ”‚
โ”‚                                      โ”‚
โ”‚ [Remove UserGrouped]  [Dismiss Agent]โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Actions in the popover are family-appropriate:

  • Semantic/UserGrouped: "Remove grouping"
  • Semantic/AgentDerived: "Dismiss suggestion" / "Accept (keep permanently)"
  • Traversal: "Clear traversal history for this pair"
  • ContainmentRelation/user-folder: "Remove from folder"
  • ContainmentRelation/derived: read-only, no action (label: "Derived from URL")
  • ArrangementRelation: "Remove from frame" (durable) or read-only for session-only
  • Any visible edge family: "Dismiss in this view" suppresses only that edge instance in the current GraphViewId's EdgePolicy; it does not delete the underlying relation/event truth

5.4 Edge Creation Gesture

Edge creation gestures per family (interaction model authority is graph_node_edge_interaction_spec.md โ€” this spec only lists the family mapping):

Family Default creation gesture Notes
Semantic/UserGrouped Shift+drag from node to node Existing gesture
Semantic/AgentDerived Agent-initiated; no user gesture Accept via popover
Hyperlink Automatic on link-follow navigation Not user-created directly
TraversalDerived Automatic on navigation Not user-created directly
ContainmentRelation/user-folder "Add to folder" command or drag-to-navigator Via navigator, not canvas drag
ContainmentRelation/derived Automatic from URL structure No gesture
ArrangementRelation Automatic from tile tree No gesture; managed via workbench
ImportedRelation Automatic from import No gesture

6. Canvas Visibility Filter Controls

When non-default families are visible (lens active), the Graph Bar chip for that lens shows which families are currently rendered. The chip expands into the current graph view's EdgePolicy, revealing per-family toggles:

[Lens: Containment โ–พ]
  โ˜‘ url-path edges
  โ˜‘ domain edges
  โ˜ clip-source edges
  โ˜‘ user-folder edges

These toggles mutate view-local EdgePolicy state, not graph state. They allow the user to reduce visual noise within a lens without deactivating it entirely.

EdgePolicy rules (normative):

  • Family/sub-kind toggles are GraphViewId-local.
  • Per-edge dismissals are stored alongside the view's EdgePolicy.
  • Dismissing one edge must not hide all edges of that family.
  • Copying a graph view clones its EdgePolicy, including per-family toggles and per-edge dismissal state, so the copied view preserves the same rendered edge arrangement and derived node layout.

The default canvas (no lens) has no visible filter controls for hidden families โ€” the absence of non-semantic edges is the default, not a user-configured state.


7. GraphEdgeVisualStyle Migration

The current GraphEdgeVisualStyle enum in egui_adapter.rs must be extended to cover the new variants. The migration is additive:

// Current (3 variants):
enum GraphEdgeVisualStyle {
    Hyperlink,
    History,
    UserGrouped,
}

// Extended (covers all families):
enum GraphEdgeVisualStyle {
    // Semantic family
    Hyperlink,
    UserGrouped,
    AgentDerived { decay_progress: f32 },
    // Traversal family
    TraversalHistory,
    // Containment family (visible only when containment lens active)
    ContainmentUrlPath,
    ContainmentDomain,
    ContainmentUserFolder,
    ContainmentClipSource,
    // Arrangement family (visible only when arrangement overlay active)
    ArrangementFrameMember,
    ArrangementTileGroup,
    // Imported family (visible only in import review mode)
    ImportedRelation,
}

style_from_payload() priority order follows ยง4 priority. The hidden field on GraphEdgeShape drives default-off families โ€” it is set based on the active lens state passed into the draw context, not hardcoded.


8. Acceptance Criteria

  • All five families have distinct, named visual encodings per ยง3.2
  • Family identity remains distinguishable in grayscale without relying on hue alone
  • Automatic style assignment is driven by a deterministic registry, not per-callsite colors
  • Monochrome accessibility mode preserves family discrimination via pattern/width/marker
  • Default canvas renders only Semantic family edges; all other families hidden unless a lens is active
  • AgentDerived edges fade from opacity 0.55 โ†’ 0.15 as decay progresses
  • Multi-kind edges show primary stroke + secondary dot indicator per ยง4
  • Edge hover tooltip shows family name, sub-kind, and key metadata within 400ms
  • Edge inspect popover shows all kinds on the edge with family-appropriate actions
  • Edge hit target is 8px wide regardless of stroke width
  • GraphEdgeVisualStyle extended with all new variants; existing Hyperlink and History colors unchanged (locked)
  • Family visibility toggles in lens chip work without mutating graph state
  • All encodings tested with dark theme and light theme tokens
  • No AgentDerived edge visible at decay_progress >= 1.0

9. Non-Goals

  • Animated edges (flowing particles, animated dashes) โ€” deferred; out of scope for initial encoding
  • 3D edge rendering โ€” follows ViewDimension stabilization; not covered here
  • Edge label rendering at zoom-out โ€” governed by layout_algorithm_portfolio_spec.md label overlap metrics
  • Per-edge custom color overrides โ€” not supported; family tokens are the encoding, not per-edge properties
  • Edge bundling for dense graphs โ€” separate layout concern, not an encoding concern; see 2026-04-03_edge_routing_follow_on_plan.md
โš ๏ธ **GitHub.com Fallback** โš ๏ธ