Skip to main content

ggplot_rs/facet/
mod.rs

1pub mod grid;
2pub mod wrap;
3
4use crate::render::Rect;
5
6/// Whether facet panels share axes.
7#[derive(Clone, Debug)]
8pub enum FacetScales {
9    Fixed,
10    FreeX,
11    FreeY,
12    Free,
13}
14
15/// Whether facet panels are sized proportionally to their data range
16/// (R's `space =`). `Fixed` = equal-size panels (default).
17#[derive(Clone, Debug, Default, PartialEq)]
18pub enum FacetSpace {
19    #[default]
20    Fixed,
21    FreeX,
22    FreeY,
23    Free,
24}
25
26impl FacetSpace {
27    pub fn free_x(&self) -> bool {
28        matches!(self, FacetSpace::FreeX | FacetSpace::Free)
29    }
30    pub fn free_y(&self) -> bool {
31        matches!(self, FacetSpace::FreeY | FacetSpace::Free)
32    }
33}
34
35/// Which side of a `facet_wrap` panel its strip sits on (ggplot2's
36/// `strip.position`, #412).
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum StripPosition {
39    #[default]
40    Top,
41    Bottom,
42    Left,
43    Right,
44}
45
46/// Move `facet_grid` strips to the opposite side (ggplot2's `switch`,
47/// #412): `X` puts the column strips below the grid, `Y` the row strips on
48/// its left, `Both` does both.
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
50pub enum FacetSwitch {
51    #[default]
52    None,
53    X,
54    Y,
55    Both,
56}
57
58impl FacetSwitch {
59    pub fn x(self) -> bool {
60        matches!(self, FacetSwitch::X | FacetSwitch::Both)
61    }
62    pub fn y(self) -> bool {
63        matches!(self, FacetSwitch::Y | FacetSwitch::Both)
64    }
65}
66
67/// Which facet panels get axes (ggplot2's `facet_*(axes =, axis.labels
68/// =)`, #412). Default: axes on the outer panels only (`"margins"`).
69#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub struct FacetAxes {
71    /// An x axis under every panel (`axes = "all_x"`).
72    pub all_x: bool,
73    /// A y axis left of every panel (`axes = "all_y"`).
74    pub all_y: bool,
75    /// With `all_x`, tick labels only on the bottom panels
76    /// (`axis.labels = "margins"` / `"all_y"`).
77    pub margin_labels_x: bool,
78    /// With `all_y`, tick labels only on the left panels.
79    pub margin_labels_y: bool,
80}
81
82impl FacetAxes {
83    /// `axes = "all"`: x and y axes on every panel, labelled.
84    pub fn all() -> Self {
85        FacetAxes {
86            all_x: true,
87            all_y: true,
88            ..Self::default()
89        }
90    }
91
92    /// `axes = "all_x"`.
93    pub fn all_x() -> Self {
94        FacetAxes {
95            all_x: true,
96            ..Self::default()
97        }
98    }
99
100    /// `axes = "all_y"`.
101    pub fn all_y() -> Self {
102        FacetAxes {
103            all_y: true,
104            ..Self::default()
105        }
106    }
107
108    /// `axis.labels = "margins"`: the inner axes get ticks but no labels.
109    pub fn margin_labels(mut self) -> Self {
110        self.margin_labels_x = true;
111        self.margin_labels_y = true;
112        self
113    }
114}
115
116/// How facet strip labels are formatted.
117#[derive(Clone, Default)]
118pub enum FacetLabeller {
119    /// Show just the value (default).
120    #[default]
121    Value,
122    /// Show "var: value".
123    Both,
124    /// Custom formatting function: fn(variable_name, value) -> label.
125    Custom(fn(&str, &str) -> String),
126    /// The value, wrapped onto lines of at most this many characters
127    /// (ggplot2's `label_wrap_gen(width)`, #412).
128    Wrap(usize),
129    /// A template with `{var}` and `{value}` placeholders, e.g.
130    /// `"n = {value}"` (ggplot2's `label_bquote(cols = n == .(n))` /
131    /// `as_labeller` glue strings, #412).
132    Template(String),
133    /// A labeller per facet variable (ggplot2's `labeller(var1 = …, var2 =
134    /// …)`, #412); variables not listed use [`Value`](Self::Value).
135    PerVar(Vec<(String, FacetLabeller)>),
136    /// Fixed `value → label` lookups (ggplot2's `as_labeller(c(a = "…"))`,
137    /// #412); values not listed show as they are.
138    Lookup(Vec<(String, String)>),
139    /// A combined multi-variable value (`facet_grid_multi`'s `"a . b"`) on
140    /// one line per variable (ggplot2's `multi_line = TRUE`, #412).
141    MultiLine,
142    /// The value read as a plotmath expression (ggplot2's `label_parsed`,
143    /// #412), drawn as Unicode text: Greek names (`alpha` → α), `x^2` →
144    /// x², `x[i]` → xᵢ, `==` → =, `%*%` → ×, `*` (juxtaposition) dropped.
145    Parsed,
146}
147
148/// A practical subset of plotmath as Unicode (see [`FacetLabeller::Parsed`]).
149pub fn plotmath_to_unicode(expr: &str) -> String {
150    const GREEK: [(&str, &str); 24] = [
151        ("alpha", "α"),
152        ("beta", "β"),
153        ("gamma", "γ"),
154        ("delta", "δ"),
155        ("epsilon", "ε"),
156        ("zeta", "ζ"),
157        ("eta", "η"),
158        ("theta", "θ"),
159        ("iota", "ι"),
160        ("kappa", "κ"),
161        ("lambda", "λ"),
162        ("mu", "μ"),
163        ("nu", "ν"),
164        ("xi", "ξ"),
165        ("pi", "π"),
166        ("rho", "ρ"),
167        ("sigma", "σ"),
168        ("tau", "τ"),
169        ("upsilon", "υ"),
170        ("phi", "φ"),
171        ("chi", "χ"),
172        ("psi", "ψ"),
173        ("omega", "ω"),
174        ("Delta", "Δ"),
175    ];
176    let sup = |c: char| -> Option<char> {
177        Some(match c {
178            '0' => '⁰',
179            '1' => '¹',
180            '2' => '²',
181            '3' => '³',
182            '4' => '⁴',
183            '5' => '⁵',
184            '6' => '⁶',
185            '7' => '⁷',
186            '8' => '⁸',
187            '9' => '⁹',
188            '+' => '⁺',
189            '-' => '⁻',
190            'n' => 'ⁿ',
191            'i' => 'ⁱ',
192            _ => return None,
193        })
194    };
195    let sub = |c: char| -> Option<char> {
196        Some(match c {
197            '0' => '₀',
198            '1' => '₁',
199            '2' => '₂',
200            '3' => '₃',
201            '4' => '₄',
202            '5' => '₅',
203            '6' => '₆',
204            '7' => '₇',
205            '8' => '₈',
206            '9' => '₉',
207            '+' => '₊',
208            '-' => '₋',
209            'a' => 'ₐ',
210            'e' => 'ₑ',
211            'i' => 'ᵢ',
212            'j' => 'ⱼ',
213            'n' => 'ₙ',
214            'o' => 'ₒ',
215            'x' => 'ₓ',
216            'k' => 'ₖ',
217            'm' => 'ₘ',
218            't' => 'ₜ',
219            _ => return None,
220        })
221    };
222    let s = expr
223        .replace("%*%", "×")
224        .replace("==", "=")
225        .replace("%+-%", "±");
226    let chars: Vec<char> = s.chars().collect();
227    let mut out = String::new();
228    let mut i = 0;
229    while i < chars.len() {
230        let c = chars[i];
231        if c.is_ascii_alphabetic() {
232            let start = i;
233            while i < chars.len() && chars[i].is_ascii_alphanumeric() {
234                i += 1;
235            }
236            let word: String = chars[start..i].iter().collect();
237            match GREEK.iter().find(|(n, _)| *n == word) {
238                Some((_, g)) => out.push_str(g),
239                None => out.push_str(&word),
240            }
241            continue;
242        }
243        if c == '^' || c == '[' {
244            let (close, map): (Option<char>, &dyn Fn(char) -> Option<char>) = if c == '^' {
245                (None, &sup)
246            } else {
247                (Some(']'), &sub)
248            };
249            i += 1;
250            let braced = c == '^' && chars.get(i) == Some(&'{');
251            if braced {
252                i += 1;
253            }
254            let mut buf = String::new();
255            while i < chars.len() {
256                let d = chars[i];
257                if Some(d) == close || (braced && d == '}') {
258                    i += 1;
259                    break;
260                }
261                if close.is_none()
262                    && !braced
263                    && !(d.is_ascii_alphanumeric() || d == '-' || d == '+')
264                {
265                    break;
266                }
267                buf.push(d);
268                i += 1;
269            }
270            let mapped: Option<String> = buf.chars().map(map).collect();
271            match mapped {
272                Some(m) => out.push_str(&m),
273                None => {
274                    out.push(if c == '^' { '^' } else { '_' });
275                    out.push_str(&buf);
276                }
277            }
278            continue;
279        }
280        if c == '*' || c == '~' {
281            if c == '~' {
282                out.push(' ');
283            }
284            i += 1;
285            continue;
286        }
287        out.push(c);
288        i += 1;
289    }
290    out
291}
292
293/// Greedy word wrap at `width` characters (`strwrap`), lines joined by `\n`.
294pub fn wrap_label(text: &str, width: usize) -> String {
295    let mut lines: Vec<String> = Vec::new();
296    for word in text.split_whitespace() {
297        match lines.last_mut() {
298            Some(l) if l.chars().count() + 1 + word.chars().count() <= width.max(1) => {
299                l.push(' ');
300                l.push_str(word);
301            }
302            _ => lines.push(word.to_string()),
303        }
304    }
305    lines.join("\n")
306}
307
308impl FacetLabeller {
309    /// `labeller(var = …)` builder: one labeller per variable (#412).
310    pub fn per_var(per: Vec<(&str, FacetLabeller)>) -> Self {
311        FacetLabeller::PerVar(per.into_iter().map(|(v, l)| (v.to_string(), l)).collect())
312    }
313
314    /// `as_labeller(c(value = "label"))` (#412).
315    pub fn lookup(map: Vec<(&str, &str)>) -> Self {
316        FacetLabeller::Lookup(
317            map.into_iter()
318                .map(|(k, v)| (k.to_string(), v.to_string()))
319                .collect(),
320        )
321    }
322
323    pub fn format(&self, var: &str, value: &str) -> String {
324        match self {
325            FacetLabeller::Value => value.to_string(),
326            FacetLabeller::Both => format!("{var}: {value}"),
327            FacetLabeller::Custom(f) => f(var, value),
328            FacetLabeller::Wrap(w) => wrap_label(value, *w),
329            FacetLabeller::Template(t) => t.replace("{var}", var).replace("{value}", value),
330            FacetLabeller::PerVar(per) => per
331                .iter()
332                .find(|(v, _)| v == var)
333                .map_or_else(|| value.to_string(), |(_, l)| l.format(var, value)),
334            FacetLabeller::MultiLine => value.replace(" . ", "\n"),
335            FacetLabeller::Parsed => plotmath_to_unicode(value),
336            FacetLabeller::Lookup(map) => map
337                .iter()
338                .find(|(k, _)| k == value)
339                .map_or_else(|| value.to_string(), |(_, l)| l.clone()),
340        }
341    }
342}
343
344/// A single panel in a faceted layout.
345#[derive(Clone, Debug)]
346pub struct Panel {
347    pub row: usize,
348    pub col: usize,
349    pub label: String,
350    pub row_label: Option<String>,
351    pub col_label: Option<String>,
352    pub rect: Rect,
353}
354
355/// Facet specification.
356#[derive(Clone, Default)]
357pub enum Facet {
358    #[default]
359    None,
360    Wrap {
361        var: String,
362        ncol: Option<usize>,
363        scales: FacetScales,
364        labeller: FacetLabeller,
365        /// Explicit panel order (ggplot2's factor levels): every level gets
366        /// a panel, even with no data (`drop = FALSE`), and rows whose value
367        /// isn't listed are dropped. `None` = values in first-appearance
368        /// order. Set with [`GGPlot::facet_levels`](crate::GGPlot::facet_levels).
369        levels: Option<Vec<String>>,
370        /// Fill panels column by column (ggplot2's `dir = "v"`, #412).
371        vertical: bool,
372        /// First row at the top (default; ggplot2's `as.table = TRUE`).
373        as_table: bool,
374        /// The side each panel's strip sits on (`strip.position`, #412).
375        strip_position: StripPosition,
376        /// Proportional panel sizes (ggplot2 3.5's `facet_wrap(space =)`,
377        /// #412): free x for a single row, free y for a single column.
378        space: FacetSpace,
379    },
380    Grid {
381        row_var: Option<String>,
382        col_var: Option<String>,
383        scales: FacetScales,
384        labeller: FacetLabeller,
385        #[doc = "Proportional panel sizing (R's `space =`)."]
386        space: FacetSpace,
387        /// Column strips on top and row strips on the right, unless
388        /// switched (`switch`, #412).
389        switch: FacetSwitch,
390        /// Fixed row / column level order: every level gets its row /
391        /// column, even with no data (`drop = FALSE`); unlisted values are
392        /// dropped (#412). `None` = values in first-appearance order.
393        row_levels: Option<Vec<String>>,
394        col_levels: Option<Vec<String>>,
395        /// An extra "(all)" row and column pooling every row / column
396        /// (ggplot2's `margins = TRUE`, #412).
397        margins: bool,
398    },
399}
400
401impl Facet {
402    pub fn is_none(&self) -> bool {
403        matches!(self, Facet::None)
404    }
405
406    pub fn labeller(&self) -> &FacetLabeller {
407        match self {
408            Facet::None => &FacetLabeller::Value,
409            Facet::Wrap { labeller, .. } => labeller,
410            Facet::Grid { labeller, .. } => labeller,
411        }
412    }
413}
414
415#[cfg(test)]
416mod labeller_tests {
417    use super::FacetLabeller;
418
419    #[test]
420    fn template_per_var_and_lookup() {
421        assert_eq!(
422            FacetLabeller::Template("{var} = {value}".into()).format("cyl", "4"),
423            "cyl = 4"
424        );
425        let l = FacetLabeller::per_var(vec![
426            ("cyl", FacetLabeller::Both),
427            (
428                "am",
429                FacetLabeller::lookup(vec![("0", "automatic"), ("1", "manual")]),
430            ),
431        ]);
432        assert_eq!(l.format("cyl", "6"), "cyl: 6");
433        assert_eq!(l.format("am", "1"), "manual");
434        assert_eq!(l.format("am", "2"), "2");
435        assert_eq!(l.format("gear", "3"), "3");
436        assert_eq!(FacetLabeller::MultiLine.format("v", "a . b"), "a\nb");
437        let p = |e: &str| FacetLabeller::Parsed.format("v", e);
438        assert_eq!(p("alpha^2"), "α²");
439        assert_eq!(p("x[i] == beta"), "xᵢ = β");
440        assert_eq!(p("10^{-3}~mu*g"), "10⁻³ μg");
441        assert_eq!(p("a %*% b"), "a × b");
442    }
443}