Skip to content

Commit fec66f9

Browse files
committed
Add diff-mode/legend + drawer-redesign specs
- `diff-mode-and-legend.md`: implementation notes for `?v0=`/`?v1=` and HeatmapLegend - `drawer-redesign.md`: light-touch redesign sketch (lucide icons, accent stripes, localStorage section persistence) — surfaces the 3 inconsistent heading styles in `Controls.tsx` for follow-up
1 parent 6949d97 commit fec66f9

2 files changed

Lines changed: 366 additions & 0 deletions

File tree

specs/diff-mode-and-legend.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Spec: Editable Diff Sources + Heatmap Color Legend
2+
3+
## Context
4+
5+
Two related gaps surfaced while testing the new `?src=diff` + `?heat=1` view on the worst-case representative materials:
6+
7+
1. **Diff sources are implicit and inflexible.** `?src=diff` hardcodes the comparison as `|label − input|`, both auto-derived from the current `mp_id`. There's no way to inspect *which* URIs are being diffed, and no way to override them. This blocks the natural next use case: comparing **predicted ρ** (from a model checkpoint) against the **DFT label** for the same material.
8+
2. **The heatmap has no legend.** Turbo colors are perceptually striking but quantitatively opaque — the user can't tell whether red means "0.01 e/ų" or "0.5 e/ų" of residual density. Currently the user has to mentally combine the Gamma + Low-cutoff sliders with a colormap they can't see to interpret the picture.
9+
10+
## Goals
11+
12+
- **Phase 1 (this spec):** make diff sources first-class and configurable; add a quantitative legend to the heatmap.
13+
- **Phase 2 (deferred):** signed (diverging) diff colormaps, multiple colormap choice, full color-scale editor a la `~/c/jc-taxes/www/src/GradientEditor.tsx` — punt until we have a 2nd consumer.
14+
15+
Out of scope: `?src=predicted` itself (separate spec), drawer reorg / icon rail (separate item per `#4` discussion).
16+
17+
## 1. Editable two-URI diff sources
18+
19+
### URL state
20+
21+
New URL params (both optional):
22+
- `?v0=<url>` — first volume operand of the diff
23+
- `?v1=<url>` — second volume operand
24+
25+
(Naming rationale: `v0`/`v1` implies "volume 0, volume 1" with no before/after ambiguity that `a`/`b` carries.)
26+
27+
When `src=diff`:
28+
- If both `v0` and `v1` are present, use them as the diff operands directly. `m=` is irrelevant for loading (but still affects the structure metadata used for atoms/lattice — see "structure source" below).
29+
- If only `v0` xor `v1` is present, fill the other from the auto-resolved label/input for `m=`. (Convenience: lets you pin one side and let the material change.)
30+
- If neither is present, fall back to current behavior: `v0 = resolveLoadUrl(record, 'label', 'zarr')`, `v1 = resolveLoadUrl(record, 'input', 'zarr')`.
31+
32+
The diff is `|v0 − v1|` regardless of ordering. (Phase 2 will add signed diff with a swap-affecting sign.)
33+
34+
### `loadDiff` API change
35+
36+
```ts
37+
const loadDiff = useCallback(async (
38+
record: MaterialRecord | null, // null when fully URL-driven (?a=&?b=)
39+
v0UrlOverride?: string,
40+
v1UrlOverride?: string,
41+
) => { … })
42+
```
43+
44+
Resolution order inside `loadDiff`:
45+
1. If `v0UrlOverride`/`v1UrlOverride` set → use those.
46+
2. Else if `record` set → use `resolveLoadUrl(record, 'label', 'zarr')` / `('input', 'zarr')`.
47+
3. If still missing → set `fetchStatus` error and return.
48+
49+
### Structure source
50+
51+
The heatmap/iso renderer needs an `atoms + lattice + dims` triple. Currently `loadDiff` borrows `lbl` for that. With user-overridden URLs, we keep the same convention: structure is taken from the **`v0` operand**'s Zarr (the "before" / "expected" side). If the user wants atoms from `v1`, they can swap.
52+
53+
### Drawer UI
54+
55+
When `srcRole === 'diff'`, expose a new collapsible "Diff sources" section in the right drawer (below `Load from URL`, above `Examples`):
56+
57+
```
58+
▼ DIFF SOURCES
59+
v0: [s3://openathena/electrai/zarr/mp-X-label.zarr/ ] [↺]
60+
v1: [s3://openathena/electrai/zarr/mp-X-input.zarr/ ] [↺]
61+
[⇄ Swap] [Reset to auto (label / input)]
62+
```
63+
64+
- Two text inputs (full-width, monospace, same style as `Load from URL`).
65+
- `` per-row reverts that row to its auto-resolved value.
66+
- `⇄ Swap` swaps v0 ↔ v1 (purely cosmetic for `|v0−v1|`; matters when Phase 2 lands signed diff).
67+
- `Reset to auto` clears both `?v0=` and `?v1=` from the URL.
68+
- Editing a field triggers a load (debounced 500 ms, like `Load from URL` already is).
69+
70+
When `srcRole !== 'diff'`, the section is hidden (don't surface useless inputs).
71+
72+
### Compatibility
73+
74+
- Existing URLs `?m=mp-X&src=diff` continue to work (auto-resolves both sides).
75+
- `?src=predicted` (when it lands later) will be orthogonal: predicted is a single-source mode, not a diff. To compare predicted vs label, the user uses `?src=diff&v0=<label-url>&v1=<predicted-url>` (no `m=` needed, or `m=` only for atoms metadata).
76+
77+
### Title bar
78+
79+
Currently: `|Label − Input|` for `src=diff`. Update to one of:
80+
- `|Label − Input|` (auto-derived, both sides from the same material) — unchanged.
81+
- `|v0 − v1|` (user-overridden) with the URL basenames as a hover tooltip.
82+
83+
## 2. Heatmap color legend
84+
85+
### What it should show
86+
87+
A small **vertical or horizontal turbo color bar** with:
88+
- Tick labels at numeric values in the volume's actual units (e/ų for density, same units for diff).
89+
- The currently effective `lowCutoff` line marked on the bar (semi-transparent black overlay below the cutoff, to signal "this range is invisible").
90+
- Tick spacing is **gamma-corrected** so the bar visually represents what's rendered: where the user sees a particular shade, the corresponding density value can be read off the same vertical position.
91+
92+
### Where it lives
93+
94+
Bottom-right of the canvas (above the existing axis gizmo) when `useHeatmap` is true. ~24 px wide × ~240 px tall. Floats over the canvas like the slice-viewer thumbnail does today.
95+
96+
Hidden when `useHeatmap` is false.
97+
98+
### Numeric range
99+
100+
Today the renderer normalizes the volume to `[0, 1]` based on per-volume `dMin`/`dMax`. Lift that calculation up so it's available to both the renderer AND the legend:
101+
102+
```ts
103+
// Move into a utility (or a useMemo at the App level):
104+
function volumeRange(data: Float32Array): { min: number; max: number } { … }
105+
```
106+
107+
Pass `dMin`/`dMax` as props into `HeatmapRenderer` (avoids recomputing) and use the same values for the legend.
108+
109+
For the diff view specifically: `dMin = 0` always (since `|a-b|`), `dMax` = max |Δρ|. That's a meaningful number for the user — the legend should display it.
110+
111+
### Tick choices
112+
113+
- 5 ticks: 0%, 25%, 50%, 75%, 100% in **gamma-corrected position** (so visually evenly spaced, but mapped through `pow(p, 1/gamma)` to recover the data value).
114+
- Labels: 3 sig figs in scientific notation if `|max| < 0.01` or `|max| > 1000`, else fixed-point.
115+
- Units suffix: configurable prop (default `'e/ų'`).
116+
117+
### Interaction (Phase 1: read-only)
118+
119+
No drag-to-set yet. Just visual.
120+
121+
(Phase 2 candidate: drag the cutoff line, click ticks to snap iso level.)
122+
123+
## 3. Color-scale lib (`$js/`) — investigation note
124+
125+
`~/c/jc-taxes/www/src/GradientEditor.tsx` (372 lines) is interesting but built for **editable color stops + scale type (linear/sqrt/log)** — overkill for elvis Phase 1, where the colormap is a fixed analytic turbo function.
126+
127+
**Decision for Phase 1:** build a small `HeatmapLegend` component locally in `pkgs/core/src/components/`. Don't copy GradientEditor.
128+
129+
**Future (deferred, separate-session work in `~/c/js/`):** if elvis grows multi-colormap support (turbo + viridis + plasma + diverging) AND/OR jc-taxes wants reuse, factor a `$js/use-color-scale` lib. That work belongs in a `~/c/js/use-color-scale/specs/v1.md` written from a session in `~/c/js/`. A stub note here:
130+
131+
```
132+
TODO (separate session, ~/c/js/use-color-scale):
133+
Extract from jc-taxes/GradientEditor.tsx + elvis/HeatmapLegend:
134+
- ColorStop type
135+
- ScaleType (linear/sqrt/log)
136+
- encodeStops/decodeStops for URL state
137+
- interpolateColor
138+
- Optional editor UI (with elvis using read-only mode)
139+
```
140+
141+
## 4. Drawer visual cues (deferred)
142+
143+
Per discussion: drawer is getting busy. Light-touch ideas (for a follow-up spec, not this one):
144+
145+
- Section heading icons (Surface, Heatmap, Display, Tiling, Camera, Slice each get a 16 px icon).
146+
- A subtle accent color per section (matches the icon).
147+
- Hotkey to collapse all sections except the most recently interacted with one.
148+
- Persist collapsed/expanded state in localStorage.
149+
150+
Not in scope for this spec.
151+
152+
## Implementation order
153+
154+
1. **Lift `dMin`/`dMax` calc** out of `HeatmapRenderer` into a util + `useMemo` at App level. Pass as props back into renderer (no rendering change).
155+
2. **`HeatmapLegend` component** in `pkgs/core/src/components/`, consuming `dMin/dMax/gamma/lowCutoff/units`. Wire into `DensityViewer` (canvas overlay, only when heatmap mode is on). Visual-only verification in browser.
156+
3. **`?a=` / `?b=` URL params** + override-aware `loadDiff`. Verify with `?m=mp-2458647&src=diff` (no override) → unchanged behavior, then with explicit `?a=...&b=...` overrides → same render.
157+
4. **Diff sources drawer section** in `Controls.tsx` (visible only when `srcRole === 'diff'`). Two inputs, swap, reset-to-auto. Wire up to URL state.
158+
5. **Title update** for user-overridden case.
159+
160+
Each step is independently committable.
161+
162+
## Open questions
163+
164+
- Legend orientation: vertical (next to gizmo) vs horizontal (above the gizmo, full canvas width)? My instinct: vertical, ~28 px wide × 240 px tall, bottom-right inset. Verify in browser before committing.
165+
- Should the legend show a tick at the **iso level** too (so it's clear how the heatmap relates to the surface threshold)? Probably yes; small overlay marker.
166+
- Diff dim mismatch: with editable URLs, the user might paste two arbitrarily-shaped Zarrs. Current `loadDiff` already errors cleanly on dim mismatch — keep that.

0 commit comments

Comments
 (0)