# 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: ``** — 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-']`); 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).