feat(frontend): render numerical-bucket plots in CSV-analysis results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 && (
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Numerical Plots</h3>
|
||||
<div className="space-y-4">
|
||||
{results.numericSeries.map(({ bucket, points, avg, skipped, total, xLabel }) => {
|
||||
const colors = getCategoryColors(bucket.name, customCategories);
|
||||
return (
|
||||
<div key={bucket.name} className="border border-gray-200 rounded-lg p-3">
|
||||
<div className="flex items-baseline justify-between mb-2">
|
||||
<p className={`text-sm font-semibold ${colors.text}`}>
|
||||
{bucket.name} <span className="text-xs text-gray-400 font-normal">vs {xLabel}</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{avg != null ? <>Avg: <strong className="text-gray-800">{avg.toFixed(2)}</strong> · </> : null}
|
||||
{points.length}/{total} rows plottable
|
||||
{skipped > 0 && <> · {skipped} skipped</>}
|
||||
</p>
|
||||
</div>
|
||||
{points.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={points} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#eee" />
|
||||
<XAxis dataKey="x" type="number" domain={['dataMin', 'dataMax']}
|
||||
tick={{ fontSize: 10 }} label={{ value: xLabel, position: 'insideBottom', offset: -2, fontSize: 10 }} />
|
||||
<YAxis tick={{ fontSize: 10 }} />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="value" stroke="currentColor" className={colors.text}
|
||||
dot={{ r: 2 }} strokeWidth={2} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<p className="text-xs text-gray-400 italic">
|
||||
No plottable values — {total} of {total} note rows had no extractable number.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Save metrics to daily status */}
|
||||
{dailyStatusId && (
|
||||
<div className="pt-3 border-t border-gray-100">
|
||||
|
||||
Reference in New Issue
Block a user