use crate::core::error::Result;
use crate::core::transform::CoordinateTransform;
use crate::render::{Color, SkiaRenderer, Theme};
#[derive(Debug, Clone, Copy)]
pub struct PlotArea {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub x_min: f64,
pub x_max: f64,
pub y_min: f64,
pub y_max: f64,
}
impl PlotArea {
pub fn new(
x: f32,
y: f32,
width: f32,
height: f32,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
) -> Self {
Self {
x,
y,
width,
height,
x_min,
x_max,
y_min,
y_max,
}
}
#[inline]
pub fn to_transform(self) -> CoordinateTransform {
CoordinateTransform::from_plot_area(
self.x,
self.y,
self.width,
self.height,
self.x_min,
self.x_max,
self.y_min,
self.y_max,
)
}
#[inline]
pub fn data_to_screen(&self, data_x: f64, data_y: f64) -> (f32, f32) {
self.to_transform().data_to_screen(data_x, data_y)
}
#[inline]
pub fn screen_to_data(&self, screen_x: f32, screen_y: f32) -> (f64, f64) {
self.to_transform().screen_to_data(screen_x, screen_y)
}
#[inline]
pub fn contains_data(&self, data_x: f64, data_y: f64) -> bool {
self.to_transform().contains_data(data_x, data_y)
}
pub fn center(&self) -> (f32, f32) {
self.to_transform().screen_center()
}
pub fn data_center(&self) -> (f64, f64) {
self.to_transform().data_center()
}
}
pub trait PlotConfig: Default + Clone {}
pub trait PlotCompute {
type Input<'a>;
type Config: PlotConfig;
type Output: PlotData;
fn compute(input: Self::Input<'_>, config: &Self::Config) -> Result<Self::Output>;
}
pub trait PlotData {
fn data_bounds(&self) -> ((f64, f64), (f64, f64));
fn is_empty(&self) -> bool;
}
pub trait PlotRender: PlotData {
fn render(
&self,
renderer: &mut SkiaRenderer,
area: &PlotArea,
theme: &Theme,
color: Color,
) -> Result<()>;
fn render_styled(
&self,
renderer: &mut SkiaRenderer,
area: &PlotArea,
theme: &Theme,
color: Color,
_alpha: f32,
_line_width: Option<f32>,
) -> Result<()> {
self.render(renderer, area, theme, color)
}
}
pub trait StyledShape {
fn fill_color(&self) -> Color;
fn edge_color(&self) -> Option<Color>;
fn edge_width(&self) -> f32;
fn alpha(&self) -> f32;
fn resolved_edge_color(&self) -> Color {
self.edge_color()
.unwrap_or_else(|| self.fill_color().darken(0.3))
}
fn fill_color_with_alpha(&self) -> Color {
self.fill_color().with_alpha(self.alpha())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plot_area_creation() {
let area = PlotArea::new(100.0, 50.0, 600.0, 400.0, 0.0, 10.0, 0.0, 100.0);
assert_eq!(area.x, 100.0);
assert_eq!(area.y, 50.0);
assert_eq!(area.width, 600.0);
assert_eq!(area.height, 400.0);
assert_eq!(area.x_min, 0.0);
assert_eq!(area.x_max, 10.0);
assert_eq!(area.y_min, 0.0);
assert_eq!(area.y_max, 100.0);
}
#[test]
fn test_plot_area_data_to_screen() {
let area = PlotArea::new(100.0, 50.0, 600.0, 400.0, 0.0, 10.0, 0.0, 100.0);
let (sx, sy) = area.data_to_screen(0.0, 0.0);
assert!((sx - 100.0).abs() < 0.01);
assert!((sy - 450.0).abs() < 0.01);
let (sx, sy) = area.data_to_screen(10.0, 100.0);
assert!((sx - 700.0).abs() < 0.01); assert!((sy - 50.0).abs() < 0.01);
let (sx, sy) = area.data_to_screen(5.0, 50.0);
assert!((sx - 400.0).abs() < 0.01); assert!((sy - 250.0).abs() < 0.01); }
#[test]
fn test_plot_area_screen_to_data() {
let area = PlotArea::new(100.0, 50.0, 600.0, 400.0, 0.0, 10.0, 0.0, 100.0);
let (data_x, data_y) = (5.0, 50.0);
let (sx, sy) = area.data_to_screen(data_x, data_y);
let (rx, ry) = area.screen_to_data(sx, sy);
assert!((rx - data_x).abs() < 0.01);
assert!((ry - data_y).abs() < 0.01);
}
#[test]
fn test_plot_area_contains_data() {
let area = PlotArea::new(100.0, 50.0, 600.0, 400.0, 0.0, 10.0, 0.0, 100.0);
assert!(area.contains_data(5.0, 50.0)); assert!(area.contains_data(0.0, 0.0)); assert!(area.contains_data(10.0, 100.0)); assert!(!area.contains_data(-1.0, 50.0)); assert!(!area.contains_data(5.0, 150.0)); }
#[test]
fn test_plot_area_center() {
let area = PlotArea::new(100.0, 50.0, 600.0, 400.0, 0.0, 10.0, 0.0, 100.0);
let (cx, cy) = area.center();
assert!((cx - 400.0).abs() < 0.01);
assert!((cy - 250.0).abs() < 0.01);
let (dx, dy) = area.data_center();
assert!((dx - 5.0).abs() < 0.01);
assert!((dy - 50.0).abs() < 0.01);
}
#[test]
fn test_plot_area_zero_range() {
let area = PlotArea::new(100.0, 50.0, 600.0, 400.0, 5.0, 5.0, 50.0, 50.0);
let (sx, sy) = area.data_to_screen(5.0, 50.0);
assert!((sx - 400.0).abs() < 0.01); assert!((sy - 250.0).abs() < 0.01); }
struct TestShape {
fill: Color,
edge: Option<Color>,
edge_width: f32,
alpha: f32,
}
impl StyledShape for TestShape {
fn fill_color(&self) -> Color {
self.fill
}
fn edge_color(&self) -> Option<Color> {
self.edge
}
fn edge_width(&self) -> f32 {
self.edge_width
}
fn alpha(&self) -> f32 {
self.alpha
}
}
#[test]
fn test_styled_shape_explicit_edge() {
let shape = TestShape {
fill: Color::BLUE,
edge: Some(Color::RED),
edge_width: 1.5,
alpha: 0.8,
};
assert_eq!(shape.fill_color(), Color::BLUE);
assert_eq!(shape.edge_color(), Some(Color::RED));
assert_eq!(shape.resolved_edge_color(), Color::RED);
assert_eq!(shape.edge_width(), 1.5);
assert_eq!(shape.alpha(), 0.8);
}
#[test]
fn test_styled_shape_auto_edge() {
let shape = TestShape {
fill: Color::new(100, 150, 200),
edge: None,
edge_width: 0.8,
alpha: 1.0,
};
let edge = shape.resolved_edge_color();
assert_eq!(edge.r, 70); assert_eq!(edge.g, 105); assert_eq!(edge.b, 140); }
#[test]
fn test_styled_shape_fill_with_alpha() {
let shape = TestShape {
fill: Color::new(100, 150, 200),
edge: None,
edge_width: 0.8,
alpha: 0.5,
};
let fill_with_alpha = shape.fill_color_with_alpha();
assert_eq!(fill_with_alpha.r, 100);
assert_eq!(fill_with_alpha.g, 150);
assert_eq!(fill_with_alpha.b, 200);
assert_eq!(fill_with_alpha.a, 127); }
}