1use crate::aes::Aesthetic;
8use crate::coord::Coord;
9use crate::data::{DataFrame, Value};
10use crate::position::identity::PositionIdentity;
11use crate::position::Position;
12use crate::render::backend::{
13 DrawBackend, FontFace, LineStyle, Linetype, PointStyle, RectStyle, TextAnchor, TextStyle,
14};
15use crate::render::RenderError;
16use crate::scale::ScaleSet;
17use crate::stat::identity::StatIdentity;
18use crate::stat::Stat;
19use crate::theme::Theme;
20
21use super::{Geom, GeomParams};
22
23pub struct GeomTimeline {
27 pub end: Option<String>,
29 pub color: (u8, u8, u8),
30 pub line_color: (u8, u8, u8),
31 pub label_size: f64,
32 pub label_color: (u8, u8, u8),
33 pub box_fill: (u8, u8, u8),
35 pub spacing: f64,
37}
38
39impl Default for GeomTimeline {
40 fn default() -> Self {
41 GeomTimeline {
42 end: None,
43 color: (31, 119, 180),
44 line_color: (120, 120, 120),
45 label_size: 9.5,
46 label_color: (30, 30, 30),
47 box_fill: (255, 255, 255),
48 spacing: 6.0,
49 }
50 }
51}
52
53impl GeomTimeline {
54 pub fn with_end(mut self, column: impl Into<String>) -> Self {
56 self.end = Some(column.into());
57 self
58 }
59}
60
61fn level(k: usize) -> i32 {
63 let n = (k / 2 + 1) as i32;
64 if k.is_multiple_of(2) {
65 n
66 } else {
67 -n
68 }
69}
70
71pub(crate) fn pick_level(
74 occupied: &mut Vec<(i32, f64, f64)>,
75 l: f64,
76 r: f64,
77 max_levels: usize,
78) -> Option<i32> {
79 (0..2 * max_levels.max(1)).map(level).find(|&lv| {
80 let free = !occupied.iter().any(|&(o, a, b)| o == lv && l < b && a < r);
81 if free {
82 occupied.push((lv, l, r));
83 }
84 free
85 })
86}
87
88impl Geom for GeomTimeline {
89 fn draw(
90 &self,
91 data: &DataFrame,
92 coord: &dyn Coord,
93 scales: &ScaleSet,
94 theme: &Theme,
95 backend: &mut dyn DrawBackend,
96 ) -> Result<(), RenderError> {
97 let x_col = data
98 .column("x")
99 .ok_or(RenderError::MissingAesthetic("x".into()))?;
100 let label = data.column("label");
101 let xend = data.column("xmax");
102 let color_col = data.column("color");
103 let area = backend.plot_area();
104 let mx = |v: &Value| scales.get(&Aesthetic::X).map(|s| s.map(v)).unwrap_or(0.5);
105 let px = |nx: f64| coord.transform((nx, 0.5), &area).0;
106 let base = area.y + area.height / 2.0;
107 backend.draw_line(
108 &[(area.x, base), (area.x + area.width, base)],
109 &LineStyle {
110 color: self.line_color,
111 alpha: 1.0,
112 width: 1.5,
113 linetype: Linetype::Solid,
114 ..Default::default()
115 },
116 )?;
117 let box_h = self.label_size + 8.0;
118 let step = box_h + 6.0;
119 let max_levels = (((area.height / 2.0) - 12.0) / step).floor().max(1.0) as usize;
120 let mut order: Vec<usize> = (0..data.nrows()).collect();
121 order.sort_by(|&a, &b| mx(&x_col[a]).total_cmp(&mx(&x_col[b])));
122 let mut occupied = Vec::new();
123 let family = super::resolve_font_family("", theme);
124 for i in order {
125 let x = px(mx(&x_col[i]));
126 if !x.is_finite() {
127 continue;
128 }
129 let color = color_col
130 .and_then(|c| scales.map_color(&Aesthetic::Color, &c[i]))
131 .unwrap_or(self.color);
132 let text = label.map(|c| super::tip_value(&c[i])).unwrap_or_default();
133 let when = super::tip_value(&x_col[i]);
134 let tip = match xend.map(|c| &c[i]).filter(|v| !v.is_na()) {
135 Some(e) => format!("{when} – {}: {text}", super::tip_value(e)),
136 None => format!("{when}: {text}"),
137 };
138 super::set_mark(
139 backend,
140 Some(tip),
141 Some(when),
142 super::series_key(data, i),
143 None,
144 );
145 super::set_row_extras(backend, data, i);
146 if let Some(xe) = xend.and_then(|c| (!c[i].is_na()).then(|| px(mx(&c[i])))) {
148 if xe.is_finite() {
149 backend.draw_rect(
150 (x.min(xe), base - 4.0),
151 (x.max(xe), base + 4.0),
152 &RectStyle {
153 fill: Some(color),
154 stroke: None,
155 stroke_width: 0.0,
156 alpha: 0.5,
157 clip: true,
158 corner_radius: 4.0,
159 },
160 )?;
161 }
162 }
163 backend.draw_circle(
164 (x, base),
165 4.5,
166 &PointStyle {
167 color,
168 alpha: 1.0,
169 filled: true,
170 ..Default::default()
171 },
172 )?;
173 if text.is_empty() {
174 continue;
175 }
176 let w = crate::render::text::text_width(&text, self.label_size) + 10.0;
177 let cx = x.clamp(area.x + w / 2.0, area.x + area.width - w / 2.0);
179 let Some(lv) = pick_level(
180 &mut occupied,
181 cx - w / 2.0 - self.spacing / 2.0,
182 cx + w / 2.0 + self.spacing / 2.0,
183 max_levels,
184 ) else {
185 continue;
186 };
187 let cy = base - lv as f64 * step;
188 let edge = if lv > 0 {
189 cy + box_h / 2.0
190 } else {
191 cy - box_h / 2.0
192 };
193 super::clear_mark(backend);
194 backend.draw_line(
195 &[(x, base), (x, edge)],
196 &LineStyle {
197 color,
198 alpha: 0.8,
199 width: 1.0,
200 linetype: Linetype::Solid,
201 ..Default::default()
202 },
203 )?;
204 backend.draw_rect(
205 (cx - w / 2.0, cy - box_h / 2.0),
206 (cx + w / 2.0, cy + box_h / 2.0),
207 &RectStyle {
208 fill: Some(self.box_fill),
209 stroke: Some(color),
210 stroke_width: 1.0,
211 alpha: 1.0,
212 clip: false,
213 corner_radius: 3.0,
214 },
215 )?;
216 backend.draw_text(
217 &text,
218 (cx, cy),
219 &TextStyle {
220 color: self.label_color,
221 size: self.label_size,
222 anchor: TextAnchor::Middle,
223 angle: 0.0,
224 family: family.clone(),
225 face: FontFace::Plain,
226 bg_colour: None,
227 bg_r: 0.0,
228 },
229 )?;
230 }
231 super::clear_mark(backend);
232 super::clear_row_extras(backend);
233 Ok(())
234 }
235
236 fn required_aes(&self) -> Vec<Aesthetic> {
237 vec![Aesthetic::X]
238 }
239
240 fn setup_data(&self, data: &mut DataFrame, _warnings: &mut Vec<String>) {
243 let n = data.nrows();
245 for (c, v) in [("ymin", -1.0), ("ymax", 1.0)] {
246 if !data.has_column(c) {
247 data.add_column(c.into(), vec![Value::Float(v); n]);
248 }
249 }
250 if let Some(end) = self.end.as_deref().and_then(|c| data.column(c)) {
251 let end = end.to_vec();
252 if !data.has_column("xmax") {
253 data.add_column("xmax".into(), end);
254 }
255 }
256 }
257 fn default_stat(&self) -> Box<dyn Stat> {
258 Box::new(StatIdentity)
259 }
260 fn default_position(&self) -> Box<dyn Position> {
261 Box::new(PositionIdentity)
262 }
263 fn default_params(&self) -> GeomParams {
264 GeomParams::default()
265 }
266 fn name(&self) -> &str {
267 "timeline"
268 }
269 fn consumes_tooltips(&self) -> bool {
270 true
271 }
272 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
273 if let Some(ink) = el.ink {
274 self.label_color = ink;
275 }
276 if let (true, Some(paper)) = (self.box_fill == (255, 255, 255), el.paper) {
277 self.box_fill = paper;
278 }
279 }
280 fn set_series_color(&mut self, color: (u8, u8, u8)) {
281 self.color = color;
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn levels_alternate_and_avoid_overlap() {
291 assert_eq!((0..4).map(level).collect::<Vec<_>>(), vec![1, -1, 2, -2]);
292 let mut occ = Vec::new();
293 assert_eq!(pick_level(&mut occ, 0.0, 10.0, 3), Some(1));
294 assert_eq!(pick_level(&mut occ, 5.0, 15.0, 3), Some(-1));
295 assert_eq!(pick_level(&mut occ, 8.0, 12.0, 3), Some(2));
296 assert_eq!(pick_level(&mut occ, 20.0, 30.0, 3), Some(1), "free again");
297 assert_eq!(
298 pick_level(&mut occ, 0.0, 30.0, 1),
299 None,
300 "only ±1 allowed, both busy"
301 );
302 }
303}