Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Python
__pycache__/
*.py[cod]
*.pyc
*.pyo
*.pyd
*.so
*.egg-info/
.pytest_cache/
.mypy_cache/
.tox/
.cache/
.coverage
build/
dist/
pip-wheel-metadata/
.env
venv/
.venv/
Comment thread
drothermel marked this conversation as resolved.
env/

# Node
node_modules/
dist/
.DS_Store
Comment thread
drothermel marked this conversation as resolved.
.env.*
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Data (add your own data files here)
backend/data/**
!backend/data/.gitkeep
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
34 changes: 34 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Repo Instructions

This repo is a transplant-oriented visualization template.
The primary goal is to help a coding agent copy the relevant pieces into an existing project with minimal churn.

## Integration Priority

- Prefer adapting this template into the host project's existing architecture over copying the entire repo structure.
- Preserve the host project's package manager, app shell, router, styling system, and backend layout unless the user explicitly asks to replace them.
- Copy only the infrastructure that materially helps:
- Plot wrapper and Plotly-specific CSS fixes
- typed API client patterns
- example visualization pages as reference material
- simple backend response/model patterns

## Replacement Priority

- Treat the experiment domain as demo code.
- Replace these first when integrating into a real project:
- `backend/models/experiment.py`
- `backend/routers/experiments.py`
- `frontend/src/types/experiment.ts`
- `frontend/src/api/experiments.ts`
- `frontend/src/pages/Overview.tsx`
- `frontend/src/pages/HeatmapExplorer.tsx`

## Local Repo Constraints

- Python uses root `pyproject.toml` with `uv`.
- Start the backend from repo root as `backend.main:app`.
- Backend env vars are loaded from `backend/.env`.
- Frontend config files are ESM; do not introduce CommonJS `require(...)` into frontend config.
- Keep Plotly's modebar on hover by default unless the user explicitly wants persistent toolbar controls.
- Be careful with global SVG styling; Tailwind base styles can interfere with Plotly UI chrome.
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.PHONY: install dev backend frontend

install:
uv sync
cd frontend && npm ci

dev:
@$(MAKE) -j2 backend frontend

backend:
uv run uvicorn backend.main:app --reload --port 8000

frontend:
cd frontend && npm run dev
105 changes: 104 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,104 @@
# viz-template
# research-tool-template

A minimal full-stack template for research data exploration tools.

**Stack:** FastAPI + Pydantic · Vite + React + TypeScript · Tailwind + shadcn/ui · TanStack Query · Plotly

## Start Here

If you want to copy this into an existing app instead of running this repo as-is:

- read [docs/integrating-into-existing-project.md](docs/integrating-into-existing-project.md)
- paste [docs/agent-prompt-template.md](docs/agent-prompt-template.md) into your coding agent
- run [docs/integration-smoke-checklist.md](docs/integration-smoke-checklist.md) after integration

## Setup

```bash
make install # uv sync + npm ci
make dev # starts backend (port 8000) + frontend (port 5173)
```

Open [http://localhost:5173](http://localhost:5173).

### Prerequisites

- `uv`
- Node.js + npm
- `make`

### Environment

If you need backend env vars, copy `backend/.env.example` to `backend/.env`.
The backend loads `backend/.env` even when started from the repo root.

### Direct commands

```bash
uv sync
cd frontend && npm ci

uv run uvicorn backend.main:app --reload --port 8000
cd frontend && npm run dev
```

## Structure

```
pyproject.toml # Python project definition for uv
uv.lock # locked Python dependencies

backend/
main.py # FastAPI app, CORS, router registration
models/
experiment.py # Pydantic models — replace with your own
routers/
experiments.py # API endpoints — replace with your own
data/ # put data files here (gitignored)

frontend/src/
api/
client.ts # base fetch wrapper + TanStack Query client
experiments.ts # typed API functions — mirror your backend here
types/
experiment.ts # TypeScript interfaces mirroring Pydantic models
components/
ui/ # shadcn/ui components (copy-paste, no magic)
layout/ # Sidebar + Layout shell
charts/Plot.tsx # thin Plotly wrapper with sensible defaults
pages/
Overview.tsx # scatter plot + filter controls (example)
HeatmapExplorer.tsx # heatmap with axis/metric selectors (example)
App.tsx # React Router routes
```

## Adding a new page

1. Create `frontend/src/pages/MyPage.tsx` (copy an existing page as a starting point)
2. Add a route in `App.tsx`
3. Add a nav link in `components/layout/Sidebar.tsx`
4. Add a backend router in `backend/routers/` and register it in `main.py`
5. Add typed API functions in `frontend/src/api/` and types in `frontend/src/types/`

## Plotly chart types

The `<Plot>` wrapper accepts any Plotly trace type. Common ones:

```tsx
// Scatter
data={[{ type: 'scatter', mode: 'markers', x: [...], y: [...] }]}

// Heatmap
data={[{ type: 'heatmap', z: [[...]], x: [...], y: [...], colorscale: 'Viridis' }]}

// Bump / line
data={[{ type: 'scatter', mode: 'lines+markers', x: [...], y: [...] }]}

// Faceted: use layout.grid
layout={{ grid: { rows: 2, columns: 3, pattern: 'independent' } }}
```

## Deploying to Railway

The backend is standard FastAPI — Railway can install from `pyproject.toml` and run it with uvicorn.
Set the `ALLOWED_ORIGINS` env var to your frontend's Railway URL.
1 change: 1 addition & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALLOWED_ORIGINS=http://localhost:5173
1 change: 1 addition & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Backend package for the research tool template."""
Empty file added backend/data/.gitkeep
Empty file.
37 changes: 37 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import os
from pathlib import Path

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv

from backend.routers import experiments

BACKEND_DIR = Path(__file__).resolve().parent

load_dotenv(BACKEND_DIR / ".env")

app = FastAPI(title="Research Tool API", version="0.1.0")

allowed_origins = [
origin.strip()
for origin in os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(",")
if origin.strip()
]

app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# --- Register routers ---
# Add your own routers here following the same pattern
app.include_router(experiments.router, prefix="/api/experiments", tags=["experiments"])


@app.get("/health")
def health():
return {"status": "ok"}
Empty file added backend/models/__init__.py
Empty file.
61 changes: 61 additions & 0 deletions backend/models/experiment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
Example Pydantic models for a hyperparameter sweep experiment.
Replace these with your own domain models.

Pattern to follow:
- One model per logical entity
- Use nested models liberally (Pydantic handles serialization)
- Response models wrap lists with a `total` field for easy pagination later
- Filter/query models as separate classes (can be used as query params or request bodies)
"""

from pydantic import BaseModel
from typing import Optional


# --- Domain models ---

class HyperparameterConfig(BaseModel):
learning_rate: float
batch_size: int
dropout: float
hidden_dim: int
num_layers: int


class Metrics(BaseModel):
accuracy: float
loss: float
val_accuracy: float
val_loss: float


class ExperimentResult(BaseModel):
id: str
name: str
config: HyperparameterConfig
metrics: Metrics
epoch: int
created_at: str


# --- Response models ---

class ExperimentsResponse(BaseModel):
experiments: list[ExperimentResult]
total: int


class HeatmapResponse(BaseModel):
x_param: str
y_param: str
metric: str
z: list[list[Optional[float]]] # 2D matrix, None = no data
x_labels: list[str]
y_labels: list[str]


# --- Filter/query param models ---

VALID_CONFIG_PARAMS = {"learning_rate", "batch_size", "dropout", "hidden_dim", "num_layers"}
VALID_METRICS = {"accuracy", "loss", "val_accuracy", "val_loss"}
Empty file added backend/routers/__init__.py
Empty file.
Loading