use crate::axes::{
style::AxisStyle,
ticks::LinearTickGenerator,
traits::{Axis, AxisRenderer, AxisValue, TickGenerator},
AxisConfig, AxisOrientation, AxisPosition,
};
use crate::error::ChartResult;
use crate::style::LineStyle;
use embedded_graphics::{
draw_target::DrawTarget,
prelude::*,
primitives::{Line, PrimitiveStyle, Rectangle},
};
#[derive(Debug, Clone)]
pub struct LinearAxis<T, C: PixelColor> {
config: AxisConfig<T>,
tick_generator: LinearTickGenerator,
style: AxisStyle<C>,
renderer: DefaultAxisRenderer<C>,
}
#[derive(Debug, Clone)]
pub struct DefaultAxisRenderer<C: PixelColor> {
_phantom: core::marker::PhantomData<C>,
}
impl<C: PixelColor> DefaultAxisRenderer<C> {
pub fn new() -> Self {
Self {
_phantom: core::marker::PhantomData,
}
}
}
impl<C: PixelColor> Default for DefaultAxisRenderer<C> {
fn default() -> Self {
Self::new()
}
}
impl<T, C> LinearAxis<T, C>
where
T: AxisValue,
C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>,
{
pub fn new(min: T, max: T, orientation: AxisOrientation, position: AxisPosition) -> Self {
Self {
config: AxisConfig::new(min, max, orientation, position),
tick_generator: LinearTickGenerator::new(5),
style: AxisStyle::new(),
renderer: DefaultAxisRenderer::new(),
}
}
pub fn with_tick_generator(mut self, generator: LinearTickGenerator) -> Self {
self.tick_generator = generator;
self
}
pub fn with_style(mut self, style: AxisStyle<C>) -> Self {
self.style = style;
self
}
pub fn with_range(mut self, min: T, max: T) -> Self {
self.config.min = min;
self.config.max = max;
self
}
pub fn show_line(mut self, show: bool) -> Self {
self.config.show_line = show;
self
}
pub fn show_ticks(mut self, show: bool) -> Self {
self.config.show_ticks = show;
self
}
pub fn show_labels(mut self, show: bool) -> Self {
self.config.show_labels = show;
self
}
pub fn show_grid(mut self, show: bool) -> Self {
self.config.show_grid = show;
self
}
fn calculate_axis_line(&self, viewport: Rectangle) -> (Point, Point) {
match (self.config.orientation, self.config.position) {
(AxisOrientation::Horizontal, AxisPosition::Bottom) => {
let y = viewport.top_left.y + viewport.size.height as i32 - 1;
(
Point::new(viewport.top_left.x, y),
Point::new(viewport.top_left.x + viewport.size.width as i32 - 1, y),
)
}
(AxisOrientation::Horizontal, AxisPosition::Top) => {
let y = viewport.top_left.y;
(
Point::new(viewport.top_left.x, y),
Point::new(viewport.top_left.x + viewport.size.width as i32 - 1, y),
)
}
(AxisOrientation::Vertical, AxisPosition::Left) => {
let x = viewport.top_left.x;
(
Point::new(x, viewport.top_left.y),
Point::new(x, viewport.top_left.y + viewport.size.height as i32 - 1),
)
}
(AxisOrientation::Vertical, AxisPosition::Right) => {
let x = viewport.top_left.x + viewport.size.width as i32 - 1;
(
Point::new(x, viewport.top_left.y),
Point::new(x, viewport.top_left.y + viewport.size.height as i32 - 1),
)
}
(AxisOrientation::Horizontal, AxisPosition::Left)
| (AxisOrientation::Horizontal, AxisPosition::Right) => {
let y = viewport.top_left.y + viewport.size.height as i32 - 1;
(
Point::new(viewport.top_left.x, y),
Point::new(viewport.top_left.x + viewport.size.width as i32 - 1, y),
)
}
(AxisOrientation::Vertical, AxisPosition::Bottom)
| (AxisOrientation::Vertical, AxisPosition::Top) => {
let x = viewport.top_left.x;
(
Point::new(x, viewport.top_left.y),
Point::new(x, viewport.top_left.y + viewport.size.height as i32 - 1),
)
}
}
}
fn calculate_tick_position(&self, value: T, viewport: Rectangle) -> Point {
let screen_coord = self.transform_value(value, viewport);
match (self.config.orientation, self.config.position) {
(AxisOrientation::Horizontal, AxisPosition::Bottom) => Point::new(
screen_coord,
viewport.top_left.y + viewport.size.height as i32 - 1,
),
(AxisOrientation::Horizontal, AxisPosition::Top) => {
Point::new(screen_coord, viewport.top_left.y)
}
(AxisOrientation::Vertical, AxisPosition::Left) => {
Point::new(viewport.top_left.x, screen_coord)
}
(AxisOrientation::Vertical, AxisPosition::Right) => Point::new(
viewport.top_left.x + viewport.size.width as i32 - 1,
screen_coord,
),
(AxisOrientation::Horizontal, AxisPosition::Left)
| (AxisOrientation::Horizontal, AxisPosition::Right) => {
Point::new(
screen_coord,
viewport.top_left.y + viewport.size.height as i32 - 1,
)
}
(AxisOrientation::Vertical, AxisPosition::Bottom)
| (AxisOrientation::Vertical, AxisPosition::Top) => {
Point::new(viewport.top_left.x, screen_coord)
}
}
}
fn calculate_grid_line(
&self,
value: T,
viewport: Rectangle,
chart_area: Rectangle,
) -> (Point, Point) {
let tick_pos = self.calculate_tick_position(value, viewport);
match self.config.orientation {
AxisOrientation::Horizontal => {
(
Point::new(tick_pos.x, chart_area.top_left.y),
Point::new(
tick_pos.x,
chart_area.top_left.y + chart_area.size.height as i32 - 1,
),
)
}
AxisOrientation::Vertical => {
(
Point::new(chart_area.top_left.x, tick_pos.y),
Point::new(
chart_area.top_left.x + chart_area.size.width as i32 - 1,
tick_pos.y,
),
)
}
}
}
pub fn draw_grid_lines<D>(
&self,
viewport: Rectangle,
chart_area: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
if !self.config.show_grid || self.style.grid_lines.is_none() {
return Ok(());
}
let grid_style = self.style.grid_lines.as_ref().unwrap();
let ticks = self
.tick_generator
.generate_ticks(self.config.min, self.config.max, 20);
for tick in &ticks {
if tick.is_major {
let (start, end) = self.calculate_grid_line(tick.value, viewport, chart_area);
self.renderer
.draw_grid_line(start, end, grid_style, target)?;
}
}
Ok(())
}
pub fn draw_axis_only<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
if self.config.show_line {
let (start, end) = self.calculate_axis_line(viewport);
self.renderer
.draw_axis_line(start, end, &self.style.axis_line, target)?;
}
let ticks = self
.tick_generator
.generate_ticks(self.config.min, self.config.max, 50);
if self.config.show_ticks {
for tick in &ticks {
let tick_pos = self.calculate_tick_position(tick.value, viewport);
let tick_style = if tick.is_major {
&self.style.major_ticks
} else {
&self.style.minor_ticks
};
if tick_style.visible {
self.renderer.draw_tick(
tick_pos,
tick_style.length,
self.config.orientation,
&tick_style.line,
target,
)?;
}
}
}
if self.config.show_labels && self.style.labels.visible {
for tick in &ticks {
if tick.is_major && tick.label.is_some() {
let tick_pos = self.calculate_tick_position(tick.value, viewport);
let label_pos = self.calculate_label_position(tick_pos);
self.renderer.draw_label(
tick.label.as_ref().unwrap().as_str(),
label_pos,
target,
)?;
}
}
}
Ok(())
}
}
impl<T, C> Axis<T, C> for LinearAxis<T, C>
where
T: AxisValue,
C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>,
{
type TickGenerator = LinearTickGenerator;
type Style = AxisStyle<C>;
fn min(&self) -> T {
self.config.min
}
fn max(&self) -> T {
self.config.max
}
fn orientation(&self) -> AxisOrientation {
self.config.orientation
}
fn position(&self) -> AxisPosition {
self.config.position
}
fn transform_value(&self, value: T, viewport: Rectangle) -> i32 {
let min_f32 = self.config.min.to_f32();
let max_f32 = self.config.max.to_f32();
let value_f32 = value.to_f32();
if max_f32 <= min_f32 {
return match self.config.orientation {
AxisOrientation::Horizontal => viewport.top_left.x + viewport.size.width as i32 / 2,
AxisOrientation::Vertical => viewport.top_left.y + viewport.size.height as i32 / 2,
};
}
let normalized = (value_f32 - min_f32) / (max_f32 - min_f32);
match self.config.orientation {
AxisOrientation::Horizontal => {
viewport.top_left.x + (normalized * (viewport.size.width as f32 - 1.0)) as i32
}
AxisOrientation::Vertical => {
viewport.top_left.y + viewport.size.height as i32
- 1
- (normalized * (viewport.size.height as f32 - 1.0)) as i32
}
}
}
fn inverse_transform(&self, coordinate: i32, viewport: Rectangle) -> T {
let min_f32 = self.config.min.to_f32();
let max_f32 = self.config.max.to_f32();
let normalized = match self.config.orientation {
AxisOrientation::Horizontal => {
(coordinate - viewport.top_left.x) as f32 / (viewport.size.width as f32 - 1.0)
}
AxisOrientation::Vertical => {
1.0 - ((coordinate - viewport.top_left.y) as f32
/ (viewport.size.height as f32 - 1.0))
}
};
let value_f32 = min_f32 + normalized * (max_f32 - min_f32);
T::from_f32(value_f32)
}
fn tick_generator(&self) -> &Self::TickGenerator {
&self.tick_generator
}
fn style(&self) -> &Self::Style {
&self.style
}
fn draw<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
if self.config.show_line {
let (start, end) = self.calculate_axis_line(viewport);
self.renderer
.draw_axis_line(start, end, &self.style.axis_line, target)?;
}
let ticks = self
.tick_generator
.generate_ticks(self.config.min, self.config.max, 50);
if self.config.show_ticks {
for tick in &ticks {
let tick_pos = self.calculate_tick_position(tick.value, viewport);
let tick_style = if tick.is_major {
&self.style.major_ticks
} else {
&self.style.minor_ticks
};
if tick_style.visible {
self.renderer.draw_tick(
tick_pos,
tick_style.length,
self.config.orientation,
&tick_style.line,
target,
)?;
}
}
}
if self.config.show_labels && self.style.labels.visible {
for tick in &ticks {
if tick.is_major && tick.label.is_some() {
let tick_pos = self.calculate_tick_position(tick.value, viewport);
let label_pos = self.calculate_label_position(tick_pos);
self.renderer.draw_label(
tick.label.as_ref().unwrap().as_str(),
label_pos,
target,
)?;
}
}
}
Ok(())
}
fn required_space(&self) -> u32 {
let mut space = 0;
if self.config.show_line {
space += self.style.axis_line.width;
}
if self.config.show_ticks {
let major_tick_space = if self.style.major_ticks.visible {
self.style.major_ticks.length
} else {
0
};
let minor_tick_space = if self.style.minor_ticks.visible {
self.style.minor_ticks.length
} else {
0
};
space += major_tick_space.max(minor_tick_space);
}
if self.config.show_labels && self.style.labels.visible {
space += self.style.label_offset + self.style.labels.font_size;
}
space
}
}
impl<T, C> LinearAxis<T, C>
where
T: AxisValue,
C: PixelColor,
{
fn calculate_label_position(&self, tick_pos: Point) -> Point {
match (self.config.orientation, self.config.position) {
(AxisOrientation::Horizontal, AxisPosition::Bottom) => {
Point::new(tick_pos.x, tick_pos.y + self.style.label_offset as i32)
}
(AxisOrientation::Horizontal, AxisPosition::Top) => {
Point::new(tick_pos.x, tick_pos.y - self.style.label_offset as i32)
}
(AxisOrientation::Vertical, AxisPosition::Left) => {
Point::new(tick_pos.x - self.style.label_offset as i32, tick_pos.y)
}
(AxisOrientation::Vertical, AxisPosition::Right) => {
Point::new(tick_pos.x + self.style.label_offset as i32, tick_pos.y)
}
(AxisOrientation::Horizontal, AxisPosition::Left)
| (AxisOrientation::Horizontal, AxisPosition::Right) => {
Point::new(tick_pos.x, tick_pos.y + self.style.label_offset as i32)
}
(AxisOrientation::Vertical, AxisPosition::Bottom)
| (AxisOrientation::Vertical, AxisPosition::Top) => {
Point::new(tick_pos.x - self.style.label_offset as i32, tick_pos.y)
}
}
}
}
impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> AxisRenderer<C>
for DefaultAxisRenderer<C>
{
fn draw_axis_line<D>(
&self,
start: Point,
end: Point,
style: &LineStyle<C>,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
Line::new(start, end)
.into_styled(PrimitiveStyle::with_stroke(style.color, style.width))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
Ok(())
}
fn draw_tick<D>(
&self,
position: Point,
length: u32,
orientation: AxisOrientation,
style: &LineStyle<C>,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
let (start, end) = match orientation {
AxisOrientation::Horizontal => {
(
Point::new(position.x, position.y),
Point::new(position.x, position.y + length as i32),
)
}
AxisOrientation::Vertical => {
(
Point::new(position.x, position.y),
Point::new(position.x - length as i32, position.y),
)
}
};
Line::new(start, end)
.into_styled(PrimitiveStyle::with_stroke(style.color, style.width))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
Ok(())
}
fn draw_grid_line<D>(
&self,
start: Point,
end: Point,
style: &LineStyle<C>,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
Line::new(start, end)
.into_styled(PrimitiveStyle::with_stroke(style.color, style.width))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
Ok(())
}
fn draw_label<D>(&self, text: &str, position: Point, target: &mut D) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
use embedded_graphics::{
mono_font::{ascii::FONT_6X10, MonoTextStyle},
text::{Alignment, Text},
};
let text_color = embedded_graphics::pixelcolor::Rgb565::BLACK.into();
let text_style = MonoTextStyle::new(&FONT_6X10, text_color);
Text::with_alignment(text, position, text_style, Alignment::Center)
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use embedded_graphics::pixelcolor::Rgb565;
#[test]
fn test_linear_axis_creation() {
let axis: LinearAxis<f32, Rgb565> =
LinearAxis::new(0.0, 10.0, AxisOrientation::Horizontal, AxisPosition::Bottom);
assert_eq!(axis.min(), 0.0);
assert_eq!(axis.max(), 10.0);
assert_eq!(axis.orientation(), AxisOrientation::Horizontal);
assert_eq!(axis.position(), AxisPosition::Bottom);
}
#[test]
fn test_value_transformation() {
let axis: LinearAxis<f32, Rgb565> =
LinearAxis::new(0.0, 10.0, AxisOrientation::Horizontal, AxisPosition::Bottom);
let viewport = Rectangle::new(Point::new(0, 0), Size::new(100, 50));
assert_eq!(axis.transform_value(0.0, viewport), 0);
assert_eq!(axis.transform_value(10.0, viewport), 99);
assert_eq!(axis.transform_value(5.0, viewport), 49);
assert!((axis.inverse_transform(0, viewport) - 0.0).abs() < 0.1);
assert!((axis.inverse_transform(99, viewport) - 10.0).abs() < 0.1);
}
#[test]
fn test_axis_builder_pattern() {
let axis: LinearAxis<f32, Rgb565> =
LinearAxis::new(0.0, 10.0, AxisOrientation::Vertical, AxisPosition::Left)
.show_grid(true)
.show_labels(false)
.with_tick_generator(LinearTickGenerator::new(8));
assert!(axis.config.show_grid);
assert!(!axis.config.show_labels);
}
}