tabiew 0.13.1

A lightweight TUI application to view and query tabular data files, such as CSV, TSV, and parquet.
use crossterm::event::{KeyCode, KeyModifiers};
use itertools::Itertools;
use ratatui::{
    layout::{Alignment, Direction, Margin},
    text::Line,
    widgets::{Bar, BarChart, BarGroup, Clear, Widget},
};
use unicode_width::UnicodeWidthStr;

use crate::{
    handler::message::Message,
    misc::config::theme,
    tui::{
        component::Component,
        tag_line::{Tag, TagLine},
        widgets::block::Block,
    },
};

#[derive(Debug)]
pub struct HistogramPlot {
    offset: usize,
    bars: Vec<Bar<'static>>,
    max_value: u64,
}

impl HistogramPlot {
    pub fn new(data: Vec<(String, u64)>) -> Self {
        Self {
            offset: 0,
            max_value: data.iter().map(|(_, v)| *v).max().unwrap_or_default(),
            bars: bars_from_data(data),
        }
    }

    fn scroll_up(&mut self) {
        self.offset = self.offset.saturating_sub(1);
    }

    fn scroll_down(&mut self) {
        self.offset = self.offset.saturating_add(1);
    }
}

impl Component for HistogramPlot {
    fn render(
        &mut self,
        _area: ratatui::prelude::Rect,
        buf: &mut ratatui::prelude::Buffer,
        _focus_state: crate::tui::component::FocusState,
    ) {
        let area = buf.area.inner(Margin::new(7, 3));
        Widget::render(Clear, area, buf);
        let area = {
            let blk = Block::default()
                .title("Histogram Plot")
                .title_alignment(Alignment::Center)
                .bottom(
                    TagLine::default()
                        .mono_color()
                        .centered()
                        .tag(Tag::new(" Scroll Up ", " Shift+K | Shift+\u{2191} "))
                        .tag(Tag::new(" Scroll Down ", " Shift+J | Shift+\u{2193} ")),
                );
            let new_area = blk.inner(area);
            blk.render(area, buf);
            new_area
        };

        self.offset = self
            .offset
            .min(self.bars.len().saturating_sub(area.height as usize));

        let end = self
            .offset
            .saturating_add(area.height as usize)
            .min(self.bars.len());

        let chart = BarChart::default()
            .style(theme().text())
            .bar_width(1)
            .max(self.max_value)
            .direction(Direction::Horizontal)
            .bar_gap(0)
            .data(BarGroup::default().bars(&self.bars[self.offset..end]));
        chart.render(area, buf);
    }

    fn handle(&mut self, event: crossterm::event::KeyEvent) -> bool {
        match (event.code, event.modifiers) {
            (KeyCode::Up, KeyModifiers::SHIFT) | (KeyCode::Char('K'), KeyModifiers::SHIFT) => {
                self.scroll_up();
                true
            }
            (KeyCode::Down, KeyModifiers::SHIFT) | (KeyCode::Char('J'), KeyModifiers::SHIFT) => {
                self.scroll_down();
                true
            }
            (KeyCode::Esc, KeyModifiers::NONE) | (KeyCode::Char('q'), KeyModifiers::NONE) => {
                Message::PaneDismissModal.enqueue();
                true
            }
            (KeyCode::Enter, KeyModifiers::NONE) => true,
            _ => false,
        }
    }
}

fn bars_from_data(data: Vec<(String, u64)>) -> Vec<Bar<'static>> {
    let label_len = data
        .iter()
        .map(|(l, _)| l.trim().width())
        .max()
        .unwrap_or_default()
        .min(24);
    let value_len = data
        .iter()
        .map(|(_, v)| v.to_string().len())
        .max()
        .unwrap_or_default();
    data.iter()
        .enumerate()
        .map(|(idx, (label, value))| {
            let label = label.trim().chars().take(label_len).collect::<String>();
            Bar::default()
                .value(*value)
                .text_value(format!("{value:>value_len$} "))
                .label(Line::styled(
                    format!("{label:>label_len$}"),
                    theme().graph(idx),
                ))
                .style(theme().graph(idx))
        })
        .collect_vec()
}