Skip to main content

plotkit_core/
axes.rs

1//! The Axes container -- holds chart data and renders a single subplot.
2//!
3//! `Axes` is the central workhorse of the plotting library. It stores a list of
4//! [`Artist`] values (one per chart call), manages auto-scaling, tick generation,
5//! and drives the full ten-step render pipeline that produces the final visual
6//! output within a subplot rectangle.
7
8use crate::annotations::{Annotation, ArrowStyle, TextAnnotation};
9use crate::artist::*;
10use crate::colorbar::{self, Colorbar};
11use crate::error::{PlotError, Result};
12use crate::layout::{self, LayoutConfig};
13use crate::legend::{self, LegendEntry, SwatchKind};
14use crate::primitives::*;
15use crate::renderer::Renderer;
16use crate::scale::Scale;
17use crate::series::{IntoCategories, IntoSeries};
18use crate::theme::{GridAxis, LineStyle, Loc, Marker, Theme, TickDirection};
19use crate::ticks;
20
21// ---------------------------------------------------------------------------
22// Constants
23// ---------------------------------------------------------------------------
24
25/// Default number of ticks to aim for on each axis.
26const DEFAULT_TICK_COUNT: usize = 7;
27
28/// Padding fraction applied to auto-computed data limits so that data points
29/// do not sit directly on the axis spines.
30const AUTOSCALE_PAD: f64 = 0.05;
31
32// ---------------------------------------------------------------------------
33// TwinSide
34// ---------------------------------------------------------------------------
35
36/// Specifies which side a twin axes occupies relative to its parent.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum TwinSide {
39    /// The twin shares the x-axis and draws an independent y-axis on the right.
40    Right,
41    /// The twin shares the y-axis and draws an independent x-axis on the top.
42    Top,
43}
44
45// ---------------------------------------------------------------------------
46// Axes
47// ---------------------------------------------------------------------------
48
49/// A single set of axes within a figure, containing chart artists and
50/// configuration.
51///
52/// Users do not construct `Axes` directly; they are created by
53/// [`Figure::add_subplot`] or the convenience function [`Figure::subplots`].
54/// Once an `Axes` handle is obtained, chart methods such as [`plot`](Axes::plot),
55/// [`scatter`](Axes::scatter), and [`bar`](Axes::bar) add data, and
56/// configuration methods like [`set_title`](Axes::set_title) control labels,
57/// limits, and styling.
58#[derive(Debug)]
59pub struct Axes {
60    /// The list of artists (one per chart call) in draw order.
61    pub(crate) artists: Vec<Artist>,
62    /// Optional title displayed above the plot area.
63    pub(crate) title: Option<String>,
64    /// Optional label for the x-axis.
65    pub(crate) xlabel: Option<String>,
66    /// Optional label for the y-axis.
67    pub(crate) ylabel: Option<String>,
68    /// User-specified x-axis limits; `None` means auto-scale.
69    pub(crate) xlim: Option<(f64, f64)>,
70    /// User-specified y-axis limits; `None` means auto-scale.
71    pub(crate) ylim: Option<(f64, f64)>,
72    /// Scale for the x-axis (linear, log, symlog).
73    pub(crate) xscale: Scale,
74    /// Scale for the y-axis (linear, log, symlog).
75    pub(crate) yscale: Scale,
76    /// Whether to show grid lines. `None` defers to the theme default.
77    pub(crate) show_grid: Option<bool>,
78    /// Which axes display grid lines (x-only, y-only, or both).
79    pub(crate) grid_axis: GridAxis,
80    /// Grid line opacity override (0.0 = transparent, 1.0 = opaque). `None`
81    /// means fully opaque.
82    pub(crate) grid_alpha: Option<f64>,
83    /// Grid line style override. `None` means use the theme default (solid).
84    pub(crate) grid_style: Option<LineStyle>,
85    /// Whether the x-axis is inverted (max on left, min on right).
86    pub(crate) x_inverted: bool,
87    /// Whether the y-axis is inverted (max on bottom, min on top).
88    pub(crate) y_inverted: bool,
89    /// Custom x-axis tick positions. `None` means auto-generate.
90    pub(crate) custom_xticks: Option<Vec<f64>>,
91    /// Custom y-axis tick positions. `None` means auto-generate.
92    pub(crate) custom_yticks: Option<Vec<f64>>,
93    /// Custom x-axis tick labels. Must match the custom tick count.
94    pub(crate) custom_xticklabels: Option<Vec<String>>,
95    /// Custom y-axis tick labels. Must match the custom tick count.
96    pub(crate) custom_yticklabels: Option<Vec<String>>,
97    /// Rotation angle in degrees for x-axis tick labels.
98    pub(crate) xtick_rotation: f64,
99    /// Rotation angle in degrees for y-axis tick labels.
100    pub(crate) ytick_rotation: f64,
101    /// Whether the legend should be drawn.
102    pub(crate) show_legend: bool,
103    /// Where to place the legend.
104    pub(crate) legend_loc: Loc,
105    /// Per-axes theme override. `None` means use the figure theme.
106    pub(crate) theme_override: Option<Theme>,
107    /// Text labels placed at data-space coordinates via [`Axes::text`].
108    pub(crate) texts: Vec<TextAnnotation>,
109    /// Annotations (text + optional arrow) placed via [`Axes::annotate`].
110    pub(crate) annotations: Vec<Annotation>,
111    /// Tracks the current position in the color cycle so that each successive
112    /// artist receives a distinct color automatically.
113    pub(crate) color_index: usize,
114    /// Whether this axes is a twin of another axes.
115    pub(crate) is_twin: bool,
116    /// Which side this twin axes occupies.
117    pub(crate) twin_side: Option<TwinSide>,
118    /// Optional colorbar attached to this axes.
119    pub(crate) colorbar: Option<Colorbar>,
120}
121
122// ---------------------------------------------------------------------------
123// Construction
124// ---------------------------------------------------------------------------
125
126impl Axes {
127    /// Creates a new, empty axes with default settings.
128    pub(crate) fn new() -> Self {
129        Self {
130            artists: Vec::new(),
131            title: None,
132            xlabel: None,
133            ylabel: None,
134            xlim: None,
135            ylim: None,
136            xscale: Scale::default(),
137            yscale: Scale::default(),
138            show_grid: None,
139            grid_axis: GridAxis::default(),
140            grid_alpha: None,
141            grid_style: None,
142            x_inverted: false,
143            y_inverted: false,
144            custom_xticks: None,
145            custom_yticks: None,
146            custom_xticklabels: None,
147            custom_yticklabels: None,
148            xtick_rotation: 0.0,
149            ytick_rotation: 0.0,
150            show_legend: false,
151            legend_loc: Loc::Best,
152            theme_override: None,
153            texts: Vec::new(),
154            annotations: Vec::new(),
155            color_index: 0,
156            is_twin: false,
157            twin_side: None,
158            colorbar: None,
159        }
160    }
161
162    /// Creates a new twin axes.
163    pub(crate) fn new_twin(side: TwinSide, color_index: usize) -> Self {
164        Self {
165            is_twin: true,
166            twin_side: Some(side),
167            color_index,
168            ..Self::new()
169        }
170    }
171
172    /// Returns `true` if this axes is a twin of another axes.
173    pub fn is_twin(&self) -> bool {
174        self.is_twin
175    }
176
177    /// Returns the twin side, if this axes is a twin.
178    pub fn twin_side(&self) -> Option<TwinSide> {
179        self.twin_side
180    }
181}
182
183// ---------------------------------------------------------------------------
184// Chart methods
185// ---------------------------------------------------------------------------
186
187impl Axes {
188    /// Plots a line connecting `(x, y)` data points.
189    ///
190    /// Returns a mutable reference to the newly created [`LineArtist`] so
191    /// that the caller can chain builder methods (`.color()`, `.width()`,
192    /// `.label()`, etc.).
193    ///
194    /// # Errors
195    ///
196    /// Returns [`PlotError::SeriesLengthMismatch`] if `x` and `y` have
197    /// different lengths, or [`PlotError::EmptyData`] if either is empty.
198    pub fn plot<X, Y>(&mut self, x: X, y: Y) -> Result<&mut LineArtist>
199    where
200        X: IntoSeries,
201        Y: IntoSeries,
202    {
203        let xs = x.into_series();
204        let ys = y.into_series();
205        if xs.len() != ys.len() {
206            return Err(PlotError::SeriesLengthMismatch {
207                expected: xs.len(),
208                got: ys.len(),
209            });
210        }
211        if xs.is_empty() {
212            return Err(PlotError::EmptyData);
213        }
214        let color = Color::TABLEAU_10[self.color_index % 10];
215        self.color_index += 1;
216        let artist = LineArtist {
217            x: xs,
218            y: ys,
219            color,
220            width: 1.5,
221            style: crate::theme::LineStyle::Solid,
222            label: None,
223            alpha: 1.0,
224            decimate: crate::decimate::DecimateMode::Auto,
225        };
226        self.artists.push(Artist::Line(artist));
227        match self.artists.last_mut().expect("just pushed") {
228            Artist::Line(a) => Ok(a),
229            _ => unreachable!(),
230        }
231    }
232
233    /// Creates a scatter plot of `(x, y)` data points.
234    ///
235    /// Returns a mutable reference to the [`ScatterArtist`] for chaining.
236    ///
237    /// # Errors
238    ///
239    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
240    /// on invalid input.
241    pub fn scatter<X, Y>(&mut self, x: X, y: Y) -> Result<&mut ScatterArtist>
242    where
243        X: IntoSeries,
244        Y: IntoSeries,
245    {
246        let xs = x.into_series();
247        let ys = y.into_series();
248        if xs.len() != ys.len() {
249            return Err(PlotError::SeriesLengthMismatch {
250                expected: xs.len(),
251                got: ys.len(),
252            });
253        }
254        if xs.is_empty() {
255            return Err(PlotError::EmptyData);
256        }
257        let color = Color::TABLEAU_10[self.color_index % 10];
258        self.color_index += 1;
259        let artist = ScatterArtist {
260            x: xs,
261            y: ys,
262            color,
263            marker: Marker::Circle,
264            size: 6.0,
265            label: None,
266            alpha: 0.8,
267            colors: None,
268            c: None,
269            cmap: None,
270            decimate: crate::decimate::DecimateMode::Auto,
271        };
272        self.artists.push(Artist::Scatter(artist));
273        match self.artists.last_mut().expect("just pushed") {
274            Artist::Scatter(a) => Ok(a),
275            _ => unreachable!(),
276        }
277    }
278
279    /// Creates a vertical bar chart from categorical data and heights.
280    ///
281    /// Each category label maps to one bar whose height is the corresponding
282    /// value in `heights`.
283    ///
284    /// # Errors
285    ///
286    /// Returns [`PlotError::SeriesLengthMismatch`] if `categories` and
287    /// `heights` have different lengths, or [`PlotError::EmptyData`] if empty.
288    pub fn bar<C, H>(&mut self, categories: C, heights: H) -> Result<&mut BarArtist>
289    where
290        C: IntoCategories,
291        H: IntoSeries,
292    {
293        let cats = categories.into_categories();
294        let vals = heights.into_series();
295        if cats.len() != vals.len() {
296            return Err(PlotError::SeriesLengthMismatch {
297                expected: cats.len(),
298                got: vals.len(),
299            });
300        }
301        if cats.is_empty() {
302            return Err(PlotError::EmptyData);
303        }
304        let color = Color::TABLEAU_10[self.color_index % 10];
305        self.color_index += 1;
306        let artist = BarArtist {
307            categories: cats,
308            heights: vals,
309            color,
310            horizontal: false,
311            bar_width: 0.8,
312            label: None,
313            alpha: 1.0,
314            bottom: None,
315            offset: None,
316        };
317        self.artists.push(Artist::Bar(artist));
318        match self.artists.last_mut().expect("just pushed") {
319            Artist::Bar(a) => Ok(a),
320            _ => unreachable!(),
321        }
322    }
323
324    /// Creates a horizontal bar chart from categorical data and widths.
325    ///
326    /// Behaves like [`bar`](Axes::bar) but draws horizontal bars from the
327    /// y-axis.
328    ///
329    /// # Errors
330    ///
331    /// Same as [`bar`](Axes::bar).
332    pub fn barh<C, W>(&mut self, categories: C, widths: W) -> Result<&mut BarArtist>
333    where
334        C: IntoCategories,
335        W: IntoSeries,
336    {
337        let cats = categories.into_categories();
338        let vals = widths.into_series();
339        if cats.len() != vals.len() {
340            return Err(PlotError::SeriesLengthMismatch {
341                expected: cats.len(),
342                got: vals.len(),
343            });
344        }
345        if cats.is_empty() {
346            return Err(PlotError::EmptyData);
347        }
348        let color = Color::TABLEAU_10[self.color_index % 10];
349        self.color_index += 1;
350        let artist = BarArtist {
351            categories: cats,
352            heights: vals,
353            color,
354            horizontal: true,
355            bar_width: 0.8,
356            label: None,
357            alpha: 1.0,
358            bottom: None,
359            offset: None,
360        };
361        self.artists.push(Artist::Bar(artist));
362        match self.artists.last_mut().expect("just pushed") {
363            Artist::Bar(a) => Ok(a),
364            _ => unreachable!(),
365        }
366    }
367
368    /// Creates a histogram from raw data.
369    ///
370    /// The data is partitioned into `bins` equal-width bins spanning the data
371    /// range, and each bin is drawn as a vertical bar whose height equals the
372    /// count of values falling in that bin.
373    ///
374    /// # Errors
375    ///
376    /// Returns [`PlotError::EmptyData`] if `data` is empty.
377    pub fn hist<D>(&mut self, data: D, bins: usize) -> Result<&mut HistArtist>
378    where
379        D: IntoSeries,
380    {
381        let series = data.into_series();
382        if series.is_empty() {
383            return Err(PlotError::EmptyData);
384        }
385        let bins = bins.max(1);
386
387        // Compute range from finite values.
388        let (data_min, data_max) = series.bounds().unwrap_or((0.0, 1.0));
389
390        // Handle degenerate case where all values are identical.
391        let (lo, hi) = if (data_max - data_min).abs() < f64::EPSILON {
392            (data_min - 0.5, data_max + 0.5)
393        } else {
394            (data_min, data_max)
395        };
396
397        let bin_width = (hi - lo) / bins as f64;
398
399        // Build bin edges: bins+1 edges.
400        let mut edges: Vec<f64> = (0..=bins).map(|i| lo + i as f64 * bin_width).collect();
401        // Ensure the last edge exactly equals hi to avoid floating-point gaps.
402        *edges.last_mut().expect("edges is non-empty") = hi;
403
404        // Count values per bin.
405        let mut counts = vec![0.0f64; bins];
406        for &v in &series.data {
407            if !v.is_finite() {
408                continue;
409            }
410            // Determine bin index.
411            let idx = if v >= hi {
412                // Values exactly at the upper edge go into the last bin.
413                bins - 1
414            } else {
415                let raw = ((v - lo) / bin_width) as usize;
416                raw.min(bins - 1)
417            };
418            counts[idx] += 1.0;
419        }
420
421        let color = Color::TABLEAU_10[self.color_index % 10];
422        self.color_index += 1;
423        let artist = HistArtist {
424            data: series,
425            bins,
426            bin_edges: edges,
427            counts,
428            color,
429            label: None,
430            alpha: 0.85,
431            density: false,
432        };
433
434        self.artists.push(Artist::Histogram(artist));
435        match self.artists.last_mut().expect("just pushed") {
436            Artist::Histogram(a) => Ok(a),
437            _ => unreachable!(),
438        }
439    }
440
441    /// Fills the area between two y-series that share the same x values.
442    ///
443    /// Useful for confidence intervals, envelopes, and area charts.
444    ///
445    /// # Errors
446    ///
447    /// Returns [`PlotError::SeriesLengthMismatch`] if any of the three
448    /// series differ in length, or [`PlotError::EmptyData`] if empty.
449    pub fn fill_between<X, Y1, Y2>(
450        &mut self,
451        x: X,
452        y1: Y1,
453        y2: Y2,
454    ) -> Result<&mut FillBetweenArtist>
455    where
456        X: IntoSeries,
457        Y1: IntoSeries,
458        Y2: IntoSeries,
459    {
460        let xs = x.into_series();
461        let y1s = y1.into_series();
462        let y2s = y2.into_series();
463        if xs.len() != y1s.len() {
464            return Err(PlotError::SeriesLengthMismatch {
465                expected: xs.len(),
466                got: y1s.len(),
467            });
468        }
469        if xs.len() != y2s.len() {
470            return Err(PlotError::SeriesLengthMismatch {
471                expected: xs.len(),
472                got: y2s.len(),
473            });
474        }
475        if xs.is_empty() {
476            return Err(PlotError::EmptyData);
477        }
478        let color = Color::TABLEAU_10[self.color_index % 10];
479        self.color_index += 1;
480        let artist = FillBetweenArtist {
481            x: xs,
482            y1: y1s,
483            y2: y2s,
484            color,
485            label: None,
486            alpha: 0.3,
487        };
488        self.artists.push(Artist::FillBetween(artist));
489        match self.artists.last_mut().expect("just pushed") {
490            Artist::FillBetween(a) => Ok(a),
491            _ => unreachable!(),
492        }
493    }
494
495    /// Plots a step (staircase) chart connecting `(x, y)` data points.
496    ///
497    /// Returns a mutable reference to the [`StepArtist`] for chaining.
498    ///
499    /// # Errors
500    ///
501    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
502    /// on invalid input.
503    pub fn step<X: IntoSeries, Y: IntoSeries>(&mut self, x: X, y: Y) -> Result<&mut StepArtist> {
504        let xs = x.into_series();
505        let ys = y.into_series();
506        if xs.len() != ys.len() {
507            return Err(PlotError::SeriesLengthMismatch {
508                expected: xs.len(),
509                got: ys.len(),
510            });
511        }
512        if xs.is_empty() {
513            return Err(PlotError::EmptyData);
514        }
515        let color = Color::TABLEAU_10[self.color_index % 10];
516        self.color_index += 1;
517        let artist = StepArtist {
518            x: xs,
519            y: ys,
520            color,
521            width: 1.5,
522            where_step: StepWhere::Pre,
523            label: None,
524            alpha: 1.0,
525        };
526        self.artists.push(Artist::Step(artist));
527        match self.artists.last_mut().expect("just pushed") {
528            Artist::Step(a) => Ok(a),
529            _ => unreachable!(),
530        }
531    }
532
533    /// Plots a stem (lollipop) chart from `(x, y)` data points.
534    ///
535    /// Returns a mutable reference to the [`StemArtist`] for chaining.
536    ///
537    /// # Errors
538    ///
539    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
540    /// on invalid input.
541    pub fn stem<X: IntoSeries, Y: IntoSeries>(&mut self, x: X, y: Y) -> Result<&mut StemArtist> {
542        let xs = x.into_series();
543        let ys = y.into_series();
544        if xs.len() != ys.len() {
545            return Err(PlotError::SeriesLengthMismatch {
546                expected: xs.len(),
547                got: ys.len(),
548            });
549        }
550        if xs.is_empty() {
551            return Err(PlotError::EmptyData);
552        }
553        let color = Color::TABLEAU_10[self.color_index % 10];
554        self.color_index += 1;
555        let artist = StemArtist {
556            x: xs,
557            y: ys,
558            color,
559            line_width: 1.5,
560            marker_size: 6.0,
561            baseline: 0.0,
562            label: None,
563            alpha: 1.0,
564        };
565        self.artists.push(Artist::Stem(artist));
566        match self.artists.last_mut().expect("just pushed") {
567            Artist::Stem(a) => Ok(a),
568            _ => unreachable!(),
569        }
570    }
571
572    /// Creates a box-and-whisker plot from one or more data groups.
573    ///
574    /// Each inner `Vec<f64>` produces one box. Groups are placed at integer
575    /// x-positions starting from 0.
576    ///
577    /// # Errors
578    ///
579    /// Returns [`PlotError::EmptyData`] if `datasets` is empty.
580    pub fn boxplot(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut BoxPlotArtist> {
581        use crate::charts::boxplot::compute_stats;
582        if datasets.is_empty() {
583            return Err(PlotError::EmptyData);
584        }
585        let color = Color::TABLEAU_10[self.color_index % 10];
586        self.color_index += 1;
587        let factor = 1.5;
588        let stats: Vec<_> = datasets.iter().map(|d| compute_stats(d, factor)).collect();
589        let labels: Vec<String> = (0..datasets.len()).map(|i| format!("{}", i + 1)).collect();
590        let artist = BoxPlotArtist {
591            stats,
592            labels,
593            color,
594            label: None,
595            alpha: 1.0,
596            box_width: 0.5,
597            show_outliers: true,
598            whisker_iq_factor: factor,
599            raw_data: datasets,
600        };
601        self.artists.push(Artist::BoxPlot(artist));
602        match self.artists.last_mut().expect("just pushed") {
603            Artist::BoxPlot(a) => Ok(a),
604            _ => unreachable!(),
605        }
606    }
607
608    /// Creates an error bar plot from `(x, y)` data points.
609    ///
610    /// Returns an owned [`ErrorBarArtist`] that can be configured with
611    /// builder methods (`.yerr_symmetric()`, `.cap_size()`, etc.) and then
612    /// added to the axes via [`add_errorbar`](Axes::add_errorbar).
613    ///
614    /// # Errors
615    ///
616    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
617    /// on invalid input.
618    pub fn errorbar<X: IntoSeries, Y: IntoSeries>(&mut self, x: X, y: Y) -> Result<ErrorBarArtist> {
619        let xs = x.into_series();
620        let ys = y.into_series();
621        if xs.len() != ys.len() {
622            return Err(PlotError::SeriesLengthMismatch {
623                expected: xs.len(),
624                got: ys.len(),
625            });
626        }
627        if xs.is_empty() {
628            return Err(PlotError::EmptyData);
629        }
630        let color = Color::TABLEAU_10[self.color_index % 10];
631        self.color_index += 1;
632        Ok(ErrorBarArtist {
633            x: xs,
634            y: ys,
635            xerr: None,
636            yerr: None,
637            color,
638            label: None,
639            cap_size: 4.0,
640            line_width: 1.0,
641        })
642    }
643
644    /// Adds a finalized [`ErrorBarArtist`] to the axes.
645    ///
646    /// Use this after configuring the artist returned by
647    /// [`errorbar`](Axes::errorbar).
648    pub fn add_errorbar(&mut self, artist: ErrorBarArtist) {
649        self.artists.push(Artist::ErrorBar(artist));
650    }
651
652    /// Creates a heatmap from a 2D grid of values.
653    ///
654    /// Returns a mutable reference to the [`HeatmapArtist`] for chaining
655    /// builder methods (`.colormap()`, `.vmin()`, `.show_values()`, etc.).
656    ///
657    /// # Errors
658    ///
659    /// Returns [`PlotError::EmptyData`] if `data` is empty.
660    pub fn heatmap(&mut self, data: Vec<Vec<f64>>) -> Result<&mut HeatmapArtist> {
661        if data.is_empty() {
662            return Err(PlotError::EmptyData);
663        }
664        let color = Color::TABLEAU_10[self.color_index % 10];
665        self.color_index += 1;
666        let artist = HeatmapArtist {
667            data,
668            x_labels: None,
669            y_labels: None,
670            cmap: crate::colormap::Colormap::Viridis,
671            vmin: None,
672            vmax: None,
673            show_values: false,
674            color,
675            label: None,
676            show_colorbar: false,
677        };
678        self.artists.push(Artist::Heatmap(artist));
679        match self.artists.last_mut().expect("just pushed") {
680            Artist::Heatmap(a) => Ok(a),
681            _ => unreachable!(),
682        }
683    }
684
685    /// Creates a pie chart from wedge sizes.
686    ///
687    /// The `sizes` are automatically normalised so that all wedges sum to
688    /// a full circle. Returns a mutable reference to the [`PieArtist`]
689    /// for chaining builder methods (`.labels()`, `.autopct()`,
690    /// `.explode()`, etc.).
691    ///
692    /// # Errors
693    ///
694    /// Returns [`PlotError::EmptyData`] if `sizes` is empty.
695    pub fn pie<S: IntoSeries>(&mut self, sizes: S) -> Result<&mut PieArtist> {
696        let series = sizes.into_series();
697        if series.is_empty() {
698            return Err(PlotError::EmptyData);
699        }
700        let color = Color::TABLEAU_10[self.color_index % 10];
701        self.color_index += 1;
702        let artist = PieArtist {
703            sizes: series.data,
704            labels: None,
705            colors: None,
706            explode: None,
707            autopct: false,
708            start_angle: 90.0,
709            radius: 1.0,
710            label: None,
711            color,
712        };
713        self.artists.push(Artist::Pie(artist));
714        match self.artists.last_mut().expect("just pushed") {
715            Artist::Pie(a) => Ok(a),
716            _ => unreachable!(),
717        }
718    }
719}
720
721// ---------------------------------------------------------------------------
722// Configuration methods (builder-style, return &mut Self)
723// ---------------------------------------------------------------------------
724
725impl Axes {
726    /// Sets the title displayed above the axes area.
727    ///
728    /// Supports `^`/`_` super/subscript markup — see [`crate::text`].
729    pub fn set_title(&mut self, title: &str) -> &mut Self {
730        self.title = Some(crate::text::format_markup(title));
731        self
732    }
733
734    /// Sets the x-axis label. Supports `^`/`_` markup — see [`crate::text`].
735    pub fn set_xlabel(&mut self, label: &str) -> &mut Self {
736        self.xlabel = Some(crate::text::format_markup(label));
737        self
738    }
739
740    /// Sets the y-axis label. Supports `^`/`_` markup — see [`crate::text`].
741    pub fn set_ylabel(&mut self, label: &str) -> &mut Self {
742        self.ylabel = Some(crate::text::format_markup(label));
743        self
744    }
745
746    /// Sets explicit x-axis limits. Pass `(min, max)`.
747    pub fn set_xlim(&mut self, min: f64, max: f64) -> &mut Self {
748        self.xlim = Some((min, max));
749        self
750    }
751
752    /// Sets explicit y-axis limits. Pass `(min, max)`.
753    pub fn set_ylim(&mut self, min: f64, max: f64) -> &mut Self {
754        self.ylim = Some((min, max));
755        self
756    }
757
758    /// Sets the x-axis scale (linear, log10, symlog).
759    pub fn set_xscale(&mut self, scale: Scale) -> &mut Self {
760        self.xscale = scale;
761        self
762    }
763
764    /// Sets the y-axis scale (linear, log10, symlog).
765    pub fn set_yscale(&mut self, scale: Scale) -> &mut Self {
766        self.yscale = scale;
767        self
768    }
769
770    /// Enables or disables grid lines on this axes.
771    pub fn grid(&mut self, show: bool) -> &mut Self {
772        self.show_grid = Some(show);
773        self
774    }
775
776    /// Enables the legend on this axes.
777    pub fn legend(&mut self) -> &mut Self {
778        self.show_legend = true;
779        self
780    }
781
782    /// Sets the legend location.
783    pub fn set_legend_loc(&mut self, loc: Loc) -> &mut Self {
784        self.legend_loc = loc;
785        self
786    }
787
788    /// Overrides the figure theme for this axes only.
789    pub fn set_theme(&mut self, theme: Theme) -> &mut Self {
790        self.theme_override = Some(theme);
791        self
792    }
793
794    /// Sets which axes display grid lines.
795    ///
796    /// Accepts `"x"`, `"y"`, or `"both"` (the default). Unrecognised values
797    /// are silently treated as `"both"`.
798    pub fn grid_axis(&mut self, axis: &str) -> &mut Self {
799        self.grid_axis = match axis {
800            "x" => GridAxis::X,
801            "y" => GridAxis::Y,
802            _ => GridAxis::Both,
803        };
804        self
805    }
806
807    /// Sets the grid line opacity (0.0 = fully transparent, 1.0 = fully opaque).
808    pub fn grid_alpha(&mut self, alpha: f64) -> &mut Self {
809        self.grid_alpha = Some(alpha.clamp(0.0, 1.0));
810        self
811    }
812
813    /// Sets the grid line style.
814    pub fn grid_style(&mut self, style: LineStyle) -> &mut Self {
815        self.grid_style = Some(style);
816        self
817    }
818
819    /// Inverts the x-axis so that larger values appear on the left.
820    pub fn invert_xaxis(&mut self) -> &mut Self {
821        self.x_inverted = true;
822        self
823    }
824
825    /// Inverts the y-axis so that larger values appear at the bottom.
826    pub fn invert_yaxis(&mut self) -> &mut Self {
827        self.y_inverted = true;
828        self
829    }
830
831    /// Sets custom tick positions on the x-axis.
832    ///
833    /// When set, the auto-generated ticks are replaced by these positions.
834    pub fn set_xticks(&mut self, ticks: &[f64]) -> &mut Self {
835        self.custom_xticks = Some(ticks.to_vec());
836        self
837    }
838
839    /// Sets custom tick positions on the y-axis.
840    ///
841    /// When set, the auto-generated ticks are replaced by these positions.
842    pub fn set_yticks(&mut self, ticks: &[f64]) -> &mut Self {
843        self.custom_yticks = Some(ticks.to_vec());
844        self
845    }
846
847    /// Sets custom tick labels for the x-axis.
848    ///
849    /// The number of labels should match the number of custom tick positions
850    /// set via [`set_xticks`](Axes::set_xticks). Extra labels are ignored;
851    /// missing labels default to the formatted tick value.
852    pub fn set_xticklabels(&mut self, labels: &[&str]) -> &mut Self {
853        self.custom_xticklabels = Some(labels.iter().map(|s| s.to_string()).collect());
854        self
855    }
856
857    /// Sets custom tick labels for the y-axis.
858    ///
859    /// The number of labels should match the number of custom tick positions
860    /// set via [`set_yticks`](Axes::set_yticks). Extra labels are ignored;
861    /// missing labels default to the formatted tick value.
862    pub fn set_yticklabels(&mut self, labels: &[&str]) -> &mut Self {
863        self.custom_yticklabels = Some(labels.iter().map(|s| s.to_string()).collect());
864        self
865    }
866
867    /// Sets the rotation angle (in degrees) for x-axis tick labels.
868    pub fn tick_params_x_rotation(&mut self, degrees: f64) -> &mut Self {
869        self.xtick_rotation = degrees;
870        self
871    }
872
873    /// Sets the rotation angle (in degrees) for y-axis tick labels.
874    pub fn tick_params_y_rotation(&mut self, degrees: f64) -> &mut Self {
875        self.ytick_rotation = degrees;
876        self
877    }
878
879    /// Places a text label at data coordinates `(x, y)`.
880    ///
881    /// Returns a mutable reference to the [`TextAnnotation`] for builder-style
882    /// configuration (`.fontsize()`, `.color()`, `.ha()`, `.va()`, `.rotation()`).
883    ///
884    /// Text annotations do **not** affect autoscale; they annotate existing
885    /// data without expanding the axis limits.
886    pub fn text(&mut self, x: f64, y: f64, text: &str) -> &mut TextAnnotation {
887        self.texts.push(TextAnnotation {
888            text: crate::text::format_markup(text),
889            x,
890            y,
891            fontsize: None,
892            color: None,
893            ha: HAlign::Left,
894            va: VAlign::Baseline,
895            rotation: 0.0,
896        });
897        self.texts.last_mut().expect("just pushed")
898    }
899
900    /// Annotates the data point `xy` with `text` placed at `xytext`.
901    ///
902    /// Returns a mutable reference to the [`Annotation`] for builder-style
903    /// configuration (`.fontsize()`, `.color()`, `.ha()`, `.va()`,
904    /// `.arrowstyle()`, `.arrow_color()`).
905    ///
906    /// When an [`ArrowStyle`] other than [`ArrowStyle::None`] is set, an arrow
907    /// is drawn from `xytext` to `xy`. Annotations do **not** affect autoscale.
908    pub fn annotate(&mut self, text: &str, xy: (f64, f64), xytext: (f64, f64)) -> &mut Annotation {
909        self.annotations.push(Annotation {
910            text: crate::text::format_markup(text),
911            xy,
912            xytext,
913            fontsize: None,
914            color: None,
915            ha: HAlign::Center,
916            va: VAlign::Bottom,
917            arrowstyle: ArrowStyle::None,
918            arrow_color: None,
919        });
920        self.annotations.last_mut().expect("just pushed")
921    }
922}
923
924// ---------------------------------------------------------------------------
925// Colorbar
926// ---------------------------------------------------------------------------
927
928impl Axes {
929    /// Adds a colorbar to this axes.
930    pub fn colorbar(
931        &mut self,
932        cmap: crate::colormap::Colormap,
933        vmin: f64,
934        vmax: f64,
935    ) -> &mut Colorbar {
936        self.colorbar = Some(Colorbar::new(cmap, vmin, vmax));
937        self.colorbar.as_mut().expect("just set")
938    }
939}
940
941// ---------------------------------------------------------------------------
942// Contour, Violin, Bar group
943// ---------------------------------------------------------------------------
944
945impl Axes {
946    /// Creates a contour plot (iso-lines) from a 2D grid of z values.
947    pub fn contour(
948        &mut self,
949        x: &[f64],
950        y: &[f64],
951        z: Vec<Vec<f64>>,
952    ) -> Result<&mut ContourArtist> {
953        if x.is_empty() || y.is_empty() || z.is_empty() {
954            return Err(PlotError::EmptyData);
955        }
956        let color = Color::TABLEAU_10[self.color_index % 10];
957        self.color_index += 1;
958        let artist = ContourArtist {
959            x: x.to_vec(),
960            y: y.to_vec(),
961            z,
962            levels: None,
963            filled: false,
964            cmap: crate::colormap::Colormap::Viridis,
965            colors: None,
966            linewidths: 1.0,
967            label: None,
968            color,
969            num_levels: 10,
970        };
971        self.artists.push(Artist::Contour(artist));
972        match self.artists.last_mut().expect("just pushed") {
973            Artist::Contour(a) => Ok(a),
974            _ => unreachable!(),
975        }
976    }
977
978    /// Creates a filled contour plot from a 2D grid of z values.
979    pub fn contourf(
980        &mut self,
981        x: &[f64],
982        y: &[f64],
983        z: Vec<Vec<f64>>,
984    ) -> Result<&mut ContourArtist> {
985        if x.is_empty() || y.is_empty() || z.is_empty() {
986            return Err(PlotError::EmptyData);
987        }
988        let color = Color::TABLEAU_10[self.color_index % 10];
989        self.color_index += 1;
990        let artist = ContourArtist {
991            x: x.to_vec(),
992            y: y.to_vec(),
993            z,
994            levels: None,
995            filled: true,
996            cmap: crate::colormap::Colormap::Viridis,
997            colors: None,
998            linewidths: 1.0,
999            label: None,
1000            color,
1001            num_levels: 10,
1002        };
1003        self.artists.push(Artist::Contour(artist));
1004        match self.artists.last_mut().expect("just pushed") {
1005            Artist::Contour(a) => Ok(a),
1006            _ => unreachable!(),
1007        }
1008    }
1009
1010    /// Creates a violin plot from one or more datasets.
1011    pub fn violin(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut ViolinArtist> {
1012        if datasets.is_empty() {
1013            return Err(PlotError::EmptyData);
1014        }
1015        let color = Color::TABLEAU_10[self.color_index % 10];
1016        self.color_index += 1;
1017        let artist = ViolinArtist {
1018            datasets,
1019            positions: None,
1020            widths: 0.7,
1021            show_median: true,
1022            show_quartiles: true,
1023            color,
1024            alpha: 0.7,
1025            label: None,
1026            bw_method: 0.0,
1027        };
1028        self.artists.push(Artist::Violin(artist));
1029        match self.artists.last_mut().expect("just pushed") {
1030            Artist::Violin(a) => Ok(a),
1031            _ => unreachable!(),
1032        }
1033    }
1034
1035    /// Creates grouped (side-by-side) bars for multiple series.
1036    ///
1037    /// `series` is a slice of `(label, heights)` pairs. Each series gets
1038    /// bars placed side-by-side within each category.
1039    pub fn bar_group<C: IntoCategories>(
1040        &mut self,
1041        categories: C,
1042        series: &[(&str, Vec<f64>)],
1043    ) -> Result<()> {
1044        let cat_labels: Vec<String> = categories
1045            .into_categories()
1046            .labels
1047            .iter()
1048            .map(|s| s.to_string())
1049            .collect();
1050        let n_series = series.len();
1051        if n_series == 0 {
1052            return Err(PlotError::EmptyData);
1053        }
1054        let total_width = 0.8;
1055        let bar_width = total_width / n_series as f64;
1056
1057        for (si, (label, heights)) in series.iter().enumerate() {
1058            let offset_val = (si as f64 - (n_series as f64 - 1.0) / 2.0) * bar_width;
1059            let offsets: Vec<f64> = vec![offset_val; heights.len()];
1060            let artist_ref = self.bar(cat_labels.clone(), heights.as_slice())?;
1061            artist_ref.bar_width(bar_width);
1062            artist_ref.offset(offsets);
1063            artist_ref.label(label);
1064        }
1065        Ok(())
1066    }
1067
1068    /// Creates a hexagonal binning plot from `(x, y)` data points.
1069    ///
1070    /// Returns a mutable reference to the [`HexbinArtist`] for chaining
1071    /// builder methods (`.gridsize()`, `.colormap()`, `.mincnt()`, etc.).
1072    ///
1073    /// # Errors
1074    ///
1075    /// Returns [`PlotError::EmptyData`] if `x` or `y` is empty.
1076    /// Returns [`PlotError::SeriesLengthMismatch`] if `x` and `y` differ in length.
1077    pub fn hexbin<X, Y>(&mut self, x: X, y: Y) -> Result<&mut HexbinArtist>
1078    where
1079        X: IntoSeries,
1080        Y: IntoSeries,
1081    {
1082        let xs = x.into_series();
1083        let ys = y.into_series();
1084        if xs.is_empty() || ys.is_empty() {
1085            return Err(PlotError::EmptyData);
1086        }
1087        if xs.len() != ys.len() {
1088            return Err(PlotError::SeriesLengthMismatch {
1089                expected: xs.len(),
1090                got: ys.len(),
1091            });
1092        }
1093        let color = Color::TABLEAU_10[self.color_index % 10];
1094        self.color_index += 1;
1095        let artist = HexbinArtist {
1096            x: xs.data,
1097            y: ys.data,
1098            gridsize: 20,
1099            cmap: crate::colormap::Colormap::Viridis,
1100            mincnt: 1,
1101            alpha: 1.0,
1102            color,
1103            label: None,
1104            edgecolor: None,
1105            show_colorbar: false,
1106        };
1107        self.artists.push(Artist::Hexbin(artist));
1108        match self.artists.last_mut().expect("just pushed") {
1109            Artist::Hexbin(a) => Ok(a),
1110            _ => unreachable!(),
1111        }
1112    }
1113
1114    /// Creates a polar line plot from `(theta, r)` data in polar coordinates.
1115    ///
1116    /// `theta` contains angles in radians, `r` contains the corresponding
1117    /// radial distances. The data is drawn as a connected polyline in polar
1118    /// space.
1119    ///
1120    /// Returns a mutable reference to the [`PolarArtist`] for chaining.
1121    ///
1122    /// # Errors
1123    ///
1124    /// Returns [`PlotError::SeriesLengthMismatch`] if `theta` and `r` have
1125    /// different lengths, or [`PlotError::EmptyData`] if either is empty.
1126    pub fn polar_plot<T, R>(&mut self, theta: T, r: R) -> Result<&mut PolarArtist>
1127    where
1128        T: IntoSeries,
1129        R: IntoSeries,
1130    {
1131        let ts = theta.into_series();
1132        let rs = r.into_series();
1133        if ts.len() != rs.len() {
1134            return Err(PlotError::SeriesLengthMismatch {
1135                expected: ts.len(),
1136                got: rs.len(),
1137            });
1138        }
1139        if ts.is_empty() {
1140            return Err(PlotError::EmptyData);
1141        }
1142        let color = Color::TABLEAU_10[self.color_index % 10];
1143        self.color_index += 1;
1144        let artist = PolarArtist {
1145            theta: ts.data,
1146            r: rs.data,
1147            color,
1148            label: None,
1149            alpha: 1.0,
1150            linewidth: 1.5,
1151            filled: false,
1152            marker: None,
1153        };
1154        self.artists.push(Artist::Polar(artist));
1155        match self.artists.last_mut().expect("just pushed") {
1156            Artist::Polar(a) => Ok(a),
1157            _ => unreachable!(),
1158        }
1159    }
1160
1161    /// Creates a filled polar (radar/area) chart from `(theta, r)` data.
1162    ///
1163    /// Like [`polar_plot`](Axes::polar_plot), but the path is automatically
1164    /// closed and filled, producing a radar or area chart.
1165    ///
1166    /// # Errors
1167    ///
1168    /// Returns [`PlotError::SeriesLengthMismatch`] if `theta` and `r` have
1169    /// different lengths, or [`PlotError::EmptyData`] if either is empty.
1170    pub fn polar_fill<T, R>(&mut self, theta: T, r: R) -> Result<&mut PolarArtist>
1171    where
1172        T: IntoSeries,
1173        R: IntoSeries,
1174    {
1175        let artist_ref = self.polar_plot(theta, r)?;
1176        artist_ref.filled(true);
1177        artist_ref.alpha(0.3);
1178        Ok(artist_ref)
1179    }
1180
1181    /// Creates a waterfall chart from categorical data and change values.
1182    ///
1183    /// Each bar represents an incremental change (positive or negative) from
1184    /// the previous running total. Positive changes are colored green,
1185    /// negative changes red, and bars marked as totals are colored blue-gray.
1186    ///
1187    /// The method also sets the x-axis tick positions and labels to match the
1188    /// category labels so they render on the axis automatically.
1189    ///
1190    /// # Errors
1191    ///
1192    /// Returns [`PlotError::SeriesLengthMismatch`] if `categories` and
1193    /// `values` have different lengths, or [`PlotError::EmptyData`] if empty.
1194    pub fn waterfall<C, V>(&mut self, categories: C, values: V) -> Result<&mut WaterfallArtist>
1195    where
1196        C: IntoCategories,
1197        V: IntoSeries,
1198    {
1199        let cats = categories.into_categories();
1200        let vals = values.into_series();
1201        if cats.len() != vals.len() {
1202            return Err(PlotError::SeriesLengthMismatch {
1203                expected: cats.len(),
1204                got: vals.len(),
1205            });
1206        }
1207        if cats.is_empty() {
1208            return Err(PlotError::EmptyData);
1209        }
1210
1211        // Set x-axis ticks to category positions with category labels.
1212        let n = cats.len();
1213        let tick_positions: Vec<f64> = (0..n).map(|i| i as f64).collect();
1214        self.custom_xticks = Some(tick_positions);
1215        self.custom_xticklabels = Some(cats.labels.iter().map(|s| s.to_string()).collect());
1216
1217        let color = Color::TABLEAU_10[self.color_index % 10];
1218        self.color_index += 1;
1219        let artist = WaterfallArtist {
1220            categories: cats,
1221            values: vals,
1222            total_indices: Vec::new(),
1223            increase_color: Color::rgb(0x2C, 0xA0, 0x2C), // Professional green
1224            decrease_color: Color::rgb(0xD6, 0x27, 0x28), // Professional red
1225            total_color: Color::rgb(0x4E, 0x79, 0xA7),    // Tableau blue-gray
1226            connector_lines: true,
1227            show_values: false,
1228            bar_width: 0.6,
1229            label: None,
1230            color,
1231            alpha: 1.0,
1232        };
1233        self.artists.push(Artist::Waterfall(artist));
1234        match self.artists.last_mut().expect("just pushed") {
1235            Artist::Waterfall(a) => Ok(a),
1236            _ => unreachable!(),
1237        }
1238    }
1239}
1240
1241// ---------------------------------------------------------------------------
1242// Rendering pipeline
1243// ---------------------------------------------------------------------------
1244
1245#[allow(clippy::too_many_arguments)]
1246impl Axes {
1247    /// Renders this axes into the given rectangle on the renderer.
1248    ///
1249    /// This is called by the `Figure` during its render pass. The ten-step
1250    /// pipeline is:
1251    ///
1252    /// 1. Compute data limits (autoscale or user-set).
1253    /// 2. Generate tick positions and labels.
1254    /// 3. Compute internal layout (margins for title, labels, ticks).
1255    /// 4. Draw axes background.
1256    /// 5. Draw grid lines (behind data).
1257    /// 6. Clip to the plot area and draw each artist.
1258    /// 7. Draw axis spines.
1259    /// 8. Draw ticks and tick labels.
1260    /// 9. Draw axis labels and title.
1261    /// 10. Draw legend (if enabled).
1262    pub(crate) fn render(&self, renderer: &mut impl Renderer, bounds: Rect, fig_theme: &Theme) {
1263        self.render_primary(renderer, bounds, fig_theme, false);
1264    }
1265
1266    pub(crate) fn render_primary(
1267        &self,
1268        renderer: &mut impl Renderer,
1269        bounds: Rect,
1270        fig_theme: &Theme,
1271        suppress_legend: bool,
1272    ) {
1273        let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
1274
1275        // Step 1: Compute data limits.
1276        let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
1277
1278        // Step 2: Generate ticks (use custom ticks if provided).
1279        let xticks = self.resolve_xticks(xmin, xmax);
1280        let yticks = self.resolve_yticks(ymin, ymax);
1281
1282        // Step 3: Compute layout (reserve space for labels, ticks, title).
1283        let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
1284        layout_config.has_title = self.title.is_some();
1285        layout_config.has_xlabel = self.xlabel.is_some();
1286        layout_config.has_ylabel = self.ylabel.is_some();
1287        layout_config.has_legend = self.show_legend;
1288
1289        let layout_result = layout::compute_layout(&layout_config);
1290
1291        let full_plot_area = Rect::new(
1292            bounds.x + layout_result.plot_area.x,
1293            bounds.y + layout_result.plot_area.y,
1294            layout_result.plot_area.width,
1295            layout_result.plot_area.height,
1296        );
1297
1298        // Resolve the effective colorbar: either an explicit one or auto from heatmap.
1299        let effective_colorbar = if self.colorbar.is_some() {
1300            self.colorbar.clone()
1301        } else {
1302            self.auto_colorbar_from_artists()
1303        };
1304
1305        // When a colorbar is present, shrink the plot area to make room.
1306        let (plot_area, colorbar_rect) = if effective_colorbar.is_some() {
1307            let cb_width = (full_plot_area.width * colorbar::COLORBAR_WIDTH_FRACTION).max(30.0);
1308            let shrunk = Rect::new(
1309                full_plot_area.x,
1310                full_plot_area.y,
1311                full_plot_area.width - cb_width - colorbar::COLORBAR_GAP,
1312                full_plot_area.height,
1313            );
1314            let cb_rect = Rect::new(
1315                full_plot_area.x + full_plot_area.width - cb_width,
1316                full_plot_area.y,
1317                cb_width,
1318                full_plot_area.height,
1319            );
1320            (shrunk, Some(cb_rect))
1321        } else {
1322            (full_plot_area, None)
1323        };
1324
1325        // Step 4: Draw axes background.
1326        let bg_path = Path::rect(plot_area);
1327        renderer.fill_path(
1328            &bg_path,
1329            &Paint::new(theme.axes_background),
1330            Affine::IDENTITY,
1331        );
1332
1333        // Step 5: Draw grid (behind data).
1334        if self.show_grid.unwrap_or(theme.show_grid) {
1335            self.draw_grid(
1336                renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme,
1337            );
1338        }
1339
1340        // Step 6: Clip to plot area and draw each artist.
1341        let clip_path = Path::rect(plot_area);
1342        renderer.push_clip(&clip_path, Affine::IDENTITY);
1343        for artist in &self.artists {
1344            self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
1345        }
1346        renderer.pop_clip();
1347
1348        // Step 6b: Draw minor ticks for log scales (behind spines, after data).
1349        let x_minor = if matches!(self.xscale, Scale::Log10) {
1350            ticks::generate_log_minor_ticks(xmin, xmax)
1351        } else {
1352            Vec::new()
1353        };
1354        let y_minor = if matches!(self.yscale, Scale::Log10) {
1355            ticks::generate_log_minor_ticks(ymin, ymax)
1356        } else {
1357            Vec::new()
1358        };
1359
1360        // Step 7: Draw spines.
1361        self.draw_spines(renderer, &plot_area, theme);
1362
1363        // Step 8: Draw ticks and tick labels (including minor ticks).
1364        self.draw_ticks(
1365            renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme,
1366        );
1367        if !x_minor.is_empty() || !y_minor.is_empty() {
1368            self.draw_minor_ticks(
1369                renderer, &plot_area, &x_minor, &y_minor, xmin, xmax, ymin, ymax, theme,
1370            );
1371        }
1372
1373        // Step 9: Draw axis labels and title.
1374        self.draw_labels(renderer, &plot_area, &bounds, theme);
1375
1376        // Step 9b: Draw text annotations and arrow annotations.
1377        self.draw_annotations(renderer, &plot_area, xmin, xmax, ymin, ymax, theme);
1378
1379        // Step 10: Draw legend if enabled.
1380        if self.show_legend && !suppress_legend {
1381            self.draw_legend(renderer, &plot_area, theme);
1382        }
1383
1384        // Draw colorbar if present.
1385        if let (Some(ref cb), Some(ref cb_rect)) = (&effective_colorbar, &colorbar_rect) {
1386            colorbar::draw_colorbar(renderer, cb, cb_rect, theme);
1387        }
1388    }
1389
1390    fn auto_colorbar_from_artists(&self) -> Option<Colorbar> {
1391        for artist in &self.artists {
1392            match artist {
1393                Artist::Heatmap(a) if a.show_colorbar => {
1394                    return Some(Colorbar::new(
1395                        a.cmap,
1396                        a.effective_vmin(),
1397                        a.effective_vmax(),
1398                    ));
1399                }
1400                Artist::Hexbin(a) if a.show_colorbar => {
1401                    let result =
1402                        crate::charts::hexbin::bin_hexagonal(&a.x, &a.y, a.gridsize, a.mincnt);
1403                    let vmin = result.min_count as f64;
1404                    let vmax = (result.max_count as f64).max(vmin + 1.0);
1405                    return Some(Colorbar::new(a.cmap, vmin, vmax));
1406                }
1407                _ => {}
1408            }
1409        }
1410        None
1411    }
1412
1413    /// Renders this twin axes overlaid on its parent's plot area.
1414    pub(crate) fn render_twin(
1415        &self,
1416        renderer: &mut impl Renderer,
1417        plot_area: Rect,
1418        bounds: Rect,
1419        fig_theme: &Theme,
1420    ) {
1421        let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
1422        let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
1423        let yticks = self.resolve_yticks(ymin, ymax);
1424        let xticks = self.resolve_xticks(xmin, xmax);
1425
1426        let clip_path = Path::rect(plot_area);
1427        renderer.push_clip(&clip_path, Affine::IDENTITY);
1428        for artist in &self.artists {
1429            self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
1430        }
1431        renderer.pop_clip();
1432
1433        let side = self.twin_side.unwrap_or(TwinSide::Right);
1434        match side {
1435            TwinSide::Right => {
1436                let paint = Paint::new(theme.spine_color);
1437                let stroke = Stroke::new(theme.spine_width);
1438                let mut p = Path::new();
1439                p.move_to(plot_area.right(), plot_area.y);
1440                p.line_to(plot_area.right(), plot_area.bottom());
1441                renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1442                self.draw_ticks_right(renderer, &plot_area, &yticks, ymin, ymax, theme);
1443                self.draw_ylabel_right(renderer, &plot_area, &bounds, theme);
1444            }
1445            TwinSide::Top => {
1446                let paint = Paint::new(theme.spine_color);
1447                let stroke = Stroke::new(theme.spine_width);
1448                let mut p = Path::new();
1449                p.move_to(plot_area.x, plot_area.y);
1450                p.line_to(plot_area.right(), plot_area.y);
1451                renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1452                self.draw_ticks_top(renderer, &plot_area, &xticks, xmin, xmax, theme);
1453                self.draw_xlabel_top(renderer, &plot_area, theme);
1454            }
1455        }
1456
1457        self.draw_annotations(renderer, &plot_area, xmin, xmax, ymin, ymax, theme);
1458    }
1459
1460    /// Returns the computed plot area for this axes given a bounds rectangle.
1461    pub(crate) fn compute_plot_area(&self, bounds: &Rect) -> Rect {
1462        let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
1463        layout_config.has_title = self.title.is_some();
1464        layout_config.has_xlabel = self.xlabel.is_some();
1465        layout_config.has_ylabel = self.ylabel.is_some();
1466        layout_config.has_legend = self.show_legend;
1467        let layout_result = layout::compute_layout(&layout_config);
1468        Rect::new(
1469            bounds.x + layout_result.plot_area.x,
1470            bounds.y + layout_result.plot_area.y,
1471            layout_result.plot_area.width,
1472            layout_result.plot_area.height,
1473        )
1474    }
1475
1476    /// Collects legend entries from this axes' artists.
1477    pub fn collect_legend_entries(&self) -> Vec<LegendEntry> {
1478        self.artists
1479            .iter()
1480            .filter_map(|a| {
1481                let (label, color, swatch) = match a {
1482                    Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1483                    Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1484                    Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1485                    Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1486                    Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1487                    Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1488                    Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1489                    Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1490                    Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1491                    Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1492                    Artist::Pie(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1493                    Artist::Violin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1494                    Artist::Contour(a) => (
1495                        a.label.as_deref(),
1496                        a.color,
1497                        if a.filled {
1498                            SwatchKind::Filled
1499                        } else {
1500                            SwatchKind::Line
1501                        },
1502                    ),
1503                    Artist::Polar(a) => (
1504                        a.label.as_deref(),
1505                        a.color,
1506                        if a.filled {
1507                            SwatchKind::Filled
1508                        } else {
1509                            SwatchKind::Line
1510                        },
1511                    ),
1512                    Artist::Hexbin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1513                    Artist::Waterfall(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1514                };
1515                label.map(|l| LegendEntry {
1516                    label: l.to_string(),
1517                    color,
1518                    swatch,
1519                })
1520            })
1521            .collect()
1522    }
1523
1524    // -----------------------------------------------------------------------
1525    // Step 1: Data limits
1526    // -----------------------------------------------------------------------
1527
1528    /// Computes the data limits from all artists, applying user overrides
1529    /// and padding. Returns `(xmin, xmax, ymin, ymax)`.
1530    fn compute_data_limits(&self) -> (f64, f64, f64, f64) {
1531        let mut x_lo = f64::INFINITY;
1532        let mut x_hi = f64::NEG_INFINITY;
1533        let mut y_lo = f64::INFINITY;
1534        let mut y_hi = f64::NEG_INFINITY;
1535
1536        for artist in &self.artists {
1537            match artist {
1538                Artist::Line(a) => {
1539                    if let Some((lo, hi)) = a.x.bounds() {
1540                        x_lo = x_lo.min(lo);
1541                        x_hi = x_hi.max(hi);
1542                    }
1543                    if let Some((lo, hi)) = a.y.bounds() {
1544                        y_lo = y_lo.min(lo);
1545                        y_hi = y_hi.max(hi);
1546                    }
1547                }
1548                Artist::Scatter(a) => {
1549                    if let Some((lo, hi)) = a.x.bounds() {
1550                        x_lo = x_lo.min(lo);
1551                        x_hi = x_hi.max(hi);
1552                    }
1553                    if let Some((lo, hi)) = a.y.bounds() {
1554                        y_lo = y_lo.min(lo);
1555                        y_hi = y_hi.max(hi);
1556                    }
1557                }
1558                Artist::Bar(a) => {
1559                    let n = a.categories.len() as f64;
1560                    if a.horizontal {
1561                        // x-axis is the value axis, y-axis is the category axis.
1562                        y_lo = 0.0_f64.min(y_lo);
1563                        y_hi = n.max(y_hi);
1564                        x_lo = 0.0_f64.min(x_lo);
1565                        if let Some((lo, hi)) = a.heights.bounds() {
1566                            x_lo = x_lo.min(lo.min(0.0));
1567                            x_hi = x_hi.max(hi);
1568                        }
1569                    } else {
1570                        // x-axis is the category axis, y-axis is the value axis.
1571                        x_lo = 0.0_f64.min(x_lo);
1572                        x_hi = n.max(x_hi);
1573                        y_lo = 0.0_f64.min(y_lo);
1574                        if let Some((lo, hi)) = a.heights.bounds() {
1575                            y_lo = y_lo.min(lo.min(0.0));
1576                            y_hi = y_hi.max(hi);
1577                        }
1578                    }
1579                }
1580                Artist::Histogram(a) => {
1581                    if let (Some(&first), Some(&last)) = (a.bin_edges.first(), a.bin_edges.last()) {
1582                        x_lo = x_lo.min(first);
1583                        x_hi = x_hi.max(last);
1584                    }
1585                    y_lo = 0.0_f64.min(y_lo);
1586                    let max_count = a.counts.iter().fold(0.0f64, |a, &b| a.max(b));
1587                    y_hi = y_hi.max(max_count);
1588                }
1589                Artist::FillBetween(a) => {
1590                    if let Some((lo, hi)) = a.x.bounds() {
1591                        x_lo = x_lo.min(lo);
1592                        x_hi = x_hi.max(hi);
1593                    }
1594                    if let Some((lo, hi)) = a.y1.bounds() {
1595                        y_lo = y_lo.min(lo);
1596                        y_hi = y_hi.max(hi);
1597                    }
1598                    if let Some((lo, hi)) = a.y2.bounds() {
1599                        y_lo = y_lo.min(lo);
1600                        y_hi = y_hi.max(hi);
1601                    }
1602                }
1603                Artist::Step(a) => {
1604                    if let Some((lo, hi)) = a.x.bounds() {
1605                        x_lo = x_lo.min(lo);
1606                        x_hi = x_hi.max(hi);
1607                    }
1608                    if let Some((lo, hi)) = a.y.bounds() {
1609                        y_lo = y_lo.min(lo);
1610                        y_hi = y_hi.max(hi);
1611                    }
1612                }
1613                Artist::Stem(a) => {
1614                    if let Some((lo, hi)) = a.x.bounds() {
1615                        x_lo = x_lo.min(lo);
1616                        x_hi = x_hi.max(hi);
1617                    }
1618                    if let Some((lo, hi)) = a.y.bounds() {
1619                        y_lo = y_lo.min(lo.min(a.baseline));
1620                        y_hi = y_hi.max(hi.max(a.baseline));
1621                    }
1622                }
1623                Artist::BoxPlot(a) => {
1624                    let n = a.stats.len() as f64;
1625                    x_lo = 0.0_f64.min(x_lo);
1626                    x_hi = n.max(x_hi);
1627                    for s in &a.stats {
1628                        y_lo = y_lo.min(s.whisker_low);
1629                        y_hi = y_hi.max(s.whisker_high);
1630                        for &o in &s.outliers {
1631                            y_lo = y_lo.min(o);
1632                            y_hi = y_hi.max(o);
1633                        }
1634                    }
1635                }
1636                Artist::ErrorBar(a) => {
1637                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1638                    x_lo = x_lo.min(bxlo);
1639                    x_hi = x_hi.max(bxhi);
1640                    y_lo = y_lo.min(bylo);
1641                    y_hi = y_hi.max(byhi);
1642                }
1643                Artist::Heatmap(a) => {
1644                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1645                    x_lo = x_lo.min(bxlo);
1646                    x_hi = x_hi.max(bxhi);
1647                    y_lo = y_lo.min(bylo);
1648                    y_hi = y_hi.max(byhi);
1649                }
1650                Artist::Pie(a) => {
1651                    // Pie charts define their own coordinate space and
1652                    // should not participate in normal autoscaling.
1653                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1654                    x_lo = x_lo.min(bxlo);
1655                    x_hi = x_hi.max(bxhi);
1656                    y_lo = y_lo.min(bylo);
1657                    y_hi = y_hi.max(byhi);
1658                }
1659                Artist::Violin(a) => {
1660                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1661                    x_lo = x_lo.min(bxlo);
1662                    x_hi = x_hi.max(bxhi);
1663                    y_lo = y_lo.min(bylo);
1664                    y_hi = y_hi.max(byhi);
1665                }
1666                Artist::Contour(a) => {
1667                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1668                    x_lo = x_lo.min(bxlo);
1669                    x_hi = x_hi.max(bxhi);
1670                    y_lo = y_lo.min(bylo);
1671                    y_hi = y_hi.max(byhi);
1672                }
1673                Artist::Polar(a) => {
1674                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1675                    x_lo = x_lo.min(bxlo);
1676                    x_hi = x_hi.max(bxhi);
1677                    y_lo = y_lo.min(bylo);
1678                    y_hi = y_hi.max(byhi);
1679                }
1680                Artist::Hexbin(a) => {
1681                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1682                    x_lo = x_lo.min(bxlo);
1683                    x_hi = x_hi.max(bxhi);
1684                    y_lo = y_lo.min(bylo);
1685                    y_hi = y_hi.max(byhi);
1686                }
1687                Artist::Waterfall(a) => {
1688                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1689                    x_lo = x_lo.min(bxlo);
1690                    x_hi = x_hi.max(bxhi);
1691                    y_lo = y_lo.min(bylo);
1692                    y_hi = y_hi.max(byhi);
1693                }
1694            }
1695        }
1696
1697        // Handle the case where there are no artists or no finite data.
1698        if !x_lo.is_finite() || !x_hi.is_finite() {
1699            x_lo = if self.xscale.requires_positive() {
1700                1.0
1701            } else {
1702                0.0
1703            };
1704            x_hi = if self.xscale.requires_positive() {
1705                10.0
1706            } else {
1707                1.0
1708            };
1709        }
1710        if !y_lo.is_finite() || !y_hi.is_finite() {
1711            y_lo = if self.yscale.requires_positive() {
1712                1.0
1713            } else {
1714                0.0
1715            };
1716            y_hi = if self.yscale.requires_positive() {
1717                10.0
1718            } else {
1719                1.0
1720            };
1721        }
1722
1723        // For log scales, clamp the lower bound to a positive value.
1724        if self.xscale.requires_positive() {
1725            if x_lo <= 0.0 {
1726                // Use a small fraction of x_hi, or fall back to a sensible default.
1727                x_lo = if x_hi > 0.0 { x_hi * 1e-4 } else { 1.0 };
1728            }
1729            if x_hi <= x_lo {
1730                x_hi = x_lo * 10.0;
1731            }
1732        }
1733        if self.yscale.requires_positive() {
1734            if y_lo <= 0.0 {
1735                y_lo = if y_hi > 0.0 { y_hi * 1e-4 } else { 1.0 };
1736            }
1737            if y_hi <= y_lo {
1738                y_hi = y_lo * 10.0;
1739            }
1740        }
1741
1742        // Handle degenerate ranges (all data at a single point).
1743        if (x_hi - x_lo).abs() < f64::EPSILON {
1744            x_lo -= 0.5;
1745            x_hi += 0.5;
1746        }
1747        if (y_hi - y_lo).abs() < f64::EPSILON {
1748            y_lo -= 0.5;
1749            y_hi += 0.5;
1750        }
1751
1752        // Apply padding (5% on each side).
1753        // For log scales, apply multiplicative padding instead of additive.
1754        let (x_pad_lo, x_pad_hi) = if self.xscale.requires_positive() {
1755            // Multiplicative padding: shrink lo, grow hi by a factor.
1756            let factor = 1.0 + AUTOSCALE_PAD;
1757            (x_lo / factor, x_hi * factor)
1758        } else {
1759            let pad = (x_hi - x_lo) * AUTOSCALE_PAD;
1760            (x_lo - pad, x_hi + pad)
1761        };
1762        let (y_pad_lo, y_pad_hi) = if self.yscale.requires_positive() {
1763            let factor = 1.0 + AUTOSCALE_PAD;
1764            (y_lo / factor, y_hi * factor)
1765        } else {
1766            let pad = (y_hi - y_lo) * AUTOSCALE_PAD;
1767            (y_lo - pad, y_hi + pad)
1768        };
1769        x_lo = x_pad_lo;
1770        x_hi = x_pad_hi;
1771        y_lo = y_pad_lo;
1772        y_hi = y_pad_hi;
1773
1774        // Apply user-set limits, overriding auto-scale.
1775        if let Some((lo, hi)) = self.xlim {
1776            x_lo = lo;
1777            x_hi = hi;
1778        }
1779        if let Some((lo, hi)) = self.ylim {
1780            y_lo = lo;
1781            y_hi = hi;
1782        }
1783
1784        // Apply axis inversion by swapping min/max.
1785        if self.x_inverted {
1786            std::mem::swap(&mut x_lo, &mut x_hi);
1787        }
1788        if self.y_inverted {
1789            std::mem::swap(&mut y_lo, &mut y_hi);
1790        }
1791
1792        (x_lo, x_hi, y_lo, y_hi)
1793    }
1794
1795    // -----------------------------------------------------------------------
1796    // Step 2 helpers: tick resolution
1797    // -----------------------------------------------------------------------
1798
1799    /// Resolves x-axis ticks: uses custom ticks when set, falls back to
1800    /// auto-generation.
1801    fn resolve_xticks(&self, xmin: f64, xmax: f64) -> Vec<ticks::Tick> {
1802        if let Some(ref positions) = self.custom_xticks {
1803            let labels = self.custom_xticklabels.as_ref();
1804            positions
1805                .iter()
1806                .enumerate()
1807                .map(|(i, &v)| ticks::Tick {
1808                    value: v,
1809                    label: labels
1810                        .and_then(|l| l.get(i))
1811                        .cloned()
1812                        .unwrap_or_else(|| ticks::format_tick_value(v)),
1813                })
1814                .collect()
1815        } else {
1816            let (lo, hi) = if xmin <= xmax {
1817                (xmin, xmax)
1818            } else {
1819                (xmax, xmin)
1820            };
1821            ticks::generate_ticks(lo, hi, DEFAULT_TICK_COUNT, &self.xscale)
1822        }
1823    }
1824
1825    /// Resolves y-axis ticks: uses custom ticks when set, falls back to
1826    /// auto-generation.
1827    fn resolve_yticks(&self, ymin: f64, ymax: f64) -> Vec<ticks::Tick> {
1828        if let Some(ref positions) = self.custom_yticks {
1829            let labels = self.custom_yticklabels.as_ref();
1830            positions
1831                .iter()
1832                .enumerate()
1833                .map(|(i, &v)| ticks::Tick {
1834                    value: v,
1835                    label: labels
1836                        .and_then(|l| l.get(i))
1837                        .cloned()
1838                        .unwrap_or_else(|| ticks::format_tick_value(v)),
1839                })
1840                .collect()
1841        } else {
1842            let (lo, hi) = if ymin <= ymax {
1843                (ymin, ymax)
1844            } else {
1845                (ymax, ymin)
1846            };
1847            ticks::generate_ticks(lo, hi, DEFAULT_TICK_COUNT, &self.yscale)
1848        }
1849    }
1850
1851    // -----------------------------------------------------------------------
1852    // Step 5: Grid
1853    // -----------------------------------------------------------------------
1854
1855    /// Draws major grid lines behind the data.
1856    fn draw_grid(
1857        &self,
1858        renderer: &mut impl Renderer,
1859        plot_area: &Rect,
1860        xticks: &[ticks::Tick],
1861        yticks: &[ticks::Tick],
1862        xmin: f64,
1863        xmax: f64,
1864        ymin: f64,
1865        ymax: f64,
1866        theme: &Theme,
1867    ) {
1868        // Apply grid alpha override.
1869        let grid_color = if let Some(alpha) = self.grid_alpha {
1870            theme.grid_color.with_alpha((alpha * 255.0) as u8)
1871        } else {
1872            theme.grid_color
1873        };
1874        let paint = Paint::new(grid_color);
1875
1876        // Apply grid line style override.
1877        let mut stroke = Stroke::new(theme.grid_width);
1878        if let Some(style) = self.grid_style {
1879            stroke = match style {
1880                LineStyle::Solid => stroke,
1881                LineStyle::Dashed => stroke.with_dash(DashPattern {
1882                    dashes: vec![6.0, 4.0],
1883                    offset: 0.0,
1884                }),
1885                LineStyle::Dotted => stroke.with_dash(DashPattern {
1886                    dashes: vec![2.0, 2.0],
1887                    offset: 0.0,
1888                }),
1889                LineStyle::DashDot => stroke.with_dash(DashPattern {
1890                    dashes: vec![6.0, 3.0, 2.0, 3.0],
1891                    offset: 0.0,
1892                }),
1893            };
1894        }
1895
1896        let draw_x = matches!(self.grid_axis, GridAxis::X | GridAxis::Both);
1897        let draw_y = matches!(self.grid_axis, GridAxis::Y | GridAxis::Both);
1898
1899        // Vertical grid lines at each x-tick.
1900        if draw_x {
1901            for tick in xticks {
1902                let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
1903                let mut path = Path::new();
1904                path.move_to(pt.x, plot_area.y);
1905                path.line_to(pt.x, plot_area.bottom());
1906                renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1907            }
1908        }
1909
1910        // Horizontal grid lines at each y-tick.
1911        if draw_y {
1912            for tick in yticks {
1913                let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
1914                let mut path = Path::new();
1915                path.move_to(plot_area.x, pt.y);
1916                path.line_to(plot_area.right(), pt.y);
1917                renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1918            }
1919        }
1920    }
1921
1922    // -----------------------------------------------------------------------
1923    // Step 6: Artist drawing
1924    // -----------------------------------------------------------------------
1925
1926    /// Dispatches drawing to the appropriate artist-type-specific method.
1927    fn draw_artist(
1928        &self,
1929        renderer: &mut impl Renderer,
1930        artist: &Artist,
1931        plot_area: &Rect,
1932        xmin: f64,
1933        xmax: f64,
1934        ymin: f64,
1935        ymax: f64,
1936        theme: &Theme,
1937    ) {
1938        match artist {
1939            Artist::Line(a) => self.draw_line(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1940            Artist::Scatter(a) => {
1941                self.draw_scatter(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme)
1942            }
1943            Artist::Bar(a) => self.draw_bar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1944            Artist::Histogram(a) => self.draw_hist(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1945            Artist::FillBetween(a) => {
1946                self.draw_fill_between(renderer, a, plot_area, xmin, xmax, ymin, ymax)
1947            }
1948            Artist::Step(a) => self.draw_step(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1949            Artist::Stem(a) => self.draw_stem(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1950            Artist::BoxPlot(a) => self.draw_boxplot(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1951            Artist::ErrorBar(a) => {
1952                self.draw_errorbar(renderer, a, plot_area, xmin, xmax, ymin, ymax)
1953            }
1954            Artist::Heatmap(a) => self.draw_heatmap(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1955            Artist::Pie(a) => self.draw_pie(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1956            Artist::Violin(a) => {
1957                self.draw_violin(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme)
1958            }
1959            Artist::Contour(a) => self.draw_contour(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1960            Artist::Polar(a) => {
1961                self.draw_polar(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme)
1962            }
1963            Artist::Hexbin(a) => self.draw_hexbin(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1964            Artist::Waterfall(a) => {
1965                self.draw_waterfall(renderer, a, plot_area, xmin, xmax, ymin, ymax)
1966            }
1967        }
1968    }
1969
1970    /// Draws a line chart: builds a polyline from data points and strokes it.
1971    ///
1972    /// When the artist has decimation enabled and the data exceeds the
1973    /// threshold, the point set is downsampled before path construction.
1974    fn draw_line(
1975        &self,
1976        renderer: &mut impl Renderer,
1977        artist: &LineArtist,
1978        plot_area: &Rect,
1979        xmin: f64,
1980        xmax: f64,
1981        ymin: f64,
1982        ymax: f64,
1983    ) {
1984        if artist.x.is_empty() {
1985            return;
1986        }
1987
1988        // Compute the set of indices to draw (possibly decimated). The mode
1989        // (auto / off / explicit) is resolved by a single pure function so the
1990        // result is deterministic for a given input.
1991        let indices: Vec<usize> = artist
1992            .decimate
1993            .resolve_indices(&artist.x.data, &artist.y.data);
1994
1995        if indices.is_empty() {
1996            return;
1997        }
1998
1999        let mut path = Path::new();
2000        let first = self.data_to_pixel(
2001            artist.x.data[indices[0]],
2002            artist.y.data[indices[0]],
2003            plot_area,
2004            xmin,
2005            xmax,
2006            ymin,
2007            ymax,
2008        );
2009        path.move_to(first.x, first.y);
2010
2011        for &i in &indices[1..] {
2012            let pt = self.data_to_pixel(
2013                artist.x.data[i],
2014                artist.y.data[i],
2015                plot_area,
2016                xmin,
2017                xmax,
2018                ymin,
2019                ymax,
2020            );
2021            path.line_to(pt.x, pt.y);
2022        }
2023
2024        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2025        let paint = Paint::new(color);
2026        let mut stroke = Stroke::new(artist.width);
2027
2028        // Apply line style dash pattern.
2029        match artist.style {
2030            crate::theme::LineStyle::Solid => {}
2031            crate::theme::LineStyle::Dashed => {
2032                stroke = stroke.with_dash(DashPattern {
2033                    dashes: vec![6.0, 4.0],
2034                    offset: 0.0,
2035                });
2036            }
2037            crate::theme::LineStyle::Dotted => {
2038                stroke = stroke.with_dash(DashPattern {
2039                    dashes: vec![2.0, 2.0],
2040                    offset: 0.0,
2041                });
2042            }
2043            crate::theme::LineStyle::DashDot => {
2044                stroke = stroke.with_dash(DashPattern {
2045                    dashes: vec![6.0, 3.0, 2.0, 3.0],
2046                    offset: 0.0,
2047                });
2048            }
2049        }
2050
2051        renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
2052    }
2053
2054    /// Draws a scatter plot: fills a marker shape at each data point.
2055    fn draw_scatter(
2056        &self,
2057        renderer: &mut impl Renderer,
2058        artist: &ScatterArtist,
2059        plot_area: &Rect,
2060        xmin: f64,
2061        xmax: f64,
2062        ymin: f64,
2063        ymax: f64,
2064        theme: &Theme,
2065    ) {
2066        let alpha_byte = (artist.alpha * 255.0) as u8;
2067
2068        // Pre-compute per-point colors from c/cmap if set.
2069        let cmap_colors: Option<Vec<Color>> = match (&artist.c, &artist.cmap) {
2070            (Some(c_vals), Some(cmap)) if !c_vals.is_empty() => Some(cmap.map_values(c_vals)),
2071            _ => None,
2072        };
2073
2074        let default_color = artist.color.with_alpha(alpha_byte);
2075        let default_paint = Paint::new(default_color);
2076        let radius = artist.size / 2.0;
2077
2078        // Resolve the set of point indices to draw (possibly decimated). The
2079        // mode (auto / off / explicit) is resolved by a single pure function,
2080        // and per-point styling is indexed by the original index so colors stay
2081        // synchronized with their points after decimation.
2082        let indices = artist
2083            .decimate
2084            .resolve_indices(&artist.x.data, &artist.y.data);
2085
2086        for &i in &indices {
2087            let pt = self.data_to_pixel(
2088                artist.x.data[i],
2089                artist.y.data[i],
2090                plot_area,
2091                xmin,
2092                xmax,
2093                ymin,
2094                ymax,
2095            );
2096
2097            // Resolve the paint for this point: c/cmap > colors > color.
2098            let paint = if let Some(ref cc) = cmap_colors {
2099                Paint::new(cc[i].with_alpha(alpha_byte))
2100            } else if let Some(ref cs) = artist.colors {
2101                Paint::new(cs[i].with_alpha(alpha_byte))
2102            } else {
2103                default_paint
2104            };
2105
2106            let marker_path = match artist.marker {
2107                Marker::Circle | Marker::Point => Path::circle(pt, radius),
2108                Marker::Square => Path::rect(Rect::new(
2109                    pt.x - radius,
2110                    pt.y - radius,
2111                    radius * 2.0,
2112                    radius * 2.0,
2113                )),
2114                Marker::Diamond => {
2115                    let mut p = Path::new();
2116                    p.move_to(pt.x, pt.y - radius);
2117                    p.line_to(pt.x + radius, pt.y);
2118                    p.line_to(pt.x, pt.y + radius);
2119                    p.line_to(pt.x - radius, pt.y);
2120                    p.close();
2121                    p
2122                }
2123                Marker::Triangle => {
2124                    let mut p = Path::new();
2125                    let h = radius * 1.1547; // 2/sqrt(3) for equilateral
2126                    p.move_to(pt.x, pt.y - radius);
2127                    p.line_to(pt.x + h * 0.5, pt.y + radius * 0.5);
2128                    p.line_to(pt.x - h * 0.5, pt.y + radius * 0.5);
2129                    p.close();
2130                    p
2131                }
2132                Marker::Plus => {
2133                    // Stroked marker: draw two perpendicular lines.
2134                    let mut p = Path::new();
2135                    p.move_to(pt.x - radius, pt.y);
2136                    p.line_to(pt.x + radius, pt.y);
2137                    p.move_to(pt.x, pt.y - radius);
2138                    p.line_to(pt.x, pt.y + radius);
2139                    let stroke = Stroke::new(theme.line_width.max(1.0));
2140                    renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2141                    continue;
2142                }
2143                Marker::Cross => {
2144                    let mut p = Path::new();
2145                    let d = radius * 0.707; // radius / sqrt(2)
2146                    p.move_to(pt.x - d, pt.y - d);
2147                    p.line_to(pt.x + d, pt.y + d);
2148                    p.move_to(pt.x + d, pt.y - d);
2149                    p.line_to(pt.x - d, pt.y + d);
2150                    let stroke = Stroke::new(theme.line_width.max(1.0));
2151                    renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2152                    continue;
2153                }
2154                Marker::Star => {
2155                    // 5-pointed star.
2156                    let mut p = Path::new();
2157                    let inner = radius * 0.382;
2158                    for j in 0..10 {
2159                        let angle =
2160                            std::f64::consts::FRAC_PI_2 + j as f64 * std::f64::consts::PI / 5.0;
2161                        let r = if j % 2 == 0 { radius } else { inner };
2162                        let sx = pt.x + r * angle.cos();
2163                        let sy = pt.y - r * angle.sin();
2164                        if j == 0 {
2165                            p.move_to(sx, sy);
2166                        } else {
2167                            p.line_to(sx, sy);
2168                        }
2169                    }
2170                    p.close();
2171                    p
2172                }
2173            };
2174
2175            renderer.fill_path(&marker_path, &paint, Affine::IDENTITY);
2176        }
2177    }
2178
2179    /// Draws a bar chart: fills rectangles for each category.
2180    fn draw_bar(
2181        &self,
2182        renderer: &mut impl Renderer,
2183        artist: &BarArtist,
2184        plot_area: &Rect,
2185        xmin: f64,
2186        xmax: f64,
2187        ymin: f64,
2188        ymax: f64,
2189    ) {
2190        let n = artist.categories.len();
2191        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2192        let paint = Paint::new(color);
2193
2194        if artist.horizontal {
2195            // Horizontal bars: categories on y-axis, values on x-axis.
2196            let cat_range = ymax - ymin;
2197            let cat_step = cat_range / n as f64;
2198            let bar_half = cat_step * artist.bar_width * 0.5;
2199
2200            for i in 0..n {
2201                let base_center = ymin + (i as f64 + 0.5) * cat_step;
2202                let cat_center = if let Some(ref off) = artist.offset {
2203                    base_center + if i < off.len() { off[i] } else { 0.0 }
2204                } else {
2205                    base_center
2206                };
2207                let value = artist.heights.data[i];
2208                let base = if let Some(ref bot) = artist.bottom {
2209                    if i < bot.len() {
2210                        bot[i]
2211                    } else {
2212                        0.0
2213                    }
2214                } else {
2215                    0.0
2216                };
2217
2218                let left_val = base.min(base + value);
2219                let right_val = base.max(base + value);
2220
2221                let p_left = self.data_to_pixel(
2222                    left_val,
2223                    cat_center - bar_half,
2224                    plot_area,
2225                    xmin,
2226                    xmax,
2227                    ymin,
2228                    ymax,
2229                );
2230                let p_right = self.data_to_pixel(
2231                    right_val,
2232                    cat_center + bar_half,
2233                    plot_area,
2234                    xmin,
2235                    xmax,
2236                    ymin,
2237                    ymax,
2238                );
2239
2240                let rect = Rect::from_points(p_left, p_right);
2241                let bar_path = Path::rect(rect);
2242                renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
2243            }
2244        } else {
2245            // Vertical bars: categories on x-axis, values on y-axis.
2246            let cat_range = xmax - xmin;
2247            let cat_step = cat_range / n as f64;
2248            let bar_half = cat_step * artist.bar_width * 0.5;
2249
2250            for i in 0..n {
2251                let base_center = xmin + (i as f64 + 0.5) * cat_step;
2252                let cat_center = if let Some(ref off) = artist.offset {
2253                    base_center + if i < off.len() { off[i] } else { 0.0 }
2254                } else {
2255                    base_center
2256                };
2257                let value = artist.heights.data[i];
2258                let base = if let Some(ref bot) = artist.bottom {
2259                    if i < bot.len() {
2260                        bot[i]
2261                    } else {
2262                        0.0
2263                    }
2264                } else {
2265                    0.0
2266                };
2267
2268                let bottom_val = base.min(base + value);
2269                let top_val = base.max(base + value);
2270
2271                let p_bl = self.data_to_pixel(
2272                    cat_center - bar_half,
2273                    bottom_val,
2274                    plot_area,
2275                    xmin,
2276                    xmax,
2277                    ymin,
2278                    ymax,
2279                );
2280                let p_tr = self.data_to_pixel(
2281                    cat_center + bar_half,
2282                    top_val,
2283                    plot_area,
2284                    xmin,
2285                    xmax,
2286                    ymin,
2287                    ymax,
2288                );
2289
2290                let rect = Rect::from_points(p_bl, p_tr);
2291                let bar_path = Path::rect(rect);
2292                renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
2293            }
2294        }
2295    }
2296
2297    /// Draws a histogram: fills rectangles from bin edges and counts.
2298    fn draw_hist(
2299        &self,
2300        renderer: &mut impl Renderer,
2301        artist: &HistArtist,
2302        plot_area: &Rect,
2303        xmin: f64,
2304        xmax: f64,
2305        ymin: f64,
2306        ymax: f64,
2307    ) {
2308        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2309        let paint = Paint::new(color);
2310        let stroke_paint = Paint::new(Color::WHITE);
2311        let stroke = Stroke::new(0.5);
2312
2313        for i in 0..artist.counts.len() {
2314            let left = artist.bin_edges[i];
2315            let right = artist.bin_edges[i + 1];
2316            let height = artist.counts[i];
2317
2318            if height <= 0.0 {
2319                continue;
2320            }
2321
2322            let p_bl = self.data_to_pixel(left, 0.0, plot_area, xmin, xmax, ymin, ymax);
2323            let p_tr = self.data_to_pixel(right, height, plot_area, xmin, xmax, ymin, ymax);
2324
2325            let rect = Rect::from_points(p_bl, p_tr);
2326            let bar_path = Path::rect(rect);
2327            renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
2328            // Thin white outline between adjacent bins for visual separation.
2329            renderer.stroke_path(&bar_path, &stroke_paint, &stroke, Affine::IDENTITY);
2330        }
2331    }
2332
2333    /// Draws a fill-between region: builds a closed path from y1 forward and
2334    /// y2 backward, then fills it.
2335    fn draw_fill_between(
2336        &self,
2337        renderer: &mut impl Renderer,
2338        artist: &FillBetweenArtist,
2339        plot_area: &Rect,
2340        xmin: f64,
2341        xmax: f64,
2342        ymin: f64,
2343        ymax: f64,
2344    ) {
2345        if artist.x.is_empty() {
2346            return;
2347        }
2348
2349        let n = artist.x.len();
2350        let mut path = Path::new();
2351
2352        // Forward pass along y1.
2353        let first = self.data_to_pixel(
2354            artist.x.data[0],
2355            artist.y1.data[0],
2356            plot_area,
2357            xmin,
2358            xmax,
2359            ymin,
2360            ymax,
2361        );
2362        path.move_to(first.x, first.y);
2363        for i in 1..n {
2364            let pt = self.data_to_pixel(
2365                artist.x.data[i],
2366                artist.y1.data[i],
2367                plot_area,
2368                xmin,
2369                xmax,
2370                ymin,
2371                ymax,
2372            );
2373            path.line_to(pt.x, pt.y);
2374        }
2375
2376        // Backward pass along y2 (in reverse order).
2377        for i in (0..n).rev() {
2378            let pt = self.data_to_pixel(
2379                artist.x.data[i],
2380                artist.y2.data[i],
2381                plot_area,
2382                xmin,
2383                xmax,
2384                ymin,
2385                ymax,
2386            );
2387            path.line_to(pt.x, pt.y);
2388        }
2389        path.close();
2390
2391        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2392        let paint = Paint::new(color);
2393        renderer.fill_path(&path, &paint, Affine::IDENTITY);
2394    }
2395
2396    // -----------------------------------------------------------------------
2397    // Step 7: Spines
2398    // -----------------------------------------------------------------------
2399
2400    /// Draws the axis spines (border lines around the plot area).
2401    fn draw_spines(&self, renderer: &mut impl Renderer, plot_area: &Rect, theme: &Theme) {
2402        let paint = Paint::new(theme.spine_color);
2403        let stroke = Stroke::new(theme.spine_width);
2404
2405        // Bottom spine.
2406        if theme.show_bottom_spine {
2407            let mut p = Path::new();
2408            p.move_to(plot_area.x, plot_area.bottom());
2409            p.line_to(plot_area.right(), plot_area.bottom());
2410            renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2411        }
2412        // Left spine.
2413        if theme.show_left_spine {
2414            let mut p = Path::new();
2415            p.move_to(plot_area.x, plot_area.y);
2416            p.line_to(plot_area.x, plot_area.bottom());
2417            renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2418        }
2419        // Top spine.
2420        if theme.show_top_spine {
2421            let mut p = Path::new();
2422            p.move_to(plot_area.x, plot_area.y);
2423            p.line_to(plot_area.right(), plot_area.y);
2424            renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2425        }
2426        // Right spine.
2427        if theme.show_right_spine {
2428            let mut p = Path::new();
2429            p.move_to(plot_area.right(), plot_area.y);
2430            p.line_to(plot_area.right(), plot_area.bottom());
2431            renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2432        }
2433    }
2434
2435    // -----------------------------------------------------------------------
2436    // Step 8: Ticks and tick labels
2437    // -----------------------------------------------------------------------
2438
2439    /// Draws tick marks and their labels along both axes.
2440    fn draw_ticks(
2441        &self,
2442        renderer: &mut impl Renderer,
2443        plot_area: &Rect,
2444        xticks: &[ticks::Tick],
2445        yticks: &[ticks::Tick],
2446        xmin: f64,
2447        xmax: f64,
2448        ymin: f64,
2449        ymax: f64,
2450        theme: &Theme,
2451    ) {
2452        let tick_paint = Paint::new(theme.tick_color);
2453        let tick_stroke = Stroke::new(1.0);
2454        let tick_len = theme.tick_length;
2455
2456        let x_label_style = TextStyle {
2457            size: theme.tick_label_size,
2458            color: theme.text_color,
2459            weight: FontWeight::Normal,
2460            family: theme.font_family.clone(),
2461            halign: if self.xtick_rotation.abs() > 1.0 {
2462                HAlign::Right
2463            } else {
2464                HAlign::Center
2465            },
2466            valign: VAlign::Top,
2467        };
2468
2469        // Tick direction offset: outward goes away from plot, inward goes in.
2470        let outward = matches!(theme.tick_direction, TickDirection::Outward);
2471
2472        // Pre-compute x-tick rotation transform builder.
2473        let x_rot_rad = -self.xtick_rotation.to_radians();
2474
2475        // --- X-axis ticks (bottom) ---
2476        for tick in xticks {
2477            let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
2478            // Clamp to plot area x-bounds.
2479            if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2480                continue;
2481            }
2482            let x = pt.x;
2483            let y_base = plot_area.bottom();
2484
2485            // Draw tick mark.
2486            let (y_start, y_end) = if outward {
2487                (y_base, y_base + tick_len)
2488            } else {
2489                (y_base - tick_len, y_base)
2490            };
2491            let mut tp = Path::new();
2492            tp.move_to(x, y_start);
2493            tp.line_to(x, y_end);
2494            renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2495
2496            // Draw tick label with optional rotation.
2497            let label_y = if outward {
2498                y_base + tick_len + 2.0
2499            } else {
2500                y_base + 2.0
2501            };
2502            let label_pos = Point::new(x, label_y);
2503            let transform = if self.xtick_rotation.abs() > 0.01 {
2504                let rotate = Affine::rotate(x_rot_rad);
2505                let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2506                let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2507                from_origin * rotate * to_origin
2508            } else {
2509                Affine::IDENTITY
2510            };
2511            renderer.draw_text(&tick.label, label_pos, &x_label_style, transform);
2512        }
2513
2514        // --- Y-axis ticks (left) ---
2515        let y_label_style = TextStyle {
2516            halign: HAlign::Right,
2517            valign: VAlign::Middle,
2518            ..x_label_style.clone()
2519        };
2520
2521        // Pre-compute y-tick rotation transform builder.
2522        let y_rot_rad = -self.ytick_rotation.to_radians();
2523
2524        for tick in yticks {
2525            let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
2526            // Clamp to plot area y-bounds.
2527            if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2528                continue;
2529            }
2530            let y = pt.y;
2531            let x_base = plot_area.x;
2532
2533            // Draw tick mark.
2534            let (x_start, x_end) = if outward {
2535                (x_base - tick_len, x_base)
2536            } else {
2537                (x_base, x_base + tick_len)
2538            };
2539            let mut tp = Path::new();
2540            tp.move_to(x_start, y);
2541            tp.line_to(x_end, y);
2542            renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2543
2544            // Draw tick label with optional rotation.
2545            let label_x = if outward {
2546                x_base - tick_len - 3.0
2547            } else {
2548                x_base - 3.0
2549            };
2550            let label_pos = Point::new(label_x, y);
2551            let transform = if self.ytick_rotation.abs() > 0.01 {
2552                let rotate = Affine::rotate(y_rot_rad);
2553                let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2554                let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2555                from_origin * rotate * to_origin
2556            } else {
2557                Affine::IDENTITY
2558            };
2559            renderer.draw_text(&tick.label, label_pos, &y_label_style, transform);
2560        }
2561    }
2562
2563    /// Draws minor tick marks (without labels) along axes.
2564    ///
2565    /// Minor ticks are drawn shorter than major ticks and have no text labels.
2566    /// They are used primarily on log-scale axes to indicate sub-decade positions
2567    /// (2, 3, 4, 5, 6, 7, 8, 9 between each power of 10).
2568    fn draw_minor_ticks(
2569        &self,
2570        renderer: &mut impl Renderer,
2571        plot_area: &Rect,
2572        x_minor: &[f64],
2573        y_minor: &[f64],
2574        xmin: f64,
2575        xmax: f64,
2576        ymin: f64,
2577        ymax: f64,
2578        theme: &Theme,
2579    ) {
2580        let tick_paint = Paint::new(theme.tick_color);
2581        let tick_stroke = Stroke::new(0.5);
2582        // Minor ticks are half the length of major ticks.
2583        let tick_len = theme.tick_length * 0.5;
2584        let outward = matches!(theme.tick_direction, TickDirection::Outward);
2585
2586        // --- X-axis minor ticks (bottom) ---
2587        for &val in x_minor {
2588            let pt = self.data_to_pixel(val, ymin, plot_area, xmin, xmax, ymin, ymax);
2589            if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2590                continue;
2591            }
2592            let x = pt.x;
2593            let y_base = plot_area.bottom();
2594            let (y_start, y_end) = if outward {
2595                (y_base, y_base + tick_len)
2596            } else {
2597                (y_base - tick_len, y_base)
2598            };
2599            let mut tp = Path::new();
2600            tp.move_to(x, y_start);
2601            tp.line_to(x, y_end);
2602            renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2603        }
2604
2605        // --- Y-axis minor ticks (left) ---
2606        for &val in y_minor {
2607            let pt = self.data_to_pixel(xmin, val, plot_area, xmin, xmax, ymin, ymax);
2608            if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2609                continue;
2610            }
2611            let y = pt.y;
2612            let x_base = plot_area.x;
2613            let (x_start, x_end) = if outward {
2614                (x_base - tick_len, x_base)
2615            } else {
2616                (x_base, x_base + tick_len)
2617            };
2618            let mut tp = Path::new();
2619            tp.move_to(x_start, y);
2620            tp.line_to(x_end, y);
2621            renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2622        }
2623    }
2624
2625    /// Draws y-axis tick marks and labels on the right side.
2626    fn draw_ticks_right(
2627        &self,
2628        renderer: &mut impl Renderer,
2629        plot_area: &Rect,
2630        yticks: &[ticks::Tick],
2631        ymin: f64,
2632        ymax: f64,
2633        theme: &Theme,
2634    ) {
2635        let tick_paint = Paint::new(theme.tick_color);
2636        let tick_stroke = Stroke::new(1.0);
2637        let tick_len = theme.tick_length;
2638        let outward = matches!(theme.tick_direction, TickDirection::Outward);
2639
2640        let y_label_style = TextStyle {
2641            size: theme.tick_label_size,
2642            color: theme.text_color,
2643            weight: FontWeight::Normal,
2644            family: theme.font_family.clone(),
2645            halign: HAlign::Left,
2646            valign: VAlign::Middle,
2647        };
2648
2649        let y_rot_rad = -self.ytick_rotation.to_radians();
2650
2651        for tick in yticks {
2652            let pt = self.data_to_pixel(0.0, tick.value, plot_area, 0.0, 1.0, ymin, ymax);
2653            if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2654                continue;
2655            }
2656            let y = pt.y;
2657            let x_base = plot_area.right();
2658            let (x_start, x_end) = if outward {
2659                (x_base, x_base + tick_len)
2660            } else {
2661                (x_base - tick_len, x_base)
2662            };
2663            let mut tp = Path::new();
2664            tp.move_to(x_start, y);
2665            tp.line_to(x_end, y);
2666            renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2667
2668            let label_x = if outward {
2669                x_base + tick_len + 3.0
2670            } else {
2671                x_base + 3.0
2672            };
2673            let label_pos = Point::new(label_x, y);
2674            let transform = if self.ytick_rotation.abs() > 0.01 {
2675                let rotate = Affine::rotate(y_rot_rad);
2676                let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2677                let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2678                from_origin * rotate * to_origin
2679            } else {
2680                Affine::IDENTITY
2681            };
2682            renderer.draw_text(&tick.label, label_pos, &y_label_style, transform);
2683        }
2684    }
2685
2686    /// Draws the y-axis label on the right side, rotated 90 degrees clockwise.
2687    fn draw_ylabel_right(
2688        &self,
2689        renderer: &mut impl Renderer,
2690        plot_area: &Rect,
2691        bounds: &Rect,
2692        theme: &Theme,
2693    ) {
2694        if let Some(ylabel) = &self.ylabel {
2695            let style = TextStyle {
2696                size: theme.axis_label_size,
2697                color: theme.text_color,
2698                weight: FontWeight::Normal,
2699                family: theme.font_family.clone(),
2700                halign: HAlign::Center,
2701                valign: VAlign::Top,
2702            };
2703            let x = bounds.right() - 4.0;
2704            let y = plot_area.y + plot_area.height / 2.0;
2705            let rotate = Affine::rotate(std::f64::consts::FRAC_PI_2);
2706            let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
2707            let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
2708            let transform = translate_to * rotate * translate_back;
2709            renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
2710        }
2711    }
2712
2713    /// Draws x-axis tick marks and labels on the top of the plot area.
2714    fn draw_ticks_top(
2715        &self,
2716        renderer: &mut impl Renderer,
2717        plot_area: &Rect,
2718        xticks: &[ticks::Tick],
2719        xmin: f64,
2720        xmax: f64,
2721        theme: &Theme,
2722    ) {
2723        let tick_paint = Paint::new(theme.tick_color);
2724        let tick_stroke = Stroke::new(1.0);
2725        let tick_len = theme.tick_length;
2726        let outward = matches!(theme.tick_direction, TickDirection::Outward);
2727
2728        let x_label_style = TextStyle {
2729            size: theme.tick_label_size,
2730            color: theme.text_color,
2731            weight: FontWeight::Normal,
2732            family: theme.font_family.clone(),
2733            halign: if self.xtick_rotation.abs() > 1.0 {
2734                HAlign::Left
2735            } else {
2736                HAlign::Center
2737            },
2738            valign: VAlign::Bottom,
2739        };
2740
2741        let x_rot_rad = -self.xtick_rotation.to_radians();
2742
2743        for tick in xticks {
2744            let pt = self.data_to_pixel(tick.value, 0.0, plot_area, xmin, xmax, 0.0, 1.0);
2745            if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2746                continue;
2747            }
2748            let x = pt.x;
2749            let y_base = plot_area.y;
2750            let (y_start, y_end) = if outward {
2751                (y_base - tick_len, y_base)
2752            } else {
2753                (y_base, y_base + tick_len)
2754            };
2755            let mut tp = Path::new();
2756            tp.move_to(x, y_start);
2757            tp.line_to(x, y_end);
2758            renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2759
2760            let label_y = if outward {
2761                y_base - tick_len - 2.0
2762            } else {
2763                y_base - 2.0
2764            };
2765            let label_pos = Point::new(x, label_y);
2766            let transform = if self.xtick_rotation.abs() > 0.01 {
2767                let rotate = Affine::rotate(x_rot_rad);
2768                let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2769                let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2770                from_origin * rotate * to_origin
2771            } else {
2772                Affine::IDENTITY
2773            };
2774            renderer.draw_text(&tick.label, label_pos, &x_label_style, transform);
2775        }
2776    }
2777
2778    /// Draws the x-axis label on the top side.
2779    fn draw_xlabel_top(&self, renderer: &mut impl Renderer, plot_area: &Rect, theme: &Theme) {
2780        if let Some(xlabel) = &self.xlabel {
2781            let style = TextStyle {
2782                size: theme.axis_label_size,
2783                color: theme.text_color,
2784                weight: FontWeight::Normal,
2785                family: theme.font_family.clone(),
2786                halign: HAlign::Center,
2787                valign: VAlign::Bottom,
2788            };
2789            let x = plot_area.x + plot_area.width / 2.0;
2790            let y = plot_area.y - theme.tick_length - theme.tick_label_size - 8.0;
2791            renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
2792        }
2793    }
2794
2795    // -----------------------------------------------------------------------
2796    // Step 9: Labels and title
2797    // -----------------------------------------------------------------------
2798
2799    /// Draws the x-axis label, y-axis label, and title.
2800    fn draw_labels(
2801        &self,
2802        renderer: &mut impl Renderer,
2803        plot_area: &Rect,
2804        bounds: &Rect,
2805        theme: &Theme,
2806    ) {
2807        // Title (centered above plot area).
2808        if let Some(title) = &self.title {
2809            let style = TextStyle {
2810                size: theme.title_size,
2811                color: theme.text_color,
2812                weight: theme.title_weight,
2813                family: theme.font_family.clone(),
2814                halign: HAlign::Center,
2815                valign: VAlign::Bottom,
2816            };
2817            let x = plot_area.x + plot_area.width / 2.0;
2818            let y = plot_area.y - 10.0;
2819            renderer.draw_text(title, Point::new(x, y), &style, Affine::IDENTITY);
2820        }
2821
2822        // X-axis label (centered below tick labels).
2823        if let Some(xlabel) = &self.xlabel {
2824            let style = TextStyle {
2825                size: theme.axis_label_size,
2826                color: theme.text_color,
2827                weight: FontWeight::Normal,
2828                family: theme.font_family.clone(),
2829                halign: HAlign::Center,
2830                valign: VAlign::Top,
2831            };
2832            let x = plot_area.x + plot_area.width / 2.0;
2833            // Position below the tick labels. Approximate tick label height.
2834            let y = plot_area.bottom() + theme.tick_length + theme.tick_label_size + 8.0;
2835            renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
2836        }
2837
2838        // Y-axis label (centered to the left of tick labels, rotated 90 degrees).
2839        if let Some(ylabel) = &self.ylabel {
2840            let style = TextStyle {
2841                size: theme.axis_label_size,
2842                color: theme.text_color,
2843                weight: FontWeight::Normal,
2844                family: theme.font_family.clone(),
2845                halign: HAlign::Center,
2846                valign: VAlign::Bottom,
2847            };
2848            let x = bounds.x + 4.0;
2849            let y = plot_area.y + plot_area.height / 2.0;
2850            // Rotate -90 degrees around the label position for vertical text.
2851            let rotate = Affine::rotate(-std::f64::consts::FRAC_PI_2);
2852            let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
2853            let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
2854            let transform = translate_to * rotate * translate_back;
2855            renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
2856        }
2857    }
2858
2859    // -----------------------------------------------------------------------
2860    // Step 10: Legend
2861    // -----------------------------------------------------------------------
2862
2863    /// Draws a box-and-whisker plot: box from Q1 to Q3, median line, whiskers,
2864    /// caps, and optional outlier dots.
2865    fn draw_boxplot(
2866        &self,
2867        renderer: &mut impl Renderer,
2868        artist: &BoxPlotArtist,
2869        plot_area: &Rect,
2870        xmin: f64,
2871        xmax: f64,
2872        ymin: f64,
2873        ymax: f64,
2874    ) {
2875        let n = artist.stats.len();
2876        if n == 0 {
2877            return;
2878        }
2879
2880        let fill_color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2881        let stroke_color = Color::BLACK.with_alpha((artist.alpha * 255.0) as u8);
2882        let paint = Paint::new(stroke_color);
2883        let thin = Stroke::new(1.0);
2884        let thick = Stroke::new(2.0);
2885        let hair = Stroke::new(0.5);
2886
2887        for (i, stats) in artist.stats.iter().enumerate() {
2888            let cx = i as f64 + 0.5;
2889            let half = artist.box_width / 2.0;
2890            let left = cx - half;
2891            let right = cx + half;
2892
2893            // --- Box (Q1 to Q3) ---
2894            let tl = self.data_to_pixel(left, stats.q3, plot_area, xmin, xmax, ymin, ymax);
2895            let br = self.data_to_pixel(right, stats.q1, plot_area, xmin, xmax, ymin, ymax);
2896            let box_rect_path = {
2897                let mut p = Path::new();
2898                p.move_to(tl.x, tl.y);
2899                p.line_to(br.x, tl.y);
2900                p.line_to(br.x, br.y);
2901                p.line_to(tl.x, br.y);
2902                p.close();
2903                p
2904            };
2905            renderer.fill_path(&box_rect_path, &Paint::new(fill_color), Affine::IDENTITY);
2906            renderer.stroke_path(&box_rect_path, &paint, &thin, Affine::IDENTITY);
2907
2908            // --- Median line ---
2909            let ml = self.data_to_pixel(left, stats.median, plot_area, xmin, xmax, ymin, ymax);
2910            let mr = self.data_to_pixel(right, stats.median, plot_area, xmin, xmax, ymin, ymax);
2911            let mut median_path = Path::new();
2912            median_path.move_to(ml.x, ml.y);
2913            median_path.line_to(mr.x, mr.y);
2914            renderer.stroke_path(&median_path, &paint, &thick, Affine::IDENTITY);
2915
2916            // --- Lower whisker ---
2917            let wl_bottom =
2918                self.data_to_pixel(cx, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
2919            let wl_top = self.data_to_pixel(cx, stats.q1, plot_area, xmin, xmax, ymin, ymax);
2920            let mut wl_path = Path::new();
2921            wl_path.move_to(wl_top.x, wl_top.y);
2922            wl_path.line_to(wl_bottom.x, wl_bottom.y);
2923            renderer.stroke_path(&wl_path, &paint, &thin, Affine::IDENTITY);
2924
2925            // Lower cap
2926            let cap_left = self.data_to_pixel(
2927                cx - half * 0.5,
2928                stats.whisker_low,
2929                plot_area,
2930                xmin,
2931                xmax,
2932                ymin,
2933                ymax,
2934            );
2935            let cap_right = self.data_to_pixel(
2936                cx + half * 0.5,
2937                stats.whisker_low,
2938                plot_area,
2939                xmin,
2940                xmax,
2941                ymin,
2942                ymax,
2943            );
2944            let mut cap_path = Path::new();
2945            cap_path.move_to(cap_left.x, cap_left.y);
2946            cap_path.line_to(cap_right.x, cap_right.y);
2947            renderer.stroke_path(&cap_path, &paint, &thin, Affine::IDENTITY);
2948
2949            // --- Upper whisker ---
2950            let wu_bottom = self.data_to_pixel(cx, stats.q3, plot_area, xmin, xmax, ymin, ymax);
2951            let wu_top =
2952                self.data_to_pixel(cx, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
2953            let mut wu_path = Path::new();
2954            wu_path.move_to(wu_bottom.x, wu_bottom.y);
2955            wu_path.line_to(wu_top.x, wu_top.y);
2956            renderer.stroke_path(&wu_path, &paint, &thin, Affine::IDENTITY);
2957
2958            // Upper cap
2959            let ucap_left = self.data_to_pixel(
2960                cx - half * 0.5,
2961                stats.whisker_high,
2962                plot_area,
2963                xmin,
2964                xmax,
2965                ymin,
2966                ymax,
2967            );
2968            let ucap_right = self.data_to_pixel(
2969                cx + half * 0.5,
2970                stats.whisker_high,
2971                plot_area,
2972                xmin,
2973                xmax,
2974                ymin,
2975                ymax,
2976            );
2977            let mut ucap_path = Path::new();
2978            ucap_path.move_to(ucap_left.x, ucap_left.y);
2979            ucap_path.line_to(ucap_right.x, ucap_right.y);
2980            renderer.stroke_path(&ucap_path, &paint, &thin, Affine::IDENTITY);
2981
2982            // --- Outliers ---
2983            if artist.show_outliers {
2984                let r = 3.0;
2985                for &val in &stats.outliers {
2986                    let pt = self.data_to_pixel(cx, val, plot_area, xmin, xmax, ymin, ymax);
2987                    let mut dot = Path::new();
2988                    for seg in 0..8 {
2989                        let angle = std::f64::consts::TAU * seg as f64 / 8.0;
2990                        let dx = r * angle.cos();
2991                        let dy = r * angle.sin();
2992                        if seg == 0 {
2993                            dot.move_to(pt.x + dx, pt.y + dy);
2994                        } else {
2995                            dot.line_to(pt.x + dx, pt.y + dy);
2996                        }
2997                    }
2998                    dot.close();
2999                    renderer.fill_path(&dot, &Paint::new(fill_color), Affine::IDENTITY);
3000                    renderer.stroke_path(&dot, &paint, &hair, Affine::IDENTITY);
3001                }
3002            }
3003        }
3004    }
3005
3006    /// Draws a step (staircase) chart.
3007    fn draw_step(
3008        &self,
3009        renderer: &mut impl Renderer,
3010        artist: &StepArtist,
3011        plot_area: &Rect,
3012        xmin: f64,
3013        xmax: f64,
3014        ymin: f64,
3015        ymax: f64,
3016    ) {
3017        if artist.x.len() < 2 {
3018            return;
3019        }
3020        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
3021        let paint = Paint::new(color);
3022        let stroke = Stroke::new(artist.width);
3023
3024        let mut path = Path::new();
3025        let first = self.data_to_pixel(
3026            artist.x.data[0],
3027            artist.y.data[0],
3028            plot_area,
3029            xmin,
3030            xmax,
3031            ymin,
3032            ymax,
3033        );
3034        path.move_to(first.x, first.y);
3035
3036        for i in 1..artist.x.len() {
3037            let prev = self.data_to_pixel(
3038                artist.x.data[i - 1],
3039                artist.y.data[i - 1],
3040                plot_area,
3041                xmin,
3042                xmax,
3043                ymin,
3044                ymax,
3045            );
3046            let cur = self.data_to_pixel(
3047                artist.x.data[i],
3048                artist.y.data[i],
3049                plot_area,
3050                xmin,
3051                xmax,
3052                ymin,
3053                ymax,
3054            );
3055            match artist.where_step {
3056                StepWhere::Pre => {
3057                    path.line_to(prev.x, cur.y);
3058                    path.line_to(cur.x, cur.y);
3059                }
3060                StepWhere::Post => {
3061                    path.line_to(cur.x, prev.y);
3062                    path.line_to(cur.x, cur.y);
3063                }
3064                StepWhere::Mid => {
3065                    let mid_x = (prev.x + cur.x) / 2.0;
3066                    path.line_to(mid_x, prev.y);
3067                    path.line_to(mid_x, cur.y);
3068                    path.line_to(cur.x, cur.y);
3069                }
3070            }
3071        }
3072        renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
3073    }
3074
3075    /// Draws a stem (lollipop) chart.
3076    fn draw_stem(
3077        &self,
3078        renderer: &mut impl Renderer,
3079        artist: &StemArtist,
3080        plot_area: &Rect,
3081        xmin: f64,
3082        xmax: f64,
3083        ymin: f64,
3084        ymax: f64,
3085    ) {
3086        if artist.x.is_empty() {
3087            return;
3088        }
3089        let alpha_byte = (artist.alpha * 255.0) as u8;
3090        let color = artist.color.with_alpha(alpha_byte);
3091        let paint = Paint::new(color);
3092        let stroke = Stroke::new(artist.line_width);
3093        let radius = artist.marker_size / 2.0;
3094
3095        // Draw baseline.
3096        let bl_left = self.data_to_pixel(
3097            artist.x.data[0],
3098            artist.baseline,
3099            plot_area,
3100            xmin,
3101            xmax,
3102            ymin,
3103            ymax,
3104        );
3105        let bl_right = self.data_to_pixel(
3106            *artist.x.data.last().unwrap(),
3107            artist.baseline,
3108            plot_area,
3109            xmin,
3110            xmax,
3111            ymin,
3112            ymax,
3113        );
3114        let mut bl_path = Path::new();
3115        bl_path.move_to(bl_left.x, bl_left.y);
3116        bl_path.line_to(bl_right.x, bl_right.y);
3117        let bl_paint = Paint::new(Color::BLACK.with_alpha(alpha_byte));
3118        let bl_stroke = Stroke::new(0.8);
3119        renderer.stroke_path(&bl_path, &bl_paint, &bl_stroke, Affine::IDENTITY);
3120
3121        // Draw stems and markers.
3122        for i in 0..artist.x.len() {
3123            let base = self.data_to_pixel(
3124                artist.x.data[i],
3125                artist.baseline,
3126                plot_area,
3127                xmin,
3128                xmax,
3129                ymin,
3130                ymax,
3131            );
3132            let tip = self.data_to_pixel(
3133                artist.x.data[i],
3134                artist.y.data[i],
3135                plot_area,
3136                xmin,
3137                xmax,
3138                ymin,
3139                ymax,
3140            );
3141            let mut stem_path = Path::new();
3142            stem_path.move_to(base.x, base.y);
3143            stem_path.line_to(tip.x, tip.y);
3144            renderer.stroke_path(&stem_path, &paint, &stroke, Affine::IDENTITY);
3145            let marker = Path::circle(tip, radius);
3146            renderer.fill_path(&marker, &paint, Affine::IDENTITY);
3147        }
3148    }
3149
3150    /// Draws an error bar plot: center line with circle markers, vertical
3151    /// and/or horizontal error bars with caps.
3152    fn draw_errorbar(
3153        &self,
3154        renderer: &mut impl Renderer,
3155        artist: &ErrorBarArtist,
3156        plot_area: &Rect,
3157        xmin: f64,
3158        xmax: f64,
3159        ymin: f64,
3160        ymax: f64,
3161    ) {
3162        if artist.x.is_empty() {
3163            return;
3164        }
3165        let paint = Paint::new(artist.color);
3166        let stroke = Stroke::new(artist.line_width);
3167        let marker_radius = 3.0;
3168
3169        // Draw center connecting line.
3170        let mut line_path = Path::new();
3171        let first = self.data_to_pixel(
3172            artist.x.data[0],
3173            artist.y.data[0],
3174            plot_area,
3175            xmin,
3176            xmax,
3177            ymin,
3178            ymax,
3179        );
3180        line_path.move_to(first.x, first.y);
3181        for i in 1..artist.x.len() {
3182            let pt = self.data_to_pixel(
3183                artist.x.data[i],
3184                artist.y.data[i],
3185                plot_area,
3186                xmin,
3187                xmax,
3188                ymin,
3189                ymax,
3190            );
3191            line_path.line_to(pt.x, pt.y);
3192        }
3193        renderer.stroke_path(&line_path, &paint, &stroke, Affine::IDENTITY);
3194
3195        // Draw markers and error bars for each point.
3196        for i in 0..artist.x.len() {
3197            let xv = artist.x.data[i];
3198            let yv = artist.y.data[i];
3199            let center = self.data_to_pixel(xv, yv, plot_area, xmin, xmax, ymin, ymax);
3200
3201            // Circle marker at the data point.
3202            let marker = Path::circle(center, marker_radius);
3203            renderer.fill_path(&marker, &paint, Affine::IDENTITY);
3204
3205            // Vertical error bars (yerr).
3206            if let Some(ref yerr) = artist.yerr {
3207                let (lo, hi) = match yerr {
3208                    ErrorBarData::Symmetric(e) => (yv - e[i], yv + e[i]),
3209                    ErrorBarData::Asymmetric { low, high } => (yv - low[i], yv + high[i]),
3210                };
3211                let pt_lo = self.data_to_pixel(xv, lo, plot_area, xmin, xmax, ymin, ymax);
3212                let pt_hi = self.data_to_pixel(xv, hi, plot_area, xmin, xmax, ymin, ymax);
3213
3214                // Vertical bar.
3215                let mut bar = Path::new();
3216                bar.move_to(pt_lo.x, pt_lo.y);
3217                bar.line_to(pt_hi.x, pt_hi.y);
3218                renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
3219
3220                // Caps.
3221                if artist.cap_size > 0.0 {
3222                    let half_cap = artist.cap_size / 2.0;
3223                    let mut cap_lo = Path::new();
3224                    cap_lo.move_to(pt_lo.x - half_cap, pt_lo.y);
3225                    cap_lo.line_to(pt_lo.x + half_cap, pt_lo.y);
3226                    renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
3227
3228                    let mut cap_hi = Path::new();
3229                    cap_hi.move_to(pt_hi.x - half_cap, pt_hi.y);
3230                    cap_hi.line_to(pt_hi.x + half_cap, pt_hi.y);
3231                    renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
3232                }
3233            }
3234
3235            // Horizontal error bars (xerr).
3236            if let Some(ref xerr) = artist.xerr {
3237                let (lo, hi) = match xerr {
3238                    ErrorBarData::Symmetric(e) => (xv - e[i], xv + e[i]),
3239                    ErrorBarData::Asymmetric { low, high } => (xv - low[i], xv + high[i]),
3240                };
3241                let pt_lo = self.data_to_pixel(lo, yv, plot_area, xmin, xmax, ymin, ymax);
3242                let pt_hi = self.data_to_pixel(hi, yv, plot_area, xmin, xmax, ymin, ymax);
3243
3244                // Horizontal bar.
3245                let mut bar = Path::new();
3246                bar.move_to(pt_lo.x, pt_lo.y);
3247                bar.line_to(pt_hi.x, pt_hi.y);
3248                renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
3249
3250                // Caps.
3251                if artist.cap_size > 0.0 {
3252                    let half_cap = artist.cap_size / 2.0;
3253                    let mut cap_lo = Path::new();
3254                    cap_lo.move_to(pt_lo.x, pt_lo.y - half_cap);
3255                    cap_lo.line_to(pt_lo.x, pt_lo.y + half_cap);
3256                    renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
3257
3258                    let mut cap_hi = Path::new();
3259                    cap_hi.move_to(pt_hi.x, pt_hi.y - half_cap);
3260                    cap_hi.line_to(pt_hi.x, pt_hi.y + half_cap);
3261                    renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
3262                }
3263            }
3264        }
3265    }
3266
3267    /// Draws a heatmap: fills rectangles for each cell, colored via the
3268    /// configured colormap. Optionally draws cell value text.
3269    fn draw_heatmap(
3270        &self,
3271        renderer: &mut impl Renderer,
3272        artist: &HeatmapArtist,
3273        plot_area: &Rect,
3274        xmin: f64,
3275        xmax: f64,
3276        ymin: f64,
3277        ymax: f64,
3278    ) {
3279        let nrows = artist.data.len();
3280        if nrows == 0 {
3281            return;
3282        }
3283        let ncols = artist.data[0].len();
3284        if ncols == 0 {
3285            return;
3286        }
3287
3288        let vmin = artist.effective_vmin();
3289        let vmax = artist.effective_vmax();
3290
3291        let text_style = TextStyle {
3292            size: 10.0,
3293            color: Color::BLACK,
3294            weight: FontWeight::Normal,
3295            family: None,
3296            halign: HAlign::Center,
3297            valign: VAlign::Middle,
3298        };
3299
3300        for row in 0..nrows {
3301            for col in 0..ncols {
3302                let val = artist.data[row][col];
3303                let cell_color = artist.cmap.map_value(val, vmin, vmax);
3304
3305                // Cell rectangle in data space: col..col+1 on x, row..row+1 on y.
3306                let p_bl =
3307                    self.data_to_pixel(col as f64, row as f64, plot_area, xmin, xmax, ymin, ymax);
3308                let p_tr = self.data_to_pixel(
3309                    (col + 1) as f64,
3310                    (row + 1) as f64,
3311                    plot_area,
3312                    xmin,
3313                    xmax,
3314                    ymin,
3315                    ymax,
3316                );
3317                let rect = Rect::from_points(p_bl, p_tr);
3318                let cell_path = Path::rect(rect);
3319                renderer.fill_path(&cell_path, &Paint::new(cell_color), Affine::IDENTITY);
3320
3321                // Optional value text.
3322                if artist.show_values {
3323                    let cx = (p_bl.x + p_tr.x) / 2.0;
3324                    let cy = (p_bl.y + p_tr.y) / 2.0;
3325                    let label = format!("{val:.1}");
3326                    renderer.draw_text(&label, Point::new(cx, cy), &text_style, Affine::IDENTITY);
3327                }
3328            }
3329        }
3330    }
3331
3332    /// Draws a polar chart: concentric r-grid circles, radial theta-grid lines,
3333    /// and the data path in polar coordinates.
3334    fn draw_polar(
3335        &self,
3336        renderer: &mut impl Renderer,
3337        artist: &PolarArtist,
3338        plot_area: &Rect,
3339        xmin: f64,
3340        xmax: f64,
3341        ymin: f64,
3342        ymax: f64,
3343        theme: &Theme,
3344    ) {
3345        let n = artist.r.len().min(artist.theta.len());
3346        if n == 0 {
3347            return;
3348        }
3349
3350        let r_max = artist.max_finite_r();
3351        if r_max <= 0.0 || !r_max.is_finite() {
3352            return;
3353        }
3354
3355        // The center of the plot in pixel space. The polar data is centered
3356        // at the origin in data space (0, 0).
3357        let center = self.data_to_pixel(0.0, 0.0, plot_area, xmin, xmax, ymin, ymax);
3358
3359        // Use the smaller dimension to avoid clipping.
3360        let max_radius_px = (plot_area.width / 2.0).min(plot_area.height / 2.0) * 0.85;
3361
3362        // --- Draw r-grid: concentric circles ---
3363        let num_r_rings = 5;
3364        let r_step = r_max / num_r_rings as f64;
3365        let grid_color = theme.grid_color;
3366        let grid_paint = Paint::new(grid_color.with_alpha(100));
3367        let grid_stroke = Stroke::new(0.5);
3368        let label_style = TextStyle {
3369            size: 9.0,
3370            color: theme.tick_color,
3371            weight: FontWeight::Normal,
3372            family: theme.font_family.clone(),
3373            halign: HAlign::Left,
3374            valign: VAlign::Middle,
3375        };
3376
3377        for i in 1..=num_r_rings {
3378            let r_val = i as f64 * r_step;
3379            let r_px = r_val / r_max * max_radius_px;
3380            let circle = Path::circle(center, r_px);
3381            renderer.stroke_path(&circle, &grid_paint, &grid_stroke, Affine::IDENTITY);
3382
3383            // R-axis label at the right side of each circle
3384            let label_pt = Point::new(center.x + r_px + 3.0, center.y - 2.0);
3385            let label_text = if r_val == r_val.floor() {
3386                format!("{:.0}", r_val)
3387            } else {
3388                format!("{:.1}", r_val)
3389            };
3390            renderer.draw_text(&label_text, label_pt, &label_style, Affine::IDENTITY);
3391        }
3392
3393        // --- Draw theta-grid: radial lines at 0, 30, 60, ..., 330 degrees ---
3394        let angle_label_style = TextStyle {
3395            size: 10.0,
3396            color: theme.tick_color,
3397            weight: FontWeight::Normal,
3398            family: theme.font_family.clone(),
3399            halign: HAlign::Center,
3400            valign: VAlign::Middle,
3401        };
3402
3403        for deg in (0..360).step_by(30) {
3404            let angle = (deg as f64).to_radians();
3405            let end_x = center.x + max_radius_px * angle.cos();
3406            let end_y = center.y - max_radius_px * angle.sin();
3407            let mut line = Path::new();
3408            line.move_to(center.x, center.y);
3409            line.line_to(end_x, end_y);
3410            renderer.stroke_path(&line, &grid_paint, &grid_stroke, Affine::IDENTITY);
3411
3412            // Angle label at the tip of each radial line
3413            let label_offset = 14.0;
3414            let lx = center.x + (max_radius_px + label_offset) * angle.cos();
3415            let ly = center.y - (max_radius_px + label_offset) * angle.sin();
3416            let label_text = format!("{}°", deg);
3417            renderer.draw_text(
3418                &label_text,
3419                Point::new(lx, ly),
3420                &angle_label_style,
3421                Affine::IDENTITY,
3422            );
3423        }
3424
3425        // --- Draw the data path ---
3426        // Helper: convert (r, theta) -> pixel coordinates
3427        let to_px = |r: f64, theta: f64| -> Point {
3428            let px_r = r / r_max * max_radius_px;
3429            Point::new(center.x + px_r * theta.cos(), center.y - px_r * theta.sin())
3430        };
3431
3432        let mut path = Path::new();
3433        let mut started = false;
3434
3435        for i in 0..n {
3436            let r = artist.r[i];
3437            let theta = artist.theta[i];
3438            if !r.is_finite() || !theta.is_finite() || r < 0.0 {
3439                continue;
3440            }
3441            let pt = to_px(r, theta);
3442            if !started {
3443                path.move_to(pt.x, pt.y);
3444                started = true;
3445            } else {
3446                path.line_to(pt.x, pt.y);
3447            }
3448        }
3449
3450        if !started {
3451            return;
3452        }
3453
3454        let alpha_byte = (artist.alpha * 255.0) as u8;
3455        let color = artist.color.with_alpha(alpha_byte);
3456
3457        if artist.filled {
3458            // Close the path and fill
3459            path.close();
3460            let fill_paint = Paint::new(color);
3461            renderer.fill_path(&path, &fill_paint, Affine::IDENTITY);
3462            // Draw the outline
3463            let stroke_paint = Paint::new(
3464                artist
3465                    .color
3466                    .with_alpha(((artist.alpha.min(1.0) * 0.8 + 0.2) * 255.0) as u8),
3467            );
3468            let stroke = Stroke::new(artist.linewidth);
3469            renderer.stroke_path(&path, &stroke_paint, &stroke, Affine::IDENTITY);
3470        } else {
3471            // Stroke the path
3472            let paint = Paint::new(color);
3473            let stroke = Stroke::new(artist.linewidth);
3474            renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
3475        }
3476
3477        // --- Draw markers at each data point ---
3478        if let Some(marker) = artist.marker {
3479            let marker_size = 5.0;
3480            let marker_radius = marker_size / 2.0;
3481            let marker_paint = Paint::new(color);
3482
3483            for i in 0..n {
3484                let r = artist.r[i];
3485                let theta = artist.theta[i];
3486                if !r.is_finite() || !theta.is_finite() || r < 0.0 {
3487                    continue;
3488                }
3489                let pt = to_px(r, theta);
3490                let marker_path = match marker {
3491                    Marker::Circle | Marker::Point => Path::circle(pt, marker_radius),
3492                    Marker::Square => Path::rect(Rect::new(
3493                        pt.x - marker_radius,
3494                        pt.y - marker_radius,
3495                        marker_size,
3496                        marker_size,
3497                    )),
3498                    Marker::Diamond => {
3499                        let mut p = Path::new();
3500                        p.move_to(pt.x, pt.y - marker_radius);
3501                        p.line_to(pt.x + marker_radius, pt.y);
3502                        p.line_to(pt.x, pt.y + marker_radius);
3503                        p.line_to(pt.x - marker_radius, pt.y);
3504                        p.close();
3505                        p
3506                    }
3507                    Marker::Triangle => {
3508                        let mut p = Path::new();
3509                        let h = marker_radius * 1.1547;
3510                        p.move_to(pt.x, pt.y - marker_radius);
3511                        p.line_to(pt.x + h * 0.5, pt.y + marker_radius * 0.5);
3512                        p.line_to(pt.x - h * 0.5, pt.y + marker_radius * 0.5);
3513                        p.close();
3514                        p
3515                    }
3516                    Marker::Plus => {
3517                        let mut p = Path::new();
3518                        p.move_to(pt.x - marker_radius, pt.y);
3519                        p.line_to(pt.x + marker_radius, pt.y);
3520                        p.move_to(pt.x, pt.y - marker_radius);
3521                        p.line_to(pt.x, pt.y + marker_radius);
3522                        let ms = Stroke::new(theme.line_width.max(1.0));
3523                        renderer.stroke_path(&p, &marker_paint, &ms, Affine::IDENTITY);
3524                        continue;
3525                    }
3526                    Marker::Cross => {
3527                        let mut p = Path::new();
3528                        let d = marker_radius * 0.707;
3529                        p.move_to(pt.x - d, pt.y - d);
3530                        p.line_to(pt.x + d, pt.y + d);
3531                        p.move_to(pt.x + d, pt.y - d);
3532                        p.line_to(pt.x - d, pt.y + d);
3533                        let ms = Stroke::new(theme.line_width.max(1.0));
3534                        renderer.stroke_path(&p, &marker_paint, &ms, Affine::IDENTITY);
3535                        continue;
3536                    }
3537                    Marker::Star => {
3538                        let mut p = Path::new();
3539                        let inner = marker_radius * 0.382;
3540                        for j in 0..10 {
3541                            let a =
3542                                std::f64::consts::FRAC_PI_2 + j as f64 * std::f64::consts::PI / 5.0;
3543                            let r = if j % 2 == 0 { marker_radius } else { inner };
3544                            let sx = pt.x + r * a.cos();
3545                            let sy = pt.y - r * a.sin();
3546                            if j == 0 {
3547                                p.move_to(sx, sy);
3548                            } else {
3549                                p.line_to(sx, sy);
3550                            }
3551                        }
3552                        p.close();
3553                        p
3554                    }
3555                };
3556                renderer.fill_path(&marker_path, &marker_paint, Affine::IDENTITY);
3557            }
3558        }
3559    }
3560
3561    /// Draws a hexbin plot: bins data points into hexagonal cells, maps
3562    /// counts through the colormap, and renders filled hexagons.
3563    fn draw_hexbin(
3564        &self,
3565        renderer: &mut impl Renderer,
3566        artist: &HexbinArtist,
3567        plot_area: &Rect,
3568        xmin: f64,
3569        xmax: f64,
3570        ymin: f64,
3571        ymax: f64,
3572    ) {
3573        use crate::charts::hexbin::{bin_hexagonal, hex_size_for_gridsize, hexagon_vertices};
3574
3575        let result = bin_hexagonal(&artist.x, &artist.y, artist.gridsize, artist.mincnt);
3576        if result.cells.is_empty() {
3577            return;
3578        }
3579
3580        let vmin = result.min_count as f64;
3581        let vmax = result.max_count as f64;
3582
3583        // Compute hex size in data space.
3584        let data_xrange = (xmax - xmin).max(f64::EPSILON);
3585        let hex_data_size = hex_size_for_gridsize(data_xrange, artist.gridsize);
3586
3587        let alpha_byte = (artist.alpha * 255.0).round() as u8;
3588
3589        for &(cx, cy, count) in &result.cells {
3590            // Map count to color via colormap.
3591            let mut fill_color = artist.cmap.map_value(count as f64, vmin, vmax);
3592            fill_color = fill_color.with_alpha(alpha_byte);
3593
3594            // Compute hex vertices in data space, then convert to pixel space.
3595            let data_verts = hexagon_vertices(cx, cy, hex_data_size);
3596
3597            let mut path = Path::new();
3598            for (i, &(vx, vy)) in data_verts.iter().enumerate() {
3599                let p = self.data_to_pixel(vx, vy, plot_area, xmin, xmax, ymin, ymax);
3600                if i == 0 {
3601                    path.move_to(p.x, p.y);
3602                } else {
3603                    path.line_to(p.x, p.y);
3604                }
3605            }
3606            path.close();
3607
3608            renderer.fill_path(&path, &Paint::new(fill_color), Affine::IDENTITY);
3609
3610            // Optional edge stroke.
3611            if let Some(edge_color) = artist.edgecolor {
3612                let stroke = Stroke::new(0.5);
3613                renderer.stroke_path(
3614                    &path,
3615                    &Paint::new(edge_color.with_alpha(alpha_byte)),
3616                    &stroke,
3617                    Affine::IDENTITY,
3618                );
3619            }
3620        }
3621    }
3622
3623    /// Draws a waterfall chart: bars showing cumulative positive/negative changes.
3624    fn draw_waterfall(
3625        &self,
3626        renderer: &mut impl Renderer,
3627        artist: &WaterfallArtist,
3628        plot_area: &Rect,
3629        xmin: f64,
3630        xmax: f64,
3631        ymin: f64,
3632        ymax: f64,
3633    ) {
3634        let n = artist.categories.len();
3635        if n == 0 {
3636            return;
3637        }
3638
3639        let positions = artist.bar_positions();
3640        let cumsum = artist.cumulative_sums();
3641
3642        let cat_range = xmax - xmin;
3643        let cat_step = cat_range / n as f64;
3644        let bar_half = cat_step * artist.bar_width * 0.5;
3645
3646        for i in 0..n {
3647            let (base, top) = positions[i];
3648
3649            // Select bar color based on type.
3650            let bar_color = if artist.total_indices.contains(&i) {
3651                artist.total_color
3652            } else if artist.values.data[i] >= 0.0 {
3653                artist.increase_color
3654            } else {
3655                artist.decrease_color
3656            };
3657
3658            let color = bar_color.with_alpha((artist.alpha * 255.0) as u8);
3659            let paint = Paint::new(color);
3660
3661            let cat_center = xmin + (i as f64 + 0.5) * cat_step;
3662            let bottom_val = base.min(top);
3663            let top_val = base.max(top);
3664
3665            let p_bl = self.data_to_pixel(
3666                cat_center - bar_half,
3667                bottom_val,
3668                plot_area,
3669                xmin,
3670                xmax,
3671                ymin,
3672                ymax,
3673            );
3674            let p_tr = self.data_to_pixel(
3675                cat_center + bar_half,
3676                top_val,
3677                plot_area,
3678                xmin,
3679                xmax,
3680                ymin,
3681                ymax,
3682            );
3683
3684            let rect = Rect::from_points(p_bl, p_tr);
3685            let bar_path = Path::rect(rect);
3686            renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
3687
3688            // Draw value label on the bar if enabled.
3689            if artist.show_values {
3690                let display_val = if artist.total_indices.contains(&i) {
3691                    cumsum[i]
3692                } else {
3693                    artist.values.data[i]
3694                };
3695                let label_text = format_waterfall_value(display_val);
3696
3697                // Position label above positive bars, below negative bars.
3698                let label_y = if display_val >= 0.0 {
3699                    top_val
3700                } else {
3701                    bottom_val
3702                };
3703                let label_pos =
3704                    self.data_to_pixel(cat_center, label_y, plot_area, xmin, xmax, ymin, ymax);
3705
3706                let text_style = TextStyle {
3707                    size: 10.0,
3708                    color: Color::BLACK,
3709                    weight: FontWeight::Normal,
3710                    family: None,
3711                    halign: HAlign::Center,
3712                    valign: if display_val >= 0.0 {
3713                        VAlign::Bottom
3714                    } else {
3715                        VAlign::Top
3716                    },
3717                };
3718                let offset_pos = Point::new(
3719                    label_pos.x,
3720                    if display_val >= 0.0 {
3721                        label_pos.y - 3.0
3722                    } else {
3723                        label_pos.y + 3.0
3724                    },
3725                );
3726                renderer.draw_text(&label_text, offset_pos, &text_style, Affine::IDENTITY);
3727            }
3728        }
3729
3730        // Draw connector lines between consecutive bars.
3731        if artist.connector_lines && n > 1 {
3732            let connector_paint = Paint::new(Color::rgb(0x80, 0x80, 0x80).with_alpha(180));
3733            let connector_stroke = Stroke::new(0.8);
3734
3735            for (i, &connector_y) in cumsum.iter().enumerate().take(n - 1) {
3736                let right_edge = xmin + (i as f64 + 0.5) * cat_step + bar_half;
3737                let left_edge = xmin + ((i + 1) as f64 + 0.5) * cat_step - bar_half;
3738
3739                let p_from =
3740                    self.data_to_pixel(right_edge, connector_y, plot_area, xmin, xmax, ymin, ymax);
3741                let p_to =
3742                    self.data_to_pixel(left_edge, connector_y, plot_area, xmin, xmax, ymin, ymax);
3743
3744                let mut path = Path::new();
3745                path.move_to(p_from.x, p_from.y);
3746                path.line_to(p_to.x, p_to.y);
3747                renderer.stroke_path(&path, &connector_paint, &connector_stroke, Affine::IDENTITY);
3748            }
3749        }
3750    }
3751
3752    // -----------------------------------------------------------------------
3753    // Step 9b: Text and arrow annotations
3754    // -----------------------------------------------------------------------
3755
3756    /// Draws all text annotations and arrow annotations.
3757    fn draw_annotations(
3758        &self,
3759        renderer: &mut impl Renderer,
3760        plot_area: &Rect,
3761        xmin: f64,
3762        xmax: f64,
3763        ymin: f64,
3764        ymax: f64,
3765        theme: &Theme,
3766    ) {
3767        // --- TextAnnotation items ---
3768        for ta in &self.texts {
3769            let pt = self.data_to_pixel(ta.x, ta.y, plot_area, xmin, xmax, ymin, ymax);
3770            let size = ta.fontsize.unwrap_or(theme.axis_label_size);
3771            let color = ta.color.unwrap_or(theme.text_color);
3772            let style = TextStyle {
3773                size,
3774                color,
3775                weight: FontWeight::Normal,
3776                family: theme.font_family.clone(),
3777                halign: ta.ha,
3778                valign: ta.va,
3779            };
3780            if ta.rotation.abs() < f64::EPSILON {
3781                renderer.draw_text(&ta.text, pt, &style, Affine::IDENTITY);
3782            } else {
3783                let angle_rad = -ta.rotation.to_radians();
3784                let rotate = Affine::rotate(angle_rad);
3785                let translate_to = Affine::translate(kurbo::Vec2::new(pt.x, pt.y));
3786                let translate_back = Affine::translate(kurbo::Vec2::new(-pt.x, -pt.y));
3787                let transform = translate_to * rotate * translate_back;
3788                renderer.draw_text(&ta.text, pt, &style, transform);
3789            }
3790        }
3791
3792        // --- Annotation items (text + optional arrow) ---
3793        for ann in &self.annotations {
3794            let text_pt = self.data_to_pixel(
3795                ann.xytext.0,
3796                ann.xytext.1,
3797                plot_area,
3798                xmin,
3799                xmax,
3800                ymin,
3801                ymax,
3802            );
3803            let target_pt =
3804                self.data_to_pixel(ann.xy.0, ann.xy.1, plot_area, xmin, xmax, ymin, ymax);
3805
3806            let size = ann.fontsize.unwrap_or(theme.axis_label_size);
3807            let color = ann.color.unwrap_or(theme.text_color);
3808            let style = TextStyle {
3809                size,
3810                color,
3811                weight: FontWeight::Normal,
3812                family: theme.font_family.clone(),
3813                halign: ann.ha,
3814                valign: ann.va,
3815            };
3816            renderer.draw_text(&ann.text, text_pt, &style, Affine::IDENTITY);
3817
3818            // Draw arrow if requested.
3819            if ann.arrowstyle != ArrowStyle::None {
3820                let arrow_col = ann.arrow_color.unwrap_or(color);
3821                self.draw_annotation_arrow(
3822                    renderer,
3823                    text_pt,
3824                    target_pt,
3825                    arrow_col,
3826                    &ann.arrowstyle,
3827                );
3828            }
3829        }
3830    }
3831
3832    /// Draws an arrow line from `from` to `to` with an arrowhead at `to`.
3833    fn draw_annotation_arrow(
3834        &self,
3835        renderer: &mut impl Renderer,
3836        from: Point,
3837        to: Point,
3838        color: Color,
3839        style: &ArrowStyle,
3840    ) {
3841        let paint = Paint::new(color);
3842        let stroke = Stroke::new(1.0);
3843
3844        // Direction vector from `from` to `to`.
3845        let dx = to.x - from.x;
3846        let dy = to.y - from.y;
3847        let len = (dx * dx + dy * dy).sqrt();
3848        if len < 1e-6 {
3849            return;
3850        }
3851
3852        // Unit direction vector.
3853        let ux = dx / len;
3854        let uy = dy / len;
3855
3856        // Draw the line from `from` to `to`.
3857        let mut line = Path::new();
3858        line.move_to(from.x, from.y);
3859        line.line_to(to.x, to.y);
3860        renderer.stroke_path(&line, &paint, &stroke, Affine::IDENTITY);
3861
3862        // Draw a filled triangular arrowhead at `to`.
3863        let head_len = match style {
3864            ArrowStyle::None => return,
3865            ArrowStyle::Simple => 8.0,
3866            ArrowStyle::Fancy => 12.0,
3867        };
3868        let head_half_width = match style {
3869            ArrowStyle::None => return,
3870            ArrowStyle::Simple => 3.0,
3871            ArrowStyle::Fancy => 5.0,
3872        };
3873
3874        // Perpendicular to the direction.
3875        let px = -uy;
3876        let py = ux;
3877
3878        // Triangle vertices: tip at `to`, base offset by head_len along -direction.
3879        let base_x = to.x - ux * head_len;
3880        let base_y = to.y - uy * head_len;
3881
3882        let left_x = base_x + px * head_half_width;
3883        let left_y = base_y + py * head_half_width;
3884        let right_x = base_x - px * head_half_width;
3885        let right_y = base_y - py * head_half_width;
3886
3887        let mut arrow = Path::new();
3888        arrow.move_to(to.x, to.y);
3889        arrow.line_to(left_x, left_y);
3890        arrow.line_to(right_x, right_y);
3891        arrow.close();
3892        renderer.fill_path(&arrow, &paint, Affine::IDENTITY);
3893    }
3894
3895    /// Draws a pie chart: fills arc wedges centered in the plot area.
3896    ///
3897    /// Each wedge is built as a closed path from the center to the arc
3898    /// perimeter, using cubic Bezier segments to approximate circular
3899    /// arcs (each sub-arc spans at most 90 degrees).
3900    fn draw_pie(
3901        &self,
3902        renderer: &mut impl Renderer,
3903        artist: &PieArtist,
3904        plot_area: &Rect,
3905        xmin: f64,
3906        xmax: f64,
3907        ymin: f64,
3908        ymax: f64,
3909        theme: &Theme,
3910    ) {
3911        let n = artist.sizes.len();
3912        if n == 0 {
3913            return;
3914        }
3915
3916        // Normalise sizes to fractions summing to 1.0.
3917        let total: f64 = artist
3918            .sizes
3919            .iter()
3920            .copied()
3921            .filter(|v| v.is_finite() && *v > 0.0)
3922            .sum();
3923        if total <= 0.0 {
3924            return;
3925        }
3926        let fractions: Vec<f64> = artist
3927            .sizes
3928            .iter()
3929            .map(|&s| {
3930                if s.is_finite() && s > 0.0 {
3931                    s / total
3932                } else {
3933                    0.0
3934                }
3935            })
3936            .collect();
3937
3938        // Center of the pie in data space is (0, 0).
3939        let center_px = self.data_to_pixel(0.0, 0.0, plot_area, xmin, xmax, ymin, ymax);
3940
3941        // Compute pixel radius: map (radius, 0) and use x-distance.
3942        let edge_px = self.data_to_pixel(artist.radius, 0.0, plot_area, xmin, xmax, ymin, ymax);
3943        let radius_px = (edge_px.x - center_px.x).abs();
3944
3945        let start_rad = artist.start_angle.to_radians();
3946        let mut current_angle = start_rad;
3947
3948        let pct_style = TextStyle {
3949            size: 10.0,
3950            color: Color::BLACK,
3951            weight: FontWeight::Normal,
3952            family: None,
3953            halign: HAlign::Center,
3954            valign: VAlign::Middle,
3955        };
3956        let label_style = TextStyle {
3957            size: 11.0,
3958            color: theme.tick_color,
3959            weight: FontWeight::Normal,
3960            family: None,
3961            halign: HAlign::Center,
3962            valign: VAlign::Middle,
3963        };
3964
3965        for i in 0..n {
3966            let frac = fractions[i];
3967            if frac <= 0.0 {
3968                current_angle += frac * std::f64::consts::TAU;
3969                continue;
3970            }
3971
3972            let sweep = frac * std::f64::consts::TAU;
3973            let mid_angle = current_angle + sweep / 2.0;
3974
3975            // Resolve wedge color.
3976            let wedge_color = if let Some(ref colors) = artist.colors {
3977                colors[i % colors.len()]
3978            } else {
3979                Color::TABLEAU_10[i % 10]
3980            };
3981
3982            // Compute explode offset in pixels.
3983            let explode_frac = artist
3984                .explode
3985                .as_ref()
3986                .map(|e| if i < e.len() { e[i] } else { 0.0 })
3987                .unwrap_or(0.0);
3988            let offset_x = explode_frac * radius_px * mid_angle.cos();
3989            let offset_y = explode_frac * radius_px * (-mid_angle.sin()); // y inverted
3990
3991            let cx = center_px.x + offset_x;
3992            let cy = center_px.y + offset_y;
3993
3994            // Build the wedge path: move to center, line to arc start,
3995            // approximate arc with cubic Beziers, close back to center.
3996            let mut path = Path::new();
3997            path.move_to(cx, cy);
3998
3999            let arc_start_x = cx + radius_px * current_angle.cos();
4000            let arc_start_y = cy - radius_px * current_angle.sin();
4001            path.line_to(arc_start_x, arc_start_y);
4002
4003            // Split the sweep into sub-arcs of at most 90 degrees.
4004            let max_sub = std::f64::consts::FRAC_PI_2;
4005            let num_segments = (sweep / max_sub).ceil() as usize;
4006            let seg_sweep = sweep / num_segments as f64;
4007            let mut seg_start = current_angle;
4008
4009            for _ in 0..num_segments {
4010                let seg_end = seg_start + seg_sweep;
4011                // Cubic Bezier approximation for a circular arc.
4012                let half = seg_sweep / 2.0;
4013                let alpha = (4.0 / 3.0) * (half / 2.0).tan();
4014
4015                let p0x = cx + radius_px * seg_start.cos();
4016                let p0y = cy - radius_px * seg_start.sin();
4017                let p3x = cx + radius_px * seg_end.cos();
4018                let p3y = cy - radius_px * seg_end.sin();
4019
4020                // Tangent direction at start: perpendicular to radial.
4021                let t0x = -seg_start.sin();
4022                let t0y = -seg_start.cos(); // y inverted in pixel space
4023                                            // Tangent direction at end: perpendicular to radial.
4024                let t1x = -seg_end.sin();
4025                let t1y = -seg_end.cos(); // y inverted in pixel space
4026
4027                let cp1x = p0x + alpha * radius_px * t0x;
4028                let cp1y = p0y + alpha * radius_px * t0y;
4029                let cp2x = p3x - alpha * radius_px * t1x;
4030                let cp2y = p3y - alpha * radius_px * t1y;
4031
4032                let _ = path.curve_to(cp1x, cp1y, cp2x, cp2y, p3x, p3y);
4033                seg_start = seg_end;
4034            }
4035
4036            path.close();
4037
4038            let paint = Paint::new(wedge_color);
4039            renderer.fill_path(&path, &paint, Affine::IDENTITY);
4040
4041            // Thin white outline for visual separation between wedges.
4042            let outline_paint = Paint::new(Color::WHITE);
4043            let outline_stroke = Stroke::new(1.5);
4044            renderer.stroke_path(&path, &outline_paint, &outline_stroke, Affine::IDENTITY);
4045
4046            // Percentage label at the midpoint of the arc.
4047            if artist.autopct {
4048                let pct_r = radius_px * 0.6;
4049                let pct_x = cx + pct_r * mid_angle.cos();
4050                let pct_y = cy - pct_r * mid_angle.sin();
4051                let pct_text = format!("{:.1}%", frac * 100.0);
4052                renderer.draw_text(
4053                    &pct_text,
4054                    Point::new(pct_x, pct_y),
4055                    &pct_style,
4056                    Affine::IDENTITY,
4057                );
4058            }
4059
4060            // Wedge labels outside the arc.
4061            if let Some(ref labels) = artist.labels {
4062                if i < labels.len() {
4063                    let label_r = radius_px * 1.15;
4064                    let lx = cx + label_r * mid_angle.cos();
4065                    let ly = cy - label_r * mid_angle.sin();
4066                    renderer.draw_text(
4067                        &labels[i],
4068                        Point::new(lx, ly),
4069                        &label_style,
4070                        Affine::IDENTITY,
4071                    );
4072                }
4073            }
4074
4075            current_angle += sweep;
4076        }
4077    }
4078
4079    /// Draws a contour or filled contour plot.
4080    fn draw_contour(
4081        &self,
4082        renderer: &mut impl Renderer,
4083        artist: &ContourArtist,
4084        plot_area: &Rect,
4085        xmin: f64,
4086        xmax: f64,
4087        ymin: f64,
4088        ymax: f64,
4089    ) {
4090        let nx = artist.x.len();
4091        let ny = artist.y.len();
4092        if nx < 2 || ny < 2 || artist.z.len() < 2 {
4093            return;
4094        }
4095
4096        let levels = artist.effective_levels();
4097        let (zmin, zmax) = artist.z_bounds();
4098
4099        if artist.filled {
4100            let avgs = artist.cell_averages();
4101            for (j, row) in avgs.iter().enumerate() {
4102                for (i, &avg) in row.iter().enumerate() {
4103                    if !avg.is_finite() {
4104                        continue;
4105                    }
4106                    let cell_color = if let Some(ref colors) = artist.colors {
4107                        let idx = levels
4108                            .iter()
4109                            .position(|&l| avg < l)
4110                            .unwrap_or(levels.len())
4111                            .saturating_sub(1);
4112                        colors[idx % colors.len()]
4113                    } else {
4114                        artist.cmap.map_value(avg, zmin, zmax)
4115                    };
4116                    let p_bl = self.data_to_pixel(
4117                        artist.x[i],
4118                        artist.y[j],
4119                        plot_area,
4120                        xmin,
4121                        xmax,
4122                        ymin,
4123                        ymax,
4124                    );
4125                    let p_tr = self.data_to_pixel(
4126                        artist.x[i + 1],
4127                        artist.y[j + 1],
4128                        plot_area,
4129                        xmin,
4130                        xmax,
4131                        ymin,
4132                        ymax,
4133                    );
4134                    let rect = Rect::from_points(p_bl, p_tr);
4135                    let cell_path = Path::rect(rect);
4136                    renderer.fill_path(&cell_path, &Paint::new(cell_color), Affine::IDENTITY);
4137                }
4138            }
4139        }
4140
4141        if !artist.filled {
4142            for (li, &level) in levels.iter().enumerate() {
4143                let segments = artist.marching_squares(level);
4144                if segments.is_empty() {
4145                    continue;
4146                }
4147                let line_color = if let Some(ref colors) = artist.colors {
4148                    colors[li % colors.len()]
4149                } else {
4150                    artist.cmap.map_value(level, zmin, zmax)
4151                };
4152                let paint = Paint::new(line_color);
4153                let stroke = Stroke::new(artist.linewidths);
4154                for (sx0, sy0, sx1, sy1) in &segments {
4155                    let p0 = self.data_to_pixel(*sx0, *sy0, plot_area, xmin, xmax, ymin, ymax);
4156                    let p1 = self.data_to_pixel(*sx1, *sy1, plot_area, xmin, xmax, ymin, ymax);
4157                    let mut path = Path::new();
4158                    path.move_to(p0.x, p0.y);
4159                    path.line_to(p1.x, p1.y);
4160                    renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
4161                }
4162            }
4163        }
4164    }
4165
4166    /// Draws a violin plot.
4167    fn draw_violin(
4168        &self,
4169        renderer: &mut impl Renderer,
4170        artist: &ViolinArtist,
4171        plot_area: &Rect,
4172        xmin: f64,
4173        xmax: f64,
4174        ymin: f64,
4175        ymax: f64,
4176        theme: &Theme,
4177    ) {
4178        use crate::charts::violin::{gaussian_kde, silverman_bandwidth};
4179
4180        let fill_color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
4181        let fill_paint = Paint::new(fill_color);
4182        let outline_paint = Paint::new(artist.color);
4183        let outline_stroke = Stroke::new(1.0);
4184
4185        for (di, data) in artist.datasets.iter().enumerate() {
4186            let mut sorted: Vec<f64> = data.iter().copied().filter(|v| v.is_finite()).collect();
4187            if sorted.is_empty() {
4188                continue;
4189            }
4190            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
4191
4192            let pos = artist
4193                .positions
4194                .as_ref()
4195                .and_then(|p| p.get(di).copied())
4196                .unwrap_or(di as f64 + 1.0);
4197
4198            let bw = if artist.bw_method > 0.0 {
4199                artist.bw_method
4200            } else {
4201                silverman_bandwidth(&sorted)
4202            };
4203
4204            let data_min = sorted[0];
4205            let data_max = sorted[sorted.len() - 1];
4206            let n_eval = 100;
4207            let eval_points: Vec<f64> = (0..n_eval)
4208                .map(|i| data_min + (data_max - data_min) * i as f64 / (n_eval - 1) as f64)
4209                .collect();
4210            let densities = gaussian_kde(&sorted, bw, &eval_points);
4211
4212            let max_density = densities.iter().copied().fold(0.0_f64, f64::max);
4213            if max_density <= 0.0 {
4214                continue;
4215            }
4216
4217            let half_width = artist.widths * 0.5;
4218
4219            // Build mirrored path
4220            let mut path = Path::new();
4221            // Right side (top to bottom in data space)
4222            let first_y = eval_points[0];
4223            let first_w = densities[0] / max_density * half_width;
4224            let fp = self.data_to_pixel(pos + first_w, first_y, plot_area, xmin, xmax, ymin, ymax);
4225            path.move_to(fp.x, fp.y);
4226            for i in 1..n_eval {
4227                let w = densities[i] / max_density * half_width;
4228                let p =
4229                    self.data_to_pixel(pos + w, eval_points[i], plot_area, xmin, xmax, ymin, ymax);
4230                path.line_to(p.x, p.y);
4231            }
4232            // Left side (bottom to top)
4233            for i in (0..n_eval).rev() {
4234                let w = densities[i] / max_density * half_width;
4235                let p =
4236                    self.data_to_pixel(pos - w, eval_points[i], plot_area, xmin, xmax, ymin, ymax);
4237                path.line_to(p.x, p.y);
4238            }
4239            path.close();
4240
4241            renderer.fill_path(&path, &fill_paint, Affine::IDENTITY);
4242            renderer.stroke_path(&path, &outline_paint, &outline_stroke, Affine::IDENTITY);
4243
4244            // Median and quartile lines
4245            let n = sorted.len();
4246            if artist.show_median {
4247                let median = if n % 2 == 0 {
4248                    (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
4249                } else {
4250                    sorted[n / 2]
4251                };
4252                let med_density = gaussian_kde(&sorted, bw, &[median])[0];
4253                let med_w = med_density / max_density * half_width;
4254                let p1 = self.data_to_pixel(pos - med_w, median, plot_area, xmin, xmax, ymin, ymax);
4255                let p2 = self.data_to_pixel(pos + med_w, median, plot_area, xmin, xmax, ymin, ymax);
4256                let mut mp = Path::new();
4257                mp.move_to(p1.x, p1.y);
4258                mp.line_to(p2.x, p2.y);
4259                let med_paint = Paint::new(theme.text_color);
4260                let med_stroke = Stroke::new(2.0);
4261                renderer.stroke_path(&mp, &med_paint, &med_stroke, Affine::IDENTITY);
4262            }
4263
4264            if artist.show_quartiles && n >= 4 {
4265                let q1 = sorted[n / 4];
4266                let q3 = sorted[3 * n / 4];
4267                for q in [q1, q3] {
4268                    let q_density = gaussian_kde(&sorted, bw, &[q])[0];
4269                    let q_w = q_density / max_density * half_width;
4270                    let p1 = self.data_to_pixel(pos - q_w, q, plot_area, xmin, xmax, ymin, ymax);
4271                    let p2 = self.data_to_pixel(pos + q_w, q, plot_area, xmin, xmax, ymin, ymax);
4272                    let mut qp = Path::new();
4273                    qp.move_to(p1.x, p1.y);
4274                    qp.line_to(p2.x, p2.y);
4275                    let q_stroke = Stroke::new(1.0).with_dash(DashPattern {
4276                        dashes: vec![4.0, 2.0],
4277                        offset: 0.0,
4278                    });
4279                    renderer.stroke_path(
4280                        &qp,
4281                        &Paint::new(theme.text_color),
4282                        &q_stroke,
4283                        Affine::IDENTITY,
4284                    );
4285                }
4286            }
4287        }
4288    }
4289
4290    fn draw_legend(&self, renderer: &mut impl Renderer, plot_area: &Rect, theme: &Theme) {
4291        // Collect labeled artists into LegendEntry items, choosing the
4292        // appropriate swatch kind for each artist type.
4293        let entries: Vec<LegendEntry> = self
4294            .artists
4295            .iter()
4296            .filter_map(|a| {
4297                let (label, color, swatch) = match a {
4298                    Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
4299                    Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4300                    Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4301                    Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4302                    Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4303                    Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
4304                    Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4305                    Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4306                    Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
4307                    Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4308                    Artist::Pie(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4309                    Artist::Violin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4310                    Artist::Contour(a) => (
4311                        a.label.as_deref(),
4312                        a.color,
4313                        if a.filled {
4314                            SwatchKind::Filled
4315                        } else {
4316                            SwatchKind::Line
4317                        },
4318                    ),
4319                    Artist::Polar(a) => (
4320                        a.label.as_deref(),
4321                        a.color,
4322                        if a.filled {
4323                            SwatchKind::Filled
4324                        } else {
4325                            SwatchKind::Line
4326                        },
4327                    ),
4328                    Artist::Hexbin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4329                    Artist::Waterfall(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4330                };
4331                label.map(|l| LegendEntry {
4332                    label: l.to_string(),
4333                    color,
4334                    swatch,
4335                })
4336            })
4337            .collect();
4338
4339        legend::draw_legend(renderer, &entries, plot_area, self.legend_loc, theme);
4340    }
4341
4342    // -----------------------------------------------------------------------
4343    // Coordinate transform
4344    // -----------------------------------------------------------------------
4345
4346    /// Maps a data-space `(x, y)` coordinate to a pixel-space [`Point`] within
4347    /// the given `plot_area` rectangle.
4348    ///
4349    /// The x-axis maps left-to-right and the y-axis maps bottom-to-top (i.e.,
4350    /// pixel y is inverted relative to data y).
4351    fn data_to_pixel(
4352        &self,
4353        x: f64,
4354        y: f64,
4355        plot_area: &Rect,
4356        xmin: f64,
4357        xmax: f64,
4358        ymin: f64,
4359        ymax: f64,
4360    ) -> Point {
4361        let tx = self.xscale.transform(x, xmin, xmax);
4362        let ty = self.yscale.transform(y, ymin, ymax);
4363        Point::new(
4364            plot_area.x + tx * plot_area.width,
4365            plot_area.y + (1.0 - ty) * plot_area.height, // y is inverted
4366        )
4367    }
4368}
4369
4370// ---------------------------------------------------------------------------
4371// Module-level helpers
4372// ---------------------------------------------------------------------------
4373
4374/// Formats a waterfall bar value for display.
4375///
4376/// Integer-valued floats are displayed without a decimal point.
4377/// Decimal values are shown with up to 2 significant decimal places,
4378/// with trailing zeros stripped.
4379fn format_waterfall_value(v: f64) -> String {
4380    if v == v.trunc() && v.abs() < 1e15 {
4381        format!("{}", v as i64)
4382    } else {
4383        let s = format!("{:.2}", v);
4384        s.trim_end_matches('0').trim_end_matches('.').to_string()
4385    }
4386}
4387
4388// ---------------------------------------------------------------------------
4389// Tests
4390// ---------------------------------------------------------------------------
4391
4392#[cfg(test)]
4393mod tests {
4394    use super::*;
4395
4396    #[test]
4397    fn new_axes_has_defaults() {
4398        let ax = Axes::new();
4399        assert!(ax.artists.is_empty());
4400        assert!(ax.title.is_none());
4401        assert!(ax.xlabel.is_none());
4402        assert!(ax.ylabel.is_none());
4403        assert!(ax.xlim.is_none());
4404        assert!(ax.ylim.is_none());
4405        assert!(!ax.show_legend);
4406        assert_eq!(ax.color_index, 0);
4407    }
4408
4409    #[test]
4410    fn plot_creates_line_artist() {
4411        let mut ax = Axes::new();
4412        let result = ax.plot(vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]);
4413        assert!(result.is_ok());
4414        assert_eq!(ax.artists.len(), 1);
4415        assert!(matches!(&ax.artists[0], Artist::Line(_)));
4416        assert_eq!(ax.color_index, 1);
4417    }
4418
4419    #[test]
4420    fn plot_length_mismatch() {
4421        let mut ax = Axes::new();
4422        let result = ax.plot(vec![1.0, 2.0], vec![1.0]);
4423        assert!(matches!(
4424            result,
4425            Err(PlotError::SeriesLengthMismatch {
4426                expected: 2,
4427                got: 1
4428            })
4429        ));
4430    }
4431
4432    #[test]
4433    fn plot_empty_data() {
4434        let mut ax = Axes::new();
4435        let result = ax.plot(Vec::<f64>::new(), Vec::<f64>::new());
4436        assert!(matches!(result, Err(PlotError::EmptyData)));
4437    }
4438
4439    #[test]
4440    fn scatter_creates_artist() {
4441        let mut ax = Axes::new();
4442        let result = ax.scatter(vec![1.0, 2.0], vec![3.0, 4.0]);
4443        assert!(result.is_ok());
4444        assert!(matches!(&ax.artists[0], Artist::Scatter(_)));
4445    }
4446
4447    #[test]
4448    fn bar_creates_artist() {
4449        let mut ax = Axes::new();
4450        let cats: &[&str] = &["a", "b", "c"];
4451        let result = ax.bar(cats, vec![10.0, 20.0, 30.0]);
4452        assert!(result.is_ok());
4453        match &ax.artists[0] {
4454            Artist::Bar(a) => {
4455                assert!(!a.horizontal);
4456                assert_eq!(a.categories.len(), 3);
4457            }
4458            _ => panic!("expected Bar artist"),
4459        }
4460    }
4461
4462    #[test]
4463    fn barh_creates_horizontal_artist() {
4464        let mut ax = Axes::new();
4465        let cats: &[&str] = &["x", "y"];
4466        let result = ax.barh(cats, vec![5.0, 10.0]);
4467        assert!(result.is_ok());
4468        match &ax.artists[0] {
4469            Artist::Bar(a) => assert!(a.horizontal),
4470            _ => panic!("expected Bar artist"),
4471        }
4472    }
4473
4474    #[test]
4475    fn hist_computes_bins() {
4476        let mut ax = Axes::new();
4477        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
4478        let result = ax.hist(data, 5);
4479        assert!(result.is_ok());
4480        match &ax.artists[0] {
4481            Artist::Histogram(a) => {
4482                assert_eq!(a.bin_edges.len(), 6); // 5 bins = 6 edges
4483                assert_eq!(a.counts.len(), 5);
4484                // Total count should equal number of data points.
4485                let total: f64 = a.counts.iter().sum();
4486                assert_eq!(total, 10.0);
4487            }
4488            _ => panic!("expected Hist artist"),
4489        }
4490    }
4491
4492    #[test]
4493    fn hist_single_value() {
4494        let mut ax = Axes::new();
4495        let result = ax.hist(vec![5.0, 5.0, 5.0], 3);
4496        assert!(result.is_ok());
4497        match &ax.artists[0] {
4498            Artist::Histogram(a) => {
4499                let total: f64 = a.counts.iter().sum();
4500                assert_eq!(total, 3.0);
4501            }
4502            _ => panic!("expected Hist artist"),
4503        }
4504    }
4505
4506    #[test]
4507    fn hist_empty_data() {
4508        let mut ax = Axes::new();
4509        let result = ax.hist(Vec::<f64>::new(), 10);
4510        assert!(matches!(result, Err(PlotError::EmptyData)));
4511    }
4512
4513    #[test]
4514    fn fill_between_creates_artist() {
4515        let mut ax = Axes::new();
4516        let result = ax.fill_between(
4517            vec![1.0, 2.0, 3.0],
4518            vec![1.0, 2.0, 1.0],
4519            vec![0.0, 0.0, 0.0],
4520        );
4521        assert!(result.is_ok());
4522        assert!(matches!(&ax.artists[0], Artist::FillBetween(_)));
4523    }
4524
4525    #[test]
4526    fn fill_between_length_mismatch() {
4527        let mut ax = Axes::new();
4528        let result = ax.fill_between(vec![1.0, 2.0], vec![1.0], vec![0.0, 0.0]);
4529        assert!(matches!(
4530            result,
4531            Err(PlotError::SeriesLengthMismatch { .. })
4532        ));
4533    }
4534
4535    #[test]
4536    fn configuration_methods_return_self() {
4537        let mut ax = Axes::new();
4538        ax.set_title("Test")
4539            .set_xlabel("X")
4540            .set_ylabel("Y")
4541            .set_xlim(0.0, 10.0)
4542            .set_ylim(-1.0, 1.0)
4543            .set_xscale(Scale::Linear)
4544            .set_yscale(Scale::Log10)
4545            .grid(true)
4546            .legend();
4547
4548        assert_eq!(ax.title.as_deref(), Some("Test"));
4549        assert_eq!(ax.xlabel.as_deref(), Some("X"));
4550        assert_eq!(ax.ylabel.as_deref(), Some("Y"));
4551        assert_eq!(ax.xlim, Some((0.0, 10.0)));
4552        assert_eq!(ax.ylim, Some((-1.0, 1.0)));
4553        assert_eq!(ax.show_grid, Some(true));
4554        assert!(ax.show_legend);
4555    }
4556
4557    #[test]
4558    fn color_cycle_advances() {
4559        let mut ax = Axes::new();
4560        for _ in 0..12 {
4561            ax.plot(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4562        }
4563        assert_eq!(ax.color_index, 12);
4564        // 12th artist wraps around: index 10 % 10 == 0, so color should
4565        // be the same as the first.
4566        match (&ax.artists[0], &ax.artists[10]) {
4567            (Artist::Line(a), Artist::Line(b)) => {
4568                assert_eq!(a.color, b.color);
4569            }
4570            _ => panic!("expected Line artists"),
4571        }
4572    }
4573
4574    #[test]
4575    fn data_to_pixel_linear() {
4576        let ax = Axes::new();
4577        let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
4578
4579        // Bottom-left corner of data space.
4580        let p = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4581        assert!((p.x - 100.0).abs() < 1e-10);
4582        assert!((p.y - 350.0).abs() < 1e-10); // bottom of plot
4583
4584        // Top-right corner of data space.
4585        let p = ax.data_to_pixel(10.0, 10.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4586        assert!((p.x - 500.0).abs() < 1e-10);
4587        assert!((p.y - 50.0).abs() < 1e-10); // top of plot
4588
4589        // Center.
4590        let p = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4591        assert!((p.x - 300.0).abs() < 1e-10);
4592        assert!((p.y - 200.0).abs() < 1e-10);
4593    }
4594
4595    #[test]
4596    fn compute_data_limits_no_artists() {
4597        let ax = Axes::new();
4598        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4599        // Should return sensible defaults.
4600        assert!(xmin < xmax);
4601        assert!(ymin < ymax);
4602    }
4603
4604    #[test]
4605    fn compute_data_limits_with_user_override() {
4606        let mut ax = Axes::new();
4607        ax.set_xlim(-5.0, 5.0).set_ylim(0.0, 100.0);
4608        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4609        assert!((xmin - (-5.0)).abs() < f64::EPSILON);
4610        assert!((xmax - 5.0).abs() < f64::EPSILON);
4611        assert!((ymin - 0.0).abs() < f64::EPSILON);
4612        assert!((ymax - 100.0).abs() < f64::EPSILON);
4613    }
4614
4615    #[test]
4616    fn compute_data_limits_from_line_data() {
4617        let mut ax = Axes::new();
4618        ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
4619        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4620        // Should encompass data with padding.
4621        assert!(xmin < 1.0);
4622        assert!(xmax > 10.0);
4623        assert!(ymin < 2.0);
4624        assert!(ymax > 8.0);
4625    }
4626    // -- Step chart tests ---------------------------------------------------
4627
4628    #[test]
4629    fn step_creates_artist() {
4630        let mut ax = Axes::new();
4631        let result = ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
4632        assert!(result.is_ok());
4633        assert!(matches!(&ax.artists[0], Artist::Step(_)));
4634    }
4635
4636    #[test]
4637    fn step_length_mismatch() {
4638        let mut ax = Axes::new();
4639        let result = ax.step(vec![1.0, 2.0], vec![1.0]);
4640        assert!(matches!(
4641            result,
4642            Err(PlotError::SeriesLengthMismatch { .. })
4643        ));
4644    }
4645
4646    #[test]
4647    fn step_empty_data() {
4648        let mut ax = Axes::new();
4649        let result = ax.step(Vec::<f64>::new(), Vec::<f64>::new());
4650        assert!(matches!(result, Err(PlotError::EmptyData)));
4651    }
4652
4653    #[test]
4654    fn step_default_where() {
4655        let mut ax = Axes::new();
4656        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4657        match &ax.artists[0] {
4658            Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Pre)),
4659            _ => panic!("expected Step"),
4660        }
4661    }
4662
4663    #[test]
4664    fn step_color_cycle() {
4665        let mut ax = Axes::new();
4666        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4667        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4668        let c0 = ax.artists[0].color();
4669        let c1 = ax.artists[1].color();
4670        assert_ne!(c0, c1);
4671    }
4672
4673    #[test]
4674    fn step_builder_chaining() {
4675        let mut ax = Axes::new();
4676        ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
4677            .unwrap()
4678            .color(Color::TAB_RED)
4679            .width(3.0)
4680            .where_step(StepWhere::Post)
4681            .label("steps")
4682            .alpha(0.5);
4683        match &ax.artists[0] {
4684            Artist::Step(a) => {
4685                assert_eq!(a.color, Color::TAB_RED);
4686                assert!((a.width - 3.0).abs() < 1e-12);
4687                assert!(matches!(a.where_step, StepWhere::Post));
4688                assert_eq!(a.label.as_deref(), Some("steps"));
4689                assert!((a.alpha - 0.5).abs() < 1e-12);
4690            }
4691            _ => panic!("expected Step"),
4692        }
4693    }
4694
4695    #[test]
4696    fn step_data_bounds() {
4697        let mut ax = Axes::new();
4698        ax.step(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
4699        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4700        assert!(xmin < 1.0);
4701        assert!(xmax > 10.0);
4702        assert!(ymin < 2.0);
4703        assert!(ymax > 8.0);
4704    }
4705
4706    #[test]
4707    fn step_legend_label() {
4708        let mut ax = Axes::new();
4709        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("S");
4710        assert_eq!(ax.artists[0].label(), Some("S"));
4711    }
4712
4713    #[test]
4714    fn step_default_alpha() {
4715        let mut ax = Axes::new();
4716        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4717        match &ax.artists[0] {
4718            Artist::Step(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
4719            _ => panic!("expected Step"),
4720        }
4721    }
4722
4723    #[test]
4724    fn step_default_width() {
4725        let mut ax = Axes::new();
4726        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4727        match &ax.artists[0] {
4728            Artist::Step(a) => assert!((a.width - 1.5).abs() < 1e-12),
4729            _ => panic!("expected Step"),
4730        }
4731    }
4732
4733    #[test]
4734    fn step_mid_mode() {
4735        let mut ax = Axes::new();
4736        ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
4737            .unwrap()
4738            .where_step(StepWhere::Mid);
4739        match &ax.artists[0] {
4740            Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Mid)),
4741            _ => panic!("expected Step"),
4742        }
4743    }
4744
4745    // -- Stem chart tests ---------------------------------------------------
4746
4747    #[test]
4748    fn stem_creates_artist() {
4749        let mut ax = Axes::new();
4750        let result = ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
4751        assert!(result.is_ok());
4752        assert!(matches!(&ax.artists[0], Artist::Stem(_)));
4753    }
4754
4755    #[test]
4756    fn stem_length_mismatch() {
4757        let mut ax = Axes::new();
4758        let result = ax.stem(vec![1.0, 2.0], vec![1.0]);
4759        assert!(matches!(
4760            result,
4761            Err(PlotError::SeriesLengthMismatch { .. })
4762        ));
4763    }
4764
4765    #[test]
4766    fn stem_empty_data() {
4767        let mut ax = Axes::new();
4768        let result = ax.stem(Vec::<f64>::new(), Vec::<f64>::new());
4769        assert!(matches!(result, Err(PlotError::EmptyData)));
4770    }
4771
4772    #[test]
4773    fn stem_default_baseline() {
4774        let mut ax = Axes::new();
4775        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4776        match &ax.artists[0] {
4777            Artist::Stem(a) => assert!((a.baseline - 0.0).abs() < 1e-12),
4778            _ => panic!("expected Stem"),
4779        }
4780    }
4781
4782    #[test]
4783    fn stem_default_marker_size() {
4784        let mut ax = Axes::new();
4785        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4786        match &ax.artists[0] {
4787            Artist::Stem(a) => assert!((a.marker_size - 6.0).abs() < 1e-12),
4788            _ => panic!("expected Stem"),
4789        }
4790    }
4791
4792    #[test]
4793    fn stem_builder_chaining() {
4794        let mut ax = Axes::new();
4795        ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
4796            .unwrap()
4797            .color(Color::TAB_GREEN)
4798            .baseline(1.0)
4799            .marker_size(8.0)
4800            .width(2.0)
4801            .label("stems")
4802            .alpha(0.7);
4803        match &ax.artists[0] {
4804            Artist::Stem(a) => {
4805                assert_eq!(a.color, Color::TAB_GREEN);
4806                assert!((a.baseline - 1.0).abs() < 1e-12);
4807                assert!((a.marker_size - 8.0).abs() < 1e-12);
4808                assert!((a.line_width - 2.0).abs() < 1e-12);
4809                assert_eq!(a.label.as_deref(), Some("stems"));
4810                assert!((a.alpha - 0.7).abs() < 1e-12);
4811            }
4812            _ => panic!("expected Stem"),
4813        }
4814    }
4815
4816    #[test]
4817    fn stem_data_bounds_include_baseline() {
4818        let mut ax = Axes::new();
4819        ax.stem(vec![1.0, 5.0], vec![2.0, 8.0])
4820            .unwrap()
4821            .baseline(-5.0);
4822        let (_xmin, _xmax, ymin, _ymax) = ax.compute_data_limits();
4823        assert!(ymin < -5.0);
4824    }
4825
4826    #[test]
4827    fn stem_legend_label() {
4828        let mut ax = Axes::new();
4829        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("L");
4830        assert_eq!(ax.artists[0].label(), Some("L"));
4831    }
4832
4833    #[test]
4834    fn stem_color_cycle() {
4835        let mut ax = Axes::new();
4836        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4837        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4838        let c0 = ax.artists[0].color();
4839        let c1 = ax.artists[1].color();
4840        assert_ne!(c0, c1);
4841    }
4842
4843    #[test]
4844    fn stem_alpha_default() {
4845        let mut ax = Axes::new();
4846        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4847        match &ax.artists[0] {
4848            Artist::Stem(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
4849            _ => panic!("expected Stem"),
4850        }
4851    }
4852
4853    #[test]
4854    fn stem_negative_baseline() {
4855        let mut ax = Axes::new();
4856        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0])
4857            .unwrap()
4858            .baseline(-3.0);
4859        match &ax.artists[0] {
4860            Artist::Stem(a) => assert!((a.baseline - (-3.0)).abs() < 1e-12),
4861            _ => panic!("expected Stem"),
4862        }
4863    }
4864
4865    // -- Text annotation tests -----------------------------------------------
4866
4867    #[test]
4868    fn text_creates_annotation() {
4869        let mut ax = Axes::new();
4870        ax.text(1.0, 2.0, "hello");
4871        assert_eq!(ax.texts.len(), 1);
4872        assert_eq!(ax.texts[0].text, "hello");
4873        assert!((ax.texts[0].x - 1.0).abs() < f64::EPSILON);
4874        assert!((ax.texts[0].y - 2.0).abs() < f64::EPSILON);
4875    }
4876
4877    #[test]
4878    fn text_default_alignment() {
4879        let mut ax = Axes::new();
4880        ax.text(0.0, 0.0, "test");
4881        assert_eq!(ax.texts[0].ha, HAlign::Left);
4882        assert_eq!(ax.texts[0].va, VAlign::Baseline);
4883        assert!((ax.texts[0].rotation - 0.0).abs() < f64::EPSILON);
4884    }
4885
4886    #[test]
4887    fn text_builder_chaining() {
4888        let mut ax = Axes::new();
4889        ax.text(1.0, 2.0, "styled")
4890            .fontsize(14.0)
4891            .color(Color::TAB_RED)
4892            .ha(HAlign::Center)
4893            .va(VAlign::Top)
4894            .rotation(45.0);
4895        let t = &ax.texts[0];
4896        assert_eq!(t.fontsize, Some(14.0));
4897        assert_eq!(t.color, Some(Color::TAB_RED));
4898        assert_eq!(t.ha, HAlign::Center);
4899        assert_eq!(t.va, VAlign::Top);
4900        assert!((t.rotation - 45.0).abs() < f64::EPSILON);
4901    }
4902
4903    #[test]
4904    fn text_does_not_affect_autoscale() {
4905        let mut ax = Axes::new();
4906        ax.plot(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
4907        let limits_before = ax.compute_data_limits();
4908        // Place text far outside the data range.
4909        ax.text(100.0, 100.0, "far away");
4910        let limits_after = ax.compute_data_limits();
4911        assert_eq!(limits_before, limits_after);
4912    }
4913
4914    #[test]
4915    fn multiple_texts() {
4916        let mut ax = Axes::new();
4917        ax.text(1.0, 1.0, "first");
4918        ax.text(2.0, 2.0, "second");
4919        ax.text(3.0, 3.0, "third");
4920        assert_eq!(ax.texts.len(), 3);
4921        assert_eq!(ax.texts[0].text, "first");
4922        assert_eq!(ax.texts[1].text, "second");
4923        assert_eq!(ax.texts[2].text, "third");
4924    }
4925
4926    // -- Annotation tests (annotate with arrow) ------------------------------
4927
4928    #[test]
4929    fn annotate_creates_annotation() {
4930        let mut ax = Axes::new();
4931        ax.annotate("peak", (1.0, 2.0), (3.0, 4.0));
4932        assert_eq!(ax.annotations.len(), 1);
4933        assert_eq!(ax.annotations[0].text, "peak");
4934        assert_eq!(ax.annotations[0].xy, (1.0, 2.0));
4935        assert_eq!(ax.annotations[0].xytext, (3.0, 4.0));
4936    }
4937
4938    #[test]
4939    fn annotate_default_no_arrow() {
4940        let mut ax = Axes::new();
4941        ax.annotate("label", (0.0, 0.0), (1.0, 1.0));
4942        assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::None);
4943    }
4944
4945    #[test]
4946    fn annotate_default_alignment() {
4947        let mut ax = Axes::new();
4948        ax.annotate("label", (0.0, 0.0), (1.0, 1.0));
4949        assert_eq!(ax.annotations[0].ha, HAlign::Center);
4950        assert_eq!(ax.annotations[0].va, VAlign::Bottom);
4951    }
4952
4953    #[test]
4954    fn annotate_with_arrow() {
4955        let mut ax = Axes::new();
4956        ax.annotate("peak", (1.0, 1.0), (2.0, 2.0))
4957            .arrowstyle(ArrowStyle::Simple);
4958        assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::Simple);
4959    }
4960
4961    #[test]
4962    fn annotate_with_fancy_arrow() {
4963        let mut ax = Axes::new();
4964        ax.annotate("label", (0.0, 0.0), (1.0, 1.0))
4965            .arrowstyle(ArrowStyle::Fancy);
4966        assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::Fancy);
4967    }
4968
4969    #[test]
4970    fn annotate_builder_chaining() {
4971        let mut ax = Axes::new();
4972        ax.annotate("note", (1.0, 2.0), (3.0, 4.0))
4973            .fontsize(12.0)
4974            .color(Color::TAB_BLUE)
4975            .ha(HAlign::Right)
4976            .va(VAlign::Top)
4977            .arrowstyle(ArrowStyle::Fancy)
4978            .arrow_color(Color::TAB_RED);
4979        let a = &ax.annotations[0];
4980        assert_eq!(a.fontsize, Some(12.0));
4981        assert_eq!(a.color, Some(Color::TAB_BLUE));
4982        assert_eq!(a.ha, HAlign::Right);
4983        assert_eq!(a.va, VAlign::Top);
4984        assert_eq!(a.arrowstyle, ArrowStyle::Fancy);
4985        assert_eq!(a.arrow_color, Some(Color::TAB_RED));
4986    }
4987
4988    #[test]
4989    fn annotate_does_not_affect_autoscale() {
4990        let mut ax = Axes::new();
4991        ax.plot(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
4992        let limits_before = ax.compute_data_limits();
4993        // Annotate far outside the data range.
4994        ax.annotate("far", (100.0, 100.0), (200.0, 200.0));
4995        let limits_after = ax.compute_data_limits();
4996        assert_eq!(limits_before, limits_after);
4997    }
4998
4999    #[test]
5000    fn multiple_annotations() {
5001        let mut ax = Axes::new();
5002        ax.annotate("a", (0.0, 0.0), (1.0, 1.0));
5003        ax.annotate("b", (2.0, 2.0), (3.0, 3.0));
5004        assert_eq!(ax.annotations.len(), 2);
5005    }
5006
5007    #[test]
5008    fn text_at_plot_boundary() {
5009        let mut ax = Axes::new();
5010        ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
5011        // Place text at the boundary of the data range.
5012        ax.text(0.0, 0.0, "origin");
5013        ax.text(10.0, 10.0, "corner");
5014        assert_eq!(ax.texts.len(), 2);
5015    }
5016
5017    #[test]
5018    fn overlapping_annotations() {
5019        let mut ax = Axes::new();
5020        // Multiple annotations at the same position -- should not panic.
5021        ax.annotate("one", (5.0, 5.0), (6.0, 6.0));
5022        ax.annotate("two", (5.0, 5.0), (6.0, 6.0));
5023        ax.text(6.0, 6.0, "three");
5024        assert_eq!(ax.annotations.len(), 2);
5025        assert_eq!(ax.texts.len(), 1);
5026    }
5027
5028    #[test]
5029    fn new_axes_has_empty_annotations() {
5030        let ax = Axes::new();
5031        assert!(ax.texts.is_empty());
5032        assert!(ax.annotations.is_empty());
5033    }
5034
5035    #[test]
5036    fn text_pixel_placement() {
5037        // Verify that text annotations are positioned using data_to_pixel.
5038        let ax = Axes::new();
5039        let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
5040        // Center of data space (5, 5) with limits (0, 10, 0, 10).
5041        let pt = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
5042        assert!((pt.x - 300.0).abs() < 1e-10);
5043        assert!((pt.y - 200.0).abs() < 1e-10);
5044    }
5045
5046    #[test]
5047    fn annotation_pixel_placement() {
5048        // Verify pixel positions for both xy and xytext.
5049        let ax = Axes::new();
5050        let plot_area = Rect::new(0.0, 0.0, 100.0, 100.0);
5051        let target = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
5052        let text_pos = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
5053        // target at (0, 0) data -> pixel (0, 100) in a y-inverted 100x100 area.
5054        assert!((target.x - 0.0).abs() < 1e-10);
5055        assert!((target.y - 100.0).abs() < 1e-10);
5056        // text at (5, 5) data -> pixel (50, 50).
5057        assert!((text_pos.x - 50.0).abs() < 1e-10);
5058        assert!((text_pos.y - 50.0).abs() < 1e-10);
5059    }
5060
5061    // -- Axis control tests ------------------------------------------------
5062
5063    #[test]
5064    fn set_xlim_overrides_autoscale() {
5065        let mut ax = Axes::new();
5066        ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
5067        ax.set_xlim(2.0, 8.0);
5068        let (xmin, xmax, _, _) = ax.compute_data_limits();
5069        assert!((xmin - 2.0).abs() < f64::EPSILON);
5070        assert!((xmax - 8.0).abs() < f64::EPSILON);
5071    }
5072
5073    #[test]
5074    fn set_ylim_overrides_autoscale() {
5075        let mut ax = Axes::new();
5076        ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
5077        ax.set_ylim(-5.0, 15.0);
5078        let (_, _, ymin, ymax) = ax.compute_data_limits();
5079        assert!((ymin - (-5.0)).abs() < f64::EPSILON);
5080        assert!((ymax - 15.0).abs() < f64::EPSILON);
5081    }
5082
5083    #[test]
5084    fn set_xlim_with_min_greater_than_max() {
5085        let mut ax = Axes::new();
5086        ax.set_xlim(10.0, 2.0);
5087        let (xmin, xmax, _, _) = ax.compute_data_limits();
5088        // User limits are stored as-is; inversion happens separately.
5089        assert!((xmin - 10.0).abs() < f64::EPSILON);
5090        assert!((xmax - 2.0).abs() < f64::EPSILON);
5091    }
5092
5093    #[test]
5094    fn invert_xaxis_swaps_limits() {
5095        let mut ax = Axes::new();
5096        ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
5097        ax.set_xlim(0.0, 10.0);
5098        ax.invert_xaxis();
5099        let (xmin, xmax, _, _) = ax.compute_data_limits();
5100        // After inversion, min and max are swapped.
5101        assert!((xmin - 10.0).abs() < f64::EPSILON);
5102        assert!((xmax - 0.0).abs() < f64::EPSILON);
5103    }
5104
5105    #[test]
5106    fn invert_yaxis_swaps_limits() {
5107        let mut ax = Axes::new();
5108        ax.set_ylim(0.0, 100.0);
5109        ax.invert_yaxis();
5110        let (_, _, ymin, ymax) = ax.compute_data_limits();
5111        assert!((ymin - 100.0).abs() < f64::EPSILON);
5112        assert!((ymax - 0.0).abs() < f64::EPSILON);
5113    }
5114
5115    #[test]
5116    fn custom_xticks_appear_in_output() {
5117        let mut ax = Axes::new();
5118        ax.set_xticks(&[1.0, 2.0, 3.0]);
5119        let ticks = ax.resolve_xticks(0.0, 10.0);
5120        assert_eq!(ticks.len(), 3);
5121        assert!((ticks[0].value - 1.0).abs() < f64::EPSILON);
5122        assert!((ticks[1].value - 2.0).abs() < f64::EPSILON);
5123        assert!((ticks[2].value - 3.0).abs() < f64::EPSILON);
5124    }
5125
5126    #[test]
5127    fn custom_yticks_appear_in_output() {
5128        let mut ax = Axes::new();
5129        ax.set_yticks(&[0.0, 50.0, 100.0]);
5130        let ticks = ax.resolve_yticks(0.0, 100.0);
5131        assert_eq!(ticks.len(), 3);
5132        assert!((ticks[0].value - 0.0).abs() < f64::EPSILON);
5133        assert!((ticks[1].value - 50.0).abs() < f64::EPSILON);
5134        assert!((ticks[2].value - 100.0).abs() < f64::EPSILON);
5135    }
5136
5137    #[test]
5138    fn custom_tick_labels_override_default_format() {
5139        let mut ax = Axes::new();
5140        ax.set_xticks(&[0.0, 3.125, 6.25]);
5141        ax.set_xticklabels(&["0", "pi", "2pi"]);
5142        let ticks = ax.resolve_xticks(0.0, 7.0);
5143        assert_eq!(ticks.len(), 3);
5144        assert_eq!(ticks[0].label, "0");
5145        assert_eq!(ticks[1].label, "pi");
5146        assert_eq!(ticks[2].label, "2pi");
5147    }
5148
5149    #[test]
5150    fn custom_tick_labels_partial_match() {
5151        // Fewer labels than ticks: missing labels fall back to format_tick.
5152        let mut ax = Axes::new();
5153        ax.set_xticks(&[1.0, 2.0, 3.0]);
5154        ax.set_xticklabels(&["one"]);
5155        let ticks = ax.resolve_xticks(0.0, 5.0);
5156        assert_eq!(ticks[0].label, "one");
5157        assert_eq!(ticks[1].label, "2");
5158        assert_eq!(ticks[2].label, "3");
5159    }
5160
5161    #[test]
5162    fn empty_custom_ticks() {
5163        let mut ax = Axes::new();
5164        ax.set_xticks(&[]);
5165        let ticks = ax.resolve_xticks(0.0, 10.0);
5166        assert!(ticks.is_empty());
5167    }
5168
5169    #[test]
5170    fn grid_visibility_toggle() {
5171        let mut ax = Axes::new();
5172        assert!(ax.show_grid.is_none());
5173        ax.grid(true);
5174        assert_eq!(ax.show_grid, Some(true));
5175        ax.grid(false);
5176        assert_eq!(ax.show_grid, Some(false));
5177    }
5178
5179    #[test]
5180    fn grid_axis_setting() {
5181        let mut ax = Axes::new();
5182        assert_eq!(ax.grid_axis, GridAxis::Both);
5183        ax.grid_axis("x");
5184        assert_eq!(ax.grid_axis, GridAxis::X);
5185        ax.grid_axis("y");
5186        assert_eq!(ax.grid_axis, GridAxis::Y);
5187        ax.grid_axis("both");
5188        assert_eq!(ax.grid_axis, GridAxis::Both);
5189    }
5190
5191    #[test]
5192    fn grid_alpha_setting() {
5193        let mut ax = Axes::new();
5194        ax.grid_alpha(0.5);
5195        assert!((ax.grid_alpha.unwrap() - 0.5).abs() < f64::EPSILON);
5196    }
5197
5198    #[test]
5199    fn grid_alpha_clamps() {
5200        let mut ax = Axes::new();
5201        ax.grid_alpha(2.0);
5202        assert!((ax.grid_alpha.unwrap() - 1.0).abs() < f64::EPSILON);
5203        ax.grid_alpha(-0.5);
5204        assert!((ax.grid_alpha.unwrap() - 0.0).abs() < f64::EPSILON);
5205    }
5206
5207    #[test]
5208    fn grid_style_setting() {
5209        let mut ax = Axes::new();
5210        ax.grid_style(crate::theme::LineStyle::Dashed);
5211        assert_eq!(ax.grid_style, Some(crate::theme::LineStyle::Dashed));
5212    }
5213
5214    #[test]
5215    fn tick_rotation_setting() {
5216        let mut ax = Axes::new();
5217        assert!((ax.xtick_rotation - 0.0).abs() < f64::EPSILON);
5218        ax.tick_params_x_rotation(45.0);
5219        assert!((ax.xtick_rotation - 45.0).abs() < f64::EPSILON);
5220        ax.tick_params_y_rotation(-30.0);
5221        assert!((ax.ytick_rotation - (-30.0)).abs() < f64::EPSILON);
5222    }
5223
5224    #[test]
5225    fn axis_control_chaining() {
5226        let mut ax = Axes::new();
5227        ax.set_xlim(0.0, 10.0)
5228            .set_ylim(-1.0, 1.0)
5229            .invert_xaxis()
5230            .grid(true)
5231            .grid_axis("y")
5232            .grid_alpha(0.3)
5233            .grid_style(crate::theme::LineStyle::Dotted)
5234            .set_xticks(&[0.0, 5.0, 10.0])
5235            .set_xticklabels(&["start", "mid", "end"])
5236            .tick_params_x_rotation(90.0);
5237
5238        assert_eq!(ax.xlim, Some((0.0, 10.0)));
5239        assert_eq!(ax.ylim, Some((-1.0, 1.0)));
5240        assert!(ax.x_inverted);
5241        assert_eq!(ax.show_grid, Some(true));
5242        assert_eq!(ax.grid_axis, GridAxis::Y);
5243        assert!((ax.grid_alpha.unwrap() - 0.3).abs() < f64::EPSILON);
5244        assert_eq!(ax.grid_style, Some(crate::theme::LineStyle::Dotted));
5245        assert_eq!(ax.custom_xticks.as_ref().unwrap().len(), 3);
5246        assert_eq!(ax.custom_xticklabels.as_ref().unwrap().len(), 3);
5247        assert!((ax.xtick_rotation - 90.0).abs() < f64::EPSILON);
5248    }
5249
5250    #[test]
5251    fn new_axes_has_axis_control_defaults() {
5252        let ax = Axes::new();
5253        assert_eq!(ax.grid_axis, GridAxis::Both);
5254        assert!(ax.grid_alpha.is_none());
5255        assert!(ax.grid_style.is_none());
5256        assert!(!ax.x_inverted);
5257        assert!(!ax.y_inverted);
5258        assert!(ax.custom_xticks.is_none());
5259        assert!(ax.custom_yticks.is_none());
5260        assert!(ax.custom_xticklabels.is_none());
5261        assert!(ax.custom_yticklabels.is_none());
5262        assert!((ax.xtick_rotation - 0.0).abs() < f64::EPSILON);
5263        assert!((ax.ytick_rotation - 0.0).abs() < f64::EPSILON);
5264    }
5265
5266    #[test]
5267    fn autoscale_not_broken_without_user_limits() {
5268        let mut ax = Axes::new();
5269        ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
5270        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
5271        // Autoscale should still pad around data.
5272        assert!(xmin < 1.0);
5273        assert!(xmax > 10.0);
5274        assert!(ymin < 2.0);
5275        assert!(ymax > 8.0);
5276    }
5277
5278    #[test]
5279    fn resolve_xticks_auto_when_not_set() {
5280        let ax = Axes::new();
5281        let ticks = ax.resolve_xticks(0.0, 10.0);
5282        // Should produce auto-generated ticks (non-empty).
5283        assert!(!ticks.is_empty());
5284    }
5285
5286    #[test]
5287    fn resolve_yticks_auto_when_not_set() {
5288        let ax = Axes::new();
5289        let ticks = ax.resolve_yticks(0.0, 100.0);
5290        assert!(!ticks.is_empty());
5291    }
5292
5293    // -- Log scale tests ---------------------------------------------------
5294
5295    #[test]
5296    fn data_to_pixel_log10() {
5297        let mut ax = Axes::new();
5298        ax.set_xscale(Scale::Log10);
5299        let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
5300
5301        // 1.0 maps to the left edge in [1, 1000] log range.
5302        let p = ax.data_to_pixel(1.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5303        assert!(
5304            (p.x - 100.0).abs() < 1e-6,
5305            "log10(1)=0 should be left edge, got {}",
5306            p.x
5307        );
5308
5309        // 1000 maps to the right edge.
5310        let p = ax.data_to_pixel(1000.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5311        assert!(
5312            (p.x - 500.0).abs() < 1e-6,
5313            "log10(1000)=3 should be right edge, got {}",
5314            p.x
5315        );
5316
5317        // 10 is 1/3 of the way (log10(10)=1 out of 3 decades).
5318        let p = ax.data_to_pixel(10.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5319        let expected_x = 100.0 + 400.0 / 3.0;
5320        assert!(
5321            (p.x - expected_x).abs() < 1e-6,
5322            "log10(10)=1/3 of range, expected {}, got {}",
5323            expected_x,
5324            p.x
5325        );
5326
5327        // 100 is 2/3 of the way.
5328        let p = ax.data_to_pixel(100.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5329        let expected_x = 100.0 + 400.0 * 2.0 / 3.0;
5330        assert!(
5331            (p.x - expected_x).abs() < 1e-6,
5332            "log10(100)=2/3 of range, expected {}, got {}",
5333            expected_x,
5334            p.x
5335        );
5336    }
5337
5338    #[test]
5339    fn data_to_pixel_log10_y() {
5340        let mut ax = Axes::new();
5341        ax.set_yscale(Scale::Log10);
5342        let plot_area = Rect::new(0.0, 0.0, 400.0, 300.0);
5343
5344        // ymin=1, ymax=100 (2 decades). y=10 is the midpoint.
5345        let p = ax.data_to_pixel(0.0, 10.0, &plot_area, 0.0, 10.0, 1.0, 100.0);
5346        // ty = (log10(10) - log10(1)) / (log10(100) - log10(1)) = 1/2 = 0.5
5347        // pixel_y = 0 + (1 - 0.5) * 300 = 150
5348        assert!(
5349            (p.y - 150.0).abs() < 1e-6,
5350            "log10(10) should be vertical center, got {}",
5351            p.y
5352        );
5353    }
5354
5355    #[test]
5356    fn compute_data_limits_log_clamps_positive() {
5357        let mut ax = Axes::new();
5358        ax.set_xscale(Scale::Log10);
5359        ax.set_yscale(Scale::Log10);
5360        ax.plot(vec![0.1, 1.0, 10.0], vec![1.0, 10.0, 100.0])
5361            .unwrap();
5362        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
5363        assert!(xmin > 0.0, "log x-min must be positive, got {}", xmin);
5364        assert!(xmax > xmin, "log x-max must be > x-min");
5365        assert!(ymin > 0.0, "log y-min must be positive, got {}", ymin);
5366        assert!(ymax > ymin, "log y-max must be > y-min");
5367    }
5368
5369    #[test]
5370    fn compute_data_limits_log_with_zeros() {
5371        // Data includes zero, which is invalid for log10 -- should clamp.
5372        let mut ax = Axes::new();
5373        ax.set_xscale(Scale::Log10);
5374        ax.plot(vec![0.0, 1.0, 10.0], vec![1.0, 2.0, 3.0]).unwrap();
5375        let (xmin, xmax, _ymin, _ymax) = ax.compute_data_limits();
5376        assert!(
5377            xmin > 0.0,
5378            "log x-min should be clamped positive, got {}",
5379            xmin
5380        );
5381        assert!(xmax > xmin);
5382    }
5383
5384    #[test]
5385    fn data_to_pixel_symlog() {
5386        let mut ax = Axes::new();
5387        ax.set_xscale(Scale::SymLog { linthresh: 1.0 });
5388        let plot_area = Rect::new(0.0, 0.0, 400.0, 300.0);
5389
5390        // For a symmetric range [-100, 100], zero should be at the center.
5391        let p = ax.data_to_pixel(0.0, 0.0, &plot_area, -100.0, 100.0, 0.0, 1.0);
5392        assert!(
5393            (p.x - 200.0).abs() < 1e-6,
5394            "symlog(0) should be center for symmetric range, got {}",
5395            p.x
5396        );
5397    }
5398
5399    #[test]
5400    fn set_scale_methods_return_self() {
5401        let mut ax = Axes::new();
5402        ax.set_xscale(Scale::Log10)
5403            .set_yscale(Scale::Log10)
5404            .set_title("Log plot");
5405        assert!(matches!(ax.xscale, Scale::Log10));
5406        assert!(matches!(ax.yscale, Scale::Log10));
5407        assert_eq!(ax.title.as_deref(), Some("Log plot"));
5408    }
5409
5410    #[test]
5411    fn compute_data_limits_log_no_artists() {
5412        let mut ax = Axes::new();
5413        ax.set_xscale(Scale::Log10);
5414        ax.set_yscale(Scale::Log10);
5415        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
5416        assert!(xmin > 0.0, "default log x-min must be positive");
5417        assert!(xmax > xmin);
5418        assert!(ymin > 0.0, "default log y-min must be positive");
5419        assert!(ymax > ymin);
5420    }
5421
5422    #[test]
5423    fn data_to_pixel_log10_very_large_range() {
5424        let mut ax = Axes::new();
5425        ax.set_xscale(Scale::Log10);
5426        let plot_area = Rect::new(0.0, 0.0, 1000.0, 100.0);
5427
5428        // 10 decades: 1e-5 to 1e5
5429        let p_lo = ax.data_to_pixel(1e-5, 0.0, &plot_area, 1e-5, 1e5, 0.0, 1.0);
5430        let p_hi = ax.data_to_pixel(1e5, 0.0, &plot_area, 1e-5, 1e5, 0.0, 1.0);
5431        assert!((p_lo.x - 0.0).abs() < 1e-6);
5432        assert!((p_hi.x - 1000.0).abs() < 1e-6);
5433    }
5434
5435    // -----------------------------------------------------------------------
5436    // Twin axes tests
5437    // -----------------------------------------------------------------------
5438
5439    #[test]
5440    fn new_axes_has_no_twin_fields() {
5441        let ax = Axes::new();
5442        assert!(!ax.is_twin());
5443        assert_eq!(ax.twin_side(), None);
5444    }
5445
5446    // -----------------------------------------------------------------------
5447    // Colorbar tests
5448    // -----------------------------------------------------------------------
5449
5450    #[test]
5451    fn colorbar_attaches_to_axes() {
5452        let mut ax = Axes::new();
5453        let cb = ax.colorbar(crate::colormap::Colormap::Viridis, 0.0, 1.0);
5454        cb.set_label("Test");
5455        assert!(ax.colorbar.is_some());
5456        assert_eq!(ax.colorbar.as_ref().unwrap().label.as_deref(), Some("Test"));
5457    }
5458
5459    #[test]
5460    fn colorbar_replaces_previous() {
5461        let mut ax = Axes::new();
5462        ax.colorbar(crate::colormap::Colormap::Viridis, 0.0, 1.0);
5463        ax.colorbar(crate::colormap::Colormap::Plasma, -10.0, 10.0);
5464        let cb = ax.colorbar.as_ref().unwrap();
5465        assert_eq!(cb.cmap, crate::colormap::Colormap::Plasma);
5466        assert!((cb.vmin - (-10.0)).abs() < f64::EPSILON);
5467        assert!((cb.vmax - 10.0).abs() < f64::EPSILON);
5468    }
5469
5470    #[test]
5471    fn heatmap_auto_colorbar() {
5472        let mut ax = Axes::new();
5473        ax.heatmap(vec![vec![1.0, 2.0], vec![3.0, 4.0]])
5474            .unwrap()
5475            .colorbar(true);
5476        let auto_cb = ax.auto_colorbar_from_artists();
5477        assert!(auto_cb.is_some());
5478        let cb = auto_cb.unwrap();
5479        assert!((cb.vmin - 1.0).abs() < f64::EPSILON);
5480        assert!((cb.vmax - 4.0).abs() < f64::EPSILON);
5481    }
5482
5483    #[test]
5484    fn heatmap_no_auto_colorbar_by_default() {
5485        let mut ax = Axes::new();
5486        ax.heatmap(vec![vec![1.0, 2.0]]).unwrap();
5487        let auto_cb = ax.auto_colorbar_from_artists();
5488        assert!(auto_cb.is_none());
5489    }
5490
5491    #[test]
5492    fn new_axes_has_no_colorbar() {
5493        let ax = Axes::new();
5494        assert!(ax.colorbar.is_none());
5495    }
5496}