diff --git a/frontend/src/components/ExperimentCalendar.jsx b/frontend/src/components/ExperimentCalendar.jsx index 63deb5b..dd3b6ba 100644 --- a/frontend/src/components/ExperimentCalendar.jsx +++ b/frontend/src/components/ExperimentCalendar.jsx @@ -48,8 +48,15 @@ function DayPanel({ date, animals, allStatuses, template, experimentId, selected return map; }, [allStatuses, dateStr]); - // Only show animals that have a status record for this day - const animalsWithData = animals.filter((a) => statusByAnimal[a.id]); + // Only show animals that have a status record for this day WITH a non-empty selected field + const animalsWithData = animals.filter((a) => { + const s = statusByAnimal[a.id]; + if (!s) return false; + if (!selectedField) return true; + const isBuiltin = BUILTIN_KEYS_SET.has(selectedField); + const val = isBuiltin ? s[selectedField] : s.custom_fields?.[selectedField]; + return val !== null && val !== undefined && String(val).trim() !== ''; + }); const fieldLabel = fieldOptions.find((f) => f.value === selectedField)?.label; diff --git a/frontend/tests/ExperimentCalendar.test.jsx b/frontend/tests/ExperimentCalendar.test.jsx index 589cfcf..ad9440d 100644 --- a/frontend/tests/ExperimentCalendar.test.jsx +++ b/frontend/tests/ExperimentCalendar.test.jsx @@ -247,33 +247,87 @@ describe('day click modal', () => { }); // ── Bar chart (inside day modal) ────────────────────────────────────────────── +// +// The bar chart reads from status.analysis_summary — a pre-computed field on +// each DailyStatus record containing trial-level session metrics: +// { total: number, counts: { Success: number, Failure: number }, success_rate: number } +// +// X-axis: animals that have a valid (non-empty) value for the chosen calendar +// field on THAT specific day. Each day can show a different set of animals. +// Bars: Total / Success / Failure from analysis_summary for that animal on that day. +// +// Chart is hidden when no animal on that day has analysis_summary.total > 0. +// Do NOT compute success/failure from the field value itself — always use analysis_summary. describe('bar chart inside day modal', () => { - it('renders session metrics bar chart when analysis_summary has data', async () => { + it('renders session metrics bar chart when at least one animal has analysis_summary', async () => { const dateStr = todayStr(); const summary = { total: 18, counts: { Success: 5, Failure: 13 }, success_rate: 0.28 }; const statuses = [ makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }, null, summary), ]; - render( - , - ); + render(); fireEvent.click(screen.getByTestId(`day-${dateStr}`)); await waitFor(() => { expect(within(screen.getByRole('dialog')).getByTestId('bar-chart')).toBeInTheDocument(); }); }); - it('hides bar chart when no animal has analysis_summary with total > 0', async () => { + it('hides bar chart when no animal has analysis_summary (null or total=0)', async () => { const dateStr = todayStr(); const statuses = [ + // Has field data (so day is clickable) but no analysis_summary makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }), ]; - render( - , - ); + render(); fireEvent.click(screen.getByTestId(`day-${dateStr}`)); await waitFor(() => screen.getByRole('dialog')); expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument(); }); + + it('hides bar chart when analysis_summary exists but total is 0', async () => { + const dateStr = todayStr(); + const emptySummary = { total: 0, counts: { Success: 0, Failure: 0 }, success_rate: null }; + const statuses = [ + makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }, null, emptySummary), + ]; + render(); + fireEvent.click(screen.getByTestId(`day-${dateStr}`)); + await waitFor(() => screen.getByRole('dialog')); + expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument(); + }); + + it('shows bar chart for multiple animals each with their own analysis_summary', async () => { + const dateStr = todayStr(); + const summaryA = { total: 18, counts: { Success: 5, Failure: 13 }, success_rate: 0.28 }; + const summaryB = { total: 10, counts: { Success: 7, Failure: 3 }, success_rate: 0.7 }; + const statuses = [ + makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }, null, summaryA), + makeStatus('a2000000-0000-0000-0000-000000000002', dateStr, { 'ww000000-0000-0000-0000-000000000099': '310' }, null, summaryB), + ]; + render(); + fireEvent.click(screen.getByTestId(`day-${dateStr}`)); + await waitFor(() => { + expect(within(screen.getByRole('dialog')).getByTestId('bar-chart')).toBeInTheDocument(); + }); + }); + + it('excludes from chart animals whose field value is empty on that day, even with analysis_summary', async () => { + const dateStr = todayStr(); + const summary = { total: 10, counts: { Success: 7, Failure: 3 }, success_rate: 0.7 }; + const statuses = [ + // Rat A: has weight AND analysis_summary → appears in chart + makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }, null, summary), + // Rat B: has analysis_summary but NO weight → not in day view at all, not in chart + makeStatus('a2000000-0000-0000-0000-000000000002', dateStr, {}, null, summary), + ]; + render(); + fireEvent.click(screen.getByTestId(`day-${dateStr}`)); + await waitFor(() => screen.getByRole('dialog')); + // Chart shows because Rat A has data + expect(screen.getByTestId('bar-chart')).toBeInTheDocument(); + // Rat B not shown in day view (no weight value) + const dialog = screen.getByRole('dialog'); + expect(within(dialog).queryAllByText('Rat B')).toHaveLength(0); + }); });