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 render::render_chart;
17
18use axis::{build_tui_ticks, format_number, resolve_bounds, TickSpec};
19use bar::draw_bar_dataset;
20use braille::draw_braille_dataset;
21use grid::{
22 apply_grid, build_legend_items, build_x_tick_col_map, build_y_tick_row_map, center_text,
23 map_value_to_cell, marker_char, overlay_legend_on_plot, sturges_bin_count, GridSpec,
24};
25
26const BRAILLE_BASE: u32 = 0x2800;
27const BRAILLE_LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
28const BRAILLE_RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
29const PALETTE: [Color; 8] = [
30 Color::Cyan,
31 Color::Yellow,
32 Color::Green,
33 Color::Magenta,
34 Color::Red,
35 Color::Blue,
36 Color::White,
37 Color::Indexed(208),
38];
39const BLOCK_FRACTIONS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
40
41pub type ColorSpan = (usize, usize, Color);
43
44pub type RenderedLine = (String, Vec<ColorSpan>);
46
47#[non_exhaustive]
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Marker {
51 Braille,
53 Dot,
55 Block,
57 HalfBlock,
59 Cross,
61 Circle,
63}
64
65#[non_exhaustive]
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum GraphType {
69 Line,
71 Area,
73 Scatter,
75 Bar,
77}
78
79#[non_exhaustive]
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum LegendPosition {
83 TopLeft,
85 TopRight,
87 BottomLeft,
89 BottomRight,
91 None,
93}
94
95#[derive(Debug, Clone)]
97pub struct Axis {
98 pub title: Option<String>,
100 pub bounds: Option<(f64, f64)>,
102 pub labels: Option<Vec<String>>,
104 pub ticks: Option<Vec<f64>>,
106 pub title_style: Option<Style>,
108 pub style: Style,
110}
111
112impl Default for Axis {
113 fn default() -> Self {
114 Self {
115 title: None,
116 bounds: None,
117 labels: None,
118 ticks: None,
119 title_style: None,
120 style: Style::new(),
121 }
122 }
123}
124
125#[derive(Debug, Clone)]
127pub struct Dataset {
128 pub name: String,
130 pub data: Vec<(f64, f64)>,
132 pub color: Color,
134 pub marker: Marker,
136 pub graph_type: GraphType,
138 pub up_color: Option<Color>,
140 pub down_color: Option<Color>,
142}
143
144#[derive(Debug, Clone, Copy)]
146pub struct Candle {
147 pub open: f64,
149 pub high: f64,
151 pub low: f64,
153 pub close: f64,
155}
156
157#[derive(Debug, Clone)]
159pub struct ChartConfig {
160 pub title: Option<String>,
162 pub title_style: Option<Style>,
164 pub x_axis: Axis,
166 pub y_axis: Axis,
168 pub datasets: Vec<Dataset>,
170 pub legend: LegendPosition,
172 pub grid: bool,
174 pub grid_style: Option<Style>,
176 pub hlines: Vec<(f64, Style)>,
178 pub vlines: Vec<(f64, Style)>,
180 pub frame_visible: bool,
182 pub x_axis_visible: bool,
184 pub y_axis_visible: bool,
186 pub width: u32,
188 pub height: u32,
190}
191
192#[derive(Debug, Clone)]
194pub(crate) struct ChartRow {
195 pub segments: Vec<(String, Style)>,
197}
198
199#[derive(Debug, Clone)]
201#[must_use = "configure histogram before rendering"]
202pub struct HistogramBuilder {
203 pub bins: Option<usize>,
205 pub color: Color,
207 pub x_title: Option<String>,
209 pub y_title: Option<String>,
211}
212
213impl Default for HistogramBuilder {
214 fn default() -> Self {
215 Self {
216 bins: None,
217 color: Color::Cyan,
218 x_title: None,
219 y_title: None,
220 }
221 }
222}
223
224impl HistogramBuilder {
225 pub fn bins(&mut self, bins: usize) -> &mut Self {
227 self.bins = Some(bins.max(1));
228 self
229 }
230
231 pub fn color(&mut self, color: Color) -> &mut Self {
233 self.color = color;
234 self
235 }
236
237 pub fn xlabel(&mut self, title: &str) -> &mut Self {
239 self.x_title = Some(title.to_string());
240 self
241 }
242
243 pub fn ylabel(&mut self, title: &str) -> &mut Self {
245 self.y_title = Some(title.to_string());
246 self
247 }
248}
249
250#[derive(Debug, Clone)]
252pub struct DatasetEntry {
253 dataset: Dataset,
254 color_overridden: bool,
255}
256
257impl DatasetEntry {
258 pub fn label(&mut self, name: &str) -> &mut Self {
260 self.dataset.name = name.to_string();
261 self
262 }
263
264 pub fn color(&mut self, color: Color) -> &mut Self {
266 self.dataset.color = color;
267 self.color_overridden = true;
268 self
269 }
270
271 pub fn marker(&mut self, marker: Marker) -> &mut Self {
273 self.dataset.marker = marker;
274 self
275 }
276
277 pub fn color_by_direction(&mut self, up: Color, down: Color) -> &mut Self {
279 self.dataset.up_color = Some(up);
280 self.dataset.down_color = Some(down);
281 self
282 }
283}
284
285#[derive(Debug, Clone)]
287#[must_use = "configure chart before rendering"]
288pub struct ChartBuilder {
289 config: ChartConfig,
290 entries: Vec<DatasetEntry>,
291}
292
293impl ChartBuilder {
294 pub fn new(width: u32, height: u32, x_style: Style, y_style: Style) -> Self {
296 Self {
297 config: ChartConfig {
298 title: None,
299 title_style: None,
300 x_axis: Axis {
301 style: x_style,
302 ..Axis::default()
303 },
304 y_axis: Axis {
305 style: y_style,
306 ..Axis::default()
307 },
308 datasets: Vec::new(),
309 legend: LegendPosition::TopRight,
310 grid: true,
311 grid_style: None,
312 hlines: Vec::new(),
313 vlines: Vec::new(),
314 frame_visible: false,
315 x_axis_visible: true,
316 y_axis_visible: true,
317 width,
318 height,
319 },
320 entries: Vec::new(),
321 }
322 }
323
324 pub fn title(&mut self, title: &str) -> &mut Self {
326 self.config.title = Some(title.to_string());
327 self
328 }
329
330 pub fn xlabel(&mut self, label: &str) -> &mut Self {
332 self.config.x_axis.title = Some(label.to_string());
333 self
334 }
335
336 pub fn ylabel(&mut self, label: &str) -> &mut Self {
338 self.config.y_axis.title = Some(label.to_string());
339 self
340 }
341
342 pub fn xlim(&mut self, min: f64, max: f64) -> &mut Self {
344 self.config.x_axis.bounds = Some((min, max));
345 self
346 }
347
348 pub fn ylim(&mut self, min: f64, max: f64) -> &mut Self {
350 self.config.y_axis.bounds = Some((min, max));
351 self
352 }
353
354 pub fn xticks(&mut self, values: &[f64]) -> &mut Self {
356 self.config.x_axis.ticks = Some(values.to_vec());
357 self
358 }
359
360 pub fn yticks(&mut self, values: &[f64]) -> &mut Self {
362 self.config.y_axis.ticks = Some(values.to_vec());
363 self
364 }
365
366 pub fn xtick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
368 self.config.x_axis.ticks = Some(values.to_vec());
369 self.config.x_axis.labels = Some(labels.iter().map(|label| (*label).to_string()).collect());
370 self
371 }
372
373 pub fn ytick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
375 self.config.y_axis.ticks = Some(values.to_vec());
376 self.config.y_axis.labels = Some(labels.iter().map(|label| (*label).to_string()).collect());
377 self
378 }
379
380 pub fn title_style(&mut self, style: Style) -> &mut Self {
382 self.config.title_style = Some(style);
383 self
384 }
385
386 pub fn grid_style(&mut self, style: Style) -> &mut Self {
388 self.config.grid_style = Some(style);
389 self
390 }
391
392 pub fn x_axis_style(&mut self, style: Style) -> &mut Self {
394 self.config.x_axis.style = style;
395 self
396 }
397
398 pub fn y_axis_style(&mut self, style: Style) -> &mut Self {
400 self.config.y_axis.style = style;
401 self
402 }
403
404 pub fn axhline(&mut self, y: f64, style: Style) -> &mut Self {
406 self.config.hlines.push((y, style));
407 self
408 }
409
410 pub fn axvline(&mut self, x: f64, style: Style) -> &mut Self {
412 self.config.vlines.push((x, style));
413 self
414 }
415
416 pub fn grid(&mut self, on: bool) -> &mut Self {
418 self.config.grid = on;
419 self
420 }
421
422 pub fn frame(&mut self, on: bool) -> &mut Self {
424 self.config.frame_visible = on;
425 self
426 }
427
428 pub fn x_axis_visible(&mut self, on: bool) -> &mut Self {
430 self.config.x_axis_visible = on;
431 self
432 }
433
434 pub fn y_axis_visible(&mut self, on: bool) -> &mut Self {
436 self.config.y_axis_visible = on;
437 self
438 }
439
440 pub fn legend(&mut self, position: LegendPosition) -> &mut Self {
442 self.config.legend = position;
443 self
444 }
445
446 pub fn line(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
448 self.push_dataset(data, GraphType::Line, Marker::Braille)
449 }
450
451 pub fn area(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
453 self.push_dataset(data, GraphType::Area, Marker::Braille)
454 }
455
456 pub fn scatter(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
458 self.push_dataset(data, GraphType::Scatter, Marker::Braille)
459 }
460
461 pub fn bar(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
463 self.push_dataset(data, GraphType::Bar, Marker::Block)
464 }
465
466 pub fn build(mut self) -> ChartConfig {
468 for (index, mut entry) in self.entries.drain(..).enumerate() {
469 if !entry.color_overridden {
470 entry.dataset.color = PALETTE[index % PALETTE.len()];
471 }
472 self.config.datasets.push(entry.dataset);
473 }
474 self.config
475 }
476
477 fn push_dataset(
478 &mut self,
479 data: &[(f64, f64)],
480 graph_type: GraphType,
481 marker: Marker,
482 ) -> &mut DatasetEntry {
483 let series_name = format!("Series {}", self.entries.len() + 1);
484 self.entries.push(DatasetEntry {
485 dataset: Dataset {
486 name: series_name,
487 data: data.to_vec(),
488 color: Color::Reset,
489 marker,
490 graph_type,
491 up_color: None,
492 down_color: None,
493 },
494 color_overridden: false,
495 });
496 let last_index = self.entries.len().saturating_sub(1);
497 &mut self.entries[last_index]
498 }
499}
500
501#[derive(Debug, Clone)]
503pub struct ChartRenderer {
504 config: ChartConfig,
505}
506
507impl ChartRenderer {
508 pub fn new(config: ChartConfig) -> Self {
510 Self { config }
511 }
512
513 pub fn render(&self) -> Vec<RenderedLine> {
515 let rows = render_chart(&self.config);
516 rows.into_iter()
517 .map(|row| {
518 let mut line = String::new();
519 let mut spans: Vec<(usize, usize, Color)> = Vec::new();
520 let mut cursor = 0usize;
521
522 for (segment, style) in row.segments {
523 let width = unicode_width::UnicodeWidthStr::width(segment.as_str());
524 line.push_str(&segment);
525 if let Some(color) = style.fg {
526 spans.push((cursor, cursor + width, color));
527 }
528 cursor += width;
529 }
530
531 (line, spans)
532 })
533 .collect()
534 }
535}