use presentar_core::{Canvas, Color, Point, TextStyle};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HeatScheme {
#[default]
Thermal,
Cool,
Warm,
Mono,
}
impl HeatScheme {
pub fn color_for_percent(&self, pct: f64) -> Color {
let p = pct.clamp(0.0, 100.0) / 100.0;
match self {
Self::Thermal => {
if p < 0.5 {
let t = p * 2.0;
Color::new(t as f32, 0.8, 0.2, 1.0)
} else {
let t = (p - 0.5) * 2.0;
Color::new(1.0, (0.8 - t * 0.6) as f32, 0.2, 1.0)
}
}
Self::Cool => {
Color::new(0.2, 0.4 + (p * 0.4) as f32, 0.9, 1.0)
}
Self::Warm => {
Color::new(1.0, (0.9 - p * 0.7) as f32, 0.1, 1.0)
}
Self::Mono => {
let v = (0.9 - p * 0.7) as f32;
Color::new(v, v, v, 1.0)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BarStyle {
#[default]
Blocks,
Gradient,
Dots,
Segments,
}
#[derive(Debug, Clone)]
pub struct MicroHeatBar {
values: Vec<f64>,
labels: Vec<String>,
scheme: HeatScheme,
style: BarStyle,
width: usize,
show_values: bool,
}
impl MicroHeatBar {
pub fn new(values: &[f64]) -> Self {
Self {
values: values.to_vec(),
labels: Vec::new(),
scheme: HeatScheme::Thermal,
style: BarStyle::Blocks,
width: 20,
show_values: false,
}
}
pub fn with_labels(mut self, labels: &[&str]) -> Self {
self.labels = labels.iter().map(|s| (*s).to_string()).collect();
self
}
pub fn with_scheme(mut self, scheme: HeatScheme) -> Self {
self.scheme = scheme;
self
}
pub fn with_style(mut self, style: BarStyle) -> Self {
self.style = style;
self
}
pub fn with_width(mut self, width: usize) -> Self {
self.width = width;
self
}
pub fn with_values(mut self, show: bool) -> Self {
self.show_values = show;
self
}
pub fn render_string(&self) -> String {
let total: f64 = self.values.iter().sum();
if total <= 0.0 || self.width == 0 {
return "░".repeat(self.width);
}
let mut result = String::new();
let mut remaining_width = self.width;
for &val in self.values.iter() {
let proportion = val / total;
let char_count =
((proportion * self.width as f64).round() as usize).min(remaining_width);
if char_count == 0 {
continue;
}
let ch = match self.style {
BarStyle::Blocks => {
if val > 70.0 {
'█'
} else if val > 40.0 {
'▓'
} else if val > 20.0 {
'▒'
} else if val > 5.0 {
'░'
} else {
' '
}
}
BarStyle::Gradient => {
let level = ((val / 100.0) * 7.0).round() as usize;
['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][level.min(7)]
}
BarStyle::Dots => {
if val > 50.0 {
'●'
} else {
'○'
}
}
BarStyle::Segments => '█',
};
for _ in 0..char_count {
result.push(ch);
}
remaining_width = remaining_width.saturating_sub(char_count);
}
while result.chars().count() < self.width {
result.push('░');
}
result
}
pub fn paint(&self, canvas: &mut dyn Canvas, pos: Point) {
let total: f64 = self.values.iter().sum();
if total <= 0.0 || self.width == 0 {
return;
}
let mut x = pos.x;
for &val in self.values.iter() {
let proportion = val / total;
let char_count = (proportion * self.width as f64).round() as usize;
if char_count == 0 {
continue;
}
let color = self.scheme.color_for_percent(val);
let ch = match self.style {
BarStyle::Blocks => '█',
BarStyle::Gradient => {
let level = ((val / 100.0) * 7.0).round() as usize;
['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][level.min(7)]
}
BarStyle::Dots => '●',
BarStyle::Segments => '█',
};
let segment: String = std::iter::repeat(ch).take(char_count).collect();
canvas.draw_text(
&segment,
Point::new(x, pos.y),
&TextStyle {
color,
..Default::default()
},
);
x += char_count as f32;
}
let remaining = self.width.saturating_sub((x - pos.x) as usize);
if remaining > 0 {
let bg: String = std::iter::repeat('░').take(remaining).collect();
canvas.draw_text(
&bg,
Point::new(x, pos.y),
&TextStyle {
color: Color::new(0.2, 0.2, 0.2, 1.0),
..Default::default()
},
);
}
}
}
pub struct CompactBreakdown {
values: Vec<f64>,
labels: Vec<String>,
scheme: HeatScheme,
}
impl CompactBreakdown {
pub fn new(labels: &[&str], values: &[f64]) -> Self {
Self {
values: values.to_vec(),
labels: labels.iter().map(|s| (*s).to_string()).collect(),
scheme: HeatScheme::Thermal,
}
}
pub fn with_scheme(mut self, scheme: HeatScheme) -> Self {
self.scheme = scheme;
self
}
pub fn render_text(&self, _width: usize) -> String {
let parts: Vec<String> = self
.labels
.iter()
.zip(self.values.iter())
.map(|(l, v)| format!("{}:{:.0}", l, v))
.collect();
parts.join(" ")
}
pub fn paint(&self, canvas: &mut dyn Canvas, pos: Point) {
let mut x = pos.x;
for (label, &val) in self.labels.iter().zip(self.values.iter()) {
let color = self.scheme.color_for_percent(val);
let text = format!("{}:{:.0} ", label, val);
canvas.draw_text(
&text,
Point::new(x, pos.y),
&TextStyle {
color,
..Default::default()
},
);
x += text.chars().count() as f32;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::direct::{CellBuffer, DirectTerminalCanvas};
#[test]
fn test_heat_scheme_default() {
assert_eq!(HeatScheme::default(), HeatScheme::Thermal);
}
#[test]
fn test_heat_scheme_thermal() {
let scheme = HeatScheme::Thermal;
let low = scheme.color_for_percent(10.0);
let high = scheme.color_for_percent(90.0);
assert!(low.g > low.r);
assert!(high.r > high.g);
}
#[test]
fn test_heat_scheme_thermal_midpoint() {
let scheme = HeatScheme::Thermal;
let mid = scheme.color_for_percent(50.0);
assert!(mid.r > 0.5);
assert!(mid.g > 0.5);
}
#[test]
fn test_heat_scheme_cool() {
let scheme = HeatScheme::Cool;
let low = scheme.color_for_percent(10.0);
let high = scheme.color_for_percent(90.0);
assert!(low.b > low.r);
assert!(high.b > high.r);
assert!(high.g > low.g);
}
#[test]
fn test_heat_scheme_warm() {
let scheme = HeatScheme::Warm;
let low = scheme.color_for_percent(10.0);
let high = scheme.color_for_percent(90.0);
assert!(low.g > high.g);
assert_eq!(low.r, 1.0);
assert_eq!(high.r, 1.0);
}
#[test]
fn test_heat_scheme_mono() {
let scheme = HeatScheme::Mono;
let low = scheme.color_for_percent(10.0);
let high = scheme.color_for_percent(90.0);
assert_eq!(low.r, low.g);
assert_eq!(low.g, low.b);
assert_eq!(high.r, high.g);
assert_eq!(high.g, high.b);
assert!(low.r > high.r);
}
#[test]
fn test_heat_scheme_clamps_values() {
let scheme = HeatScheme::Thermal;
let neg = scheme.color_for_percent(-50.0);
let over = scheme.color_for_percent(150.0);
let zero = scheme.color_for_percent(0.0);
let hundred = scheme.color_for_percent(100.0);
assert_eq!(neg.r, zero.r);
assert_eq!(over.r, hundred.r);
}
#[test]
fn test_bar_style_default() {
assert_eq!(BarStyle::default(), BarStyle::Blocks);
}
#[test]
fn test_micro_heat_bar_new() {
let bar = MicroHeatBar::new(&[50.0, 30.0, 20.0]);
assert_eq!(bar.values.len(), 3);
assert_eq!(bar.width, 20);
assert!(!bar.show_values);
}
#[test]
fn test_micro_heat_bar_with_labels() {
let bar = MicroHeatBar::new(&[50.0, 50.0]).with_labels(&["A", "B"]);
assert_eq!(bar.labels.len(), 2);
assert_eq!(bar.labels[0], "A");
}
#[test]
fn test_micro_heat_bar_with_scheme() {
let bar = MicroHeatBar::new(&[50.0]).with_scheme(HeatScheme::Cool);
assert_eq!(bar.scheme, HeatScheme::Cool);
}
#[test]
fn test_micro_heat_bar_with_style() {
let bar = MicroHeatBar::new(&[50.0]).with_style(BarStyle::Gradient);
assert_eq!(bar.style, BarStyle::Gradient);
}
#[test]
fn test_micro_heat_bar_with_width() {
let bar = MicroHeatBar::new(&[50.0]).with_width(40);
assert_eq!(bar.width, 40);
}
#[test]
fn test_micro_heat_bar_with_values() {
let bar = MicroHeatBar::new(&[50.0]).with_values(true);
assert!(bar.show_values);
}
#[test]
fn test_micro_heat_bar_render() {
let bar = MicroHeatBar::new(&[54.0, 19.0, 4.0, 23.0]).with_width(20);
let rendered = bar.render_string();
assert_eq!(rendered.chars().count(), 20);
}
#[test]
fn test_micro_heat_bar_render_empty() {
let bar = MicroHeatBar::new(&[]).with_width(10);
let rendered = bar.render_string();
assert_eq!(rendered, "░░░░░░░░░░");
}
#[test]
fn test_micro_heat_bar_render_zero_width() {
let bar = MicroHeatBar::new(&[50.0]).with_width(0);
let rendered = bar.render_string();
assert_eq!(rendered, "");
}
#[test]
fn test_micro_heat_bar_render_all_zeros() {
let bar = MicroHeatBar::new(&[0.0, 0.0, 0.0]).with_width(10);
let rendered = bar.render_string();
assert_eq!(rendered, "░░░░░░░░░░");
}
#[test]
fn test_micro_heat_bar_render_blocks_style() {
let bar = MicroHeatBar::new(&[80.0, 50.0, 30.0, 10.0, 2.0])
.with_style(BarStyle::Blocks)
.with_width(10);
let rendered = bar.render_string();
assert!(rendered.contains('█') || rendered.contains('▓') || rendered.contains('▒'));
}
#[test]
fn test_micro_heat_bar_render_gradient_style() {
let bar = MicroHeatBar::new(&[50.0, 50.0])
.with_style(BarStyle::Gradient)
.with_width(10);
let rendered = bar.render_string();
assert!(rendered
.chars()
.any(|c| matches!(c, '▁' | '▂' | '▃' | '▄' | '▅' | '▆' | '▇' | '█')));
}
#[test]
fn test_micro_heat_bar_render_dots_style() {
let bar = MicroHeatBar::new(&[60.0, 40.0])
.with_style(BarStyle::Dots)
.with_width(10);
let rendered = bar.render_string();
assert!(rendered.contains('●') || rendered.contains('○'));
}
#[test]
fn test_micro_heat_bar_render_segments_style() {
let bar = MicroHeatBar::new(&[50.0, 50.0])
.with_style(BarStyle::Segments)
.with_width(10);
let rendered = bar.render_string();
assert!(rendered.contains('█'));
}
#[test]
fn test_micro_heat_bar_paint() {
let mut buffer = CellBuffer::new(30, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
let bar = MicroHeatBar::new(&[54.0, 19.0, 4.0, 23.0]).with_width(20);
bar.paint(&mut canvas, Point::new(0.0, 0.0));
}
#[test]
fn test_micro_heat_bar_paint_empty() {
let mut buffer = CellBuffer::new(30, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
let bar = MicroHeatBar::new(&[]).with_width(10);
bar.paint(&mut canvas, Point::new(0.0, 0.0));
}
#[test]
fn test_micro_heat_bar_paint_gradient() {
let mut buffer = CellBuffer::new(30, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
let bar = MicroHeatBar::new(&[50.0, 50.0])
.with_style(BarStyle::Gradient)
.with_width(20);
bar.paint(&mut canvas, Point::new(0.0, 0.0));
}
#[test]
fn test_micro_heat_bar_paint_dots() {
let mut buffer = CellBuffer::new(30, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
let bar = MicroHeatBar::new(&[60.0, 40.0])
.with_style(BarStyle::Dots)
.with_width(20);
bar.paint(&mut canvas, Point::new(0.0, 0.0));
}
#[test]
fn test_micro_heat_bar_paint_with_remaining() {
let mut buffer = CellBuffer::new(50, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
let bar = MicroHeatBar::new(&[10.0]).with_width(30);
bar.paint(&mut canvas, Point::new(0.0, 0.0));
}
#[test]
fn test_compact_breakdown_new() {
let breakdown = CompactBreakdown::new(&["U", "S", "I"], &[50.0, 30.0, 20.0]);
assert_eq!(breakdown.labels.len(), 3);
assert_eq!(breakdown.values.len(), 3);
}
#[test]
fn test_compact_breakdown_with_scheme() {
let breakdown = CompactBreakdown::new(&["A"], &[50.0]).with_scheme(HeatScheme::Cool);
assert_eq!(breakdown.scheme, HeatScheme::Cool);
}
#[test]
fn test_compact_breakdown_render_text() {
let breakdown = CompactBreakdown::new(&["U", "S", "I", "Id"], &[54.0, 19.0, 4.0, 23.0]);
let text = breakdown.render_text(40);
assert!(text.contains("U:54"));
assert!(text.contains("S:19"));
assert!(text.contains("I:4"));
assert!(text.contains("Id:23"));
}
#[test]
fn test_compact_breakdown_paint() {
let mut buffer = CellBuffer::new(50, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
let breakdown = CompactBreakdown::new(&["U", "S"], &[60.0, 40.0]);
breakdown.paint(&mut canvas, Point::new(0.0, 0.0));
}
#[test]
fn test_compact_breakdown_paint_with_scheme() {
let mut buffer = CellBuffer::new(50, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
let breakdown =
CompactBreakdown::new(&["A", "B"], &[80.0, 20.0]).with_scheme(HeatScheme::Warm);
breakdown.paint(&mut canvas, Point::new(0.0, 0.0));
}
}