1use crate::core::buffer::Buffer;
2use crate::core::color::Color;
3use crate::core::rect::Rect;
4use crate::widgets::Widget;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum AxisPosition {
8 Left,
9 Right,
10 Bottom,
11 Top,
12}
13
14#[derive(Debug, Clone)]
15pub struct Axis {
16 pub title: String,
17 pub labels: Vec<String>,
18 pub position: AxisPosition,
19}
20
21impl Axis {
22 pub fn new(position: AxisPosition) -> Self {
23 Self {
24 title: String::new(),
25 labels: Vec::new(),
26 position,
27 }
28 }
29
30 pub fn with_title(mut self, title: &str) -> Self {
31 self.title = title.to_string();
32 self
33 }
34
35 pub fn with_labels(mut self, labels: Vec<String>) -> Self {
36 self.labels = labels;
37 self
38 }
39}
40
41#[derive(Debug, Clone)]
42pub struct Dataset {
43 pub name: String,
44 pub color: Color,
45 pub data: Vec<(f64, f64)>,
46 pub marker: char,
47}
48
49impl Dataset {
50 pub fn new(name: &str, data: Vec<(f64, f64)>) -> Self {
51 Self {
52 name: name.to_string(),
53 color: Color::rgb(88, 166, 255),
54 data,
55 marker: '●',
56 }
57 }
58
59 pub fn with_color(mut self, color: Color) -> Self {
60 self.color = color;
61 self
62 }
63}
64
65#[derive(Debug, Clone)]
66pub struct Chart {
67 pub datasets: Vec<Dataset>,
68 pub x_axis: Axis,
69 pub y_axis: Axis,
70 pub labels: Vec<String>,
71 pub graph_area: Option<Rect>,
72}
73
74impl Chart {
75 pub fn new(datasets: Vec<Dataset>) -> Self {
76 Self {
77 datasets,
78 x_axis: Axis::new(AxisPosition::Bottom),
79 y_axis: Axis::new(AxisPosition::Left),
80 labels: Vec::new(),
81 graph_area: None,
82 }
83 }
84
85 pub fn with_x_axis(mut self, axis: Axis) -> Self {
86 self.x_axis = axis;
87 self
88 }
89
90 pub fn with_y_axis(mut self, axis: Axis) -> Self {
91 self.y_axis = axis;
92 self
93 }
94
95 fn render_axes(&self, buffer: &mut Buffer, area: Rect) {
96 let axis_color = Color::rgb(48, 54, 61);
97
98 match self.y_axis.position {
99 AxisPosition::Left => {
100 for y in (area.y + 1) as usize..(area.bottom() - 1) as usize {
101 buffer.set(
102 area.x as usize,
103 y,
104 crate::core::buffer::Cell {
105 ch: '│',
106 fg: axis_color,
107 bg: None,
108 bold: false,
109 italic: false,
110 underlined: false,
111 },
112 );
113 }
114 }
115 AxisPosition::Right => {
116 for y in (area.y + 1) as usize..(area.bottom() - 1) as usize {
117 buffer.set(
118 (area.right() - 1) as usize,
119 y,
120 crate::core::buffer::Cell {
121 ch: '│',
122 fg: axis_color,
123 bg: None,
124 bold: false,
125 italic: false,
126 underlined: false,
127 },
128 );
129 }
130 }
131 _ => {}
132 }
133
134 match self.x_axis.position {
135 AxisPosition::Bottom => {
136 for x in (area.x + 1) as usize..(area.right() - 1) as usize {
137 buffer.set(
138 x,
139 (area.bottom() - 1) as usize,
140 crate::core::buffer::Cell {
141 ch: '─',
142 fg: axis_color,
143 bg: None,
144 bold: false,
145 italic: false,
146 underlined: false,
147 },
148 );
149 }
150 }
151 AxisPosition::Top => {
152 for x in (area.x + 1) as usize..(area.right() - 1) as usize {
153 buffer.set(
154 x,
155 area.y as usize,
156 crate::core::buffer::Cell {
157 ch: '─',
158 fg: axis_color,
159 bg: None,
160 bold: false,
161 italic: false,
162 underlined: false,
163 },
164 );
165 }
166 }
167 _ => {}
168 }
169
170 buffer.set(
171 area.x as usize,
172 (area.bottom() - 1) as usize,
173 crate::core::buffer::Cell {
174 ch: '└',
175 fg: axis_color,
176 bg: None,
177 bold: false,
178 italic: false,
179 underlined: false,
180 },
181 );
182 }
183
184 fn render_points(&self, buffer: &mut Buffer, area: Rect) {
185 if self.datasets.is_empty() || area.width < 4 || area.height < 4 {
186 return;
187 }
188
189 let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2);
190
191 let all_x: Vec<f64> = self
192 .datasets
193 .iter()
194 .flat_map(|d| d.data.iter().map(|(x, _)| *x))
195 .collect();
196 let all_y: Vec<f64> = self
197 .datasets
198 .iter()
199 .flat_map(|d| d.data.iter().map(|(_, y)| *y))
200 .collect();
201 let x_min = all_x.iter().copied().fold(f64::INFINITY, f64::min);
202 let x_max = all_x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
203 let y_min = all_y.iter().copied().fold(f64::INFINITY, f64::min);
204 let y_max = all_y.iter().copied().fold(f64::NEG_INFINITY, f64::max);
205 let x_range = (x_max - x_min).max(1.0);
206 let y_range = (y_max - y_min).max(1.0);
207
208 for dataset in &self.datasets {
209 for &(x, y) in &dataset.data {
210 let nx = ((x - x_min) / x_range * (inner.width as f64 - 1.0)) as u16;
211 let ny = ((1.0 - (y - y_min) / y_range) * (inner.height as f64 - 1.0)) as u16;
212 let px = (inner.x + nx) as usize;
213 let py = (inner.y + ny) as usize;
214 if px < inner.right() as usize && py < inner.bottom() as usize {
215 buffer.set(
216 px,
217 py,
218 crate::core::buffer::Cell {
219 ch: dataset.marker,
220 fg: dataset.color,
221 bg: None,
222 bold: true,
223 italic: false,
224 underlined: false,
225 },
226 );
227 }
228 }
229 }
230 }
231}
232
233impl Widget for Chart {
234 fn render(&self, buffer: &mut Buffer, area: Rect) {
235 self.render_axes(buffer, area);
236 self.render_points(buffer, area);
237 }
238}