1use crate::render::Rect;
2
3use super::Panel;
4
5pub fn wrap_cell(
10 i: usize,
11 n: usize,
12 ncol: usize,
13 vertical: bool,
14 as_table: bool,
15) -> (usize, usize) {
16 let ncol = ncol.max(1);
17 let nrow = n.div_ceil(ncol).max(1);
18 let (row, col) = if vertical {
19 (i % nrow, i / nrow)
20 } else {
21 (i / ncol, i % ncol)
22 };
23 (if as_table { row } else { nrow - 1 - row }, col)
24}
25
26pub fn compute_wrap_panels(
28 levels: &[String],
29 ncol: Option<usize>,
30 total_area: &Rect,
31 strip_height: f64,
32) -> Vec<Panel> {
33 let n = levels.len();
34 if n == 0 {
35 return vec![];
36 }
37
38 let ncol = ncol.unwrap_or_else(|| {
39 let sqrt = (n as f64).sqrt().ceil() as usize;
40 sqrt.max(1)
41 });
42 let nrow = n.div_ceil(ncol);
43
44 let panel_width = total_area.width / ncol as f64;
45 let panel_height = total_area.height / nrow as f64;
46
47 levels
48 .iter()
49 .enumerate()
50 .map(|(i, label)| {
51 let row = i / ncol;
52 let col = i % ncol;
53
54 Panel {
55 row,
56 col,
57 label: label.clone(),
58 row_label: None,
59 col_label: Some(label.clone()),
60 rect: Rect {
61 x: total_area.x + col as f64 * panel_width,
62 y: total_area.y + row as f64 * panel_height + strip_height,
63 width: panel_width,
64 height: panel_height - strip_height,
65 },
66 }
67 })
68 .collect()
69}
70
71#[derive(Debug, Clone, Copy, PartialEq)]
74pub struct FacetGrid {
75 pub ncol: usize,
76 pub nrow: usize,
77 pub panel_w: f64,
78 pub panel_h: f64,
79}
80
81#[allow(clippy::too_many_arguments)]
88pub fn fit_grid(
89 n: usize,
90 ncol: Option<usize>,
91 aspect: f64,
92 area_w: f64,
93 area_h: f64,
94 strip_h: f64,
95 gap_x: f64,
96 gap_y: f64,
97) -> FacetGrid {
98 let n = n.max(1);
99 let aspect = if aspect.is_finite() && aspect > 0.0 {
100 aspect
101 } else {
102 1.0
103 };
104 let size = |c: usize| {
105 let r = n.div_ceil(c);
106 let pw = (area_w - gap_x * (c as f64 - 1.0)) / c as f64;
107 let ph = (area_h - r as f64 * strip_h - gap_y * (r as f64 - 1.0)) / r as f64;
108 (pw.min(ph / aspect)).max(0.0)
109 };
110 let c = match ncol {
111 Some(c) => c.clamp(1, n),
112 None => {
113 let mut best = (1, size(1));
114 for c in 2..=n {
115 let s = size(c);
116 if s > best.1 + 1e-9
118 || ((s - best.1).abs() <= 1e-9 && n.div_ceil(c) < n.div_ceil(best.0))
119 {
120 best = (c, s);
121 }
122 }
123 best.0
124 }
125 };
126 let s = size(c);
127 FacetGrid {
128 ncol: c,
129 nrow: n.div_ceil(c),
130 panel_w: s,
131 panel_h: s * aspect,
132 }
133}
134
135#[allow(clippy::too_many_arguments)]
141pub fn natural_height(
142 n: usize,
143 ncol: Option<usize>,
144 aspect: f64,
145 width: f64,
146 strip_h: f64,
147 gap_x: f64,
148 gap_y: f64,
149 min_panel_w: f64,
150) -> (usize, f64) {
151 let n = n.max(1);
152 let aspect = if aspect.is_finite() && aspect > 0.0 {
153 aspect
154 } else {
155 1.0
156 };
157 let c = match ncol {
158 Some(c) => c.clamp(1, n),
159 None => (((width + gap_x) / (min_panel_w + gap_x)).floor() as usize).clamp(1, n),
160 };
161 let r = n.div_ceil(c);
162 let pw = ((width - gap_x * (c as f64 - 1.0)) / c as f64).max(0.0);
163 (
164 c,
165 r as f64 * (pw * aspect + strip_h) + gap_y * (r as f64 - 1.0),
166 )
167}
168
169#[cfg(test)]
170mod grid_tests {
171 use super::*;
172
173 #[test]
174 fn fit_grid_picks_the_largest_panels() {
175 let g = fit_grid(4, None, 1.3, 1200.0, 400.0, 0.0, 0.0, 0.0);
177 assert_eq!((g.ncol, g.nrow), (4, 1));
178 let g = fit_grid(4, None, 1.0, 600.0, 600.0, 0.0, 0.0, 0.0);
180 assert_eq!((g.ncol, g.nrow), (2, 2));
181 assert!((g.panel_w - 300.0).abs() < 1e-9 && (g.panel_h - 300.0).abs() < 1e-9);
182 }
183
184 #[test]
185 fn fit_grid_respects_strips_gaps_and_a_given_ncol() {
186 let g = fit_grid(3, Some(3), 0.5, 620.0, 400.0, 20.0, 10.0, 10.0);
187 assert_eq!((g.ncol, g.nrow), (3, 1));
188 assert!((g.panel_w - 200.0).abs() < 1e-9);
189 assert!((g.panel_h - 100.0).abs() < 1e-9);
190 assert_eq!(
192 fit_grid(2, Some(9), 1.0, 100.0, 100.0, 0.0, 0.0, 0.0).ncol,
193 2
194 );
195 let g = fit_grid(2, Some(1), 1.0, 1000.0, 220.0, 10.0, 0.0, 0.0);
197 assert!((g.panel_h - 100.0).abs() < 1e-9 && (g.panel_w - 100.0).abs() < 1e-9);
198 }
199
200 #[test]
201 fn natural_height_fills_the_width() {
202 let (c, h) = natural_height(4, None, 1.0, 700.0, 20.0, 10.0, 10.0, 160.0);
204 assert_eq!(c, 4);
205 assert!((h - (167.5 + 20.0)).abs() < 1e-9);
206 let (c, h) = natural_height(4, None, 1.0, 390.0, 20.0, 10.0, 10.0, 160.0);
208 assert_eq!(c, 2);
209 assert!((h - (2.0 * (190.0 + 20.0) + 10.0)).abs() < 1e-9);
210 assert_eq!(
212 natural_height(4, Some(1), 1.0, 700.0, 0.0, 0.0, 0.0, 160.0).0,
213 1
214 );
215 }
216}
217
218#[cfg(test)]
219mod cell_tests {
220 use super::wrap_cell;
221
222 #[test]
223 fn dir_and_as_table_match_ggplot2() {
224 let h: Vec<_> = (0..5).map(|i| wrap_cell(i, 5, 3, false, true)).collect();
226 assert_eq!(h, [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1)]);
227 let v: Vec<_> = (0..5).map(|i| wrap_cell(i, 5, 3, true, true)).collect();
229 assert_eq!(v, [(0, 0), (1, 0), (0, 1), (1, 1), (0, 2)]);
230 let b: Vec<_> = (0..5).map(|i| wrap_cell(i, 5, 3, false, false)).collect();
232 assert_eq!(b, [(1, 0), (1, 1), (1, 2), (0, 0), (0, 1)]);
233 }
234}