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::{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}
149
150impl Theme {
151    /// Resolve the effective series color: the theme's brand color if set,
152    /// otherwise the geom's own default.
153    pub fn primary_or(&self, fallback: (u8, u8, u8)) -> (u8, u8, u8) {
154        self.primary.unwrap_or(fallback)
155    }
156
157    /// Set the brand/primary color (builder style).
158    pub fn with_primary(mut self, color: (u8, u8, u8)) -> Self {
159        self.primary = Some(color);
160        self
161    }
162
163    /// Resolve R-style element inheritance: text-child elements still at the
164    /// default `family`/`color` inherit them from the root `text` element, so
165    /// setting only `theme.text.family` restyles every text element. Child
166    /// elements that already override a property keep it. Called once before
167    /// rendering.
168    pub fn resolve_inheritance(&mut self) {
169        let root = self.text.clone();
170        let def = ElementText::default();
171        let children: [&mut ElementText; 10] = [
172            &mut self.title,
173            &mut self.subtitle,
174            &mut self.caption,
175            &mut self.axis_text_x,
176            &mut self.axis_text_y,
177            &mut self.axis_title_x,
178            &mut self.axis_title_y,
179            &mut self.legend_title,
180            &mut self.legend_text,
181            &mut self.strip_text,
182        ];
183        for el in children {
184            if el.family == def.family {
185                el.family = root.family.clone();
186            }
187            if el.color == def.color {
188                el.color = root.color;
189            }
190        }
191    }
192
193    // ── Fallback accessors for Option fields ──
194
195    pub fn get_axis_line_x(&self) -> &ElementLine {
196        self.axis_line_x.as_ref().unwrap_or(&self.axis_line)
197    }
198
199    pub fn get_axis_line_y(&self) -> &ElementLine {
200        self.axis_line_y.as_ref().unwrap_or(&self.axis_line)
201    }
202
203    pub fn get_axis_ticks_x(&self) -> &ElementLine {
204        self.axis_ticks_x.as_ref().unwrap_or(&self.axis_ticks)
205    }
206
207    pub fn get_axis_ticks_y(&self) -> &ElementLine {
208        self.axis_ticks_y.as_ref().unwrap_or(&self.axis_ticks)
209    }
210
211    pub fn get_panel_grid_major_x(&self) -> &ElementLine {
212        self.panel_grid_major_x
213            .as_ref()
214            .unwrap_or(&self.panel_grid_major)
215    }
216
217    pub fn get_panel_grid_major_y(&self) -> &ElementLine {
218        self.panel_grid_major_y
219            .as_ref()
220            .unwrap_or(&self.panel_grid_major)
221    }
222
223    pub fn get_panel_grid_minor_x(&self) -> &ElementLine {
224        self.panel_grid_minor_x
225            .as_ref()
226            .unwrap_or(&self.panel_grid_minor)
227    }
228
229    pub fn get_panel_grid_minor_y(&self) -> &ElementLine {
230        self.panel_grid_minor_y
231            .as_ref()
232            .unwrap_or(&self.panel_grid_minor)
233    }
234
235    pub fn get_panel_spacing_x(&self) -> f64 {
236        self.panel_spacing_x.unwrap_or(self.panel_spacing)
237    }
238
239    pub fn get_panel_spacing_y(&self) -> f64 {
240        self.panel_spacing_y.unwrap_or(self.panel_spacing)
241    }
242
243    // ── Existing setters ──
244
245    pub fn set_axis_text_x(mut self, el: ElementText) -> Self {
246        self.axis_text_x = el;
247        self
248    }
249
250    pub fn set_axis_text_y(mut self, el: ElementText) -> Self {
251        self.axis_text_y = el;
252        self
253    }
254
255    pub fn set_axis_title_x(mut self, el: ElementText) -> Self {
256        self.axis_title_x = el;
257        self
258    }
259
260    pub fn set_axis_title_y(mut self, el: ElementText) -> Self {
261        self.axis_title_y = el;
262        self
263    }
264
265    pub fn set_axis_line(mut self, el: ElementLine) -> Self {
266        self.axis_line = el;
267        self
268    }
269
270    pub fn set_axis_ticks(mut self, el: ElementLine) -> Self {
271        self.axis_ticks = el;
272        self
273    }
274
275    pub fn set_panel_background(mut self, el: ElementRect) -> Self {
276        self.panel_background = el;
277        self
278    }
279
280    pub fn set_panel_grid_major(mut self, el: ElementLine) -> Self {
281        self.panel_grid_major = el;
282        self
283    }
284
285    pub fn set_panel_grid_minor(mut self, el: ElementLine) -> Self {
286        self.panel_grid_minor = el;
287        self
288    }
289
290    pub fn set_plot_background(mut self, el: ElementRect) -> Self {
291        self.plot_background = el;
292        self
293    }
294
295    pub fn set_legend_position(mut self, pos: LegendPosition) -> Self {
296        self.legend_position = pos;
297        self
298    }
299
300    pub fn set_plot_margin(mut self, margin: Margin) -> Self {
301        self.plot_margin = margin;
302        self
303    }
304
305    pub fn set_title(mut self, el: ElementText) -> Self {
306        self.title = el;
307        self
308    }
309
310    pub fn set_text(mut self, el: ElementText) -> Self {
311        self.text = el;
312        self
313    }
314
315    // ── New setters ──
316
317    pub fn set_subtitle(mut self, el: ElementText) -> Self {
318        self.subtitle = el;
319        self
320    }
321
322    pub fn set_caption(mut self, el: ElementText) -> Self {
323        self.caption = el;
324        self
325    }
326
327    pub fn set_legend_title(mut self, el: ElementText) -> Self {
328        self.legend_title = el;
329        self
330    }
331
332    pub fn set_legend_text(mut self, el: ElementText) -> Self {
333        self.legend_text = el;
334        self
335    }
336
337    pub fn set_strip_text(mut self, el: ElementText) -> Self {
338        self.strip_text = el;
339        self
340    }
341
342    pub fn set_axis_line_x(mut self, el: Option<ElementLine>) -> Self {
343        self.axis_line_x = el;
344        self
345    }
346
347    pub fn set_axis_line_y(mut self, el: Option<ElementLine>) -> Self {
348        self.axis_line_y = el;
349        self
350    }
351
352    pub fn set_axis_ticks_x(mut self, el: Option<ElementLine>) -> Self {
353        self.axis_ticks_x = el;
354        self
355    }
356
357    pub fn set_axis_ticks_y(mut self, el: Option<ElementLine>) -> Self {
358        self.axis_ticks_y = el;
359        self
360    }
361
362    pub fn set_panel_grid_major_x(mut self, el: Option<ElementLine>) -> Self {
363        self.panel_grid_major_x = el;
364        self
365    }
366
367    pub fn set_panel_grid_major_y(mut self, el: Option<ElementLine>) -> Self {
368        self.panel_grid_major_y = el;
369        self
370    }
371
372    pub fn set_panel_grid_minor_x(mut self, el: Option<ElementLine>) -> Self {
373        self.panel_grid_minor_x = el;
374        self
375    }
376
377    pub fn set_panel_grid_minor_y(mut self, el: Option<ElementLine>) -> Self {
378        self.panel_grid_minor_y = el;
379        self
380    }
381
382    pub fn set_panel_border(mut self, el: ElementLine) -> Self {
383        self.panel_border = el;
384        self
385    }
386
387    /// The effective colourbar frame: [`legend_bar_border`](Self::legend_bar_border),
388    /// else `legend_key.color` (dark grey when unset), 0.5 wide.
389    pub fn legend_bar_border_line(&self) -> ElementLine {
390        self.legend_bar_border.clone().unwrap_or(ElementLine {
391            color: self.legend_key.color.unwrap_or((50, 50, 50)),
392            width: 0.5,
393            visible: true,
394            linetype: crate::render::backend::Linetype::Solid,
395        })
396    }
397
398    pub fn set_calendar_month_border(mut self, el: ElementLine) -> Self {
399        self.calendar_month_border = el;
400        self
401    }
402
403    pub fn set_legend_bar_border(mut self, el: Option<ElementLine>) -> Self {
404        self.legend_bar_border = el;
405        self
406    }
407
408    pub fn set_legend_background(mut self, el: ElementRect) -> Self {
409        self.legend_background = el;
410        self
411    }
412
413    pub fn set_legend_key(mut self, el: ElementRect) -> Self {
414        self.legend_key = el;
415        self
416    }
417
418    pub fn set_strip_background(mut self, el: ElementRect) -> Self {
419        self.strip_background = el;
420        self
421    }
422
423    pub fn set_axis_ticks_length(mut self, val: f64) -> Self {
424        self.axis_ticks_length = val;
425        self
426    }
427
428    pub fn set_legend_key_width(mut self, val: f64) -> Self {
429        self.legend_key_width = val;
430        self
431    }
432
433    pub fn set_legend_key_height(mut self, val: f64) -> Self {
434        self.legend_key_height = val;
435        self
436    }
437
438    pub fn set_legend_spacing(mut self, val: f64) -> Self {
439        self.legend_spacing = val;
440        self
441    }
442
443    pub fn set_legend_margin(mut self, margin: Margin) -> Self {
444        self.legend_margin = margin;
445        self
446    }
447
448    pub fn set_panel_spacing(mut self, val: f64) -> Self {
449        self.panel_spacing = val;
450        self
451    }
452
453    pub fn set_panel_spacing_x(mut self, val: Option<f64>) -> Self {
454        self.panel_spacing_x = val;
455        self
456    }
457
458    pub fn set_panel_spacing_y(mut self, val: Option<f64>) -> Self {
459        self.panel_spacing_y = val;
460        self
461    }
462
463    /// Apply incremental theme modifications.
464    /// Only fields that are `Some` in the update are applied.
465    pub fn update(mut self, upd: ThemeUpdate) -> Self {
466        if let Some(v) = upd.text {
467            self.text = v;
468        }
469        if let Some(v) = upd.title {
470            self.title = v;
471        }
472        if let Some(v) = upd.subtitle {
473            self.subtitle = v;
474        }
475        if let Some(v) = upd.caption {
476            self.caption = v;
477        }
478        if let Some(v) = upd.axis_text_x {
479            self.axis_text_x = v;
480        }
481        if let Some(v) = upd.axis_text_y {
482            self.axis_text_y = v;
483        }
484        if let Some(v) = upd.axis_title_x {
485            self.axis_title_x = v;
486        }
487        if let Some(v) = upd.axis_title_y {
488            self.axis_title_y = v;
489        }
490        if let Some(v) = upd.axis_line {
491            self.axis_line = v;
492        }
493        if let Some(v) = upd.axis_ticks {
494            self.axis_ticks = v;
495        }
496        if let Some(v) = upd.panel_background {
497            self.panel_background = v;
498        }
499        if let Some(v) = upd.panel_grid_major {
500            self.panel_grid_major = v;
501        }
502        if let Some(v) = upd.panel_grid_minor {
503            self.panel_grid_minor = v;
504        }
505        if let Some(v) = upd.panel_border {
506            self.panel_border = v;
507        }
508        if let Some(v) = upd.calendar_month_border {
509            self.calendar_month_border = v;
510        }
511        if let Some(v) = upd.legend_bar_border {
512            self.legend_bar_border = Some(v);
513        }
514        if let Some(v) = upd.plot_background {
515            self.plot_background = v;
516        }
517        if let Some(v) = upd.legend_position {
518            self.legend_position = v;
519        }
520        if let Some(v) = upd.legend_title {
521            self.legend_title = v;
522        }
523        if let Some(v) = upd.legend_text {
524            self.legend_text = v;
525        }
526        if let Some(v) = upd.legend_background {
527            self.legend_background = v;
528        }
529        if let Some(v) = upd.strip_text {
530            self.strip_text = v;
531        }
532        if let Some(v) = upd.strip_background {
533            self.strip_background = v;
534        }
535        if let Some(v) = upd.plot_margin {
536            self.plot_margin = v;
537        }
538        self
539    }
540}
541
542/// Incremental theme modifications. All fields are optional — only `Some` values are applied.
543/// Like R's `theme(axis.text.x = element_text(...))`.
544#[derive(Clone, Debug, Default)]
545pub struct ThemeUpdate {
546    pub text: Option<ElementText>,
547    pub title: Option<ElementText>,
548    pub subtitle: Option<ElementText>,
549    pub caption: Option<ElementText>,
550    pub axis_text_x: Option<ElementText>,
551    pub axis_text_y: Option<ElementText>,
552    pub axis_title_x: Option<ElementText>,
553    pub axis_title_y: Option<ElementText>,
554    pub axis_line: Option<ElementLine>,
555    pub axis_ticks: Option<ElementLine>,
556    pub panel_background: Option<ElementRect>,
557    pub panel_grid_major: Option<ElementLine>,
558    pub panel_grid_minor: Option<ElementLine>,
559    pub panel_border: Option<ElementLine>,
560    pub calendar_month_border: Option<ElementLine>,
561    pub legend_bar_border: Option<ElementLine>,
562    pub plot_background: Option<ElementRect>,
563    pub legend_position: Option<LegendPosition>,
564    pub legend_title: Option<ElementText>,
565    pub legend_text: Option<ElementText>,
566    pub legend_background: Option<ElementRect>,
567    pub strip_text: Option<ElementText>,
568    pub strip_background: Option<ElementRect>,
569    pub plot_margin: Option<Margin>,
570}
571
572impl Default for Theme {
573    fn default() -> Self {
574        presets::theme_gray()
575    }
576}