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