Skip to main content

ggplot_rs/render/
layout.rs

1use crate::theme::{LegendPosition, Theme};
2
3use super::Rect;
4
5/// Computed layout areas for the plot.
6pub 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
17impl PlotLayout {
18    /// Compute layout from total dimensions and theme settings.
19    pub fn compute(
20        width: f64,
21        height: f64,
22        theme: &Theme,
23        has_title: bool,
24        has_legend: bool,
25    ) -> Self {
26        Self::compute_full(width, height, theme, has_title, false, false, has_legend)
27    }
28
29    /// Compute layout with full subtitle/caption support.
30    pub fn compute_full(
31        width: f64,
32        height: f64,
33        theme: &Theme,
34        has_title: bool,
35        has_subtitle: bool,
36        has_caption: bool,
37        has_legend: bool,
38    ) -> Self {
39        let margin = &theme.plot_margin;
40
41        let title_height = if has_title {
42            theme.title.size * 2.0
43        } else {
44            0.0
45        };
46
47        let subtitle_height = if has_subtitle {
48            theme.subtitle.size * 1.5
49        } else {
50            0.0
51        };
52
53        let caption_height = if has_caption {
54            theme.caption.size * 1.8
55        } else {
56            0.0
57        };
58
59        let x_axis_height = theme.axis_ticks_length
60            + if theme.axis_text_x.visible {
61                // Rotated labels extend vertically, so reserve more bottom space.
62                if theme.axis_text_x.angle.abs() > 10.0 {
63                    theme.axis_text_x.size * 5.0
64                } else {
65                    theme.axis_text_x.size + 4.0
66                }
67            } else {
68                0.0
69            }
70            + if theme.axis_title_x.visible {
71                theme.axis_title_x.size + 8.0
72            } else {
73                0.0
74            };
75
76        let y_axis_width = theme.axis_ticks_length
77            + if theme.axis_text_y.visible {
78                theme.axis_text_y.size * 3.5 + 4.0
79            } else {
80                0.0
81            }
82            + if theme.axis_title_y.visible {
83                theme.axis_title_y.size + 8.0
84            } else {
85                0.0
86            };
87
88        let legend_size = if has_legend {
89            theme.legend_margin.left
90                + theme.legend_key_width
91                + theme.legend_spacing
92                + theme.legend_text.size * 6.0
93                + theme.legend_margin.right
94        } else {
95            0.0
96        };
97
98        // Determine legend space allocation per position
99        let (legend_right, legend_left, legend_top, legend_bottom) = if has_legend {
100            match theme.legend_position {
101                LegendPosition::Right => (legend_size, 0.0, 0.0, 0.0),
102                LegendPosition::Left => (0.0, legend_size, 0.0, 0.0),
103                LegendPosition::Top => (0.0, 0.0, legend_size, 0.0),
104                LegendPosition::Bottom => (0.0, 0.0, 0.0, legend_size),
105                // Inside/None overlay the panel, reserving no external space.
106                LegendPosition::None | LegendPosition::Inside(..) => (0.0, 0.0, 0.0, 0.0),
107            }
108        } else {
109            (0.0, 0.0, 0.0, 0.0)
110        };
111
112        let plot_x = margin.left + y_axis_width + legend_left;
113        let plot_y = margin.top + title_height + subtitle_height + legend_top;
114        let plot_width =
115            width - margin.left - margin.right - y_axis_width - legend_right - legend_left;
116        let plot_height = height
117            - margin.top
118            - margin.bottom
119            - title_height
120            - subtitle_height
121            - caption_height
122            - x_axis_height
123            - legend_top
124            - legend_bottom;
125
126        let plot_width = plot_width.max(50.0);
127        let plot_height = plot_height.max(50.0);
128
129        PlotLayout {
130            total: Rect {
131                x: 0.0,
132                y: 0.0,
133                width,
134                height,
135            },
136            plot_area: Rect {
137                x: plot_x,
138                y: plot_y,
139                width: plot_width,
140                height: plot_height,
141            },
142            title_area: Rect {
143                x: plot_x,
144                y: margin.top,
145                width: plot_width,
146                height: title_height,
147            },
148            subtitle_area: Rect {
149                x: plot_x,
150                y: margin.top + title_height,
151                width: plot_width,
152                height: subtitle_height,
153            },
154            caption_area: Rect {
155                x: plot_x,
156                y: plot_y + plot_height + x_axis_height,
157                width: plot_width,
158                height: caption_height,
159            },
160            x_axis_area: Rect {
161                x: plot_x,
162                y: plot_y + plot_height,
163                width: plot_width,
164                height: x_axis_height,
165            },
166            y_axis_area: Rect {
167                x: margin.left + legend_left,
168                y: plot_y,
169                width: y_axis_width,
170                height: plot_height,
171            },
172            legend_area: Rect {
173                x: plot_x + plot_width,
174                y: plot_y,
175                width: legend_size,
176                height: plot_height,
177            },
178        }
179    }
180}