diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e078438 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,16 @@ +# todo: spin our own image +image: registry.gitlab.com/mchodzikiewicz/embedded-plots-docker:latest + +stages: + - build + - test + +build: + stage: build + script: + - cargo build + +test: + stage: test + script: + - cargo test \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 5ae3ceb..e765d10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,14 +3,21 @@ name = "embedded-plots" version = "0.1.0" authors = ["MichaƂ Chodzikiewicz "] edition = "2018" +license-file = "LICENSE" +description = "Heapless plotting library for embedded targets based on embedded-graphics crate" +homepage = "https://gitlab.com/mchodzikiewicz/embedded-plots" +repository = "https://gitlab.com/mchodzikiewicz/embedded-plots" +readme = "README.md" +keywords = ["embedded", "plot", "graphics"] +categories = ["embedded","visualization"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] embedded-graphics = "0.6.0" - +itertools = "0.9.0" +heapless = "0.5.6" [dev-dependencies] embedded-graphics-simulator = "0.2.1" test-case = "1.0.0" - diff --git a/README.md b/README.md new file mode 100644 index 0000000..54e712e --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Embedded Plots +Heapless plotting library for small embedded targets, based on [embedded-graphics](https://crates.io/crates/embedded-graphics) +crate. + +This is very beginning of the development, however it is functional to the point where single plot can be drawn. \ No newline at end of file diff --git a/examples/basic-plot/main.rs b/examples/basic-plot/main.rs deleted file mode 100644 index aefa48b..0000000 --- a/examples/basic-plot/main.rs +++ /dev/null @@ -1,32 +0,0 @@ -use embedded_graphics::{ - pixelcolor::Rgb565, - prelude::*, -}; - -use embedded_graphics_simulator::{ - SimulatorDisplay, - Window, - OutputSettingsBuilder -}; - -use embedded_plots::curve::{PlotPoint, Curve}; - -fn main() -> Result<(), core::convert::Infallible> { - let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(480, 272)); - - let data = vec![ - PlotPoint{x: 0,y: 0}, - PlotPoint{x: 1,y: 1}, - PlotPoint{x: 2,y: 1}, - PlotPoint{x: 3,y: 0}, - ]; - Curve::new(data.as_slice()) - .into_drawable_curve(&(0..3),&(0..1),&Point{x: 20, y: 20}, &Point{x:450,y:250},RgbColor::WHITE) - .draw(&mut display)?; - - let output_settings = OutputSettingsBuilder::new() - .build(); - Window::new("Hello World", &output_settings).show_static(&display); - - Ok(()) -} \ No newline at end of file diff --git a/examples/free_axis.rs b/examples/free_axis.rs new file mode 100644 index 0000000..b47b82c --- /dev/null +++ b/examples/free_axis.rs @@ -0,0 +1,129 @@ +use embedded_graphics::{ + pixelcolor::Rgb565, + prelude::*, + style::TextStyleBuilder, + fonts::{Font6x8, Font6x6}, +}; + +use embedded_graphics_simulator::{ + SimulatorDisplay, + Window, + OutputSettingsBuilder, +}; + +use embedded_plots::{ + axis::{Axis, Placement, Scale}, +}; + +fn main() -> Result<(), core::convert::Infallible> { + let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(480, 272)); + + let text_style_white = TextStyleBuilder::new(Font6x8) + .text_color(RgbColor::WHITE) + .build(); + + let text_style_yellow_compact = TextStyleBuilder::new(Font6x6) + .text_color(RgbColor::YELLOW) + .build(); + + Axis::new(0..100) + .set_title("X Fixed 0-100(10)") + .set_scale(Scale::Fixed(10)) + .into_drawable_axis(Placement::X { x1: 40, x2: 230, y: 10 }) + .set_color(RgbColor::WHITE) + .set_text_style(text_style_white) + .set_thickness(2) + .set_tick_size(2) + .draw(&mut display)?; + + Axis::new(0..200) + .set_title("X Fixed 0-200(100)") + .set_scale(Scale::Fixed(100)) + .into_drawable_axis(Placement::X { x1: 240, x2: 470, y: 10 }) + .set_color(RgbColor::YELLOW) + .set_text_style(text_style_yellow_compact) + .set_tick_size(2) + .draw(&mut display)?; + + Axis::new(0..100) + .set_title("X Frac 0-100(7)") + .set_scale(Scale::RangeFraction(7)) + .into_drawable_axis(Placement::X { x1: 50, x2: 220, y: 30 }) + .set_color(RgbColor::BLUE) + .set_text_style(text_style_white) + .set_tick_size(3) + .draw(&mut display)?; + + Axis::new(0..200) + .set_title("X Frac 0-200(4)") + .set_scale(Scale::RangeFraction(4)) + .into_drawable_axis(Placement::X { x1: 250, x2: 460, y: 40 }) + .set_color(RgbColor::RED) + .set_text_style(text_style_yellow_compact) + .set_tick_size(7) + .draw(&mut display)?; + + Axis::new(0..100) + .set_title("Y Fixed 0-100(10)") + .set_scale(Scale::Fixed(10)) + .into_drawable_axis( + Placement::Y { y1: 70, y2: 230, x: 160 }) + .set_color(RgbColor::WHITE) + .set_text_style(text_style_white) + .set_tick_size(2) + .draw(&mut display)?; + + Axis::new(0..200) + .set_title("Y Fixed 0-200(100)") + .set_scale(Scale::Fixed(100)) + .into_drawable_axis( + Placement::Y { y1: 70, y2: 210, x: 260 }) + .set_color(RgbColor::YELLOW) + .set_text_style(text_style_yellow_compact) + .set_tick_size(1) + .draw(&mut display)?; + + Axis::new(0..100) + .set_title("Y Frac 0-100(7)") + .set_scale(Scale::RangeFraction(7)) + .into_drawable_axis(Placement::Y { y1: 60, y2: 180, x: 370 }) + .set_color(RgbColor::BLUE) + .set_text_style(text_style_white) + .set_tick_size(3) + .draw(&mut display)?; + + Axis::new(0..200) + .set_title("Y Frac 0-200(4)") + .set_scale(Scale::RangeFraction(4)) + .into_drawable_axis(Placement::Y { y1: 90, y2: 220, x: 470 }) + .set_color(RgbColor::RED) + .set_text_style(text_style_yellow_compact) + .set_tick_size(7) + .draw(&mut display)?; + + Axis::new(123..2137) + .set_title("X") + .set_scale(Scale::Fixed(150)) + .into_drawable_axis(Placement::X { x1: 30, x2: 470, y: 250 }) + .set_color(RgbColor::YELLOW) + .set_text_style(text_style_white) + .set_tick_size(2) + .draw(&mut display)?; + + Axis::new(0..2137) + .set_title("Y") + .set_scale(Scale::RangeFraction(15)) + .into_drawable_axis(Placement::Y { y1: 10, y2: 250, x: 30 }) + .set_color(RgbColor::WHITE) + .set_text_style(text_style_white) + .set_tick_size(2) + .draw(&mut display)?; + + + let output_settings = OutputSettingsBuilder::new() + .pixel_spacing(1) + .build(); + Window::new("Free axis", &output_settings).show_static(&display); + + Ok(()) +} \ No newline at end of file diff --git a/examples/single_plot_mono.rs b/examples/single_plot_mono.rs new file mode 100644 index 0000000..5fc011a --- /dev/null +++ b/examples/single_plot_mono.rs @@ -0,0 +1,42 @@ +use embedded_graphics::{ + prelude::*, + pixelcolor::BinaryColor, +}; + +use embedded_graphics_simulator::{SimulatorDisplay, Window, OutputSettingsBuilder, BinaryColorTheme}; + +use embedded_plots::{ + single_plot::{SinglePlot}, + curve::{PlotPoint, Curve}, + axis::Scale, +}; + +fn main() -> Result<(), core::convert::Infallible> { + let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(128, 48)); + + let data = vec![ + PlotPoint { x: 0, y: 0 }, + PlotPoint { x: 1, y: 2 }, + PlotPoint { x: 2, y: 2 }, + PlotPoint { x: 3, y: 0 }, + ]; + let curve = Curve::from_data(data.as_slice()); + + let plot = SinglePlot::new( + &curve, + Scale::RangeFraction(3), + Scale::RangeFraction(2), + ).into_drawable( + Point { x: 18, y: 2 }, + Point { x: 120, y: 30 }, + ).set_color(BinaryColor::On); + + plot.draw(&mut display)?; + let output_settings = OutputSettingsBuilder::new() + .theme(BinaryColorTheme::OledBlue) + .build(); + Window::new("Basic plot", &output_settings) + .show_static(&display); + + Ok(()) +} \ No newline at end of file diff --git a/examples/single_plot_rgb.rs b/examples/single_plot_rgb.rs new file mode 100644 index 0000000..f3c0c29 --- /dev/null +++ b/examples/single_plot_rgb.rs @@ -0,0 +1,43 @@ +use embedded_graphics::{ + pixelcolor::Rgb565, + prelude::*, +}; + +use embedded_graphics_simulator::{ + SimulatorDisplay, + Window, + OutputSettingsBuilder, +}; + +use embedded_plots::{ + single_plot::{SinglePlot}, + curve::{PlotPoint, Curve}, + axis::Scale, +}; + +fn main() -> Result<(), core::convert::Infallible> { + let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(480, 272)); + + let data = vec![ + PlotPoint { x: 0, y: 0 }, + PlotPoint { x: 1, y: 2 }, + PlotPoint { x: 2, y: 2 }, + PlotPoint { x: 3, y: 0 }, + ]; + let curve = Curve::from_data(data.as_slice()); + + let plot = SinglePlot::new( + &curve, + Scale::RangeFraction(3), + Scale::RangeFraction(2)).into_drawable( + Point { x: 50, y: 10 }, + Point { x: 430, y: 250 }, + ).set_color(RgbColor::YELLOW).set_text_color(RgbColor::WHITE); + + plot.draw(&mut display)?; + let output_settings = OutputSettingsBuilder::new() + .build(); + Window::new("Basic plot", &output_settings).show_static(&display); + + Ok(()) +} \ No newline at end of file diff --git a/src/axis.rs b/src/axis.rs new file mode 100644 index 0000000..f3e8ab3 --- /dev/null +++ b/src/axis.rs @@ -0,0 +1,188 @@ +use core::ops::Range; +use core::fmt::Write; +use heapless::{consts::*, String}; + +use embedded_graphics::{ + prelude::*, + style::{TextStyle, PrimitiveStyle}, + primitives::Line, + fonts::Text, +}; +use crate::range_conv::Scalable; + + +pub enum Placement { + X { + x1: i32, + x2: i32, + y: i32, + }, + Y { + y1: i32, + y2: i32, + x: i32, + }, +} + +pub enum Scale { + Fixed(usize), + RangeFraction(usize), +} + +impl Default for Scale { + fn default() -> Self { + Scale::RangeFraction(5) + } +} + +pub struct Axis<'a> { + range: Range, + title: Option<&'a str>, + scale: Option, +} + +impl<'a> Axis<'a> +{ + pub fn new(range: Range) -> Axis<'a> { + Axis { range, title: None, scale: None } + } + + pub fn set_scale(mut self, scale: Scale) -> Axis<'a> { + self.scale = Some(scale); + self + } + + pub fn set_title(mut self, title: &'a str) -> Axis<'a> { + self.title = Some(title); + self + } + + pub fn into_drawable_axis(self, placement: Placement) -> DrawableAxis<'a, C, F> + where + C: PixelColor + Default, + F: Font, + TextStyle: Clone + Default, + { + DrawableAxis{ + axis: self, + placement, + color: None, + text_style: None, + tick_size: None, + thickness: None, + } + } +} + +pub struct DrawableAxis<'a, C, F> + where + C: PixelColor, + F: Font, + TextStyle: Clone + Default, +{ + axis: Axis<'a>, + placement: Placement, + color: Option, + text_style: Option>, + tick_size: Option, + thickness: Option, +} + +impl<'a, C, F> DrawableAxis<'a, C, F> + where + C: PixelColor + Default, + F: Font, + TextStyle: Clone + Default, +{ + pub fn set_color(mut self, val: C) -> DrawableAxis<'a, C, F> { + self.color = Some(val); + self + } + pub fn set_text_style(mut self, val: TextStyle) -> DrawableAxis<'a, C, F> { + self.text_style = Some(val); + self + } + pub fn set_tick_size(mut self, val: usize) -> DrawableAxis<'a, C, F> { + self.tick_size = Some(val); + self + } + pub fn set_thickness(mut self, val: usize) -> DrawableAxis<'a, C, F> { + self.thickness = Some(val); + self + } +} + + +impl<'a, C, F> Drawable for DrawableAxis<'a, C, F> + where + C: PixelColor + Default, + F: Font + Copy, + TextStyle: Clone + Default, +{ + fn draw>(self, display: &mut D) -> Result<(), D::Error> { + let color = self.color.unwrap_or_default(); + let text_style = self.text_style.unwrap_or_default(); + let thickness = self.thickness.unwrap_or(1); + let tick_size = self.tick_size.unwrap_or(2); + + + let scale_marks = match self.axis.scale.unwrap_or_default() { + Scale::Fixed(interval) => { + self.axis.range.clone().into_iter().step_by(interval) + } + Scale::RangeFraction(fraction) => { + let len = self.axis.range.len(); + self.axis.range.clone().into_iter().step_by(len / fraction) + } + }; + match self.placement { + Placement::X { x1, x2, y } => { + Line { start: Point { x: x1, y }, end: Point { x: x2, y } } + .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32)) + .draw(display)?; + if let Some(title) = self.axis.title { + let title = Text::new(title, Point { x: x1, y: y + 10 }) + .into_styled(text_style); + let title = title.translate(Point { x: (x2 - x1) / 2 - title.size().width as i32 / 2, y: 0 }); + title.draw(display)?; + } + + for mark in scale_marks { + let x = mark.scale_between_ranges(&self.axis.range, &(x1..x2)); + Line { start: Point { x, y: y - tick_size as i32 }, end: Point { x, y: y + tick_size as i32 } } + .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32)) + .draw(display)?; + let mut buf: String:: = String::new(); + write!(buf, "{}", mark).unwrap(); + Text::new(&buf, Point { x: x + 2, y: y + 2 }).into_styled(text_style).draw(display)?; + } + } + Placement::Y { y1, y2, x } => { + Line { start: Point { x, y: y1 }, end: Point { x, y: y2 } } + .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32)) + .draw(display)?; + + let mut max_tick_text_width = 0; + for mark in scale_marks { + let y = mark.scale_between_ranges(&self.axis.range, &(y2..y1)); + Line { start: Point { x: x - tick_size as i32, y }, end: Point { x: x + tick_size as i32, y } } + .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32)) + .draw(display)?; + let mut buf: String:: = String::new(); + write!(buf, "{}", mark).unwrap(); + let tick_val = Text::new(&buf, Point { x, y }).into_styled(text_style); + let tick_val = tick_val.translate(Point { x: -(tick_val.size().width as i32) - 2, y: 2 }); + if tick_val.size().width > max_tick_text_width { max_tick_text_width = tick_val.size().width } + tick_val.draw(display)?; + } + if let Some(title) = self.axis.title { + let title = Text::new(title, Point { x, y: y1 }) + .into_styled(text_style); + let title = title.translate(Point { x: -(title.size().width as i32) - max_tick_text_width as i32 - tick_size as i32 - 2, y: (y2 - y1) / 2 }); + title.draw(display)?; + } + } + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/curve.rs b/src/curve.rs index 4915622..0036141 100644 --- a/src/curve.rs +++ b/src/curve.rs @@ -1,53 +1,128 @@ use core::ops::{Range}; use crate::range_conv::Scalable; -use crate::drawable_curve::DrawableCurve; -use embedded_graphics::prelude::*; +use itertools::{Itertools, MinMaxResult::MinMax, MinMaxResult}; + +use embedded_graphics::drawable::{Drawable}; +use embedded_graphics::DrawTarget; +use embedded_graphics::geometry::Point; +use embedded_graphics::pixelcolor::{PixelColor}; +use embedded_graphics::primitives::{Line, Primitive}; +use embedded_graphics::style::PrimitiveStyle; + pub struct PlotPoint { pub x: i32, pub y: i32, } -pub struct Curve<'a>{ +pub struct Curve<'a> { points: &'a [PlotPoint], + pub x_range: Range, + pub y_range: Range, } impl<'a> Curve<'a> { - pub fn new(points: &'a [PlotPoint]) -> Curve { - Curve {points} + pub fn new(points: &'a [PlotPoint], x_range: Range, y_range: Range) -> Curve { + Curve { points, x_range, y_range } } - pub fn into_drawable_curve(self, - x_range: &'a Range, - y_range: &'a Range, - top_left : &'a Point, + pub fn from_data(points: &'a [PlotPoint]) -> Curve { + let x_range = match points + .iter() + .map(|p| (p.x)) + .minmax() { + MinMaxResult::NoElements => 0..0, + MinMaxResult::OneElement(v) => v..v, + MinMax(min, max) => min..max, + }; + + let y_range = match points.iter().map(|p| (p.y)).minmax() { + MinMaxResult::NoElements => 0..0, + MinMaxResult::OneElement(v) => v..v, + MinMax(min, max) => min..max, + }; + + Curve { points, x_range, y_range } + } + + pub fn into_drawable_curve(&self, + top_left: &'a Point, bottom_right: &'a Point, - color: C - ) -> DrawableCurve + 'a> + ) -> DrawableCurve + '_> where C: PixelColor { assert!(top_left.x < bottom_right.x); assert!(top_left.y < bottom_right.y); - assert!(!x_range.is_empty()); - assert!(!y_range.is_empty()); + assert!(!self.x_range.is_empty()); + assert!(!self.y_range.is_empty()); let it = self.points.iter() - .map(move |p| Point{ + .map(move |p| Point { x: p.x.scale_between_ranges( - x_range, - &Range{start: top_left.x, end: bottom_right.x} + &self.x_range, + &Range { start: top_left.x, end: bottom_right.x }, ), y: p.y.scale_between_ranges( - y_range, - &Range{start: bottom_right.y, end: top_left.y} + &self.y_range, + &Range { start: bottom_right.y, end: top_left.y }, ), }); - DrawableCurve::new(it,color) + DrawableCurve { + scaled_data: it, + color: None, + thickness: None, + } } } +pub struct DrawableCurve +{ + scaled_data: I, + color: Option, + thickness: Option, +} +impl DrawableCurve + where + C: PixelColor, + I: Iterator, +{ + pub fn set_color(mut self, color: C) -> DrawableCurve { + self.color = Some(color); + self + } + pub fn set_thickness(mut self, thickness: usize) -> DrawableCurve { + self.thickness = Some(thickness); + self + } +} + +impl Drawable for DrawableCurve + where C: PixelColor + Default, + I: Iterator, +{ + fn draw>(self, display: &mut D) -> Result<(), D::Error> { + let color = match self.color { + None => C::default(), + Some(c) => c, + }; + let thickness = match self.thickness { + None => 2, + Some(t) => t, + }; + let style = PrimitiveStyle::with_stroke(color, thickness as u32); + let mut iter = self.scaled_data.into_iter(); + let mut prev = iter.next().unwrap(); + for point in iter { + Line::new(prev, point) + .into_styled(style) + .draw(display)?; + prev = point; + } + Ok(()) + } +} #[cfg(test)] mod tests { diff --git a/src/drawable_curve.rs b/src/drawable_curve.rs deleted file mode 100644 index 2421ed9..0000000 --- a/src/drawable_curve.rs +++ /dev/null @@ -1,44 +0,0 @@ -use embedded_graphics::drawable::{Drawable}; -use embedded_graphics::DrawTarget; -use embedded_graphics::geometry::Point; -use embedded_graphics::pixelcolor::{PixelColor}; -use embedded_graphics::primitives::{Line, Primitive}; -use embedded_graphics::style::PrimitiveStyle; - -pub struct DrawableCurve - where - I: Iterator, -{ - scaled_data: I, - color: C, -} - -impl DrawableCurve - where - C: PixelColor, - I: Iterator, -{ - pub fn new(data: I,color : C) -> DrawableCurve { - DrawableCurve { - scaled_data: data, - color, - } - } -} -impl Drawable for DrawableCurve - where C: PixelColor, - I: Iterator, -{ - fn draw>(self, display: &mut D) -> Result<(), >::Error> { - let style = PrimitiveStyle::with_stroke(self.color,2); - let mut iter = self.scaled_data.into_iter(); - let mut prev = iter.next().unwrap(); - for point in iter { - Line::new(prev,point) - .into_styled(style) - .draw(display)?; - prev = point; - } - Ok(()) - } -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 62c1e0b..7b2b50c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] -pub mod curve; -pub mod plot; -mod drawable_curve; -mod range_conv; +pub mod curve; +pub mod axis; +pub mod single_plot; + +mod range_conv; \ No newline at end of file diff --git a/src/plot.rs b/src/plot.rs deleted file mode 100644 index 44d6f95..0000000 --- a/src/plot.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::curve::Curve; -use embedded_graphics::drawable::Drawable; -use embedded_graphics::DrawTarget; -use embedded_graphics::prelude::Point; -use embedded_graphics::pixelcolor::PixelColor; - -struct Plot<'a, C> - where - C: PixelColor -{ - curves: &'a [Curve<'a>], - color: C, -} - -impl<'a, C> Plot<'a, C> { - fn new(curves: &'a [Curve<'a>], color : C) -> Plot { - Plot{curves, color} - } -} - -impl Drawable for Plot<'_, C> - where - C: PixelColor -{ - fn draw>(self, display: &mut D) -> Result<(), >::Error> { - for curve in self.curves { - curve.into_drawable_curve( - &(0..50), - &(-10..10), - &Point{x: 10,y: 10}, - &Point{x: 470, y: 270}, - self.color - ).draw(display)?; - } - Ok(()) - } -} \ No newline at end of file diff --git a/src/range_conv.rs b/src/range_conv.rs index e30c8e2..1821970 100644 --- a/src/range_conv.rs +++ b/src/range_conv.rs @@ -4,7 +4,7 @@ pub trait Scalable where T: Copy + Add + Sub + Mul + Div, { - fn scale_between_ranges(&self,input_range: &Range, output_range: &Range) -> T; + fn scale_between_ranges(&self, input_range: &Range, output_range: &Range) -> T; } impl Scalable for T @@ -19,21 +19,20 @@ impl Scalable for T } - #[cfg(test)] mod tests { use core::ops::Range; use test_case::test_case; use crate::range_conv::Scalable; - #[test_case(0..10,0..10,5 => 5 ; "equal ranges")] - #[test_case(0..10,0..20,5 => 10 ; "double")] - #[test_case(0..20,0..10,10 => 5 ; "half")] - #[test_case(-20..20,0..10,0 => 5 ; "negative input range")] - #[test_case(0..10,-20..20,5 => 0 ; "negative output range")] - #[test_case(0..10,10..0,2 => 8 ; "reversing")] - #[test_case(-20..20,0..20,-10 => 5 ; "reversing negative range")] - fn convert(in_range: Range,out_range: Range,val: i32) -> i32 { - val.scale_between_ranges(&in_range,&out_range) + #[test_case(0..10, 0..10, 5 => 5; "equal ranges")] + #[test_case(0..10, 0..20, 5 => 10; "double")] + #[test_case(0..20, 0..10, 10 => 5; "half")] + #[test_case(- 20..20, 0..10, 0 => 5; "negative input range")] + #[test_case(0..10, - 20..20, 5 => 0; "negative output range")] + #[test_case(0..10, 10..0, 2 => 8; "reversing")] + #[test_case(- 20..20, 0..20, - 10 => 5; "reversing negative range")] + fn convert(in_range: Range, out_range: Range, val: i32) -> i32 { + val.scale_between_ranges(&in_range, &out_range) } } diff --git a/src/single_plot.rs b/src/single_plot.rs new file mode 100644 index 0000000..58f5ddc --- /dev/null +++ b/src/single_plot.rs @@ -0,0 +1,114 @@ +use crate::curve::Curve; +use embedded_graphics::drawable::Drawable; +use embedded_graphics::DrawTarget; +use embedded_graphics::prelude::Point; +use embedded_graphics::pixelcolor::PixelColor; +use crate::axis::{Scale, Placement, Axis}; +use embedded_graphics::style::TextStyleBuilder; +use embedded_graphics::fonts::Font6x8; + +pub struct SinglePlot<'a> { + curve: &'a Curve<'a>, + x_scale: Scale, + y_scale: Scale, +} + +impl<'a> SinglePlot<'a> { + pub fn new(curve: &'a Curve<'a>, x_scale: Scale, y_scale: Scale) -> SinglePlot { + SinglePlot { curve, x_scale, y_scale } + } + + pub fn into_drawable(self, top_left: Point, bottom_right: Point) -> DrawableSinglePlot<'a, C> { + DrawableSinglePlot { plot: self, color: None, text_color: None, axis_color: None, thickness: None, axis_thickness: None, top_left, bottom_right } + } +} + +pub struct DrawableSinglePlot<'a, C> + where + C: PixelColor + Default, +{ + plot: SinglePlot<'a>, + color: Option, + text_color: Option, + axis_color: Option, + thickness: Option, + axis_thickness: Option, + top_left: Point, + bottom_right: Point, +} + +impl<'a, C> DrawableSinglePlot<'a, C> + where + C: PixelColor + Default, +{ + pub fn set_color(mut self, color: C) -> DrawableSinglePlot<'a, C> { + self.color = Some(color); + self + } + + pub fn set_text_color(mut self, color: C) -> DrawableSinglePlot<'a, C> { + self.text_color = Some(color); + self + } + + pub fn set_axis_color(mut self, color: C) -> DrawableSinglePlot<'a, C> { + self.axis_color = Some(color); + self + } + + pub fn set_thickness(mut self, thickness: usize) -> DrawableSinglePlot<'a, C> { + self.thickness = Some(thickness); + self + } + + pub fn set_axis_thickness(mut self, thickness: usize) -> DrawableSinglePlot<'a, C> { + self.axis_thickness = Some(thickness); + self + } + +} + +impl<'a, C> Drawable for DrawableSinglePlot<'a, C> + where + C: PixelColor + Default, +{ + fn draw>(self, display: &mut D) -> Result<(), D::Error> { + let color = self.color.unwrap_or_default(); + let text_color = self.text_color.unwrap_or(color); + let axis_color = self.axis_color.unwrap_or(color); + let thickness = self.thickness.unwrap_or(2); + let axis_thickness = self.axis_thickness.unwrap_or(thickness); + + let text_style = TextStyleBuilder::new(Font6x8) + .text_color(text_color) + .build(); + + Axis::new( self.plot.curve.x_range.clone()) + .set_title("X") + .set_scale(self.plot.x_scale) + .into_drawable_axis(Placement::X { x1: self.top_left.x, x2: self.bottom_right.x, y: self.bottom_right.y }) + .set_color(axis_color) + .set_text_style(text_style) + .set_tick_size(2) + .set_thickness(axis_thickness) + .draw(display)?; + + Axis::new(self.plot.curve.y_range.clone()) + .set_title("Y") + .set_scale(self.plot.y_scale) + .into_drawable_axis(Placement::Y { y1: self.top_left.y, y2: self.bottom_right.y, x: self.top_left.x }) + .set_color(axis_color) + .set_text_style(text_style) + .set_tick_size(2) + .set_thickness(axis_thickness) + .draw(display)?; + + self.plot.curve.into_drawable_curve( + &self.top_left, + &self.bottom_right, + ).set_color(color) + .set_thickness(thickness) + .draw(display)?; + Ok(()) + } +} \ No newline at end of file