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,