fix: bar chart uses analysis_summary from each day's status records
X-axis: animals with valid field data on that day. Bars: Success / Failure / Total from status.analysis_summary for that day. Each day shows its own session metrics. Animals without analysis_summary or with total=0 are excluded from the chart. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,25 +53,22 @@ function DayPanel({ date, animals, allStatuses, template, experimentId, selected
|
|||||||
|
|
||||||
const fieldLabel = fieldOptions.find((f) => f.value === selectedField)?.label;
|
const fieldLabel = fieldOptions.find((f) => f.value === selectedField)?.label;
|
||||||
|
|
||||||
// Bar chart: this day's field value per animal (numeric only, filtered to animals present that day)
|
// Bar chart: animals with analysis_summary on this day → Success/Failure/Total from that summary
|
||||||
const barData = useMemo(() => {
|
const barData = useMemo(() => {
|
||||||
if (!selectedField) return [];
|
|
||||||
const isBuiltin = BUILTIN_KEYS_SET.has(selectedField);
|
|
||||||
return animalsWithData
|
return animalsWithData
|
||||||
.map((animal) => {
|
.map((animal) => {
|
||||||
const status = statusByAnimal[animal.id];
|
const status = statusByAnimal[animal.id];
|
||||||
if (!status) return null;
|
const summary = status?.analysis_summary;
|
||||||
const raw = isBuiltin ? status[selectedField] : status.custom_fields?.[selectedField];
|
if (!summary || summary.total === 0) return null;
|
||||||
if (raw === null || raw === undefined || String(raw).trim() === '') return null;
|
|
||||||
const num = parseFloat(raw);
|
|
||||||
if (isNaN(num)) return null;
|
|
||||||
return {
|
return {
|
||||||
name: animal.animal_name || animal.animal_id_string || animal.id.slice(0, 8),
|
name: animal.animal_name || animal.animal_id_string || animal.id.slice(0, 8),
|
||||||
value: num,
|
total: summary.total,
|
||||||
|
success: summary.counts?.Success ?? 0,
|
||||||
|
failure: summary.counts?.Failure ?? 0,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}, [animalsWithData, statusByAnimal, selectedField]);
|
}, [animalsWithData, statusByAnimal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 max-h-[80vh] overflow-y-auto pr-1">
|
<div className="space-y-3 max-h-[80vh] overflow-y-auto pr-1">
|
||||||
@@ -120,16 +117,17 @@ function DayPanel({ date, animals, allStatuses, template, experimentId, selected
|
|||||||
{/* Bar chart: overall per-subject completeness */}
|
{/* Bar chart: overall per-subject completeness */}
|
||||||
{barData.length > 0 && (
|
{barData.length > 0 && (
|
||||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||||
<h4 className="text-sm font-semibold text-gray-700 mb-3">
|
<h4 className="text-sm font-semibold text-gray-700 mb-3">Session metrics</h4>
|
||||||
{fieldLabel ?? 'Field'} — this session
|
|
||||||
</h4>
|
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
<BarChart data={barData} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
|
<BarChart data={barData} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||||
<XAxis dataKey="name" tick={{ fontSize: 11 }} />
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} />
|
||||||
<YAxis tick={{ fontSize: 11 }} />
|
<YAxis tick={{ fontSize: 11 }} />
|
||||||
<Tooltip formatter={(v) => [v, fieldLabel ?? 'Value']} />
|
<Tooltip />
|
||||||
<Bar dataKey="value" name={fieldLabel ?? 'Value'} fill="#6366f1" radius={[3, 3, 0, 0]} />
|
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||||
|
<Bar dataKey="total" name="Total" fill="#94a3b8" radius={[3, 3, 0, 0]} />
|
||||||
|
<Bar dataKey="success" name="Success" fill="#22c55e" radius={[3, 3, 0, 0]} />
|
||||||
|
<Bar dataKey="failure" name="Failure" fill="#f87171" radius={[3, 3, 0, 0]} minPointSize={2} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function todayStr() {
|
|||||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeStatus(animalId, date, customFields = {}, vitals = null) {
|
function makeStatus(animalId, date, customFields = {}, vitals = null, analysisSummary = null) {
|
||||||
return {
|
return {
|
||||||
id: `s-${animalId}-${date}`,
|
id: `s-${animalId}-${date}`,
|
||||||
animal_id: animalId,
|
animal_id: animalId,
|
||||||
@@ -48,6 +48,7 @@ function makeStatus(animalId, date, customFields = {}, vitals = null) {
|
|||||||
treatment: null,
|
treatment: null,
|
||||||
notes: null,
|
notes: null,
|
||||||
custom_fields: customFields,
|
custom_fields: customFields,
|
||||||
|
analysis_summary: analysisSummary,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,10 +249,11 @@ describe('day click modal', () => {
|
|||||||
// ── Bar chart (inside day modal) ──────────────────────────────────────────────
|
// ── Bar chart (inside day modal) ──────────────────────────────────────────────
|
||||||
|
|
||||||
describe('bar chart inside day modal', () => {
|
describe('bar chart inside day modal', () => {
|
||||||
it('renders bar chart with that day\'s numeric field values', async () => {
|
it('renders session metrics bar chart when analysis_summary has data', async () => {
|
||||||
const dateStr = todayStr();
|
const dateStr = todayStr();
|
||||||
|
const summary = { total: 18, counts: { Success: 5, Failure: 13 }, success_rate: 0.28 };
|
||||||
const statuses = [
|
const statuses = [
|
||||||
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }),
|
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }, null, summary),
|
||||||
];
|
];
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
||||||
@@ -262,31 +264,16 @@ describe('bar chart inside day modal', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('omits animals with empty or non-numeric field value from bar chart', async () => {
|
it('hides bar chart when no animal has analysis_summary with total > 0', async () => {
|
||||||
const dateStr = todayStr();
|
const dateStr = todayStr();
|
||||||
// Rat A has numeric weight; Rat B has empty weight — Rat B excluded from chart but shown in day list
|
|
||||||
const statuses = [
|
const statuses = [
|
||||||
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }),
|
makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }),
|
||||||
makeStatus('a2000000-0000-0000-0000-000000000002', dateStr, {}),
|
|
||||||
];
|
];
|
||||||
render(
|
render(
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
||||||
);
|
);
|
||||||
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
|
fireEvent.click(screen.getByTestId(`day-${dateStr}`));
|
||||||
await waitFor(() => screen.getByRole('dialog'));
|
await waitFor(() => screen.getByRole('dialog'));
|
||||||
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(
|
|
||||||
<ExperimentCalendar experimentId={EXP_ID} template={TEMPLATE} allStatuses={statuses} animals={ANIMALS} />,
|
|
||||||
);
|
|
||||||
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();
|
expect(screen.queryByTestId('bar-chart')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user