feat(export): dimension primitives, date members, status index

This commit is contained in:
Experiments DB Dev
2026-07-19 13:16:33 -04:00
parent cd4e213d6f
commit a411d59305
2 changed files with 113 additions and 0 deletions
+48
View File
@@ -54,6 +54,54 @@ export function listExportableParams(dailyTemplate, statuses) {
return params; return params;
} }
export const EXPORT_DIMENSIONS = [
{ dim: 'date', label: 'Date' },
{ dim: 'subject', label: 'Subject' },
{ dim: 'parameter', label: 'Parameter' },
];
export function dimLabel(dim) {
return EXPORT_DIMENSIONS.find((d) => d.dim === dim)?.label ?? '';
}
export function otherDimension(rowDim, colDim) {
return EXPORT_DIMENSIONS.map((d) => d.dim).find((d) => d !== rowDim && d !== colDim) ?? null;
}
// Resolve a subject's group value for a given group field id.
// null -> no grouping (field is falsy or '__none__')
// '—' -> grouping is on but this subject has no value
export function subjectGroupValue(animal, groupField) {
if (!groupField || groupField === '__none__') return null;
if (groupField === '__name__') return animal?.animal_name ?? '—';
if (groupField === '__id__') return animal?.animal_id_string ?? '—';
const v = animal?.subject_info?.[groupField];
return v === null || v === undefined || v === '' ? '—' : v;
}
export function listDateMembers(statuses) {
const keys = new Set();
for (const s of statuses ?? []) {
if (s?.date) keys.add(dateKey(s.date));
}
return [...keys].sort().map((k) => ({ id: k, label: k, dateKey: k }));
}
// dateKey -> Map(animalId -> status); first status per (date, animal) wins,
// scanning in date order (matches the legacy buildMatrix tie-break).
export function buildStatusIndex(statuses) {
const index = new Map();
const ordered = [...(statuses ?? [])].sort((a, b) => dateKey(a?.date).localeCompare(dateKey(b?.date)));
for (const s of ordered) {
if (!s?.date || s.animal_id === null || s.animal_id === undefined) continue;
const dk = dateKey(s.date);
if (!index.has(dk)) index.set(dk, new Map());
const byAnimal = index.get(dk);
if (!byAnimal.has(s.animal_id)) byAnimal.set(s.animal_id, s);
}
return index;
}
// 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) {
+65
View File
@@ -155,3 +155,68 @@ describe('csvFilename', () => {
expect(csvFilename('', '')).toBe('export.csv'); expect(csvFilename('', '')).toBe('export.csv');
}); });
}); });
import {
EXPORT_DIMENSIONS, dimLabel, otherDimension, subjectGroupValue,
listDateMembers, buildStatusIndex,
} from '../src/lib/dataExport';
describe('dimension primitives', () => {
it('exposes the three dimensions with labels', () => {
expect(EXPORT_DIMENSIONS.map((d) => d.dim)).toEqual(['date', 'subject', 'parameter']);
expect(dimLabel('date')).toBe('Date');
expect(dimLabel('subject')).toBe('Subject');
expect(dimLabel('parameter')).toBe('Parameter');
});
it('otherDimension returns the leftover', () => {
expect(otherDimension('date', 'subject')).toBe('parameter');
expect(otherDimension('parameter', 'date')).toBe('subject');
});
});
describe('subjectGroupValue', () => {
const animal = { animal_name: 'Alpha', animal_id_string: 'R-001', subject_info: { sex: 'M', cohort: '' } };
it('returns null when no group field', () => {
expect(subjectGroupValue(animal, '__none__')).toBeNull();
expect(subjectGroupValue(animal, '')).toBeNull();
});
it('resolves name / id / subject_info fields', () => {
expect(subjectGroupValue(animal, '__name__')).toBe('Alpha');
expect(subjectGroupValue(animal, '__id__')).toBe('R-001');
expect(subjectGroupValue(animal, 'sex')).toBe('M');
});
it('renders missing or empty values as an em dash', () => {
expect(subjectGroupValue(animal, 'cohort')).toBe('—');
expect(subjectGroupValue({}, 'sex')).toBe('—');
});
});
describe('listDateMembers', () => {
it('returns sorted unique dateKeys with id/label', () => {
const statuses = [
{ date: '2026-07-02T00:00:00.000Z' },
{ date: '2026-07-01T12:00:00.000Z' },
{ date: '2026-07-02T09:00:00.000Z' },
];
expect(listDateMembers(statuses)).toEqual([
{ id: '2026-07-01', label: '2026-07-01', dateKey: '2026-07-01' },
{ id: '2026-07-02', label: '2026-07-02', dateKey: '2026-07-02' },
]);
});
it('tolerates empty input', () => {
expect(listDateMembers(undefined)).toEqual([]);
});
});
describe('buildStatusIndex', () => {
it('indexes by dateKey then animalId, first status wins', () => {
const statuses = [
{ animal_id: 'a1', date: '2026-07-01T00:00:00.000Z', analysis_summary: { total: 7 } },
{ animal_id: 'a1', date: '2026-07-01T06:00:00.000Z', analysis_summary: { total: 99 } },
{ animal_id: 'a2', date: '2026-07-01T00:00:00.000Z', analysis_summary: { total: 3 } },
];
const idx = buildStatusIndex(statuses);
expect(idx.get('2026-07-01').get('a1').analysis_summary.total).toBe(7);
expect(idx.get('2026-07-01').get('a2').analysis_summary.total).toBe(3);
});
});