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}