Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.8 KiB
Experiment Data Export (CSV) — Design
Date: 2026-07-06 Status: Approved, ready for implementation plan
Goal
Let a user on the experiment page export a chosen daily parameter as a CSV
file shaped as a matrix: each row is a date, each column is a subject, each
cell is that subject's value for the parameter on that date. Every daily
parameter must be exportable, including the session metrics derived from
analysis_summary.
Scope
- In: CSV export, one parameter per file, client-side generation, a picker UI on the experiment page.
- Out (deferred): true
.xlsxoutput (would add a client-side library such as SheetJS later; the pure helpers stay format-agnostic so the workbook builder can be added without reshaping the data), multi-parameter / multi-sheet export, a backend export endpoint.
Approach
Fully client-side. The existing GET /api/experiments/:id/daily-statuses
endpoint already returns every field required per status — builtin fields
(experiment_description, vitals, treatment, notes), custom_fields, and
analysis_summary. The experiment page currently fetches only the
analysisOnly=true subset (for charts); the export modal fetches the full
set on open, pivots it into a date×subject matrix in the browser, serializes to
CSV, and triggers a download. No backend change.
Rejected alternative: a backend /export route streaming CSV. It only pays off
for very large datasets or server-side xlsx, at the cost of a new route, tests,
and duplicating the template/parameter logic on the server. Research datasets
here are small (tens of subjects, hundreds of dates), so client-side is simpler
and unit-testable.
Exportable parameters
The picker is grouped into two sections.
Session metrics (from analysis_summary)
- Total attempts →
analysis_summary.total - Success rate →
analysis_summary.success_rate, exported as the raw 0–1 fraction (not multiplied to a percentage) - One entry per dynamic count category found in the data →
analysis_summary.counts[category]. Categories are enumerated by scanning the fetched statuses (e.g.Success,Failure,Other, and any others present).
Daily record fields (from the daily template)
- Every field defined in the daily template, including ones currently hidden/inactive, since historical data may exist for them.
- Builtin fields read from
status[field.key]; custom fields read fromstatus.custom_fields[field.fieldId](same accessor logic asAnalysisCharts.numericFieldVal). - Values export as-is: text fields (e.g.
notes,vitals) as strings, numeric fields as numbers. No numeric coercion is required for export.
Matrix shape
- Rows = every date that has at least one value for the chosen parameter
across any subject, sorted ascending, formatted
YYYY-MM-DD. The first column header isDate. - Columns = all enrolled animals, ordered by
animal_name. A subject is a column even if it has no value for the parameter (blank cells). On duplicateanimal_name, disambiguate the header with the subject'sanimal_id_string(e.g.Rat A (R-001)). - Cell = the subject's value for the parameter on that date. Assumes one daily status per (subject, date); if duplicates are ever present, the first in date-ascending order wins.
- Empty result (no dates have any value) is surfaced in the UI as a disabled export / "no data for this parameter" note rather than producing an empty file.
Components
frontend/src/lib/dataExport.js (new, pure — no React, no I/O)
Mirrors the existing frontend/src/lib/crossSubjectChart.js pure-helper pattern.
listExportableParams(dailyTemplate, statuses)→ grouped, ordered parameter descriptors. Each descriptor carries an id, label, group ('metrics'|'daily'), and enough info to extract a value from a status. Session-metric categories are derived fromstatuses; template params fromdailyTemplate.extractValue(status, param)→ the raw cell value for one status/param (ornull/blank when absent). Encapsulates the builtin-vs-custom andanalysis_summaryaccessor logic.buildMatrix(statuses, animals, param)→{ columns, rows }wherecolumnsare the ordered subject headers androwsare{ date, values: [...] }aligned tocolumns, including blank cells; rows limited to dates with data.toCSV(matrix)→ CSV string. Escapes any field containing a comma, double quote, or newline per RFC 4180 (wrap in quotes, double embedded quotes).
frontend/src/components/ExportDataModal.jsx (new)
- On open, fetches the full daily-statuses set via
experimentsApi.getDailyStatuses(id, {})(noanalysisOnly). - Renders a grouped
<select>of exportable parameters (Session metrics / Daily record fields). - Shows a small summary line: "N dates × M subjects".
- Export CSV button builds a
Blob, creates an object URL, clicks a temporary anchor, and revokes the URL. Filename:<sanitized-experiment-title>-<sanitized-param-label>.csv. - Handles loading and error states for the fetch.
frontend/src/pages/ExperimentDetail.jsx (edit)
- Add an Export data button in the header area (near + Add Animal) that
opens
ExportDataModal, passing the experiment id, title,dailyTemplate, andanimals.
Data flow
- User clicks Export data → modal opens.
- Modal fetches full statuses (all fields, all subjects).
listExportableParams(dailyTemplate, statuses)builds the picker options.- User selects a parameter →
buildMatrix(statuses, animals, param)computes the date×subject matrix; summary line updates. - User clicks Export CSV →
toCSV(matrix)→ Blob download.
Error handling
- Fetch failure: show an inline error in the modal; no download.
- Parameter with no data across all subjects/dates: disable export and show a brief note.
- CSV correctness relies on
toCSVescaping; covered by tests.
Testing
Unit tests for dataExport.js, following frontend/tests/crossSubjectChart.test.js:
listExportableParams: includes all template fields (incl. inactive), the two fixed session metrics, and each dynamic count category discovered in the data; correct grouping/ordering.extractValue: builtin vs custom field access;total,success_rate(raw 0–1), andcounts[category]; missing values → blank/null.buildMatrix: rows only for dates with data, ascending; all subjects as columns with blanks; duplicate-name disambiguation; one-status-per-cell.toCSV: escaping of commas, quotes, and newlines; header row; blank cells.
Out of scope / future
.xlsxoutput via a client-side workbook library (helpers already format-agnostic).- Selecting multiple parameters (multi-sheet xlsx or multi-file/zip CSV).
- Server-side export for very large datasets.