feat(export): subject/parameter members + dimension dispatcher
This commit is contained in:
@@ -102,6 +102,42 @@ export function buildStatusIndex(statuses) {
|
||||
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.
|
||||
// (0 and false are real values.)
|
||||
function hasValue(v) {
|
||||
|
||||
@@ -220,3 +220,49 @@ describe('buildStatusIndex', () => {
|
||||
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__');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user