Skip to main content

data_beans/figure/
stat.rs

1//! The statistics figure: the histogram of each of nnz, sum, mean and sd
2//! over the rows or the columns, and mean against sd with the Poisson line
3//! `sd = √mean`. Marked entries are drawn in the accent on every panel;
4//! the entry under the explorer's cursor gets a pointer.
5
6use super::*;
7
8/// The statistics in figure order.
9pub const STATS: [&str; 4] = ["nnz", "sum", "mean", "sd"];
10
11/// What the figure is drawn from.
12pub struct StatData<'a> {
13    /// The data's name, for the title.
14    pub title: &'a str,
15    /// `rows` or `columns`.
16    pub side: &'a str,
17    /// nnz, sum, mean and sd per entry.
18    pub values: [&'a [f32]; 4],
19    /// Entries marked, if any.
20    pub marked: Option<&'a [bool]>,
21    /// The entry under the cursor, with its name.
22    pub cursor: Option<(usize, &'a str)>,
23}
24
25/// The histogram of statistic `k` (see [`STATS`]) in the box given.
26pub fn draw_stat(d: &StatData, k: usize, c: &mut Canvas, (x, y, w, h): (f64, f64, f64, f64)) {
27    let values = d.values[k];
28    let marked = d.marked.filter(|m| m.iter().any(|&b| b));
29    let mut sorted: Vec<f32> = values.iter().copied().filter(|v| v.is_finite()).collect();
30    sorted.sort_by(f32::total_cmp);
31    let median = sorted.get(sorted.len() / 2).copied().unwrap_or(0.0);
32    let mut subtitle = format!("median {}", short(median as f64));
33    if let Some(m) = marked {
34        subtitle.push_str(&format!(" · {} marked", m.iter().filter(|&&b| b).count()));
35    }
36    if let Some((i, name)) = d.cursor {
37        let rank = sorted.len() - sorted.partition_point(|&v| v <= values[i]) + 1;
38        subtitle.push_str(&format!(" · ▲ {name}, rank {rank} of {}", sorted.len()));
39    }
40    let side = d.side.trim_end_matches('s');
41    let mut layers = vec![(None, BAR)];
42    if let Some(m) = marked {
43        layers.push((Some(m), ACCENT));
44    }
45    Hist {
46        title: format!("{} per {side}", STATS[k]),
47        subtitle,
48        x_title: format!("{} per {side}", STATS[k]),
49        axis: Axis::fit(values, true),
50        values,
51        layers,
52        edge: None,
53        cursor: d.cursor.map(|(i, _)| values[i] as f64),
54    }
55    .draw(c, x, y, w, h);
56}
57
58/// Mean against sd per entry, log on both, with the Poisson line.
59pub fn draw_mean_sd(d: &StatData, c: &mut Canvas, (x, y, w, h): (f64, f64, f64, f64)) {
60    let (mean, sd) = (d.values[2], d.values[3]);
61    let marked = d.marked.filter(|m| m.iter().any(|&b| b));
62    let points: Vec<Point> = (0..mean.len())
63        .map(|i| {
64            let on = marked.is_some_and(|m| m[i]) || d.cursor.is_some_and(|(k, _)| k == i);
65            Point {
66                x: mean[i],
67                y: sd[i],
68                fill: if on { ACCENT } else { BAR },
69                ringed: false,
70            }
71        })
72        .collect();
73    let (xa, ya) = (Axis::fit(mean, true), Axis::fit(sd, true));
74    let side = d.side.trim_end_matches('s');
75    let mut key = vec![];
76    if marked.is_some() {
77        key.push((ACCENT, "marked".to_string()));
78    }
79    Scatter {
80        title: "Mean against sd".into(),
81        subtitle: format!("per {side} · dashed: Poisson, sd = √mean"),
82        x_title: "mean".into(),
83        y_title: "sd".into(),
84        x: xa,
85        y: ya,
86        points: &points,
87        x_edge: None,
88        y_edge: None,
89        key,
90    }
91    .draw(c, x, y, w, h);
92    // The Poisson line, over the plot area Scatter used.
93    let (px, py, pw, ph) = plot_area(x, y, w, h);
94    let line: Vec<(f64, f64)> = (0..=60)
95        .map(|k| xa.at(k as f64 / 60.0))
96        .filter(|&m| m.sqrt() >= ya.lo && m.sqrt() <= ya.hi)
97        .map(|m| (px + xa.frac(m) * pw, py + ph - ya.frac(m.sqrt()) * ph))
98        .collect();
99    if line.len() > 1 {
100        c.polyline(&line, MUTED, 0.9, true);
101    }
102}
103
104/// The whole figure.
105pub fn figure(d: &StatData) -> String {
106    let (cols, pw, ph) = (3usize, 320.0, 230.0);
107    let (left, top) = (16.0, 74.0);
108    let (w, h) = (left * 2.0 + cols as f64 * pw, top + 2.0 * ph + 12.0);
109    let mut c = Canvas::new(w, h);
110    let n = d.values[0].len();
111    c.bold(
112        left + 8.0,
113        28.0,
114        &format!("{} · {} statistics", d.title, d.side.trim_end_matches('s')),
115        15.0,
116        Anchor::Start,
117        INK,
118    );
119    c.text(
120        left + 8.0,
121        46.0,
122        &format!("{n} {}", d.side),
123        10.0,
124        Anchor::Start,
125        MUTED,
126    );
127    let mut boxes = (0..6).map(|k| {
128        (
129            left + (k % cols) as f64 * pw,
130            top + (k / cols) as f64 * ph,
131            pw - 12.0,
132            ph - 14.0,
133        )
134    });
135    for k in 0..4 {
136        draw_stat(d, k, &mut c, boxes.next().unwrap());
137    }
138    draw_mean_sd(d, &mut c, boxes.next().unwrap());
139    c.finish()
140}