From b888c1b76de224172e4bcd1a1197815df6740f57 Mon Sep 17 00:00:00 2001 From: Experiments DB Dev Date: Thu, 23 Apr 2026 12:24:16 -0400 Subject: [PATCH] fix: day modal bar chart plots that session's field values per animal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-axis: animal names, Y-axis: the selected field's numeric value recorded that specific day. Animals with empty or non-numeric values are excluded. Each day's chart is independent — different animals, different values. Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/ExperimentCalendar.jsx | 38 ++++++++----------- frontend/tests/ExperimentCalendar.test.jsx | 23 ++++++++--- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/frontend/src/components/ExperimentCalendar.jsx b/frontend/src/components/ExperimentCalendar.jsx index 8352e35..2d53ae7 100644 --- a/frontend/src/components/ExperimentCalendar.jsx +++ b/frontend/src/components/ExperimentCalendar.jsx @@ -53,29 +53,25 @@ function DayPanel({ date, animals, allStatuses, template, experimentId, selected const fieldLabel = fieldOptions.find((f) => f.value === selectedField)?.label; - // Bar chart: session stats across ALL days, for animals with at least one valid field value + // Bar chart: this day's field value per animal (numeric only, filtered to animals present that day) const barData = useMemo(() => { if (!selectedField) return []; const isBuiltin = BUILTIN_KEYS_SET.has(selectedField); - return animals + return animalsWithData .map((animal) => { - const statuses = allStatuses.filter((s) => s.animal_id === animal.id); - const total = statuses.length; - if (total === 0) return null; - const success = statuses.filter((s) => { - const val = isBuiltin ? s[selectedField] : s.custom_fields?.[selectedField]; - return val !== null && val !== undefined && String(val).trim() !== ''; - }).length; - if (success === 0) return null; // skip animals with no valid field data + const status = statusByAnimal[animal.id]; + if (!status) return null; + const raw = isBuiltin ? status[selectedField] : status.custom_fields?.[selectedField]; + if (raw === null || raw === undefined || String(raw).trim() === '') return null; + const num = parseFloat(raw); + if (isNaN(num)) return null; return { name: animal.animal_name || animal.animal_id_string || animal.id.slice(0, 8), - total, - success, - failure: total - success, + value: num, }; }) .filter(Boolean); - }, [animals, allStatuses, selectedField]); + }, [animalsWithData, statusByAnimal, selectedField]); return (
@@ -124,20 +120,16 @@ function DayPanel({ date, animals, allStatuses, template, experimentId, selected {/* Bar chart: overall per-subject completeness */} {barData.length > 0 && (
-

- Session statistics — {fieldLabel ?? 'selected field'} +

+ {fieldLabel ?? 'Field'} — this session

-

All sessions across the experiment

- - - - - - + + [v, fieldLabel ?? 'Value']} /> +
diff --git a/frontend/tests/ExperimentCalendar.test.jsx b/frontend/tests/ExperimentCalendar.test.jsx index a17dfb9..7aefa27 100644 --- a/frontend/tests/ExperimentCalendar.test.jsx +++ b/frontend/tests/ExperimentCalendar.test.jsx @@ -248,7 +248,7 @@ describe('day click modal', () => { // ── Bar chart (inside day modal) ────────────────────────────────────────────── describe('bar chart inside day modal', () => { - it('renders session stats bar chart when animals have valid field data', async () => { + it('renders bar chart with that day\'s numeric field values', async () => { const dateStr = todayStr(); const statuses = [ makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }), @@ -258,14 +258,13 @@ describe('bar chart inside day modal', () => { ); fireEvent.click(screen.getByTestId(`day-${dateStr}`)); await waitFor(() => { - const dialog = screen.getByRole('dialog'); - expect(within(dialog).getByTestId('bar-chart')).toBeInTheDocument(); + expect(within(screen.getByRole('dialog')).getByTestId('bar-chart')).toBeInTheDocument(); }); }); - it('excludes animals with no valid field data from bar chart', async () => { + it('omits animals with empty or non-numeric field value from bar chart', async () => { const dateStr = todayStr(); - // Rat A has weight data; Rat B has only empty custom_fields — should be excluded from chart + // Rat A has numeric weight; Rat B has empty weight — Rat B excluded from chart but shown in day list const statuses = [ makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }), makeStatus('a2000000-0000-0000-0000-000000000002', dateStr, {}), @@ -275,7 +274,19 @@ describe('bar chart inside day modal', () => { ); fireEvent.click(screen.getByTestId(`day-${dateStr}`)); await waitFor(() => screen.getByRole('dialog')); - // Both animals are in the day view (both have records), but bar chart only has Rat A expect(screen.getByTestId('bar-chart')).toBeInTheDocument(); }); + + it('hides bar chart when no animals have a numeric value for the selected field that day', async () => { + const dateStr = todayStr(); + // vitals is text — no bar chart + const statuses = [makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, {}, 'HR 72')]; + render( + , + ); + fireEvent.change(screen.getByTestId('field-select'), { target: { value: 'vitals' } }); + fireEvent.click(screen.getByTestId(`day-${dateStr}`)); + await waitFor(() => screen.getByRole('dialog')); + expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument(); + }); });