1use thiserror::Error;
2
3use crate::DecorationPosition;
4use crate::border::BorderType;
5use crate::canvas::{
6 AsciiCanvas, BlockCanvas, BrailleCanvas, Canvas, CanvasType, DensityCanvas, DotCanvas,
7};
8use crate::color::{CanvasColor, NamedColor, TermColor, canvas_color_from_term};
9use crate::math::{extend_limits, format_axis_value, usize_to_f64};
10use crate::plot::Plot;
11
12const MIN_WIDTH: usize = 5;
13const MIN_HEIGHT: usize = 2;
14const DEFAULT_HEIGHT: usize = 15;
15
16#[derive(Debug, Clone, PartialEq)]
21#[non_exhaustive]
22pub enum GridCanvas {
23 Ascii(AsciiCanvas),
24 Block(BlockCanvas),
25 Braille(BrailleCanvas),
26 Density(DensityCanvas),
27 Dot(DotCanvas),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32#[non_exhaustive]
33pub enum StairStyle {
34 Pre,
36 #[default]
38 Post,
39}
40
41impl Canvas for GridCanvas {
42 fn pixel(&mut self, x: usize, y: usize, color: CanvasColor) {
43 match self {
44 Self::Ascii(canvas) => canvas.pixel(x, y, color),
45 Self::Block(canvas) => canvas.pixel(x, y, color),
46 Self::Braille(canvas) => canvas.pixel(x, y, color),
47 Self::Density(canvas) => canvas.pixel(x, y, color),
48 Self::Dot(canvas) => canvas.pixel(x, y, color),
49 }
50 }
51
52 fn glyph_at(&self, col: usize, row: usize) -> char {
53 match self {
54 Self::Ascii(canvas) => canvas.glyph_at(col, row),
55 Self::Block(canvas) => canvas.glyph_at(col, row),
56 Self::Braille(canvas) => canvas.glyph_at(col, row),
57 Self::Density(canvas) => canvas.glyph_at(col, row),
58 Self::Dot(canvas) => canvas.glyph_at(col, row),
59 }
60 }
61
62 fn color_at(&self, col: usize, row: usize) -> CanvasColor {
63 match self {
64 Self::Ascii(canvas) => canvas.color_at(col, row),
65 Self::Block(canvas) => canvas.color_at(col, row),
66 Self::Braille(canvas) => canvas.color_at(col, row),
67 Self::Density(canvas) => canvas.color_at(col, row),
68 Self::Dot(canvas) => canvas.color_at(col, row),
69 }
70 }
71
72 fn char_width(&self) -> usize {
73 match self {
74 Self::Ascii(canvas) => canvas.char_width(),
75 Self::Block(canvas) => canvas.char_width(),
76 Self::Braille(canvas) => canvas.char_width(),
77 Self::Density(canvas) => canvas.char_width(),
78 Self::Dot(canvas) => canvas.char_width(),
79 }
80 }
81
82 fn char_height(&self) -> usize {
83 match self {
84 Self::Ascii(canvas) => canvas.char_height(),
85 Self::Block(canvas) => canvas.char_height(),
86 Self::Braille(canvas) => canvas.char_height(),
87 Self::Density(canvas) => canvas.char_height(),
88 Self::Dot(canvas) => canvas.char_height(),
89 }
90 }
91
92 fn pixel_width(&self) -> usize {
93 match self {
94 Self::Ascii(canvas) => canvas.pixel_width(),
95 Self::Block(canvas) => canvas.pixel_width(),
96 Self::Braille(canvas) => canvas.pixel_width(),
97 Self::Density(canvas) => canvas.pixel_width(),
98 Self::Dot(canvas) => canvas.pixel_width(),
99 }
100 }
101
102 fn pixel_height(&self) -> usize {
103 match self {
104 Self::Ascii(canvas) => canvas.pixel_height(),
105 Self::Block(canvas) => canvas.pixel_height(),
106 Self::Braille(canvas) => canvas.pixel_height(),
107 Self::Density(canvas) => canvas.pixel_height(),
108 Self::Dot(canvas) => canvas.pixel_height(),
109 }
110 }
111
112 fn transform(&self) -> &crate::canvas::Transform2D {
113 match self {
114 Self::Ascii(canvas) => canvas.transform(),
115 Self::Block(canvas) => canvas.transform(),
116 Self::Braille(canvas) => canvas.transform(),
117 Self::Density(canvas) => canvas.transform(),
118 Self::Dot(canvas) => canvas.transform(),
119 }
120 }
121
122 fn transform_mut(&mut self) -> &mut crate::canvas::Transform2D {
123 match self {
124 Self::Ascii(canvas) => canvas.transform_mut(),
125 Self::Block(canvas) => canvas.transform_mut(),
126 Self::Braille(canvas) => canvas.transform_mut(),
127 Self::Density(canvas) => canvas.transform_mut(),
128 Self::Dot(canvas) => canvas.transform_mut(),
129 }
130 }
131}
132
133#[derive(Debug, Error, PartialEq)]
135#[non_exhaustive]
136pub enum LineplotError {
137 #[error("x and y must be the same length")]
139 LengthMismatch,
140 #[error("x and y must not be empty")]
142 EmptySeries,
143 #[error("axis limits must contain finite values")]
145 InvalidAxisLimits,
146 #[error("invalid numeric value: {value}")]
148 InvalidNumericValue { value: String },
149 #[error("densityplot_add requires a density plot")]
151 DensityPlotRequired,
152}
153
154#[derive(Debug, Clone, Default)]
156#[non_exhaustive]
157pub struct LineplotSeriesOptions {
158 pub color: Option<TermColor>,
160 pub name: Option<String>,
162}
163
164#[derive(Debug, Clone)]
166#[non_exhaustive]
167pub struct LineplotOptions {
168 pub title: Option<String>,
170 pub xlabel: Option<String>,
172 pub ylabel: Option<String>,
174 pub border: BorderType,
176 pub margin: u16,
178 pub padding: u16,
180 pub labels: bool,
182 pub width: usize,
184 pub height: usize,
186 pub xlim: (f64, f64),
188 pub ylim: (f64, f64),
190 pub canvas: CanvasType,
192 pub grid: bool,
194 pub color: Option<TermColor>,
196 pub name: Option<String>,
198}
199
200impl Default for LineplotOptions {
201 fn default() -> Self {
202 Self {
203 title: None,
204 xlabel: None,
205 ylabel: None,
206 border: BorderType::Solid,
207 margin: Plot::<GridCanvas>::DEFAULT_MARGIN,
208 padding: Plot::<GridCanvas>::DEFAULT_PADDING,
209 labels: true,
210 width: 40,
211 height: DEFAULT_HEIGHT,
212 xlim: (0.0, 0.0),
213 ylim: (0.0, 0.0),
214 canvas: CanvasType::Braille,
215 grid: true,
216 color: None,
217 name: None,
218 }
219 }
220}
221
222pub fn lineplot<X: ToString, Y: ToString>(
244 x: &[X],
245 y: &[Y],
246 options: LineplotOptions,
247) -> Result<Plot<GridCanvas>, LineplotError> {
248 let x = parse_numbers(x)?;
249 let y = parse_numbers(y)?;
250 build_lineplot(&x, &y, options)
251}
252
253pub fn lineplot_y<Y: ToString>(
260 y: &[Y],
261 options: LineplotOptions,
262) -> Result<Plot<GridCanvas>, LineplotError> {
263 let y = parse_numbers(y)?;
264 if y.is_empty() {
265 return Err(LineplotError::EmptySeries);
266 }
267 let x: Vec<f64> = (1..=y.len()).map(usize_to_f64).collect();
268 build_lineplot(&x, &y, options)
269}
270
271pub fn scatterplot<X: ToString, Y: ToString>(
293 x: &[X],
294 y: &[Y],
295 options: LineplotOptions,
296) -> Result<Plot<GridCanvas>, LineplotError> {
297 let x = parse_numbers(x)?;
298 let y = parse_numbers(y)?;
299 build_scatterplot(&x, &y, options)
300}
301
302pub fn scatterplot_y<Y: ToString>(
309 y: &[Y],
310 options: LineplotOptions,
311) -> Result<Plot<GridCanvas>, LineplotError> {
312 let y = parse_numbers(y)?;
313 if y.is_empty() {
314 return Err(LineplotError::EmptySeries);
315 }
316 let x: Vec<f64> = (1..=y.len()).map(usize_to_f64).collect();
317 build_scatterplot(&x, &y, options)
318}
319
320pub fn densityplot<X: ToString, Y: ToString>(
349 x: &[X],
350 y: &[Y],
351 mut options: LineplotOptions,
352) -> Result<Plot<GridCanvas>, LineplotError> {
353 let x = parse_numbers(x)?;
354 let y = parse_numbers(y)?;
355 options.canvas = CanvasType::Density;
356 options.grid = false;
357 build_scatterplot(&x, &y, options)
358}
359
360pub fn stairs<X: ToString, Y: ToString>(
386 x: &[X],
387 y: &[Y],
388 style: StairStyle,
389 options: LineplotOptions,
390) -> Result<Plot<GridCanvas>, LineplotError> {
391 let x = parse_numbers(x)?;
392 let y = parse_numbers(y)?;
393 validate_series(&x, &y)?;
394 let (stair_x, stair_y) = compute_stair_lines(&x, &y, style);
395 build_lineplot(&stair_x, &stair_y, options)
396}
397
398pub fn lineplot_add<X: ToString, Y: ToString>(
410 plot: &mut Plot<GridCanvas>,
411 x: &[X],
412 y: &[Y],
413 options: LineplotSeriesOptions,
414) -> Result<(), LineplotError> {
415 let x = parse_numbers(x)?;
416 let y = parse_numbers(y)?;
417 add_series(plot, &x, &y, options)
418}
419
420pub fn scatterplot_add<X: ToString, Y: ToString>(
428 plot: &mut Plot<GridCanvas>,
429 x: &[X],
430 y: &[Y],
431 options: LineplotSeriesOptions,
432) -> Result<(), LineplotError> {
433 let x = parse_numbers(x)?;
434 let y = parse_numbers(y)?;
435 add_scatter_series(plot, &x, &y, options)
436}
437
438pub fn densityplot_add<X: ToString, Y: ToString>(
447 plot: &mut Plot<GridCanvas>,
448 x: &[X],
449 y: &[Y],
450 options: LineplotSeriesOptions,
451) -> Result<(), LineplotError> {
452 let x = parse_numbers(x)?;
453 let y = parse_numbers(y)?;
454 add_density_series(plot, &x, &y, options)
455}
456
457pub fn lineplot_add_slope(
466 plot: &mut Plot<GridCanvas>,
467 intercept: f64,
468 slope: f64,
469 options: LineplotSeriesOptions,
470) -> Result<(), LineplotError> {
471 if !intercept.is_finite() {
472 return Err(LineplotError::InvalidNumericValue {
473 value: intercept.to_string(),
474 });
475 }
476 if !slope.is_finite() {
477 return Err(LineplotError::InvalidNumericValue {
478 value: slope.to_string(),
479 });
480 }
481
482 add_series(plot, &[intercept], &[slope], options)
483}
484
485pub fn stairs_add<X: ToString, Y: ToString>(
493 plot: &mut Plot<GridCanvas>,
494 x: &[X],
495 y: &[Y],
496 style: StairStyle,
497 options: LineplotSeriesOptions,
498) -> Result<(), LineplotError> {
499 let x = parse_numbers(x)?;
500 let y = parse_numbers(y)?;
501 validate_series(&x, &y)?;
502 let (stair_x, stair_y) = compute_stair_lines(&x, &y, style);
503 add_series(plot, &stair_x, &stair_y, options)
504}
505
506pub fn lineplot_add_y<Y: ToString>(
513 plot: &mut Plot<GridCanvas>,
514 y: &[Y],
515 options: LineplotSeriesOptions,
516) -> Result<(), LineplotError> {
517 let y = parse_numbers(y)?;
518 if y.is_empty() {
519 return Err(LineplotError::EmptySeries);
520 }
521 let x: Vec<f64> = (1..=y.len()).map(usize_to_f64).collect();
522 add_series(plot, &x, &y, options)
523}
524
525pub fn scatterplot_add_y<Y: ToString>(
532 plot: &mut Plot<GridCanvas>,
533 y: &[Y],
534 options: LineplotSeriesOptions,
535) -> Result<(), LineplotError> {
536 let y = parse_numbers(y)?;
537 if y.is_empty() {
538 return Err(LineplotError::EmptySeries);
539 }
540 let x: Vec<f64> = (1..=y.len()).map(usize_to_f64).collect();
541 add_scatter_series(plot, &x, &y, options)
542}
543
544pub fn annotate(
546 plot: &mut Plot<GridCanvas>,
547 position: DecorationPosition,
548 text: impl Into<String>,
549) {
550 plot.set_decoration(position, text);
551}
552
553fn compute_stair_lines(x: &[f64], y: &[f64], style: StairStyle) -> (Vec<f64>, Vec<f64>) {
554 let mut x_out = Vec::with_capacity(x.len().saturating_mul(2).saturating_sub(1));
555 let mut y_out = Vec::with_capacity(y.len().saturating_mul(2).saturating_sub(1));
556
557 x_out.push(x[0]);
558 y_out.push(y[0]);
559
560 for i in 1..x.len() {
561 match style {
562 StairStyle::Post => {
563 x_out.push(x[i]);
564 y_out.push(y[i - 1]);
565 x_out.push(x[i]);
566 y_out.push(y[i]);
567 }
568 StairStyle::Pre => {
569 x_out.push(x[i - 1]);
570 y_out.push(y[i]);
571 x_out.push(x[i]);
572 y_out.push(y[i]);
573 }
574 }
575 }
576
577 (x_out, y_out)
578}
579
580fn build_lineplot(
581 x: &[f64],
582 y: &[f64],
583 options: LineplotOptions,
584) -> Result<Plot<GridCanvas>, LineplotError> {
585 validate_series(x, y)?;
586 validate_limits(options.xlim, options.ylim)?;
587
588 let width = options.width.max(MIN_WIDTH);
589 let height = options.height.max(MIN_HEIGHT);
590 let (min_x, max_x) = extend_limits(x, options.xlim);
591 let (min_y, max_y) = extend_limits(y, options.ylim);
592
593 let canvas = create_canvas(
594 options.canvas,
595 width,
596 height,
597 min_x,
598 min_y,
599 max_x - min_x,
600 max_y - min_y,
601 );
602
603 let mut plot = Plot::new(canvas);
604 plot.title = options.title;
605 plot.xlabel = options.xlabel;
606 plot.ylabel = options.ylabel;
607 plot.border = options.border;
608 plot.margin = options.margin;
609 plot.padding = options.padding;
610 plot.show_labels = options.labels;
611
612 annotate_axes(&mut plot, min_x, max_x, min_y, max_y);
613
614 if options.grid {
615 draw_grid_lines(plot.graphics_mut(), min_x, max_x, min_y, max_y);
616 }
617
618 let series_options = LineplotSeriesOptions {
619 color: options.color,
620 name: options.name,
621 };
622 add_series(&mut plot, x, y, series_options)?;
623
624 Ok(plot)
625}
626
627fn build_scatterplot(
628 x: &[f64],
629 y: &[f64],
630 options: LineplotOptions,
631) -> Result<Plot<GridCanvas>, LineplotError> {
632 validate_series(x, y)?;
633 validate_limits(options.xlim, options.ylim)?;
634
635 let width = options.width.max(MIN_WIDTH);
636 let height = options.height.max(MIN_HEIGHT);
637 let (min_x, max_x) = extend_limits(x, options.xlim);
638 let (min_y, max_y) = extend_limits(y, options.ylim);
639
640 let canvas = create_canvas(
641 options.canvas,
642 width,
643 height,
644 min_x,
645 min_y,
646 max_x - min_x,
647 max_y - min_y,
648 );
649
650 let mut plot = Plot::new(canvas);
651 plot.title = options.title;
652 plot.xlabel = options.xlabel;
653 plot.ylabel = options.ylabel;
654 plot.border = options.border;
655 plot.margin = options.margin;
656 plot.padding = options.padding;
657 plot.show_labels = options.labels;
658
659 annotate_axes(&mut plot, min_x, max_x, min_y, max_y);
660
661 if options.grid {
662 draw_grid_lines(plot.graphics_mut(), min_x, max_x, min_y, max_y);
663 }
664
665 let series_options = LineplotSeriesOptions {
666 color: options.color,
667 name: options.name,
668 };
669 add_scatter_series(&mut plot, x, y, series_options)?;
670
671 Ok(plot)
672}
673
674fn add_series(
675 plot: &mut Plot<GridCanvas>,
676 x: &[f64],
677 y: &[f64],
678 options: LineplotSeriesOptions,
679) -> Result<(), LineplotError> {
680 validate_series(x, y)?;
681 let color = options
682 .color
683 .unwrap_or_else(|| TermColor::Named(plot.next_color()));
684
685 let canvas_color = canvas_color_from_term(color);
686 if x.len() == 1 {
687 let (slope_x, slope_y) = slope_segment_for_plot(plot, x[0], y[0]);
688 plot.graphics_mut().lines(&slope_x, &slope_y, canvas_color);
689 } else {
690 plot.graphics_mut().lines(x, y, canvas_color);
691 }
692
693 if let Some(name) = options.name.filter(|value| !value.is_empty()) {
694 annotate_next_right(plot, name, color);
695 }
696
697 Ok(())
698}
699
700fn add_scatter_series(
701 plot: &mut Plot<GridCanvas>,
702 x: &[f64],
703 y: &[f64],
704 options: LineplotSeriesOptions,
705) -> Result<(), LineplotError> {
706 validate_series(x, y)?;
707 let color = options
708 .color
709 .unwrap_or_else(|| TermColor::Named(plot.next_color()));
710
711 let canvas_color = canvas_color_from_term(color);
712 plot.graphics_mut().points(x, y, canvas_color);
713
714 if let Some(name) = options.name.filter(|value| !value.is_empty()) {
715 annotate_next_right(plot, name, color);
716 }
717
718 Ok(())
719}
720
721fn add_density_series(
722 plot: &mut Plot<GridCanvas>,
723 x: &[f64],
724 y: &[f64],
725 options: LineplotSeriesOptions,
726) -> Result<(), LineplotError> {
727 if !matches!(plot.graphics(), GridCanvas::Density(_)) {
728 return Err(LineplotError::DensityPlotRequired);
729 }
730 add_scatter_series(plot, x, y, options)
731}
732
733fn slope_segment_for_plot(
734 plot: &Plot<GridCanvas>,
735 intercept: f64,
736 slope: f64,
737) -> ([f64; 2], [f64; 2]) {
738 let x_axis = plot.graphics().transform().x();
739 let min_x = x_axis.origin();
740 let max_x = x_axis.origin() + x_axis.span();
741 let slope_x = [min_x, max_x];
742 let slope_y = [intercept + min_x * slope, intercept + max_x * slope];
743 (slope_x, slope_y)
744}
745
746fn annotate_next_right(plot: &mut Plot<GridCanvas>, text: String, color: TermColor) {
747 for row in 0..plot.graphics().char_height() {
748 if !plot.annotations().right().contains_key(&row) {
749 plot.annotate_right(row, text, Some(color));
750 return;
751 }
752 }
753}
754
755fn draw_grid_lines(canvas: &mut GridCanvas, min_x: f64, max_x: f64, min_y: f64, max_y: f64) {
756 if min_y < 0.0 && 0.0 < max_y {
757 let x_steps = canvas.pixel_width().saturating_sub(1);
758 if x_steps > 0 {
759 let step = (max_x - min_x) / usize_to_f64(x_steps);
760 if step.is_finite() && step > 0.0 {
761 let mut value = min_x;
762 while value <= max_x {
763 canvas.point(value, 0.0, CanvasColor::NORMAL);
764 value += step;
765 }
766 canvas.point(max_x, 0.0, CanvasColor::NORMAL);
767 }
768 }
769 }
770
771 if min_x < 0.0 && 0.0 < max_x {
772 let y_steps = canvas.pixel_height().saturating_sub(1);
773 if y_steps > 0 {
774 let step = (max_y - min_y) / usize_to_f64(y_steps);
775 if step.is_finite() && step > 0.0 {
776 let mut value = min_y;
777 while value <= max_y {
778 canvas.point(0.0, value, CanvasColor::NORMAL);
779 value += step;
780 }
781 canvas.point(0.0, max_y, CanvasColor::NORMAL);
782 }
783 }
784 }
785}
786
787fn annotate_axes(plot: &mut Plot<GridCanvas>, min_x: f64, max_x: f64, min_y: f64, max_y: f64) {
788 let y_max = format_axis_value(max_y);
789 let y_min = format_axis_value(min_y);
790 plot.annotate_left(0, y_max, Some(TermColor::Named(NamedColor::LightBlack)));
791 plot.annotate_left(
792 plot.graphics().char_height().saturating_sub(1),
793 y_min,
794 Some(TermColor::Named(NamedColor::LightBlack)),
795 );
796
797 plot.set_decoration(crate::DecorationPosition::Bl, format_axis_value(min_x));
798 plot.set_decoration(crate::DecorationPosition::Br, format_axis_value(max_x));
799}
800
801fn validate_limits(xlim: (f64, f64), ylim: (f64, f64)) -> Result<(), LineplotError> {
802 if !xlim.0.is_finite() || !xlim.1.is_finite() || !ylim.0.is_finite() || !ylim.1.is_finite() {
803 return Err(LineplotError::InvalidAxisLimits);
804 }
805
806 Ok(())
807}
808
809fn validate_series(x: &[f64], y: &[f64]) -> Result<(), LineplotError> {
810 if x.len() != y.len() {
811 return Err(LineplotError::LengthMismatch);
812 }
813 if x.is_empty() {
814 return Err(LineplotError::EmptySeries);
815 }
816 Ok(())
817}
818
819fn parse_numbers<T: ToString>(values: &[T]) -> Result<Vec<f64>, LineplotError> {
820 values
821 .iter()
822 .map(|value| {
823 let display = value.to_string();
824 let numeric =
825 display
826 .parse::<f64>()
827 .map_err(|_| LineplotError::InvalidNumericValue {
828 value: display.clone(),
829 })?;
830 if !numeric.is_finite() {
831 return Err(LineplotError::InvalidNumericValue { value: display });
832 }
833 Ok(numeric)
834 })
835 .collect()
836}
837
838fn create_canvas(
839 canvas_type: CanvasType,
840 width: usize,
841 height: usize,
842 origin_x: f64,
843 origin_y: f64,
844 plot_width: f64,
845 plot_height: f64,
846) -> GridCanvas {
847 match canvas_type {
848 CanvasType::Ascii => GridCanvas::Ascii(AsciiCanvas::new(
849 width,
850 height,
851 origin_x,
852 origin_y,
853 plot_width,
854 plot_height,
855 )),
856 CanvasType::Block => GridCanvas::Block(BlockCanvas::new(
857 width,
858 height,
859 origin_x,
860 origin_y,
861 plot_width,
862 plot_height,
863 )),
864 CanvasType::Braille => GridCanvas::Braille(BrailleCanvas::new(
865 width,
866 height,
867 origin_x,
868 origin_y,
869 plot_width,
870 plot_height,
871 )),
872 CanvasType::Density => GridCanvas::Density(DensityCanvas::new(
873 width,
874 height,
875 origin_x,
876 origin_y,
877 plot_width,
878 plot_height,
879 )),
880 CanvasType::Dot => GridCanvas::Dot(DotCanvas::new(
881 width,
882 height,
883 origin_x,
884 origin_y,
885 plot_width,
886 plot_height,
887 )),
888 }
889}
890
891#[cfg(test)]
892mod tests {
893 use std::fs;
894 use std::path::Path;
895
896 use super::{
897 GridCanvas, LineplotError, LineplotOptions, LineplotSeriesOptions, StairStyle, annotate,
898 densityplot, densityplot_add, lineplot, lineplot_add, lineplot_add_slope, lineplot_add_y,
899 lineplot_y, scatterplot, scatterplot_add, scatterplot_add_y, scatterplot_y, stairs,
900 stairs_add,
901 };
902 use crate::DecorationPosition;
903 use crate::canvas::{Canvas, CanvasType};
904 use crate::color::{NamedColor, TermColor};
905 use crate::math::usize_to_f64;
906 use crate::parse_border_type;
907 use crate::test_util::{assert_fixture_eq, render_plot_text};
908
909 fn load_density_fixture_data() -> (Vec<f64>, Vec<f64>) {
910 let fixture_path =
911 Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/data/randn_1338_2000.txt");
912 let values: Vec<f64> = fs::read_to_string(&fixture_path)
913 .unwrap_or_else(|error| panic!("failed to read {}: {error}", fixture_path.display()))
914 .lines()
915 .map(str::trim)
916 .filter(|line| !line.is_empty())
917 .map(|line| {
918 line.parse::<f64>().unwrap_or_else(|error| {
919 panic!("failed to parse density fixture value `{line}`: {error}")
920 })
921 })
922 .collect();
923 assert_eq!(
924 values.len(),
925 2000,
926 "density fixture should have 2000 values"
927 );
928 let dx = values[0..1000].to_vec();
929 let dy = values[1000..2000].to_vec();
930 (dx, dy)
931 }
932
933 fn assert_fixture_render(plot: &crate::Plot<GridCanvas>, color: bool, fixture: &str) {
934 assert_fixture_eq(&render_plot_text(plot, color), fixture);
935 }
936
937 fn base_x() -> Vec<i32> {
938 vec![-1, 1, 3, 3, -1]
939 }
940
941 fn base_y() -> Vec<i32> {
942 vec![2, 0, -5, 2, -5]
943 }
944
945 #[test]
946 fn validates_arguments_and_inputs() {
947 let x = [1, 2];
948 let y = [1, 2, 3];
949 let mismatch = lineplot(&x, &y, LineplotOptions::default());
950 assert!(matches!(mismatch, Err(LineplotError::LengthMismatch)));
951
952 let empty = lineplot(&[] as &[i32], &[] as &[i32], LineplotOptions::default());
953 assert!(matches!(empty, Err(LineplotError::EmptySeries)));
954
955 let invalid = lineplot(&["a"], &["1"], LineplotOptions::default());
956 assert!(matches!(
957 invalid,
958 Err(LineplotError::InvalidNumericValue { .. })
959 ));
960 }
961
962 #[test]
963 fn default_fixture() {
964 let plot = lineplot(&base_x(), &base_y(), LineplotOptions::default())
965 .expect("lineplot should succeed");
966 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/default.txt");
967 }
968
969 #[test]
970 fn shared_renderer_matches_lineplot_default_fixture() {
971 let plot = lineplot(&base_x(), &base_y(), LineplotOptions::default())
972 .expect("lineplot should succeed");
973
974 let ansi_rendered = render_plot_text(&plot, true);
975 assert_fixture_eq(&ansi_rendered, "tests/fixtures/lineplot/default.txt");
976
977 let plain_rendered = render_plot_text(&plot, false);
978 assert!(!plain_rendered.contains("\x1b["));
979 }
980
981 #[test]
982 fn y_only_and_range_fixtures() {
983 let plot =
984 lineplot_y(&base_y(), LineplotOptions::default()).expect("lineplot should succeed");
985 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/y_only.txt");
986
987 let range1: Vec<i32> = (6..=10).collect();
988 let plot =
989 lineplot_y(&range1, LineplotOptions::default()).expect("lineplot should succeed");
990 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/range1.txt");
991
992 let x: Vec<i32> = (11..=15).collect();
993 let y: Vec<i32> = (6..=10).collect();
994 let plot = lineplot(&x, &y, LineplotOptions::default()).expect("lineplot should succeed");
995 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/range2.txt");
996 }
997
998 #[test]
999 fn limits_nogrid_and_canvas_size_fixtures() {
1000 let plot = lineplot(
1001 &base_x(),
1002 &base_y(),
1003 LineplotOptions {
1004 xlim: (-1.5, 3.5),
1005 ylim: (-5.5, 2.5),
1006 ..LineplotOptions::default()
1007 },
1008 )
1009 .expect("lineplot should succeed");
1010 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/limits.txt");
1011
1012 let plot = lineplot(
1013 &base_x(),
1014 &base_y(),
1015 LineplotOptions {
1016 grid: false,
1017 ..LineplotOptions::default()
1018 },
1019 )
1020 .expect("lineplot should succeed");
1021 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/nogrid.txt");
1022
1023 let plot = lineplot(
1024 &base_x(),
1025 &base_y(),
1026 LineplotOptions {
1027 title: Some(String::from("Scatter")),
1028 canvas: CanvasType::Dot,
1029 width: 10,
1030 height: 5,
1031 ..LineplotOptions::default()
1032 },
1033 )
1034 .expect("lineplot should succeed");
1035 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/canvassize.txt");
1036 }
1037
1038 #[test]
1039 fn named_color_and_parameters_fixtures() {
1040 let mut plot = lineplot(
1041 &base_x(),
1042 &base_y(),
1043 LineplotOptions {
1044 color: Some(TermColor::Named(NamedColor::Blue)),
1045 name: Some(String::from("points1")),
1046 ..LineplotOptions::default()
1047 },
1048 )
1049 .expect("lineplot should succeed");
1050 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/blue.txt");
1051
1052 plot = lineplot(
1053 &base_x(),
1054 &base_y(),
1055 LineplotOptions {
1056 name: Some(String::from("points1")),
1057 title: Some(String::from("Scatter")),
1058 xlabel: Some(String::from("x")),
1059 ylabel: Some(String::from("y")),
1060 ..LineplotOptions::default()
1061 },
1062 )
1063 .expect("lineplot should succeed");
1064 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/parameters1.txt");
1065
1066 lineplot_add_y(
1067 &mut plot,
1068 &[0.5, 1.0, 1.5],
1069 LineplotSeriesOptions {
1070 name: Some(String::from("points2")),
1071 ..LineplotSeriesOptions::default()
1072 },
1073 )
1074 .expect("append should succeed");
1075 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/parameters2.txt");
1076
1077 lineplot_add(
1078 &mut plot,
1079 &[-0.5, 0.5, 1.5],
1080 &[0.5, 1.0, 1.5],
1081 LineplotSeriesOptions {
1082 name: Some(String::from("points3")),
1083 ..LineplotSeriesOptions::default()
1084 },
1085 )
1086 .expect("append should succeed");
1087 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/parameters3.txt");
1088 assert_fixture_render(&plot, false, "tests/fixtures/lineplot/nocolor.txt");
1089 }
1090
1091 #[test]
1092 fn scale_and_issue32_fixtures() {
1093 let x1: Vec<f64> = base_x()
1094 .into_iter()
1095 .map(|value| f64::from(value) * 1e3 + 15.0)
1096 .collect();
1097 let y1: Vec<f64> = base_y()
1098 .into_iter()
1099 .map(|value| f64::from(value) * 1e-3 - 15.0)
1100 .collect();
1101 let plot = lineplot(&x1, &y1, LineplotOptions::default()).expect("lineplot should succeed");
1102 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/scale1.txt");
1103
1104 let x2: Vec<f64> = base_x()
1105 .into_iter()
1106 .map(|value| f64::from(value) * 1e-3 + 15.0)
1107 .collect();
1108 let y2: Vec<f64> = base_y()
1109 .into_iter()
1110 .map(|value| f64::from(value) * 1e3 - 15.0)
1111 .collect();
1112 let plot = lineplot(&x2, &y2, LineplotOptions::default()).expect("lineplot should succeed");
1113 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/scale2.txt");
1114
1115 let tx = [-1.0, 2.0, 3.0, 700_000.0];
1116 let ty = [1.0, 2.0, 9.0, 4_000_000.0];
1117 let plot = lineplot(&tx, &ty, LineplotOptions::default()).expect("lineplot should succeed");
1118 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/scale3.txt");
1119
1120 let plot = lineplot(
1121 &tx,
1122 &ty,
1123 LineplotOptions {
1124 width: 5,
1125 height: 5,
1126 ..LineplotOptions::default()
1127 },
1128 )
1129 .expect("lineplot should succeed");
1130 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/scale3_small.txt");
1131
1132 let ys = [
1133 261.0, 272.0, 277.0, 283.0, 289.0, 294.0, 298.0, 305.0, 309.0, 314.0, 319.0, 320.0,
1134 322.0, 323.0, 324.0,
1135 ];
1136 let xs: Vec<f64> = (0..ys.len()).map(usize_to_f64).collect();
1137 let plot = lineplot(
1138 &xs,
1139 &ys,
1140 LineplotOptions {
1141 height: 26,
1142 ylim: (0.0, 700.0),
1143 ..LineplotOptions::default()
1144 },
1145 )
1146 .expect("lineplot should succeed");
1147 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/issue32_fix.txt");
1148 }
1149
1150 #[test]
1151 fn stairs_pre_and_post_fixtures() {
1152 let sx = [1, 2, 4, 7, 8];
1153 let sy = [1, 3, 4, 2, 7];
1154
1155 let plot = stairs(&sx, &sy, StairStyle::Pre, LineplotOptions::default())
1156 .expect("stairs should succeed");
1157 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/stairs_pre.txt");
1158
1159 let plot = stairs(&sx, &sy, StairStyle::Post, LineplotOptions::default())
1160 .expect("stairs should succeed");
1161 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/stairs_post.txt");
1162 }
1163
1164 #[test]
1165 fn stairs_parameter_and_nocolor_fixtures() {
1166 let sx = [1.0, 2.0, 4.0, 7.0, 8.0];
1167 let sy = [1.0, 3.0, 4.0, 2.0, 7.0];
1168
1169 let mut plot = stairs(
1170 &sx,
1171 &sy,
1172 StairStyle::Post,
1173 LineplotOptions {
1174 title: Some(String::from("Foo")),
1175 color: Some(TermColor::Named(NamedColor::Red)),
1176 xlabel: Some(String::from("x")),
1177 name: Some(String::from("1")),
1178 ..LineplotOptions::default()
1179 },
1180 )
1181 .expect("stairs should succeed");
1182 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/stairs_parameters.txt");
1183
1184 let sx2: Vec<f64> = sx.iter().map(|value| *value - 0.2).collect();
1185 let sy2: Vec<f64> = sy.iter().map(|value| *value + 1.5).collect();
1186 stairs_add(
1187 &mut plot,
1188 &sx2,
1189 &sy2,
1190 StairStyle::Post,
1191 LineplotSeriesOptions {
1192 name: Some(String::from("2")),
1193 ..LineplotSeriesOptions::default()
1194 },
1195 )
1196 .expect("stairs add should succeed");
1197 assert_fixture_render(
1198 &plot,
1199 true,
1200 "tests/fixtures/lineplot/stairs_parameters2.txt",
1201 );
1202 assert_fixture_render(
1203 &plot,
1204 false,
1205 "tests/fixtures/lineplot/stairs_parameters2_nocolor.txt",
1206 );
1207
1208 stairs_add(
1209 &mut plot,
1210 &sx,
1211 &sy,
1212 StairStyle::Pre,
1213 LineplotSeriesOptions {
1214 name: Some(String::from("3")),
1215 ..LineplotSeriesOptions::default()
1216 },
1217 )
1218 .expect("stairs add should succeed");
1219 let rendered = render_plot_text(&plot, false);
1220 assert!(rendered.contains("Foo"));
1221 assert!(rendered.contains('1'));
1222 assert!(rendered.contains('2'));
1223 assert!(rendered.contains('3'));
1224 }
1225
1226 #[test]
1227 fn stairs_edgecase_fixture() {
1228 let sx = [1, 2, 4, 7, 8];
1229 let sy = [1, 3, 4, 2, 7000];
1230 let plot = stairs(&sx, &sy, StairStyle::Post, LineplotOptions::default())
1231 .expect("stairs should succeed");
1232 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/stairs_edgecase.txt");
1233 }
1234
1235 #[test]
1236 fn slope_fixtures() {
1237 let mut plot =
1238 lineplot_y(&base_y(), LineplotOptions::default()).expect("lineplot should succeed");
1239
1240 lineplot_add_slope(&mut plot, -3.0, 1.0, LineplotSeriesOptions::default())
1241 .expect("lineplot add should succeed");
1242 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/slope1.txt");
1243
1244 lineplot_add(
1245 &mut plot,
1246 &[-4.0],
1247 &[0.5],
1248 LineplotSeriesOptions {
1249 color: Some(TermColor::Named(NamedColor::Cyan)),
1250 name: Some(String::from("foo")),
1251 },
1252 )
1253 .expect("lineplot add should succeed");
1254 assert_fixture_render(&plot, true, "tests/fixtures/lineplot/slope2.txt");
1255 }
1256
1257 #[test]
1258 fn slope_mode_uses_plot_x_axis_bounds() {
1259 let mut plot = lineplot(
1260 &[0.0, 1.0],
1261 &[0.0, 1.0],
1262 LineplotOptions {
1263 xlim: (-2.0, 3.0),
1264 ylim: (-2.0, 2.0),
1265 ..LineplotOptions::default()
1266 },
1267 )
1268 .expect("lineplot should succeed");
1269
1270 lineplot_add_slope(&mut plot, 1.0, 0.5, LineplotSeriesOptions::default())
1271 .expect("slope add should succeed");
1272
1273 let rendered = render_plot_text(&plot, false);
1274 let has_bounds_line = rendered
1275 .lines()
1276 .any(|line| line.contains("-2") && line.contains('3'));
1277 assert!(
1278 has_bounds_line,
1279 "expected axis bounds line in rendered plot"
1280 );
1281 }
1282
1283 #[test]
1284 fn squeeze_annotations_fixture() {
1285 let sx = [1, 2, 4, 7, 8];
1286 let sy = [1, 3, 4, 2, 7];
1287 let mut plot = stairs(
1288 &sx,
1289 &sy,
1290 StairStyle::Post,
1291 LineplotOptions {
1292 width: 10,
1293 padding: 3,
1294 ..LineplotOptions::default()
1295 },
1296 )
1297 .expect("stairs should succeed");
1298
1299 annotate(&mut plot, DecorationPosition::Tl, "Hello");
1300 annotate(&mut plot, DecorationPosition::T, "how are");
1301 annotate(&mut plot, DecorationPosition::Tr, "you?");
1302 annotate(&mut plot, DecorationPosition::Bl, "Hello");
1303 annotate(&mut plot, DecorationPosition::B, "how are");
1304 annotate(&mut plot, DecorationPosition::Br, "you?");
1305
1306 lineplot_add(&mut plot, &[1.0], &[0.5], LineplotSeriesOptions::default())
1307 .expect("lineplot add should succeed");
1308
1309 assert_fixture_render(
1310 &plot,
1311 true,
1312 "tests/fixtures/lineplot/squeeze_annotations.txt",
1313 );
1314 }
1315
1316 #[test]
1317 fn scatterplot_validates_arguments_and_inputs() {
1318 let x = [1, 2];
1319 let y = [1, 2, 3];
1320 let mismatch = scatterplot(&x, &y, LineplotOptions::default());
1321 assert!(matches!(mismatch, Err(LineplotError::LengthMismatch)));
1322
1323 let empty = scatterplot(&[] as &[i32], &[] as &[i32], LineplotOptions::default());
1324 assert!(matches!(empty, Err(LineplotError::EmptySeries)));
1325
1326 let invalid = scatterplot(&["a"], &["1"], LineplotOptions::default());
1327 assert!(matches!(
1328 invalid,
1329 Err(LineplotError::InvalidNumericValue { .. })
1330 ));
1331
1332 let invalid_limits = scatterplot(
1333 &[1],
1334 &[1],
1335 LineplotOptions {
1336 xlim: (f64::NAN, 1.0),
1337 ..LineplotOptions::default()
1338 },
1339 );
1340 assert!(matches!(
1341 invalid_limits,
1342 Err(LineplotError::InvalidAxisLimits)
1343 ));
1344
1345 let y_empty = scatterplot_y(&[] as &[i32], LineplotOptions::default());
1346 assert!(matches!(y_empty, Err(LineplotError::EmptySeries)));
1347
1348 let y_invalid = scatterplot_y(&["NaN"], LineplotOptions::default());
1349 assert!(matches!(
1350 y_invalid,
1351 Err(LineplotError::InvalidNumericValue { .. })
1352 ));
1353
1354 let mut base_plot = scatterplot(&[1.0, 2.0], &[1.0, 2.0], LineplotOptions::default())
1355 .expect("base scatterplot should succeed");
1356 let add_mismatch = scatterplot_add(
1357 &mut base_plot,
1358 &[1.0, 2.0],
1359 &[1.0],
1360 LineplotSeriesOptions::default(),
1361 );
1362 assert!(matches!(add_mismatch, Err(LineplotError::LengthMismatch)));
1363
1364 let add_y_empty = scatterplot_add_y(
1365 &mut base_plot,
1366 &[] as &[f64],
1367 LineplotSeriesOptions::default(),
1368 );
1369 assert!(matches!(add_y_empty, Err(LineplotError::EmptySeries)));
1370
1371 let add_invalid = scatterplot_add(
1372 &mut base_plot,
1373 &["bad"],
1374 &["1"],
1375 LineplotSeriesOptions::default(),
1376 );
1377 assert!(matches!(
1378 add_invalid,
1379 Err(LineplotError::InvalidNumericValue { .. })
1380 ));
1381 }
1382
1383 #[test]
1384 fn scatterplot_draws_points_without_connecting_segments() {
1385 let options = LineplotOptions {
1386 canvas: CanvasType::Dot,
1387 width: 10,
1388 height: 5,
1389 grid: false,
1390 labels: false,
1391 xlim: (0.0, 3.0),
1392 ylim: (0.0, 3.0),
1393 ..LineplotOptions::default()
1394 };
1395
1396 let line =
1397 lineplot(&[0.0, 3.0], &[0.0, 3.0], options.clone()).expect("lineplot should succeed");
1398 let scatter =
1399 scatterplot(&[0.0, 3.0], &[0.0, 3.0], options).expect("scatterplot should succeed");
1400
1401 let line_cells = (0..line.graphics().char_height())
1402 .flat_map(|row| line.graphics().row_cells(row))
1403 .filter(|(glyph, _)| *glyph != ' ')
1404 .count();
1405 let scatter_cells = (0..scatter.graphics().char_height())
1406 .flat_map(|row| scatter.graphics().row_cells(row))
1407 .filter(|(glyph, _)| *glyph != ' ')
1408 .count();
1409
1410 assert!(
1411 line_cells > scatter_cells,
1412 "lineplot should occupy more cells than scatterplot"
1413 );
1414 }
1415
1416 #[test]
1417 fn scatterplot_default_y_and_range_fixtures() {
1418 let plot = scatterplot(&base_x(), &base_y(), LineplotOptions::default())
1419 .expect("scatterplot should succeed");
1420 assert_fixture_eq(
1421 &render_plot_text(&plot, true),
1422 "tests/fixtures/scatterplot/default.txt",
1423 );
1424
1425 let plot = scatterplot_y(&base_y(), LineplotOptions::default())
1426 .expect("scatterplot should succeed");
1427 assert_fixture_eq(
1428 &render_plot_text(&plot, true),
1429 "tests/fixtures/scatterplot/y_only.txt",
1430 );
1431
1432 let range1: Vec<i32> = (6..=10).collect();
1433 let plot =
1434 scatterplot_y(&range1, LineplotOptions::default()).expect("scatterplot should succeed");
1435 assert_fixture_eq(
1436 &render_plot_text(&plot, true),
1437 "tests/fixtures/scatterplot/range1.txt",
1438 );
1439
1440 let x: Vec<i32> = (11..=15).collect();
1441 let y: Vec<i32> = (6..=10).collect();
1442 let plot =
1443 scatterplot(&x, &y, LineplotOptions::default()).expect("scatterplot should succeed");
1444 assert_fixture_eq(
1445 &render_plot_text(&plot, true),
1446 "tests/fixtures/scatterplot/range2.txt",
1447 );
1448 }
1449
1450 #[test]
1451 fn scatterplot_scale_limits_and_nogrid_fixtures() {
1452 let x1: Vec<f64> = base_x()
1453 .into_iter()
1454 .map(|value| f64::from(value) * 1e3 + 15.0)
1455 .collect();
1456 let y1: Vec<f64> = base_y()
1457 .into_iter()
1458 .map(|value| f64::from(value) * 1e-3 - 15.0)
1459 .collect();
1460 let plot =
1461 scatterplot(&x1, &y1, LineplotOptions::default()).expect("scatterplot should succeed");
1462 assert_fixture_eq(
1463 &render_plot_text(&plot, true),
1464 "tests/fixtures/scatterplot/scale1.txt",
1465 );
1466
1467 let x2: Vec<f64> = base_x()
1468 .into_iter()
1469 .map(|value| f64::from(value) * 1e-3 + 15.0)
1470 .collect();
1471 let y2: Vec<f64> = base_y()
1472 .into_iter()
1473 .map(|value| f64::from(value) * 1e3 - 15.0)
1474 .collect();
1475 let plot =
1476 scatterplot(&x2, &y2, LineplotOptions::default()).expect("scatterplot should succeed");
1477 assert_fixture_eq(
1478 &render_plot_text(&plot, true),
1479 "tests/fixtures/scatterplot/scale2.txt",
1480 );
1481
1482 let tx = [-1.0, 2.0, 3.0, 700_000.0];
1483 let ty = [1.0, 2.0, 9.0, 4_000_000.0];
1484 let plot =
1485 scatterplot(&tx, &ty, LineplotOptions::default()).expect("scatterplot should succeed");
1486 assert_fixture_eq(
1487 &render_plot_text(&plot, true),
1488 "tests/fixtures/scatterplot/scale3.txt",
1489 );
1490
1491 let plot = scatterplot(
1492 &base_x(),
1493 &base_y(),
1494 LineplotOptions {
1495 xlim: (-1.5, 3.5),
1496 ylim: (-5.5, 2.5),
1497 ..LineplotOptions::default()
1498 },
1499 )
1500 .expect("scatterplot should succeed");
1501 assert_fixture_eq(
1502 &render_plot_text(&plot, true),
1503 "tests/fixtures/scatterplot/limits.txt",
1504 );
1505
1506 let plot = scatterplot(
1507 &base_x(),
1508 &base_y(),
1509 LineplotOptions {
1510 grid: false,
1511 ..LineplotOptions::default()
1512 },
1513 )
1514 .expect("scatterplot should succeed");
1515 assert_fixture_eq(
1516 &render_plot_text(&plot, true),
1517 "tests/fixtures/scatterplot/nogrid.txt",
1518 );
1519 }
1520
1521 #[test]
1522 fn scatterplot_color_parameters_and_canvas_size_fixtures() {
1523 let plot = scatterplot(
1524 &base_x(),
1525 &base_y(),
1526 LineplotOptions {
1527 color: Some(TermColor::Named(NamedColor::Blue)),
1528 name: Some(String::from("points1")),
1529 ..LineplotOptions::default()
1530 },
1531 )
1532 .expect("scatterplot should succeed");
1533 assert_fixture_eq(
1534 &render_plot_text(&plot, true),
1535 "tests/fixtures/scatterplot/blue.txt",
1536 );
1537
1538 let mut plot = scatterplot(
1539 &base_x(),
1540 &base_y(),
1541 LineplotOptions {
1542 name: Some(String::from("points1")),
1543 title: Some(String::from("Scatter")),
1544 xlabel: Some(String::from("x")),
1545 ylabel: Some(String::from("y")),
1546 ..LineplotOptions::default()
1547 },
1548 )
1549 .expect("scatterplot should succeed");
1550 assert_fixture_eq(
1551 &render_plot_text(&plot, true),
1552 "tests/fixtures/scatterplot/parameters1.txt",
1553 );
1554
1555 scatterplot_add_y(
1556 &mut plot,
1557 &[2.0, 0.5, -1.0, 1.0],
1558 LineplotSeriesOptions {
1559 name: Some(String::from("points2")),
1560 ..LineplotSeriesOptions::default()
1561 },
1562 )
1563 .expect("scatterplot add should succeed");
1564 assert_fixture_eq(
1565 &render_plot_text(&plot, true),
1566 "tests/fixtures/scatterplot/parameters2.txt",
1567 );
1568
1569 scatterplot_add(
1570 &mut plot,
1571 &[-0.5, 1.0, 2.5],
1572 &[0.5, 1.0, 1.5],
1573 LineplotSeriesOptions {
1574 name: Some(String::from("points3")),
1575 ..LineplotSeriesOptions::default()
1576 },
1577 )
1578 .expect("scatterplot add should succeed");
1579 assert_fixture_eq(
1580 &render_plot_text(&plot, true),
1581 "tests/fixtures/scatterplot/parameters3.txt",
1582 );
1583 assert_fixture_eq(
1584 &render_plot_text(&plot, false),
1585 "tests/fixtures/scatterplot/nocolor.txt",
1586 );
1587
1588 let plot = scatterplot(
1589 &base_x(),
1590 &base_y(),
1591 LineplotOptions {
1592 title: Some(String::from("Scatter")),
1593 canvas: CanvasType::Dot,
1594 width: 10,
1595 height: 5,
1596 ..LineplotOptions::default()
1597 },
1598 )
1599 .expect("scatterplot should succeed");
1600 assert_fixture_eq(
1601 &render_plot_text(&plot, true),
1602 "tests/fixtures/scatterplot/canvassize.txt",
1603 );
1604 }
1605
1606 #[test]
1607 fn densityplot_unknown_border_name_errors() {
1608 let err =
1609 parse_border_type("invalid_border_name").expect_err("unknown border name should fail");
1610 assert_eq!(
1611 err,
1612 crate::BarplotError::UnknownBorderType {
1613 name: String::from("invalid_border_name")
1614 }
1615 );
1616 }
1617
1618 #[test]
1619 fn densityplot_validates_inputs_and_add_requires_density_plot() {
1620 let mismatch = densityplot(&[1.0, 2.0], &[1.0], LineplotOptions::default());
1621 assert!(matches!(mismatch, Err(LineplotError::LengthMismatch)));
1622
1623 let empty = densityplot(&[] as &[f64], &[] as &[f64], LineplotOptions::default());
1624 assert!(matches!(empty, Err(LineplotError::EmptySeries)));
1625
1626 let invalid = densityplot(&["bad"], &["1"], LineplotOptions::default());
1627 assert!(matches!(
1628 invalid,
1629 Err(LineplotError::InvalidNumericValue { .. })
1630 ));
1631
1632 let mut non_density_plot =
1633 scatterplot(&[1.0, 2.0], &[1.0, 2.0], LineplotOptions::default())
1634 .expect("scatterplot should succeed");
1635 let add_err = densityplot_add(
1636 &mut non_density_plot,
1637 &[1.0, 2.0],
1638 &[1.0, 2.0],
1639 LineplotSeriesOptions::default(),
1640 );
1641 assert!(matches!(add_err, Err(LineplotError::DensityPlotRequired)));
1642
1643 let mut density_plot = densityplot(&[1.0, 2.0], &[1.0, 2.0], LineplotOptions::default())
1644 .expect("densityplot should succeed");
1645
1646 let add_mismatch = densityplot_add(
1647 &mut density_plot,
1648 &[1.0, 2.0],
1649 &[1.0],
1650 LineplotSeriesOptions::default(),
1651 );
1652 assert!(matches!(add_mismatch, Err(LineplotError::LengthMismatch)));
1653
1654 let add_empty = densityplot_add(
1655 &mut density_plot,
1656 &[] as &[f64],
1657 &[] as &[f64],
1658 LineplotSeriesOptions::default(),
1659 );
1660 assert!(matches!(add_empty, Err(LineplotError::EmptySeries)));
1661
1662 let add_invalid = densityplot_add(
1663 &mut density_plot,
1664 &["bad"],
1665 &["1"],
1666 LineplotSeriesOptions::default(),
1667 );
1668 assert!(matches!(
1669 add_invalid,
1670 Err(LineplotError::InvalidNumericValue { .. })
1671 ));
1672 }
1673
1674 #[test]
1675 fn densityplot_forces_density_canvas_and_disables_grid() {
1676 let x = [-1.0, 0.0, 1.0];
1677 let y = [-1.0, 0.0, 1.0];
1678
1679 let default_plot = densityplot(&x, &y, LineplotOptions::default())
1680 .expect("default densityplot should succeed");
1681 let forced_plot = densityplot(
1682 &x,
1683 &y,
1684 LineplotOptions {
1685 canvas: CanvasType::Dot,
1686 grid: true,
1687 ..LineplotOptions::default()
1688 },
1689 )
1690 .expect("forced densityplot should succeed");
1691
1692 assert!(matches!(default_plot.graphics(), GridCanvas::Density(_)));
1693 assert!(matches!(forced_plot.graphics(), GridCanvas::Density(_)));
1694 assert_eq!(
1695 render_plot_text(&forced_plot, false),
1696 render_plot_text(&default_plot, false)
1697 );
1698 }
1699
1700 #[test]
1701 fn densityplot_default_fixture() {
1702 let (dx, dy) = load_density_fixture_data();
1703 assert_eq!(dx.len(), 1000);
1704 assert_eq!(dy.len(), 1000);
1705
1706 let mut plot =
1707 densityplot(&dx, &dy, LineplotOptions::default()).expect("densityplot should succeed");
1708 assert!(matches!(plot.graphics(), GridCanvas::Density(_)));
1709
1710 let dx2: Vec<f64> = dx.iter().map(|value| value + 2.0).collect();
1711 let dy2: Vec<f64> = dy.iter().map(|value| value + 2.0).collect();
1712 densityplot_add(&mut plot, &dx2, &dy2, LineplotSeriesOptions::default())
1713 .expect("densityplot add should succeed");
1714
1715 assert_fixture_eq(
1716 &render_plot_text(&plot, true),
1717 "tests/fixtures/scatterplot/densityplot.txt",
1718 );
1719 }
1720
1721 #[test]
1722 fn densityplot_parameters_fixture() {
1723 let (dx, dy) = load_density_fixture_data();
1724
1725 let mut plot = densityplot(
1726 &dx,
1727 &dy,
1728 LineplotOptions {
1729 name: Some(String::from("foo")),
1730 color: Some(TermColor::Named(NamedColor::Red)),
1731 title: Some(String::from("Title")),
1732 xlabel: Some(String::from("x")),
1733 ..LineplotOptions::default()
1734 },
1735 )
1736 .expect("densityplot should succeed");
1737
1738 let dx2: Vec<f64> = dx.iter().map(|value| value + 2.0).collect();
1739 let dy2: Vec<f64> = dy.iter().map(|value| value + 2.0).collect();
1740 densityplot_add(
1741 &mut plot,
1742 &dx2,
1743 &dy2,
1744 LineplotSeriesOptions {
1745 name: Some(String::from("bar")),
1746 ..LineplotSeriesOptions::default()
1747 },
1748 )
1749 .expect("densityplot add should succeed");
1750
1751 assert_fixture_eq(
1752 &render_plot_text(&plot, true),
1753 "tests/fixtures/scatterplot/densityplot_parameters.txt",
1754 );
1755 }
1756}