1use crate::aes::Aesthetic;
2use crate::coord::Coord;
3use crate::data::{DataFrame, Value};
4use crate::position::identity::PositionIdentity;
5use crate::position::Position;
6use crate::render::backend::{DrawBackend, FontFace, LineStyle, Linetype, TextAnchor, TextStyle};
7use crate::render::RenderError;
8use crate::scale::ScaleSet;
9use crate::stat::identity::StatIdentity;
10use crate::stat::Stat;
11use crate::theme::Theme;
12
13use super::{Geom, GeomParams};
14
15pub struct GeomBracket {
20 pub color: (u8, u8, u8),
21 pub line_width: f64,
22 pub tip_length: f64,
24 pub label_size: f64,
26}
27
28impl Default for GeomBracket {
29 fn default() -> Self {
30 GeomBracket {
31 color: (0, 0, 0),
32 line_width: 1.0,
33 tip_length: 8.0,
34 label_size: 12.0,
35 }
36 }
37}
38
39impl Geom for GeomBracket {
40 fn draw(
41 &self,
42 data: &DataFrame,
43 coord: &dyn Coord,
44 scales: &ScaleSet,
45 theme: &Theme,
46 backend: &mut dyn DrawBackend,
47 ) -> Result<(), RenderError> {
48 let xmin_col = data
49 .column("xmin")
50 .ok_or(RenderError::MissingAesthetic("xmin".into()))?;
51 let xmax_col = data
52 .column("xmax")
53 .ok_or(RenderError::MissingAesthetic("xmax".into()))?;
54 let y_col = data
55 .column("y")
56 .ok_or(RenderError::MissingAesthetic("y".into()))?;
57 let label_col = data.column("label");
58
59 let plot_area = backend.plot_area();
60 let x_scale = scales.get(&Aesthetic::X);
61 let y_scale = scales.get(&Aesthetic::Y);
62
63 for i in 0..data.nrows() {
64 let nxmin = x_scale.map(|s| s.map(&xmin_col[i])).unwrap_or(0.0);
65 let nxmax = x_scale.map(|s| s.map(&xmax_col[i])).unwrap_or(0.0);
66 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
67
68 let (px_min, py) = coord.transform((nxmin, ny), &plot_area);
69 let (px_max, _) = coord.transform((nxmax, ny), &plot_area);
70
71 let label_text = label_col.map(|lc| lc[i].to_group_key()).unwrap_or_default();
74 let comparison = format!(
75 "{} vs {}",
76 super::tip_value(&xmin_col[i]),
77 super::tip_value(&xmax_col[i])
78 );
79 let series = data
80 .column(BRACKET_SERIES_COL)
81 .and_then(|c| c.get(i))
82 .filter(|v| !v.is_na())
83 .map(super::tip_value)
84 .unwrap_or_else(|| comparison.clone());
85 let value = data
86 .column(BRACKET_P_COL)
87 .and_then(|c| c.get(i))
88 .and_then(super::raw_value);
89 let tooltip = if label_text.is_empty() {
90 comparison.clone()
91 } else {
92 format!("{comparison}: {label_text}")
93 };
94 super::set_mark(
95 backend,
96 Some(tooltip),
97 Some(comparison),
98 Some(series),
99 value,
100 );
101 super::set_mark_formatted(backend, scales, None, &Aesthetic::Y, None);
105
106 let tip = self.tip_length;
109 backend.draw_line(
110 &[
111 (px_min, py + tip),
112 (px_min, py),
113 (px_max, py),
114 (px_max, py + tip),
115 ],
116 &LineStyle {
117 color: self.color,
118 alpha: 1.0,
119 width: self.line_width,
120 linetype: Linetype::Solid,
121 ..Default::default()
122 },
123 )?;
124 super::clear_mark(backend);
125
126 if let Some(lc) = label_col {
128 let text = lc[i].to_group_key();
129 if !text.is_empty() {
130 let cx = (px_min + px_max) / 2.0;
131 backend.draw_text(
132 &text,
133 (cx, py - self.label_size * 0.3 - 2.0),
134 &TextStyle {
135 color: self.color,
136 size: self.label_size,
137 anchor: TextAnchor::Middle,
138 angle: 0.0,
139 family: super::resolve_font_family("", theme),
140 face: FontFace::Plain,
141 bg_colour: None,
142 bg_r: 0.0,
143 },
144 )?;
145 }
146 }
147 }
148
149 Ok(())
150 }
151
152 fn required_aes(&self) -> Vec<Aesthetic> {
153 vec![Aesthetic::Xmin, Aesthetic::Xmax, Aesthetic::Y]
154 }
155
156 fn default_stat(&self) -> Box<dyn Stat> {
157 Box::new(StatIdentity)
158 }
159 fn default_position(&self) -> Box<dyn Position> {
160 Box::new(PositionIdentity)
161 }
162 fn default_params(&self) -> GeomParams {
163 GeomParams::default()
164 }
165 fn name(&self) -> &str {
166 "bracket"
167 }
168
169 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
170 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
171 self.color = ink;
172 }
173 }
174
175 fn set_series_color(&mut self, color: (u8, u8, u8)) {
176 self.color = color;
177 }
178}
179
180pub(crate) const BRACKET_P_COL: &str = ".bracket_p";
182pub(crate) const BRACKET_SERIES_COL: &str = ".bracket_series";
185
186#[derive(Clone)]
233pub struct BracketTable {
234 pub label: Option<String>,
236 pub group1: String,
238 pub group2: String,
240 pub p_column: Option<String>,
242 pub y_position: String,
244 pub step_increase: f64,
246 pub hide_ns: bool,
249 pub cutpoints: Vec<(f64, String)>,
253 pub geom: GeomBracketStyle,
255}
256
257#[derive(Clone, Copy, Debug)]
260pub struct GeomBracketStyle {
261 pub color: (u8, u8, u8),
262 pub line_width: f64,
263 pub tip_length: f64,
264 pub label_size: f64,
265}
266
267impl Default for GeomBracketStyle {
268 fn default() -> Self {
269 let g = GeomBracket::default();
270 GeomBracketStyle {
271 color: g.color,
272 line_width: g.line_width,
273 tip_length: g.tip_length,
274 label_size: g.label_size,
275 }
276 }
277}
278
279impl From<GeomBracketStyle> for GeomBracket {
280 fn from(s: GeomBracketStyle) -> Self {
281 GeomBracket {
282 color: s.color,
283 line_width: s.line_width,
284 tip_length: s.tip_length,
285 label_size: s.label_size,
286 }
287 }
288}
289
290impl Default for BracketTable {
291 fn default() -> Self {
292 BracketTable {
293 label: None,
294 group1: "group1".into(),
295 group2: "group2".into(),
296 p_column: None,
297 y_position: "y_position".into(),
298 step_increase: 0.12,
299 hide_ns: false,
300 cutpoints: vec![
301 (1e-4, "****".into()),
302 (1e-3, "***".into()),
303 (1e-2, "**".into()),
304 (0.05, "*".into()),
305 (f64::INFINITY, "ns".into()),
306 ],
307 geom: GeomBracketStyle::default(),
308 }
309 }
310}
311
312impl BracketTable {
313 pub fn new() -> Self {
314 Self::default()
315 }
316
317 pub fn label(mut self, template: &str) -> Self {
319 self.label = Some(template.to_string());
320 self
321 }
322
323 pub fn stars(self) -> Self {
325 self.label("{p.signif}")
326 }
327
328 pub fn p_column(mut self, col: &str) -> Self {
330 self.p_column = Some(col.to_string());
331 self
332 }
333
334 pub fn groups(mut self, group1: &str, group2: &str) -> Self {
336 self.group1 = group1.to_string();
337 self.group2 = group2.to_string();
338 self
339 }
340
341 pub fn y_position(mut self, col: &str) -> Self {
343 self.y_position = col.to_string();
344 self
345 }
346
347 pub fn step_increase(mut self, step: f64) -> Self {
349 self.step_increase = step;
350 self
351 }
352
353 pub fn hide_ns(mut self, hide: bool) -> Self {
355 self.hide_ns = hide;
356 self
357 }
358
359 pub fn cutpoints(mut self, cutpoints: &[(f64, &str)]) -> Self {
361 self.cutpoints = cutpoints.iter().map(|(c, s)| (*c, s.to_string())).collect();
362 self
363 }
364
365 pub fn style(mut self, style: GeomBracketStyle) -> Self {
367 self.geom = style;
368 self
369 }
370
371 pub fn signif(&self, p: f64) -> String {
373 if !p.is_finite() {
374 return String::new();
375 }
376 self.cutpoints
377 .iter()
378 .find(|(c, _)| p <= *c)
379 .map(|(_, s)| s.clone())
380 .unwrap_or_default()
381 }
382
383 fn is_ns(&self, p: f64) -> bool {
385 match self.cutpoints.len() {
386 0 => false,
387 n => {
388 let below = if n >= 2 {
389 self.cutpoints[n - 2].0
390 } else {
391 f64::NEG_INFINITY
392 };
393 p.is_finite() && p > below
394 }
395 }
396 }
397
398 pub(crate) fn resolve(
403 &self,
404 table: &DataFrame,
405 x_levels: &[String],
406 y_range: Option<(f64, f64)>,
407 warnings: &mut Vec<String>,
408 ) -> Option<DataFrame> {
409 const WHO: &str = "geom_bracket";
410 if let Err(e) = table.validate() {
411 warnings.push(format!(
412 "{WHO}: test table rejected ({e}); no brackets drawn"
413 ));
414 return None;
415 }
416 let (Some(g1), Some(g2)) = (table.column(&self.group1), table.column(&self.group2)) else {
417 warnings.push(format!(
418 "{WHO}: test table needs '{}' and '{}' columns; no brackets drawn",
419 self.group1, self.group2
420 ));
421 return None;
422 };
423 let p_of = |i: usize| -> Option<f64> {
424 let get = |c: &str| {
425 table
426 .column(c)
427 .and_then(|col| col.get(i))
428 .and_then(|v| v.as_f64())
429 .filter(|p| !p.is_nan())
430 };
431 match &self.p_column {
432 Some(c) => get(c),
433 None => get("p_adj").or_else(|| get("p_value")),
434 }
435 };
436
437 let mut rows: Vec<(usize, f64)> = Vec::new(); let (mut missing, mut unknown, mut ns) = (0usize, Vec::new(), 0usize);
439 for i in 0..table.nrows() {
440 let (a, b) = (&g1[i], &g2[i]);
441 if a.is_na() || b.is_na() {
442 missing += 1;
443 continue;
444 }
445 if !x_levels.is_empty() {
446 let bad: Vec<String> = [a, b]
447 .iter()
448 .map(|v| v.to_group_key())
449 .filter(|k| !x_levels.contains(k))
450 .collect();
451 if !bad.is_empty() {
452 unknown.extend(bad);
453 continue;
454 }
455 }
456 let p = p_of(i).unwrap_or(f64::NAN);
457 if self.hide_ns && self.is_ns(p) {
458 ns += 1;
459 continue;
460 }
461 rows.push((i, p));
462 }
463 if missing > 0 {
464 warnings.push(format!(
465 "{WHO}: removed {missing} row{} with a missing group",
466 if missing == 1 { "" } else { "s" }
467 ));
468 }
469 if !unknown.is_empty() {
470 unknown.dedup();
471 warnings.push(format!(
472 "{WHO}: removed {} row{} comparing groups not on the x axis ({})",
473 unknown.len(),
474 if unknown.len() == 1 { "" } else { "s" },
475 unknown.join(", ")
476 ));
477 }
478 let _ = ns; if rows.is_empty() {
480 return None;
481 }
482
483 let given = |i: usize| {
485 table
486 .column(&self.y_position)
487 .and_then(|c| c.get(i))
488 .and_then(|v| v.as_f64())
489 .filter(|y| y.is_finite())
490 };
491 let max_given = rows
492 .iter()
493 .filter_map(|(i, _)| given(*i))
494 .fold(f64::NEG_INFINITY, f64::max);
495 let (base, step) = match y_range {
496 Some((lo, hi)) => {
497 let span = hi - lo;
498 let unit = if span > 0.0 {
499 span
500 } else if hi != 0.0 {
501 hi.abs()
502 } else {
503 1.0
504 };
505 let step = if self.step_increase.is_finite() && self.step_increase > 0.0 {
506 unit * self.step_increase
507 } else {
508 unit * 0.12
509 };
510 (hi.max(max_given), step)
511 }
512 None if max_given.is_finite() => (max_given, max_given.abs().max(1.0) * 0.12),
513 None => (0.0, 1.0),
514 };
515 let needs_auto = rows.iter().any(|(i, _)| given(*i).is_none());
516 if needs_auto && y_range.is_none() && !max_given.is_finite() {
517 warnings.push(format!(
518 "{WHO}: no y_position and no y data to stack above; brackets placed from 0"
519 ));
520 }
521
522 let mut out = DataFrame::new();
523 let (mut xmin, mut xmax, mut y, mut label, mut pv, mut series) =
524 (vec![], vec![], vec![], vec![], vec![], vec![]);
525 let mut auto_k = 0usize;
526 for (i, p) in rows {
527 xmin.push(g1[i].clone());
528 xmax.push(g2[i].clone());
529 let yy = given(i).unwrap_or_else(|| {
530 auto_k += 1;
531 base + step * auto_k as f64
532 });
533 y.push(Value::Float(yy));
534 label.push(Value::Str(self.label_for(table, i, p)));
535 pv.push(if p.is_finite() {
536 Value::Float(p)
537 } else {
538 Value::Na
539 });
540 series.push(
541 table
542 .column("test_id")
543 .and_then(|c| c.get(i))
544 .cloned()
545 .unwrap_or(Value::Na),
546 );
547 }
548 out.add_column("xmin".into(), xmin);
549 out.add_column("xmax".into(), xmax);
550 out.add_column("y".into(), y);
551 out.add_column("label".into(), label);
552 out.add_column(BRACKET_P_COL.into(), pv);
553 out.add_column(BRACKET_SERIES_COL.into(), series);
554 Some(out)
555 }
556
557 fn label_for(&self, table: &DataFrame, i: usize, p: f64) -> String {
558 let template = match &self.label {
559 Some(t) => t.clone(),
560 None => match table.column("label").and_then(|c| c.get(i)) {
561 Some(v) if !v.is_na() => return v.to_group_key(),
562 _ => "p = {p}".to_string(),
563 },
564 };
565 let mut out = String::with_capacity(template.len());
566 let mut rest = template.as_str();
567 while let Some(open) = rest.find('{') {
568 out.push_str(&rest[..open]);
569 let after = &rest[open + 1..];
570 let Some(close) = after.find('}') else {
571 out.push_str(&rest[open..]);
572 rest = "";
573 break;
574 };
575 let key = &after[..close];
576 let fmt_p = |p: f64| {
577 if p.is_finite() {
578 format_p_number(p)
579 } else {
580 "NA".to_string()
581 }
582 };
583 let sub = match key {
584 "p" => Some(fmt_p(p)),
585 "p.signif" | "stars" => Some(if p.is_finite() {
586 self.signif(p)
587 } else {
588 "NA".to_string()
589 }),
590 "p.format" => Some(if p.is_finite() {
591 format!("p = {}", format_p_number(p))
592 } else {
593 "p = NA".to_string()
594 }),
595 col => table.column(col).and_then(|c| c.get(i)).map(|v| match v {
596 Value::Na => "NA".to_string(),
597 v if col.starts_with('p') && v.as_f64().is_some() && !v.is_datetime() => {
598 fmt_p(v.as_f64().unwrap_or(f64::NAN))
599 }
600 v => crate::format::format_value(v),
601 }),
602 };
603 match sub {
604 Some(s) => out.push_str(&s),
605 None => {
606 out.push('{');
607 out.push_str(key);
608 out.push('}');
609 }
610 }
611 rest = &after[close + 1..];
612 }
613 out.push_str(rest);
614 out
615 }
616}
617
618pub fn format_p_number(p: f64) -> String {
621 if !p.is_finite() {
622 return "NA".to_string();
623 }
624 if p < 2.2e-16 {
625 return "< 2.2e-16".to_string();
626 }
627 if p < 1e-4 {
628 return format!("{p:.2e}");
629 }
630 let digits = (4 - 1 - p.abs().log10().floor() as i32).clamp(0, 12) as usize;
631 let s = format!("{p:.digits$}");
632 if s.contains('.') {
633 s.trim_end_matches('0').trim_end_matches('.').to_string()
634 } else {
635 s
636 }
637}
638
639#[cfg(test)]
640mod table_tests {
641 use super::*;
642
643 fn s(v: &str) -> Value {
644 Value::Str(v.into())
645 }
646
647 fn table() -> DataFrame {
648 let mut t = DataFrame::new();
649 t.add_column("group1".into(), vec![s("a"), s("a"), s("b"), Value::Na]);
650 t.add_column("group2".into(), vec![s("b"), s("c"), s("c"), s("c")]);
651 t.add_column(
652 "p_value".into(),
653 vec![
654 Value::Float(0.0004),
655 Value::Float(0.03),
656 Value::Float(0.4),
657 Value::Float(0.01),
658 ],
659 );
660 t.add_column(
661 "p_adj".into(),
662 vec![
663 Value::Float(0.0012),
664 Value::Na,
665 Value::Float(0.9),
666 Value::Na,
667 ],
668 );
669 t
670 }
671
672 fn levels() -> Vec<String> {
673 vec!["a".into(), "b".into(), "c".into()]
674 }
675
676 #[test]
677 fn p_adj_falls_back_to_p_value_and_stacks() {
678 let mut w = Vec::new();
679 let out = BracketTable::new()
680 .label("{p} {stars}")
681 .resolve(&table(), &levels(), Some((0.0, 10.0)), &mut w)
682 .unwrap();
683 let labels: Vec<String> = out
684 .column("label")
685 .unwrap()
686 .iter()
687 .map(|v| v.to_group_key())
688 .collect();
689 assert_eq!(labels, vec!["0.0012 **", "0.03 *", "0.9 ns"]);
690 let ys: Vec<f64> = out
691 .column("y")
692 .unwrap()
693 .iter()
694 .map(|v| v.as_f64().unwrap())
695 .collect();
696 assert!(
697 (ys[0] - 11.2).abs() < 1e-9
698 && (ys[1] - 12.4).abs() < 1e-9
699 && (ys[2] - 13.6).abs() < 1e-9
700 );
701 assert!(w.iter().any(|m| m.contains("missing group")), "{w:?}");
702 }
703
704 #[test]
705 fn hide_ns_and_unknown_groups() {
706 let mut t = table();
707 t.add_column("y_position".into(), vec![Value::Float(20.0); 4]);
708 let mut w = Vec::new();
709 let out = BracketTable::new()
710 .hide_ns(true)
711 .resolve(&t, &["a".into(), "b".into()], Some((0.0, 1.0)), &mut w)
712 .unwrap();
713 assert_eq!(out.nrows(), 1);
715 assert_eq!(out.column("y").unwrap()[0].as_f64(), Some(20.0));
716 assert!(w.iter().any(|m| m.contains("not on the x axis")), "{w:?}");
717 }
718
719 #[test]
720 fn label_column_and_unknown_keys() {
721 let mut t = table();
722 t.add_column("label".into(), vec![s("custom"), Value::Na, s("x"), s("y")]);
723 let mut w = Vec::new();
724 let out = BracketTable::new().resolve(&t, &[], None, &mut w).unwrap();
725 let labels: Vec<String> = out
726 .column("label")
727 .unwrap()
728 .iter()
729 .map(|v| v.to_group_key())
730 .collect();
731 assert_eq!(labels[0], "custom");
732 assert_eq!(labels[1], "p = 0.03");
733 let mut w = Vec::new();
734 let out = BracketTable::new()
735 .label("{nope} {group1}")
736 .resolve(&t, &[], None, &mut w)
737 .unwrap();
738 assert_eq!(out.column("label").unwrap()[0].to_group_key(), "{nope} a");
739 }
740
741 #[test]
742 fn formats_p_numbers() {
743 assert_eq!(format_p_number(0.03), "0.03");
744 assert_eq!(format_p_number(0.012345), "0.01235");
745 assert_eq!(format_p_number(0.5), "0.5");
746 assert_eq!(format_p_number(1.0), "1");
747 assert_eq!(format_p_number(3e-5), "3.00e-5");
748 assert_eq!(format_p_number(1e-20), "< 2.2e-16");
749 assert_eq!(format_p_number(f64::NAN), "NA");
750 }
751}