Skip to main content

celestial_pointing/plot/
terminal.rs

1use 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}