1use crate::theme::{LegendPosition, Theme};
2
3use super::Rect;
4
5pub struct PlotLayout {
7 pub total: Rect,
8 pub plot_area: Rect,
9 pub title_area: Rect,
10 pub subtitle_area: Rect,
11 pub caption_area: Rect,
12 pub x_axis_area: Rect,
13 pub y_axis_area: Rect,
14 pub legend_area: Rect,
15}
16
17#[derive(Clone, Copy, Debug, Default, PartialEq)]
23pub(crate) struct LayoutExtras {
24 pub left: f64,
25 pub right: f64,
26 pub legend_extra_rows: usize,
27 pub legend_extra_width: f64,
32}
33
34impl PlotLayout {
35 pub fn compute(
37 width: f64,
38 height: f64,
39 theme: &Theme,
40 has_title: bool,
41 has_legend: bool,
42 ) -> Self {
43 Self::compute_full(
44 width, height, theme, has_title, false, false, has_legend, false, None, None,
45 )
46 }
47
48 #[allow(clippy::too_many_arguments)]
55 pub fn compute_full(
56 width: f64,
57 height: f64,
58 theme: &Theme,
59 has_title: bool,
60 has_subtitle: bool,
61 has_caption: bool,
62 has_legend: bool,
63 x_axis_top: bool,
64 y_label_width: Option<f64>,
65 x_label_height: Option<f64>,
66 ) -> Self {
67 Self::compute_with_extras(
68 width,
69 height,
70 theme,
71 has_title,
72 has_subtitle,
73 has_caption,
74 has_legend,
75 x_axis_top,
76 y_label_width,
77 x_label_height,
78 LayoutExtras::default(),
79 )
80 }
81
82 #[allow(clippy::too_many_arguments)]
87 pub(crate) fn compute_with_extras(
88 width: f64,
89 height: f64,
90 theme: &Theme,
91 has_title: bool,
92 has_subtitle: bool,
93 has_caption: bool,
94 has_legend: bool,
95 x_axis_top: bool,
96 y_label_width: Option<f64>,
97 x_label_height: Option<f64>,
98 extras: LayoutExtras,
99 ) -> Self {
100 let margin = &theme.plot_margin;
101
102 let title_height = if has_title {
103 theme.title.size * 2.0
104 } else {
105 0.0
106 };
107
108 let subtitle_height = if has_subtitle {
109 theme.subtitle.size * 1.5
110 } else {
111 0.0
112 };
113
114 let caption_height = if has_caption {
115 theme.caption.size * 1.8
116 } else {
117 0.0
118 };
119
120 let x_axis_height = theme.axis_ticks_length
121 + if theme.axis_text_x.visible {
122 if theme.axis_text_x.angle.abs() > 10.0 {
125 x_label_height.unwrap_or(theme.axis_text_x.size * 5.0)
126 } else {
127 (theme.axis_text_x.size + 4.0) * theme.axis_text_x_dodge.max(1) as f64
129 }
130 } else {
131 0.0
132 }
133 + if theme.axis_title_x.visible {
134 theme.axis_title_x.size + 8.0
135 } else {
136 0.0
137 };
138
139 let y_axis_width = theme.axis_ticks_length
140 + if theme.axis_text_y.visible {
141 y_label_width.unwrap_or(theme.axis_text_y.size * 3.5 + 4.0)
142 } else {
143 0.0
144 }
145 + if theme.axis_title_y.visible {
146 theme.axis_title_y.size + 8.0
147 } else {
148 0.0
149 };
150
151 let legend_size = if has_legend {
152 theme.legend_margin.left
153 + theme.legend_key_width
154 + theme.legend_spacing
155 + theme.legend_text.size * 6.0
156 + theme.legend_margin.right
157 + extras.legend_extra_width
158 } else {
159 0.0
160 };
161
162 let legend_band = if has_legend {
165 theme.legend_margin.top
166 + theme.legend_key_height
167 + theme.legend_spacing
168 + theme.legend_margin.bottom
169 + extras.legend_extra_rows as f64 * (theme.legend_key_height + theme.legend_spacing)
170 } else {
171 0.0
172 };
173 let (legend_right, legend_left, legend_top, legend_bottom) = if has_legend {
175 match theme.legend_position {
176 LegendPosition::Right => (legend_size, 0.0, 0.0, 0.0),
177 LegendPosition::Left => (0.0, legend_size, 0.0, 0.0),
178 LegendPosition::Top => (0.0, 0.0, legend_band, 0.0),
179 LegendPosition::Bottom => (0.0, 0.0, 0.0, legend_band),
180 LegendPosition::None | LegendPosition::Inside(..) => (0.0, 0.0, 0.0, 0.0),
182 }
183 } else {
184 (0.0, 0.0, 0.0, 0.0)
185 };
186
187 let plot_x = margin.left + y_axis_width + legend_left + extras.left;
188 let plot_y = margin.top
190 + title_height
191 + subtitle_height
192 + legend_top
193 + if x_axis_top { x_axis_height } else { 0.0 };
194 let plot_width = width
195 - margin.left
196 - margin.right
197 - y_axis_width
198 - legend_right
199 - legend_left
200 - extras.left
201 - extras.right;
202 let plot_height = height
203 - margin.top
204 - margin.bottom
205 - title_height
206 - subtitle_height
207 - caption_height
208 - x_axis_height
209 - legend_top
210 - legend_bottom;
211
212 let mut plot_width = plot_width.max(50.0);
213 let mut plot_height = plot_height.max(50.0);
214 let mut plot_x = plot_x;
215 let mut plot_y = plot_y;
216
217 if let Some(r) = theme.aspect_ratio {
220 if r > 0.0 {
221 let (avail_w, avail_h) = (plot_width, plot_height);
222 if avail_w * r <= avail_h {
223 plot_height = avail_w * r;
224 plot_y += (avail_h - plot_height) / 2.0;
225 } else {
226 plot_width = avail_h / r;
227 plot_x += (avail_w - plot_width) / 2.0;
228 }
229 }
230 }
231
232 PlotLayout {
233 total: Rect {
234 x: 0.0,
235 y: 0.0,
236 width,
237 height,
238 },
239 plot_area: Rect {
240 x: plot_x,
241 y: plot_y,
242 width: plot_width,
243 height: plot_height,
244 },
245 title_area: Rect {
246 x: plot_x,
247 y: margin.top,
248 width: plot_width,
249 height: title_height,
250 },
251 subtitle_area: Rect {
252 x: plot_x,
253 y: margin.top + title_height,
254 width: plot_width,
255 height: subtitle_height,
256 },
257 caption_area: Rect {
258 x: plot_x,
259 y: plot_y + plot_height + x_axis_height,
260 width: plot_width,
261 height: caption_height,
262 },
263 x_axis_area: Rect {
264 x: plot_x,
265 y: plot_y + plot_height,
266 width: plot_width,
267 height: x_axis_height,
268 },
269 y_axis_area: Rect {
270 x: margin.left + legend_left,
271 y: plot_y,
272 width: y_axis_width,
273 height: plot_height,
274 },
275 legend_area: Rect {
276 x: plot_x + plot_width,
277 y: plot_y,
278 width: legend_size,
279 height: plot_height,
280 },
281 }
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::theme::presets::theme_gray;
289
290 #[test]
295 fn compute_full_matches_compute_with_extras_default() {
296 let theme = theme_gray();
297 let a = PlotLayout::compute_full(
298 640.0, 400.0, &theme, true, false, false, true, false, None, None,
299 );
300 let b = PlotLayout::compute_with_extras(
301 640.0,
302 400.0,
303 &theme,
304 true,
305 false,
306 false,
307 true,
308 false,
309 None,
310 None,
311 LayoutExtras::default(),
312 );
313 assert_eq!(a.plot_area.x, b.plot_area.x);
314 assert_eq!(a.plot_area.width, b.plot_area.width);
315 assert_eq!(a.plot_area.height, b.plot_area.height);
316 }
317
318 #[test]
319 fn compute_delegates_to_compute_full() {
320 let theme = theme_gray();
321 let a = PlotLayout::compute(640.0, 400.0, &theme, true, false);
322 let b = PlotLayout::compute_full(
323 640.0, 400.0, &theme, true, false, false, false, false, None, None,
324 );
325 assert_eq!(a.plot_area.x, b.plot_area.x);
326 }
327}