Skip to main content

plotkit_core/
annotations.rs

1//! Text annotations and arrow annotations for axes.
2//!
3//! This module defines the types used by [`Axes::text`] and [`Axes::annotate`]
4//! to place text labels and annotated callouts on a plot. Both types support
5//! builder-style configuration for font size, color, alignment, and (for
6//! annotations) arrow styling.
7
8use crate::primitives::Color;
9
10// ---------------------------------------------------------------------------
11// ArrowStyle
12// ---------------------------------------------------------------------------
13
14/// Arrow style for annotations connecting text to a data point.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ArrowStyle {
17    /// No arrow is drawn; only the text is shown.
18    None,
19    /// A simple line arrow with a small triangular head.
20    Simple,
21    /// A wider, more prominent arrowhead.
22    Fancy,
23}
24
25// ---------------------------------------------------------------------------
26// HAlign / VAlign (re-exports for annotation API convenience)
27// ---------------------------------------------------------------------------
28
29// We re-use HAlign and VAlign from primitives. The annotation structs store
30// them directly so that the builder methods can set them.
31
32use crate::primitives::{HAlign, VAlign};
33
34// ---------------------------------------------------------------------------
35// TextAnnotation
36// ---------------------------------------------------------------------------
37
38/// A text label placed at a data-space coordinate.
39///
40/// Created by [`Axes::text`]. Supports builder-style chaining to customise
41/// font size, color, alignment, and rotation.
42#[derive(Debug, Clone)]
43pub struct TextAnnotation {
44    /// The text string to render.
45    pub text: String,
46    /// X position in data coordinates.
47    pub x: f64,
48    /// Y position in data coordinates.
49    pub y: f64,
50    /// Optional font size override (in points). `None` uses the theme default.
51    pub fontsize: Option<f64>,
52    /// Optional text color override. `None` uses the theme text color.
53    pub color: Option<Color>,
54    /// Horizontal alignment of the text relative to `(x, y)`.
55    pub ha: HAlign,
56    /// Vertical alignment of the text relative to `(x, y)`.
57    pub va: VAlign,
58    /// Rotation angle in degrees (counter-clockwise).
59    pub rotation: f64,
60}
61
62impl TextAnnotation {
63    /// Sets the font size (in points).
64    pub fn fontsize(&mut self, size: f64) -> &mut Self {
65        self.fontsize = Some(size);
66        self
67    }
68
69    /// Sets the text color.
70    pub fn color(&mut self, color: Color) -> &mut Self {
71        self.color = Some(color);
72        self
73    }
74
75    /// Sets the horizontal alignment.
76    pub fn ha(&mut self, ha: HAlign) -> &mut Self {
77        self.ha = ha;
78        self
79    }
80
81    /// Sets the vertical alignment.
82    pub fn va(&mut self, va: VAlign) -> &mut Self {
83        self.va = va;
84        self
85    }
86
87    /// Sets the rotation angle in degrees (counter-clockwise).
88    pub fn rotation(&mut self, degrees: f64) -> &mut Self {
89        self.rotation = degrees;
90        self
91    }
92}
93
94// ---------------------------------------------------------------------------
95// Annotation
96// ---------------------------------------------------------------------------
97
98/// An annotation with optional arrow from a text position to a data point.
99///
100/// Created by [`Axes::annotate`]. The text is drawn at `xytext` and, when an
101/// arrow style other than [`ArrowStyle::None`] is set, an arrow is drawn from
102/// `xytext` to `xy`.
103#[derive(Debug, Clone)]
104pub struct Annotation {
105    /// The annotation text string.
106    pub text: String,
107    /// The data-space point being annotated.
108    pub xy: (f64, f64),
109    /// The data-space position where the text is placed.
110    pub xytext: (f64, f64),
111    /// Optional font size override (in points). `None` uses the theme default.
112    pub fontsize: Option<f64>,
113    /// Optional text color override. `None` uses the theme text color.
114    pub color: Option<Color>,
115    /// Horizontal alignment of the text relative to `xytext`.
116    pub ha: HAlign,
117    /// Vertical alignment of the text relative to `xytext`.
118    pub va: VAlign,
119    /// The style of arrow drawn from `xytext` to `xy`.
120    pub arrowstyle: ArrowStyle,
121    /// Optional arrow color override. `None` uses the text color.
122    pub arrow_color: Option<Color>,
123}
124
125impl Annotation {
126    /// Sets the font size (in points).
127    pub fn fontsize(&mut self, size: f64) -> &mut Self {
128        self.fontsize = Some(size);
129        self
130    }
131
132    /// Sets the text color.
133    pub fn color(&mut self, color: Color) -> &mut Self {
134        self.color = Some(color);
135        self
136    }
137
138    /// Sets the horizontal alignment.
139    pub fn ha(&mut self, ha: HAlign) -> &mut Self {
140        self.ha = ha;
141        self
142    }
143
144    /// Sets the vertical alignment.
145    pub fn va(&mut self, va: VAlign) -> &mut Self {
146        self.va = va;
147        self
148    }
149
150    /// Sets the arrow style.
151    pub fn arrowstyle(&mut self, style: ArrowStyle) -> &mut Self {
152        self.arrowstyle = style;
153        self
154    }
155
156    /// Sets the arrow color.
157    pub fn arrow_color(&mut self, color: Color) -> &mut Self {
158        self.arrow_color = Some(color);
159        self
160    }
161}
162
163// ---------------------------------------------------------------------------
164// Tests
165// ---------------------------------------------------------------------------
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn text_annotation_defaults() {
173        let mut t = TextAnnotation {
174            text: "hello".to_string(),
175            x: 1.0,
176            y: 2.0,
177            fontsize: None,
178            color: None,
179            ha: HAlign::Left,
180            va: VAlign::Baseline,
181            rotation: 0.0,
182        };
183        assert_eq!(t.text, "hello");
184        assert_eq!(t.x, 1.0);
185        assert_eq!(t.y, 2.0);
186        assert!(t.fontsize.is_none());
187        assert!(t.color.is_none());
188        assert_eq!(t.ha, HAlign::Left);
189        assert_eq!(t.va, VAlign::Baseline);
190        assert!((t.rotation - 0.0).abs() < f64::EPSILON);
191
192        // Builder chaining.
193        t.fontsize(14.0).color(Color::TAB_RED).ha(HAlign::Center).va(VAlign::Top).rotation(45.0);
194        assert_eq!(t.fontsize, Some(14.0));
195        assert_eq!(t.color, Some(Color::TAB_RED));
196        assert_eq!(t.ha, HAlign::Center);
197        assert_eq!(t.va, VAlign::Top);
198        assert!((t.rotation - 45.0).abs() < f64::EPSILON);
199    }
200
201    #[test]
202    fn annotation_defaults() {
203        let mut a = Annotation {
204            text: "peak".to_string(),
205            xy: (1.0, 2.0),
206            xytext: (3.0, 4.0),
207            fontsize: None,
208            color: None,
209            ha: HAlign::Center,
210            va: VAlign::Bottom,
211            arrowstyle: ArrowStyle::None,
212            arrow_color: None,
213        };
214        assert_eq!(a.text, "peak");
215        assert_eq!(a.xy, (1.0, 2.0));
216        assert_eq!(a.xytext, (3.0, 4.0));
217        assert_eq!(a.arrowstyle, ArrowStyle::None);
218
219        // Builder chaining.
220        a.arrowstyle(ArrowStyle::Simple).arrow_color(Color::TAB_BLUE).fontsize(12.0);
221        assert_eq!(a.arrowstyle, ArrowStyle::Simple);
222        assert_eq!(a.arrow_color, Some(Color::TAB_BLUE));
223        assert_eq!(a.fontsize, Some(12.0));
224    }
225
226    #[test]
227    fn arrow_style_equality() {
228        assert_eq!(ArrowStyle::None, ArrowStyle::None);
229        assert_eq!(ArrowStyle::Simple, ArrowStyle::Simple);
230        assert_eq!(ArrowStyle::Fancy, ArrowStyle::Fancy);
231        assert_ne!(ArrowStyle::None, ArrowStyle::Simple);
232        assert_ne!(ArrowStyle::Simple, ArrowStyle::Fancy);
233    }
234}