Skip to content

Implement webring graph visualizationΒ #15

@AJaccP

Description

@AJaccP

🧠 Context

src/components/RingGraph.astro is a stub panel ("Ring graph β€” coming soon") rendered in the webring page's aside. This ticket turns it into a real, interactive visualization of the ring: each inRing member is a node, nodes are linked into a cycle, and the whole thing is Carleton-themed via the design tokens. The look we're after is an organic, force-directed scatter (not a rigid circle) β€” members floating, connected, draggable.

Approach: a force-directed graph using d3-force rendered as SVG. This is the well-trodden way to do a webring graph and keeps the data model trivial β€” our member data is just { name, url, year }. Reference implementations exist in the wild (other CS webrings); if needed, use them only for understanding the shape of the solution, not to copy.

Feel free to experiment with other approaches as well and share them for feedback!

This component blocks the webring search ticket, which highlights/focuses nodes by query. To keep that ticket from having to rewrite the graph, this ticket also exposes a dormant highlight/focus hook β€” a documented function the search ticket can call β€” but wires it to no UI here.

Files you'll touch:

  • src/components/RingGraph.astro (the whole job)
  • package.json / lockfile (add the d3 dependency β€” see step 1)

Don't touch:

  • src/pages/webring.astro β€” it already renders <RingGraph />; keep the component's public usage (no props needed) unchanged so the page is untouched.
  • The webring schema, the directory table, and ring.json generation β€” the graph reads the collection directly at build; it does not consume /ring.json (that's the external embed's file).
  • Don't build the search input here β€” only the dormant hook the search ticket calls.

πŸ› οΈ Implementation Plan

Note:

  • Any code snippets may not work immediately, adjust as needed based on errors.
  • If sample data hasn't already been seeded, you may need to add more webring member entries temporarily for development/testing
  1. Add d3. Install the force/render modules β€” either d3 or just the pieces you use (d3-force, d3-selection, d3-zoom, d3-drag). The repo enforces a 7-day supply-chain cooldown in pnpm-workspace.yaml; if the install is blocked by a policy error, flag it to Jacc rather than working around it. (d3 modules are long-stable, so the cooldown likely passes.)

  2. Load the member data at build, embed it for the client. In the component frontmatter, pull the inRing members, ordered the same way as the ring (graduation year ascending, then name), keeping just what the graph needs:

    ---
    import { getCollection } from 'astro:content';
    
    // Build `members` from the webring collection:
    //   - .filter() to keep only inRing === true
    //   - .sort() by graduation year ascending, then name as a tiebreak
    //     (year difference, falling back to localeCompare on name)
    //   - .map() each to { name, url, year } β€” what the graph needs
    const members = /* ... */;
    ---

    A client <script> can't read frontmatter variables directly when it also needs imports, so you may need to hand the data over as an embedded JSON blob and parse it in the script (this may or may not be the right fix):

    <div id="ring-graph" class="<existing stub sizing classes>"></div>
    <script type="application/json" id="ring-graph-data" set:html={JSON.stringify(members)}
    ></script>

    Keep the stub's existing sizing (square on mobile, lg:min-h-[28rem]) on the container so the page layout is stable.

  3. Build the graph in a client <script>.
    (This describes a potential approach, it may or may not be the exact right way. Adjust as needed.)
    Docs for reference: d3, d3-force, d3-zoom, d3-drag.
    Import the d3 pieces, parse #ring-graph-data, and:

    • Build links as a cycle: link node i to node (i + 1) % n, so the members form a closed ring in the chosen order.
    • Run a forceSimulation with link / many-body (charge) / center / collision forces; let it settle (alpha decay) then stop ticking, leaving nodes draggable. Append an <svg> sized to the container; draw edges as <line> and nodes as a <g> with a <circle> plus a <text> label (the URL hostname β€” strip the protocol).
    • Interactions: node click opens url in a new tab (window.open(url, '_blank', 'noopener')); node hover highlights it and shows/raises its label; support drag (fix node on drag, release after) and pan/zoom (d3-zoom on the svg, into a <g> group). Add a fit-to-view that frames all nodes on load and on resize.
    • Respect prefers-reduced-motion: skip/shorten the settle animation (render at rest) when it's set.
  4. Theme via tokens β€” not hardcoded hex. Color the graph with var(--color-*) so it follows the theme (and dark mode) for free. Note an Astro gotcha: scoped <style> does not reach SVG elements created in JS. Use a <style is:global> block namespaced under #ring-graph (so it doesn't leak), or set inline styles with var(...). Suggested mapping (adjust as needed):

    #ring-graph .ring-edge {
      stroke: var(--color-line);
    }
    #ring-graph .ring-node {
      fill: var(--color-muted);
      transition: fill 0.2s;
      cursor: pointer;
    }
    #ring-graph .ring-node:hover,
    #ring-graph .ring-node.is-match {
      fill: var(--color-accent);
    }
    #ring-graph .ring-node.is-dimmed {
      opacity: 0.35;
    }
    #ring-graph .ring-label {
      fill: var(--color-muted);
      font-size: 12px;
    }
  5. Tag nodes + expose a dormant highlight/focus hook (for search). Give each node group stable data attributes β€” data-name, data-year, data-url β€” so matches can be found by attribute. Then expose a small, documented imperative API the search ticket will drive, for example:

    • highlight(term: string) β€” lowercase-match term against name/year/url; add .is-match to matches and .is-dimmed to the rest; if there are matches, zoom/pan to frame them (reuse the fit-to-view logic on the matched subset). Empty term clears all classes and fits the whole graph.

    Attach it where the search ticket can reach it without importing graph internals β€” e.g. window.ringGraph = { highlight, clear }, or have the component listen for a CustomEvent('ring:highlight', { detail: { term } }). Pick one, and document it in a comment at the top of the file. Do not add a search input or call this hook from any UI in this ticket β€” it stays dormant until the search ticket wires it up.

  6. Verify in pnpm dev on /webring: nodes render and settle into an organic layout; edges form a single cycle through all members; hovering highlights + labels a node; clicking opens that member's site; drag and pan/zoom work; it fits to view on load and resize; colors come from the tokens and flip correctly in dark mode; toggling a member's inRing to false removes their node after rebuild. Manually call the highlight hook from the console to confirm it highlights + focuses matches.


βœ… Acceptance Criteria

  • RingGraph.astro renders a force-directed (d3-force) SVG graph of the inRing members, linked in a cycle in ring order (grad year asc, then name).
  • Nodes are clickable (open the member's site in a new tab) and hover-highlight with a hostname label; drag and pan/zoom work; the graph fits to view on load and resize.
  • All colors come from var(--color-*) (no hardcoded hex); the graph follows light/dark theme.
  • Each node carries data-name / data-year / data-url, and a documented dormant highlight/clear hook is exposed for the search ticket β€” not wired to any UI here.
  • Data comes from the webring collection at build (no /ring.json fetch); webring.astro and the schema are untouched.
  • The d3 dependency is added (cooldown blocks flagged to Jacc, not worked around).
  • prefers-reduced-motion is respected (no forced settle animation).
  • pnpm format:check, pnpm check, and pnpm build all pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Ready

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions