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