pinax/
chart.rs

1use std::fmt;
2
3#[derive(Debug, Clone, Copy)]
4pub enum ChartType {
5    Bar,
6    Line,
7}
8
9pub struct Chart {
10    data: Vec<(String, f64)>,
11    chart_type: ChartType,
12    height: usize,
13    title: Option<String>,
14}
15
16impl Chart {
17    pub fn new(chart_type: ChartType) -> Self {
18        Self {
19            data: Vec::new(),
20            chart_type,
21            height: 10,
22            title: None,
23        }
24    }
25
26    pub fn add_data_point(&mut self, label: impl Into<String>, value: f64) {
27        self.data.push((label.into(), value));
28    }
29
30    pub fn with_height(mut self, height: usize) -> Self {
31        self.height = height;
32        self
33    }
34
35    pub fn with_title(mut self, title: impl Into<String>) -> Self {
36        self.title = Some(title.into());
37        self
38    }
39
40    fn generate_bar_chart(&self) -> Vec<String> {
41        let max_value = self.data.iter().map(|(_, v)| *v).fold(0.0, f64::max);
42        let mut output = Vec::new();
43
44        // Generate bars with fixed width and proper alignment
45        for (label, value) in &self.data {
46            let bar_width = ((value / max_value) * 40.0) as usize;
47            let bar = "█".repeat(bar_width);
48            output.push(format!("{:<6} │ {:<40} {:.1}", label, bar, value));
49        }
50
51        output
52    }
53
54    fn generate_line_chart(&self) -> Vec<String> {
55        if self.data.is_empty() {
56            return Vec::new();
57        }
58
59        let max_value = self.data.iter().map(|(_, v)| *v).fold(0.0, f64::max);
60        let min_value = self.data.iter().map(|(_, v)| *v).fold(f64::MAX, f64::min);
61        let col_width = 12; // Width for each column
62        let width = self.data.len() * col_width;
63        let mut chart = vec![vec![' '; width]; self.height];
64
65        // Calculate y-axis values for key points
66        let y_range = max_value - min_value;
67        let y_values = [
68            max_value,                        // Top
69            max_value - (y_range / 2.0),      // Middle
70            min_value,                        // Bottom
71        ];
72
73        // Plot points and lines
74        for i in 0..self.data.len() - 1 {
75            let (x1, y1) = (
76                i * col_width,
77                self.height - 1 - ((self.data[i].1 - min_value) / (max_value - min_value) * (self.height - 1) as f64) as usize
78            );
79            let (x2, y2) = (
80                (i + 1) * col_width,
81                self.height - 1 - ((self.data[i + 1].1 - min_value) / (max_value - min_value) * (self.height - 1) as f64) as usize
82            );
83
84            // Draw points
85            chart[y1][x1] = '●';
86            if i == self.data.len() - 2 {
87                chart[y2][x2] = '●';
88            }
89
90            // Draw connecting line
91            let dx = x2 - x1;
92            let dy = y2 as i32 - y1 as i32;
93            let steps = dx.max(dy.abs() as usize);
94            
95            for step in 1..steps {
96                let x = x1 + step;
97                let y = y1 as i32 + (dy * step as i32 / steps as i32);
98                if y >= 0 && y < self.height as i32 {
99                    // Choose character based on line direction
100                    let char = if dy == 0 {
101                        '─'
102                    } else if dy < 0 {
103                        '/'
104                    } else {
105                        '\\'
106                    };
107                    chart[y as usize][x] = char;
108                }
109            }
110        }
111
112        // Convert to strings with axis and y-values
113        let mut output = Vec::new();
114        for (i, row) in chart.iter().enumerate() {
115            let y_value = if i == 0 {
116                format!("{:>8.1} ", y_values[0])
117            } else if i == self.height / 2 {
118                format!("{:>8.1} ", y_values[1])
119            } else if i == self.height - 1 {
120                format!("{:>8.1} ", y_values[2])
121            } else {
122                format!("{:<10}", "")
123            };
124            output.push(format!("{:<10}│ {}", y_value, row.iter().collect::<String>()));
125        }
126
127        // Add x-axis
128        output.push(format!("{:<10}└{}", "", "─".repeat(width)));
129
130        // Add labels with improved spacing
131        let mut labels = String::from("            ");
132        for (i, (label, _)) in self.data.iter().enumerate() {
133            labels.push_str(&format!("{:<11}", label));
134            if i < self.data.len() - 1 {
135                labels.push(' ');
136            }
137        }
138        output.push(labels);
139
140        output
141    }
142}
143
144impl fmt::Display for Chart {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        // Write title if present
147        if let Some(title) = &self.title {
148            writeln!(f, "{}", title)?;
149            writeln!(f, "{}", "=".repeat(title.len()))?;
150            writeln!(f)?; // Add extra newline after title
151        }
152
153        let chart_data = match self.chart_type {
154            ChartType::Bar => self.generate_bar_chart(),
155            ChartType::Line => self.generate_line_chart(),
156        };
157
158        for line in chart_data {
159            writeln!(f, "{}", line)?;
160        }
161        Ok(())
162    }
163}