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: None,
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        };
271        self.artists.push(Artist::Scatter(artist));
272        match self.artists.last_mut().expect("just pushed") {
273            Artist::Scatter(a) => Ok(a),
274            _ => unreachable!(),
275        }
276    }
277
278    /// Creates a vertical bar chart from categorical data and heights.
279    ///
280    /// Each category label maps to one bar whose height is the corresponding
281    /// value in `heights`.
282    ///
283    /// # Errors
284    ///
285    /// Returns [`PlotError::SeriesLengthMismatch`] if `categories` and
286    /// `heights` have different lengths, or [`PlotError::EmptyData`] if empty.
287    pub fn bar<C, H>(&mut self, categories: C, heights: H) -> Result<&mut BarArtist>
288    where
289        C: IntoCategories,
290        H: IntoSeries,
291    {
292        let cats = categories.into_categories();
293        let vals = heights.into_series();
294        if cats.len() != vals.len() {
295            return Err(PlotError::SeriesLengthMismatch {
296                expected: cats.len(),
297                got: vals.len(),
298            });
299        }
300        if cats.is_empty() {
301            return Err(PlotError::EmptyData);
302        }
303        let color = Color::TABLEAU_10[self.color_index % 10];
304        self.color_index += 1;
305        let artist = BarArtist {
306            categories: cats,
307            heights: vals,
308            color,
309            horizontal: false,
310            bar_width: 0.8,
311            label: None,
312            alpha: 1.0,
313            bottom: None,
314            offset: None,
315        };
316        self.artists.push(Artist::Bar(artist));
317        match self.artists.last_mut().expect("just pushed") {
318            Artist::Bar(a) => Ok(a),
319            _ => unreachable!(),
320        }
321    }
322
323    /// Creates a horizontal bar chart from categorical data and widths.
324    ///
325    /// Behaves like [`bar`](Axes::bar) but draws horizontal bars from the
326    /// y-axis.
327    ///
328    /// # Errors
329    ///
330    /// Same as [`bar`](Axes::bar).
331    pub fn barh<C, W>(&mut self, categories: C, widths: W) -> Result<&mut BarArtist>
332    where
333        C: IntoCategories,
334        W: IntoSeries,
335    {
336        let cats = categories.into_categories();
337        let vals = widths.into_series();
338        if cats.len() != vals.len() {
339            return Err(PlotError::SeriesLengthMismatch {
340                expected: cats.len(),
341                got: vals.len(),
342            });
343        }
344        if cats.is_empty() {
345            return Err(PlotError::EmptyData);
346        }
347        let color = Color::TABLEAU_10[self.color_index % 10];
348        self.color_index += 1;
349        let artist = BarArtist {
350            categories: cats,
351            heights: vals,
352            color,
353            horizontal: true,
354            bar_width: 0.8,
355            label: None,
356            alpha: 1.0,
357            bottom: None,
358            offset: None,
359        };
360        self.artists.push(Artist::Bar(artist));
361        match self.artists.last_mut().expect("just pushed") {
362            Artist::Bar(a) => Ok(a),
363            _ => unreachable!(),
364        }
365    }
366
367    /// Creates a histogram from raw data.
368    ///
369    /// The data is partitioned into `bins` equal-width bins spanning the data
370    /// range, and each bin is drawn as a vertical bar whose height equals the
371    /// count of values falling in that bin.
372    ///
373    /// # Errors
374    ///
375    /// Returns [`PlotError::EmptyData`] if `data` is empty.
376    pub fn hist<D>(&mut self, data: D, bins: usize) -> Result<&mut HistArtist>
377    where
378        D: IntoSeries,
379    {
380        let series = data.into_series();
381        if series.is_empty() {
382            return Err(PlotError::EmptyData);
383        }
384        let bins = bins.max(1);
385
386        // Compute range from finite values.
387        let (data_min, data_max) = series.bounds().unwrap_or((0.0, 1.0));
388
389        // Handle degenerate case where all values are identical.
390        let (lo, hi) = if (data_max - data_min).abs() < f64::EPSILON {
391            (data_min - 0.5, data_max + 0.5)
392        } else {
393            (data_min, data_max)
394        };
395
396        let bin_width = (hi - lo) / bins as f64;
397
398        // Build bin edges: bins+1 edges.
399        let mut edges: Vec<f64> = (0..=bins).map(|i| lo + i as f64 * bin_width).collect();
400        // Ensure the last edge exactly equals hi to avoid floating-point gaps.
401        *edges.last_mut().expect("edges is non-empty") = hi;
402
403        // Count values per bin.
404        let mut counts = vec![0.0f64; bins];
405        for &v in &series.data {
406            if !v.is_finite() {
407                continue;
408            }
409            // Determine bin index.
410            let idx = if v >= hi {
411                // Values exactly at the upper edge go into the last bin.
412                bins - 1
413            } else {
414                let raw = ((v - lo) / bin_width) as usize;
415                raw.min(bins - 1)
416            };
417            counts[idx] += 1.0;
418        }
419
420        let color = Color::TABLEAU_10[self.color_index % 10];
421        self.color_index += 1;
422        let artist = HistArtist {
423            data: series,
424            bins,
425            bin_edges: edges,
426            counts,
427            color,
428            label: None,
429            alpha: 0.85,
430            density: false,
431        };
432
433
434
435
436
437
438
439        self.artists.push(Artist::Histogram(artist));
440        match self.artists.last_mut().expect("just pushed") {
441            Artist::Histogram(a) => Ok(a),
442            _ => unreachable!(),
443        }
444    }
445
446    /// Fills the area between two y-series that share the same x values.
447    ///
448    /// Useful for confidence intervals, envelopes, and area charts.
449    ///
450    /// # Errors
451    ///
452    /// Returns [`PlotError::SeriesLengthMismatch`] if any of the three
453    /// series differ in length, or [`PlotError::EmptyData`] if empty.
454    pub fn fill_between<X, Y1, Y2>(
455        &mut self,
456        x: X,
457        y1: Y1,
458        y2: Y2,
459    ) -> Result<&mut FillBetweenArtist>
460    where
461        X: IntoSeries,
462        Y1: IntoSeries,
463        Y2: IntoSeries,
464    {
465        let xs = x.into_series();
466        let y1s = y1.into_series();
467        let y2s = y2.into_series();
468        if xs.len() != y1s.len() {
469            return Err(PlotError::SeriesLengthMismatch {
470                expected: xs.len(),
471                got: y1s.len(),
472            });
473        }
474        if xs.len() != y2s.len() {
475            return Err(PlotError::SeriesLengthMismatch {
476                expected: xs.len(),
477                got: y2s.len(),
478            });
479        }
480        if xs.is_empty() {
481            return Err(PlotError::EmptyData);
482        }
483        let color = Color::TABLEAU_10[self.color_index % 10];
484        self.color_index += 1;
485        let artist = FillBetweenArtist {
486            x: xs,
487            y1: y1s,
488            y2: y2s,
489            color,
490            label: None,
491            alpha: 0.3,
492        };
493        self.artists.push(Artist::FillBetween(artist));
494        match self.artists.last_mut().expect("just pushed") {
495            Artist::FillBetween(a) => Ok(a),
496            _ => unreachable!(),
497        }
498    }
499
500    /// Plots a step (staircase) chart connecting `(x, y)` data points.
501    ///
502    /// Returns a mutable reference to the [`StepArtist`] for chaining.
503    ///
504    /// # Errors
505    ///
506    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
507    /// on invalid input.
508    pub fn step<X: IntoSeries, Y: IntoSeries>(
509        &mut self,
510        x: X,
511        y: Y,
512    ) -> Result<&mut StepArtist> {
513        let xs = x.into_series();
514        let ys = y.into_series();
515        if xs.len() != ys.len() {
516            return Err(PlotError::SeriesLengthMismatch {
517                expected: xs.len(),
518                got: ys.len(),
519            });
520        }
521        if xs.is_empty() {
522            return Err(PlotError::EmptyData);
523        }
524        let color = Color::TABLEAU_10[self.color_index % 10];
525        self.color_index += 1;
526        let artist = StepArtist {
527            x: xs,
528            y: ys,
529            color,
530            width: 1.5,
531            where_step: StepWhere::Pre,
532            label: None,
533            alpha: 1.0,
534        };
535        self.artists.push(Artist::Step(artist));
536        match self.artists.last_mut().expect("just pushed") {
537            Artist::Step(a) => Ok(a),
538            _ => unreachable!(),
539        }
540    }
541
542    /// Plots a stem (lollipop) chart from `(x, y)` data points.
543    ///
544    /// Returns a mutable reference to the [`StemArtist`] for chaining.
545    ///
546    /// # Errors
547    ///
548    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
549    /// on invalid input.
550    pub fn stem<X: IntoSeries, Y: IntoSeries>(
551        &mut self,
552        x: X,
553        y: Y,
554    ) -> Result<&mut StemArtist> {
555        let xs = x.into_series();
556        let ys = y.into_series();
557        if xs.len() != ys.len() {
558            return Err(PlotError::SeriesLengthMismatch {
559                expected: xs.len(),
560                got: ys.len(),
561            });
562        }
563        if xs.is_empty() {
564            return Err(PlotError::EmptyData);
565        }
566        let color = Color::TABLEAU_10[self.color_index % 10];
567        self.color_index += 1;
568        let artist = StemArtist {
569            x: xs,
570            y: ys,
571            color,
572            line_width: 1.5,
573            marker_size: 6.0,
574            baseline: 0.0,
575            label: None,
576            alpha: 1.0,
577        };
578        self.artists.push(Artist::Stem(artist));
579        match self.artists.last_mut().expect("just pushed") {
580            Artist::Stem(a) => Ok(a),
581            _ => unreachable!(),
582        }
583    }
584
585    /// Creates a box-and-whisker plot from one or more data groups.
586    ///
587    /// Each inner `Vec<f64>` produces one box. Groups are placed at integer
588    /// x-positions starting from 0.
589    ///
590    /// # Errors
591    ///
592    /// Returns [`PlotError::EmptyData`] if `datasets` is empty.
593    pub fn boxplot(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut BoxPlotArtist> {
594        use crate::charts::boxplot::compute_stats;
595        if datasets.is_empty() {
596            return Err(PlotError::EmptyData);
597        }
598        let color = Color::TABLEAU_10[self.color_index % 10];
599        self.color_index += 1;
600        let factor = 1.5;
601        let stats: Vec<_> = datasets.iter().map(|d| compute_stats(d, factor)).collect();
602        let labels: Vec<String> = (0..datasets.len()).map(|i| format!("{}", i + 1)).collect();
603        let artist = BoxPlotArtist {
604            stats,
605            labels,
606            color,
607            label: None,
608            alpha: 1.0,
609            box_width: 0.5,
610            show_outliers: true,
611            whisker_iq_factor: factor,
612            raw_data: datasets,
613        };
614        self.artists.push(Artist::BoxPlot(artist));
615        match self.artists.last_mut().expect("just pushed") {
616            Artist::BoxPlot(a) => Ok(a),
617            _ => unreachable!(),
618        }
619    }
620
621    /// Creates an error bar plot from `(x, y)` data points.
622    ///
623    /// Returns an owned [`ErrorBarArtist`] that can be configured with
624    /// builder methods (`.yerr_symmetric()`, `.cap_size()`, etc.) and then
625    /// added to the axes via [`add_errorbar`](Axes::add_errorbar).
626    ///
627    /// # Errors
628    ///
629    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
630    /// on invalid input.
631    pub fn errorbar<X: IntoSeries, Y: IntoSeries>(
632        &mut self,
633        x: X,
634        y: Y,
635    ) -> Result<ErrorBarArtist> {
636        let xs = x.into_series();
637        let ys = y.into_series();
638        if xs.len() != ys.len() {
639            return Err(PlotError::SeriesLengthMismatch {
640                expected: xs.len(),
641                got: ys.len(),
642            });
643        }
644        if xs.is_empty() {
645            return Err(PlotError::EmptyData);
646        }
647        let color = Color::TABLEAU_10[self.color_index % 10];
648        self.color_index += 1;
649        Ok(ErrorBarArtist {
650            x: xs,
651            y: ys,
652            xerr: None,
653            yerr: None,
654            color,
655            label: None,
656            cap_size: 4.0,
657            line_width: 1.0,
658        })
659    }
660
661    /// Adds a finalized [`ErrorBarArtist`] to the axes.
662    ///
663    /// Use this after configuring the artist returned by
664    /// [`errorbar`](Axes::errorbar).
665    pub fn add_errorbar(&mut self, artist: ErrorBarArtist) {
666        self.artists.push(Artist::ErrorBar(artist));
667    }
668
669    /// Creates a heatmap from a 2D grid of values.
670    ///
671    /// Returns a mutable reference to the [`HeatmapArtist`] for chaining
672    /// builder methods (`.colormap()`, `.vmin()`, `.show_values()`, etc.).
673    ///
674    /// # Errors
675    ///
676    /// Returns [`PlotError::EmptyData`] if `data` is empty.
677    pub fn heatmap(&mut self, data: Vec<Vec<f64>>) -> Result<&mut HeatmapArtist> {
678        if data.is_empty() {
679            return Err(PlotError::EmptyData);
680        }
681        let color = Color::TABLEAU_10[self.color_index % 10];
682        self.color_index += 1;
683        let artist = HeatmapArtist {
684            data,
685            x_labels: None,
686            y_labels: None,
687            cmap: crate::colormap::Colormap::Viridis,
688            vmin: None,
689            vmax: None,
690            show_values: false,
691            color,
692            label: None,
693            show_colorbar: false,
694        };
695        self.artists.push(Artist::Heatmap(artist));
696        match self.artists.last_mut().expect("just pushed") {
697            Artist::Heatmap(a) => Ok(a),
698            _ => unreachable!(),
699        }
700    }
701
702    /// Creates a pie chart from wedge sizes.
703    ///
704    /// The `sizes` are automatically normalised so that all wedges sum to
705    /// a full circle. Returns a mutable reference to the [`PieArtist`]
706    /// for chaining builder methods (`.labels()`, `.autopct()`,
707    /// `.explode()`, etc.).
708    ///
709    /// # Errors
710    ///
711    /// Returns [`PlotError::EmptyData`] if `sizes` is empty.
712    pub fn pie<S: IntoSeries>(&mut self, sizes: S) -> Result<&mut PieArtist> {
713        let series = sizes.into_series();
714        if series.is_empty() {
715            return Err(PlotError::EmptyData);
716        }
717        let color = Color::TABLEAU_10[self.color_index % 10];
718        self.color_index += 1;
719        let artist = PieArtist {
720            sizes: series.data,
721            labels: None,
722            colors: None,
723            explode: None,
724            autopct: false,
725            start_angle: 90.0,
726            radius: 1.0,
727            label: None,
728            color,
729        };
730        self.artists.push(Artist::Pie(artist));
731        match self.artists.last_mut().expect("just pushed") {
732            Artist::Pie(a) => Ok(a),
733            _ => unreachable!(),
734        }
735    }
736}
737
738// ---------------------------------------------------------------------------
739// Configuration methods (builder-style, return &mut Self)
740// ---------------------------------------------------------------------------
741
742impl Axes {
743    /// Sets the title displayed above the axes area.
744    pub fn set_title(&mut self, title: &str) -> &mut Self {
745        self.title = Some(title.to_string());
746        self
747    }
748
749    /// Sets the x-axis label.
750    pub fn set_xlabel(&mut self, label: &str) -> &mut Self {
751        self.xlabel = Some(label.to_string());
752        self
753    }
754
755    /// Sets the y-axis label.
756    pub fn set_ylabel(&mut self, label: &str) -> &mut Self {
757        self.ylabel = Some(label.to_string());
758        self
759    }
760
761    /// Sets explicit x-axis limits. Pass `(min, max)`.
762    pub fn set_xlim(&mut self, min: f64, max: f64) -> &mut Self {
763        self.xlim = Some((min, max));
764        self
765    }
766
767    /// Sets explicit y-axis limits. Pass `(min, max)`.
768    pub fn set_ylim(&mut self, min: f64, max: f64) -> &mut Self {
769        self.ylim = Some((min, max));
770        self
771    }
772
773    /// Sets the x-axis scale (linear, log10, symlog).
774    pub fn set_xscale(&mut self, scale: Scale) -> &mut Self {
775        self.xscale = scale;
776        self
777    }
778
779    /// Sets the y-axis scale (linear, log10, symlog).
780    pub fn set_yscale(&mut self, scale: Scale) -> &mut Self {
781        self.yscale = scale;
782        self
783    }
784
785    /// Enables or disables grid lines on this axes.
786    pub fn grid(&mut self, show: bool) -> &mut Self {
787        self.show_grid = Some(show);
788        self
789    }
790
791    /// Enables the legend on this axes.
792    pub fn legend(&mut self) -> &mut Self {
793        self.show_legend = true;
794        self
795    }
796
797    /// Sets the legend location.
798    pub fn set_legend_loc(&mut self, loc: Loc) -> &mut Self {
799        self.legend_loc = loc;
800        self
801    }
802
803    /// Overrides the figure theme for this axes only.
804    pub fn set_theme(&mut self, theme: Theme) -> &mut Self {
805        self.theme_override = Some(theme);
806        self
807    }
808
809    /// Sets which axes display grid lines.
810    ///
811    /// Accepts `"x"`, `"y"`, or `"both"` (the default). Unrecognised values
812    /// are silently treated as `"both"`.
813    pub fn grid_axis(&mut self, axis: &str) -> &mut Self {
814        self.grid_axis = match axis {
815            "x" => GridAxis::X,
816            "y" => GridAxis::Y,
817            _ => GridAxis::Both,
818        };
819        self
820    }
821
822    /// Sets the grid line opacity (0.0 = fully transparent, 1.0 = fully opaque).
823    pub fn grid_alpha(&mut self, alpha: f64) -> &mut Self {
824        self.grid_alpha = Some(alpha.clamp(0.0, 1.0));
825        self
826    }
827
828    /// Sets the grid line style.
829    pub fn grid_style(&mut self, style: LineStyle) -> &mut Self {
830        self.grid_style = Some(style);
831        self
832    }
833
834    /// Inverts the x-axis so that larger values appear on the left.
835    pub fn invert_xaxis(&mut self) -> &mut Self {
836        self.x_inverted = true;
837        self
838    }
839
840    /// Inverts the y-axis so that larger values appear at the bottom.
841    pub fn invert_yaxis(&mut self) -> &mut Self {
842        self.y_inverted = true;
843        self
844    }
845
846    /// Sets custom tick positions on the x-axis.
847    ///
848    /// When set, the auto-generated ticks are replaced by these positions.
849    pub fn set_xticks(&mut self, ticks: &[f64]) -> &mut Self {
850        self.custom_xticks = Some(ticks.to_vec());
851        self
852    }
853
854    /// Sets custom tick positions on the y-axis.
855    ///
856    /// When set, the auto-generated ticks are replaced by these positions.
857    pub fn set_yticks(&mut self, ticks: &[f64]) -> &mut Self {
858        self.custom_yticks = Some(ticks.to_vec());
859        self
860    }
861
862    /// Sets custom tick labels for the x-axis.
863    ///
864    /// The number of labels should match the number of custom tick positions
865    /// set via [`set_xticks`](Axes::set_xticks). Extra labels are ignored;
866    /// missing labels default to the formatted tick value.
867    pub fn set_xticklabels(&mut self, labels: &[&str]) -> &mut Self {
868        self.custom_xticklabels = Some(labels.iter().map(|s| s.to_string()).collect());
869        self
870    }
871
872    /// Sets custom tick labels for the y-axis.
873    ///
874    /// The number of labels should match the number of custom tick positions
875    /// set via [`set_yticks`](Axes::set_yticks). Extra labels are ignored;
876    /// missing labels default to the formatted tick value.
877    pub fn set_yticklabels(&mut self, labels: &[&str]) -> &mut Self {
878        self.custom_yticklabels = Some(labels.iter().map(|s| s.to_string()).collect());
879        self
880    }
881
882    /// Sets the rotation angle (in degrees) for x-axis tick labels.
883    pub fn tick_params_x_rotation(&mut self, degrees: f64) -> &mut Self {
884        self.xtick_rotation = degrees;
885        self
886    }
887
888    /// Sets the rotation angle (in degrees) for y-axis tick labels.
889    pub fn tick_params_y_rotation(&mut self, degrees: f64) -> &mut Self {
890        self.ytick_rotation = degrees;
891        self
892    }
893
894    /// Places a text label at data coordinates `(x, y)`.
895    ///
896    /// Returns a mutable reference to the [`TextAnnotation`] for builder-style
897    /// configuration (`.fontsize()`, `.color()`, `.ha()`, `.va()`, `.rotation()`).
898    ///
899    /// Text annotations do **not** affect autoscale; they annotate existing
900    /// data without expanding the axis limits.
901    pub fn text(&mut self, x: f64, y: f64, text: &str) -> &mut TextAnnotation {
902        self.texts.push(TextAnnotation {
903            text: text.to_string(),
904            x,
905            y,
906            fontsize: None,
907            color: None,
908            ha: HAlign::Left,
909            va: VAlign::Baseline,
910            rotation: 0.0,
911        });
912        self.texts.last_mut().expect("just pushed")
913    }
914
915    /// Annotates the data point `xy` with `text` placed at `xytext`.
916    ///
917    /// Returns a mutable reference to the [`Annotation`] for builder-style
918    /// configuration (`.fontsize()`, `.color()`, `.ha()`, `.va()`,
919    /// `.arrowstyle()`, `.arrow_color()`).
920    ///
921    /// When an [`ArrowStyle`] other than [`ArrowStyle::None`] is set, an arrow
922    /// is drawn from `xytext` to `xy`. Annotations do **not** affect autoscale.
923    pub fn annotate(&mut self, text: &str, xy: (f64, f64), xytext: (f64, f64)) -> &mut Annotation {
924        self.annotations.push(Annotation {
925            text: text.to_string(),
926            xy,
927            xytext,
928            fontsize: None,
929            color: None,
930            ha: HAlign::Center,
931            va: VAlign::Bottom,
932            arrowstyle: ArrowStyle::None,
933            arrow_color: None,
934        });
935        self.annotations.last_mut().expect("just pushed")
936    }
937}
938
939// ---------------------------------------------------------------------------
940// Colorbar
941// ---------------------------------------------------------------------------
942
943impl Axes {
944    /// Adds a colorbar to this axes.
945    pub fn colorbar(&mut self, cmap: crate::colormap::Colormap, vmin: f64, vmax: f64) -> &mut Colorbar {
946        self.colorbar = Some(Colorbar::new(cmap, vmin, vmax));
947        self.colorbar.as_mut().expect("just set")
948    }
949}
950
951// ---------------------------------------------------------------------------
952// Contour, Violin, Bar group
953// ---------------------------------------------------------------------------
954
955impl Axes {
956    /// Creates a contour plot (iso-lines) from a 2D grid of z values.
957    pub fn contour(
958        &mut self,
959        x: &[f64],
960        y: &[f64],
961        z: Vec<Vec<f64>>,
962    ) -> Result<&mut ContourArtist> {
963        if x.is_empty() || y.is_empty() || z.is_empty() {
964            return Err(PlotError::EmptyData);
965        }
966        let color = Color::TABLEAU_10[self.color_index % 10];
967        self.color_index += 1;
968        let artist = ContourArtist {
969            x: x.to_vec(),
970            y: y.to_vec(),
971            z,
972            levels: None,
973            filled: false,
974            cmap: crate::colormap::Colormap::Viridis,
975            colors: None,
976            linewidths: 1.0,
977            label: None,
978            color,
979            num_levels: 10,
980        };
981        self.artists.push(Artist::Contour(artist));
982        match self.artists.last_mut().expect("just pushed") {
983            Artist::Contour(a) => Ok(a),
984            _ => unreachable!(),
985        }
986    }
987
988    /// Creates a filled contour plot from a 2D grid of z values.
989    pub fn contourf(
990        &mut self,
991        x: &[f64],
992        y: &[f64],
993        z: Vec<Vec<f64>>,
994    ) -> Result<&mut ContourArtist> {
995        if x.is_empty() || y.is_empty() || z.is_empty() {
996            return Err(PlotError::EmptyData);
997        }
998        let color = Color::TABLEAU_10[self.color_index % 10];
999        self.color_index += 1;
1000        let artist = ContourArtist {
1001            x: x.to_vec(),
1002            y: y.to_vec(),
1003            z,
1004            levels: None,
1005            filled: true,
1006            cmap: crate::colormap::Colormap::Viridis,
1007            colors: None,
1008            linewidths: 1.0,
1009            label: None,
1010            color,
1011            num_levels: 10,
1012        };
1013        self.artists.push(Artist::Contour(artist));
1014        match self.artists.last_mut().expect("just pushed") {
1015            Artist::Contour(a) => Ok(a),
1016            _ => unreachable!(),
1017        }
1018    }
1019
1020    /// Creates a violin plot from one or more datasets.
1021    pub fn violin(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut ViolinArtist> {
1022        if datasets.is_empty() {
1023            return Err(PlotError::EmptyData);
1024        }
1025        let color = Color::TABLEAU_10[self.color_index % 10];
1026        self.color_index += 1;
1027        let artist = ViolinArtist {
1028            datasets,
1029            positions: None,
1030            widths: 0.7,
1031            show_median: true,
1032            show_quartiles: true,
1033            color,
1034            alpha: 0.7,
1035            label: None,
1036            bw_method: 0.0,
1037        };
1038        self.artists.push(Artist::Violin(artist));
1039        match self.artists.last_mut().expect("just pushed") {
1040            Artist::Violin(a) => Ok(a),
1041            _ => unreachable!(),
1042        }
1043    }
1044
1045    /// Creates grouped (side-by-side) bars for multiple series.
1046    ///
1047    /// `series` is a slice of `(label, heights)` pairs. Each series gets
1048    /// bars placed side-by-side within each category.
1049    pub fn bar_group<C: IntoCategories>(
1050        &mut self,
1051        categories: C,
1052        series: &[(&str, Vec<f64>)],
1053    ) -> Result<()> {
1054        let cat_labels: Vec<String> = categories.into_categories().labels.iter().map(|s| s.to_string()).collect();
1055        let n_series = series.len();
1056        if n_series == 0 {
1057            return Err(PlotError::EmptyData);
1058        }
1059        let total_width = 0.8;
1060        let bar_width = total_width / n_series as f64;
1061
1062        for (si, (label, heights)) in series.iter().enumerate() {
1063            let offset_val = (si as f64 - (n_series as f64 - 1.0) / 2.0) * bar_width;
1064            let offsets: Vec<f64> = vec![offset_val; heights.len()];
1065            let artist_ref = self.bar(cat_labels.clone(), heights.as_slice())?;
1066            artist_ref.bar_width(bar_width);
1067            artist_ref.offset(offsets);
1068            artist_ref.label(label);
1069        }
1070        Ok(())
1071    }
1072
1073    /// Creates a hexagonal binning plot from `(x, y)` data points.
1074    ///
1075    /// Returns a mutable reference to the [`HexbinArtist`] for chaining
1076    /// builder methods (`.gridsize()`, `.colormap()`, `.mincnt()`, etc.).
1077    ///
1078    /// # Errors
1079    ///
1080    /// Returns [`PlotError::EmptyData`] if `x` or `y` is empty.
1081    /// Returns [`PlotError::SeriesLengthMismatch`] if `x` and `y` differ in length.
1082    pub fn hexbin<X, Y>(&mut self, x: X, y: Y) -> Result<&mut HexbinArtist>
1083    where
1084        X: IntoSeries,
1085        Y: IntoSeries,
1086    {
1087        let xs = x.into_series();
1088        let ys = y.into_series();
1089        if xs.is_empty() || ys.is_empty() {
1090            return Err(PlotError::EmptyData);
1091        }
1092        if xs.len() != ys.len() {
1093            return Err(PlotError::SeriesLengthMismatch {
1094                expected: xs.len(),
1095                got: ys.len(),
1096            });
1097        }
1098        let color = Color::TABLEAU_10[self.color_index % 10];
1099        self.color_index += 1;
1100        let artist = HexbinArtist {
1101            x: xs.data,
1102            y: ys.data,
1103            gridsize: 20,
1104            cmap: crate::colormap::Colormap::Viridis,
1105            mincnt: 1,
1106            alpha: 1.0,
1107            color,
1108            label: None,
1109            edgecolor: None,
1110            show_colorbar: false,
1111        };
1112        self.artists.push(Artist::Hexbin(artist));
1113        match self.artists.last_mut().expect("just pushed") {
1114            Artist::Hexbin(a) => Ok(a),
1115            _ => unreachable!(),
1116        }
1117    }
1118
1119    /// Creates a polar line plot from `(theta, r)` data in polar coordinates.
1120    ///
1121    /// `theta` contains angles in radians, `r` contains the corresponding
1122    /// radial distances. The data is drawn as a connected polyline in polar
1123    /// space.
1124    ///
1125    /// Returns a mutable reference to the [`PolarArtist`] for chaining.
1126    ///
1127    /// # Errors
1128    ///
1129    /// Returns [`PlotError::SeriesLengthMismatch`] if `theta` and `r` have
1130    /// different lengths, or [`PlotError::EmptyData`] if either is empty.
1131    pub fn polar_plot<T, R>(&mut self, theta: T, r: R) -> Result<&mut PolarArtist>
1132    where
1133        T: IntoSeries,
1134        R: IntoSeries,
1135    {
1136        let ts = theta.into_series();
1137        let rs = r.into_series();
1138        if ts.len() != rs.len() {
1139            return Err(PlotError::SeriesLengthMismatch {
1140                expected: ts.len(),
1141                got: rs.len(),
1142            });
1143        }
1144        if ts.is_empty() {
1145            return Err(PlotError::EmptyData);
1146        }
1147        let color = Color::TABLEAU_10[self.color_index % 10];
1148        self.color_index += 1;
1149        let artist = PolarArtist {
1150            theta: ts.data,
1151            r: rs.data,
1152            color,
1153            label: None,
1154            alpha: 1.0,
1155            linewidth: 1.5,
1156            filled: false,
1157            marker: None,
1158        };
1159        self.artists.push(Artist::Polar(artist));
1160        match self.artists.last_mut().expect("just pushed") {
1161            Artist::Polar(a) => Ok(a),
1162            _ => unreachable!(),
1163        }
1164    }
1165
1166    /// Creates a filled polar (radar/area) chart from `(theta, r)` data.
1167    ///
1168    /// Like [`polar_plot`](Axes::polar_plot), but the path is automatically
1169    /// closed and filled, producing a radar or area chart.
1170    ///
1171    /// # Errors
1172    ///
1173    /// Returns [`PlotError::SeriesLengthMismatch`] if `theta` and `r` have
1174    /// different lengths, or [`PlotError::EmptyData`] if either is empty.
1175    pub fn polar_fill<T, R>(&mut self, theta: T, r: R) -> Result<&mut PolarArtist>
1176    where
1177        T: IntoSeries,
1178        R: IntoSeries,
1179    {
1180        let artist_ref = self.polar_plot(theta, r)?;
1181        artist_ref.filled(true);
1182        artist_ref.alpha(0.3);
1183        Ok(artist_ref)
1184    }
1185
1186    /// Creates a waterfall chart from categorical data and change values.
1187    ///
1188    /// Each bar represents an incremental change (positive or negative) from
1189    /// the previous running total. Positive changes are colored green,
1190    /// negative changes red, and bars marked as totals are colored blue-gray.
1191    ///
1192    /// The method also sets the x-axis tick positions and labels to match the
1193    /// category labels so they render on the axis automatically.
1194    ///
1195    /// # Errors
1196    ///
1197    /// Returns [`PlotError::SeriesLengthMismatch`] if `categories` and
1198    /// `values` have different lengths, or [`PlotError::EmptyData`] if empty.
1199    pub fn waterfall<C, V>(&mut self, categories: C, values: V) -> Result<&mut WaterfallArtist>
1200    where
1201        C: IntoCategories,
1202        V: IntoSeries,
1203    {
1204        let cats = categories.into_categories();
1205        let vals = values.into_series();
1206        if cats.len() != vals.len() {
1207            return Err(PlotError::SeriesLengthMismatch {
1208                expected: cats.len(),
1209                got: vals.len(),
1210            });
1211        }
1212        if cats.is_empty() {
1213            return Err(PlotError::EmptyData);
1214        }
1215
1216        // Set x-axis ticks to category positions with category labels.
1217        let n = cats.len();
1218        let tick_positions: Vec<f64> = (0..n).map(|i| i as f64).collect();
1219        self.custom_xticks = Some(tick_positions);
1220        self.custom_xticklabels = Some(cats.labels.iter().map(|s| s.to_string()).collect());
1221
1222        let color = Color::TABLEAU_10[self.color_index % 10];
1223        self.color_index += 1;
1224        let artist = WaterfallArtist {
1225            categories: cats,
1226            values: vals,
1227            total_indices: Vec::new(),
1228            increase_color: Color::rgb(0x2C, 0xA0, 0x2C), // Professional green
1229            decrease_color: Color::rgb(0xD6, 0x27, 0x28), // Professional red
1230            total_color: Color::rgb(0x4E, 0x79, 0xA7),    // Tableau blue-gray
1231            connector_lines: true,
1232            show_values: false,
1233            bar_width: 0.6,
1234            label: None,
1235            color,
1236            alpha: 1.0,
1237        };
1238        self.artists.push(Artist::Waterfall(artist));
1239        match self.artists.last_mut().expect("just pushed") {
1240            Artist::Waterfall(a) => Ok(a),
1241            _ => unreachable!(),
1242        }
1243    }
1244}
1245
1246// ---------------------------------------------------------------------------
1247// Rendering pipeline
1248// ---------------------------------------------------------------------------
1249
1250#[allow(clippy::too_many_arguments)]
1251impl Axes {
1252    /// Renders this axes into the given rectangle on the renderer.
1253    ///
1254    /// This is called by the `Figure` during its render pass. The ten-step
1255    /// pipeline is:
1256    ///
1257    /// 1. Compute data limits (autoscale or user-set).
1258    /// 2. Generate tick positions and labels.
1259    /// 3. Compute internal layout (margins for title, labels, ticks).
1260    /// 4. Draw axes background.
1261    /// 5. Draw grid lines (behind data).
1262    /// 6. Clip to the plot area and draw each artist.
1263    /// 7. Draw axis spines.
1264    /// 8. Draw ticks and tick labels.
1265    /// 9. Draw axis labels and title.
1266    /// 10. Draw legend (if enabled).
1267    pub(crate) fn render(&self, renderer: &mut impl Renderer, bounds: Rect, fig_theme: &Theme) {
1268        self.render_primary(renderer, bounds, fig_theme, false);
1269    }
1270
1271    pub(crate) fn render_primary(
1272        &self,
1273        renderer: &mut impl Renderer,
1274        bounds: Rect,
1275        fig_theme: &Theme,
1276        suppress_legend: bool,
1277    ) {
1278        let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
1279
1280        // Step 1: Compute data limits.
1281        let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
1282
1283        // Step 2: Generate ticks (use custom ticks if provided).
1284        let xticks = self.resolve_xticks(xmin, xmax);
1285        let yticks = self.resolve_yticks(ymin, ymax);
1286
1287        // Step 3: Compute layout (reserve space for labels, ticks, title).
1288        let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
1289        layout_config.has_title = self.title.is_some();
1290        layout_config.has_xlabel = self.xlabel.is_some();
1291        layout_config.has_ylabel = self.ylabel.is_some();
1292        layout_config.has_legend = self.show_legend;
1293
1294        let layout_result = layout::compute_layout(&layout_config);
1295
1296        let full_plot_area = Rect::new(
1297            bounds.x + layout_result.plot_area.x,
1298            bounds.y + layout_result.plot_area.y,
1299            layout_result.plot_area.width,
1300            layout_result.plot_area.height,
1301        );
1302
1303        // Resolve the effective colorbar: either an explicit one or auto from heatmap.
1304        let effective_colorbar = if self.colorbar.is_some() {
1305            self.colorbar.clone()
1306        } else {
1307            self.auto_colorbar_from_artists()
1308        };
1309
1310        // When a colorbar is present, shrink the plot area to make room.
1311        let (plot_area, colorbar_rect) = if effective_colorbar.is_some() {
1312            let cb_width = (full_plot_area.width * colorbar::COLORBAR_WIDTH_FRACTION).max(30.0);
1313            let shrunk = Rect::new(
1314                full_plot_area.x,
1315                full_plot_area.y,
1316                full_plot_area.width - cb_width - colorbar::COLORBAR_GAP,
1317                full_plot_area.height,
1318            );
1319            let cb_rect = Rect::new(
1320                full_plot_area.x + full_plot_area.width - cb_width,
1321                full_plot_area.y,
1322                cb_width,
1323                full_plot_area.height,
1324            );
1325            (shrunk, Some(cb_rect))
1326        } else {
1327            (full_plot_area, None)
1328        };
1329
1330        // Step 4: Draw axes background.
1331        let bg_path = Path::rect(plot_area);
1332        renderer.fill_path(&bg_path, &Paint::new(theme.axes_background), Affine::IDENTITY);
1333
1334        // Step 5: Draw grid (behind data).
1335        if self.show_grid.unwrap_or(theme.show_grid) {
1336            self.draw_grid(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
1337        }
1338
1339        // Step 6: Clip to plot area and draw each artist.
1340        let clip_path = Path::rect(plot_area);
1341        renderer.push_clip(&clip_path, Affine::IDENTITY);
1342        for artist in &self.artists {
1343            self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
1344        }
1345        renderer.pop_clip();
1346
1347        // Step 6b: Draw minor ticks for log scales (behind spines, after data).
1348        let x_minor = if matches!(self.xscale, Scale::Log10) {
1349            ticks::generate_log_minor_ticks(xmin, xmax)
1350        } else {
1351            Vec::new()
1352        };
1353        let y_minor = if matches!(self.yscale, Scale::Log10) {
1354            ticks::generate_log_minor_ticks(ymin, ymax)
1355        } else {
1356            Vec::new()
1357        };
1358
1359        // Step 7: Draw spines.
1360        self.draw_spines(renderer, &plot_area, theme);
1361
1362        // Step 8: Draw ticks and tick labels (including minor ticks).
1363        self.draw_ticks(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
1364        if !x_minor.is_empty() || !y_minor.is_empty() {
1365            self.draw_minor_ticks(renderer, &plot_area, &x_minor, &y_minor, xmin, xmax, ymin, ymax, theme);
1366        }
1367
1368        // Step 9: Draw axis labels and title.
1369        self.draw_labels(renderer, &plot_area, &bounds, theme);
1370
1371        // Step 9b: Draw text annotations and arrow annotations.
1372        self.draw_annotations(renderer, &plot_area, xmin, xmax, ymin, ymax, theme);
1373
1374        // Step 10: Draw legend if enabled.
1375        if self.show_legend && !suppress_legend {
1376            self.draw_legend(renderer, &plot_area, theme);
1377        }
1378
1379        // Draw colorbar if present.
1380        if let (Some(ref cb), Some(ref cb_rect)) = (&effective_colorbar, &colorbar_rect) {
1381            colorbar::draw_colorbar(renderer, cb, cb_rect, theme);
1382        }
1383    }
1384
1385    fn auto_colorbar_from_artists(&self) -> Option<Colorbar> {
1386        for artist in &self.artists {
1387            match artist {
1388                Artist::Heatmap(a) if a.show_colorbar => {
1389                    return Some(Colorbar::new(
1390                        a.cmap,
1391                        a.effective_vmin(),
1392                        a.effective_vmax(),
1393                    ));
1394                }
1395                Artist::Hexbin(a) if a.show_colorbar => {
1396                    let result = crate::charts::hexbin::bin_hexagonal(
1397                        &a.x, &a.y, a.gridsize, a.mincnt,
1398                    );
1399                    let vmin = result.min_count as f64;
1400                    let vmax = (result.max_count as f64).max(vmin + 1.0);
1401                    return Some(Colorbar::new(a.cmap, vmin, vmax));
1402                }
1403                _ => {}
1404            }
1405        }
1406        None
1407    }
1408
1409    /// Renders this twin axes overlaid on its parent's plot area.
1410    pub(crate) fn render_twin(
1411        &self,
1412        renderer: &mut impl Renderer,
1413        plot_area: Rect,
1414        bounds: Rect,
1415        fig_theme: &Theme,
1416    ) {
1417        let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
1418        let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
1419        let yticks = self.resolve_yticks(ymin, ymax);
1420        let xticks = self.resolve_xticks(xmin, xmax);
1421
1422        let clip_path = Path::rect(plot_area);
1423        renderer.push_clip(&clip_path, Affine::IDENTITY);
1424        for artist in &self.artists {
1425            self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
1426        }
1427        renderer.pop_clip();
1428
1429        let side = self.twin_side.unwrap_or(TwinSide::Right);
1430        match side {
1431            TwinSide::Right => {
1432                let paint = Paint::new(theme.spine_color);
1433                let stroke = Stroke::new(theme.spine_width);
1434                let mut p = Path::new();
1435                p.move_to(plot_area.right(), plot_area.y);
1436                p.line_to(plot_area.right(), plot_area.bottom());
1437                renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1438                self.draw_ticks_right(renderer, &plot_area, &yticks, ymin, ymax, theme);
1439                self.draw_ylabel_right(renderer, &plot_area, &bounds, theme);
1440            }
1441            TwinSide::Top => {
1442                let paint = Paint::new(theme.spine_color);
1443                let stroke = Stroke::new(theme.spine_width);
1444                let mut p = Path::new();
1445                p.move_to(plot_area.x, plot_area.y);
1446                p.line_to(plot_area.right(), plot_area.y);
1447                renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1448                self.draw_ticks_top(renderer, &plot_area, &xticks, xmin, xmax, theme);
1449                self.draw_xlabel_top(renderer, &plot_area, theme);
1450            }
1451        }
1452
1453        self.draw_annotations(renderer, &plot_area, xmin, xmax, ymin, ymax, theme);
1454    }
1455
1456    /// Returns the computed plot area for this axes given a bounds rectangle.
1457    pub(crate) fn compute_plot_area(&self, bounds: &Rect) -> Rect {
1458        let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
1459        layout_config.has_title = self.title.is_some();
1460        layout_config.has_xlabel = self.xlabel.is_some();
1461        layout_config.has_ylabel = self.ylabel.is_some();
1462        layout_config.has_legend = self.show_legend;
1463        let layout_result = layout::compute_layout(&layout_config);
1464        Rect::new(
1465            bounds.x + layout_result.plot_area.x,
1466            bounds.y + layout_result.plot_area.y,
1467            layout_result.plot_area.width,
1468            layout_result.plot_area.height,
1469        )
1470    }
1471
1472    /// Collects legend entries from this axes' artists.
1473    pub fn collect_legend_entries(&self) -> Vec<LegendEntry> {
1474        self.artists
1475            .iter()
1476            .filter_map(|a| {
1477                let (label, color, swatch) = match a {
1478                    Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1479                    Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1480                    Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1481                    Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1482                    Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1483                    Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1484                    Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1485                    Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1486                    Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1487                    Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1488                    Artist::Pie(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1489                    Artist::Violin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1490                    Artist::Contour(a) => (a.label.as_deref(), a.color, if a.filled { SwatchKind::Filled } else { SwatchKind::Line }),
1491                    Artist::Polar(a) => (a.label.as_deref(), a.color, if a.filled { SwatchKind::Filled } else { SwatchKind::Line }),
1492                    Artist::Hexbin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1493                    Artist::Waterfall(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1494                };
1495                label.map(|l| LegendEntry {
1496                    label: l.to_string(),
1497                    color,
1498                    swatch,
1499                })
1500            })
1501            .collect()
1502    }
1503
1504    // -----------------------------------------------------------------------
1505    // Step 1: Data limits
1506    // -----------------------------------------------------------------------
1507
1508    /// Computes the data limits from all artists, applying user overrides
1509    /// and padding. Returns `(xmin, xmax, ymin, ymax)`.
1510    fn compute_data_limits(&self) -> (f64, f64, f64, f64) {
1511        let mut x_lo = f64::INFINITY;
1512        let mut x_hi = f64::NEG_INFINITY;
1513        let mut y_lo = f64::INFINITY;
1514        let mut y_hi = f64::NEG_INFINITY;
1515
1516        for artist in &self.artists {
1517            match artist {
1518                Artist::Line(a) => {
1519                    if let Some((lo, hi)) = a.x.bounds() {
1520                        x_lo = x_lo.min(lo);
1521                        x_hi = x_hi.max(hi);
1522                    }
1523                    if let Some((lo, hi)) = a.y.bounds() {
1524                        y_lo = y_lo.min(lo);
1525                        y_hi = y_hi.max(hi);
1526                    }
1527                }
1528                Artist::Scatter(a) => {
1529                    if let Some((lo, hi)) = a.x.bounds() {
1530                        x_lo = x_lo.min(lo);
1531                        x_hi = x_hi.max(hi);
1532                    }
1533                    if let Some((lo, hi)) = a.y.bounds() {
1534                        y_lo = y_lo.min(lo);
1535                        y_hi = y_hi.max(hi);
1536                    }
1537                }
1538                Artist::Bar(a) => {
1539                    let n = a.categories.len() as f64;
1540                    if a.horizontal {
1541                        // x-axis is the value axis, y-axis is the category axis.
1542                        y_lo = 0.0_f64.min(y_lo);
1543                        y_hi = n.max(y_hi);
1544                        x_lo = 0.0_f64.min(x_lo);
1545                        if let Some((lo, hi)) = a.heights.bounds() {
1546                            x_lo = x_lo.min(lo.min(0.0));
1547                            x_hi = x_hi.max(hi);
1548                        }
1549                    } else {
1550                        // x-axis is the category axis, y-axis is the value axis.
1551                        x_lo = 0.0_f64.min(x_lo);
1552                        x_hi = n.max(x_hi);
1553                        y_lo = 0.0_f64.min(y_lo);
1554                        if let Some((lo, hi)) = a.heights.bounds() {
1555                            y_lo = y_lo.min(lo.min(0.0));
1556                            y_hi = y_hi.max(hi);
1557                        }
1558                    }
1559                }
1560                Artist::Histogram(a) => {
1561                    if let (Some(&first), Some(&last)) = (a.bin_edges.first(), a.bin_edges.last()) {
1562                        x_lo = x_lo.min(first);
1563                        x_hi = x_hi.max(last);
1564                    }
1565                    y_lo = 0.0_f64.min(y_lo);
1566                    let max_count = a.counts.iter().fold(0.0f64, |a, &b| a.max(b));
1567                    y_hi = y_hi.max(max_count);
1568                }
1569                Artist::FillBetween(a) => {
1570                    if let Some((lo, hi)) = a.x.bounds() {
1571                        x_lo = x_lo.min(lo);
1572                        x_hi = x_hi.max(hi);
1573                    }
1574                    if let Some((lo, hi)) = a.y1.bounds() {
1575                        y_lo = y_lo.min(lo);
1576                        y_hi = y_hi.max(hi);
1577                    }
1578                    if let Some((lo, hi)) = a.y2.bounds() {
1579                        y_lo = y_lo.min(lo);
1580                        y_hi = y_hi.max(hi);
1581                    }
1582                }
1583                Artist::Step(a) => {
1584                    if let Some((lo, hi)) = a.x.bounds() {
1585                        x_lo = x_lo.min(lo);
1586                        x_hi = x_hi.max(hi);
1587                    }
1588                    if let Some((lo, hi)) = a.y.bounds() {
1589                        y_lo = y_lo.min(lo);
1590                        y_hi = y_hi.max(hi);
1591                    }
1592                }
1593                Artist::Stem(a) => {
1594                    if let Some((lo, hi)) = a.x.bounds() {
1595                        x_lo = x_lo.min(lo);
1596                        x_hi = x_hi.max(hi);
1597                    }
1598                    if let Some((lo, hi)) = a.y.bounds() {
1599                        y_lo = y_lo.min(lo.min(a.baseline));
1600                        y_hi = y_hi.max(hi.max(a.baseline));
1601                    }
1602                }
1603                Artist::BoxPlot(a) => {
1604                    let n = a.stats.len() as f64;
1605                    x_lo = 0.0_f64.min(x_lo);
1606                    x_hi = n.max(x_hi);
1607                    for s in &a.stats {
1608                        y_lo = y_lo.min(s.whisker_low);
1609                        y_hi = y_hi.max(s.whisker_high);
1610                        for &o in &s.outliers {
1611                            y_lo = y_lo.min(o);
1612                            y_hi = y_hi.max(o);
1613                        }
1614                    }
1615                }
1616                Artist::ErrorBar(a) => {
1617                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1618                    x_lo = x_lo.min(bxlo);
1619                    x_hi = x_hi.max(bxhi);
1620                    y_lo = y_lo.min(bylo);
1621                    y_hi = y_hi.max(byhi);
1622                }
1623                Artist::Heatmap(a) => {
1624                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1625                    x_lo = x_lo.min(bxlo);
1626                    x_hi = x_hi.max(bxhi);
1627                    y_lo = y_lo.min(bylo);
1628                    y_hi = y_hi.max(byhi);
1629                }
1630                Artist::Pie(a) => {
1631                    // Pie charts define their own coordinate space and
1632                    // should not participate in normal autoscaling.
1633                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1634                    x_lo = x_lo.min(bxlo);
1635                    x_hi = x_hi.max(bxhi);
1636                    y_lo = y_lo.min(bylo);
1637                    y_hi = y_hi.max(byhi);
1638                }
1639                Artist::Violin(a) => {
1640                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1641                    x_lo = x_lo.min(bxlo);
1642                    x_hi = x_hi.max(bxhi);
1643                    y_lo = y_lo.min(bylo);
1644                    y_hi = y_hi.max(byhi);
1645                }
1646                Artist::Contour(a) => {
1647                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1648                    x_lo = x_lo.min(bxlo);
1649                    x_hi = x_hi.max(bxhi);
1650                    y_lo = y_lo.min(bylo);
1651                    y_hi = y_hi.max(byhi);
1652                }
1653                Artist::Polar(a) => {
1654                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1655                    x_lo = x_lo.min(bxlo);
1656                    x_hi = x_hi.max(bxhi);
1657                    y_lo = y_lo.min(bylo);
1658                    y_hi = y_hi.max(byhi);
1659                }
1660                Artist::Hexbin(a) => {
1661                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1662                    x_lo = x_lo.min(bxlo);
1663                    x_hi = x_hi.max(bxhi);
1664                    y_lo = y_lo.min(bylo);
1665                    y_hi = y_hi.max(byhi);
1666                }
1667                Artist::Waterfall(a) => {
1668                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1669                    x_lo = x_lo.min(bxlo);
1670                    x_hi = x_hi.max(bxhi);
1671                    y_lo = y_lo.min(bylo);
1672                    y_hi = y_hi.max(byhi);
1673                }
1674            }
1675        }
1676
1677        // Handle the case where there are no artists or no finite data.
1678        if !x_lo.is_finite() || !x_hi.is_finite() {
1679            x_lo = if self.xscale.requires_positive() { 1.0 } else { 0.0 };
1680            x_hi = if self.xscale.requires_positive() { 10.0 } else { 1.0 };
1681        }
1682        if !y_lo.is_finite() || !y_hi.is_finite() {
1683            y_lo = if self.yscale.requires_positive() { 1.0 } else { 0.0 };
1684            y_hi = if self.yscale.requires_positive() { 10.0 } else { 1.0 };
1685        }
1686
1687        // For log scales, clamp the lower bound to a positive value.
1688        if self.xscale.requires_positive() {
1689            if x_lo <= 0.0 {
1690                // Use a small fraction of x_hi, or fall back to a sensible default.
1691                x_lo = if x_hi > 0.0 { x_hi * 1e-4 } else { 1.0 };
1692            }
1693            if x_hi <= x_lo {
1694                x_hi = x_lo * 10.0;
1695            }
1696        }
1697        if self.yscale.requires_positive() {
1698            if y_lo <= 0.0 {
1699                y_lo = if y_hi > 0.0 { y_hi * 1e-4 } else { 1.0 };
1700            }
1701            if y_hi <= y_lo {
1702                y_hi = y_lo * 10.0;
1703            }
1704        }
1705
1706        // Handle degenerate ranges (all data at a single point).
1707        if (x_hi - x_lo).abs() < f64::EPSILON {
1708            x_lo -= 0.5;
1709            x_hi += 0.5;
1710        }
1711        if (y_hi - y_lo).abs() < f64::EPSILON {
1712            y_lo -= 0.5;
1713            y_hi += 0.5;
1714        }
1715
1716        // Apply padding (5% on each side).
1717        // For log scales, apply multiplicative padding instead of additive.
1718        let (x_pad_lo, x_pad_hi) = if self.xscale.requires_positive() {
1719            // Multiplicative padding: shrink lo, grow hi by a factor.
1720            let factor = 1.0 + AUTOSCALE_PAD;
1721            (x_lo / factor, x_hi * factor)
1722        } else {
1723            let pad = (x_hi - x_lo) * AUTOSCALE_PAD;
1724            (x_lo - pad, x_hi + pad)
1725        };
1726        let (y_pad_lo, y_pad_hi) = if self.yscale.requires_positive() {
1727            let factor = 1.0 + AUTOSCALE_PAD;
1728            (y_lo / factor, y_hi * factor)
1729        } else {
1730            let pad = (y_hi - y_lo) * AUTOSCALE_PAD;
1731            (y_lo - pad, y_hi + pad)
1732        };
1733        x_lo = x_pad_lo;
1734        x_hi = x_pad_hi;
1735        y_lo = y_pad_lo;
1736        y_hi = y_pad_hi;
1737
1738        // Apply user-set limits, overriding auto-scale.
1739        if let Some((lo, hi)) = self.xlim {
1740            x_lo = lo;
1741            x_hi = hi;
1742        }
1743        if let Some((lo, hi)) = self.ylim {
1744            y_lo = lo;
1745            y_hi = hi;
1746        }
1747
1748        // Apply axis inversion by swapping min/max.
1749        if self.x_inverted {
1750            std::mem::swap(&mut x_lo, &mut x_hi);
1751        }
1752        if self.y_inverted {
1753            std::mem::swap(&mut y_lo, &mut y_hi);
1754        }
1755
1756        (x_lo, x_hi, y_lo, y_hi)
1757    }
1758
1759    // -----------------------------------------------------------------------
1760    // Step 2 helpers: tick resolution
1761    // -----------------------------------------------------------------------
1762
1763    /// Resolves x-axis ticks: uses custom ticks when set, falls back to
1764    /// auto-generation.
1765    fn resolve_xticks(&self, xmin: f64, xmax: f64) -> Vec<ticks::Tick> {
1766        if let Some(ref positions) = self.custom_xticks {
1767            let labels = self.custom_xticklabels.as_ref();
1768            positions
1769                .iter()
1770                .enumerate()
1771                .map(|(i, &v)| ticks::Tick {
1772                    value: v,
1773                    label: labels
1774                        .and_then(|l| l.get(i))
1775                        .cloned()
1776                        .unwrap_or_else(|| ticks::format_tick_value(v)),
1777                })
1778                .collect()
1779        } else {
1780            let (lo, hi) = if xmin <= xmax { (xmin, xmax) } else { (xmax, xmin) };
1781            ticks::generate_ticks(lo, hi, DEFAULT_TICK_COUNT, &self.xscale)
1782        }
1783    }
1784
1785    /// Resolves y-axis ticks: uses custom ticks when set, falls back to
1786    /// auto-generation.
1787    fn resolve_yticks(&self, ymin: f64, ymax: f64) -> Vec<ticks::Tick> {
1788        if let Some(ref positions) = self.custom_yticks {
1789            let labels = self.custom_yticklabels.as_ref();
1790            positions
1791                .iter()
1792                .enumerate()
1793                .map(|(i, &v)| ticks::Tick {
1794                    value: v,
1795                    label: labels
1796                        .and_then(|l| l.get(i))
1797                        .cloned()
1798                        .unwrap_or_else(|| ticks::format_tick_value(v)),
1799                })
1800                .collect()
1801        } else {
1802            let (lo, hi) = if ymin <= ymax { (ymin, ymax) } else { (ymax, ymin) };
1803            ticks::generate_ticks(lo, hi, DEFAULT_TICK_COUNT, &self.yscale)
1804        }
1805    }
1806
1807    // -----------------------------------------------------------------------
1808    // Step 5: Grid
1809    // -----------------------------------------------------------------------
1810
1811    /// Draws major grid lines behind the data.
1812    fn draw_grid(
1813        &self,
1814        renderer: &mut impl Renderer,
1815        plot_area: &Rect,
1816        xticks: &[ticks::Tick],
1817        yticks: &[ticks::Tick],
1818        xmin: f64,
1819        xmax: f64,
1820        ymin: f64,
1821        ymax: f64,
1822        theme: &Theme,
1823    ) {
1824        // Apply grid alpha override.
1825        let grid_color = if let Some(alpha) = self.grid_alpha {
1826            theme.grid_color.with_alpha((alpha * 255.0) as u8)
1827        } else {
1828            theme.grid_color
1829        };
1830        let paint = Paint::new(grid_color);
1831
1832        // Apply grid line style override.
1833        let mut stroke = Stroke::new(theme.grid_width);
1834        if let Some(style) = self.grid_style {
1835            stroke = match style {
1836                LineStyle::Solid => stroke,
1837                LineStyle::Dashed => stroke.with_dash(DashPattern {
1838                    dashes: vec![6.0, 4.0],
1839                    offset: 0.0,
1840                }),
1841                LineStyle::Dotted => stroke.with_dash(DashPattern {
1842                    dashes: vec![2.0, 2.0],
1843                    offset: 0.0,
1844                }),
1845                LineStyle::DashDot => stroke.with_dash(DashPattern {
1846                    dashes: vec![6.0, 3.0, 2.0, 3.0],
1847                    offset: 0.0,
1848                }),
1849            };
1850        }
1851
1852        let draw_x = matches!(self.grid_axis, GridAxis::X | GridAxis::Both);
1853        let draw_y = matches!(self.grid_axis, GridAxis::Y | GridAxis::Both);
1854
1855        // Vertical grid lines at each x-tick.
1856        if draw_x {
1857            for tick in xticks {
1858                let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
1859                let mut path = Path::new();
1860                path.move_to(pt.x, plot_area.y);
1861                path.line_to(pt.x, plot_area.bottom());
1862                renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1863            }
1864        }
1865
1866        // Horizontal grid lines at each y-tick.
1867        if draw_y {
1868            for tick in yticks {
1869                let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
1870                let mut path = Path::new();
1871                path.move_to(plot_area.x, pt.y);
1872                path.line_to(plot_area.right(), pt.y);
1873                renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1874            }
1875        }
1876    }
1877
1878    // -----------------------------------------------------------------------
1879    // Step 6: Artist drawing
1880    // -----------------------------------------------------------------------
1881
1882    /// Dispatches drawing to the appropriate artist-type-specific method.
1883    fn draw_artist(
1884        &self,
1885        renderer: &mut impl Renderer,
1886        artist: &Artist,
1887        plot_area: &Rect,
1888        xmin: f64,
1889        xmax: f64,
1890        ymin: f64,
1891        ymax: f64,
1892        theme: &Theme,
1893    ) {
1894        match artist {
1895            Artist::Line(a) => self.draw_line(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1896            Artist::Scatter(a) => self.draw_scatter(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1897            Artist::Bar(a) => self.draw_bar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1898            Artist::Histogram(a) => self.draw_hist(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1899            Artist::FillBetween(a) => self.draw_fill_between(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1900            Artist::Step(a) => self.draw_step(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1901            Artist::Stem(a) => self.draw_stem(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1902            Artist::BoxPlot(a) => self.draw_boxplot(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1903            Artist::ErrorBar(a) => self.draw_errorbar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1904            Artist::Heatmap(a) => self.draw_heatmap(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1905            Artist::Pie(a) => self.draw_pie(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1906            Artist::Violin(a) => self.draw_violin(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1907            Artist::Contour(a) => self.draw_contour(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1908            Artist::Polar(a) => self.draw_polar(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1909            Artist::Hexbin(a) => self.draw_hexbin(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1910            Artist::Waterfall(a) => self.draw_waterfall(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1911        }
1912    }
1913
1914    /// Draws a line chart: builds a polyline from data points and strokes it.
1915    ///
1916    /// When the artist has decimation enabled and the data exceeds the
1917    /// threshold, the point set is downsampled before path construction.
1918    fn draw_line(
1919        &self,
1920        renderer: &mut impl Renderer,
1921        artist: &LineArtist,
1922        plot_area: &Rect,
1923        xmin: f64,
1924        xmax: f64,
1925        ymin: f64,
1926        ymax: f64,
1927    ) {
1928        if artist.x.is_empty() {
1929            return;
1930        }
1931
1932        // Compute the set of indices to draw (possibly decimated).
1933        let indices: Vec<usize> = match artist.decimate {
1934            Some((threshold, method)) if artist.x.len() > threshold => {
1935                use crate::decimate::{self, DecimateMethod};
1936                match method {
1937                    DecimateMethod::Lttb => {
1938                        decimate::lttb(&artist.x.data, &artist.y.data, threshold)
1939                    }
1940                    DecimateMethod::MinMax => {
1941                        decimate::minmax(&artist.x.data, &artist.y.data, threshold)
1942                    }
1943                }
1944            }
1945            _ => (0..artist.x.len()).collect(),
1946        };
1947
1948        if indices.is_empty() {
1949            return;
1950        }
1951
1952        let mut path = Path::new();
1953        let first = self.data_to_pixel(
1954            artist.x.data[indices[0]],
1955            artist.y.data[indices[0]],
1956            plot_area,
1957            xmin, xmax, ymin, ymax,
1958        );
1959        path.move_to(first.x, first.y);
1960
1961        for &i in &indices[1..] {
1962            let pt = self.data_to_pixel(
1963                artist.x.data[i],
1964                artist.y.data[i],
1965                plot_area,
1966                xmin, xmax, ymin, ymax,
1967            );
1968            path.line_to(pt.x, pt.y);
1969        }
1970
1971        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1972        let paint = Paint::new(color);
1973        let mut stroke = Stroke::new(artist.width);
1974
1975        // Apply line style dash pattern.
1976        match artist.style {
1977            crate::theme::LineStyle::Solid => {}
1978            crate::theme::LineStyle::Dashed => {
1979                stroke = stroke.with_dash(DashPattern {
1980                    dashes: vec![6.0, 4.0],
1981                    offset: 0.0,
1982                });
1983            }
1984            crate::theme::LineStyle::Dotted => {
1985                stroke = stroke.with_dash(DashPattern {
1986                    dashes: vec![2.0, 2.0],
1987                    offset: 0.0,
1988                });
1989            }
1990            crate::theme::LineStyle::DashDot => {
1991                stroke = stroke.with_dash(DashPattern {
1992                    dashes: vec![6.0, 3.0, 2.0, 3.0],
1993                    offset: 0.0,
1994                });
1995            }
1996        }
1997
1998        renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1999    }
2000
2001    /// Draws a scatter plot: fills a marker shape at each data point.
2002    fn draw_scatter(
2003        &self,
2004        renderer: &mut impl Renderer,
2005        artist: &ScatterArtist,
2006        plot_area: &Rect,
2007        xmin: f64,
2008        xmax: f64,
2009        ymin: f64,
2010        ymax: f64,
2011        theme: &Theme,
2012    ) {
2013        let alpha_byte = (artist.alpha * 255.0) as u8;
2014
2015        // Pre-compute per-point colors from c/cmap if set.
2016        let cmap_colors: Option<Vec<Color>> = match (&artist.c, &artist.cmap) {
2017            (Some(c_vals), Some(cmap)) if !c_vals.is_empty() => Some(cmap.map_values(c_vals)),
2018            _ => None,
2019        };
2020
2021        let default_color = artist.color.with_alpha(alpha_byte);
2022        let default_paint = Paint::new(default_color);
2023        let radius = artist.size / 2.0;
2024
2025        for i in 0..artist.x.len() {
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}