Skip to main content

ggplot_rs/
plot.rs

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