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/// A side of the panel an axis can sit on (#413's per-side elements).
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum AxisSide {
11    Top,
12    Bottom,
13    Left,
14    Right,
15}
16
17/// Position of the legend.
18#[derive(Clone, Debug, PartialEq)]
19pub enum LegendPosition {
20    Right,
21    Left,
22    Top,
23    Bottom,
24    None,
25    /// Inside the panel at panel-relative coordinates (0..1, 0..1), like R's
26    /// `legend.position = c(x, y)`. `(0, 0)` is bottom-left, `(1, 1)` top-right.
27    Inside(f64, f64),
28}
29
30/// A panel corner for an inside legend
31/// ([`GGPlot::legend_inside_auto_with`](crate::GGPlot::legend_inside_auto_with)).
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum LegendCorner {
34    TopLeft,
35    TopRight,
36    BottomLeft,
37    BottomRight,
38}
39
40impl LegendCorner {
41    /// Every corner, in the default preference order.
42    pub const ALL: [LegendCorner; 4] = [
43        LegendCorner::BottomRight,
44        LegendCorner::BottomLeft,
45        LegendCorner::TopRight,
46        LegendCorner::TopLeft,
47    ];
48
49    /// The matching [`LegendPosition::Inside`] fractions.
50    pub fn inside(self) -> LegendPosition {
51        match self {
52            LegendCorner::TopLeft => LegendPosition::Inside(0.0, 1.0),
53            LegendCorner::TopRight => LegendPosition::Inside(1.0, 1.0),
54            LegendCorner::BottomLeft => LegendPosition::Inside(0.0, 0.0),
55            LegendCorner::BottomRight => LegendPosition::Inside(1.0, 0.0),
56        }
57    }
58
59    /// `"top-left"` etc. (the `data-legend-at` value after `inside-`).
60    pub fn name(self) -> &'static str {
61        match self {
62            LegendCorner::TopLeft => "top-left",
63            LegendCorner::TopRight => "top-right",
64            LegendCorner::BottomLeft => "bottom-left",
65            LegendCorner::BottomRight => "bottom-right",
66        }
67    }
68}
69
70/// Whether title/subtitle/caption align to the panel or the whole plot width
71/// (R's `plot.title.position` / `plot.caption.position`).
72#[derive(Clone, Copy, Debug, Default, PartialEq)]
73pub enum TitlePosition {
74    #[default]
75    Panel,
76    Plot,
77}
78
79/// Corner for the `labs(tag)` label (R's `plot.tag.position`).
80#[derive(Clone, Copy, Debug, Default, PartialEq)]
81pub enum TagPosition {
82    #[default]
83    TopLeft,
84    TopRight,
85    BottomLeft,
86    BottomRight,
87}
88
89/// Layout direction of the legend keys (R's `legend.direction`).
90#[derive(Clone, Copy, Debug, PartialEq)]
91pub enum LegendDirection {
92    Vertical,
93    Horizontal,
94}
95
96/// How an outer x-axis tick label that would cross the SVG's edge is
97/// handled (B3-1; ggplot2 itself leaves this to the viewport/device and has
98/// no direct equivalent — this is a ggplot-rs extension, D4).
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
100pub enum AxisLabelFit {
101    /// The 0.21 behaviour: the label is drawn exactly where ggplot2's own
102    /// math puts it, even if that crosses the SVG edge.
103    Clip,
104    /// The panel keeps its size; an outer label that would cross the edge
105    /// is shifted inward just enough to stay inside, at draw time. Use this
106    /// for panels that must stay pixel-aligned with other elements (e.g. a
107    /// consumer's `::TRACK`-aligned strip).
108    Shift,
109    /// The layout reserves just the overflow as extra left/right panel
110    /// margin, so a plot whose outer labels already fit is byte-identical.
111    /// Anything still outside after that (e.g. a faceted plot, where the
112    /// reservation is skipped) is shifted inward at draw time, same as
113    /// [`Shift`](Self::Shift).
114    #[default]
115    Reserve,
116}
117
118/// Plot margins (in pixels).
119#[derive(Clone, Debug)]
120pub struct Margin {
121    pub top: f64,
122    pub right: f64,
123    pub bottom: f64,
124    pub left: f64,
125}
126
127impl Margin {
128    /// ggplot2's `margin_auto(t, r, b, l)`: CSS-style shorthand — one value
129    /// for all sides, two for (vertical, horizontal), three for (top,
130    /// horizontal, bottom), four for (top, right, bottom, left) (#413).
131    /// An empty slice is all zeros.
132    pub fn auto(values: &[f64]) -> Margin {
133        let (t, r, b, l) = match *values {
134            [] => (0.0, 0.0, 0.0, 0.0),
135            [a] => (a, a, a, a),
136            [v, h] => (v, h, v, h),
137            [t, h, b] => (t, h, b, h),
138            [t, r, b, l, ..] => (t, r, b, l),
139        };
140        Margin {
141            top: t,
142            right: r,
143            bottom: b,
144            left: l,
145        }
146    }
147
148    /// ggplot2's `margin_part()`: replace only the given sides of `self`
149    /// (`None` keeps the current value) (#413).
150    pub fn part(
151        self,
152        top: Option<f64>,
153        right: Option<f64>,
154        bottom: Option<f64>,
155        left: Option<f64>,
156    ) -> Margin {
157        Margin {
158            top: top.unwrap_or(self.top),
159            right: right.unwrap_or(self.right),
160            bottom: bottom.unwrap_or(self.bottom),
161            left: left.unwrap_or(self.left),
162        }
163    }
164}
165
166impl Default for Margin {
167    fn default() -> Self {
168        Margin {
169            top: 10.0,
170            right: 10.0,
171            bottom: 10.0,
172            left: 10.0,
173        }
174    }
175}
176
177/// Complete theme specification for a plot.
178#[derive(Clone, Debug)]
179pub struct Theme {
180    // ── Existing fields ──
181    pub text: ElementText,
182    pub title: ElementText,
183    pub axis_text_x: ElementText,
184    pub axis_text_y: ElementText,
185    pub axis_title_x: ElementText,
186    pub axis_title_y: ElementText,
187    pub axis_line: ElementLine,
188    pub axis_ticks: ElementLine,
189    pub panel_background: ElementRect,
190    pub panel_grid_major: ElementLine,
191    pub panel_grid_minor: ElementLine,
192    pub plot_background: ElementRect,
193    pub legend_position: LegendPosition,
194    pub plot_margin: Margin,
195
196    // ── New text elements ──
197    pub subtitle: ElementText,
198    pub caption: ElementText,
199    pub legend_title: ElementText,
200    pub legend_text: ElementText,
201    pub strip_text: ElementText,
202
203    // ── Per-axis optional overrides (None = inherit from parent) ──
204    pub axis_line_x: Option<ElementLine>,
205    pub axis_line_y: Option<ElementLine>,
206    pub axis_ticks_x: Option<ElementLine>,
207    pub axis_ticks_y: Option<ElementLine>,
208    /// One side's axis line / ticks (ggplot2's `axis.line.x.top`,
209    /// `axis.ticks.y.right`, …, #413); `None` = the axis's own
210    /// (`axis_line_x` …), then the shared one.
211    pub axis_line_x_top: Option<ElementLine>,
212    pub axis_line_x_bottom: Option<ElementLine>,
213    pub axis_line_y_left: Option<ElementLine>,
214    pub axis_line_y_right: Option<ElementLine>,
215    pub axis_ticks_x_top: Option<ElementLine>,
216    pub axis_ticks_x_bottom: Option<ElementLine>,
217    pub axis_ticks_y_left: Option<ElementLine>,
218    pub axis_ticks_y_right: Option<ElementLine>,
219    pub panel_grid_major_x: Option<ElementLine>,
220    pub panel_grid_major_y: Option<ElementLine>,
221    pub panel_grid_minor_x: Option<ElementLine>,
222    pub panel_grid_minor_y: Option<ElementLine>,
223
224    // ── New rect/line elements ──
225    pub panel_border: ElementLine,
226    pub legend_background: ElementRect,
227    pub legend_key: ElementRect,
228    pub strip_background: ElementRect,
229
230    // ── Scalar spacing/sizing ──
231    pub axis_ticks_length: f64,
232    /// Number of rows to stagger x-axis tick labels across (R's
233    /// `guide_axis(n.dodge = ...)`); 1 = no dodging.
234    pub axis_text_x_dodge: usize,
235    pub legend_key_width: f64,
236    pub legend_key_height: f64,
237    pub legend_spacing: f64,
238    /// A colourbar's tick length (ggplot2's `legend.ticks.length`, #413).
239    pub legend_ticks_length: f64,
240    /// What the caption aligns to (ggplot2's `plot.caption.position`,
241    /// #413); `None` follows `title_position`.
242    pub caption_position: Option<TitlePosition>,
243    /// Align an outside legend with the whole plot instead of the panel
244    /// (ggplot2's `legend.location = "plot"`, #413).
245    pub legend_location_plot: bool,
246    /// One axis's tick length (ggplot2's `axis.ticks.length.x` / `.y`,
247    /// #413); `None` = `axis_ticks_length`.
248    pub axis_ticks_length_x: Option<f64>,
249    pub axis_ticks_length_y: Option<f64>,
250    /// Extra gap between legend keys (ggplot2's `legend.key.spacing.x` /
251    /// `.y`, #413): `x` between the keys of a horizontal legend (default
252    /// `3 × legend_spacing`), `y` between rows (default none).
253    pub legend_key_spacing_x: Option<f64>,
254    pub legend_key_spacing_y: Option<f64>,
255    /// Gap between the panel and an outside legend (ggplot2's
256    /// `legend.box.spacing`, #413) — replaces `legend_margin`'s side that
257    /// faces the panel; `None` keeps it.
258    pub legend_box_spacing: Option<f64>,
259    /// Line several legends up along the stack's cross axis (ggplot2's
260    /// `legend.box.just`, #413): `0` left / top (the default), `0.5`
261    /// centre, `1` right / bottom.
262    pub legend_box_just: f64,
263    /// Read titles, axis titles, strip and legend text as a Markdown / HTML
264    /// subset (ggtext's `element_markdown()`, #413): `**bold**`, `*italic*`,
265    /// `^sup^`, `~sub~`, `<span style='color:…'>` — see [`crate::render::rich`].
266    pub rich_text: bool,
267    /// Place the tag in the panel's corner (ggplot2's `plot.tag.location =
268    /// "panel"`, #413); default the plot's.
269    pub tag_location_panel: bool,
270    /// Facet strips below / left of a panel sit outside its axis
271    /// (ggplot2's `strip.placement = "outside"`, #412); default inside.
272    pub strip_placement_outside: bool,
273    /// Gap between an axis and an outside strip (`strip.switch.pad.*`).
274    pub strip_switch_pad: f64,
275    pub legend_margin: Margin,
276    pub panel_spacing: f64,
277    pub panel_spacing_x: Option<f64>,
278    pub panel_spacing_y: Option<f64>,
279
280    // ── Brand / primary color ──
281    /// Optional brand color. When set, geoms that draw a single un-mapped series
282    /// (no color/fill aesthetic) use it as their default instead of the geom's
283    /// built-in color. Lets one render process serve multiple tenants' brands.
284    pub primary: Option<(u8, u8, u8)>,
285
286    // ── Advanced layout (R's theme() extras) ──
287    /// Fix the panel's height:width ratio (R's `aspect.ratio`). `None` = free.
288    pub aspect_ratio: Option<f64>,
289    /// Draw gridlines on top of the data layers (R's `panel.ontop`).
290    pub panel_ontop: bool,
291    /// Draw minor tick marks between major ticks (R's `axis.minor.ticks`).
292    pub axis_minor_ticks: bool,
293    /// Whether title/subtitle/caption align to the panel or the whole plot
294    /// (R's `plot.title.position` / `plot.caption.position`).
295    pub title_position: TitlePosition,
296    /// Corner for the `labs(tag)` label (R's `plot.tag.position`).
297    pub tag_position: TagPosition,
298    /// Legend key layout direction; `None` = auto from `legend_position`.
299    pub legend_direction: Option<LegendDirection>,
300    /// Month outlines of `geom_calendar`; `ElementLine::blank()` hides them.
301    pub calendar_month_border: ElementLine,
302    /// Frame around a continuous colourbar. `None` keeps the default
303    /// (`legend_key.color`, else dark grey, 0.5 wide); `ElementLine::blank()`
304    /// hides it.
305    pub legend_bar_border: Option<ElementLine>,
306    /// Geom ink/paper/accent/family palette (ggplot2 4.0's `element_geom`,
307    /// B2-7). Every field `None` (the default) changes nothing.
308    pub geom: ElementGeom,
309
310    /// How an outer x-axis label that would cross the SVG edge is handled
311    /// (B3-1, D6). Default [`AxisLabelFit::Reserve`].
312    pub axis_text_x_fit: AxisLabelFit,
313    /// Thin crowded x-axis labels in ggplot2's `check.overlap` priority
314    /// order (B3-1, D5): off by default, so default output is unaffected.
315    pub axis_text_x_check_overlap: bool,
316    /// The labels of an x axis drawn on top (ggplot2's `axis.text.x.top`,
317    /// #413); `None` = `axis_text_x`.
318    pub axis_text_x_top: Option<ElementText>,
319    /// The labels of a right-hand (secondary) y axis (`axis.text.y.right`,
320    /// #413); `None` = `axis_text_y`.
321    pub axis_text_y_right: Option<ElementText>,
322    /// Horizontal (top / bottom) strips' text and background (ggplot2's
323    /// `strip.text.x` / `strip.background.x`, #413); `None` = `strip_text`
324    /// / `strip_background`.
325    pub strip_text_x: Option<ElementText>,
326    pub strip_background_x: Option<ElementRect>,
327    /// Vertical (left / right) strips' (`strip.text.y` /
328    /// `strip.background.y`).
329    pub strip_text_y: Option<ElementText>,
330    pub strip_background_y: Option<ElementRect>,
331}
332
333impl Theme {
334    /// The gap between two rows of legend keys (`legend_key_spacing_y`).
335    pub fn legend_key_gap_y(&self) -> f64 {
336        self.legend_key_spacing_y.unwrap_or(0.0).max(0.0)
337    }
338
339    /// `legend_margin` with its panel-facing side replaced by
340    /// `legend_box_spacing` (#413) for the outside legend positions.
341    pub fn legend_margin_outer(&self) -> Margin {
342        let mut m = self.legend_margin.clone();
343        if let Some(g) = self.legend_box_spacing {
344            let g = g.max(0.0);
345            match self.legend_position {
346                LegendPosition::Right => m.left = g,
347                LegendPosition::Left => m.right = g,
348                LegendPosition::Top => m.bottom = g,
349                LegendPosition::Bottom => m.top = g,
350                LegendPosition::None | LegendPosition::Inside(..) => {}
351            }
352        }
353        m
354    }
355
356    /// The x axis's tick length (`axis_ticks_length_x`, else the shared one).
357    pub fn ticks_len_x(&self) -> f64 {
358        self.axis_ticks_length_x.unwrap_or(self.axis_ticks_length)
359    }
360
361    /// The y axis's tick length (`axis_ticks_length_y`, else the shared one).
362    pub fn ticks_len_y(&self) -> f64 {
363        self.axis_ticks_length_y.unwrap_or(self.axis_ticks_length)
364    }
365
366    /// Hide only the y position guides (axis text, title, line, ticks and
367    /// the horizontal grid lines) — layout charts with a real x axis but a
368    /// meaningless y (`geom_timeline`).
369    pub fn hide_y_guides(&mut self) {
370        self.axis_text_y.visible = false;
371        self.axis_title_y.visible = false;
372        let hidden = |base: &ElementLine| ElementLine {
373            visible: false,
374            ..base.clone()
375        };
376        self.axis_line_y = Some(hidden(self.axis_line_y.as_ref().unwrap_or(&self.axis_line)));
377        self.axis_ticks_y = Some(hidden(
378            self.axis_ticks_y.as_ref().unwrap_or(&self.axis_ticks),
379        ));
380        self.panel_grid_major_y = Some(hidden(
381            self.panel_grid_major_y
382                .as_ref()
383                .unwrap_or(&self.panel_grid_major),
384        ));
385        self.panel_grid_minor_y = Some(hidden(
386            self.panel_grid_minor_y
387                .as_ref()
388                .unwrap_or(&self.panel_grid_minor),
389        ));
390    }
391
392    /// Hide the position guides — axis text, titles, lines, ticks and panel
393    /// grid — for layout-only charts whose x/y carry no data meaning
394    /// (`geom_sankey`). Backgrounds, legend and titles are untouched.
395    pub fn hide_position_guides(&mut self) {
396        for t in [
397            &mut self.axis_text_x,
398            &mut self.axis_text_y,
399            &mut self.axis_title_x,
400            &mut self.axis_title_y,
401        ] {
402            t.visible = false;
403        }
404        for l in [
405            &mut self.axis_line,
406            &mut self.axis_ticks,
407            &mut self.panel_grid_major,
408            &mut self.panel_grid_minor,
409        ] {
410            l.visible = false;
411        }
412        for l in [
413            &mut self.axis_line_x,
414            &mut self.axis_line_y,
415            &mut self.axis_ticks_x,
416            &mut self.axis_ticks_y,
417            &mut self.panel_grid_major_x,
418            &mut self.panel_grid_major_y,
419            &mut self.panel_grid_minor_x,
420            &mut self.panel_grid_minor_y,
421        ]
422        .into_iter()
423        .flatten()
424        {
425            l.visible = false;
426        }
427    }
428
429    /// Resolve the effective series color: the theme's brand color if set,
430    /// otherwise the geom's own default.
431    pub fn primary_or(&self, fallback: (u8, u8, u8)) -> (u8, u8, u8) {
432        self.primary.unwrap_or(fallback)
433    }
434
435    /// Set the brand/primary color (builder style).
436    pub fn with_primary(mut self, color: (u8, u8, u8)) -> Self {
437        self.primary = Some(color);
438        self
439    }
440
441    /// Set the geom ink/paper/accent/family palette (B2-7).
442    pub fn set_geom(mut self, el: ElementGeom) -> Self {
443        self.geom = el;
444        self
445    }
446
447    /// Set just the geom ink and paper colours (B2-7's stretch goal),
448    /// leaving `accent`/`family` as they were.
449    pub fn with_ink_paper(mut self, ink: (u8, u8, u8), paper: (u8, u8, u8)) -> Self {
450        self.geom.ink = Some(ink);
451        self.geom.paper = Some(paper);
452        self
453    }
454
455    /// Resolve R-style element inheritance: text-child elements still at the
456    /// default `family`/`color` inherit them from the root `text` element, so
457    /// setting only `theme.text.family` restyles every text element. Child
458    /// elements that already override a property keep it. Called once before
459    /// rendering.
460    pub fn resolve_inheritance(&mut self) {
461        let root = self.text.clone();
462        let def = ElementText::default();
463        let children: [&mut ElementText; 10] = [
464            &mut self.title,
465            &mut self.subtitle,
466            &mut self.caption,
467            &mut self.axis_text_x,
468            &mut self.axis_text_y,
469            &mut self.axis_title_x,
470            &mut self.axis_title_y,
471            &mut self.legend_title,
472            &mut self.legend_text,
473            &mut self.strip_text,
474        ];
475        for el in children {
476            if el.family == def.family {
477                el.family = root.family.clone();
478            }
479            if el.color == def.color {
480                el.color = root.color;
481            }
482        }
483    }
484
485    // ── Fallback accessors for Option fields ──
486
487    pub fn get_axis_line_x(&self) -> &ElementLine {
488        self.axis_line_x.as_ref().unwrap_or(&self.axis_line)
489    }
490
491    pub fn get_axis_line_y(&self) -> &ElementLine {
492        self.axis_line_y.as_ref().unwrap_or(&self.axis_line)
493    }
494
495    pub fn get_axis_ticks_x(&self) -> &ElementLine {
496        self.axis_ticks_x.as_ref().unwrap_or(&self.axis_ticks)
497    }
498
499    pub fn get_axis_ticks_y(&self) -> &ElementLine {
500        self.axis_ticks_y.as_ref().unwrap_or(&self.axis_ticks)
501    }
502
503    /// The axis line on one side of the panel (#413): its side's own
504    /// element, else the x / y one, else `axis_line`.
505    pub fn axis_line_at(&self, side: AxisSide) -> &ElementLine {
506        let own = match side {
507            AxisSide::Top => &self.axis_line_x_top,
508            AxisSide::Bottom => &self.axis_line_x_bottom,
509            AxisSide::Left => &self.axis_line_y_left,
510            AxisSide::Right => &self.axis_line_y_right,
511        };
512        own.as_ref().unwrap_or(match side {
513            AxisSide::Top | AxisSide::Bottom => self.get_axis_line_x(),
514            AxisSide::Left | AxisSide::Right => self.get_axis_line_y(),
515        })
516    }
517
518    /// The axis ticks on one side of the panel (#413), inheriting like
519    /// [`Theme::axis_line_at`].
520    pub fn axis_ticks_at(&self, side: AxisSide) -> &ElementLine {
521        let own = match side {
522            AxisSide::Top => &self.axis_ticks_x_top,
523            AxisSide::Bottom => &self.axis_ticks_x_bottom,
524            AxisSide::Left => &self.axis_ticks_y_left,
525            AxisSide::Right => &self.axis_ticks_y_right,
526        };
527        own.as_ref().unwrap_or(match side {
528            AxisSide::Top | AxisSide::Bottom => self.get_axis_ticks_x(),
529            AxisSide::Left | AxisSide::Right => self.get_axis_ticks_y(),
530        })
531    }
532
533    pub fn get_panel_grid_major_x(&self) -> &ElementLine {
534        self.panel_grid_major_x
535            .as_ref()
536            .unwrap_or(&self.panel_grid_major)
537    }
538
539    pub fn get_panel_grid_major_y(&self) -> &ElementLine {
540        self.panel_grid_major_y
541            .as_ref()
542            .unwrap_or(&self.panel_grid_major)
543    }
544
545    pub fn get_panel_grid_minor_x(&self) -> &ElementLine {
546        self.panel_grid_minor_x
547            .as_ref()
548            .unwrap_or(&self.panel_grid_minor)
549    }
550
551    pub fn get_panel_grid_minor_y(&self) -> &ElementLine {
552        self.panel_grid_minor_y
553            .as_ref()
554            .unwrap_or(&self.panel_grid_minor)
555    }
556
557    pub fn get_panel_spacing_x(&self) -> f64 {
558        self.panel_spacing_x.unwrap_or(self.panel_spacing)
559    }
560
561    pub fn get_panel_spacing_y(&self) -> f64 {
562        self.panel_spacing_y.unwrap_or(self.panel_spacing)
563    }
564
565    // ── Existing setters ──
566
567    pub fn set_axis_text_x(mut self, el: ElementText) -> Self {
568        self.axis_text_x = el;
569        self
570    }
571
572    pub fn set_axis_text_y(mut self, el: ElementText) -> Self {
573        self.axis_text_y = el;
574        self
575    }
576
577    pub fn set_axis_title_x(mut self, el: ElementText) -> Self {
578        self.axis_title_x = el;
579        self
580    }
581
582    pub fn set_axis_title_y(mut self, el: ElementText) -> Self {
583        self.axis_title_y = el;
584        self
585    }
586
587    pub fn set_axis_line(mut self, el: ElementLine) -> Self {
588        self.axis_line = el;
589        self
590    }
591
592    pub fn set_axis_ticks(mut self, el: ElementLine) -> Self {
593        self.axis_ticks = el;
594        self
595    }
596
597    pub fn set_panel_background(mut self, el: ElementRect) -> Self {
598        self.panel_background = el;
599        self
600    }
601
602    pub fn set_panel_grid_major(mut self, el: ElementLine) -> Self {
603        self.panel_grid_major = el;
604        self
605    }
606
607    pub fn set_panel_grid_minor(mut self, el: ElementLine) -> Self {
608        self.panel_grid_minor = el;
609        self
610    }
611
612    pub fn set_plot_background(mut self, el: ElementRect) -> Self {
613        self.plot_background = el;
614        self
615    }
616
617    pub fn set_legend_position(mut self, pos: LegendPosition) -> Self {
618        self.legend_position = pos;
619        self
620    }
621
622    pub fn set_plot_margin(mut self, margin: Margin) -> Self {
623        self.plot_margin = margin;
624        self
625    }
626
627    pub fn set_title(mut self, el: ElementText) -> Self {
628        self.title = el;
629        self
630    }
631
632    pub fn set_text(mut self, el: ElementText) -> Self {
633        self.text = el;
634        self
635    }
636
637    // ── New setters ──
638
639    pub fn set_subtitle(mut self, el: ElementText) -> Self {
640        self.subtitle = el;
641        self
642    }
643
644    pub fn set_caption(mut self, el: ElementText) -> Self {
645        self.caption = el;
646        self
647    }
648
649    pub fn set_legend_title(mut self, el: ElementText) -> Self {
650        self.legend_title = el;
651        self
652    }
653
654    pub fn set_legend_text(mut self, el: ElementText) -> Self {
655        self.legend_text = el;
656        self
657    }
658
659    pub fn set_strip_text(mut self, el: ElementText) -> Self {
660        self.strip_text = el;
661        self
662    }
663
664    pub fn set_axis_line_x(mut self, el: Option<ElementLine>) -> Self {
665        self.axis_line_x = el;
666        self
667    }
668
669    pub fn set_axis_line_y(mut self, el: Option<ElementLine>) -> Self {
670        self.axis_line_y = el;
671        self
672    }
673
674    pub fn set_axis_ticks_x(mut self, el: Option<ElementLine>) -> Self {
675        self.axis_ticks_x = el;
676        self
677    }
678
679    pub fn set_axis_ticks_y(mut self, el: Option<ElementLine>) -> Self {
680        self.axis_ticks_y = el;
681        self
682    }
683
684    pub fn set_panel_grid_major_x(mut self, el: Option<ElementLine>) -> Self {
685        self.panel_grid_major_x = el;
686        self
687    }
688
689    pub fn set_panel_grid_major_y(mut self, el: Option<ElementLine>) -> Self {
690        self.panel_grid_major_y = el;
691        self
692    }
693
694    pub fn set_panel_grid_minor_x(mut self, el: Option<ElementLine>) -> Self {
695        self.panel_grid_minor_x = el;
696        self
697    }
698
699    pub fn set_panel_grid_minor_y(mut self, el: Option<ElementLine>) -> Self {
700        self.panel_grid_minor_y = el;
701        self
702    }
703
704    pub fn set_panel_border(mut self, el: ElementLine) -> Self {
705        self.panel_border = el;
706        self
707    }
708
709    /// The effective colourbar frame: [`legend_bar_border`](Self::legend_bar_border),
710    /// else `legend_key.color` (dark grey when unset), 0.5 wide.
711    pub fn legend_bar_border_line(&self) -> ElementLine {
712        self.legend_bar_border.clone().unwrap_or(ElementLine {
713            color: self.legend_key.color.unwrap_or((50, 50, 50)),
714            width: 0.5,
715            visible: true,
716            linetype: crate::render::backend::Linetype::Solid,
717        })
718    }
719
720    pub fn set_calendar_month_border(mut self, el: ElementLine) -> Self {
721        self.calendar_month_border = el;
722        self
723    }
724
725    pub fn set_legend_bar_border(mut self, el: Option<ElementLine>) -> Self {
726        self.legend_bar_border = el;
727        self
728    }
729
730    pub fn set_legend_background(mut self, el: ElementRect) -> Self {
731        self.legend_background = el;
732        self
733    }
734
735    pub fn set_legend_key(mut self, el: ElementRect) -> Self {
736        self.legend_key = el;
737        self
738    }
739
740    pub fn set_strip_background(mut self, el: ElementRect) -> Self {
741        self.strip_background = el;
742        self
743    }
744
745    pub fn set_axis_ticks_length(mut self, val: f64) -> Self {
746        self.axis_ticks_length = val;
747        self
748    }
749
750    pub fn set_legend_key_width(mut self, val: f64) -> Self {
751        self.legend_key_width = val;
752        self
753    }
754
755    pub fn set_legend_key_height(mut self, val: f64) -> Self {
756        self.legend_key_height = val;
757        self
758    }
759
760    pub fn set_legend_spacing(mut self, val: f64) -> Self {
761        self.legend_spacing = val;
762        self
763    }
764
765    pub fn set_legend_margin(mut self, margin: Margin) -> Self {
766        self.legend_margin = margin;
767        self
768    }
769
770    pub fn set_panel_spacing(mut self, val: f64) -> Self {
771        self.panel_spacing = val;
772        self
773    }
774
775    pub fn set_panel_spacing_x(mut self, val: Option<f64>) -> Self {
776        self.panel_spacing_x = val;
777        self
778    }
779
780    pub fn set_panel_spacing_y(mut self, val: Option<f64>) -> Self {
781        self.panel_spacing_y = val;
782        self
783    }
784
785    /// Apply incremental theme modifications.
786    /// Only fields that are `Some` in the update are applied.
787    pub fn update(mut self, upd: ThemeUpdate) -> Self {
788        if let Some(v) = upd.text {
789            self.text = v;
790        }
791        if let Some(v) = upd.title {
792            self.title = v;
793        }
794        if let Some(v) = upd.subtitle {
795            self.subtitle = v;
796        }
797        if let Some(v) = upd.caption {
798            self.caption = v;
799        }
800        if let Some(v) = upd.axis_text_x {
801            self.axis_text_x = v;
802        }
803        if let Some(v) = upd.axis_text_y {
804            self.axis_text_y = v;
805        }
806        if let Some(v) = upd.axis_title_x {
807            self.axis_title_x = v;
808        }
809        if let Some(v) = upd.axis_title_y {
810            self.axis_title_y = v;
811        }
812        if let Some(v) = upd.axis_line {
813            self.axis_line = v;
814        }
815        if let Some(v) = upd.axis_ticks {
816            self.axis_ticks = v;
817        }
818        if let Some(v) = upd.panel_background {
819            self.panel_background = v;
820        }
821        if let Some(v) = upd.panel_grid_major {
822            self.panel_grid_major = v;
823        }
824        if let Some(v) = upd.panel_grid_minor {
825            self.panel_grid_minor = v;
826        }
827        if let Some(v) = upd.panel_border {
828            self.panel_border = v;
829        }
830        if let Some(v) = upd.calendar_month_border {
831            self.calendar_month_border = v;
832        }
833        if let Some(v) = upd.legend_bar_border {
834            self.legend_bar_border = Some(v);
835        }
836        if let Some(v) = upd.plot_background {
837            self.plot_background = v;
838        }
839        if let Some(v) = upd.legend_position {
840            self.legend_position = v;
841        }
842        if let Some(v) = upd.legend_title {
843            self.legend_title = v;
844        }
845        if let Some(v) = upd.legend_text {
846            self.legend_text = v;
847        }
848        if let Some(v) = upd.legend_background {
849            self.legend_background = v;
850        }
851        if let Some(v) = upd.strip_text {
852            self.strip_text = v;
853        }
854        if let Some(v) = upd.strip_background {
855            self.strip_background = v;
856        }
857        if let Some(v) = upd.plot_margin {
858            self.plot_margin = v;
859        }
860        if let Some(v) = upd.geom {
861            self.geom = v;
862        }
863        for (v, slot) in [
864            (upd.axis_line_x, &mut self.axis_line_x),
865            (upd.axis_line_y, &mut self.axis_line_y),
866            (upd.axis_ticks_x, &mut self.axis_ticks_x),
867            (upd.axis_ticks_y, &mut self.axis_ticks_y),
868        ] {
869            if v.is_some() {
870                *slot = v;
871            }
872        }
873        self
874    }
875}
876
877/// Incremental theme modifications. All fields are optional — only `Some` values are applied.
878/// Like R's `theme(axis.text.x = element_text(...))`.
879#[derive(Clone, Debug, Default)]
880pub struct ThemeUpdate {
881    pub text: Option<ElementText>,
882    pub title: Option<ElementText>,
883    pub subtitle: Option<ElementText>,
884    pub caption: Option<ElementText>,
885    pub axis_text_x: Option<ElementText>,
886    pub axis_text_y: Option<ElementText>,
887    pub axis_title_x: Option<ElementText>,
888    pub axis_title_y: Option<ElementText>,
889    pub axis_line: Option<ElementLine>,
890    pub axis_ticks: Option<ElementLine>,
891    /// One axis's line / ticks only (`axis.line.x`, …; #413).
892    pub axis_line_x: Option<ElementLine>,
893    pub axis_line_y: Option<ElementLine>,
894    pub axis_ticks_x: Option<ElementLine>,
895    pub axis_ticks_y: Option<ElementLine>,
896    pub panel_background: Option<ElementRect>,
897    pub panel_grid_major: Option<ElementLine>,
898    pub panel_grid_minor: Option<ElementLine>,
899    pub panel_border: Option<ElementLine>,
900    pub calendar_month_border: Option<ElementLine>,
901    pub legend_bar_border: Option<ElementLine>,
902    pub plot_background: Option<ElementRect>,
903    pub legend_position: Option<LegendPosition>,
904    pub legend_title: Option<ElementText>,
905    pub legend_text: Option<ElementText>,
906    pub legend_background: Option<ElementRect>,
907    pub strip_text: Option<ElementText>,
908    pub strip_background: Option<ElementRect>,
909    pub plot_margin: Option<Margin>,
910    /// Geom ink/paper/accent/family palette (B2-7).
911    pub geom: Option<ElementGeom>,
912}
913
914impl Default for Theme {
915    fn default() -> Self {
916        presets::theme_gray()
917    }
918}
919
920/// ggplot2 4.0's `theme_sub_axis_x(text =, title =, line =, ticks =)` (#413):
921/// a [`ThemeUpdate`] for the x axis's elements only.
922pub fn theme_sub_axis_x(
923    text: Option<ElementText>,
924    title: Option<ElementText>,
925    line: Option<ElementLine>,
926    ticks: Option<ElementLine>,
927) -> ThemeUpdate {
928    ThemeUpdate {
929        axis_text_x: text,
930        axis_title_x: title,
931        axis_line_x: line,
932        axis_ticks_x: ticks,
933        ..Default::default()
934    }
935}
936
937/// ggplot2 4.0's `theme_sub_axis_y()` — see [`theme_sub_axis_x`].
938pub fn theme_sub_axis_y(
939    text: Option<ElementText>,
940    title: Option<ElementText>,
941    line: Option<ElementLine>,
942    ticks: Option<ElementLine>,
943) -> ThemeUpdate {
944    ThemeUpdate {
945        axis_text_y: text,
946        axis_title_y: title,
947        axis_line_y: line,
948        axis_ticks_y: ticks,
949        ..Default::default()
950    }
951}
952
953/// ggplot2 4.0's `theme_sub_legend(title =, text =, background =, position
954/// =)` (#413).
955pub fn theme_sub_legend(
956    title: Option<ElementText>,
957    text: Option<ElementText>,
958    background: Option<ElementRect>,
959    position: Option<LegendPosition>,
960) -> ThemeUpdate {
961    ThemeUpdate {
962        legend_title: title,
963        legend_text: text,
964        legend_background: background,
965        legend_position: position,
966        ..Default::default()
967    }
968}
969
970/// ggplot2 4.0's `theme_sub_panel(background =, border =, grid.major =,
971/// grid.minor =)` (#413).
972pub fn theme_sub_panel(
973    background: Option<ElementRect>,
974    border: Option<ElementLine>,
975    grid_major: Option<ElementLine>,
976    grid_minor: Option<ElementLine>,
977) -> ThemeUpdate {
978    ThemeUpdate {
979        panel_background: background,
980        panel_border: border,
981        panel_grid_major: grid_major,
982        panel_grid_minor: grid_minor,
983        ..Default::default()
984    }
985}
986
987/// ggplot2 4.0's `theme_sub_strip(text =, background =)` (#413).
988pub fn theme_sub_strip(text: Option<ElementText>, background: Option<ElementRect>) -> ThemeUpdate {
989    ThemeUpdate {
990        strip_text: text,
991        strip_background: background,
992        ..Default::default()
993    }
994}
995
996/// ggplot2 4.0's `theme_sub_plot(title =, subtitle =, caption =, background
997/// =, margin =)` (#413).
998pub fn theme_sub_plot(
999    title: Option<ElementText>,
1000    subtitle: Option<ElementText>,
1001    caption: Option<ElementText>,
1002    background: Option<ElementRect>,
1003    margin: Option<Margin>,
1004) -> ThemeUpdate {
1005    ThemeUpdate {
1006        title,
1007        subtitle,
1008        caption,
1009        plot_background: background,
1010        plot_margin: margin,
1011        ..Default::default()
1012    }
1013}