1use crate::build::BuiltPlot;
17use crate::data::{format_epoch_secs, DataFrame, Value};
18
19const SERIES_COLS: &[&str] = &["colour", "color", "fill", "group"];
21pub const MAX_SERIES: usize = 12;
23
24#[derive(Debug, Clone, PartialEq)]
26pub struct LayerSummary {
27 pub geom: String,
29 pub panel: Option<String>,
31 pub series: Vec<SeriesSummary>,
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct SeriesSummary {
37 pub key: Option<String>,
39 pub n: usize,
41 pub x_from: String,
44 pub x_to: String,
45 pub y_first: f64,
46 pub y_last: f64,
47 pub y_min: (f64, String),
49 pub y_max: (f64, String),
50 pub band: Option<(f64, f64)>,
52}
53
54#[derive(Debug, Clone, PartialEq, Default)]
56pub struct PlotSummary {
57 pub layers: Vec<LayerSummary>,
58}
59
60impl BuiltPlot {
61 pub fn summary(&self) -> PlotSummary {
63 let date_x = self.layers.iter().any(|l| {
66 l.data
67 .column("x")
68 .is_some_and(|c| c.iter().any(|v| v.is_datetime()))
69 });
70 let mut layers = Vec::new();
71 let faceted = self.panels.len() > 1 && !self.panels_data.is_empty();
72 if faceted {
73 for (p, data) in self.panels.iter().zip(&self.panels_data) {
74 for (l, d) in self.layers.iter().zip(data) {
75 if let Some(s) = layer_summary(l.geom.name(), d, date_x) {
76 layers.push(LayerSummary {
77 panel: Some(p.label.clone()),
78 ..s
79 });
80 }
81 }
82 }
83 } else {
84 for l in &self.layers {
85 layers.extend(layer_summary(l.geom.name(), &l.data, date_x));
86 }
87 }
88 let mut merged: Vec<LayerSummary> = Vec::new();
91 for l in layers {
92 match merged.last_mut() {
93 Some(m) if m.panel == l.panel && m.series == l.series => {
94 m.geom = format!("{}+{}", m.geom, l.geom);
95 }
96 _ => merged.push(l),
97 }
98 }
99 PlotSummary { layers: merged }
100 }
101
102 pub fn summary_text(&self) -> String {
104 self.summary().to_string()
105 }
106}
107
108impl std::fmt::Display for PlotSummary {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 for (i, l) in self.layers.iter().enumerate() {
111 if i > 0 {
112 writeln!(f)?;
113 }
114 match &l.panel {
115 Some(p) => write!(f, "{} [{p}]:", l.geom)?,
116 None => write!(f, "{}:", l.geom)?,
117 }
118 let shown = l.series.len().min(MAX_SERIES);
119 for (j, s) in l.series[..shown].iter().enumerate() {
120 if j > 0 {
121 write!(f, ";")?;
122 }
123 if let Some(k) = &s.key {
124 write!(f, " {k} —")?;
125 }
126 if let (true, Some((lo, hi))) = (l.geom == "ribbon", s.band) {
128 write!(
129 f,
130 " {} mark{}, x {} … {}, band {} … {}",
131 s.n,
132 if s.n == 1 { "" } else { "s" },
133 s.x_from,
134 s.x_to,
135 num(lo),
136 num(hi)
137 )?;
138 continue;
139 }
140 write!(
141 f,
142 " {} mark{}, x {} … {}; y first {}, last {}, min {} at {}, max {} at {}",
143 s.n,
144 if s.n == 1 { "" } else { "s" },
145 s.x_from,
146 s.x_to,
147 num(s.y_first),
148 num(s.y_last),
149 num(s.y_min.0),
150 s.y_min.1,
151 num(s.y_max.0),
152 s.y_max.1
153 )?;
154 if let Some((lo, hi)) = s.band {
155 write!(f, ", band {} … {}", num(lo), num(hi))?;
156 }
157 }
158 if l.series.len() > shown {
159 write!(f, "; +{} more series", l.series.len() - shown)?;
160 }
161 }
162 Ok(())
163 }
164}
165
166fn layer_summary(geom: &str, d: &DataFrame, date_x: bool) -> Option<LayerSummary> {
167 let x = d.column("x")?;
168 let y = d.column(".y_raw").or_else(|| d.column("y"))?;
170 let key = SERIES_COLS.iter().find_map(|c| d.column(c));
171 let (ymin, ymax) = (d.column("ymin"), d.column("ymax"));
172 let banded = matches!(
175 geom,
176 "ribbon" | "smooth" | "errorbar" | "linerange" | "pointrange" | "crossbar"
177 );
178 let mut order: Vec<String> = Vec::new();
179 let mut rows: Vec<Vec<usize>> = Vec::new();
180 for i in 0..d.nrows() {
181 let k = key.map(|k| label(&k[i], false)).unwrap_or_default();
182 let at = match order.iter().position(|o| *o == k) {
183 Some(at) => at,
184 None => {
185 order.push(k);
186 rows.push(Vec::new());
187 order.len() - 1
188 }
189 };
190 rows[at].push(i);
191 }
192 let series: Vec<SeriesSummary> = order
193 .into_iter()
194 .zip(rows)
195 .filter_map(|(k, ix)| {
196 let pts: Vec<(usize, f64)> = ix
197 .into_iter()
198 .filter_map(|i| y[i].as_f64().filter(|v| v.is_finite()).map(|v| (i, v)))
199 .collect();
200 let (&(i0, y0), &(i1, y1)) = (pts.first()?, pts.last()?);
201 let lo = pts
202 .iter()
203 .fold(pts[0], |a, b| if b.1 < a.1 { *b } else { a });
204 let hi = pts
205 .iter()
206 .fold(pts[0], |a, b| if b.1 > a.1 { *b } else { a });
207 let xl = |i: usize| label(&x[i], date_x);
208 let band = banded.then(|| {
209 let f = |c: Option<&[Value]>, pick: fn(f64, f64) -> f64| {
210 c.and_then(|c| {
211 pts.iter()
212 .filter_map(|(i, _)| c[*i].as_f64().filter(|v| v.is_finite()))
213 .reduce(pick)
214 })
215 };
216 f(ymin, f64::min).zip(f(ymax, f64::max))
217 });
218 let xs: Vec<(usize, f64)> = pts
221 .iter()
222 .filter(|(i, _)| !matches!(x[*i], Value::Str(_)))
223 .filter_map(|(i, _)| x[*i].as_f64().map(|v| (*i, v)))
224 .collect();
225 let (x_from, x_to) = if xs.len() == pts.len() {
226 let a = xs.iter().fold(xs[0], |a, b| if b.1 < a.1 { *b } else { a });
227 let b = xs.iter().fold(xs[0], |a, b| if b.1 > a.1 { *b } else { a });
228 (xl(a.0), xl(b.0))
229 } else {
230 (xl(i0), xl(i1))
231 };
232 Some(SeriesSummary {
233 key: key.map(|_| k),
234 n: pts.len(),
235 x_from,
236 x_to,
237 y_first: y0,
238 y_last: y1,
239 y_min: (lo.1, xl(lo.0)),
240 y_max: (hi.1, xl(hi.0)),
241 band: band.flatten(),
242 })
243 })
244 .collect();
245 (!series.is_empty()).then(|| LayerSummary {
246 geom: geom.to_string(),
247 panel: None,
248 series,
249 })
250}
251
252fn label(v: &Value, date_x: bool) -> String {
255 let date = |secs: f64| {
256 let s = format_epoch_secs(secs.round() as i64);
257 s.strip_suffix(" 00:00:00").map(str::to_string).unwrap_or(s)
258 };
259 match v {
260 Value::DateTime(_) => date(v.as_f64().unwrap_or(0.0)),
261 Value::Str(s) => s.clone(),
262 Value::Na => "NA".into(),
263 _ => match v.as_f64() {
264 Some(n) if date_x => date(n),
265 Some(n) => num(n),
266 None => v.key_str().into_owned(),
267 },
268 }
269}
270
271fn num(n: f64) -> String {
273 if n == n.trunc() && n.abs() < 1e15 {
274 return format!("{}", n as i64);
275 }
276 let digits = (4 - n.abs().log10().floor() as i32).clamp(0, 6) as usize;
277 let s = format!("{n:.digits$}");
278 let s = s.trim_end_matches('0').trim_end_matches('.');
279 s.to_string()
280}
281
282#[cfg(test)]
283mod tests {
284 use crate::prelude::*;
285
286 fn strs(v: &[&str]) -> Vec<Value> {
287 v.iter().map(|s| Value::Str(s.to_string())).collect()
288 }
289
290 #[test]
291 fn series_dates_and_a_smooth_band() {
292 let day = |d: i64| Value::from_timestamp(1_700_006_400 + d * 86_400);
293 let df = vec![
294 (
295 "x".to_string(),
296 vec![day(0), day(1), day(2), day(0), day(1), day(2)],
297 ),
298 (
299 "y".to_string(),
300 [3.0, 5.0, 1.0, 2.0, 2.5, 4.0].map(Value::Float).to_vec(),
301 ),
302 ("g".to_string(), strs(&["A", "A", "A", "B", "B", "B"])),
303 ];
304 let s = GGPlot::new(df)
305 .aes(Aes::new().x("x").y("y").color("g"))
306 .geom_line()
307 .geom_smooth()
308 .build()
309 .summary();
310 let line = &s.layers[0];
311 assert_eq!(line.geom, "line");
312 assert_eq!(line.series.len(), 2);
313 let a = &line.series[0];
314 assert_eq!(a.key.as_deref(), Some("A"));
315 assert_eq!(
316 (a.n, a.x_from.as_str(), a.x_to.as_str()),
317 (3, "2023-11-15", "2023-11-17")
318 );
319 assert_eq!(a.y_max, (5.0, "2023-11-16".to_string()));
320 assert_eq!(a.y_min, (1.0, "2023-11-17".to_string()));
321 let smooth = &s.layers[1];
322 assert_eq!(smooth.geom, "smooth");
323 assert!(
325 smooth.series[0].x_from.starts_with("2023-11-15"),
326 "{smooth:?}"
327 );
328 assert!(smooth.series[0].band.is_some());
329 }
330
331 #[test]
332 fn stacked_bars_report_their_own_values_and_facets_their_panel() {
333 let df = vec![
334 ("x".to_string(), strs(&["W1", "W1", "W2", "W2"])),
335 (
336 "y".to_string(),
337 [3.0, 5.0, 7.0, 2.0].map(Value::Float).to_vec(),
338 ),
339 ("c".to_string(), strs(&["a", "b", "a", "b"])),
340 ];
341 let s = GGPlot::new(df.clone())
342 .aes(Aes::new().x("x").y("y").fill("c"))
343 .geom_col()
344 .position(PositionStack)
345 .build()
346 .summary();
347 let b = &s.layers[0].series[1];
348 assert_eq!(b.key.as_deref(), Some("b"));
349 assert_eq!(
350 (b.y_first, b.y_last),
351 (5.0, 2.0),
352 "own values, not the stack tops"
353 );
354 assert_eq!(b.band, None, "a bar's base is not a band");
355
356 let s = GGPlot::new(df)
357 .aes(Aes::new().x("x").y("y"))
358 .geom_col()
359 .facet_wrap("c", None)
360 .build()
361 .summary();
362 let panels: Vec<_> = s.layers.iter().map(|l| l.panel.clone().unwrap()).collect();
363 assert_eq!(panels, ["a", "b"]);
364 assert_eq!(s.layers[1].series[0].y_max, (5.0, "W1".to_string()));
365 }
366
367 #[test]
368 fn scatter_x_extent_ribbon_band_and_markers_merged() {
369 let df = vec![
370 ("x".to_string(), [5.0, 1.0, 9.0].map(Value::Float).to_vec()),
371 ("y".to_string(), [2.0, 4.0, 3.0].map(Value::Float).to_vec()),
372 ("lo".to_string(), [1.0, 3.0, 2.0].map(Value::Float).to_vec()),
373 ("hi".to_string(), [3.0, 6.0, 4.0].map(Value::Float).to_vec()),
374 ];
375 let t = GGPlot::new(df)
376 .aes(Aes::new().x("x").y("y").ymin("lo").ymax("hi"))
377 .geom_ribbon()
378 .geom_line()
379 .geom_point()
380 .build()
381 .summary_text();
382 let lines: Vec<&str> = t.lines().collect();
383 assert_eq!(lines[0], "ribbon: 3 marks, x 1 … 9, band 1 … 6", "{t}");
384 assert!(
385 lines[1].starts_with("line+point: 3 marks, x 1 … 9; y first"),
386 "{t}"
387 );
388 assert_eq!(lines.len(), 2, "{t}");
389 }
390
391 #[test]
392 fn many_series_are_counted() {
393 let n = 15;
394 let df = vec![
395 (
396 "x".to_string(),
397 (0..n).map(|i| Value::Float(i as f64)).collect(),
398 ),
399 (
400 "y".to_string(),
401 (0..n).map(|i| Value::Float(i as f64 * 1.5)).collect(),
402 ),
403 (
404 "g".to_string(),
405 (0..n).map(|i| Value::Str(format!("s{i}"))).collect(),
406 ),
407 ];
408 let t = GGPlot::new(df)
409 .aes(Aes::new().x("x").y("y").color("g"))
410 .geom_point()
411 .build()
412 .summary_text();
413 assert!(t.ends_with("; +3 more series"), "{t}");
414 assert!(t.contains("s1 — 1 mark, x 1 … 1; y first 1.5"), "{t}");
415 }
416}