use std::fmt;
use std::fmt::Write;
use super::compact_display::CompactDisplay;
use super::detailed_display::DetailedDisplay;
use super::histogram::Histogram;
use super::series::Series;
pub(crate) const BAR_CHARS: [char; 8] = ['█', '▒', '░', '▓', '▞', '▚', '▖', '▘'];
#[derive(Debug, Clone)]
pub struct AsciiChart<T = (), const WIDTH: usize = 3> {
pub(crate) series: Vec<Series<T, WIDTH>>,
bar_width: usize,
}
impl<T, const WIDTH: usize> Default for AsciiChart<T, WIDTH> {
fn default() -> Self {
Self::new()
}
}
impl<T, const WIDTH: usize> AsciiChart<T, WIDTH> {
pub fn new() -> Self {
Self {
series: Vec::new(),
bar_width: 40,
}
}
pub fn from_series(series: impl IntoIterator<Item = (impl ToString, Histogram<T, WIDTH>)>) -> Self {
Self {
series: series.into_iter().map(|(name, hist)| Series::new(name.to_string(), hist)).collect(),
bar_width: 40,
}
}
pub fn add(mut self, name: &str, hist: Histogram<T, WIDTH>) -> Self {
self.series.push(Series::new(name, hist));
self
}
pub fn bar_width(mut self, width: usize) -> Self {
self.bar_width = width;
self
}
pub fn compact(&self) -> CompactDisplay<'_, T, WIDTH> {
CompactDisplay::new(self)
}
pub fn detailed(&self) -> DetailedDisplay<'_, T, WIDTH> {
DetailedDisplay::new(self)
}
pub(crate) fn non_empty_indices(&self) -> Vec<usize> {
(0..Histogram::<T, WIDTH>::total_buckets())
.filter(|&i| self.series.iter().any(|s| s.histogram.bucket(i).count() > 0))
.collect()
}
pub(crate) fn max_stacked_count(&self, indices: &[usize]) -> u64 {
indices
.iter()
.map(|&i| self.series.iter().map(|s| s.histogram.bucket(i).count()).sum::<u64>())
.max()
.unwrap_or(0)
}
pub(crate) fn write_bar(&self, f: &mut fmt::Formatter<'_>, bucket_idx: usize, max_count: u64) -> fmt::Result {
if max_count == 0 {
return Ok(());
}
for (si, series) in self.series.iter().enumerate() {
let count = series.histogram.bucket(bucket_idx).count();
let segment = if count > 0 {
(count as f64 / max_count as f64 * self.bar_width as f64).round().max(1.0) as usize
} else {
0
};
let ch = BAR_CHARS[si % BAR_CHARS.len()];
for _ in 0..segment {
f.write_char(ch)?;
}
}
Ok(())
}
}
impl<T, const WIDTH: usize> fmt::Display for AsciiChart<T, WIDTH> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.detailed().fmt(f)
}
}
pub(crate) fn digit_count(n: u64) -> usize {
if n == 0 {
return 1;
}
(n.ilog10() + 1) as usize
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_digit_count() {
assert_eq!(digit_count(0), 1);
assert_eq!(digit_count(1), 1);
assert_eq!(digit_count(9), 1);
assert_eq!(digit_count(10), 2);
assert_eq!(digit_count(99), 2);
assert_eq!(digit_count(100), 3);
assert_eq!(digit_count(999), 3);
assert_eq!(digit_count(u64::MAX), 20);
}
#[test]
fn test_empty_chart() {
let chart: AsciiChart = AsciiChart::new();
assert_eq!(chart.compact().to_string(), "");
assert_eq!(chart.detailed().to_string(), "");
}
#[test]
fn test_empty_histogram() {
let hist = Histogram::<()>::new();
let chart = AsciiChart::new().add("test", hist.clone());
assert_eq!(chart.compact().to_string(), "");
assert_eq!(chart.detailed().to_string(), "");
}
#[test]
fn test_display_uses_detailed() {
let mut hist = Histogram::<()>::new();
hist.record_n(5, 10);
let chart = AsciiChart::new().add("test", hist.clone());
assert_eq!(format!("{}", chart), chart.detailed().to_string());
}
}