feat(export): toCSV emits context line + subject group cell
This commit is contained in:
@@ -206,12 +206,35 @@ function escapeCsvCell(value) {
|
||||
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
||||
}
|
||||
|
||||
// Serialize a matrix to an RFC-4180-ish CSV string (LF line endings).
|
||||
// First column is the date; remaining columns follow matrix.columns order.
|
||||
// Serialize a matrix to an RFC-4180-ish CSV string (LF line endings):
|
||||
// <pinned label>,<pinned value>
|
||||
// (blank line)
|
||||
// [Group,<group per subject column>] -- only when groupAxis === 'col'
|
||||
// <corner>[,Group?],<column headers...>
|
||||
// <row label>[,group?],<values...>
|
||||
export function toCSV(matrix) {
|
||||
const header = ['Date', ...matrix.columns.map((c) => c.header)].map(escapeCsvCell).join(',');
|
||||
const lines = matrix.rows.map((r) => [r.date, ...r.values].map(escapeCsvCell).join(','));
|
||||
return [header, ...lines].join('\n');
|
||||
const lines = [];
|
||||
lines.push([matrix.context.label, matrix.context.value].map(escapeCsvCell).join(','));
|
||||
lines.push('');
|
||||
|
||||
if (matrix.groupAxis === 'col') {
|
||||
const groupRow = ['Group', ...matrix.columns.map((c) => c.group ?? '—')];
|
||||
lines.push(groupRow.map(escapeCsvCell).join(','));
|
||||
}
|
||||
|
||||
const header = matrix.groupAxis === 'row'
|
||||
? ['Group', matrix.corner, ...matrix.columns.map((c) => c.label)]
|
||||
: [matrix.corner, ...matrix.columns.map((c) => c.label)];
|
||||
lines.push(header.map(escapeCsvCell).join(','));
|
||||
|
||||
for (const r of matrix.rows) {
|
||||
const lead = matrix.groupAxis === 'row'
|
||||
? [r.member.group ?? '—', r.member.label]
|
||||
: [r.member.label];
|
||||
lines.push([...lead, ...r.values].map(escapeCsvCell).join(','));
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Build a download filename from the experiment title and parameter label.
|
||||
|
||||
@@ -141,20 +141,58 @@ describe('buildMatrix', () => {
|
||||
import { toCSV, csvFilename } from '../src/lib/dataExport';
|
||||
|
||||
describe('toCSV', () => {
|
||||
it('writes a header row of Date + column headers, then data rows', () => {
|
||||
it('emits context line, blank line, header, data (no group)', () => {
|
||||
const matrix = {
|
||||
columns: [{ id: 'a1', header: 'Alpha' }, { id: 'a2', header: 'Beta' }],
|
||||
rows: [{ date: '2026-07-01', values: [5, ''] }, { date: '2026-07-02', values: ['', 9] }],
|
||||
};
|
||||
expect(toCSV(matrix)).toBe('Date,Alpha,Beta\n2026-07-01,5,\n2026-07-02,,9');
|
||||
});
|
||||
it('escapes commas, quotes, and newlines per RFC 4180', () => {
|
||||
const matrix = {
|
||||
columns: [{ id: 'a1', header: 'Note, field' }],
|
||||
rows: [{ date: '2026-07-01', values: ['he said "hi"'] }, { date: '2026-07-02', values: ['line1\nline2'] }],
|
||||
corner: 'Date',
|
||||
context: { label: 'Parameter', value: 'Total attempts' },
|
||||
groupAxis: null,
|
||||
columns: [{ id: 'a1', label: 'Alpha', group: null }, { id: 'a2', label: 'Beta', group: null }],
|
||||
rows: [{ member: { label: '2026-07-01' }, values: [5, ''] }, { member: { label: '2026-07-02' }, values: ['', 9] }],
|
||||
};
|
||||
expect(toCSV(matrix)).toBe(
|
||||
'Date,"Note, field"\n2026-07-01,"he said ""hi"""\n2026-07-02,"line1\nline2"',
|
||||
'Parameter,Total attempts\n\nDate,Alpha,Beta\n2026-07-01,5,\n2026-07-02,,9',
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a Group header row when subjects are columns', () => {
|
||||
const matrix = {
|
||||
corner: 'Date',
|
||||
context: { label: 'Parameter', value: 'Total attempts' },
|
||||
groupAxis: 'col',
|
||||
columns: [{ id: 'a1', label: 'Alpha', group: 'Control' }, { id: 'a2', label: 'Beta', group: 'Drug' }],
|
||||
rows: [{ member: { label: '2026-07-01' }, values: [5, 9] }],
|
||||
};
|
||||
expect(toCSV(matrix)).toBe(
|
||||
'Parameter,Total attempts\n\nGroup,Control,Drug\nDate,Alpha,Beta\n2026-07-01,5,9',
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a leading Group column when subjects are rows', () => {
|
||||
const matrix = {
|
||||
corner: 'Subject',
|
||||
context: { label: 'Date', value: '2026-07-01' },
|
||||
groupAxis: 'row',
|
||||
columns: [{ id: 'p1', label: 'Total attempts', group: 'metrics' }],
|
||||
rows: [
|
||||
{ member: { label: 'Alpha', group: 'Control' }, values: [5] },
|
||||
{ member: { label: 'Beta', group: 'Drug' }, values: [9] },
|
||||
],
|
||||
};
|
||||
expect(toCSV(matrix)).toBe(
|
||||
'Date,2026-07-01\n\nGroup,Subject,Total attempts\nControl,Alpha,5\nDrug,Beta,9',
|
||||
);
|
||||
});
|
||||
|
||||
it('escapes commas, quotes, and newlines per RFC 4180', () => {
|
||||
const matrix = {
|
||||
corner: 'Date',
|
||||
context: { label: 'Parameter', value: 'Note, field' },
|
||||
groupAxis: null,
|
||||
columns: [{ id: 'a1', label: 'he said "hi"', group: null }],
|
||||
rows: [{ member: { label: '2026-07-01' }, values: ['line1\nline2'] }],
|
||||
};
|
||||
expect(toCSV(matrix)).toBe(
|
||||
'Parameter,"Note, field"\n\nDate,"he said ""hi"""\n2026-07-01,"line1\nline2"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user