celestial_pointing/plot/
terminal.rs1use textplots::{Chart, Plot, Shape};
2
3pub fn scatter_terminal(
4 points: &[(f64, f64)],
5 title: &str,
6 x_label: &str,
7 y_label: &str,
8) -> String {
9 if points.is_empty() {
10 return format!("{title}\n (no data)\n");
11 }
12 let f32_pts = to_f32_points(points);
13 let (xmin, xmax) = f32_extent(f32_pts.iter().map(|p| p.0));
14 let chart_body = render_chart(&f32_pts, xmin, xmax);
15 format!("{title}\n {y_label} vs {x_label}\n{chart_body}")
16}
17
18pub fn histogram_terminal(values: &[f64], title: &str, label: &str) -> String {
19 if values.is_empty() {
20 return format!("{title}\n (no data)\n");
21 }
22 let (bins, bin_width, min_val) = bin_values(values, 20);
23 let max_count = bins.iter().copied().max().unwrap_or(1);
24 let (mean, sigma) = mean_sigma(values);
25 let mut out = format!("{title}\n\n");
26 for (i, &count) in bins.iter().enumerate() {
27 let edge = min_val + i as f64 * bin_width;
28 let bar_len = if max_count > 0 {
29 (count * 40) / max_count
30 } else {
31 0
32 };
33 let bar: String = "\u{2588}".repeat(bar_len);
34 out.push_str(&format!(" {edge:>8.1} \u{2502}{bar}\n"));
35 }
36 out.push_str(&format!(
37 "\n {label} mean: {mean:.1}\" sigma: {sigma:.1}\"\n"
38 ));
39 out
40}
41
42pub fn xy_plot_terminal(
43 points: &[(f64, f64)],
44 title: &str,
45 x_label: &str,
46 y_label: &str,
47) -> String {
48 if points.is_empty() {
49 return format!("{title}\n (no data)\n");
50 }
51 let f32_pts = to_f32_points(points);
52 let (xmin, xmax) = f32_extent(f32_pts.iter().map(|p| p.0));
53 let chart_body = render_chart(&f32_pts, xmin, xmax);
54 format!("{title}\n {y_label} vs {x_label}\n{chart_body}")
55}
56
57fn render_chart(pts: &[(f32, f32)], xmin: f32, xmax: f32) -> String {
58 let shape = Shape::Points(pts);
59 let mut chart = Chart::new(80, 24, xmin, xmax);
60 let rendered = chart.lineplot(&shape);
61 rendered.axis();
62 rendered.figures();
63 format!("{rendered}")
64}
65
66fn to_f32_points(points: &[(f64, f64)]) -> Vec<(f32, f32)> {
67 points.iter().map(|&(x, y)| (x as f32, y as f32)).collect()
68}
69
70fn f32_extent(iter: impl Iterator<Item = f32>) -> (f32, f32) {
71 let (lo, hi) = iter.fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), v| {
72 (lo.min(v), hi.max(v))
73 });
74 if (hi - lo).abs() < 1e-6 {
75 (lo - 1.0, hi + 1.0)
76 } else {
77 (lo, hi)
78 }
79}
80
81fn bin_values(values: &[f64], n_bins: usize) -> (Vec<usize>, f64, f64) {
82 let (min_val, max_val) = f64_extent(values.iter().copied());
83 let range = (max_val - min_val).max(1e-10);
84 let bin_width = range / n_bins as f64;
85 let mut bins = vec![0usize; n_bins];
86 for &v in values {
87 let idx = libm::floor((v - min_val) / bin_width) as usize;
88 bins[idx.min(n_bins - 1)] += 1;
89 }
90 (bins, bin_width, min_val)
91}
92
93fn f64_extent(iter: impl Iterator<Item = f64>) -> (f64, f64) {
94 iter.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), v| {
95 (lo.min(v), hi.max(v))
96 })
97}
98
99fn mean_sigma(values: &[f64]) -> (f64, f64) {
100 let n = values.len() as f64;
101 let mean = values.iter().sum::<f64>() / n;
102 let variance = values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / n;
103 (mean, libm::sqrt(variance))
104}