use crate::output::width::text_columns;
const STEPS: [char; 8] = [
'\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}',
];
#[must_use]
pub fn gauge_fill(value: u64, ceiling: Option<u64>, cells: usize) -> usize {
match ceiling {
Some(ceiling) if ceiling > 0 => {
let scaled = (value as f64 / ceiling as f64 * cells as f64).round();
(scaled as usize).min(cells)
}
_ => 0,
}
}
#[must_use]
pub fn gauge(value: u64, ceiling: Option<u64>, cells: usize) -> String {
let filled = gauge_fill(value, ceiling, cells);
let mut out = String::with_capacity(cells * 3);
out.extend(std::iter::repeat_n('\u{2588}', filled));
out.extend(std::iter::repeat_n('\u{2591}', cells - filled));
out
}
#[must_use]
pub fn sparkline(samples: &[f32], cells: usize, ceiling: f32) -> String {
if samples.is_empty() || cells == 0 {
return " ".repeat(cells);
}
let window = &samples[samples.len().saturating_sub(cells)..];
let mut out = String::with_capacity(cells * 3);
for _ in window.len()..cells {
out.push(' ');
}
let ceiling = if ceiling > 0.0 { ceiling } else { 1.0 };
for sample in window {
let clamped = sample.clamp(0.0, ceiling);
let scaled = (clamped / ceiling * (STEPS.len() - 1) as f32).round();
let step = (scaled as usize).min(STEPS.len() - 1);
out.push(STEPS[step]);
}
out
}
#[must_use]
pub fn chart(samples: &[f32], ceiling: f32, cols: usize, rows: usize) -> Vec<String> {
let window = &samples[samples.len().saturating_sub(cols)..];
let pad = cols - window.len();
let ceiling = if ceiling > 0.0 { ceiling } else { 1.0 };
let steps = rows * 2;
let heights: Vec<usize> = window
.iter()
.map(|sample| (sample / ceiling * steps as f32).round() as usize)
.collect();
(0..rows)
.map(|row| {
let floor = (rows - 1 - row) * 2;
let mut line = " ".repeat(pad);
for height in &heights {
line.push(match height.saturating_sub(floor) {
0 => ' ',
1 => '\u{2584}',
_ => '\u{2588}',
});
}
line
})
.collect()
}
#[must_use]
pub fn rule(cells: usize) -> String {
"\u{2500}".repeat(cells)
}
#[must_use]
pub fn band(label: &str, cells: usize) -> String {
let head = format!(" \u{2588}\u{2588} {label}");
let drawn: usize = text_columns(&head);
if drawn >= cells {
return super::flock::fit(&head, cells as u16);
}
let mut out = head;
out.extend(std::iter::repeat_n(' ', cells - drawn));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_gauge_fills_from_the_left_and_pads_with_the_light_shade() {
assert_eq!(gauge(0, Some(100), 10), "░░░░░░░░░░");
assert_eq!(gauge(50, Some(100), 10), "█████░░░░░");
assert_eq!(gauge(100, Some(100), 10), "██████████");
}
#[test]
fn a_gauge_over_its_ceiling_is_full_rather_than_wider() {
assert_eq!(gauge(250, Some(100), 10), "██████████");
}
#[test]
fn a_gauge_with_no_ceiling_is_all_tail() {
assert_eq!(gauge(48, None, 10), "░░░░░░░░░░");
}
#[test]
fn a_zero_ceiling_is_all_tail_rather_than_a_division_by_zero() {
assert_eq!(gauge(48, Some(0), 10), "░░░░░░░░░░");
}
#[test]
fn a_gauge_rounds_to_the_nearest_cell() {
assert_eq!(gauge(44, Some(100), 10), "████░░░░░░");
assert_eq!(gauge(45, Some(100), 10), "█████░░░░░");
}
#[test]
fn a_chart_at_the_ceiling_fills_every_row() {
assert_eq!(chart(&[100.0], 100.0, 1, 4), ["█", "█", "█", "█"]);
}
#[test]
fn a_chart_at_half_the_ceiling_fills_the_bottom_half() {
assert_eq!(chart(&[50.0], 100.0, 1, 4), [" ", " ", "█", "█"]);
}
#[test]
fn an_odd_half_step_draws_the_lower_half_block() {
assert_eq!(chart(&[12.5], 100.0, 1, 4), [" ", " ", " ", "▄"]);
}
#[test]
fn a_chart_over_its_ceiling_fills_its_column() {
assert_eq!(chart(&[250.0], 100.0, 1, 2), ["█", "█"]);
}
#[test]
fn a_negative_sample_reads_as_blank() {
assert_eq!(chart(&[-50.0], 100.0, 1, 2), [" ", " "]);
}
#[test]
fn a_chart_shorter_than_its_columns_pads_on_the_left() {
assert_eq!(chart(&[100.0], 100.0, 3, 1), [" █"]);
}
#[test]
fn a_chart_longer_than_its_columns_keeps_the_newest() {
assert_eq!(chart(&[100.0, 0.0, 0.0], 100.0, 2, 1), [" "]);
}
#[test]
fn an_empty_chart_is_blank_rather_than_a_floor_line() {
assert_eq!(chart(&[], 100.0, 3, 2), [" ", " "]);
}
#[test]
fn a_sparkline_is_one_cell_per_sample_scaled_to_a_fixed_ceiling() {
assert_eq!(sparkline(&[0.0, 50.0, 100.0], 3, 100.0), "▁▅█");
}
#[test]
fn a_sparkline_over_its_ceiling_is_full_rather_than_wider() {
assert_eq!(sparkline(&[250.0], 4, 100.0), " █");
}
#[test]
fn a_sparkline_shorter_than_its_cells_pads_on_the_left() {
assert_eq!(sparkline(&[100.0], 4, 100.0), " █");
}
#[test]
fn a_sparkline_longer_than_its_cells_keeps_the_newest() {
assert_eq!(sparkline(&[100.0, 0.0, 0.0], 2, 100.0), "▁▁");
}
#[test]
fn an_empty_sparkline_is_blank_rather_than_a_flat_line() {
assert_eq!(sparkline(&[], 4, 100.0), " ");
}
#[test]
fn a_flat_sparkline_sits_at_the_floor() {
assert_eq!(sparkline(&[0.0, 0.0, 0.0], 3, 100.0), "▁▁▁");
}
#[test]
fn two_rows_at_different_loads_render_differently() {
let idle = sparkline(&[0.5, 0.5, 0.5], 3, 100.0);
let busy = sparkline(&[80.0, 80.0, 80.0], 3, 100.0);
assert_ne!(idle, busy);
assert_eq!(idle, "▁▁▁");
}
#[test]
fn a_shared_ceiling_keeps_a_busy_row_taller_than_a_quiet_one() {
let ceiling = 40.0;
assert_eq!(sparkline(&[4.0, 4.0, 4.0], 3, ceiling), "▂▂▂");
assert_eq!(sparkline(&[40.0, 40.0, 40.0], 3, ceiling), "███");
}
#[test]
fn a_ceiling_of_zero_does_not_divide_by_zero() {
assert_eq!(sparkline(&[0.0, 1.0], 2, 0.0), "▁█");
}
#[test]
fn a_rule_is_exactly_its_cells() {
assert_eq!(rule(4), "────");
assert_eq!(rule(0), "");
}
#[test]
fn a_band_marks_its_label_and_pads_to_width() {
assert_eq!(band("FLOCK", 20), " ██ FLOCK ");
}
#[test]
fn a_band_narrower_than_its_label_truncates_rather_than_overflowing() {
assert_eq!(band("FLOCK", 6), " ██ F…");
}
}