Skip to main content

plotkit_core/
axes.rs

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