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