feat(export): buildSubjectSeriesMatrix — subjects as columns over an X field

This commit is contained in:
Experiments DB Dev
2026-07-19 14:59:42 -04:00
parent 8510903771
commit 39a5a4ec0c
2 changed files with 94 additions and 0 deletions
+36
View File
@@ -120,6 +120,42 @@ export function buildSampleIndex(statuses, xField, dailyTemplate) {
return index; return index;
} }
function xFieldLabel(xField, dailyTemplate) {
if (xField === '__date__') return 'Date';
if (xField === '__days__') return '# Days Reach';
return (dailyTemplate ?? []).find((f) => f.fieldId === xField)?.label ?? '';
}
// Subjects as columns, X-field values as rows, one Data value per cell.
// Returns the { context, corner, groupAxis, columns, rows } shape toCSV consumes.
export function buildSubjectSeriesMatrix({ xField, dataParam, groupField }, { statuses, animals, dailyTemplate }) {
const columns = listSubjectMembers(animals, groupField);
const rowMembers = listSampleMembers(statuses, xField, dailyTemplate);
const index = buildSampleIndex(statuses, xField, dailyTemplate);
const cellValue = (sampleValue, animalId) => {
const status = index.get(animalId)?.get(sampleValue);
if (!status || !dataParam) return '';
const v = extractValue(status, dataParam);
return hasValue(v) ? v : '';
};
let rows = rowMembers.map((rm) => ({
member: rm,
values: columns.map((c) => cellValue(rm.sampleValue, c.animalId)),
}));
rows = rows.filter((r) => r.values.some((v) => v !== '')); // drop entirely-blank sample rows
const grouped = !!groupField && groupField !== '__none__';
return {
corner: xFieldLabel(xField, dailyTemplate),
context: { label: 'Data', value: dataParam?.label ?? '' },
groupAxis: grouped ? 'col' : null,
columns,
rows,
};
}
export const EXPORT_DIMENSIONS = [ export const EXPORT_DIMENSIONS = [
{ dim: 'date', label: 'Date' }, { dim: 'date', label: 'Date' },
{ dim: 'subject', label: 'Subject' }, { dim: 'subject', label: 'Subject' },
+58
View File
@@ -387,3 +387,61 @@ describe('buildSampleIndex', () => {
expect(idx.get('a1').get(260).date).toContain('2026-07-03'); expect(idx.get('a1').get(260).date).toContain('2026-07-03');
}); });
}); });
import { buildSubjectSeriesMatrix } from '../src/lib/dataExport';
const ssAnimals = [
{ id: 'a1', animal_name: 'Alpha', animal_id_string: 'R-001', subject_info: { grp: 'Control' } },
{ id: 'a2', animal_name: 'Beta', animal_id_string: 'R-002', subject_info: { grp: 'Drug' } },
];
const ssStatuses = [
{ animal_id: 'a1', date: '2026-07-01T00:00:00Z', analysis_summary: { total: 5 } },
{ animal_id: 'a1', date: '2026-07-02T00:00:00Z', analysis_summary: { total: 7 } },
{ animal_id: 'a2', date: '2026-07-01T00:00:00Z', analysis_summary: { total: 9 } },
];
const totalParam = { id: '__total__', label: 'Total attempts', kind: 'total', group: 'metrics' };
describe('buildSubjectSeriesMatrix', () => {
it('date X: subjects as columns, date rows, blank where missing, context names the metric', () => {
const m = buildSubjectSeriesMatrix(
{ xField: '__date__', dataParam: totalParam, groupField: '__none__' },
{ statuses: ssStatuses, animals: ssAnimals, dailyTemplate: [] },
);
expect(m.corner).toBe('Date');
expect(m.context).toEqual({ label: 'Data', value: 'Total attempts' });
expect(m.groupAxis).toBeNull();
expect(m.columns.map((c) => c.label)).toEqual(['Alpha', 'Beta']);
expect(m.rows.map((r) => r.member.label)).toEqual(['2026-07-01', '2026-07-02']);
expect(m.rows[0].values).toEqual([5, 9]);
expect(m.rows[1].values).toEqual([7, '']);
});
it('# days reach X: rows align each subject by day ordinal', () => {
const m = buildSubjectSeriesMatrix(
{ xField: '__days__', dataParam: totalParam, groupField: '__none__' },
{ statuses: ssStatuses, animals: ssAnimals, dailyTemplate: [] },
);
expect(m.corner).toBe('# Days Reach');
expect(m.rows.map((r) => r.member.label)).toEqual(['1', '2']);
expect(m.rows[0].values).toEqual([5, 9]);
expect(m.rows[1].values).toEqual([7, '']);
});
it('grouping sets groupAxis col and clusters subjects', () => {
const m = buildSubjectSeriesMatrix(
{ xField: '__date__', dataParam: totalParam, groupField: 'grp' },
{ statuses: ssStatuses, animals: ssAnimals, dailyTemplate: [] },
);
expect(m.groupAxis).toBe('col');
expect(m.columns.map((c) => [c.group, c.label])).toEqual([['Control', 'Alpha'], ['Drug', 'Beta']]);
});
it('drops entirely-blank rows', () => {
const statuses = [
{ animal_id: 'a1', date: '2026-07-01T00:00:00Z', analysis_summary: { total: 5 } },
{ animal_id: 'a1', date: '2026-07-05T00:00:00Z', analysis_summary: null },
];
const m = buildSubjectSeriesMatrix(
{ xField: '__date__', dataParam: totalParam, groupField: '__none__' },
{ statuses, animals: ssAnimals, dailyTemplate: [] },
);
expect(m.rows.map((r) => r.member.label)).toEqual(['2026-07-01']);
});
});