Skip to main content

ggplot_rs/theme/
elements.rs

1/// Text element styling.
2#[derive(Clone, Debug)]
3pub struct ElementText {
4    pub family: String,
5    /// Font face (R's `element_text(face = ...)`).
6    pub face: crate::render::backend::FontFace,
7    pub size: f64,
8    pub color: (u8, u8, u8),
9    pub angle: f64,
10    pub hjust: f64,
11    pub vjust: f64,
12    pub visible: bool,
13}
14
15impl ElementText {
16    /// ggplot2's `rel(factor)` for a text size (#413): this element with its
17    /// size scaled, e.g. `theme.title.rel(1.2)` — 120 % of the current.
18    pub fn rel(&self, factor: f64) -> ElementText {
19        ElementText {
20            size: self.size * factor,
21            ..self.clone()
22        }
23    }
24
25    /// Create an invisible (blank) text element.
26    pub fn blank() -> Self {
27        ElementText {
28            visible: false,
29            ..Default::default()
30        }
31    }
32}
33
34impl Default for ElementText {
35    fn default() -> Self {
36        ElementText {
37            family: "sans-serif".to_string(),
38            face: crate::render::backend::FontFace::Plain,
39            size: 12.0,
40            color: (0, 0, 0),
41            angle: 0.0,
42            hjust: 0.5,
43            vjust: 0.5,
44            visible: true,
45        }
46    }
47}
48
49/// Line element styling.
50#[derive(Clone, Debug)]
51pub struct ElementLine {
52    pub color: (u8, u8, u8),
53    pub width: f64,
54    pub visible: bool,
55    /// Line style (R's `element_line(linetype = ...)`).
56    pub linetype: crate::render::backend::Linetype,
57}
58
59impl ElementLine {
60    /// ggplot2's `rel(factor)` for a line width (#413).
61    pub fn rel(&self, factor: f64) -> ElementLine {
62        ElementLine {
63            width: self.width * factor,
64            ..self.clone()
65        }
66    }
67
68    /// Create an invisible (blank) line element.
69    pub fn blank() -> Self {
70        ElementLine {
71            visible: false,
72            ..Default::default()
73        }
74    }
75}
76
77impl Default for ElementLine {
78    fn default() -> Self {
79        ElementLine {
80            color: (0, 0, 0),
81            width: 1.0,
82            visible: true,
83            linetype: crate::render::backend::Linetype::Solid,
84        }
85    }
86}
87
88/// Rectangle element styling.
89#[derive(Clone, Debug)]
90pub struct ElementRect {
91    pub fill: Option<(u8, u8, u8)>,
92    pub color: Option<(u8, u8, u8)>,
93    pub width: f64,
94    pub visible: bool,
95}
96
97impl ElementRect {
98    /// Create an invisible (blank) rect element.
99    pub fn blank() -> Self {
100        ElementRect {
101            visible: false,
102            ..Default::default()
103        }
104    }
105}
106
107impl Default for ElementRect {
108    fn default() -> Self {
109        ElementRect {
110            fill: Some((255, 255, 255)),
111            color: None,
112            width: 0.0,
113            visible: true,
114        }
115    }
116}
117
118/// Geom "ink"/"paper"/"accent" palette (ggplot2 4.0's `element_geom`,
119/// B2-7): recolours a geom's *built-in default* colour without touching
120/// every geom's own color/fill field or needing a mapped/explicit colour
121/// to be set first. `None` everywhere (the default) changes nothing —
122/// byte-identical output. Marked `#[non_exhaustive]` so a later field can
123/// be added without an `..Default::default()` break.
124///
125/// A default [`Geom::apply_element_geom`](crate::geom::Geom::apply_element_geom)
126/// is a no-op; a geom that honours this overrides it, replacing only
127/// fields still at their own built-in default — called once per layer,
128/// before the brand [`Theme::primary`](super::Theme::primary) colour is
129/// applied, and (like the brand colour) only for a layer that was *not*
130/// configured explicitly via `geom_*_with(...)`: that is the actual guard
131/// against touching an explicit colour (an `explicit_style` gate in the
132/// build pipeline), not a comparison of the field's *value* against the
133/// hard-coded default, which can't otherwise tell an explicitly-set black
134/// apart from a black nobody touched.
135///
136/// **Geoms covered:** `GeomPoint`, `GeomLine`, `GeomPath`, `GeomText`,
137/// `GeomLabel`, `GeomBoxplot`, `GeomSegment`, `GeomCurve`, the reference
138/// lines (`GeomHline`/`GeomVline`/`GeomAbline`), `GeomErrorbar`/
139/// `GeomErrorbarh`/`GeomLinerange`/`GeomPointrange`, `GeomStep`, the
140/// outline colour of `GeomRect`/`GeomTile`/`GeomBar`/`GeomCol`/
141/// `GeomPolygon`/`GeomViolin`/`GeomDensity`/`GeomArea`/`GeomCount`/
142/// `GeomHex`/`GeomContour`/`GeomHistogram`/`GeomDensity2d`/`GeomBin2d`/
143/// `GeomSf` (feature `sf`), `GeomCrossbar`, `GeomTextRepel`,
144/// `GeomLabelRepel`, `GeomSpoke`, `GeomRug`, `GeomBracket`, `GeomJitter`,
145/// `GeomQQ`, `GeomCensorMarks` and `GeomFreqpoly` (`ink` for a black/
146/// `(50, 50, 50)` line, text or outline; `paper` for a white fill, on
147/// `GeomBoxplot`/`GeomLabel`/`GeomCrossbar`/`GeomLabelRepel`) and
148/// `GeomSmooth` (`accent` for its default blue line and ribbon fill) —
149/// plus the legend key swatch colour, which reads `ink` directly rather
150/// than going through this trait method. A handful of geoms are left
151/// uncovered on purpose: `GeomDotplot`'s `color` field is dead config
152/// (never drawn); `GeomQQLine`/`GeomCandlestick`/`GeomOhlc`'s defaults are
153/// deliberate semantic colours, not neutral ones; `GeomQQBand`'s mid-grey
154/// fill doesn't match this type's black/white checks; and
155/// `GeomCalendarBorder` already reads its colour from the dedicated
156/// [`Theme::calendar_month_border`](super::Theme::calendar_month_border)
157/// element. See sipemu/ggplot-rs#142.
158#[non_exhaustive]
159#[derive(Clone, Debug, Default)]
160pub struct ElementGeom {
161    /// Replaces a geom's built-in black / `(50, 50, 50)` line, text or
162    /// outline colour (including the legend key colour).
163    pub ink: Option<(u8, u8, u8)>,
164    /// Replaces a geom's built-in white fill.
165    pub paper: Option<(u8, u8, u8)>,
166    /// Replaces the smooth line's built-in colour.
167    pub accent: Option<(u8, u8, u8)>,
168    /// Font family — the middle tier of B2-6's resolution order
169    /// (a geom's own `fontfamily`, then this, then `theme.text.family`).
170    pub family: Option<String>,
171    /// Replaces `geom_point`'s built-in size (ggplot2 4.0's
172    /// `element_geom(pointsize =)`, #413).
173    pub pointsize: Option<f64>,
174    /// Replaces `geom_line` / `geom_path`'s built-in width (`linewidth =`,
175    /// #413).
176    pub linewidth: Option<f64>,
177}
178
179impl ElementGeom {
180    /// Builder for [`ink`](Self::ink) — `#[non_exhaustive]` blocks a
181    /// cross-crate struct literal even with `..Default::default()`, so
182    /// each field gets one of these.
183    pub fn with_ink(mut self, color: (u8, u8, u8)) -> Self {
184        self.ink = Some(color);
185        self
186    }
187
188    /// Builder for [`paper`](Self::paper).
189    pub fn with_paper(mut self, color: (u8, u8, u8)) -> Self {
190        self.paper = Some(color);
191        self
192    }
193
194    /// Builder for [`accent`](Self::accent).
195    pub fn with_accent(mut self, color: (u8, u8, u8)) -> Self {
196        self.accent = Some(color);
197        self
198    }
199
200    /// Builder for [`family`](Self::family).
201    pub fn with_family(mut self, family: &str) -> Self {
202        self.family = Some(family.to_string());
203        self
204    }
205
206    /// Builder for [`pointsize`](Self::pointsize) (#413).
207    pub fn with_pointsize(mut self, size: f64) -> Self {
208        self.pointsize = Some(size);
209        self
210    }
211
212    /// Builder for [`linewidth`](Self::linewidth) (#413).
213    pub fn with_linewidth(mut self, width: f64) -> Self {
214        self.linewidth = Some(width);
215        self
216    }
217}
218
219#[cfg(test)]
220mod element_geom_tests {
221    use super::ElementGeom;
222
223    #[test]
224    fn builders_set_every_field() {
225        let el = ElementGeom::default()
226            .with_ink((1, 2, 3))
227            .with_paper((4, 5, 6))
228            .with_accent((7, 8, 9))
229            .with_family("Georgia");
230        assert_eq!(el.ink, Some((1, 2, 3)));
231        assert_eq!(el.paper, Some((4, 5, 6)));
232        assert_eq!(el.accent, Some((7, 8, 9)));
233        assert_eq!(el.family, Some("Georgia".to_string()));
234    }
235}