Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.5 KiB
Subject-Series Data Export — Design
Date: 2026-07-19
Status: Approved (design)
Area: frontend/src/lib/dataExport.js, frontend/src/components/ExportDataModal.jsx, frontend/tests/
Supersedes: the assignable-axes UI from 2026-07-19-configurable-data-export-design.md (that branch's buildMatrix/toCSV/subject-group work is reused; its rows/columns/pinned modal is replaced).
Problem
The export modal currently offers assignable row/column/pinned axes. That is more than needed. The user wants the export to mirror the cross-subject metrics plot: pick an X field and a data value, with each subject as a column series, empty where a subject has no data point, plus each subject's group included. The one capability missing today is choosing the X (row) field — it is fixed to calendar Date, whereas the plot's X-axis offers # Days Reach and daily fields.
Layout (fixed)
- Columns = subjects, one series per subject, ordered clustered-by-group then by name (existing
listSubjectMembers). - Rows = the chosen X field's values.
- Cells = the chosen Data value for that (subject, X); empty (
'') when absent.0and''remain real values (existinghasValue/extractValue). - Group row gives each subject's group value (existing group-row support).
There is no pinned dimension and no assignable columns.
The three pickers
- X (rows). Options:
Date,# Days Reach, and each active daily-template field. DefaultDate(keeps the same date-rows × subject-columns grid as today; only the line-1 label changes toData, see CSV output). - Data (cells). Options:
listExportableParams(dailyTemplate, statuses)— total, success rate, each count category, each daily field. Default: first parameter (Total attempts). - Group by. Options:
— None —,Name,Subject ID, and each activesubject_infofield. Defaults to the experiment's saved group field (localStorage['exp-subject-group-<id>']), validated against the available options and coerced to__none__when unset or stale. Always visible (subjects are always the columns).
Preview line: N rows × M subjects. Export disabled when there are no data rows.
X-field semantics
Each X coordinate maps a (subject, X-value) pair to a single daily status, from which the Data value is read:
Date→ rows are the distinct calendar dates present (YYYY-MM-DD), sorted. Cell reads the subject's status on that date. (Today's behavior.)# Days Reach→ rows are1, 2, 3, …up to the maximum, per subject, of the count of that subject's recorded daily statuses. A cell for(subject, N)reads the subject's Nth recorded day, counting all recorded statuses sorted ascending by date (Day 1 = earliest record; days without an analysis summary are still counted — this intentionally differs from the plot, which counts analyzed sessions only).- daily field (e.g.
Weight (g)) → rows are the distinct numeric values that field takes across the data (numericFieldVal: builtin viastatus[field.key], custom viastatus.custom_fields[field.fieldId],parseFloat, non-numeric → excluded), sorted ascending. A cell for(subject, v)reads that subject's status where the field equalsv; on ties, first status by date wins.
Row members that are entirely blank across all subjects are dropped (matching Date's existing behavior); subject columns are always kept even when empty.
CSV output
Structure (X = # Days Reach, Data = Success rate, Group by = Treatment):
Data,Success rate
Group,Control,Control,Drug
# Days Reach,Mouse-A,Mouse-B,Mouse-C
1,0.5,0.6,0.7
2,0.55,,0.72
3,0.6,0.65,
- Line 1 names the Data metric so the file is self-describing: two cells,
Dataand the metric label. - Blank line.
- Group row (
Group+ each subject's group), only when Group by ≠ None; missing group value →—. - Header row: the X-field label in the corner, then subject headers.
- Data rows: the X value, then one cell per subject.
LF line endings; every cell escaped via existing escapeCsvCell. Filename: existing csvFilename(title, dataLabel).
Implementation notes
Reuse from the current branch: extractValue, listExportableParams, subjectGroupValue, listSubjectMembers, escapeCsvCell, toCSV (its { context, corner, groupAxis, columns, rows } shape is kept), and csvFilename.
Replace: the general assignable-axes buildMatrix(config, ctx) and its members/dispatch helpers (otherDimension, dimLabel, EXPORT_DIMENSIONS, listDateMembers, listDimensionMembers, listParameterMembers, buildStatusIndex in its current form) — remove what the fixed layout no longer uses rather than leaving dead code.
Add:
numericFieldVal(status, field)— mirror of the plot's helper (or import/share it).listSampleMembers(statuses, xField, dailyTemplate)→ the ordered row members for the chosen X coordinate ({ id, label, ... }).buildSampleIndex(statuses, xField, dailyTemplate)→Map(animalId → Map(sampleValue → status)), first-status-per-cell wins, encoding the date/#days/field resolution above.buildSubjectSeriesMatrix({ xField, dataParam, groupField }, { statuses, animals, dailyTemplate })→{ context: { label: 'Data', value: dataParam.label }, corner: <X label>, groupAxis: grouping ? 'col' : null, columns: <subjects>, rows: [{ member, values }] }, consumable by the existingtoCSV.
Simplify ExportDataModal.jsx to the three pickers over these helpers; drop the Rows/Columns/Fixed controls.
Testing
listSampleMembers/buildSampleIndexfor each X coordinate: Date (unchanged), # Days Reach (all-records ordinal, per-subject alignment), daily field (distinct numeric values sorted, non-numeric dropped, tie first-by-date).buildSubjectSeriesMatrix: subjects as columns clustered by group; empty cells where a subject lacks a point; blank row dropped;groupAxisnull when Group by None.toCSVreused; add/keep a snapshot for the new context line + group row.ExportDataModal: renders three pickers; default X = Date produces date rows × subject columns with the chosen metric in cells; switching X to # Days Reach changes rows to ordinals; group row present when a valid group field is chosen; stale/unset group field coerces to None.
Out of scope (YAGNI)
- Assignable columns / pinned dimension (removed).
- Choosing a subset of subjects (all subjects always included).
- Matching the plot's analyzed-only day counting (we count all recorded days).
- A footer or repeated context line.