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 pub legend_max_key_size: f64,
41}
42
43impl PlotLayout {
44 pub fn compute(
46 width: f64,
47 height: f64,
48 theme: &Theme,
49 has_title: bool,
50 has_legend: bool,
51 ) -> Self {
52 Self::compute_full(
53 width, height, theme, has_title, false, false, has_legend, false, None, None,
54 )
55 }
56
57 #[allow(clippy::too_many_arguments)]
64 pub fn compute_full(
65 width: f64,
66 height: f64,
67 theme: &Theme,
68 has_title: bool,
69 has_subtitle: bool,
70 has_caption: bool,
71 has_legend: bool,
72 x_axis_top: bool,
73 y_label_width: Option<f64>,
74 x_label_height: Option<f64>,
75 ) -> Self {
76 Self::compute_with_extras(
77 width,
78 height,
79 theme,
80 has_title,
81 has_subtitle,
82 has_caption,
83 has_legend,
84 x_axis_top,
85 y_label_width,
86 x_label_height,
87 LayoutExtras::default(),
88 )
89 }
90
91 #[allow(clippy::too_many_arguments)]
96 pub(crate) fn compute_with_extras(
97 width: f64,
98 height: f64,
99 theme: &Theme,
100 has_title: bool,
101 has_subtitle: bool,
102 has_caption: bool,
103 has_legend: bool,
104 x_axis_top: bool,
105 y_label_width: Option<f64>,
106 x_label_height: Option<f64>,
107 extras: LayoutExtras,
108 ) -> Self {
109 let margin = &theme.plot_margin;
110
111 let title_height = if has_title {
112 theme.title.size * 2.0
113 } else {
114 0.0
115 };
116
117 let subtitle_height = if has_subtitle {
118 theme.subtitle.size * 1.5
119 } else {
120 0.0
121 };
122
123 let caption_height = if has_caption {
124 theme.caption.size * 1.8
125 } else {
126 0.0
127 };
128
129 let x_axis_height = theme.axis_ticks_length
130 + if theme.axis_text_x.visible {
131 if theme.axis_text_x.angle.abs() > 10.0 {
134 x_label_height.unwrap_or(theme.axis_text_x.size * 5.0)
135 } else {
136 (theme.axis_text_x.size + 4.0) * theme.axis_text_x_dodge.max(1) as f64
138 }
139 } else {
140 0.0
141 }
142 + if theme.axis_title_x.visible {
143 theme.axis_title_x.size + 8.0
144 } else {
145 0.0
146 };
147
148 let y_axis_width = theme.axis_ticks_length
149 + if theme.axis_text_y.visible {
150 y_label_width.unwrap_or(theme.axis_text_y.size * 3.5 + 4.0)
151 } else {
152 0.0
153 }
154 + if theme.axis_title_y.visible {
155 theme.axis_title_y.size + 8.0
156 } else {
157 0.0
158 };
159
160 let key_w = theme.legend_key_width.max(extras.legend_max_key_size);
166 let key_h = theme.legend_key_height.max(key_w);
167
168 let legend_size = if has_legend {
169 theme.legend_margin.left
170 + key_w
171 + theme.legend_spacing
172 + theme.legend_text.size * 6.0
173 + theme.legend_margin.right
174 + extras.legend_extra_width
175 } else {
176 0.0
177 };
178
179 let legend_band = if has_legend {
182 theme.legend_margin.top
183 + key_h
184 + theme.legend_spacing
185 + theme.legend_margin.bottom
186 + extras.legend_extra_rows as f64 * (key_h + theme.legend_spacing)
187 } else {
188 0.0
189 };
190 let (legend_right, legend_left, legend_top, legend_bottom) = if has_legend {
192 match theme.legend_position {
193 LegendPosition::Right => (legend_size, 0.0, 0.0, 0.0),
194 LegendPosition::Left => (0.0, legend_size, 0.0, 0.0),
195 LegendPosition::Top => (0.0, 0.0, legend_band, 0.0),
196 LegendPosition::Bottom => (0.0, 0.0, 0.0, legend_band),
197 LegendPosition::None | LegendPosition::Inside(..) => (0.0, 0.0, 0.0, 0.0),
199 }
200 } else {
201 (0.0, 0.0, 0.0, 0.0)
202 };
203
204 let plot_x = margin.left + y_axis_width + legend_left + extras.left;
205 let plot_y = margin.top
207 + title_height
208 + subtitle_height
209 + legend_top
210 + if x_axis_top { x_axis_height } else { 0.0 };
211 let plot_width = width
212 - margin.left
213 - margin.right
214 - y_axis_width
215 - legend_right
216 - legend_left
217 - extras.left
218 - extras.right;
219 let plot_height = height
220 - margin.top
221 - margin.bottom
222 - title_height
223 - subtitle_height
224 - caption_height
225 - x_axis_height
226 - legend_top
227 - legend_bottom;
228
229 let mut plot_width = plot_width.max(50.0);
230 let mut plot_height = plot_height.max(50.0);
231 let mut plot_x = plot_x;
232 let mut plot_y = plot_y;
233
234 if let Some(r) = theme.aspect_ratio {
237 if r > 0.0 {
238 let (avail_w, avail_h) = (plot_width, plot_height);
239 if avail_w * r <= avail_h {
240 plot_height = avail_w * r;
241 plot_y += (avail_h - plot_height) / 2.0;
242 } else {
243 plot_width = avail_h / r;
244 plot_x += (avail_w - plot_width) / 2.0;
245 }
246 }
247 }
248
249 PlotLayout {
250 total: Rect {
251 x: 0.0,
252 y: 0.0,
253 width,
254 height,
255 },
256 plot_area: Rect {
257 x: plot_x,
258 y: plot_y,
259 width: plot_width,
260 height: plot_height,
261 },
262 title_area: Rect {
263 x: plot_x,
264 y: margin.top,
265 width: plot_width,
266 height: title_height,
267 },
268 subtitle_area: Rect {
269 x: plot_x,
270 y: margin.top + title_height,
271 width: plot_width,
272 height: subtitle_height,
273 },
274 caption_area: Rect {
275 x: plot_x,
276 y: plot_y + plot_height + x_axis_height,
277 width: plot_width,
278 height: caption_height,
279 },
280 x_axis_area: Rect {
281 x: plot_x,
282 y: plot_y + plot_height,
283 width: plot_width,
284 height: x_axis_height,
285 },
286 y_axis_area: Rect {
287 x: margin.left + legend_left,
288 y: plot_y,
289 width: y_axis_width,
290 height: plot_height,
291 },
292 legend_area: Rect {
293 x: plot_x + plot_width,
294 y: plot_y,
295 width: legend_size,
296 height: plot_height,
297 },
298 }
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::theme::presets::theme_gray;
306
307 #[test]
312 fn compute_full_matches_compute_with_extras_default() {
313 let theme = theme_gray();
314 let a = PlotLayout::compute_full(
315 640.0, 400.0, &theme, true, false, false, true, false, None, None,
316 );
317 let b = PlotLayout::compute_with_extras(
318 640.0,
319 400.0,
320 &theme,
321 true,
322 false,
323 false,
324 true,
325 false,
326 None,
327 None,
328 LayoutExtras::default(),
329 );
330 assert_eq!(a.plot_area.x, b.plot_area.x);
331 assert_eq!(a.plot_area.width, b.plot_area.width);
332 assert_eq!(a.plot_area.height, b.plot_area.height);
333 }
334
335 #[test]
336 fn compute_delegates_to_compute_full() {
337 let theme = theme_gray();
338 let a = PlotLayout::compute(640.0, 400.0, &theme, true, false);
339 let b = PlotLayout::compute_full(
340 640.0, 400.0, &theme, true, false, false, false, false, None, None,
341 );
342 assert_eq!(a.plot_area.x, b.plot_area.x);
343 }
344}