use crate::error::ChartResult;
use crate::grid::style::GridStyle;
use crate::grid::traits::{DefaultGridRenderer, Grid, GridOrientation, GridRenderer};
use embedded_graphics::{prelude::*, primitives::Rectangle};
use crate::axes::traits::TickGenerator;
use crate::grid::traits::TickAlignedGrid;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GridSpacing {
Pixels(u32),
DataUnits(f32),
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridType {
Linear,
TickBased,
Custom,
}
#[derive(Debug, Clone)]
pub struct LinearGrid<C: PixelColor> {
orientation: GridOrientation,
spacing: GridSpacing,
style: GridStyle<C>,
visible: bool,
renderer: DefaultGridRenderer,
}
impl<C: PixelColor> LinearGrid<C> {
pub fn new(orientation: GridOrientation, spacing: GridSpacing) -> Self
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
Self {
orientation,
spacing,
style: GridStyle::default(),
visible: true,
renderer: DefaultGridRenderer,
}
}
pub fn horizontal(spacing: GridSpacing) -> Self
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
Self::new(GridOrientation::Horizontal, spacing)
}
pub fn vertical(spacing: GridSpacing) -> Self
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
Self::new(GridOrientation::Vertical, spacing)
}
pub fn with_style(mut self, style: GridStyle<C>) -> Self {
self.style = style;
self
}
pub fn with_visibility(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
fn calculate_pixel_spacing(&self, viewport: Rectangle) -> u32 {
match self.spacing {
GridSpacing::Pixels(pixels) => pixels,
GridSpacing::DataUnits(_) => {
match self.orientation {
GridOrientation::Horizontal => viewport.size.height / 10,
GridOrientation::Vertical => viewport.size.width / 10,
}
}
GridSpacing::Auto => match self.orientation {
GridOrientation::Horizontal => (viewport.size.height / 8).max(20),
GridOrientation::Vertical => (viewport.size.width / 8).max(20),
},
}
}
}
impl<C: PixelColor + 'static> Grid<C> for LinearGrid<C> {
fn draw<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
if !self.visible || !self.style.visibility.any_visible() {
return Ok(());
}
let positions = self.calculate_positions(viewport);
for &pos in positions.iter() {
let (start, end) = match self.orientation {
GridOrientation::Horizontal => (
Point::new(viewport.top_left.x, pos),
Point::new(viewport.top_left.x + viewport.size.width as i32, pos),
),
GridOrientation::Vertical => (
Point::new(pos, viewport.top_left.y),
Point::new(pos, viewport.top_left.y + viewport.size.height as i32),
),
};
if self.style.major.enabled && self.style.visibility.major {
self.renderer.draw_major_line(
start,
end,
&self.style.major.line.line_style,
target,
)?;
}
}
Ok(())
}
fn orientation(&self) -> GridOrientation {
self.orientation
}
fn is_visible(&self) -> bool {
self.visible
}
fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
fn style(&self) -> &GridStyle<C> {
&self.style
}
fn set_style(&mut self, style: GridStyle<C>) {
self.style = style;
}
fn calculate_positions(&self, viewport: Rectangle) -> heapless::Vec<i32, 64> {
let mut positions = heapless::Vec::new();
let spacing = self.calculate_pixel_spacing(viewport);
match self.orientation {
GridOrientation::Horizontal => {
let mut y = viewport.top_left.y + spacing as i32;
while y < viewport.top_left.y + viewport.size.height as i32 {
let _ = positions.push(y);
y += spacing as i32;
}
}
GridOrientation::Vertical => {
let mut x = viewport.top_left.x + spacing as i32;
while x < viewport.top_left.x + viewport.size.width as i32 {
let _ = positions.push(x);
x += spacing as i32;
}
}
}
positions
}
fn spacing(&self) -> f32 {
match self.spacing {
GridSpacing::Pixels(pixels) => pixels as f32,
GridSpacing::DataUnits(units) => units,
GridSpacing::Auto => 1.0,
}
}
fn set_spacing(&mut self, spacing: f32) {
self.spacing = GridSpacing::DataUnits(spacing);
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
}
#[derive(Debug, Clone)]
pub struct TickBasedGrid<T, C>
where
T: Copy + PartialOrd + core::fmt::Display,
C: PixelColor,
{
orientation: GridOrientation,
style: GridStyle<C>,
visible: bool,
major_ticks_only: bool,
renderer: DefaultGridRenderer,
_phantom: core::marker::PhantomData<T>,
}
impl<T, C> TickBasedGrid<T, C>
where
T: Copy + PartialOrd + core::fmt::Display,
C: PixelColor,
{
pub fn new(orientation: GridOrientation) -> Self
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
Self {
orientation,
style: GridStyle::default(),
visible: true,
major_ticks_only: false,
renderer: DefaultGridRenderer,
_phantom: core::marker::PhantomData,
}
}
pub fn horizontal() -> Self
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
Self::new(GridOrientation::Horizontal)
}
pub fn vertical() -> Self
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
Self::new(GridOrientation::Vertical)
}
pub fn with_style(mut self, style: GridStyle<C>) -> Self {
self.style = style;
self
}
pub fn with_major_ticks_only(mut self, major_only: bool) -> Self {
self.major_ticks_only = major_only;
self
}
pub fn is_major_ticks_only(&self) -> bool {
self.major_ticks_only
}
pub fn set_major_ticks_only(&mut self, major_only: bool) {
self.major_ticks_only = major_only;
}
}
impl<T, C> Grid<C> for TickBasedGrid<T, C>
where
T: Copy + PartialOrd + core::fmt::Display + 'static,
C: PixelColor + 'static,
{
fn draw<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
if !self.visible || !self.style.visibility.any_visible() {
return Ok(());
}
let positions = self.calculate_positions(viewport);
for &pos in positions.iter() {
let (start, end) = match self.orientation {
GridOrientation::Horizontal => (
Point::new(viewport.top_left.x, pos),
Point::new(viewport.top_left.x + viewport.size.width as i32, pos),
),
GridOrientation::Vertical => (
Point::new(pos, viewport.top_left.y),
Point::new(pos, viewport.top_left.y + viewport.size.height as i32),
),
};
if self.style.major.enabled && self.style.visibility.major {
self.renderer.draw_major_line(
start,
end,
&self.style.major.line.line_style,
target,
)?;
}
}
Ok(())
}
fn orientation(&self) -> GridOrientation {
self.orientation
}
fn is_visible(&self) -> bool {
self.visible
}
fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
fn style(&self) -> &GridStyle<C> {
&self.style
}
fn set_style(&mut self, style: GridStyle<C>) {
self.style = style;
}
fn calculate_positions(&self, viewport: Rectangle) -> heapless::Vec<i32, 64> {
let mut positions = heapless::Vec::new();
let spacing = match self.orientation {
GridOrientation::Horizontal => viewport.size.height / 8,
GridOrientation::Vertical => viewport.size.width / 8,
};
match self.orientation {
GridOrientation::Horizontal => {
let mut y = viewport.top_left.y + spacing as i32;
while y < viewport.top_left.y + viewport.size.height as i32 {
let _ = positions.push(y);
y += spacing as i32;
}
}
GridOrientation::Vertical => {
let mut x = viewport.top_left.x + spacing as i32;
while x < viewport.top_left.x + viewport.size.width as i32 {
let _ = positions.push(x);
x += spacing as i32;
}
}
}
positions
}
fn spacing(&self) -> f32 {
1.0 }
fn set_spacing(&mut self, _spacing: f32) {
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
}
impl<T, C> TickAlignedGrid<T, C> for TickBasedGrid<T, C>
where
T: crate::axes::traits::AxisValue + 'static,
C: PixelColor + 'static,
{
fn draw_with_axis<D, A>(&self, viewport: Rectangle, axis: &A, target: &mut D) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
A: crate::axes::traits::Axis<T, C>,
{
if !self.visible || !self.style.visibility.any_visible() {
return Ok(());
}
let positions = self.calculate_tick_positions(viewport, axis);
for &pos in positions.iter() {
let (start, end) = match self.orientation {
GridOrientation::Horizontal => (
Point::new(viewport.top_left.x, pos),
Point::new(viewport.top_left.x + viewport.size.width as i32, pos),
),
GridOrientation::Vertical => (
Point::new(pos, viewport.top_left.y),
Point::new(pos, viewport.top_left.y + viewport.size.height as i32),
),
};
if self.style.major.enabled && self.style.visibility.major {
self.renderer.draw_major_line(
start,
end,
&self.style.major.line.line_style,
target,
)?;
}
}
Ok(())
}
fn calculate_tick_positions<A>(&self, viewport: Rectangle, axis: &A) -> heapless::Vec<i32, 64>
where
A: crate::axes::traits::Axis<T, C>,
{
let mut positions = heapless::Vec::new();
let ticks = axis
.tick_generator()
.generate_ticks(axis.min(), axis.max(), 16);
for tick in ticks.iter() {
if self.major_ticks_only && !tick.is_major {
continue;
}
let screen_pos = axis.transform_value(tick.value, viewport);
let _ = positions.push(screen_pos);
}
positions
}
fn set_major_ticks_only(&mut self, major_only: bool) {
self.major_ticks_only = major_only;
}
fn is_major_ticks_only(&self) -> bool {
self.major_ticks_only
}
}
#[derive(Debug, Clone)]
pub struct CustomGrid<C: PixelColor> {
orientation: GridOrientation,
positions: heapless::Vec<i32, 64>,
style: GridStyle<C>,
visible: bool,
renderer: DefaultGridRenderer,
}
impl<C: PixelColor> CustomGrid<C> {
pub fn new(orientation: GridOrientation) -> Self
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
Self {
orientation,
positions: heapless::Vec::new(),
style: GridStyle::default(),
visible: true,
renderer: DefaultGridRenderer,
}
}
pub fn horizontal() -> Self
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
Self::new(GridOrientation::Horizontal)
}
pub fn vertical() -> Self
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
Self::new(GridOrientation::Vertical)
}
pub fn add_line(&mut self, position: i32) -> Result<(), crate::error::DataError> {
self.positions
.push(position)
.map_err(|_| crate::error::DataError::buffer_full("add grid line", 32))
}
pub fn add_lines(&mut self, positions: &[i32]) {
for &pos in positions {
let _ = self.add_line(pos);
}
}
pub fn clear_lines(&mut self) {
self.positions.clear();
}
pub fn with_style(mut self, style: GridStyle<C>) -> Self {
self.style = style;
self
}
pub fn with_lines(mut self, positions: &[i32]) -> Self {
self.add_lines(positions);
self
}
}
impl<C: PixelColor + 'static> Grid<C> for CustomGrid<C> {
fn draw<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
if !self.visible || !self.style.visibility.any_visible() {
return Ok(());
}
for &pos in self.positions.iter() {
let (start, end) = match self.orientation {
GridOrientation::Horizontal => (
Point::new(viewport.top_left.x, pos),
Point::new(viewport.top_left.x + viewport.size.width as i32, pos),
),
GridOrientation::Vertical => (
Point::new(pos, viewport.top_left.y),
Point::new(pos, viewport.top_left.y + viewport.size.height as i32),
),
};
if self.style.major.enabled && self.style.visibility.major {
self.renderer.draw_major_line(
start,
end,
&self.style.major.line.line_style,
target,
)?;
}
}
Ok(())
}
fn orientation(&self) -> GridOrientation {
self.orientation
}
fn is_visible(&self) -> bool {
self.visible
}
fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
fn style(&self) -> &GridStyle<C> {
&self.style
}
fn set_style(&mut self, style: GridStyle<C>) {
self.style = style;
}
fn calculate_positions(&self, _viewport: Rectangle) -> heapless::Vec<i32, 64> {
self.positions.clone()
}
fn spacing(&self) -> f32 {
if self.positions.len() < 2 {
return 1.0;
}
let mut total_spacing = 0;
for window in self.positions.windows(2) {
if let [a, b] = window {
total_spacing += (b - a).abs();
}
}
total_spacing as f32 / (self.positions.len() - 1) as f32
}
fn set_spacing(&mut self, _spacing: f32) {
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use embedded_graphics::pixelcolor::Rgb565;
#[test]
fn test_linear_grid_creation() {
let grid: LinearGrid<Rgb565> = LinearGrid::horizontal(GridSpacing::Pixels(20));
assert_eq!(grid.orientation(), GridOrientation::Horizontal);
assert!(grid.is_visible());
}
#[test]
fn test_tick_based_grid_creation() {
let grid: TickBasedGrid<f32, Rgb565> = TickBasedGrid::vertical();
assert_eq!(grid.orientation(), GridOrientation::Vertical);
assert!(!grid.is_major_ticks_only());
}
#[test]
fn test_custom_grid_creation() {
let mut grid: CustomGrid<Rgb565> = CustomGrid::horizontal();
assert_eq!(grid.orientation(), GridOrientation::Horizontal);
grid.add_line(100).unwrap();
grid.add_line(200).unwrap();
let positions =
grid.calculate_positions(Rectangle::new(Point::zero(), Size::new(400, 300)));
assert_eq!(positions.len(), 2);
}
#[test]
fn test_grid_spacing() {
assert_eq!(GridSpacing::Pixels(20), GridSpacing::Pixels(20));
assert_ne!(GridSpacing::Pixels(20), GridSpacing::Pixels(30));
assert_ne!(GridSpacing::Pixels(20), GridSpacing::Auto);
}
#[test]
fn test_grid_type() {
assert_eq!(GridType::Linear, GridType::Linear);
assert_ne!(GridType::Linear, GridType::TickBased);
}
}