1pub fn text_width(text: &str, size: f64) -> f64 {
16 let em: f64 = text
17 .chars()
18 .map(|ch| match ch {
19 ' ' => 0.27,
20 '.' | ',' | ':' | ';' | '\'' | '|' | '!' | 'i' | 'j' | 'l' | 'I' => 0.27,
21 'f' | 't' | 'r' | '(' | ')' | '[' | ']' | '-' | '/' => 0.36,
22 '0'..='9' | '$' | '€' | '£' | '+' | '=' | '<' | '>' | '#' | '*' | '?' => 0.57,
23 'm' | 'w' => 0.8,
24 'M' | 'W' | '%' | '@' => 0.86,
25 'a'..='z' => 0.53,
26 'A'..='Z' | '&' => 0.66,
27 _ => 0.62,
28 })
29 .sum();
30 em * size
31}
32
33pub(crate) fn label_bbox(
39 text: &str,
40 size: f64,
41 x: f64,
42 y: f64,
43 angle: f64,
44 anchor: f64,
45) -> [f64; 4] {
46 let w = text_width(text, size);
47 let h = size * 1.2;
48 let x0 = -w * anchor;
49 let (s, c) = angle.to_radians().sin_cos();
50 let mut b = [
51 f64::INFINITY,
52 f64::INFINITY,
53 f64::NEG_INFINITY,
54 f64::NEG_INFINITY,
55 ];
56 for (dx, dy) in [
57 (x0, -h / 2.0),
58 (x0 + w, -h / 2.0),
59 (x0, h / 2.0),
60 (x0 + w, h / 2.0),
61 ] {
62 let (px, py) = (x + dx * c - dy * s, y + dx * s + dy * c);
63 b = [b[0].min(px), b[1].min(py), b[2].max(px), b[3].max(py)];
64 }
65 b
66}
67
68pub(crate) fn axis_label_priority(n: usize) -> Vec<usize> {
75 if n == 0 {
76 return Vec::new();
77 }
78 if n == 1 {
79 return vec![1];
80 }
81 let mut out = vec![1, n];
82 bisect(2, n - 1, &mut out);
83 out
84}
85
86fn bisect(lo: usize, hi: usize, out: &mut Vec<usize>) {
89 if lo > hi {
90 return;
91 }
92 let mid = (lo + hi) / 2;
93 out.push(mid);
94 if mid > lo {
95 bisect(lo, mid - 1, out);
96 }
97 bisect(mid + 1, hi, out);
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn priority_matches_r_ggplot2_4_0_3() {
106 assert_eq!(axis_label_priority(5), vec![1, 5, 3, 2, 4]);
107 assert_eq!(axis_label_priority(10), vec![1, 10, 5, 3, 2, 4, 7, 6, 8, 9]);
108 }
109
110 #[test]
111 fn priority_edge_cases() {
112 assert_eq!(axis_label_priority(0), Vec::<usize>::new());
113 assert_eq!(axis_label_priority(1), vec![1]);
114 assert_eq!(axis_label_priority(2), vec![1, 2]);
115 }
116
117 #[test]
118 fn text_width_matches_consumer_em_table() {
119 assert_eq!(text_width("", 10.0), 0.0);
120 assert!((text_width("i", 10.0) - 2.7).abs() < 1e-9);
121 assert!((text_width("M", 10.0) - 8.6).abs() < 1e-9);
122 }
123
124 #[test]
125 fn label_bbox_unrotated_middle_anchor() {
126 let b = label_bbox("ab", 10.0, 100.0, 50.0, 0.0, 0.5);
127 let w = text_width("ab", 10.0);
128 assert!((b[0] - (100.0 - w / 2.0)).abs() < 1e-9);
129 assert!((b[2] - (100.0 + w / 2.0)).abs() < 1e-9);
130 assert!((b[1] - (50.0 - 6.0)).abs() < 1e-9);
131 assert!((b[3] - (50.0 + 6.0)).abs() < 1e-9);
132 }
133}