Skip to main content

ggplot_rs/theme/
mod.rs

1pub mod elements;
2pub mod presets;
3
4pub use elements::{ElementLine, ElementRect, ElementText};
5
6/// Position of the legend.
7#[derive(Clone, Debug)]
8pub enum LegendPosition {
9    Right,
10    Left,
11    Top,
12    Bottom,
13    None,
14    /// Inside the panel at panel-relative coordinates (0..1, 0..1), like R's
15    /// `legend.position = c(x, y)`. `(0, 0)` is bottom-left, `(1, 1)` top-right.
16    Inside(f64, f64),
17}
18
19/// Plot margins (in pixels).
20#[derive(Clone, Debug)]
21pub struct Margin {
22    pub top: f64,
23    pub right: f64,
24    pub bottom: f64,
25    pub left: f64,
26}
27
28impl Default for Margin {
29    fn default() -> Self {
30        Margin {
31            top: 10.0,
32            right: 10.0,
33            bottom: 10.0,
34            left: 10.0,
35        }
36    }
37}
38
39/// Complete theme specification for a plot.
40#[derive(Clone, Debug)]
41pub struct Theme {
42    // ── Existing fields ──
43    pub text: ElementText,
44    pub title: ElementText,
45    pub axis_text_x: ElementText,
46    pub axis_text_y: ElementText,
47    pub axis_title_x: ElementText,
48    pub axis_title_y: ElementText,
49    pub axis_line: ElementLine,
50    pub axis_ticks: ElementLine,
51    pub panel_background: ElementRect,
52    pub panel_grid_major: ElementLine,
53    pub panel_grid_minor: ElementLine,
54    pub plot_background: ElementRect,
55    pub legend_position: LegendPosition,
56    pub plot_margin: Margin,
57
58    // ── New text elements ──
59    pub subtitle: ElementText,
60    pub caption: ElementText,
61    pub legend_title: ElementText,
62    pub legend_text: ElementText,
63    pub strip_text: ElementText,
64
65    // ── Per-axis optional overrides (None = inherit from parent) ──
66    pub axis_line_x: Option<ElementLine>,
67    pub axis_line_y: Option<ElementLine>,
68    pub axis_ticks_x: Option<ElementLine>,
69    pub axis_ticks_y: Option<ElementLine>,
70    pub panel_grid_major_x: Option<ElementLine>,
71    pub panel_grid_major_y: Option<ElementLine>,
72    pub panel_grid_minor_x: Option<ElementLine>,
73    pub panel_grid_minor_y: Option<ElementLine>,
74
75    // ── New rect/line elements ──
76    pub panel_border: ElementLine,
77    pub legend_background: ElementRect,
78    pub legend_key: ElementRect,
79    pub strip_background: ElementRect,
80
81    // ── Scalar spacing/sizing ──
82    pub axis_ticks_length: f64,
83    pub legend_key_width: f64,
84    pub legend_key_height: f64,
85    pub legend_spacing: f64,
86    pub legend_margin: Margin,
87    pub panel_spacing: f64,
88    pub panel_spacing_x: Option<f64>,
89    pub panel_spacing_y: Option<f64>,
90
91    // ── Brand / primary color ──
92    /// Optional brand color. When set, geoms that draw a single un-mapped series
93    /// (no color/fill aesthetic) use it as their default instead of the geom's
94    /// built-in color. Lets one render process serve multiple tenants' brands.
95    pub primary: Option<(u8, u8, u8)>,
96}
97
98impl Theme {
99    /// Resolve the effective series color: the theme's brand color if set,
100    /// otherwise the geom's own default.
101    pub fn primary_or(&self, fallback: (u8, u8, u8)) -> (u8, u8, u8) {
102        self.primary.unwrap_or(fallback)
103    }
104
105    /// Set the brand/primary color (builder style).
106    pub fn with_primary(mut self, color: (u8, u8, u8)) -> Self {
107        self.primary = Some(color);
108        self
109    }
110
111    // ── Fallback accessors for Option fields ──
112
113    pub fn get_axis_line_x(&self) -> &ElementLine {
114        self.axis_line_x.as_ref().unwrap_or(&self.axis_line)
115    }
116
117    pub fn get_axis_line_y(&self) -> &ElementLine {
118        self.axis_line_y.as_ref().unwrap_or(&self.axis_line)
119    }
120
121    pub fn get_axis_ticks_x(&self) -> &ElementLine {
122        self.axis_ticks_x.as_ref().unwrap_or(&self.axis_ticks)
123    }
124
125    pub fn get_axis_ticks_y(&self) -> &ElementLine {
126        self.axis_ticks_y.as_ref().unwrap_or(&self.axis_ticks)
127    }
128
129    pub fn get_panel_grid_major_x(&self) -> &ElementLine {
130        self.panel_grid_major_x
131            .as_ref()
132            .unwrap_or(&self.panel_grid_major)
133    }
134
135    pub fn get_panel_grid_major_y(&self) -> &ElementLine {
136        self.panel_grid_major_y
137            .as_ref()
138            .unwrap_or(&self.panel_grid_major)
139    }
140
141    pub fn get_panel_grid_minor_x(&self) -> &ElementLine {
142        self.panel_grid_minor_x
143            .as_ref()
144            .unwrap_or(&self.panel_grid_minor)
145    }
146
147    pub fn get_panel_grid_minor_y(&self) -> &ElementLine {
148        self.panel_grid_minor_y
149            .as_ref()
150            .unwrap_or(&self.panel_grid_minor)
151    }
152
153    pub fn get_panel_spacing_x(&self) -> f64 {
154        self.panel_spacing_x.unwrap_or(self.panel_spacing)
155    }
156
157    pub fn get_panel_spacing_y(&self) -> f64 {
158        self.panel_spacing_y.unwrap_or(self.panel_spacing)
159    }
160
161    // ── Existing setters ──
162
163    pub fn set_axis_text_x(mut self, el: ElementText) -> Self {
164        self.axis_text_x = el;
165        self
166    }
167
168    pub fn set_axis_text_y(mut self, el: ElementText) -> Self {
169        self.axis_text_y = el;
170        self
171    }
172
173    pub fn set_axis_title_x(mut self, el: ElementText) -> Self {
174        self.axis_title_x = el;
175        self
176    }
177
178    pub fn set_axis_title_y(mut self, el: ElementText) -> Self {
179        self.axis_title_y = el;
180        self
181    }
182
183    pub fn set_axis_line(mut self, el: ElementLine) -> Self {
184        self.axis_line = el;
185        self
186    }
187
188    pub fn set_axis_ticks(mut self, el: ElementLine) -> Self {
189        self.axis_ticks = el;
190        self
191    }
192
193    pub fn set_panel_background(mut self, el: ElementRect) -> Self {
194        self.panel_background = el;
195        self
196    }
197
198    pub fn set_panel_grid_major(mut self, el: ElementLine) -> Self {
199        self.panel_grid_major = el;
200        self
201    }
202
203    pub fn set_panel_grid_minor(mut self, el: ElementLine) -> Self {
204        self.panel_grid_minor = el;
205        self
206    }
207
208    pub fn set_plot_background(mut self, el: ElementRect) -> Self {
209        self.plot_background = el;
210        self
211    }
212
213    pub fn set_legend_position(mut self, pos: LegendPosition) -> Self {
214        self.legend_position = pos;
215        self
216    }
217
218    pub fn set_plot_margin(mut self, margin: Margin) -> Self {
219        self.plot_margin = margin;
220        self
221    }
222
223    pub fn set_title(mut self, el: ElementText) -> Self {
224        self.title = el;
225        self
226    }
227
228    pub fn set_text(mut self, el: ElementText) -> Self {
229        self.text = el;
230        self
231    }
232
233    // ── New setters ──
234
235    pub fn set_subtitle(mut self, el: ElementText) -> Self {
236        self.subtitle = el;
237        self
238    }
239
240    pub fn set_caption(mut self, el: ElementText) -> Self {
241        self.caption = el;
242        self
243    }
244
245    pub fn set_legend_title(mut self, el: ElementText) -> Self {
246        self.legend_title = el;
247        self
248    }
249
250    pub fn set_legend_text(mut self, el: ElementText) -> Self {
251        self.legend_text = el;
252        self
253    }
254
255    pub fn set_strip_text(mut self, el: ElementText) -> Self {
256        self.strip_text = el;
257        self
258    }
259
260    pub fn set_axis_line_x(mut self, el: Option<ElementLine>) -> Self {
261        self.axis_line_x = el;
262        self
263    }
264
265    pub fn set_axis_line_y(mut self, el: Option<ElementLine>) -> Self {
266        self.axis_line_y = el;
267        self
268    }
269
270    pub fn set_axis_ticks_x(mut self, el: Option<ElementLine>) -> Self {
271        self.axis_ticks_x = el;
272        self
273    }
274
275    pub fn set_axis_ticks_y(mut self, el: Option<ElementLine>) -> Self {
276        self.axis_ticks_y = el;
277        self
278    }
279
280    pub fn set_panel_grid_major_x(mut self, el: Option<ElementLine>) -> Self {
281        self.panel_grid_major_x = el;
282        self
283    }
284
285    pub fn set_panel_grid_major_y(mut self, el: Option<ElementLine>) -> Self {
286        self.panel_grid_major_y = el;
287        self
288    }
289
290    pub fn set_panel_grid_minor_x(mut self, el: Option<ElementLine>) -> Self {
291        self.panel_grid_minor_x = el;
292        self
293    }
294
295    pub fn set_panel_grid_minor_y(mut self, el: Option<ElementLine>) -> Self {
296        self.panel_grid_minor_y = el;
297        self
298    }
299
300    pub fn set_panel_border(mut self, el: ElementLine) -> Self {
301        self.panel_border = el;
302        self
303    }
304
305    pub fn set_legend_background(mut self, el: ElementRect) -> Self {
306        self.legend_background = el;
307        self
308    }
309
310    pub fn set_legend_key(mut self, el: ElementRect) -> Self {
311        self.legend_key = el;
312        self
313    }
314
315    pub fn set_strip_background(mut self, el: ElementRect) -> Self {
316        self.strip_background = el;
317        self
318    }
319
320    pub fn set_axis_ticks_length(mut self, val: f64) -> Self {
321        self.axis_ticks_length = val;
322        self
323    }
324
325    pub fn set_legend_key_width(mut self, val: f64) -> Self {
326        self.legend_key_width = val;
327        self
328    }
329
330    pub fn set_legend_key_height(mut self, val: f64) -> Self {
331        self.legend_key_height = val;
332        self
333    }
334
335    pub fn set_legend_spacing(mut self, val: f64) -> Self {
336        self.legend_spacing = val;
337        self
338    }
339
340    pub fn set_legend_margin(mut self, margin: Margin) -> Self {
341        self.legend_margin = margin;
342        self
343    }
344
345    pub fn set_panel_spacing(mut self, val: f64) -> Self {
346        self.panel_spacing = val;
347        self
348    }
349
350    pub fn set_panel_spacing_x(mut self, val: Option<f64>) -> Self {
351        self.panel_spacing_x = val;
352        self
353    }
354
355    pub fn set_panel_spacing_y(mut self, val: Option<f64>) -> Self {
356        self.panel_spacing_y = val;
357        self
358    }
359
360    /// Apply incremental theme modifications.
361    /// Only fields that are `Some` in the update are applied.
362    pub fn update(mut self, upd: ThemeUpdate) -> Self {
363        if let Some(v) = upd.text {
364            self.text = v;
365        }
366        if let Some(v) = upd.title {
367            self.title = v;
368        }
369        if let Some(v) = upd.subtitle {
370            self.subtitle = v;
371        }
372        if let Some(v) = upd.caption {
373            self.caption = v;
374        }
375        if let Some(v) = upd.axis_text_x {
376            self.axis_text_x = v;
377        }
378        if let Some(v) = upd.axis_text_y {
379            self.axis_text_y = v;
380        }
381        if let Some(v) = upd.axis_title_x {
382            self.axis_title_x = v;
383        }
384        if let Some(v) = upd.axis_title_y {
385            self.axis_title_y = v;
386        }
387        if let Some(v) = upd.axis_line {
388            self.axis_line = v;
389        }
390        if let Some(v) = upd.axis_ticks {
391            self.axis_ticks = v;
392        }
393        if let Some(v) = upd.panel_background {
394            self.panel_background = v;
395        }
396        if let Some(v) = upd.panel_grid_major {
397            self.panel_grid_major = v;
398        }
399        if let Some(v) = upd.panel_grid_minor {
400            self.panel_grid_minor = v;
401        }
402        if let Some(v) = upd.panel_border {
403            self.panel_border = v;
404        }
405        if let Some(v) = upd.plot_background {
406            self.plot_background = v;
407        }
408        if let Some(v) = upd.legend_position {
409            self.legend_position = v;
410        }
411        if let Some(v) = upd.legend_title {
412            self.legend_title = v;
413        }
414        if let Some(v) = upd.legend_text {
415            self.legend_text = v;
416        }
417        if let Some(v) = upd.legend_background {
418            self.legend_background = v;
419        }
420        if let Some(v) = upd.strip_text {
421            self.strip_text = v;
422        }
423        if let Some(v) = upd.strip_background {
424            self.strip_background = v;
425        }
426        if let Some(v) = upd.plot_margin {
427            self.plot_margin = v;
428        }
429        self
430    }
431}
432
433/// Incremental theme modifications. All fields are optional — only `Some` values are applied.
434/// Like R's `theme(axis.text.x = element_text(...))`.
435#[derive(Clone, Debug, Default)]
436pub struct ThemeUpdate {
437    pub text: Option<ElementText>,
438    pub title: Option<ElementText>,
439    pub subtitle: Option<ElementText>,
440    pub caption: Option<ElementText>,
441    pub axis_text_x: Option<ElementText>,
442    pub axis_text_y: Option<ElementText>,
443    pub axis_title_x: Option<ElementText>,
444    pub axis_title_y: Option<ElementText>,
445    pub axis_line: Option<ElementLine>,
446    pub axis_ticks: Option<ElementLine>,
447    pub panel_background: Option<ElementRect>,
448    pub panel_grid_major: Option<ElementLine>,
449    pub panel_grid_minor: Option<ElementLine>,
450    pub panel_border: Option<ElementLine>,
451    pub plot_background: Option<ElementRect>,
452    pub legend_position: Option<LegendPosition>,
453    pub legend_title: Option<ElementText>,
454    pub legend_text: Option<ElementText>,
455    pub legend_background: Option<ElementRect>,
456    pub strip_text: Option<ElementText>,
457    pub strip_background: Option<ElementRect>,
458    pub plot_margin: Option<Margin>,
459}
460
461impl Default for Theme {
462    fn default() -> Self {
463        presets::theme_gray()
464    }
465}