1use crate::style::{Color, Style};
8
9mod axis;
10mod bar;
11mod braille;
12mod grid;
13mod render;
14
15pub(crate) use bar::build_histogram_config;
16pub(crate) use grid::truncate_label;
17pub(crate) use render::render_chart;
18
19use axis::{build_tui_ticks, format_number, resolve_bounds, TickSpec};
20use bar::draw_bar_dataset;
21use braille::draw_braille_dataset;
22use grid::{
23 apply_grid, build_legend_items, build_x_tick_col_map, build_y_tick_row_map, center_text,
24 map_value_to_cell, marker_char, overlay_legend_on_plot, sturges_bin_count, GridSpec,
25};
26
27const BRAILLE_BASE: u32 = 0x2800;
28pub(crate) const BRAILLE_LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
29pub(crate) const BRAILLE_RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
30const PALETTE: [Color; 8] = [
31 Color::Cyan,
32 Color::Yellow,
33 Color::Green,
34 Color::Magenta,
35 Color::Red,
36 Color::Blue,
37 Color::White,
38 Color::Indexed(208),
39];
40const BLOCK_FRACTIONS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
41
42pub type ColorSpan = (usize, usize, Color);
44
45pub type RenderedLine = (String, Vec<ColorSpan>);
47
48#[non_exhaustive]
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Marker {
52 Braille,
54 Dot,
56 Block,
58 HalfBlock,
60 Cross,
62 Circle,
64}
65
66#[non_exhaustive]
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum GraphType {
70 Line,
72 Area,
74 Scatter,
76 Bar,
78}
79
80#[non_exhaustive]
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum LegendPosition {
84 TopLeft,
86 TopRight,
88 BottomLeft,
90 BottomRight,
92 None,
94}
95
96#[derive(Debug, Clone)]
98pub struct Axis {
99 pub title: Option<String>,
101 pub bounds: Option<(f64, f64)>,
103 pub labels: Option<Vec<String>>,
105 pub ticks: Option<Vec<f64>>,
107 pub title_style: Option<Style>,
109 pub style: Style,
111}
112
113impl Default for Axis {
114 fn default() -> Self {
115 Self {
116 title: None,
117 bounds: None,
118 labels: None,
119 ticks: None,
120 title_style: None,
121 style: Style::new(),
122 }
123 }
124}
125
126#[derive(Debug, Clone)]
128pub struct Dataset {
129 pub name: String,
131 pub data: Vec<(f64, f64)>,
133 pub color: Color,
135 pub marker: Marker,
137 pub graph_type: GraphType,
139 pub up_color: Option<Color>,
141 pub down_color: Option<Color>,
143}
144
145#[derive(Debug, Clone, Copy)]
147pub struct Candle {
148 pub open: f64,
150 pub high: f64,
152 pub low: f64,
154 pub close: f64,
156}
157
158#[derive(Debug, Clone)]
160pub struct ChartConfig {
161 pub title: Option<String>,
163 pub title_style: Option<Style>,
165 pub x_axis: Axis,
167 pub y_axis: Axis,
169 pub datasets: Vec<Dataset>,
171 pub legend: LegendPosition,
173 pub grid: bool,
175 pub grid_style: Option<Style>,
177 pub hlines: Vec<(f64, Style)>,
179 pub vlines: Vec<(f64, Style)>,
181 pub frame_visible: bool,
183 pub x_axis_visible: bool,
185 pub y_axis_visible: bool,
187 pub width: u32,
189 pub height: u32,
191}
192
193#[derive(Debug, Clone)]
195pub(crate) struct ChartRow {
196 pub segments: Vec<(String, Style)>,
198}
199
200#[derive(Debug, Clone)]
202#[must_use = "configure histogram before rendering"]
203pub struct HistogramBuilder {
204 pub bins: Option<usize>,
206 pub color: Color,
208 pub x_title: Option<String>,
210 pub y_title: Option<String>,
212}
213
214impl Default for HistogramBuilder {
215 fn default() -> Self {
216 Self {
217 bins: None,
218 color: Color::Cyan,
219 x_title: None,
220 y_title: None,
221 }
222 }
223}
224
225impl HistogramBuilder {
226 pub fn bins(&mut self, bins: usize) -> &mut Self {
228 self.bins = Some(bins.max(1));
229 self
230 }
231
232 pub fn color(&mut self, color: Color) -> &mut Self {
234 self.color = color;
235 self
236 }
237
238 pub fn xlabel(&mut self, title: &str) -> &mut Self {
240 self.x_title = Some(title.to_string());
241 self
242 }
243
244 pub fn ylabel(&mut self, title: &str) -> &mut Self {
246 self.y_title = Some(title.to_string());
247 self
248 }
249}
250
251#[derive(Debug, Clone)]
253pub struct DatasetEntry {
254 dataset: Dataset,
255 color_overridden: bool,
256}
257
258impl DatasetEntry {
259 pub fn label(&mut self, name: &str) -> &mut Self {
261 self.dataset.name = name.to_string();
262 self
263 }
264
265 pub fn color(&mut self, color: Color) -> &mut Self {
267 self.dataset.color = color;
268 self.color_overridden = true;
269 self
270 }
271
272 pub fn marker(&mut self, marker: Marker) -> &mut Self {
274 self.dataset.marker = marker;
275 self
276 }
277
278 pub fn color_by_direction(&mut self, up: Color, down: Color) -> &mut Self {
280 self.dataset.up_color = Some(up);
281 self.dataset.down_color = Some(down);
282 self
283 }
284}
285
286#[derive(Debug, Clone)]
288#[must_use = "configure chart before rendering"]
289pub struct ChartBuilder {
290 config: ChartConfig,
291 entries: Vec<DatasetEntry>,
292}
293
294impl ChartBuilder {
295 pub fn new(width: u32, height: u32, x_style: Style, y_style: Style) -> Self {
297 Self {
298 config: ChartConfig {
299 title: None,
300 title_style: None,
301 x_axis: Axis {
302 style: x_style,
303 ..Axis::default()
304 },
305 y_axis: Axis {
306 style: y_style,
307 ..Axis::default()
308 },
309 datasets: Vec::new(),
310 legend: LegendPosition::TopRight,
311 grid: true,
312 grid_style: None,
313 hlines: Vec::new(),
314 vlines: Vec::new(),
315 frame_visible: false,
316 x_axis_visible: true,
317 y_axis_visible: true,
318 width,
319 height,
320 },
321 entries: Vec::new(),
322 }
323 }
324
325 pub fn title(&mut self, title: &str) -> &mut Self {
327 self.config.title = Some(title.to_string());
328 self
329 }
330
331 pub fn xlabel(&mut self, label: &str) -> &mut Self {
333 self.config.x_axis.title = Some(label.to_string());
334 self
335 }
336
337 pub fn ylabel(&mut self, label: &str) -> &mut Self {
339 self.config.y_axis.title = Some(label.to_string());
340 self
341 }
342
343 pub fn xlim(&mut self, min: f64, max: f64) -> &mut Self {
345 self.config.x_axis.bounds = Some((min, max));
346 self
347 }
348
349 pub fn ylim(&mut self, min: f64, max: f64) -> &mut Self {
351 self.config.y_axis.bounds = Some((min, max));
352 self
353 }
354
355 pub fn xticks(&mut self, values: &[f64]) -> &mut Self {
357 self.config.x_axis.ticks = Some(values.to_vec());
358 self
359 }
360
361 pub fn yticks(&mut self, values: &[f64]) -> &mut Self {
363 self.config.y_axis.ticks = Some(values.to_vec());
364 self
365 }
366
367 pub fn xtick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
369 self.config.x_axis.ticks = Some(values.to_vec());
370 self.config.x_axis.labels = Some(labels.iter().map(|label| (*label).to_string()).collect());
371 self
372 }
373
374 pub fn ytick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
376 self.config.y_axis.ticks = Some(values.to_vec());
377 self.config.y_axis.labels = Some(labels.iter().map(|label| (*label).to_string()).collect());
378 self
379 }
380
381 pub fn title_style(&mut self, style: Style) -> &mut Self {
383 self.config.title_style = Some(style);
384 self
385 }
386
387 pub fn grid_style(&mut self, style: Style) -> &mut Self {
389 self.config.grid_style = Some(style);
390 self
391 }
392
393 pub fn x_axis_style(&mut self, style: Style) -> &mut Self {
395 self.config.x_axis.style = style;
396 self
397 }
398
399 pub fn y_axis_style(&mut self, style: Style) -> &mut Self {
401 self.config.y_axis.style = style;
402 self
403 }
404
405 pub fn axhline(&mut self, y: f64, style: Style) -> &mut Self {
407 self.config.hlines.push((y, style));
408 self
409 }
410
411 pub fn axvline(&mut self, x: f64, style: Style) -> &mut Self {
413 self.config.vlines.push((x, style));
414 self
415 }
416
417 pub fn grid(&mut self, on: bool) -> &mut Self {
419 self.config.grid = on;
420 self
421 }
422
423 pub fn frame(&mut self, on: bool) -> &mut Self {
425 self.config.frame_visible = on;
426 self
427 }
428
429 pub fn x_axis_visible(&mut self, on: bool) -> &mut Self {
431 self.config.x_axis_visible = on;
432 self
433 }
434
435 pub fn y_axis_visible(&mut self, on: bool) -> &mut Self {
437 self.config.y_axis_visible = on;
438 self
439 }
440
441 pub fn legend(&mut self, position: LegendPosition) -> &mut Self {
443 self.config.legend = position;
444 self
445 }
446
447 pub fn line(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
449 self.push_dataset(data, GraphType::Line, Marker::Braille)
450 }
451
452 pub fn area(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
454 self.push_dataset(data, GraphType::Area, Marker::Braille)
455 }
456
457 pub fn scatter(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
459 self.push_dataset(data, GraphType::Scatter, Marker::Braille)
460 }
461
462 pub fn bar(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
464 self.push_dataset(data, GraphType::Bar, Marker::Block)
465 }
466
467 pub fn build(mut self) -> ChartConfig {
469 for (index, mut entry) in self.entries.drain(..).enumerate() {
470 if !entry.color_overridden {
471 entry.dataset.color = PALETTE[index % PALETTE.len()];
472 }
473 self.config.datasets.push(entry.dataset);
474 }
475 self.config
476 }
477
478 fn push_dataset(
479 &mut self,
480 data: &[(f64, f64)],
481 graph_type: GraphType,
482 marker: Marker,
483 ) -> &mut DatasetEntry {
484 let series_name = format!("Series {}", self.entries.len() + 1);
485 self.entries.push(DatasetEntry {
486 dataset: Dataset {
487 name: series_name,
488 data: data.to_vec(),
489 color: Color::Reset,
490 marker,
491 graph_type,
492 up_color: None,
493 down_color: None,
494 },
495 color_overridden: false,
496 });
497 let last_index = self.entries.len().saturating_sub(1);
498 &mut self.entries[last_index]
499 }
500}
501
502#[derive(Debug, Clone)]
504pub struct ChartRenderer {
505 config: ChartConfig,
506}
507
508impl ChartRenderer {
509 pub fn new(config: ChartConfig) -> Self {
511 Self { config }
512 }
513
514 pub fn render(&self) -> Vec<RenderedLine> {
516 let rows = render_chart(&self.config);
517 rows.into_iter()
518 .map(|row| {
519 let mut line = String::new();
520 let mut spans: Vec<(usize, usize, Color)> = Vec::new();
521 let mut cursor = 0usize;
522
523 for (segment, style) in row.segments {
524 let width = unicode_width::UnicodeWidthStr::width(segment.as_str());
525 line.push_str(&segment);
526 if let Some(color) = style.fg {
527 spans.push((cursor, cursor + width, color));
528 }
529 cursor += width;
530 }
531
532 (line, spans)
533 })
534 .collect()
535 }
536}