diff --git a/frontend/src/components/CsvAnalysis.jsx b/frontend/src/components/CsvAnalysis.jsx index e92ace5..6731fbf 100644 --- a/frontend/src/components/CsvAnalysis.jsx +++ b/frontend/src/components/CsvAnalysis.jsx @@ -1,5 +1,8 @@ import React, { useState, useRef, useCallback } from 'react'; -import { parseCSVHeaders, parseCSV, getUniqueNoteValues, runAnalysis, checkFrameConsecutive } from '../lib/csvAnalysis'; +import { parseCSVHeaders, parseCSV, getUniqueNoteValues, runAnalysis, checkFrameConsecutive, computeNumericSeries } from '../lib/csvAnalysis'; +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, +} from 'recharts'; import { analysesApi, dailyStatusesApi } from '../api/client'; import Button from './ui/Button'; import RunSequenceView from './RunSequenceView'; @@ -246,8 +249,20 @@ export default function CsvAnalysis({ dailyStatusId, animal, onSaved, onSummaryP // ── Run analysis ───────────────────────────────────────────────────────────── async function compute() { - const res = runAnalysis(rows, timestampCol, noteCol, classifications); - setResults(res); + // Note-only classifications so numerical buckets don't pollute counts/runs + const noteClassifications = Object.fromEntries( + noteBuckets.flatMap((b) => b.notes.map((n) => [n, b.name])) + ); + const res = runAnalysis(rows, timestampCol, noteCol, noteClassifications); + + // Numerical series — one per numerical bucket. xCol = frame if set, else timestamp. + const xCol = frameCol || timestampCol; + const numericSeries = numericalBuckets.map((b) => ({ + bucket: b, + ...computeNumericSeries(rows, xCol, noteCol, b), + })); + + setResults({ ...res, numericSeries }); setPhase('results'); if (!dailyStatusId) return; @@ -258,7 +273,7 @@ export default function CsvAnalysis({ dailyStatusId, animal, onSaved, onSummaryP file_name: fileName || null, timestamp_col: timestampCol, note_col: noteCol, - classifications, + classifications: noteClassifications, total_note_rows: res.totalNoteRows, session_end_ts: res.sessionEndTs, sequence: res.sequence, @@ -643,6 +658,49 @@ export default function CsvAnalysis({ dailyStatusId, animal, onSaved, onSummaryP sessionEndTs={results.sessionEndTs} /> + {/* Numerical plots — one panel per numerical bucket */} + {results.numericSeries && results.numericSeries.length > 0 && ( +
+

Numerical Plots

+
+ {results.numericSeries.map(({ bucket, points, avg, skipped, total, xLabel }) => { + const colors = getCategoryColors(bucket.name, customCategories); + return ( +
+
+

+ {bucket.name} vs {xLabel} +

+

+ {avg != null ? <>Avg: {avg.toFixed(2)} · : null} + {points.length}/{total} rows plottable + {skipped > 0 && <> · {skipped} skipped} +

+
+ {points.length > 0 ? ( + + + + + + + + + + ) : ( +

+ No plottable values — {total} of {total} note rows had no extractable number. +

+ )} +
+ ); + })} +
+
+ )} + {/* Save metrics to daily status */} {dailyStatusId && (