diff --git a/client/src/api.ts b/client/src/api.ts index 65568c9..604118e 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -15,6 +15,8 @@ export interface SiteSummary { site_num: number; n_events: number; n_hits: number; + n_no_hit: number; + n_ambiguous: number; } // Raw (unbinned) distribution values backing the dashboard's native charts — // a live modernisation of PanDDA1's pandda_analyse.html graphs. The client diff --git a/client/src/components/InspectDrawer.tsx b/client/src/components/InspectDrawer.tsx index 1fe259b..8e19e61 100644 --- a/client/src/components/InspectDrawer.tsx +++ b/client/src/components/InspectDrawer.tsx @@ -27,6 +27,8 @@ import { import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ViewInArIcon from "@mui/icons-material/ViewInAr"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import CancelIcon from "@mui/icons-material/Cancel"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; import BuildCircleIcon from "@mui/icons-material/BuildCircle"; import NavigateBeforeIcon from "@mui/icons-material/NavigateBefore"; import NavigateNextIcon from "@mui/icons-material/NavigateNext"; @@ -1436,6 +1438,14 @@ export function InspectDrawer({ const nHits = g.events.filter( (e) => e.decision === "hit" ).length; + // How many events here have a human decision (any of + // hit/no_hit/ambiguous) vs are still unreviewed — so while + // scanning sites you can see which still need analysing. + // A "built"/"hit" badge alone made a partly-done site look + // fully analysed (Erin's feedback); this corrects it. + const nReviewed = g.events.filter( + (e) => e.decision !== "unreviewed" + ).length; const built = isAutobuilt(g.events); const candidate = !built && hasCandidatePose(g.events); const topQ = bestQuality(g.events); @@ -1450,6 +1460,27 @@ export function InspectDrawer({ }`} /> + {nEvents > 0 && ( + + + + )} {built && ( decision > + // build (undecided) > unreviewed. loadingId === ev.id ? ( ) : ev.decision === "hit" ? ( + ) : ev.decision === "no_hit" ? ( + + ) : ev.decision === "ambiguous" ? ( + ) : poseState !== "none" ? ( - // A built/candidate ligand backs this event — - // flag it with the build icon (solid for - // merged, outlined-tint for candidate via sx). + // Undecided but a built/candidate ligand backs + // it — flag with the build icon (solid=merged, + // dashed-tint=candidate via sx). ) : ( diff --git a/client/src/components/SummaryCharts.tsx b/client/src/components/SummaryCharts.tsx index 758e3ce..b226a45 100644 --- a/client/src/components/SummaryCharts.tsx +++ b/client/src/components/SummaryCharts.tsx @@ -90,21 +90,41 @@ export function SummaryCharts({ distributions: Distributions; sites: SiteSummary[]; }) { - // Events-per-site bar (replaces PanDDA1's analyse_events_site_N pies); hits - // overlaid as a second series so curation progress reads off the same axis. + // Events-per-site, STACKED by decision (replaces PanDDA1's + // analyse_events_site_N pies). One bar per site whose total height is the + // event count, segmented into hit / no-hit / ambiguous / unreviewed — so + // curation progress AND the decision mix read off the same axis, and no-hit / + // ambiguous are visible (they weren't on the old Events+Hits overlay). Colours + // mirror the event-chip decision semantics. Unreviewed = events minus decided. const siteData = useMemo( () => ({ labels: sites.map((s) => `Site ${s.site_num}`), datasets: [ - { - label: "Events", - data: sites.map((s) => s.n_events), - backgroundColor: "#90caf9", - }, { label: "Hits", data: sites.map((s) => s.n_hits), backgroundColor: "#66bb6a", + stack: "decisions", + }, + { + label: "No-hit", + data: sites.map((s) => s.n_no_hit), + backgroundColor: "#ef5350", + stack: "decisions", + }, + { + label: "Ambiguous", + data: sites.map((s) => s.n_ambiguous), + backgroundColor: "#ffb74d", + stack: "decisions", + }, + { + label: "Unreviewed", + data: sites.map((s) => + Math.max(0, s.n_events - s.n_hits - s.n_no_hit - s.n_ambiguous) + ), + backgroundColor: "#e0e0e0", + stack: "decisions", }, ], }), @@ -152,6 +172,14 @@ export function SummaryCharts({ options={{ ...CHART_OPTS, plugins: { legend: { display: true } }, + scales: { + x: { stacked: true }, + y: { + stacked: true, + beginAtZero: true, + ticks: { precision: 0 }, + }, + }, }} /> diff --git a/inspect_api/serializers.py b/inspect_api/serializers.py index 2b483ea..5d19db8 100644 --- a/inspect_api/serializers.py +++ b/inspect_api/serializers.py @@ -360,6 +360,12 @@ def _rd_vals(fld): "n_hits": se.filter( finding__decision=Event.Decision.HIT ).count(), + "n_no_hit": se.filter( + finding__decision=Event.Decision.NO_HIT + ).count(), + "n_ambiguous": se.filter( + finding__decision=Event.Decision.AMBIGUOUS + ).count(), }) return { "analysed": n_events > 0,