diff --git a/Cargo.lock b/Cargo.lock index b1b9122..0b2de42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "textcanvas" -version = "3.5.0" +version = "3.6.0" diff --git a/Cargo.toml b/Cargo.toml index a87aee3..b4d3933 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textcanvas" -version = "3.5.0" +version = "3.6.0" edition = "2021" authors = ["Quentin Richert "] description = "Draw to the terminal like an HTML Canvas." diff --git a/README.md b/README.md index 4da143d..a46b58e 100644 --- a/README.md +++ b/README.md @@ -92,18 +92,17 @@ features. ## Plots ``` -⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠉ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠂⠈ ⠱⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜ -⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠁⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠂⠀⠀⠀⠀ ⠀⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀ -⠀⠀⠀⠀⠀⢀⠤⠊⠁⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⢀⠀⠂⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠣⡀⠀⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀ -⠀⠀⢀⠤⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⢀⠀⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠑⡄⠀⠀⠀⠀⠀⢀⠎⠀⠀⠀ -⡠⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⡀⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠈⠒⠤⣀⠤⠒⠁⠀⠀⠀⠀ - - -⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⢀⠤⠒⠉ ⠀⠀⠀⠀⡇⢠⠋⠑⡄⠀⠀⠀⠀⠀⢀ -⠀⠀⠀⠀⠀⠀⠀⡇⢀⠤⠊⠁⠀⠀⠀ ⠀⠀⠀⠀⣇⠇⠀⠀⢱⠀⠀⠀⠀⠀⡎ -⠤⠤⠤⠤⠤⢤⠤⡯⠥⠤⠤⠤⠤⠤⠤ ⡤⠤⠤⠤⡿⠤⠤⠤⠤⡧⠤⠤⠤⡼⠤ -⠀⠀⢀⠤⠊⠁⠀⡇⠀⠀⠀⠀⠀⠀⠀ ⠸⡀⠀⢰⡇⠀⠀⠀⠀⠸⡀⠀⢠⠃⠀ -⡠⠊⠁⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀ ⠀⠱⡠⠃⡇⠀⠀⠀⠀⠀⠑⠤⠊⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠒⠉ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠂⠈ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⡆⢸ ⠱⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜ +⠀⠀⠀⠀⠀⠀⠀⠀⢀⠤⠊⠁⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠂⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⡆⢸⠀⡇⢸ ⠀⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀ +⠀⠀⠀⠀⠀⢀⠤⠊⠁⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⢀⠀⠂⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⢀⠀⡆⢸⠀⡇⢸⠀⡇⢸ ⠀⠀⠣⡀⠀⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀ +⠀⠀⢀⠤⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⢀⠀⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⢀⠀⡆⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ ⠀⠀⠀⠑⡄⠀⠀⠀⠀⠀⢀⠎⠀⠀⠀ +⡠⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⡀⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⡀⡆⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ ⠀⠀⠀⠀⠈⠒⠤⣀⠤⠒⠁⠀⠀⠀⠀ + +⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼ ⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⢀⠤⠒⠉ ⠀⠀⠀⠀⡇⢠⠋⠑⡄⠀⠀⠀⠀⠀⢀ +⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿ ⠀⠀⠀⠀⠀⠀⠀⡇⢀⠤⠊⠁⠀⠀⠀ ⠀⠀⠀⠀⣇⠇⠀⠀⢱⠀⠀⠀⠀⠀⡎ +⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿ ⠤⠤⠤⠤⠤⢤⠤⡯⠥⠤⠤⠤⠤⠤⠤ ⡤⠤⠤⠤⡿⠤⠤⠤⠤⡧⠤⠤⠤⡼⠤ +⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿ ⠀⠀⢀⠤⠊⠁⠀⡇⠀⠀⠀⠀⠀⠀⠀ ⠸⡀⠀⢰⡇⠀⠀⠀⠀⠸⡀⠀⢠⠃⠀ +⣿⣿⣿⣿⣿⣶⣤⣀⣤⣶⣿⣿⣿⣿⣿ ⡠⠊⠁⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀ ⠀⠱⡠⠃⡇⠀⠀⠀⠀⠀⠑⠤⠊⠀⠀ ``` ## Charts @@ -131,6 +130,17 @@ features. ⠀⠀⠀⠀⠀⠀-5⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ ⠀⠀⠀⠀⠀⠀⠀⠀-5⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 +⠀⠀⠀⠀⠀⠀⠀5⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⢠⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢰⠀⢸⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⡀⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀-5⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ +⠀⠀⠀⠀⠀⠀⠀⠀-5⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 + ⠀⠀⠀⠀⠀⠀⠀1⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠉⠉⠢⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠱⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ @@ -141,6 +151,17 @@ features. ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⠤⡠⠤⠒⠁⠀⠀⠀⠀⠀⢸⠀ ⠀⠀⠀⠀⠀⠀-1⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 + +⠀⠀⠀⠀⠀⠀⠀1⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣶⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣠⣤⣶⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀-1⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 ``` ## 3D diff --git a/pyproject.toml b/pyproject.toml index 9495302..0cce861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "textcanvas" -version = "3.5.0" +version = "3.6.0" authors = [ { name="Quentin Richert", email="noreply@richert.co" }, ] diff --git a/src/charts.rs b/src/charts.rs index e0df713..06bb529 100644 --- a/src/charts.rs +++ b/src/charts.rs @@ -17,6 +17,7 @@ fn cmp_f64(a: &&f64, b: &&f64) -> Ordering { enum PlotType { Line, Scatter, + Bars, } /// Helper functions to plot data on a [`TextCanvas`]. @@ -585,6 +586,46 @@ impl Plot { Self::plot(canvas, x, y, PlotType::Scatter); } + /// Plot bars. + /// + /// The data is scaled to take up the entire canvas. + /// + ///
+ /// + /// `x` and `y` _should_ match in length, + /// + /// If `x` and `y` are not the same length, plotting will stop once + /// the smallest of the two collections is consumed. + /// + ///
+ /// + /// # Examples + /// + /// ```rust + /// use textcanvas::{TextCanvas, charts::Plot}; + /// + /// let mut canvas = TextCanvas::new(15, 5); + /// + /// let x: Vec = (-5..=5).map(f64::from).collect(); + /// let y: Vec = (-5..=5).map(f64::from).collect(); + /// + /// Plot::bars(&mut canvas, &x, &y); + /// + /// assert_eq!( + /// canvas.to_string(), + /// "\ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⡆⢸ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⡆⢸⠀⡇⢸ + /// ⠀⠀⠀⠀⠀⢀⠀⡆⢸⠀⡇⢸⠀⡇⢸ + /// ⠀⠀⢀⠀⡆⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ + /// ⡀⡆⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ + /// " + /// ); + /// ``` + pub fn bars(canvas: &mut TextCanvas, x: &[f64], y: &[f64]) { + Self::plot(canvas, x, y, PlotType::Bars); + } + #[allow(clippy::cast_possible_truncation)] fn plot(canvas: &mut TextCanvas, x: &[f64], y: &[f64], plot_type: PlotType) { if x.is_empty() || y.is_empty() { @@ -648,6 +689,9 @@ impl Plot { PlotType::Scatter => { canvas.set_pixel(x, y, true); } + PlotType::Bars => { + canvas.stroke_line(x, y, x, canvas.h()); + } } } } @@ -674,9 +718,17 @@ impl Plot { // Draw a dot in the middle to show the user we tried to do // something, but the values are off. canvas.set_pixel(canvas.cx(), canvas.cy(), true); + + if plot_type == PlotType::Bars { + // Add the bar for bar plots. + canvas.stroke_line(canvas.cx(), canvas.cy(), canvas.cx(), canvas.h()); + } } } + /// Draw all points at the same Y coordinate. + /// + /// This is a fallback for when the data has no range on the Y axis. fn draw_horizontally_centered_line(canvas: &mut TextCanvas, x: &[f64], plot_type: PlotType) { match plot_type { PlotType::Line => { @@ -689,12 +741,22 @@ impl Plot { } } } + PlotType::Bars => { + for &x_val in x { + if let Some(x) = Self::compute_screen_x(canvas, x_val, x) { + canvas.stroke_line(x, canvas.cy(), x, canvas.h()); + } + } + } } } + /// Draw all points at the same X coordinate. + /// + /// This is a fallback for when the data has no range on the X axis. fn draw_vertically_centered_line(canvas: &mut TextCanvas, y: &[f64], plot_type: PlotType) { match plot_type { - PlotType::Line => { + PlotType::Line | PlotType::Bars => { canvas.stroke_line(canvas.cx(), 0, canvas.cx(), canvas.h()); } PlotType::Scatter => { @@ -738,6 +800,45 @@ impl Plot { Self::line(canvas, &x, &y); } + /// Plot a function, and fill the area under the curve. + /// + /// The function is scaled to take up the entire canvas, and is + /// assumed to be continuous (points will be line-joined together). + /// + /// # Examples + /// + /// ```rust + /// use textcanvas::{TextCanvas, charts::Plot}; + /// + /// let mut canvas = TextCanvas::new(15, 5); + /// + /// Plot::function_filled(&mut canvas, -10.0, 10.0, &|x| x * x); + /// + /// assert_eq!( + /// canvas.to_string(), + /// "\ + /// ⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼ + /// ⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿ + /// ⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿ + /// ⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿ + /// ⣿⣿⣿⣿⣿⣶⣤⣀⣤⣶⣿⣿⣿⣿⣿ + /// " + /// ); + /// ``` + pub fn function_filled( + canvas: &mut TextCanvas, + from_x: f64, + to_x: f64, + f: &impl Fn(f64) -> f64, + ) { + let nb_values = canvas.screen.fwidth(); + let (x, y) = Self::compute_function(from_x, to_x, nb_values, f); + // This is a "trick". Since we've just computed the value of the + // function for every horizontal pixel, we can now plot the + // points as bars to fill up the whole area under the curve. + Self::bars(canvas, &x, &y); + } + /// Compute the values of a function. /// /// This is mainly used internally to compute values for functions. @@ -917,6 +1018,44 @@ impl Chart { Self::chart(canvas, x, y, PlotType::Scatter); } + /// Render chart with a bars plot. + /// + /// # Examples + /// + /// ```rust + /// use textcanvas::{charts::Chart, TextCanvas}; + /// + /// let mut canvas = TextCanvas::new(35, 10); + /// + /// let x: Vec = (-5..=5).map(f64::from).collect(); + /// let y: Vec = (-5..=5).map(f64::from).collect(); + /// + /// Chart::bars(&mut canvas, &x, &y); + /// + /// assert_eq!( + /// canvas.to_string(), + /// "\ + /// ⠀⠀⠀⠀⠀⠀⠀5⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⢸⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⡇⠀⢸⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⢠⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢰⠀⢸⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⡀⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ + /// ⠀⠀⠀⠀⠀⠀-5⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀-5⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 + /// " + /// ); + /// ``` + /// + /// # Panics + /// + /// Panics if chart is < 13×4, because it would make plot < 1×1. + pub fn bars(canvas: &mut TextCanvas, x: &[f64], y: &[f64]) { + Self::chart(canvas, x, y, PlotType::Bars); + } + fn chart(canvas: &mut TextCanvas, x: &[f64], y: &[f64], plot_type: PlotType) { if x.is_empty() || y.is_empty() { return; @@ -951,6 +1090,9 @@ impl Chart { PlotType::Scatter => { Plot::scatter(&mut plot, x, y); } + PlotType::Bars => { + Plot::bars(&mut plot, x, y); + } } canvas.draw_canvas(&plot, Self::MARGIN_LEFT * 2, Self::MARGIN_TOP * 4); @@ -1062,6 +1204,53 @@ impl Chart { let (x, y) = Plot::compute_function(from_x, to_x, nb_values, f); Self::line(canvas, &x, &y); } + + /// Render chart with a function, and fill the area under the curve. + /// + /// # Examples + /// + /// ```rust + /// use textcanvas::{charts::Chart, TextCanvas}; + /// + /// let mut canvas = TextCanvas::new(35, 10); + /// + /// let f = |x: f64| x.cos(); + /// + /// Chart::function_filled(&mut canvas, 0.0, 5.0, &f); + /// + /// assert_eq!( + /// canvas.to_string(), + /// "\ + /// ⠀⠀⠀⠀⠀⠀⠀1⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣶⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣿⣿⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⢸⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣠⣤⣶⣿⣿⣿⣿⣿⣿⢸⠀ + /// ⠀⠀⠀⠀⠀⠀-1⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ + /// ⠀⠀⠀⠀⠀⠀⠀⠀⠀0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 + /// " + /// ); + /// ``` + /// + /// # Panics + /// + /// Panics if chart is < 13×4, because it would make plot < 1×1. + pub fn function_filled( + canvas: &mut TextCanvas, + from_x: f64, + to_x: f64, + f: &impl Fn(f64) -> f64, + ) { + let nb_values = f64::from((canvas.output.width() - (Self::HORIZONTAL_MARGIN)) * 2); + let (x, y) = Plot::compute_function(from_x, to_x, nb_values, f); + // This is a "trick". Since we've just computed the value of the + // function for every horizontal pixel, we can now plot the + // points as bars to fill up the whole area under the curve. + Self::bars(canvas, &x, &y); + } } #[cfg(test)] @@ -2051,6 +2240,206 @@ mod tests { ); } + #[test] + fn plot_bars() { + let mut canvas = TextCanvas::new(15, 5); + + let x: Vec = (-5..=5).map(f64::from).collect(); + let y: Vec = (-5..=5).map(f64::from).collect(); + + Plot::stroke_xy_axes(&mut canvas, &x, &y); + Plot::bars(&mut canvas, &x, &y); + + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⢀⠀⡆⢸ +⠀⠀⠀⠀⠀⠀⠀⡇⢀⠀⡆⢸⠀⡇⢸ +⠤⠤⠤⠤⠤⢤⠤⡧⢼⠤⡧⢼⠤⡧⢼ +⠀⠀⢀⠀⡆⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ +⡀⡆⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ +" + ); + } + + #[test] + fn plot_bars_with_empty_x() { + let mut canvas = TextCanvas::new(15, 5); + + let x: Vec = vec![]; + let y: Vec = (-5..=5).map(f64::from).collect(); + + Plot::stroke_xy_axes(&mut canvas, &x, &y); + Plot::bars(&mut canvas, &x, &y); + + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +" + ); + } + + #[test] + fn plot_bars_with_empty_y() { + let mut canvas = TextCanvas::new(15, 5); + + let x: Vec = (-5..=5).map(f64::from).collect(); + let y: Vec = vec![]; + + Plot::stroke_xy_axes(&mut canvas, &x, &y); + Plot::bars(&mut canvas, &x, &y); + + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀ +" + ); + } + + #[test] + fn plot_bars_with_single_value() { + let mut canvas = TextCanvas::new(15, 5); + + let x: Vec = vec![0.0]; + let y: Vec = vec![0.0]; + + Plot::bars(&mut canvas, &x, &y); + + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢠⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +" + ); + } + + #[test] + fn plot_bars_with_range_xy_zero() { + let mut canvas = TextCanvas::new(15, 5); + + let x: Vec = (-5..=5).map(|_| 0.0).collect(); + let y: Vec = (-5..=5).map(|_| 0.0).collect(); + + Plot::bars(&mut canvas, &x, &y); + + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢠⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +" + ); + } + + #[test] + fn plot_bars_with_range_x_zero() { + let mut canvas = TextCanvas::new(15, 5); + + let x: Vec = (-5..=5).map(|_| 0.0).collect(); + let y: Vec = (-5..=5).map(f64::from).collect(); + + Plot::bars(&mut canvas, &x, &y); + + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +" + ); + } + + #[test] + fn plot_bars_with_range_y_zero() { + let mut canvas = TextCanvas::new(15, 5); + + let x: Vec = (-5..=5).map(f64::from).collect(); + let y: Vec = (-5..=5).map(|_| 0.0).collect(); + + Plot::bars(&mut canvas, &x, &y); + + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⡄⡄⢠⠀⡄⢠⠀⡄⢠⠀⡄⢠⠀⡄⢠ +⡇⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ +⡇⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ +" + ); + } + + #[test] + fn plot_bars_with_x_and_y_of_different_lengths_more_x() { + let mut canvas = TextCanvas::new(15, 5); + + let x: Vec = (-10..=10).map(f64::from).collect(); + let y: Vec = (-5..=5).map(f64::from).collect(); + + Plot::stroke_xy_axes(&mut canvas, &x, &y); + Plot::bars(&mut canvas, &x, &y); + + // The scale is correct. At X = 0, Y = 5. To see values on the + // right, you'd have to increase the range of Y (up to 15, to + // match X). + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⢀⢰⡇⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⡀⣾⢸⡇⠀⠀⠀⠀⠀⠀⠀ +⠤⠤⢤⢴⡧⣿⢼⡧⠤⠤⠤⠤⠤⠤⠤ +⠀⡀⣾⢸⡇⣿⢸⡇⠀⠀⠀⠀⠀⠀⠀ +⣰⡇⣿⢸⡇⣿⢸⡇⠀⠀⠀⠀⠀⠀⠀ +" + ); + } + + #[test] + fn plot_bars_with_x_and_y_of_different_lengths_more_y() { + let mut canvas = TextCanvas::new(15, 5); + + let x: Vec = (-5..=5).map(f64::from).collect(); + let y: Vec = (-10..=10).map(f64::from).collect(); + + Plot::stroke_xy_axes(&mut canvas, &x, &y); + Plot::bars(&mut canvas, &x, &y); + + // The scale is correct. Y range is [-10;10], (0;10) is just + // not rendered because X stops when Y = 0. If you'd continue + // to the right, Y would reach 10 at X = 15. + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀ +⠤⠤⠤⠤⠤⠤⠤⡧⠤⠤⠤⢤⠤⡤⢴ +⠀⠀⠀⠀⠀⢀⠀⡇⢰⠀⡇⢸⠀⡇⢸ +⡀⡄⢰⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ +" + ); + } + #[test] fn plot_function() { let mut canvas = TextCanvas::new(15, 5); @@ -2112,6 +2501,67 @@ mod tests { ); } + #[test] + fn plot_function_filled() { + let mut canvas = TextCanvas::new(15, 5); + + let f = |x| x * x; + + Plot::stroke_xy_axes_of_function(&mut canvas, -10.0, 10.0, &f); + Plot::function_filled(&mut canvas, -10.0, 10.0, &f); + + assert_eq!( + canvas.to_string(), + "\ +⣧⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⣼ +⣿⣇⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⣸⣿ +⣿⣿⣦⠀⠀⠀⠀⡇⠀⠀⠀⠀⣴⣿⣿ +⣿⣿⣿⣷⡀⠀⠀⡇⠀⠀⢀⣾⣿⣿⣿ +⣿⣿⣿⣿⣿⣶⣤⣇⣤⣶⣿⣿⣿⣿⣿ +" + ); + } + + #[test] + fn plot_function_filled_with_single_value() { + let mut canvas = TextCanvas::new(15, 5); + + let f = |_| 0.0; + + Plot::function_filled(&mut canvas, 0.0, 0.0, &f); + + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢠⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀ +" + ); + } + + #[test] + fn plot_function_filled_with_range_zero() { + let mut canvas = TextCanvas::new(15, 5); + + let f = |_| 0.0; + + Plot::function_filled(&mut canvas, -10.0, 10.0, &f); + + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤ +⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ +" + ); + } + #[test] fn compute_function_works_with_structs() { #[derive(Debug, PartialEq)] @@ -2161,7 +2611,7 @@ mod tests { } #[test] - fn chart_x_squared() { + fn chart_function_x_squared() { let mut canvas = TextCanvas::new(71, 19); let f = |x| x * x; @@ -2196,7 +2646,7 @@ mod tests { } #[test] - fn chart_polynomial() { + fn chart_function_polynomial() { let mut canvas = TextCanvas::new(71, 19); let f = |x: f64| x.powi(3) - 2.0 * x.powi(2) + 3.0 * x; @@ -2231,7 +2681,7 @@ mod tests { } #[test] - fn chart_cos() { + fn chart_function_cos() { let mut canvas = TextCanvas::new(71, 19); let f = |x: f64| x.cos(); @@ -2265,6 +2715,111 @@ mod tests { ); } + #[test] + fn chart_function_filled_x_squared() { + let mut canvas = TextCanvas::new(71, 19); + + let f = |x| x * x; + + Chart::function_filled(&mut canvas, -10.0, 10.0, &f); + + println!("{canvas}"); + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀100⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡄⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣶⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣶⣦⣤⣤⣄⣠⣤⣤⢰⡆⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀0.0073⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ +⠀⠀⠀⠀⠀⠀⠀-10⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀10 +" + ); + } + + #[test] + fn chart_function_filled_polynomial() { + let mut canvas = TextCanvas::new(71, 19); + + let f = |x: f64| x.powi(3) - 2.0 * x.powi(2) + 3.0 * x; + + Chart::function_filled(&mut canvas, -5.0, 5.0, &f); + + println!("{canvas}"); + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀90⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣶⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⣠⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⡀⣀⣀⣀⣀⣀⣠⣤⣤⡄⣶⢰⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣤⣶⣶⣶⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⡄⣾⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣶⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀-190⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ +⠀⠀⠀⠀⠀⠀⠀⠀-5⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 +" + ); + } + + #[test] + fn chart_function_filled_cos() { + let mut canvas = TextCanvas::new(71, 19); + + let f = |x: f64| x.cos(); + + Chart::function_filled(&mut canvas, 0.0, 5.0, &f); + + println!("{canvas}"); + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀1⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣶⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢰⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣤⣤⣄⣤⣤⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀ +⠀⠀⠀⠀⠀⠀-1⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 +" + ); + } + #[test] fn chart_line() { let mut canvas = TextCanvas::new(35, 10); @@ -2319,6 +2874,33 @@ mod tests { ); } + #[test] + fn chart_bars() { + let mut canvas = TextCanvas::new(35, 10); + + let x: Vec = (-5..=5).map(f64::from).collect(); + let y: Vec = (-5..=5).map(f64::from).collect(); + + Chart::bars(&mut canvas, &x, &y); + + println!("{canvas}"); + assert_eq!( + canvas.to_string(), + "\ +⠀⠀⠀⠀⠀⠀⠀5⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⢠⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢰⠀⢸⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⡀⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ +⠀⠀⠀⠀⠀⠀-5⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ +⠀⠀⠀⠀⠀⠀⠀⠀-5⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 +" + ); + } + #[test] fn chart_empty() { let mut canvas = TextCanvas::new(35, 10); diff --git a/tests/test_charts.py b/tests/test_charts.py index d37fc84..aa9703b 100644 --- a/tests/test_charts.py +++ b/tests/test_charts.py @@ -826,6 +826,170 @@ def test_plot_scatter_with_x_and_y_of_different_lengths_more_y(self) -> None: "⡀⠄⠐⠀⠁⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀\n", ) + def test_plot_bars(self) -> None: + canvas = TextCanvas(15, 5) + + x: list[float] = list(range(-5, 6)) + y: list[float] = list(range(-5, 6)) + + Plot.stroke_xy_axes(canvas, x, y) + Plot.bars(canvas, x, y) + + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⢀⠀⡆⢸\n" + "⠀⠀⠀⠀⠀⠀⠀⡇⢀⠀⡆⢸⠀⡇⢸\n" + "⠤⠤⠤⠤⠤⢤⠤⡧⢼⠤⡧⢼⠤⡧⢼\n" + "⠀⠀⢀⠀⡆⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸\n" + "⡀⡆⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸\n", + ) + + def test_plot_bars_with_empty_x(self) -> None: + canvas = TextCanvas(15, 5) + + x: list[float] = [] + y: list[float] = list(range(-5, 6)) + + Plot.stroke_xy_axes(canvas, x, y) + Plot.bars(canvas, x, y) + + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n", + ) + + def test_plot_bars_with_empty_y(self) -> None: + canvas = TextCanvas(15, 5) + + x: list[float] = list(range(-5, 6)) + y: list[float] = [] + + Plot.stroke_xy_axes(canvas, x, y) + Plot.bars(canvas, x, y) + + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀\n", + ) + + def test_plot_bars_with_single_value(self) -> None: + canvas = TextCanvas(15, 5) + + x: list[float] = [0] + y: list[float] = [0] + + Plot.bars(canvas, x, y) + + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢠⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n", + ) + + def test_plot_bars_with_range_xy_zero(self) -> None: + canvas = TextCanvas(15, 5) + + x: list[float] = [0 for _ in range(-5, 6)] + y: list[float] = [0 for _ in range(-5, 6)] + + Plot.bars(canvas, x, y) + + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢠⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n", + ) + + def test_plot_bars_with_range_x_zero(self) -> None: + canvas = TextCanvas(15, 5) + + x: list[float] = [0 for _ in range(-5, 6)] + y: list[float] = list(range(-5, 6)) + + Plot.bars(canvas, x, y) + + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n", + ) + + def test_plot_bars_with_range_y_zero(self) -> None: + canvas = TextCanvas(15, 5) + + x: list[float] = list(range(-5, 6)) + y: list[float] = [0 for _ in range(-5, 6)] + + Plot.bars(canvas, x, y) + + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⡄⡄⢠⠀⡄⢠⠀⡄⢠⠀⡄⢠⠀⡄⢠\n" + "⡇⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸\n" + "⡇⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸\n", + ) + + def test_plot_bars_with_x_and_y_of_different_lengths_more_x(self) -> None: + canvas = TextCanvas(15, 5) + + x: list[float] = list(range(-10, 11)) + y: list[float] = list(range(-5, 6)) + + Plot.stroke_xy_axes(canvas, x, y) + Plot.bars(canvas, x, y) + + # The scale is correct. At X = 0, Y = 5. To see values on the + # right, you'd have to increase the range of Y (up to 15, to + # match X). + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⢀⢰⡇⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⡀⣾⢸⡇⠀⠀⠀⠀⠀⠀⠀\n" + "⠤⠤⢤⢴⡧⣿⢼⡧⠤⠤⠤⠤⠤⠤⠤\n" + "⠀⡀⣾⢸⡇⣿⢸⡇⠀⠀⠀⠀⠀⠀⠀\n" + "⣰⡇⣿⢸⡇⣿⢸⡇⠀⠀⠀⠀⠀⠀⠀\n", + ) + + def test_plot_bars_with_x_and_y_of_different_lengths_more_y(self) -> None: + canvas = TextCanvas(15, 5) + + x: list[float] = list(range(-5, 6)) + y: list[float] = list(range(-10, 11)) + + Plot.stroke_xy_axes(canvas, x, y) + Plot.bars(canvas, x, y) + + # The scale is correct. Y range is [-10;10], (0;10) is just + # not rendered because X stops when Y = 0. If you'd continue + # to the right, Y would reach 10 at X = 15. + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀\n" + "⠤⠤⠤⠤⠤⠤⠤⡧⠤⠤⠤⢤⠤⡤⢴\n" + "⠀⠀⠀⠀⠀⢀⠀⡇⢰⠀⡇⢸⠀⡇⢸\n" + "⡀⡄⢰⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸\n", + ) + def test_plot_function(self) -> None: canvas = TextCanvas(15, 5) @@ -878,6 +1042,58 @@ def f(_: float) -> float: "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n", ) + def test_plot_function_filled(self) -> None: + canvas = TextCanvas(15, 5) + + def f(x: float) -> float: + return x * x + + Plot.stroke_xy_axes_of_function(canvas, -10.0, 10.0, f) + Plot.function_filled(canvas, -10.0, 10.0, f) + + self.assertEqual( + canvas.to_string(), + "⣧⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⣼\n" + "⣿⣇⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⣸⣿\n" + "⣿⣿⣦⠀⠀⠀⠀⡇⠀⠀⠀⠀⣴⣿⣿\n" + "⣿⣿⣿⣷⡀⠀⠀⡇⠀⠀⢀⣾⣿⣿⣿\n" + "⣿⣿⣿⣿⣿⣶⣤⣇⣤⣶⣿⣿⣿⣿⣿\n", + ) + + def test_plot_function_filled_with_single_value(self) -> None: + canvas = TextCanvas(15, 5) + + def f(_: float) -> float: + return 0 + + Plot.function_filled(canvas, 0.0, 0.0, f) + + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢠⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀\n", + ) + + def test_plot_function_filled_with_range_zero(self) -> None: + canvas = TextCanvas(15, 5) + + def f(_: float) -> float: + return 0 + + Plot.function_filled(canvas, -10.0, 10.0, f) + + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤\n" + "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n" + "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n", + ) + def test_compute_function_works_with_structs(self) -> None: @dataclass class Mock: @@ -911,7 +1127,7 @@ def f(x: float) -> Mock: class TestChart(unittest.TestCase): - def test_chart_x_squared(self) -> None: + def test_chart_function_x_squared(self) -> None: canvas = TextCanvas(71, 19) def f(x: float) -> float: @@ -943,7 +1159,7 @@ def f(x: float) -> float: "⠀⠀⠀⠀⠀⠀⠀-10⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀10\n", ) - def test_chart_polynomial(self) -> None: + def test_chart_function_polynomial(self) -> None: canvas = TextCanvas(71, 19) def f(x: float) -> float: @@ -975,7 +1191,7 @@ def f(x: float) -> float: "⠀⠀⠀⠀⠀⠀⠀⠀-5⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5\n", ) - def test_chart_cos(self) -> None: + def test_chart_function_cos(self) -> None: canvas = TextCanvas(71, 19) def f(x: float) -> float: @@ -1007,6 +1223,102 @@ def f(x: float) -> float: "⠀⠀⠀⠀⠀⠀⠀⠀⠀0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5\n", ) + def test_chart_function_filled_x_squared(self) -> None: + canvas = TextCanvas(71, 19) + + def f(x: float) -> float: + return x * x + + Chart.function_filled(canvas, -10.0, 10.0, f) + + # print(f"{canvas}") + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀100⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡄⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣶⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣶⣦⣤⣤⣄⣠⣤⣤⢰⡆⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀0.0073⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀\n" + "⠀⠀⠀⠀⠀⠀⠀-10⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀10\n", + ) + + def test_chart_function_filled_polynomial(self) -> None: + canvas = TextCanvas(71, 19) + + def f(x: float) -> float: + return x**3 - 2 * x**2 + 3 * x + + Chart.function_filled(canvas, -5.0, 5.0, f) + + # print(f"{canvas}") + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀90⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣶⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⣠⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⡀⣀⣀⣀⣀⣀⣠⣤⣤⡄⣶⢰⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣤⣶⣶⣶⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⡄⣾⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣶⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⡇⡇⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⢸⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀-190⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀-5⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5\n", + ) + + def test_chart_function_filled_cos(self) -> None: + canvas = TextCanvas(71, 19) + + def f(x: float) -> float: + return math.cos(x) + + Chart.function_filled(canvas, 0.0, 5.0, f) + + # print(f"{canvas}") + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀1⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣶⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢰⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣤⣤⣄⣤⣤⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⠀\n" + "⠀⠀⠀⠀⠀⠀-1⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5\n", + ) + def test_chart_line(self) -> None: canvas = TextCanvas(35, 10) @@ -1053,6 +1365,29 @@ def test_chart_scatter(self) -> None: "⠀⠀⠀⠀⠀⠀⠀⠀-5⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5\n", ) + def test_chart_bars(self) -> None: + canvas = TextCanvas(35, 10) + + x: list[float] = list(range(-5, 6)) + y: list[float] = list(range(-5, 6)) + + Chart.bars(canvas, x, y) + + # print(f"{canvas}") + self.assertEqual( + canvas.to_string(), + "⠀⠀⠀⠀⠀⠀⠀5⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⢸⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⡇⠀⢸⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⢠⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢰⠀⢸⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⡀⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀\n" + "⠀⠀⠀⠀⠀⠀-5⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀\n" + "⠀⠀⠀⠀⠀⠀⠀⠀-5⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5\n", + ) + def test_chart_empty(self) -> None: canvas = TextCanvas(35, 10) diff --git a/textcanvas/charts.py b/textcanvas/charts.py index 1c4fe9b..b5ab4e6 100644 --- a/textcanvas/charts.py +++ b/textcanvas/charts.py @@ -7,6 +7,7 @@ class PlotType(enum.Enum): LINE = "LINE" SCATTER = "SCATTER" + BARS = "BARS" class Plot: @@ -454,6 +455,35 @@ def scatter(canvas: TextCanvas, x: list[float], y: list[float]) -> None: """ Plot._plot(canvas, x, y, PlotType.SCATTER) + @staticmethod + def bars(canvas: TextCanvas, x: list[float], y: list[float]) -> None: + """Plot bars. + + The data is scaled to take up the entire canvas. + +
+ + `x` and `y` _should_ match in length, + + If `x` and `y` are not the same length, plotting will stop once + the smallest of the two collections is consumed. + +
+ + Examples: + >>> canvas = TextCanvas(15, 5) + >>> x: list[float] = list(range(-5, 6)) + >>> y: list[float] = list(range(-5, 6)) + >>> Plot.bars(canvas, x, y) + >>> print(canvas, end="") + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⡆⢸ + ⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⡆⢸⠀⡇⢸ + ⠀⠀⠀⠀⠀⢀⠀⡆⢸⠀⡇⢸⠀⡇⢸ + ⠀⠀⢀⠀⡆⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ + ⡀⡆⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸⠀⡇⢸ + """ + Plot._plot(canvas, x, y, PlotType.BARS) + @staticmethod def _plot( canvas: TextCanvas, @@ -522,6 +552,8 @@ def _plot( previous = pair case PlotType.SCATTER: canvas.set_pixel(x, y, True) + case PlotType.BARS: + canvas.stroke_line(x, y, x, canvas.h) @staticmethod def _handle_axes_without_range( @@ -547,10 +579,18 @@ def _handle_axes_without_range( # something, but the values are off. canvas.set_pixel(canvas.cx, canvas.cy, True) + if plot_type == PlotType.BARS: + # Add the bar for bar plots. + canvas.stroke_line(canvas.cx, canvas.cy, canvas.cx, canvas.h) + @staticmethod def _draw_horizontally_centered_line( canvas: TextCanvas, x_vals: list[float], plot_type: PlotType ) -> None: + """Draw all points at the same Y coordinate. + + This is a fallback for when the data has no range on the Y axis. + """ match plot_type: case PlotType.LINE: canvas.stroke_line(0, canvas.cy, canvas.w, canvas.cy) @@ -558,13 +598,21 @@ def _draw_horizontally_centered_line( for x_val in x_vals: if (x := Plot.compute_screen_x(canvas, x_val, x_vals)) is not None: canvas.set_pixel(x, canvas.cy, True) + case PlotType.BARS: + for x_val in x_vals: + if (x := Plot.compute_screen_x(canvas, x_val, x_vals)) is not None: + canvas.stroke_line(x, canvas.cy, x, canvas.h) @staticmethod def _draw_vertically_centered_line( canvas: TextCanvas, y_vals: list[float], plot_type: PlotType ) -> None: + """Draw all points at the same X coordinate. + + This is a fallback for when the data has no range on the X axis. + """ match plot_type: - case PlotType.LINE: + case PlotType.LINE | PlotType.BARS: canvas.stroke_line(canvas.cx, 0, canvas.cx, canvas.h) case PlotType.SCATTER: for y_val in y_vals: @@ -594,6 +642,32 @@ def function( (x, y) = Plot.compute_function(from_x, to_x, nb_values, f) Plot.line(canvas, x, y) + @staticmethod + def function_filled( + canvas: TextCanvas, from_x: float, to_x: float, f: Callable[[float], float] + ) -> None: + """Plot a function, and fill the area under the curve. + + The function is scaled to take up the entire canvas, and is + assumed to be continuous (points will be line-joined together). + + Examples: + >>> canvas = TextCanvas(15, 5) + >>> Plot.function_filled(canvas, -10.0, 10.0, lambda x: x ** 2) + >>> print(canvas, end="") + ⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼ + ⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿ + ⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿ + ⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿ + ⣿⣿⣿⣿⣿⣶⣤⣀⣤⣶⣿⣿⣿⣿⣿ + """ + nb_values: int = canvas.screen.width + (x, y) = Plot.compute_function(from_x, to_x, nb_values, f) + # This is a "trick". Since we've just computed the value of the + # function for every horizontal pixel, we can now plot the + # points as bars to fill up the whole area under the curve. + Plot.bars(canvas, x, y) + @staticmethod def compute_function[T]( from_x: float, @@ -738,6 +812,33 @@ def scatter(canvas: TextCanvas, x: list[float], y: list[float]) -> None: """ Chart._chart(canvas, x, y, PlotType.SCATTER) + @staticmethod + def bars(canvas: TextCanvas, x: list[float], y: list[float]) -> None: + """Render chart with a bars plot. + + Examples: + >>> canvas = TextCanvas(35, 10) + >>> x: list[float] = list(range(-5, 6)) + >>> y: list[float] = list(range(-5, 6)) + >>> Chart.bars(canvas, x, y) + >>> print(canvas, end="") + ⠀⠀⠀⠀⠀⠀⠀5⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⢸⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⡇⠀⢸⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⢠⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢰⠀⢸⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⡀⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⠀⢸⠀⠀⡇⠀⡇⠀⢸⢸⠀ + ⠀⠀⠀⠀⠀⠀-5⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀-5⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 + + Raises: + ValueError: If chart is < 13×4, because it would make plot + size < 1×1. + """ + Chart._chart(canvas, x, y, PlotType.BARS) + @staticmethod def _chart( canvas: TextCanvas, x: list[float], y: list[float], plot_type: PlotType @@ -774,6 +875,8 @@ def _plot_values( Plot.line(plot, x, y) case PlotType.SCATTER: Plot.scatter(plot, x, y) + case PlotType.BARS: + Plot.bars(plot, x, y) canvas.draw_canvas(plot, Chart.MARGIN_LEFT * 2, Chart.MARGIN_TOP * 4) @@ -874,3 +977,38 @@ def function( nb_values = (canvas.output.width - Chart.HORIZONTAL_MARGIN) * 2 (x, y) = Plot.compute_function(from_x, to_x, nb_values, f) Chart.line(canvas, x, y) + + @staticmethod + def function_filled( + canvas: TextCanvas, from_x: float, to_x: float, f: Callable[[float], float] + ) -> None: + """Render chart with a function, and fill the area under the + curve. + + Examples: + >>> import math + >>> canvas = TextCanvas(35, 10) + >>> f = lambda x: math.cos(x) + >>> Chart.function_filled(canvas, 0.0, 5.0, f) + >>> print(canvas, end="") + ⠀⠀⠀⠀⠀⠀⠀1⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣶⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣿⣿⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⢸⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣠⣤⣶⣿⣿⣿⣿⣿⣿⢸⠀ + ⠀⠀⠀⠀⠀⠀-1⠀⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠀ + ⠀⠀⠀⠀⠀⠀⠀⠀⠀0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀5 + + Raises: + ValueError: If chart is < 13×4, because it would make plot + size < 1×1. + """ + nb_values = (canvas.output.width - Chart.HORIZONTAL_MARGIN) * 2 + (x, y) = Plot.compute_function(from_x, to_x, nb_values, f) + # This is a "trick". Since we've just computed the value of the + # function for every horizontal pixel, we can now plot the + # points as bars to fill up the whole area under the curve. + Chart.bars(canvas, x, y)