feat(frontend): add computeNumericSeries helper
This commit is contained in:
@@ -230,3 +230,36 @@ export function extractNumber(str) {
|
|||||||
const m = String(str).match(/-?\d+(\.\d+)?/);
|
const m = String(str).match(/-?\d+(\.\d+)?/);
|
||||||
return m ? parseFloat(m[0]) : null;
|
return m ? parseFloat(m[0]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a numeric-vs-x series for a numerical bucket.
|
||||||
|
*
|
||||||
|
* @param {object[]} rows - Parsed CSV rows
|
||||||
|
* @param {string} xColName - Column to plot on the x-axis (frame col, or timestamp as fallback)
|
||||||
|
* @param {string} noteCol - Column containing note values
|
||||||
|
* @param {object} bucket - { name, type:'numerical', notes: string[] }
|
||||||
|
* @returns {{points: {x:number,value:number,raw:string}[], avg:number|null, skipped:number, total:number, xLabel:string}}
|
||||||
|
*/
|
||||||
|
export function computeNumericSeries(rows, xColName, noteCol, bucket) {
|
||||||
|
const noteSet = new Set(bucket.notes);
|
||||||
|
const points = [];
|
||||||
|
let skipped = 0;
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for (const r of rows) {
|
||||||
|
const note = String(r[noteCol] ?? '').trim();
|
||||||
|
if (!noteSet.has(note)) continue;
|
||||||
|
total++;
|
||||||
|
const value = extractNumber(note);
|
||||||
|
const x = parseFloat(r[xColName]);
|
||||||
|
if (value == null || Number.isNaN(x)) { skipped++; continue; }
|
||||||
|
points.push({ x, value, raw: note });
|
||||||
|
}
|
||||||
|
|
||||||
|
points.sort((a, b) => a.x - b.x);
|
||||||
|
const avg = points.length === 0
|
||||||
|
? null
|
||||||
|
: points.reduce((s, p) => s + p.value, 0) / points.length;
|
||||||
|
|
||||||
|
return { points, avg, skipped, total, xLabel: xColName };
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
getUniqueNoteValues,
|
getUniqueNoteValues,
|
||||||
runAnalysis,
|
runAnalysis,
|
||||||
extractNumber,
|
extractNumber,
|
||||||
|
computeNumericSeries,
|
||||||
} from '../src/lib/csvAnalysis';
|
} from '../src/lib/csvAnalysis';
|
||||||
|
|
||||||
// ── Fixtures ───────────────────────────────────────────────────────────────────
|
// ── Fixtures ───────────────────────────────────────────────────────────────────
|
||||||
@@ -306,3 +307,55 @@ describe('extractNumber', () => {
|
|||||||
expect(extractNumber(undefined)).toBeNull();
|
expect(extractNumber(undefined)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('computeNumericSeries', () => {
|
||||||
|
const rows = [
|
||||||
|
{ frame: '1', timestamp: '0.0', note: 'L25' },
|
||||||
|
{ frame: '3', timestamp: '2.0', note: 'L30.5' },
|
||||||
|
{ frame: '2', timestamp: '1.0', note: 'L40' },
|
||||||
|
{ frame: '4', timestamp: '3.0', note: 'F' },
|
||||||
|
{ frame: '5', timestamp: '4.0', note: 'abc' }, // bucket-member but no number
|
||||||
|
{ frame: 'x', timestamp: '5.0', note: 'L99' }, // bad x
|
||||||
|
{ frame: '6', timestamp: '6.0', note: '' }, // not in bucket
|
||||||
|
];
|
||||||
|
const bucket = { name: 'Lick latency', type: 'numerical', notes: ['L25', 'L30.5', 'L40', 'abc', 'L99'] };
|
||||||
|
|
||||||
|
test('returns sorted points with average and counts', () => {
|
||||||
|
const out = computeNumericSeries(rows, 'frame', 'note', bucket);
|
||||||
|
expect(out.points).toEqual([
|
||||||
|
{ x: 1, value: 25, raw: 'L25' },
|
||||||
|
{ x: 2, value: 40, raw: 'L40' },
|
||||||
|
{ x: 3, value: 30.5, raw: 'L30.5' },
|
||||||
|
]);
|
||||||
|
expect(out.avg).toBeCloseTo((25 + 40 + 30.5) / 3);
|
||||||
|
expect(out.skipped).toBe(2); // 'abc' (no number) + bad x
|
||||||
|
expect(out.total).toBe(5); // bucket-member rows
|
||||||
|
expect(out.xLabel).toBe('frame');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to timestamp x-column when frame is empty string', () => {
|
||||||
|
const out = computeNumericSeries(rows, 'timestamp', 'note', bucket);
|
||||||
|
expect(out.xLabel).toBe('timestamp');
|
||||||
|
expect(out.points[0].x).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns avg=null and empty points when nothing plottable', () => {
|
||||||
|
const onlyBad = [
|
||||||
|
{ frame: '1', note: 'abc' },
|
||||||
|
];
|
||||||
|
const b = { name: 'Z', type: 'numerical', notes: ['abc'] };
|
||||||
|
const out = computeNumericSeries(onlyBad, 'frame', 'note', b);
|
||||||
|
expect(out.points).toEqual([]);
|
||||||
|
expect(out.avg).toBeNull();
|
||||||
|
expect(out.skipped).toBe(1);
|
||||||
|
expect(out.total).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns total=0 when bucket notes match nothing in rows', () => {
|
||||||
|
const b = { name: 'Empty', type: 'numerical', notes: ['nope'] };
|
||||||
|
const out = computeNumericSeries(rows, 'frame', 'note', b);
|
||||||
|
expect(out.total).toBe(0);
|
||||||
|
expect(out.points).toEqual([]);
|
||||||
|
expect(out.avg).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user