From 5331ae6a56863e0d3b65f52daae69e27bcccdfe9 Mon Sep 17 00:00:00 2001 From: Martin Noble Date: Wed, 10 Jun 2026 18:50:34 +0100 Subject: [PATCH 1/3] =?UTF-8?q?feat(client):=20C1=20=E2=80=94=20per-site?= =?UTF-8?q?=20'analysed=20N/total'=20count=20in=20group=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A built/hit badge alone made a partly-reviewed site look fully analysed (Erin: 'it makes it look like I've analysed all the events at the site... could you list the number analysed'). Add an analysed-count chip beside the event count: amber while events remain unreviewed (decision==unreviewed), green when the whole site is decided, with a 'N still to analyse' tooltip. Reuses the existing decision!=unreviewed definition (grouping.decisionTally). Co-Authored-By: Claude Opus 4.8 (1M context) --- client/src/components/InspectDrawer.tsx | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/client/src/components/InspectDrawer.tsx b/client/src/components/InspectDrawer.tsx index 1fe259b..9cd927c 100644 --- a/client/src/components/InspectDrawer.tsx +++ b/client/src/components/InspectDrawer.tsx @@ -1436,6 +1436,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 +1458,27 @@ export function InspectDrawer({ }`} /> + {nEvents > 0 && ( + + + + )} {built && ( Date: Wed, 10 Jun 2026 18:55:46 +0100 Subject: [PATCH 2/3] =?UTF-8?q?feat(client):=20C2=20=E2=80=94=20distinct?= =?UTF-8?q?=20decision=20icons=20on=20event=20chips?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no_hit and ambiguous events shared the generic view icon, so they read as unreviewed (Erin: 'no hit and ambiguous didn't always show status'). Make the icon carry the decision: hit=check, no_hit=cancel (X), ambiguous=help (?), unreviewed=cube. Build status stays on the chip border, so no new chips — the icon just becomes legible per decision. Co-Authored-By: Claude Opus 4.8 (1M context) --- client/src/components/InspectDrawer.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/client/src/components/InspectDrawer.tsx b/client/src/components/InspectDrawer.tsx index 9cd927c..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"; @@ -1693,14 +1695,25 @@ export function InspectDrawer({ isLive ? "warning" : decisionColour(ev.decision) } icon={ + // Icon = DECISION status first, so no_hit and + // ambiguous are legible (they previously shared the + // generic view icon → looked unreviewed). Build + // status is still conveyed by the chip border (sx + // below), so we don't need the build icon once a + // decision exists. Order: loading > 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). ) : ( From aa674a38fc61da0124fdc8b3e77306a13e26a5ff Mon Sep 17 00:00:00 2001 From: Martin Noble Date: Wed, 10 Jun 2026 19:06:05 +0100 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20C3=20=E2=80=94=20per-site=20chart?= =?UTF-8?q?=20stacked=20by=20decision=20(show=20no-hit/ambiguous)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'events per site' chart overlaid only Events + Hits, so no-hit and ambiguous never showed (Erin: 'no hits/ambiguous don't show up on the dashboard graphs'). Make it a STACKED bar per site — hit/no-hit/ambiguous/ unreviewed segments summing to the event count — so the full decision mix and curation progress read off one axis (and it's less busy than overlaid bars). Backend per-site rollup now emits n_no_hit/n_ambiguous (additive); client SiteSummary + chart consume them. Colours mirror the event-chip decision semantics. Co-Authored-By: Claude Opus 4.8 (1M context) --- client/src/api.ts | 2 ++ client/src/components/SummaryCharts.tsx | 42 ++++++++++++++++++++----- inspect_api/serializers.py | 6 ++++ 3 files changed, 43 insertions(+), 7 deletions(-) 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/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,