splot/
text.rs

1// text.rs
2//
3// Copyright (c) 2021-2025  Douglas P Lau
4//
5use crate::point::{IntoPoint, Point};
6use crate::rect::{Edge, Rect};
7use std::fmt;
8
9/// Vertical offset relative to point
10#[derive(Copy, Clone, Debug, PartialEq)]
11pub enum VerticalOffset {
12    /// Label below point
13    Below,
14    /// Label at point
15    At,
16    /// Label above point
17    Above,
18}
19
20/// Text anchor
21#[derive(Copy, Clone, Debug, PartialEq)]
22pub enum Anchor {
23    /// Anchor at start of text
24    Start,
25    /// Anchor at middle of text
26    Middle,
27    /// Anchor at end of text
28    End,
29}
30
31#[derive(Clone, Debug, PartialEq)]
32pub struct Label {
33    offset: VerticalOffset,
34    anchor: Anchor,
35    rounding_precision: Option<usize>,
36}
37
38pub struct Text<'a> {
39    edge: Edge,
40    anchor: Anchor,
41    rect: Option<Rect>,
42    dy: Option<f32>,
43    class_name: Option<&'a str>,
44}
45
46pub struct Tspan<'a> {
47    text: &'a str,
48    x: Option<i32>,
49    y: Option<i32>,
50    dy: Option<f32>,
51}
52
53/// Tick marks for axis labels
54#[derive(Debug, PartialEq)]
55pub struct Tick {
56    value: f32,
57    text: String,
58}
59
60impl fmt::Display for Anchor {
61    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
62        match self {
63            Anchor::Start => write!(f, " text-anchor='start'"),
64            Anchor::Middle => write!(f, " text-anchor='middle'"),
65            Anchor::End => write!(f, " text-anchor='end'"),
66        }
67    }
68}
69
70impl Default for Label {
71    fn default() -> Self {
72        Label {
73            offset: VerticalOffset::At,
74            anchor: Anchor::Middle,
75            rounding_precision: None,
76        }
77    }
78}
79
80#[allow(dead_code)]
81impl Label {
82    pub fn new() -> Self {
83        Self::default()
84    }
85
86    pub fn vertical_offset(&self) -> f32 {
87        match self.offset {
88            VerticalOffset::Above => -1.0,
89            VerticalOffset::At => 0.0,
90            VerticalOffset::Below => 1.0,
91        }
92    }
93
94    pub fn above(mut self) -> Self {
95        self.offset = VerticalOffset::Above;
96        self
97    }
98
99    pub fn below(mut self) -> Self {
100        self.offset = VerticalOffset::Below;
101        self
102    }
103
104    pub fn start(mut self) -> Self {
105        self.anchor = Anchor::Start;
106        self
107    }
108
109    pub fn end(mut self) -> Self {
110        self.anchor = Anchor::End;
111        self
112    }
113
114    pub fn rounded(&self, value: f32) -> String {
115        match self.rounding_precision {
116            None => value.to_string(),
117            Some(digits) => format!("{:.1$}", value, digits),
118        }
119    }
120
121    pub fn display<P>(
122        &self,
123        f: &mut fmt::Formatter,
124        x: i32,
125        y: i32,
126        pt: P,
127    ) -> fmt::Result
128    where
129        P: IntoPoint,
130    {
131        let pt: Point = pt.into();
132        let lbl = format!("({} {})", pt.x, pt.y);
133        let tspan = Tspan::new(&lbl).x(x).y(y).dy(-0.66);
134        write!(f, "{tspan}")
135    }
136}
137
138impl<'a> Text<'a> {
139    pub fn new(edge: Edge) -> Self {
140        Text {
141            edge,
142            anchor: Anchor::Middle,
143            rect: None,
144            dy: None,
145            class_name: None,
146        }
147    }
148
149    pub fn anchor(mut self, anchor: Anchor) -> Self {
150        self.anchor = anchor;
151        self
152    }
153
154    #[allow(dead_code)]
155    pub fn dy(mut self, dy: f32) -> Self {
156        self.dy = Some(dy);
157        self
158    }
159
160    pub fn rect(mut self, rect: Rect) -> Self {
161        self.rect = Some(rect);
162        self
163    }
164
165    pub fn class_name(mut self, class_name: &'a str) -> Self {
166        self.class_name = Some(class_name);
167        self
168    }
169
170    pub fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
171        write!(f, "<text")?;
172        if let Some(class_name) = self.class_name {
173            write!(f, " class='{}'", class_name)?;
174        }
175        if let Some(rect) = self.rect {
176            self.transform(f, rect)?;
177        }
178        if let Some(dy) = self.dy {
179            write!(f, " dy='{dy}em'")?;
180        }
181        writeln!(f, "{}>", self.anchor)
182    }
183
184    pub fn display_done(&self, f: &mut fmt::Formatter) -> fmt::Result {
185        writeln!(f, "</text>")
186    }
187
188    fn transform(&self, f: &mut fmt::Formatter, rect: Rect) -> fmt::Result {
189        let x = match (self.edge, self.anchor) {
190            (Edge::Top, Anchor::Start) | (Edge::Bottom, Anchor::Start) => {
191                rect.x
192            }
193            (Edge::Top, Anchor::End) | (Edge::Bottom, Anchor::End) => {
194                rect.right()
195            }
196            _ => rect.x + i32::from(rect.width) / 2,
197        };
198        let y = match (self.edge, self.anchor) {
199            (Edge::Left, Anchor::End) | (Edge::Right, Anchor::Start) => rect.y,
200            (Edge::Left, Anchor::Start) | (Edge::Right, Anchor::End) => {
201                rect.bottom()
202            }
203            _ => rect.y + i32::from(rect.height) / 2,
204        };
205        write!(f, " transform='translate({x} {y})")?;
206        match self.edge {
207            Edge::Left => write!(f, " rotate(-90)")?,
208            Edge::Right => write!(f, " rotate(90)")?,
209            _ => (),
210        }
211        write!(f, "'")
212    }
213}
214
215impl fmt::Display for Tspan<'_> {
216    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
217        write!(f, "<tspan")?;
218        if let Some(x) = self.x {
219            write!(f, " x='{x}'")?;
220        }
221        if let Some(y) = self.y {
222            write!(f, " y='{y}'")?;
223        }
224        if let Some(dy) = self.dy {
225            write!(f, " dy='{dy}em'")?;
226        }
227        write!(f, ">{}", &self.text)?;
228        writeln!(f, "</tspan>")
229    }
230}
231
232impl<'a> Tspan<'a> {
233    pub fn new(text: &'a str) -> Self {
234        Tspan {
235            text,
236            x: None,
237            y: None,
238            dy: None,
239        }
240    }
241
242    pub fn x(mut self, x: i32) -> Self {
243        self.x = Some(x);
244        self
245    }
246
247    pub fn y(mut self, y: i32) -> Self {
248        self.y = Some(y);
249        self
250    }
251
252    pub fn dy(mut self, dy: f32) -> Self {
253        self.dy = Some(dy);
254        self
255    }
256}
257
258impl Tick {
259    pub const LEN: i32 = 20;
260    pub const HLEN: i32 = Tick::LEN + 8;
261    pub const VLEN: i32 = Tick::LEN * 2;
262
263    pub fn new<T>(value: f32, text: T) -> Self
264    where
265        T: Into<String>,
266    {
267        let text = text.into();
268        Tick { value, text }
269    }
270
271    pub fn text(&self) -> &str {
272        &self.text
273    }
274
275    pub fn x(&self, edge: Edge, rect: Rect, len: i32) -> i32 {
276        match edge {
277            Edge::Left => rect.right() - len,
278            Edge::Right => rect.x + len,
279            _ => rect.x + (self.value * rect.width as f32).round() as i32,
280        }
281    }
282
283    pub fn y(&self, edge: Edge, rect: Rect, len: i32) -> i32 {
284        match edge {
285            Edge::Top => rect.bottom() - len,
286            Edge::Bottom => rect.y + len,
287            _ => rect.y + (self.value * rect.height as f32).round() as i32,
288        }
289    }
290
291    pub fn tspan(&self, edge: Edge, rect: Rect) -> Tspan {
292        let x = self.x(edge, rect, Tick::HLEN);
293        let y = self.y(edge, rect, Tick::VLEN);
294        Tspan::new(self.text()).x(x).y(y).dy(0.33)
295    }
296}