diff --git a/api/main.py b/api/main.py
index 886ecc6c23..c030f6b159 100644
--- a/api/main.py
+++ b/api/main.py
@@ -27,6 +27,7 @@
debug_router,
download_router,
health_router,
+ insights_router,
libraries_router,
og_images_router,
plots_router,
@@ -128,6 +129,9 @@ async def add_cache_headers(request: Request, call_next):
# Individual spec details (5 min cache, 1h stale-while-revalidate)
elif path.startswith("/specs/"):
response.headers["Cache-Control"] = "public, max-age=300, stale-while-revalidate=3600"
+ # Insights endpoints (5 min cache, 1h stale-while-revalidate)
+ elif path.startswith("/insights/"):
+ response.headers["Cache-Control"] = "public, max-age=300, stale-while-revalidate=3600"
return response
@@ -141,6 +145,7 @@ async def add_cache_headers(request: Request, call_next):
app.include_router(specs_router)
app.include_router(libraries_router)
app.include_router(plots_router)
+app.include_router(insights_router)
app.include_router(download_router)
app.include_router(seo_router)
app.include_router(og_images_router)
diff --git a/api/routers/__init__.py b/api/routers/__init__.py
index 69c1f77502..8271c1306c 100644
--- a/api/routers/__init__.py
+++ b/api/routers/__init__.py
@@ -3,6 +3,7 @@
from api.routers.debug import router as debug_router
from api.routers.download import router as download_router
from api.routers.health import router as health_router
+from api.routers.insights import router as insights_router
from api.routers.libraries import router as libraries_router
from api.routers.og_images import router as og_images_router
from api.routers.plots import router as plots_router
@@ -16,6 +17,7 @@
"debug_router",
"download_router",
"health_router",
+ "insights_router",
"libraries_router",
"og_images_router",
"plots_router",
diff --git a/api/routers/insights.py b/api/routers/insights.py
new file mode 100644
index 0000000000..ab869e8128
--- /dev/null
+++ b/api/routers/insights.py
@@ -0,0 +1,545 @@
+"""
+Insights endpoints for pyplots platform.
+
+Public analytics and discovery features that leverage aggregated database data:
+- Dashboard: Rich platform statistics and visualizations
+- Plot of the Day: Daily featured high-quality implementation
+- Related Plots: Tag-based similarity recommendations
+"""
+
+from __future__ import annotations
+
+import hashlib
+from collections import Counter, defaultdict
+from datetime import date, datetime, timezone
+
+from fastapi import APIRouter, Depends, Query
+from pydantic import BaseModel
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from api.cache import cache_key, get_or_set_cache
+from api.dependencies import require_db
+from core.constants import SUPPORTED_LIBRARIES
+from core.database import ImplRepository, SpecRepository
+from core.database.connection import get_db_context
+from core.utils import strip_noqa_comments
+
+
+router = APIRouter(prefix="/insights", tags=["insights"])
+
+
+# =============================================================================
+# Response Models
+# =============================================================================
+
+
+class LibraryDashboardStats(BaseModel):
+ """Per-library statistics for the dashboard."""
+
+ id: str
+ name: str
+ impl_count: int
+ avg_score: float | None
+ min_score: float | None
+ max_score: float | None
+ score_buckets: dict[str, int] # "50-55": 2, "90-95": 45, etc.
+ loc_buckets: dict[str, int] # "0-20": 5, "20-40": 12, etc.
+ avg_loc: float | None
+
+
+class CoverageCell(BaseModel):
+ """Single cell in coverage heatmap."""
+
+ score: float | None = None
+ has_impl: bool = False
+
+
+class CoverageRow(BaseModel):
+ """One spec row in coverage heatmap."""
+
+ spec_id: str
+ title: str
+ libraries: dict[str, CoverageCell]
+
+
+class TopImpl(BaseModel):
+ """A top-rated implementation."""
+
+ spec_id: str
+ spec_title: str
+ library_id: str
+ quality_score: float
+ preview_url: str | None = None
+
+
+class TimelinePoint(BaseModel):
+ """Monthly implementation count."""
+
+ month: str # "2025-01"
+ count: int
+
+
+class DashboardResponse(BaseModel):
+ """Full dashboard statistics."""
+
+ total_specs: int
+ total_implementations: int
+ total_interactive: int
+ total_lines_of_code: int
+ avg_quality_score: float | None
+ coverage_percent: float
+
+ library_stats: list[LibraryDashboardStats]
+ coverage_matrix: list[CoverageRow]
+ top_implementations: list[TopImpl]
+ tag_distribution: dict[str, dict[str, int]]
+ score_distribution: dict[str, int]
+ timeline: list[TimelinePoint]
+
+
+class PlotOfTheDayResponse(BaseModel):
+ """Daily featured plot."""
+
+ spec_id: str
+ spec_title: str
+ description: str | None = None
+ library_id: str
+ library_name: str
+ quality_score: float
+ preview_url: str | None = None
+ image_description: str | None = None
+ code: str | None = None
+ date: str
+
+
+class RelatedSpecItem(BaseModel):
+ """A related spec with similarity info."""
+
+ id: str
+ title: str
+ preview_url: str | None = None
+ library_id: str | None = None
+ similarity: float
+ shared_tags: list[str]
+
+
+class RelatedSpecsResponse(BaseModel):
+ """Related specs for a given spec."""
+
+ related: list[RelatedSpecItem]
+
+
+# =============================================================================
+# Shared helpers
+# =============================================================================
+
+LIBRARY_NAMES = {
+ "altair": "Altair",
+ "bokeh": "Bokeh",
+ "highcharts": "Highcharts",
+ "letsplot": "lets-plot",
+ "matplotlib": "Matplotlib",
+ "plotly": "Plotly",
+ "plotnine": "plotnine",
+ "pygal": "Pygal",
+ "seaborn": "Seaborn",
+}
+
+
+def _score_bucket(score: float) -> str:
+ """Map a quality score (50-100) to a 5-step histogram bucket label."""
+ clamped = max(50, min(score, 100))
+ bucket = min(int((clamped - 50) // 5), 9)
+ lo = 50 + bucket * 5
+ hi = lo + 5
+ return f"{lo}-{hi}"
+
+
+def _flatten_tags(tags: dict | None) -> set[str]:
+ """Flatten a spec's tags JSON into a set of 'category:value' strings."""
+ if not tags:
+ return set()
+ flat: set[str] = set()
+ for category, values in tags.items():
+ if isinstance(values, list):
+ for v in values:
+ flat.add(f"{category}:{v}")
+ return flat
+
+
+def _parse_iso(s: str | None) -> datetime | None:
+ """Parse an ISO datetime string, returning None on failure."""
+ if not s:
+ return None
+ try:
+ dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
+ if dt.tzinfo is None:
+ dt = dt.replace(tzinfo=timezone.utc)
+ return dt
+ except Exception:
+ return None
+
+
+# =============================================================================
+# 1. Dashboard
+# =============================================================================
+
+
+async def _refresh_dashboard() -> DashboardResponse:
+ """Standalone factory for background refresh."""
+ async with get_db_context() as db:
+ return await _build_dashboard(SpecRepository(db), ImplRepository(db))
+
+
+async def _build_dashboard(repo: SpecRepository, impl_repo: ImplRepository) -> DashboardResponse:
+ """Build the full dashboard response from DB data."""
+ all_specs = await repo.get_all()
+ total_loc = await impl_repo.get_total_code_lines()
+ loc_per_impl = await impl_repo.get_loc_per_impl()
+
+ # Build LOC buckets per library (0-20, 20-40, ..., 380-400, 400+)
+ loc_bucket_ranges = [(i, i + 20) for i in range(0, 400, 20)] + [(400, 999)]
+ library_loc_buckets: dict[str, Counter[str]] = {lib: Counter() for lib in SUPPORTED_LIBRARIES}
+ for lib_id, lines in loc_per_impl:
+ if lib_id in library_loc_buckets:
+ for lo, hi in loc_bucket_ranges:
+ if lo <= lines < hi or (hi == 999 and lines >= lo):
+ label = f"{lo}-{hi}" if hi != 999 else f"{lo}+"
+ library_loc_buckets[lib_id][label] += 1
+ break
+
+ # Avg LOC per library
+ library_loc_lists: dict[str, list[int]] = {lib: [] for lib in SUPPORTED_LIBRARIES}
+ for lib_id, lines in loc_per_impl:
+ if lib_id in library_loc_lists:
+ library_loc_lists[lib_id].append(lines)
+
+ total_impls = 0
+ total_interactive = 0
+ all_scores: list[float] = []
+
+ library_scores: dict[str, list[float]] = {lib: [] for lib in SUPPORTED_LIBRARIES}
+ library_counts: dict[str, int] = dict.fromkeys(SUPPORTED_LIBRARIES, 0) # type: ignore[arg-type]
+
+ tag_counter: dict[str, Counter[str]] = defaultdict(Counter)
+ monthly_counts: Counter[str] = Counter()
+ score_buckets: Counter[str] = Counter()
+
+ coverage_rows: list[CoverageRow] = []
+ top_impls: list[TopImpl] = []
+
+ for spec in all_specs:
+ row_libs: dict[str, CoverageCell] = {}
+
+ for impl in spec.impls:
+ lib_id = impl.library_id
+ score = impl.quality_score
+ total_impls += 1
+ if impl.preview_html:
+ total_interactive += 1
+ library_counts[lib_id] = library_counts.get(lib_id, 0) + 1
+ row_libs[lib_id] = CoverageCell(score=score, has_impl=True)
+
+ if score is not None:
+ library_scores[lib_id].append(score)
+ all_scores.append(score)
+ score_buckets[_score_bucket(score)] += 1
+
+ if score >= 95:
+ top_impls.append(
+ TopImpl(
+ spec_id=spec.id,
+ spec_title=spec.title,
+ library_id=lib_id,
+ quality_score=score,
+ preview_url=impl.preview_url,
+ )
+ )
+
+ # Timeline from generated_at (datetime field, not string)
+ gen_dt = impl.generated_at
+ if gen_dt:
+ monthly_counts[gen_dt.strftime("%Y-%m")] += 1
+
+ coverage_rows.append(CoverageRow(spec_id=spec.id, title=spec.title, libraries=row_libs))
+
+ # Tag distribution (spec-level + impl-level)
+ if spec.tags:
+ for category, values in spec.tags.items():
+ if isinstance(values, list):
+ for v in values:
+ tag_counter[category][v] += 1
+ for impl in spec.impls:
+ if impl.impl_tags and isinstance(impl.impl_tags, dict):
+ for category, values in impl.impl_tags.items():
+ if isinstance(values, list):
+ for v in values:
+ tag_counter[category][v] += 1
+
+ # Build library stats
+ lib_stats: list[LibraryDashboardStats] = []
+ for lib_id in sorted(SUPPORTED_LIBRARIES):
+ scores = library_scores[lib_id]
+ buckets: Counter[str] = Counter()
+ for s in scores:
+ buckets[_score_bucket(s)] += 1
+ lib_stats.append(
+ LibraryDashboardStats(
+ id=lib_id,
+ name=LIBRARY_NAMES.get(lib_id, lib_id),
+ impl_count=library_counts[lib_id],
+ avg_score=round(sum(scores) / len(scores), 1) if scores else None,
+ min_score=round(min(scores), 1) if scores else None,
+ max_score=round(max(scores), 1) if scores else None,
+ score_buckets=dict(buckets),
+ loc_buckets=dict(library_loc_buckets[lib_id]),
+ avg_loc=round(sum(library_loc_lists[lib_id]) / len(library_loc_lists[lib_id]), 1)
+ if library_loc_lists[lib_id]
+ else None,
+ )
+ )
+ lib_stats.sort(key=lambda x: x.impl_count, reverse=True)
+
+ # Top impls sorted by score desc, limit 20
+ top_impls.sort(key=lambda x: x.quality_score, reverse=True)
+ top_impls = top_impls[:20]
+
+ # Score distribution — ensure all buckets present
+ score_dist = {f"{50 + i * 5}-{55 + i * 5}": score_buckets.get(f"{50 + i * 5}-{55 + i * 5}", 0) for i in range(10)}
+
+ # Timeline sorted by month
+ timeline = [TimelinePoint(month=m, count=c) for m, c in sorted(monthly_counts.items())]
+
+ # Coverage
+ coverage = (total_impls / (len(all_specs) * len(SUPPORTED_LIBRARIES)) * 100) if all_specs else 0
+
+ # Coverage matrix sorted by title
+ coverage_rows.sort(key=lambda r: r.title.lower())
+
+ return DashboardResponse(
+ total_specs=len(all_specs),
+ total_implementations=total_impls,
+ total_interactive=total_interactive,
+ total_lines_of_code=total_loc,
+ avg_quality_score=round(sum(all_scores) / len(all_scores), 1) if all_scores else None,
+ coverage_percent=round(coverage, 1),
+ library_stats=lib_stats,
+ coverage_matrix=coverage_rows,
+ top_implementations=top_impls,
+ tag_distribution={cat: dict(counter.most_common(20)) for cat, counter in sorted(tag_counter.items())},
+ score_distribution=score_dist,
+ timeline=timeline,
+ )
+
+
+@router.get("/dashboard", response_model=DashboardResponse)
+async def get_dashboard(db: AsyncSession = Depends(require_db)) -> DashboardResponse:
+ """
+ Get rich platform statistics for the public stats dashboard.
+
+ Includes per-library scores, coverage heatmap, top implementations,
+ tag distribution, score histogram, and implementation timeline.
+ """
+ repo = SpecRepository(db)
+ impl_repo = ImplRepository(db)
+
+ async def _fetch() -> DashboardResponse:
+ return await _build_dashboard(repo, impl_repo)
+
+ return await get_or_set_cache(
+ cache_key("insights", "dashboard"), _fetch, refresh_after=3600, refresh_factory=_refresh_dashboard
+ )
+
+
+# =============================================================================
+# 2. Plot of the Day
+# =============================================================================
+
+
+async def _refresh_potd() -> PlotOfTheDayResponse | None:
+ """Standalone factory for background refresh."""
+ async with get_db_context() as db:
+ return await _build_potd(SpecRepository(db), ImplRepository(db))
+
+
+async def _build_potd(spec_repo: SpecRepository, impl_repo: ImplRepository) -> PlotOfTheDayResponse | None:
+ """Select the plot of the day deterministically."""
+ all_specs = await spec_repo.get_all()
+ today = date.today().isoformat()
+
+ # Collect candidates: implementations with quality_score >= 90 (lightweight, no code loaded)
+ candidates: list[tuple[str, str, str, str | None, float, str | None]] = []
+ for spec in all_specs:
+ for impl in spec.impls:
+ if impl.quality_score is not None and impl.quality_score >= 90 and impl.preview_url:
+ candidates.append(
+ (spec.id, spec.title, spec.description or "", impl.library_id, impl.quality_score, impl.preview_url)
+ )
+
+ if not candidates:
+ return None
+
+ # Deterministic selection based on date
+ seed = int(hashlib.md5(today.encode()).hexdigest(), 16) # noqa: S324
+ idx = seed % len(candidates)
+ spec_id, spec_title, description, library_id, quality_score, preview_url = candidates[idx]
+
+ # Load deferred fields (code, image_description) for just this one impl
+ full_impl = await impl_repo.get_by_spec_and_library(spec_id, library_id)
+
+ return PlotOfTheDayResponse(
+ spec_id=spec_id,
+ spec_title=spec_title,
+ description=description,
+ library_id=library_id,
+ library_name=LIBRARY_NAMES.get(library_id, library_id),
+ quality_score=quality_score,
+ preview_url=preview_url,
+ image_description=full_impl.review_image_description if full_impl else None,
+ code=strip_noqa_comments(full_impl.code) if full_impl and full_impl.code else None,
+ date=today,
+ )
+
+
+@router.get("/plot-of-the-day", response_model=PlotOfTheDayResponse | None)
+async def get_plot_of_the_day(db: AsyncSession = Depends(require_db)) -> PlotOfTheDayResponse | None:
+ """
+ Get the featured plot of the day.
+
+ Deterministically selects a high-quality implementation (score >= 90)
+ based on today's date. Returns the same result for the entire day.
+ """
+ spec_repo = SpecRepository(db)
+ impl_repo = ImplRepository(db)
+
+ async def _fetch() -> PlotOfTheDayResponse | None:
+ return await _build_potd(spec_repo, impl_repo)
+
+ return await get_or_set_cache(
+ cache_key("insights", "potd", date.today().isoformat()),
+ _fetch,
+ refresh_after=3600,
+ refresh_factory=_refresh_potd,
+ )
+
+
+# =============================================================================
+# 3. Related Plots
+# =============================================================================
+
+
+def _collect_impl_tags(spec: object, library: str | None = None) -> set[str]:
+ """Collect spec-level tags + impl-level tags for a spec.
+
+ If library is specified, only include that library's impl_tags.
+ Otherwise, collect impl_tags from all implementations.
+ """
+ tags = _flatten_tags(spec.tags)
+ for impl in spec.impls:
+ if library and impl.library_id != library:
+ continue
+ if impl.impl_tags and isinstance(impl.impl_tags, dict):
+ tags |= _flatten_tags(impl.impl_tags)
+ return tags
+
+
+async def _build_related(
+ repo: SpecRepository, spec_id: str, limit: int, mode: str, library: str | None = None
+) -> RelatedSpecsResponse:
+ """Find related specs using Jaccard similarity on tags.
+
+ mode="spec": only spec-level tags (for overview page)
+ mode="full": spec + impl tags for the given library (for impl detail page)
+ """
+ all_specs = await repo.get_all()
+
+ # Find target spec
+ target = None
+ for s in all_specs:
+ if s.id == spec_id:
+ target = s
+ break
+ if target is None:
+ return RelatedSpecsResponse(related=[])
+
+ # Build target tags based on mode
+ if mode == "full":
+ target_tags = _collect_impl_tags(target, library)
+ else:
+ target_tags = _flatten_tags(target.tags)
+ if not target_tags:
+ return RelatedSpecsResponse(related=[])
+
+ # Compute similarity for all other specs
+ scored: list[tuple[float, list[str], object]] = []
+ for spec in all_specs:
+ if spec.id == spec_id:
+ continue
+ # For other specs: use same library's impl_tags if available, else best match
+ if mode == "full":
+ other_tags = _collect_impl_tags(spec, library)
+ else:
+ other_tags = _flatten_tags(spec.tags)
+ if not other_tags:
+ continue
+ intersection = target_tags & other_tags
+ union = target_tags | other_tags
+ similarity = len(intersection) / len(union) if union else 0
+ if similarity > 0:
+ shared = [t.split(":", 1)[1] for t in sorted(intersection)]
+ scored.append((similarity, shared, spec))
+
+ # Sort by similarity desc, then shuffle within same-score groups for variety
+ scored.sort(key=lambda x: x[0], reverse=True)
+ # Take top candidates (2x limit) and shuffle lightly to add variety
+ top_pool = scored[: limit * 2]
+ if len(top_pool) > limit:
+ import random
+
+ # Group by rounded similarity, shuffle within groups
+ seed = int(hashlib.md5(spec_id.encode()).hexdigest(), 16) % (2**32) # noqa: S324
+ rng = random.Random(seed)
+ rng.shuffle(top_pool)
+ # Re-sort but with jitter: high similarity still wins, just not deterministic order
+ top_pool.sort(key=lambda x: x[0] + rng.uniform(0, 0.05), reverse=True)
+
+ # Build response with best-quality preview per spec
+ related: list[RelatedSpecItem] = []
+ for similarity, shared_tags, spec in top_pool[:limit]:
+ impls_with_preview = [i for i in spec.impls if i.preview_url]
+ best_impl = max(impls_with_preview, key=lambda i: i.quality_score or 0) if impls_with_preview else None
+ related.append(
+ RelatedSpecItem(
+ id=spec.id,
+ title=spec.title,
+ preview_url=best_impl.preview_url if best_impl else None,
+ library_id=best_impl.library_id if best_impl else None,
+ similarity=round(similarity, 3),
+ shared_tags=shared_tags,
+ )
+ )
+
+ return RelatedSpecsResponse(related=related)
+
+
+@router.get("/related/{spec_id}", response_model=RelatedSpecsResponse)
+async def get_related_specs(
+ spec_id: str,
+ limit: int = Query(default=6, ge=1, le=12),
+ mode: str = Query(default="spec", pattern="^(spec|full)$"),
+ library: str | None = Query(default=None),
+ db: AsyncSession = Depends(require_db),
+) -> RelatedSpecsResponse:
+ """
+ Get specs related to the given spec, based on tag similarity.
+
+ mode=spec: only spec-level tags (plot_type, domain, etc.)
+ mode=full: spec tags + impl tags for the given library
+ library: in full mode, use this library's impl_tags (required for accurate tag matching)
+ """
+ repo = SpecRepository(db)
+
+ async def _fetch() -> RelatedSpecsResponse:
+ return await _build_related(repo, spec_id, limit, mode, library)
+
+ return await get_or_set_cache(cache_key("insights", "related", spec_id, str(limit), mode, library or ""), _fetch)
diff --git a/api/routers/libraries.py b/api/routers/libraries.py
index 58cca151ba..1dd02e062a 100644
--- a/api/routers/libraries.py
+++ b/api/routers/libraries.py
@@ -88,7 +88,7 @@ async def get_library_images(library_id: str, db: AsyncSession = Depends(require
return cached
repo = SpecRepository(db)
- specs = await repo.get_all()
+ specs = await repo.get_all_with_code()
images = []
for spec in specs:
diff --git a/api/routers/seo.py b/api/routers/seo.py
index a7824e2432..2c85f5ee28 100644
--- a/api/routers/seo.py
+++ b/api/routers/seo.py
@@ -31,6 +31,7 @@ def _build_sitemap_xml(specs: list) -> str:
" https://pyplots.ai/catalog",
" https://pyplots.ai/mcp",
" https://pyplots.ai/legal",
+ " https://pyplots.ai/stats",
]
for spec in specs:
diff --git a/api/routers/specs.py b/api/routers/specs.py
index df0dc76c7f..11a78782f6 100644
--- a/api/routers/specs.py
+++ b/api/routers/specs.py
@@ -8,7 +8,7 @@
from api.exceptions import raise_not_found
from api.schemas import ImplementationResponse, SpecDetailResponse, SpecListItem
from core.config import settings
-from core.database import SpecRepository
+from core.database import ImplRepository, SpecRepository
from core.database.connection import get_db_context
from core.utils import strip_noqa_comments
@@ -96,8 +96,9 @@ async def get_spec(spec_id: str, db: AsyncSession = Depends(require_db)):
preview_url=impl.preview_url,
preview_html=impl.preview_html,
quality_score=impl.quality_score,
- code=strip_noqa_comments(impl.code),
+ code=None, # Code loaded separately via /specs/{spec_id}/{library}/code
generated_at=impl.generated_at.isoformat() if impl.generated_at else None,
+ updated=impl.updated.isoformat() if impl.updated else None,
generated_by=impl.generated_by,
python_version=impl.python_version,
library_version=impl.library_version,
@@ -129,6 +130,29 @@ async def get_spec(spec_id: str, db: AsyncSession = Depends(require_db)):
return result
+@router.get("/specs/{spec_id}/{library}/code")
+async def get_impl_code(spec_id: str, library: str, db: AsyncSession = Depends(require_db)):
+ """
+ Get implementation code for a specific spec + library.
+
+ Lightweight endpoint that loads only the code field (deferred in main query).
+ """
+ key = cache_key("impl_code", spec_id, library)
+ cached = get_cache(key)
+ if cached is not None:
+ return cached
+
+ repo = ImplRepository(db)
+ impl = await repo.get_code(spec_id, library)
+
+ if not impl or not impl.code:
+ raise_not_found("Implementation code", f"{spec_id}/{library}")
+
+ result = {"spec_id": spec_id, "library": library, "code": strip_noqa_comments(impl.code)}
+ set_cache(key, result)
+ return result
+
+
@router.get("/specs/{spec_id}/images")
async def get_spec_images(spec_id: str, db: AsyncSession = Depends(require_db)):
"""
diff --git a/api/schemas.py b/api/schemas.py
index 71493c8e93..f5bdd385dc 100644
--- a/api/schemas.py
+++ b/api/schemas.py
@@ -19,6 +19,7 @@ class ImplementationResponse(BaseModel):
quality_score: float | None = None
code: str | None = None
generated_at: str | None = None
+ updated: str | None = None
generated_by: str | None = None
python_version: str | None = None
library_version: str | None = None
diff --git a/app/src/components/Footer.tsx b/app/src/components/Footer.tsx
index d7d6d610af..1e327ac873 100644
--- a/app/src/components/Footer.tsx
+++ b/app/src/components/Footer.tsx
@@ -38,10 +38,9 @@ export function Footer({ onTrackEvent, selectedSpec, selectedLibrary }: FooterPr
·
onTrackEvent?.('external_link', { destination: 'stats', spec: selectedSpec, library: selectedLibrary })}
+ component={RouterLink}
+ to="/stats"
+ onClick={() => onTrackEvent?.('internal_link', { destination: 'stats', spec: selectedSpec, library: selectedLibrary })}
sx={{
color: '#9ca3af',
textDecoration: 'none',
diff --git a/app/src/components/PlotOfTheDay.tsx b/app/src/components/PlotOfTheDay.tsx
new file mode 100644
index 0000000000..f489edac1c
--- /dev/null
+++ b/app/src/components/PlotOfTheDay.tsx
@@ -0,0 +1,126 @@
+import { useEffect, useState, useCallback } from 'react';
+import { Link as RouterLink } from 'react-router-dom';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import Link from '@mui/material/Link';
+import IconButton from '@mui/material/IconButton';
+import CloseIcon from '@mui/icons-material/Close';
+
+import { API_URL } from '../constants';
+import { buildSrcSet, getFallbackSrc } from '../utils/responsiveImage';
+
+interface PlotOfTheDayData {
+ spec_id: string;
+ spec_title: string;
+ description: string | null;
+ library_id: string;
+ library_name: string;
+ quality_score: number;
+ preview_url: string | null;
+ image_description: string | null;
+ date: string;
+}
+
+const mono = '"MonoLisa", "MonoLisa Fallback", monospace';
+
+export function PlotOfTheDay() {
+ const [data, setData] = useState(null);
+ const [dismissed, setDismissed] = useState(() => window.sessionStorage.getItem('potd_dismissed') === 'true');
+
+ useEffect(() => {
+ if (dismissed) return;
+ fetch(`${API_URL}/insights/plot-of-the-day`)
+ .then(r => { if (!r.ok) throw new Error(); return r.json(); })
+ .then(setData)
+ .catch(() => {});
+ }, [dismissed]);
+
+ const handleDismiss = useCallback((e: React.MouseEvent) => {
+ e.stopPropagation();
+ setDismissed(true);
+ window.sessionStorage.setItem('potd_dismissed', 'true');
+ }, []);
+
+ if (!data || dismissed) return null;
+
+ return (
+
+
+
+
+ {/* Preview image */}
+ {data.preview_url && (
+
+
+
+
+
+
+
+ )}
+
+ {/* Info */}
+
+
+
+ plot of the day
+
+
+ {data.date}
+
+
+
+
+
+ {data.spec_title}
+
+
+
+
+ {data.library_name} · {data.quality_score}/100
+
+
+ {data.image_description && (
+
+ "{data.image_description.trim()}"
+
+ )}
+
+
+ );
+}
diff --git a/app/src/components/RelatedSpecs.tsx b/app/src/components/RelatedSpecs.tsx
new file mode 100644
index 0000000000..30e7c744eb
--- /dev/null
+++ b/app/src/components/RelatedSpecs.tsx
@@ -0,0 +1,111 @@
+import { useEffect, useState } from 'react';
+import { Link as RouterLink } from 'react-router-dom';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import Link from '@mui/material/Link';
+
+import { API_URL } from '../constants';
+import { buildSrcSet, getFallbackSrc } from '../utils/responsiveImage';
+
+interface RelatedSpec {
+ id: string;
+ title: string;
+ preview_url: string | null;
+ library_id: string | null;
+ similarity: number;
+ shared_tags: string[];
+}
+
+const mono = '"MonoLisa", "MonoLisa Fallback", monospace';
+
+// 6 columns max at md+, ~160px each → 400w is plenty
+const SIZES = '(max-width: 599px) 50vw, (max-width: 899px) 33vw, 17vw';
+
+interface RelatedSpecsProps {
+ specId: string;
+ /** "spec" = spec tags only (overview), "full" = spec + impl tags (detail) */
+ mode?: 'spec' | 'full';
+ /** Current library — in full mode, matches tags against this library's impl_tags */
+ library?: string;
+ /** Called when hovering a related spec card — passes shared tag values */
+ onHoverTags?: (tags: string[]) => void;
+}
+
+export function RelatedSpecs({ specId, mode = 'spec', library, onHoverTags }: RelatedSpecsProps) {
+ const [related, setRelated] = useState([]);
+
+ useEffect(() => {
+ let cancelled = false;
+ const params = new URLSearchParams({ limit: '6', mode });
+ if (library && mode === 'full') params.set('library', library);
+ fetch(`${API_URL}/insights/related/${specId}?${params}`)
+ .then(r => { if (!r.ok) throw new Error(); return r.json(); })
+ .then(data => { if (!cancelled) setRelated(data.related ?? []); })
+ .catch(() => { if (!cancelled) setRelated([]); });
+ return () => { cancelled = true; };
+ }, [specId, mode, library]);
+
+ if (related.length === 0) return null;
+
+ return (
+
+
+ {mode === 'full' ? 'similar implementations' : 'similar specifications'}
+
+
+ {related.map(spec => (
+ onHoverTags?.(spec.shared_tags)}
+ onMouseLeave={() => onHoverTags?.([])}
+ sx={{
+ textDecoration: 'none',
+ color: 'inherit',
+ border: '1px solid #f3f4f6',
+ borderRadius: 1,
+ overflow: 'hidden',
+ transition: 'transform 0.15s ease',
+ '&:hover': { transform: 'scale(1.02)', borderColor: '#e5e7eb' },
+ }}
+ >
+ {spec.preview_url ? (
+
+
+
+
+
+ ) : (
+
+ no preview
+
+ )}
+
+
+ {spec.title}
+
+
+
+ {spec.shared_tags.length} tags in common
+
+ {mode === 'full' && spec.library_id && (
+
+ {spec.library_id}
+
+ )}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/app/src/components/SpecTabs.tsx b/app/src/components/SpecTabs.tsx
index 5a1e416aff..4be4f3bf46 100644
--- a/app/src/components/SpecTabs.tsx
+++ b/app/src/components/SpecTabs.tsx
@@ -46,6 +46,7 @@ interface SpecTabsProps {
notes?: string[];
tags?: Record;
created?: string;
+ updated?: string;
// Implementation tab
imageDescription?: string;
strengths?: string[];
@@ -54,11 +55,15 @@ interface SpecTabsProps {
// Quality tab
qualityScore: number | null;
criteriaChecklist?: Record;
+ // Implementation date
+ generatedAt?: string;
// Common
libraryId: string;
onTrackEvent?: (name: string, props?: Record) => void;
// Overview mode - only show Spec tab
overviewMode?: boolean;
+ // Tags to highlight (from similar specs hover)
+ highlightedTags?: string[];
}
interface TabPanelProps {
@@ -158,19 +163,22 @@ export function SpecTabs({
notes,
tags,
created,
+ updated,
imageDescription,
strengths,
weaknesses,
implTags,
qualityScore,
criteriaChecklist,
+ generatedAt,
libraryId,
onTrackEvent,
overviewMode = false,
+ highlightedTags = [],
}: SpecTabsProps) {
const [copied, setCopied] = useState(false);
// In overview mode, start with Spec tab open; in detail mode, all collapsed
- const [tabIndex, setTabIndex] = useState(overviewMode ? 0 : null);
+ const [tabIndex, setTabIndex] = useState(null);
const [expandedCategories, setExpandedCategories] = useState>({});
// Handle tag click - navigate to filtered catalog (full page navigation)
@@ -255,6 +263,12 @@ export function SpecTabs({
textTransform: 'none',
fontSize: '0.875rem',
minHeight: 48,
+ transition: 'background-color 0.15s ease, color 0.15s ease',
+ borderRadius: '4px 4px 0 0',
+ '&:hover': {
+ backgroundColor: '#f3f4f6',
+ color: '#3776AB',
+ },
},
'& .Mui-selected': {
color: '#3776AB',
@@ -265,14 +279,15 @@ export function SpecTabs({
}}
>
{!overviewMode && (
- } iconPosition="start" label="Code" />
+ tabIndex === 0 && handleTabChange(e, 0)} icon={} iconPosition="start" label="Code" />
)}
- } iconPosition="start" label="Spec" />
+ tabIndex === specTabIndex && handleTabChange(e, specTabIndex)} icon={} iconPosition="start" label="Spec" />
{!overviewMode && (
- } iconPosition="start" label="Impl" />
+ tabIndex === 2 && handleTabChange(e, 2)} icon={} iconPosition="start" label="Impl" />
)}
{!overviewMode && (
tabIndex === 3 && handleTabChange(e, 3)}
icon={}
iconPosition="start"
label={qualityScore ? `${Math.round(qualityScore)}` : 'Quality'}
@@ -388,57 +403,6 @@ export function SpecTabs({
>
)}
- {/* Tags grouped by category - compact inline, clickable */}
- {tags && Object.keys(tags).length > 0 && (
-
- {Object.entries(tags).map(([category, values]) => {
- const paramName = SPEC_TAG_PARAM_MAP[category];
- return (
-
-
- {category.replace(/_/g, ' ')}:
-
- {values.map((value, i) => (
- handleTagClick(paramName, value) : undefined}
- sx={{
- fontFamily: '"MonoLisa", monospace',
- fontSize: '0.65rem',
- height: 20,
- bgcolor: '#f3f4f6',
- color: '#4b5563',
- cursor: paramName ? 'pointer' : 'default',
- '&:hover': paramName ? { bgcolor: '#e5e7eb' } : {},
- }}
- />
- ))}
-
- );
- })}
-
- )}
-
- {/* Metadata footer */}
-
- {specId}{created && ` · ${formatDate(created)}`}
-
@@ -522,48 +486,6 @@ export function SpecTabs({
>
)}
- {/* Implementation Tags - only show non-empty categories, clickable */}
- {implTags && Object.entries(implTags).some(([, values]) => values && values.length > 0) && (
-
- {Object.entries(implTags)
- .filter(([, values]) => values && values.length > 0)
- .map(([category, values]) => {
- const paramName = IMPL_TAG_PARAM_MAP[category];
- return (
-
-
- {category}:
-
- {values.map((value, i) => (
- handleTagClick(paramName, value) : undefined}
- sx={{
- fontFamily: '"MonoLisa", monospace',
- fontSize: '0.65rem',
- height: 20,
- bgcolor: '#f3f4f6',
- color: '#4b5563',
- cursor: paramName ? 'pointer' : 'default',
- '&:hover': paramName ? { bgcolor: '#e5e7eb' } : {},
- }}
- />
- ))}
-
- );
- })}
-
- )}
-
{/* No data message */}
{!imageDescription && (!strengths || strengths.length === 0) && (!weaknesses || weaknesses.length === 0) && (
@@ -713,6 +635,78 @@ export function SpecTabs({
)}
+
+ {/* Tags — always visible after tab content (spec tags + impl tags on detail page) */}
+ {((tags && Object.keys(tags).length > 0) || (implTags && Object.values(implTags).some(v => v?.length > 0))) && (
+
+ {tags && Object.entries(tags).map(([category, values]) => {
+ const paramName = SPEC_TAG_PARAM_MAP[category];
+ return (
+
+
+ {category.replace(/_/g, ' ')}:
+
+ {values.map((value, i) => {
+ const isHighlighted = highlightedTags.includes(value);
+ return (
+ handleTagClick(paramName, value) : undefined}
+ sx={{
+ fontFamily: '"MonoLisa", monospace', fontSize: '0.65rem', height: 20,
+ bgcolor: isHighlighted ? '#dbeafe' : '#f3f4f6',
+ color: isHighlighted ? '#1e40af' : '#4b5563',
+ cursor: paramName ? 'pointer' : 'default',
+ transition: 'all 0.2s ease',
+ fontWeight: isHighlighted ? 600 : 400,
+ '&:hover': paramName ? { bgcolor: '#e5e7eb' } : {},
+ }}
+ />
+ );
+ })}
+
+ );
+ })}
+ {!overviewMode && implTags && Object.entries(implTags)
+ .filter(([, values]) => values && values.length > 0)
+ .map(([category, values]) => {
+ const paramName = IMPL_TAG_PARAM_MAP[category];
+ return (
+
+
+ {category}:
+
+ {values.map((value, i) => {
+ const isHighlighted = highlightedTags.includes(value);
+ return (
+ handleTagClick(paramName, value) : undefined}
+ sx={{
+ fontFamily: '"MonoLisa", monospace', fontSize: '0.65rem', height: 20,
+ bgcolor: isHighlighted ? '#dbeafe' : '#f3f4f6',
+ color: isHighlighted ? '#1e40af' : '#4b5563',
+ cursor: paramName ? 'pointer' : 'default',
+ transition: 'all 0.2s ease',
+ fontWeight: isHighlighted ? 600 : 400,
+ '&:hover': paramName ? { bgcolor: '#e5e7eb' } : {},
+ }}
+ />
+ );
+ })}
+
+ );
+ })}
+
+ )}
+
+ {/* Metadata footer — always visible */}
+
+ {specId}
+ {!overviewMode && libraryId && ` · ${libraryId}`}
+ {(() => {
+ const date = !overviewMode ? (generatedAt || updated || created) : (updated || created);
+ return date ? ` · ${formatDate(date)}` : '';
+ })()}
+
);
}
diff --git a/app/src/hooks/useCodeFetch.ts b/app/src/hooks/useCodeFetch.ts
index 5167074d24..cdd9bdad53 100644
--- a/app/src/hooks/useCodeFetch.ts
+++ b/app/src/hooks/useCodeFetch.ts
@@ -1,8 +1,8 @@
/**
* Hook for fetching plot code on-demand.
*
- * Code is excluded from /plots/filter to reduce payload size (~2MB savings).
- * This hook fetches code from /specs/{spec_id} when needed.
+ * Code is deferred in the database and excluded from /specs/{id} responses.
+ * This hook fetches code from the lightweight /specs/{spec_id}/{library}/code endpoint.
*/
import { useState, useCallback, useRef } from 'react';
@@ -42,21 +42,18 @@ export function useCodeFetch(): UseCodeFetchReturn {
return pending;
}
- // Fetch from API
+ // Fetch from lightweight code endpoint
setIsLoading(true);
const promise = (async () => {
try {
- const response = await fetch(`${API_URL}/specs/${specId}`);
+ const response = await fetch(`${API_URL}/specs/${specId}/${library}/code`);
if (!response.ok) {
cacheRef.current[key] = null;
return null;
}
const data = await response.json();
- const impl = data.implementations?.find(
- (i: { library_id: string }) => i.library_id === library
- );
- const code = impl?.code ?? null;
+ const code = data.code ?? null;
cacheRef.current[key] = code;
return code;
} catch {
diff --git a/app/src/pages/HomePage.tsx b/app/src/pages/HomePage.tsx
index 2abbd65dfa..d2f025b266 100644
--- a/app/src/pages/HomePage.tsx
+++ b/app/src/pages/HomePage.tsx
@@ -13,6 +13,7 @@ import { Header } from '../components/Header';
import { Footer } from '../components/Footer';
import { FilterBar } from '../components/FilterBar';
import { ImagesGrid } from '../components/ImagesGrid';
+import { PlotOfTheDay } from '../components/PlotOfTheDay';
import { useAppData, useHomeState } from '../hooks';
export function HomePage() {
@@ -166,6 +167,8 @@ export function HomePage() {
+ {isFiltersEmpty(activeFilters) && !loading && }
+
{error && (
{error}
diff --git a/app/src/pages/SpecPage.tsx b/app/src/pages/SpecPage.tsx
index fe85b4af41..319c1d605d 100644
--- a/app/src/pages/SpecPage.tsx
+++ b/app/src/pages/SpecPage.tsx
@@ -11,11 +11,12 @@ import BugReportIcon from '@mui/icons-material/BugReport';
import { NotFoundPage } from './NotFoundPage';
import { API_URL, GITHUB_URL } from '../constants';
-import { useAnalytics } from '../hooks';
+import { useAnalytics, useCodeFetch } from '../hooks';
import { useAppData } from '../hooks';
import { LibraryPills } from '../components/LibraryPills';
import { Breadcrumb } from '../components/Breadcrumb';
import { Footer } from '../components/Footer';
+import { RelatedSpecs } from '../components/RelatedSpecs';
const SpecTabs = lazy(() => import('../components/SpecTabs').then(m => ({ default: m.SpecTabs })));
const SpecOverview = lazy(() => import('../components/SpecOverview').then(m => ({ default: m.SpecOverview })));
@@ -48,6 +49,8 @@ export function SpecPage() {
const [descExpanded, setDescExpanded] = useState(false);
const [codeCopied, setCodeCopied] = useState(null);
const [openTooltip, setOpenTooltip] = useState(null);
+ const [highlightedTags, setHighlightedTags] = useState([]);
+ const { fetchCode, getCode } = useCodeFetch();
// Get library metadata by ID
const getLibraryMeta = useCallback(
@@ -99,6 +102,16 @@ export function SpecPage() {
return specData.implementations.find((impl) => impl.library_id === selectedLibrary) || null;
}, [specData, selectedLibrary]);
+ // Prefetch code in background when impl detail page opens
+ useEffect(() => {
+ if (specId && selectedLibrary) {
+ fetchCode(specId, selectedLibrary);
+ }
+ }, [specId, selectedLibrary, fetchCode]);
+
+ // Get code from cache (populated by prefetch or on-demand)
+ const currentCode = specId && selectedLibrary ? getCode(specId, selectedLibrary) : null;
+
// Handle library switch (in detail mode)
const handleLibrarySelect = useCallback(
(libraryId: string) => {
@@ -138,12 +151,13 @@ export function SpecPage() {
[specId, trackEvent, isOverviewMode]
);
- // Handle copy code
+ // Handle copy code (fetches on-demand if not prefetched yet)
const handleCopyCode = useCallback(
async (impl: Implementation) => {
- if (!impl?.code) return;
try {
- await navigator.clipboard.writeText(impl.code);
+ const code = impl.code || (specId ? await fetchCode(specId, impl.library_id) : null);
+ if (!code) return;
+ await navigator.clipboard.writeText(code);
setCodeCopied(impl.library_id);
trackEvent('copy_code', {
spec: specId,
@@ -352,6 +366,7 @@ export function SpecPage() {
notes={specData.notes}
tags={specData.tags}
created={specData.created}
+ updated={specData.updated}
imageDescription={undefined}
strengths={undefined}
weaknesses={undefined}
@@ -360,6 +375,7 @@ export function SpecPage() {
libraryId=""
onTrackEvent={trackEvent}
overviewMode={true}
+ highlightedTags={highlightedTags}
/>
>
) : (
@@ -387,7 +403,7 @@ export function SpecPage() {
/>
>
)}
+
+
>
diff --git a/app/src/pages/StatsPage.tsx b/app/src/pages/StatsPage.tsx
new file mode 100644
index 0000000000..88be67543b
--- /dev/null
+++ b/app/src/pages/StatsPage.tsx
@@ -0,0 +1,386 @@
+import { useEffect, useState } from 'react';
+import { Helmet } from 'react-helmet-async';
+import { Link as RouterLink } from 'react-router-dom';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import Link from '@mui/material/Link';
+import Tooltip from '@mui/material/Tooltip';
+
+import { useAnalytics } from '../hooks';
+import { Breadcrumb } from '../components/Breadcrumb';
+import { Footer } from '../components/Footer';
+import { API_URL } from '../constants';
+import { buildSrcSet, getFallbackSrc } from '../utils/responsiveImage';
+
+interface LibraryStats {
+ id: string;
+ name: string;
+ impl_count: number;
+ avg_score: number | null;
+ min_score: number | null;
+ max_score: number | null;
+ score_buckets: Record;
+ loc_buckets: Record;
+ avg_loc: number | null;
+}
+
+interface CoverageCell {
+ score: number | null;
+ has_impl: boolean;
+}
+
+interface CoverageRow {
+ spec_id: string;
+ title: string;
+ libraries: Record;
+}
+
+interface TopImpl {
+ spec_id: string;
+ spec_title: string;
+ library_id: string;
+ quality_score: number;
+ preview_url: string | null;
+}
+
+interface TimelinePoint {
+ month: string;
+ count: number;
+}
+
+interface DashboardData {
+ total_specs: number;
+ total_implementations: number;
+ total_interactive: number;
+ total_lines_of_code: number;
+ avg_quality_score: number | null;
+ coverage_percent: number;
+ library_stats: LibraryStats[];
+ coverage_matrix: CoverageRow[];
+ top_implementations: TopImpl[];
+ tag_distribution: Record>;
+ score_distribution: Record;
+ timeline: TimelinePoint[];
+}
+
+const mono = '"MonoLisa", "MonoLisa Fallback", monospace';
+
+function scoreColor(score: number | null): string {
+ if (score === null) return '#e5e7eb';
+ if (score >= 90) return '#22c55e';
+ if (score >= 75) return '#eab308';
+ return '#ef4444';
+}
+
+
+function formatNum(n: number): string {
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
+ return n.toLocaleString();
+}
+
+export function StatsPage() {
+ const { trackPageview, trackEvent } = useAnalytics();
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ trackPageview('/stats');
+ }, [trackPageview]);
+
+ useEffect(() => {
+ fetch(`${API_URL}/insights/dashboard`)
+ .then(r => { if (!r.ok) throw new Error(`${r.status}`); return r.json(); })
+ .then(setData)
+ .catch(e => setError(e.message))
+ .finally(() => setLoading(false));
+ }, []);
+
+ const subheadingStyle = { fontFamily: mono, fontWeight: 600, fontSize: '0.95rem', color: '#374151', mt: 4, mb: 1.5 };
+
+ if (loading) return (
+
+ loading stats...
+
+ );
+
+ if (error || !data) return (
+
+ failed to load stats{error ? `: ${error}` : ''}
+
+ );
+
+ const maxTimeline = Math.max(...data.timeline.map(t => t.count), 1);
+
+ return (
+ <>
+
+ stats | pyplots.ai
+
+
+
+
+
+
+ {/* Summary Counters */}
+
+ {[
+ { label: 'specifications', value: data.total_specs, suffix: '' },
+ { label: 'implementations', value: data.total_implementations, suffix: '' },
+ { label: 'interactive', value: data.total_interactive, suffix: '' },
+ { label: 'lines of code', value: data.total_lines_of_code, suffix: '' },
+ { label: 'avg quality', value: data.avg_quality_score, suffix: '' },
+ { label: 'coverage', value: data.coverage_percent, suffix: '%' },
+ ].map(item => (
+
+
+ {typeof item.value === 'number' ? `${formatNum(item.value)}${item.suffix}` : '—'}
+
+
+ {item.label}
+
+
+ ))}
+
+
+ visitor analytics at{' '}
+ trackEvent('external_link', { destination: 'plausible' })}
+ sx={{ color: '#9ca3af', textDecoration: 'none', '&:hover': { color: '#306998' } }}
+ >plausible.io/pyplots.ai
+
+
+ {/* Library Stats — dual mini histograms per library */}
+ libraries
+ {/* Quality distribution per library */}
+
+ quality distribution 50–100 · count · avg
+
+
+ {[...data.library_stats].sort((a, b) => (b.avg_score ?? 0) - (a.avg_score ?? 0)).map(lib => {
+ const allBuckets = Array.from({ length: 10 }, (_, i) => {
+ const lo = 50 + i * 5;
+ const key = `${lo}-${lo + 5}`;
+ return [key, lib.score_buckets[key] ?? 0] as const;
+ });
+ const maxBucket = Math.max(...allBuckets.map(([, c]) => c), 1);
+ return (
+
+
+ {lib.name}
+
+
+ {allBuckets.map(([bucket, count]) => {
+ const lo = parseInt(bucket.split('-')[0]);
+ return (
+
+ 0 ? `${Math.max((count / maxBucket) * 100, 15)}%` : 0,
+ bgcolor: scoreColor(lo >= 90 ? 95 : lo >= 75 ? 80 : 50),
+ opacity: 0.6,
+ borderRadius: '1px 1px 0 0',
+ }} />
+
+ );
+ })}
+
+
+ {lib.impl_count} · {lib.avg_score ?? '—'}
+
+
+ );
+ })}
+
+
+ {/* LOC distribution per library */}
+
+ lines of code per implementation 0–400+ · avg
+
+
+ {[...data.library_stats].sort((a, b) => (a.avg_loc ?? 999) - (b.avg_loc ?? 999)).map(lib => {
+ const locRanges = Array.from({ length: 20 }, (_, i) => `${i * 20}-${(i + 1) * 20}`).concat('400+');
+ const locBuckets = locRanges.map(key => [key, lib.loc_buckets[key] ?? 0] as const);
+ const maxLoc = Math.max(...locBuckets.map(([, c]) => c), 1);
+ return (
+
+
+ {lib.name}
+
+
+ {locBuckets.map(([bucket, count]) => (
+
+ 0 ? `${Math.max((count / maxLoc) * 100, 15)}%` : 0,
+ bgcolor: '#306998',
+ opacity: 0.4,
+ borderRadius: '1px 1px 0 0',
+ }} />
+
+ ))}
+
+
+ avg {lib.avg_loc?.toFixed(0) ?? '—'}
+
+
+ );
+ })}
+
+
+ {/* Coverage dot matrix — right after libraries */}
+ coverage
+
+ {data.coverage_percent}% · {data.total_implementations} of {data.total_specs * 9} possible
+
+
+ {data.coverage_matrix.map(row => {
+ const count = Object.values(row.libraries).filter(c => c.has_impl).length;
+ const intensity = count / 9;
+ return (
+
+
+
+ );
+ })}
+
+
+ less
+ {[0, 0.25, 0.5, 0.75, 1].map(v => (
+
+ ))}
+ more
+
+
+ {/* Timeline */}
+ {data.timeline.length > 0 && (
+ <>
+ timeline
+
+ {data.timeline.slice(-24).map(point => (
+
+
+
+ ))}
+
+
+
+ {data.timeline.slice(-24)[0]?.month ?? data.timeline[0]?.month}
+
+
+ {data.timeline[data.timeline.length - 1]?.month}
+
+
+ >
+ )}
+
+ {/* Top Implementations */}
+ top rated
+
+ {data.top_implementations.slice(0, 8).map((impl) => (
+ trackEvent('stats_top_impl_click', { spec: impl.spec_id, library: impl.library_id })}
+ >
+
+
+ {impl.preview_url ? (
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {impl.spec_title}
+
+
+
+ {impl.library_id}
+
+
+ {impl.quality_score}
+
+
+
+
+
+ ))}
+
+
+ {/* Tag Distribution */}
+ tags
+
+ {Object.entries(data.tag_distribution).map(([category, values]) => {
+ const paramMap: Record = {
+ plot_type: 'plot', data_type: 'data', domain: 'dom', features: 'feat',
+ dependencies: 'dep', techniques: 'tech', patterns: 'pat', dataprep: 'prep', styling: 'style',
+ };
+ const param = paramMap[category];
+ const entries = Object.entries(values);
+ return (
+
+
+ {category.replace('_', ' ')}
+
+
+ {entries.slice(0, 20).map(([tag, count]) => {
+ const size = count >= 100 ? '0.85rem' : count >= 50 ? '0.75rem' : count >= 10 ? '0.68rem' : '0.6rem';
+ const weight = count >= 50 ? 600 : count >= 10 ? 500 : 400;
+ const opacity = count >= 100 ? 1 : count >= 50 ? 0.85 : count >= 10 ? 0.7 : 0.5;
+ return (
+
+ {tag}{count}
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/src/router.tsx b/app/src/router.tsx
index 300c9c829f..f3cec3e500 100644
--- a/app/src/router.tsx
+++ b/app/src/router.tsx
@@ -23,6 +23,7 @@ const router = createBrowserRouter([
{ path: 'catalog', lazy: () => import('./pages/CatalogPage').then(m => ({ Component: m.CatalogPage, HydrateFallback: LazyFallback })) },
{ path: 'legal', lazy: () => import('./pages/LegalPage').then(m => ({ Component: m.LegalPage, HydrateFallback: LazyFallback })) },
{ path: 'mcp', lazy: () => import('./pages/McpPage').then(m => ({ Component: m.McpPage, HydrateFallback: LazyFallback })) },
+ { path: 'stats', lazy: () => import('./pages/StatsPage').then(m => ({ Component: m.StatsPage, HydrateFallback: LazyFallback })) },
{ path: ':specId', lazy: () => import('./pages/SpecPage').then(m => ({ Component: m.SpecPage, HydrateFallback: LazyFallback })) },
{ path: ':specId/:library', lazy: () => import('./pages/SpecPage').then(m => ({ Component: m.SpecPage, HydrateFallback: LazyFallback })) },
{ path: '*', element: },
diff --git a/app/src/types/index.ts b/app/src/types/index.ts
index 36724427db..39c0cf90c4 100644
--- a/app/src/types/index.ts
+++ b/app/src/types/index.ts
@@ -121,6 +121,7 @@ export interface Implementation {
quality_score: number | null;
code: string | null;
generated_at?: string;
+ updated?: string;
library_version?: string;
review_strengths?: string[];
review_weaknesses?: string[];
diff --git a/core/database/models.py b/core/database/models.py
index c0c4a9896b..3678d03a4f 100644
--- a/core/database/models.py
+++ b/core/database/models.py
@@ -9,7 +9,7 @@
from uuid import uuid4
from sqlalchemy import BigInteger, CheckConstraint, DateTime, Float, ForeignKey, Integer, String, Text, UniqueConstraint
-from sqlalchemy.orm import Mapped, mapped_column, relationship
+from sqlalchemy.orm import Mapped, deferred, mapped_column, relationship
from sqlalchemy.sql import func
from core.constants import LIBRARIES_METADATA
@@ -87,8 +87,8 @@ class Impl(Base):
spec_id: Mapped[str] = mapped_column(String, ForeignKey("specs.id", ondelete="CASCADE"), nullable=False)
library_id: Mapped[str] = mapped_column(String, ForeignKey("libraries.id", ondelete="CASCADE"), nullable=False)
- # Code
- code: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # Python source
+ # Code (deferred — ~13 MB total, only loaded when explicitly accessed or undeferred)
+ code: Mapped[Optional[str]] = deferred(mapped_column(Text, nullable=True)) # Python source
# Previews (filled by workflow, synced from metadata YAML)
preview_url: Mapped[Optional[str]] = mapped_column(String, nullable=True) # Full PNG
@@ -98,8 +98,8 @@ class Impl(Base):
python_version: Mapped[Optional[str]] = mapped_column(String, nullable=True) # e.g., "3.13"
library_version: Mapped[Optional[str]] = mapped_column(String, nullable=True) # e.g., "3.9.0"
- # Test matrix: [{"py": "3.11", "lib": "3.8.5", "ok": true}, ...]
- tested: Mapped[Optional[list]] = mapped_column(UniversalJSON, nullable=True)
+ # Test matrix (deferred — unused by any endpoint)
+ tested: Mapped[Optional[list]] = deferred(mapped_column(UniversalJSON, nullable=True))
# Quality & Generation - quality_score constrained to 0-100 range
quality_score: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
@@ -113,10 +113,12 @@ class Impl(Base):
review_strengths: Mapped[list[str]] = mapped_column(StringArray, default=list) # What's good
review_weaknesses: Mapped[list[str]] = mapped_column(StringArray, default=list) # What needs work
- # Extended review data (from issue #2845)
- review_image_description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # AI's visual description
- review_criteria_checklist: Mapped[Optional[dict[str, Any]]] = mapped_column(
- UniversalJSON, nullable=True
+ # Extended review data (deferred — ~12 MB total, only needed on detail pages)
+ review_image_description: Mapped[Optional[str]] = deferred(
+ mapped_column(Text, nullable=True)
+ ) # AI's visual description
+ review_criteria_checklist: Mapped[Optional[dict[str, Any]]] = deferred(
+ mapped_column(UniversalJSON, nullable=True)
) # Detailed scoring
review_verdict: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # "APPROVED" or "REJECTED"
diff --git a/core/database/repositories.py b/core/database/repositories.py
index edc781b74e..85c45b3e07 100644
--- a/core/database/repositories.py
+++ b/core/database/repositories.py
@@ -6,9 +6,9 @@
from typing import Generic, Optional, TypeVar
-from sqlalchemy import select
+from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.orm import selectinload
+from sqlalchemy.orm import selectinload, undefer
from core.database.models import Impl, Library, Spec
@@ -112,17 +112,31 @@ class SpecRepository(BaseRepository[Spec]):
updatable_fields = SPEC_UPDATABLE_FIELDS
async def get_by_id(self, spec_id: str) -> Optional[Spec]:
- """Get a spec by ID with all implementations and library info."""
+ """Get a spec by ID with implementations and library info.
+
+ Loads review_image_description and review_criteria_checklist (needed for detail display).
+ Code and tested remain deferred — use /specs/{id}/{lib}/code for code.
+ """
result = await self.session.execute(
- select(Spec).where(Spec.id == spec_id).options(selectinload(Spec.impls).selectinload(Impl.library))
+ select(Spec)
+ .where(Spec.id == spec_id)
+ .options(
+ selectinload(Spec.impls).selectinload(Impl.library),
+ selectinload(Spec.impls).undefer(Impl.review_image_description).undefer(Impl.review_criteria_checklist),
+ )
)
return result.scalar_one_or_none()
async def get_all(self) -> list[Spec]:
- """Get all specs with their implementations."""
+ """Get all specs with their implementations (deferred heavy fields excluded)."""
result = await self.session.execute(select(Spec).options(selectinload(Spec.impls)))
return list(result.scalars().all())
+ async def get_all_with_code(self) -> list[Spec]:
+ """Get all specs with implementations including deferred code field."""
+ result = await self.session.execute(select(Spec).options(selectinload(Spec.impls).undefer(Impl.code)))
+ return list(result.scalars().all())
+
async def get_ids(self) -> list[str]:
"""Get all spec IDs."""
result = await self.session.execute(select(Spec.id).order_by(Spec.id))
@@ -187,6 +201,21 @@ class ImplRepository(BaseRepository[Impl]):
model = Impl
updatable_fields = IMPL_UPDATABLE_FIELDS
+ async def get_total_code_lines(self) -> int:
+ """Count total lines of code across all implementations (lightweight, no code loading)."""
+ result = await self.session.execute(
+ select(func.sum(func.length(Impl.code) - func.length(func.replace(Impl.code, "\n", "")) + 1)).where(
+ Impl.code.isnot(None)
+ )
+ )
+ return result.scalar_one() or 0
+
+ async def get_loc_per_impl(self) -> list[tuple[str, int]]:
+ """Get (library_id, line_count) for each implementation (lightweight, no code loading)."""
+ loc_expr = func.length(Impl.code) - func.length(func.replace(Impl.code, "\n", "")) + 1
+ result = await self.session.execute(select(Impl.library_id, loc_expr).where(Impl.code.isnot(None)))
+ return [(row[0], row[1]) for row in result.all()]
+
async def get_by_spec(self, spec_id: str) -> list[Impl]:
"""Get all implementations for a spec."""
result = await self.session.execute(
@@ -201,9 +230,22 @@ async def get_by_library(self, library_id: str) -> list[Impl]:
)
return list(result.scalars().all())
+ async def get_code(self, spec_id: str, library_id: str) -> Optional[Impl]:
+ """Get a specific implementation with only the code field undeferred."""
+ result = await self.session.execute(
+ select(Impl).where(Impl.spec_id == spec_id, Impl.library_id == library_id).options(undefer(Impl.code))
+ )
+ return result.scalar_one_or_none()
+
async def get_by_spec_and_library(self, spec_id: str, library_id: str) -> Optional[Impl]:
- """Get a specific implementation by spec and library."""
- result = await self.session.execute(select(Impl).where(Impl.spec_id == spec_id, Impl.library_id == library_id))
+ """Get a specific implementation by spec and library (includes all deferred fields)."""
+ result = await self.session.execute(
+ select(Impl)
+ .where(Impl.spec_id == spec_id, Impl.library_id == library_id)
+ .options(
+ undefer(Impl.code), undefer(Impl.review_image_description), undefer(Impl.review_criteria_checklist)
+ )
+ )
return result.scalar_one_or_none()
async def upsert(self, spec_id: str, library_id: str, impl_data: dict) -> Impl:
diff --git a/docs/reference/api.md b/docs/reference/api.md
index 444baa3f68..46108fe416 100644
--- a/docs/reference/api.md
+++ b/docs/reference/api.md
@@ -271,6 +271,42 @@ The pyplots API is a **FastAPI-based REST API** serving plot data to the fronten
---
+## Insights Endpoints
+
+### GET `/insights/dashboard`
+
+**Purpose**: Rich platform statistics for the public stats page
+
+Returns aggregated data: per-library quality and LOC distributions, coverage matrix, top implementations, tag distribution, implementation timeline.
+
+Cached with stale-while-revalidate (1h refresh, 24h TTL).
+
+### GET `/insights/plot-of-the-day`
+
+**Purpose**: Daily featured high-quality implementation
+
+Deterministically selects an implementation with quality_score >= 90 based on today's date. Returns spec info, preview URL, AI image description, and code.
+
+### GET `/insights/related/{spec_id}`
+
+**Purpose**: Tag-based similarity recommendations
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `limit` | int | 6 | Number of results (1-12) |
+| `mode` | string | `spec` | `spec` = spec tags only, `full` = spec + impl tags |
+| `library` | string | null | In `full` mode, match against this library's impl_tags |
+
+Returns related specs sorted by Jaccard similarity with preview thumbnails and shared tags.
+
+### GET `/specs/{spec_id}/{library}/code`
+
+**Purpose**: Lightweight endpoint for implementation code
+
+Returns only the code field for a single implementation. Used by the frontend to lazy-load code on demand (code is deferred in the main `/specs/{spec_id}` response).
+
+---
+
## SEO Endpoints
### GET `/sitemap.xml`
diff --git a/docs/reference/plausible.md b/docs/reference/plausible.md
index 6a7d1333c4..18f5105d08 100644
--- a/docs/reference/plausible.md
+++ b/docs/reference/plausible.md
@@ -44,6 +44,7 @@ https://pyplots.ai/{category}/{value}/{category}/{value}/...
| `/catalog` | Catalog page (alphabetical spec list) |
| `/legal` | Legal notice, privacy policy, transparency |
| `/mcp` | MCP server documentation (AI assistant integration) |
+| `/stats` | Platform statistics (library scores, coverage, tags, top implementations) |
| `/{spec_id}` | Spec overview page (grid of all implementations) |
| `/{spec_id}/{library}` | Spec detail page (single library implementation) |
| `/interactive/{spec_id}/{library}` | Interactive fullscreen view (HTML plots) |
@@ -230,7 +231,7 @@ To see event properties in Plausible dashboard, you **MUST** register them as cu
| `category` | Filter category (lib, spec, plot, data, dom, feat, dep, tech, pat, prep, style) | `search`, `random_filter`, `filter_remove` |
| `value` | Filter value | `random_filter`, `filter_remove`, `tag_click` |
| `query` | Search query text | `search`, `search_no_results` |
-| `destination` | Link target (github, stats, linkedin, mcp, legal) | `external_link`, `internal_link` |
+| `destination` | Link target (github, plausible, stats, compare, linkedin, mcp, legal) | `external_link`, `internal_link` |
| `tab` | Tab name (code, specification, implementation, quality) | `tab_toggle` |
| `action` | Toggle action (open, close) | `tab_toggle` |
| `size` | Grid size (normal, compact) | `grid_resize` |
diff --git a/tests/unit/api/test_routers.py b/tests/unit/api/test_routers.py
index 0f2838886b..37304ab679 100644
--- a/tests/unit/api/test_routers.py
+++ b/tests/unit/api/test_routers.py
@@ -76,6 +76,7 @@ def mock_spec():
mock_impl.quality_score = 92.5
mock_impl.code = "import matplotlib.pyplot as plt"
mock_impl.generated_at = None
+ mock_impl.updated = None
mock_impl.generated_by = "claude"
mock_impl.python_version = "3.13"
mock_impl.library_version = "3.10.0"
@@ -244,7 +245,7 @@ def test_library_images_with_db(self, db_client, mock_spec) -> None:
client, _ = db_client
mock_spec_repo = MagicMock()
- mock_spec_repo.get_all = AsyncMock(return_value=[mock_spec])
+ mock_spec_repo.get_all_with_code = AsyncMock(return_value=[mock_spec])
with (
patch("api.routers.libraries.get_cache", return_value=None),
@@ -1376,3 +1377,248 @@ def test_image_matches_groups_style_no_match(self) -> None:
impl_lookup = {("scatter-basic", "matplotlib"): {"styling": ["alpha-blending"]}}
groups = [{"category": "style", "values": ["minimal-chrome"]}]
assert _image_matches_groups("scatter-basic", "matplotlib", groups, spec_lookup, impl_lookup) is False
+
+
+class TestInsightsRouter:
+ """Tests for insights router."""
+
+ def test_dashboard_without_db(self, client: TestClient) -> None:
+ """Dashboard should return 503 when DB not configured."""
+ with patch(DB_CONFIG_PATCH, return_value=False):
+ response = client.get("/insights/dashboard")
+ assert response.status_code == 503
+
+ def test_dashboard_with_db(self, client: TestClient, mock_spec) -> None:
+ """Dashboard should return aggregated stats."""
+ mock_spec_repo = MagicMock()
+ mock_spec_repo.get_all = AsyncMock(return_value=[mock_spec])
+ mock_impl_repo = MagicMock()
+ mock_impl_repo.get_total_code_lines = AsyncMock(return_value=500)
+ mock_impl_repo.get_loc_per_impl = AsyncMock(return_value=[("matplotlib", 50)])
+
+ with (
+ patch(DB_CONFIG_PATCH, return_value=True),
+ patch("api.routers.insights.get_or_set_cache", side_effect=_passthrough_cache),
+ patch("api.routers.insights.SpecRepository", return_value=mock_spec_repo),
+ patch("api.routers.insights.ImplRepository", return_value=mock_impl_repo),
+ ):
+ response = client.get("/insights/dashboard")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["total_specs"] == 1
+ assert data["total_implementations"] == 1
+ assert data["total_lines_of_code"] == 500
+ assert data["total_interactive"] == 0
+ assert len(data["library_stats"]) == 9
+ assert isinstance(data["coverage_matrix"], list)
+ assert isinstance(data["score_distribution"], dict)
+ assert isinstance(data["tag_distribution"], dict)
+
+ def test_potd_without_db(self, client: TestClient) -> None:
+ """Plot of the day should return 503 when DB not configured."""
+ with patch(DB_CONFIG_PATCH, return_value=False):
+ response = client.get("/insights/plot-of-the-day")
+ assert response.status_code == 503
+
+ def test_potd_with_db(self, client: TestClient, mock_spec) -> None:
+ """Plot of the day should return a featured implementation."""
+ mock_spec_repo = MagicMock()
+ mock_spec_repo.get_all = AsyncMock(return_value=[mock_spec])
+ mock_impl = MagicMock()
+ mock_impl.code = "import matplotlib"
+ mock_impl.review_image_description = "A scatter plot"
+ mock_impl_repo = MagicMock()
+ mock_impl_repo.get_by_spec_and_library = AsyncMock(return_value=mock_impl)
+
+ with (
+ patch(DB_CONFIG_PATCH, return_value=True),
+ patch("api.routers.insights.get_or_set_cache", side_effect=_passthrough_cache),
+ patch("api.routers.insights.SpecRepository", return_value=mock_spec_repo),
+ patch("api.routers.insights.ImplRepository", return_value=mock_impl_repo),
+ ):
+ response = client.get("/insights/plot-of-the-day")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["spec_id"] == "scatter-basic"
+ assert data["library_id"] == "matplotlib"
+ assert data["quality_score"] == 92.5
+
+ def test_potd_no_candidates(self, client: TestClient) -> None:
+ """Plot of the day should return null when no high-quality implementations."""
+ mock_impl = MagicMock()
+ mock_impl.library_id = "matplotlib"
+ mock_impl.quality_score = 50.0 # Below threshold
+ mock_impl.preview_url = TEST_IMAGE_URL
+ mock_spec = MagicMock()
+ mock_spec.id = "low-quality"
+ mock_spec.impls = [mock_impl]
+
+ mock_spec_repo = MagicMock()
+ mock_spec_repo.get_all = AsyncMock(return_value=[mock_spec])
+ mock_impl_repo = MagicMock()
+
+ with (
+ patch(DB_CONFIG_PATCH, return_value=True),
+ patch("api.routers.insights.get_or_set_cache", side_effect=_passthrough_cache),
+ patch("api.routers.insights.SpecRepository", return_value=mock_spec_repo),
+ patch("api.routers.insights.ImplRepository", return_value=mock_impl_repo),
+ ):
+ response = client.get("/insights/plot-of-the-day")
+ assert response.status_code == 200
+ assert response.json() is None
+
+ def test_related_without_db(self, client: TestClient) -> None:
+ """Related should return 503 when DB not configured."""
+ with patch(DB_CONFIG_PATCH, return_value=False):
+ response = client.get("/insights/related/scatter-basic")
+ assert response.status_code == 503
+
+ def test_related_spec_mode(self, client: TestClient) -> None:
+ """Related in spec mode should return similar specs based on spec tags."""
+ mock_impl1 = MagicMock()
+ mock_impl1.library_id = "matplotlib"
+ mock_impl1.quality_score = 90.0
+ mock_impl1.preview_url = TEST_IMAGE_URL
+ mock_impl1.impl_tags = {}
+
+ mock_spec1 = MagicMock()
+ mock_spec1.id = "scatter-basic"
+ mock_spec1.title = "Basic Scatter"
+ mock_spec1.tags = {"plot_type": ["scatter"], "domain": ["statistics"]}
+ mock_spec1.impls = [mock_impl1]
+
+ mock_impl2 = MagicMock()
+ mock_impl2.library_id = "matplotlib"
+ mock_impl2.quality_score = 88.0
+ mock_impl2.preview_url = TEST_IMAGE_URL
+ mock_impl2.impl_tags = {}
+
+ mock_spec2 = MagicMock()
+ mock_spec2.id = "scatter-regression"
+ mock_spec2.title = "Scatter with Regression"
+ mock_spec2.tags = {"plot_type": ["scatter"], "domain": ["machine-learning"]}
+ mock_spec2.impls = [mock_impl2]
+
+ mock_spec_repo = MagicMock()
+ mock_spec_repo.get_all = AsyncMock(return_value=[mock_spec1, mock_spec2])
+
+ with (
+ patch(DB_CONFIG_PATCH, return_value=True),
+ patch("api.routers.insights.get_or_set_cache", side_effect=_passthrough_cache),
+ patch("api.routers.insights.SpecRepository", return_value=mock_spec_repo),
+ ):
+ response = client.get("/insights/related/scatter-basic?mode=spec&limit=3")
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data["related"]) == 1
+ assert data["related"][0]["id"] == "scatter-regression"
+ assert data["related"][0]["similarity"] > 0
+ assert "scatter" in data["related"][0]["shared_tags"]
+
+ def test_related_not_found(self, client: TestClient) -> None:
+ """Related should return empty list for nonexistent spec."""
+ mock_spec_repo = MagicMock()
+ mock_spec_repo.get_all = AsyncMock(return_value=[])
+
+ with (
+ patch(DB_CONFIG_PATCH, return_value=True),
+ patch("api.routers.insights.get_or_set_cache", side_effect=_passthrough_cache),
+ patch("api.routers.insights.SpecRepository", return_value=mock_spec_repo),
+ ):
+ response = client.get("/insights/related/nonexistent")
+ assert response.status_code == 200
+ assert response.json()["related"] == []
+
+ def test_related_full_mode_with_library(self, client: TestClient) -> None:
+ """Related in full mode should include impl tags."""
+ mock_impl1 = MagicMock()
+ mock_impl1.library_id = "matplotlib"
+ mock_impl1.quality_score = 90.0
+ mock_impl1.preview_url = TEST_IMAGE_URL
+ mock_impl1.impl_tags = {"techniques": ["annotations"], "patterns": ["data-generation"]}
+
+ mock_spec1 = MagicMock()
+ mock_spec1.id = "scatter-basic"
+ mock_spec1.title = "Basic Scatter"
+ mock_spec1.tags = {"plot_type": ["scatter"]}
+ mock_spec1.impls = [mock_impl1]
+
+ mock_impl2 = MagicMock()
+ mock_impl2.library_id = "matplotlib"
+ mock_impl2.quality_score = 85.0
+ mock_impl2.preview_url = TEST_IMAGE_URL
+ mock_impl2.impl_tags = {"techniques": ["annotations"], "patterns": ["other"]}
+
+ mock_spec2 = MagicMock()
+ mock_spec2.id = "bar-annotated"
+ mock_spec2.title = "Annotated Bar"
+ mock_spec2.tags = {"plot_type": ["bar"]}
+ mock_spec2.impls = [mock_impl2]
+
+ mock_spec_repo = MagicMock()
+ mock_spec_repo.get_all = AsyncMock(return_value=[mock_spec1, mock_spec2])
+
+ with (
+ patch(DB_CONFIG_PATCH, return_value=True),
+ patch("api.routers.insights.get_or_set_cache", side_effect=_passthrough_cache),
+ patch("api.routers.insights.SpecRepository", return_value=mock_spec_repo),
+ ):
+ response = client.get("/insights/related/scatter-basic?mode=full&library=matplotlib")
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data["related"]) == 1
+ assert "annotations" in data["related"][0]["shared_tags"]
+
+
+class TestSpecCodeEndpoint:
+ """Tests for the /specs/{spec_id}/{library}/code endpoint."""
+
+ def test_code_without_db(self, client: TestClient) -> None:
+ """Code endpoint should return 503 when DB not configured."""
+ with patch(DB_CONFIG_PATCH, return_value=False):
+ response = client.get("/specs/scatter-basic/matplotlib/code")
+ assert response.status_code == 503
+
+ def test_code_with_db(self, client: TestClient) -> None:
+ """Code endpoint should return code for a specific implementation."""
+ mock_impl = MagicMock()
+ mock_impl.code = "import matplotlib.pyplot as plt\nplt.plot([1,2,3])"
+ mock_impl_repo = MagicMock()
+ mock_impl_repo.get_code = AsyncMock(return_value=mock_impl)
+
+ with (
+ patch(DB_CONFIG_PATCH, return_value=True),
+ patch("api.routers.specs.get_cache", return_value=None),
+ patch("api.routers.specs.set_cache"),
+ patch("api.routers.specs.ImplRepository", return_value=mock_impl_repo),
+ ):
+ response = client.get("/specs/scatter-basic/matplotlib/code")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["spec_id"] == "scatter-basic"
+ assert data["library"] == "matplotlib"
+ assert "matplotlib" in data["code"]
+
+ def test_code_not_found(self, client: TestClient) -> None:
+ """Code endpoint should return 404 when implementation not found."""
+ mock_impl_repo = MagicMock()
+ mock_impl_repo.get_code = AsyncMock(return_value=None)
+
+ with (
+ patch(DB_CONFIG_PATCH, return_value=True),
+ patch("api.routers.specs.get_cache", return_value=None),
+ patch("api.routers.specs.ImplRepository", return_value=mock_impl_repo),
+ ):
+ response = client.get("/specs/nonexistent/matplotlib/code")
+ assert response.status_code == 404
+
+ def test_code_cache_hit(self, client: TestClient) -> None:
+ """Code endpoint should return cached data when available."""
+ cached = {"spec_id": "scatter-basic", "library": "matplotlib", "code": "cached code"}
+ with (
+ patch(DB_CONFIG_PATCH, return_value=True),
+ patch("api.routers.specs.get_cache", return_value=cached),
+ ):
+ response = client.get("/specs/scatter-basic/matplotlib/code")
+ assert response.status_code == 200
+ assert response.json()["code"] == "cached code"