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