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