900759074d
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
6.1 KiB
Markdown
102 lines
6.1 KiB
Markdown
# Configurable Data Export — Design
|
||
|
||
**Date:** 2026-07-19
|
||
**Status:** Approved (design)
|
||
**Area:** `frontend/src/lib/dataExport.js`, `frontend/src/components/ExportDataModal.jsx`, `frontend/tests/dataExport.test.js`
|
||
|
||
## Problem
|
||
|
||
Today's data export has a fixed layout: each **row is a date**, each **column is a subject**, and the user picks **one parameter** whose value fills every cell (`buildMatrix` in `dataExport.js`). Users want to:
|
||
|
||
1. Choose which dimension goes on rows and which on columns (fully assignable axes).
|
||
2. See the pinned/third dimension labeled at the top of the file ("group information at the beginning").
|
||
3. When Subject is an axis, see each subject's **group** in a separate cell, similar to the cross-subject metrics view.
|
||
|
||
## Model: three interchangeable dimensions
|
||
|
||
Export data spans three dimensions: **Date**, **Subject**, **Parameter**. A 2-D CSV grid can place two of them on the axes; the third must be **pinned** to a single value (a cell holds exactly one value).
|
||
|
||
Each dimension yields an ordered list of **members**:
|
||
|
||
- **Date** → sorted unique `YYYY-MM-DD` keys present in the data.
|
||
- **Subject** → animals, headers disambiguated by `animal_id_string` on duplicate names (existing logic). When a group field is chosen, subjects are **clustered by group value, then sorted by name** within each group.
|
||
- **Parameter** → `listExportableParams(dailyTemplate, statuses)` (session metrics, then daily fields — unchanged).
|
||
|
||
Every cell is identified by a full `(dateKey, animalId, param)` triple: two coordinates come from the row/column members, the third from the pinned value. Lookup goes through a status index `Map(dateKey → Map(animalId → status))` (first-status-per-cell wins, as today), then `extractValue(status, param)`.
|
||
|
||
Because Subject is always exactly one of the three dimensions (row, column, or pinned), the group annotation attaches to **at most one axis** — there is no 2-D corner-block complexity.
|
||
|
||
## Modal UI (`ExportDataModal.jsx`)
|
||
|
||
Replaces today's single "Parameter" dropdown with four controls:
|
||
|
||
- **Rows** — Date / Subject / Parameter.
|
||
- **Columns** — the two dimensions not chosen for Rows (auto-excludes the row pick; if a change collides, the other axis auto-shifts to a still-valid dimension).
|
||
- **Fixed: `<leftover dim>`** — value dropdown for the auto-pinned third dimension. Its options are that dimension's members (a date, a subject, or a parameter, grouped like today for parameters).
|
||
- **Group by** — subject_info fields plus "— None —". **Shown only when Subject is a row or column axis.** Defaults to the experiment's saved group field (`localStorage['exp-subject-group-<id>']`); falls back to None when unset or the field no longer exists.
|
||
|
||
Live preview line stays: `N rows × M columns` (or "No data …" when empty). **Default state reproduces today's export exactly**: Rows = Date, Columns = Subject, Fixed = Parameter (first parameter). Export button disabled when the grid has no data rows.
|
||
|
||
## CSV output format
|
||
|
||
Structure: pinned-dimension context line, blank line, optional group row, header row, data rows.
|
||
|
||
- **Line 1 (context):** two cells — the pinned dimension's label and its value, e.g. `Parameter,Total attempts` or `Subject,Mouse-01`. Beginning only (no footer).
|
||
- **Blank line.**
|
||
- **Grid**, with the group cell riding on whichever axis holds Subject.
|
||
|
||
**Subject = columns** (e.g. Rows = Date, Fixed = Parameter) → extra **header row** labeled `Group`:
|
||
|
||
```
|
||
Parameter,Total attempts
|
||
|
||
Group,Control,Control,Drug,Drug
|
||
Date,Mouse-01,Mouse-02,Mouse-03,Mouse-04
|
||
2026-01-01,12,10,8,9
|
||
```
|
||
|
||
**Subject = rows** (e.g. Cols = Parameter, Fixed = Date) → extra **leading column** labeled `Group`:
|
||
|
||
```
|
||
Date,2026-01-01
|
||
|
||
Group,Subject,Total attempts,Success rate
|
||
Control,Mouse-01,12,0.83
|
||
Drug,Mouse-03,8,0.71
|
||
```
|
||
|
||
Rules:
|
||
|
||
- The grid **corner label** is the row dimension's name (`Date`, `Subject`, or `Parameter`) — no longer hardcoded `Date`.
|
||
- Missing group value → `—`. When **Group by = None**, the group row/column is omitted entirely.
|
||
- **Empty-member rule:** Subject members are **always kept** (matches today's "keep all subject columns"). Date and Parameter members are **dropped when entirely blank** in the current grid.
|
||
- Cell values use existing semantics: `0` / `''` are real; genuinely-missing lookups render as blank. CSV escaping via existing `escapeCsvCell`.
|
||
|
||
## Refactor of `dataExport.js`
|
||
|
||
Keep `extractValue` and `listExportableParams` unchanged. Replace single-purpose `buildMatrix` with dimension-agnostic pieces:
|
||
|
||
- `listDateMembers(statuses)` → sorted unique dateKeys.
|
||
- `listSubjectMembers(animals, groupField)` → `[{ id, header, group }]`, clustered by group then name; header disambiguation preserved.
|
||
- `buildStatusIndex(statuses)` → `Map(dateKey → Map(animalId → status))`, first-wins.
|
||
- `buildMatrix({ rowDim, colDim, pinnedDim, pinnedMember, groupField }, { statuses, animals, dailyTemplate })` → `{ context, groupAxis, corner, columns, rows }`. Resolves each cell's `(dateKey, animalId, param)` triple through the index + `extractValue`; applies the keep-subjects / drop-blank-date-and-param rule.
|
||
- `toCSV(matrix)` → emits context line, blank line, optional group row, header row, data rows, with the dynamic corner label.
|
||
- `csvFilename(title, pinnedMember.label)` → signature unchanged.
|
||
|
||
## Testing (`tests/dataExport.test.js`)
|
||
|
||
Existing `extractValue` / `listExportableParams` tests stay green. Add:
|
||
|
||
- `listDateMembers`, `listSubjectMembers` (including group clustering + name tie-break + header disambiguation), `buildStatusIndex` (first-wins).
|
||
- `buildMatrix` for all three axis pairings in both orientations.
|
||
- Group row (Subject = columns) vs. group column (Subject = rows) placement; `—` fallback; Group = None omits it.
|
||
- Empty-member rule: blank date/parameter members dropped, subjects retained.
|
||
- `toCSV` snapshot including the context line and a group row.
|
||
|
||
## Out of scope (YAGNI)
|
||
|
||
- Stacking multiple values of the pinned dimension into one file (chose "pick one value").
|
||
- Footer/repeat of the context line (beginning only).
|
||
- Experiment-level metadata block (pinned dimension only).
|
||
- Persisting axis choices across sessions (default reproduces current behavior; group field reuses existing saved value only).
|