1use 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
23pub struct GeomXrange {
26 pub progress: Option<String>,
28 pub depends_on: Option<String>,
31 pub fill: (u8, u8, u8),
33 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 pub fn with_progress(mut self, column: impl Into<String>) -> Self {
55 self.progress = Some(column.into());
56 self
57 }
58
59 pub fn with_dependencies(mut self, column: impl Into<String>) -> Self {
61 self.depends_on = Some(column.into());
62 self
63 }
64}
65
66type 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 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 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}