1use super::*;
9use crate::qc_rules::{CellRules, Mode, Rule};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Panel {
14 Rule(Rule),
15 Joint,
16 Overlap,
17}
18
19pub fn panels(rules: &CellRules) -> Vec<Panel> {
21 let mut p: Vec<Panel> = rules
22 .rules()
23 .iter()
24 .filter(|r| r.on_cells())
25 .map(|&r| Panel::Rule(r))
26 .collect();
27 p.push(Panel::Joint);
28 if rules.rules().contains(&Rule::GeneCells) {
29 p.push(Panel::Rule(Rule::GeneCells));
30 }
31 p.push(Panel::Overlap);
32 p
33}
34
35pub fn shown(rule: Rule, v: f32) -> String {
37 if rule.count() {
38 format!("{v:.1}")
39 } else {
40 format!("{v:.4}")
41 }
42}
43
44pub fn edge_text(rules: &CellRules, rule: Rule) -> String {
46 let Some(edge) = rules.edge(rule) else {
47 return if rules.is_on(rule) {
48 "no spread".into()
49 } else {
50 "off".into()
51 };
52 };
53 let side = if rule.keeps_above() { "≥" } else { "≤" };
54 let how = match rules.mode(rule) {
55 Mode::Mad => format!(" ({} MADs)", short(rules.config().n_mads as f64)),
56 Mode::Value => String::new(),
57 };
58 format!("{side} {}{how}", shown(rule, edge))
59}
60
61fn rule_panel(
62 rules: &CellRules,
63 rule: Rule,
64 cursor: Option<f64>,
65 c: &mut Canvas,
66 (x, y, w, h): (f64, f64, f64, f64),
67) {
68 let count = rules.count(rule);
69 let values = rules.values(rule);
70 let mut axis = Axis::fit(values, rule.count());
71 if let Some(e) = rules.edge(rule) {
72 axis = axis.including(e as f64);
73 }
74 let what = if rule.on_cells() { "" } else { " genes" };
75 let subtitle = match rules.is_on(rule) {
76 true => format!(
77 "{} · removes {}{what}, {} only by this",
78 edge_text(rules, rule),
79 count.removes,
80 count.only
81 ),
82 false => "off".into(),
83 };
84 let (title, x_title) = match rule {
85 Rule::Genes => ("Detected genes", "genes per cell"),
86 Rule::Counts => ("Total counts", "counts per cell"),
87 Rule::Mito => ("Mitochondrial fraction", "fraction of counts"),
88 Rule::Ribo => ("Ribosomal fraction", "fraction of counts"),
89 Rule::GeneCells => ("Cells detecting each gene", "cells per gene"),
90 };
91 Hist {
92 title: title.into(),
93 subtitle,
94 x_title: x_title.into(),
95 axis,
96 values,
97 layers: vec![
98 (None, FAINT),
99 (Some(&count.pass_others), BAR),
100 (Some(&count.only_mask), ACCENT),
101 ],
102 edge: rules.edge(rule).map(|e| (e as f64, shown(rule, e))),
103 cursor,
104 }
105 .draw(c, x, y, w, h);
106}
107
108fn joint_panel(rules: &CellRules, c: &mut Canvas, (x, y, w, h): (f64, f64, f64, f64)) {
109 let m = rules.metrics();
110 let v = rules.verdicts();
111 let mito = m.mito_frac.as_deref();
112 let top = rules
114 .edge(Rule::Mito)
115 .map(|e| 2.0 * e as f64)
116 .or_else(|| mito.map(|f| f.iter().copied().fold(0.0f32, f32::max) as f64))
117 .filter(|t| *t > 0.0)
118 .unwrap_or(1.0);
119 let points: Vec<Point> = (0..m.n_genes.len())
120 .map(|i| Point {
121 x: m.total_counts[i],
122 y: m.n_genes[i],
123 fill: mito.map_or(BAR, |f| ramp(f[i] as f64 / top)),
124 ringed: !v.emitted(i),
125 })
126 .collect();
127 let subtitle = match mito {
128 Some(_) => "colour: mito fraction, darker higher · ring: removed",
129 None => "ring: removed",
130 };
131 let mut x_axis = Axis::fit(&m.total_counts, true);
132 let mut y_axis = Axis::fit(&m.n_genes, true);
133 if let Some(e) = rules.edge(Rule::Counts) {
134 x_axis = x_axis.including(e as f64);
135 }
136 if let Some(e) = rules.edge(Rule::Genes) {
137 y_axis = y_axis.including(e as f64);
138 }
139 Scatter {
140 title: "Counts against genes".into(),
141 subtitle: subtitle.into(),
142 x_title: "counts per cell".into(),
143 y_title: "genes per cell".into(),
144 x: x_axis,
145 y: y_axis,
146 points: &points,
147 x_edge: rules.edge(Rule::Counts).map(|e| e as f64),
148 y_edge: rules.edge(Rule::Genes).map(|e| e as f64),
149 key: vec![],
150 }
151 .draw(c, x, y, w, h);
152}
153
154fn overlap_panel(rules: &CellRules, c: &mut Canvas, (x, y, w, _h): (f64, f64, f64, f64)) {
155 let groups = rules.overlap();
156 let removed: usize = groups.iter().map(|g| g.1).sum();
157 c.bold(
158 x + 44.0,
159 y + 12.0,
160 "Why cells were removed",
161 11.0,
162 Anchor::Start,
163 INK,
164 );
165 c.text(
166 x + 44.0,
167 y + 25.0,
168 &format!("{removed} cells, by the rules each fails on its own"),
169 9.0,
170 Anchor::Start,
171 MUTED,
172 );
173 let cell_rules: Vec<Rule> = rules
174 .rules()
175 .iter()
176 .copied()
177 .filter(|r| r.on_cells())
178 .collect();
179 let (dx, x0) = (16.0, x + 50.0);
180 for (j, r) in cell_rules.iter().enumerate() {
181 c.turned(
182 x0 + j as f64 * dx + 3.0,
183 y + 58.0,
184 r.short(),
185 8.0,
186 -50.0,
187 Anchor::Start,
188 MUTED,
189 );
190 }
191 let bars_x = x0 + cell_rules.len() as f64 * dx + 8.0;
192 let room = (x + w - 40.0 - bars_x).max(10.0);
193 let most = groups.first().map_or(1, |g| g.1).max(1) as f64;
194 for (i, (set, n)) in groups.iter().take(8).enumerate() {
195 let gy = y + 74.0 + i as f64 * 18.0;
196 let on: Vec<usize> = (0..cell_rules.len())
197 .filter(|&j| set.contains(&cell_rules[j]))
198 .collect();
199 if let (Some(&a), Some(&b)) = (on.first(), on.last()) {
200 c.line(x0 + a as f64 * dx, gy, x0 + b as f64 * dx, gy, INK, 1.5);
201 }
202 for j in 0..cell_rules.len() {
203 let colour = if on.contains(&j) { INK } else { GRID };
204 c.dot(x0 + j as f64 * dx, gy, 3.6, colour, 1.0);
205 }
206 let bw = *n as f64 / most * room;
207 c.rect(bars_x, gy - 5.0, bw, 10.0, ACCENT);
208 c.text(
209 bars_x + bw + 4.0,
210 gy + 3.5,
211 &n.to_string(),
212 9.0,
213 Anchor::Start,
214 INK,
215 );
216 }
217 if groups.is_empty() {
218 c.text(
219 x0,
220 y + 74.0,
221 "no cell is removed",
222 9.0,
223 Anchor::Start,
224 MUTED,
225 );
226 } else if groups.len() > 8 {
227 let rest: usize = groups[8..].iter().map(|g| g.1).sum();
228 c.text(
229 x0,
230 y + 74.0 + 8.0 * 18.0,
231 &format!("and {rest} more in {} other sets", groups.len() - 8),
232 8.5,
233 Anchor::Start,
234 MUTED,
235 );
236 }
237}
238
239pub fn draw_panel(
242 rules: &CellRules,
243 panel: Panel,
244 cursor: Option<f64>,
245 c: &mut Canvas,
246 area: (f64, f64, f64, f64),
247) {
248 match panel {
249 Panel::Rule(rule) => rule_panel(rules, rule, cursor, c, area),
250 Panel::Joint => joint_panel(rules, c, area),
251 Panel::Overlap => overlap_panel(rules, c, area),
252 }
253}
254
255pub fn panel(rules: &CellRules, panel: Panel, cursor: Option<f64>, w: f64, h: f64) -> String {
257 let mut c = Canvas::new(w, h);
258 draw_panel(rules, panel, cursor, &mut c, (0.0, 4.0, w, h - 4.0));
259 c.finish()
260}
261
262pub fn figure(rules: &CellRules, title: &str) -> String {
264 let panels = panels(rules);
265 let (cols, pw, ph) = (3usize, 320.0, 230.0);
266 let rows = panels.len().div_ceil(cols);
267 let (left, top) = (16.0, 92.0);
268 let (w, h) = (left * 2.0 + cols as f64 * pw, top + rows as f64 * ph + 12.0);
269 let mut c = Canvas::new(w, h);
270 let v = rules.verdicts();
271 let cells = v.train_keep.len();
272 let kept = rules.kept_cells().len();
273 let genes = v.feature_keep.len();
274 let kept_genes = rules.kept_genes().len();
275 c.bold(
276 left + 8.0,
277 28.0,
278 &format!("{title} · cell QC"),
279 15.0,
280 Anchor::Start,
281 INK,
282 );
283 let mut sub = format!(
284 "{cells} cells → {kept} kept ({:.1}%) · {genes} genes → {kept_genes} kept",
285 100.0 * kept as f64 / cells.max(1) as f64
286 );
287 if let Some(p) = &rules.config().mito_pattern {
288 sub.push_str(&format!(" · mito '{p}'"));
289 }
290 c.text(left + 8.0, 46.0, &sub, 10.0, Anchor::Start, MUTED);
291 c.legend(
292 left + 8.0,
293 70.0,
294 &[
295 (FAINT, "all"),
296 (BAR, "pass the other rules"),
297 (ACCENT, "removed only by this rule"),
298 ],
299 );
300 for (k, &p) in panels.iter().enumerate() {
301 let (col, row) = (k % cols, k / cols);
302 let area = (
303 left + col as f64 * pw,
304 top + row as f64 * ph,
305 pw - 12.0,
306 ph - 14.0,
307 );
308 draw_panel(rules, p, None, &mut c, area);
309 }
310 c.finish()
311}