Skip to main content

ggplot_rs/geom/
xrange.rs

1//! X-range / Gantt chart: `geom_xrange`. One horizontal bar per row from
2//! `xmin` to `xmax` on a discrete `y` (task) axis, with an optional
3//! progress fill, milestones (zero-length tasks as diamonds) and dependency
4//! arrows from a predecessor's end to a task's start.
5
6use std::collections::HashMap;
7
8use crate::aes::Aesthetic;
9use crate::coord::Coord;
10use crate::data::{DataFrame, Value};
11use crate::position::identity::PositionIdentity;
12use crate::position::Position;
13use crate::render::backend::{DrawBackend, LineStyle, Linetype, RectStyle};
14use crate::render::RenderError;
15use crate::scale::ScaleSet;
16use crate::stat::identity::StatIdentity;
17use crate::stat::Stat;
18use crate::theme::Theme;
19
20use super::arrow::{arrow, draw_heads, heads, ArrowType};
21use super::{Geom, GeomParams};
22
23/// X-range / Gantt chart. Map `y` (task), `xmin` (start), `xmax` (end) and
24/// optionally `fill`; name progress and dependency columns on the geom.
25pub struct GeomXrange {
26    /// Column with each task's completed fraction (0–1).
27    pub progress: Option<String>,
28    /// Column naming each task's predecessor (its `y` value); an arrow runs
29    /// from the predecessor's end to this task's start.
30    pub depends_on: Option<String>,
31    /// Bar fill when `fill` is unmapped.
32    pub fill: (u8, u8, u8),
33    /// Bar height as a fraction of the task slot.
34    pub height: f64,
35    pub alpha: f64,
36    pub arrow_color: (u8, u8, u8),
37}
38
39impl Default for GeomXrange {
40    fn default() -> Self {
41        GeomXrange {
42            progress: None,
43            depends_on: None,
44            fill: (97, 156, 255),
45            height: 0.6,
46            alpha: 1.0,
47            arrow_color: (90, 90, 90),
48        }
49    }
50}
51
52impl GeomXrange {
53    /// Shade each bar's completed share from `column` (0–1).
54    pub fn with_progress(mut self, column: impl Into<String>) -> Self {
55        self.progress = Some(column.into());
56        self
57    }
58
59    /// Draw dependency arrows from the predecessor named in `column`.
60    pub fn with_dependencies(mut self, column: impl Into<String>) -> Self {
61        self.depends_on = Some(column.into());
62        self
63    }
64}
65
66/// A pixel position.
67type Px = (f64, f64);
68
69fn darken(c: (u8, u8, u8), f: f64) -> (u8, u8, u8) {
70    let d = |v: u8| (v as f64 * f) as u8;
71    (d(c.0), d(c.1), d(c.2))
72}
73
74impl Geom for GeomXrange {
75    fn draw(
76        &self,
77        data: &DataFrame,
78        coord: &dyn Coord,
79        scales: &ScaleSet,
80        _theme: &Theme,
81        backend: &mut dyn DrawBackend,
82    ) -> Result<(), RenderError> {
83        let col = |n: &str| {
84            data.column(n)
85                .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
86        };
87        let (y_col, x0c, x1c) = (col("y")?, col("xmin")?, col("xmax")?);
88        let fill_col = data.column("fill");
89        let progress = self.progress.as_deref().and_then(|c| data.column(c));
90        let deps = self.depends_on.as_deref().and_then(|c| data.column(c));
91        let area = backend.plot_area();
92        let y_scale = scales.get(&Aesthetic::Y);
93        let mx = |v: &Value| scales.get(&Aesthetic::X).map(|s| s.map(v)).unwrap_or(0.0);
94        let my = |v: &Value| y_scale.map(|s| s.map(v)).unwrap_or(0.5);
95        let n_levels = y_scale.map(|s| s.breaks().len()).unwrap_or(1).max(1) as f64;
96        let half = self.height / (n_levels * if coord.discrete_expand() { 1.1 } else { 1.0 }) / 2.0;
97
98        // Pixel anchors per task for dependency arrows: (start, end, centre y).
99        let mut anchors: HashMap<String, (Px, Px)> = HashMap::new();
100        for i in 0..data.nrows() {
101            let (ny, nx0, nx1) = (my(&y_col[i]), mx(&x0c[i]), mx(&x1c[i]));
102            if ![ny, nx0, nx1].iter().all(|v| v.is_finite()) {
103                continue;
104            }
105            let task = super::tip_value(&y_col[i]);
106            let fill = fill_col
107                .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
108                .unwrap_or(self.fill);
109            let p = progress
110                .and_then(|c| c[i].as_f64())
111                .map(|p| p.clamp(0.0, 1.0));
112            let (s, e) = (super::tip_value(&x0c[i]), super::tip_value(&x1c[i]));
113            let milestone = (nx1 - nx0).abs() < 1e-12;
114            let tip = match (milestone, p) {
115                (true, _) => format!("{task}: {s}"),
116                (false, Some(p)) => format!("{task}: {s} – {e} ({}% done)", (p * 100.0).round()),
117                (false, None) => format!("{task}: {s} – {e}"),
118            };
119            super::set_mark(
120                backend,
121                Some(tip),
122                Some(task.clone()),
123                super::series_key(data, i),
124                None,
125            );
126            super::set_row_extras(backend, data, i);
127            let style = |fill| RectStyle {
128                fill: Some(fill),
129                stroke: None,
130                stroke_width: 0.0,
131                alpha: self.alpha,
132                clip: true,
133                corner_radius: 3.0,
134            };
135            let start = coord.transform((nx0, ny), &area);
136            let end = coord.transform((nx1, ny), &area);
137            if milestone {
138                let r =
139                    (0.75 * (coord.transform((nx0, ny + half), &area).1 - start.1).abs()).max(4.0);
140                backend.draw_polygon(
141                    &[
142                        (start.0, start.1 - r),
143                        (start.0 + r, start.1),
144                        (start.0, start.1 + r),
145                        (start.0 - r, start.1),
146                    ],
147                    &style(darken(fill, 0.8)),
148                )?;
149                anchors.insert(task, ((start.0 - r, start.1), (start.0 + r, start.1)));
150                continue;
151            }
152            let a = coord.transform((nx0, ny + half), &area);
153            let b = coord.transform((nx1, ny - half), &area);
154            let (l, t, r, btm) = (a.0.min(b.0), a.1.min(b.1), a.0.max(b.0), a.1.max(b.1));
155            backend.draw_rect((l, t), (r, btm), &style(fill))?;
156            if let Some(p) = p {
157                super::clear_mark(backend);
158                let w = (r - l) * p;
159                if w > 0.0 {
160                    let inset = (btm - t) * 0.25;
161                    backend.draw_rect(
162                        (l, t + inset),
163                        (l + w, btm - inset),
164                        &style(darken(fill, 0.6)),
165                    )?;
166                }
167            }
168            anchors.insert(task, (start, end));
169        }
170        super::clear_row_extras(backend);
171
172        if let Some(deps) = deps {
173            let head = arrow().kind(ArrowType::Closed).length(6.0);
174            let line = LineStyle {
175                color: self.arrow_color,
176                alpha: 1.0,
177                width: 1.0,
178                linetype: Linetype::Solid,
179                ..Default::default()
180            };
181            for i in 0..data.nrows() {
182                if deps[i].is_na() {
183                    continue;
184                }
185                let task = super::tip_value(&y_col[i]);
186                let pred = super::tip_value(&deps[i]);
187                let (Some(&(_, from)), Some(&(to, _))) = (anchors.get(&pred), anchors.get(&task))
188                else {
189                    continue;
190                };
191                // Elbow: out of the predecessor's end, down/up to the task's
192                // row, into its start.
193                let bend = from.0 + 8.0;
194                let path = if bend <= to.0 - 6.0 {
195                    vec![from, (bend, from.1), (bend, to.1), to]
196                } else {
197                    let mid = (from.1 + to.1) / 2.0;
198                    vec![
199                        from,
200                        (bend, from.1),
201                        (bend, mid),
202                        (to.0 - 8.0, mid),
203                        (to.0 - 8.0, to.1),
204                        to,
205                    ]
206                };
207                super::set_mark(backend, Some(format!("{pred} → {task}")), None, None, None);
208                backend.draw_line(&path, &line)?;
209                let hs = heads(&path, &head, 1.0);
210                draw_heads(backend, &hs, &head, self.arrow_color, 1.0, 1.0)?;
211                backend.set_mark_part(None);
212            }
213        }
214        super::clear_mark(backend);
215        Ok(())
216    }
217
218    fn required_aes(&self) -> Vec<Aesthetic> {
219        vec![Aesthetic::Y, Aesthetic::Xmin, Aesthetic::Xmax]
220    }
221    fn default_stat(&self) -> Box<dyn Stat> {
222        Box::new(StatIdentity)
223    }
224    fn default_position(&self) -> Box<dyn Position> {
225        Box::new(PositionIdentity)
226    }
227    fn default_params(&self) -> GeomParams {
228        GeomParams::default()
229    }
230    fn name(&self) -> &str {
231        "xrange"
232    }
233    fn consumes_tooltips(&self) -> bool {
234        true
235    }
236    fn set_series_color(&mut self, color: (u8, u8, u8)) {
237        self.fill = color;
238    }
239}