Skip to main content

ggplot_rs/
plot.rs

1#[cfg(all(feature = "plotters", 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, Value};
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(all(feature = "plotters", 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// Builder methods for the 0.17 statistical-graphics layers (QQ, step ribbons,
64// ECDF bands, Cook's contours, horizontal error bars).
65mod stat_layers;
66
67/// Labels for the plot.
68#[derive(Clone, Debug, Default)]
69pub struct Labels {
70    pub title: Option<String>,
71    pub subtitle: Option<String>,
72    pub x: Option<String>,
73    pub y: Option<String>,
74    pub caption: Option<String>,
75    /// Corner tag (R's `labs(tag = ...)`), e.g. "A" for figure panels.
76    pub tag: Option<String>,
77}
78
79/// A single layer in the plot.
80pub struct Layer {
81    pub data: Option<DataFrame>,
82    pub mapping: Aes,
83    pub geom: Box<dyn Geom>,
84    pub stat: Box<dyn Stat>,
85    pub position: Box<dyn Position>,
86    pub params: GeomParams,
87    pub show_legend: Option<bool>,
88    /// The geom was configured explicitly (`geom_*_with(...)`), so its colours
89    /// are deliberate and the theme's `primary_color` must not override them.
90    pub explicit_style: bool,
91}
92
93/// The top-level plot specification — builder pattern.
94pub struct GGPlot {
95    pub(crate) data: DataFrame,
96    pub(crate) mapping: Aes,
97    pub(crate) layers: Vec<Layer>,
98    pub(crate) scales: Vec<Box<dyn Scale>>,
99    pub(crate) coord: Box<dyn Coord>,
100    pub(crate) theme: Theme,
101    pub(crate) labels: Labels,
102    pub(crate) facet: Facet,
103    pub(crate) annotations: Vec<Annotation>,
104    pub(crate) guide_legend: crate::guide::config::GuideLegend,
105    /// Warnings raised while specifying the plot (e.g. a clipped calendar
106    /// span); prepended to the build warnings.
107    pub(crate) warnings: Vec<String>,
108    /// Panel aspect ratio used when the theme sets none (helpers such as
109    /// `geom_calendar` need square cells; survives theme presets).
110    pub(crate) default_aspect_ratio: Option<f64>,
111    /// Plot-wide default locale for axis / legend labels ([`GGPlot::locale`]).
112    pub(crate) locale: Option<crate::locale::Locale>,
113    /// Whether the rendered root `<svg>` carries `data-plot`/`data-domain`/…
114    /// ([`svg_root_data`](Self::svg_root_data)). Default `true`; a consumer
115    /// overlaying a fragment on its own basemap (`render_svg_native_at`)
116    /// turns it off so the fragment adds no root attributes of its own.
117    pub(crate) svg_root_data: bool,
118}
119
120impl GGPlot {
121    /// Create a new plot with the given data source.
122    ///
123    /// Never panics on malformed input: e.g. column-oriented data whose columns
124    /// have different lengths is padded with `NA` and reported as a
125    /// [`GGError::ValidationError`] by [`try_build`](Self::try_build) and the
126    /// `render_*`/`save*` methods.
127    pub fn new(data: impl GGData) -> Self {
128        GGPlot {
129            data: data.into_dataframe(),
130            mapping: Aes::default(),
131            layers: Vec::new(),
132            scales: Vec::new(),
133            coord: Box::new(CoordCartesian::new()),
134            theme: Theme::default(),
135            labels: Labels::default(),
136            facet: Facet::default(),
137            annotations: Vec::new(),
138            guide_legend: crate::guide::config::GuideLegend::default(),
139            warnings: Vec::new(),
140            default_aspect_ratio: None,
141            locale: None,
142            svg_root_data: true,
143        }
144    }
145
146    /// Set the plot-level aesthetic mapping.
147    pub fn aes(mut self, mapping: Aes) -> Self {
148        self.warnings.extend(mapping.warnings.iter().cloned());
149        self.mapping = mapping;
150        self
151    }
152
153    // ─── Geom shortcuts ──────────────────────────────────────────
154
155    pub fn geom_point(self) -> Self {
156        self.add_geom(GeomPoint::default())
157    }
158
159    pub fn geom_point_with(self, geom: GeomPoint) -> Self {
160        self.add_geom_with(geom)
161    }
162
163    pub fn geom_line(self) -> Self {
164        self.add_geom(GeomLine::default())
165    }
166
167    pub fn geom_line_with(self, geom: GeomLine) -> Self {
168        self.add_geom_with(geom)
169    }
170
171    pub fn geom_bar(self) -> Self {
172        self.add_geom(GeomBar::default())
173    }
174
175    pub fn geom_bar_with(self, geom: GeomBar) -> Self {
176        self.add_geom_with(geom)
177    }
178
179    pub fn geom_histogram(self) -> Self {
180        self.add_geom(GeomHistogram::default())
181    }
182
183    pub fn geom_histogram_with(self, geom: GeomHistogram) -> Self {
184        self.add_geom_with(geom)
185    }
186
187    pub fn geom_boxplot(self) -> Self {
188        self.add_geom(GeomBoxplot::default())
189    }
190
191    pub fn geom_boxplot_with(self, geom: GeomBoxplot) -> Self {
192        self.add_geom_with(geom)
193    }
194
195    pub fn geom_smooth(self) -> Self {
196        self.add_geom(GeomSmooth::default())
197    }
198
199    pub fn geom_smooth_with(self, geom: GeomSmooth) -> Self {
200        self.add_geom_with(geom)
201    }
202
203    pub fn geom_col(self) -> Self {
204        self.add_geom(GeomCol::default())
205    }
206
207    pub fn geom_col_with(self, geom: GeomCol) -> Self {
208        self.add_geom_with(geom)
209    }
210
211    /// Horizontal reference line at a constant `yintercept`. Like ggplot2 the
212    /// intercept trains the y scale (the line is always visible) and the line
213    /// appears in every facet panel.
214    pub fn geom_hline(self, yintercept: f64) -> Self {
215        self.add_refline(
216            GeomHline::new(yintercept),
217            false,
218            &[("yintercept", yintercept)],
219        )
220    }
221
222    /// Add a horizontal reference line with custom styling (color/linetype/width).
223    pub fn geom_hline_with(self, geom: GeomHline) -> Self {
224        let y = geom.yintercept;
225        self.add_refline(geom, true, &[("yintercept", y)])
226    }
227
228    /// Data-mapped horizontal lines (ggplot2's `geom_hline(aes(yintercept =
229    /// …))`): one line per row of the layer data (the plot data unless
230    /// [`layer_data`](Self::layer_data) follows), per facet panel, styled by
231    /// any `color`/`linetype`/`alpha` in `mapping`. The plot-level mapping is
232    /// not inherited.
233    ///
234    /// ```
235    /// # use ggplot_rs::prelude::*;
236    /// let thresholds: Vec<(String, Vec<Value>)> = vec![
237    ///     ("bound".into(), vec![Value::Float(-0.2), Value::Float(0.2)]),
238    /// ];
239    /// let svg = GGPlot::new(vec![
240    ///         ("lag".to_string(), vec![Value::Float(1.0), Value::Float(2.0)]),
241    ///         ("acf".to_string(), vec![Value::Float(0.5), Value::Float(0.1)]),
242    ///     ])
243    ///     .aes(Aes::new().x("lag").y("acf"))
244    ///     .geom_col()
245    ///     .geom_hline_aes(Aes::new().yintercept("bound"))
246    ///     .layer_data(thresholds)
247    ///     .render_svg_native()
248    ///     .unwrap();
249    /// assert!(svg.contains("data-value=\"0.2\""));
250    /// ```
251    pub fn geom_hline_aes(self, mapping: Aes) -> Self {
252        self.add_geom(GeomHline::mapped()).layer_aes(mapping)
253    }
254
255    /// [`geom_hline_aes`](Self::geom_hline_aes) with custom default styling.
256    pub fn geom_hline_aes_with(self, geom: GeomHline, mapping: Aes) -> Self {
257        self.add_geom_with(geom).layer_aes(mapping)
258    }
259
260    /// Vertical reference line at a constant `xintercept` (trains the x
261    /// scale, appears in every panel).
262    pub fn geom_vline(self, xintercept: f64) -> Self {
263        self.add_refline(
264            GeomVline::new(xintercept),
265            false,
266            &[("xintercept", xintercept)],
267        )
268    }
269
270    /// Add a vertical reference line with custom styling (color/linetype/width).
271    pub fn geom_vline_with(self, geom: GeomVline) -> Self {
272        let x = geom.xintercept;
273        self.add_refline(geom, true, &[("xintercept", x)])
274    }
275
276    /// Data-mapped vertical lines (`aes(xintercept = …)`), one per row; see
277    /// [`geom_hline_aes`](Self::geom_hline_aes).
278    pub fn geom_vline_aes(self, mapping: Aes) -> Self {
279        self.add_geom(GeomVline::mapped()).layer_aes(mapping)
280    }
281
282    /// [`geom_vline_aes`](Self::geom_vline_aes) with custom default styling.
283    pub fn geom_vline_aes_with(self, geom: GeomVline, mapping: Aes) -> Self {
284        self.add_geom_with(geom).layer_aes(mapping)
285    }
286
287    /// Line `y = intercept + slope · x` in data space, clipped to the panel.
288    /// It does not train any scale (as in ggplot2).
289    pub fn geom_abline(self, slope: f64, intercept: f64) -> Self {
290        self.add_refline(
291            GeomAbline::new(slope, intercept),
292            false,
293            &[("slope", slope), ("intercept", intercept)],
294        )
295    }
296
297    /// Add a slope/intercept reference line with custom styling.
298    pub fn geom_abline_with(self, geom: GeomAbline) -> Self {
299        let (b, a) = (geom.slope, geom.intercept);
300        self.add_refline(geom, true, &[("slope", b), ("intercept", a)])
301    }
302
303    /// Data-mapped ablines (`aes(slope = …, intercept = …)`), one per row; a
304    /// missing aesthetic defaults to slope 1 / intercept 0.
305    pub fn geom_abline_aes(self, mapping: Aes) -> Self {
306        self.add_geom(GeomAbline::mapped()).layer_aes(mapping)
307    }
308
309    /// [`geom_abline_aes`](Self::geom_abline_aes) with custom default styling.
310    pub fn geom_abline_aes_with(self, geom: GeomAbline, mapping: Aes) -> Self {
311        self.add_geom_with(geom).layer_aes(mapping)
312    }
313
314    /// A constant reference line as ggplot2 builds it: a one-row layer frame
315    /// holding the constants, mapped to their aesthetics.
316    fn add_refline(
317        self,
318        geom: impl Geom + 'static,
319        explicit: bool,
320        values: &[(&str, f64)],
321    ) -> Self {
322        let mut data = DataFrame::new();
323        let mut mapping = Aes::new();
324        for (col, v) in values {
325            data.add_column(col.to_string(), vec![Value::Float(*v)]);
326            mapping = match *col {
327                "xintercept" => mapping.xintercept(col),
328                "yintercept" => mapping.yintercept(col),
329                "slope" => mapping.slope(col),
330                _ => mapping.intercept(col),
331            };
332        }
333        let plot = if explicit {
334            self.add_geom_with(geom)
335        } else {
336            self.add_geom(geom)
337        };
338        plot.layer_aes(mapping).layer_data(data)
339    }
340
341    pub fn geom_text(self) -> Self {
342        self.add_geom(GeomText::default())
343    }
344
345    pub fn geom_text_with(self, geom: GeomText) -> Self {
346        self.add_geom_with(geom)
347    }
348
349    /// Annotate with a correlation coefficient + p-value (`ggpubr::stat_cor()`).
350    /// Adds a text layer whose statistic computes Pearson correlation via
351    /// anofox-statistics; use [`GGPlot::stat_cor_with`] to pick Spearman or set
352    /// the label position.
353    #[cfg(feature = "ggpubr")]
354    pub fn stat_cor(self) -> Self {
355        self.geom_text().stat(crate::stat::cor::StatCor::default())
356    }
357
358    /// [`GGPlot::stat_cor`] with an explicit [`StatCor`](crate::stat::cor::StatCor)
359    /// (method / label position).
360    #[cfg(feature = "ggpubr")]
361    pub fn stat_cor_with(self, stat: crate::stat::cor::StatCor) -> Self {
362        self.geom_text().stat(stat)
363    }
364
365    /// Annotate grouped data with a group-comparison p-value
366    /// (`ggpubr::stat_compare_means()`). Adds a text layer whose statistic
367    /// compares y across the discrete x groups (Wilcoxon for two groups,
368    /// Kruskal-Wallis for more) via anofox-statistics.
369    #[cfg(feature = "ggpubr")]
370    pub fn stat_compare_means(self) -> Self {
371        self.geom_text()
372            .stat(crate::stat::compare_means::StatCompareMeans::default())
373    }
374
375    /// [`GGPlot::stat_compare_means`] with an explicit
376    /// [`StatCompareMeans`](crate::stat::compare_means::StatCompareMeans).
377    #[cfg(feature = "ggpubr")]
378    pub fn stat_compare_means_with(
379        self,
380        stat: crate::stat::compare_means::StatCompareMeans,
381    ) -> Self {
382        self.geom_text().stat(stat)
383    }
384
385    /// Auto-draw pairwise significance brackets (`ggpubr::stat_compare_means`
386    /// with `comparisons`). For each `(group_a, group_b)` pair it runs a
387    /// two-sample test (Wilcoxon by default) on the plot's y-values, formats the
388    /// p-value, and stacks a labelled [`geom_bracket`](GGPlot::geom_bracket)
389    /// above the data. Uses the plot's x/y mapping and data.
390    #[cfg(feature = "ggpubr")]
391    pub fn stat_compare_means_pairwise(self, comparisons: &[(&str, &str)]) -> Self {
392        self.stat_compare_means_pairwise_with(
393            crate::stat::compare_means::CompareMethod::Auto,
394            comparisons,
395        )
396    }
397
398    /// [`stat_compare_means_pairwise`](GGPlot::stat_compare_means_pairwise) with
399    /// an explicit test method.
400    #[cfg(feature = "ggpubr")]
401    pub fn stat_compare_means_pairwise_with(
402        self,
403        method: crate::stat::compare_means::CompareMethod,
404        comparisons: &[(&str, &str)],
405    ) -> Self {
406        // Resolve the x/y source columns from the plot mapping + data.
407        let col_for = |a: crate::aes::Aesthetic| {
408            self.mapping
409                .mappings
410                .iter()
411                .find(|m| m.aesthetic == a)
412                .map(|m| m.column.clone())
413        };
414        let (xcol, ycol) = match (
415            col_for(crate::aes::Aesthetic::X),
416            col_for(crate::aes::Aesthetic::Y),
417        ) {
418            (Some(x), Some(y)) => (x, y),
419            _ => return self,
420        };
421        let (xc, yc) = match (self.data.column(&xcol), self.data.column(&ycol)) {
422            (Some(x), Some(y)) => (x, y),
423            _ => return self,
424        };
425
426        // Bucket y by x category (first-seen order) and find the data range.
427        let mut groups: Vec<(String, Vec<f64>)> = Vec::new();
428        let mut ymax = f64::NEG_INFINITY;
429        let mut ymin = f64::INFINITY;
430        for (xv, yv) in xc.iter().zip(yc.iter()) {
431            let y = match yv.as_f64() {
432                Some(y) if y.is_finite() => y,
433                _ => continue,
434            };
435            ymax = ymax.max(y);
436            ymin = ymin.min(y);
437            let key = xv.to_group_key();
438            if let Some(g) = groups.iter_mut().find(|(k, _)| *k == key) {
439                g.1.push(y);
440            } else {
441                groups.push((key, vec![y]));
442            }
443        }
444        if groups.len() < 2 {
445            return self;
446        }
447        let step = if ymax > ymin {
448            (ymax - ymin) * 0.12
449        } else {
450            1.0
451        };
452
453        // One stacked bracket per resolvable comparison.
454        let (mut xmin_v, mut xmax_v, mut y_v, mut label_v) =
455            (Vec::new(), Vec::new(), Vec::new(), Vec::new());
456        let mut placed = 0usize;
457        for (a, b) in comparisons {
458            let ga = groups.iter().find(|(k, _)| k == a).map(|g| &g.1);
459            let gb = groups.iter().find(|(k, _)| k == b).map(|g| &g.1);
460            let (ga, gb) = match (ga, gb) {
461                (Some(x), Some(y)) if x.len() >= 2 && y.len() >= 2 => (x, y),
462                _ => continue,
463            };
464            let p = match crate::stat::compare_means::pairwise_p(method, ga, gb) {
465                Some(p) => p,
466                None => continue,
467            };
468            xmin_v.push(Value::Str((*a).to_string()));
469            xmax_v.push(Value::Str((*b).to_string()));
470            y_v.push(Value::Float(ymax + step * (placed as f64 + 1.0)));
471            label_v.push(Value::Str(crate::stat::cor::format_p_value(p)));
472            placed += 1;
473        }
474        if xmin_v.is_empty() {
475            return self;
476        }
477
478        let data = vec![
479            ("xmin".to_string(), xmin_v),
480            ("xmax".to_string(), xmax_v),
481            ("y".to_string(), y_v),
482            ("label".to_string(), label_v),
483        ];
484        self.add_geom(crate::geom::bracket::GeomBracket::default())
485            .layer_data(data)
486            .layer_aes(Aes::new().xmin("xmin").xmax("xmax").y("y").label("label"))
487    }
488
489    /// Draw a significance bracket over the plot (`ggpubr::geom_bracket`): a bar
490    /// spanning the categorical positions `xmin`..`xmax` at height `y`, captioned
491    /// with `label` (e.g. a p-value or significance stars). Pairs with a boxplot
492    /// and [`stat_compare_means`](GGPlot::stat_compare_means).
493    pub fn geom_bracket(self, xmin: &str, xmax: &str, y: f64, label: &str) -> Self {
494        self.geom_bracket_many(
495            crate::geom::bracket::GeomBracket::default(),
496            &[(xmin, xmax, y, label)],
497        )
498    }
499
500    /// Draw several significance brackets at once with a configured
501    /// [`GeomBracket`](crate::geom::bracket::GeomBracket). Each tuple is
502    /// `(xmin, xmax, y, label)`.
503    pub fn geom_bracket_many(
504        self,
505        geom: crate::geom::bracket::GeomBracket,
506        brackets: &[(&str, &str, f64, &str)],
507    ) -> Self {
508        let xmin = brackets
509            .iter()
510            .map(|b| Value::Str(b.0.to_string()))
511            .collect::<Vec<_>>();
512        let xmax = brackets
513            .iter()
514            .map(|b| Value::Str(b.1.to_string()))
515            .collect::<Vec<_>>();
516        let y = brackets
517            .iter()
518            .map(|b| Value::Float(b.2))
519            .collect::<Vec<_>>();
520        let label = brackets
521            .iter()
522            .map(|b| Value::Str(b.3.to_string()))
523            .collect::<Vec<_>>();
524        let data = vec![
525            ("xmin".to_string(), xmin),
526            ("xmax".to_string(), xmax),
527            ("y".to_string(), y),
528            ("label".to_string(), label),
529        ];
530        self.add_geom(geom)
531            .layer_data(data)
532            .layer_aes(Aes::new().xmin("xmin").xmax("xmax").y("y").label("label"))
533    }
534
535    /// Significance brackets from a precomputed test table (ggpubr's
536    /// `stat_pvalue_manual` / `geom_bracket(data = …)`) — e.g. the anofox
537    /// `test` contract output (`group1`, `group2`, `p_adj`/`p_value`, optional
538    /// `y_position`, `label`). No test is recomputed. See
539    /// [`BracketTable`](crate::geom::bracket::BracketTable) for the column
540    /// rules, label templates (`"p = {p_adj}"`, `"{p.signif}"`) and the
541    /// automatic stacking of rows without a `y_position`. Rows that cannot be
542    /// drawn are dropped with a build warning.
543    pub fn geom_bracket_table(
544        mut self,
545        table: impl GGData,
546        spec: crate::geom::bracket::BracketTable,
547    ) -> Self {
548        let table = table.into_dataframe();
549        let col_for = |a: crate::aes::Aesthetic| {
550            self.mapping
551                .mappings
552                .iter()
553                .find(|m| m.aesthetic == a)
554                .map(|m| m.column.clone())
555        };
556        // The plot's x categories (to reject unknown groups) and finite y
557        // range (to stack brackets above the data).
558        let x_levels: Vec<String> = col_for(crate::aes::Aesthetic::X)
559            .and_then(|c| self.data.column(&c))
560            .filter(|col| col.iter().any(|v| matches!(v, Value::Str(_))))
561            .map(|col| {
562                let mut seen = std::collections::HashSet::new();
563                col.iter()
564                    .filter(|v| !v.is_na())
565                    .map(|v| v.to_group_key())
566                    .filter(|k| seen.insert(k.clone()))
567                    .collect()
568            })
569            .unwrap_or_default();
570        let y_range = col_for(crate::aes::Aesthetic::Y)
571            .and_then(|c| self.data.column(&c))
572            .and_then(|col| {
573                let (lo, hi) = col
574                    .iter()
575                    .filter_map(|v| v.as_f64())
576                    .filter(|v| v.is_finite())
577                    .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), v| {
578                        (lo.min(v), hi.max(v))
579                    });
580                (lo <= hi).then_some((lo, hi))
581            });
582        let mut warnings = Vec::new();
583        let resolved = spec.resolve(&table, &x_levels, y_range, &mut warnings);
584        self.warnings.extend(warnings);
585        match resolved {
586            Some(data) => self
587                .add_geom(crate::geom::bracket::GeomBracket::from(spec.geom))
588                .layer_data(data)
589                .layer_aes(Aes::new().xmin("xmin").xmax("xmax").y("y").label("label")),
590            None => self,
591        }
592    }
593
594    /// Text labels that repel each other and their points
595    /// (`ggrepel::geom_text_repel`) — deterministic (seeded) layout; see
596    /// [`GeomTextRepel`](crate::geom::repel::GeomTextRepel). Requires `x`,
597    /// `y` and `label`.
598    pub fn geom_text_repel(self) -> Self {
599        self.add_geom(crate::geom::repel::GeomTextRepel::default())
600    }
601
602    /// [`geom_text_repel`](Self::geom_text_repel) with a configured geom
603    /// (padding, nudge, `max_overlaps`, seed, …).
604    pub fn geom_text_repel_with(self, geom: crate::geom::repel::GeomTextRepel) -> Self {
605        self.add_geom_with(geom)
606    }
607
608    /// Boxed labels that repel each other and their points
609    /// (`ggrepel::geom_label_repel`).
610    pub fn geom_label_repel(self) -> Self {
611        self.add_geom(crate::geom::repel::GeomLabelRepel::default())
612    }
613
614    /// [`geom_label_repel`](Self::geom_label_repel) with a configured geom.
615    pub fn geom_label_repel_with(self, geom: crate::geom::repel::GeomLabelRepel) -> Self {
616        self.add_geom_with(geom)
617    }
618
619    pub fn geom_label(self) -> Self {
620        self.add_geom(GeomLabel::default())
621    }
622
623    pub fn geom_label_with(self, geom: GeomLabel) -> Self {
624        self.add_geom_with(geom)
625    }
626
627    pub fn geom_area(self) -> Self {
628        self.add_geom(GeomArea::default())
629    }
630
631    pub fn geom_area_with(self, geom: GeomArea) -> Self {
632        self.add_geom_with(geom)
633    }
634
635    pub fn geom_ribbon(self) -> Self {
636        self.add_geom(GeomRibbon::default())
637    }
638
639    pub fn geom_ribbon_with(self, geom: GeomRibbon) -> Self {
640        self.add_geom_with(geom)
641    }
642
643    pub fn geom_errorbar(self) -> Self {
644        self.add_geom(GeomErrorbar::default())
645    }
646
647    pub fn geom_errorbar_with(self, geom: GeomErrorbar) -> Self {
648        self.add_geom_with(geom)
649    }
650
651    pub fn geom_segment(self) -> Self {
652        self.add_geom(GeomSegment::default())
653    }
654
655    pub fn geom_segment_with(self, geom: GeomSegment) -> Self {
656        self.add_geom_with(geom)
657    }
658
659    pub fn geom_density(self) -> Self {
660        self.add_geom(GeomDensity::default())
661    }
662
663    pub fn geom_density_with(self, geom: GeomDensity) -> Self {
664        self.add_geom_with(geom)
665    }
666
667    pub fn geom_rug(self) -> Self {
668        self.add_geom(GeomRug::default())
669    }
670
671    pub fn geom_rug_with(self, geom: GeomRug) -> Self {
672        self.add_geom_with(geom)
673    }
674
675    pub fn geom_jitter(self) -> Self {
676        self.add_geom(GeomJitter::default())
677    }
678
679    pub fn geom_jitter_with(self, geom: GeomJitter) -> Self {
680        self.add_geom_with(geom)
681    }
682
683    pub fn geom_path(self) -> Self {
684        self.add_geom(GeomPath::default())
685    }
686
687    pub fn geom_path_with(self, geom: GeomPath) -> Self {
688        self.add_geom_with(geom)
689    }
690
691    /// Add a confidence-ellipse layer (default 95%) as a path per group.
692    pub fn stat_ellipse(self) -> Self {
693        self.geom_path()
694            .stat(crate::stat::ellipse::StatEllipse::default())
695    }
696
697    /// Add a confidence-ellipse layer at the given level (0, 1).
698    pub fn stat_ellipse_level(self, level: f64) -> Self {
699        self.geom_path()
700            .stat(crate::stat::ellipse::StatEllipse::new(level))
701    }
702
703    /// Add a quantile-regression line for each `tau` as a separate path layer
704    /// (R's `stat_quantile`). Backed by anofox-regression (feature `regression`).
705    #[cfg(feature = "regression")]
706    pub fn stat_quantile(mut self, taus: &[f64]) -> Self {
707        for &tau in taus {
708            self = self
709                .geom_path()
710                .stat(crate::stat::quantile::StatQuantile::new(tau));
711        }
712        self
713    }
714
715    /// Quantile-regression lines at the quartiles (0.25, 0.5, 0.75).
716    #[cfg(feature = "regression")]
717    pub fn geom_quantile(self) -> Self {
718        self.stat_quantile(&[0.25, 0.5, 0.75])
719    }
720
721    pub fn geom_step(self) -> Self {
722        self.add_geom(GeomStep::default())
723    }
724
725    pub fn geom_step_with(self, geom: GeomStep) -> Self {
726        self.add_geom_with(geom)
727    }
728
729    pub fn geom_freqpoly(self) -> Self {
730        self.add_geom(GeomFreqpoly::default())
731    }
732
733    pub fn geom_freqpoly_with(self, geom: GeomFreqpoly) -> Self {
734        self.add_geom_with(geom)
735    }
736
737    pub fn geom_linerange(self) -> Self {
738        self.add_geom(GeomLinerange::default())
739    }
740
741    pub fn geom_linerange_with(self, geom: GeomLinerange) -> Self {
742        self.add_geom_with(geom)
743    }
744
745    pub fn geom_pointrange(self) -> Self {
746        self.add_geom(GeomPointrange::default())
747    }
748
749    pub fn geom_pointrange_with(self, geom: GeomPointrange) -> Self {
750        self.add_geom_with(geom)
751    }
752
753    pub fn geom_crossbar(self) -> Self {
754        self.add_geom(GeomCrossbar::default())
755    }
756
757    pub fn geom_crossbar_with(self, geom: GeomCrossbar) -> Self {
758        self.add_geom_with(geom)
759    }
760
761    pub fn geom_spoke(self) -> Self {
762        self.add_geom(GeomSpoke::default())
763    }
764
765    pub fn geom_spoke_with(self, geom: GeomSpoke) -> Self {
766        self.add_geom_with(geom)
767    }
768
769    pub fn geom_rect(self) -> Self {
770        self.add_geom(GeomRect::default())
771    }
772
773    pub fn geom_rect_with(self, geom: GeomRect) -> Self {
774        self.add_geom_with(geom)
775    }
776
777    pub fn geom_tile(self) -> Self {
778        self.add_geom(GeomTile::default())
779    }
780
781    pub fn geom_tile_with(self, geom: GeomTile) -> Self {
782        self.add_geom_with(geom)
783    }
784
785    /// Dense regular grid of filled cells (heatmap/raster) from x, y, fill.
786    pub fn geom_raster(self) -> Self {
787        self.add_geom(crate::geom::raster::GeomRaster::default())
788    }
789
790    pub fn geom_raster_with(self, geom: crate::geom::raster::GeomRaster) -> Self {
791        self.add_geom_with(geom)
792    }
793
794    pub fn geom_polygon(self) -> Self {
795        self.add_geom(GeomPolygon::default())
796    }
797
798    pub fn geom_polygon_with(self, geom: GeomPolygon) -> Self {
799        self.add_geom_with(geom)
800    }
801
802    /// Render simple-features geometry from a WKT `geometry` column (feature `sf`).
803    #[cfg(feature = "sf")]
804    pub fn geom_sf(self) -> Self {
805        self.add_geom(crate::geom::sf::GeomSf::default())
806    }
807
808    #[cfg(feature = "sf")]
809    pub fn geom_sf_with(self, geom: crate::geom::sf::GeomSf) -> Self {
810        self.add_geom_with(geom)
811    }
812
813    pub fn geom_curve(self) -> Self {
814        self.add_geom(GeomCurve::default())
815    }
816
817    pub fn geom_curve_with(self, geom: GeomCurve) -> Self {
818        self.add_geom_with(geom)
819    }
820
821    pub fn geom_violin(self) -> Self {
822        self.add_geom(GeomViolin::default())
823    }
824
825    pub fn geom_violin_with(self, geom: GeomViolin) -> Self {
826        self.add_geom_with(geom)
827    }
828
829    pub fn geom_dotplot(self) -> Self {
830        self.add_geom(GeomDotplot::default())
831    }
832
833    pub fn geom_dotplot_with(self, geom: GeomDotplot) -> Self {
834        self.add_geom_with(geom)
835    }
836
837    pub fn geom_qq(self) -> Self {
838        self.add_geom(GeomQQ::default())
839    }
840
841    pub fn geom_qq_with(self, geom: GeomQQ) -> Self {
842        self.add_geom_with(geom)
843    }
844
845    pub fn geom_qq_line(self) -> Self {
846        self.add_geom(GeomQQLine::default())
847    }
848
849    pub fn geom_qq_line_with(self, geom: GeomQQLine) -> Self {
850        self.add_geom_with(geom)
851    }
852
853    pub fn geom_bin2d(self) -> Self {
854        self.add_geom(GeomBin2d::default())
855    }
856
857    pub fn geom_bin2d_with(self, geom: GeomBin2d) -> Self {
858        self.add_geom_with(geom)
859    }
860
861    pub fn geom_hex(self) -> Self {
862        self.add_geom(GeomHex::default())
863    }
864
865    pub fn geom_hex_with(self, geom: GeomHex) -> Self {
866        self.add_geom_with(geom)
867    }
868
869    pub fn geom_count(self) -> Self {
870        self.add_geom(GeomCount::default())
871    }
872
873    pub fn geom_count_with(self, geom: GeomCount) -> Self {
874        self.add_geom_with(geom)
875    }
876
877    pub fn geom_contour(self) -> Self {
878        self.add_geom(GeomContour::default())
879    }
880
881    pub fn geom_contour_with(self, geom: GeomContour) -> Self {
882        self.add_geom_with(geom)
883    }
884
885    /// Filled contour bands from gridded (x, y, z) data — draws polygons filled by
886    /// band level. Pair with a continuous fill scale (e.g. `scale_fill_viridis_c`).
887    pub fn geom_contour_filled(self) -> Self {
888        self.add_geom(GeomPolygon {
889            line_width: 0.0,
890            alpha: 1.0,
891            ..GeomPolygon::default()
892        })
893        .stat(crate::stat::contour_filled::StatContourFilled::default())
894    }
895
896    pub fn geom_density2d(self) -> Self {
897        self.add_geom(GeomDensity2d::default())
898    }
899
900    pub fn geom_density2d_with(self, geom: GeomDensity2d) -> Self {
901        self.add_geom_with(geom)
902    }
903
904    /// Candlestick chart: map `x` and `open`/`high`/`low`/`close`
905    /// (`Aes::new().x("date").open("o").high("h").low("l").close("c")`).
906    pub fn geom_candlestick(self) -> Self {
907        self.add_geom(crate::geom::candlestick::GeomCandlestick::default())
908    }
909
910    pub fn geom_candlestick_with(self, geom: crate::geom::candlestick::GeomCandlestick) -> Self {
911        self.add_geom_with(geom)
912    }
913
914    /// OHLC bar chart (high–low bar with open/close ticks); same aesthetics as
915    /// [`geom_candlestick`](Self::geom_candlestick).
916    pub fn geom_ohlc(self) -> Self {
917        self.add_geom(crate::geom::candlestick::GeomOhlc::default())
918    }
919
920    pub fn geom_ohlc_with(self, geom: crate::geom::candlestick::GeomOhlc) -> Self {
921        self.add_geom_with(geom)
922    }
923
924    /// Calendar heatmap (GitHub/ECharts style): the plot's `x` (a date —
925    /// `DateTime`, epoch seconds or `"YYYY-MM-DD"`) is laid out as week
926    /// columns × weekday rows (Sunday on top) via [`StatCalendar`], coloured
927    /// by `fill`, with month labels along x, Mon/Wed/Fri along y, month
928    /// boundary outlines and square cells. Spans over
929    /// [`MAX_CALENDAR_YEARS`] are clipped to the most recent years (with a
930    /// build warning). Uses the plot-level data and `x` mapping.
931    ///
932    /// [`StatCalendar`]: crate::stat::calendar::StatCalendar
933    /// [`MAX_CALENDAR_YEARS`]: crate::stat::calendar::MAX_CALENDAR_YEARS
934    pub fn geom_calendar(self) -> Self {
935        self.geom_calendar_with(
936            GeomTile {
937                color: (255, 255, 255),
938                line_width: 1.5,
939                ..Default::default()
940            },
941            false,
942        )
943    }
944
945    /// [`geom_calendar`](Self::geom_calendar) with a custom cell style and
946    /// optionally Monday-first weeks.
947    pub fn geom_calendar_with(mut self, tile: GeomTile, monday_first: bool) -> Self {
948        use crate::stat::calendar::{
949            day_number, month_boundaries, month_breaks, month_breaks_localized, weekday_labels,
950            CalendarGrid, StatCalendar, MAX_CALENDAR_YEARS,
951        };
952        let grid = self
953            .mapping
954            .get_mapping(&crate::aes::Aesthetic::X)
955            .and_then(|c| self.data.column(c))
956            .and_then(|col| {
957                CalendarGrid::from_days(col.iter().filter_map(day_number), monday_first)
958            });
959        let Some((grid, clipped)) = grid else {
960            // No dates: an empty calendar layer (renders an empty panel).
961            return self.add_geom_with(tile).stat(StatCalendar {
962                grid: None,
963                monday_first,
964            });
965        };
966        if clipped {
967            self.warnings.push(format!(
968                "geom_calendar: dates more than {MAX_CALENDAR_YEARS} years before the newest were dropped"
969            ));
970        }
971        let (xb, _) = month_breaks(&grid);
972        // Rows: y = 6 - weekday; label Mon/Wed/Fri.
973        let yb = if monday_first {
974            vec![6.0, 4.0, 2.0]
975        } else {
976            vec![5.0, 3.0, 1.0]
977        };
978        let segs = month_boundaries(&grid);
979        let col = |f: fn(&(f64, f64, f64, f64)) -> f64| -> Vec<Value> {
980            segs.iter().map(|s| Value::Float(f(s))).collect()
981        };
982        let boundaries = vec![
983            ("x".to_string(), col(|s| s.0)),
984            ("y".to_string(), col(|s| s.1)),
985            ("xend".to_string(), col(|s| s.2)),
986            ("yend".to_string(), col(|s| s.3)),
987        ];
988        self.default_aspect_ratio = Some(7.0 / grid.n_weeks().max(1) as f64);
989        let plot = self
990            .add_geom_with(tile)
991            .stat(StatCalendar {
992                grid: Some(grid),
993                monday_first,
994            })
995            .scale_x_continuous(
996                ScaleContinuous::new()
997                    .with_breaks(xb)
998                    // Month / weekday names follow the plot locale.
999                    .with_localized_labels(move |l| month_breaks_localized(&grid, l).1)
1000                    .with_expand(0.0, 0.0),
1001            )
1002            .scale_y_continuous(
1003                ScaleContinuous::new()
1004                    .with_breaks(yb)
1005                    .with_localized_labels(weekday_labels)
1006                    .with_expand(0.0, 0.0),
1007            );
1008        if segs.is_empty() {
1009            return plot;
1010        }
1011        // Styled by the theme's `calendar_month_border` at draw time.
1012        plot.add_geom_with(crate::geom::segment::GeomCalendarBorder)
1013            .layer_data(boundaries)
1014            .layer_aes(Aes::new().x("x").y("y").xend("xend").yend("yend"))
1015            .show_legend(false)
1016    }
1017
1018    pub fn geom_blank(self) -> Self {
1019        self.add_geom(GeomBlank)
1020    }
1021
1022    /// Add a geom configured explicitly by the caller: its colours win over
1023    /// the theme's `primary_color`.
1024    fn add_geom_with(self, geom: impl Geom + 'static) -> Self {
1025        let mut plot = self.add_geom(geom);
1026        if let Some(layer) = plot.layers.last_mut() {
1027            layer.explicit_style = true;
1028        }
1029        plot
1030    }
1031
1032    fn add_geom(mut self, geom: impl Geom + 'static) -> Self {
1033        let stat = geom.default_stat();
1034        let position = geom.default_position();
1035        let params = geom.default_params();
1036        self.layers.push(Layer {
1037            data: None,
1038            mapping: Aes::default(),
1039            geom: Box::new(geom),
1040            stat,
1041            position,
1042            params,
1043            show_legend: None,
1044            explicit_style: false,
1045        });
1046        self
1047    }
1048
1049    // ─── Layer-level overrides ──────────────────────────────────
1050
1051    /// Override the stat for the most recently added layer.
1052    pub fn stat(mut self, stat: impl Stat + 'static) -> Self {
1053        if let Some(layer) = self.layers.last_mut() {
1054            layer.stat = Box::new(stat);
1055        }
1056        self
1057    }
1058
1059    /// Override the position for the most recently added layer.
1060    pub fn position(mut self, pos: impl Position + 'static) -> Self {
1061        if let Some(layer) = self.layers.last_mut() {
1062            layer.position = Box::new(pos);
1063        }
1064        self
1065    }
1066
1067    /// Override the data for the most recently added layer.
1068    pub fn layer_data(mut self, data: impl GGData) -> Self {
1069        if let Some(layer) = self.layers.last_mut() {
1070            layer.data = Some(data.into_dataframe());
1071        }
1072        self
1073    }
1074
1075    /// Override the aesthetic mapping for the most recently added layer.
1076    pub fn layer_aes(mut self, mapping: Aes) -> Self {
1077        self.warnings.extend(mapping.warnings.iter().cloned());
1078        if let Some(layer) = self.layers.last_mut() {
1079            layer.mapping = mapping;
1080        }
1081        self
1082    }
1083
1084    /// Control whether the most recently added layer contributes to the legend.
1085    /// `true` = always show, `false` = always hide, default (None) = auto.
1086    pub fn show_legend(mut self, show: bool) -> Self {
1087        if let Some(layer) = self.layers.last_mut() {
1088            layer.show_legend = Some(show);
1089        }
1090        self
1091    }
1092
1093    // ─── Scales ──────────────────────────────────────────────────
1094
1095    pub fn scale_x_continuous(mut self, s: ScaleContinuous) -> Self {
1096        let s = s.for_aesthetic(crate::aes::Aesthetic::X);
1097        self.scales.push(Box::new(s));
1098        self
1099    }
1100
1101    pub fn scale_y_continuous(mut self, s: ScaleContinuous) -> Self {
1102        let s = s.for_aesthetic(crate::aes::Aesthetic::Y);
1103        self.scales.push(Box::new(s));
1104        self
1105    }
1106
1107    pub fn scale_x_discrete(mut self, s: crate::scale::discrete::ScaleDiscrete) -> Self {
1108        let s = s.for_aesthetic(crate::aes::Aesthetic::X);
1109        self.scales.push(Box::new(s));
1110        self
1111    }
1112
1113    pub fn scale_y_discrete(mut self, s: crate::scale::discrete::ScaleDiscrete) -> Self {
1114        let s = s.for_aesthetic(crate::aes::Aesthetic::Y);
1115        self.scales.push(Box::new(s));
1116        self
1117    }
1118
1119    pub fn scale_color(mut self, s: impl Scale + 'static) -> Self {
1120        self.scales.push(Box::new(s));
1121        self
1122    }
1123
1124    pub fn scale_fill(mut self, s: impl Scale + 'static) -> Self {
1125        self.scales.push(Box::new(s));
1126        self
1127    }
1128
1129    pub fn scale_color_manual(self, values: Vec<(&str, crate::scale::color::RGBAColor)>) -> Self {
1130        let s = crate::scale::manual::ScaleManual::new(crate::aes::Aesthetic::Color, values);
1131        self.scale_color(s)
1132    }
1133
1134    pub fn scale_fill_manual(self, values: Vec<(&str, crate::scale::color::RGBAColor)>) -> Self {
1135        let s = crate::scale::manual::ScaleManual::new(crate::aes::Aesthetic::Fill, values);
1136        self.scale_fill(s)
1137    }
1138
1139    /// The `colour`/`color` column already holds `#RRGGBB`/`#RGB` hex
1140    /// strings, used as-is — no legend (R's `scale_colour_identity`).
1141    pub fn scale_colour_identity(self) -> Self {
1142        let s = crate::scale::identity::ScaleColorIdentity::new(crate::aes::Aesthetic::Color);
1143        self.scale_color(s)
1144    }
1145
1146    /// American-spelling alias for [`scale_colour_identity`](Self::scale_colour_identity).
1147    pub fn scale_color_identity(self) -> Self {
1148        self.scale_colour_identity()
1149    }
1150
1151    /// The `fill` column already holds `#RRGGBB`/`#RGB` hex strings, used
1152    /// as-is — no legend (R's `scale_fill_identity`).
1153    pub fn scale_fill_identity(self) -> Self {
1154        let s = crate::scale::identity::ScaleColorIdentity::new(crate::aes::Aesthetic::Fill);
1155        self.scale_fill(s)
1156    }
1157
1158    pub fn scale_color_viridis(self) -> Self {
1159        use crate::scale::color::ScaleColorDiscrete;
1160        use crate::scale::palettes::PaletteName;
1161        let s = ScaleColorDiscrete::new(crate::aes::Aesthetic::Color)
1162            .with_named_palette(&PaletteName::Viridis);
1163        self.scale_color(s)
1164    }
1165
1166    pub fn scale_color_brewer(self, name: crate::scale::palettes::PaletteName) -> Self {
1167        use crate::scale::color::ScaleColorDiscrete;
1168        let s = ScaleColorDiscrete::new(crate::aes::Aesthetic::Color).with_named_palette(&name);
1169        self.scale_color(s)
1170    }
1171
1172    pub fn scale_color_gradient(
1173        self,
1174        low: crate::scale::color::RGBAColor,
1175        high: crate::scale::color::RGBAColor,
1176    ) -> Self {
1177        use crate::scale::color::ScaleColorContinuous;
1178        let s = ScaleColorContinuous::new(crate::aes::Aesthetic::Color).with_colors(low, high);
1179        self.scale_color(s)
1180    }
1181
1182    pub fn scale_fill_gradient(
1183        self,
1184        low: crate::scale::color::RGBAColor,
1185        high: crate::scale::color::RGBAColor,
1186    ) -> Self {
1187        use crate::scale::color::ScaleColorContinuous;
1188        let s = ScaleColorContinuous::new(crate::aes::Aesthetic::Fill).with_colors(low, high);
1189        self.scale_fill(s)
1190    }
1191
1192    pub fn scale_color_gradient2(
1193        self,
1194        low: crate::scale::color::RGBAColor,
1195        mid: crate::scale::color::RGBAColor,
1196        high: crate::scale::color::RGBAColor,
1197    ) -> Self {
1198        use crate::scale::gradient::ScaleColorGradient2;
1199        let s = ScaleColorGradient2::new(crate::aes::Aesthetic::Color).with_colors(low, mid, high);
1200        self.scale_color(s)
1201    }
1202
1203    pub fn scale_fill_gradient2(
1204        self,
1205        low: crate::scale::color::RGBAColor,
1206        mid: crate::scale::color::RGBAColor,
1207        high: crate::scale::color::RGBAColor,
1208    ) -> Self {
1209        use crate::scale::gradient::ScaleColorGradient2;
1210        let s = ScaleColorGradient2::new(crate::aes::Aesthetic::Fill).with_colors(low, mid, high);
1211        self.scale_fill(s)
1212    }
1213
1214    pub fn scale_fill_viridis(self) -> Self {
1215        use crate::scale::color::ScaleColorDiscrete;
1216        use crate::scale::palettes::PaletteName;
1217        let s = ScaleColorDiscrete::new(crate::aes::Aesthetic::Fill)
1218            .with_named_palette(&PaletteName::Viridis);
1219        self.scale_fill(s)
1220    }
1221
1222    /// Continuous viridis color scale (for numeric data).
1223    pub fn scale_color_viridis_c(self) -> Self {
1224        use crate::scale::gradient_n::ScaleColorGradientN;
1225        let s = ScaleColorGradientN::viridis(crate::aes::Aesthetic::Color);
1226        self.scale_color(s)
1227    }
1228
1229    /// Continuous viridis fill scale (for numeric data).
1230    pub fn scale_fill_viridis_c(self) -> Self {
1231        use crate::scale::gradient_n::ScaleColorGradientN;
1232        let s = ScaleColorGradientN::viridis(crate::aes::Aesthetic::Fill);
1233        self.scale_fill(s)
1234    }
1235
1236    /// N-stop continuous color gradient.
1237    pub fn scale_color_gradientn(self, stops: Vec<(f64, crate::scale::color::RGBAColor)>) -> Self {
1238        use crate::scale::gradient_n::ScaleColorGradientN;
1239        let s = ScaleColorGradientN::new(crate::aes::Aesthetic::Color, stops);
1240        self.scale_color(s)
1241    }
1242
1243    /// N-stop continuous fill gradient.
1244    pub fn scale_fill_gradientn(self, stops: Vec<(f64, crate::scale::color::RGBAColor)>) -> Self {
1245        use crate::scale::gradient_n::ScaleColorGradientN;
1246        let s = ScaleColorGradientN::new(crate::aes::Aesthetic::Fill, stops);
1247        self.scale_fill(s)
1248    }
1249
1250    /// Binned (stepped) two-colour continuous colour scale — buckets the mapped
1251    /// variable into `n_bins` bins, each a discrete colour, with a stepped legend.
1252    pub fn scale_color_steps(
1253        self,
1254        low: crate::scale::color::RGBAColor,
1255        high: crate::scale::color::RGBAColor,
1256        n_bins: usize,
1257    ) -> Self {
1258        let s = crate::scale::steps::ScaleColorSteps::two(
1259            crate::aes::Aesthetic::Color,
1260            (low.r, low.g, low.b),
1261            (high.r, high.g, high.b),
1262            n_bins,
1263        );
1264        self.scale_color(s)
1265    }
1266
1267    /// Binned N-stop continuous colour scale.
1268    pub fn scale_color_stepsn(
1269        self,
1270        stops: Vec<crate::scale::color::RGBAColor>,
1271        n_bins: usize,
1272    ) -> Self {
1273        let s = crate::scale::steps::ScaleColorSteps::new(
1274            crate::aes::Aesthetic::Color,
1275            stops.iter().map(|c| (c.r, c.g, c.b)).collect(),
1276            n_bins,
1277        );
1278        self.scale_color(s)
1279    }
1280
1281    /// Binned ColorBrewer colour scale (R's `scale_color_fermenter`).
1282    pub fn scale_color_fermenter(
1283        self,
1284        name: crate::scale::palettes::PaletteName,
1285        n_bins: usize,
1286    ) -> Self {
1287        let stops = crate::scale::palettes::palette(&name)
1288            .iter()
1289            .map(|c| (c.r, c.g, c.b))
1290            .collect();
1291        let s =
1292            crate::scale::steps::ScaleColorSteps::new(crate::aes::Aesthetic::Color, stops, n_bins);
1293        self.scale_color(s)
1294    }
1295
1296    /// Binned (stepped) two-colour continuous fill scale.
1297    pub fn scale_fill_steps(
1298        self,
1299        low: crate::scale::color::RGBAColor,
1300        high: crate::scale::color::RGBAColor,
1301        n_bins: usize,
1302    ) -> Self {
1303        let s = crate::scale::steps::ScaleColorSteps::two(
1304            crate::aes::Aesthetic::Fill,
1305            (low.r, low.g, low.b),
1306            (high.r, high.g, high.b),
1307            n_bins,
1308        );
1309        self.scale_fill(s)
1310    }
1311
1312    /// Binned ColorBrewer fill scale.
1313    pub fn scale_fill_fermenter(
1314        self,
1315        name: crate::scale::palettes::PaletteName,
1316        n_bins: usize,
1317    ) -> Self {
1318        let stops = crate::scale::palettes::palette(&name)
1319            .iter()
1320            .map(|c| (c.r, c.g, c.b))
1321            .collect();
1322        let s =
1323            crate::scale::steps::ScaleColorSteps::new(crate::aes::Aesthetic::Fill, stops, n_bins);
1324        self.scale_fill(s)
1325    }
1326
1327    /// Default discrete fill palette with levels in sorted order, so a level
1328    /// keeps its colour across charts regardless of data order. For a custom
1329    /// palette: `scale_fill(ScaleColorDiscrete::new(Aesthetic::Fill)
1330    /// .with_palette(p).sorted())`.
1331    pub fn scale_fill_discrete_sorted(self) -> Self {
1332        use crate::scale::color::ScaleColorDiscrete;
1333        self.scale_fill(ScaleColorDiscrete::new(crate::aes::Aesthetic::Fill).sorted())
1334    }
1335
1336    /// Default discrete colour palette with levels in sorted order (see
1337    /// [`scale_fill_discrete_sorted`](Self::scale_fill_discrete_sorted)).
1338    pub fn scale_color_discrete_sorted(self) -> Self {
1339        use crate::scale::color::ScaleColorDiscrete;
1340        self.scale_color(ScaleColorDiscrete::new(crate::aes::Aesthetic::Color).sorted())
1341    }
1342
1343    pub fn scale_fill_brewer(self, name: crate::scale::palettes::PaletteName) -> Self {
1344        use crate::scale::color::ScaleColorDiscrete;
1345        let s = ScaleColorDiscrete::new(crate::aes::Aesthetic::Fill).with_named_palette(&name);
1346        self.scale_fill(s)
1347    }
1348
1349    pub fn scale_linetype_manual(
1350        self,
1351        values: Vec<(&str, crate::render::backend::Linetype)>,
1352    ) -> Self {
1353        let s = crate::scale::linetype_manual::ScaleLinetypeManual::new(values);
1354        self.scale_color(s)
1355    }
1356
1357    pub fn scale_shape_manual(
1358        self,
1359        values: Vec<(&str, crate::render::backend::PointShape)>,
1360    ) -> Self {
1361        let s = crate::scale::shape_manual::ScaleShapeManual::new(values);
1362        self.scale_color(s)
1363    }
1364
1365    pub fn scale_color_grey(self) -> Self {
1366        let s = crate::scale::grey::ScaleColorGrey::new(crate::aes::Aesthetic::Color);
1367        self.scale_color(s)
1368    }
1369
1370    pub fn scale_fill_grey(self) -> Self {
1371        let s = crate::scale::grey::ScaleColorGrey::new(crate::aes::Aesthetic::Fill);
1372        self.scale_fill(s)
1373    }
1374
1375    pub fn scale_color_grey_with(self, s: crate::scale::grey::ScaleColorGrey) -> Self {
1376        self.scale_color(s)
1377    }
1378
1379    pub fn scale_fill_grey_with(self, s: crate::scale::grey::ScaleColorGrey) -> Self {
1380        self.scale_fill(s)
1381    }
1382
1383    pub fn scale_x_reverse(self) -> Self {
1384        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Reverse))
1385    }
1386
1387    pub fn scale_y_reverse(self) -> Self {
1388        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Reverse))
1389    }
1390
1391    pub fn scale_x_datetime(mut self, s: crate::scale::datetime::ScaleDateTime) -> Self {
1392        let s = s.for_aesthetic(crate::aes::Aesthetic::X);
1393        self.scales.push(Box::new(s));
1394        self
1395    }
1396
1397    pub fn scale_y_datetime(mut self, s: crate::scale::datetime::ScaleDateTime) -> Self {
1398        let s = s.for_aesthetic(crate::aes::Aesthetic::Y);
1399        self.scales.push(Box::new(s));
1400        self
1401    }
1402
1403    pub fn scale_size(mut self, s: crate::scale::size::ScaleSizeContinuous) -> Self {
1404        self.scales.push(Box::new(s));
1405        self
1406    }
1407
1408    pub fn scale_alpha(mut self, s: crate::scale::alpha::ScaleAlphaContinuous) -> Self {
1409        self.scales.push(Box::new(s));
1410        self
1411    }
1412
1413    /// Continuous linewidth scale for the `linewidth` aesthetic (segment,
1414    /// curve, path, line) — see
1415    /// [`ScaleLinewidthContinuous`](crate::scale::linewidth::ScaleLinewidthContinuous).
1416    pub fn scale_linewidth(mut self, s: crate::scale::linewidth::ScaleLinewidthContinuous) -> Self {
1417        self.scales.push(Box::new(s));
1418        self
1419    }
1420
1421    pub fn xlim(self, min: f64, max: f64) -> Self {
1422        self.scale_x_continuous(ScaleContinuous::new().with_limits(min, max))
1423    }
1424
1425    pub fn ylim(self, min: f64, max: f64) -> Self {
1426        self.scale_y_continuous(ScaleContinuous::new().with_limits(min, max))
1427    }
1428
1429    pub fn scale_x_log10(self) -> Self {
1430        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Log10))
1431    }
1432
1433    pub fn scale_y_log10(self) -> Self {
1434        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Log10))
1435    }
1436
1437    pub fn scale_x_sqrt(self) -> Self {
1438        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Sqrt))
1439    }
1440
1441    pub fn scale_y_sqrt(self) -> Self {
1442        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Sqrt))
1443    }
1444
1445    pub fn scale_x_log2(self) -> Self {
1446        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Log2))
1447    }
1448
1449    pub fn scale_y_log2(self) -> Self {
1450        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Log2))
1451    }
1452
1453    pub fn scale_x_ln(self) -> Self {
1454        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Ln))
1455    }
1456
1457    pub fn scale_y_ln(self) -> Self {
1458        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Ln))
1459    }
1460
1461    /// Logit-transformed x axis (for proportions in (0, 1)).
1462    pub fn scale_x_logit(self) -> Self {
1463        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Logit))
1464    }
1465
1466    pub fn scale_y_logit(self) -> Self {
1467        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Logit))
1468    }
1469
1470    /// Probit-transformed x axis (inverse normal CDF, for proportions in (0, 1)).
1471    pub fn scale_x_probit(self) -> Self {
1472        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Probit))
1473    }
1474
1475    pub fn scale_y_probit(self) -> Self {
1476        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Probit))
1477    }
1478
1479    /// Sign-preserving pseudo-log x axis (handles zero and negative values).
1480    pub fn scale_x_pseudo_log(self) -> Self {
1481        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::PseudoLog))
1482    }
1483
1484    pub fn scale_y_pseudo_log(self) -> Self {
1485        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::PseudoLog))
1486    }
1487
1488    /// Reciprocal (1/x) x axis.
1489    pub fn scale_x_reciprocal(self) -> Self {
1490        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Reciprocal))
1491    }
1492
1493    pub fn scale_y_reciprocal(self) -> Self {
1494        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Reciprocal))
1495    }
1496
1497    /// Exponential x axis (labels spaced logarithmically).
1498    pub fn scale_x_exp(self) -> Self {
1499        self.scale_x_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Exp))
1500    }
1501
1502    pub fn scale_y_exp(self) -> Self {
1503        self.scale_y_continuous(ScaleContinuous::new().with_transform(ScaleTransform::Exp))
1504    }
1505
1506    /// Box–Cox x axis with the given lambda (x > 0).
1507    pub fn scale_x_boxcox(self, lambda: f64) -> Self {
1508        self.scale_x_continuous(
1509            ScaleContinuous::new().with_transform(ScaleTransform::BoxCox(lambda)),
1510        )
1511    }
1512
1513    pub fn scale_y_boxcox(self, lambda: f64) -> Self {
1514        self.scale_y_continuous(
1515            ScaleContinuous::new().with_transform(ScaleTransform::BoxCox(lambda)),
1516        )
1517    }
1518
1519    // ─── Faceting ─────────────────────────────────────────────────
1520
1521    pub fn facet_wrap(mut self, var: &str, ncol: Option<usize>) -> Self {
1522        self.facet = Facet::Wrap {
1523            var: var.to_string(),
1524            ncol,
1525            scales: FacetScales::Fixed,
1526            labeller: FacetLabeller::default(),
1527        };
1528        self
1529    }
1530
1531    pub fn facet_wrap_free(mut self, var: &str, ncol: Option<usize>, scales: FacetScales) -> Self {
1532        self.facet = Facet::Wrap {
1533            var: var.to_string(),
1534            ncol,
1535            scales,
1536            labeller: FacetLabeller::default(),
1537        };
1538        self
1539    }
1540
1541    pub fn facet_wrap_labeller(
1542        mut self,
1543        var: &str,
1544        ncol: Option<usize>,
1545        labeller: FacetLabeller,
1546    ) -> Self {
1547        self.facet = Facet::Wrap {
1548            var: var.to_string(),
1549            ncol,
1550            scales: FacetScales::Fixed,
1551            labeller,
1552        };
1553        self
1554    }
1555
1556    pub fn facet_grid(mut self, row: Option<&str>, col: Option<&str>) -> Self {
1557        self.facet = Facet::Grid {
1558            row_var: row.map(String::from),
1559            col_var: col.map(String::from),
1560            scales: FacetScales::Fixed,
1561            labeller: FacetLabeller::default(),
1562            space: FacetSpace::Fixed,
1563        };
1564        self
1565    }
1566
1567    pub fn facet_grid_free(
1568        mut self,
1569        row: Option<&str>,
1570        col: Option<&str>,
1571        scales: FacetScales,
1572    ) -> Self {
1573        self.facet = Facet::Grid {
1574            row_var: row.map(String::from),
1575            col_var: col.map(String::from),
1576            scales,
1577            labeller: FacetLabeller::default(),
1578            space: FacetSpace::Fixed,
1579        };
1580        self
1581    }
1582
1583    /// `facet_grid` over **multiple** column variables (R's `rows ~ b + c`):
1584    /// the columns become the combination of the given variables' values.
1585    /// Also accepts free scales + proportional `space` for full parity.
1586    pub fn facet_grid_multi(
1587        mut self,
1588        row: Option<&str>,
1589        cols: &[&str],
1590        scales: FacetScales,
1591        space: FacetSpace,
1592    ) -> Self {
1593        let col_var = if cols.len() > 1 {
1594            // Build a synthetic column that is the interaction of the col vars.
1595            let n = self.data.nrows();
1596            let mut combined = Vec::with_capacity(n);
1597            for i in 0..n {
1598                let parts: Vec<String> = cols
1599                    .iter()
1600                    .map(|c| {
1601                        self.data
1602                            .column(c)
1603                            .and_then(|col| col.get(i))
1604                            .map(|v| v.to_group_key())
1605                            .unwrap_or_default()
1606                    })
1607                    .collect();
1608                combined.push(crate::data::Value::Str(parts.join(" . ")));
1609            }
1610            let name = "__facet_cols__".to_string();
1611            self.data.add_column(name.clone(), combined);
1612            Some(name)
1613        } else {
1614            cols.first().map(|s| s.to_string())
1615        };
1616        self.facet = Facet::Grid {
1617            row_var: row.map(String::from),
1618            col_var,
1619            scales,
1620            labeller: FacetLabeller::default(),
1621            space,
1622        };
1623        self
1624    }
1625
1626    /// `facet_grid` with proportional panel sizing (R's `space =`): panels are
1627    /// sized to their data range. Typically paired with free scales.
1628    pub fn facet_grid_space(
1629        mut self,
1630        row: Option<&str>,
1631        col: Option<&str>,
1632        scales: FacetScales,
1633        space: FacetSpace,
1634    ) -> Self {
1635        self.facet = Facet::Grid {
1636            row_var: row.map(String::from),
1637            col_var: col.map(String::from),
1638            scales,
1639            labeller: FacetLabeller::default(),
1640            space,
1641        };
1642        self
1643    }
1644
1645    pub fn facet_grid_labeller(
1646        mut self,
1647        row: Option<&str>,
1648        col: Option<&str>,
1649        labeller: FacetLabeller,
1650    ) -> Self {
1651        self.facet = Facet::Grid {
1652            row_var: row.map(String::from),
1653            col_var: col.map(String::from),
1654            scales: FacetScales::Fixed,
1655            labeller,
1656            space: FacetSpace::Fixed,
1657        };
1658        self
1659    }
1660
1661    // ─── Coordinates ─────────────────────────────────────────────
1662
1663    pub fn coord_flip(mut self) -> Self {
1664        self.coord = Box::new(CoordFlip);
1665        self
1666    }
1667
1668    pub fn coord_fixed(mut self, ratio: f64) -> Self {
1669        self.coord = Box::new(CoordFixed::new(ratio));
1670        self
1671    }
1672
1673    /// Spatial coordinate system (feature `sf`): equal aspect derived from the
1674    /// data extent, so projected geometry keeps its shape. Pair with a `geom_sf`
1675    /// projection (e.g. Mercator) for a conformal map.
1676    #[cfg(feature = "sf")]
1677    pub fn coord_sf(mut self) -> Self {
1678        self.coord = Box::new(crate::coord::sf::CoordSf::new());
1679        self
1680    }
1681
1682    /// Spatial coordinate system with explicit settings (`xlim`/`ylim`/
1683    /// `expand`) — see [`CoordSf`](crate::coord::sf::CoordSf). Used by
1684    /// consumers overlaying a fragment on their own projected basemap: a
1685    /// fixed pixel window via `xlim`/`ylim` and `expand(false)`.
1686    #[cfg(feature = "sf")]
1687    pub fn coord_sf_with(mut self, coord: crate::coord::sf::CoordSf) -> Self {
1688        self.coord = Box::new(coord);
1689        self
1690    }
1691
1692    /// Whether the rendered root `<svg>` carries `data-plot`/`data-domain`/
1693    /// `data-xdomain`/… (see [`root_data_attrs`](crate::render::svg_backend::root_data_attrs)).
1694    /// Default `true`. A consumer that overlays a rendered fragment
1695    /// (`render_svg_native_at`) on its own basemap — which already has its
1696    /// own root attributes and window — turns this off so the fragment adds
1697    /// none of its own.
1698    pub fn svg_root_data(mut self, on: bool) -> Self {
1699        self.svg_root_data = on;
1700        self
1701    }
1702
1703    /// Transform the coordinate space at draw time (R's `coord_trans`) — stats are
1704    /// computed on raw data but drawn on non-linear axes. Pass a per-axis
1705    /// [`ScaleTransform`] (e.g. `Some(ScaleTransform::Log10)`), `None` to leave an
1706    /// axis linear.
1707    pub fn coord_trans(mut self, x: Option<ScaleTransform>, y: Option<ScaleTransform>) -> Self {
1708        self.coord = Box::new(crate::coord::trans::CoordTrans::new(x, y));
1709        self
1710    }
1711
1712    /// `coord_trans` on the y-axis only.
1713    pub fn coord_trans_y(self, y: ScaleTransform) -> Self {
1714        self.coord_trans(None, Some(y))
1715    }
1716
1717    /// `coord_trans` on the x-axis only.
1718    pub fn coord_trans_x(self, x: ScaleTransform) -> Self {
1719        self.coord_trans(Some(x), None)
1720    }
1721
1722    /// Zoom into a region without filtering data (unlike xlim/ylim which filter).
1723    pub fn coord_cartesian_zoom(
1724        mut self,
1725        xlim: Option<(f64, f64)>,
1726        ylim: Option<(f64, f64)>,
1727    ) -> Self {
1728        let mut c = CoordCartesian::new();
1729        if let Some((min, max)) = xlim {
1730            c = c.xlim(min, max);
1731        }
1732        if let Some((min, max)) = ylim {
1733            c = c.ylim(min, max);
1734        }
1735        self.coord = Box::new(c);
1736        self
1737    }
1738
1739    pub fn coord_polar(mut self) -> Self {
1740        self.coord = Box::new(CoordPolar::new());
1741        self
1742    }
1743
1744    /// Radar / spider chart coordinates: discrete `x` → spokes, `y` → distance
1745    /// from the centre, straight segments, ring/spoke guides. Pair with
1746    /// `geom_polygon` (closed series) and `geom_point`.
1747    pub fn coord_radar(mut self) -> Self {
1748        self.coord = Box::new(crate::coord::radar::CoordRadar::new());
1749        self
1750    }
1751
1752    pub fn coord_radar_with(mut self, coord: crate::coord::radar::CoordRadar) -> Self {
1753        self.coord = Box::new(coord);
1754        self
1755    }
1756
1757    pub fn coord_polar_with(mut self, coord: CoordPolar) -> Self {
1758        self.coord = Box::new(coord);
1759        self
1760    }
1761
1762    // ─── Theme ───────────────────────────────────────────────────
1763
1764    /// Replace the theme. A `primary_color` set earlier is kept unless the new
1765    /// theme sets its own, so preset/`primary_color` call order doesn't matter.
1766    pub fn theme(mut self, theme: Theme) -> Self {
1767        self.apply_preset(theme);
1768        self
1769    }
1770
1771    fn apply_preset(&mut self, theme: Theme) {
1772        let primary = self.theme.primary;
1773        self.theme = theme;
1774        if self.theme.primary.is_none() {
1775            self.theme.primary = primary;
1776        }
1777    }
1778
1779    /// Rotate the x-axis tick labels by `degrees` (R's
1780    /// `guides(x = guide_axis(angle = ...))` / `axis.text.x = element_text(angle)`).
1781    /// Useful for long category labels. Call after any `theme_*()` preset.
1782    pub fn axis_text_x_angle(mut self, degrees: f64) -> Self {
1783        self.theme.axis_text_x.angle = degrees;
1784        self
1785    }
1786
1787    /// Rotate the y-axis tick labels by `degrees`.
1788    pub fn axis_text_y_angle(mut self, degrees: f64) -> Self {
1789        self.theme.axis_text_y.angle = degrees;
1790        self
1791    }
1792
1793    /// Stagger x-axis tick labels across `n` rows to avoid overlap (R's
1794    /// `guides(x = guide_axis(n.dodge = n))`). `1` = no dodging.
1795    pub fn axis_text_x_dodge(mut self, n: usize) -> Self {
1796        self.theme.axis_text_x_dodge = n.max(1);
1797        self
1798    }
1799
1800    /// Fix the panel's height:width ratio (R's `aspect.ratio`). Call after any
1801    /// `theme_*()` preset.
1802    pub fn aspect_ratio(mut self, ratio: f64) -> Self {
1803        self.theme.aspect_ratio = Some(ratio);
1804        self
1805    }
1806
1807    /// Draw gridlines on top of the data layers (R's `panel.ontop`).
1808    pub fn panel_ontop(mut self) -> Self {
1809        self.theme.panel_ontop = true;
1810        self
1811    }
1812
1813    /// Draw minor tick marks between major ticks (R's `axis.minor.ticks`).
1814    pub fn axis_minor_ticks(mut self) -> Self {
1815        self.theme.axis_minor_ticks = true;
1816        self
1817    }
1818
1819    /// Align title/subtitle/caption to the panel or the whole plot width
1820    /// (R's `plot.title.position`).
1821    pub fn title_position(mut self, pos: crate::theme::TitlePosition) -> Self {
1822        self.theme.title_position = pos;
1823        self
1824    }
1825
1826    /// Corner for the `tag()` label (R's `plot.tag.position`).
1827    pub fn tag_position(mut self, pos: crate::theme::TagPosition) -> Self {
1828        self.theme.tag_position = pos;
1829        self
1830    }
1831
1832    /// Force the legend key layout direction (R's `legend.direction`).
1833    pub fn legend_direction(mut self, dir: crate::theme::LegendDirection) -> Self {
1834        self.theme.legend_direction = Some(dir);
1835        self
1836    }
1837
1838    /// Set the brand/primary color used as the default for single-series geoms
1839    /// that have no color/fill aesthetic mapped. Composes with any theme — one
1840    /// render process can serve different tenants' brands at render time.
1841    /// Place the legend inside the panel at panel-relative coordinates
1842    /// (0..1, 0..1) — `(0,0)` bottom-left, `(1,1)` top-right (R's
1843    /// `legend.position = c(x, y)`).
1844    pub fn legend_position_inside(mut self, x: f64, y: f64) -> Self {
1845        self.theme.legend_position = crate::theme::LegendPosition::Inside(x, y);
1846        self
1847    }
1848
1849    /// Set the legend position (R's `legend.position`, e.g. `Top`/`Bottom`).
1850    pub fn legend_position(mut self, pos: crate::theme::LegendPosition) -> Self {
1851        self.theme.legend_position = pos;
1852        self
1853    }
1854
1855    pub fn primary_color(mut self, color: (u8, u8, u8)) -> Self {
1856        self.theme.primary = Some(color);
1857        self
1858    }
1859
1860    pub fn theme_minimal(mut self) -> Self {
1861        self.apply_preset(crate::theme::presets::theme_minimal());
1862        self
1863    }
1864
1865    pub fn theme_bw(mut self) -> Self {
1866        self.apply_preset(crate::theme::presets::theme_bw());
1867        self
1868    }
1869
1870    pub fn theme_gray(mut self) -> Self {
1871        self.apply_preset(crate::theme::presets::theme_gray());
1872        self
1873    }
1874
1875    pub fn theme_classic(mut self) -> Self {
1876        self.apply_preset(crate::theme::presets::theme_classic());
1877        self
1878    }
1879
1880    /// Apply the publication-ready `theme_pubr()` (ggpubr style).
1881    pub fn theme_pubr(mut self) -> Self {
1882        self.apply_preset(crate::theme::presets::theme_pubr());
1883        self
1884    }
1885
1886    pub fn theme_linedraw(mut self) -> Self {
1887        self.apply_preset(crate::theme::presets::theme_linedraw());
1888        self
1889    }
1890
1891    pub fn theme_light(mut self) -> Self {
1892        self.apply_preset(crate::theme::presets::theme_light());
1893        self
1894    }
1895
1896    pub fn theme_dark(mut self) -> Self {
1897        self.apply_preset(crate::theme::presets::theme_dark());
1898        self
1899    }
1900
1901    pub fn theme_void(mut self) -> Self {
1902        self.apply_preset(crate::theme::presets::theme_void());
1903        self
1904    }
1905
1906    /// Apply incremental theme modifications on top of the current theme.
1907    /// Like R's `+ theme(axis.text.x = element_text(...))`.
1908    pub fn theme_update(mut self, update: crate::theme::ThemeUpdate) -> Self {
1909        self.theme = self.theme.update(update);
1910        self
1911    }
1912
1913    // ─── Guides ──────────────────────────────────────────────────
1914
1915    /// Configure legend guide (title, ncol, reverse).
1916    pub fn guides(mut self, guide: crate::guide::config::GuideLegend) -> Self {
1917        self.guide_legend = guide;
1918        self
1919    }
1920
1921    // ─── Labels ──────────────────────────────────────────────────
1922
1923    pub fn labs(mut self, labels: Labels) -> Self {
1924        if labels.title.is_some() {
1925            self.labels.title = labels.title;
1926        }
1927        if labels.subtitle.is_some() {
1928            self.labels.subtitle = labels.subtitle;
1929        }
1930        if labels.x.is_some() {
1931            self.labels.x = labels.x;
1932        }
1933        if labels.y.is_some() {
1934            self.labels.y = labels.y;
1935        }
1936        if labels.caption.is_some() {
1937            self.labels.caption = labels.caption;
1938        }
1939        if labels.tag.is_some() {
1940            self.labels.tag = labels.tag;
1941        }
1942        self
1943    }
1944
1945    pub fn title(mut self, title: &str) -> Self {
1946        self.labels.title = Some(title.to_string());
1947        self
1948    }
1949
1950    /// Corner tag label (R's `labs(tag = ...)`), drawn at the top-left — handy
1951    /// for labelling figure panels ("A", "B", …).
1952    pub fn tag(mut self, tag: &str) -> Self {
1953        self.labels.tag = Some(tag.to_string());
1954        self
1955    }
1956
1957    pub fn subtitle(mut self, subtitle: &str) -> Self {
1958        self.labels.subtitle = Some(subtitle.to_string());
1959        self
1960    }
1961
1962    pub fn xlab(mut self, label: &str) -> Self {
1963        self.labels.x = Some(label.to_string());
1964        self
1965    }
1966
1967    pub fn ylab(mut self, label: &str) -> Self {
1968        self.labels.y = Some(label.to_string());
1969        self
1970    }
1971
1972    pub fn caption(mut self, caption: &str) -> Self {
1973        self.labels.caption = Some(caption.to_string());
1974        self
1975    }
1976
1977    // ─── Annotations ──────────────────────────────────────────────
1978
1979    /// Add an annotation to the plot.
1980    pub fn annotate(mut self, annotation: Annotation) -> Self {
1981        self.annotations.push(annotation);
1982        self
1983    }
1984
1985    /// Add a text annotation at data coordinates.
1986    pub fn annotate_text(self, label: &str, x: f64, y: f64) -> Self {
1987        self.annotate(Annotation::text(label, x, y))
1988    }
1989
1990    /// Add a rectangle annotation at data coordinates.
1991    pub fn annotate_rect(self, xmin: f64, xmax: f64, ymin: f64, ymax: f64) -> Self {
1992        self.annotate(Annotation::rect(xmin, xmax, ymin, ymax))
1993    }
1994
1995    /// Add a segment annotation between data coordinates.
1996    pub fn annotate_segment(self, x: f64, y: f64, xend: f64, yend: f64) -> Self {
1997        self.annotate(Annotation::segment(x, y, xend, yend))
1998    }
1999
2000    /// Default [`Locale`](crate::locale::Locale) for the plot's axis and
2001    /// legend labels: every continuous position / colour / size / alpha scale
2002    /// labels its default breaks with the locale's decimal and thousands marks
2003    /// (`1.000`, `2,5` in de-DE), and date/datetime scales use its month names
2004    /// and date formats (`Okt. 2026`, `15. Okt. 2026`). A scale's own
2005    /// `with_locale` and custom `label_*` formatters take precedence.
2006    /// Without this call labels are unchanged (`.` decimal mark, no grouping,
2007    /// ISO dates).
2008    ///
2009    /// ```
2010    /// use ggplot_rs::prelude::*;
2011    /// let svg = GGPlot::new(vec![
2012    ///         ("x".to_string(), vec![Value::Float(0.0), Value::Float(3000.0)]),
2013    ///         ("y".to_string(), vec![Value::Float(0.5), Value::Float(2.5)]),
2014    ///     ])
2015    ///     .aes(Aes::new().x("x").y("y"))
2016    ///     .geom_point()
2017    ///     .locale(Locale::from_tag("de-DE"))
2018    ///     .render_svg_native()
2019    ///     .unwrap();
2020    /// assert!(svg.contains(">1.000<") && svg.contains(">1,5<"));
2021    /// ```
2022    pub fn locale(mut self, locale: crate::locale::Locale) -> Self {
2023        self.locale = Some(locale);
2024        self
2025    }
2026
2027    // ─── Build and Render ────────────────────────────────────────
2028
2029    /// Build the plot without rendering, returning errors on validation failure.
2030    pub fn try_build(self) -> Result<crate::build::BuiltPlot, GGError> {
2031        PlotBuilder::build(self)
2032    }
2033
2034    /// Build the plot without rendering (analogous to R's ggplot_build()).
2035    /// Returns the fully computed BuiltPlot with layer data ready for inspection.
2036    /// Panics on validation errors — use `try_build()` for error handling.
2037    pub fn build(self) -> crate::build::BuiltPlot {
2038        self.try_build().expect("plot build failed")
2039    }
2040
2041    /// Build and save the plot to a file. Format determined by extension.
2042    /// (Requires the `plotters` feature, on by default; native only — wasm has
2043    /// no filesystem/plotters backend.)
2044    #[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
2045    pub fn save(self, path: &str) -> Result<(), GGError> {
2046        self.save_with_size(path, 800, 600)
2047    }
2048
2049    /// Build and save with custom dimensions. (Feature `plotters`; native only.)
2050    #[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
2051    pub fn save_with_size(self, path: &str, w: u32, h: u32) -> Result<(), GGError> {
2052        let (built, layout) = self.prepare(w, h)?;
2053
2054        // Determine backend from file extension
2055        let ext = path.rsplit('.').next().unwrap_or("svg").to_lowercase();
2056
2057        match ext.as_str() {
2058            "svg" => {
2059                let backend = plotters::prelude::SVGBackend::new(path, (w, h));
2060                Self::render_into(backend.into_drawing_area(), &built, &layout)?;
2061            }
2062            "png" | "bmp" | "gif" | "jpeg" | "jpg" | "tiff" => {
2063                let backend = plotters::prelude::BitMapBackend::new(path, (w, h));
2064                Self::render_into(backend.into_drawing_area(), &built, &layout)?;
2065            }
2066            _ => {
2067                return Err(GGError::UnsupportedFormat(ext));
2068            }
2069        }
2070
2071        Ok(())
2072    }
2073
2074    /// Render the plot to an in-memory SVG document (default 800x600).
2075    ///
2076    /// Unlike [`save`](Self::save), this writes nothing to disk — handy for
2077    /// serving charts from a web/MCP service. Requires the `plotters` feature
2078    /// (on by default); without it use [`render_svg_native`](Self::render_svg_native).
2079    #[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
2080    pub fn render_svg(self) -> Result<String, GGError> {
2081        self.render_svg_with_size(800, 600)
2082    }
2083
2084    /// Render the plot to an in-memory SVG document with custom dimensions.
2085    /// (Feature `plotters`; native only — on wasm or without `plotters` use
2086    /// [`render_svg_native_with_size`](Self::render_svg_native_with_size).)
2087    #[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
2088    pub fn render_svg_with_size(self, w: u32, h: u32) -> Result<String, GGError> {
2089        let (built, layout) = self.prepare(w, h)?;
2090        let mut buf = String::new();
2091        {
2092            let backend = plotters::prelude::SVGBackend::with_string(&mut buf, (w, h));
2093            Self::render_into(backend.into_drawing_area(), &built, &layout)?;
2094        }
2095        Ok(buf)
2096    }
2097
2098    /// Render to SVG through the self-contained [`SvgBackend`](crate::render::svg_backend::SvgBackend)
2099    /// — a plotters-free path that drives the same `PlotRenderer`, proving the
2100    /// `DrawBackend` abstraction (and needing no glyph rasterization).
2101    pub fn render_svg_native(self) -> Result<String, GGError> {
2102        self.render_svg_native_with_size(800, 600)
2103    }
2104
2105    /// [`render_svg_native`](Self::render_svg_native) with an explicit size.
2106    ///
2107    /// The root `<svg>` carries `data-plot="x y w h"` (the panel rect) and the
2108    /// trained position domains (`data-domain`, `data-xdomain`, `data-ydomain`,
2109    /// `data-xlevels`, `data-ylevels` — see
2110    /// [`root_data_attrs`](crate::render::svg_backend::root_data_attrs)).
2111    /// Hoverable marks carry `data-x`, `data-series` and `data-value`.
2112    pub fn render_svg_native_with_size(self, w: u32, h: u32) -> Result<String, GGError> {
2113        Ok(self.render_native(w, h)?.0.finish())
2114    }
2115
2116    /// Like [`render_svg_native_with_size`](Self::render_svg_native_with_size),
2117    /// but also returns the build warnings (rows dropped for non-finite
2118    /// positions, layers whose stat produced no data, …) — see
2119    /// [`BuiltPlot::warnings`](crate::build::BuiltPlot::warnings).
2120    pub fn render_svg_native_with_warnings(
2121        self,
2122        w: u32,
2123        h: u32,
2124    ) -> Result<(String, Vec<String>), GGError> {
2125        let (backend, warnings, _) = self.render_native(w, h)?;
2126        Ok((backend.finish(), warnings))
2127    }
2128
2129    /// Render a `w`×`h` chart as a *nested* `<svg x=.. y=.. width=.. height=..
2130    /// viewBox="0 0 w h">` fragment for composition into a larger SVG (a
2131    /// dashboard): no `xmlns` (inherited from the parent), positioned at
2132    /// `(x, y)` in the parent's user space. No string surgery needed:
2133    ///
2134    /// ```ignore
2135    /// let mut page = String::from(r#"<svg xmlns="http://www.w3.org/2000/svg" width="900" height="400">"#);
2136    /// page += &plot_a.render_svg_native_at(0.0, 0.0, 450, 400)?;
2137    /// page += &plot_b.render_svg_native_at(450.0, 0.0, 450, 400)?;
2138    /// page += "</svg>";
2139    /// ```
2140    pub fn render_svg_native_at(self, x: f64, y: f64, w: u32, h: u32) -> Result<String, GGError> {
2141        Ok(self.render_native(w, h)?.0.finish_fragment(x, y))
2142    }
2143
2144    /// Like [`render_svg_native_with_size`](Self::render_svg_native_with_size),
2145    /// but also returns the panel rect in pixels `[x, y, w, h]` — enough to
2146    /// overlay a WebGL/canvas layer that draws marks in data coordinates.
2147    pub fn render_svg_area_with_size(self, w: u32, h: u32) -> Result<(String, [f64; 4]), GGError> {
2148        let (backend, _, pa) = self.render_native(w, h)?;
2149        Ok((backend.finish(), [pa.x, pa.y, pa.width, pa.height]))
2150    }
2151
2152    /// Shared native-SVG pipeline: build, lay out, render into an
2153    /// [`SvgBackend`](crate::render::svg_backend::SvgBackend) (not yet
2154    /// finished), returning it with the build warnings and the panel rect.
2155    fn render_native(
2156        self,
2157        w: u32,
2158        h: u32,
2159    ) -> Result<
2160        (
2161            crate::render::svg_backend::SvgBackend,
2162            Vec<String>,
2163            crate::render::Rect,
2164        ),
2165        GGError,
2166    > {
2167        let svg_root_data = self.svg_root_data;
2168        let (built, layout) = self.prepare(w, h)?;
2169        let pa = layout.plot_area.clone();
2170        let mut backend = crate::render::svg_backend::SvgBackend::new(w, h, pa.clone());
2171        if svg_root_data {
2172            backend.set_root_attrs(crate::render::svg_backend::root_data_attrs(&built));
2173        } else {
2174            backend.without_plot_attr();
2175        }
2176        PlotRenderer::render(&built, &mut backend).map_err(GGError::Render)?;
2177        let mut warnings = built.warnings;
2178        warnings.extend(backend.take_warnings());
2179        Ok((backend, warnings, pa))
2180    }
2181
2182    /// Render to a raw RGBA pixel buffer via the self-contained raster
2183    /// [`PixelBackend`](crate::render::pixel_backend::PixelBackend) (feature
2184    /// `canvas`) — fast for large-N and wasm-compatible. Returns `(w, h, rgba)`,
2185    /// ready for a `<canvas>` `putImageData`.
2186    #[cfg(feature = "canvas")]
2187    pub fn render_rgba_with_size(self, w: u32, h: u32) -> Result<(u32, u32, Vec<u8>), GGError> {
2188        let (built, layout) = self.prepare(w, h)?;
2189        let mut backend =
2190            crate::render::pixel_backend::PixelBackend::new(w, h, layout.plot_area.clone());
2191        PlotRenderer::render(&built, &mut backend).map_err(GGError::Render)?;
2192        let (ow, oh) = backend.dimensions();
2193        Ok((ow, oh, backend.into_rgba()))
2194    }
2195
2196    /// Like [`render_rgba_with_size`](Self::render_rgba_with_size), but also
2197    /// returns the panel rect in pixels `[x, y, w, h]` — enough to map a canvas
2198    /// pixel back to data coordinates for interactive hover.
2199    #[cfg(feature = "canvas")]
2200    pub fn render_rgba_area_with_size(
2201        self,
2202        w: u32,
2203        h: u32,
2204    ) -> Result<(Vec<u8>, [f64; 4]), GGError> {
2205        let (built, layout) = self.prepare(w, h)?;
2206        let pa = layout.plot_area.clone();
2207        let mut backend = crate::render::pixel_backend::PixelBackend::new(w, h, pa.clone());
2208        PlotRenderer::render(&built, &mut backend).map_err(GGError::Render)?;
2209        Ok((backend.into_rgba(), [pa.x, pa.y, pa.width, pa.height]))
2210    }
2211
2212    /// Render to PNG bytes via the raster [`PixelBackend`](crate::render::pixel_backend::PixelBackend)
2213    /// (feature `canvas`). Unlike [`render_png`](Self::render_png) this needs no
2214    /// plotters bitmap backend, so it also works on wasm.
2215    #[cfg(feature = "canvas")]
2216    pub fn render_png_raster_with_size(self, w: u32, h: u32) -> Result<Vec<u8>, GGError> {
2217        let (built, layout) = self.prepare(w, h)?;
2218        let mut backend =
2219            crate::render::pixel_backend::PixelBackend::new(w, h, layout.plot_area.clone());
2220        PlotRenderer::render(&built, &mut backend).map_err(GGError::Render)?;
2221        backend.into_png().map_err(GGError::Render)
2222    }
2223
2224    /// Render the plot to in-memory PNG bytes (default 800x600).
2225    ///
2226    /// Returns a fully-encoded PNG, ready to write to an HTTP response or
2227    /// embed as a data URI — no temp files involved. Requires the `plotters`
2228    /// feature (on by default); see also `render_png_raster_with_size`
2229    /// (feature `canvas`).
2230    #[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
2231    pub fn render_png(self) -> Result<Vec<u8>, GGError> {
2232        self.render_png_with_size(800, 600)
2233    }
2234
2235    /// Render the plot to in-memory PNG bytes with custom dimensions. (Feature
2236    /// `plotters`; native only — PNG needs the plotters bitmap backend.)
2237    #[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
2238    pub fn render_png_with_size(self, w: u32, h: u32) -> Result<Vec<u8>, GGError> {
2239        let (built, layout) = self.prepare(w, h)?;
2240
2241        // plotters' BitMapBackend draws into a raw RGB buffer; we then encode
2242        // that buffer to PNG via the `image` crate.
2243        let mut rgb = vec![0u8; (w as usize) * (h as usize) * 3];
2244        {
2245            let backend = plotters::prelude::BitMapBackend::with_buffer(&mut rgb, (w, h));
2246            Self::render_into(backend.into_drawing_area(), &built, &layout)?;
2247        }
2248
2249        let img = image::RgbImage::from_raw(w, h, rgb).ok_or_else(|| {
2250            GGError::Render(RenderError::BackendError(
2251                "PNG buffer size mismatch".to_string(),
2252            ))
2253        })?;
2254        let mut out = std::io::Cursor::new(Vec::new());
2255        img.write_to(&mut out, image::ImageOutputFormat::Png)
2256            .map_err(|e| GGError::Render(RenderError::BackendError(format!("{:?}", e))))?;
2257        Ok(out.into_inner())
2258    }
2259
2260    /// Shared pipeline: build the plot, apply label overrides, compute layout.
2261    pub(crate) fn prepare(
2262        self,
2263        w: u32,
2264        h: u32,
2265    ) -> Result<(crate::build::BuiltPlot, PlotLayout), GGError> {
2266        let (mut built, meta) = self.build_for_render()?;
2267        let layout = Self::layout_built(&mut built, &meta, w, h);
2268        Ok((built, layout))
2269    }
2270
2271    /// The size-independent half of [`prepare`](Self::prepare): build the plot,
2272    /// resolve theme inheritance and apply axis-label overrides.
2273    pub(crate) fn build_for_render(self) -> Result<(crate::build::BuiltPlot, RenderMeta), GGError> {
2274        let plot = self;
2275
2276        let has_title = plot.labels.title.is_some();
2277        let has_subtitle = plot.labels.subtitle.is_some();
2278        let has_caption = plot.labels.caption.is_some();
2279        let has_legend = plot.has_legend_mapping();
2280        let x_label = plot.labels.x.clone();
2281        let y_label = plot.labels.y.clone();
2282
2283        let mut built = PlotBuilder::build(plot)?;
2284
2285        // Resolve R-style theme inheritance (root `text` → child text elements).
2286        built.theme.resolve_inheritance();
2287
2288        // Apply user label overrides to scales
2289        if let Some(ref label) = x_label {
2290            if let Some(s) = built.scales.get_mut(&crate::aes::Aesthetic::X) {
2291                s.set_name(label);
2292            }
2293        }
2294        if let Some(ref label) = y_label {
2295            if let Some(s) = built.scales.get_mut(&crate::aes::Aesthetic::Y) {
2296                s.set_name(label);
2297            }
2298        }
2299
2300        Ok((
2301            built,
2302            RenderMeta {
2303                has_title,
2304                has_subtitle,
2305                has_caption,
2306                has_legend,
2307            },
2308        ))
2309    }
2310
2311    /// The size-dependent half of [`prepare`](Self::prepare): auto-tune axis
2312    /// labels for `w`×`h` and compute the layout.
2313    pub(crate) fn layout_built(
2314        built: &mut crate::build::BuiltPlot,
2315        meta: &RenderMeta,
2316        w: u32,
2317        h: u32,
2318    ) -> PlotLayout {
2319        let RenderMeta {
2320            has_title,
2321            has_subtitle,
2322            has_caption,
2323            has_legend,
2324        } = *meta;
2325        let x_axis_top = built
2326            .scales
2327            .get(&crate::aes::Aesthetic::X)
2328            .map(|s| s.axis_position_opposite())
2329            .unwrap_or(false);
2330
2331        // Auto-tune categorical axis labels so they stay legible. A flip swaps
2332        // which trained scale sits on each visual axis: the bottom (x) axis reads
2333        // the Y scale when flipped, the left (y) axis reads the X scale.
2334        let flipped = built.coord.is_flipped();
2335        let h_aes = if flipped {
2336            crate::aes::Aesthetic::Y
2337        } else {
2338            crate::aes::Aesthetic::X
2339        };
2340        let v_aes = if flipped {
2341            crate::aes::Aesthetic::X
2342        } else {
2343            crate::aes::Aesthetic::Y
2344        };
2345
2346        // Crowded discrete bottom-axis labels overlap when drawn flat. Rotate
2347        // them — 45° if that de-collides, otherwise vertical (90°) — and reserve
2348        // the bottom space they actually need. Only when no explicit angle was set.
2349        let x_rot: Option<(f64, f64)> =
2350            if built.theme.axis_text_x.visible && built.theme.axis_text_x.angle.abs() < 1.0 {
2351                built.scales.get(&h_aes).and_then(|hs| {
2352                    if !hs.is_discrete() {
2353                        return None;
2354                    }
2355                    let breaks = hs.breaks();
2356                    let n = breaks.len().max(1) as f64;
2357                    let max_ch = breaks
2358                        .iter()
2359                        .map(|(_, l)| l.chars().count())
2360                        .max()
2361                        .unwrap_or(0) as f64;
2362                    let font = built.theme.axis_text_x.size;
2363                    let label_w = max_ch * font * 0.6; // horizontal extent, drawn flat
2364                    let spacing = 0.9 * w as f64 / n; // ~px between adjacent ticks
2365                    if label_w <= spacing {
2366                        return None; // fits flat — leave it
2367                    }
2368                    // 45° shrinks the horizontal footprint by cos45; if that fits,
2369                    // use it, else go vertical (270° = reads bottom-to-top, hanging
2370                    // below the axis like the y-axis title). Reserve the drop height.
2371                    let vertical = label_w * 0.707 > spacing;
2372                    let angle = if vertical { 270.0 } else { 45.0 };
2373                    let drop = if vertical { label_w } else { label_w * 0.707 };
2374                    Some((angle, drop + font + 4.0))
2375                })
2376            } else {
2377                None
2378            };
2379        let x_label_height = x_rot.map(|(_, drop)| drop);
2380        if let Some((angle, _)) = x_rot {
2381            built.theme.axis_text_x.angle = angle;
2382        }
2383
2384        // Reserve the left-axis width from the actual label lengths so long
2385        // category names (a flipped bar chart's rows) aren't clipped. Never
2386        // shrinks below the default; grows up to 35% of the width.
2387        let y_label_width = if built.theme.axis_text_y.visible {
2388            built.scales.get(&v_aes).map(|vs| {
2389                let max_ch = vs
2390                    .breaks()
2391                    .iter()
2392                    .map(|(_, l)| l.chars().count())
2393                    .max()
2394                    .unwrap_or(0);
2395                let measured = max_ch as f64 * built.theme.axis_text_y.size * 0.6 + 4.0;
2396                let default = built.theme.axis_text_y.size * 3.5 + 4.0;
2397                measured.max(default).min(0.35 * w as f64)
2398            })
2399        } else {
2400            None
2401        };
2402
2403        PlotLayout::compute_full(
2404            w as f64,
2405            h as f64,
2406            &built.theme,
2407            has_title,
2408            has_subtitle,
2409            has_caption,
2410            has_legend,
2411            x_axis_top,
2412            y_label_width,
2413            x_label_height,
2414        )
2415    }
2416
2417    /// Fill the background, render the built plot, and flush — for any plotters
2418    /// backend. (Native only.)
2419    #[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
2420    fn render_into<DB>(
2421        area: plotters::drawing::DrawingArea<DB, plotters::coord::Shift>,
2422        built: &crate::build::BuiltPlot,
2423        layout: &PlotLayout,
2424    ) -> Result<(), GGError>
2425    where
2426        DB: plotters::prelude::DrawingBackend,
2427        DB::ErrorType: 'static,
2428    {
2429        area.fill(&plotters::prelude::WHITE)
2430            .map_err(|e| GGError::Render(RenderError::BackendError(format!("{:?}", e))))?;
2431        let mut adapter = PlottersAdapter::new(&area, layout.plot_area.clone());
2432        PlotRenderer::render(built, &mut adapter).map_err(GGError::Render)?;
2433        area.present()
2434            .map_err(|e| GGError::Render(RenderError::BackendError(format!("{:?}", e))))?;
2435        Ok(())
2436    }
2437
2438    /// Save with physical dimensions (inches) and DPI. (Feature `plotters`;
2439    /// native only.)
2440    #[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
2441    pub fn ggsave(
2442        self,
2443        path: &str,
2444        width_inches: f64,
2445        height_inches: f64,
2446        dpi: f64,
2447    ) -> Result<(), GGError> {
2448        let w = (width_inches * dpi) as u32;
2449        let h = (height_inches * dpi) as u32;
2450        self.save_with_size(path, w, h)
2451    }
2452
2453    fn has_legend_mapping(&self) -> bool {
2454        self.mapping.mappings.iter().any(|m| {
2455            matches!(
2456                m.aesthetic,
2457                crate::aes::Aesthetic::Color
2458                    | crate::aes::Aesthetic::Fill
2459                    | crate::aes::Aesthetic::Shape
2460                    | crate::aes::Aesthetic::Linetype
2461                    | crate::aes::Aesthetic::Size
2462                    | crate::aes::Aesthetic::Alpha
2463            )
2464        })
2465    }
2466}
2467
2468/// Size-independent facts about a built plot that its layout needs.
2469#[derive(Clone, Copy, Debug)]
2470pub(crate) struct RenderMeta {
2471    pub(crate) has_title: bool,
2472    pub(crate) has_subtitle: bool,
2473    pub(crate) has_caption: bool,
2474    pub(crate) has_legend: bool,
2475}
2476
2477/// Top-level error type.
2478#[derive(Debug)]
2479pub enum GGError {
2480    Render(RenderError),
2481    UnsupportedFormat(String),
2482    Io(std::io::Error),
2483    ValidationError(String),
2484}
2485
2486impl std::fmt::Display for GGError {
2487    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2488        match self {
2489            GGError::Render(e) => write!(f, "Render error: {e}"),
2490            GGError::UnsupportedFormat(ext) => write!(f, "Unsupported output format: {ext}"),
2491            GGError::Io(e) => write!(f, "IO error: {e}"),
2492            GGError::ValidationError(msg) => write!(f, "Validation error: {msg}"),
2493        }
2494    }
2495}
2496
2497impl std::error::Error for GGError {}