feat(export): generalized buildMatrix over assignable dimensions
This commit is contained in:
@@ -148,53 +148,57 @@ function dateKey(date) {
|
||||
return String(date).slice(0, 10); // ISO datetime or date → YYYY-MM-DD
|
||||
}
|
||||
|
||||
// Pivot statuses into { columns, rows }.
|
||||
// columns: [{ id, header }] — all animals, ordered by name, headers
|
||||
// disambiguated with animal_id_string on duplicate names.
|
||||
// rows: [{ date, values }] — one per date that has ≥1 value; values are
|
||||
// aligned to columns, blank cells are '' (empty string).
|
||||
export function buildMatrix(statuses, animals, param) {
|
||||
const sortedAnimals = [...(animals ?? [])].sort((a, b) =>
|
||||
String(a.animal_name ?? '').localeCompare(String(b.animal_name ?? '')),
|
||||
);
|
||||
// Pivot statuses into a 2-D matrix per the chosen row/column dimensions.
|
||||
// The leftover (pinned) dimension is fixed to pinnedMember. Every cell resolves
|
||||
// a (dateKey, animalId, param) triple through the status index.
|
||||
export function buildMatrix(config, ctx) {
|
||||
const { rowDim, colDim, pinnedMember, groupField } = config;
|
||||
const pinnedDim = otherDimension(rowDim, colDim);
|
||||
const index = buildStatusIndex(ctx.statuses);
|
||||
const full = { ...ctx, groupField };
|
||||
const rowMembers = listDimensionMembers(rowDim, full);
|
||||
const colMembers = listDimensionMembers(colDim, full);
|
||||
|
||||
const nameCounts = new Map();
|
||||
for (const a of sortedAnimals) {
|
||||
const n = a.animal_name ?? '';
|
||||
nameCounts.set(n, (nameCounts.get(n) ?? 0) + 1);
|
||||
}
|
||||
const columns = sortedAnimals.map((a) => {
|
||||
const name = a.animal_name ?? '';
|
||||
const header = nameCounts.get(name) > 1 ? `${name} (${a.animal_id_string ?? ''})` : name;
|
||||
return { id: a.id, header };
|
||||
});
|
||||
const colIndex = new Map(columns.map((c, i) => [c.id, i]));
|
||||
const cellValue = (rowM, colM) => {
|
||||
const byDim = { [rowDim]: rowM, [colDim]: colM, [pinnedDim]: pinnedMember };
|
||||
const dk = byDim.date?.dateKey ?? null;
|
||||
const animalId = byDim.subject?.animalId ?? null;
|
||||
const param = byDim.parameter?.param ?? null;
|
||||
if (dk === null || animalId === null || animalId === undefined || !param) return '';
|
||||
const status = index.get(dk)?.get(animalId);
|
||||
if (!status) return '';
|
||||
const v = extractValue(status, param);
|
||||
return hasValue(v) ? v : '';
|
||||
};
|
||||
|
||||
// cells: dateKey -> Map(animalId -> value); first status per (date, animal) wins.
|
||||
const cells = new Map();
|
||||
const datesWithData = new Set();
|
||||
const ordered = [...(statuses ?? [])].sort((a, b) => dateKey(a.date).localeCompare(dateKey(b.date)));
|
||||
for (const s of ordered) {
|
||||
if (!colIndex.has(s.animal_id)) continue;
|
||||
const dk = dateKey(s.date);
|
||||
if (!cells.has(dk)) cells.set(dk, new Map());
|
||||
const byAnimal = cells.get(dk);
|
||||
if (byAnimal.has(s.animal_id)) continue; // first wins
|
||||
const v = extractValue(s, param);
|
||||
byAnimal.set(s.animal_id, v);
|
||||
if (hasValue(v)) datesWithData.add(dk);
|
||||
}
|
||||
let rows = rowMembers.map((rowM) => ({
|
||||
member: rowM,
|
||||
values: colMembers.map((colM) => cellValue(rowM, colM)),
|
||||
}));
|
||||
|
||||
const rows = [...datesWithData].sort().map((dk) => {
|
||||
const byAnimal = cells.get(dk);
|
||||
const values = columns.map((c) => {
|
||||
const v = byAnimal.has(c.id) ? byAnimal.get(c.id) : null;
|
||||
return hasValue(v) ? v : '';
|
||||
});
|
||||
return { date: dk, values };
|
||||
});
|
||||
// Empty-member rule: keep all subjects; drop entirely-blank date/parameter rows & columns.
|
||||
if (rowDim !== 'subject') rows = rows.filter((r) => r.values.some((v) => v !== ''));
|
||||
|
||||
return { columns, rows };
|
||||
let keptCols = colMembers.map((_, i) => i);
|
||||
if (colDim !== 'subject') keptCols = keptCols.filter((i) => rows.some((r) => r.values[i] !== ''));
|
||||
const columns = keptCols.map((i) => colMembers[i]);
|
||||
rows = rows.map((r) => ({ member: r.member, values: keptCols.map((i) => r.values[i]) }));
|
||||
|
||||
const grouped = !!groupField && groupField !== '__none__';
|
||||
const groupAxis = grouped
|
||||
? (rowDim === 'subject' ? 'row' : colDim === 'subject' ? 'col' : null)
|
||||
: null;
|
||||
|
||||
return {
|
||||
rowDim,
|
||||
colDim,
|
||||
pinnedDim,
|
||||
corner: dimLabel(rowDim),
|
||||
context: { label: dimLabel(pinnedDim), value: pinnedMember?.label ?? '' },
|
||||
groupAxis,
|
||||
columns,
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
function escapeCsvCell(value) {
|
||||
|
||||
Reference in New Issue
Block a user