Skip to main content

ggplot_rs/theme/
mod.rs

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