Skip to main content

ggplot_rs/theme/
presets.rs

1use super::elements::{ElementLine, ElementRect, ElementText};
2use super::{LegendPosition, Margin, Theme};
3
4/// Default base font size (matches ggplot2's default of 11pt).
5const DEFAULT_BASE_SIZE: f64 = 11.0;
6
7/// Compute text sizes relative to a base size, matching ggplot2 proportions.
8fn text_sizes(base_size: f64) -> (f64, f64, f64) {
9    let title = base_size * 1.2; // rel(1.2)
10    let axis_title = base_size; // inherits from text (rel(1.0))
11    let axis_text = base_size * 0.8; // rel(0.8)
12    (title, axis_title, axis_text)
13}
14
15// ─── theme_gray ──────────────────────────────────────────────────
16
17/// Classic ggplot2 gray theme with default base size.
18pub fn theme_gray() -> Theme {
19    theme_gray_base(DEFAULT_BASE_SIZE)
20}
21
22/// Classic ggplot2 gray theme with custom base font size.
23///
24/// Matches ggplot2's `theme_grey()` defaults:
25/// - Black text, grey30 axis text, grey20 ticks
26/// - Grey92 panel background with white gridlines
27/// - Axis lines blank (panel defined by background, not lines)
28/// - Grey85 strip background, grey10 strip text
29pub fn theme_gray_base(base_size: f64) -> Theme {
30    let (title_size, axis_title_size, axis_text_size) = text_sizes(base_size);
31    let half_line = base_size / 2.0;
32    Theme {
33        text: ElementText {
34            size: base_size,
35            color: (0, 0, 0),
36            ..Default::default()
37        },
38        title: ElementText {
39            size: title_size,
40            color: (0, 0, 0),
41            ..Default::default()
42        },
43        axis_text_x: ElementText {
44            size: axis_text_size,
45            color: (77, 77, 77), // grey30
46            ..Default::default()
47        },
48        axis_text_y: ElementText {
49            size: axis_text_size,
50            color: (77, 77, 77), // grey30
51            hjust: 1.0,
52            ..Default::default()
53        },
54        axis_title_x: ElementText {
55            size: axis_title_size,
56            color: (0, 0, 0),
57            ..Default::default()
58        },
59        axis_title_y: ElementText {
60            size: axis_title_size,
61            color: (0, 0, 0),
62            angle: 90.0,
63            ..Default::default()
64        },
65        axis_line: ElementLine::blank(), // ggplot2: element_blank()
66        axis_ticks: ElementLine {
67            color: (51, 51, 51), // grey20
68            width: 0.5,
69            visible: true,
70            linetype: crate::render::backend::Linetype::Solid,
71        },
72        panel_background: ElementRect {
73            fill: Some((235, 235, 235)), // grey92
74            color: None,
75            width: 0.0,
76            visible: true,
77        },
78        panel_grid_major: ElementLine {
79            color: (255, 255, 255), // white
80            width: 1.0,
81            visible: true,
82            linetype: crate::render::backend::Linetype::Solid,
83        },
84        panel_grid_minor: ElementLine {
85            color: (255, 255, 255), // white (same as major, just thinner)
86            width: 0.5,
87            visible: true,
88            linetype: crate::render::backend::Linetype::Solid,
89        },
90        plot_background: ElementRect {
91            fill: Some((255, 255, 255)),
92            color: None,
93            width: 0.0,
94            visible: true,
95        },
96        legend_position: LegendPosition::Right,
97        plot_margin: Margin {
98            top: half_line,
99            right: half_line,
100            bottom: half_line,
101            left: half_line,
102        },
103
104        // ── New text elements ──
105        subtitle: ElementText {
106            size: base_size,
107            color: (0, 0, 0),
108            ..Default::default()
109        },
110        caption: ElementText {
111            size: axis_text_size, // rel(0.8)
112            color: (0, 0, 0),
113            hjust: 1.0, // right-aligned by default (R's plot.caption)
114            ..Default::default()
115        },
116        legend_title: ElementText {
117            size: base_size,
118            color: (0, 0, 0),
119            ..Default::default()
120        },
121        legend_text: ElementText {
122            size: axis_text_size, // rel(0.8)
123            color: (0, 0, 0),
124            ..Default::default()
125        },
126        strip_text: ElementText {
127            size: axis_text_size, // rel(0.8)
128            color: (26, 26, 26),  // grey10
129            ..Default::default()
130        },
131
132        // ── Per-axis overrides (None = inherit) ──
133        axis_line_x: None,
134        axis_line_y: None,
135        axis_ticks_x: None,
136        axis_ticks_y: None,
137        panel_grid_major_x: None,
138        panel_grid_major_y: None,
139        panel_grid_minor_x: None,
140        panel_grid_minor_y: None,
141
142        // ── New rect/line elements ──
143        panel_border: ElementLine::blank(),
144        legend_background: ElementRect {
145            fill: Some((255, 255, 255)),
146            color: None,
147            width: 0.0,
148            visible: true,
149        },
150        legend_key: ElementRect {
151            fill: Some((242, 242, 242)), // grey95
152            color: None,
153            width: 0.0,
154            visible: true,
155        },
156        strip_background: ElementRect {
157            fill: Some((217, 217, 217)), // grey85
158            color: None,                 // colour = NA
159            width: 0.0,
160            visible: true,
161        },
162
163        // ── Scalar spacing/sizing ──
164        axis_ticks_length: half_line / 2.0,
165        axis_text_x_dodge: 1,
166        legend_key_width: 12.0,
167        legend_key_height: 18.0,
168        legend_spacing: 4.0,
169        legend_ticks_length: 3.0,
170        strip_placement_outside: false,
171        caption_position: None,
172        legend_location_plot: false,
173        axis_ticks_length_x: None,
174        axis_ticks_length_y: None,
175        axis_line_x_top: None,
176        axis_line_x_bottom: None,
177        axis_line_y_left: None,
178        axis_line_y_right: None,
179        axis_ticks_x_top: None,
180        axis_ticks_x_bottom: None,
181        axis_ticks_y_left: None,
182        axis_ticks_y_right: None,
183        legend_key_spacing_x: None,
184        legend_key_spacing_y: None,
185        legend_box_spacing: None,
186        legend_box_just: 0.0,
187        rich_text: false,
188        tag_location_panel: false,
189        strip_switch_pad: 2.75,
190        legend_margin: Margin {
191            top: 10.0,
192            right: 15.0,
193            bottom: 10.0,
194            left: 10.0,
195        },
196        panel_spacing: half_line,
197        panel_spacing_x: None,
198        panel_spacing_y: None,
199        primary: None,
200        aspect_ratio: None,
201        panel_ontop: false,
202        axis_minor_ticks: false,
203        title_position: crate::theme::TitlePosition::Panel,
204        tag_position: crate::theme::TagPosition::TopLeft,
205        legend_direction: None,
206        calendar_month_border: ElementLine {
207            color: (120, 128, 140),
208            width: 1.2,
209            visible: true,
210            linetype: crate::render::backend::Linetype::Solid,
211        },
212        legend_bar_border: None,
213        geom: crate::theme::elements::ElementGeom::default(),
214        axis_text_x_fit: crate::theme::AxisLabelFit::default(),
215        axis_text_x_check_overlap: false,
216        axis_text_x_top: None,
217        axis_text_y_right: None,
218        strip_text_x: None,
219        strip_background_x: None,
220        strip_text_y: None,
221        strip_background_y: None,
222    }
223}
224
225// ─── theme_bw ────────────────────────────────────────────────────
226
227/// Black and white theme (default base size).
228pub fn theme_bw() -> Theme {
229    theme_bw_base(DEFAULT_BASE_SIZE)
230}
231
232/// Black and white theme with custom base font size.
233///
234/// Inherits from theme_grey. White panel with grey20 border,
235/// grey92 gridlines on white background.
236pub fn theme_bw_base(base_size: f64) -> Theme {
237    Theme {
238        panel_background: ElementRect {
239            fill: Some((255, 255, 255)),
240            color: None,
241            width: 0.0,
242            visible: true,
243        },
244        panel_border: ElementLine {
245            color: (51, 51, 51), // grey20
246            width: 1.0,
247            visible: true,
248            linetype: crate::render::backend::Linetype::Solid,
249        },
250        panel_grid_major: ElementLine {
251            color: (235, 235, 235), // grey92
252            width: 0.5,
253            visible: true,
254            linetype: crate::render::backend::Linetype::Solid,
255        },
256        panel_grid_minor: ElementLine {
257            color: (235, 235, 235), // grey92, thinner
258            width: 0.25,
259            visible: true,
260            linetype: crate::render::backend::Linetype::Solid,
261        },
262        strip_background: ElementRect {
263            fill: Some((217, 217, 217)), // grey85
264            color: Some((51, 51, 51)),   // grey20
265            width: 0.5,
266            visible: true,
267        },
268        legend_key: ElementRect {
269            fill: Some((255, 255, 255)), // white
270            color: None,
271            width: 0.0,
272            visible: true,
273        },
274        ..theme_gray_base(base_size)
275    }
276}
277
278// ─── theme_test ──────────────────────────────────────────────────
279
280/// ggplot2's `theme_test()` (#413): `theme_bw` without gridlines — a
281/// stable look for visual tests.
282pub fn theme_test() -> Theme {
283    theme_test_base(DEFAULT_BASE_SIZE)
284}
285
286/// [`theme_test`] with a custom base font size.
287pub fn theme_test_base(base_size: f64) -> Theme {
288    let bw = theme_bw_base(base_size);
289    Theme {
290        panel_grid_major: ElementLine {
291            visible: false,
292            ..bw.panel_grid_major.clone()
293        },
294        panel_grid_minor: ElementLine {
295            visible: false,
296            ..bw.panel_grid_minor.clone()
297        },
298        ..bw
299    }
300}
301
302// ─── theme_minimal ───────────────────────────────────────────────
303
304/// Minimal theme with no panel background (default base size).
305pub fn theme_minimal() -> Theme {
306    theme_minimal_base(DEFAULT_BASE_SIZE)
307}
308
309/// Minimal theme with no backgrounds. Inherits from theme_bw.
310pub fn theme_minimal_base(base_size: f64) -> Theme {
311    Theme {
312        axis_ticks: ElementLine::blank(),
313        panel_background: ElementRect::blank(),
314        panel_border: ElementLine::blank(),
315        panel_grid_major: ElementLine {
316            color: (235, 235, 235), // grey92 (from bw)
317            width: 0.5,
318            visible: true,
319            linetype: crate::render::backend::Linetype::Solid,
320        },
321        panel_grid_minor: ElementLine {
322            color: (235, 235, 235),
323            width: 0.25,
324            visible: true,
325            linetype: crate::render::backend::Linetype::Solid,
326        },
327        plot_background: ElementRect::blank(),
328        legend_background: ElementRect::blank(),
329        legend_key: ElementRect::blank(),
330        strip_background: ElementRect::blank(),
331        ..theme_bw_base(base_size)
332    }
333}
334
335// ─── theme_classic ───────────────────────────────────────────────
336
337/// Classic theme: white background, no gridlines, L-shaped axis lines only.
338/// Traditional academic/publication style. Inherits from theme_bw.
339pub fn theme_classic() -> Theme {
340    theme_classic_base(DEFAULT_BASE_SIZE)
341}
342
343/// Classic theme with custom base font size.
344pub fn theme_classic_base(base_size: f64) -> Theme {
345    Theme {
346        panel_border: ElementLine::blank(),
347        panel_grid_major: ElementLine::blank(),
348        panel_grid_minor: ElementLine::blank(),
349        axis_line: ElementLine {
350            color: (0, 0, 0),
351            width: 0.5,
352            visible: true,
353            linetype: crate::render::backend::Linetype::Solid,
354        },
355        axis_ticks: ElementLine {
356            color: (0, 0, 0),
357            width: 0.5,
358            visible: true,
359            linetype: crate::render::backend::Linetype::Solid,
360        },
361        strip_background: ElementRect {
362            fill: Some((255, 255, 255)),
363            color: Some((0, 0, 0)),
364            width: 1.0,
365            visible: true,
366        },
367        ..theme_bw_base(base_size)
368    }
369}
370
371// ─── theme_pubr (ggpubr) ─────────────────────────────────────────
372
373/// Publication-ready theme, matching R's `ggpubr::theme_pubr()`: white
374/// background, no gridlines, black axis lines and ticks, and the legend placed
375/// at the top. Built for clean, journal-style figures. Inherits from
376/// [`theme_classic_base`].
377pub fn theme_pubr() -> Theme {
378    theme_pubr_base(12.0)
379}
380
381/// [`theme_pubr`] with a custom base font size (ggpubr's default is 12 pt).
382pub fn theme_pubr_base(base_size: f64) -> Theme {
383    Theme {
384        legend_position: LegendPosition::Top,
385        // ggpubr draws no strip background box (labels sit directly above panels).
386        strip_background: ElementRect::blank(),
387        ..theme_classic_base(base_size)
388    }
389}
390
391// ─── theme_linedraw ──────────────────────────────────────────────
392
393/// Linedraw theme: white background, black panel border, very thin black gridlines.
394/// Technical drawing aesthetic. Inherits from theme_bw.
395pub fn theme_linedraw() -> Theme {
396    theme_linedraw_base(DEFAULT_BASE_SIZE)
397}
398
399/// Linedraw theme with custom base font size.
400pub fn theme_linedraw_base(base_size: f64) -> Theme {
401    Theme {
402        panel_border: ElementLine {
403            color: (0, 0, 0),
404            width: 1.0,
405            visible: true,
406            linetype: crate::render::backend::Linetype::Solid,
407        },
408        panel_grid_major: ElementLine {
409            color: (0, 0, 0), // black, very thin
410            width: 0.1,
411            visible: true,
412            linetype: crate::render::backend::Linetype::Solid,
413        },
414        panel_grid_minor: ElementLine {
415            color: (0, 0, 0), // black, extremely thin
416            width: 0.05,
417            visible: true,
418            linetype: crate::render::backend::Linetype::Solid,
419        },
420        axis_ticks: ElementLine {
421            color: (0, 0, 0),
422            width: 0.5,
423            visible: true,
424            linetype: crate::render::backend::Linetype::Solid,
425        },
426        strip_background: ElementRect {
427            fill: Some((0, 0, 0)), // black
428            color: None,
429            width: 0.0,
430            visible: true,
431        },
432        strip_text: ElementText {
433            size: base_size * 0.8,
434            color: (255, 255, 255), // white text on black strip
435            ..Default::default()
436        },
437        ..theme_bw_base(base_size)
438    }
439}
440
441// ─── theme_light ─────────────────────────────────────────────────
442
443/// Light theme: white background, light gray panel border and gridlines.
444/// Softer version with grey70 accents. Inherits from theme_grey.
445pub fn theme_light() -> Theme {
446    theme_light_base(DEFAULT_BASE_SIZE)
447}
448
449/// Light theme with custom base font size.
450pub fn theme_light_base(base_size: f64) -> Theme {
451    Theme {
452        panel_background: ElementRect {
453            fill: Some((255, 255, 255)),
454            color: None,
455            width: 0.0,
456            visible: true,
457        },
458        panel_border: ElementLine {
459            color: (179, 179, 179), // grey70
460            width: 1.0,
461            visible: true,
462            linetype: crate::render::backend::Linetype::Solid,
463        },
464        panel_grid_major: ElementLine {
465            color: (222, 222, 222), // grey87
466            width: 0.5,
467            visible: true,
468            linetype: crate::render::backend::Linetype::Solid,
469        },
470        panel_grid_minor: ElementLine {
471            color: (222, 222, 222), // grey87, thinner
472            width: 0.25,
473            visible: true,
474            linetype: crate::render::backend::Linetype::Solid,
475        },
476        axis_ticks: ElementLine {
477            color: (179, 179, 179), // grey70
478            width: 0.5,
479            visible: true,
480            linetype: crate::render::backend::Linetype::Solid,
481        },
482        legend_key: ElementRect {
483            fill: Some((255, 255, 255)),
484            color: None,
485            width: 0.0,
486            visible: true,
487        },
488        strip_background: ElementRect {
489            fill: Some((179, 179, 179)), // grey70
490            color: None,
491            width: 0.0,
492            visible: true,
493        },
494        strip_text: ElementText {
495            size: base_size * 0.8,
496            color: (255, 255, 255), // white text on grey70 strip
497            ..Default::default()
498        },
499        ..theme_gray_base(base_size)
500    }
501}
502
503// ─── theme_dark ──────────────────────────────────────────────────
504
505/// Dark theme: white plot background with dark grey50 panel.
506/// Makes colored data pop. Inherits from theme_grey.
507pub fn theme_dark() -> Theme {
508    theme_dark_base(DEFAULT_BASE_SIZE)
509}
510
511/// Dark theme with custom base font size.
512///
513/// Note: ggplot2's theme_dark has a white plot background but dark panel.
514/// Text remains black (inherited from theme_grey).
515pub fn theme_dark_base(base_size: f64) -> Theme {
516    Theme {
517        panel_background: ElementRect {
518            fill: Some((127, 127, 127)), // grey50
519            color: None,
520            width: 0.0,
521            visible: true,
522        },
523        panel_grid_major: ElementLine {
524            color: (107, 107, 107), // ~grey42
525            width: 0.5,
526            visible: true,
527            linetype: crate::render::backend::Linetype::Solid,
528        },
529        panel_grid_minor: ElementLine {
530            color: (107, 107, 107), // ~grey42, thinner
531            width: 0.25,
532            visible: true,
533            linetype: crate::render::backend::Linetype::Solid,
534        },
535        axis_ticks: ElementLine {
536            color: (51, 51, 51), // grey20
537            width: 0.5,
538            visible: true,
539            linetype: crate::render::backend::Linetype::Solid,
540        },
541        strip_background: ElementRect {
542            fill: Some((38, 38, 38)), // ~grey15
543            color: None,
544            width: 0.0,
545            visible: true,
546        },
547        strip_text: ElementText {
548            size: base_size * 0.8,
549            color: (230, 230, 230), // grey90
550            ..Default::default()
551        },
552        legend_key: ElementRect {
553            fill: Some((127, 127, 127)), // grey50, matches panel
554            color: None,
555            width: 0.0,
556            visible: true,
557        },
558        ..theme_gray_base(base_size)
559    }
560}
561
562// ─── theme_void ──────────────────────────────────────────────────
563
564/// Void theme: completely blank — no axes, ticks, gridlines, labels, or background.
565/// Canvas for maps or custom visualizations. Legend is retained.
566pub fn theme_void() -> Theme {
567    theme_void_base(DEFAULT_BASE_SIZE)
568}
569
570/// Void theme with custom base font size.
571pub fn theme_void_base(base_size: f64) -> Theme {
572    let (title_size, _, axis_text_size) = text_sizes(base_size);
573    Theme {
574        text: ElementText::blank(),
575        title: ElementText {
576            size: title_size,
577            ..Default::default()
578        },
579        axis_text_x: ElementText::blank(),
580        axis_text_y: ElementText::blank(),
581        axis_title_x: ElementText::blank(),
582        axis_title_y: ElementText::blank(),
583        axis_line: ElementLine::blank(),
584        axis_ticks: ElementLine::blank(),
585        panel_background: ElementRect::blank(),
586        panel_grid_major: ElementLine::blank(),
587        panel_grid_minor: ElementLine::blank(),
588        plot_background: ElementRect::blank(),
589        legend_position: LegendPosition::Right, // ggplot2 keeps legend in void
590        plot_margin: Margin {
591            top: 0.0,
592            right: 0.0,
593            bottom: 0.0,
594            left: 0.0,
595        },
596
597        subtitle: ElementText::blank(),
598        caption: ElementText::blank(),
599        legend_title: ElementText {
600            size: axis_text_size, // rel(0.8)
601            ..Default::default()
602        },
603        legend_text: ElementText {
604            size: axis_text_size, // rel(0.8)
605            ..Default::default()
606        },
607        strip_text: ElementText {
608            size: axis_text_size, // rel(0.8)
609            ..Default::default()
610        },
611
612        axis_line_x: None,
613        axis_line_y: None,
614        axis_ticks_x: None,
615        axis_ticks_y: None,
616        panel_grid_major_x: None,
617        panel_grid_major_y: None,
618        panel_grid_minor_x: None,
619        panel_grid_minor_y: None,
620
621        panel_border: ElementLine::blank(),
622        legend_background: ElementRect::blank(),
623        legend_key: ElementRect::blank(),
624        strip_background: ElementRect::blank(),
625
626        axis_ticks_length: 0.0,
627        axis_text_x_dodge: 1,
628        legend_key_width: 12.0,
629        legend_key_height: 18.0,
630        legend_spacing: 4.0,
631        legend_ticks_length: 3.0,
632        strip_placement_outside: false,
633        caption_position: None,
634        legend_location_plot: false,
635        axis_ticks_length_x: None,
636        axis_ticks_length_y: None,
637        axis_line_x_top: None,
638        axis_line_x_bottom: None,
639        axis_line_y_left: None,
640        axis_line_y_right: None,
641        axis_ticks_x_top: None,
642        axis_ticks_x_bottom: None,
643        axis_ticks_y_left: None,
644        axis_ticks_y_right: None,
645        legend_key_spacing_x: None,
646        legend_key_spacing_y: None,
647        legend_box_spacing: None,
648        legend_box_just: 0.0,
649        rich_text: false,
650        tag_location_panel: false,
651        strip_switch_pad: 2.75,
652        legend_margin: Margin {
653            top: 0.0,
654            right: 0.0,
655            bottom: 0.0,
656            left: 0.0,
657        },
658        panel_spacing: 0.0,
659        panel_spacing_x: None,
660        panel_spacing_y: None,
661        primary: None,
662        aspect_ratio: None,
663        panel_ontop: false,
664        axis_minor_ticks: false,
665        title_position: crate::theme::TitlePosition::Panel,
666        tag_position: crate::theme::TagPosition::TopLeft,
667        legend_direction: None,
668        calendar_month_border: ElementLine {
669            color: (120, 128, 140),
670            width: 1.2,
671            visible: true,
672            linetype: crate::render::backend::Linetype::Solid,
673        },
674        legend_bar_border: None,
675        geom: crate::theme::elements::ElementGeom::default(),
676        axis_text_x_fit: crate::theme::AxisLabelFit::default(),
677        axis_text_x_check_overlap: false,
678        axis_text_x_top: None,
679        axis_text_y_right: None,
680        strip_text_x: None,
681        strip_background_x: None,
682        strip_text_y: None,
683        strip_background_y: None,
684    }
685}