use crate::render::{Color, LineStyle};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextAlign {
Left,
#[default]
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextVAlign {
Top,
#[default]
Middle,
Bottom,
}
#[derive(Debug, Clone)]
pub struct TextStyle {
pub font_size: f32,
pub color: Color,
pub align: TextAlign,
pub valign: TextVAlign,
pub rotation: f32,
pub background: Option<Color>,
pub padding: f32,
pub border_color: Option<Color>,
pub border_width: f32,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
font_size: 10.0,
color: Color::BLACK,
align: TextAlign::Center,
valign: TextVAlign::Middle,
rotation: 0.0,
background: None,
padding: 2.0,
border_color: None,
border_width: 1.0,
}
}
}
impl TextStyle {
pub fn new() -> Self {
Self::default()
}
pub fn font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn align(mut self, align: TextAlign) -> Self {
self.align = align;
self
}
pub fn valign(mut self, valign: TextVAlign) -> Self {
self.valign = valign;
self
}
pub fn rotation(mut self, degrees: f32) -> Self {
self.rotation = degrees;
self
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn padding(mut self, padding: f32) -> Self {
self.padding = padding;
self
}
pub fn border(mut self, color: Color, width: f32) -> Self {
self.border_color = Some(color);
self.border_width = width;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ArrowHead {
None,
#[default]
Triangle,
Stealth,
Open,
}
#[derive(Debug, Clone)]
pub struct ArrowStyle {
pub color: Color,
pub line_width: f32,
pub line_style: LineStyle,
pub head_style: ArrowHead,
pub tail_style: ArrowHead,
pub head_length: f32,
pub head_width: f32,
}
impl Default for ArrowStyle {
fn default() -> Self {
Self {
color: Color::BLACK,
line_width: 1.0,
line_style: LineStyle::Solid,
head_style: ArrowHead::Triangle,
tail_style: ArrowHead::None,
head_length: 10.0,
head_width: 6.0,
}
}
}
impl ArrowStyle {
pub fn new() -> Self {
Self::default()
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn line_width(mut self, width: f32) -> Self {
self.line_width = width;
self
}
pub fn line_style(mut self, style: LineStyle) -> Self {
self.line_style = style;
self
}
pub fn head_style(mut self, style: ArrowHead) -> Self {
self.head_style = style;
self
}
pub fn tail_style(mut self, style: ArrowHead) -> Self {
self.tail_style = style;
self
}
pub fn head_size(mut self, length: f32, width: f32) -> Self {
self.head_length = length;
self.head_width = width;
self
}
pub fn double_headed(mut self) -> Self {
self.tail_style = self.head_style;
self
}
}
#[derive(Debug, Clone)]
pub struct ShapeStyle {
pub fill_color: Option<Color>,
pub fill_alpha: f32,
pub edge_color: Option<Color>,
pub edge_width: f32,
pub edge_style: LineStyle,
}
impl Default for ShapeStyle {
fn default() -> Self {
Self {
fill_color: None,
fill_alpha: 0.3,
edge_color: Some(Color::BLACK),
edge_width: 1.0,
edge_style: LineStyle::Solid,
}
}
}
impl ShapeStyle {
pub fn new() -> Self {
Self::default()
}
pub fn fill(mut self, color: Color) -> Self {
self.fill_color = Some(color);
self
}
pub fn fill_alpha(mut self, alpha: f32) -> Self {
self.fill_alpha = alpha.clamp(0.0, 1.0);
self
}
pub fn edge(mut self, color: Color) -> Self {
self.edge_color = Some(color);
self
}
pub fn edge_width(mut self, width: f32) -> Self {
self.edge_width = width;
self
}
pub fn edge_style(mut self, style: LineStyle) -> Self {
self.edge_style = style;
self
}
pub fn no_edge(mut self) -> Self {
self.edge_color = None;
self
}
pub fn no_fill(mut self) -> Self {
self.fill_color = None;
self
}
}
#[derive(Debug, Clone)]
pub struct FillStyle {
pub color: Color,
pub alpha: f32,
pub edge_color: Option<Color>,
pub edge_width: f32,
pub hatch: Option<HatchPattern>,
}
impl Default for FillStyle {
fn default() -> Self {
Self {
color: Color::BLUE,
alpha: 0.3,
edge_color: None,
edge_width: 0.0,
hatch: None,
}
}
}
impl FillStyle {
pub fn new() -> Self {
Self::default()
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha.clamp(0.0, 1.0);
self
}
pub fn edge(mut self, color: Color, width: f32) -> Self {
self.edge_color = Some(color);
self.edge_width = width;
self
}
pub fn hatch(mut self, pattern: HatchPattern) -> Self {
self.hatch = Some(pattern);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HatchPattern {
Diagonal,
BackDiagonal,
Horizontal,
Vertical,
Cross,
DiagonalCross,
Dots,
}
#[derive(Debug, Clone)]
pub enum Annotation {
Text {
x: f64,
y: f64,
text: String,
style: TextStyle,
},
Arrow {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
style: ArrowStyle,
},
HLine {
y: f64,
style: LineStyle,
color: Color,
width: f32,
},
VLine {
x: f64,
style: LineStyle,
color: Color,
width: f32,
},
Rectangle {
x: f64,
y: f64,
width: f64,
height: f64,
style: ShapeStyle,
},
FillBetween {
x: Vec<f64>,
y1: Vec<f64>,
y2: Vec<f64>,
style: FillStyle,
where_positive: bool,
},
HSpan {
x_min: f64,
x_max: f64,
style: ShapeStyle,
},
VSpan {
y_min: f64,
y_max: f64,
style: ShapeStyle,
},
}
impl Annotation {
pub fn text(x: f64, y: f64, text: impl Into<String>) -> Self {
Annotation::Text {
x,
y,
text: text.into(),
style: TextStyle::default(),
}
}
pub fn text_styled(x: f64, y: f64, text: impl Into<String>, style: TextStyle) -> Self {
Annotation::Text {
x,
y,
text: text.into(),
style,
}
}
pub fn arrow(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
Annotation::Arrow {
x1,
y1,
x2,
y2,
style: ArrowStyle::default(),
}
}
pub fn arrow_styled(x1: f64, y1: f64, x2: f64, y2: f64, style: ArrowStyle) -> Self {
Annotation::Arrow {
x1,
y1,
x2,
y2,
style,
}
}
pub fn hline(y: f64) -> Self {
Annotation::HLine {
y,
style: LineStyle::Dashed,
color: Color::new(128, 128, 128),
width: 1.0,
}
}
pub fn hline_styled(y: f64, color: Color, width: f32, style: LineStyle) -> Self {
Annotation::HLine {
y,
style,
color,
width,
}
}
pub fn vline(x: f64) -> Self {
Annotation::VLine {
x,
style: LineStyle::Dashed,
color: Color::new(128, 128, 128),
width: 1.0,
}
}
pub fn vline_styled(x: f64, color: Color, width: f32, style: LineStyle) -> Self {
Annotation::VLine {
x,
style,
color,
width,
}
}
pub fn rectangle(x: f64, y: f64, width: f64, height: f64) -> Self {
Annotation::Rectangle {
x,
y,
width,
height,
style: ShapeStyle::default(),
}
}
pub fn rectangle_styled(x: f64, y: f64, width: f64, height: f64, style: ShapeStyle) -> Self {
Annotation::Rectangle {
x,
y,
width,
height,
style,
}
}
pub fn fill_between(x: Vec<f64>, y1: Vec<f64>, y2: Vec<f64>) -> Self {
Annotation::FillBetween {
x,
y1,
y2,
style: FillStyle::default(),
where_positive: false,
}
}
pub fn fill_to_baseline(x: Vec<f64>, y: Vec<f64>, baseline: f64) -> Self {
let y2 = vec![baseline; x.len()];
Annotation::FillBetween {
x,
y1: y,
y2,
style: FillStyle::default(),
where_positive: false,
}
}
pub fn fill_between_styled(
x: Vec<f64>,
y1: Vec<f64>,
y2: Vec<f64>,
style: FillStyle,
where_positive: bool,
) -> Self {
Annotation::FillBetween {
x,
y1,
y2,
style,
where_positive,
}
}
pub fn hspan(x_min: f64, x_max: f64) -> Self {
Annotation::HSpan {
x_min,
x_max,
style: ShapeStyle::default().fill(Color::new_rgba(128, 128, 128, 50)),
}
}
pub fn vspan(y_min: f64, y_max: f64) -> Self {
Annotation::VSpan {
y_min,
y_max,
style: ShapeStyle::default().fill(Color::new_rgba(128, 128, 128, 50)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_style_builder() {
let style = TextStyle::new()
.font_size(12.0)
.color(Color::RED)
.align(TextAlign::Left)
.rotation(45.0);
assert!((style.font_size - 12.0).abs() < 0.001);
assert_eq!(style.color, Color::RED);
assert_eq!(style.align, TextAlign::Left);
assert!((style.rotation - 45.0).abs() < 0.001);
}
#[test]
fn test_arrow_style_builder() {
let style = ArrowStyle::new()
.color(Color::BLUE)
.line_width(2.0)
.head_style(ArrowHead::Stealth)
.double_headed();
assert_eq!(style.color, Color::BLUE);
assert!((style.line_width - 2.0).abs() < 0.001);
assert_eq!(style.head_style, ArrowHead::Stealth);
assert_eq!(style.tail_style, ArrowHead::Stealth);
}
#[test]
fn test_shape_style_builder() {
let style = ShapeStyle::new()
.fill(Color::GREEN)
.fill_alpha(0.5)
.edge(Color::BLACK)
.edge_width(2.0);
assert_eq!(style.fill_color, Some(Color::GREEN));
assert!((style.fill_alpha - 0.5).abs() < 0.001);
assert_eq!(style.edge_color, Some(Color::BLACK));
assert!((style.edge_width - 2.0).abs() < 0.001);
}
#[test]
fn test_fill_style_builder() {
let style = FillStyle::new()
.color(Color::RED)
.alpha(0.2)
.hatch(HatchPattern::Diagonal);
assert_eq!(style.color, Color::RED);
assert!((style.alpha - 0.2).abs() < 0.001);
assert_eq!(style.hatch, Some(HatchPattern::Diagonal));
}
#[test]
fn test_annotation_constructors() {
let text = Annotation::text(1.0, 2.0, "Hello");
assert!(
matches!(text, Annotation::Text { x, y, .. } if (x - 1.0).abs() < 0.001 && (y - 2.0).abs() < 0.001)
);
let arrow = Annotation::arrow(0.0, 0.0, 1.0, 1.0);
assert!(matches!(arrow, Annotation::Arrow { .. }));
let hline = Annotation::hline(5.0);
assert!(matches!(hline, Annotation::HLine { y, .. } if (y - 5.0).abs() < 0.001));
let vline = Annotation::vline(3.0);
assert!(matches!(vline, Annotation::VLine { x, .. } if (x - 3.0).abs() < 0.001));
let rect = Annotation::rectangle(0.0, 0.0, 10.0, 5.0);
assert!(
matches!(rect, Annotation::Rectangle { width, height, .. } if (width - 10.0).abs() < 0.001 && (height - 5.0).abs() < 0.001)
);
}
#[test]
fn test_fill_between() {
let x = vec![1.0, 2.0, 3.0];
let y1 = vec![1.0, 4.0, 9.0];
let y2 = vec![0.0, 0.0, 0.0];
let fill = Annotation::fill_between(x.clone(), y1.clone(), y2.clone());
assert!(matches!(
fill,
Annotation::FillBetween {
where_positive: false,
..
}
));
let baseline_fill = Annotation::fill_to_baseline(x.clone(), y1.clone(), 0.0);
if let Annotation::FillBetween { y2, .. } = baseline_fill {
assert!(y2.iter().all(|&v| (v - 0.0).abs() < 0.001));
}
}
#[test]
fn test_alpha_clamping() {
let style = FillStyle::new().alpha(1.5);
assert!((style.alpha - 1.0).abs() < 0.001);
let style = FillStyle::new().alpha(-0.5);
assert!((style.alpha - 0.0).abs() < 0.001);
let shape = ShapeStyle::new().fill_alpha(2.0);
assert!((shape.fill_alpha - 1.0).abs() < 0.001);
}
}