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/// Whether title/subtitle/caption align to the panel or the whole plot width
20/// (R's `plot.title.position` / `plot.caption.position`).
21#[derive(Clone, Copy, Debug, Default, PartialEq)]
22pub enum TitlePosition {
23    #[default]
24    Panel,
25    Plot,
26}
27
28/// Corner for the `labs(tag)` label (R's `plot.tag.position`).
29#[derive(Clone, Copy, Debug, Default, PartialEq)]
30pub enum TagPosition {
31    #[default]
32    TopLeft,
33    TopRight,
34    BottomLeft,
35    BottomRight,
36}
37
38/// Layout direction of the legend keys (R's `legend.direction`).
39#[derive(Clone, Copy, Debug, PartialEq)]
40pub enum LegendDirection {
41    Vertical,
42    Horizontal,
43}
44
45/// Plot margins (in pixels).
46#[derive(Clone, Debug)]
47pub struct Margin {
48    pub top: f64,
49    pub right: f64,
50    pub bottom: f64,
51    pub left: f64,
52}
53
54impl Default for Margin {
55    fn default() -> Self {
56        Margin {
57            top: 10.0,
58            right: 10.0,
59            bottom: 10.0,
60            left: 10.0,
61        }
62    }
63}
64
65/// Complete theme specification for a plot.
66#[derive(Clone, Debug)]
67pub struct Theme {
68    // ── Existing fields ──
69    pub text: ElementText,
70    pub title: ElementText,
71    pub axis_text_x: ElementText,
72    pub axis_text_y: ElementText,
73    pub axis_title_x: ElementText,
74    pub axis_title_y: ElementText,
75    pub axis_line: ElementLine,
76    pub axis_ticks: ElementLine,
77    pub panel_background: ElementRect,
78    pub panel_grid_major: ElementLine,
79    pub panel_grid_minor: ElementLine,
80    pub plot_background: ElementRect,
81    pub legend_position: LegendPosition,
82    pub plot_margin: Margin,
83
84    // ── New text elements ──
85    pub subtitle: ElementText,
86    pub caption: ElementText,
87    pub legend_title: ElementText,
88    pub legend_text: ElementText,
89    pub strip_text: ElementText,
90
91    // ── Per-axis optional overrides (None = inherit from parent) ──
92    pub axis_line_x: Option<ElementLine>,
93    pub axis_line_y: Option<ElementLine>,
94    pub axis_ticks_x: Option<ElementLine>,
95    pub axis_ticks_y: Option<ElementLine>,
96    pub panel_grid_major_x: Option<ElementLine>,
97    pub panel_grid_major_y: Option<ElementLine>,
98    pub panel_grid_minor_x: Option<ElementLine>,
99    pub panel_grid_minor_y: Option<ElementLine>,
100
101    // ── New rect/line elements ──
102    pub panel_border: ElementLine,
103    pub legend_background: ElementRect,
104    pub legend_key: ElementRect,
105    pub strip_background: ElementRect,
106
107    // ── Scalar spacing/sizing ──
108    pub axis_ticks_length: f64,
109    /// Number of rows to stagger x-axis tick labels across (R's
110    /// `guide_axis(n.dodge = ...)`); 1 = no dodging.
111    pub axis_text_x_dodge: usize,
112    pub legend_key_width: f64,
113    pub legend_key_height: f64,
114    pub legend_spacing: f64,
115    pub legend_margin: Margin,
116    pub panel_spacing: f64,
117    pub panel_spacing_x: Option<f64>,
118    pub panel_spacing_y: Option<f64>,
119
120    // ── Brand / primary color ──
121    /// Optional brand color. When set, geoms that draw a single un-mapped series
122    /// (no color/fill aesthetic) use it as their default instead of the geom's
123    /// built-in color. Lets one render process serve multiple tenants' brands.
124    pub primary: Option<(u8, u8, u8)>,
125
126    // ── Advanced layout (R's theme() extras) ──
127    /// Fix the panel's height:width ratio (R's `aspect.ratio`). `None` = free.
128    pub aspect_ratio: Option<f64>,
129    /// Draw gridlines on top of the data layers (R's `panel.ontop`).
130    pub panel_ontop: bool,
131    /// Draw minor tick marks between major ticks (R's `axis.minor.ticks`).
132    pub axis_minor_ticks: bool,
133    /// Whether title/subtitle/caption align to the panel or the whole plot
134    /// (R's `plot.title.position` / `plot.caption.position`).
135    pub title_position: TitlePosition,
136    /// Corner for the `labs(tag)` label (R's `plot.tag.position`).
137    pub tag_position: TagPosition,
138    /// Legend key layout direction; `None` = auto from `legend_position`.
139    pub legend_direction: Option<LegendDirection>,
140}
141
142impl Theme {
143    /// Resolve the effective series color: the theme's brand color if set,
144    /// otherwise the geom's own default.
145    pub fn primary_or(&self, fallback: (u8, u8, u8)) -> (u8, u8, u8) {
146        self.primary.unwrap_or(fallback)
147    }
148
149    /// Set the brand/primary color (builder style).
150    pub fn with_primary(mut self, color: (u8, u8, u8)) -> Self {
151        self.primary = Some(color);
152        self
153    }
154
155    /// Resolve R-style element inheritance: text-child elements still at the
156    /// default `family`/`color` inherit them from the root `text` element, so
157    /// setting only `theme.text.family` restyles every text element. Child
158    /// elements that already override a property keep it. Called once before
159    /// rendering.
160    pub fn resolve_inheritance(&mut self) {
161        let root = self.text.clone();
162        let def = ElementText::default();
163        let children: [&mut ElementText; 10] = [
164            &mut self.title,
165            &mut self.subtitle,
166            &mut self.caption,
167            &mut self.axis_text_x,
168            &mut self.axis_text_y,
169            &mut self.axis_title_x,
170            &mut self.axis_title_y,
171            &mut self.legend_title,
172            &mut self.legend_text,
173            &mut self.strip_text,
174        ];
175        for el in children {
176            if el.family == def.family {
177                el.family = root.family.clone();
178            }
179            if el.color == def.color {
180                el.color = root.color;
181            }
182        }
183    }
184
185    // ── Fallback accessors for Option fields ──
186
187    pub fn get_axis_line_x(&self) -> &ElementLine {
188        self.axis_line_x.as_ref().unwrap_or(&self.axis_line)
189    }
190
191    pub fn get_axis_line_y(&self) -> &ElementLine {
192        self.axis_line_y.as_ref().unwrap_or(&self.axis_line)
193    }
194
195    pub fn get_axis_ticks_x(&self) -> &ElementLine {
196        self.axis_ticks_x.as_ref().unwrap_or(&self.axis_ticks)
197    }
198
199    pub fn get_axis_ticks_y(&self) -> &ElementLine {
200        self.axis_ticks_y.as_ref().unwrap_or(&self.axis_ticks)
201    }
202
203    pub fn get_panel_grid_major_x(&self) -> &ElementLine {
204        self.panel_grid_major_x
205            .as_ref()
206            .unwrap_or(&self.panel_grid_major)
207    }
208
209    pub fn get_panel_grid_major_y(&self) -> &ElementLine {
210        self.panel_grid_major_y
211            .as_ref()
212            .unwrap_or(&self.panel_grid_major)
213    }
214
215    pub fn get_panel_grid_minor_x(&self) -> &ElementLine {
216        self.panel_grid_minor_x
217            .as_ref()
218            .unwrap_or(&self.panel_grid_minor)
219    }
220
221    pub fn get_panel_grid_minor_y(&self) -> &ElementLine {
222        self.panel_grid_minor_y
223            .as_ref()
224            .unwrap_or(&self.panel_grid_minor)
225    }
226
227    pub fn get_panel_spacing_x(&self) -> f64 {
228        self.panel_spacing_x.unwrap_or(self.panel_spacing)
229    }
230
231    pub fn get_panel_spacing_y(&self) -> f64 {
232        self.panel_spacing_y.unwrap_or(self.panel_spacing)
233    }
234
235    // ── Existing setters ──
236
237    pub fn set_axis_text_x(mut self, el: ElementText) -> Self {
238        self.axis_text_x = el;
239        self
240    }
241
242    pub fn set_axis_text_y(mut self, el: ElementText) -> Self {
243        self.axis_text_y = el;
244        self
245    }
246
247    pub fn set_axis_title_x(mut self, el: ElementText) -> Self {
248        self.axis_title_x = el;
249        self
250    }
251
252    pub fn set_axis_title_y(mut self, el: ElementText) -> Self {
253        self.axis_title_y = el;
254        self
255    }
256
257    pub fn set_axis_line(mut self, el: ElementLine) -> Self {
258        self.axis_line = el;
259        self
260    }
261
262    pub fn set_axis_ticks(mut self, el: ElementLine) -> Self {
263        self.axis_ticks = el;
264        self
265    }
266
267    pub fn set_panel_background(mut self, el: ElementRect) -> Self {
268        self.panel_background = el;
269        self
270    }
271
272    pub fn set_panel_grid_major(mut self, el: ElementLine) -> Self {
273        self.panel_grid_major = el;
274        self
275    }
276
277    pub fn set_panel_grid_minor(mut self, el: ElementLine) -> Self {
278        self.panel_grid_minor = el;
279        self
280    }
281
282    pub fn set_plot_background(mut self, el: ElementRect) -> Self {
283        self.plot_background = el;
284        self
285    }
286
287    pub fn set_legend_position(mut self, pos: LegendPosition) -> Self {
288        self.legend_position = pos;
289        self
290    }
291
292    pub fn set_plot_margin(mut self, margin: Margin) -> Self {
293        self.plot_margin = margin;
294        self
295    }
296
297    pub fn set_title(mut self, el: ElementText) -> Self {
298        self.title = el;
299        self
300    }
301
302    pub fn set_text(mut self, el: ElementText) -> Self {
303        self.text = el;
304        self
305    }
306
307    // ── New setters ──
308
309    pub fn set_subtitle(mut self, el: ElementText) -> Self {
310        self.subtitle = el;
311        self
312    }
313
314    pub fn set_caption(mut self, el: ElementText) -> Self {
315        self.caption = el;
316        self
317    }
318
319    pub fn set_legend_title(mut self, el: ElementText) -> Self {
320        self.legend_title = el;
321        self
322    }
323
324    pub fn set_legend_text(mut self, el: ElementText) -> Self {
325        self.legend_text = el;
326        self
327    }
328
329    pub fn set_strip_text(mut self, el: ElementText) -> Self {
330        self.strip_text = el;
331        self
332    }
333
334    pub fn set_axis_line_x(mut self, el: Option<ElementLine>) -> Self {
335        self.axis_line_x = el;
336        self
337    }
338
339    pub fn set_axis_line_y(mut self, el: Option<ElementLine>) -> Self {
340        self.axis_line_y = el;
341        self
342    }
343
344    pub fn set_axis_ticks_x(mut self, el: Option<ElementLine>) -> Self {
345        self.axis_ticks_x = el;
346        self
347    }
348
349    pub fn set_axis_ticks_y(mut self, el: Option<ElementLine>) -> Self {
350        self.axis_ticks_y = el;
351        self
352    }
353
354    pub fn set_panel_grid_major_x(mut self, el: Option<ElementLine>) -> Self {
355        self.panel_grid_major_x = el;
356        self
357    }
358
359    pub fn set_panel_grid_major_y(mut self, el: Option<ElementLine>) -> Self {
360        self.panel_grid_major_y = el;
361        self
362    }
363
364    pub fn set_panel_grid_minor_x(mut self, el: Option<ElementLine>) -> Self {
365        self.panel_grid_minor_x = el;
366        self
367    }
368
369    pub fn set_panel_grid_minor_y(mut self, el: Option<ElementLine>) -> Self {
370        self.panel_grid_minor_y = el;
371        self
372    }
373
374    pub fn set_panel_border(mut self, el: ElementLine) -> Self {
375        self.panel_border = el;
376        self
377    }
378
379    pub fn set_legend_background(mut self, el: ElementRect) -> Self {
380        self.legend_background = el;
381        self
382    }
383
384    pub fn set_legend_key(mut self, el: ElementRect) -> Self {
385        self.legend_key = el;
386        self
387    }
388
389    pub fn set_strip_background(mut self, el: ElementRect) -> Self {
390        self.strip_background = el;
391        self
392    }
393
394    pub fn set_axis_ticks_length(mut self, val: f64) -> Self {
395        self.axis_ticks_length = val;
396        self
397    }
398
399    pub fn set_legend_key_width(mut self, val: f64) -> Self {
400        self.legend_key_width = val;
401        self
402    }
403
404    pub fn set_legend_key_height(mut self, val: f64) -> Self {
405        self.legend_key_height = val;
406        self
407    }
408
409    pub fn set_legend_spacing(mut self, val: f64) -> Self {
410        self.legend_spacing = val;
411        self
412    }
413
414    pub fn set_legend_margin(mut self, margin: Margin) -> Self {
415        self.legend_margin = margin;
416        self
417    }
418
419    pub fn set_panel_spacing(mut self, val: f64) -> Self {
420        self.panel_spacing = val;
421        self
422    }
423
424    pub fn set_panel_spacing_x(mut self, val: Option<f64>) -> Self {
425        self.panel_spacing_x = val;
426        self
427    }
428
429    pub fn set_panel_spacing_y(mut self, val: Option<f64>) -> Self {
430        self.panel_spacing_y = val;
431        self
432    }
433
434    /// Apply incremental theme modifications.
435    /// Only fields that are `Some` in the update are applied.
436    pub fn update(mut self, upd: ThemeUpdate) -> Self {
437        if let Some(v) = upd.text {
438            self.text = v;
439        }
440        if let Some(v) = upd.title {
441            self.title = v;
442        }
443        if let Some(v) = upd.subtitle {
444            self.subtitle = v;
445        }
446        if let Some(v) = upd.caption {
447            self.caption = v;
448        }
449        if let Some(v) = upd.axis_text_x {
450            self.axis_text_x = v;
451        }
452        if let Some(v) = upd.axis_text_y {
453            self.axis_text_y = v;
454        }
455        if let Some(v) = upd.axis_title_x {
456            self.axis_title_x = v;
457        }
458        if let Some(v) = upd.axis_title_y {
459            self.axis_title_y = v;
460        }
461        if let Some(v) = upd.axis_line {
462            self.axis_line = v;
463        }
464        if let Some(v) = upd.axis_ticks {
465            self.axis_ticks = v;
466        }
467        if let Some(v) = upd.panel_background {
468            self.panel_background = v;
469        }
470        if let Some(v) = upd.panel_grid_major {
471            self.panel_grid_major = v;
472        }
473        if let Some(v) = upd.panel_grid_minor {
474            self.panel_grid_minor = v;
475        }
476        if let Some(v) = upd.panel_border {
477            self.panel_border = v;
478        }
479        if let Some(v) = upd.plot_background {
480            self.plot_background = v;
481        }
482        if let Some(v) = upd.legend_position {
483            self.legend_position = v;
484        }
485        if let Some(v) = upd.legend_title {
486            self.legend_title = v;
487        }
488        if let Some(v) = upd.legend_text {
489            self.legend_text = v;
490        }
491        if let Some(v) = upd.legend_background {
492            self.legend_background = v;
493        }
494        if let Some(v) = upd.strip_text {
495            self.strip_text = v;
496        }
497        if let Some(v) = upd.strip_background {
498            self.strip_background = v;
499        }
500        if let Some(v) = upd.plot_margin {
501            self.plot_margin = v;
502        }
503        self
504    }
505}
506
507/// Incremental theme modifications. All fields are optional — only `Some` values are applied.
508/// Like R's `theme(axis.text.x = element_text(...))`.
509#[derive(Clone, Debug, Default)]
510pub struct ThemeUpdate {
511    pub text: Option<ElementText>,
512    pub title: Option<ElementText>,
513    pub subtitle: Option<ElementText>,
514    pub caption: Option<ElementText>,
515    pub axis_text_x: Option<ElementText>,
516    pub axis_text_y: Option<ElementText>,
517    pub axis_title_x: Option<ElementText>,
518    pub axis_title_y: Option<ElementText>,
519    pub axis_line: Option<ElementLine>,
520    pub axis_ticks: Option<ElementLine>,
521    pub panel_background: Option<ElementRect>,
522    pub panel_grid_major: Option<ElementLine>,
523    pub panel_grid_minor: Option<ElementLine>,
524    pub panel_border: Option<ElementLine>,
525    pub plot_background: Option<ElementRect>,
526    pub legend_position: Option<LegendPosition>,
527    pub legend_title: Option<ElementText>,
528    pub legend_text: Option<ElementText>,
529    pub legend_background: Option<ElementRect>,
530    pub strip_text: Option<ElementText>,
531    pub strip_background: Option<ElementRect>,
532    pub plot_margin: Option<Margin>,
533}
534
535impl Default for Theme {
536    fn default() -> Self {
537        presets::theme_gray()
538    }
539}