Rho Graph

A ρ-shape (Greek letter rho) — a straight tail flowing into a circular cycle — rendered with directed edges, a curved back-edge that closes the loop, animated walker pointer dots, segment highlights, and labelled distance brackets (a, b, nC). This is the canonical picture for Floyd's tortoise-and-hare cycle detection, the entry-finding second pass, and any transfer-distance argument that names arcs of the flattened tail+cycle sequence.

RhoGraph is a pure layout primitive. The component does not run the algorithm; it lays out the ρ-shape from a single config and renders whatever pointer positions, highlighted segments, and distance annotations the caller drives in from outside. Pair it with any tortoise/hare loop in your own state.

a01234567TH
Customize
Shape
3
5
Pointers
Overlay

Installation

npx shadcn@latest add https://craftbits.dev/r/rho-graph.json

Usage

import { RhoGraph } from "@craft-bits/core";
 
<RhoGraph
  config={{ tailLength: 3, cycleLength: 5 }}
  pointers={[
    { id: "tortoise", label: "T", position: 2 },
    { id: "hare", label: "H", position: 5 },
  ]}
  highlightSegments={["cycle"]}
  annotations={[{ label: "a", fromIndex: 0, toIndex: 2 }]}
/>;

Driven by a real tortoise/hare loop from parent state:

const [tortoise, setTortoise] = useState(0);
const [hare, setHare] = useState(0);
 
useEffect(() => {
  const id = window.setInterval(() => {
    setTortoise((t) => next(t));
    setHare((h) => next(next(h)));
  }, 750);
  return () => window.clearInterval(id);
}, []);
 
<RhoGraph
  config={{ tailLength: 3, cycleLength: 5 }}
  pointers={[
    { id: "tortoise", label: "T", position: tortoise },
    { id: "hare", label: "H", position: hare },
  ]}
/>;

Highlight a specific arc and annotate it as the cycle length:

<RhoGraph
  config={{ tailLength: 2, cycleLength: 6 }}
  highlightSegments={[{ type: "arc", from: 2, to: 7 }]}
  annotations={[{ label: "nC", fromIndex: 2, toIndex: 7 }]}
/>

Understanding the component

  1. Single config drives the shape. tailLength nodes are laid out left-to-right as the tail, then cycleLength nodes are arranged on a circle. The cycle entry sits at 9 o'clock so the tail's last node lands next to cycle index 0.
  2. Flat index addressing. Every external prop — pointers[].position, highlightSegments arcs, annotations.fromIndex / toIndex — addresses nodes by their flat index in the combined tail+cycle array. The tail occupies indices 0 through tailLength - 1; the cycle occupies indices tailLength through tailLength + cycleLength - 1.
  3. Directed edges via arrowEndpoint. Tail and cycle edges are straight lines shortened to stop at each node's boundary. The closing back-edge (last cycle node → entry) is a quadratic Bézier via curvedEdgePath so the loop is spatially obvious.
  4. Segment highlights. Pass "tail", "cycle", or { type: "arc", from, to } to colour-emphasise a region. Nodes and edges entirely inside the segment switch to the accent stroke and use the highlighted arrow marker.
  5. Distance annotations. Each annotation draws a dashed bracket above the arc from fromIndex to toIndex with a centred label (a, b, nC — whatever the algorithm narrative names). Labels float above the arc so they never overlap node circles.
  6. Pointer fan-out + meet halo. When two or more pointers share a node, the dots stack vertically above the node so both labels stay visible. When 2+ pointers share the same position, an accent halo pulses around that node — the canonical "tortoise meets hare" beat.
  7. Reduced motion. usePrefersReducedMotion() collapses every transition to instant — node entries, edge draws, pointer glides, and the meet halo all snap.

Props

PropTypeDefaultDescription
configRhoConfigrequiredρ-shape config: tailLength, cycleLength, optional nodeLabels.
pointersRhoPointer[][]Walker pointers — id, label, position, optional hex.
highlightSegmentsRhoSegment[][]Regions to colour-emphasise: "tail", "cycle", or arc objects.
annotationsRhoAnnotation[][]Distance brackets — label, fromIndex, toIndex, optional hex.
nodeRadiusnumber20Node circle radius in px.
transitionTransitionSPRINGS.smoothOverride the spring driving pointer glide and node enter motion.
classNamestringMerged onto the outer SVG via cn().

Accessibility

  • The outer SVG is role="img" with an aria-label summarising the ρ-shape, pointer positions, and collision state (for example, "Rho-graph: 3 tail nodes, 5 cycle nodes. T at index 2, H at index 5. — pointers meet at index 5").
  • Every node renders its label as a child text element, so position is never communicated through colour alone.
  • Pointer dots fan out vertically when they share a node, so the "two pointers at the same node" state is visible without colour cues.
  • Motion respects prefers-reduced-motion: pointer glides, node entries, edge draws, segment highlights, and the meet halo all collapse to instant.

Distinct from Loop Trap

LoopTrap renders any directed graph from caller-supplied nodes[] + edges[] and is driven by pointer occupancy. RhoGraph is intrinsically tied to the ρ-shape: a single config declares tailLength + cycleLength, the back-edge closing the cycle is computed automatically, and the segment + annotation API addresses positions by flat index in the tail+cycle array. Pick LoopTrap when you need a bespoke graph topology; pick RhoGraph when the narrative is about tail length, cycle length, and named distances along the ρ.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/viz/RhoGraph.tsx). The original was already a clean layout primitive used by SC-B / SC-D / SC-E (Floyd's cycle detection, entry-finding, transfer-distance) lessons. The library extract drops project-specific motion springs and track-aware colour propagation in favour of SPRINGS.smooth from @craft-bits/core/motion and the shared cb-* token system; otherwise the surface is preserved.