Skip to content

Commit 5b38a60

Browse files
committed
Documentation/Note: add TODO comment to legend for per-group control approximation
1 parent 4521ef7 commit 5b38a60

2 files changed

Lines changed: 130 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# CLAUDE.md
2+
3+
Guidance for Claude Code when working in this repository. Keep this file honest — if the architecture changes, update it here.
4+
5+
## Commands
6+
7+
```bash
8+
npm run dev # Start Vite dev server
9+
npm run build # Build for production (outputs to dist/)
10+
npm run lint # ESLint + Prettier check
11+
npm run lint:fix # Auto-fix lint issues
12+
npm test # Jest (runs with --passWithNoTests)
13+
```
14+
15+
Formatting: 2-space indent, single quotes, no semicolons, trailing commas. Enforced by Prettier. Lint is set to `--max-warnings 0`, so warnings fail CI.
16+
17+
Path alias: `@eplant/*` maps to `Eplant/*` (see `tsconfig.json` and `vite-tsconfig-paths`).
18+
19+
## What ePlant Is
20+
21+
A gene-centric visualization tool for plant genomes (currently _Arabidopsis thaliana_ only). The user picks a gene of interest, then explores it through multiple "views" — each a distinct visualization (gene info, publications, eFP expression maps, chromosome viewer, interaction network, etc.). External data comes from the BAR API at `bar.utoronto.ca`.
22+
23+
## Architecture
24+
25+
### Routing Is the Backbone
26+
27+
Routes are defined in [`Eplant/main.tsx`](Eplant/main.tsx) using React Router v6's `createBrowserRouter`. The root route renders [`Eplant/Eplant.tsx`](Eplant/Eplant.tsx), and each view is a child route matching a path like `/{view-id}/:geneid?`. The child route renders inside `<Outlet />` in [`Eplant/UI/Layout/ViewContainer/index.tsx`](Eplant/UI/Layout/ViewContainer/index.tsx).
28+
29+
The component tree:
30+
```
31+
main.tsx (Providers: Jotai, Config, QueryClient, Router)
32+
└─ Eplant.tsx (ThemeProvider, Sidebar, URLStateProvider)
33+
└─ ViewContainer (TopBar, ErrorBoundary, Outlet)
34+
└─ {view component} — e.g. PlantEFPView, GeneInfoView
35+
```
36+
37+
### Views Are Plain React Components
38+
39+
A view has two parts that are registered separately:
40+
41+
1. **The component** — a React function component under `Eplant/views/{Name}/{Name}.tsx`. It reads the current gene via `useOutletContext<ViewContext>()`, fetches its own data with `useQuery`, manages URL-synced state with `useURLState`, and renders. Registered as a route in `main.tsx`.
42+
43+
2. **The metadata** — a `ViewMetadata` object (see [`Eplant/View/index.ts`](Eplant/View/index.ts)) exported as default from `Eplant/views/{Name}/index.tsx`. Holds `id`, `name`, `icon`, `description`, `thumbnail`, `citation`, and `actions`. Registered in [`Eplant/config.tsx`](Eplant/config.tsx) under `userViewMetadata` (gene-specific) or `genericViewMetadata` (standalone like GetStarted).
44+
45+
**These two registrations must stay in sync** — the `id` in the metadata must match the path in `main.tsx`.
46+
47+
### Data Fetching
48+
49+
Each view owns its data fetching via `useQuery` from `@tanstack/react-query`. The `QueryClient` is constructed in `main.tsx` and provided app-wide. Pattern used in existing views:
50+
51+
```tsx
52+
const { data, isLoading, isError, error } = useQuery<TData, ViewDataError>({
53+
queryKey: [`view-id-${geneticElement?.id}`],
54+
queryFn: async () => loaderFn(geneticElement, setLoadAmount),
55+
retry: false,
56+
})
57+
```
58+
59+
Loader functions live in the view's own directory (e.g. `views/GeneInfoView/loader.ts`). There is **no shared API client yet** — loaders call `fetch` or `axios` directly against hardcoded `bar.utoronto.ca` URLs. When new backend APIs arrive, wrap them in a proper client layer before adopting.
60+
61+
### State Management
62+
63+
Four layers, each with a specific job. Respect the boundaries.
64+
65+
| Layer | Purpose | Where |
66+
|---|---|---|
67+
| **TanStack Query** | Server data, caching, loading states | Per-view `useQuery` |
68+
| **URL search params** | View-specific state that should be shareable/bookmarkable | `useURLState<T>()` from [`state/URLStateProvider.tsx`](Eplant/state/URLStateProvider.tsx), validated by Zod schemas |
69+
| **Jotai atoms** | Global UI state (dark mode, sidebar, active gene/view ids, gene collections) | [`state/index.tsx`](Eplant/state/index.tsx) |
70+
| **IndexedDB** | Persisting Jotai atoms across sessions | `atomWithOptionalStorage` wrapper over [`util/Storage/index.tsx`](Eplant/util/Storage/index.tsx) |
71+
72+
**URL state pattern:** define a Zod schema with `.default(...)` on each field, call `initializeState(schema)` in a `useEffect` on mount, read `state` and call `setState(...)` to update. The provider serializes to query params with a 50ms debounce. See any eFP view for an example.
73+
74+
### Active Gene / Active View
75+
76+
Stored in Jotai atoms (`activeGeneIdAtom`, `activeViewIdAtom`) and also reflected in the URL. `ViewContainer` has two `useEffect`s that sync them — one reads URL → atoms on mount, one writes atoms → URL on change. This is fragile and worth keeping in mind when editing routing logic.
77+
78+
## Adding a New View
79+
80+
1. Create `Eplant/views/{Name}/{Name}.tsx` — the component. Read gene via `useOutletContext<ViewContext>()`, fetch with `useQuery`, handle loading/error states via `<LoadingPage />`.
81+
2. Create `Eplant/views/{Name}/index.tsx` — export `ViewMetadata` as default. Include `id`, `name`, `icon`, `citation`.
82+
3. If the view has URL-persisted state, define a Zod schema alongside it.
83+
4. Register the route in [`Eplant/main.tsx`](Eplant/main.tsx) with path `{view-id}/:geneid?`.
84+
5. Register the metadata in [`Eplant/config.tsx`](Eplant/config.tsx) under `userViewMetadata`.
85+
86+
Copy [`views/PlantEFP/`](Eplant/views/PlantEFP/) or [`views/GeneInfoView/`](Eplant/views/GeneInfoView/) as templates — they follow the current pattern cleanly.
87+
88+
## Known Legacy Code — Don't Copy These Patterns
89+
90+
These exist but predate the routing refactor. Treat them as refactor targets, not references.
91+
92+
- **`EFP` class** in [`Eplant/views/eFP/index.tsx`](Eplant/views/eFP/index.tsx) — defines a `component` method that calls React hooks. Violates rules-of-hooks assumptions. Mixes data fetching (`getInitialData`) with rendering. Should be split into a hook + function component.
93+
- **`Species` static registry** in [`Eplant/GeneticElement.ts`](Eplant/GeneticElement.ts) — species self-register into a static array at module load. Hidden global mutation, hard to test. Eventually replace with explicit registration via Config context.
94+
- **Top-level `await`** in [`Eplant/state/index.tsx`](Eplant/state/index.tsx) (`citationsAtom`) — blocks module load on a network request. Move to a React Query call.
95+
- **`atomWithOptionalStorage`** is marked with a `TODO` for removal.
96+
- **`dangerouslySetInnerHTML`** in eFP views renders SVGs from the BAR server. If the source ever becomes untrusted, route through `dompurify` (already a dep).
97+
- **`flexlayout-react`** is still imported in `state/index.tsx` but the dockable-panel layout it provided was replaced by React Router. Remove when convenient.
98+
- **eFP colour calculation** has historically been a source of confusion. Downstream renderers (`useStyles`, `EFPTooltip`) must use `group.control` (per-group) not `data.control` (cross-group average) for correct relative-mode colours and log2 fold change values. `data.control` is a valid fallback only.
99+
100+
## Stack
101+
102+
React 18 · TypeScript 5 · Vite 4 · MUI v5 · Jotai · TanStack Query v5 · React Router v6 · Zod · Cytoscape (+ cose-bilkent, automove, popper) · D3 · idb (IndexedDB) · axios · lodash · flexlayout-react (legacy) · Jest + React Testing Library + MSW · ESLint + Prettier
103+
104+
## Environment Variables
105+
106+
Set via `.env` (Vite convention: must be prefixed `VITE_`):
107+
108+
- `VITE_MAPS_API_KEY` — Google Maps API key for the WorldEFP view
109+
- `VITE_MAP_ID` — Google Maps style ID for the WorldEFP view
110+
- `BASE_URL` — build-time base path (set to `/ePlant` in CI for GitHub Pages)
111+
112+
## CI / Deploy
113+
114+
Three workflows in `.github/workflows/`:
115+
- `build.yml` — runs on branches other than main/staging; Node 22
116+
- `linting.yml` — Prettier + ESLint on all pushes/PRs; Node 20
117+
- `deploy.yml` — builds and publishes to GitHub Pages from `staging-debug`; Node 22
118+
- `node.js.yml` — legacy, runs on main with Node 16 (EOL — scheduled for removal/update)
119+
120+
## When In Doubt
121+
122+
- Check [`RoutingChanges.md`](RoutingChanges.md) for the rationale behind the current view architecture.
123+
- Prefer editing an existing working view as a reference over inferring patterns from this file.
124+
- If you find this file contradicts the code, trust the code and flag the discrepancy.

Eplant/views/eFP/Viewer/legend.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Box, styled, useTheme } from '@mui/material'
22

33
import { getColor } from '../svg'
4-
import { ColorMode, EFPData, EFPState } from '../types'
4+
import { ColorMode, EFPData } from '../types'
55

66
interface ILegendProps {
77
data: EFPData
@@ -18,6 +18,11 @@ export default styled(function Legend({
1818
...rest
1919
}: ILegendProps) {
2020
const theme = useTheme()
21+
// TODO: legend uses data.control (cross-group average) and data.min/max as a
22+
// global approximation. This can diverge from per-group colours when groups
23+
// have different controls. A per-group legend is out of scope for this fix.
24+
// I.e. The colours may not line up in this commit/PR due to group colours being different
25+
// than the global legend
2126
const control = data.control ?? 1
2227
const values = Array(GRADIENT_STEPS)
2328
.fill(0)

0 commit comments

Comments
 (0)