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::artist::*;
9use crate::error::{PlotError, Result};
10use crate::layout::{self, LayoutConfig};
11use crate::legend::{self, LegendEntry, SwatchKind};
12use crate::primitives::*;
13use crate::renderer::Renderer;
14use crate::scale::Scale;
15use crate::series::{IntoCategories, IntoSeries};
16use crate::theme::{Loc, Marker, Theme, TickDirection};
17use crate::ticks;
18
19// ---------------------------------------------------------------------------
20// Constants
21// ---------------------------------------------------------------------------
22
23/// Default number of ticks to aim for on each axis.
24const DEFAULT_TICK_COUNT: usize = 7;
25
26/// Padding fraction applied to auto-computed data limits so that data points
27/// do not sit directly on the axis spines.
28const AUTOSCALE_PAD: f64 = 0.05;
29
30// ---------------------------------------------------------------------------
31// Axes
32// ---------------------------------------------------------------------------
33
34/// A single set of axes within a figure, containing chart artists and
35/// configuration.
36///
37/// Users do not construct `Axes` directly; they are created by
38/// [`Figure::add_subplot`] or the convenience function [`Figure::subplots`].
39/// Once an `Axes` handle is obtained, chart methods such as [`plot`](Axes::plot),
40/// [`scatter`](Axes::scatter), and [`bar`](Axes::bar) add data, and
41/// configuration methods like [`set_title`](Axes::set_title) control labels,
42/// limits, and styling.
43#[derive(Debug)]
44pub struct Axes {
45    /// The list of artists (one per chart call) in draw order.
46    pub(crate) artists: Vec<Artist>,
47    /// Optional title displayed above the plot area.
48    pub(crate) title: Option<String>,
49    /// Optional label for the x-axis.
50    pub(crate) xlabel: Option<String>,
51    /// Optional label for the y-axis.
52    pub(crate) ylabel: Option<String>,
53    /// User-specified x-axis limits; `None` means auto-scale.
54    pub(crate) xlim: Option<(f64, f64)>,
55    /// User-specified y-axis limits; `None` means auto-scale.
56    pub(crate) ylim: Option<(f64, f64)>,
57    /// Scale for the x-axis (linear, log, symlog).
58    pub(crate) xscale: Scale,
59    /// Scale for the y-axis (linear, log, symlog).
60    pub(crate) yscale: Scale,
61    /// Whether to show grid lines. `None` defers to the theme default.
62    pub(crate) show_grid: Option<bool>,
63    /// Whether the legend should be drawn.
64    pub(crate) show_legend: bool,
65    /// Where to place the legend.
66    pub(crate) legend_loc: Loc,
67    /// Per-axes theme override. `None` means use the figure theme.
68    pub(crate) theme_override: Option<Theme>,
69    /// Tracks the current position in the color cycle so that each successive
70    /// artist receives a distinct color automatically.
71    color_index: usize,
72}
73
74// ---------------------------------------------------------------------------
75// Construction
76// ---------------------------------------------------------------------------
77
78impl Axes {
79    /// Creates a new, empty axes with default settings.
80    pub(crate) fn new() -> Self {
81        Self {
82            artists: Vec::new(),
83            title: None,
84            xlabel: None,
85            ylabel: None,
86            xlim: None,
87            ylim: None,
88            xscale: Scale::default(),
89            yscale: Scale::default(),
90            show_grid: None,
91            show_legend: false,
92            legend_loc: Loc::Best,
93            theme_override: None,
94            color_index: 0,
95        }
96    }
97
98}
99
100// ---------------------------------------------------------------------------
101// Chart methods
102// ---------------------------------------------------------------------------
103
104impl Axes {
105    /// Plots a line connecting `(x, y)` data points.
106    ///
107    /// Returns a mutable reference to the newly created [`LineArtist`] so
108    /// that the caller can chain builder methods (`.color()`, `.width()`,
109    /// `.label()`, etc.).
110    ///
111    /// # Errors
112    ///
113    /// Returns [`PlotError::SeriesLengthMismatch`] if `x` and `y` have
114    /// different lengths, or [`PlotError::EmptyData`] if either is empty.
115    pub fn plot<X, Y>(&mut self, x: X, y: Y) -> Result<&mut LineArtist>
116    where
117        X: IntoSeries,
118        Y: IntoSeries,
119    {
120        let xs = x.into_series();
121        let ys = y.into_series();
122        if xs.len() != ys.len() {
123            return Err(PlotError::SeriesLengthMismatch {
124                expected: xs.len(),
125                got: ys.len(),
126            });
127        }
128        if xs.is_empty() {
129            return Err(PlotError::EmptyData);
130        }
131        let color = Color::TABLEAU_10[self.color_index % 10];
132        self.color_index += 1;
133        let artist = LineArtist {
134            x: xs,
135            y: ys,
136            color,
137            width: 1.5,
138            style: crate::theme::LineStyle::Solid,
139            label: None,
140            alpha: 1.0,
141        };
142        self.artists.push(Artist::Line(artist));
143        match self.artists.last_mut().expect("just pushed") {
144            Artist::Line(a) => Ok(a),
145            _ => unreachable!(),
146        }
147    }
148
149    /// Creates a scatter plot of `(x, y)` data points.
150    ///
151    /// Returns a mutable reference to the [`ScatterArtist`] for chaining.
152    ///
153    /// # Errors
154    ///
155    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
156    /// on invalid input.
157    pub fn scatter<X, Y>(&mut self, x: X, y: Y) -> Result<&mut ScatterArtist>
158    where
159        X: IntoSeries,
160        Y: IntoSeries,
161    {
162        let xs = x.into_series();
163        let ys = y.into_series();
164        if xs.len() != ys.len() {
165            return Err(PlotError::SeriesLengthMismatch {
166                expected: xs.len(),
167                got: ys.len(),
168            });
169        }
170        if xs.is_empty() {
171            return Err(PlotError::EmptyData);
172        }
173        let color = Color::TABLEAU_10[self.color_index % 10];
174        self.color_index += 1;
175        let artist = ScatterArtist {
176            x: xs,
177            y: ys,
178            color,
179            marker: Marker::Circle,
180            size: 6.0,
181            label: None,
182            alpha: 0.8,
183            colors: None,
184            c: None,
185            cmap: None,
186        };
187        self.artists.push(Artist::Scatter(artist));
188        match self.artists.last_mut().expect("just pushed") {
189            Artist::Scatter(a) => Ok(a),
190            _ => unreachable!(),
191        }
192    }
193
194    /// Creates a vertical bar chart from categorical data and heights.
195    ///
196    /// Each category label maps to one bar whose height is the corresponding
197    /// value in `heights`.
198    ///
199    /// # Errors
200    ///
201    /// Returns [`PlotError::SeriesLengthMismatch`] if `categories` and
202    /// `heights` have different lengths, or [`PlotError::EmptyData`] if empty.
203    pub fn bar<C, H>(&mut self, categories: C, heights: H) -> Result<&mut BarArtist>
204    where
205        C: IntoCategories,
206        H: IntoSeries,
207    {
208        let cats = categories.into_categories();
209        let vals = heights.into_series();
210        if cats.len() != vals.len() {
211            return Err(PlotError::SeriesLengthMismatch {
212                expected: cats.len(),
213                got: vals.len(),
214            });
215        }
216        if cats.is_empty() {
217            return Err(PlotError::EmptyData);
218        }
219        let color = Color::TABLEAU_10[self.color_index % 10];
220        self.color_index += 1;
221        let artist = BarArtist {
222            categories: cats,
223            heights: vals,
224            color,
225            horizontal: false,
226            bar_width: 0.8,
227            label: None,
228            alpha: 1.0,
229        };
230        self.artists.push(Artist::Bar(artist));
231        match self.artists.last_mut().expect("just pushed") {
232            Artist::Bar(a) => Ok(a),
233            _ => unreachable!(),
234        }
235    }
236
237    /// Creates a horizontal bar chart from categorical data and widths.
238    ///
239    /// Behaves like [`bar`](Axes::bar) but draws horizontal bars from the
240    /// y-axis.
241    ///
242    /// # Errors
243    ///
244    /// Same as [`bar`](Axes::bar).
245    pub fn barh<C, W>(&mut self, categories: C, widths: W) -> Result<&mut BarArtist>
246    where
247        C: IntoCategories,
248        W: IntoSeries,
249    {
250        let cats = categories.into_categories();
251        let vals = widths.into_series();
252        if cats.len() != vals.len() {
253            return Err(PlotError::SeriesLengthMismatch {
254                expected: cats.len(),
255                got: vals.len(),
256            });
257        }
258        if cats.is_empty() {
259            return Err(PlotError::EmptyData);
260        }
261        let color = Color::TABLEAU_10[self.color_index % 10];
262        self.color_index += 1;
263        let artist = BarArtist {
264            categories: cats,
265            heights: vals,
266            color,
267            horizontal: true,
268            bar_width: 0.8,
269            label: None,
270            alpha: 1.0,
271        };
272        self.artists.push(Artist::Bar(artist));
273        match self.artists.last_mut().expect("just pushed") {
274            Artist::Bar(a) => Ok(a),
275            _ => unreachable!(),
276        }
277    }
278
279    /// Creates a histogram from raw data.
280    ///
281    /// The data is partitioned into `bins` equal-width bins spanning the data
282    /// range, and each bin is drawn as a vertical bar whose height equals the
283    /// count of values falling in that bin.
284    ///
285    /// # Errors
286    ///
287    /// Returns [`PlotError::EmptyData`] if `data` is empty.
288    pub fn hist<D>(&mut self, data: D, bins: usize) -> Result<&mut HistArtist>
289    where
290        D: IntoSeries,
291    {
292        let series = data.into_series();
293        if series.is_empty() {
294            return Err(PlotError::EmptyData);
295        }
296        let bins = bins.max(1);
297
298        // Compute range from finite values.
299        let (data_min, data_max) = series.bounds().unwrap_or((0.0, 1.0));
300
301        // Handle degenerate case where all values are identical.
302        let (lo, hi) = if (data_max - data_min).abs() < f64::EPSILON {
303            (data_min - 0.5, data_max + 0.5)
304        } else {
305            (data_min, data_max)
306        };
307
308        let bin_width = (hi - lo) / bins as f64;
309
310        // Build bin edges: bins+1 edges.
311        let mut edges: Vec<f64> = (0..=bins).map(|i| lo + i as f64 * bin_width).collect();
312        // Ensure the last edge exactly equals hi to avoid floating-point gaps.
313        *edges.last_mut().expect("edges is non-empty") = hi;
314
315        // Count values per bin.
316        let mut counts = vec![0.0f64; bins];
317        for &v in &series.data {
318            if !v.is_finite() {
319                continue;
320            }
321            // Determine bin index.
322            let idx = if v >= hi {
323                // Values exactly at the upper edge go into the last bin.
324                bins - 1
325            } else {
326                let raw = ((v - lo) / bin_width) as usize;
327                raw.min(bins - 1)
328            };
329            counts[idx] += 1.0;
330        }
331
332        let color = Color::TABLEAU_10[self.color_index % 10];
333        self.color_index += 1;
334        let artist = HistArtist {
335            data: series,
336            bins,
337            bin_edges: edges,
338            counts,
339            color,
340            label: None,
341            alpha: 0.85,
342            density: false,
343        };
344
345
346
347
348
349
350
351        self.artists.push(Artist::Histogram(artist));
352        match self.artists.last_mut().expect("just pushed") {
353            Artist::Histogram(a) => Ok(a),
354            _ => unreachable!(),
355        }
356    }
357
358    /// Fills the area between two y-series that share the same x values.
359    ///
360    /// Useful for confidence intervals, envelopes, and area charts.
361    ///
362    /// # Errors
363    ///
364    /// Returns [`PlotError::SeriesLengthMismatch`] if any of the three
365    /// series differ in length, or [`PlotError::EmptyData`] if empty.
366    pub fn fill_between<X, Y1, Y2>(
367        &mut self,
368        x: X,
369        y1: Y1,
370        y2: Y2,
371    ) -> Result<&mut FillBetweenArtist>
372    where
373        X: IntoSeries,
374        Y1: IntoSeries,
375        Y2: IntoSeries,
376    {
377        let xs = x.into_series();
378        let y1s = y1.into_series();
379        let y2s = y2.into_series();
380        if xs.len() != y1s.len() {
381            return Err(PlotError::SeriesLengthMismatch {
382                expected: xs.len(),
383                got: y1s.len(),
384            });
385        }
386        if xs.len() != y2s.len() {
387            return Err(PlotError::SeriesLengthMismatch {
388                expected: xs.len(),
389                got: y2s.len(),
390            });
391        }
392        if xs.is_empty() {
393            return Err(PlotError::EmptyData);
394        }
395        let color = Color::TABLEAU_10[self.color_index % 10];
396        self.color_index += 1;
397        let artist = FillBetweenArtist {
398            x: xs,
399            y1: y1s,
400            y2: y2s,
401            color,
402            label: None,
403            alpha: 0.3,
404        };
405        self.artists.push(Artist::FillBetween(artist));
406        match self.artists.last_mut().expect("just pushed") {
407            Artist::FillBetween(a) => Ok(a),
408            _ => unreachable!(),
409        }
410    }
411
412    /// Plots a step (staircase) chart connecting `(x, y)` data points.
413    ///
414    /// Returns a mutable reference to the [`StepArtist`] for chaining.
415    ///
416    /// # Errors
417    ///
418    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
419    /// on invalid input.
420    pub fn step<X: IntoSeries, Y: IntoSeries>(
421        &mut self,
422        x: X,
423        y: Y,
424    ) -> Result<&mut StepArtist> {
425        let xs = x.into_series();
426        let ys = y.into_series();
427        if xs.len() != ys.len() {
428            return Err(PlotError::SeriesLengthMismatch {
429                expected: xs.len(),
430                got: ys.len(),
431            });
432        }
433        if xs.is_empty() {
434            return Err(PlotError::EmptyData);
435        }
436        let color = Color::TABLEAU_10[self.color_index % 10];
437        self.color_index += 1;
438        let artist = StepArtist {
439            x: xs,
440            y: ys,
441            color,
442            width: 1.5,
443            where_step: StepWhere::Pre,
444            label: None,
445            alpha: 1.0,
446        };
447        self.artists.push(Artist::Step(artist));
448        match self.artists.last_mut().expect("just pushed") {
449            Artist::Step(a) => Ok(a),
450            _ => unreachable!(),
451        }
452    }
453
454    /// Plots a stem (lollipop) chart from `(x, y)` data points.
455    ///
456    /// Returns a mutable reference to the [`StemArtist`] for chaining.
457    ///
458    /// # Errors
459    ///
460    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
461    /// on invalid input.
462    pub fn stem<X: IntoSeries, Y: IntoSeries>(
463        &mut self,
464        x: X,
465        y: Y,
466    ) -> Result<&mut StemArtist> {
467        let xs = x.into_series();
468        let ys = y.into_series();
469        if xs.len() != ys.len() {
470            return Err(PlotError::SeriesLengthMismatch {
471                expected: xs.len(),
472                got: ys.len(),
473            });
474        }
475        if xs.is_empty() {
476            return Err(PlotError::EmptyData);
477        }
478        let color = Color::TABLEAU_10[self.color_index % 10];
479        self.color_index += 1;
480        let artist = StemArtist {
481            x: xs,
482            y: ys,
483            color,
484            line_width: 1.5,
485            marker_size: 6.0,
486            baseline: 0.0,
487            label: None,
488            alpha: 1.0,
489        };
490        self.artists.push(Artist::Stem(artist));
491        match self.artists.last_mut().expect("just pushed") {
492            Artist::Stem(a) => Ok(a),
493            _ => unreachable!(),
494        }
495    }
496
497    /// Creates a box-and-whisker plot from one or more data groups.
498    ///
499    /// Each inner `Vec<f64>` produces one box. Groups are placed at integer
500    /// x-positions starting from 0.
501    ///
502    /// # Errors
503    ///
504    /// Returns [`PlotError::EmptyData`] if `datasets` is empty.
505    pub fn boxplot(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut BoxPlotArtist> {
506        use crate::charts::boxplot::compute_stats;
507        if datasets.is_empty() {
508            return Err(PlotError::EmptyData);
509        }
510        let color = Color::TABLEAU_10[self.color_index % 10];
511        self.color_index += 1;
512        let factor = 1.5;
513        let stats: Vec<_> = datasets.iter().map(|d| compute_stats(d, factor)).collect();
514        let labels: Vec<String> = (0..datasets.len()).map(|i| format!("{}", i + 1)).collect();
515        let artist = BoxPlotArtist {
516            stats,
517            labels,
518            color,
519            label: None,
520            alpha: 1.0,
521            box_width: 0.5,
522            show_outliers: true,
523            whisker_iq_factor: factor,
524            raw_data: datasets,
525        };
526        self.artists.push(Artist::BoxPlot(artist));
527        match self.artists.last_mut().expect("just pushed") {
528            Artist::BoxPlot(a) => Ok(a),
529            _ => unreachable!(),
530        }
531    }
532
533    /// Creates an error bar plot from `(x, y)` data points.
534    ///
535    /// Returns an owned [`ErrorBarArtist`] that can be configured with
536    /// builder methods (`.yerr_symmetric()`, `.cap_size()`, etc.) and then
537    /// added to the axes via [`add_errorbar`](Axes::add_errorbar).
538    ///
539    /// # Errors
540    ///
541    /// Returns [`PlotError::SeriesLengthMismatch`] or [`PlotError::EmptyData`]
542    /// on invalid input.
543    pub fn errorbar<X: IntoSeries, Y: IntoSeries>(
544        &mut self,
545        x: X,
546        y: Y,
547    ) -> Result<ErrorBarArtist> {
548        let xs = x.into_series();
549        let ys = y.into_series();
550        if xs.len() != ys.len() {
551            return Err(PlotError::SeriesLengthMismatch {
552                expected: xs.len(),
553                got: ys.len(),
554            });
555        }
556        if xs.is_empty() {
557            return Err(PlotError::EmptyData);
558        }
559        let color = Color::TABLEAU_10[self.color_index % 10];
560        self.color_index += 1;
561        Ok(ErrorBarArtist {
562            x: xs,
563            y: ys,
564            xerr: None,
565            yerr: None,
566            color,
567            label: None,
568            cap_size: 4.0,
569            line_width: 1.0,
570        })
571    }
572
573    /// Adds a finalized [`ErrorBarArtist`] to the axes.
574    ///
575    /// Use this after configuring the artist returned by
576    /// [`errorbar`](Axes::errorbar).
577    pub fn add_errorbar(&mut self, artist: ErrorBarArtist) {
578        self.artists.push(Artist::ErrorBar(artist));
579    }
580
581    /// Creates a heatmap from a 2D grid of values.
582    ///
583    /// Returns a mutable reference to the [`HeatmapArtist`] for chaining
584    /// builder methods (`.colormap()`, `.vmin()`, `.show_values()`, etc.).
585    ///
586    /// # Errors
587    ///
588    /// Returns [`PlotError::EmptyData`] if `data` is empty.
589    pub fn heatmap(&mut self, data: Vec<Vec<f64>>) -> Result<&mut HeatmapArtist> {
590        if data.is_empty() {
591            return Err(PlotError::EmptyData);
592        }
593        let color = Color::TABLEAU_10[self.color_index % 10];
594        self.color_index += 1;
595        let artist = HeatmapArtist {
596            data,
597            x_labels: None,
598            y_labels: None,
599            cmap: crate::colormap::Colormap::Viridis,
600            vmin: None,
601            vmax: None,
602            show_values: false,
603            color,
604            label: None,
605        };
606        self.artists.push(Artist::Heatmap(artist));
607        match self.artists.last_mut().expect("just pushed") {
608            Artist::Heatmap(a) => Ok(a),
609            _ => unreachable!(),
610        }
611    }
612}
613
614// ---------------------------------------------------------------------------
615// Configuration methods (builder-style, return &mut Self)
616// ---------------------------------------------------------------------------
617
618impl Axes {
619    /// Sets the title displayed above the axes area.
620    pub fn set_title(&mut self, title: &str) -> &mut Self {
621        self.title = Some(title.to_string());
622        self
623    }
624
625    /// Sets the x-axis label.
626    pub fn set_xlabel(&mut self, label: &str) -> &mut Self {
627        self.xlabel = Some(label.to_string());
628        self
629    }
630
631    /// Sets the y-axis label.
632    pub fn set_ylabel(&mut self, label: &str) -> &mut Self {
633        self.ylabel = Some(label.to_string());
634        self
635    }
636
637    /// Sets explicit x-axis limits. Pass `(min, max)`.
638    pub fn set_xlim(&mut self, min: f64, max: f64) -> &mut Self {
639        self.xlim = Some((min, max));
640        self
641    }
642
643    /// Sets explicit y-axis limits. Pass `(min, max)`.
644    pub fn set_ylim(&mut self, min: f64, max: f64) -> &mut Self {
645        self.ylim = Some((min, max));
646        self
647    }
648
649    /// Sets the x-axis scale (linear, log10, symlog).
650    pub fn set_xscale(&mut self, scale: Scale) -> &mut Self {
651        self.xscale = scale;
652        self
653    }
654
655    /// Sets the y-axis scale (linear, log10, symlog).
656    pub fn set_yscale(&mut self, scale: Scale) -> &mut Self {
657        self.yscale = scale;
658        self
659    }
660
661    /// Enables or disables grid lines on this axes.
662    pub fn grid(&mut self, show: bool) -> &mut Self {
663        self.show_grid = Some(show);
664        self
665    }
666
667    /// Enables the legend on this axes.
668    pub fn legend(&mut self) -> &mut Self {
669        self.show_legend = true;
670        self
671    }
672
673    /// Sets the legend location.
674    pub fn set_legend_loc(&mut self, loc: Loc) -> &mut Self {
675        self.legend_loc = loc;
676        self
677    }
678
679    /// Overrides the figure theme for this axes only.
680    pub fn set_theme(&mut self, theme: Theme) -> &mut Self {
681        self.theme_override = Some(theme);
682        self
683    }
684}
685
686// ---------------------------------------------------------------------------
687// Rendering pipeline
688// ---------------------------------------------------------------------------
689
690#[allow(clippy::too_many_arguments)]
691impl Axes {
692    /// Renders this axes into the given rectangle on the renderer.
693    ///
694    /// This is called by the `Figure` during its render pass. The ten-step
695    /// pipeline is:
696    ///
697    /// 1. Compute data limits (autoscale or user-set).
698    /// 2. Generate tick positions and labels.
699    /// 3. Compute internal layout (margins for title, labels, ticks).
700    /// 4. Draw axes background.
701    /// 5. Draw grid lines (behind data).
702    /// 6. Clip to the plot area and draw each artist.
703    /// 7. Draw axis spines.
704    /// 8. Draw ticks and tick labels.
705    /// 9. Draw axis labels and title.
706    /// 10. Draw legend (if enabled).
707    pub(crate) fn render(&self, renderer: &mut impl Renderer, bounds: Rect, fig_theme: &Theme) {
708        let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
709
710        // Step 1: Compute data limits.
711        let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
712
713        // Step 2: Generate ticks.
714        let xticks = ticks::generate_ticks(xmin, xmax, DEFAULT_TICK_COUNT, &self.xscale);
715        let yticks = ticks::generate_ticks(ymin, ymax, DEFAULT_TICK_COUNT, &self.yscale);
716
717        // Step 3: Compute layout (reserve space for labels, ticks, title).
718        let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
719        layout_config.has_title = self.title.is_some();
720        layout_config.has_xlabel = self.xlabel.is_some();
721        layout_config.has_ylabel = self.ylabel.is_some();
722        layout_config.has_legend = self.show_legend;
723
724
725
726
727
728        let layout_result = layout::compute_layout(&layout_config);
729
730        // Offset the computed plot area by the bounds position.
731        let plot_area = Rect::new(
732            bounds.x + layout_result.plot_area.x,
733            bounds.y + layout_result.plot_area.y,
734            layout_result.plot_area.width,
735            layout_result.plot_area.height,
736        );
737
738        // Step 4: Draw axes background.
739        let bg_path = Path::rect(plot_area);
740        renderer.fill_path(&bg_path, &Paint::new(theme.axes_background), Affine::IDENTITY);
741
742        // Step 5: Draw grid (behind data).
743        if self.show_grid.unwrap_or(theme.show_grid) {
744            self.draw_grid(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
745        }
746
747        // Step 6: Clip to plot area and draw each artist.
748        let clip_path = Path::rect(plot_area);
749        renderer.push_clip(&clip_path, Affine::IDENTITY);
750        for artist in &self.artists {
751            self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
752        }
753        renderer.pop_clip();
754
755        // Step 7: Draw spines.
756        self.draw_spines(renderer, &plot_area, theme);
757
758        // Step 8: Draw ticks and tick labels.
759        self.draw_ticks(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
760
761        // Step 9: Draw axis labels and title.
762        self.draw_labels(renderer, &plot_area, &bounds, theme);
763
764        // Step 10: Draw legend if enabled.
765        if self.show_legend {
766            self.draw_legend(renderer, &plot_area, theme);
767        }
768    }
769
770    // -----------------------------------------------------------------------
771    // Step 1: Data limits
772    // -----------------------------------------------------------------------
773
774    /// Computes the data limits from all artists, applying user overrides
775    /// and padding. Returns `(xmin, xmax, ymin, ymax)`.
776    fn compute_data_limits(&self) -> (f64, f64, f64, f64) {
777        let mut x_lo = f64::INFINITY;
778        let mut x_hi = f64::NEG_INFINITY;
779        let mut y_lo = f64::INFINITY;
780        let mut y_hi = f64::NEG_INFINITY;
781
782        for artist in &self.artists {
783            match artist {
784                Artist::Line(a) => {
785                    if let Some((lo, hi)) = a.x.bounds() {
786                        x_lo = x_lo.min(lo);
787                        x_hi = x_hi.max(hi);
788                    }
789                    if let Some((lo, hi)) = a.y.bounds() {
790                        y_lo = y_lo.min(lo);
791                        y_hi = y_hi.max(hi);
792                    }
793                }
794                Artist::Scatter(a) => {
795                    if let Some((lo, hi)) = a.x.bounds() {
796                        x_lo = x_lo.min(lo);
797                        x_hi = x_hi.max(hi);
798                    }
799                    if let Some((lo, hi)) = a.y.bounds() {
800                        y_lo = y_lo.min(lo);
801                        y_hi = y_hi.max(hi);
802                    }
803                }
804                Artist::Bar(a) => {
805                    let n = a.categories.len() as f64;
806                    if a.horizontal {
807                        // x-axis is the value axis, y-axis is the category axis.
808                        y_lo = 0.0_f64.min(y_lo);
809                        y_hi = n.max(y_hi);
810                        x_lo = 0.0_f64.min(x_lo);
811                        if let Some((lo, hi)) = a.heights.bounds() {
812                            x_lo = x_lo.min(lo.min(0.0));
813                            x_hi = x_hi.max(hi);
814                        }
815                    } else {
816                        // x-axis is the category axis, y-axis is the value axis.
817                        x_lo = 0.0_f64.min(x_lo);
818                        x_hi = n.max(x_hi);
819                        y_lo = 0.0_f64.min(y_lo);
820                        if let Some((lo, hi)) = a.heights.bounds() {
821                            y_lo = y_lo.min(lo.min(0.0));
822                            y_hi = y_hi.max(hi);
823                        }
824                    }
825                }
826                Artist::Histogram(a) => {
827                    if let (Some(&first), Some(&last)) = (a.bin_edges.first(), a.bin_edges.last()) {
828                        x_lo = x_lo.min(first);
829                        x_hi = x_hi.max(last);
830                    }
831                    y_lo = 0.0_f64.min(y_lo);
832                    let max_count = a.counts.iter().fold(0.0f64, |a, &b| a.max(b));
833                    y_hi = y_hi.max(max_count);
834                }
835                Artist::FillBetween(a) => {
836                    if let Some((lo, hi)) = a.x.bounds() {
837                        x_lo = x_lo.min(lo);
838                        x_hi = x_hi.max(hi);
839                    }
840                    if let Some((lo, hi)) = a.y1.bounds() {
841                        y_lo = y_lo.min(lo);
842                        y_hi = y_hi.max(hi);
843                    }
844                    if let Some((lo, hi)) = a.y2.bounds() {
845                        y_lo = y_lo.min(lo);
846                        y_hi = y_hi.max(hi);
847                    }
848                }
849                Artist::Step(a) => {
850                    if let Some((lo, hi)) = a.x.bounds() {
851                        x_lo = x_lo.min(lo);
852                        x_hi = x_hi.max(hi);
853                    }
854                    if let Some((lo, hi)) = a.y.bounds() {
855                        y_lo = y_lo.min(lo);
856                        y_hi = y_hi.max(hi);
857                    }
858                }
859                Artist::Stem(a) => {
860                    if let Some((lo, hi)) = a.x.bounds() {
861                        x_lo = x_lo.min(lo);
862                        x_hi = x_hi.max(hi);
863                    }
864                    if let Some((lo, hi)) = a.y.bounds() {
865                        y_lo = y_lo.min(lo.min(a.baseline));
866                        y_hi = y_hi.max(hi.max(a.baseline));
867                    }
868                }
869                Artist::BoxPlot(a) => {
870                    let n = a.stats.len() as f64;
871                    x_lo = 0.0_f64.min(x_lo);
872                    x_hi = n.max(x_hi);
873                    for s in &a.stats {
874                        y_lo = y_lo.min(s.whisker_low);
875                        y_hi = y_hi.max(s.whisker_high);
876                        for &o in &s.outliers {
877                            y_lo = y_lo.min(o);
878                            y_hi = y_hi.max(o);
879                        }
880                    }
881                }
882                Artist::ErrorBar(a) => {
883                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
884                    x_lo = x_lo.min(bxlo);
885                    x_hi = x_hi.max(bxhi);
886                    y_lo = y_lo.min(bylo);
887                    y_hi = y_hi.max(byhi);
888                }
889                Artist::Heatmap(a) => {
890                    let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
891                    x_lo = x_lo.min(bxlo);
892                    x_hi = x_hi.max(bxhi);
893                    y_lo = y_lo.min(bylo);
894                    y_hi = y_hi.max(byhi);
895                }
896            }
897        }
898
899        // Handle the case where there are no artists or no finite data.
900        if !x_lo.is_finite() || !x_hi.is_finite() {
901            x_lo = 0.0;
902            x_hi = 1.0;
903        }
904        if !y_lo.is_finite() || !y_hi.is_finite() {
905            y_lo = 0.0;
906            y_hi = 1.0;
907        }
908
909        // Handle degenerate ranges (all data at a single point).
910        if (x_hi - x_lo).abs() < f64::EPSILON {
911            x_lo -= 0.5;
912            x_hi += 0.5;
913        }
914        if (y_hi - y_lo).abs() < f64::EPSILON {
915            y_lo -= 0.5;
916            y_hi += 0.5;
917        }
918
919        // Apply padding (5% on each side).
920        let x_pad = (x_hi - x_lo) * AUTOSCALE_PAD;
921        let y_pad = (y_hi - y_lo) * AUTOSCALE_PAD;
922        x_lo -= x_pad;
923        x_hi += x_pad;
924        y_lo -= y_pad;
925        y_hi += y_pad;
926
927        // Apply user-set limits, overriding auto-scale.
928        if let Some((lo, hi)) = self.xlim {
929            x_lo = lo;
930            x_hi = hi;
931        }
932        if let Some((lo, hi)) = self.ylim {
933            y_lo = lo;
934            y_hi = hi;
935        }
936
937        (x_lo, x_hi, y_lo, y_hi)
938    }
939
940    // -----------------------------------------------------------------------
941    // Step 5: Grid
942    // -----------------------------------------------------------------------
943
944    /// Draws major grid lines behind the data.
945    fn draw_grid(
946        &self,
947        renderer: &mut impl Renderer,
948        plot_area: &Rect,
949        xticks: &[ticks::Tick],
950        yticks: &[ticks::Tick],
951        xmin: f64,
952        xmax: f64,
953        ymin: f64,
954        ymax: f64,
955        theme: &Theme,
956    ) {
957        let paint = Paint::new(theme.grid_color);
958        let stroke = Stroke::new(theme.grid_width);
959
960        // Vertical grid lines at each x-tick.
961        for tick in xticks {
962            let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
963            let mut path = Path::new();
964            path.move_to(pt.x, plot_area.y);
965            path.line_to(pt.x, plot_area.bottom());
966            renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
967        }
968
969        // Horizontal grid lines at each y-tick.
970        for tick in yticks {
971            let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
972            let mut path = Path::new();
973            path.move_to(plot_area.x, pt.y);
974            path.line_to(plot_area.right(), pt.y);
975            renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
976        }
977    }
978
979    // -----------------------------------------------------------------------
980    // Step 6: Artist drawing
981    // -----------------------------------------------------------------------
982
983    /// Dispatches drawing to the appropriate artist-type-specific method.
984    fn draw_artist(
985        &self,
986        renderer: &mut impl Renderer,
987        artist: &Artist,
988        plot_area: &Rect,
989        xmin: f64,
990        xmax: f64,
991        ymin: f64,
992        ymax: f64,
993        theme: &Theme,
994    ) {
995        match artist {
996            Artist::Line(a) => self.draw_line(renderer, a, plot_area, xmin, xmax, ymin, ymax),
997            Artist::Scatter(a) => self.draw_scatter(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
998            Artist::Bar(a) => self.draw_bar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
999            Artist::Histogram(a) => self.draw_hist(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1000            Artist::FillBetween(a) => self.draw_fill_between(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1001            Artist::Step(a) => self.draw_step(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1002            Artist::Stem(a) => self.draw_stem(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1003            Artist::BoxPlot(a) => self.draw_boxplot(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1004            Artist::ErrorBar(a) => self.draw_errorbar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1005            Artist::Heatmap(a) => self.draw_heatmap(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1006        }
1007    }
1008
1009    /// Draws a line chart: builds a polyline from data points and strokes it.
1010    fn draw_line(
1011        &self,
1012        renderer: &mut impl Renderer,
1013        artist: &LineArtist,
1014        plot_area: &Rect,
1015        xmin: f64,
1016        xmax: f64,
1017        ymin: f64,
1018        ymax: f64,
1019    ) {
1020        if artist.x.is_empty() {
1021            return;
1022        }
1023
1024        let mut path = Path::new();
1025        let first = self.data_to_pixel(
1026            artist.x.data[0],
1027            artist.y.data[0],
1028            plot_area,
1029            xmin, xmax, ymin, ymax,
1030        );
1031        path.move_to(first.x, first.y);
1032
1033        for i in 1..artist.x.len() {
1034            let pt = self.data_to_pixel(
1035                artist.x.data[i],
1036                artist.y.data[i],
1037                plot_area,
1038                xmin, xmax, ymin, ymax,
1039            );
1040            path.line_to(pt.x, pt.y);
1041        }
1042
1043        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1044        let paint = Paint::new(color);
1045        let mut stroke = Stroke::new(artist.width);
1046
1047        // Apply line style dash pattern.
1048        match artist.style {
1049            crate::theme::LineStyle::Solid => {}
1050            crate::theme::LineStyle::Dashed => {
1051                stroke = stroke.with_dash(DashPattern {
1052                    dashes: vec![6.0, 4.0],
1053                    offset: 0.0,
1054                });
1055            }
1056            crate::theme::LineStyle::Dotted => {
1057                stroke = stroke.with_dash(DashPattern {
1058                    dashes: vec![2.0, 2.0],
1059                    offset: 0.0,
1060                });
1061            }
1062            crate::theme::LineStyle::DashDot => {
1063                stroke = stroke.with_dash(DashPattern {
1064                    dashes: vec![6.0, 3.0, 2.0, 3.0],
1065                    offset: 0.0,
1066                });
1067            }
1068        }
1069
1070        renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1071    }
1072
1073    /// Draws a scatter plot: fills a marker shape at each data point.
1074    fn draw_scatter(
1075        &self,
1076        renderer: &mut impl Renderer,
1077        artist: &ScatterArtist,
1078        plot_area: &Rect,
1079        xmin: f64,
1080        xmax: f64,
1081        ymin: f64,
1082        ymax: f64,
1083        theme: &Theme,
1084    ) {
1085        let alpha_byte = (artist.alpha * 255.0) as u8;
1086
1087        // Pre-compute per-point colors from c/cmap if set.
1088        let cmap_colors: Option<Vec<Color>> = match (&artist.c, &artist.cmap) {
1089            (Some(c_vals), Some(cmap)) => Some(cmap.map_values(c_vals)),
1090            _ => None,
1091        };
1092
1093        let default_color = artist.color.with_alpha(alpha_byte);
1094        let default_paint = Paint::new(default_color);
1095        let radius = artist.size / 2.0;
1096
1097        for i in 0..artist.x.len() {
1098            let pt = self.data_to_pixel(
1099                artist.x.data[i],
1100                artist.y.data[i],
1101                plot_area,
1102                xmin, xmax, ymin, ymax,
1103            );
1104
1105            // Resolve the paint for this point: c/cmap > colors > color.
1106            let paint = if let Some(ref cc) = cmap_colors {
1107                Paint::new(cc[i].with_alpha(alpha_byte))
1108            } else if let Some(ref cs) = artist.colors {
1109                Paint::new(cs[i].with_alpha(alpha_byte))
1110            } else {
1111                default_paint
1112            };
1113
1114            let marker_path = match artist.marker {
1115                Marker::Circle | Marker::Point => Path::circle(pt, radius),
1116                Marker::Square => {
1117                    Path::rect(Rect::new(pt.x - radius, pt.y - radius, radius * 2.0, radius * 2.0))
1118                }
1119                Marker::Diamond => {
1120                    let mut p = Path::new();
1121                    p.move_to(pt.x, pt.y - radius);
1122                    p.line_to(pt.x + radius, pt.y);
1123                    p.line_to(pt.x, pt.y + radius);
1124                    p.line_to(pt.x - radius, pt.y);
1125                    p.close();
1126                    p
1127                }
1128                Marker::Triangle => {
1129                    let mut p = Path::new();
1130                    let h = radius * 1.1547; // 2/sqrt(3) for equilateral
1131                    p.move_to(pt.x, pt.y - radius);
1132                    p.line_to(pt.x + h * 0.5, pt.y + radius * 0.5);
1133                    p.line_to(pt.x - h * 0.5, pt.y + radius * 0.5);
1134                    p.close();
1135                    p
1136                }
1137                Marker::Plus => {
1138                    // Stroked marker: draw two perpendicular lines.
1139                    let mut p = Path::new();
1140                    p.move_to(pt.x - radius, pt.y);
1141                    p.line_to(pt.x + radius, pt.y);
1142                    p.move_to(pt.x, pt.y - radius);
1143                    p.line_to(pt.x, pt.y + radius);
1144                    let stroke = Stroke::new(theme.line_width.max(1.0));
1145                    renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1146                    continue;
1147                }
1148                Marker::Cross => {
1149                    let mut p = Path::new();
1150                    let d = radius * 0.707; // radius / sqrt(2)
1151                    p.move_to(pt.x - d, pt.y - d);
1152                    p.line_to(pt.x + d, pt.y + d);
1153                    p.move_to(pt.x + d, pt.y - d);
1154                    p.line_to(pt.x - d, pt.y + d);
1155                    let stroke = Stroke::new(theme.line_width.max(1.0));
1156                    renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1157                    continue;
1158                }
1159                Marker::Star => {
1160                    // 5-pointed star.
1161                    let mut p = Path::new();
1162                    let inner = radius * 0.382;
1163                    for j in 0..10 {
1164                        let angle = std::f64::consts::FRAC_PI_2
1165                            + j as f64 * std::f64::consts::PI / 5.0;
1166                        let r = if j % 2 == 0 { radius } else { inner };
1167                        let sx = pt.x + r * angle.cos();
1168                        let sy = pt.y - r * angle.sin();
1169                        if j == 0 {
1170                            p.move_to(sx, sy);
1171                        } else {
1172                            p.line_to(sx, sy);
1173                        }
1174                    }
1175                    p.close();
1176                    p
1177                }
1178            };
1179
1180            renderer.fill_path(&marker_path, &paint, Affine::IDENTITY);
1181        }
1182    }
1183
1184    /// Draws a bar chart: fills rectangles for each category.
1185    fn draw_bar(
1186        &self,
1187        renderer: &mut impl Renderer,
1188        artist: &BarArtist,
1189        plot_area: &Rect,
1190        xmin: f64,
1191        xmax: f64,
1192        ymin: f64,
1193        ymax: f64,
1194    ) {
1195        let n = artist.categories.len();
1196        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1197        let paint = Paint::new(color);
1198
1199        if artist.horizontal {
1200            // Horizontal bars: categories on y-axis, values on x-axis.
1201            let cat_range = ymax - ymin;
1202            let cat_step = cat_range / n as f64;
1203            let bar_half = cat_step * artist.bar_width * 0.5;
1204
1205            for i in 0..n {
1206                let cat_center = ymin + (i as f64 + 0.5) * cat_step;
1207                let value = artist.heights.data[i];
1208
1209                let left_val = 0.0_f64.min(value);
1210                let right_val = 0.0_f64.max(value);
1211
1212                let p_left = self.data_to_pixel(left_val, cat_center - bar_half, plot_area, xmin, xmax, ymin, ymax);
1213                let p_right = self.data_to_pixel(right_val, cat_center + bar_half, plot_area, xmin, xmax, ymin, ymax);
1214
1215                let rect = Rect::from_points(p_left, p_right);
1216                let bar_path = Path::rect(rect);
1217                renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
1218            }
1219        } else {
1220            // Vertical bars: categories on x-axis, values on y-axis.
1221            let cat_range = xmax - xmin;
1222            let cat_step = cat_range / n as f64;
1223            let bar_half = cat_step * artist.bar_width * 0.5;
1224
1225            for i in 0..n {
1226                let cat_center = xmin + (i as f64 + 0.5) * cat_step;
1227                let value = artist.heights.data[i];
1228
1229                let bottom_val = 0.0_f64.min(value);
1230                let top_val = 0.0_f64.max(value);
1231
1232                let p_bl = self.data_to_pixel(cat_center - bar_half, bottom_val, plot_area, xmin, xmax, ymin, ymax);
1233                let p_tr = self.data_to_pixel(cat_center + bar_half, top_val, plot_area, xmin, xmax, ymin, ymax);
1234
1235                let rect = Rect::from_points(p_bl, p_tr);
1236                let bar_path = Path::rect(rect);
1237                renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
1238            }
1239        }
1240    }
1241
1242    /// Draws a histogram: fills rectangles from bin edges and counts.
1243    fn draw_hist(
1244        &self,
1245        renderer: &mut impl Renderer,
1246        artist: &HistArtist,
1247        plot_area: &Rect,
1248        xmin: f64,
1249        xmax: f64,
1250        ymin: f64,
1251        ymax: f64,
1252    ) {
1253        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1254        let paint = Paint::new(color);
1255        let stroke_paint = Paint::new(Color::WHITE);
1256        let stroke = Stroke::new(0.5);
1257
1258        for i in 0..artist.counts.len() {
1259            let left = artist.bin_edges[i];
1260            let right = artist.bin_edges[i + 1];
1261            let height = artist.counts[i];
1262
1263            if height <= 0.0 {
1264                continue;
1265            }
1266
1267            let p_bl = self.data_to_pixel(left, 0.0, plot_area, xmin, xmax, ymin, ymax);
1268            let p_tr = self.data_to_pixel(right, height, plot_area, xmin, xmax, ymin, ymax);
1269
1270            let rect = Rect::from_points(p_bl, p_tr);
1271            let bar_path = Path::rect(rect);
1272            renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
1273            // Thin white outline between adjacent bins for visual separation.
1274            renderer.stroke_path(&bar_path, &stroke_paint, &stroke, Affine::IDENTITY);
1275        }
1276    }
1277
1278    /// Draws a fill-between region: builds a closed path from y1 forward and
1279    /// y2 backward, then fills it.
1280    fn draw_fill_between(
1281        &self,
1282        renderer: &mut impl Renderer,
1283        artist: &FillBetweenArtist,
1284        plot_area: &Rect,
1285        xmin: f64,
1286        xmax: f64,
1287        ymin: f64,
1288        ymax: f64,
1289    ) {
1290        if artist.x.is_empty() {
1291            return;
1292        }
1293
1294        let n = artist.x.len();
1295        let mut path = Path::new();
1296
1297        // Forward pass along y1.
1298        let first = self.data_to_pixel(
1299            artist.x.data[0],
1300            artist.y1.data[0],
1301            plot_area,
1302            xmin, xmax, ymin, ymax,
1303        );
1304        path.move_to(first.x, first.y);
1305        for i in 1..n {
1306            let pt = self.data_to_pixel(
1307                artist.x.data[i],
1308                artist.y1.data[i],
1309                plot_area,
1310                xmin, xmax, ymin, ymax,
1311            );
1312            path.line_to(pt.x, pt.y);
1313        }
1314
1315        // Backward pass along y2 (in reverse order).
1316        for i in (0..n).rev() {
1317            let pt = self.data_to_pixel(
1318                artist.x.data[i],
1319                artist.y2.data[i],
1320                plot_area,
1321                xmin, xmax, ymin, ymax,
1322            );
1323            path.line_to(pt.x, pt.y);
1324        }
1325        path.close();
1326
1327        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1328        let paint = Paint::new(color);
1329        renderer.fill_path(&path, &paint, Affine::IDENTITY);
1330    }
1331
1332
1333    // -----------------------------------------------------------------------
1334    // Step 7: Spines
1335    // -----------------------------------------------------------------------
1336
1337    /// Draws the axis spines (border lines around the plot area).
1338    fn draw_spines(
1339        &self,
1340        renderer: &mut impl Renderer,
1341        plot_area: &Rect,
1342        theme: &Theme,
1343    ) {
1344        let paint = Paint::new(theme.spine_color);
1345        let stroke = Stroke::new(theme.spine_width);
1346
1347        // Bottom spine.
1348        if theme.show_bottom_spine {
1349            let mut p = Path::new();
1350            p.move_to(plot_area.x, plot_area.bottom());
1351            p.line_to(plot_area.right(), plot_area.bottom());
1352            renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1353        }
1354        // Left spine.
1355        if theme.show_left_spine {
1356            let mut p = Path::new();
1357            p.move_to(plot_area.x, plot_area.y);
1358            p.line_to(plot_area.x, plot_area.bottom());
1359            renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1360        }
1361        // Top spine.
1362        if theme.show_top_spine {
1363            let mut p = Path::new();
1364            p.move_to(plot_area.x, plot_area.y);
1365            p.line_to(plot_area.right(), plot_area.y);
1366            renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1367        }
1368        // Right spine.
1369        if theme.show_right_spine {
1370            let mut p = Path::new();
1371            p.move_to(plot_area.right(), plot_area.y);
1372            p.line_to(plot_area.right(), plot_area.bottom());
1373            renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1374        }
1375    }
1376
1377    // -----------------------------------------------------------------------
1378    // Step 8: Ticks and tick labels
1379    // -----------------------------------------------------------------------
1380
1381    /// Draws tick marks and their labels along both axes.
1382    fn draw_ticks(
1383        &self,
1384        renderer: &mut impl Renderer,
1385        plot_area: &Rect,
1386        xticks: &[ticks::Tick],
1387        yticks: &[ticks::Tick],
1388        xmin: f64,
1389        xmax: f64,
1390        ymin: f64,
1391        ymax: f64,
1392        theme: &Theme,
1393    ) {
1394        let tick_paint = Paint::new(theme.tick_color);
1395        let tick_stroke = Stroke::new(1.0);
1396        let tick_len = theme.tick_length;
1397
1398        let label_style = TextStyle {
1399            size: theme.tick_label_size,
1400            color: theme.text_color,
1401            weight: FontWeight::Normal,
1402            family: theme.font_family.clone(),
1403            halign: HAlign::Center,
1404            valign: VAlign::Top,
1405        };
1406
1407        // Tick direction offset: outward goes away from plot, inward goes in.
1408        let outward = matches!(theme.tick_direction, TickDirection::Outward);
1409
1410        // --- X-axis ticks (bottom) ---
1411        for tick in xticks {
1412            let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
1413            // Clamp to plot area x-bounds.
1414            if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
1415                continue;
1416            }
1417            let x = pt.x;
1418            let y_base = plot_area.bottom();
1419
1420            // Draw tick mark.
1421            let (y_start, y_end) = if outward {
1422                (y_base, y_base + tick_len)
1423            } else {
1424                (y_base - tick_len, y_base)
1425            };
1426            let mut tp = Path::new();
1427            tp.move_to(x, y_start);
1428            tp.line_to(x, y_end);
1429            renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
1430
1431            // Draw tick label.
1432            let label_y = if outward {
1433                y_base + tick_len + 2.0
1434            } else {
1435                y_base + 2.0
1436            };
1437            renderer.draw_text(
1438                &tick.label,
1439                Point::new(x, label_y),
1440                &label_style,
1441                Affine::IDENTITY,
1442            );
1443        }
1444
1445        // --- Y-axis ticks (left) ---
1446        let y_label_style = TextStyle {
1447            halign: HAlign::Right,
1448            valign: VAlign::Middle,
1449            ..label_style.clone()
1450        };
1451
1452        for tick in yticks {
1453            let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
1454            // Clamp to plot area y-bounds.
1455            if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
1456                continue;
1457            }
1458            let y = pt.y;
1459            let x_base = plot_area.x;
1460
1461            // Draw tick mark.
1462            let (x_start, x_end) = if outward {
1463                (x_base - tick_len, x_base)
1464            } else {
1465                (x_base, x_base + tick_len)
1466            };
1467            let mut tp = Path::new();
1468            tp.move_to(x_start, y);
1469            tp.line_to(x_end, y);
1470            renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
1471
1472            // Draw tick label.
1473            let label_x = if outward {
1474                x_base - tick_len - 3.0
1475            } else {
1476                x_base - 3.0
1477            };
1478            renderer.draw_text(
1479                &tick.label,
1480                Point::new(label_x, y),
1481                &y_label_style,
1482                Affine::IDENTITY,
1483            );
1484        }
1485    }
1486
1487    // -----------------------------------------------------------------------
1488    // Step 9: Labels and title
1489    // -----------------------------------------------------------------------
1490
1491    /// Draws the x-axis label, y-axis label, and title.
1492    fn draw_labels(
1493        &self,
1494        renderer: &mut impl Renderer,
1495        plot_area: &Rect,
1496        bounds: &Rect,
1497        theme: &Theme,
1498    ) {
1499        // Title (centered above plot area).
1500        if let Some(title) = &self.title {
1501            let style = TextStyle {
1502                size: theme.title_size,
1503                color: theme.text_color,
1504                weight: theme.title_weight,
1505                family: theme.font_family.clone(),
1506                halign: HAlign::Center,
1507                valign: VAlign::Bottom,
1508            };
1509            let x = plot_area.x + plot_area.width / 2.0;
1510            let y = plot_area.y - 10.0;
1511            renderer.draw_text(title, Point::new(x, y), &style, Affine::IDENTITY);
1512        }
1513
1514        // X-axis label (centered below tick labels).
1515        if let Some(xlabel) = &self.xlabel {
1516            let style = TextStyle {
1517                size: theme.axis_label_size,
1518                color: theme.text_color,
1519                weight: FontWeight::Normal,
1520                family: theme.font_family.clone(),
1521                halign: HAlign::Center,
1522                valign: VAlign::Top,
1523            };
1524            let x = plot_area.x + plot_area.width / 2.0;
1525            // Position below the tick labels. Approximate tick label height.
1526            let y = plot_area.bottom() + theme.tick_length + theme.tick_label_size + 8.0;
1527            renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
1528        }
1529
1530        // Y-axis label (centered to the left of tick labels, rotated 90 degrees).
1531        if let Some(ylabel) = &self.ylabel {
1532            let style = TextStyle {
1533                size: theme.axis_label_size,
1534                color: theme.text_color,
1535                weight: FontWeight::Normal,
1536                family: theme.font_family.clone(),
1537                halign: HAlign::Center,
1538                valign: VAlign::Bottom,
1539            };
1540            let x = bounds.x + 4.0;
1541            let y = plot_area.y + plot_area.height / 2.0;
1542            // Rotate -90 degrees around the label position for vertical text.
1543            let rotate = Affine::rotate(-std::f64::consts::FRAC_PI_2);
1544            let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
1545            let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
1546            let transform = translate_to * rotate * translate_back;
1547            renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
1548        }
1549    }
1550
1551    // -----------------------------------------------------------------------
1552    // Step 10: Legend
1553    // -----------------------------------------------------------------------
1554
1555
1556    /// Draws a box-and-whisker plot: box from Q1 to Q3, median line, whiskers,
1557    /// caps, and optional outlier dots.
1558    fn draw_boxplot(
1559        &self,
1560        renderer: &mut impl Renderer,
1561        artist: &BoxPlotArtist,
1562        plot_area: &Rect,
1563        xmin: f64,
1564        xmax: f64,
1565        ymin: f64,
1566        ymax: f64,
1567    ) {
1568        let n = artist.stats.len();
1569        if n == 0 {
1570            return;
1571        }
1572
1573        let fill_color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1574        let stroke_color = Color::BLACK.with_alpha((artist.alpha * 255.0) as u8);
1575        let paint = Paint::new(stroke_color);
1576        let thin = Stroke::new(1.0);
1577        let thick = Stroke::new(2.0);
1578        let hair = Stroke::new(0.5);
1579
1580        for (i, stats) in artist.stats.iter().enumerate() {
1581            let cx = i as f64 + 0.5;
1582            let half = artist.box_width / 2.0;
1583            let left = cx - half;
1584            let right = cx + half;
1585
1586            // --- Box (Q1 to Q3) ---
1587            let tl = self.data_to_pixel(left, stats.q3, plot_area, xmin, xmax, ymin, ymax);
1588            let br = self.data_to_pixel(right, stats.q1, plot_area, xmin, xmax, ymin, ymax);
1589            let box_rect_path = {
1590                let mut p = Path::new();
1591                p.move_to(tl.x, tl.y);
1592                p.line_to(br.x, tl.y);
1593                p.line_to(br.x, br.y);
1594                p.line_to(tl.x, br.y);
1595                p.close();
1596                p
1597            };
1598            renderer.fill_path(&box_rect_path, &Paint::new(fill_color), Affine::IDENTITY);
1599            renderer.stroke_path(&box_rect_path, &paint, &thin, Affine::IDENTITY);
1600
1601            // --- Median line ---
1602            let ml = self.data_to_pixel(left, stats.median, plot_area, xmin, xmax, ymin, ymax);
1603            let mr = self.data_to_pixel(right, stats.median, plot_area, xmin, xmax, ymin, ymax);
1604            let mut median_path = Path::new();
1605            median_path.move_to(ml.x, ml.y);
1606            median_path.line_to(mr.x, mr.y);
1607            renderer.stroke_path(&median_path, &paint, &thick, Affine::IDENTITY);
1608
1609            // --- Lower whisker ---
1610            let wl_bottom = self.data_to_pixel(cx, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
1611            let wl_top = self.data_to_pixel(cx, stats.q1, plot_area, xmin, xmax, ymin, ymax);
1612            let mut wl_path = Path::new();
1613            wl_path.move_to(wl_top.x, wl_top.y);
1614            wl_path.line_to(wl_bottom.x, wl_bottom.y);
1615            renderer.stroke_path(&wl_path, &paint, &thin, Affine::IDENTITY);
1616
1617            // Lower cap
1618            let cap_left = self.data_to_pixel(cx - half * 0.5, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
1619            let cap_right = self.data_to_pixel(cx + half * 0.5, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
1620            let mut cap_path = Path::new();
1621            cap_path.move_to(cap_left.x, cap_left.y);
1622            cap_path.line_to(cap_right.x, cap_right.y);
1623            renderer.stroke_path(&cap_path, &paint, &thin, Affine::IDENTITY);
1624
1625            // --- Upper whisker ---
1626            let wu_bottom = self.data_to_pixel(cx, stats.q3, plot_area, xmin, xmax, ymin, ymax);
1627            let wu_top = self.data_to_pixel(cx, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
1628            let mut wu_path = Path::new();
1629            wu_path.move_to(wu_bottom.x, wu_bottom.y);
1630            wu_path.line_to(wu_top.x, wu_top.y);
1631            renderer.stroke_path(&wu_path, &paint, &thin, Affine::IDENTITY);
1632
1633            // Upper cap
1634            let ucap_left = self.data_to_pixel(cx - half * 0.5, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
1635            let ucap_right = self.data_to_pixel(cx + half * 0.5, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
1636            let mut ucap_path = Path::new();
1637            ucap_path.move_to(ucap_left.x, ucap_left.y);
1638            ucap_path.line_to(ucap_right.x, ucap_right.y);
1639            renderer.stroke_path(&ucap_path, &paint, &thin, Affine::IDENTITY);
1640
1641            // --- Outliers ---
1642            if artist.show_outliers {
1643                let r = 3.0;
1644                for &val in &stats.outliers {
1645                    let pt = self.data_to_pixel(cx, val, plot_area, xmin, xmax, ymin, ymax);
1646                    let mut dot = Path::new();
1647                    for seg in 0..8 {
1648                        let angle = std::f64::consts::TAU * seg as f64 / 8.0;
1649                        let dx = r * angle.cos();
1650                        let dy = r * angle.sin();
1651                        if seg == 0 {
1652                            dot.move_to(pt.x + dx, pt.y + dy);
1653                        } else {
1654                            dot.line_to(pt.x + dx, pt.y + dy);
1655                        }
1656                    }
1657                    dot.close();
1658                    renderer.fill_path(&dot, &Paint::new(fill_color), Affine::IDENTITY);
1659                    renderer.stroke_path(&dot, &paint, &hair, Affine::IDENTITY);
1660                }
1661            }
1662        }
1663    }
1664
1665    /// Draws the legend box showing labeled artists.
1666    ///
1667    /// Builds [`LegendEntry`] items from the axes' artists and delegates to
1668    /// [`legend::draw_legend`] for measurement, positioning, and rendering.
1669
1670    /// Draws a step (staircase) chart.
1671    fn draw_step(
1672        &self,
1673        renderer: &mut impl Renderer,
1674        artist: &StepArtist,
1675        plot_area: &Rect,
1676        xmin: f64,
1677        xmax: f64,
1678        ymin: f64,
1679        ymax: f64,
1680    ) {
1681        if artist.x.len() < 2 {
1682            return;
1683        }
1684        let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1685        let paint = Paint::new(color);
1686        let stroke = Stroke::new(artist.width);
1687
1688        let mut path = Path::new();
1689        let first = self.data_to_pixel(
1690            artist.x.data[0], artist.y.data[0],
1691            plot_area, xmin, xmax, ymin, ymax,
1692        );
1693        path.move_to(first.x, first.y);
1694
1695        for i in 1..artist.x.len() {
1696            let prev = self.data_to_pixel(
1697                artist.x.data[i - 1], artist.y.data[i - 1],
1698                plot_area, xmin, xmax, ymin, ymax,
1699            );
1700            let cur = self.data_to_pixel(
1701                artist.x.data[i], artist.y.data[i],
1702                plot_area, xmin, xmax, ymin, ymax,
1703            );
1704            match artist.where_step {
1705                StepWhere::Pre => {
1706                    path.line_to(prev.x, cur.y);
1707                    path.line_to(cur.x, cur.y);
1708                }
1709                StepWhere::Post => {
1710                    path.line_to(cur.x, prev.y);
1711                    path.line_to(cur.x, cur.y);
1712                }
1713                StepWhere::Mid => {
1714                    let mid_x = (prev.x + cur.x) / 2.0;
1715                    path.line_to(mid_x, prev.y);
1716                    path.line_to(mid_x, cur.y);
1717                    path.line_to(cur.x, cur.y);
1718                }
1719            }
1720        }
1721        renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1722    }
1723
1724    /// Draws a stem (lollipop) chart.
1725    fn draw_stem(
1726        &self,
1727        renderer: &mut impl Renderer,
1728        artist: &StemArtist,
1729        plot_area: &Rect,
1730        xmin: f64,
1731        xmax: f64,
1732        ymin: f64,
1733        ymax: f64,
1734    ) {
1735        if artist.x.is_empty() {
1736            return;
1737        }
1738        let alpha_byte = (artist.alpha * 255.0) as u8;
1739        let color = artist.color.with_alpha(alpha_byte);
1740        let paint = Paint::new(color);
1741        let stroke = Stroke::new(artist.line_width);
1742        let radius = artist.marker_size / 2.0;
1743
1744        // Draw baseline.
1745        let bl_left = self.data_to_pixel(
1746            artist.x.data[0], artist.baseline,
1747            plot_area, xmin, xmax, ymin, ymax,
1748        );
1749        let bl_right = self.data_to_pixel(
1750            *artist.x.data.last().unwrap(), artist.baseline,
1751            plot_area, xmin, xmax, ymin, ymax,
1752        );
1753        let mut bl_path = Path::new();
1754        bl_path.move_to(bl_left.x, bl_left.y);
1755        bl_path.line_to(bl_right.x, bl_right.y);
1756        let bl_paint = Paint::new(Color::BLACK.with_alpha(alpha_byte));
1757        let bl_stroke = Stroke::new(0.8);
1758        renderer.stroke_path(&bl_path, &bl_paint, &bl_stroke, Affine::IDENTITY);
1759
1760        // Draw stems and markers.
1761        for i in 0..artist.x.len() {
1762            let base = self.data_to_pixel(
1763                artist.x.data[i], artist.baseline,
1764                plot_area, xmin, xmax, ymin, ymax,
1765            );
1766            let tip = self.data_to_pixel(
1767                artist.x.data[i], artist.y.data[i],
1768                plot_area, xmin, xmax, ymin, ymax,
1769            );
1770            let mut stem_path = Path::new();
1771            stem_path.move_to(base.x, base.y);
1772            stem_path.line_to(tip.x, tip.y);
1773            renderer.stroke_path(&stem_path, &paint, &stroke, Affine::IDENTITY);
1774            let marker = Path::circle(tip, radius);
1775            renderer.fill_path(&marker, &paint, Affine::IDENTITY);
1776        }
1777    }
1778
1779    /// Draws an error bar plot: center line with circle markers, vertical
1780    /// and/or horizontal error bars with caps.
1781    fn draw_errorbar(
1782        &self,
1783        renderer: &mut impl Renderer,
1784        artist: &ErrorBarArtist,
1785        plot_area: &Rect,
1786        xmin: f64,
1787        xmax: f64,
1788        ymin: f64,
1789        ymax: f64,
1790    ) {
1791        if artist.x.is_empty() {
1792            return;
1793        }
1794        let paint = Paint::new(artist.color);
1795        let stroke = Stroke::new(artist.line_width);
1796        let marker_radius = 3.0;
1797
1798        // Draw center connecting line.
1799        let mut line_path = Path::new();
1800        let first = self.data_to_pixel(
1801            artist.x.data[0], artist.y.data[0],
1802            plot_area, xmin, xmax, ymin, ymax,
1803        );
1804        line_path.move_to(first.x, first.y);
1805        for i in 1..artist.x.len() {
1806            let pt = self.data_to_pixel(
1807                artist.x.data[i], artist.y.data[i],
1808                plot_area, xmin, xmax, ymin, ymax,
1809            );
1810            line_path.line_to(pt.x, pt.y);
1811        }
1812        renderer.stroke_path(&line_path, &paint, &stroke, Affine::IDENTITY);
1813
1814        // Draw markers and error bars for each point.
1815        for i in 0..artist.x.len() {
1816            let xv = artist.x.data[i];
1817            let yv = artist.y.data[i];
1818            let center = self.data_to_pixel(xv, yv, plot_area, xmin, xmax, ymin, ymax);
1819
1820            // Circle marker at the data point.
1821            let marker = Path::circle(center, marker_radius);
1822            renderer.fill_path(&marker, &paint, Affine::IDENTITY);
1823
1824            // Vertical error bars (yerr).
1825            if let Some(ref yerr) = artist.yerr {
1826                let (lo, hi) = match yerr {
1827                    ErrorBarData::Symmetric(e) => (yv - e[i], yv + e[i]),
1828                    ErrorBarData::Asymmetric { low, high } => (yv - low[i], yv + high[i]),
1829                };
1830                let pt_lo = self.data_to_pixel(xv, lo, plot_area, xmin, xmax, ymin, ymax);
1831                let pt_hi = self.data_to_pixel(xv, hi, plot_area, xmin, xmax, ymin, ymax);
1832
1833                // Vertical bar.
1834                let mut bar = Path::new();
1835                bar.move_to(pt_lo.x, pt_lo.y);
1836                bar.line_to(pt_hi.x, pt_hi.y);
1837                renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
1838
1839                // Caps.
1840                if artist.cap_size > 0.0 {
1841                    let half_cap = artist.cap_size / 2.0;
1842                    let mut cap_lo = Path::new();
1843                    cap_lo.move_to(pt_lo.x - half_cap, pt_lo.y);
1844                    cap_lo.line_to(pt_lo.x + half_cap, pt_lo.y);
1845                    renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
1846
1847                    let mut cap_hi = Path::new();
1848                    cap_hi.move_to(pt_hi.x - half_cap, pt_hi.y);
1849                    cap_hi.line_to(pt_hi.x + half_cap, pt_hi.y);
1850                    renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
1851                }
1852            }
1853
1854            // Horizontal error bars (xerr).
1855            if let Some(ref xerr) = artist.xerr {
1856                let (lo, hi) = match xerr {
1857                    ErrorBarData::Symmetric(e) => (xv - e[i], xv + e[i]),
1858                    ErrorBarData::Asymmetric { low, high } => (xv - low[i], xv + high[i]),
1859                };
1860                let pt_lo = self.data_to_pixel(lo, yv, plot_area, xmin, xmax, ymin, ymax);
1861                let pt_hi = self.data_to_pixel(hi, yv, plot_area, xmin, xmax, ymin, ymax);
1862
1863                // Horizontal bar.
1864                let mut bar = Path::new();
1865                bar.move_to(pt_lo.x, pt_lo.y);
1866                bar.line_to(pt_hi.x, pt_hi.y);
1867                renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
1868
1869                // Caps.
1870                if artist.cap_size > 0.0 {
1871                    let half_cap = artist.cap_size / 2.0;
1872                    let mut cap_lo = Path::new();
1873                    cap_lo.move_to(pt_lo.x, pt_lo.y - half_cap);
1874                    cap_lo.line_to(pt_lo.x, pt_lo.y + half_cap);
1875                    renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
1876
1877                    let mut cap_hi = Path::new();
1878                    cap_hi.move_to(pt_hi.x, pt_hi.y - half_cap);
1879                    cap_hi.line_to(pt_hi.x, pt_hi.y + half_cap);
1880                    renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
1881                }
1882            }
1883        }
1884    }
1885
1886    /// Draws a heatmap: fills rectangles for each cell, colored via the
1887    /// configured colormap. Optionally draws cell value text.
1888    fn draw_heatmap(
1889        &self,
1890        renderer: &mut impl Renderer,
1891        artist: &HeatmapArtist,
1892        plot_area: &Rect,
1893        xmin: f64,
1894        xmax: f64,
1895        ymin: f64,
1896        ymax: f64,
1897    ) {
1898        let nrows = artist.data.len();
1899        if nrows == 0 {
1900            return;
1901        }
1902        let ncols = artist.data[0].len();
1903        if ncols == 0 {
1904            return;
1905        }
1906
1907        let vmin = artist.effective_vmin();
1908        let vmax = artist.effective_vmax();
1909
1910        let text_style = TextStyle {
1911            size: 10.0,
1912            color: Color::BLACK,
1913            weight: FontWeight::Normal,
1914            family: None,
1915            halign: HAlign::Center,
1916            valign: VAlign::Middle,
1917        };
1918
1919        for row in 0..nrows {
1920            for col in 0..ncols {
1921                let val = artist.data[row][col];
1922                let cell_color = artist.cmap.map_value(val, vmin, vmax);
1923
1924                // Cell rectangle in data space: col..col+1 on x, row..row+1 on y.
1925                let p_bl = self.data_to_pixel(
1926                    col as f64, row as f64,
1927                    plot_area, xmin, xmax, ymin, ymax,
1928                );
1929                let p_tr = self.data_to_pixel(
1930                    (col + 1) as f64, (row + 1) as f64,
1931                    plot_area, xmin, xmax, ymin, ymax,
1932                );
1933                let rect = Rect::from_points(p_bl, p_tr);
1934                let cell_path = Path::rect(rect);
1935                renderer.fill_path(&cell_path, &Paint::new(cell_color), Affine::IDENTITY);
1936
1937                // Optional value text.
1938                if artist.show_values {
1939                    let cx = (p_bl.x + p_tr.x) / 2.0;
1940                    let cy = (p_bl.y + p_tr.y) / 2.0;
1941                    let label = format!("{val:.1}");
1942                    renderer.draw_text(
1943                        &label,
1944                        Point::new(cx, cy),
1945                        &text_style,
1946                        Affine::IDENTITY,
1947                    );
1948                }
1949            }
1950        }
1951    }
1952
1953    fn draw_legend(
1954        &self,
1955        renderer: &mut impl Renderer,
1956        plot_area: &Rect,
1957        theme: &Theme,
1958    ) {
1959        // Collect labeled artists into LegendEntry items, choosing the
1960        // appropriate swatch kind for each artist type.
1961        let entries: Vec<LegendEntry> = self
1962            .artists
1963            .iter()
1964            .filter_map(|a| {
1965                let (label, color, swatch) = match a {
1966                    Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1967                    Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1968                    Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1969                    Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1970                    Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1971                    Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1972                    Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1973                    Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1974                    Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1975                    Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1976                };
1977                label.map(|l| LegendEntry { label: l.to_string(), color, swatch })
1978            })
1979            .collect();
1980
1981        legend::draw_legend(renderer, &entries, plot_area, self.legend_loc, theme);
1982    }
1983
1984    // -----------------------------------------------------------------------
1985    // Coordinate transform
1986    // -----------------------------------------------------------------------
1987
1988    /// Maps a data-space `(x, y)` coordinate to a pixel-space [`Point`] within
1989    /// the given `plot_area` rectangle.
1990    ///
1991    /// The x-axis maps left-to-right and the y-axis maps bottom-to-top (i.e.,
1992    /// pixel y is inverted relative to data y).
1993    fn data_to_pixel(
1994        &self,
1995        x: f64,
1996        y: f64,
1997        plot_area: &Rect,
1998        xmin: f64,
1999        xmax: f64,
2000        ymin: f64,
2001        ymax: f64,
2002    ) -> Point {
2003        let tx = self.xscale.transform(x, xmin, xmax);
2004        let ty = self.yscale.transform(y, ymin, ymax);
2005        Point::new(
2006            plot_area.x + tx * plot_area.width,
2007            plot_area.y + (1.0 - ty) * plot_area.height, // y is inverted
2008        )
2009    }
2010}
2011
2012// ---------------------------------------------------------------------------
2013// Tests
2014// ---------------------------------------------------------------------------
2015
2016#[cfg(test)]
2017mod tests {
2018    use super::*;
2019
2020    #[test]
2021    fn new_axes_has_defaults() {
2022        let ax = Axes::new();
2023        assert!(ax.artists.is_empty());
2024        assert!(ax.title.is_none());
2025        assert!(ax.xlabel.is_none());
2026        assert!(ax.ylabel.is_none());
2027        assert!(ax.xlim.is_none());
2028        assert!(ax.ylim.is_none());
2029        assert!(!ax.show_legend);
2030        assert_eq!(ax.color_index, 0);
2031    }
2032
2033    #[test]
2034    fn plot_creates_line_artist() {
2035        let mut ax = Axes::new();
2036        let result = ax.plot(vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]);
2037        assert!(result.is_ok());
2038        assert_eq!(ax.artists.len(), 1);
2039        assert!(matches!(&ax.artists[0], Artist::Line(_)));
2040        assert_eq!(ax.color_index, 1);
2041    }
2042
2043    #[test]
2044    fn plot_length_mismatch() {
2045        let mut ax = Axes::new();
2046        let result = ax.plot(vec![1.0, 2.0], vec![1.0]);
2047        assert!(matches!(
2048            result,
2049            Err(PlotError::SeriesLengthMismatch { expected: 2, got: 1 })
2050        ));
2051    }
2052
2053    #[test]
2054    fn plot_empty_data() {
2055        let mut ax = Axes::new();
2056        let result = ax.plot(Vec::<f64>::new(), Vec::<f64>::new());
2057        assert!(matches!(result, Err(PlotError::EmptyData)));
2058    }
2059
2060    #[test]
2061    fn scatter_creates_artist() {
2062        let mut ax = Axes::new();
2063        let result = ax.scatter(vec![1.0, 2.0], vec![3.0, 4.0]);
2064        assert!(result.is_ok());
2065        assert!(matches!(&ax.artists[0], Artist::Scatter(_)));
2066    }
2067
2068    #[test]
2069    fn bar_creates_artist() {
2070        let mut ax = Axes::new();
2071        let cats: &[&str] = &["a", "b", "c"];
2072        let result = ax.bar(cats, vec![10.0, 20.0, 30.0]);
2073        assert!(result.is_ok());
2074        match &ax.artists[0] {
2075            Artist::Bar(a) => {
2076                assert!(!a.horizontal);
2077                assert_eq!(a.categories.len(), 3);
2078            }
2079            _ => panic!("expected Bar artist"),
2080        }
2081    }
2082
2083    #[test]
2084    fn barh_creates_horizontal_artist() {
2085        let mut ax = Axes::new();
2086        let cats: &[&str] = &["x", "y"];
2087        let result = ax.barh(cats, vec![5.0, 10.0]);
2088        assert!(result.is_ok());
2089        match &ax.artists[0] {
2090            Artist::Bar(a) => assert!(a.horizontal),
2091            _ => panic!("expected Bar artist"),
2092        }
2093    }
2094
2095    #[test]
2096    fn hist_computes_bins() {
2097        let mut ax = Axes::new();
2098        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2099        let result = ax.hist(data, 5);
2100        assert!(result.is_ok());
2101        match &ax.artists[0] {
2102            Artist::Histogram(a) => {
2103                assert_eq!(a.bin_edges.len(), 6); // 5 bins = 6 edges
2104                assert_eq!(a.counts.len(), 5);
2105                // Total count should equal number of data points.
2106                let total: f64 = a.counts.iter().sum();
2107                assert_eq!(total, 10.0);
2108            }
2109            _ => panic!("expected Hist artist"),
2110        }
2111    }
2112
2113    #[test]
2114    fn hist_single_value() {
2115        let mut ax = Axes::new();
2116        let result = ax.hist(vec![5.0, 5.0, 5.0], 3);
2117        assert!(result.is_ok());
2118        match &ax.artists[0] {
2119            Artist::Histogram(a) => {
2120                let total: f64 = a.counts.iter().sum();
2121                assert_eq!(total, 3.0);
2122            }
2123            _ => panic!("expected Hist artist"),
2124        }
2125    }
2126
2127    #[test]
2128    fn hist_empty_data() {
2129        let mut ax = Axes::new();
2130        let result = ax.hist(Vec::<f64>::new(), 10);
2131        assert!(matches!(result, Err(PlotError::EmptyData)));
2132    }
2133
2134    #[test]
2135    fn fill_between_creates_artist() {
2136        let mut ax = Axes::new();
2137        let result = ax.fill_between(
2138            vec![1.0, 2.0, 3.0],
2139            vec![1.0, 2.0, 1.0],
2140            vec![0.0, 0.0, 0.0],
2141        );
2142        assert!(result.is_ok());
2143        assert!(matches!(&ax.artists[0], Artist::FillBetween(_)));
2144    }
2145
2146    #[test]
2147    fn fill_between_length_mismatch() {
2148        let mut ax = Axes::new();
2149        let result = ax.fill_between(vec![1.0, 2.0], vec![1.0], vec![0.0, 0.0]);
2150        assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
2151    }
2152
2153    #[test]
2154    fn configuration_methods_return_self() {
2155        let mut ax = Axes::new();
2156        ax.set_title("Test")
2157            .set_xlabel("X")
2158            .set_ylabel("Y")
2159            .set_xlim(0.0, 10.0)
2160            .set_ylim(-1.0, 1.0)
2161            .set_xscale(Scale::Linear)
2162            .set_yscale(Scale::Log10)
2163            .grid(true)
2164            .legend();
2165
2166        assert_eq!(ax.title.as_deref(), Some("Test"));
2167        assert_eq!(ax.xlabel.as_deref(), Some("X"));
2168        assert_eq!(ax.ylabel.as_deref(), Some("Y"));
2169        assert_eq!(ax.xlim, Some((0.0, 10.0)));
2170        assert_eq!(ax.ylim, Some((-1.0, 1.0)));
2171        assert_eq!(ax.show_grid, Some(true));
2172        assert!(ax.show_legend);
2173    }
2174
2175    #[test]
2176    fn color_cycle_advances() {
2177        let mut ax = Axes::new();
2178        for _ in 0..12 {
2179            ax.plot(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2180        }
2181        assert_eq!(ax.color_index, 12);
2182        // 12th artist wraps around: index 10 % 10 == 0, so color should
2183        // be the same as the first.
2184        match (&ax.artists[0], &ax.artists[10]) {
2185            (Artist::Line(a), Artist::Line(b)) => {
2186                assert_eq!(a.color, b.color);
2187            }
2188            _ => panic!("expected Line artists"),
2189        }
2190    }
2191
2192    #[test]
2193    fn data_to_pixel_linear() {
2194        let ax = Axes::new();
2195        let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
2196
2197        // Bottom-left corner of data space.
2198        let p = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
2199        assert!((p.x - 100.0).abs() < 1e-10);
2200        assert!((p.y - 350.0).abs() < 1e-10); // bottom of plot
2201
2202        // Top-right corner of data space.
2203        let p = ax.data_to_pixel(10.0, 10.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
2204        assert!((p.x - 500.0).abs() < 1e-10);
2205        assert!((p.y - 50.0).abs() < 1e-10); // top of plot
2206
2207        // Center.
2208        let p = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
2209        assert!((p.x - 300.0).abs() < 1e-10);
2210        assert!((p.y - 200.0).abs() < 1e-10);
2211    }
2212
2213    #[test]
2214    fn compute_data_limits_no_artists() {
2215        let ax = Axes::new();
2216        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
2217        // Should return sensible defaults.
2218        assert!(xmin < xmax);
2219        assert!(ymin < ymax);
2220    }
2221
2222    #[test]
2223    fn compute_data_limits_with_user_override() {
2224        let mut ax = Axes::new();
2225        ax.set_xlim(-5.0, 5.0).set_ylim(0.0, 100.0);
2226        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
2227        assert!((xmin - (-5.0)).abs() < f64::EPSILON);
2228        assert!((xmax - 5.0).abs() < f64::EPSILON);
2229        assert!((ymin - 0.0).abs() < f64::EPSILON);
2230        assert!((ymax - 100.0).abs() < f64::EPSILON);
2231    }
2232
2233    #[test]
2234    fn compute_data_limits_from_line_data() {
2235        let mut ax = Axes::new();
2236        ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
2237        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
2238        // Should encompass data with padding.
2239        assert!(xmin < 1.0);
2240        assert!(xmax > 10.0);
2241        assert!(ymin < 2.0);
2242        assert!(ymax > 8.0);
2243    }
2244    // -- Step chart tests ---------------------------------------------------
2245
2246    #[test]
2247    fn step_creates_artist() {
2248        let mut ax = Axes::new();
2249        let result = ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
2250        assert!(result.is_ok());
2251        assert!(matches!(&ax.artists[0], Artist::Step(_)));
2252    }
2253
2254    #[test]
2255    fn step_length_mismatch() {
2256        let mut ax = Axes::new();
2257        let result = ax.step(vec![1.0, 2.0], vec![1.0]);
2258        assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
2259    }
2260
2261    #[test]
2262    fn step_empty_data() {
2263        let mut ax = Axes::new();
2264        let result = ax.step(Vec::<f64>::new(), Vec::<f64>::new());
2265        assert!(matches!(result, Err(PlotError::EmptyData)));
2266    }
2267
2268    #[test]
2269    fn step_default_where() {
2270        let mut ax = Axes::new();
2271        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2272        match &ax.artists[0] {
2273            Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Pre)),
2274            _ => panic!("expected Step"),
2275        }
2276    }
2277
2278    #[test]
2279    fn step_color_cycle() {
2280        let mut ax = Axes::new();
2281        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2282        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2283        let c0 = ax.artists[0].color();
2284        let c1 = ax.artists[1].color();
2285        assert_ne!(c0, c1);
2286    }
2287
2288    #[test]
2289    fn step_builder_chaining() {
2290        let mut ax = Axes::new();
2291        ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
2292            .unwrap()
2293            .color(Color::TAB_RED)
2294            .width(3.0)
2295            .where_step(StepWhere::Post)
2296            .label("steps")
2297            .alpha(0.5);
2298        match &ax.artists[0] {
2299            Artist::Step(a) => {
2300                assert_eq!(a.color, Color::TAB_RED);
2301                assert!((a.width - 3.0).abs() < 1e-12);
2302                assert!(matches!(a.where_step, StepWhere::Post));
2303                assert_eq!(a.label.as_deref(), Some("steps"));
2304                assert!((a.alpha - 0.5).abs() < 1e-12);
2305            }
2306            _ => panic!("expected Step"),
2307        }
2308    }
2309
2310    #[test]
2311    fn step_data_bounds() {
2312        let mut ax = Axes::new();
2313        ax.step(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
2314        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
2315        assert!(xmin < 1.0);
2316        assert!(xmax > 10.0);
2317        assert!(ymin < 2.0);
2318        assert!(ymax > 8.0);
2319    }
2320
2321    #[test]
2322    fn step_legend_label() {
2323        let mut ax = Axes::new();
2324        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("S");
2325        assert_eq!(ax.artists[0].label(), Some("S"));
2326    }
2327
2328    #[test]
2329    fn step_default_alpha() {
2330        let mut ax = Axes::new();
2331        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2332        match &ax.artists[0] {
2333            Artist::Step(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
2334            _ => panic!("expected Step"),
2335        }
2336    }
2337
2338    #[test]
2339    fn step_default_width() {
2340        let mut ax = Axes::new();
2341        ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2342        match &ax.artists[0] {
2343            Artist::Step(a) => assert!((a.width - 1.5).abs() < 1e-12),
2344            _ => panic!("expected Step"),
2345        }
2346    }
2347
2348    #[test]
2349    fn step_mid_mode() {
2350        let mut ax = Axes::new();
2351        ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
2352            .unwrap()
2353            .where_step(StepWhere::Mid);
2354        match &ax.artists[0] {
2355            Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Mid)),
2356            _ => panic!("expected Step"),
2357        }
2358    }
2359
2360    // -- Stem chart tests ---------------------------------------------------
2361
2362    #[test]
2363    fn stem_creates_artist() {
2364        let mut ax = Axes::new();
2365        let result = ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
2366        assert!(result.is_ok());
2367        assert!(matches!(&ax.artists[0], Artist::Stem(_)));
2368    }
2369
2370    #[test]
2371    fn stem_length_mismatch() {
2372        let mut ax = Axes::new();
2373        let result = ax.stem(vec![1.0, 2.0], vec![1.0]);
2374        assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
2375    }
2376
2377    #[test]
2378    fn stem_empty_data() {
2379        let mut ax = Axes::new();
2380        let result = ax.stem(Vec::<f64>::new(), Vec::<f64>::new());
2381        assert!(matches!(result, Err(PlotError::EmptyData)));
2382    }
2383
2384    #[test]
2385    fn stem_default_baseline() {
2386        let mut ax = Axes::new();
2387        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2388        match &ax.artists[0] {
2389            Artist::Stem(a) => assert!((a.baseline - 0.0).abs() < 1e-12),
2390            _ => panic!("expected Stem"),
2391        }
2392    }
2393
2394    #[test]
2395    fn stem_default_marker_size() {
2396        let mut ax = Axes::new();
2397        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2398        match &ax.artists[0] {
2399            Artist::Stem(a) => assert!((a.marker_size - 6.0).abs() < 1e-12),
2400            _ => panic!("expected Stem"),
2401        }
2402    }
2403
2404    #[test]
2405    fn stem_builder_chaining() {
2406        let mut ax = Axes::new();
2407        ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
2408            .unwrap()
2409            .color(Color::TAB_GREEN)
2410            .baseline(1.0)
2411            .marker_size(8.0)
2412            .width(2.0)
2413            .label("stems")
2414            .alpha(0.7);
2415        match &ax.artists[0] {
2416            Artist::Stem(a) => {
2417                assert_eq!(a.color, Color::TAB_GREEN);
2418                assert!((a.baseline - 1.0).abs() < 1e-12);
2419                assert!((a.marker_size - 8.0).abs() < 1e-12);
2420                assert!((a.line_width - 2.0).abs() < 1e-12);
2421                assert_eq!(a.label.as_deref(), Some("stems"));
2422                assert!((a.alpha - 0.7).abs() < 1e-12);
2423            }
2424            _ => panic!("expected Stem"),
2425        }
2426    }
2427
2428    #[test]
2429    fn stem_data_bounds_include_baseline() {
2430        let mut ax = Axes::new();
2431        ax.stem(vec![1.0, 5.0], vec![2.0, 8.0]).unwrap().baseline(-5.0);
2432        let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
2433        assert!(ymin < -5.0);
2434    }
2435
2436    #[test]
2437    fn stem_legend_label() {
2438        let mut ax = Axes::new();
2439        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("L");
2440        assert_eq!(ax.artists[0].label(), Some("L"));
2441    }
2442
2443    #[test]
2444    fn stem_color_cycle() {
2445        let mut ax = Axes::new();
2446        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2447        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2448        let c0 = ax.artists[0].color();
2449        let c1 = ax.artists[1].color();
2450        assert_ne!(c0, c1);
2451    }
2452
2453    #[test]
2454    fn stem_alpha_default() {
2455        let mut ax = Axes::new();
2456        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2457        match &ax.artists[0] {
2458            Artist::Stem(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
2459            _ => panic!("expected Stem"),
2460        }
2461    }
2462
2463    #[test]
2464    fn stem_negative_baseline() {
2465        let mut ax = Axes::new();
2466        ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().baseline(-3.0);
2467        match &ax.artists[0] {
2468            Artist::Stem(a) => assert!((a.baseline - (-3.0)).abs() < 1e-12),
2469            _ => panic!("expected Stem"),
2470        }
2471    }
2472
2473}