Skip to main content

ggplot_rs/
plot.rs

1use plotters::prelude::IntoDrawingArea;
2
3use crate::aes::Aes;
4use crate::annotate::Annotation;
5use crate::build::PlotBuilder;
6use crate::coord::cartesian::CoordCartesian;
7use crate::coord::fixed::CoordFixed;
8use crate::coord::flip::CoordFlip;
9use crate::coord::polar::CoordPolar;
10use crate::coord::Coord;
11use crate::data::{DataFrame, GGData};
12use crate::facet::{Facet, FacetLabeller, FacetScales};
13use crate::geom::area::GeomArea;
14use crate::geom::bar::GeomBar;
15use crate::geom::bin2d::GeomBin2d;
16use crate::geom::blank::GeomBlank;
17use crate::geom::boxplot::GeomBoxplot;
18use crate::geom::col::GeomCol;
19use crate::geom::contour::GeomContour;
20use crate::geom::count::GeomCount;
21use crate::geom::crossbar::GeomCrossbar;
22use crate::geom::curve::GeomCurve;
23use crate::geom::density::GeomDensity;
24use crate::geom::density2d::GeomDensity2d;
25use crate::geom::dotplot::GeomDotplot;
26use crate::geom::errorbar::GeomErrorbar;
27use crate::geom::freqpoly::GeomFreqpoly;
28use crate::geom::hex::GeomHex;
29use crate::geom::histogram::GeomHistogram;
30use crate::geom::jitter::GeomJitter;
31use crate::geom::line::GeomLine;
32use crate::geom::linerange::GeomLinerange;
33use crate::geom::path::GeomPath;
34use crate::geom::point::GeomPoint;
35use crate::geom::pointrange::GeomPointrange;
36use crate::geom::polygon::GeomPolygon;
37use crate::geom::qq::{GeomQQ, GeomQQLine};
38use crate::geom::rect::GeomRect;
39use crate::geom::refline::{GeomAbline, GeomHline, GeomVline};
40use crate::geom::ribbon::GeomRibbon;
41use crate::geom::rug::GeomRug;
42use crate::geom::segment::GeomSegment;
43use crate::geom::smooth::GeomSmooth;
44use crate::geom::spoke::GeomSpoke;
45use crate::geom::step::GeomStep;
46use crate::geom::text::{GeomLabel, GeomText};
47use crate::geom::tile::GeomTile;
48use crate::geom::violin::GeomViolin;
49use crate::geom::{Geom, GeomParams};
50use crate::position::Position;
51use crate::render::layout::PlotLayout;
52use crate::render::plotters_backend::PlottersAdapter;
53use crate::render::renderer::PlotRenderer;
54use crate::render::RenderError;
55use crate::scale::continuous::ScaleContinuous;
56use crate::scale::transform::ScaleTransform;
57use crate::scale::Scale;
58use crate::stat::Stat;
59use crate::theme::Theme;
60
61/// Labels for the plot.
62#[derive(Clone, Debug, Default)]
63pub struct Labels {
64    pub title: Option<String>,
65    pub subtitle: Option<String>,
66    pub x: Option<String>,
67    pub y: Option<String>,
68    pub caption: Option<String>,
69}
70
71/// A single layer in the plot.
72pub struct Layer {
73    pub data: Option<DataFrame>,
74    pub mapping: Aes,
75    pub geom: Box<dyn Geom>,
76    pub stat: Box<dyn Stat>,
77    pub position: Box<dyn Position>,
78    pub params: GeomParams,
79    pub show_legend: Option<bool>,
80}
81
82/// The top-level plot specification — builder pattern.
83pub struct GGPlot {
84    pub(crate) data: DataFrame,
85    pub(crate) mapping: Aes,
86    pub(crate) layers: Vec<Layer>,
87    pub(crate) scales: Vec<Box<dyn Scale>>,
88    pub(crate) coord: Box<dyn Coord>,
89    pub(crate) theme: Theme,
90    pub(crate) labels: Labels,
91    pub(crate) facet: Facet,
92    pub(crate) annotations: Vec<Annotation>,
93    pub(crate) guide_legend: crate::guide::config::GuideLegend,
94}
95
96impl GGPlot {
97    /// Create a new plot with the given data source.
98    pub fn new(data: impl GGData) -> Self {
99        GGPlot {
100            data: data.into_dataframe(),
101            mapping: Aes::default(),
102            layers: Vec::new(),
103            scales: Vec::new(),
104            coord: Box::new(CoordCartesian::new()),
105            theme: Theme::default(),
106            labels: Labels::default(),
107            facet: Facet::default(),
108            annotations: Vec::new(),
109            guide_legend: crate::guide::config::GuideLegend::default(),
110        }
111    }
112
113    /// Set the plot-level aesthetic mapping.
114    pub fn aes(mut self, mapping: Aes) -> Self {
115        self.mapping = mapping;
116        self
117    }
118
119    // ─── Geom shortcuts ──────────────────────────────────────────
120
121    pub fn geom_point(self) -> Self {
122        self.add_geom(GeomPoint::default())
123    }
124
125    pub fn geom_point_with(self, geom: GeomPoint) -> Self {
126        self.add_geom(geom)
127    }
128
129    pub fn geom_line(self) -> Self {
130        self.add_geom(GeomLine::default())
131    }
132
133    pub fn geom_line_with(self, geom: GeomLine) -> Self {
134        self.add_geom(geom)
135    }
136
137    pub fn geom_bar(self) -> Self {
138        self.add_geom(GeomBar::default())
139    }
140
141    pub fn geom_bar_with(self, geom: GeomBar) -> Self {
142        self.add_geom(geom)
143    }
144
145    pub fn geom_histogram(self) -> Self {
146        self.add_geom(GeomHistogram::default())
147    }
148
149    pub fn geom_histogram_with(self, geom: GeomHistogram) -> Self {
150        self.add_geom(geom)
151    }
152
153    pub fn geom_boxplot(self) -> Self {
154        self.add_geom(GeomBoxplot::default())
155    }
156
157    pub fn geom_boxplot_with(self, geom: GeomBoxplot) -> Self {
158        self.add_geom(geom)
159    }
160
161    pub fn geom_smooth(self) -> Self {
162        self.add_geom(GeomSmooth::default())
163    }
164
165    pub fn geom_smooth_with(self, geom: GeomSmooth) -> Self {
166        self.add_geom(geom)
167    }
168
169    pub fn geom_col(self) -> Self {
170        self.add_geom(GeomCol::default())
171    }
172
173    pub fn geom_col_with(self, geom: GeomCol) -> Self {
174        self.add_geom(geom)
175    }
176
177    pub fn geom_hline(self, yintercept: f64) -> Self {
178        self.add_geom(GeomHline::new(yintercept))
179    }
180
181    /// Add a horizontal reference line with custom styling (color/linetype/width).
182    pub fn geom_hline_with(self, geom: GeomHline) -> Self {
183        self.add_geom(geom)
184    }
185
186    pub fn geom_vline(self, xintercept: f64) -> Self {
187        self.add_geom(GeomVline::new(xintercept))
188    }
189
190    /// Add a vertical reference line with custom styling (color/linetype/width).
191    pub fn geom_vline_with(self, geom: GeomVline) -> Self {
192        self.add_geom(geom)
193    }
194
195    pub fn geom_abline(self, slope: f64, intercept: f64) -> Self {
196        self.add_geom(GeomAbline::new(slope, intercept))
197    }
198
199    /// Add a slope/intercept reference line with custom styling.
200    pub fn geom_abline_with(self, geom: GeomAbline) -> Self {
201        self.add_geom(geom)
202    }
203
204    pub fn geom_text(self) -> Self {
205        self.add_geom(GeomText::default())
206    }
207
208    pub fn geom_text_with(self, geom: GeomText) -> Self {
209        self.add_geom(geom)
210    }
211
212    pub fn geom_label(self) -> Self {
213        self.add_geom(GeomLabel::default())
214    }
215
216    pub fn geom_label_with(self, geom: GeomLabel) -> Self {
217        self.add_geom(geom)
218    }
219
220    pub fn geom_area(self) -> Self {
221        self.add_geom(GeomArea::default())
222    }
223
224    pub fn geom_area_with(self, geom: GeomArea) -> Self {
225        self.add_geom(geom)
226    }
227
228    pub fn geom_ribbon(self) -> Self {
229        self.add_geom(GeomRibbon::default())
230    }
231
232    pub fn geom_ribbon_with(self, geom: GeomRibbon) -> Self {
233        self.add_geom(geom)
234    }
235
236    pub fn geom_errorbar(self) -> Self {
237        self.add_geom(GeomErrorbar::default())
238    }
239
240    pub fn geom_errorbar_with(self, geom: GeomErrorbar) -> Self {
241        self.add_geom(geom)
242    }
243
244    pub fn geom_segment(self) -> Self {
245        self.add_geom(GeomSegment::default())
246    }
247
248    pub fn geom_segment_with(self, geom: GeomSegment) -> Self {
249        self.add_geom(geom)
250    }
251
252    pub fn geom_density(self) -> Self {
253        self.add_geom(GeomDensity::default())
254    }
255
256    pub fn geom_density_with(self, geom: GeomDensity) -> Self {
257        self.add_geom(geom)
258    }
259
260    pub fn geom_rug(self) -> Self {
261        self.add_geom(GeomRug::default())
262    }
263
264    pub fn geom_rug_with(self, geom: GeomRug) -> Self {
265        self.add_geom(geom)
266    }
267
268    pub fn geom_jitter(self) -> Self {
269        self.add_geom(GeomJitter::default())
270    }
271
272    pub fn geom_jitter_with(self, geom: GeomJitter) -> Self {
273        self.add_geom(geom)
274    }
275
276    pub fn geom_path(self) -> Self {
277        self.add_geom(GeomPath::default())
278    }
279
280    pub fn geom_path_with(self, geom: GeomPath) -> Self {
281        self.add_geom(geom)
282    }
283
284    /// Add a confidence-ellipse layer (default 95%) as a path per group.
285    pub fn stat_ellipse(self) -> Self {
286        self.geom_path()
287            .stat(crate::stat::ellipse::StatEllipse::default())
288    }
289
290    /// Add a confidence-ellipse layer at the given level (0, 1).
291    pub fn stat_ellipse_level(self, level: f64) -> Self {
292        self.geom_path()
293            .stat(crate::stat::ellipse::StatEllipse::new(level))
294    }
295
296    /// Add a quantile-regression line for each `tau` as a separate path layer
297    /// (R's `stat_quantile`). Backed by anofox-regression (feature `regression`).
298    #[cfg(feature = "regression")]
299    pub fn stat_quantile(mut self, taus: &[f64]) -> Self {
300        for &tau in taus {
301            self = self
302                .geom_path()
303                .stat(crate::stat::quantile::StatQuantile::new(tau));
304        }
305        self
306    }
307
308    /// Quantile-regression lines at the quartiles (0.25, 0.5, 0.75).
309    #[cfg(feature = "regression")]
310    pub fn geom_quantile(self) -> Self {
311        self.stat_quantile(&[0.25, 0.5, 0.75])
312    }
313
314    pub fn geom_step(self) -> Self {
315        self.add_geom(GeomStep::default())
316    }
317
318    pub fn geom_step_with(self, geom: GeomStep) -> Self {
319        self.add_geom(geom)
320    }
321
322    pub fn geom_freqpoly(self) -> Self {
323        self.add_geom(GeomFreqpoly::default())
324    }
325
326    pub fn geom_freqpoly_with(self, geom: GeomFreqpoly) -> Self {
327        self.add_geom(geom)
328    }
329
330    pub fn geom_linerange(self) -> Self {
331        self.add_geom(GeomLinerange::default())
332    }
333
334    pub fn geom_linerange_with(self, geom: GeomLinerange) -> Self {
335        self.add_geom(geom)
336    }
337
338    pub fn geom_pointrange(self) -> Self {
339        self.add_geom(GeomPointrange::default())
340    }
341
342    pub fn geom_pointrange_with(self, geom: GeomPointrange) -> Self {
343        self.add_geom(geom)
344    }
345
346    pub fn geom_crossbar(self) -> Self {
347        self.add_geom(GeomCrossbar::default())
348    }
349
350    pub fn geom_crossbar_with(self, geom: GeomCrossbar) -> Self {
351        self.add_geom(geom)
352    }
353
354    pub fn geom_spoke(self) -> Self {
355        self.add_geom(GeomSpoke::default())
356    }
357
358    pub fn geom_spoke_with(self, geom: GeomSpoke) -> Self {
359        self.add_geom(geom)
360    }
361
362    pub fn geom_rect(self) -> Self {
363        self.add_geom(GeomRect::default())
364    }
365
366    pub fn geom_rect_with(self, geom: GeomRect) -> Self {
367        self.add_geom(geom)
368    }
369
370    pub fn geom_tile(self) -> Self {
371        self.add_geom(GeomTile::default())
372    }
373
374    pub fn geom_tile_with(self, geom: GeomTile) -> Self {
375        self.add_geom(geom)
376    }
377
378    /// Dense regular grid of filled cells (heatmap/raster) from x, y, fill.
379    pub fn geom_raster(self) -> Self {
380        self.add_geom(crate::geom::raster::GeomRaster::default())
381    }
382
383    pub fn geom_raster_with(self, geom: crate::geom::raster::GeomRaster) -> Self {
384        self.add_geom(geom)
385    }
386
387    pub fn geom_polygon(self) -> Self {
388        self.add_geom(GeomPolygon::default())
389    }
390
391    pub fn geom_polygon_with(self, geom: GeomPolygon) -> Self {
392        self.add_geom(geom)
393    }
394
395    pub fn geom_curve(self) -> Self {
396        self.add_geom(GeomCurve::default())
397    }
398
399    pub fn geom_curve_with(self, geom: GeomCurve) -> Self {
400        self.add_geom(geom)
401    }
402
403    pub fn geom_violin(self) -> Self {
404        self.add_geom(GeomViolin::default())
405    }
406
407    pub fn geom_violin_with(self, geom: GeomViolin) -> Self {
408        self.add_geom(geom)
409    }
410
411    pub fn geom_dotplot(self) -> Self {
412        self.add_geom(GeomDotplot::default())
413    }
414
415    pub fn geom_dotplot_with(self, geom: GeomDotplot) -> Self {
416        self.add_geom(geom)
417    }
418
419    pub fn geom_qq(self) -> Self {
420        self.add_geom(GeomQQ::default())
421    }
422
423    pub fn geom_qq_with(self, geom: GeomQQ) -> Self {
424        self.add_geom(geom)
425    }
426
427    pub fn geom_qq_line(self) -> Self {
428        self.add_geom(GeomQQLine::default())
429    }
430
431    pub fn geom_qq_line_with(self, geom: GeomQQLine) -> Self {
432        self.add_geom(geom)
433    }
434
435    pub fn geom_bin2d(self) -> Self {
436        self.add_geom(GeomBin2d::default())
437    }
438
439    pub fn geom_bin2d_with(self, geom: GeomBin2d) -> Self {
440        self.add_geom(geom)
441    }
442
443    pub fn geom_hex(self) -> Self {
444        self.add_geom(GeomHex::default())
445    }
446
447    pub fn geom_hex_with(self, geom: GeomHex) -> Self {
448        self.add_geom(geom)
449    }
450
451    pub fn geom_count(self) -> Self {
452        self.add_geom(GeomCount::default())
453    }
454
455    pub fn geom_count_with(self, geom: GeomCount) -> Self {
456        self.add_geom(geom)
457    }
458
459    pub fn geom_contour(self) -> Self {
460        self.add_geom(GeomContour::default())
461    }
462
463    pub fn geom_contour_with(self, geom: GeomContour) -> Self {
464        self.add_geom(geom)
465    }
466
467    /// Filled contour bands from gridded (x, y, z) data — draws polygons filled by
468    /// band level. Pair with a continuous fill scale (e.g. `scale_fill_viridis_c`).
469    pub fn geom_contour_filled(self) -> Self {
470        self.add_geom(GeomPolygon {
471            line_width: 0.0,
472            alpha: 1.0,
473            ..GeomPolygon::default()
474        })
475        .stat(crate::stat::contour_filled::StatContourFilled::default())
476    }
477
478    pub fn geom_density2d(self) -> Self {
479        self.add_geom(GeomDensity2d::default())
480    }
481
482    pub fn geom_density2d_with(self, geom: GeomDensity2d) -> Self {
483        self.add_geom(geom)
484    }
485
486    pub fn geom_blank(self) -> Self {
487        self.add_geom(GeomBlank)
488    }
489
490    fn add_geom(mut self, geom: impl Geom + 'static) -> Self {
491        let stat = geom.default_stat();
492        let position = geom.default_position();
493        let params = geom.default_params();
494        self.layers.push(Layer {
495            data: None,
496            mapping: Aes::default(),
497            geom: Box::new(geom),
498            stat,
499            position,
500            params,
501            show_legend: None,
502        });
503        self
504    }
505
506    // ─── Layer-level overrides ──────────────────────────────────
507
508    /// Override the stat for the most recently added layer.
509    pub fn stat(mut self, stat: impl Stat + 'static) -> Self {
510        if let Some(layer) = self.layers.last_mut() {
511            layer.stat = Box::new(stat);
512        }
513        self
514    }
515
516    /// Override the position for the most recently added layer.
517    pub fn position(mut self, pos: impl Position + 'static) -> Self {
518        if let Some(layer) = self.layers.last_mut() {
519            layer.position = Box::new(pos);
520        }
521        self
522    }
523
524    /// Override the data for the most recently added layer.
525    pub fn layer_data(mut self, data: impl GGData) -> Self {
526        if let Some(layer) = self.layers.last_mut() {
527            layer.data = Some(data.into_dataframe());
528        }
529        self
530    }
531
532    /// Override the aesthetic mapping for the most recently added layer.
533    pub fn layer_aes(mut self, mapping: Aes) -> Self {
534        if let Some(layer) = self.layers.last_mut() {
535            layer.mapping = mapping;
536        }
537        self
538    }
539
540    /// Control whether the most recently added layer contributes to the legend.
541    /// `true` = always show, `false` = always hide, default (None) = auto.
542    pub fn show_legend(mut self, show: bool) -> Self {
543        if let Some(layer) = self.layers.last_mut() {
544            layer.show_legend = Some(show);
545        }
546        self
547    }
548
549    // ─── Scales ──────────────────────────────────────────────────
550
551    pub fn scale_x_continuous(mut self, s: ScaleContinuous) -> Self {
552        let s = s.for_aesthetic(crate::aes::Aesthetic::X);
553        self.scales.push(Box::new(s));
554        self
555    }
556
557    pub fn scale_y_continuous(mut self, s: ScaleContinuous) -> Self {
558        let s = s.for_aesthetic(crate::aes::Aesthetic::Y);
559        self.scales.push(Box::new(s));
560        self
561    }
562
563    pub fn scale_x_discrete(mut self, s: crate::scale::discrete::ScaleDiscrete) -> Self {
564        let s = s.for_aesthetic(crate::aes::Aesthetic::X);
565        self.scales.push(Box::new(s));
566        self
567    }
568
569    pub fn scale_y_discrete(mut self, s: crate::scale::discrete::ScaleDiscrete) -> Self {
570        let s = s.for_aesthetic(crate::aes::Aesthetic::Y);
571        self.scales.push(Box::new(s));
572        self
573    }
574
575    pub fn scale_color(mut self, s: impl Scale + 'static) -> Self {
576        self.scales.push(Box::new(s));
577        self
578    }
579
580    pub fn scale_fill(mut self, s: impl Scale + 'static) -> Self {
581        self.scales.push(Box::new(s));
582        self
583    }
584
585    pub fn scale_color_manual(self, values: Vec<(&str, crate::scale::color::RGBAColor)>) -> Self {
586        let s = crate::scale::manual::ScaleManual::new(crate::aes::Aesthetic::Color, values);
587        self.scale_color(s)
588    }
589
590    pub fn scale_fill_manual(self, values: Vec<(&str, crate::scale::color::RGBAColor)>) -> Self {
591        let s = crate::scale::manual::ScaleManual::new(crate::aes::Aesthetic::Fill, values);
592        self.scale_fill(s)
593    }
594
595    pub fn scale_color_viridis(self) -> Self {
596        use crate::scale::color::ScaleColorDiscrete;
597        use crate::scale::palettes::PaletteName;
598        let s = ScaleColorDiscrete::new(crate::aes::Aesthetic::Color)
599            .with_named_palette(&PaletteName::Viridis);
600        self.scale_color(s)
601    }
602
603    pub fn scale_color_brewer(self, name: crate::scale::palettes::PaletteName) -> Self {
604        use crate::scale::color::ScaleColorDiscrete;
605        let s = ScaleColorDiscrete::new(crate::aes::Aesthetic::Color).with_named_palette(&name);
606        self.scale_color(s)
607    }
608
609    pub fn scale_color_gradient(
610        self,
611        low: crate::scale::color::RGBAColor,
612        high: crate::scale::color::RGBAColor,
613    ) -> Self {
614        use crate::scale::color::ScaleColorContinuous;
615        let s = ScaleColorContinuous::new(crate::aes::Aesthetic::Color).with_colors(low, high);
616        self.scale_color(s)
617    }
618
619    pub fn scale_fill_gradient(
620        self,
621        low: crate::scale::color::RGBAColor,
622        high: crate::scale::color::RGBAColor,
623    ) -> Self {
624        use crate::scale::color::ScaleColorContinuous;
625        let s = ScaleColorContinuous::new(crate::aes::Aesthetic::Fill).with_colors(low, high);
626        self.scale_fill(s)
627    }
628
629    pub fn scale_color_gradient2(
630        self,
631        low: crate::scale::color::RGBAColor,
632        mid: crate::scale::color::RGBAColor,
633        high: crate::scale::color::RGBAColor,
634    ) -> Self {
635        use crate::scale::gradient::ScaleColorGradient2;
636        let s = ScaleColorGradient2::new(crate::aes::Aesthetic::Color).with_colors(low, mid, high);
637        self.scale_color(s)
638    }
639
640    pub fn scale_fill_gradient2(
641        self,
642        low: crate::scale::color::RGBAColor,
643        mid: crate::scale::color::RGBAColor,
644        high: crate::scale::color::RGBAColor,
645    ) -> Self {
646        use crate::scale::gradient::ScaleColorGradient2;
647        let s = ScaleColorGradient2::new(crate::aes::Aesthetic::Fill).with_colors(low, mid, high);
648        self.scale_fill(s)
649    }
650
651    pub fn scale_fill_viridis(self) -> Self {
652        use crate::scale::color::ScaleColorDiscrete;
653        use crate::scale::palettes::PaletteName;
654        let s = ScaleColorDiscrete::new(crate::aes::Aesthetic::Fill)
655            .with_named_palette(&PaletteName::Viridis);
656        self.scale_fill(s)
657    }
658
659    /// Continuous viridis color scale (for numeric data).
660    pub fn scale_color_viridis_c(self) -> Self {
661        use crate::scale::gradient_n::ScaleColorGradientN;
662        let s = ScaleColorGradientN::viridis(crate::aes::Aesthetic::Color);
663        self.scale_color(s)
664    }
665
666    /// Continuous viridis fill scale (for numeric data).
667    pub fn scale_fill_viridis_c(self) -> Self {
668        use crate::scale::gradient_n::ScaleColorGradientN;
669        let s = ScaleColorGradientN::viridis(crate::aes::Aesthetic::Fill);
670        self.scale_fill(s)
671    }
672
673    /// N-stop continuous color gradient.
674    pub fn scale_color_gradientn(self, stops: Vec<(f64, crate::scale::color::RGBAColor)>) -> Self {
675        use crate::scale::gradient_n::ScaleColorGradientN;
676        let s = ScaleColorGradientN::new(crate::aes::Aesthetic::Color, stops);
677        self.scale_color(s)
678    }
679
680    /// N-stop continuous fill gradient.
681    pub fn scale_fill_gradientn(self, stops: Vec<(f64, crate::scale::color::RGBAColor)>) -> Self {
682        use crate::scale::gradient_n::ScaleColorGradientN;
683        let s = ScaleColorGradientN::new(crate::aes::Aesthetic::Fill, stops);
684        self.scale_fill(s)
685    }
686
687    /// Binned (stepped) two-colour continuous colour scale — buckets the mapped
688    /// variable into `n_bins` bins, each a discrete colour, with a stepped legend.
689    pub fn scale_color_steps(
690        self,
691        low: crate::scale::color::RGBAColor,
692        high: crate::scale::color::RGBAColor,
693        n_bins: usize,
694    ) -> Self {
695        let s = crate::scale::steps::ScaleColorSteps::two(
696            crate::aes::Aesthetic::Color,
697            (low.r, low.g, low.b),
698            (high.r, high.g, high.b),
699            n_bins,
700        );
701        self.scale_color(s)
702    }
703
704    /// Binned N-stop continuous colour scale.
705    pub fn scale_color_stepsn(
706        self,
707        stops: Vec<crate::scale::color::RGBAColor>,
708        n_bins: usize,
709    ) -> Self {
710        let s = crate::scale::steps::ScaleColorSteps::new(
711            crate::aes::Aesthetic::Color,
712            stops.iter().map(|c| (c.r, c.g, c.b)).collect(),
713            n_bins,
714        );
715        self.scale_color(s)
716    }
717
718    /// Binned ColorBrewer colour scale (R's `scale_color_fermenter`).
719    pub fn scale_color_fermenter(
720        self,
721        name: crate::scale::palettes::PaletteName,
722        n_bins: usize,
723    ) -> Self {
724        let stops = crate::scale::palettes::palette(&name)
725            .iter()
726            .map(|c| (c.r, c.g, c.b))
727            .collect();
728        let s =
729            crate::scale::steps::ScaleColorSteps::new(crate::aes::Aesthetic::Color, stops, n_bins);
730        self.scale_color(s)
731    }
732
733    /// Binned (stepped) two-colour continuous fill scale.
734    pub fn scale_fill_steps(
735        self,
736        low: crate::scale::color::RGBAColor,
737        high: crate::scale::color::RGBAColor,
738        n_bins: usize,
739    ) -> Self {
740        let s = crate::scale::steps::ScaleColorSteps::two(
741            crate::aes::Aesthetic::Fill,
742            (low.r, low.g, low.b),
743            (high.r, high.g, high.b),
744            n_bins,
745        );
746        self.scale_fill(s)
747    }
748
749    /// Binned ColorBrewer fill scale.
750    pub fn scale_fill_fermenter(
751        self,
752        name: crate::scale::palettes::PaletteName,
753        n_bins: usize,
754    ) -> Self {
755        let stops = crate::scale::palettes::palette(&name)
756            .iter()
757            .map(|c| (c.r, c.g, c.b))
758            .collect();
759        let s =
760            crate::scale::steps::ScaleColorSteps::new(crate::aes::Aesthetic::Fill, stops, n_bins);
761        self.scale_fill(s)
762    }
763
764    pub fn scale_fill_brewer(self, name: crate::scale::palettes::PaletteName) -> Self {
765        use crate::scale::color::ScaleColorDiscrete;
766        let s = ScaleColorDiscrete::new(crate::aes::Aesthetic::Fill).with_named_palette(&name);
767        self.scale_fill(s)
768    }
769
770    pub fn scale_linetype_manual(
771        self,
772        values: Vec<(&str, crate::render::backend::Linetype)>,
773    ) -> Self {
774        let s = crate::scale::linetype_manual::ScaleLinetypeManual::new(values);
775        self.scale_color(s)
776    }
777
778    pub fn scale_shape_manual(
779        self,
780        values: Vec<(&str, crate::render::backend::PointShape)>,
781    ) -> Self {
782        let s = crate::scale::shape_manual::ScaleShapeManual::new(values);
783        self.scale_color(s)
784    }
785
786    pub fn scale_color_grey(self) -> Self {
787        let s = crate::scale::grey::ScaleColorGrey::new(crate::aes::Aesthetic::Color);
788        self.scale_color(s)
789    }
790
791    pub fn scale_fill_grey(self) -> Self {
792        let s = crate::scale::grey::ScaleColorGrey::new(crate::aes::Aesthetic::Fill);
793        self.scale_fill(s)
794    }
795
796    pub fn scale_color_grey_with(self, s: crate::scale::grey::ScaleColorGrey) -> Self {
797        self.scale_color(s)
798    }
799
800    pub fn scale_fill_grey_with(self, s: crate::scale::grey::ScaleColorGrey) -> Self {
801        self.scale_fill(s)
802    }
803
804    pub fn scale_x_reverse(self) -> Self {
805        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Reverse))
806    }
807
808    pub fn scale_y_reverse(self) -> Self {
809        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Reverse))
810    }
811
812    pub fn scale_x_datetime(mut self, s: crate::scale::datetime::ScaleDateTime) -> Self {
813        let s = s.for_aesthetic(crate::aes::Aesthetic::X);
814        self.scales.push(Box::new(s));
815        self
816    }
817
818    pub fn scale_y_datetime(mut self, s: crate::scale::datetime::ScaleDateTime) -> Self {
819        let s = s.for_aesthetic(crate::aes::Aesthetic::Y);
820        self.scales.push(Box::new(s));
821        self
822    }
823
824    pub fn scale_size(mut self, s: crate::scale::size::ScaleSizeContinuous) -> Self {
825        self.scales.push(Box::new(s));
826        self
827    }
828
829    pub fn scale_alpha(mut self, s: crate::scale::alpha::ScaleAlphaContinuous) -> Self {
830        self.scales.push(Box::new(s));
831        self
832    }
833
834    pub fn xlim(self, min: f64, max: f64) -> Self {
835        self.scale_x_continuous(ScaleContinuous::new().with_limits(min, max))
836    }
837
838    pub fn ylim(self, min: f64, max: f64) -> Self {
839        self.scale_y_continuous(ScaleContinuous::new().with_limits(min, max))
840    }
841
842    pub fn scale_x_log10(self) -> Self {
843        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Log10))
844    }
845
846    pub fn scale_y_log10(self) -> Self {
847        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Log10))
848    }
849
850    pub fn scale_x_sqrt(self) -> Self {
851        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Sqrt))
852    }
853
854    pub fn scale_y_sqrt(self) -> Self {
855        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Sqrt))
856    }
857
858    pub fn scale_x_log2(self) -> Self {
859        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Log2))
860    }
861
862    pub fn scale_y_log2(self) -> Self {
863        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Log2))
864    }
865
866    pub fn scale_x_ln(self) -> Self {
867        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Ln))
868    }
869
870    pub fn scale_y_ln(self) -> Self {
871        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Ln))
872    }
873
874    /// Logit-transformed x axis (for proportions in (0, 1)).
875    pub fn scale_x_logit(self) -> Self {
876        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Logit))
877    }
878
879    pub fn scale_y_logit(self) -> Self {
880        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Logit))
881    }
882
883    /// Probit-transformed x axis (inverse normal CDF, for proportions in (0, 1)).
884    pub fn scale_x_probit(self) -> Self {
885        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Probit))
886    }
887
888    pub fn scale_y_probit(self) -> Self {
889        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Probit))
890    }
891
892    /// Sign-preserving pseudo-log x axis (handles zero and negative values).
893    pub fn scale_x_pseudo_log(self) -> Self {
894        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::PseudoLog))
895    }
896
897    pub fn scale_y_pseudo_log(self) -> Self {
898        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::PseudoLog))
899    }
900
901    /// Reciprocal (1/x) x axis.
902    pub fn scale_x_reciprocal(self) -> Self {
903        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Reciprocal))
904    }
905
906    pub fn scale_y_reciprocal(self) -> Self {
907        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Reciprocal))
908    }
909
910    /// Exponential x axis (labels spaced logarithmically).
911    pub fn scale_x_exp(self) -> Self {
912        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Exp))
913    }
914
915    pub fn scale_y_exp(self) -> Self {
916        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Exp))
917    }
918
919    /// Box–Cox x axis with the given lambda (x > 0).
920    pub fn scale_x_boxcox(self, lambda: f64) -> Self {
921        self.scale_x_continuous(
922            ScaleContinuous::new().with_transform(ScaleTransform::BoxCox(lambda)),
923        )
924    }
925
926    pub fn scale_y_boxcox(self, lambda: f64) -> Self {
927        self.scale_y_continuous(
928            ScaleContinuous::new().with_transform(ScaleTransform::BoxCox(lambda)),
929        )
930    }
931
932    // ─── Faceting ─────────────────────────────────────────────────
933
934    pub fn facet_wrap(mut self, var: &str, ncol: Option<usize>) -> Self {
935        self.facet = Facet::Wrap {
936            var: var.to_string(),
937            ncol,
938            scales: FacetScales::Fixed,
939            labeller: FacetLabeller::default(),
940        };
941        self
942    }
943
944    pub fn facet_wrap_free(mut self, var: &str, ncol: Option<usize>, scales: FacetScales) -> Self {
945        self.facet = Facet::Wrap {
946            var: var.to_string(),
947            ncol,
948            scales,
949            labeller: FacetLabeller::default(),
950        };
951        self
952    }
953
954    pub fn facet_wrap_labeller(
955        mut self,
956        var: &str,
957        ncol: Option<usize>,
958        labeller: FacetLabeller,
959    ) -> Self {
960        self.facet = Facet::Wrap {
961            var: var.to_string(),
962            ncol,
963            scales: FacetScales::Fixed,
964            labeller,
965        };
966        self
967    }
968
969    pub fn facet_grid(mut self, row: Option<&str>, col: Option<&str>) -> Self {
970        self.facet = Facet::Grid {
971            row_var: row.map(String::from),
972            col_var: col.map(String::from),
973            scales: FacetScales::Fixed,
974            labeller: FacetLabeller::default(),
975        };
976        self
977    }
978
979    pub fn facet_grid_free(
980        mut self,
981        row: Option<&str>,
982        col: Option<&str>,
983        scales: FacetScales,
984    ) -> Self {
985        self.facet = Facet::Grid {
986            row_var: row.map(String::from),
987            col_var: col.map(String::from),
988            scales,
989            labeller: FacetLabeller::default(),
990        };
991        self
992    }
993
994    pub fn facet_grid_labeller(
995        mut self,
996        row: Option<&str>,
997        col: Option<&str>,
998        labeller: FacetLabeller,
999    ) -> Self {
1000        self.facet = Facet::Grid {
1001            row_var: row.map(String::from),
1002            col_var: col.map(String::from),
1003            scales: FacetScales::Fixed,
1004            labeller,
1005        };
1006        self
1007    }
1008
1009    // ─── Coordinates ─────────────────────────────────────────────
1010
1011    pub fn coord_flip(mut self) -> Self {
1012        self.coord = Box::new(CoordFlip);
1013        self
1014    }
1015
1016    pub fn coord_fixed(mut self, ratio: f64) -> Self {
1017        self.coord = Box::new(CoordFixed::new(ratio));
1018        self
1019    }
1020
1021    /// Zoom into a region without filtering data (unlike xlim/ylim which filter).
1022    pub fn coord_cartesian_zoom(
1023        mut self,
1024        xlim: Option<(f64, f64)>,
1025        ylim: Option<(f64, f64)>,
1026    ) -> Self {
1027        let mut c = CoordCartesian::new();
1028        if let Some((min, max)) = xlim {
1029            c = c.xlim(min, max);
1030        }
1031        if let Some((min, max)) = ylim {
1032            c = c.ylim(min, max);
1033        }
1034        self.coord = Box::new(c);
1035        self
1036    }
1037
1038    pub fn coord_polar(mut self) -> Self {
1039        self.coord = Box::new(CoordPolar::new());
1040        self
1041    }
1042
1043    pub fn coord_polar_with(mut self, coord: CoordPolar) -> Self {
1044        self.coord = Box::new(coord);
1045        self
1046    }
1047
1048    // ─── Theme ───────────────────────────────────────────────────
1049
1050    pub fn theme(mut self, theme: Theme) -> Self {
1051        self.theme = theme;
1052        self
1053    }
1054
1055    /// Set the brand/primary color used as the default for single-series geoms
1056    /// that have no color/fill aesthetic mapped. Composes with any theme — one
1057    /// render process can serve different tenants' brands at render time.
1058    pub fn primary_color(mut self, color: (u8, u8, u8)) -> Self {
1059        self.theme.primary = Some(color);
1060        self
1061    }
1062
1063    pub fn theme_minimal(mut self) -> Self {
1064        self.theme = crate::theme::presets::theme_minimal();
1065        self
1066    }
1067
1068    pub fn theme_bw(mut self) -> Self {
1069        self.theme = crate::theme::presets::theme_bw();
1070        self
1071    }
1072
1073    pub fn theme_gray(mut self) -> Self {
1074        self.theme = crate::theme::presets::theme_gray();
1075        self
1076    }
1077
1078    pub fn theme_classic(mut self) -> Self {
1079        self.theme = crate::theme::presets::theme_classic();
1080        self
1081    }
1082
1083    pub fn theme_linedraw(mut self) -> Self {
1084        self.theme = crate::theme::presets::theme_linedraw();
1085        self
1086    }
1087
1088    pub fn theme_light(mut self) -> Self {
1089        self.theme = crate::theme::presets::theme_light();
1090        self
1091    }
1092
1093    pub fn theme_dark(mut self) -> Self {
1094        self.theme = crate::theme::presets::theme_dark();
1095        self
1096    }
1097
1098    pub fn theme_void(mut self) -> Self {
1099        self.theme = crate::theme::presets::theme_void();
1100        self
1101    }
1102
1103    /// Apply incremental theme modifications on top of the current theme.
1104    /// Like R's `+ theme(axis.text.x = element_text(...))`.
1105    pub fn theme_update(mut self, update: crate::theme::ThemeUpdate) -> Self {
1106        self.theme = self.theme.update(update);
1107        self
1108    }
1109
1110    // ─── Guides ──────────────────────────────────────────────────
1111
1112    /// Configure legend guide (title, ncol, reverse).
1113    pub fn guides(mut self, guide: crate::guide::config::GuideLegend) -> Self {
1114        self.guide_legend = guide;
1115        self
1116    }
1117
1118    // ─── Labels ──────────────────────────────────────────────────
1119
1120    pub fn labs(mut self, labels: Labels) -> Self {
1121        if labels.title.is_some() {
1122            self.labels.title = labels.title;
1123        }
1124        if labels.subtitle.is_some() {
1125            self.labels.subtitle = labels.subtitle;
1126        }
1127        if labels.x.is_some() {
1128            self.labels.x = labels.x;
1129        }
1130        if labels.y.is_some() {
1131            self.labels.y = labels.y;
1132        }
1133        if labels.caption.is_some() {
1134            self.labels.caption = labels.caption;
1135        }
1136        self
1137    }
1138
1139    pub fn title(mut self, title: &str) -> Self {
1140        self.labels.title = Some(title.to_string());
1141        self
1142    }
1143
1144    pub fn subtitle(mut self, subtitle: &str) -> Self {
1145        self.labels.subtitle = Some(subtitle.to_string());
1146        self
1147    }
1148
1149    pub fn xlab(mut self, label: &str) -> Self {
1150        self.labels.x = Some(label.to_string());
1151        self
1152    }
1153
1154    pub fn ylab(mut self, label: &str) -> Self {
1155        self.labels.y = Some(label.to_string());
1156        self
1157    }
1158
1159    pub fn caption(mut self, caption: &str) -> Self {
1160        self.labels.caption = Some(caption.to_string());
1161        self
1162    }
1163
1164    // ─── Annotations ──────────────────────────────────────────────
1165
1166    /// Add an annotation to the plot.
1167    pub fn annotate(mut self, annotation: Annotation) -> Self {
1168        self.annotations.push(annotation);
1169        self
1170    }
1171
1172    /// Add a text annotation at data coordinates.
1173    pub fn annotate_text(self, label: &str, x: f64, y: f64) -> Self {
1174        self.annotate(Annotation::text(label, x, y))
1175    }
1176
1177    /// Add a rectangle annotation at data coordinates.
1178    pub fn annotate_rect(self, xmin: f64, xmax: f64, ymin: f64, ymax: f64) -> Self {
1179        self.annotate(Annotation::rect(xmin, xmax, ymin, ymax))
1180    }
1181
1182    /// Add a segment annotation between data coordinates.
1183    pub fn annotate_segment(self, x: f64, y: f64, xend: f64, yend: f64) -> Self {
1184        self.annotate(Annotation::segment(x, y, xend, yend))
1185    }
1186
1187    // ─── Build and Render ────────────────────────────────────────
1188
1189    /// Build the plot without rendering, returning errors on validation failure.
1190    pub fn try_build(self) -> Result<crate::build::BuiltPlot, GGError> {
1191        PlotBuilder::build(self)
1192    }
1193
1194    /// Build the plot without rendering (analogous to R's ggplot_build()).
1195    /// Returns the fully computed BuiltPlot with layer data ready for inspection.
1196    /// Panics on validation errors — use `try_build()` for error handling.
1197    pub fn build(self) -> crate::build::BuiltPlot {
1198        self.try_build().expect("plot build failed")
1199    }
1200
1201    /// Build and save the plot to a file. Format determined by extension.
1202    pub fn save(self, path: &str) -> Result<(), GGError> {
1203        self.save_with_size(path, 800, 600)
1204    }
1205
1206    /// Build and save with custom dimensions.
1207    pub fn save_with_size(self, path: &str, w: u32, h: u32) -> Result<(), GGError> {
1208        let (built, layout) = self.prepare(w, h)?;
1209
1210        // Determine backend from file extension
1211        let ext = path.rsplit('.').next().unwrap_or("svg").to_lowercase();
1212
1213        match ext.as_str() {
1214            "svg" => {
1215                let backend = plotters::prelude::SVGBackend::new(path, (w, h));
1216                Self::render_into(backend.into_drawing_area(), &built, &layout)?;
1217            }
1218            "png" | "bmp" | "gif" | "jpeg" | "jpg" | "tiff" => {
1219                let backend = plotters::prelude::BitMapBackend::new(path, (w, h));
1220                Self::render_into(backend.into_drawing_area(), &built, &layout)?;
1221            }
1222            _ => {
1223                return Err(GGError::UnsupportedFormat(ext));
1224            }
1225        }
1226
1227        Ok(())
1228    }
1229
1230    /// Render the plot to an in-memory SVG document (default 800x600).
1231    ///
1232    /// Unlike [`save`](Self::save), this writes nothing to disk — handy for
1233    /// serving charts from a web/MCP service.
1234    pub fn render_svg(self) -> Result<String, GGError> {
1235        self.render_svg_with_size(800, 600)
1236    }
1237
1238    /// Render the plot to an in-memory SVG document with custom dimensions.
1239    pub fn render_svg_with_size(self, w: u32, h: u32) -> Result<String, GGError> {
1240        let (built, layout) = self.prepare(w, h)?;
1241        let mut buf = String::new();
1242        {
1243            let backend = plotters::prelude::SVGBackend::with_string(&mut buf, (w, h));
1244            Self::render_into(backend.into_drawing_area(), &built, &layout)?;
1245        }
1246        Ok(buf)
1247    }
1248
1249    /// Render the plot to in-memory PNG bytes (default 800x600).
1250    ///
1251    /// Returns a fully-encoded PNG, ready to write to an HTTP response or
1252    /// embed as a data URI — no temp files involved.
1253    pub fn render_png(self) -> Result<Vec<u8>, GGError> {
1254        self.render_png_with_size(800, 600)
1255    }
1256
1257    /// Render the plot to in-memory PNG bytes with custom dimensions.
1258    pub fn render_png_with_size(self, w: u32, h: u32) -> Result<Vec<u8>, GGError> {
1259        let (built, layout) = self.prepare(w, h)?;
1260
1261        // plotters' BitMapBackend draws into a raw RGB buffer; we then encode
1262        // that buffer to PNG via the `image` crate.
1263        let mut rgb = vec![0u8; (w as usize) * (h as usize) * 3];
1264        {
1265            let backend = plotters::prelude::BitMapBackend::with_buffer(&mut rgb, (w, h));
1266            Self::render_into(backend.into_drawing_area(), &built, &layout)?;
1267        }
1268
1269        let img = image::RgbImage::from_raw(w, h, rgb).ok_or_else(|| {
1270            GGError::Render(RenderError::BackendError(
1271                "PNG buffer size mismatch".to_string(),
1272            ))
1273        })?;
1274        let mut out = std::io::Cursor::new(Vec::new());
1275        img.write_to(&mut out, image::ImageOutputFormat::Png)
1276            .map_err(|e| GGError::Render(RenderError::BackendError(format!("{:?}", e))))?;
1277        Ok(out.into_inner())
1278    }
1279
1280    /// Shared pipeline: build the plot, apply label overrides, compute layout.
1281    fn prepare(self, w: u32, h: u32) -> Result<(crate::build::BuiltPlot, PlotLayout), GGError> {
1282        let plot = self;
1283
1284        let has_title = plot.labels.title.is_some();
1285        let has_subtitle = plot.labels.subtitle.is_some();
1286        let has_caption = plot.labels.caption.is_some();
1287        let has_legend = plot.has_legend_mapping();
1288        let x_label = plot.labels.x.clone();
1289        let y_label = plot.labels.y.clone();
1290
1291        let mut built = PlotBuilder::build(plot)?;
1292
1293        // Apply user label overrides to scales
1294        if let Some(ref label) = x_label {
1295            if let Some(s) = built.scales.get_mut(&crate::aes::Aesthetic::X) {
1296                s.set_name(label);
1297            }
1298        }
1299        if let Some(ref label) = y_label {
1300            if let Some(s) = built.scales.get_mut(&crate::aes::Aesthetic::Y) {
1301                s.set_name(label);
1302            }
1303        }
1304
1305        let layout = PlotLayout::compute_full(
1306            w as f64,
1307            h as f64,
1308            &built.theme,
1309            has_title,
1310            has_subtitle,
1311            has_caption,
1312            has_legend,
1313        );
1314
1315        Ok((built, layout))
1316    }
1317
1318    /// Fill the background, render the built plot, and flush — for any backend.
1319    fn render_into<DB>(
1320        area: plotters::drawing::DrawingArea<DB, plotters::coord::Shift>,
1321        built: &crate::build::BuiltPlot,
1322        layout: &PlotLayout,
1323    ) -> Result<(), GGError>
1324    where
1325        DB: plotters::prelude::DrawingBackend,
1326        DB::ErrorType: 'static,
1327    {
1328        area.fill(&plotters::prelude::WHITE)
1329            .map_err(|e| GGError::Render(RenderError::BackendError(format!("{:?}", e))))?;
1330        let mut adapter = PlottersAdapter::new(&area, layout.plot_area.clone());
1331        PlotRenderer::render(built, &mut adapter).map_err(GGError::Render)?;
1332        area.present()
1333            .map_err(|e| GGError::Render(RenderError::BackendError(format!("{:?}", e))))?;
1334        Ok(())
1335    }
1336
1337    /// Save with physical dimensions (inches) and DPI.
1338    pub fn ggsave(
1339        self,
1340        path: &str,
1341        width_inches: f64,
1342        height_inches: f64,
1343        dpi: f64,
1344    ) -> Result<(), GGError> {
1345        let w = (width_inches * dpi) as u32;
1346        let h = (height_inches * dpi) as u32;
1347        self.save_with_size(path, w, h)
1348    }
1349
1350    fn has_legend_mapping(&self) -> bool {
1351        self.mapping.mappings.iter().any(|m| {
1352            matches!(
1353                m.aesthetic,
1354                crate::aes::Aesthetic::Color
1355                    | crate::aes::Aesthetic::Fill
1356                    | crate::aes::Aesthetic::Shape
1357                    | crate::aes::Aesthetic::Linetype
1358                    | crate::aes::Aesthetic::Size
1359                    | crate::aes::Aesthetic::Alpha
1360            )
1361        })
1362    }
1363}
1364
1365/// Top-level error type.
1366#[derive(Debug)]
1367pub enum GGError {
1368    Render(RenderError),
1369    UnsupportedFormat(String),
1370    Io(std::io::Error),
1371    ValidationError(String),
1372}
1373
1374impl std::fmt::Display for GGError {
1375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1376        match self {
1377            GGError::Render(e) => write!(f, "Render error: {e}"),
1378            GGError::UnsupportedFormat(ext) => write!(f, "Unsupported output format: {ext}"),
1379            GGError::Io(e) => write!(f, "IO error: {e}"),
1380            GGError::ValidationError(msg) => write!(f, "Validation error: {msg}"),
1381        }
1382    }
1383}
1384
1385impl std::error::Error for GGError {}