feat(export): X-coordinate helpers (date, # days reach, daily field)
This commit is contained in:
@@ -56,6 +56,70 @@ export function listExportableParams(dailyTemplate, statuses) {
|
||||
return params;
|
||||
}
|
||||
|
||||
// Numeric value of a daily field on one status (mirrors the metrics plot's helper).
|
||||
export function numericFieldVal(status, field) {
|
||||
if (!field) return null;
|
||||
const raw = field.builtin ? status?.[field.key] : status?.custom_fields?.[field.fieldId];
|
||||
const n = parseFloat(raw);
|
||||
return Number.isNaN(n) ? null : n;
|
||||
}
|
||||
|
||||
// Ordered row members for the chosen X coordinate.
|
||||
// xField: '__date__' | '__days__' | <daily fieldId>
|
||||
export function listSampleMembers(statuses, xField, dailyTemplate) {
|
||||
const list = statuses ?? [];
|
||||
if (xField === '__date__') {
|
||||
const keys = new Set();
|
||||
for (const s of list) if (s?.date) keys.add(dateKey(s.date));
|
||||
return [...keys].sort().map((k) => ({ id: k, label: k, sampleValue: k }));
|
||||
}
|
||||
if (xField === '__days__') {
|
||||
const counts = new Map();
|
||||
for (const s of list) {
|
||||
if (s?.animal_id == null || !s?.date) continue;
|
||||
counts.set(s.animal_id, (counts.get(s.animal_id) ?? 0) + 1);
|
||||
}
|
||||
const max = counts.size ? Math.max(...counts.values()) : 0;
|
||||
return Array.from({ length: max }, (_, i) => ({ id: String(i + 1), label: String(i + 1), sampleValue: i + 1 }));
|
||||
}
|
||||
const field = (dailyTemplate ?? []).find((f) => f.fieldId === xField);
|
||||
const vals = new Set();
|
||||
for (const s of list) {
|
||||
const v = numericFieldVal(s, field);
|
||||
if (v !== null) vals.add(v);
|
||||
}
|
||||
return [...vals].sort((a, b) => a - b).map((v) => ({ id: String(v), label: String(v), sampleValue: v }));
|
||||
}
|
||||
|
||||
// animalId -> Map(sampleValue -> status). First status per (subject, sampleValue) wins,
|
||||
// scanning each subject's statuses in ascending date order.
|
||||
export function buildSampleIndex(statuses, xField, dailyTemplate) {
|
||||
const bySubject = new Map();
|
||||
for (const s of statuses ?? []) {
|
||||
if (s?.animal_id == null || !s?.date) continue;
|
||||
if (!bySubject.has(s.animal_id)) bySubject.set(s.animal_id, []);
|
||||
bySubject.get(s.animal_id).push(s);
|
||||
}
|
||||
const field = xField !== '__date__' && xField !== '__days__'
|
||||
? (dailyTemplate ?? []).find((f) => f.fieldId === xField)
|
||||
: null;
|
||||
|
||||
const index = new Map();
|
||||
for (const [animalId, subjStatuses] of bySubject) {
|
||||
const ordered = [...subjStatuses].sort((a, b) => dateKey(a.date).localeCompare(dateKey(b.date)));
|
||||
const m = new Map();
|
||||
ordered.forEach((s, i) => {
|
||||
let key;
|
||||
if (xField === '__date__') key = dateKey(s.date);
|
||||
else if (xField === '__days__') key = i + 1;
|
||||
else { const v = numericFieldVal(s, field); if (v === null) return; key = v; }
|
||||
if (!m.has(key)) m.set(key, s);
|
||||
});
|
||||
index.set(animalId, m);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
export const EXPORT_DIMENSIONS = [
|
||||
{ dim: 'date', label: 'Date' },
|
||||
{ dim: 'subject', label: 'Subject' },
|
||||
|
||||
@@ -335,3 +335,55 @@ describe('listParameterMembers / listDimensionMembers', () => {
|
||||
expect(listDimensionMembers('parameter', ctx)[0].id).toBe('__total__');
|
||||
});
|
||||
});
|
||||
|
||||
import { numericFieldVal, listSampleMembers, buildSampleIndex } from '../src/lib/dataExport';
|
||||
|
||||
const sDaily = [{ fieldId: 'c-w', key: 'w', label: 'Weight', builtin: false, active: true }];
|
||||
const sStatuses = [
|
||||
{ animal_id: 'a1', date: '2026-07-01T00:00:00Z', custom_fields: { 'c-w': '250' } },
|
||||
{ animal_id: 'a1', date: '2026-07-03T00:00:00Z', custom_fields: { 'c-w': '260' } },
|
||||
{ animal_id: 'a2', date: '2026-07-01T00:00:00Z', custom_fields: { 'c-w': '250' } },
|
||||
];
|
||||
|
||||
describe('numericFieldVal', () => {
|
||||
it('reads builtin via key and custom via fieldId, parsed to number', () => {
|
||||
expect(numericFieldVal({ weight: '250' }, { builtin: true, key: 'weight' })).toBe(250);
|
||||
expect(numericFieldVal({ custom_fields: { 'c-w': '12.5' } }, { builtin: false, fieldId: 'c-w' })).toBe(12.5);
|
||||
});
|
||||
it('returns null for non-numeric, missing, or no field', () => {
|
||||
expect(numericFieldVal({ custom_fields: { 'c-w': 'abc' } }, { builtin: false, fieldId: 'c-w' })).toBeNull();
|
||||
expect(numericFieldVal({}, { builtin: true, key: 'weight' })).toBeNull();
|
||||
expect(numericFieldVal({ weight: 1 }, null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listSampleMembers', () => {
|
||||
it('date: distinct dates sorted', () => {
|
||||
expect(listSampleMembers(sStatuses, '__date__', sDaily).map((m) => m.sampleValue)).toEqual(['2026-07-01', '2026-07-03']);
|
||||
});
|
||||
it('# days reach: 1..max records per subject', () => {
|
||||
expect(listSampleMembers(sStatuses, '__days__', sDaily).map((m) => m.sampleValue)).toEqual([1, 2]);
|
||||
});
|
||||
it('daily field: distinct numeric values sorted ascending', () => {
|
||||
expect(listSampleMembers(sStatuses, 'c-w', sDaily).map((m) => m.sampleValue)).toEqual([250, 260]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildSampleIndex', () => {
|
||||
it('date coord maps animalId -> dateKey -> status', () => {
|
||||
const idx = buildSampleIndex(sStatuses, '__date__', sDaily);
|
||||
expect(idx.get('a1').get('2026-07-03').date).toContain('2026-07-03');
|
||||
});
|
||||
it('# days reach assigns per-subject ordinals in date order', () => {
|
||||
const idx = buildSampleIndex(sStatuses, '__days__', sDaily);
|
||||
expect(idx.get('a1').get(1).date).toContain('2026-07-01');
|
||||
expect(idx.get('a1').get(2).date).toContain('2026-07-03');
|
||||
expect(idx.get('a2').get(1).date).toContain('2026-07-01');
|
||||
expect(idx.get('a2').has(2)).toBe(false);
|
||||
});
|
||||
it('daily field maps numeric value to first status by date', () => {
|
||||
const idx = buildSampleIndex(sStatuses, 'c-w', sDaily);
|
||||
expect(idx.get('a1').get(250).date).toContain('2026-07-01');
|
||||
expect(idx.get('a1').get(260).date).toContain('2026-07-03');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user