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::{clip_text_cells, truncate_label, write_text_cells};
17pub(crate) use render::render_chart;
18
19pub(crate) use axis::finite_ratio;
20use axis::{TickSpec, build_tui_ticks, format_number, normalize_bounds, resolve_bounds};
21use bar::draw_bar_dataset;
22use braille::draw_braille_dataset;
23use grid::{
24 GridSpec, apply_grid, build_legend_items, build_x_tick_col_map, build_y_tick_row_map,
25 center_text, map_value_to_cell, marker_char, overlay_legend_on_plot, sturges_bin_count,
26};
27
28const BRAILLE_BASE: u32 = 0x2800;
29pub(crate) const BRAILLE_LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
30pub(crate) const BRAILLE_RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
31const PALETTE: [Color; 8] = [
32 Color::Cyan,
33 Color::Yellow,
34 Color::Green,
35 Color::Magenta,
36 Color::Red,
37 Color::Blue,
38 Color::White,
39 Color::Indexed(208),
40];
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 =
346 (min.is_finite() && max.is_finite()).then(|| normalize_bounds(min, max));
347 self
348 }
349
350 pub fn ylim(&mut self, min: f64, max: f64) -> &mut Self {
352 self.config.y_axis.bounds =
353 (min.is_finite() && max.is_finite()).then(|| normalize_bounds(min, max));
354 self
355 }
356
357 pub fn xticks(&mut self, values: &[f64]) -> &mut Self {
359 self.config.x_axis.ticks = Some(
360 values
361 .iter()
362 .copied()
363 .filter(|value| value.is_finite())
364 .collect(),
365 );
366 self
367 }
368
369 pub fn yticks(&mut self, values: &[f64]) -> &mut Self {
371 self.config.y_axis.ticks = Some(
372 values
373 .iter()
374 .copied()
375 .filter(|value| value.is_finite())
376 .collect(),
377 );
378 self
379 }
380
381 pub fn xtick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
383 let pairs: Vec<(f64, String)> = values
384 .iter()
385 .copied()
386 .zip(labels.iter().copied())
387 .filter(|(value, _)| value.is_finite())
388 .map(|(value, label)| (value, label.to_string()))
389 .collect();
390 self.config.x_axis.ticks = Some(pairs.iter().map(|(value, _)| *value).collect());
391 self.config.x_axis.labels = Some(pairs.into_iter().map(|(_, label)| label).collect());
392 self
393 }
394
395 pub fn ytick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
397 let pairs: Vec<(f64, String)> = values
398 .iter()
399 .copied()
400 .zip(labels.iter().copied())
401 .filter(|(value, _)| value.is_finite())
402 .map(|(value, label)| (value, label.to_string()))
403 .collect();
404 self.config.y_axis.ticks = Some(pairs.iter().map(|(value, _)| *value).collect());
405 self.config.y_axis.labels = Some(pairs.into_iter().map(|(_, label)| label).collect());
406 self
407 }
408
409 pub fn title_style(&mut self, style: Style) -> &mut Self {
411 self.config.title_style = Some(style);
412 self
413 }
414
415 pub fn grid_style(&mut self, style: Style) -> &mut Self {
417 self.config.grid_style = Some(style);
418 self
419 }
420
421 pub fn x_axis_style(&mut self, style: Style) -> &mut Self {
423 self.config.x_axis.style = style;
424 self
425 }
426
427 pub fn y_axis_style(&mut self, style: Style) -> &mut Self {
429 self.config.y_axis.style = style;
430 self
431 }
432
433 pub fn axhline(&mut self, y: f64, style: Style) -> &mut Self {
435 if y.is_finite() {
436 self.config.hlines.push((y, style));
437 }
438 self
439 }
440
441 pub fn axvline(&mut self, x: f64, style: Style) -> &mut Self {
443 if x.is_finite() {
444 self.config.vlines.push((x, style));
445 }
446 self
447 }
448
449 pub fn grid(&mut self, on: bool) -> &mut Self {
451 self.config.grid = on;
452 self
453 }
454
455 pub fn frame(&mut self, on: bool) -> &mut Self {
457 self.config.frame_visible = on;
458 self
459 }
460
461 pub fn x_axis_visible(&mut self, on: bool) -> &mut Self {
463 self.config.x_axis_visible = on;
464 self
465 }
466
467 pub fn y_axis_visible(&mut self, on: bool) -> &mut Self {
469 self.config.y_axis_visible = on;
470 self
471 }
472
473 pub fn legend(&mut self, position: LegendPosition) -> &mut Self {
475 self.config.legend = position;
476 self
477 }
478
479 pub fn line(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
481 self.push_dataset(data, GraphType::Line, Marker::Braille)
482 }
483
484 pub fn area(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
486 self.push_dataset(data, GraphType::Area, Marker::Braille)
487 }
488
489 pub fn scatter(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
491 self.push_dataset(data, GraphType::Scatter, Marker::Braille)
492 }
493
494 pub fn bar(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
496 self.push_dataset(data, GraphType::Bar, Marker::Block)
497 }
498
499 pub fn build(mut self) -> ChartConfig {
501 for (index, mut entry) in self.entries.drain(..).enumerate() {
502 if !entry.color_overridden {
503 entry.dataset.color = PALETTE[index % PALETTE.len()];
504 }
505 self.config.datasets.push(entry.dataset);
506 }
507 self.config
508 }
509
510 fn push_dataset(
511 &mut self,
512 data: &[(f64, f64)],
513 graph_type: GraphType,
514 marker: Marker,
515 ) -> &mut DatasetEntry {
516 let series_name = format!("Series {}", self.entries.len() + 1);
517 self.entries.push(DatasetEntry {
518 dataset: Dataset {
519 name: series_name,
520 data: data
521 .iter()
522 .copied()
523 .filter(|(x, y)| x.is_finite() && y.is_finite())
524 .collect(),
525 color: Color::Reset,
526 marker,
527 graph_type,
528 up_color: None,
529 down_color: None,
530 },
531 color_overridden: false,
532 });
533 let last_index = self.entries.len().saturating_sub(1);
534 &mut self.entries[last_index]
535 }
536}
537
538#[derive(Debug, Clone)]
540pub struct ChartRenderer {
541 config: ChartConfig,
542}
543
544impl ChartRenderer {
545 pub fn new(config: ChartConfig) -> Self {
547 Self { config }
548 }
549
550 pub fn render(&self) -> Vec<RenderedLine> {
552 let rows = render_chart(&self.config);
553 rows.into_iter()
554 .map(|row| {
555 let mut line = String::new();
556 let mut spans: Vec<(usize, usize, Color)> = Vec::new();
557 let mut cursor = 0usize;
558
559 for (segment, style) in row.segments {
560 let width = unicode_width::UnicodeWidthStr::width(segment.as_str());
561 line.push_str(&segment);
562 if let Some(color) = style.fg {
563 spans.push((cursor, cursor + width, color));
564 }
565 cursor += width;
566 }
567
568 (line, spans)
569 })
570 .collect()
571 }
572}