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