Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.1 KiB
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:
- Choose which dimension goes on rows and which on columns (fully assignable axes).
- See the pinned/third dimension labeled at the top of the file ("group information at the beginning").
- 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-DDkeys present in the data. - Subject → animals, headers disambiguated by
animal_id_stringon 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 attemptsorSubject,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, orParameter) — no longer hardcodedDate. - 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 existingescapeCsvCell.
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).buildMatrixfor 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.
toCSVsnapshot 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).