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