Skip to main content

ggplot_rs/geom/
step.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, LineStyle, Linetype};
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/// Step direction: horizontal-then-vertical or vertical-then-horizontal.
16pub enum StepDirection {
17    /// Draw horizontal first, then vertical (default).
18    Hv,
19    /// Draw vertical first, then horizontal.
20    Vh,
21    /// Step half-way between adjacent x values (ggplot2's `"mid"`).
22    Mid,
23}
24
25/// Step function line geometry.
26pub struct GeomStep {
27    pub color: (u8, u8, u8),
28    pub width: f64,
29    pub alpha: f64,
30    pub direction: StepDirection,
31}
32
33impl Default for GeomStep {
34    fn default() -> Self {
35        GeomStep {
36            color: (0, 0, 0),
37            width: 1.5,
38            alpha: 1.0,
39            direction: StepDirection::Hv,
40        }
41    }
42}
43
44impl Geom for GeomStep {
45    fn draw(
46        &self,
47        data: &DataFrame,
48        coord: &dyn Coord,
49        scales: &ScaleSet,
50        _theme: &Theme,
51        backend: &mut dyn DrawBackend,
52    ) -> Result<(), RenderError> {
53        let x_col = data
54            .column("x")
55            .ok_or(RenderError::MissingAesthetic("x".into()))?;
56        let y_col = data
57            .column("y")
58            .ok_or(RenderError::MissingAesthetic("y".into()))?;
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        // One step line per group (colour / group / linetype level), so e.g.
64        // a Kaplan–Meier curve per stratum or an ECDF per group.
65        for rows in super::support::row_groups(data) {
66            let mut raw: Vec<(f64, f64)> = rows
67                .iter()
68                .map(|&i| {
69                    let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
70                    let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
71                    (nx, ny)
72                })
73                .collect();
74            // Sort by x (stable, so tied x keep their row order).
75            raw.sort_by(|a, b| a.0.total_cmp(&b.0));
76            let step_points = super::ribbon::stepped(&raw, &self.direction);
77
78            let points: Vec<(f64, f64)> = step_points
79                .iter()
80                .map(|&(nx, ny)| {
81                    let (px, py) = coord.transform((nx, ny), &plot_area);
82                    // Clamp an *infinite* coord (e.g. stat_ecdf's ±Inf padding,
83                    // which extends the step to the panel edge) to the panel
84                    // border so the flat segments draw to the edge instead of
85                    // off-canvas. A NaN coord is left as NaN — not clamped to
86                    // an edge — so `draw_line` splits the step there (B2-1's
87                    // break semantics) instead of drawing a phantom spike to
88                    // whichever edge the sign-comparison happened to pick.
89                    let px = if px.is_infinite() {
90                        if nx < 0.0 {
91                            plot_area.x
92                        } else {
93                            plot_area.x + plot_area.width
94                        }
95                    } else {
96                        px
97                    };
98                    let py = if py.is_infinite() {
99                        if ny < 0.0 {
100                            plot_area.y + plot_area.height
101                        } else {
102                            plot_area.y
103                        }
104                    } else {
105                        py
106                    };
107                    (px, py)
108                })
109                .collect();
110
111            let first = rows[0];
112            let line_color = data
113                .column("color")
114                .and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[first]))
115                .unwrap_or(self.color);
116            let linetype = data
117                .column("linetype")
118                .and_then(|c| scales.map_linetype(&c[first]))
119                .unwrap_or(Linetype::Solid);
120            let series = super::series_key(data, first);
121            super::set_mark(backend, series.clone(), None, series, None);
122            super::set_mark_formatted(backend, scales, None, &Aesthetic::Y, None);
123
124            if points.len() >= 2 {
125                backend.draw_line(
126                    &points,
127                    &LineStyle {
128                        color: line_color,
129                        alpha: self.alpha,
130                        width: self.width,
131                        linetype,
132                        ..Default::default()
133                    },
134                )?;
135            }
136        }
137        super::clear_mark(backend);
138
139        Ok(())
140    }
141
142    fn required_aes(&self) -> Vec<Aesthetic> {
143        vec![Aesthetic::X, Aesthetic::Y]
144    }
145
146    fn default_stat(&self) -> Box<dyn Stat> {
147        Box::new(StatIdentity)
148    }
149
150    fn default_position(&self) -> Box<dyn Position> {
151        Box::new(PositionIdentity)
152    }
153
154    fn default_params(&self) -> GeomParams {
155        GeomParams::default()
156    }
157
158    fn name(&self) -> &str {
159        "step"
160    }
161
162    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
163        if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
164            self.color = ink;
165        }
166    }
167
168    /// stat_ecdf pads the step with ±Inf (ggplot2), drawn to the panel edge.
169    fn allows_infinite(&self) -> bool {
170        true
171    }
172
173    fn keeps_na_as_break(&self) -> bool {
174        true
175    }
176
177    fn set_series_color(&mut self, color: (u8, u8, u8)) {
178        self.color = color;
179    }
180}