#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_lossless,
clippy::too_many_lines
)]
use crate::tui::app::App;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph};
const MIN_WIDTH: u16 = 20;
pub fn render_diverging_bar_chart(frame: &mut Frame, area: Rect, app: &App) {
let data = app.additions_deletions_data();
if area.width < MIN_WIDTH {
let msg = Paragraph::new("Too narrow")
.alignment(Alignment::Center)
.block(
Block::default()
.title(" Additions / Deletions ")
.borders(Borders::ALL),
);
frame.render_widget(msg, area);
return;
}
if data.is_empty() {
let empty = Paragraph::new("No data to display")
.alignment(Alignment::Center)
.block(
Block::default()
.title(" Additions / Deletions ")
.borders(Borders::ALL),
);
frame.render_widget(empty, area);
return;
}
let total_additions: u64 = data.iter().map(|d| d.additions).sum();
let total_deletions: u64 = data.iter().map(|d| d.deletions).sum();
let title = format!(
" Additions / Deletions (+{} / -{}) ",
format_number(total_additions),
format_number(total_deletions)
);
let block = Block::default()
.title(title)
.title_style(Style::default().fg(Color::Yellow).bold())
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::White));
let inner = block.inner(area);
frame.render_widget(block, area);
if inner.height < 1 || inner.width < 10 {
return;
}
let available_rows = inner.height as usize;
let total = data.len();
let scroll_offset = app.scroll_offset.min(total.saturating_sub(1));
let end = total.saturating_sub(scroll_offset);
let start = end.saturating_sub(available_rows);
let display_data: Vec<_> = data[start..end].iter().collect();
let max_value = display_data
.iter()
.map(|d| d.additions.max(d.deletions))
.max()
.unwrap_or(1)
.max(1);
let label_width = display_data
.iter()
.map(|d| d.label.chars().count())
.max()
.unwrap_or(10)
.min(12) as u16;
let bar_area_width = inner.width.saturating_sub(label_width + 3); let half_bar_width = bar_area_width / 2;
for (i, point) in display_data.iter().enumerate() {
let y = inner.y + i as u16;
if y >= inner.y + inner.height {
break;
}
let label = truncate_tail(&point.label, label_width as usize);
let label_span = Span::styled(
format!("{:>width$}", label, width = label_width as usize),
Style::default().fg(Color::DarkGray),
);
frame.render_widget(
Paragraph::new(label_span),
Rect::new(inner.x, y, label_width, 1),
);
let del_bar_len = if max_value > 0 {
((point.deletions as f64 / max_value as f64) * half_bar_width as f64) as u16
} else {
0
};
let add_bar_len = if max_value > 0 {
((point.additions as f64 / max_value as f64) * half_bar_width as f64) as u16
} else {
0
};
let bar_start_x = inner.x + label_width + 1;
let center_x = bar_start_x + half_bar_width;
if del_bar_len > 0 {
let del_start = center_x.saturating_sub(del_bar_len);
let del_bar = Span::styled(
"\u{2588}".repeat(del_bar_len as usize),
Style::default().fg(Color::Red),
);
frame.render_widget(
Paragraph::new(del_bar),
Rect::new(del_start, y, del_bar_len, 1),
);
}
let center_span = Span::styled("|", Style::default().fg(Color::DarkGray));
frame.render_widget(Paragraph::new(center_span), Rect::new(center_x, y, 1, 1));
if add_bar_len > 0 {
let add_bar = Span::styled(
"\u{2588}".repeat(add_bar_len as usize),
Style::default().fg(Color::Green),
);
frame.render_widget(
Paragraph::new(add_bar),
Rect::new(center_x + 1, y, add_bar_len, 1),
);
}
}
}
fn truncate_tail(label: &str, max_chars: usize) -> String {
let count = label.chars().count();
if count <= max_chars {
return label.to_string();
}
label
.chars()
.rev()
.take(max_chars)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect()
}
fn format_number(value: u64) -> String {
if value >= 1_000_000 {
format!("{:.1}M", value as f64 / 1_000_000.0)
} else if value >= 1_000 {
format!("{:.1}K", value as f64 / 1_000.0)
} else {
value.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_number() {
assert_eq!(format_number(100), "100");
assert_eq!(format_number(2500), "2.5K");
assert_eq!(format_number(2_500_000), "2.5M");
}
#[test]
fn test_truncate_tail_ascii() {
assert_eq!(truncate_tail("2024-01-15", 8), "24-01-15");
assert_eq!(truncate_tail("abcdefghij", 5), "fghij");
}
#[test]
fn test_truncate_tail_no_truncation_needed() {
assert_eq!(truncate_tail("hello", 10), "hello");
assert_eq!(truncate_tail("hello", 5), "hello");
assert_eq!(truncate_tail("", 5), "");
}
#[test]
fn test_truncate_tail_non_ascii() {
assert_eq!(truncate_tail("こんにちは", 3), "にちは");
assert_eq!(truncate_tail("日本語テスト", 4), "語テスト");
assert_eq!(truncate_tail("Hello世界", 4), "lo世界");
}
}