feat(export): subject/parameter members + dimension dispatcher

This commit is contained in:
Experiments DB Dev
2026-07-19 13:19:15 -04:00
parent a411d59305
commit 7d112de0d7
2 changed files with 82 additions and 0 deletions
+36
View File
@@ -102,6 +102,42 @@ export function buildStatusIndex(statuses) {
return index; return index;
} }
export function listSubjectMembers(animals, groupField) {
const list = [...(animals ?? [])];
const nameCounts = new Map();
for (const a of list) {
const n = a?.animal_name ?? '';
nameCounts.set(n, (nameCounts.get(n) ?? 0) + 1);
}
const members = list.map((a) => {
const name = a?.animal_name ?? '';
const label = nameCounts.get(name) > 1 ? `${name} (${a?.animal_id_string ?? ''})` : name;
return { id: a.id, label, animalId: a.id, group: subjectGroupValue(a, groupField) };
});
const grouped = !!groupField && groupField !== '__none__';
members.sort((x, y) => {
if (grouped) {
const g = String(x.group ?? '').localeCompare(String(y.group ?? ''));
if (g !== 0) return g;
}
return String(x.label).localeCompare(String(y.label));
});
return members;
}
export function listParameterMembers(dailyTemplate, statuses) {
return listExportableParams(dailyTemplate, statuses).map((p) => ({
id: p.id, label: p.label, param: p, group: p.group,
}));
}
export function listDimensionMembers(dim, { statuses, animals, dailyTemplate, groupField }) {
if (dim === 'date') return listDateMembers(statuses);
if (dim === 'subject') return listSubjectMembers(animals, groupField);
if (dim === 'parameter') return listParameterMembers(dailyTemplate, statuses);
return [];
}
// A cell has a value unless it is null, undefined, or the empty string. // A cell has a value unless it is null, undefined, or the empty string.
// (0 and false are real values.) // (0 and false are real values.)
function hasValue(v) { function hasValue(v) {
+46
View File
@@ -220,3 +220,49 @@ describe('buildStatusIndex', () => {
expect(idx.get('2026-07-01').get('a2').analysis_summary.total).toBe(3); expect(idx.get('2026-07-01').get('a2').analysis_summary.total).toBe(3);
}); });
}); });
import { listSubjectMembers, listParameterMembers, listDimensionMembers } from '../src/lib/dataExport';
describe('listSubjectMembers', () => {
const animals = [
{ id: 'a2', animal_name: 'Beta', animal_id_string: 'R-002', subject_info: { grp: 'Drug' } },
{ id: 'a1', animal_name: 'Alpha', animal_id_string: 'R-001', subject_info: { grp: 'Control' } },
{ id: 'a3', animal_name: 'Gamma', animal_id_string: 'R-003', subject_info: { grp: 'Control' } },
];
it('orders by name and carries group=null when no group field', () => {
const m = listSubjectMembers(animals, '__none__');
expect(m.map((s) => s.label)).toEqual(['Alpha', 'Beta', 'Gamma']);
expect(m.every((s) => s.group === null)).toBe(true);
expect(m[0]).toMatchObject({ id: 'a1', animalId: 'a1' });
});
it('clusters by group value then name when grouping is on', () => {
const m = listSubjectMembers(animals, 'grp');
expect(m.map((s) => [s.group, s.label])).toEqual([
['Control', 'Alpha'], ['Control', 'Gamma'], ['Drug', 'Beta'],
]);
});
it('disambiguates duplicate names with the id string', () => {
const dup = [
{ id: 'a1', animal_name: 'Rat', animal_id_string: 'R-001' },
{ id: 'a2', animal_name: 'Rat', animal_id_string: 'R-002' },
];
expect(listSubjectMembers(dup, '__none__').map((s) => s.label)).toEqual(['Rat (R-001)', 'Rat (R-002)']);
});
});
describe('listParameterMembers / listDimensionMembers', () => {
const dailyTemplate = [{ fieldId: 'b-vitals', key: 'vitals', label: 'Vitals', builtin: true, active: true }];
const statuses = [{ date: '2026-07-01T00:00:00.000Z', animal_id: 'a1', analysis_summary: { total: 1 } }];
const animals = [{ id: 'a1', animal_name: 'Alpha', animal_id_string: 'R-001' }];
it('wraps params with id/label/param/group', () => {
const m = listParameterMembers(dailyTemplate, statuses);
expect(m[0]).toMatchObject({ id: '__total__', label: 'Total attempts', group: 'metrics' });
expect(m[0].param).toMatchObject({ kind: 'total' });
});
it('dispatches by dimension', () => {
const ctx = { statuses, animals, dailyTemplate, groupField: '__none__' };
expect(listDimensionMembers('date', ctx).map((d) => d.id)).toEqual(['2026-07-01']);
expect(listDimensionMembers('subject', ctx).map((d) => d.id)).toEqual(['a1']);
expect(listDimensionMembers('parameter', ctx)[0].id).toBe('__total__');
});
});