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);
+ });
});