1use crate::style::{Color, Style};
8use unicode_width::UnicodeWidthStr;
9
10const BRAILLE_BASE: u32 = 0x2800;
11const BRAILLE_LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
12const BRAILLE_RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
13const PALETTE: [Color; 8] = [
14 Color::Cyan,
15 Color::Yellow,
16 Color::Green,
17 Color::Magenta,
18 Color::Red,
19 Color::Blue,
20 Color::White,
21 Color::Indexed(208),
22];
23const BLOCK_FRACTIONS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
24
25pub type ColorSpan = (usize, usize, Color);
27
28pub type RenderedLine = (String, Vec<ColorSpan>);
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Marker {
34 Braille,
36 Dot,
38 Block,
40 HalfBlock,
42 Cross,
44 Circle,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum GraphType {
51 Line,
53 Area,
55 Scatter,
57 Bar,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum LegendPosition {
64 TopLeft,
66 TopRight,
68 BottomLeft,
70 BottomRight,
72 None,
74}
75
76#[derive(Debug, Clone)]
78pub struct Axis {
79 pub title: Option<String>,
81 pub bounds: Option<(f64, f64)>,
83 pub labels: Option<Vec<String>>,
85 pub ticks: Option<Vec<f64>>,
87 pub title_style: Option<Style>,
89 pub style: Style,
91}
92
93impl Default for Axis {
94 fn default() -> Self {
95 Self {
96 title: None,
97 bounds: None,
98 labels: None,
99 ticks: None,
100 title_style: None,
101 style: Style::new(),
102 }
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct Dataset {
109 pub name: String,
111 pub data: Vec<(f64, f64)>,
113 pub color: Color,
115 pub marker: Marker,
117 pub graph_type: GraphType,
119 pub up_color: Option<Color>,
121 pub down_color: Option<Color>,
123}
124
125#[derive(Debug, Clone, Copy)]
127pub struct Candle {
128 pub open: f64,
130 pub high: f64,
132 pub low: f64,
134 pub close: f64,
136}
137
138#[derive(Debug, Clone)]
140pub struct ChartConfig {
141 pub title: Option<String>,
143 pub title_style: Option<Style>,
145 pub x_axis: Axis,
147 pub y_axis: Axis,
149 pub datasets: Vec<Dataset>,
151 pub legend: LegendPosition,
153 pub grid: bool,
155 pub grid_style: Option<Style>,
157 pub hlines: Vec<(f64, Style)>,
159 pub vlines: Vec<(f64, Style)>,
161 pub frame_visible: bool,
163 pub x_axis_visible: bool,
165 pub y_axis_visible: bool,
167 pub width: u32,
169 pub height: u32,
171}
172
173#[derive(Debug, Clone)]
175pub(crate) struct ChartRow {
176 pub segments: Vec<(String, Style)>,
178}
179
180#[derive(Debug, Clone)]
182#[must_use = "configure histogram before rendering"]
183pub struct HistogramBuilder {
184 pub bins: Option<usize>,
186 pub color: Color,
188 pub x_title: Option<String>,
190 pub y_title: Option<String>,
192}
193
194impl Default for HistogramBuilder {
195 fn default() -> Self {
196 Self {
197 bins: None,
198 color: Color::Cyan,
199 x_title: None,
200 y_title: Some("Count".to_string()),
201 }
202 }
203}
204
205impl HistogramBuilder {
206 pub fn bins(&mut self, bins: usize) -> &mut Self {
208 self.bins = Some(bins.max(1));
209 self
210 }
211
212 pub fn color(&mut self, color: Color) -> &mut Self {
214 self.color = color;
215 self
216 }
217
218 pub fn xlabel(&mut self, title: &str) -> &mut Self {
220 self.x_title = Some(title.to_string());
221 self
222 }
223
224 pub fn ylabel(&mut self, title: &str) -> &mut Self {
226 self.y_title = Some(title.to_string());
227 self
228 }
229}
230
231#[derive(Debug, Clone)]
233pub struct DatasetEntry {
234 dataset: Dataset,
235 color_overridden: bool,
236}
237
238impl DatasetEntry {
239 pub fn label(&mut self, name: &str) -> &mut Self {
241 self.dataset.name = name.to_string();
242 self
243 }
244
245 pub fn color(&mut self, color: Color) -> &mut Self {
247 self.dataset.color = color;
248 self.color_overridden = true;
249 self
250 }
251
252 pub fn marker(&mut self, marker: Marker) -> &mut Self {
254 self.dataset.marker = marker;
255 self
256 }
257
258 pub fn color_by_direction(&mut self, up: Color, down: Color) -> &mut Self {
260 self.dataset.up_color = Some(up);
261 self.dataset.down_color = Some(down);
262 self
263 }
264}
265
266#[derive(Debug, Clone)]
268#[must_use = "configure chart before rendering"]
269pub struct ChartBuilder {
270 config: ChartConfig,
271 entries: Vec<DatasetEntry>,
272}
273
274impl ChartBuilder {
275 pub fn new(width: u32, height: u32, x_style: Style, y_style: Style) -> Self {
277 Self {
278 config: ChartConfig {
279 title: None,
280 title_style: None,
281 x_axis: Axis {
282 style: x_style,
283 ..Axis::default()
284 },
285 y_axis: Axis {
286 style: y_style,
287 ..Axis::default()
288 },
289 datasets: Vec::new(),
290 legend: LegendPosition::TopRight,
291 grid: true,
292 grid_style: None,
293 hlines: Vec::new(),
294 vlines: Vec::new(),
295 frame_visible: true,
296 x_axis_visible: true,
297 y_axis_visible: true,
298 width,
299 height,
300 },
301 entries: Vec::new(),
302 }
303 }
304
305 pub fn title(&mut self, title: &str) -> &mut Self {
307 self.config.title = Some(title.to_string());
308 self
309 }
310
311 pub fn xlabel(&mut self, label: &str) -> &mut Self {
313 self.config.x_axis.title = Some(label.to_string());
314 self
315 }
316
317 pub fn ylabel(&mut self, label: &str) -> &mut Self {
319 self.config.y_axis.title = Some(label.to_string());
320 self
321 }
322
323 pub fn xlim(&mut self, min: f64, max: f64) -> &mut Self {
325 self.config.x_axis.bounds = Some((min, max));
326 self
327 }
328
329 pub fn ylim(&mut self, min: f64, max: f64) -> &mut Self {
331 self.config.y_axis.bounds = Some((min, max));
332 self
333 }
334
335 pub fn xticks(&mut self, values: &[f64]) -> &mut Self {
337 self.config.x_axis.ticks = Some(values.to_vec());
338 self
339 }
340
341 pub fn yticks(&mut self, values: &[f64]) -> &mut Self {
343 self.config.y_axis.ticks = Some(values.to_vec());
344 self
345 }
346
347 pub fn xtick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
349 self.config.x_axis.ticks = Some(values.to_vec());
350 self.config.x_axis.labels = Some(labels.iter().map(|label| (*label).to_string()).collect());
351 self
352 }
353
354 pub fn ytick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
356 self.config.y_axis.ticks = Some(values.to_vec());
357 self.config.y_axis.labels = Some(labels.iter().map(|label| (*label).to_string()).collect());
358 self
359 }
360
361 pub fn title_style(&mut self, style: Style) -> &mut Self {
363 self.config.title_style = Some(style);
364 self
365 }
366
367 pub fn grid_style(&mut self, style: Style) -> &mut Self {
369 self.config.grid_style = Some(style);
370 self
371 }
372
373 pub fn x_axis_style(&mut self, style: Style) -> &mut Self {
375 self.config.x_axis.style = style;
376 self
377 }
378
379 pub fn y_axis_style(&mut self, style: Style) -> &mut Self {
381 self.config.y_axis.style = style;
382 self
383 }
384
385 pub fn axhline(&mut self, y: f64, style: Style) -> &mut Self {
387 self.config.hlines.push((y, style));
388 self
389 }
390
391 pub fn axvline(&mut self, x: f64, style: Style) -> &mut Self {
393 self.config.vlines.push((x, style));
394 self
395 }
396
397 pub fn grid(&mut self, on: bool) -> &mut Self {
399 self.config.grid = on;
400 self
401 }
402
403 pub fn frame(&mut self, on: bool) -> &mut Self {
405 self.config.frame_visible = on;
406 self
407 }
408
409 pub fn x_axis_visible(&mut self, on: bool) -> &mut Self {
411 self.config.x_axis_visible = on;
412 self
413 }
414
415 pub fn y_axis_visible(&mut self, on: bool) -> &mut Self {
417 self.config.y_axis_visible = on;
418 self
419 }
420
421 pub fn legend(&mut self, position: LegendPosition) -> &mut Self {
423 self.config.legend = position;
424 self
425 }
426
427 pub fn line(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
429 self.push_dataset(data, GraphType::Line, Marker::Braille)
430 }
431
432 pub fn area(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
434 self.push_dataset(data, GraphType::Area, Marker::Braille)
435 }
436
437 pub fn scatter(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
439 self.push_dataset(data, GraphType::Scatter, Marker::Braille)
440 }
441
442 pub fn bar(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
444 self.push_dataset(data, GraphType::Bar, Marker::Block)
445 }
446
447 pub fn build(mut self) -> ChartConfig {
449 for (index, mut entry) in self.entries.drain(..).enumerate() {
450 if !entry.color_overridden {
451 entry.dataset.color = PALETTE[index % PALETTE.len()];
452 }
453 self.config.datasets.push(entry.dataset);
454 }
455 self.config
456 }
457
458 fn push_dataset(
459 &mut self,
460 data: &[(f64, f64)],
461 graph_type: GraphType,
462 marker: Marker,
463 ) -> &mut DatasetEntry {
464 let series_name = format!("Series {}", self.entries.len() + 1);
465 self.entries.push(DatasetEntry {
466 dataset: Dataset {
467 name: series_name,
468 data: data.to_vec(),
469 color: Color::Reset,
470 marker,
471 graph_type,
472 up_color: None,
473 down_color: None,
474 },
475 color_overridden: false,
476 });
477 let last_index = self.entries.len().saturating_sub(1);
478 &mut self.entries[last_index]
479 }
480}
481
482#[derive(Debug, Clone)]
484pub struct ChartRenderer {
485 config: ChartConfig,
486}
487
488impl ChartRenderer {
489 pub fn new(config: ChartConfig) -> Self {
491 Self { config }
492 }
493
494 pub fn render(&self) -> Vec<RenderedLine> {
496 let rows = render_chart(&self.config);
497 rows.into_iter()
498 .map(|row| {
499 let mut line = String::new();
500 let mut spans: Vec<(usize, usize, Color)> = Vec::new();
501 let mut cursor = 0usize;
502
503 for (segment, style) in row.segments {
504 let width = UnicodeWidthStr::width(segment.as_str());
505 line.push_str(&segment);
506 if let Some(color) = style.fg {
507 spans.push((cursor, cursor + width, color));
508 }
509 cursor += width;
510 }
511
512 (line, spans)
513 })
514 .collect()
515 }
516}
517
518pub(crate) fn build_histogram_config(
520 data: &[f64],
521 options: &HistogramBuilder,
522 width: u32,
523 height: u32,
524 axis_style: Style,
525) -> ChartConfig {
526 let mut sorted: Vec<f64> = data.iter().copied().filter(|v| v.is_finite()).collect();
527 sorted.sort_by(f64::total_cmp);
528
529 if sorted.is_empty() {
530 return ChartConfig {
531 title: Some("Histogram".to_string()),
532 title_style: None,
533 x_axis: Axis {
534 title: options.x_title.clone(),
535 bounds: Some((0.0, 1.0)),
536 labels: None,
537 ticks: None,
538 title_style: None,
539 style: axis_style,
540 },
541 y_axis: Axis {
542 title: options.y_title.clone(),
543 bounds: Some((0.0, 1.0)),
544 labels: None,
545 ticks: None,
546 title_style: None,
547 style: axis_style,
548 },
549 datasets: Vec::new(),
550 legend: LegendPosition::None,
551 grid: true,
552 grid_style: None,
553 hlines: Vec::new(),
554 vlines: Vec::new(),
555 frame_visible: true,
556 x_axis_visible: true,
557 y_axis_visible: true,
558 width,
559 height,
560 };
561 }
562
563 let n = sorted.len();
564 let min = sorted[0];
565 let max = sorted[n.saturating_sub(1)];
566 let bin_count = options.bins.unwrap_or_else(|| sturges_bin_count(n));
567
568 let span = if (max - min).abs() < f64::EPSILON {
569 1.0
570 } else {
571 max - min
572 };
573 let bin_width = span / bin_count as f64;
574
575 let mut counts = vec![0usize; bin_count];
576 for value in sorted {
577 let raw = ((value - min) / bin_width).floor();
578 let mut idx = if raw.is_finite() { raw as isize } else { 0 };
579 if idx < 0 {
580 idx = 0;
581 }
582 if idx as usize >= bin_count {
583 idx = (bin_count.saturating_sub(1)) as isize;
584 }
585 counts[idx as usize] = counts[idx as usize].saturating_add(1);
586 }
587
588 let mut data_points = Vec::with_capacity(bin_count);
589 for (i, count) in counts.iter().enumerate() {
590 let center = min + (i as f64 + 0.5) * bin_width;
591 data_points.push((center, *count as f64));
592 }
593
594 let mut labels: Vec<String> = Vec::new();
595 let step = (bin_count / 4).max(1);
596 for i in (0..=bin_count).step_by(step) {
597 let edge = min + i as f64 * bin_width;
598 labels.push(format_number(edge, bin_width));
599 }
600
601 ChartConfig {
602 title: Some("Histogram".to_string()),
603 title_style: None,
604 x_axis: Axis {
605 title: options.x_title.clone(),
606 bounds: Some((min, max.max(min + bin_width))),
607 labels: Some(labels),
608 ticks: None,
609 title_style: None,
610 style: axis_style,
611 },
612 y_axis: Axis {
613 title: options.y_title.clone(),
614 bounds: Some((0.0, counts.iter().copied().max().unwrap_or(1) as f64)),
615 labels: None,
616 ticks: None,
617 title_style: None,
618 style: axis_style,
619 },
620 datasets: vec![Dataset {
621 name: "Histogram".to_string(),
622 data: data_points,
623 color: options.color,
624 marker: Marker::Block,
625 graph_type: GraphType::Bar,
626 up_color: None,
627 down_color: None,
628 }],
629 legend: LegendPosition::None,
630 grid: true,
631 grid_style: None,
632 hlines: Vec::new(),
633 vlines: Vec::new(),
634 frame_visible: true,
635 x_axis_visible: true,
636 y_axis_visible: true,
637 width,
638 height,
639 }
640}
641
642pub(crate) fn render_chart(config: &ChartConfig) -> Vec<ChartRow> {
644 let width = config.width as usize;
645 let height = config.height as usize;
646 if width == 0 || height == 0 {
647 return Vec::new();
648 }
649
650 let frame_style = config.x_axis.style;
651 let dim_style = Style::new().dim();
652 let axis_style = config.y_axis.style;
653 let title_style = Style::new()
654 .bold()
655 .fg(config.x_axis.style.fg.unwrap_or(Color::White));
656 let title_style = config.title_style.unwrap_or(title_style);
657
658 let title_rows = usize::from(config.title.is_some());
659 let has_x_title = config.x_axis_visible && config.x_axis.title.is_some();
660 let x_title_rows = usize::from(has_x_title);
661 let frame_rows = if config.frame_visible { 2 } else { 0 };
662 let x_axis_rows = if config.x_axis_visible {
663 2 + x_title_rows
664 } else {
665 0
666 };
667
668 let overhead = title_rows + frame_rows + x_axis_rows;
673 if height <= overhead || width < 3 {
674 return minimal_chart(config, width, frame_style, title_style);
675 }
676 let plot_height = height.saturating_sub(overhead).max(1);
677
678 let (x_min, x_max) = resolve_bounds(
679 config
680 .datasets
681 .iter()
682 .flat_map(|d| d.data.iter().map(|p| p.0)),
683 config.x_axis.bounds,
684 );
685 let (y_min, y_max) = resolve_bounds(
686 config
687 .datasets
688 .iter()
689 .flat_map(|d| d.data.iter().map(|p| p.1)),
690 config.y_axis.bounds,
691 );
692
693 let y_label_chars: Vec<char> = if config.y_axis_visible {
694 config
695 .y_axis
696 .title
697 .as_deref()
698 .map(|t| t.chars().collect())
699 .unwrap_or_default()
700 } else {
701 Vec::new()
702 };
703 let y_label_col_width = if y_label_chars.is_empty() { 0 } else { 2 };
704
705 let legend_items = build_legend_items(&config.datasets);
706 let legend_on_right = matches!(
707 config.legend,
708 LegendPosition::TopRight | LegendPosition::BottomRight
709 );
710 let legend_width = if legend_on_right && !legend_items.is_empty() {
711 legend_items
712 .iter()
713 .map(|(_, name, _)| 4 + UnicodeWidthStr::width(name.as_str()))
714 .max()
715 .unwrap_or(0)
716 } else {
717 0
718 };
719
720 let y_ticks = if let Some(ref manual) = config.y_axis.ticks {
721 TickSpec {
722 values: manual.clone(),
723 step: if manual.len() > 1 {
724 manual[1] - manual[0]
725 } else {
726 1.0
727 },
728 }
729 } else {
730 build_tui_ticks(y_min, y_max, plot_height)
731 };
732 let y_min = y_ticks.values.first().copied().unwrap_or(y_min).min(y_min);
733 let y_max = y_ticks.values.last().copied().unwrap_or(y_max).max(y_max);
734
735 let use_manual_y_labels = config.y_axis.ticks.is_some() && config.y_axis.labels.is_some();
736 let y_tick_labels: Vec<String> = if use_manual_y_labels {
737 config
738 .y_axis
739 .labels
740 .as_deref()
741 .unwrap_or(&[])
742 .iter()
743 .take(y_ticks.values.len())
744 .cloned()
745 .collect()
746 } else {
747 y_ticks
748 .values
749 .iter()
750 .map(|v| format_number(*v, y_ticks.step))
751 .collect()
752 };
753 let y_tick_width = y_tick_labels
754 .iter()
755 .map(|s| UnicodeWidthStr::width(s.as_str()))
756 .max()
757 .unwrap_or(1);
758 let y_axis_width = if config.y_axis_visible {
759 y_tick_width + 2
760 } else {
761 0
762 };
763
764 let inner_width = if config.frame_visible {
765 width.saturating_sub(2)
766 } else {
767 width
768 };
769 let plot_width = inner_width
770 .saturating_sub(y_label_col_width)
771 .saturating_sub(y_axis_width)
772 .saturating_sub(legend_width)
773 .max(1);
774 let content_width = y_label_col_width + y_axis_width + plot_width + legend_width;
775
776 let x_ticks = if let Some(ref manual) = config.x_axis.ticks {
777 TickSpec {
778 values: manual.clone(),
779 step: if manual.len() > 1 {
780 manual[1] - manual[0]
781 } else {
782 1.0
783 },
784 }
785 } else {
786 build_tui_ticks(x_min, x_max, plot_width)
787 };
788 let x_min = x_ticks.values.first().copied().unwrap_or(x_min).min(x_min);
789 let x_max = x_ticks.values.last().copied().unwrap_or(x_max).max(x_max);
790
791 let mut plot_chars = vec![vec![' '; plot_width]; plot_height];
792 let mut plot_styles = vec![vec![Style::new(); plot_width]; plot_height];
793
794 apply_grid(
795 config,
796 GridSpec {
797 x_ticks: &x_ticks.values,
798 y_ticks: &y_ticks.values,
799 x_min,
800 x_max,
801 y_min,
802 y_max,
803 },
804 &mut plot_chars,
805 &mut plot_styles,
806 config.grid_style.unwrap_or(dim_style),
807 );
808
809 for &(y_val, ref style) in &config.hlines {
810 let row = map_value_to_cell(y_val, y_min, y_max, plot_height, true);
811 if row < plot_height {
812 for col in 0..plot_width {
813 plot_chars[row][col] = '─';
814 plot_styles[row][col] = *style;
815 }
816 }
817 }
818 for &(x_val, ref style) in &config.vlines {
819 let col = map_value_to_cell(x_val, x_min, x_max, plot_width, false);
820 if col < plot_width {
821 for row in 0..plot_height {
822 if plot_chars[row][col] == ' ' || plot_chars[row][col] == '·' {
823 plot_chars[row][col] = '│';
824 plot_styles[row][col] = *style;
825 }
826 }
827 }
828 }
829
830 for dataset in &config.datasets {
831 match dataset.graph_type {
832 GraphType::Line | GraphType::Area | GraphType::Scatter => {
833 draw_braille_dataset(
834 dataset,
835 x_min,
836 x_max,
837 y_min,
838 y_max,
839 &mut plot_chars,
840 &mut plot_styles,
841 );
842 }
843 GraphType::Bar => {
844 draw_bar_dataset(
845 dataset,
846 x_min,
847 x_max,
848 y_min,
849 y_max,
850 &mut plot_chars,
851 &mut plot_styles,
852 );
853 }
854 }
855 }
856
857 if !legend_items.is_empty()
858 && matches!(
859 config.legend,
860 LegendPosition::TopLeft | LegendPosition::BottomLeft
861 )
862 {
863 overlay_legend_on_plot(
864 config.legend,
865 &legend_items,
866 &mut plot_chars,
867 &mut plot_styles,
868 axis_style,
869 );
870 }
871
872 let y_tick_rows = build_y_tick_row_map(
873 &y_ticks.values,
874 if use_manual_y_labels {
875 config.y_axis.labels.as_deref()
876 } else {
877 None
878 },
879 y_min,
880 y_max,
881 plot_height,
882 );
883 let x_tick_cols = build_x_tick_col_map(
884 &x_ticks.values,
885 config.x_axis.labels.as_deref(),
886 config.x_axis.ticks.is_some() && config.x_axis.labels.is_some(),
887 x_min,
888 x_max,
889 plot_width,
890 );
891
892 let mut rows: Vec<ChartRow> = Vec::with_capacity(height);
893
894 if let Some(title) = &config.title {
896 rows.push(ChartRow {
897 segments: vec![(center_text(title, width), title_style)],
898 });
899 }
900
901 if config.frame_visible {
902 rows.push(ChartRow {
903 segments: vec![(format!("┌{}┐", "─".repeat(content_width)), frame_style)],
904 });
905 }
906
907 let y_label_start = if y_label_chars.is_empty() {
908 0
909 } else {
910 plot_height.saturating_sub(y_label_chars.len()) / 2
911 };
912 let y_title_style = config.y_axis.title_style.unwrap_or(axis_style);
913
914 let zero_label = format_number(0.0, y_ticks.step);
915 for row in 0..plot_height {
916 let mut segments: Vec<(String, Style)> = Vec::new();
917 if config.frame_visible {
918 segments.push(("│".to_string(), frame_style));
919 }
920
921 if config.y_axis_visible {
922 if y_label_col_width > 0 {
923 let label_idx = row.wrapping_sub(y_label_start);
924 if label_idx < y_label_chars.len() {
925 segments.push((format!("{} ", y_label_chars[label_idx]), y_title_style));
926 } else {
927 segments.push((" ".to_string(), Style::new()));
928 }
929 }
930
931 let (label, divider) =
932 if let Some(index) = y_tick_rows.iter().position(|(r, _)| *r == row) {
933 let is_zero = y_tick_rows[index].1 == zero_label;
934 (
935 y_tick_rows[index].1.clone(),
936 if is_zero { '┼' } else { '┤' },
937 )
938 } else {
939 (String::new(), '│')
940 };
941 let padded = format!("{:>w$}", label, w = y_tick_width);
942 segments.push((padded, axis_style));
943 segments.push((format!("{divider} "), axis_style));
944 }
945
946 let mut current_style = Style::new();
947 let mut buffer = String::new();
948 for col in 0..plot_width {
949 let style = plot_styles[row][col];
950 if col == 0 {
951 current_style = style;
952 }
953 if style != current_style {
954 if !buffer.is_empty() {
955 segments.push((buffer.clone(), current_style));
956 buffer.clear();
957 }
958 current_style = style;
959 }
960 buffer.push(plot_chars[row][col]);
961 }
962 if !buffer.is_empty() {
963 segments.push((buffer, current_style));
964 }
965
966 if legend_on_right && legend_width > 0 {
967 let legend_row = match config.legend {
968 LegendPosition::TopRight => row,
969 LegendPosition::BottomRight => {
970 row.wrapping_add(legend_items.len().saturating_sub(plot_height))
971 }
972 _ => usize::MAX,
973 };
974 if let Some((symbol, name, color)) = legend_items.get(legend_row) {
975 let raw = format!(" {symbol} {name}");
976 let raw_w = UnicodeWidthStr::width(raw.as_str());
977 let pad = legend_width.saturating_sub(raw_w);
978 let text = format!("{raw}{}", " ".repeat(pad));
979 segments.push((text, Style::new().fg(*color)));
980 } else {
981 segments.push((" ".repeat(legend_width), Style::new()));
982 }
983 }
984
985 if config.frame_visible {
986 segments.push(("│".to_string(), frame_style));
987 }
988 rows.push(ChartRow { segments });
989 }
990
991 if config.x_axis_visible {
992 let mut axis_line = vec!['─'; plot_width];
993 for (col, _) in &x_tick_cols {
994 if *col < plot_width {
995 axis_line[*col] = '┬';
996 }
997 }
998 let footer_legend_pad = " ".repeat(legend_width);
999 let footer_ylabel_pad = if config.y_axis_visible {
1000 " ".repeat(y_label_col_width)
1001 } else {
1002 String::new()
1003 };
1004
1005 let mut axis_segments: Vec<(String, Style)> = Vec::new();
1006 if config.frame_visible {
1007 axis_segments.push(("│".to_string(), frame_style));
1008 }
1009 if config.y_axis_visible {
1010 axis_segments.push((footer_ylabel_pad.clone(), Style::new()));
1011 axis_segments.push((" ".repeat(y_tick_width), axis_style));
1012 axis_segments.push(("┴─".to_string(), axis_style));
1013 }
1014 axis_segments.push((axis_line.into_iter().collect(), axis_style));
1015 axis_segments.push((footer_legend_pad.clone(), Style::new()));
1016 if config.frame_visible {
1017 axis_segments.push(("│".to_string(), frame_style));
1018 }
1019 rows.push(ChartRow {
1020 segments: axis_segments,
1021 });
1022
1023 let mut x_label_line: Vec<char> = vec![' '; plot_width];
1024 let mut occupied_until: usize = 0;
1025 for (col, label) in &x_tick_cols {
1026 if label.is_empty() {
1027 continue;
1028 }
1029 let label_width = UnicodeWidthStr::width(label.as_str());
1030 let start = col
1031 .saturating_sub(label_width / 2)
1032 .min(plot_width.saturating_sub(label_width));
1033 if start < occupied_until {
1034 continue;
1035 }
1036 for (offset, ch) in label.chars().enumerate() {
1037 let idx = start + offset;
1038 if idx < plot_width {
1039 x_label_line[idx] = ch;
1040 }
1041 }
1042 occupied_until = start + label_width + 1;
1043 }
1044
1045 let mut x_label_segments: Vec<(String, Style)> = Vec::new();
1046 if config.frame_visible {
1047 x_label_segments.push(("│".to_string(), frame_style));
1048 }
1049 if config.y_axis_visible {
1050 x_label_segments.push((footer_ylabel_pad.clone(), Style::new()));
1051 x_label_segments.push((" ".repeat(y_axis_width), Style::new()));
1052 }
1053 x_label_segments.push((x_label_line.into_iter().collect(), axis_style));
1054 x_label_segments.push((footer_legend_pad.clone(), Style::new()));
1055 if config.frame_visible {
1056 x_label_segments.push(("│".to_string(), frame_style));
1057 }
1058 rows.push(ChartRow {
1059 segments: x_label_segments,
1060 });
1061
1062 if has_x_title {
1063 let x_title_text = config.x_axis.title.as_deref().unwrap_or_default();
1064 let x_title = center_text(x_title_text, plot_width);
1065 let x_title_style = config.x_axis.title_style.unwrap_or(axis_style);
1066 let mut x_title_segments: Vec<(String, Style)> = Vec::new();
1067 if config.frame_visible {
1068 x_title_segments.push(("│".to_string(), frame_style));
1069 }
1070 if config.y_axis_visible {
1071 x_title_segments.push((footer_ylabel_pad, Style::new()));
1072 x_title_segments.push((" ".repeat(y_axis_width), Style::new()));
1073 }
1074 x_title_segments.push((x_title, x_title_style));
1075 x_title_segments.push((footer_legend_pad, Style::new()));
1076 if config.frame_visible {
1077 x_title_segments.push(("│".to_string(), frame_style));
1078 }
1079 rows.push(ChartRow {
1080 segments: x_title_segments,
1081 });
1082 }
1083 }
1084
1085 if config.frame_visible {
1086 rows.push(ChartRow {
1087 segments: vec![(format!("└{}┘", "─".repeat(content_width)), frame_style)],
1088 });
1089 }
1090
1091 rows
1092}
1093
1094fn minimal_chart(
1095 config: &ChartConfig,
1096 width: usize,
1097 frame_style: Style,
1098 title_style: Style,
1099) -> Vec<ChartRow> {
1100 let mut rows = Vec::new();
1101 if let Some(title) = &config.title {
1102 rows.push(ChartRow {
1103 segments: vec![(center_text(title, width), title_style)],
1104 });
1105 }
1106 if config.frame_visible {
1107 let inner = width.saturating_sub(2);
1108 rows.push(ChartRow {
1109 segments: vec![(format!("┌{}┐", "─".repeat(inner)), frame_style)],
1110 });
1111 rows.push(ChartRow {
1112 segments: vec![(format!("│{}│", " ".repeat(inner)), frame_style)],
1113 });
1114 rows.push(ChartRow {
1115 segments: vec![(format!("└{}┘", "─".repeat(inner)), frame_style)],
1116 });
1117 } else {
1118 rows.push(ChartRow {
1119 segments: vec![(" ".repeat(width), Style::new())],
1120 });
1121 }
1122 rows
1123}
1124
1125fn resolve_bounds<I>(values: I, manual: Option<(f64, f64)>) -> (f64, f64)
1126where
1127 I: Iterator<Item = f64>,
1128{
1129 if let Some((min, max)) = manual {
1130 return normalize_bounds(min, max);
1131 }
1132
1133 let mut min = f64::INFINITY;
1134 let mut max = f64::NEG_INFINITY;
1135 for value in values {
1136 if !value.is_finite() {
1137 continue;
1138 }
1139 min = min.min(value);
1140 max = max.max(value);
1141 }
1142
1143 if !min.is_finite() || !max.is_finite() {
1144 return (0.0, 1.0);
1145 }
1146
1147 normalize_bounds(min, max)
1148}
1149
1150fn normalize_bounds(min: f64, max: f64) -> (f64, f64) {
1151 if (max - min).abs() < f64::EPSILON {
1152 let pad = if min.abs() < 1.0 {
1153 1.0
1154 } else {
1155 min.abs() * 0.1
1156 };
1157 (min - pad, max + pad)
1158 } else if min < max {
1159 (min, max)
1160 } else {
1161 (max, min)
1162 }
1163}
1164
1165#[derive(Debug, Clone)]
1166struct TickSpec {
1167 values: Vec<f64>,
1168 step: f64,
1169}
1170
1171fn build_ticks(min: f64, max: f64, target: usize) -> TickSpec {
1172 let span = (max - min).abs().max(f64::EPSILON);
1173 let range = nice_number(span, false);
1174 let raw_step = range / (target.max(2) as f64 - 1.0);
1175 let step = nice_number(raw_step, true).max(f64::EPSILON);
1176 let nice_min = (min / step).floor() * step;
1177 let nice_max = (max / step).ceil() * step;
1178
1179 let mut values = Vec::new();
1180 let mut value = nice_min;
1181 let limit = nice_max + step * 0.5;
1182 let mut guard = 0usize;
1183 while value <= limit && guard < 128 {
1184 values.push(value);
1185 value += step;
1186 guard = guard.saturating_add(1);
1187 }
1188
1189 if values.is_empty() {
1190 values.push(min);
1191 values.push(max);
1192 }
1193
1194 TickSpec { values, step }
1195}
1196
1197fn build_tui_ticks(data_min: f64, data_max: f64, cell_count: usize) -> TickSpec {
1201 let last = cell_count.saturating_sub(1).max(1);
1202 let span = (data_max - data_min).abs().max(f64::EPSILON);
1203 let log = span.log10().floor();
1204
1205 let mut candidates: Vec<(f64, f64, usize, usize)> = Vec::new();
1206
1207 for exp_off in -1..=1i32 {
1208 let base = 10.0_f64.powf(log + f64::from(exp_off));
1209 for &mult in &[1.0, 2.0, 2.5, 5.0] {
1210 let step = base * mult;
1211 if step <= 0.0 || !step.is_finite() {
1212 continue;
1213 }
1214 let lo = (data_min / step).floor() * step;
1215 let hi = (data_max / step).ceil() * step;
1216 let n = ((hi - lo) / step + 0.5) as usize;
1217 if (3..=8).contains(&n) && last / n >= 2 {
1218 let rem = last % n;
1219 candidates.push((step, lo, n, rem));
1220 }
1221 }
1222 }
1223
1224 candidates.sort_by(|a, b| {
1225 a.3.cmp(&b.3).then_with(|| {
1226 let da = (a.2 as i32 - 5).unsigned_abs();
1227 let db = (b.2 as i32 - 5).unsigned_abs();
1228 da.cmp(&db)
1229 })
1230 });
1231
1232 if let Some(&(step, lo, n, _)) = candidates.first() {
1233 let values: Vec<f64> = (0..=n).map(|i| lo + step * i as f64).collect();
1234 return TickSpec { values, step };
1235 }
1236
1237 build_ticks(data_min, data_max, 5)
1238}
1239
1240fn nice_number(value: f64, round: bool) -> f64 {
1241 if value <= 0.0 || !value.is_finite() {
1242 return 1.0;
1243 }
1244 let exponent = value.log10().floor();
1245 let power = 10.0_f64.powf(exponent);
1246 let fraction = value / power;
1247
1248 let nice_fraction = if round {
1249 if fraction < 1.5 {
1250 1.0
1251 } else if fraction < 3.0 {
1252 2.0
1253 } else if fraction < 7.0 {
1254 5.0
1255 } else {
1256 10.0
1257 }
1258 } else if fraction <= 1.0 {
1259 1.0
1260 } else if fraction <= 2.0 {
1261 2.0
1262 } else if fraction <= 5.0 {
1263 5.0
1264 } else {
1265 10.0
1266 };
1267
1268 nice_fraction * power
1269}
1270
1271fn format_number(value: f64, step: f64) -> String {
1272 if !value.is_finite() {
1273 return "0".to_string();
1274 }
1275 let abs_step = step.abs().max(f64::EPSILON);
1276 let precision = if abs_step >= 1.0 {
1277 0
1278 } else {
1279 (-abs_step.log10().floor() as i32 + 1).clamp(0, 6) as usize
1280 };
1281 format!("{value:.precision$}")
1282}
1283
1284fn build_legend_items(datasets: &[Dataset]) -> Vec<(char, String, Color)> {
1285 datasets
1286 .iter()
1287 .filter(|d| !d.name.is_empty())
1288 .map(|d| {
1289 let symbol = match d.graph_type {
1290 GraphType::Line => '─',
1291 GraphType::Area => '█',
1292 GraphType::Scatter => marker_char(d.marker),
1293 GraphType::Bar => '█',
1294 };
1295 (symbol, d.name.clone(), d.color)
1296 })
1297 .collect()
1298}
1299
1300fn marker_char(marker: Marker) -> char {
1301 match marker {
1302 Marker::Braille => '⣿',
1303 Marker::Dot => '•',
1304 Marker::Block => '█',
1305 Marker::HalfBlock => '▀',
1306 Marker::Cross => '×',
1307 Marker::Circle => '○',
1308 }
1309}
1310
1311struct GridSpec<'a> {
1312 x_ticks: &'a [f64],
1313 y_ticks: &'a [f64],
1314 x_min: f64,
1315 x_max: f64,
1316 y_min: f64,
1317 y_max: f64,
1318}
1319
1320fn apply_grid(
1321 config: &ChartConfig,
1322 grid: GridSpec<'_>,
1323 plot_chars: &mut [Vec<char>],
1324 plot_styles: &mut [Vec<Style>],
1325 grid_style: Style,
1326) {
1327 if !config.grid || plot_chars.is_empty() || plot_chars[0].is_empty() {
1328 return;
1329 }
1330 let h = plot_chars.len();
1331 let w = plot_chars[0].len();
1332
1333 for tick in grid.y_ticks {
1334 let row = map_value_to_cell(*tick, grid.y_min, grid.y_max, h, true);
1335 if row < h {
1336 for col in 0..w {
1337 if plot_chars[row][col] == ' ' {
1338 plot_chars[row][col] = '·';
1339 plot_styles[row][col] = grid_style;
1340 }
1341 }
1342 }
1343 }
1344
1345 for tick in grid.x_ticks {
1346 let col = map_value_to_cell(*tick, grid.x_min, grid.x_max, w, false);
1347 if col < w {
1348 for row in 0..h {
1349 if plot_chars[row][col] == ' ' {
1350 plot_chars[row][col] = '·';
1351 plot_styles[row][col] = grid_style;
1352 }
1353 }
1354 }
1355 }
1356}
1357
1358fn draw_braille_dataset(
1359 dataset: &Dataset,
1360 x_min: f64,
1361 x_max: f64,
1362 y_min: f64,
1363 y_max: f64,
1364 plot_chars: &mut [Vec<char>],
1365 plot_styles: &mut [Vec<Style>],
1366) {
1367 if dataset.data.is_empty() || plot_chars.is_empty() || plot_chars[0].is_empty() {
1368 return;
1369 }
1370
1371 let cols = plot_chars[0].len();
1372 let rows = plot_chars.len();
1373 let px_w = cols * 2;
1374 let px_h = rows * 4;
1375 let mut bits = vec![vec![0u32; cols]; rows];
1376 let mut color_map = vec![vec![None::<Color>; cols]; rows];
1377
1378 let mut set_dot_colored = |px: usize, py: usize, color: Color| {
1379 set_braille_dot(px, py, &mut bits, cols, rows);
1380 let char_col = px / 2;
1381 let char_row = py / 4;
1382 if char_col < cols && char_row < rows {
1383 color_map[char_row][char_col] = Some(color);
1384 }
1385 };
1386
1387 let points = dataset
1388 .data
1389 .iter()
1390 .filter(|(x, y)| x.is_finite() && y.is_finite())
1391 .map(|(x, y)| {
1392 (
1393 map_value_to_cell(*x, x_min, x_max, px_w, false),
1394 map_value_to_cell(*y, y_min, y_max, px_h, true),
1395 *y,
1396 )
1397 })
1398 .collect::<Vec<_>>();
1399
1400 if points.is_empty() {
1401 return;
1402 }
1403
1404 if matches!(dataset.graph_type, GraphType::Line | GraphType::Area) {
1405 let mut line_y_by_x = if matches!(dataset.graph_type, GraphType::Area) {
1406 vec![None::<usize>; px_w]
1407 } else {
1408 Vec::new()
1409 };
1410 let mut line_color_by_x = if matches!(dataset.graph_type, GraphType::Area) {
1411 vec![None::<Color>; px_w]
1412 } else {
1413 Vec::new()
1414 };
1415
1416 for idx in 0..points.len().saturating_sub(1) {
1417 let a = points[idx];
1418 let b = points[idx + 1];
1419 let seg_color = if let (Some(up), Some(down)) = (dataset.up_color, dataset.down_color) {
1420 if b.2 > a.2 {
1421 up
1422 } else {
1423 down
1424 }
1425 } else {
1426 dataset.color
1427 };
1428
1429 plot_bresenham(
1430 a.0 as isize,
1431 a.1 as isize,
1432 b.0 as isize,
1433 b.1 as isize,
1434 |x, y| {
1435 if x < 0 || y < 0 {
1436 return;
1437 }
1438 let px = x as usize;
1439 let py = y as usize;
1440 set_dot_colored(px, py, seg_color);
1441 if matches!(dataset.graph_type, GraphType::Area) && px < px_w && py < px_h {
1442 line_y_by_x[px] = Some(match line_y_by_x[px] {
1443 Some(existing) => existing.min(py),
1444 None => py,
1445 });
1446 line_color_by_x[px] = Some(seg_color);
1447 }
1448 },
1449 );
1450 }
1451
1452 if matches!(dataset.graph_type, GraphType::Area) {
1453 for px in 0..px_w {
1454 if let Some(line_y) = line_y_by_x[px] {
1455 let fill_color = line_color_by_x[px].unwrap_or(dataset.color);
1456 for py in line_y..px_h {
1457 set_dot_colored(px, py, fill_color);
1458 }
1459 }
1460 }
1461 }
1462 } else {
1463 for (x, y, _) in &points {
1464 set_dot_colored(*x, *y, dataset.color);
1465 }
1466 }
1467
1468 for row in 0..rows {
1469 for col in 0..cols {
1470 if bits[row][col] != 0 {
1471 let ch = char::from_u32(BRAILLE_BASE + bits[row][col]).unwrap_or(' ');
1472 plot_chars[row][col] = ch;
1473 let color = color_map[row][col].unwrap_or(dataset.color);
1474 plot_styles[row][col] = Style::new().fg(color);
1475 }
1476 }
1477 }
1478
1479 if !matches!(dataset.marker, Marker::Braille) {
1480 let m = marker_char(dataset.marker);
1481 for (x, y) in dataset
1482 .data
1483 .iter()
1484 .filter(|(x, y)| x.is_finite() && y.is_finite())
1485 {
1486 let col = map_value_to_cell(*x, x_min, x_max, cols, false);
1487 let row = map_value_to_cell(*y, y_min, y_max, rows, true);
1488 if row < rows && col < cols {
1489 plot_chars[row][col] = m;
1490 plot_styles[row][col] = Style::new().fg(dataset.color);
1491 }
1492 }
1493 }
1494}
1495
1496fn draw_bar_dataset(
1497 dataset: &Dataset,
1498 _x_min: f64,
1499 _x_max: f64,
1500 y_min: f64,
1501 y_max: f64,
1502 plot_chars: &mut [Vec<char>],
1503 plot_styles: &mut [Vec<Style>],
1504) {
1505 if dataset.data.is_empty() || plot_chars.is_empty() || plot_chars[0].is_empty() {
1506 return;
1507 }
1508
1509 let rows = plot_chars.len();
1510 let cols = plot_chars[0].len();
1511 let n = dataset.data.len();
1512 let slot_width = cols as f64 / n as f64;
1513 let zero_row = map_value_to_cell(0.0, y_min, y_max, rows, true);
1514
1515 for (index, (_, value)) in dataset.data.iter().enumerate() {
1516 if !value.is_finite() {
1517 continue;
1518 }
1519
1520 let start_f = index as f64 * slot_width;
1521 let bar_width_f = (slot_width * 0.75).max(1.0);
1522 let full_w = bar_width_f.floor() as usize;
1523 let frac_w = ((bar_width_f - full_w as f64) * 8.0).round() as usize;
1524
1525 let x_start = start_f.floor() as usize;
1526 let x_end = (x_start + full_w).min(cols.saturating_sub(1));
1527 let frac_col = (x_end + 1).min(cols.saturating_sub(1));
1528
1529 let value_row = map_value_to_cell(*value, y_min, y_max, rows, true);
1530 let (top, bottom) = if value_row <= zero_row {
1531 (value_row, zero_row)
1532 } else {
1533 (zero_row, value_row)
1534 };
1535
1536 for row in top..=bottom.min(rows.saturating_sub(1)) {
1537 for col in x_start..=x_end {
1538 if col < cols {
1539 plot_chars[row][col] = '█';
1540 plot_styles[row][col] = Style::new().fg(dataset.color);
1541 }
1542 }
1543 if frac_w > 0 && frac_col < cols {
1544 plot_chars[row][frac_col] = BLOCK_FRACTIONS[frac_w.min(8)];
1545 plot_styles[row][frac_col] = Style::new().fg(dataset.color);
1546 }
1547 }
1548 }
1549}
1550
1551fn overlay_legend_on_plot(
1552 position: LegendPosition,
1553 items: &[(char, String, Color)],
1554 plot_chars: &mut [Vec<char>],
1555 plot_styles: &mut [Vec<Style>],
1556 axis_style: Style,
1557) {
1558 if plot_chars.is_empty() || plot_chars[0].is_empty() || items.is_empty() {
1559 return;
1560 }
1561
1562 let rows = plot_chars.len();
1563 let cols = plot_chars[0].len();
1564 let start_row = match position {
1565 LegendPosition::TopLeft => 0,
1566 LegendPosition::BottomLeft => rows.saturating_sub(items.len()),
1567 _ => 0,
1568 };
1569
1570 for (i, (symbol, name, color)) in items.iter().enumerate() {
1571 let row = start_row + i;
1572 if row >= rows {
1573 break;
1574 }
1575 let legend_text = format!("{symbol} {name}");
1576 for (col, ch) in legend_text.chars().enumerate() {
1577 if col >= cols {
1578 break;
1579 }
1580 plot_chars[row][col] = ch;
1581 plot_styles[row][col] = if col == 0 {
1582 Style::new().fg(*color)
1583 } else {
1584 axis_style
1585 };
1586 }
1587 }
1588}
1589
1590fn build_y_tick_row_map(
1591 ticks: &[f64],
1592 labels: Option<&[String]>,
1593 y_min: f64,
1594 y_max: f64,
1595 plot_height: usize,
1596) -> Vec<(usize, String)> {
1597 let step = if ticks.len() > 1 {
1598 (ticks[1] - ticks[0]).abs()
1599 } else {
1600 1.0
1601 };
1602 ticks
1603 .iter()
1604 .enumerate()
1605 .map(|(idx, v)| {
1606 let label = labels
1607 .and_then(|manual| manual.get(idx).cloned())
1608 .unwrap_or_else(|| format_number(*v, step));
1609 (
1610 map_value_to_cell(*v, y_min, y_max, plot_height, true),
1611 label,
1612 )
1613 })
1614 .collect()
1615}
1616
1617fn build_x_tick_col_map(
1618 ticks: &[f64],
1619 labels: Option<&[String]>,
1620 labels_match_manual_ticks: bool,
1621 x_min: f64,
1622 x_max: f64,
1623 plot_width: usize,
1624) -> Vec<(usize, String)> {
1625 if let Some(labels) = labels {
1626 if labels.is_empty() {
1627 return Vec::new();
1628 }
1629 if labels_match_manual_ticks {
1630 return ticks
1631 .iter()
1632 .zip(labels.iter())
1633 .map(|(tick, label)| {
1634 (
1635 map_value_to_cell(*tick, x_min, x_max, plot_width, false),
1636 label.clone(),
1637 )
1638 })
1639 .collect();
1640 }
1641 let denom = labels.len().saturating_sub(1).max(1);
1642 return labels
1643 .iter()
1644 .enumerate()
1645 .map(|(i, label)| {
1646 let col = (i * plot_width.saturating_sub(1)) / denom;
1647 (col, label.clone())
1648 })
1649 .collect();
1650 }
1651
1652 let step = if ticks.len() > 1 {
1653 (ticks[1] - ticks[0]).abs()
1654 } else {
1655 1.0
1656 };
1657 ticks
1658 .iter()
1659 .map(|v| {
1660 (
1661 map_value_to_cell(*v, x_min, x_max, plot_width, false),
1662 format_number(*v, step),
1663 )
1664 })
1665 .collect()
1666}
1667
1668fn map_value_to_cell(value: f64, min: f64, max: f64, size: usize, invert: bool) -> usize {
1669 if size == 0 {
1670 return 0;
1671 }
1672 let span = (max - min).abs().max(f64::EPSILON);
1673 let mut t = ((value - min) / span).clamp(0.0, 1.0);
1674 if invert {
1675 t = 1.0 - t;
1676 }
1677 (t * (size.saturating_sub(1)) as f64).round() as usize
1678}
1679
1680fn set_braille_dot(px: usize, py: usize, bits: &mut [Vec<u32>], cols: usize, rows: usize) {
1681 if cols == 0 || rows == 0 {
1682 return;
1683 }
1684 let char_col = px / 2;
1685 let char_row = py / 4;
1686 if char_col >= cols || char_row >= rows {
1687 return;
1688 }
1689 let sub_col = px % 2;
1690 let sub_row = py % 4;
1691 bits[char_row][char_col] |= if sub_col == 0 {
1692 BRAILLE_LEFT_BITS[sub_row]
1693 } else {
1694 BRAILLE_RIGHT_BITS[sub_row]
1695 };
1696}
1697
1698fn plot_bresenham(x0: isize, y0: isize, x1: isize, y1: isize, mut plot: impl FnMut(isize, isize)) {
1699 let mut x = x0;
1700 let mut y = y0;
1701 let dx = (x1 - x0).abs();
1702 let sx = if x0 < x1 { 1 } else { -1 };
1703 let dy = -(y1 - y0).abs();
1704 let sy = if y0 < y1 { 1 } else { -1 };
1705 let mut err = dx + dy;
1706
1707 loop {
1708 plot(x, y);
1709 if x == x1 && y == y1 {
1710 break;
1711 }
1712 let e2 = 2 * err;
1713 if e2 >= dy {
1714 err += dy;
1715 x += sx;
1716 }
1717 if e2 <= dx {
1718 err += dx;
1719 y += sy;
1720 }
1721 }
1722}
1723
1724fn center_text(text: &str, width: usize) -> String {
1725 let text_width = UnicodeWidthStr::width(text);
1726 if text_width >= width {
1727 return text.chars().take(width).collect();
1728 }
1729 let left = (width - text_width) / 2;
1730 let right = width - text_width - left;
1731 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
1732}
1733
1734fn sturges_bin_count(n: usize) -> usize {
1735 if n <= 1 {
1736 return 1;
1737 }
1738 (1.0 + (n as f64).log2()).ceil() as usize
1739}