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.
Installation
npx shadcn@latest add https://craftbits.dev/r/rho-graph.jsonUsage
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
- Single
configdrives the shape.tailLengthnodes are laid out left-to-right as the tail, thencycleLengthnodes 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. - Flat index addressing. Every external prop —
pointers[].position,highlightSegmentsarcs,annotations.fromIndex/toIndex— addresses nodes by their flat index in the combined tail+cycle array. The tail occupies indices 0 throughtailLength - 1; the cycle occupies indicestailLengththroughtailLength + cycleLength - 1. - 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 viacurvedEdgePathso the loop is spatially obvious. - 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. - Distance annotations. Each annotation draws a dashed bracket above the arc from
fromIndextotoIndexwith a centred label (a,b,nC— whatever the algorithm narrative names). Labels float above the arc so they never overlap node circles. - 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. - Reduced motion.
usePrefersReducedMotion()collapses every transition to instant — node entries, edge draws, pointer glides, and the meet halo all snap.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
config | RhoConfig | required | ρ-shape config: tailLength, cycleLength, optional nodeLabels. |
pointers | RhoPointer[] | [] | Walker pointers — id, label, position, optional hex. |
highlightSegments | RhoSegment[] | [] | Regions to colour-emphasise: "tail", "cycle", or arc objects. |
annotations | RhoAnnotation[] | [] | Distance brackets — label, fromIndex, toIndex, optional hex. |
nodeRadius | number | 20 | Node circle radius in px. |
transition | Transition | SPRINGS.smooth | Override the spring driving pointer glide and node enter motion. |
className | string | — | Merged onto the outer SVG via cn(). |
Accessibility
- The outer SVG is
role="img"with anaria-labelsummarising 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 ofSPRINGS.smoothfrom@craft-bits/core/motionand the sharedcb-*token system; otherwise the surface is preserved.