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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum Marker {
50 Braille,
52 Dot,
54 Block,
56 HalfBlock,
58 Cross,
60 Circle,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum GraphType {
67 Line,
69 Area,
71 Scatter,
73 Bar,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum LegendPosition {
80 TopLeft,
82 TopRight,
84 BottomLeft,
86 BottomRight,
88 None,
90}
91
92#[derive(Debug, Clone)]
94pub struct Axis {
95 pub title: Option<String>,
97 pub bounds: Option<(f64, f64)>,
99 pub labels: Option<Vec<String>>,
101 pub ticks: Option<Vec<f64>>,
103 pub title_style: Option<Style>,
105 pub style: Style,
107}
108
109impl Default for Axis {
110 fn default() -> Self {
111 Self {
112 title: None,
113 bounds: None,
114 labels: None,
115 ticks: None,
116 title_style: None,
117 style: Style::new(),
118 }
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct Dataset {
125 pub name: String,
127 pub data: Vec<(f64, f64)>,
129 pub color: Color,
131 pub marker: Marker,
133 pub graph_type: GraphType,
135 pub up_color: Option<Color>,
137 pub down_color: Option<Color>,
139}
140
141#[derive(Debug, Clone, Copy)]
143pub struct Candle {
144 pub open: f64,
146 pub high: f64,
148 pub low: f64,
150 pub close: f64,
152}
153
154#[derive(Debug, Clone)]
156pub struct ChartConfig {
157 pub title: Option<String>,
159 pub title_style: Option<Style>,
161 pub x_axis: Axis,
163 pub y_axis: Axis,
165 pub datasets: Vec<Dataset>,
167 pub legend: LegendPosition,
169 pub grid: bool,
171 pub grid_style: Option<Style>,
173 pub hlines: Vec<(f64, Style)>,
175 pub vlines: Vec<(f64, Style)>,
177 pub frame_visible: bool,
179 pub x_axis_visible: bool,
181 pub y_axis_visible: bool,
183 pub width: u32,
185 pub height: u32,
187}
188
189#[derive(Debug, Clone)]
191pub(crate) struct ChartRow {
192 pub segments: Vec<(String, Style)>,
194}
195
196#[derive(Debug, Clone)]
198#[must_use = "configure histogram before rendering"]
199pub struct HistogramBuilder {
200 pub bins: Option<usize>,
202 pub color: Color,
204 pub x_title: Option<String>,
206 pub y_title: Option<String>,
208}
209
210impl Default for HistogramBuilder {
211 fn default() -> Self {
212 Self {
213 bins: None,
214 color: Color::Cyan,
215 x_title: None,
216 y_title: Some("Count".to_string()),
217 }
218 }
219}
220
221impl HistogramBuilder {
222 pub fn bins(&mut self, bins: usize) -> &mut Self {
224 self.bins = Some(bins.max(1));
225 self
226 }
227
228 pub fn color(&mut self, color: Color) -> &mut Self {
230 self.color = color;
231 self
232 }
233
234 pub fn xlabel(&mut self, title: &str) -> &mut Self {
236 self.x_title = Some(title.to_string());
237 self
238 }
239
240 pub fn ylabel(&mut self, title: &str) -> &mut Self {
242 self.y_title = Some(title.to_string());
243 self
244 }
245}
246
247#[derive(Debug, Clone)]
249pub struct DatasetEntry {
250 dataset: Dataset,
251 color_overridden: bool,
252}
253
254impl DatasetEntry {
255 pub fn label(&mut self, name: &str) -> &mut Self {
257 self.dataset.name = name.to_string();
258 self
259 }
260
261 pub fn color(&mut self, color: Color) -> &mut Self {
263 self.dataset.color = color;
264 self.color_overridden = true;
265 self
266 }
267
268 pub fn marker(&mut self, marker: Marker) -> &mut Self {
270 self.dataset.marker = marker;
271 self
272 }
273
274 pub fn color_by_direction(&mut self, up: Color, down: Color) -> &mut Self {
276 self.dataset.up_color = Some(up);
277 self.dataset.down_color = Some(down);
278 self
279 }
280}
281
282#[derive(Debug, Clone)]
284#[must_use = "configure chart before rendering"]
285pub struct ChartBuilder {
286 config: ChartConfig,
287 entries: Vec<DatasetEntry>,
288}
289
290impl ChartBuilder {
291 pub fn new(width: u32, height: u32, x_style: Style, y_style: Style) -> Self {
293 Self {
294 config: ChartConfig {
295 title: None,
296 title_style: None,
297 x_axis: Axis {
298 style: x_style,
299 ..Axis::default()
300 },
301 y_axis: Axis {
302 style: y_style,
303 ..Axis::default()
304 },
305 datasets: Vec::new(),
306 legend: LegendPosition::TopRight,
307 grid: true,
308 grid_style: None,
309 hlines: Vec::new(),
310 vlines: Vec::new(),
311 frame_visible: true,
312 x_axis_visible: true,
313 y_axis_visible: true,
314 width,
315 height,
316 },
317 entries: Vec::new(),
318 }
319 }
320
321 pub fn title(&mut self, title: &str) -> &mut Self {
323 self.config.title = Some(title.to_string());
324 self
325 }
326
327 pub fn xlabel(&mut self, label: &str) -> &mut Self {
329 self.config.x_axis.title = Some(label.to_string());
330 self
331 }
332
333 pub fn ylabel(&mut self, label: &str) -> &mut Self {
335 self.config.y_axis.title = Some(label.to_string());
336 self
337 }
338
339 pub fn xlim(&mut self, min: f64, max: f64) -> &mut Self {
341 self.config.x_axis.bounds = Some((min, max));
342 self
343 }
344
345 pub fn ylim(&mut self, min: f64, max: f64) -> &mut Self {
347 self.config.y_axis.bounds = Some((min, max));
348 self
349 }
350
351 pub fn xticks(&mut self, values: &[f64]) -> &mut Self {
353 self.config.x_axis.ticks = Some(values.to_vec());
354 self
355 }
356
357 pub fn yticks(&mut self, values: &[f64]) -> &mut Self {
359 self.config.y_axis.ticks = Some(values.to_vec());
360 self
361 }
362
363 pub fn xtick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
365 self.config.x_axis.ticks = Some(values.to_vec());
366 self.config.x_axis.labels = Some(labels.iter().map(|label| (*label).to_string()).collect());
367 self
368 }
369
370 pub fn ytick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
372 self.config.y_axis.ticks = Some(values.to_vec());
373 self.config.y_axis.labels = Some(labels.iter().map(|label| (*label).to_string()).collect());
374 self
375 }
376
377 pub fn title_style(&mut self, style: Style) -> &mut Self {
379 self.config.title_style = Some(style);
380 self
381 }
382
383 pub fn grid_style(&mut self, style: Style) -> &mut Self {
385 self.config.grid_style = Some(style);
386 self
387 }
388
389 pub fn x_axis_style(&mut self, style: Style) -> &mut Self {
391 self.config.x_axis.style = style;
392 self
393 }
394
395 pub fn y_axis_style(&mut self, style: Style) -> &mut Self {
397 self.config.y_axis.style = style;
398 self
399 }
400
401 pub fn axhline(&mut self, y: f64, style: Style) -> &mut Self {
403 self.config.hlines.push((y, style));
404 self
405 }
406
407 pub fn axvline(&mut self, x: f64, style: Style) -> &mut Self {
409 self.config.vlines.push((x, style));
410 self
411 }
412
413 pub fn grid(&mut self, on: bool) -> &mut Self {
415 self.config.grid = on;
416 self
417 }
418
419 pub fn frame(&mut self, on: bool) -> &mut Self {
421 self.config.frame_visible = on;
422 self
423 }
424
425 pub fn x_axis_visible(&mut self, on: bool) -> &mut Self {
427 self.config.x_axis_visible = on;
428 self
429 }
430
431 pub fn y_axis_visible(&mut self, on: bool) -> &mut Self {
433 self.config.y_axis_visible = on;
434 self
435 }
436
437 pub fn legend(&mut self, position: LegendPosition) -> &mut Self {
439 self.config.legend = position;
440 self
441 }
442
443 pub fn line(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
445 self.push_dataset(data, GraphType::Line, Marker::Braille)
446 }
447
448 pub fn area(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
450 self.push_dataset(data, GraphType::Area, Marker::Braille)
451 }
452
453 pub fn scatter(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
455 self.push_dataset(data, GraphType::Scatter, Marker::Braille)
456 }
457
458 pub fn bar(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
460 self.push_dataset(data, GraphType::Bar, Marker::Block)
461 }
462
463 pub fn build(mut self) -> ChartConfig {
465 for (index, mut entry) in self.entries.drain(..).enumerate() {
466 if !entry.color_overridden {
467 entry.dataset.color = PALETTE[index % PALETTE.len()];
468 }
469 self.config.datasets.push(entry.dataset);
470 }
471 self.config
472 }
473
474 fn push_dataset(
475 &mut self,
476 data: &[(f64, f64)],
477 graph_type: GraphType,
478 marker: Marker,
479 ) -> &mut DatasetEntry {
480 let series_name = format!("Series {}", self.entries.len() + 1);
481 self.entries.push(DatasetEntry {
482 dataset: Dataset {
483 name: series_name,
484 data: data.to_vec(),
485 color: Color::Reset,
486 marker,
487 graph_type,
488 up_color: None,
489 down_color: None,
490 },
491 color_overridden: false,
492 });
493 let last_index = self.entries.len().saturating_sub(1);
494 &mut self.entries[last_index]
495 }
496}
497
498#[derive(Debug, Clone)]
500pub struct ChartRenderer {
501 config: ChartConfig,
502}
503
504impl ChartRenderer {
505 pub fn new(config: ChartConfig) -> Self {
507 Self { config }
508 }
509
510 pub fn render(&self) -> Vec<RenderedLine> {
512 let rows = render_chart(&self.config);
513 rows.into_iter()
514 .map(|row| {
515 let mut line = String::new();
516 let mut spans: Vec<(usize, usize, Color)> = Vec::new();
517 let mut cursor = 0usize;
518
519 for (segment, style) in row.segments {
520 let width = unicode_width::UnicodeWidthStr::width(segment.as_str());
521 line.push_str(&segment);
522 if let Some(color) = style.fg {
523 spans.push((cursor, cursor + width, color));
524 }
525 cursor += width;
526 }
527
528 (line, spans)
529 })
530 .collect()
531 }
532}