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