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_margin: Margin {
170            top: 10.0,
171            right: 15.0,
172            bottom: 10.0,
173            left: 10.0,
174        },
175        panel_spacing: half_line,
176        panel_spacing_x: None,
177        panel_spacing_y: None,
178        primary: None,
179        aspect_ratio: None,
180        panel_ontop: false,
181        axis_minor_ticks: false,
182        title_position: crate::theme::TitlePosition::Panel,
183        tag_position: crate::theme::TagPosition::TopLeft,
184        legend_direction: None,
185        calendar_month_border: ElementLine {
186            color: (120, 128, 140),
187            width: 1.2,
188            visible: true,
189            linetype: crate::render::backend::Linetype::Solid,
190        },
191        legend_bar_border: None,
192        geom: crate::theme::elements::ElementGeom::default(),
193    }
194}
195
196// ─── theme_bw ────────────────────────────────────────────────────
197
198/// Black and white theme (default base size).
199pub fn theme_bw() -> Theme {
200    theme_bw_base(DEFAULT_BASE_SIZE)
201}
202
203/// Black and white theme with custom base font size.
204///
205/// Inherits from theme_grey. White panel with grey20 border,
206/// grey92 gridlines on white background.
207pub fn theme_bw_base(base_size: f64) -> Theme {
208    Theme {
209        panel_background: ElementRect {
210            fill: Some((255, 255, 255)),
211            color: None,
212            width: 0.0,
213            visible: true,
214        },
215        panel_border: ElementLine {
216            color: (51, 51, 51), // grey20
217            width: 1.0,
218            visible: true,
219            linetype: crate::render::backend::Linetype::Solid,
220        },
221        panel_grid_major: ElementLine {
222            color: (235, 235, 235), // grey92
223            width: 0.5,
224            visible: true,
225            linetype: crate::render::backend::Linetype::Solid,
226        },
227        panel_grid_minor: ElementLine {
228            color: (235, 235, 235), // grey92, thinner
229            width: 0.25,
230            visible: true,
231            linetype: crate::render::backend::Linetype::Solid,
232        },
233        strip_background: ElementRect {
234            fill: Some((217, 217, 217)), // grey85
235            color: Some((51, 51, 51)),   // grey20
236            width: 0.5,
237            visible: true,
238        },
239        legend_key: ElementRect {
240            fill: Some((255, 255, 255)), // white
241            color: None,
242            width: 0.0,
243            visible: true,
244        },
245        ..theme_gray_base(base_size)
246    }
247}
248
249// ─── theme_minimal ───────────────────────────────────────────────
250
251/// Minimal theme with no panel background (default base size).
252pub fn theme_minimal() -> Theme {
253    theme_minimal_base(DEFAULT_BASE_SIZE)
254}
255
256/// Minimal theme with no backgrounds. Inherits from theme_bw.
257pub fn theme_minimal_base(base_size: f64) -> Theme {
258    Theme {
259        axis_ticks: ElementLine::blank(),
260        panel_background: ElementRect::blank(),
261        panel_border: ElementLine::blank(),
262        panel_grid_major: ElementLine {
263            color: (235, 235, 235), // grey92 (from bw)
264            width: 0.5,
265            visible: true,
266            linetype: crate::render::backend::Linetype::Solid,
267        },
268        panel_grid_minor: ElementLine {
269            color: (235, 235, 235),
270            width: 0.25,
271            visible: true,
272            linetype: crate::render::backend::Linetype::Solid,
273        },
274        plot_background: ElementRect::blank(),
275        legend_background: ElementRect::blank(),
276        legend_key: ElementRect::blank(),
277        strip_background: ElementRect::blank(),
278        ..theme_bw_base(base_size)
279    }
280}
281
282// ─── theme_classic ───────────────────────────────────────────────
283
284/// Classic theme: white background, no gridlines, L-shaped axis lines only.
285/// Traditional academic/publication style. Inherits from theme_bw.
286pub fn theme_classic() -> Theme {
287    theme_classic_base(DEFAULT_BASE_SIZE)
288}
289
290/// Classic theme with custom base font size.
291pub fn theme_classic_base(base_size: f64) -> Theme {
292    Theme {
293        panel_border: ElementLine::blank(),
294        panel_grid_major: ElementLine::blank(),
295        panel_grid_minor: ElementLine::blank(),
296        axis_line: ElementLine {
297            color: (0, 0, 0),
298            width: 0.5,
299            visible: true,
300            linetype: crate::render::backend::Linetype::Solid,
301        },
302        axis_ticks: ElementLine {
303            color: (0, 0, 0),
304            width: 0.5,
305            visible: true,
306            linetype: crate::render::backend::Linetype::Solid,
307        },
308        strip_background: ElementRect {
309            fill: Some((255, 255, 255)),
310            color: Some((0, 0, 0)),
311            width: 1.0,
312            visible: true,
313        },
314        ..theme_bw_base(base_size)
315    }
316}
317
318// ─── theme_pubr (ggpubr) ─────────────────────────────────────────
319
320/// Publication-ready theme, matching R's `ggpubr::theme_pubr()`: white
321/// background, no gridlines, black axis lines and ticks, and the legend placed
322/// at the top. Built for clean, journal-style figures. Inherits from
323/// [`theme_classic_base`].
324pub fn theme_pubr() -> Theme {
325    theme_pubr_base(12.0)
326}
327
328/// [`theme_pubr`] with a custom base font size (ggpubr's default is 12 pt).
329pub fn theme_pubr_base(base_size: f64) -> Theme {
330    Theme {
331        legend_position: LegendPosition::Top,
332        // ggpubr draws no strip background box (labels sit directly above panels).
333        strip_background: ElementRect::blank(),
334        ..theme_classic_base(base_size)
335    }
336}
337
338// ─── theme_linedraw ──────────────────────────────────────────────
339
340/// Linedraw theme: white background, black panel border, very thin black gridlines.
341/// Technical drawing aesthetic. Inherits from theme_bw.
342pub fn theme_linedraw() -> Theme {
343    theme_linedraw_base(DEFAULT_BASE_SIZE)
344}
345
346/// Linedraw theme with custom base font size.
347pub fn theme_linedraw_base(base_size: f64) -> Theme {
348    Theme {
349        panel_border: ElementLine {
350            color: (0, 0, 0),
351            width: 1.0,
352            visible: true,
353            linetype: crate::render::backend::Linetype::Solid,
354        },
355        panel_grid_major: ElementLine {
356            color: (0, 0, 0), // black, very thin
357            width: 0.1,
358            visible: true,
359            linetype: crate::render::backend::Linetype::Solid,
360        },
361        panel_grid_minor: ElementLine {
362            color: (0, 0, 0), // black, extremely thin
363            width: 0.05,
364            visible: true,
365            linetype: crate::render::backend::Linetype::Solid,
366        },
367        axis_ticks: ElementLine {
368            color: (0, 0, 0),
369            width: 0.5,
370            visible: true,
371            linetype: crate::render::backend::Linetype::Solid,
372        },
373        strip_background: ElementRect {
374            fill: Some((0, 0, 0)), // black
375            color: None,
376            width: 0.0,
377            visible: true,
378        },
379        strip_text: ElementText {
380            size: base_size * 0.8,
381            color: (255, 255, 255), // white text on black strip
382            ..Default::default()
383        },
384        ..theme_bw_base(base_size)
385    }
386}
387
388// ─── theme_light ─────────────────────────────────────────────────
389
390/// Light theme: white background, light gray panel border and gridlines.
391/// Softer version with grey70 accents. Inherits from theme_grey.
392pub fn theme_light() -> Theme {
393    theme_light_base(DEFAULT_BASE_SIZE)
394}
395
396/// Light theme with custom base font size.
397pub fn theme_light_base(base_size: f64) -> Theme {
398    Theme {
399        panel_background: ElementRect {
400            fill: Some((255, 255, 255)),
401            color: None,
402            width: 0.0,
403            visible: true,
404        },
405        panel_border: ElementLine {
406            color: (179, 179, 179), // grey70
407            width: 1.0,
408            visible: true,
409            linetype: crate::render::backend::Linetype::Solid,
410        },
411        panel_grid_major: ElementLine {
412            color: (222, 222, 222), // grey87
413            width: 0.5,
414            visible: true,
415            linetype: crate::render::backend::Linetype::Solid,
416        },
417        panel_grid_minor: ElementLine {
418            color: (222, 222, 222), // grey87, thinner
419            width: 0.25,
420            visible: true,
421            linetype: crate::render::backend::Linetype::Solid,
422        },
423        axis_ticks: ElementLine {
424            color: (179, 179, 179), // grey70
425            width: 0.5,
426            visible: true,
427            linetype: crate::render::backend::Linetype::Solid,
428        },
429        legend_key: ElementRect {
430            fill: Some((255, 255, 255)),
431            color: None,
432            width: 0.0,
433            visible: true,
434        },
435        strip_background: ElementRect {
436            fill: Some((179, 179, 179)), // grey70
437            color: None,
438            width: 0.0,
439            visible: true,
440        },
441        strip_text: ElementText {
442            size: base_size * 0.8,
443            color: (255, 255, 255), // white text on grey70 strip
444            ..Default::default()
445        },
446        ..theme_gray_base(base_size)
447    }
448}
449
450// ─── theme_dark ──────────────────────────────────────────────────
451
452/// Dark theme: white plot background with dark grey50 panel.
453/// Makes colored data pop. Inherits from theme_grey.
454pub fn theme_dark() -> Theme {
455    theme_dark_base(DEFAULT_BASE_SIZE)
456}
457
458/// Dark theme with custom base font size.
459///
460/// Note: ggplot2's theme_dark has a white plot background but dark panel.
461/// Text remains black (inherited from theme_grey).
462pub fn theme_dark_base(base_size: f64) -> Theme {
463    Theme {
464        panel_background: ElementRect {
465            fill: Some((127, 127, 127)), // grey50
466            color: None,
467            width: 0.0,
468            visible: true,
469        },
470        panel_grid_major: ElementLine {
471            color: (107, 107, 107), // ~grey42
472            width: 0.5,
473            visible: true,
474            linetype: crate::render::backend::Linetype::Solid,
475        },
476        panel_grid_minor: ElementLine {
477            color: (107, 107, 107), // ~grey42, thinner
478            width: 0.25,
479            visible: true,
480            linetype: crate::render::backend::Linetype::Solid,
481        },
482        axis_ticks: ElementLine {
483            color: (51, 51, 51), // grey20
484            width: 0.5,
485            visible: true,
486            linetype: crate::render::backend::Linetype::Solid,
487        },
488        strip_background: ElementRect {
489            fill: Some((38, 38, 38)), // ~grey15
490            color: None,
491            width: 0.0,
492            visible: true,
493        },
494        strip_text: ElementText {
495            size: base_size * 0.8,
496            color: (230, 230, 230), // grey90
497            ..Default::default()
498        },
499        legend_key: ElementRect {
500            fill: Some((127, 127, 127)), // grey50, matches panel
501            color: None,
502            width: 0.0,
503            visible: true,
504        },
505        ..theme_gray_base(base_size)
506    }
507}
508
509// ─── theme_void ──────────────────────────────────────────────────
510
511/// Void theme: completely blank — no axes, ticks, gridlines, labels, or background.
512/// Canvas for maps or custom visualizations. Legend is retained.
513pub fn theme_void() -> Theme {
514    theme_void_base(DEFAULT_BASE_SIZE)
515}
516
517/// Void theme with custom base font size.
518pub fn theme_void_base(base_size: f64) -> Theme {
519    let (title_size, _, axis_text_size) = text_sizes(base_size);
520    Theme {
521        text: ElementText::blank(),
522        title: ElementText {
523            size: title_size,
524            ..Default::default()
525        },
526        axis_text_x: ElementText::blank(),
527        axis_text_y: ElementText::blank(),
528        axis_title_x: ElementText::blank(),
529        axis_title_y: ElementText::blank(),
530        axis_line: ElementLine::blank(),
531        axis_ticks: ElementLine::blank(),
532        panel_background: ElementRect::blank(),
533        panel_grid_major: ElementLine::blank(),
534        panel_grid_minor: ElementLine::blank(),
535        plot_background: ElementRect::blank(),
536        legend_position: LegendPosition::Right, // ggplot2 keeps legend in void
537        plot_margin: Margin {
538            top: 0.0,
539            right: 0.0,
540            bottom: 0.0,
541            left: 0.0,
542        },
543
544        subtitle: ElementText::blank(),
545        caption: ElementText::blank(),
546        legend_title: ElementText {
547            size: axis_text_size, // rel(0.8)
548            ..Default::default()
549        },
550        legend_text: ElementText {
551            size: axis_text_size, // rel(0.8)
552            ..Default::default()
553        },
554        strip_text: ElementText {
555            size: axis_text_size, // rel(0.8)
556            ..Default::default()
557        },
558
559        axis_line_x: None,
560        axis_line_y: None,
561        axis_ticks_x: None,
562        axis_ticks_y: None,
563        panel_grid_major_x: None,
564        panel_grid_major_y: None,
565        panel_grid_minor_x: None,
566        panel_grid_minor_y: None,
567
568        panel_border: ElementLine::blank(),
569        legend_background: ElementRect::blank(),
570        legend_key: ElementRect::blank(),
571        strip_background: ElementRect::blank(),
572
573        axis_ticks_length: 0.0,
574        axis_text_x_dodge: 1,
575        legend_key_width: 12.0,
576        legend_key_height: 18.0,
577        legend_spacing: 4.0,
578        legend_margin: Margin {
579            top: 0.0,
580            right: 0.0,
581            bottom: 0.0,
582            left: 0.0,
583        },
584        panel_spacing: 0.0,
585        panel_spacing_x: None,
586        panel_spacing_y: None,
587        primary: None,
588        aspect_ratio: None,
589        panel_ontop: false,
590        axis_minor_ticks: false,
591        title_position: crate::theme::TitlePosition::Panel,
592        tag_position: crate::theme::TagPosition::TopLeft,
593        legend_direction: None,
594        calendar_month_border: ElementLine {
595            color: (120, 128, 140),
596            width: 1.2,
597            visible: true,
598            linetype: crate::render::backend::Linetype::Solid,
599        },
600        legend_bar_border: None,
601        geom: crate::theme::elements::ElementGeom::default(),
602    }
603}