Skip to content

Add /map page: force-directed spec map clustered by tag similarity #5646

@MarkusNeusinger

Description

@MarkusNeusinger

Summary

Add a new top-level page /map that renders all ~327 plot specs as image-thumbnail nodes in a force-directed graph, positioned by tag overlap. Specs that share many tags pull together; specs that share few drift apart. Acts as a visual discovery surface complementing the existing list-based /specs, /plots, and library pages.

Reference visual target: vasturiano/force-graph image-nodes example.

Motivation

Today there is no spatial way to discover related specs — to see at a glance that "annotated scatter" lives next door to "regression scatter" and a few jumps away from "bar with error bars." Tag-based clustering on a force-directed map exposes the implicit topology of the catalog, surfaces unexpected neighborhoods, and gives the gallery a novel browsing affordance.

Approach

Backend — one new lean endpoint:

  • GET /api/specs/map returns one row per spec containing id, title, preview_url_light, preview_url_dark, quality_score, tags (spec-level), impl_tags (best-impl). Image is the highest-quality_score implementation. Cached via the existing pattern.
  • New schema SpecMapItem in api/schemas.py. Route declared before /specs/{spec_id} so the path-parameter route doesn't capture it.

Frontend — new page powered by react-force-graph-2d:

  • app/src/pages/MapPage.tsx — full-bleed canvas, theme-aware thumbnails, click-to-spec navigation, screen-reader fallback list.
  • app/src/pages/MapPage.helpers.ts (+ tests) — pure functions for tag flattening with category:value prefixes, IDF weighting (down-weights ubiquitous tags like data_type:numeric), weighted Jaccard similarity, sparse KNN edge construction (K=5, min-sim 0.05).
  • Layout computed by force-graph's built-in d3-force engine; no UMAP/MST/A* on the server. With ~1.6k edges over 327 nodes the cluster structure forms naturally.
  • Wire-up: app/src/router.tsx lazy route + app/src/components/NavBar.tsx nav link.

Why client-side similarity: 327×327 pairwise weighted-Jaccard runs in ~30 ms in the browser; a precomputed-positions endpoint would block future filter-driven re-layouts and add numpy/scipy dep creep on the API.

Out of scope (follow-up)

  • Edge labels showing which shared tags connect two nodes — explicitly deferred.
  • Filter UI on the map (e.g., toggle dependencies:scipy, re-cluster live).
  • Smaller thumbnail variants in the responsive-image pipeline if perf review pushes back on the ~13 MB warm cache load.
  • Cleaner full-bleed layout via a router-level switch (analog to the existing mastheadSticks flag) instead of negative-margin Container opt-out.

File map

Created:

  • app/src/pages/MapPage.tsx
  • app/src/pages/MapPage.helpers.ts + .test.ts
  • app/src/pages/MapPage.test.tsx

Modified:

  • api/schemas.py, api/routers/specs.py, api/cache.py
  • tests/unit/api/test_routers.py
  • app/src/router.tsx, app/src/components/NavBar.tsx
  • app/package.json + app/yarn.lock (new deps: force-graph, react-force-graph-2d)

Test plan

  • uv run ruff check api/ tests/ && uv run ruff format api/ tests/
  • uv run pytest tests/unit/api/test_routers.py -v — 4 new tests: list, picks-best-impl, skips-impl-less, empty-db
  • cd app && yarn lint && yarn tsc --noEmit && yarn test && yarn build
  • Manual visual QA: nav link appears, /map renders 327 thumbnails, visible scatter/bar/line clusters, click → spec page, theme toggle switches link colors + thumbnails, mobile breakpoint usable
  • Bundle-size check: new /map chunk in ~40–60 KB gz range

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestinfrastructureWorkflow, backend, or frontend issue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions