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