Skip to main content

ggplot_rs/geom/
bracket.rs

1use crate::aes::Aesthetic;
2use crate::coord::Coord;
3use crate::data::DataFrame;
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
15/// Significance bracket — a horizontal bar with two downward end ticks spanning
16/// `[xmin, xmax]` at height `y`, captioned with a `label` above it (R's
17/// `ggpubr::geom_bracket`). Typically annotates a pairwise-comparison p-value or
18/// significance stars over a boxplot.
19pub struct GeomBracket {
20    pub color: (u8, u8, u8),
21    pub line_width: f64,
22    /// Length of the downward end ticks, in pixels.
23    pub tip_length: f64,
24    /// Label font size.
25    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            // Bar at `py` with end ticks pointing toward the data (+y is down in
72            // screen space, so the ticks drop below the bar).
73            let tip = self.tip_length;
74            backend.draw_line(
75                &[
76                    (px_min, py + tip),
77                    (px_min, py),
78                    (px_max, py),
79                    (px_max, py + tip),
80                ],
81                &LineStyle {
82                    color: self.color,
83                    alpha: 1.0,
84                    width: self.line_width,
85                    linetype: Linetype::Solid,
86                },
87            )?;
88
89            // Centered label just above the bar.
90            if let Some(lc) = label_col {
91                let text = lc[i].to_group_key();
92                if !text.is_empty() {
93                    let cx = (px_min + px_max) / 2.0;
94                    backend.draw_text(
95                        &text,
96                        (cx, py - self.label_size * 0.3 - 2.0),
97                        &TextStyle {
98                            color: self.color,
99                            size: self.label_size,
100                            anchor: TextAnchor::Middle,
101                            angle: 0.0,
102                            family: None,
103                            face: FontFace::Plain,
104                        },
105                    )?;
106                }
107            }
108        }
109
110        Ok(())
111    }
112
113    fn required_aes(&self) -> Vec<Aesthetic> {
114        vec![Aesthetic::Xmin, Aesthetic::Xmax, Aesthetic::Y]
115    }
116
117    fn default_stat(&self) -> Box<dyn Stat> {
118        Box::new(StatIdentity)
119    }
120    fn default_position(&self) -> Box<dyn Position> {
121        Box::new(PositionIdentity)
122    }
123    fn default_params(&self) -> GeomParams {
124        GeomParams::default()
125    }
126    fn name(&self) -> &str {
127        "bracket"
128    }
129
130    fn set_series_color(&mut self, color: (u8, u8, u8)) {
131        self.color = color;
132    }
133}