//! Core Plot implementation and types
//!
//! This module provides the main [`Plot`] struct and related types for creating
//! visualizations in ruviz.
//!
//! # Architecture
//!
//! The `Plot` struct is decomposed into focused component managers:
//!
//! - [`PlotConfiguration`] - Display settings (title, labels, dimensions, theme)
//! - [`SeriesManager`] - Data series storage and auto-coloring
//! - [`LayoutManager`] - Legend, grid, ticks, margins, axis limits/scales
//! - [`RenderPipeline`] - Backend selection, parallel/pooled rendering
//!
//! # Usage
//!
//! ```rust,ignore
//! use ruviz::prelude::*;
//!
//! // Simple plot
//! Plot::new()
//!     .line(&x, &y)
//!     .title("My Plot")
//!     .save("plot.png")?;
//!
//! // Multi-series with styling
//! Plot::new()
//!     .line(&x, &y1)
//!     .color(Color::RED)
//!     .label("Series 1")
//!     .line(&x, &y2)
//!     .color(Color::BLUE)
//!     .label("Series 2")
//!     .legend(Position::TopRight)
//!     .save("multi.png")?;
//! ```
//!
//! # Builder Pattern
//!
//! Series methods return [`PlotBuilder<C>`] which provides:
//! - Series-specific configuration (color, line_width, markers)
//! - Plot-level methods forwarded to inner Plot (title, xlabel, theme)
//! - Terminal methods (save, render) that auto-finalize series
//!
//! See [`PlotBuilder`] for details on the generic builder implementation.

mod builder;
mod config;
mod configuration;
pub mod data;
mod image;
mod layout_manager;
mod render_pipeline;
mod series_manager;

pub use builder::{IntoPlot, PlotBuilder, PlotInput, SeriesStyle};
pub use config::{BackendType, GridMode, TickDirection};
pub use configuration::{PlotConfiguration, TextEngineMode};
pub use data::{IntoPlotData, PlotData, PlotText};
pub use image::Image;
pub use layout_manager::LayoutManager;
pub use render_pipeline::RenderPipeline;
pub use series_manager::SeriesManager;

use crate::{
    axes::AxisScale,
    core::{
        Annotation, ArrowStyle, FillStyle, GridStyle, LayoutCalculator, LayoutConfig, Legend,
        LegendItem, LegendItemType, LegendPosition, MarginConfig, MeasuredDimensions, PlotConfig,
        PlotContent, PlotLayout, PlotStyle, PlottingError, Position, REFERENCE_DPI, Result,
        ShapeStyle, TextStyle, pt_to_px,
    },
    data::{
        Data1D, DataShader, NullPolicy, NumericData1D, NumericData2D, StreamingXY,
        collect_numeric_data_1d, collect_numeric_data_2d,
    },
    plots::boxplot::BoxPlotConfig,
    plots::error::errorbar::{ErrorBarConfig, ErrorValues},
    plots::histogram::HistogramConfig,
    plots::traits::PlotRender,
    render::skia::{
        SkiaRenderer, calculate_plot_area_config, calculate_plot_area_dpi, generate_ticks,
        map_data_to_pixels,
    },
    render::{Color, LineStyle, MarkerStyle, Theme},
};
use std::{
    collections::{HashMap, HashSet},
    path::Path,
};

#[cfg(feature = "parallel")]
use crate::render::{ParallelRenderer, SeriesRenderData};

#[cfg(feature = "gpu")]
use crate::render::gpu::GpuRenderer;

/// Main Plot struct - the core API entry point for creating visualizations
///
/// `Plot` provides a fluent builder interface for creating plots with multiple
/// data series, styling options, and export capabilities.
///
/// # Architecture
///
/// The Plot struct delegates to focused component managers:
/// - [`PlotConfiguration`] - Title, labels, dimensions, theme, DPI
/// - [`SeriesManager`] - Data series storage, auto-color assignment
/// - [`LayoutManager`] - Legend, grid, ticks, margins, axis limits/scales
/// - [`RenderPipeline`] - Backend selection, parallel/pooled rendering
///
/// # Example
///
/// ```rust,ignore
/// use ruviz::prelude::*;
///
/// Plot::new()
///     .line(&x, &y)
///     .color(Color::BLUE)
///     .line_width(2.0)
///     .title("My Plot")
///     .xlabel("X")
///     .ylabel("Y")
///     .save("plot.png")?;
/// ```
///
/// # Builder Pattern
///
/// Series methods (`.line()`, `.scatter()`, `.bar()`) return a [`PlotBuilder<C>`]
/// that auto-finalizes when terminal methods (`.save()`, `.render()`) are called.
/// No explicit `.end_series()` is needed.
#[derive(Clone, Debug)]
pub struct Plot {
    /// Display configuration (title, labels, dimensions, theme)
    display: PlotConfiguration,
    /// Series manager (handles all data series and auto-coloring)
    series_mgr: SeriesManager,
    /// Layout manager (handles legend, grid, ticks, margins, axis limits)
    layout: LayoutManager,
    /// Render pipeline (handles backend selection, parallel/pooled rendering)
    render: RenderPipeline,
    /// Annotations (text, arrows, lines, shapes)
    annotations: Vec<Annotation>,
    /// Null policy for dataframe-backed numeric ingestion.
    null_policy: NullPolicy,
    /// Deferred ingestion error captured during builder-style API calls.
    pending_ingestion_error: Option<PendingIngestionError>,
    /// Group metadata used for grouped-series legend behavior.
    series_groups: Vec<SeriesGroupMeta>,
    /// Monotonic group ID allocator for grouped-series builder scopes.
    next_group_id: usize,
}

#[derive(Clone, Debug)]
struct SeriesGroupMeta {
    id: usize,
    label: Option<String>,
}

#[derive(Clone, Debug)]
struct PendingIngestionError {
    kind: PendingIngestionErrorKind,
    additional_count: usize,
}

#[derive(Clone, Debug)]
enum PendingIngestionErrorKind {
    DataTypeUnsupported {
        source: String,
        dtype: String,
        expected: String,
    },
    NullValueNotAllowed {
        source: String,
        column: Option<String>,
        null_count: usize,
    },
    DataExtractionFailed {
        source: String,
        message: String,
    },
    InvalidInput(String),
}

impl PendingIngestionError {
    fn from_plotting_error(err: PlottingError) -> Self {
        let kind = match err {
            PlottingError::DataTypeUnsupported {
                source,
                dtype,
                expected,
            } => PendingIngestionErrorKind::DataTypeUnsupported {
                source,
                dtype,
                expected,
            },
            PlottingError::NullValueNotAllowed {
                source,
                column,
                null_count,
            } => PendingIngestionErrorKind::NullValueNotAllowed {
                source,
                column,
                null_count,
            },
            PlottingError::DataExtractionFailed { source, message } => {
                PendingIngestionErrorKind::DataExtractionFailed { source, message }
            }
            PlottingError::InvalidInput(message) => {
                PendingIngestionErrorKind::InvalidInput(message)
            }
            other => PendingIngestionErrorKind::DataExtractionFailed {
                source: "ruviz::plot-ingestion".to_string(),
                message: other.to_string(),
            },
        };

        Self {
            kind,
            additional_count: 0,
        }
    }

    fn record_additional_error(&mut self) {
        self.additional_count = self.additional_count.saturating_add(1);
    }

    fn to_plotting_error(&self) -> PlottingError {
        let primary_error = match &self.kind {
            PendingIngestionErrorKind::DataTypeUnsupported {
                source,
                dtype,
                expected,
            } => PlottingError::DataTypeUnsupported {
                source: source.clone(),
                dtype: dtype.clone(),
                expected: expected.clone(),
            },
            PendingIngestionErrorKind::NullValueNotAllowed {
                source,
                column,
                null_count,
            } => PlottingError::NullValueNotAllowed {
                source: source.clone(),
                column: column.clone(),
                null_count: *null_count,
            },
            PendingIngestionErrorKind::DataExtractionFailed { source, message } => {
                PlottingError::DataExtractionFailed {
                    source: source.clone(),
                    message: message.clone(),
                }
            }
            PendingIngestionErrorKind::InvalidInput(message) => {
                PlottingError::InvalidInput(message.clone())
            }
        };

        if self.additional_count == 0 {
            return primary_error;
        }

        PlottingError::DataExtractionFailed {
            source: "ruviz::plot-ingestion".to_string(),
            message: format!(
                "{} (and {} additional ingestion error{})",
                primary_error,
                self.additional_count,
                if self.additional_count == 1 { "" } else { "s" }
            ),
        }
    }
}

/// Configuration for a single data series
#[derive(Clone, Debug)]
pub(crate) struct PlotSeries {
    /// Series type
    series_type: SeriesType,
    /// Series label for legend
    label: Option<String>,
    /// Series color (None for auto-color)
    color: Option<Color>,
    /// Line width override
    line_width: Option<f32>,
    /// Line style override
    line_style: Option<LineStyle>,
    /// Marker style for scatter plots
    marker_style: Option<MarkerStyle>,
    /// Marker size for scatter plots
    marker_size: Option<f32>,
    /// Alpha/transparency override
    alpha: Option<f32>,
    /// Optional Y error bar data (attached to series)
    y_errors: Option<ErrorValues>,
    /// Optional X error bar data (attached to series)
    x_errors: Option<ErrorValues>,
    /// Error bar configuration (cap size, line width, etc.)
    error_config: Option<ErrorBarConfig>,
    /// Optional group ID if this series was created inside `Plot::group(...)`.
    group_id: Option<usize>,
}

impl PlotSeries {
    fn to_legend_item_with_label(
        &self,
        label: String,
        default_color: Color,
        theme: &Theme,
    ) -> Option<LegendItem> {
        if label.is_empty() {
            return None;
        }

        self.build_legend_item(label, default_color, theme)
    }

    /// Create a LegendItem from this series
    ///
    /// Returns None if the series has no label
    fn to_legend_item(&self, default_color: Color, theme: &Theme) -> Option<LegendItem> {
        let label = self.label.as_ref()?;
        self.build_legend_item(label.clone(), default_color, theme)
    }

    fn build_legend_item(
        &self,
        label: String,
        default_color: Color,
        theme: &Theme,
    ) -> Option<LegendItem> {
        let color = self.color.unwrap_or(default_color);
        let line_width = self.line_width.unwrap_or(theme.line_width);
        let line_style = self.line_style.clone().unwrap_or(LineStyle::Solid);
        let marker_style = self.marker_style.unwrap_or(MarkerStyle::Circle);
        let marker_size = self.marker_size.unwrap_or(6.0);

        let item_type = match &self.series_type {
            SeriesType::Line { .. } => {
                if self.marker_style.is_some() {
                    LegendItemType::LineMarker {
                        line_style,
                        line_width,
                        marker: marker_style,
                        marker_size,
                    }
                } else {
                    LegendItemType::Line {
                        style: line_style,
                        width: line_width,
                    }
                }
            }
            SeriesType::Scatter { .. } => LegendItemType::Scatter {
                marker: marker_style,
                size: marker_size,
            },
            SeriesType::Bar { .. } => LegendItemType::Bar,
            SeriesType::ErrorBars { .. } | SeriesType::ErrorBarsXY { .. } => {
                LegendItemType::ErrorBar
            }
            SeriesType::Histogram { .. } => LegendItemType::Histogram,
            SeriesType::BoxPlot { .. } => LegendItemType::Bar,
            SeriesType::Heatmap { .. } => return None,
            SeriesType::Kde { .. } | SeriesType::Ecdf { .. } | SeriesType::Polar { .. } => {
                LegendItemType::Line {
                    style: line_style,
                    width: line_width,
                }
            }
            SeriesType::Violin { .. } | SeriesType::Boxen { .. } | SeriesType::Pie { .. } => {
                LegendItemType::Bar
            }
            SeriesType::Contour { .. } => return None,
            SeriesType::Radar { .. } => LegendItemType::Area {
                edge_color: Some(color),
            },
        };

        let has_error_bars = self.y_errors.is_some() || self.x_errors.is_some();

        Some(LegendItem {
            label,
            color,
            item_type,
            has_error_bars,
        })
    }

    /// Create multiple LegendItems from this series (for Radar series that contain multiple internal series)
    ///
    /// Returns a Vec of legend items, expanding radar series into individual entries per data series.
    fn to_legend_items(&self, base_color_idx: usize, theme: &Theme) -> Vec<LegendItem> {
        let line_width = self.line_width.unwrap_or(theme.line_width);
        let line_style = self.line_style.clone().unwrap_or(LineStyle::Solid);

        match &self.series_type {
            SeriesType::Radar { data } => {
                // For radar charts, create a legend item for each internal series
                // Use Area type to show filled swatches matching the filled polygon style
                data.series
                    .iter()
                    .enumerate()
                    .map(|(idx, radar_series)| {
                        let color = data
                            .config
                            .colors
                            .as_ref()
                            .and_then(|colors| colors.get(idx).copied())
                            .unwrap_or_else(|| theme.get_color(base_color_idx + idx));
                        // Use a more visible alpha for legend swatches (0.6 instead of fill_alpha)
                        // This ensures legend items are clearly visible while still showing fill style
                        let fill_color = color.with_alpha(0.6);
                        LegendItem {
                            label: radar_series.label.clone(),
                            color: fill_color,
                            item_type: LegendItemType::Area {
                                edge_color: Some(color), // Use full-opacity color for edge
                            },
                            has_error_bars: false,
                        }
                    })
                    .collect()
            }
            _ => {
                // For other series types, use the single-item method
                let default_color = theme.get_color(base_color_idx);
                self.to_legend_item(default_color, theme)
                    .into_iter()
                    .collect()
            }
        }
    }
}

/// Types of plot series
#[derive(Clone, Debug)]
pub(crate) enum SeriesType {
    Line {
        x_data: PlotData,
        y_data: PlotData,
    },
    Scatter {
        x_data: PlotData,
        y_data: PlotData,
    },
    Bar {
        categories: Vec<String>,
        values: PlotData,
    },
    ErrorBars {
        x_data: PlotData,
        y_data: PlotData,
        y_errors: PlotData,
    },
    ErrorBarsXY {
        x_data: PlotData,
        y_data: PlotData,
        x_errors: PlotData,
        y_errors: PlotData,
    },
    Histogram {
        data: PlotData,
        config: crate::plots::histogram::HistogramConfig,
    },
    BoxPlot {
        data: PlotData,
        config: crate::plots::boxplot::BoxPlotConfig,
    },
    Heatmap {
        data: crate::plots::heatmap::HeatmapData,
    },
    /// KDE (Kernel Density Estimation) plot
    Kde {
        data: crate::plots::KdeData,
    },
    /// ECDF (Empirical Cumulative Distribution Function) plot
    Ecdf {
        data: crate::plots::EcdfData,
    },
    /// Violin plot
    Violin {
        data: crate::plots::ViolinData,
    },
    /// Boxen (Letter-Value) plot
    Boxen {
        data: crate::plots::BoxenData,
    },
    /// Contour plot
    Contour {
        data: crate::plots::continuous::contour::ContourPlotData,
    },
    /// Pie chart
    Pie {
        data: crate::plots::composition::pie::PieData,
    },
    /// Radar chart
    Radar {
        data: crate::plots::polar::radar::RadarPlotData,
    },
    /// Polar plot
    Polar {
        data: crate::plots::polar::polar_plot::PolarPlotData,
    },
}

impl SeriesType {
    /// Check if this series contains any reactive data
    pub fn is_reactive(&self) -> bool {
        match self {
            SeriesType::Line { x_data, y_data } => x_data.is_reactive() || y_data.is_reactive(),
            SeriesType::Scatter { x_data, y_data } => x_data.is_reactive() || y_data.is_reactive(),
            SeriesType::Bar { values, .. } => values.is_reactive(),
            SeriesType::ErrorBars {
                x_data,
                y_data,
                y_errors,
            } => x_data.is_reactive() || y_data.is_reactive() || y_errors.is_reactive(),
            SeriesType::ErrorBarsXY {
                x_data,
                y_data,
                x_errors,
                y_errors,
            } => {
                x_data.is_reactive()
                    || y_data.is_reactive()
                    || x_errors.is_reactive()
                    || y_errors.is_reactive()
            }
            SeriesType::Histogram { data, .. } => data.is_reactive(),
            SeriesType::BoxPlot { data, .. } => data.is_reactive(),
            // Other types use their own data structures, not PlotData
            _ => false,
        }
    }

    /// Resolve all PlotData in this series to static Vec<f64> at the given time
    ///
    /// Returns a new SeriesType with all PlotData converted to PlotData::Static
    pub fn resolve(&self, time: f64) -> SeriesType {
        match self {
            SeriesType::Line { x_data, y_data } => SeriesType::Line {
                x_data: PlotData::Static(x_data.resolve(time)),
                y_data: PlotData::Static(y_data.resolve(time)),
            },
            SeriesType::Scatter { x_data, y_data } => SeriesType::Scatter {
                x_data: PlotData::Static(x_data.resolve(time)),
                y_data: PlotData::Static(y_data.resolve(time)),
            },
            SeriesType::Bar { categories, values } => SeriesType::Bar {
                categories: categories.clone(),
                values: PlotData::Static(values.resolve(time)),
            },
            SeriesType::ErrorBars {
                x_data,
                y_data,
                y_errors,
            } => SeriesType::ErrorBars {
                x_data: PlotData::Static(x_data.resolve(time)),
                y_data: PlotData::Static(y_data.resolve(time)),
                y_errors: PlotData::Static(y_errors.resolve(time)),
            },
            SeriesType::ErrorBarsXY {
                x_data,
                y_data,
                x_errors,
                y_errors,
            } => SeriesType::ErrorBarsXY {
                x_data: PlotData::Static(x_data.resolve(time)),
                y_data: PlotData::Static(y_data.resolve(time)),
                x_errors: PlotData::Static(x_errors.resolve(time)),
                y_errors: PlotData::Static(y_errors.resolve(time)),
            },
            SeriesType::Histogram { data, config } => SeriesType::Histogram {
                data: PlotData::Static(data.resolve(time)),
                config: config.clone(),
            },
            SeriesType::BoxPlot { data, config } => SeriesType::BoxPlot {
                data: PlotData::Static(data.resolve(time)),
                config: config.clone(),
            },
            // Other types don't use PlotData - clone as-is
            other => other.clone(),
        }
    }

    /// Get resolved x_data as Vec<f64> for series that carry x-data.
    #[inline]
    pub fn try_x_data_resolved(&self, time: f64) -> Option<Vec<f64>> {
        match self {
            SeriesType::Line { x_data, .. }
            | SeriesType::Scatter { x_data, .. }
            | SeriesType::ErrorBars { x_data, .. }
            | SeriesType::ErrorBarsXY { x_data, .. } => Some(x_data.resolve(time)),
            _ => None,
        }
    }

    /// Get resolved y_data as Vec<f64> for series that carry y-data.
    #[inline]
    pub fn try_y_data_resolved(&self, time: f64) -> Option<Vec<f64>> {
        match self {
            SeriesType::Line { y_data, .. }
            | SeriesType::Scatter { y_data, .. }
            | SeriesType::ErrorBars { y_data, .. }
            | SeriesType::ErrorBarsXY { y_data, .. } => Some(y_data.resolve(time)),
            _ => None,
        }
    }

    /// Get resolved x_data as Vec<f64> (panics if not Line/Scatter/ErrorBars).
    #[deprecated(note = "Use try_x_data_resolved() for non-panicking behavior")]
    #[inline]
    pub fn x_data_resolved(&self, time: f64) -> Vec<f64> {
        self.try_x_data_resolved(time)
            .expect("x_data not available for this series type")
    }

    /// Get resolved y_data as Vec<f64> (panics if not Line/Scatter/ErrorBars).
    #[deprecated(note = "Use try_y_data_resolved() for non-panicking behavior")]
    #[inline]
    pub fn y_data_resolved(&self, time: f64) -> Vec<f64> {
        self.try_y_data_resolved(time)
            .expect("y_data not available for this series type")
    }
}

/// Legend configuration (legacy, for backward compatibility)
#[derive(Clone, Debug)]
pub(crate) struct LegendConfig {
    /// Whether to show legend
    enabled: bool,
    /// Legend position
    position: Position,
    /// Font size override
    font_size: Option<f32>,
    /// Corner radius for rounded corners
    corner_radius: Option<f32>,
    /// Number of columns (1 = vertical, >1 = horizontal/multi-column)
    columns: Option<usize>,
}

impl Default for LegendConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            position: Position::TopRight,
            font_size: None,
            corner_radius: None,
            columns: None,
        }
    }
}

impl LegendConfig {
    /// Convert to new Legend type for rendering
    fn to_legend(&self) -> Legend {
        let mut legend = Legend {
            enabled: self.enabled,
            position: LegendPosition::from_position(self.position),
            font_size: self.font_size.unwrap_or(10.0),
            ..Legend::default()
        };
        if let Some(radius) = self.corner_radius {
            legend.style.corner_radius = radius;
        }
        if let Some(cols) = self.columns {
            legend.columns = cols;
        }
        legend
    }
}

// NOTE: GridConfig has been replaced by the unified GridStyle from core module.
// See `grid_style: GridStyle` field in Plot struct for grid configuration.

/// Tick configuration for axes
#[derive(Clone, Debug)]
pub(crate) struct TickConfig {
    /// Direction ticks point (inside or outside)
    direction: TickDirection,
    /// Number of major ticks on X axis
    major_ticks_x: usize,
    /// Number of minor ticks between major ticks on X axis
    minor_ticks_x: usize,
    /// Number of major ticks on Y axis
    major_ticks_y: usize,
    /// Number of minor ticks between major ticks on Y axis
    minor_ticks_y: usize,
    /// Grid display mode
    grid_mode: GridMode,
}

impl Default for TickConfig {
    fn default() -> Self {
        TickConfig {
            direction: TickDirection::Inside,
            major_ticks_x: 10,
            minor_ticks_x: 0,
            major_ticks_y: 8,
            minor_ticks_y: 0,
            grid_mode: GridMode::MajorOnly,
        }
    }
}

impl Plot {
    /// Create a new Plot with default settings
    ///
    /// Uses matplotlib-compatible defaults:
    /// - Figure size: 6.4 × 4.8 inches
    /// - DPI: 100 (640 × 480 pixels)
    /// - Font size: 10pt base
    /// - Line width: 1.5pt
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    /// let y = vec![1.0, 4.0, 9.0, 16.0, 25.0];
    ///
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .end_series()
    ///     .save("plot.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn new() -> Self {
        let config = PlotConfig::default();
        let (width, height) = config.canvas_size();
        let display = PlotConfiguration {
            title: None,
            xlabel: None,
            ylabel: None,
            dimensions: (width, height),
            dpi: config.figure.dpi as u32,
            theme: Theme::default(),
            text_engine: TextEngineMode::Plain,
            config,
        };
        Self {
            display,
            series_mgr: SeriesManager::new(),
            layout: LayoutManager::new(),
            render: RenderPipeline::new(),
            annotations: Vec::new(),
            null_policy: NullPolicy::Error,
            pending_ingestion_error: None,
            series_groups: Vec::new(),
            next_group_id: 0,
        }
    }

    /// Create a new Plot with a specific configuration
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let config = PlotConfig::builder()
    ///     .figure(8.0, 6.0)
    ///     .dpi(300.0)
    ///     .build();
    /// Plot::with_config(config).line(&x, &y).save("plot.png")?;
    /// ```
    pub fn with_config(config: PlotConfig) -> Self {
        let (width, height) = config.canvas_size();
        let mut plot = Self::new();
        plot.display.dimensions = (width, height);
        plot.display.dpi = config.figure.dpi as u32;
        plot.display.config = config;
        plot
    }

    /// Create a new Plot with a preset style
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::with_style(PlotStyle::Publication)
    ///     .line(&x, &y)
    ///     .save("paper_figure.png")?;
    /// ```
    pub fn with_style(style: PlotStyle) -> Self {
        Self::with_config(style.config())
    }

    /// Create a new Plot with a specific theme
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x = vec![1.0, 2.0, 3.0, 4.0];
    /// let y = vec![1.0, 4.0, 2.0, 3.0];
    ///
    /// Plot::with_theme(Theme::dark())
    ///     .line(&x, &y)
    ///     .end_series()
    ///     .save("dark_plot.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn with_theme(theme: Theme) -> Self {
        let mut plot = Self::new();
        plot.display.theme = theme;
        plot
    }

    /// Set null handling policy for dataframe-backed numeric ingestion.
    ///
    /// Default is [`NullPolicy::Error`], which fails on null values with
    /// an explicit plotting error.
    pub fn null_policy(mut self, policy: NullPolicy) -> Self {
        self.null_policy = policy;
        self
    }

    fn set_pending_ingestion_error(&mut self, err: PlottingError) {
        if let Some(existing) = &mut self.pending_ingestion_error {
            existing.record_additional_error();
        } else {
            self.pending_ingestion_error = Some(PendingIngestionError::from_plotting_error(err));
        }
    }

    fn pending_ingestion_error(&self) -> Option<PlottingError> {
        self.pending_ingestion_error
            .as_ref()
            .map(PendingIngestionError::to_plotting_error)
    }

    fn register_series_group(&mut self) -> usize {
        let group_id = self.next_group_id;
        self.next_group_id = self
            .next_group_id
            .checked_add(1)
            .expect("series group id overflow");
        self.series_groups.push(SeriesGroupMeta {
            id: group_id,
            label: None,
        });
        group_id
    }

    fn set_series_group_label(&mut self, group_id: usize, label: String) {
        let mut found = false;
        if let Some(group) = self
            .series_groups
            .iter_mut()
            .find(|group| group.id == group_id)
        {
            group.label = Some(label);
            found = true;
        }
        debug_assert!(
            found,
            "set_series_group_label called with unknown group_id: {}",
            group_id
        );
    }

    fn collect_legend_items(&self) -> Vec<LegendItem> {
        let mut legend_items = Vec::new();
        let mut seen_group_ids = HashSet::new();
        let group_labels: HashMap<usize, &str> = self
            .series_groups
            .iter()
            .filter_map(|group| group.label.as_deref().map(|label| (group.id, label)))
            .collect();

        for (idx, series) in self.series_mgr.series.iter().enumerate() {
            if let Some(group_id) = series.group_id {
                if !seen_group_ids.insert(group_id) {
                    continue;
                }

                let Some(label) = group_labels.get(&group_id).copied() else {
                    continue;
                };

                let default_color = self.display.theme.get_color(idx);
                if let Some(item) = series.to_legend_item_with_label(
                    label.to_string(),
                    default_color,
                    &self.display.theme,
                ) {
                    legend_items.push(item);
                }
                continue;
            }

            legend_items.extend(series.to_legend_items(idx, &self.display.theme));
        }

        legend_items
    }

    /// Set the theme for the plot (fluent API)
    ///
    /// Available themes include:
    /// - `Theme::light()` - default light theme
    /// - `Theme::dark()` - dark mode theme
    /// - `Theme::seaborn()` - seaborn-style theme
    /// - `Theme::publication()` - publication-ready theme
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
    /// let y: Vec<f64> = x.iter().map(|&v| v.sin()).collect();
    ///
    /// Plot::new()
    ///     .theme(Theme::dark())
    ///     .line(&x, &y)
    ///     .end_series()
    ///     .save("dark_theme.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// | Default | Dark | Seaborn | Publication |
    /// |---------|------|---------|-------------|
    /// | ![Default](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/theme_default.png) | ![Dark](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/theme_dark.png) | ![Seaborn](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/theme_seaborn.png) | ![Publication](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/theme_publication.png) |
    pub fn theme(mut self, theme: Theme) -> Self {
        self.display.theme = theme;
        self
    }

    /// Get the current theme
    pub fn get_theme(&self) -> Theme {
        self.display.theme.clone()
    }

    /// Scale typography by a factor
    ///
    /// This is useful for rendering plots to smaller canvases (like subplots)
    /// where the default font sizes would be too large.
    ///
    /// # Example
    ///
    /// ```rust
    /// use ruviz::prelude::*;
    ///
    /// let plot = Plot::new()
    ///     .line(&[1.0, 2.0], &[1.0, 4.0])
    ///     .end_series()
    ///     .title("Scaled Plot")
    ///     .scale_typography(0.7); // 70% of normal font sizes
    /// ```
    pub fn scale_typography(mut self, factor: f32) -> Self {
        self.display.config.typography = self.display.config.typography.scale(factor);
        self
    }

    /// Configure parallel rendering settings
    #[cfg(feature = "parallel")]
    pub fn with_parallel(mut self, threads: Option<usize>) -> Self {
        if let Some(thread_count) = threads {
            self.render.parallel_renderer = ParallelRenderer::with_threads(thread_count);
        }
        self
    }

    /// Set parallel processing threshold
    #[cfg(feature = "parallel")]
    pub fn parallel_threshold(mut self, threshold: usize) -> Self {
        self.render.parallel_renderer = self.render.parallel_renderer.with_threshold(threshold);
        self
    }

    /// Enable memory pooled rendering for allocation optimization
    ///
    /// This reduces allocation overhead by 30-50% for large datasets by reusing
    /// memory buffers for coordinate transformations and rendering operations.
    pub fn with_memory_pooling(mut self, enable: bool) -> Self {
        self.render.enable_pooled_rendering = enable;
        if enable && self.render.pooled_renderer.is_none() {
            self.render.pooled_renderer = Some(crate::render::PooledRenderer::new());
        }
        self
    }

    /// Configure memory pool sizes for specific workloads
    ///
    /// # Arguments
    /// * `f32_pool_size` - Initial capacity for coordinate transformation pools
    /// * `position_pool_size` - Initial capacity for position/point pools  
    /// * `segment_pool_size` - Initial capacity for line segment pools
    pub fn with_pool_sizes(
        mut self,
        f32_pool_size: usize,
        position_pool_size: usize,
        segment_pool_size: usize,
    ) -> Self {
        self.render.pooled_renderer = Some(crate::render::PooledRenderer::with_pool_sizes(
            f32_pool_size,
            position_pool_size,
            segment_pool_size,
        ));
        self.render.enable_pooled_rendering = true;
        self
    }

    /// Get memory pool statistics for monitoring and optimization
    pub fn pool_stats(&self) -> Option<crate::render::PooledRendererStats> {
        self.render
            .pooled_renderer
            .as_ref()
            .map(|renderer| renderer.get_pool_stats())
    }

    /// Set the plot title
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .title("My Plot Title")
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .end_series()
    ///     .save("titled.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn title<S: Into<String>>(mut self, title: S) -> Self {
        self.display.title = Some(data::PlotText::Static(title.into()));
        self
    }

    /// Set the plot title as a reactive signal.
    ///
    /// Use this for animations where the title changes over time.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use ruviz::prelude::*;
    /// use ruviz::animation::signal;
    ///
    /// let title_signal = signal::of(|t| format!("Time: {:.2}s", t));
    /// let plot = Plot::new()
    ///     .title_signal(title_signal)
    ///     .line(&x, &y);
    /// ```
    pub fn title_signal(mut self, title: impl Into<data::PlotText>) -> Self {
        self.display.title = Some(title.into());
        self
    }

    /// Set the X-axis label
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .xlabel("Time (s)")
    ///     .ylabel("Amplitude")
    ///     .line(&[0.0, 1.0, 2.0], &[0.0, 0.5, 1.0])
    ///     .end_series()
    ///     .save("labeled.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn xlabel<S: Into<String>>(mut self, label: S) -> Self {
        self.display.xlabel = Some(data::PlotText::Static(label.into()));
        self
    }

    /// Set the X-axis label as a reactive signal.
    pub fn xlabel_signal(mut self, label: impl Into<data::PlotText>) -> Self {
        self.display.xlabel = Some(label.into());
        self
    }

    /// Set the Y-axis label
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .ylabel("Temperature (°C)")
    ///     .line(&[1.0, 2.0, 3.0], &[20.0, 22.0, 21.0])
    ///     .end_series()
    ///     .save("ylabel.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn ylabel<S: Into<String>>(mut self, label: S) -> Self {
        self.display.ylabel = Some(data::PlotText::Static(label.into()));
        self
    }

    /// Set the Y-axis label as a reactive signal.
    pub fn ylabel_signal(mut self, label: impl Into<data::PlotText>) -> Self {
        self.display.ylabel = Some(label.into());
        self
    }

    /// Set X-axis limits (min, max)
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .xlim(0.0, 10.0)
    ///     .ylim(-1.0, 1.0)
    ///     .line(&[0.0, 5.0, 10.0], &[0.0, 1.0, 0.0])
    ///     .end_series()
    ///     .save("limits.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn xlim(mut self, min: f64, max: f64) -> Self {
        if min < max && min.is_finite() && max.is_finite() {
            self.layout.x_limits = Some((min, max));
        }
        self
    }

    /// Set Y-axis limits (min, max)
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .ylim(0.0, 100.0)
    ///     .line(&[1.0, 2.0, 3.0], &[25.0, 50.0, 75.0])
    ///     .end_series()
    ///     .save("ylim.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn ylim(mut self, min: f64, max: f64) -> Self {
        if min < max && min.is_finite() && max.is_finite() {
            self.layout.y_limits = Some((min, max));
        }
        self
    }

    /// Set X-axis scale type
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use ruviz::prelude::*;
    ///
    /// // Logarithmic X axis
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .xscale(AxisScale::Log)
    ///     .save("log_x.png")?;
    ///
    /// // Symmetric log for data with zeros or negatives
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .xscale(AxisScale::symlog(1.0))
    ///     .save("symlog_x.png")?;
    /// ```
    pub fn xscale(mut self, scale: AxisScale) -> Self {
        self.layout.x_scale = scale;
        self
    }

    /// Set Y-axis scale type
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use ruviz::prelude::*;
    ///
    /// // Logarithmic Y axis (common for exponential data)
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .yscale(AxisScale::Log)
    ///     .save("log_y.png")?;
    ///
    /// // Log-log plot
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .xscale(AxisScale::Log)
    ///     .yscale(AxisScale::Log)
    ///     .save("loglog.png")?;
    /// ```
    pub fn yscale(mut self, scale: AxisScale) -> Self {
        self.layout.y_scale = scale;
        self
    }

    /// Set canvas dimensions in pixels
    ///
    /// This method automatically scales DPI based on canvas size to maintain
    /// proportional text and element sizes on larger canvases.
    ///
    /// # Deprecation
    ///
    /// Prefer `size(width, height)` which takes dimensions in inches for
    /// DPI-independent sizing, or `size_pixels(width, height)` for pixel-based sizing.
    #[deprecated(since = "0.2.0", note = "Use size() or size_pixels() instead")]
    pub fn dimensions(mut self, width: u32, height: u32) -> Self {
        self.display.dimensions = (width.max(100), height.max(100));

        // Auto-scale DPI based on canvas size relative to reference (640x480)
        // This ensures text maintains proportional size on larger canvases
        let reference_diagonal = ((640.0_f32).powi(2) + (480.0_f32).powi(2)).sqrt();
        let canvas_diagonal = ((width as f32).powi(2) + (height as f32).powi(2)).sqrt();
        let scale_factor = (canvas_diagonal / reference_diagonal).max(1.0);
        let auto_dpi = (REFERENCE_DPI * scale_factor).round().max(100.0);

        self.display.dpi = auto_dpi as u32;
        self.display.config.figure.dpi = auto_dpi;
        self.display.config.figure.width = width as f32 / auto_dpi;
        self.display.config.figure.height = height as f32 / auto_dpi;
        self
    }

    /// Set figure size in inches
    ///
    /// This is the recommended way to set figure size for DPI-independent plots.
    /// The final pixel dimensions are calculated as `width × dpi` by `height × dpi`.
    ///
    /// # Size and DPI Relationship
    ///
    /// | Size (inches) | DPI | Pixel Dimensions |
    /// |---------------|-----|------------------|
    /// | 8×6           | 100 | 800×600          |
    /// | 8×6           | 150 | 1200×900         |
    /// | 8×6           | 300 | 2400×1800        |
    ///
    /// # Arguments
    ///
    /// * `width` - Figure width in inches (minimum 1.0)
    /// * `height` - Figure height in inches (minimum 1.0)
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// // Screen display (100 DPI default)
    /// Plot::new()
    ///     .size(8.0, 6.0)  // 800×600 pixels
    ///     .save("screen.png")?;
    ///
    /// // Print quality (300 DPI)
    /// Plot::new()
    ///     .size(8.0, 6.0)  // Same size in inches
    ///     .dpi(300)        // 2400×1800 pixels
    ///     .save("print.png")?;
    /// ```
    pub fn size(mut self, width: f32, height: f32) -> Self {
        self.display.config.figure.width = width.max(1.0);
        self.display.config.figure.height = height.max(1.0);
        // Update legacy fields for backward compatibility
        let (w, h) = self.display.config.canvas_size();
        self.display.dimensions = (w, h);
        self
    }

    /// Set figure size in pixels
    ///
    /// Convenience method for users who prefer to think in pixels.
    /// Internally converts to inches using reference DPI (100).
    ///
    /// # Arguments
    ///
    /// * `width` - Figure width in pixels
    /// * `height` - Figure height in pixels
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .size_px(800, 600)  // 800×600 pixels at 100 DPI
    ///     .save("figure.png")?;
    /// ```
    pub fn size_px(mut self, width: u32, height: u32) -> Self {
        use crate::core::units::REFERENCE_DPI;
        self.display.config.figure.width = width as f32 / REFERENCE_DPI;
        self.display.config.figure.height = height as f32 / REFERENCE_DPI;
        // Update legacy fields
        let (w, h) = self.display.config.canvas_size();
        self.display.dimensions = (w, h);
        self
    }

    /// Set DPI for export quality
    ///
    /// DPI only affects output resolution, not layout proportions.
    /// Higher DPI produces larger files with more detail.
    ///
    /// # Common values
    ///
    /// * 72-100: Screen/web display
    /// * 150: Good quality print
    /// * 300: Publication quality
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .size(6.4, 4.8)  // Same size in inches
    ///     .dpi(300)        // High resolution: 1920×1440 pixels
    ///     .save("print.png")?;
    /// ```
    pub fn dpi(mut self, dpi: u32) -> Self {
        self.display.config.figure.dpi = dpi.max(72) as f32;
        self.display.dpi = dpi.max(72);
        // Update dimensions to reflect new DPI
        let (w, h) = self.display.config.canvas_size();
        self.display.dimensions = (w, h);
        self
    }

    /// Set maximum output resolution while preserving figure aspect ratio
    ///
    /// This method provides intuitive pixel-based sizing for screen outputs
    /// (PNG, GIF, video) while maintaining consistent visual styling with
    /// matplotlib/seaborn conventions.
    ///
    /// # How it works
    ///
    /// The `max_width` and `max_height` define a bounding box. The output
    /// dimensions are calculated to fit within this box while preserving
    /// the figure's aspect ratio. The internal DPI is automatically set
    /// to achieve the target pixel size.
    ///
    /// # Examples
    ///
    /// ```rust,ignore
    /// // Fit within 1920×1080 (e.g., for HD video)
    /// // Default 4:3 figure → outputs 1440×1080 (height-constrained)
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .max_resolution(1920, 1080)
    ///     .save("plot.png")?;
    ///
    /// // Fit within 1920×1440 (exact fit for 4:3)
    /// // → outputs 1920×1440 at 300 DPI
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .max_resolution(1920, 1440)
    ///     .save("hires.png")?;
    /// ```
    ///
    /// # Comparison with `dpi()`
    ///
    /// - Use `max_resolution()` for screen outputs where you care about pixel dimensions
    /// - Use `dpi()` for print outputs where you care about physical size and resolution
    ///
    /// Both produce identical results when properly configured:
    /// - `.max_resolution(1920, 1440)` ≈ `.dpi(300)` for default 6.4×4.8 figure
    pub fn max_resolution(mut self, max_width: u32, max_height: u32) -> Self {
        let fig_width = self.display.config.figure.width;
        let fig_height = self.display.config.figure.height;
        let aspect = fig_width / fig_height;

        // Try fitting to max_width
        let by_width = (max_width, (max_width as f32 / aspect).round() as u32);

        // Try fitting to max_height
        let by_height = ((max_height as f32 * aspect).round() as u32, max_height);

        // Choose the one that fits within both constraints
        let (width, height) = if by_width.1 <= max_height {
            by_width // Width-constrained
        } else {
            by_height // Height-constrained
        };

        // Calculate DPI to achieve these dimensions with current figure size
        let dpi = width as f32 / fig_width;

        self.display.config.figure.dpi = dpi;
        self.display.dpi = dpi as u32;
        self.display.dimensions = (width, height);
        self
    }

    /// Apply a style preset
    ///
    /// Style presets configure typography, line widths, and spacing
    /// for specific use cases.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .plot_style(PlotStyle::Publication)
    ///     .line(&x, &y)
    ///     .save("paper.png")?;
    /// ```
    pub fn plot_style(mut self, style: PlotStyle) -> Self {
        self.display.config = style.config();
        let (w, h) = self.display.config.canvas_size();
        self.display.dimensions = (w, h);
        self.display.dpi = self.display.config.figure.dpi as u32;
        self
    }

    /// Set the full plot configuration
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let config = PlotConfig::builder()
    ///     .figure(10.0, 7.5)
    ///     .font_size(14.0)
    ///     .build();
    /// Plot::new().plot_config(config).line(&x, &y).save("plot.png")?;
    /// ```
    pub fn plot_config(mut self, config: PlotConfig) -> Self {
        let (w, h) = config.canvas_size();
        self.display.dimensions = (w, h);
        self.display.dpi = config.figure.dpi as u32;
        self.display.config = config;
        self
    }

    /// Set the base font size in points
    ///
    /// All other font sizes (title, labels, ticks) scale relative to this.
    pub fn font_size(mut self, size: f32) -> Self {
        self.display.config.typography.base_size = size.max(4.0);
        self
    }

    /// Set the title font size in points (absolute)
    pub fn title_size(mut self, size: f32) -> Self {
        // Convert to scale factor
        self.display.config.typography.title_scale =
            size / self.display.config.typography.base_size;
        self
    }

    /// Set the data line width in points
    pub fn line_width_pt(mut self, width: f32) -> Self {
        self.display.config.lines.data_width = width.max(0.1);
        self
    }

    /// Get the current PlotConfig
    pub fn get_config(&self) -> &PlotConfig {
        &self.display.config
    }

    /// Adjust margins to tightly fit text with custom padding
    ///
    /// # Arguments
    ///
    /// * `pad` - Extra padding in points around text elements
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .tight_layout_pad(4.0)  // 4pt extra padding
    ///     .save("plot.png")?;
    /// ```
    pub fn tight_layout_pad(mut self, pad: f32) -> Self {
        let width = self.display.config.figure.width;
        let height = self.display.config.figure.height;

        // Estimate text sizes in inches
        let pt_to_in = |pt: f32| pt / 72.0;
        let pad_in = pt_to_in(pad);

        // Calculate required top margin (title)
        let top_margin = if self.display.title.is_some() {
            let title_size = self.display.config.typography.title_size();
            let title_pad = self.display.config.spacing.title_pad;
            pt_to_in(title_size) + pt_to_in(title_pad) + pad_in
        } else {
            pad_in.max(0.1) // Minimal margin
        };

        // Calculate required bottom margin (xlabel + tick labels)
        let tick_size = self.display.config.typography.tick_size();
        let label_size = self.display.config.typography.label_size();
        let tick_pad = self.display.config.spacing.tick_pad;
        let label_pad = self.display.config.spacing.label_pad;

        let bottom_margin = if self.display.xlabel.is_some() {
            pt_to_in(tick_size)
                + pt_to_in(tick_pad)
                + pt_to_in(label_size)
                + pt_to_in(label_pad)
                + pad_in
        } else {
            pt_to_in(tick_size) + pt_to_in(tick_pad) + pad_in
        };

        // Calculate required left margin (ylabel + tick labels)
        // Y-axis tick labels are typically 4-5 characters wide
        let estimated_tick_width = pt_to_in(tick_size) * 4.0;

        let left_margin = if self.display.ylabel.is_some() {
            estimated_tick_width
                + pt_to_in(tick_pad)
                + pt_to_in(label_size)
                + pt_to_in(label_pad)
                + pad_in
        } else {
            estimated_tick_width + pt_to_in(tick_pad) + pad_in
        };

        // Right margin is minimal (just padding)
        let right_margin = pad_in.max(0.1);

        // Ensure margins don't exceed half the figure size
        let max_horizontal = width * 0.4;
        let max_vertical = height * 0.4;

        self.display.config.margins = MarginConfig::Fixed {
            left: left_margin.min(max_horizontal),
            right: right_margin.min(max_horizontal),
            top: top_margin.min(max_vertical),
            bottom: bottom_margin.min(max_vertical),
        };

        self
    }

    /// Calculate canvas dimensions from config
    fn config_canvas_size(&self) -> (u32, u32) {
        self.display.config.canvas_size()
    }

    /// Get font size in pixels for rendering
    fn font_size_px(&self, points: f32) -> f32 {
        pt_to_px(points, self.display.config.figure.dpi)
    }

    /// Get line width in pixels for rendering
    fn line_width_px(&self, points: f32) -> f32 {
        pt_to_px(points, self.display.config.figure.dpi)
    }

    /// Calculate DPI-scaled canvas dimensions
    /// **Deprecated**: Use config_canvas_size() instead
    fn dpi_scaled_dimensions(&self) -> (u32, u32) {
        self.config_canvas_size()
    }

    /// Calculate DPI scaling factor
    /// **Deprecated**: Use config.figure.dpi with pt_to_px/in_to_px instead
    fn dpi_scale(&self) -> f32 {
        self.display.config.figure.dpi / 72.0 // Scale relative to 72 DPI (1pt = 1px)
    }

    /// Calculate DPI-scaled font size
    /// **Deprecated**: Use font_size_px() with config.typography instead
    pub fn dpi_scaled_font_size(&self, base_size: f32) -> f32 {
        pt_to_px(base_size, self.display.config.figure.dpi)
    }

    /// Calculate DPI-scaled line width
    /// **Deprecated**: Use line_width_px() with config.lines instead
    pub fn dpi_scaled_line_width(&self, base_width: f32) -> f32 {
        pt_to_px(base_width, self.display.config.figure.dpi)
    }

    /// Set margin around plot area
    ///
    /// The margin is specified as a fraction of the canvas size (0.0 to 0.5).
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .margin(0.15)  // 15% margin on all sides
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .end_series()
    ///     .save("margin.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn margin(mut self, margin: f32) -> Self {
        self.layout.margin = Some(margin.clamp(0.0, 0.5));
        self
    }

    /// Enable/disable scientific notation on axes
    pub fn scientific_notation(mut self, enabled: bool) -> Self {
        self.layout.scientific_notation = enabled;
        self
    }

    /// Add a scoped group of series that share style defaults.
    ///
    /// Styles configured on the group builder apply to every member series
    /// added inside the closure and do not leak outside the group.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x = vec![0.0, 1.0, 2.0, 3.0];
    /// let y1 = vec![0.0, 1.0, 2.0, 3.0];
    /// let y2 = vec![0.0, 1.5, 3.0, 4.5];
    ///
    /// Plot::new()
    ///     .group(|g| {
    ///         g.group_label("Sensors")
    ///             .line_width(2.0)
    ///             .line_style(LineStyle::Dashed)
    ///             .line(&x, &y1)
    ///             .line(&x, &y2)
    ///     })
    ///     .legend_best()
    ///     .save("grouped.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn group<F>(self, f: F) -> Self
    where
        F: FnOnce(SeriesGroupBuilder) -> SeriesGroupBuilder,
    {
        f(SeriesGroupBuilder::new(self)).finalize()
    }

    /// Add a line plot series
    ///
    /// Creates a line chart connecting data points in order.
    /// Returns a `PlotBuilder<LineConfig>` for method chaining with line-specific options.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
    /// let y: Vec<f64> = x.iter().map(|&v| v.sin()).collect();
    ///
    /// // Simple usage - just call save() directly
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .save("line.png")?;
    ///
    /// // With configuration
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .line_width(2.0)
    ///     .color(Color::BLUE)
    ///     .marker(MarkerStyle::Circle)
    ///     .title("Sine Wave")
    ///     .save("line_styled.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// ![Line plot example](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/line_plot.png)
    pub fn line<X, Y>(self, x_data: &X, y_data: &Y) -> PlotBuilder<crate::plots::basic::LineConfig>
    where
        X: NumericData1D,
        Y: NumericData1D,
    {
        let mut plot = self;
        let x_vec = match collect_numeric_data_1d(x_data, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let y_vec = match collect_numeric_data_1d(y_data, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };

        PlotBuilder::new(
            plot,
            PlotInput::XY(x_vec, y_vec),
            crate::plots::basic::LineConfig::default(),
        )
    }

    /// Add a line plot series from streaming data
    ///
    /// This method reads the current data from the StreamingXY buffer at render time.
    /// The buffer can continue to receive updates, and subsequent renders will
    /// include the new data.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    /// use ruviz::data::StreamingXY;
    ///
    /// let stream = StreamingXY::new(1000);
    ///
    /// // Push data (can be from another thread)
    /// stream.push(0.0, 0.0);
    /// stream.push(1.0, 1.0);
    /// stream.push(2.0, 4.0);
    ///
    /// // Render current state
    /// Plot::new()
    ///     .line_streaming(&stream)
    ///     .title("Streaming Data")
    ///     .save("stream.png")?;
    ///
    /// // More data arrives
    /// stream.push(3.0, 9.0);
    ///
    /// // Re-render with new data
    /// Plot::new()
    ///     .line_streaming(&stream)
    ///     .save("stream_updated.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn line_streaming(self, stream: &StreamingXY) -> PlotSeriesBuilder {
        // Read current data from the streaming buffer
        let x_data = PlotData::Static(stream.read_x());
        let y_data = PlotData::Static(stream.read_y());

        let series = PlotSeries {
            series_type: SeriesType::Line { x_data, y_data },
            label: None,
            color: None,
            line_width: None,
            line_style: None,
            marker_style: None,
            marker_size: None,
            alpha: None,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        // Mark as rendered so partial updates can be tracked
        stream.mark_rendered();

        PlotSeriesBuilder::new(self, series)
    }

    /// Add a scatter plot series
    ///
    /// Creates a scatter plot showing individual data points as markers.
    /// Returns a `PlotBuilder<ScatterConfig>` for method chaining with scatter-specific options.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x: Vec<f64> = (0..50).map(|i| i as f64 * 0.2).collect();
    /// let y: Vec<f64> = x.iter().map(|&v| v.sin()).collect();
    ///
    /// // Simple usage - just call save() directly
    /// Plot::new()
    ///     .scatter(&x, &y)
    ///     .save("scatter.png")?;
    ///
    /// // With configuration
    /// Plot::new()
    ///     .scatter(&x, &y)
    ///     .marker(MarkerStyle::Triangle)
    ///     .marker_size(10.0)
    ///     .color(Color::RED)
    ///     .title("Data Points")
    ///     .save("scatter_styled.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// ![Scatter plot example](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/scatter_plot.png)
    pub fn scatter<X, Y>(
        self,
        x_data: &X,
        y_data: &Y,
    ) -> PlotBuilder<crate::plots::basic::ScatterConfig>
    where
        X: NumericData1D,
        Y: NumericData1D,
    {
        let mut plot = self;
        let x_vec = match collect_numeric_data_1d(x_data, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let y_vec = match collect_numeric_data_1d(y_data, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };

        PlotBuilder::new(
            plot,
            PlotInput::XY(x_vec, y_vec),
            crate::plots::basic::ScatterConfig::default(),
        )
    }

    /// Add a scatter plot series from streaming data
    ///
    /// Similar to `line_streaming`, reads current data from the buffer at render time.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    /// use ruviz::data::StreamingXY;
    ///
    /// let stream = StreamingXY::new(1000);
    /// stream.push_many(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)]);
    ///
    /// Plot::new()
    ///     .scatter_streaming(&stream)
    ///     .title("Streaming Scatter")
    ///     .save("stream_scatter.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn scatter_streaming(self, stream: &StreamingXY) -> PlotSeriesBuilder {
        let x_data = PlotData::Static(stream.read_x());
        let y_data = PlotData::Static(stream.read_y());

        let series = PlotSeries {
            series_type: SeriesType::Scatter { x_data, y_data },
            label: None,
            color: None,
            line_width: None,
            line_style: None,
            marker_style: Some(MarkerStyle::Circle),
            marker_size: None,
            alpha: None,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        stream.mark_rendered();

        PlotSeriesBuilder::new(self, series)
    }

    /// Add a bar plot series
    ///
    /// Creates a bar chart with categorical x-axis labels.
    ///
    /// # Example
    ///
    /// Returns a `PlotBuilder<BarConfig>` for method chaining with bar-specific options.
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let categories = vec!["A", "B", "C", "D", "E"];
    /// let values = vec![23.0, 45.0, 56.0, 78.0, 32.0];
    ///
    /// // Simple usage - just call save() directly
    /// Plot::new()
    ///     .bar(&categories, &values)
    ///     .save("bar.png")?;
    ///
    /// // With configuration
    /// Plot::new()
    ///     .bar(&categories, &values)
    ///     .bar_width(0.6)
    ///     .color(Color::GREEN)
    ///     .edge_width(1.5)
    ///     .title("Category Values")
    ///     .save("bar_styled.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// ![Bar chart example](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/bar_chart.png)
    pub fn bar<S, V>(
        self,
        categories: &[S],
        values: &V,
    ) -> PlotBuilder<crate::plots::basic::BarConfig>
    where
        S: ToString,
        V: NumericData1D,
    {
        let mut plot = self;
        let cat_vec: Vec<String> = categories.iter().map(|s| s.to_string()).collect();
        let val_vec = match collect_numeric_data_1d(values, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };

        PlotBuilder::new(
            plot,
            PlotInput::Categorical {
                categories: cat_vec,
                values: val_vec,
            },
            crate::plots::basic::BarConfig::default(),
        )
    }

    /// Add a histogram plot series
    ///
    /// Creates a histogram showing the distribution of data values.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let data: Vec<f64> = (0..1000).map(|i| (i as f64 / 100.0).sin()).collect();
    ///
    /// Plot::new()
    ///     .histogram(&data, None)
    ///     .end_series()
    ///     .save("histogram.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// ![Histogram example](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/histogram.png)
    pub fn histogram<D: NumericData1D>(
        self,
        data: &D,
        config: Option<HistogramConfig>,
    ) -> PlotSeriesBuilder {
        let mut plot = self;
        let data_vec = match collect_numeric_data_1d(data, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let hist_config = config.unwrap_or_default();

        let series = PlotSeries {
            series_type: SeriesType::Histogram {
                data: PlotData::Static(data_vec),
                config: hist_config,
            },
            label: None,
            color: None,
            line_width: None,
            line_style: None,
            marker_style: None,
            marker_size: None,
            alpha: None,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        PlotSeriesBuilder::new(plot, series)
    }

    /// Add a box plot series
    ///
    /// Creates a box plot showing the distribution of data with quartiles,
    /// median, and outliers.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    /// use ruviz::plots::boxplot::BoxPlotConfig;
    ///
    /// let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0,
    ///                 11.0, 12.0, 35.0, 40.0, -5.0]; // includes outliers
    ///
    /// Plot::new()
    ///     .boxplot(&data, Some(BoxPlotConfig::new()))
    ///     .end_series()
    ///     .save("boxplot.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// ![Box plot example](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/boxplot.png)
    pub fn boxplot<D: NumericData1D>(
        self,
        data: &D,
        config: Option<BoxPlotConfig>,
    ) -> PlotSeriesBuilder {
        let mut plot = self;
        let data_vec = match collect_numeric_data_1d(data, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let box_config = config.unwrap_or_default();

        let series = PlotSeries {
            series_type: SeriesType::BoxPlot {
                data: PlotData::Static(data_vec),
                config: box_config,
            },
            label: None,
            color: None,
            line_width: None,
            line_style: None,
            marker_style: None,
            marker_size: None,
            alpha: None,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        PlotSeriesBuilder::new(plot, series)
    }

    /// Add a heatmap visualization for 2D array data
    ///
    /// Creates a color-mapped visualization of 2D data.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let data: Vec<Vec<f64>> = (0..10)
    ///     .map(|i| (0..10).map(|j| (i + j) as f64).collect())
    ///     .collect();
    ///
    /// Plot::new()
    ///     .heatmap(&data, None)
    ///     .end_series()
    ///     .save("heatmap.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// ![Heatmap example](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/heatmap.png)
    pub fn heatmap<D>(
        mut self,
        data: &D,
        config: Option<crate::plots::heatmap::HeatmapConfig>,
    ) -> PlotSeriesBuilder
    where
        D: NumericData2D + ?Sized,
    {
        let heatmap_config = config.unwrap_or_default();

        // Disable grid for heatmaps (grid doesn't make sense behind heatmap cells)
        self.layout.grid_style.visible = false;

        // Process heatmap data
        let (flat, rows, cols) = match collect_numeric_data_2d(data) {
            Ok(values) => values,
            Err(err) => {
                self.set_pending_ingestion_error(err);
                (vec![], 0, 0)
            }
        };
        match crate::plots::heatmap::process_heatmap_flat(&flat, rows, cols, heatmap_config) {
            Ok(heatmap_data) => {
                let series = PlotSeries {
                    series_type: SeriesType::Heatmap { data: heatmap_data },
                    label: None,
                    color: None,
                    line_width: None,
                    line_style: None,
                    marker_style: None,
                    marker_size: None,
                    alpha: None,
                    y_errors: None,
                    x_errors: None,
                    error_config: None,
                    group_id: None,
                };
                PlotSeriesBuilder::new(self, series)
            }
            Err(err) => {
                // Return empty plot if data processing fails
                // This allows chaining to continue without panicking
                self.set_pending_ingestion_error(PlottingError::DataExtractionFailed {
                    source: "heatmap".to_string(),
                    message: err,
                });
                let series = PlotSeries {
                    series_type: SeriesType::Heatmap {
                        data: crate::plots::heatmap::HeatmapData {
                            values: vec![vec![0.0]],
                            n_rows: 1,
                            n_cols: 1,
                            data_min: 0.0,
                            data_max: 0.0,
                            vmin: 0.0,
                            vmax: 1.0,
                            config: crate::plots::heatmap::HeatmapConfig::default(),
                        },
                    },
                    label: None,
                    color: None,
                    line_width: None,
                    line_style: None,
                    marker_style: None,
                    marker_size: None,
                    alpha: None,
                    y_errors: None,
                    x_errors: None,
                    error_config: None,
                    group_id: None,
                };
                PlotSeriesBuilder::new(self, series)
            }
        }
    }

    /// Add error bars (Y-direction only)
    pub fn error_bars<X, Y, E>(self, x_data: &X, y_data: &Y, y_errors: &E) -> PlotSeriesBuilder
    where
        X: NumericData1D,
        Y: NumericData1D,
        E: NumericData1D,
    {
        let mut plot = self;
        let x_vec = match collect_numeric_data_1d(x_data, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let y_vec = match collect_numeric_data_1d(y_data, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let e_vec = match collect_numeric_data_1d(y_errors, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };

        let series = PlotSeries {
            series_type: SeriesType::ErrorBars {
                x_data: PlotData::Static(x_vec),
                y_data: PlotData::Static(y_vec),
                y_errors: PlotData::Static(e_vec),
            },
            label: None,
            color: None,
            line_width: None,
            line_style: None,
            marker_style: None,
            marker_size: None,
            alpha: None,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        PlotSeriesBuilder::new(plot, series)
    }

    /// Add error bars in both X and Y directions
    pub fn error_bars_xy<X, Y, EX, EY>(
        self,
        x_data: &X,
        y_data: &Y,
        x_errors: &EX,
        y_errors: &EY,
    ) -> PlotSeriesBuilder
    where
        X: NumericData1D,
        Y: NumericData1D,
        EX: NumericData1D,
        EY: NumericData1D,
    {
        let mut plot = self;
        let x_vec = match collect_numeric_data_1d(x_data, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let y_vec = match collect_numeric_data_1d(y_data, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let ex_vec = match collect_numeric_data_1d(x_errors, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let ey_vec = match collect_numeric_data_1d(y_errors, plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                plot.set_pending_ingestion_error(err);
                vec![]
            }
        };

        let series = PlotSeries {
            series_type: SeriesType::ErrorBarsXY {
                x_data: PlotData::Static(x_vec),
                y_data: PlotData::Static(y_vec),
                x_errors: PlotData::Static(ex_vec),
                y_errors: PlotData::Static(ey_vec),
            },
            label: None,
            color: None,
            line_width: None,
            line_style: None,
            marker_style: None,
            marker_size: None,
            alpha: None,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        PlotSeriesBuilder::new(plot, series)
    }

    /// Add a KDE (Kernel Density Estimation) plot
    ///
    /// Creates a smooth density estimate visualization of the data distribution.
    /// Returns a `PlotBuilder<KdeConfig>` for method chaining with KDE-specific options.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let data: Vec<f64> = (0..1000).map(|i| (i as f64 / 100.0).sin()).collect();
    ///
    /// // Simple usage - just save directly
    /// Plot::new()
    ///     .kde(&data)
    ///     .save("kde.png")?;
    ///
    /// // With configuration
    /// Plot::new()
    ///     .kde(&data)
    ///     .bandwidth(0.5)
    ///     .fill(true)
    ///     .fill_alpha(0.3)
    ///     .title("KDE Distribution")
    ///     .save("kde_configured.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn kde<T, D: Data1D<T>>(self, data: &D) -> PlotBuilder<crate::plots::KdeConfig>
    where
        T: Into<f64> + Copy,
    {
        let mut data_vec = Vec::with_capacity(data.len());
        for i in 0..data.len() {
            if let Some(val) = data.get(i) {
                data_vec.push((*val).into());
            }
        }

        PlotBuilder::new(
            self,
            PlotInput::Single(data_vec),
            crate::plots::KdeConfig::default(),
        )
    }

    /// Start building an ECDF (Empirical Cumulative Distribution Function) plot
    ///
    /// Returns a `PlotBuilder<EcdfConfig>` for configuring the ECDF plot.
    ///
    /// # Example
    ///
    /// ```ignore
    /// Plot::new()
    ///     .ecdf(&data)
    ///     .stat(EcdfStat::Proportion)
    ///     .show_ci(true)
    ///     .save("ecdf.png")?;
    /// ```
    pub fn ecdf<T, D: Data1D<T>>(self, data: &D) -> PlotBuilder<crate::plots::EcdfConfig>
    where
        T: Into<f64> + Copy,
    {
        let mut data_vec = Vec::with_capacity(data.len());
        for i in 0..data.len() {
            if let Some(val) = data.get(i) {
                data_vec.push((*val).into());
            }
        }

        PlotBuilder::new(
            self,
            PlotInput::Single(data_vec),
            crate::plots::EcdfConfig::default(),
        )
    }

    /// Add a contour plot for 2D scalar field visualization
    ///
    /// Creates contour lines or filled contours from grid data.
    /// Returns a `PlotBuilder<ContourConfig>` for method chaining.
    ///
    /// # Arguments
    ///
    /// * `x` - X coordinates of the grid (1D array)
    /// * `y` - Y coordinates of the grid (1D array)
    /// * `z` - Z values as a flattened 2D array (row-major, len = x.len() * y.len())
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x: Vec<f64> = (-50..=50).map(|i| i as f64 / 10.0).collect();
    /// let y: Vec<f64> = (-50..=50).map(|i| i as f64 / 10.0).collect();
    /// let z: Vec<f64> = y.iter().flat_map(|yv| {
    ///     x.iter().map(move |xv| (-xv*xv - yv*yv).exp())
    /// }).collect();
    ///
    /// Plot::new()
    ///     .title("Gaussian Surface")
    ///     .contour(&x, &y, &z)
    ///     .levels(10)
    ///     .filled(true)
    ///     .save("contour.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn contour<X, Y, Z>(self, x: &X, y: &Y, z: &Z) -> PlotBuilder<crate::plots::ContourConfig>
    where
        X: Data1D<f64>,
        Y: Data1D<f64>,
        Z: Data1D<f64>,
    {
        let x_vec: Vec<f64> = (0..x.len()).filter_map(|i| x.get(i).copied()).collect();
        let y_vec: Vec<f64> = (0..y.len()).filter_map(|i| y.get(i).copied()).collect();
        let z_vec: Vec<f64> = (0..z.len()).filter_map(|i| z.get(i).copied()).collect();

        // Convert flat z to 2D grid (row-major)
        let ny = y_vec.len();
        let nx = x_vec.len();
        let z_2d: Vec<Vec<f64>> = (0..ny)
            .map(|j| {
                (0..nx)
                    .map(|i| z_vec.get(j * nx + i).copied().unwrap_or(0.0))
                    .collect()
            })
            .collect();

        PlotBuilder::new(
            self,
            PlotInput::Grid2D {
                x: x_vec,
                y: y_vec,
                z: z_2d,
            },
            crate::plots::ContourConfig::default(),
        )
    }

    /// Add a pie chart for proportional data visualization
    ///
    /// Creates a pie chart with optional labels, exploded segments, and donut style.
    /// Returns a `PlotBuilder<PieConfig>` for method chaining.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let values = vec![35.0, 25.0, 20.0, 15.0, 5.0];
    ///
    /// Plot::new()
    ///     .title("Market Share")
    ///     .pie(&values)
    ///     .labels(&["A", "B", "C", "D", "Other"])
    ///     .donut(0.4)
    ///     .save("pie.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn pie<V>(self, values: &V) -> PlotBuilder<crate::plots::PieConfig>
    where
        V: Data1D<f64>,
    {
        let values_vec: Vec<f64> = (0..values.len())
            .filter_map(|i| values.get(i).copied())
            .collect();

        PlotBuilder::new(
            self,
            PlotInput::Single(values_vec),
            crate::plots::PieConfig::default(),
        )
    }

    /// Add a radar/spider chart for multivariate data comparison
    ///
    /// Creates a radar chart with multiple axes arranged in a circle.
    /// Returns a `PlotBuilder<RadarConfig>` for method chaining.
    ///
    /// # Arguments
    ///
    /// * `labels` - Labels for each axis spoke
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .title("Player Stats")
    ///     .radar(&["Speed", "Power", "Defense", "Magic", "Luck"])
    ///     .series(&[85.0, 92.0, 78.0, 65.0, 88.0])
    ///     .label("Player 1")
    ///     .fill_alpha(0.3)
    ///     .series(&[72.0, 68.0, 95.0, 82.0, 75.0])
    ///     .label("Player 2")
    ///     .fill_alpha(0.3)
    ///     .save("radar.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn radar<S: AsRef<str>>(self, labels: &[S]) -> PlotBuilder<crate::plots::RadarConfig> {
        let label_strings: Vec<String> = labels.iter().map(|s| s.as_ref().to_string()).collect();

        let config = crate::plots::RadarConfig::default().labels(label_strings);

        PlotBuilder::new(self, PlotInput::Single(vec![]), config)
    }

    /// Add a Violin plot for visualizing distribution shapes
    ///
    /// Creates a violin plot combining KDE density estimation with optional
    /// box/strip components for statistical visualization.
    /// Returns a `PlotBuilder<ViolinConfig>` for method chaining with violin-specific options.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let data: Vec<f64> = (0..200).map(|i| {
    ///     let x = (i as f64 * 0.1).sin() * 3.0 + 5.0;
    ///     x
    /// }).collect();
    ///
    /// // Simple usage
    /// Plot::new()
    ///     .violin(&data)
    ///     .save("violin.png")?;
    ///
    /// // With configuration
    /// Plot::new()
    ///     .violin(&data)
    ///     .show_box(true)
    ///     .show_median(true)
    ///     .fill_alpha(0.6)
    ///     .title("Distribution")
    ///     .save("violin_configured.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn violin<T, D: Data1D<T>>(self, data: &D) -> PlotBuilder<crate::plots::ViolinConfig>
    where
        T: Into<f64> + Copy,
    {
        let mut data_vec = Vec::with_capacity(data.len());
        for i in 0..data.len() {
            if let Some(val) = data.get(i) {
                data_vec.push((*val).into());
            }
        }

        PlotBuilder::new(
            self,
            PlotInput::Single(data_vec),
            crate::plots::ViolinConfig::default(),
        )
    }

    /// Add a Polar line plot for visualizing data in polar coordinates
    ///
    /// Creates a polar plot with r (radius) and theta (angle in radians) data.
    /// Returns a `PlotBuilder<PolarPlotConfig>` for method chaining with polar-specific options.
    ///
    /// # Arguments
    ///
    /// * `r` - Radius values (distance from center)
    /// * `theta` - Angle values in radians
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    /// use std::f64::consts::PI;
    ///
    /// // Rose curve
    /// let n_points = 200;
    /// let theta: Vec<f64> = (0..n_points)
    ///     .map(|i| i as f64 * 2.0 * PI / n_points as f64)
    ///     .collect();
    /// let r: Vec<f64> = theta.iter().map(|&t| (3.0 * t).cos().abs()).collect();
    ///
    /// Plot::new()
    ///     .title("Rose Curve")
    ///     .polar_line(&r, &theta)
    ///     .fill(true)
    ///     .fill_alpha(0.3)
    ///     .save("polar.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn polar_line<R, T>(self, r: &R, theta: &T) -> PlotBuilder<crate::plots::PolarPlotConfig>
    where
        R: Data1D<f64>,
        T: Data1D<f64>,
    {
        let r_vec: Vec<f64> = (0..r.len()).filter_map(|i| r.get(i).copied()).collect();
        let theta_vec: Vec<f64> = (0..theta.len())
            .filter_map(|i| theta.get(i).copied())
            .collect();

        PlotBuilder::new(
            self,
            PlotInput::XY(r_vec, theta_vec),
            crate::plots::PolarPlotConfig::default(),
        )
    }

    /// Configure legend with position
    ///
    /// For matplotlib-compatible position codes, use `legend_position()` with `LegendPosition`.
    /// For automatic positioning (like matplotlib's `plt.legend()`), use `legend_best()`.
    pub fn legend(mut self, position: Position) -> Self {
        self.layout.legend.enabled = true;
        self.layout.legend.position = position;
        self
    }

    /// Configure legend with new position system
    ///
    /// Uses the matplotlib-compatible position codes including:
    /// - `LegendPosition::Best` - automatic position to minimize data overlap
    /// - `LegendPosition::UpperRight`, `UpperLeft`, etc. - standard positions
    /// - `LegendPosition::OutsideRight`, etc. - outside plot positions
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
    /// let sin_y: Vec<f64> = x.iter().map(|&v| v.sin()).collect();
    /// let cos_y: Vec<f64> = x.iter().map(|&v| v.cos()).collect();
    ///
    /// Plot::new()
    ///     .legend_position(LegendPosition::Best)
    ///     .line(&x, &sin_y).label("sin(x)")
    ///     .line(&x, &cos_y).label("cos(x)")
    ///     .end_series()
    ///     .save("legend.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// ![Legend example](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/images/legend.png)
    pub fn legend_position(mut self, position: LegendPosition) -> Self {
        self.layout.legend.enabled = true;
        // Convert LegendPosition to old Position for backward compatibility
        self.layout.legend.position = match position {
            LegendPosition::UpperRight | LegendPosition::Right => Position::TopRight,
            LegendPosition::UpperLeft => Position::TopLeft,
            LegendPosition::LowerLeft => Position::BottomLeft,
            LegendPosition::LowerRight => Position::BottomRight,
            LegendPosition::CenterLeft => Position::CenterLeft,
            LegendPosition::CenterRight => Position::CenterRight,
            LegendPosition::LowerCenter => Position::BottomCenter,
            LegendPosition::UpperCenter => Position::TopCenter,
            LegendPosition::Center => Position::Center,
            LegendPosition::Best => Position::TopRight, // Default, actual best calculated at render time
            LegendPosition::OutsideRight
            | LegendPosition::OutsideLeft
            | LegendPosition::OutsideUpper
            | LegendPosition::OutsideLower => Position::TopRight,
            LegendPosition::Custom { x, y, .. } => Position::Custom { x, y },
        };
        self
    }

    /// Enable legend with "best" automatic positioning
    ///
    /// This is equivalent to matplotlib's `plt.legend()`. The legend will be placed
    /// in the position that minimizes overlap with data points.
    ///
    /// Note: Like matplotlib, legends are NOT shown unless you explicitly call
    /// `legend_best()` or `legend_position()`, even if series have labels.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
    /// let sin_y: Vec<f64> = x.iter().map(|&v| v.sin()).collect();
    /// let cos_y: Vec<f64> = x.iter().map(|&v| v.cos()).collect();
    ///
    /// // Labels define legend entries, legend_best() enables the legend
    /// Plot::new()
    ///     .line(&x, &sin_y).label("sin(x)")
    ///     .line(&x, &cos_y).label("cos(x)")
    ///     .end_series()       // Finish series chain
    ///     .legend_best()      // Like plt.legend() in matplotlib
    ///     .save("legend.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn legend_best(mut self) -> Self {
        self.layout.legend.enabled = true;
        self.layout.legend.position = Position::TopRight; // Actual best computed at render time
        self
    }

    /// Set legend font size
    pub fn legend_font_size(mut self, size: f32) -> Self {
        self.layout.legend.font_size = Some(size);
        self
    }

    /// Set legend corner radius for rounded corners
    ///
    /// A value of 0.0 gives sharp corners (default).
    /// Typical values are 3.0 to 8.0 for subtle rounded corners.
    pub fn legend_corner_radius(mut self, radius: f32) -> Self {
        self.layout.legend.corner_radius = Some(radius);
        self
    }

    /// Set number of legend columns
    ///
    /// - 1 column (default): vertical layout with all items stacked
    /// - 2+ columns: horizontal/multi-column layout
    ///
    /// For a single-row layout with N items, use `legend_columns(N)`.
    pub fn legend_columns(mut self, columns: usize) -> Self {
        self.layout.legend.columns = Some(columns.max(1));
        self
    }

    /// Enable/disable grid
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// // Disable grid
    /// Plot::new()
    ///     .grid(false)
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .end_series()
    ///     .save("no_grid.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn grid(mut self, enabled: bool) -> Self {
        self.layout.grid_style.visible = enabled;
        self
    }

    /// Configure grid styling with unified GridStyle
    ///
    /// This is the preferred way to configure grid appearance, using
    /// the unified `GridStyle` configuration that ensures consistency
    /// across all plot types.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use ruviz::core::GridStyle;
    /// use ruviz::render::Color;
    ///
    /// Plot::new()
    ///     .with_grid_style(GridStyle::default().color(Color::BLUE).alpha(0.5))
    ///     .line(&x, &y)
    ///     .save("styled_grid.png")?;
    /// ```
    pub fn with_grid_style(mut self, style: GridStyle) -> Self {
        self.layout.grid_style = style;
        self
    }

    /// Set tick direction to inside (default)
    pub fn tick_direction_inside(mut self) -> Self {
        self.layout.tick_config.direction = TickDirection::Inside;
        self
    }

    /// Set tick direction to outside
    pub fn tick_direction_outside(mut self) -> Self {
        self.layout.tick_config.direction = TickDirection::Outside;
        self
    }

    /// Set number of major ticks for both axes
    pub fn major_ticks(mut self, count: usize) -> Self {
        self.layout.tick_config.major_ticks_x = count;
        self.layout.tick_config.major_ticks_y = count;
        self
    }

    /// Set number of minor ticks between major ticks for both axes
    pub fn minor_ticks(mut self, count: usize) -> Self {
        self.layout.tick_config.minor_ticks_x = count;
        self.layout.tick_config.minor_ticks_y = count;
        self
    }

    /// Set number of major ticks for X axis
    pub fn major_ticks_x(mut self, count: usize) -> Self {
        self.layout.tick_config.major_ticks_x = count;
        self
    }

    /// Set number of minor ticks between major ticks for X axis
    pub fn minor_ticks_x(mut self, count: usize) -> Self {
        self.layout.tick_config.minor_ticks_x = count;
        self
    }

    /// Set number of major ticks for Y axis
    pub fn major_ticks_y(mut self, count: usize) -> Self {
        self.layout.tick_config.major_ticks_y = count;
        self
    }

    /// Set number of minor ticks between major ticks for Y axis
    pub fn minor_ticks_y(mut self, count: usize) -> Self {
        self.layout.tick_config.minor_ticks_y = count;
        self
    }

    /// Grid lines only at major ticks
    pub fn grid_major_only(mut self) -> Self {
        self.layout.tick_config.grid_mode = GridMode::MajorOnly;
        self
    }

    /// Grid lines only at minor ticks
    pub fn grid_minor_only(mut self) -> Self {
        self.layout.tick_config.grid_mode = GridMode::MinorOnly;
        self
    }

    /// Grid lines at both major and minor ticks
    pub fn grid_both(mut self) -> Self {
        self.layout.tick_config.grid_mode = GridMode::Both;
        self
    }

    /// Enable tight layout (automatic margin adjustment like matplotlib)
    ///
    /// When enabled, computes minimum required margins based on:
    /// - Title dimensions (if present)
    /// - X-axis label and tick label dimensions
    /// - Y-axis label and tick label dimensions
    ///
    /// The result is `Fixed` margins that eliminate dead space while
    /// ensuring no text is clipped.
    ///
    /// # Arguments
    ///
    /// * `enabled` - If true, compute tight margins; if false, use default proportional margins
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .title("My Plot")
    ///     .xlabel("X Values")
    ///     .ylabel("Y Values")
    ///     .line(&x, &y)
    ///     .tight_layout(true)  // Compute optimal margins
    ///     .save("tight.png")?;
    /// ```
    pub fn tight_layout(self, enabled: bool) -> Self {
        if enabled {
            self.tight_layout_pad(2.0) // Default 2pt padding
        } else {
            // Reset to default proportional margins
            let mut s = self;
            s.display.config.margins = MarginConfig::default();
            s
        }
    }

    /// Set grid color
    ///
    /// # Deprecated
    ///
    /// Use `with_grid_style(GridStyle::default().color(color))` instead for
    /// unified grid configuration.
    #[deprecated(since = "0.2.0", note = "Use with_grid_style() instead")]
    pub fn grid_color(mut self, color: Color) -> Self {
        self.layout.grid_style.color = color;
        self
    }

    /// Set grid line style
    ///
    /// # Deprecated
    ///
    /// Use `with_grid_style(GridStyle::default().line_style(style))` instead for
    /// unified grid configuration.
    #[deprecated(since = "0.2.0", note = "Use with_grid_style() instead")]
    pub fn grid_line_style(mut self, style: LineStyle) -> Self {
        self.layout.grid_style.line_style = style;
        self
    }

    /// Set grid line width
    pub fn grid_line_width(mut self, width: f32) -> Self {
        self.layout.grid_style.line_width = width;
        self
    }

    /// Set grid transparency
    pub fn grid_alpha(mut self, alpha: f32) -> Self {
        self.layout.grid_style.alpha = alpha;
        self
    }

    // ========== Annotation Methods ==========

    /// Add a text annotation at data coordinates
    ///
    /// # Arguments
    ///
    /// * `x` - X coordinate in data space
    /// * `y` - Y coordinate in data space
    /// * `text` - Text content to display
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .text(2.5, 100.0, "Peak value")
    ///     .save("annotated.png")?;
    /// ```
    pub fn text<S: Into<String>>(mut self, x: f64, y: f64, text: S) -> Self {
        self.annotations.push(Annotation::text(x, y, text));
        self
    }

    /// Add a text annotation with custom styling
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let style = TextStyle::new()
    ///     .font_size(14.0)
    ///     .color(Color::RED)
    ///     .align(TextAlign::Left);
    ///
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .text_styled(2.5, 100.0, "Peak value", style)
    ///     .save("annotated.png")?;
    /// ```
    pub fn text_styled<S: Into<String>>(
        mut self,
        x: f64,
        y: f64,
        text: S,
        style: TextStyle,
    ) -> Self {
        self.annotations
            .push(Annotation::text_styled(x, y, text, style));
        self
    }

    /// Add an arrow annotation between two points
    ///
    /// The arrow points from (x1, y1) to (x2, y2).
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .arrow(1.0, 50.0, 2.5, 100.0)  // Arrow pointing to peak
    ///     .save("annotated.png")?;
    /// ```
    pub fn arrow(mut self, x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
        self.annotations.push(Annotation::arrow(x1, y1, x2, y2));
        self
    }

    /// Add an arrow annotation with custom styling
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let style = ArrowStyle::new()
    ///     .color(Color::RED)
    ///     .line_width(2.0)
    ///     .head_style(ArrowHead::Stealth);
    ///
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .arrow_styled(1.0, 50.0, 2.5, 100.0, style)
    ///     .save("annotated.png")?;
    /// ```
    pub fn arrow_styled(mut self, x1: f64, y1: f64, x2: f64, y2: f64, style: ArrowStyle) -> Self {
        self.annotations
            .push(Annotation::arrow_styled(x1, y1, x2, y2, style));
        self
    }

    /// Add a horizontal reference line spanning the plot width
    ///
    /// Uses dashed gray style by default.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .hline(50.0)  // Add reference line at y=50
    ///     .save("annotated.png")?;
    /// ```
    pub fn hline(mut self, y: f64) -> Self {
        self.annotations.push(Annotation::hline(y));
        self
    }

    /// Add a horizontal reference line with custom styling
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .hline_styled(50.0, Color::RED, 2.0, LineStyle::Solid)
    ///     .save("annotated.png")?;
    /// ```
    pub fn hline_styled(mut self, y: f64, color: Color, width: f32, style: LineStyle) -> Self {
        self.annotations
            .push(Annotation::hline_styled(y, color, width, style));
        self
    }

    /// Add a vertical reference line spanning the plot height
    ///
    /// Uses dashed gray style by default.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .vline(2.5)  // Add reference line at x=2.5
    ///     .save("annotated.png")?;
    /// ```
    pub fn vline(mut self, x: f64) -> Self {
        self.annotations.push(Annotation::vline(x));
        self
    }

    /// Add a vertical reference line with custom styling
    pub fn vline_styled(mut self, x: f64, color: Color, width: f32, style: LineStyle) -> Self {
        self.annotations
            .push(Annotation::vline_styled(x, color, width, style));
        self
    }

    /// Add a rectangle annotation in data coordinates
    ///
    /// # Arguments
    ///
    /// * `x` - Left X coordinate in data space
    /// * `y` - Bottom Y coordinate in data space
    /// * `width` - Width in data units
    /// * `height` - Height in data units
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .rect(1.0, 20.0, 2.0, 60.0)  // Highlight region
    ///     .save("annotated.png")?;
    /// ```
    pub fn rect(mut self, x: f64, y: f64, width: f64, height: f64) -> Self {
        self.annotations
            .push(Annotation::rectangle(x, y, width, height));
        self
    }

    /// Add a rectangle annotation with custom styling
    pub fn rect_styled(
        mut self,
        x: f64,
        y: f64,
        width: f64,
        height: f64,
        style: ShapeStyle,
    ) -> Self {
        self.annotations
            .push(Annotation::rectangle_styled(x, y, width, height, style));
        self
    }

    /// Add a fill between two curves
    ///
    /// Fills the region between y1 and y2 at each x position.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let x = vec![1.0, 2.0, 3.0, 4.0];
    /// let y_upper = vec![10.0, 15.0, 12.0, 18.0];
    /// let y_lower = vec![5.0, 8.0, 6.0, 9.0];
    ///
    /// Plot::new()
    ///     .line(&x, &y_upper)
    ///     .fill_between(&x, &y_lower, &y_upper)
    ///     .save("filled.png")?;
    /// ```
    pub fn fill_between(mut self, x: &[f64], y1: &[f64], y2: &[f64]) -> Self {
        self.annotations.push(Annotation::fill_between(
            x.to_vec(),
            y1.to_vec(),
            y2.to_vec(),
        ));
        self
    }

    /// Add a fill between a curve and a constant baseline
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .fill_to_baseline(&x, &y, 0.0)  // Fill to y=0
    ///     .save("filled.png")?;
    /// ```
    pub fn fill_to_baseline(mut self, x: &[f64], y: &[f64], baseline: f64) -> Self {
        self.annotations.push(Annotation::fill_to_baseline(
            x.to_vec(),
            y.to_vec(),
            baseline,
        ));
        self
    }

    /// Add a fill between with custom styling
    pub fn fill_between_styled(
        mut self,
        x: &[f64],
        y1: &[f64],
        y2: &[f64],
        style: FillStyle,
        where_positive: bool,
    ) -> Self {
        self.annotations.push(Annotation::fill_between_styled(
            x.to_vec(),
            y1.to_vec(),
            y2.to_vec(),
            style,
            where_positive,
        ));
        self
    }

    /// Add a horizontal span (shaded vertical region)
    ///
    /// Highlights a vertical region from x_min to x_max across the full plot height.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .axvspan(2.0, 3.0)  // Highlight region between x=2 and x=3
    ///     .save("annotated.png")?;
    /// ```
    pub fn axvspan(mut self, x_min: f64, x_max: f64) -> Self {
        self.annotations.push(Annotation::hspan(x_min, x_max));
        self
    }

    /// Add a vertical span (shaded horizontal region)
    ///
    /// Highlights a horizontal region from y_min to y_max across the full plot width.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .axhspan(20.0, 80.0)  // Highlight region between y=20 and y=80
    ///     .save("annotated.png")?;
    /// ```
    pub fn axhspan(mut self, y_min: f64, y_max: f64) -> Self {
        self.annotations.push(Annotation::vspan(y_min, y_max));
        self
    }

    /// Add a generic annotation
    ///
    /// Use this method to add pre-constructed annotations.
    pub fn annotate(mut self, annotation: Annotation) -> Self {
        self.annotations.push(annotation);
        self
    }

    /// Get annotations for iteration (used during rendering)
    pub fn get_annotations(&self) -> &[Annotation] {
        &self.annotations
    }

    // ========== End Annotation Methods ==========

    /// Enable or disable Typst rendering for all plot text surfaces.
    ///
    /// When enabled, all static text surfaces (title, axis labels, tick labels,
    /// legend labels, category labels, and annotations) are rendered through Typst.
    pub fn typst(mut self, enabled: bool) -> Self {
        self.display.text_engine = if enabled {
            TextEngineMode::Typst
        } else {
            TextEngineMode::Plain
        };
        self
    }

    /// Set transparency for the next series
    pub fn alpha(self, _alpha: f32) -> Self {
        // This would be handled by the series builder
        // Keeping for API compatibility
        self
    }

    /// Add a new line to existing plot (for incremental updates)
    pub fn add_line<X, Y>(&mut self, x_data: &X, y_data: &Y) -> Result<()>
    where
        X: NumericData1D,
        Y: NumericData1D,
    {
        let x_vec = collect_numeric_data_1d(x_data, self.null_policy)?;
        let y_vec = collect_numeric_data_1d(y_data, self.null_policy)?;

        if x_vec.len() != y_vec.len() {
            return Err(PlottingError::DataLengthMismatch {
                x_len: x_vec.len(),
                y_len: y_vec.len(),
                series_index: None,
            });
        }

        if x_vec.is_empty() {
            return Err(PlottingError::EmptyDataSet);
        }

        let series = PlotSeries {
            series_type: SeriesType::Line {
                x_data: PlotData::Static(x_vec),
                y_data: PlotData::Static(y_vec),
            },
            label: None,
            color: Some(
                self.display
                    .theme
                    .get_color(self.series_mgr.auto_color_index),
            ),
            line_width: None,
            line_style: None,
            marker_style: None,
            marker_size: None,
            alpha: None,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        self.series_mgr.series.push(series);
        self.series_mgr.auto_color_index += 1;

        Ok(())
    }

    /// Internal method to add a KDE series (used by PlotBuilder)
    ///
    /// This method is called by the PlotBuilder when finalizing a KDE series.
    pub(crate) fn add_kde_series(
        mut self,
        kde_data: crate::plots::KdeData,
        style: crate::core::plot::builder::SeriesStyle,
    ) -> Self {
        let series = PlotSeries {
            series_type: SeriesType::Kde { data: kde_data },
            label: style.label,
            color: style.color.or_else(|| {
                Some(
                    self.display
                        .theme
                        .get_color(self.series_mgr.auto_color_index),
                )
            }),
            line_width: style.line_width,
            line_style: style.line_style,
            marker_style: style.marker_style,
            marker_size: style.marker_size,
            alpha: style.alpha,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        self.series_mgr.series.push(series);
        self.series_mgr.auto_color_index += 1;
        self
    }

    /// Internal method to add an ECDF series
    pub(crate) fn add_ecdf_series(
        mut self,
        ecdf_data: crate::plots::EcdfData,
        style: crate::core::plot::builder::SeriesStyle,
    ) -> Self {
        let series = PlotSeries {
            series_type: SeriesType::Ecdf { data: ecdf_data },
            label: style.label,
            color: style.color.or_else(|| {
                Some(
                    self.display
                        .theme
                        .get_color(self.series_mgr.auto_color_index),
                )
            }),
            line_width: style.line_width,
            line_style: style.line_style,
            marker_style: style.marker_style,
            marker_size: style.marker_size,
            alpha: style.alpha,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        self.series_mgr.series.push(series);
        self.series_mgr.auto_color_index += 1;
        self
    }

    /// Internal method to add a Contour series
    pub(crate) fn add_contour_series(
        mut self,
        contour_data: crate::plots::continuous::contour::ContourPlotData,
        style: crate::core::plot::builder::SeriesStyle,
    ) -> Self {
        let series = PlotSeries {
            series_type: SeriesType::Contour { data: contour_data },
            label: style.label,
            color: style.color.or_else(|| {
                Some(
                    self.display
                        .theme
                        .get_color(self.series_mgr.auto_color_index),
                )
            }),
            line_width: style.line_width,
            line_style: style.line_style,
            marker_style: style.marker_style,
            marker_size: style.marker_size,
            alpha: style.alpha,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        self.series_mgr.series.push(series);
        self.series_mgr.auto_color_index += 1;
        self
    }

    /// Internal method to add a Pie series
    pub(crate) fn add_pie_series(
        mut self,
        pie_data: crate::plots::composition::pie::PieData,
        style: crate::core::plot::builder::SeriesStyle,
    ) -> Self {
        let series = PlotSeries {
            series_type: SeriesType::Pie { data: pie_data },
            label: style.label,
            color: style.color.or_else(|| {
                Some(
                    self.display
                        .theme
                        .get_color(self.series_mgr.auto_color_index),
                )
            }),
            line_width: style.line_width,
            line_style: style.line_style,
            marker_style: style.marker_style,
            marker_size: style.marker_size,
            alpha: style.alpha,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        self.series_mgr.series.push(series);
        self.series_mgr.auto_color_index += 1;
        self
    }

    /// Internal method to add a Radar series
    pub(crate) fn add_radar_series(
        mut self,
        radar_data: crate::plots::polar::radar::RadarPlotData,
        style: crate::core::plot::builder::SeriesStyle,
    ) -> Self {
        let series = PlotSeries {
            series_type: SeriesType::Radar { data: radar_data },
            label: style.label,
            color: style.color.or_else(|| {
                Some(
                    self.display
                        .theme
                        .get_color(self.series_mgr.auto_color_index),
                )
            }),
            line_width: style.line_width,
            line_style: style.line_style,
            marker_style: style.marker_style,
            marker_size: style.marker_size,
            alpha: style.alpha,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        self.series_mgr.series.push(series);
        self.series_mgr.auto_color_index += 1;
        self
    }

    /// Internal method to add a Violin series
    pub(crate) fn add_violin_series(
        mut self,
        violin_data: crate::plots::ViolinData,
        style: crate::core::plot::builder::SeriesStyle,
    ) -> Self {
        let series = PlotSeries {
            series_type: SeriesType::Violin { data: violin_data },
            label: style.label,
            color: style.color.or_else(|| {
                Some(
                    self.display
                        .theme
                        .get_color(self.series_mgr.auto_color_index),
                )
            }),
            line_width: style.line_width,
            line_style: style.line_style,
            marker_style: style.marker_style,
            marker_size: style.marker_size,
            alpha: style.alpha,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        self.series_mgr.series.push(series);
        self.series_mgr.auto_color_index += 1;
        self
    }

    /// Internal method to add a Polar series
    pub(crate) fn add_polar_series(
        mut self,
        polar_data: crate::plots::polar::polar_plot::PolarPlotData,
        style: crate::core::plot::builder::SeriesStyle,
    ) -> Self {
        let series = PlotSeries {
            series_type: SeriesType::Polar { data: polar_data },
            label: style.label,
            color: style.color.or_else(|| {
                Some(
                    self.display
                        .theme
                        .get_color(self.series_mgr.auto_color_index),
                )
            }),
            line_width: style.line_width,
            line_style: style.line_style,
            marker_style: style.marker_style,
            marker_size: style.marker_size,
            alpha: style.alpha,
            y_errors: None,
            x_errors: None,
            error_config: None,
            group_id: None,
        };

        self.series_mgr.series.push(series);
        self.series_mgr.auto_color_index += 1;
        self
    }

    /// Internal method to add a Line series (used by PlotBuilder<LineConfig>)
    ///
    /// This method is called by the PlotBuilder when finalizing a line series.
    pub(crate) fn add_line_series(
        self,
        x_data: PlotData,
        y_data: PlotData,
        config: &crate::plots::basic::LineConfig,
        style: crate::core::plot::builder::SeriesStyle,
    ) -> Self {
        self.add_line_series_grouped(x_data, y_data, config, style, None, true)
    }

    /// Internal method to add a Line series with optional grouped-series metadata.
    pub(crate) fn add_line_series_grouped(
        mut self,
        x_data: PlotData,
        y_data: PlotData,
        config: &crate::plots::basic::LineConfig,
        style: crate::core::plot::builder::SeriesStyle,
        group_id: Option<usize>,
        consume_palette_index: bool,
    ) -> Self {
        let series = PlotSeries {
            series_type: SeriesType::Line { x_data, y_data },
            label: style.label,
            color: style.color.or(config.color).or_else(|| {
                Some(
                    self.display
                        .theme
                        .get_color(self.series_mgr.auto_color_index),
                )
            }),
            line_width: style.line_width.or(config.line_width),
            line_style: style.line_style.or(Some(config.line_style.clone())),
            marker_style: style.marker_style.or(config.marker),
            marker_size: style.marker_size.or(if config.show_markers {
                Some(config.marker_size)
            } else {
                None
            }),
            alpha: style.alpha.or(Some(config.alpha)),
            y_errors: style.y_errors,
            x_errors: style.x_errors,
            error_config: style.error_config,
            group_id,
        };

        self.series_mgr.series.push(series);
        if consume_palette_index {
            self.series_mgr.auto_color_index += 1;
        }
        self
    }

    /// Internal method to add a Scatter series (used by PlotBuilder<ScatterConfig>)
    ///
    /// This method is called by the PlotBuilder when finalizing a scatter series.
    pub(crate) fn add_scatter_series(
        self,
        x_data: PlotData,
        y_data: PlotData,
        config: &crate::plots::basic::ScatterConfig,
        style: crate::core::plot::builder::SeriesStyle,
    ) -> Self {
        self.add_scatter_series_grouped(x_data, y_data, config, style, None, true)
    }

    /// Internal method to add a Scatter series with optional grouped-series metadata.
    pub(crate) fn add_scatter_series_grouped(
        mut self,
        x_data: PlotData,
        y_data: PlotData,
        config: &crate::plots::basic::ScatterConfig,
        style: crate::core::plot::builder::SeriesStyle,
        group_id: Option<usize>,
        consume_palette_index: bool,
    ) -> Self {
        let series = PlotSeries {
            series_type: SeriesType::Scatter { x_data, y_data },
            label: style.label,
            color: style.color.or(config.color).or_else(|| {
                Some(
                    self.display
                        .theme
                        .get_color(self.series_mgr.auto_color_index),
                )
            }),
            line_width: style.line_width.or(Some(config.edge_width)),
            line_style: style.line_style,
            marker_style: style.marker_style.or(Some(config.marker)),
            marker_size: style.marker_size.or(Some(config.size)),
            alpha: style.alpha.or(Some(config.alpha)),
            y_errors: style.y_errors,
            x_errors: style.x_errors,
            error_config: style.error_config,
            group_id,
        };

        self.series_mgr.series.push(series);
        if consume_palette_index {
            self.series_mgr.auto_color_index += 1;
        }
        self
    }

    /// Internal method to add a Bar series (used by PlotBuilder<BarConfig>)
    ///
    /// This method is called by the PlotBuilder when finalizing a bar series.
    pub(crate) fn add_bar_series(
        self,
        categories: Vec<String>,
        values: PlotData,
        config: &crate::plots::basic::BarConfig,
        style: crate::core::plot::builder::SeriesStyle,
    ) -> Self {
        self.add_bar_series_grouped(categories, values, config, style, None, true)
    }

    /// Internal method to add a Bar series with optional grouped-series metadata.
    pub(crate) fn add_bar_series_grouped(
        mut self,
        categories: Vec<String>,
        values: PlotData,
        config: &crate::plots::basic::BarConfig,
        style: crate::core::plot::builder::SeriesStyle,
        group_id: Option<usize>,
        consume_palette_index: bool,
    ) -> Self {
        let series = PlotSeries {
            series_type: SeriesType::Bar { categories, values },
            label: style.label,
            color: style.color.or(config.color).or_else(|| {
                Some(
                    self.display
                        .theme
                        .get_color(self.series_mgr.auto_color_index),
                )
            }),
            line_width: style.line_width.or(Some(config.edge_width)),
            line_style: style.line_style,
            marker_style: style.marker_style,
            marker_size: style.marker_size,
            alpha: style.alpha.or(Some(config.alpha)),
            y_errors: style.y_errors,
            x_errors: style.x_errors,
            error_config: style.error_config,
            group_id,
        };

        self.series_mgr.series.push(series);
        if consume_palette_index {
            self.series_mgr.auto_color_index += 1;
        }
        self
    }

    /// Helper method to render a single series using normal (non-DataShader) rendering
    fn render_series_normal(
        &self,
        series: &PlotSeries,
        renderer: &mut SkiaRenderer,
        plot_area: tiny_skia::Rect,
        x_min: f64,
        x_max: f64,
        y_min: f64,
        y_max: f64,
    ) -> Result<()> {
        let color = series.color.unwrap_or(Color::new(0, 0, 0)); // Default black
        let line_width = self.dpi_scaled_line_width(series.line_width.unwrap_or(2.0));
        let line_style = series.line_style.clone().unwrap_or(LineStyle::Solid);

        match &series.series_type {
            SeriesType::Line { x_data, y_data } => {
                let x_data = x_data.resolve(0.0);
                let y_data = y_data.resolve(0.0);
                let points: Vec<(f32, f32)> = x_data
                    .iter()
                    .zip(y_data.iter())
                    .map(|(&x, &y)| {
                        crate::render::skia::map_data_to_pixels(
                            x, y, x_min, x_max, y_min, y_max, plot_area,
                        )
                    })
                    .collect();

                renderer.draw_polyline(&points, color, line_width, line_style)?;

                // Draw attached error bars if present
                if series.y_errors.is_some() || series.x_errors.is_some() {
                    let dpi_scale = self.dpi_scale();
                    Self::render_attached_error_bars(
                        renderer,
                        &x_data,
                        &y_data,
                        series.y_errors.as_ref(),
                        series.x_errors.as_ref(),
                        series.error_config.as_ref(),
                        color,
                        x_min,
                        x_max,
                        y_min,
                        y_max,
                        plot_area,
                        line_width,
                        dpi_scale,
                    )?;
                }
            }
            SeriesType::Scatter { x_data, y_data } => {
                let x_data = x_data.resolve(0.0);
                let y_data = y_data.resolve(0.0);
                let marker_size = self.dpi_scaled_line_width(series.marker_size.unwrap_or(10.0)); // DPI-scaled marker size
                let marker_style = series.marker_style.unwrap_or(MarkerStyle::Circle);

                for (&x, &y) in x_data.iter().zip(y_data.iter()) {
                    let (px, py) = crate::render::skia::map_data_to_pixels(
                        x, y, x_min, x_max, y_min, y_max, plot_area,
                    );
                    renderer.draw_marker(px, py, marker_size, marker_style, color)?;
                }

                // Draw attached error bars if present
                if series.y_errors.is_some() || series.x_errors.is_some() {
                    let dpi_scale = self.dpi_scale();
                    Self::render_attached_error_bars(
                        renderer,
                        &x_data,
                        &y_data,
                        series.y_errors.as_ref(),
                        series.x_errors.as_ref(),
                        series.error_config.as_ref(),
                        color,
                        x_min,
                        x_max,
                        y_min,
                        y_max,
                        plot_area,
                        line_width,
                        dpi_scale,
                    )?;
                }
            }
            SeriesType::Bar { values, .. } => {
                let values = values.resolve(0.0);
                // Bar width as fraction of category spacing (0.8 = 80%, matching matplotlib)
                let bar_width_fraction = 0.8;
                let data_range = (x_max - x_min) as f32;
                let pixels_per_unit = plot_area.width() / data_range;
                let bar_width = bar_width_fraction * pixels_per_unit;

                for (i, &value) in values.iter().enumerate() {
                    let x = i as f64;
                    let (px, py) = crate::render::skia::map_data_to_pixels(
                        x, value, x_min, x_max, y_min, y_max, plot_area,
                    );
                    let (_, py_zero) = crate::render::skia::map_data_to_pixels(
                        x, 0.0, x_min, x_max, y_min, y_max, plot_area,
                    );
                    renderer.draw_rectangle(
                        px - bar_width / 2.0,
                        py.min(py_zero),
                        bar_width,
                        (py - py_zero).abs(),
                        color,
                        true,
                    )?;
                }
            }
            SeriesType::Histogram { data, config } => {
                let data = data.resolve(0.0);
                // Calculate histogram data
                let hist_data = crate::plots::histogram::calculate_histogram(&data, config)
                    .map_err(|e| {
                        PlottingError::RenderError(format!("Histogram calculation failed: {}", e))
                    })?;

                // Render histogram bars
                for (i, &count) in hist_data.counts.iter().enumerate() {
                    if count > 0.0 {
                        let x_left = hist_data.bin_edges[i];
                        let x_right = hist_data.bin_edges[i + 1];
                        let x_center = (x_left + x_right) / 2.0;

                        // Convert bar width from data coordinates to pixel coordinates
                        let (px_left, _) = crate::render::skia::map_data_to_pixels(
                            x_left, 0.0, x_min, x_max, y_min, y_max, plot_area,
                        );
                        let (px_right, _) = crate::render::skia::map_data_to_pixels(
                            x_right, 0.0, x_min, x_max, y_min, y_max, plot_area,
                        );
                        let bar_width_px = (px_right - px_left).abs();

                        let (px, py) = crate::render::skia::map_data_to_pixels(
                            x_center, count, x_min, x_max, y_min, y_max, plot_area,
                        );
                        let (_, py_zero) = crate::render::skia::map_data_to_pixels(
                            x_center, 0.0, x_min, x_max, y_min, y_max, plot_area,
                        );

                        renderer.draw_rectangle(
                            px - bar_width_px / 2.0,
                            py.min(py_zero),
                            bar_width_px,
                            (py - py_zero).abs(),
                            color,
                            true,
                        )?;
                    }
                }
            }
            SeriesType::BoxPlot { data, config } => {
                let data = data.resolve(0.0);
                // Calculate box plot statistics
                let box_data =
                    crate::plots::boxplot::calculate_box_plot(&data, config).map_err(|e| {
                        PlottingError::RenderError(format!("Box plot calculation failed: {}", e))
                    })?;

                // Box plot positioning
                let x_center = 0.5; // Center the box plot
                let box_width = 0.3; // Box width

                // Map coordinates to pixels
                let (x_center_px, _) = crate::render::skia::map_data_to_pixels(
                    x_center, 0.0, x_min, x_max, y_min, y_max, plot_area,
                );
                let (_, q1_y) = crate::render::skia::map_data_to_pixels(
                    0.0,
                    box_data.q1,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                );
                let (_, median_y) = crate::render::skia::map_data_to_pixels(
                    0.0,
                    box_data.median,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                );
                let (_, q3_y) = crate::render::skia::map_data_to_pixels(
                    0.0,
                    box_data.q3,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                );
                let (_, lower_whisker_y) = crate::render::skia::map_data_to_pixels(
                    0.0,
                    box_data.min,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                );
                let (_, upper_whisker_y) = crate::render::skia::map_data_to_pixels(
                    0.0,
                    box_data.max,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                );

                let box_half_width = box_width * plot_area.width() * 0.5;
                let box_left = x_center_px - box_half_width;
                let box_right = x_center_px + box_half_width;

                // Draw the box (IQR) - ensure positive dimensions
                let box_width = box_right - box_left;
                let box_height = (q1_y - q3_y).abs(); // Ensure positive height
                let box_top = q3_y.min(q1_y); // Use the smaller y value as top

                // Validate dimensions before drawing
                if box_width > 0.0
                    && box_height > 0.0
                    && box_width.is_finite()
                    && box_height.is_finite()
                {
                    renderer.draw_rectangle(
                        box_left, box_top, box_width, box_height, color, false, // outline only
                    )?;
                }

                // Draw median line - validate coordinates
                if box_left.is_finite() && median_y.is_finite() && box_right.is_finite() {
                    renderer.draw_line(
                        box_left,
                        median_y,
                        box_right,
                        median_y,
                        color,
                        line_width * 1.5, // thicker median line
                        line_style.clone(),
                    )?;
                }

                // Draw lower whisker - validate coordinates
                if x_center_px.is_finite() && q1_y.is_finite() && lower_whisker_y.is_finite() {
                    renderer.draw_line(
                        x_center_px,
                        q1_y,
                        x_center_px,
                        lower_whisker_y,
                        color,
                        line_width,
                        line_style.clone(),
                    )?;
                }

                // Draw upper whisker - validate coordinates
                if x_center_px.is_finite() && q3_y.is_finite() && upper_whisker_y.is_finite() {
                    renderer.draw_line(
                        x_center_px,
                        q3_y,
                        x_center_px,
                        upper_whisker_y,
                        color,
                        line_width,
                        line_style.clone(),
                    )?;
                }

                // Draw whisker caps - validate coordinates
                let cap_width = box_half_width * 0.6;
                if x_center_px.is_finite() && lower_whisker_y.is_finite() && cap_width.is_finite() {
                    renderer.draw_line(
                        x_center_px - cap_width,
                        lower_whisker_y,
                        x_center_px + cap_width,
                        lower_whisker_y,
                        color,
                        line_width,
                        line_style.clone(),
                    )?;
                }

                if x_center_px.is_finite() && upper_whisker_y.is_finite() && cap_width.is_finite() {
                    renderer.draw_line(
                        x_center_px - cap_width,
                        upper_whisker_y,
                        x_center_px + cap_width,
                        upper_whisker_y,
                        color,
                        line_width,
                        line_style.clone(),
                    )?;
                }

                // Draw outliers - validate coordinates
                for &outlier in &box_data.outliers {
                    let (_, outlier_y) = crate::render::skia::map_data_to_pixels(
                        0.0, outlier, x_min, x_max, y_min, y_max, plot_area,
                    );
                    if x_center_px.is_finite() && outlier_y.is_finite() {
                        renderer.draw_marker(
                            x_center_px,
                            outlier_y,
                            4.0, // outlier marker size
                            MarkerStyle::Circle,
                            color,
                        )?;
                    }
                }
            }
            SeriesType::Heatmap { data } => {
                // Calculate cell dimensions in pixel space
                let cell_width = plot_area.width() / data.n_cols as f32;
                let cell_height = plot_area.height() / data.n_rows as f32;

                // Render each cell as a filled rectangle
                for (row_idx, row) in data.values.iter().enumerate() {
                    for (col_idx, &value) in row.iter().enumerate() {
                        let cell_color = data.get_color(value);

                        // Apply alpha from config
                        let cell_color = if data.config.alpha < 1.0 {
                            Color::new_rgba(
                                cell_color.r,
                                cell_color.g,
                                cell_color.b,
                                (data.config.alpha * 255.0) as u8,
                            )
                        } else {
                            cell_color
                        };

                        // Calculate cell position (row 0 at top)
                        let cell_x = plot_area.x() + col_idx as f32 * cell_width;
                        let cell_y =
                            plot_area.y() + (data.n_rows - 1 - row_idx) as f32 * cell_height;

                        renderer.draw_rectangle(
                            cell_x,
                            cell_y,
                            cell_width,
                            cell_height,
                            cell_color,
                            true,
                        )?;

                        // Draw cell annotation if enabled
                        if data.config.annotate {
                            let text = format!("{:.2}", value);
                            let text_color = data.get_text_color(cell_color);
                            let text_x = cell_x + cell_width / 2.0;
                            let font_size = (cell_height * 0.3).clamp(8.0, 20.0);
                            // Center vertically: y position at cell center + half font size
                            let text_y = cell_y + cell_height / 2.0 + font_size / 3.0;
                            renderer
                                .draw_text_centered(&text, text_x, text_y, font_size, text_color)?;
                        }
                    }
                }

                // Draw colorbar if enabled
                if data.config.colorbar {
                    let colorbar_width = 20.0;
                    let colorbar_margin = 10.0;
                    let colorbar_x = plot_area.right() + colorbar_margin;
                    let colorbar_y = plot_area.y();
                    let colorbar_height = plot_area.height();

                    renderer.draw_colorbar(
                        &data.config.colormap,
                        data.vmin,
                        data.vmax,
                        colorbar_x,
                        colorbar_y,
                        colorbar_width,
                        colorbar_height,
                        data.config.colorbar_label.as_deref(),
                        color,
                        data.config.colorbar_tick_font_size,
                        Some(data.config.colorbar_label_font_size),
                    )?;
                }
            }
            SeriesType::ErrorBars {
                x_data,
                y_data,
                y_errors,
            } => {
                let x_data = x_data.resolve(0.0);
                let y_data = y_data.resolve(0.0);
                let y_errors = y_errors.resolve(0.0);
                // Draw markers at data points
                let marker_size = self.dpi_scaled_line_width(series.marker_size.unwrap_or(8.0));
                let marker_style = series.marker_style.unwrap_or(MarkerStyle::Circle);

                for (&x, &y) in x_data.iter().zip(y_data.iter()) {
                    if x.is_finite() && y.is_finite() {
                        let (px, py) = crate::render::skia::map_data_to_pixels(
                            x, y, x_min, x_max, y_min, y_max, plot_area,
                        );
                        renderer.draw_marker(px, py, marker_size, marker_style, color)?;
                    }
                }

                // Draw Y error bars
                let y_err_values = ErrorValues::symmetric(y_errors);
                let dpi_scale = self.dpi_scale();
                Self::render_attached_error_bars(
                    renderer,
                    &x_data,
                    &y_data,
                    Some(&y_err_values),
                    None,
                    series.error_config.as_ref(),
                    color,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                    line_width,
                    dpi_scale,
                )?;
            }
            SeriesType::ErrorBarsXY {
                x_data,
                y_data,
                x_errors,
                y_errors,
            } => {
                let x_data = x_data.resolve(0.0);
                let y_data = y_data.resolve(0.0);
                let x_errors = x_errors.resolve(0.0);
                let y_errors = y_errors.resolve(0.0);
                // Draw markers at data points
                let marker_size = self.dpi_scaled_line_width(series.marker_size.unwrap_or(8.0));
                let marker_style = series.marker_style.unwrap_or(MarkerStyle::Circle);

                for (&x, &y) in x_data.iter().zip(y_data.iter()) {
                    if x.is_finite() && y.is_finite() {
                        let (px, py) = crate::render::skia::map_data_to_pixels(
                            x, y, x_min, x_max, y_min, y_max, plot_area,
                        );
                        renderer.draw_marker(px, py, marker_size, marker_style, color)?;
                    }
                }

                // Draw X and Y error bars
                let x_err_values = ErrorValues::symmetric(x_errors);
                let y_err_values = ErrorValues::symmetric(y_errors);
                let dpi_scale = self.dpi_scale();
                Self::render_attached_error_bars(
                    renderer,
                    &x_data,
                    &y_data,
                    Some(&y_err_values),
                    Some(&x_err_values),
                    series.error_config.as_ref(),
                    color,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                    line_width,
                    dpi_scale,
                )?;
            }
            SeriesType::Kde { data } => {
                // Use PlotRender trait to render KDE
                let plot_area = crate::plots::PlotArea::new(
                    plot_area.x(),
                    plot_area.y(),
                    plot_area.width(),
                    plot_area.height(),
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                );
                data.render(renderer, &plot_area, &self.display.theme, color)?;
            }
            SeriesType::Ecdf { data } => {
                // Use PlotRender trait to render ECDF
                let plot_area = crate::plots::PlotArea::new(
                    plot_area.x(),
                    plot_area.y(),
                    plot_area.width(),
                    plot_area.height(),
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                );
                data.render(renderer, &plot_area, &self.display.theme, color)?;
            }
            SeriesType::Violin { data } => {
                // Use PlotRender trait to render Violin
                let plot_area = crate::plots::PlotArea::new(
                    plot_area.x(),
                    plot_area.y(),
                    plot_area.width(),
                    plot_area.height(),
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                );
                data.render(renderer, &plot_area, &self.display.theme, color)?;
            }
            SeriesType::Boxen { data } => {
                // Use PlotRender trait to render Boxen
                let plot_area = crate::plots::PlotArea::new(
                    plot_area.x(),
                    plot_area.y(),
                    plot_area.width(),
                    plot_area.height(),
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                );
                data.render(renderer, &plot_area, &self.display.theme, color)?;
            }
            SeriesType::Contour { data } => {
                // Use PlotRender trait to render Contour
                let contour_plot_area = crate::plots::PlotArea::new(
                    plot_area.x(),
                    plot_area.y(),
                    plot_area.width(),
                    plot_area.height(),
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                );
                data.render(renderer, &contour_plot_area, &self.display.theme, color)?;

                // Draw colorbar if enabled
                if data.config.colorbar {
                    let colorbar_width = 20.0;
                    let colorbar_margin = 10.0;
                    let colorbar_x = plot_area.right() + colorbar_margin;
                    let colorbar_y = plot_area.y();
                    let colorbar_height = plot_area.height();

                    // Get value range from contour data
                    let (vmin, vmax) = if data.levels.is_empty() {
                        (0.0, 1.0)
                    } else {
                        (
                            data.levels.first().copied().unwrap_or(0.0),
                            data.levels.last().copied().unwrap_or(1.0),
                        )
                    };

                    // Get colormap from contour config
                    let colormap = crate::render::ColorMap::by_name(&data.config.cmap)
                        .unwrap_or_else(crate::render::ColorMap::viridis);

                    renderer.draw_colorbar(
                        &colormap,
                        vmin,
                        vmax,
                        colorbar_x,
                        colorbar_y,
                        colorbar_width,
                        colorbar_height,
                        data.config.colorbar_label.as_deref(),
                        self.display.theme.foreground,
                        data.config.colorbar_tick_font_size,
                        Some(data.config.colorbar_label_font_size),
                    )?;
                }
            }
            SeriesType::Pie { data } => {
                // Use PlotRender trait to render Pie with 1:1 aspect ratio
                // (uses normalized 0-1 coordinates)
                let (pie_x, pie_y, pie_size) = {
                    let size = plot_area.width().min(plot_area.height());
                    let x_offset = (plot_area.width() - size) / 2.0;
                    let y_offset = (plot_area.height() - size) / 2.0;
                    (plot_area.x() + x_offset, plot_area.y() + y_offset, size)
                };
                let pie_plot_area = crate::plots::PlotArea::new(
                    pie_x, pie_y, pie_size, pie_size, 0.0, 1.0, 0.0, 1.0,
                );
                data.render(renderer, &pie_plot_area, &self.display.theme, color)?;
            }
            SeriesType::Radar { data } => {
                // Use PlotRender trait to render Radar with 1:1 aspect ratio
                // and extra top padding for title clearance
                let (square_area, adjusted_y, adjusted_height) = {
                    let size = plot_area.width().min(plot_area.height());
                    let x_offset = (plot_area.width() - size) / 2.0;
                    // Add extra top margin to avoid title overlap with top axis labels
                    // Radar axis labels are positioned at r=1.15 (15% outside the chart)
                    // Plus we need space for the label text itself (~10% more)
                    // Total: 25% clearance for proper separation
                    let title_clearance = size * 0.20;
                    let y_offset = (plot_area.height() - size) / 2.0 + title_clearance;
                    let adjusted_size = size - title_clearance;
                    (
                        plot_area.x() + x_offset,
                        plot_area.y() + y_offset,
                        adjusted_size,
                    )
                };
                let radar_plot_area = crate::plots::PlotArea::new(
                    square_area,
                    adjusted_y,
                    adjusted_height, // square
                    adjusted_height, // square
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                );
                data.render(renderer, &radar_plot_area, &self.display.theme, color)?;
            }
            SeriesType::Polar { data } => {
                // Use PlotRender trait to render Polar with 1:1 aspect ratio
                // Center the square plot area within available space
                let (polar_x, polar_y, polar_size) = {
                    let size = plot_area.width().min(plot_area.height());
                    let x_offset = (plot_area.width() - size) / 2.0;
                    let y_offset = (plot_area.height() - size) / 2.0;
                    (plot_area.x() + x_offset, plot_area.y() + y_offset, size)
                };
                let polar_plot_area = crate::plots::PlotArea::new(
                    polar_x, polar_y, polar_size, polar_size, x_min, x_max, y_min, y_max,
                );
                data.render(renderer, &polar_plot_area, &self.display.theme, color)?;
            }
        }

        Ok(())
    }

    /// Render a series using GPU-accelerated coordinate transformation
    ///
    /// Uses GPU compute shaders for coordinate transformation when available,
    /// falling back to CPU for the actual drawing operations.
    #[cfg(feature = "gpu")]
    fn render_series_gpu(
        &self,
        series: &PlotSeries,
        renderer: &mut SkiaRenderer,
        gpu_renderer: &mut GpuRenderer,
        plot_area: tiny_skia::Rect,
        x_min: f64,
        x_max: f64,
        y_min: f64,
        y_max: f64,
    ) -> Result<()> {
        let color = series.color.unwrap_or(Color::new(0, 0, 0));
        let line_width = self.dpi_scaled_line_width(series.line_width.unwrap_or(2.0));
        let line_style = series.line_style.clone().unwrap_or(LineStyle::Solid);

        match &series.series_type {
            SeriesType::Line { x_data, y_data } => {
                // Resolve PlotData to concrete Vec<f64>
                let x_data = x_data.resolve(0.0);
                let y_data = y_data.resolve(0.0);

                // Use GPU for coordinate transformation
                let viewport = (
                    plot_area.x(),
                    plot_area.y(),
                    plot_area.x() + plot_area.width(),
                    plot_area.y() + plot_area.height(),
                );

                let (x_transformed, y_transformed) = gpu_renderer
                    .transform_coordinates_optimal(
                        &x_data,
                        &y_data,
                        (x_min, x_max),
                        (y_min, y_max),
                        viewport,
                    )
                    .map_err(|e| {
                        PlottingError::RenderError(format!("GPU transform failed: {}", e))
                    })?;

                // Convert to points for drawing
                let points: Vec<(f32, f32)> = x_transformed
                    .iter()
                    .zip(y_transformed.iter())
                    .map(|(&x, &y)| (x, y))
                    .collect();

                renderer.draw_polyline(&points, color, line_width, line_style)?;
            }
            SeriesType::Scatter { x_data, y_data } => {
                // Resolve PlotData to concrete Vec<f64>
                let x_data = x_data.resolve(0.0);
                let y_data = y_data.resolve(0.0);

                // Use GPU for coordinate transformation
                let viewport = (
                    plot_area.x(),
                    plot_area.y(),
                    plot_area.x() + plot_area.width(),
                    plot_area.y() + plot_area.height(),
                );

                let (x_transformed, y_transformed) = gpu_renderer
                    .transform_coordinates_optimal(
                        &x_data,
                        &y_data,
                        (x_min, x_max),
                        (y_min, y_max),
                        viewport,
                    )
                    .map_err(|e| {
                        PlottingError::RenderError(format!("GPU transform failed: {}", e))
                    })?;

                let marker_size = self.dpi_scaled_line_width(series.marker_size.unwrap_or(10.0));
                let marker_style = series.marker_style.unwrap_or(MarkerStyle::Circle);

                // Draw markers at transformed coordinates
                for (&px, &py) in x_transformed.iter().zip(y_transformed.iter()) {
                    renderer.draw_marker(px, py, marker_size, marker_style, color)?;
                }
            }
            // For other series types, fall back to normal rendering
            _ => {
                self.render_series_normal(series, renderer, plot_area, x_min, x_max, y_min, y_max)?;
            }
        }

        Ok(())
    }

    /// Internal validation logic for series data
    fn validate_series(&self) -> Result<()> {
        if let Some(err) = self.pending_ingestion_error() {
            return Err(err);
        }

        // Validate we have at least one series
        if self.series_mgr.series.is_empty() {
            return Err(PlottingError::NoDataSeries);
        }

        // Validate all series data
        for (idx, series) in self.series_mgr.series.iter().enumerate() {
            match &series.series_type {
                SeriesType::Line { x_data, y_data } | SeriesType::Scatter { x_data, y_data } => {
                    if x_data.len() != y_data.len() {
                        return Err(PlottingError::DataLengthMismatch {
                            x_len: x_data.len(),
                            y_len: y_data.len(),
                            series_index: Some(idx),
                        });
                    }
                    if x_data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Bar { categories, values } => {
                    if categories.len() != values.len() {
                        return Err(PlottingError::DataLengthMismatch {
                            x_len: categories.len(),
                            y_len: values.len(),
                            series_index: Some(idx),
                        });
                    }
                    if categories.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::ErrorBars {
                    x_data,
                    y_data,
                    y_errors,
                } => {
                    if x_data.len() != y_data.len() || y_data.len() != y_errors.len() {
                        return Err(PlottingError::DataLengthMismatch {
                            x_len: x_data.len(),
                            y_len: y_data.len(),
                            series_index: Some(idx),
                        });
                    }
                }
                SeriesType::ErrorBarsXY {
                    x_data,
                    y_data,
                    x_errors,
                    y_errors,
                } => {
                    if x_data.len() != y_data.len()
                        || x_data.len() != x_errors.len()
                        || x_data.len() != y_errors.len()
                    {
                        return Err(PlottingError::DataLengthMismatch {
                            x_len: x_data.len(),
                            y_len: y_data.len(),
                            series_index: Some(idx),
                        });
                    }
                }
                SeriesType::Histogram { data, .. } => {
                    if data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::BoxPlot { data, .. } => {
                    if data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Heatmap { data } => {
                    if data.values.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Kde { data } => {
                    if data.x.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Ecdf { data } => {
                    if data.x.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Violin { data } => {
                    if data.data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Boxen { data } => {
                    if data.boxes.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Contour { data } => {
                    if data.levels.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Pie { data } => {
                    if data.values.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Radar { data } => {
                    if data.series.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Polar { data } => {
                    if data.points.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
            }
        }

        Ok(())
    }

    /// Render the plot to an in-memory image
    pub fn render(&self) -> Result<Image> {
        // Validate all series data
        self.validate_series()?;

        // Check if DataShader optimization should be used
        let total_points = self.calculate_total_points();

        // Warn for very large datasets
        const LARGE_DATASET_THRESHOLD: usize = 1_000_000;
        if total_points > LARGE_DATASET_THRESHOLD {
            log::warn!(
                "Rendering {} points (>1M). DataShader optimization is available for large datasets.",
                total_points
            );
        }

        let use_datashader = DataShader::should_activate(total_points);

        if use_datashader {
            // Use DataShader for large datasets
            return self.render_with_datashader();
        }

        // Check if parallel processing should be used
        // Note: Parallel rendering not supported for reactive plots (Signal uses Rc which is !Send)
        #[cfg(feature = "parallel")]
        {
            let series_count = self.series_mgr.series.len();
            if !self.is_reactive()
                && self
                    .render
                    .parallel_renderer
                    .should_use_parallel(series_count, total_points)
            {
                return self.render_with_parallel();
            }
        }

        // Create renderer for standard rendering with DPI scaling
        let (scaled_width, scaled_height) = self.config_canvas_size();
        let mut renderer =
            SkiaRenderer::new(scaled_width, scaled_height, self.display.theme.clone())?;
        renderer.set_text_engine_mode(self.display.text_engine);
        let dpi = self.display.config.figure.dpi;
        let dpi_scale = dpi / 100.0;
        renderer.set_dpi_scale(dpi_scale);

        // Calculate or use manual data bounds
        let (mut x_min, mut x_max, mut y_min, mut y_max) =
            if let (Some((x_min_manual, x_max_manual)), Some((y_min_manual, y_max_manual))) =
                (self.layout.x_limits, self.layout.y_limits)
            {
                // Use both manual limits
                (x_min_manual, x_max_manual, y_min_manual, y_max_manual)
            } else if let Some((x_min_manual, x_max_manual)) = self.layout.x_limits {
                // Use manual X limits, calculate Y bounds from data
                let (_, _, y_min_calc, y_max_calc) = self.calculate_data_bounds()?;
                (x_min_manual, x_max_manual, y_min_calc, y_max_calc)
            } else if let Some((y_min_manual, y_max_manual)) = self.layout.y_limits {
                // Use manual Y limits, calculate X bounds from data
                let (x_min_calc, x_max_calc, _, _) = self.calculate_data_bounds()?;
                (x_min_calc, x_max_calc, y_min_manual, y_max_manual)
            } else {
                self.calculate_data_bounds()?
            };

        // Handle edge case where all data is the same
        if (x_max - x_min).abs() < f64::EPSILON {
            x_min -= 1.0;
            x_max += 1.0;
        }
        if (y_max - y_min).abs() < f64::EPSILON {
            y_min -= 1.0;
            y_max += 1.0;
        }

        // Extract bar chart categories if present (for categorical x-axis labels)
        let bar_categories: Option<Vec<String>> = self.series_mgr.series.iter().find_map(|s| {
            if let SeriesType::Bar { categories, .. } = &s.series_type {
                Some(categories.clone())
            } else {
                None
            }
        });

        // Extract violin categories and positions if present (for categorical x-axis labels)
        let violin_data: Vec<(String, f64)> = self
            .series_mgr
            .series
            .iter()
            .filter_map(|s| {
                if let SeriesType::Violin { data } = &s.series_type {
                    data.config
                        .category
                        .clone()
                        .map(|cat| (cat, data.config.x_position))
                } else {
                    None
                }
            })
            .collect();

        let (violin_categories, violin_positions): (Vec<String>, Vec<f64>) =
            violin_data.into_iter().unzip();

        // Check if this is a violin categorical plot (to use special rendering)
        let is_violin_categorical = !violin_categories.is_empty();

        // Use violin categories if bar_categories is not present and violin categories exist
        let bar_categories = bar_categories.or_else(|| {
            if is_violin_categorical {
                Some(violin_categories.clone())
            } else {
                None
            }
        });
        let content = self.create_plot_content(y_min, y_max);
        let measured_dimensions = self.measure_layout_text(&renderer, &content, dpi)?;
        let measurements = measured_dimensions.as_ref();

        // Choose layout method based on MarginConfig
        let (plot_area, layout_opt): (tiny_skia::Rect, Option<PlotLayout>) =
            match &self.display.config.margins {
                MarginConfig::ContentDriven {
                    edge_buffer,
                    center_plot,
                } => {
                    // Use content-driven layout calculator
                    let layout_config = LayoutConfig {
                        edge_buffer_pt: *edge_buffer,
                        center_plot: *center_plot,
                        ..Default::default()
                    };
                    let calculator = LayoutCalculator::new(layout_config);
                    let layout = calculator.compute(
                        (scaled_width, scaled_height),
                        &content,
                        &self.display.config.typography,
                        &self.display.config.spacing,
                        dpi,
                        measurements,
                    );
                    let skia_rect = tiny_skia::Rect::from_ltrb(
                        layout.plot_area.left,
                        layout.plot_area.top,
                        layout.plot_area.right,
                        layout.plot_area.bottom,
                    )
                    .ok_or(PlottingError::InvalidData {
                        message: "Invalid plot area from layout".to_string(),
                        position: None,
                    })?;
                    (skia_rect, Some(layout))
                }
                _ => {
                    // Always use LayoutCalculator for consistent plot area and title/label positioning
                    let layout_config = LayoutConfig::default();
                    let calculator = LayoutCalculator::new(layout_config);
                    let layout = calculator.compute(
                        (scaled_width, scaled_height),
                        &content,
                        &self.display.config.typography,
                        &self.display.config.spacing,
                        dpi,
                        measurements,
                    );
                    let skia_rect = tiny_skia::Rect::from_ltrb(
                        layout.plot_area.left,
                        layout.plot_area.top,
                        layout.plot_area.right,
                        layout.plot_area.bottom,
                    )
                    .ok_or(PlottingError::InvalidData {
                        message: "Invalid plot area from layout".to_string(),
                        position: None,
                    })?;
                    (skia_rect, Some(layout))
                }
            };

        // Generate nice tick values - adapt count to available space
        // Use ~100px per x-tick and ~60px per y-tick as minimum spacing for readability
        let x_tick_count = ((plot_area.width() / 100.0) as usize).clamp(2, 10);
        let y_tick_count = ((plot_area.height() / 60.0) as usize).clamp(2, 8);
        let x_ticks = generate_ticks(x_min, x_max, x_tick_count);
        let y_ticks = generate_ticks(y_min, y_max, y_tick_count);

        // Convert ticks to pixel coordinates
        let x_tick_pixels: Vec<f32> = x_ticks
            .iter()
            .map(|&tick| map_data_to_pixels(tick, 0.0, x_min, x_max, y_min, y_max, plot_area).0)
            .collect();
        let y_tick_pixels: Vec<f32> = y_ticks
            .iter()
            .map(|&tick| map_data_to_pixels(0.0, tick, x_min, x_max, y_min, y_max, plot_area).1)
            .collect();

        // Draw grid if enabled - using unified GridStyle
        // Skip grid for non-Cartesian plots (Pie, Radar, Polar)
        if self.layout.grid_style.visible && self.needs_cartesian_axes() {
            let grid_color = self.layout.grid_style.effective_color();
            let grid_width_px = self.line_width_px(self.layout.grid_style.line_width);
            renderer.draw_grid(
                &x_tick_pixels,
                &y_tick_pixels,
                plot_area,
                grid_color,
                self.layout.grid_style.line_style.clone(),
                grid_width_px,
            )?;
        }

        // Draw axes and labels using computed layout positions
        // Note: layout_opt is always Some since all render paths now compute layout
        let layout = layout_opt.expect("layout should always be computed");
        let tick_size_px = pt_to_px(self.display.config.typography.tick_size(), dpi);

        // Draw tick labels using layout positions
        // Use categorical labels for bar/violin charts, numeric for others
        if is_violin_categorical {
            // Violin plots use their own x-positions for category labels
            renderer.draw_axis_labels_at_categorical_violin(
                &layout.plot_area,
                &violin_categories,
                &violin_positions,
                x_min,
                x_max,
                y_min,
                y_max,
                &y_ticks,
                layout.xtick_baseline_y,
                layout.ytick_right_x,
                tick_size_px,
                self.display.theme.foreground,
                dpi,
            )?;
        } else if let Some(ref categories) = bar_categories {
            renderer.draw_axis_labels_at_categorical(
                &layout.plot_area,
                categories,
                y_min,
                y_max,
                &y_ticks,
                layout.xtick_baseline_y,
                layout.ytick_right_x,
                tick_size_px,
                self.display.theme.foreground,
                dpi,
            )?;
        } else {
            renderer.draw_axis_labels_at(
                &layout.plot_area,
                x_min,
                x_max,
                y_min,
                y_max,
                &x_ticks,
                &y_ticks,
                layout.xtick_baseline_y,
                layout.ytick_right_x,
                tick_size_px,
                self.display.theme.foreground,
                dpi,
            )?;
        }

        // Draw title if present (resolve at t=0 for static render)
        if let Some(ref pos) = layout.title_pos {
            if let Some(ref title) = self.display.title {
                let title_str = title.resolve(0.0);
                renderer.draw_title_at(pos, &title_str, self.display.theme.foreground)?;
            }
        }

        // Draw xlabel if present
        if let Some(ref pos) = layout.xlabel_pos {
            if let Some(ref xlabel) = self.display.xlabel {
                let xlabel_str = xlabel.resolve(0.0);
                renderer.draw_xlabel_at(pos, &xlabel_str, self.display.theme.foreground)?;
            }
        }

        // Draw ylabel if present
        if let Some(ref pos) = layout.ylabel_pos {
            if let Some(ref ylabel) = self.display.ylabel {
                let ylabel_str = ylabel.resolve(0.0);
                renderer.draw_ylabel_at(pos, &ylabel_str, self.display.theme.foreground)?;
            }
        }

        // Render each data series
        for series in &self.series_mgr.series {
            // Get series styling with defaults
            let color = series.color.unwrap_or_else(|| {
                let palette = Color::default_palette();
                palette[self.series_mgr.auto_color_index % palette.len()]
            });
            // Use config data line width, or series override if specified
            let line_width_pt = series
                .line_width
                .unwrap_or(self.display.config.lines.data_width);
            let line_width = self.line_width_px(line_width_pt);
            let line_style = series.line_style.clone().unwrap_or(LineStyle::Solid);
            let marker_style = series.marker_style.unwrap_or(MarkerStyle::Circle);

            match &series.series_type {
                SeriesType::Line { x_data, y_data } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    // Convert data to pixel coordinates
                    let mut points = Vec::new();
                    for i in 0..x_data.len() {
                        let x_val = x_data[i];
                        let y_val = y_data[i];
                        if x_val.is_finite() && y_val.is_finite() {
                            let (px, py) = map_data_to_pixels(
                                x_val, y_val, x_min, x_max, y_min, y_max, plot_area,
                            );
                            points.push((px, py));
                        }
                    }

                    if points.len() >= 2 {
                        renderer.draw_polyline(&points, color, line_width, line_style)?;
                    }

                    // Draw attached error bars if present
                    if series.y_errors.is_some() || series.x_errors.is_some() {
                        let dpi_scale = self.dpi_scale();
                        Self::render_attached_error_bars(
                            &mut renderer,
                            &x_data,
                            &y_data,
                            series.y_errors.as_ref(),
                            series.x_errors.as_ref(),
                            series.error_config.as_ref(),
                            color,
                            x_min,
                            x_max,
                            y_min,
                            y_max,
                            plot_area,
                            line_width,
                            dpi_scale,
                        )?;
                    }
                }
                SeriesType::Scatter { x_data, y_data } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    // Draw individual markers
                    let marker_size_px = self.line_width_px(series.marker_size.unwrap_or(8.0)); // 8pt default marker
                    for i in 0..x_data.len() {
                        let x_val = x_data[i];
                        let y_val = y_data[i];
                        if x_val.is_finite() && y_val.is_finite() {
                            let (px, py) = map_data_to_pixels(
                                x_val, y_val, x_min, x_max, y_min, y_max, plot_area,
                            );
                            renderer.draw_marker(px, py, marker_size_px, marker_style, color)?;
                        }
                    }

                    // Draw attached error bars if present
                    if series.y_errors.is_some() || series.x_errors.is_some() {
                        let dpi_scale = self.dpi_scale();
                        Self::render_attached_error_bars(
                            &mut renderer,
                            &x_data,
                            &y_data,
                            series.y_errors.as_ref(),
                            series.x_errors.as_ref(),
                            series.error_config.as_ref(),
                            color,
                            x_min,
                            x_max,
                            y_min,
                            y_max,
                            plot_area,
                            line_width,
                            dpi_scale,
                        )?;
                    }
                }
                SeriesType::Bar { categories, values } => {
                    let values = values.resolve(0.0);
                    // Bar width as fraction of category spacing (0.8 = 80%, matching matplotlib)
                    let bar_width_fraction = 0.8;
                    let data_range = (x_max - x_min) as f32;
                    let pixels_per_unit = plot_area.width() / data_range;
                    let bar_width = bar_width_fraction * pixels_per_unit;

                    // Draw bars from baseline to data value
                    let baseline =
                        map_data_to_pixels(0.0, 0.0, x_min, x_max, y_min, y_max, plot_area).1;

                    for (i, &value) in values.iter().enumerate() {
                        if value.is_finite() {
                            let x_val = i as f64;
                            let (px, py) = map_data_to_pixels(
                                x_val, value, x_min, x_max, y_min, y_max, plot_area,
                            );
                            let bar_height = (baseline - py).abs();
                            let bar_x = px - bar_width * 0.5;

                            if value >= 0.0 {
                                renderer.draw_rectangle(
                                    bar_x, py, bar_width, bar_height, color, true,
                                )?;
                            } else {
                                renderer.draw_rectangle(
                                    bar_x, baseline, bar_width, bar_height, color, true,
                                )?;
                            }
                        }
                    }
                }
                SeriesType::Histogram { data, config } => {
                    let data = data.resolve(0.0);
                    // Calculate histogram data
                    let hist_data = crate::plots::histogram::calculate_histogram(&data, config)
                        .map_err(|e| {
                            PlottingError::RenderError(format!(
                                "Histogram calculation failed: {}",
                                e
                            ))
                        })?;

                    // Calculate bar width from bin edges
                    let bar_width_data = if hist_data.bin_edges.len() > 1 {
                        hist_data.bin_edges[1] - hist_data.bin_edges[0]
                    } else {
                        1.0
                    };

                    // Convert to pixel width
                    let left_px = map_data_to_pixels(
                        hist_data.bin_edges[0],
                        0.0,
                        x_min,
                        x_max,
                        y_min,
                        y_max,
                        plot_area,
                    )
                    .0;
                    let right_px = map_data_to_pixels(
                        hist_data.bin_edges[0] + bar_width_data,
                        0.0,
                        x_min,
                        x_max,
                        y_min,
                        y_max,
                        plot_area,
                    )
                    .0;
                    let bar_width_px = (right_px - left_px).abs();

                    // Draw histogram bars
                    let baseline =
                        map_data_to_pixels(0.0, 0.0, x_min, x_max, y_min, y_max, plot_area).1;

                    for (i, &count) in hist_data.counts.iter().enumerate() {
                        if count > 0.0 && count.is_finite() {
                            // Use bin center for x position
                            let bin_center =
                                (hist_data.bin_edges[i] + hist_data.bin_edges[i + 1]) / 2.0;
                            let (px, py) = map_data_to_pixels(
                                bin_center, count, x_min, x_max, y_min, y_max, plot_area,
                            );
                            let bar_height = (baseline - py).abs();
                            let bar_x = px - bar_width_px * 0.5;

                            renderer.draw_rectangle(
                                bar_x,
                                py,
                                bar_width_px,
                                bar_height,
                                color,
                                true,
                            )?;
                        }
                    }
                }
                _ => {
                    // For unsupported plot types (error bars), render as scatter points for now
                    // This is a placeholder - full implementation would handle error bars properly
                    match &series.series_type {
                        SeriesType::ErrorBars { x_data, y_data, .. }
                        | SeriesType::ErrorBarsXY { x_data, y_data, .. } => {
                            let x_data = x_data.resolve(0.0);
                            let y_data = y_data.resolve(0.0);
                            let marker_size_px =
                                self.line_width_px(series.marker_size.unwrap_or(8.0));
                            for i in 0..x_data.len() {
                                let x_val = x_data[i];
                                let y_val = y_data[i];
                                if x_val.is_finite() && y_val.is_finite() {
                                    let (px, py) = map_data_to_pixels(
                                        x_val, y_val, x_min, x_max, y_min, y_max, plot_area,
                                    );
                                    renderer.draw_marker(
                                        px,
                                        py,
                                        marker_size_px,
                                        MarkerStyle::Circle,
                                        color,
                                    )?;
                                }
                            }
                        }
                        _ => {} // Already handled above
                    }
                }
            }
        }

        // Convert renderer output to Image
        Ok(renderer.into_image())
    }

    /// Render the plot at a specific animation time.
    ///
    /// This method resolves all reactive data (Signal/Observable) at the given time
    /// before rendering. For static plots, this is equivalent to `render()`.
    ///
    /// # Arguments
    ///
    /// * `time` - The animation time in seconds at which to render
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use ruviz::prelude::*;
    /// use ruviz::animation::signal;
    ///
    /// let y_signal = signal::of(|t| (0..10).map(|i| (i as f64 * 0.1 + t).sin()).collect());
    /// let plot = Plot::new()
    ///     .line(&x, y_signal)
    ///     .title_signal(signal::of(|t| format!("t={:.2}s", t)));
    ///
    /// // Render at different times
    /// let frame_0 = plot.render_at(0.0)?;
    /// let frame_1 = plot.render_at(1.0)?;
    /// ```
    pub fn render_at(&self, _time: f64) -> Result<Image> {
        // For now, delegate to render() for backward compatibility.
        // Future: resolve reactive PlotData and PlotText at the given time.
        //
        // TODO: When series modification is complete:
        // 1. Resolve all PlotData in series at `time`
        // 2. Resolve PlotText (title, xlabel, ylabel) at `time`
        // 3. Build resolved data and call render pipeline
        self.render()
    }

    /// Check if this plot contains any reactive data (Signal or Observable).
    ///
    /// Returns `true` if any series data or text attributes are reactive,
    /// `false` if all data is static.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let static_plot = Plot::new().line(&x, &y);
    /// assert!(!static_plot.is_reactive());
    ///
    /// let reactive_plot = Plot::new().line(&x, y_signal);
    /// assert!(reactive_plot.is_reactive());
    /// ```
    pub fn is_reactive(&self) -> bool {
        self.series_mgr
            .series
            .iter()
            .any(|s| s.series_type.is_reactive())
    }

    /// Render the plot to an external renderer (used for subplots)
    pub fn render_to_renderer(&self, renderer: &mut SkiaRenderer, dpi: f32) -> Result<()> {
        if let Some(err) = self.pending_ingestion_error() {
            return Err(err);
        }
        renderer.set_text_engine_mode(self.display.text_engine);

        // Validate we have at least one series
        if self.series_mgr.series.is_empty() {
            return Err(PlottingError::NoDataSeries);
        }

        // Validate all series data (same validation as render method)
        for (idx, series) in self.series_mgr.series.iter().enumerate() {
            match &series.series_type {
                SeriesType::Line { x_data, y_data } | SeriesType::Scatter { x_data, y_data } => {
                    if x_data.len() != y_data.len() {
                        return Err(PlottingError::DataLengthMismatch {
                            x_len: x_data.len(),
                            y_len: y_data.len(),
                            series_index: Some(idx),
                        });
                    }
                    if x_data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Bar { categories, values } => {
                    if categories.len() != values.len() {
                        return Err(PlottingError::DataLengthMismatch {
                            x_len: categories.len(),
                            y_len: values.len(),
                            series_index: Some(idx),
                        });
                    }
                    if categories.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::ErrorBars {
                    x_data,
                    y_data,
                    y_errors,
                } => {
                    if x_data.len() != y_data.len() || y_data.len() != y_errors.len() {
                        return Err(PlottingError::DataLengthMismatch {
                            x_len: x_data.len(),
                            y_len: y_data.len(),
                            series_index: Some(idx),
                        });
                    }
                }
                SeriesType::ErrorBarsXY {
                    x_data,
                    y_data,
                    x_errors,
                    y_errors,
                } => {
                    if x_data.len() != y_data.len()
                        || x_data.len() != x_errors.len()
                        || x_data.len() != y_errors.len()
                    {
                        return Err(PlottingError::DataLengthMismatch {
                            x_len: x_data.len(),
                            y_len: y_data.len(),
                            series_index: Some(idx),
                        });
                    }
                }
                SeriesType::Histogram { data, .. } => {
                    if data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::BoxPlot { data, .. } => {
                    if data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Heatmap { data } => {
                    if data.values.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Kde { data } => {
                    if data.x.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Ecdf { data } => {
                    if data.x.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Violin { data } => {
                    if data.data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Boxen { data } => {
                    if data.boxes.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Contour { data } => {
                    if data.levels.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Pie { data } => {
                    if data.values.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Radar { data } => {
                    if data.series.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Polar { data } => {
                    if data.points.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
            }
        }

        // Calculate data bounds across all series
        let (x_min, x_max, y_min, y_max) = self.calculate_data_bounds()?;

        // Extract bar chart categories if present (for categorical x-axis labels)
        let bar_categories: Option<Vec<String>> = self.series_mgr.series.iter().find_map(|s| {
            if let SeriesType::Bar { categories, .. } = &s.series_type {
                Some(categories.clone())
            } else {
                None
            }
        });

        // Extract violin categories if present (for categorical x-axis labels)
        let violin_categories: Vec<String> = self
            .series_mgr
            .series
            .iter()
            .filter_map(|s| {
                if let SeriesType::Violin { data } = &s.series_type {
                    data.config.category.clone()
                } else {
                    None
                }
            })
            .collect();

        // Use violin categories if bar_categories is not present and violin categories exist
        let bar_categories = bar_categories.or({
            if !violin_categories.is_empty() {
                Some(violin_categories)
            } else {
                None
            }
        });
        let content = self.create_plot_content(y_min, y_max);
        let measured_dimensions = self.measure_layout_text(renderer, &content, dpi)?;
        let measurements = measured_dimensions.as_ref();

        // Choose layout method based on MarginConfig
        let (plot_area, layout_opt): (tiny_skia::Rect, Option<PlotLayout>) =
            match &self.display.config.margins {
                MarginConfig::ContentDriven {
                    edge_buffer,
                    center_plot,
                } => {
                    // Use content-driven layout calculator
                    let layout_config = LayoutConfig {
                        edge_buffer_pt: *edge_buffer,
                        center_plot: *center_plot,
                        ..Default::default()
                    };
                    let calculator = LayoutCalculator::new(layout_config);
                    let layout = calculator.compute(
                        (renderer.width(), renderer.height()),
                        &content,
                        &self.display.config.typography,
                        &self.display.config.spacing,
                        dpi,
                        measurements,
                    );
                    let skia_rect = tiny_skia::Rect::from_ltrb(
                        layout.plot_area.left,
                        layout.plot_area.top,
                        layout.plot_area.right,
                        layout.plot_area.bottom,
                    )
                    .ok_or(PlottingError::InvalidData {
                        message: "Invalid plot area from layout".to_string(),
                        position: None,
                    })?;
                    (skia_rect, Some(layout))
                }
                _ => {
                    // Always use LayoutCalculator for consistent plot area and title/label positioning
                    let layout_config = LayoutConfig::default();
                    let calculator = LayoutCalculator::new(layout_config);
                    let layout = calculator.compute(
                        (renderer.width(), renderer.height()),
                        &content,
                        &self.display.config.typography,
                        &self.display.config.spacing,
                        dpi,
                        measurements,
                    );
                    let skia_rect = tiny_skia::Rect::from_ltrb(
                        layout.plot_area.left,
                        layout.plot_area.top,
                        layout.plot_area.right,
                        layout.plot_area.bottom,
                    )
                    .ok_or(PlottingError::InvalidData {
                        message: "Invalid plot area from layout".to_string(),
                        position: None,
                    })?;
                    (skia_rect, Some(layout))
                }
            };

        // Generate nice tick values - adapt count to available space
        // Use ~100px per x-tick and ~60px per y-tick as minimum spacing for readability
        let x_tick_count = ((plot_area.width() / 100.0) as usize).clamp(2, 10);
        let y_tick_count = ((plot_area.height() / 60.0) as usize).clamp(2, 8);
        let x_ticks = generate_ticks(x_min, x_max, x_tick_count);
        let y_ticks = generate_ticks(y_min, y_max, y_tick_count);

        // Convert ticks to pixel coordinates
        let x_tick_pixels: Vec<f32> = x_ticks
            .iter()
            .map(|&tick| map_data_to_pixels(tick, 0.0, x_min, x_max, y_min, y_max, plot_area).0)
            .collect();
        let y_tick_pixels: Vec<f32> = y_ticks
            .iter()
            .map(|&tick| map_data_to_pixels(0.0, tick, x_min, x_max, y_min, y_max, plot_area).1)
            .collect();

        // Draw grid if enabled - using unified GridStyle
        // Skip grid for non-Cartesian plots (Pie, Radar, Polar)
        if self.layout.grid_style.visible && self.needs_cartesian_axes() {
            let grid_color = self.layout.grid_style.effective_color();
            let grid_width_px = pt_to_px(self.layout.grid_style.line_width, dpi);
            renderer.draw_grid(
                &x_tick_pixels,
                &y_tick_pixels,
                plot_area,
                grid_color,
                self.layout.grid_style.line_style.clone(),
                grid_width_px,
            )?;
        }

        // Draw axes and labels using computed layout positions
        // Note: layout_opt is always Some since all render paths now compute layout
        let layout = layout_opt.expect("layout should always be computed");
        let tick_size_px = pt_to_px(self.display.config.typography.tick_size(), dpi);

        // Draw tick labels using layout positions
        // Use categorical labels for bar charts, numeric for others
        if let Some(ref categories) = bar_categories {
            renderer.draw_axis_labels_at_categorical(
                &layout.plot_area,
                categories,
                y_min,
                y_max,
                &y_ticks,
                layout.xtick_baseline_y,
                layout.ytick_right_x,
                tick_size_px,
                self.display.theme.foreground,
                dpi,
            )?;
        } else {
            renderer.draw_axis_labels_at(
                &layout.plot_area,
                x_min,
                x_max,
                y_min,
                y_max,
                &x_ticks,
                &y_ticks,
                layout.xtick_baseline_y,
                layout.ytick_right_x,
                tick_size_px,
                self.display.theme.foreground,
                dpi,
            )?;
        }

        // Draw title if present (resolve at t=0 for static render)
        if let Some(ref pos) = layout.title_pos {
            if let Some(ref title) = self.display.title {
                let title_str = title.resolve(0.0);
                renderer.draw_title_at(pos, &title_str, self.display.theme.foreground)?;
            }
        }

        // Draw xlabel if present
        if let Some(ref pos) = layout.xlabel_pos {
            if let Some(ref xlabel) = self.display.xlabel {
                let xlabel_str = xlabel.resolve(0.0);
                renderer.draw_xlabel_at(pos, &xlabel_str, self.display.theme.foreground)?;
            }
        }

        // Draw ylabel if present
        if let Some(ref pos) = layout.ylabel_pos {
            if let Some(ref ylabel) = self.display.ylabel {
                let ylabel_str = ylabel.resolve(0.0);
                renderer.draw_ylabel_at(pos, &ylabel_str, self.display.theme.foreground)?;
            }
        }

        // Render each data series
        for (color_index, series) in self.series_mgr.series.iter().enumerate() {
            // Get series styling with defaults
            let color = series.color.unwrap_or_else(|| {
                let palette = Color::default_palette();
                palette[color_index % palette.len()]
            });
            // Use config data line width, or series override if specified
            let line_width_pt = series
                .line_width
                .unwrap_or(self.display.config.lines.data_width);
            let line_width = pt_to_px(line_width_pt, dpi);
            let line_style = series.line_style.clone().unwrap_or(LineStyle::Solid);
            let marker_style = series.marker_style.unwrap_or(MarkerStyle::Circle);

            match &series.series_type {
                SeriesType::Line { x_data, y_data } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    // Convert data to pixel coordinates
                    let mut points = Vec::new();
                    for i in 0..x_data.len() {
                        let x_val = x_data[i];
                        let y_val = y_data[i];
                        if x_val.is_finite() && y_val.is_finite() {
                            let (px, py) = map_data_to_pixels(
                                x_val, y_val, x_min, x_max, y_min, y_max, plot_area,
                            );
                            points.push((px, py));
                        }
                    }

                    if points.len() >= 2 {
                        renderer.draw_polyline(&points, color, line_width, line_style)?;
                    }

                    // Draw attached error bars if present
                    if series.y_errors.is_some() || series.x_errors.is_some() {
                        let dpi_scale = self.dpi_scale();
                        Self::render_attached_error_bars(
                            renderer,
                            &x_data,
                            &y_data,
                            series.y_errors.as_ref(),
                            series.x_errors.as_ref(),
                            series.error_config.as_ref(),
                            color,
                            x_min,
                            x_max,
                            y_min,
                            y_max,
                            plot_area,
                            line_width,
                            dpi_scale,
                        )?;
                    }
                }
                SeriesType::Scatter { x_data, y_data } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    // Draw individual markers
                    let marker_size_px = pt_to_px(series.marker_size.unwrap_or(8.0), dpi);
                    for i in 0..x_data.len() {
                        let x_val = x_data[i];
                        let y_val = y_data[i];
                        if x_val.is_finite() && y_val.is_finite() {
                            let (px, py) = map_data_to_pixels(
                                x_val, y_val, x_min, x_max, y_min, y_max, plot_area,
                            );
                            renderer.draw_marker(px, py, marker_size_px, marker_style, color)?;
                        }
                    }

                    // Draw attached error bars if present
                    if series.y_errors.is_some() || series.x_errors.is_some() {
                        let dpi_scale = dpi / 100.0;
                        Self::render_attached_error_bars(
                            renderer,
                            &x_data,
                            &y_data,
                            series.y_errors.as_ref(),
                            series.x_errors.as_ref(),
                            series.error_config.as_ref(),
                            color,
                            x_min,
                            x_max,
                            y_min,
                            y_max,
                            plot_area,
                            line_width,
                            dpi_scale,
                        )?;
                    }
                }
                SeriesType::Bar { categories, values } => {
                    let values = values.resolve(0.0);
                    // Bar width as fraction of category spacing (0.8 = 80%, matching matplotlib)
                    let bar_width_fraction = 0.8;
                    let data_range = (x_max - x_min) as f32;
                    let pixels_per_unit = plot_area.width() / data_range;
                    let bar_width = bar_width_fraction * pixels_per_unit;

                    // Draw bars from baseline to data value
                    let baseline =
                        map_data_to_pixels(0.0, 0.0, x_min, x_max, y_min, y_max, plot_area).1;

                    for (i, &value) in values.iter().enumerate() {
                        if value.is_finite() {
                            let x_val = i as f64;
                            let (px, py) = map_data_to_pixels(
                                x_val, value, x_min, x_max, y_min, y_max, plot_area,
                            );
                            let bar_height = (baseline - py).abs();
                            let bar_x = px - bar_width * 0.5;

                            if value >= 0.0 {
                                renderer.draw_rectangle(
                                    bar_x, py, bar_width, bar_height, color, true,
                                )?;
                            } else {
                                renderer.draw_rectangle(
                                    bar_x, baseline, bar_width, bar_height, color, true,
                                )?;
                            }
                        }
                    }
                }
                _ => {
                    // For unsupported plot types (error bars), render as scatter points
                    match &series.series_type {
                        SeriesType::ErrorBars { x_data, y_data, .. }
                        | SeriesType::ErrorBarsXY { x_data, y_data, .. } => {
                            let x_data = x_data.resolve(0.0);
                            let y_data = y_data.resolve(0.0);
                            let marker_size_px = pt_to_px(series.marker_size.unwrap_or(8.0), dpi);
                            for i in 0..x_data.len() {
                                let x_val = x_data[i];
                                let y_val = y_data[i];
                                if x_val.is_finite() && y_val.is_finite() {
                                    let (px, py) = map_data_to_pixels(
                                        x_val, y_val, x_min, x_max, y_min, y_max, plot_area,
                                    );
                                    renderer.draw_marker(
                                        px,
                                        py,
                                        marker_size_px,
                                        MarkerStyle::Circle,
                                        color,
                                    )?;
                                }
                            }
                        }
                        _ => {} // Already handled above
                    }
                }
            }
        }

        // Draw annotations after data series but before legend
        if !self.annotations.is_empty() {
            renderer.draw_annotations(
                &self.annotations,
                plot_area,
                x_min,
                x_max,
                y_min,
                y_max,
                dpi,
            )?;
        }

        // Collect legend items, including grouped-series collapse behavior.
        let legend_items = self.collect_legend_items();

        // Draw legend if there are labeled series and legend is enabled
        if !legend_items.is_empty() && self.layout.legend.enabled {
            let mut legend = self.layout.legend.to_legend();
            // Scale legend font size from points to pixels for proper DPI handling
            legend.font_size = pt_to_px(legend.font_size, dpi);

            // Collect data bounding boxes for best position algorithm
            let data_bboxes: Vec<(f32, f32, f32, f32)> =
                if matches!(legend.position, LegendPosition::Best) {
                    let marker_radius = 4.0_f32;
                    self.series_mgr
                        .series
                        .iter()
                        .flat_map(|series| match &series.series_type {
                            SeriesType::Line { x_data, y_data }
                            | SeriesType::Scatter { x_data, y_data } => {
                                let x_data = x_data.resolve(0.0);
                                let y_data = y_data.resolve(0.0);
                                x_data
                                    .iter()
                                    .zip(y_data.iter())
                                    .filter_map(|(&x, &y)| {
                                        if x.is_finite() && y.is_finite() {
                                            let (px, py) = map_data_to_pixels(
                                                x, y, x_min, x_max, y_min, y_max, plot_area,
                                            );
                                            Some((
                                                px - marker_radius,
                                                py - marker_radius,
                                                px + marker_radius,
                                                py + marker_radius,
                                            ))
                                        } else {
                                            None
                                        }
                                    })
                                    .collect::<Vec<_>>()
                            }
                            _ => vec![],
                        })
                        .collect()
                } else {
                    vec![]
                };

            let bbox_slice: Option<&[(f32, f32, f32, f32)]> = if data_bboxes.is_empty() {
                None
            } else {
                Some(&data_bboxes)
            };

            renderer.draw_legend_full(&legend_items, &legend, plot_area, bbox_slice)?;
        }

        Ok(())
    }

    /// Calculate total number of data points across all series
    fn calculate_total_points(&self) -> usize {
        self.series_mgr
            .series
            .iter()
            .map(|series| match &series.series_type {
                SeriesType::Line { x_data, .. }
                | SeriesType::Scatter { x_data, .. }
                | SeriesType::ErrorBars { x_data, .. }
                | SeriesType::ErrorBarsXY { x_data, .. } => x_data.len(),
                SeriesType::Bar { categories, .. } => categories.len(),
                SeriesType::Histogram { data, .. } => data.len(),
                SeriesType::BoxPlot { data, .. } => data.len(),
                SeriesType::Heatmap { data } => data.n_rows * data.n_cols,
                SeriesType::Kde { data } => data.x.len(),
                SeriesType::Ecdf { data } => data.x.len(),
                SeriesType::Violin { data } => data.data.len(),
                SeriesType::Boxen { data } => data.boxes.len() * 4, // Each box has 4 points
                SeriesType::Contour { data } => data.x.len() * data.y.len(),
                SeriesType::Pie { data } => data.values.len(),
                SeriesType::Radar { data } => data.series.iter().map(|s| s.values.len()).sum(),
                SeriesType::Polar { data } => data.points.len(),
            })
            .sum()
    }

    /// Check if the plot needs standard Cartesian axes
    ///
    /// Some plot types (Pie, Radar, Polar) have their own coordinate system
    /// and don't use standard X/Y axes with tick labels.
    fn needs_cartesian_axes(&self) -> bool {
        // If any series is a non-Cartesian type, skip standard axes
        for series in &self.series_mgr.series {
            match &series.series_type {
                SeriesType::Pie { .. } | SeriesType::Radar { .. } | SeriesType::Polar { .. } => {
                    return false;
                }
                _ => {}
            }
        }
        true
    }

    /// Helper to render attached error bars on Line/Scatter series
    #[allow(clippy::too_many_arguments)]
    fn render_attached_error_bars(
        renderer: &mut SkiaRenderer,
        x_data: &[f64],
        y_data: &[f64],
        y_errors: Option<&ErrorValues>,
        x_errors: Option<&ErrorValues>,
        error_config: Option<&ErrorBarConfig>,
        series_color: Color,
        x_min: f64,
        x_max: f64,
        y_min: f64,
        y_max: f64,
        plot_area: tiny_skia::Rect,
        default_line_width: f32,
        dpi_scale: f32,
    ) -> Result<()> {
        let config = error_config.cloned().unwrap_or_default();
        let bar_color = config
            .color
            .unwrap_or(series_color)
            .with_alpha(config.alpha);
        // Scale error bar line width by DPI (config.line_width is in pt at 100 DPI base)
        let scaled_config_width = config.line_width * dpi_scale;
        let line_width = scaled_config_width.max(default_line_width * 0.75); // Slightly thinner than data line

        // Scale cap size by DPI (config.cap_size is in pt at 100 DPI base)
        let cap_size_px = config.cap_size * dpi_scale;
        let half_cap = cap_size_px / 2.0;

        let n = x_data.len().min(y_data.len());

        for i in 0..n {
            let x_val = x_data[i];
            let y_val = y_data[i];

            if !x_val.is_finite() || !y_val.is_finite() {
                continue;
            }

            // Get Y error bounds (skip NaN/Infinity values)
            let (y_lower, y_upper) = if let Some(yerr) = y_errors {
                if let Some((lo, hi)) = yerr.bounds_at(i) {
                    let lo_abs = lo.abs();
                    let hi_abs = hi.abs();
                    if lo_abs.is_finite() && hi_abs.is_finite() {
                        (lo_abs, hi_abs)
                    } else {
                        (0.0, 0.0) // Skip invalid error values
                    }
                } else {
                    (0.0, 0.0)
                }
            } else {
                (0.0, 0.0)
            };

            // Get X error bounds (skip NaN/Infinity values)
            let (x_lower, x_upper) = if let Some(xerr) = x_errors {
                if let Some((lo, hi)) = xerr.bounds_at(i) {
                    let lo_abs = lo.abs();
                    let hi_abs = hi.abs();
                    if lo_abs.is_finite() && hi_abs.is_finite() {
                        (lo_abs, hi_abs)
                    } else {
                        (0.0, 0.0) // Skip invalid error values
                    }
                } else {
                    (0.0, 0.0)
                }
            } else {
                (0.0, 0.0)
            };

            // Draw Y error bar (vertical line + caps) with clipping
            if y_lower > 0.0 || y_upper > 0.0 {
                let (px, py_top_raw) = map_data_to_pixels(
                    x_val,
                    y_val + y_upper,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                );
                let (_, py_bottom_raw) = map_data_to_pixels(
                    x_val,
                    y_val - y_lower,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                );

                // Clip to plot area bounds
                let plot_top = plot_area.y();
                let plot_bottom = plot_area.y() + plot_area.height();
                let plot_left = plot_area.x();
                let plot_right = plot_area.x() + plot_area.width();

                let py_top = py_top_raw.max(plot_top).min(plot_bottom);
                let py_bottom = py_bottom_raw.max(plot_top).min(plot_bottom);

                // Only draw if there's visible area
                if (py_bottom - py_top).abs() > 0.5 {
                    // Vertical error line (clipped)
                    renderer.draw_line(
                        px,
                        py_top,
                        px,
                        py_bottom,
                        bar_color,
                        line_width,
                        LineStyle::Solid,
                    )?;

                    // Draw caps (horizontal lines) - use pixel offsets directly
                    let cap_left_x = (px - half_cap).max(plot_left);
                    let cap_right_x = (px + half_cap).min(plot_right);

                    // Top cap (only if within plot area)
                    if py_top_raw >= plot_top && py_top_raw <= plot_bottom {
                        renderer.draw_line(
                            cap_left_x,
                            py_top,
                            cap_right_x,
                            py_top,
                            bar_color,
                            line_width,
                            LineStyle::Solid,
                        )?;
                    }
                    // Bottom cap (only if within plot area)
                    if py_bottom_raw >= plot_top && py_bottom_raw <= plot_bottom {
                        renderer.draw_line(
                            cap_left_x,
                            py_bottom,
                            cap_right_x,
                            py_bottom,
                            bar_color,
                            line_width,
                            LineStyle::Solid,
                        )?;
                    }
                }
            }

            // Draw X error bar (horizontal line + caps) with clipping
            if x_lower > 0.0 || x_upper > 0.0 {
                let (_, py) =
                    map_data_to_pixels(x_val, y_val, x_min, x_max, y_min, y_max, plot_area);
                let (px_left_raw, _) = map_data_to_pixels(
                    x_val - x_lower,
                    y_val,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                );
                let (px_right_raw, _) = map_data_to_pixels(
                    x_val + x_upper,
                    y_val,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    plot_area,
                );

                // Clip to plot area bounds
                let plot_top = plot_area.y();
                let plot_bottom = plot_area.y() + plot_area.height();
                let plot_left = plot_area.x();
                let plot_right = plot_area.x() + plot_area.width();

                let px_left = px_left_raw.max(plot_left).min(plot_right);
                let px_right = px_right_raw.max(plot_left).min(plot_right);

                // Only draw if there's visible area
                if (px_right - px_left).abs() > 0.5 {
                    // Horizontal error line (clipped)
                    renderer.draw_line(
                        px_left,
                        py,
                        px_right,
                        py,
                        bar_color,
                        line_width,
                        LineStyle::Solid,
                    )?;

                    // Draw caps (vertical lines) - use pixel offsets directly
                    let cap_top_y = (py - half_cap).max(plot_top);
                    let cap_bottom_y = (py + half_cap).min(plot_bottom);

                    // Left cap (only if within plot area)
                    if px_left_raw >= plot_left && px_left_raw <= plot_right {
                        renderer.draw_line(
                            px_left,
                            cap_top_y,
                            px_left,
                            cap_bottom_y,
                            bar_color,
                            line_width,
                            LineStyle::Solid,
                        )?;
                    }
                    // Right cap (only if within plot area)
                    if px_right_raw >= plot_left && px_right_raw <= plot_right {
                        renderer.draw_line(
                            px_right,
                            cap_top_y,
                            px_right,
                            cap_bottom_y,
                            bar_color,
                            line_width,
                            LineStyle::Solid,
                        )?;
                    }
                }
            }
        }

        Ok(())
    }

    /// Create PlotContent for layout calculation
    fn create_plot_content(&self, y_min: f64, y_max: f64) -> PlotContent {
        // Estimate max characters in y-tick labels
        let y_ticks = generate_ticks(y_min, y_max, 6);
        let max_ytick_chars = y_ticks
            .iter()
            .map(|&v| {
                if v.abs() < 0.001 {
                    1 // "0"
                } else if v.abs() > 1000.0 {
                    format!("{:.0e}", v).len()
                } else {
                    format!("{:.1}", v).len()
                }
            })
            .max()
            .unwrap_or(5);

        PlotContent {
            title: self.display.title.as_ref().map(|t| t.resolve(0.0)),
            xlabel: self.display.xlabel.as_ref().map(|t| t.resolve(0.0)),
            ylabel: self.display.ylabel.as_ref().map(|t| t.resolve(0.0)),
            max_ytick_chars,
            max_xtick_chars: 5, // Reasonable default
        }
    }

    /// Pre-measure title/xlabel/ylabel for Typst layout parity.
    fn measure_layout_text(
        &self,
        renderer: &SkiaRenderer,
        content: &PlotContent,
        dpi: f32,
    ) -> Result<Option<MeasuredDimensions>> {
        let title_size_px = pt_to_px(self.display.config.typography.title_size(), dpi);
        let label_size_px = pt_to_px(self.display.config.typography.label_size(), dpi);

        let mut measurements = MeasuredDimensions::default();

        if let Some(title) = content.title.as_deref() {
            measurements.title = Some(renderer.measure_text(title, title_size_px)?);
        }
        if let Some(xlabel) = content.xlabel.as_deref() {
            measurements.xlabel = Some(renderer.measure_text(xlabel, label_size_px)?);
        }
        if let Some(ylabel) = content.ylabel.as_deref() {
            measurements.ylabel = Some(renderer.measure_text(ylabel, label_size_px)?);
        }

        Ok(Some(measurements))
    }

    /// Render plot using DataShader optimization for large datasets
    fn render_with_datashader(&self) -> Result<Image> {
        // Calculate combined data bounds across all series
        let mut all_points = Vec::new();

        // Collect all points from all series
        for series in &self.series_mgr.series {
            match &series.series_type {
                SeriesType::Line { x_data, y_data } | SeriesType::Scatter { x_data, y_data } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    for i in 0..x_data.len() {
                        let x = x_data[i];
                        let y = y_data[i];
                        if x.is_finite() && y.is_finite() {
                            all_points.push(crate::core::types::Point2f::new(x as f32, y as f32));
                        }
                    }
                }
                SeriesType::ErrorBars { x_data, y_data, .. }
                | SeriesType::ErrorBarsXY { x_data, y_data, .. } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    for i in 0..x_data.len() {
                        let x = x_data[i];
                        let y = y_data[i];
                        if x.is_finite() && y.is_finite() {
                            all_points.push(crate::core::types::Point2f::new(x as f32, y as f32));
                        }
                    }
                }
                SeriesType::Bar { values, .. } => {
                    let values = values.resolve(0.0);
                    // For bar charts, convert category indices to points
                    for (i, &value) in values.iter().enumerate() {
                        if value.is_finite() {
                            all_points
                                .push(crate::core::types::Point2f::new(i as f32, value as f32));
                        }
                    }
                }
                SeriesType::Heatmap { data } => {
                    // Heatmap has its own grid, convert to points
                    for (row, row_values) in data.values.iter().enumerate() {
                        for (col, &value) in row_values.iter().enumerate() {
                            if value.is_finite() {
                                all_points
                                    .push(crate::core::types::Point2f::new(col as f32, row as f32));
                            }
                        }
                    }
                }
                SeriesType::Histogram { data, config } => {
                    let data = data.resolve(0.0);
                    // Calculate histogram and add bin center points
                    if let Ok(hist_data) =
                        crate::plots::histogram::calculate_histogram(&data, config)
                    {
                        for (i, &count) in hist_data.counts.iter().enumerate() {
                            if count > 0.0 {
                                let x_center =
                                    (hist_data.bin_edges[i] + hist_data.bin_edges[i + 1]) / 2.0;
                                all_points.push(crate::core::types::Point2f::new(
                                    x_center as f32,
                                    count as f32,
                                ));
                            }
                        }
                    }
                }
                SeriesType::BoxPlot { data, .. } => {
                    if data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Kde { data } => {
                    // Add KDE points
                    for i in 0..data.x.len() {
                        let x = data.x[i];
                        let y = data.y[i];
                        if x.is_finite() && y.is_finite() {
                            all_points.push(crate::core::types::Point2f::new(x as f32, y as f32));
                        }
                    }
                }
                SeriesType::Ecdf { data } => {
                    // Add ECDF points
                    for i in 0..data.x.len() {
                        let x = data.x[i];
                        let y = data.y[i];
                        if x.is_finite() && y.is_finite() {
                            all_points.push(crate::core::types::Point2f::new(x as f32, y as f32));
                        }
                    }
                }
                SeriesType::Violin { data } => {
                    // Add violin KDE points
                    for i in 0..data.kde.x.len() {
                        let x = 0.5; // Centered position
                        let y = data.kde.x[i];
                        if y.is_finite() {
                            all_points.push(crate::core::types::Point2f::new(x as f32, y as f32));
                        }
                    }
                }
                SeriesType::Boxen { data } => {
                    // Add boxen box corner points
                    for boxen_box in &data.boxes {
                        let rect = crate::plots::distribution::boxen_rect(
                            boxen_box,
                            0.5,
                            data.config.orient,
                        );
                        for (x, y) in rect {
                            if x.is_finite() && y.is_finite() {
                                all_points
                                    .push(crate::core::types::Point2f::new(x as f32, y as f32));
                            }
                        }
                    }
                }
                SeriesType::Contour { data } => {
                    // Add contour line segment endpoints
                    for level in &data.lines {
                        for &(x1, y1, x2, y2) in &level.segments {
                            if x1.is_finite() && y1.is_finite() {
                                all_points
                                    .push(crate::core::types::Point2f::new(x1 as f32, y1 as f32));
                            }
                            if x2.is_finite() && y2.is_finite() {
                                all_points
                                    .push(crate::core::types::Point2f::new(x2 as f32, y2 as f32));
                            }
                        }
                    }
                }
                SeriesType::Pie { .. } => {
                    // Pie charts don't use point-based datashader, use normalized coords
                    all_points.push(crate::core::types::Point2f::new(0.5, 0.5));
                }
                SeriesType::Radar { data } => {
                    // Add radar series points (already in cartesian coordinates from polygon)
                    for series_data in &data.series {
                        for &(x, y) in &series_data.polygon {
                            all_points.push(crate::core::types::Point2f::new(x as f32, y as f32));
                        }
                    }
                }
                SeriesType::Polar { data } => {
                    // Add polar plot points (already in cartesian coordinates)
                    for point in &data.points {
                        all_points.push(crate::core::types::Point2f::new(
                            point.x as f32,
                            point.y as f32,
                        ));
                    }
                }
            }
        }

        if all_points.is_empty() {
            return Err(PlottingError::EmptyDataSet);
        }

        // Simple DataShader implementation - create basic aggregated image
        let mut datashader = DataShader::with_canvas_size(
            self.display.dimensions.0 as usize,
            self.display.dimensions.1 as usize,
        );

        // Convert points to (f64, f64) format for aggregation
        let points_f64: Vec<(f64, f64)> = all_points
            .iter()
            .map(|p| (p.x as f64, p.y as f64))
            .collect();

        // Aggregate points (this will auto-set bounds)
        let x_data: Vec<f64> = points_f64.iter().map(|p| p.0).collect();
        let y_data: Vec<f64> = points_f64.iter().map(|p| p.1).collect();

        datashader.aggregate(&x_data, &y_data)?;
        let ds_image = datashader.render();

        // Convert to Image format
        let image = Image {
            width: ds_image.width as u32,
            height: ds_image.height as u32,
            pixels: ds_image.pixels,
        };

        Ok(image)
    }

    /// Render plot using parallel processing for multiple series
    #[cfg(feature = "parallel")]
    fn render_with_parallel(&self) -> Result<Image> {
        use crate::render::parallel::{DataBounds, PlotArea, RenderSeriesType};

        // Start timing for performance measurement
        let start_time = std::time::Instant::now();

        // Create renderer with DPI scaling
        let (scaled_width, scaled_height) = self.dpi_scaled_dimensions();
        let mut renderer =
            SkiaRenderer::new(scaled_width, scaled_height, self.display.theme.clone())?;
        renderer.set_text_engine_mode(self.display.text_engine);
        let dpi = self.display.dpi as f32;
        let dpi_scale = dpi / 100.0;
        renderer.set_dpi_scale(dpi_scale);

        // Calculate data bounds across all series (sequential - small operation)
        let bounds = self.calculate_data_bounds()?;

        // Compute content-driven layout FIRST for consistent positioning
        let content = self.create_plot_content(bounds.2, bounds.3);
        let layout_config = LayoutConfig {
            edge_buffer_pt: 8.0,
            center_plot: true,
            max_margin_fraction: 0.4,
        };
        let layout_calculator = LayoutCalculator::new(layout_config);
        let measured_dimensions = self.measure_layout_text(&renderer, &content, dpi)?;
        let layout = layout_calculator.compute(
            (scaled_width, scaled_height),
            &content,
            &self.display.config.typography,
            &self.display.config.spacing,
            dpi,
            measured_dimensions.as_ref(),
        );

        // Convert layout plot_area to tiny_skia::Rect for series rendering
        let plot_area = tiny_skia::Rect::from_ltrb(
            layout.plot_area.left,
            layout.plot_area.top,
            layout.plot_area.right,
            layout.plot_area.bottom,
        )
        .unwrap_or_else(|| {
            // Fallback to simple calculation if layout rect is invalid
            calculate_plot_area_dpi(scaled_width, scaled_height, self.dpi_scale())
        });

        // Convert to parallel renderer format
        let parallel_plot_area = PlotArea {
            left: plot_area.left(),
            right: plot_area.right(),
            top: plot_area.top(),
            bottom: plot_area.bottom(),
        };
        let data_bounds = DataBounds {
            x_min: bounds.0,
            x_max: bounds.1,
            y_min: bounds.2,
            y_max: bounds.3,
        };

        // Generate nice tick values - adapt count to available space
        // Use ~100px per x-tick and ~60px per y-tick as minimum spacing for readability
        let x_tick_count = ((plot_area.width() / 100.0) as usize).clamp(2, 10);
        let y_tick_count = ((plot_area.height() / 60.0) as usize).clamp(2, 8);
        let x_ticks = generate_ticks(bounds.0, bounds.1, x_tick_count);
        let y_ticks = generate_ticks(bounds.2, bounds.3, y_tick_count);

        // Convert ticks to pixel coordinates
        let x_tick_pixels: Vec<f32> = x_ticks
            .iter()
            .map(|&tick| {
                map_data_to_pixels(tick, 0.0, bounds.0, bounds.1, bounds.2, bounds.3, plot_area).0
            })
            .collect();
        let y_tick_pixels: Vec<f32> = y_ticks
            .iter()
            .map(|&tick| {
                map_data_to_pixels(0.0, tick, bounds.0, bounds.1, bounds.2, bounds.3, plot_area).1
            })
            .collect();

        // Draw grid if enabled - using unified GridStyle (sequential - UI elements)
        // Skip grid for non-Cartesian plots (Pie, Radar, Polar)
        if self.layout.grid_style.visible && self.needs_cartesian_axes() {
            let grid_color = self.layout.grid_style.effective_color();
            renderer.draw_grid(
                &x_tick_pixels,
                &y_tick_pixels,
                plot_area,
                grid_color,
                self.layout.grid_style.line_style.clone(),
                self.dpi_scaled_line_width(self.layout.grid_style.line_width),
            )?;
        }

        // Draw axes (sequential - UI elements) - only for Cartesian plots
        if self.needs_cartesian_axes() {
            renderer.draw_axes(
                plot_area,
                &x_tick_pixels,
                &y_tick_pixels,
                self.display.theme.foreground,
            )?;
        }

        // Process all series in parallel
        let processed_series = self.render.parallel_renderer.process_series_parallel(
            &self.series_mgr.series,
            |series, index| -> Result<SeriesRenderData> {
                // Get series styling with defaults
                let color = series
                    .color
                    .unwrap_or_else(|| self.display.theme.get_color(index));
                let line_width = self.dpi_scaled_line_width(
                    series.line_width.unwrap_or(self.display.theme.line_width),
                );
                let alpha = series.alpha.unwrap_or(1.0);

                // Process each series type
                let render_series_type = match &series.series_type {
                    SeriesType::Line { x_data, y_data } => {
                        let x_data = x_data.resolve(0.0);
                        let y_data = y_data.resolve(0.0);
                        // Transform coordinates in parallel
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &x_data,
                                &y_data,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        // Process line segments in parallel
                        let segments = self.render.parallel_renderer.process_polyline_parallel(
                            &points,
                            series.line_style.clone().unwrap_or(LineStyle::Solid),
                            color,
                            line_width,
                        )?;

                        RenderSeriesType::Line { segments }
                    }
                    SeriesType::Scatter { x_data, y_data } => {
                        let x_data = x_data.resolve(0.0);
                        let y_data = y_data.resolve(0.0);
                        // Transform coordinates in parallel
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &x_data,
                                &y_data,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        // Process markers in parallel
                        let markers = self.render.parallel_renderer.process_markers_parallel(
                            &points,
                            series.marker_style.unwrap_or(MarkerStyle::Circle),
                            color,
                            8.0, // Default marker size
                        )?;

                        RenderSeriesType::Scatter { markers }
                    }
                    SeriesType::Bar { categories, values } => {
                        let values = values.resolve(0.0);
                        // Convert categories to x-coordinates
                        let x_data: Vec<f64> = (0..categories.len()).map(|i| i as f64).collect();

                        // Transform coordinates
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &x_data,
                                &values,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        // Bar width as fraction of category spacing (0.8 = 80%, matching matplotlib)
                        let bar_width_fraction = 0.8;
                        let data_range = (bounds.1 - bounds.0) as f32;
                        let pixels_per_unit = parallel_plot_area.width() / data_range;
                        let bar_width = bar_width_fraction * pixels_per_unit;

                        let baseline_y = map_data_to_pixels(
                            0.0, 0.0, bounds.0, bounds.1, bounds.2, bounds.3, plot_area,
                        )
                        .1;

                        let bars = points
                            .iter()
                            .enumerate()
                            .map(|(i, point)| {
                                let height = (baseline_y - point.y).abs();
                                crate::render::parallel::BarInstance {
                                    x: point.x - bar_width * 0.5,
                                    y: if values[i] >= 0.0 {
                                        point.y
                                    } else {
                                        baseline_y
                                    },
                                    width: bar_width,
                                    height,
                                    color,
                                }
                            })
                            .collect();

                        RenderSeriesType::Bar { bars }
                    }
                    SeriesType::ErrorBars { x_data, y_data, .. }
                    | SeriesType::ErrorBarsXY { x_data, y_data, .. } => {
                        let x_data = x_data.resolve(0.0);
                        let y_data = y_data.resolve(0.0);
                        // For now, render error bars as scatter points
                        // Full error bar implementation would be added here
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &x_data,
                                &y_data,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        let markers = self.render.parallel_renderer.process_markers_parallel(
                            &points,
                            MarkerStyle::Circle,
                            color,
                            6.0,
                        )?;

                        RenderSeriesType::Scatter { markers }
                    }
                    SeriesType::Histogram { data, config } => {
                        let data = data.resolve(0.0);
                        // Calculate histogram data
                        let hist_data = crate::plots::histogram::calculate_histogram(&data, config)
                            .map_err(|e| {
                                PlottingError::RenderError(format!(
                                    "Histogram calculation failed: {}",
                                    e
                                ))
                            })?;

                        // Convert histogram to bar format for parallel rendering
                        let x_data: Vec<f64> = hist_data
                            .bin_edges
                            .windows(2)
                            .map(|w| (w[0] + w[1]) / 2.0) // bin centers
                            .collect();

                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &x_data,
                                &hist_data.counts,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        // Create bar instances for histogram
                        let baseline_y = map_data_to_pixels(
                            0.0, 0.0, bounds.0, bounds.1, bounds.2, bounds.3, plot_area,
                        )
                        .1;

                        let bars = points
                            .iter()
                            .enumerate()
                            .map(|(i, point)| {
                                let bar_width =
                                    (hist_data.bin_edges[i + 1] - hist_data.bin_edges[i]) as f32;
                                let height = (baseline_y - point.y).abs();
                                crate::render::parallel::BarInstance {
                                    x: point.x - bar_width * 0.5,
                                    y: point.y,
                                    width: bar_width,
                                    height,
                                    color,
                                }
                            })
                            .collect();

                        RenderSeriesType::Bar { bars }
                    }
                    SeriesType::BoxPlot { data, config } => {
                        let data = data.resolve(0.0);
                        // Calculate box plot statistics
                        let box_data = crate::plots::boxplot::calculate_box_plot(&data, config)
                            .map_err(|e| {
                                PlottingError::RenderError(format!(
                                    "Box plot calculation failed: {}",
                                    e
                                ))
                            })?;

                        // Transform coordinates for box plot elements
                        let x_center = 0.5; // Center the box plot
                        let box_width = 0.3; // Box width

                        // Map Y coordinates to plot area
                        let q1_y = map_data_to_pixels(
                            box_data.q1,
                            0.0,
                            bounds.0,
                            bounds.1,
                            bounds.2,
                            bounds.3,
                            plot_area,
                        )
                        .1;
                        let median_y = map_data_to_pixels(
                            box_data.median,
                            0.0,
                            bounds.0,
                            bounds.1,
                            bounds.2,
                            bounds.3,
                            plot_area,
                        )
                        .1;
                        let q3_y = map_data_to_pixels(
                            box_data.q3,
                            0.0,
                            bounds.0,
                            bounds.1,
                            bounds.2,
                            bounds.3,
                            plot_area,
                        )
                        .1;
                        let lower_whisker_y = map_data_to_pixels(
                            box_data.min,
                            0.0,
                            bounds.0,
                            bounds.1,
                            bounds.2,
                            bounds.3,
                            plot_area,
                        )
                        .1;
                        let upper_whisker_y = map_data_to_pixels(
                            box_data.max,
                            0.0,
                            bounds.0,
                            bounds.1,
                            bounds.2,
                            bounds.3,
                            plot_area,
                        )
                        .1;

                        // Map X coordinate
                        let x_center_px = map_data_to_pixels(
                            x_center, 0.0, bounds.0, bounds.1, bounds.2, bounds.3, plot_area,
                        )
                        .0;
                        let box_left = x_center_px - box_width * plot_area.width() * 0.5;
                        let box_right = x_center_px + box_width * plot_area.width() * 0.5;

                        // Transform outliers
                        let mut outliers = Vec::new();
                        for &outlier in &box_data.outliers {
                            let outlier_y = map_data_to_pixels(
                                outlier, 0.0, bounds.0, bounds.1, bounds.2, bounds.3, plot_area,
                            )
                            .1;
                            outliers.push(crate::core::types::Point2f {
                                x: x_center_px,
                                y: outlier_y,
                            });
                        }

                        let box_render_data = crate::render::parallel::BoxPlotRenderData {
                            x_center: x_center_px,
                            box_left,
                            box_right,
                            q1_y,
                            median_y,
                            q3_y,
                            lower_whisker_y,
                            upper_whisker_y,
                            outliers,
                            box_color: color,
                            line_color: color,
                            outlier_color: color,
                        };

                        RenderSeriesType::BoxPlot {
                            box_data: box_render_data,
                        }
                    }
                    SeriesType::Heatmap { data } => {
                        // Calculate cell dimensions in pixel space
                        let cell_width = parallel_plot_area.width() / data.n_cols as f32;
                        let cell_height = parallel_plot_area.height() / data.n_rows as f32;

                        // Create heatmap cells with colors
                        let cells: Vec<crate::render::parallel::HeatmapCell> = data
                            .values
                            .iter()
                            .enumerate()
                            .flat_map(|(row_idx, row)| {
                                row.iter().enumerate().map(move |(col_idx, &value)| {
                                    let cell_color = data.get_color(value);
                                    crate::render::parallel::HeatmapCell {
                                        x: parallel_plot_area.left + col_idx as f32 * cell_width,
                                        // Flip Y axis (row 0 at top)
                                        y: parallel_plot_area.top
                                            + (data.n_rows - 1 - row_idx) as f32 * cell_height,
                                        width: cell_width,
                                        height: cell_height,
                                        color: cell_color,
                                    }
                                })
                            })
                            .collect();

                        RenderSeriesType::Heatmap {
                            cells,
                            n_rows: data.n_rows,
                            n_cols: data.n_cols,
                        }
                    }
                    SeriesType::Kde { data: kde_data } => {
                        // Transform KDE coordinates in parallel
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &kde_data.x,
                                &kde_data.y,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        // Process line segments in parallel
                        let segments = self.render.parallel_renderer.process_polyline_parallel(
                            &points,
                            series.line_style.clone().unwrap_or(LineStyle::Solid),
                            color,
                            line_width,
                        )?;

                        RenderSeriesType::Line { segments }
                    }
                    SeriesType::Ecdf { data: ecdf_data } => {
                        // Transform ECDF step vertices in parallel
                        let step_x: Vec<f64> =
                            ecdf_data.step_vertices.iter().map(|(x, _)| *x).collect();
                        let step_y: Vec<f64> =
                            ecdf_data.step_vertices.iter().map(|(_, y)| *y).collect();
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &step_x,
                                &step_y,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        // Process line segments in parallel
                        let segments = self.render.parallel_renderer.process_polyline_parallel(
                            &points,
                            series.line_style.clone().unwrap_or(LineStyle::Solid),
                            color,
                            line_width,
                        )?;

                        RenderSeriesType::Line { segments }
                    }
                    SeriesType::Violin { data: violin_data } => {
                        // Violin plots use polygon rendering, not supported in parallel mode
                        // Fall back to simple representation using KDE outline
                        let half_width = violin_data.config.width / 2.0;
                        let (left, right) = crate::plots::distribution::violin_polygon(
                            violin_data,
                            0.5,
                            half_width,
                            &violin_data.config,
                        );
                        let polygon =
                            crate::plots::distribution::close_violin_polygon(&left, &right);

                        let poly_x: Vec<f64> = polygon.iter().map(|(x, _)| *x).collect();
                        let poly_y: Vec<f64> = polygon.iter().map(|(_, y)| *y).collect();
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &poly_x,
                                &poly_y,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        let segments = self.render.parallel_renderer.process_polyline_parallel(
                            &points,
                            LineStyle::Solid,
                            color,
                            line_width,
                        )?;

                        RenderSeriesType::Line { segments }
                    }
                    SeriesType::Boxen { data: boxen_data } => {
                        // Boxen plots use polygon rendering, not supported in parallel mode
                        // Fall back to simple representation using box outlines
                        let mut all_points = Vec::new();
                        for boxen_box in &boxen_data.boxes {
                            let rect = crate::plots::distribution::boxen_rect(
                                boxen_box,
                                0.5,
                                boxen_data.config.orient,
                            );
                            for (x, y) in &rect {
                                all_points.push((*x, *y));
                            }
                        }

                        let poly_x: Vec<f64> = all_points.iter().map(|(x, _)| *x).collect();
                        let poly_y: Vec<f64> = all_points.iter().map(|(_, y)| *y).collect();
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &poly_x,
                                &poly_y,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        let segments = self.render.parallel_renderer.process_polyline_parallel(
                            &points,
                            LineStyle::Solid,
                            color,
                            line_width,
                        )?;

                        RenderSeriesType::Line { segments }
                    }
                    SeriesType::Contour { data: contour_data } => {
                        // Contour plots use line segment rendering
                        let mut all_points = Vec::new();
                        for level in &contour_data.lines {
                            for &(x1, y1, x2, y2) in &level.segments {
                                all_points.push((x1, y1));
                                all_points.push((x2, y2));
                            }
                        }

                        let poly_x: Vec<f64> = all_points.iter().map(|(x, _)| *x).collect();
                        let poly_y: Vec<f64> = all_points.iter().map(|(_, y)| *y).collect();
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &poly_x,
                                &poly_y,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        let segments = self.render.parallel_renderer.process_polyline_parallel(
                            &points,
                            LineStyle::Solid,
                            color,
                            line_width,
                        )?;

                        RenderSeriesType::Line { segments }
                    }
                    SeriesType::Pie { .. } => {
                        // Pie charts use polygon rendering, not supported in parallel mode
                        // Return empty segments (will be rendered using normal path)
                        RenderSeriesType::Line { segments: vec![] }
                    }
                    SeriesType::Radar { data: radar_data } => {
                        // Radar plots use polygon rendering, not supported in parallel mode
                        // Fall back to line rendering of radar polygon outlines
                        let mut all_points = Vec::new();
                        for series_data in &radar_data.series {
                            for &(x, y) in &series_data.polygon {
                                all_points.push((x, y));
                            }
                        }

                        let poly_x: Vec<f64> = all_points.iter().map(|(x, _)| *x).collect();
                        let poly_y: Vec<f64> = all_points.iter().map(|(_, y)| *y).collect();
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &poly_x,
                                &poly_y,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        let segments = self.render.parallel_renderer.process_polyline_parallel(
                            &points,
                            LineStyle::Solid,
                            color,
                            line_width,
                        )?;

                        RenderSeriesType::Line { segments }
                    }
                    SeriesType::Polar { data: polar_data } => {
                        // Polar plots use polygon rendering, not supported in parallel mode
                        // Fall back to line rendering of polar points
                        let poly_x: Vec<f64> = polar_data.points.iter().map(|p| p.x).collect();
                        let poly_y: Vec<f64> = polar_data.points.iter().map(|p| p.y).collect();
                        let points = self
                            .render
                            .parallel_renderer
                            .transform_coordinates_parallel(
                                &poly_x,
                                &poly_y,
                                data_bounds.clone(),
                                parallel_plot_area.clone(),
                            )?;

                        let segments = self.render.parallel_renderer.process_polyline_parallel(
                            &points,
                            LineStyle::Solid,
                            color,
                            line_width,
                        )?;

                        RenderSeriesType::Line { segments }
                    }
                };

                Ok(SeriesRenderData {
                    series_type: render_series_type,
                    color,
                    line_width,
                    alpha,
                    label: series.label.clone(),
                })
            },
        )?;

        // Render processed series (sequential - final drawing)
        for processed in processed_series {
            match processed.series_type {
                RenderSeriesType::Line { segments } => {
                    // Draw all line segments
                    for segment in segments {
                        renderer.draw_polyline(
                            &[
                                (segment.start.x, segment.start.y),
                                (segment.end.x, segment.end.y),
                            ],
                            segment.color,
                            segment.width,
                            segment.style,
                        )?;
                    }
                }
                RenderSeriesType::Scatter { markers } => {
                    // Draw all markers
                    for marker in markers {
                        renderer.draw_marker(
                            marker.position.x,
                            marker.position.y,
                            marker.size,
                            marker.style,
                            marker.color,
                        )?;
                    }
                }
                RenderSeriesType::Bar { bars } => {
                    // Draw all bars
                    for bar in bars {
                        renderer
                            .draw_rectangle(bar.x, bar.y, bar.width, bar.height, bar.color, true)?;
                    }
                }
                RenderSeriesType::BoxPlot { box_data } => {
                    // Draw box plot components

                    // Draw the box (IQR)
                    renderer.draw_rectangle(
                        box_data.box_left,
                        box_data.q3_y,
                        box_data.box_right - box_data.box_left,
                        box_data.q1_y - box_data.q3_y,
                        box_data.box_color,
                        false, // outline only
                    )?;

                    // Draw median line
                    renderer.draw_line(
                        box_data.box_left,
                        box_data.median_y,
                        box_data.box_right,
                        box_data.median_y,
                        box_data.line_color,
                        2.0, // median line width
                        LineStyle::Solid,
                    )?;

                    // Draw lower whisker
                    renderer.draw_line(
                        box_data.x_center,
                        box_data.q1_y,
                        box_data.x_center,
                        box_data.lower_whisker_y,
                        box_data.line_color,
                        1.0,
                        LineStyle::Solid,
                    )?;

                    // Draw upper whisker
                    renderer.draw_line(
                        box_data.x_center,
                        box_data.q3_y,
                        box_data.x_center,
                        box_data.upper_whisker_y,
                        box_data.line_color,
                        1.0,
                        LineStyle::Solid,
                    )?;

                    // Draw whisker caps
                    let cap_width = (box_data.box_right - box_data.box_left) * 0.3;
                    renderer.draw_line(
                        box_data.x_center - cap_width,
                        box_data.lower_whisker_y,
                        box_data.x_center + cap_width,
                        box_data.lower_whisker_y,
                        box_data.line_color,
                        1.0,
                        LineStyle::Solid,
                    )?;

                    renderer.draw_line(
                        box_data.x_center - cap_width,
                        box_data.upper_whisker_y,
                        box_data.x_center + cap_width,
                        box_data.upper_whisker_y,
                        box_data.line_color,
                        1.0,
                        LineStyle::Solid,
                    )?;

                    // Draw outliers
                    for outlier in &box_data.outliers {
                        renderer.draw_marker(
                            outlier.x,
                            outlier.y,
                            4.0, // outlier marker size
                            MarkerStyle::Circle,
                            box_data.outlier_color,
                        )?;
                    }
                }
                RenderSeriesType::ErrorBars { .. } => {
                    // Error bars implementation would go here
                }
                RenderSeriesType::Heatmap { cells, .. } => {
                    // Draw all heatmap cells as filled rectangles
                    for cell in cells {
                        renderer.draw_rectangle(
                            cell.x,
                            cell.y,
                            cell.width,
                            cell.height,
                            cell.color,
                            true, // filled
                        )?;
                    }
                }
            }
        }

        // Draw tick labels (only for Cartesian plots)
        if self.needs_cartesian_axes() {
            let tick_size_px = pt_to_px(self.display.config.typography.tick_size(), dpi);
            renderer.draw_axis_labels_at(
                &layout.plot_area,
                bounds.0,
                bounds.1,
                bounds.2,
                bounds.3,
                &x_ticks,
                &y_ticks,
                layout.xtick_baseline_y,
                layout.ytick_right_x,
                tick_size_px,
                self.display.theme.foreground,
                dpi,
            )?;
        }

        // Draw title if present
        if let Some(ref pos) = layout.title_pos {
            if let Some(ref title) = self.display.title {
                let title_str = title.resolve(0.0);
                renderer.draw_title_at(pos, &title_str, self.display.theme.foreground)?;
            }
        }

        // Draw xlabel if present (only for Cartesian plots)
        if self.needs_cartesian_axes() {
            if let Some(ref pos) = layout.xlabel_pos {
                if let Some(ref xlabel) = self.display.xlabel {
                    let xlabel_str = xlabel.resolve(0.0);
                    renderer.draw_xlabel_at(pos, &xlabel_str, self.display.theme.foreground)?;
                }
            }

            // Draw ylabel if present
            if let Some(ref pos) = layout.ylabel_pos {
                if let Some(ref ylabel) = self.display.ylabel {
                    let ylabel_str = ylabel.resolve(0.0);
                    renderer.draw_ylabel_at(pos, &ylabel_str, self.display.theme.foreground)?;
                }
            }
        }

        // Record performance statistics
        let duration = start_time.elapsed();
        let total_points = self.calculate_total_points();

        // Performance stats available via self.render.parallel_renderer.performance_stats()
        // Uncomment for debugging:
        // let stats = self.render.parallel_renderer.performance_stats();
        // println!("⚡ Parallel: {} series, {} points in {:.1}ms ({:.1}x speedup, {} threads)",
        //     self.series_mgr.series.len(), total_points, duration.as_millis(),
        //     stats.estimated_speedup, stats.configured_threads);
        let _ = (start_time, total_points); // suppress unused warnings

        // Convert renderer output to Image
        Ok(renderer.into_image())
    }

    /// Calculate data bounds across all series
    fn calculate_data_bounds(&self) -> Result<(f64, f64, f64, f64)> {
        if let Some(err) = self.pending_ingestion_error() {
            return Err(err);
        }

        let mut x_min = f64::INFINITY;
        let mut x_max = f64::NEG_INFINITY;
        let mut y_min = f64::INFINITY;
        let mut y_max = f64::NEG_INFINITY;

        for series in &self.series_mgr.series {
            match &series.series_type {
                SeriesType::Line { x_data, y_data } | SeriesType::Scatter { x_data, y_data } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    for i in 0..x_data.len() {
                        let x_val = x_data[i];
                        let y_val = y_data[i];

                        if x_val.is_finite() {
                            x_min = x_min.min(x_val);
                            x_max = x_max.max(x_val);
                        }
                        if y_val.is_finite() {
                            y_min = y_min.min(y_val);
                            y_max = y_max.max(y_val);
                        }
                    }
                }
                SeriesType::Bar { categories, values } => {
                    let values = values.resolve(0.0);
                    // Add 0.5-unit padding on each side for bar charts (matplotlib-compatible)
                    // This ensures bars at positions 0 and n-1 are fully visible
                    x_min = x_min.min(-0.5);
                    x_max = x_max.max(categories.len() as f64 - 0.5);

                    for &val in &values {
                        if val.is_finite() {
                            y_min = y_min.min(val.min(0.0));
                            y_max = y_max.max(val.max(0.0));
                        }
                    }
                }
                SeriesType::ErrorBars {
                    x_data,
                    y_data,
                    y_errors,
                } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    let y_errors = y_errors.resolve(0.0);
                    for i in 0..x_data.len() {
                        let x_val = x_data[i];
                        let y_val = y_data[i];
                        let y_err = y_errors[i];

                        if x_val.is_finite() {
                            x_min = x_min.min(x_val);
                            x_max = x_max.max(x_val);
                        }
                        if y_val.is_finite() && y_err.is_finite() {
                            y_min = y_min.min(y_val - y_err);
                            y_max = y_max.max(y_val + y_err);
                        }
                    }
                }
                SeriesType::ErrorBarsXY {
                    x_data,
                    y_data,
                    x_errors,
                    y_errors,
                } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    let x_errors = x_errors.resolve(0.0);
                    let y_errors = y_errors.resolve(0.0);
                    for i in 0..x_data.len() {
                        let x_val = x_data[i];
                        let y_val = y_data[i];
                        let x_err = x_errors[i];
                        let y_err = y_errors[i];

                        if x_val.is_finite() && x_err.is_finite() {
                            x_min = x_min.min(x_val - x_err);
                            x_max = x_max.max(x_val + x_err);
                        }
                        if y_val.is_finite() && y_err.is_finite() {
                            y_min = y_min.min(y_val - y_err);
                            y_max = y_max.max(y_val + y_err);
                        }
                    }
                }
                SeriesType::Histogram { data, config } => {
                    let data = data.resolve(0.0);
                    // Calculate histogram to get data bounds
                    if let Ok(hist_data) =
                        crate::plots::histogram::calculate_histogram(&data, config)
                    {
                        // X bounds from bin edges
                        if !hist_data.bin_edges.is_empty() {
                            x_min = x_min.min(*hist_data.bin_edges.first().unwrap());
                            x_max = x_max.max(*hist_data.bin_edges.last().unwrap());
                        }

                        // Y bounds from counts (include zero baseline)
                        y_min = y_min.min(0.0);
                        for &count in &hist_data.counts {
                            if count.is_finite() && count > 0.0 {
                                y_max = y_max.max(count);
                            }
                        }
                    }
                }
                SeriesType::BoxPlot { data, .. } => {
                    let data = data.resolve(0.0);
                    if data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }

                    // Set x bounds for box plot (centered at 0.5)
                    x_min = x_min.min(0.0);
                    x_max = x_max.max(1.0);

                    // Y bounds include all data values
                    for &value in &data {
                        if value.is_finite() {
                            y_min = y_min.min(value);
                            y_max = y_max.max(value);
                        }
                    }
                }
                SeriesType::Heatmap { data } => {
                    // Heatmap bounds: x from 0 to n_cols, y from 0 to n_rows
                    x_min = x_min.min(0.0);
                    x_max = x_max.max(data.n_cols as f64);
                    y_min = y_min.min(0.0);
                    y_max = y_max.max(data.n_rows as f64);
                }
                SeriesType::Kde { data } => {
                    // KDE bounds from x/y data
                    for i in 0..data.x.len() {
                        let x_val = data.x[i];
                        let y_val = data.y[i];

                        if x_val.is_finite() {
                            x_min = x_min.min(x_val);
                            x_max = x_max.max(x_val);
                        }
                        if y_val.is_finite() {
                            y_min = y_min.min(y_val);
                            y_max = y_max.max(y_val);
                        }
                    }
                    // Include zero baseline for density plots
                    y_min = y_min.min(0.0);
                }
                SeriesType::Ecdf { data } => {
                    // ECDF bounds from x/y data
                    for i in 0..data.x.len() {
                        let x_val = data.x[i];
                        let y_val = data.y[i];

                        if x_val.is_finite() {
                            x_min = x_min.min(x_val);
                            x_max = x_max.max(x_val);
                        }
                        if y_val.is_finite() {
                            y_min = y_min.min(y_val);
                            y_max = y_max.max(y_val);
                        }
                    }
                    // Include zero baseline for ECDF
                    y_min = y_min.min(0.0);
                }
                SeriesType::Violin { data } => {
                    // Violin bounds from KDE range (extends beyond data range by 3 bandwidths)
                    // Use KDE's x values which represent the evaluation range
                    let (kde_min, kde_max) = if !data.kde.x.is_empty() {
                        (
                            data.kde.x.first().copied().unwrap_or(data.range.0),
                            data.kde.x.last().copied().unwrap_or(data.range.1),
                        )
                    } else {
                        data.range
                    };
                    if kde_min.is_finite() {
                        y_min = y_min.min(kde_min);
                    }
                    if kde_max.is_finite() {
                        y_max = y_max.max(kde_max);
                    }
                    // X bounds for centered violin
                    x_min = x_min.min(0.0);
                    x_max = x_max.max(1.0);
                }
                SeriesType::Boxen { data } => {
                    // Boxen bounds from data range
                    let (data_min, data_max) = data.data_range;
                    if data_min.is_finite() {
                        y_min = y_min.min(data_min);
                    }
                    if data_max.is_finite() {
                        y_max = y_max.max(data_max);
                    }
                    // X bounds for centered boxen
                    x_min = x_min.min(0.0);
                    x_max = x_max.max(1.0);
                }
                SeriesType::Contour { data } => {
                    // Contour bounds from grid coordinates
                    for &x_val in &data.x {
                        if x_val.is_finite() {
                            x_min = x_min.min(x_val);
                            x_max = x_max.max(x_val);
                        }
                    }
                    for &y_val in &data.y {
                        if y_val.is_finite() {
                            y_min = y_min.min(y_val);
                            y_max = y_max.max(y_val);
                        }
                    }
                }
                SeriesType::Pie { .. } => {
                    // Pie charts use normalized 0-1 coordinate space
                    x_min = x_min.min(0.0);
                    x_max = x_max.max(1.0);
                    y_min = y_min.min(0.0);
                    y_max = y_max.max(1.0);
                }
                SeriesType::Radar { data } => {
                    // Radar charts use normalized coordinate space
                    // Bounds are determined by the polygon coordinates
                    for series_data in &data.series {
                        for &(x_val, y_val) in &series_data.polygon {
                            if x_val.is_finite() {
                                x_min = x_min.min(x_val);
                                x_max = x_max.max(x_val);
                            }
                            if y_val.is_finite() {
                                y_min = y_min.min(y_val);
                                y_max = y_max.max(y_val);
                            }
                        }
                    }
                }
                SeriesType::Polar { data } => {
                    // Polar plots need symmetric space centered at origin
                    // Use ONLY the label margin bounds, ignoring actual data points
                    // This ensures asymmetric curves (like cardioid) still have centered labels
                    let label_margin = data.r_max * 1.5;
                    x_min = -label_margin;
                    x_max = label_margin;
                    y_min = -label_margin;
                    y_max = label_margin;
                }
            }
        }

        // Handle edge cases
        if (x_max - x_min).abs() < f64::EPSILON {
            x_min -= 1.0;
            x_max += 1.0;
        }
        if (y_max - y_min).abs() < f64::EPSILON {
            y_min -= 1.0;
            y_max += 1.0;
        }

        Ok((x_min, x_max, y_min, y_max))
    }

    /// Automatically optimize backend selection based on data size
    ///
    /// Selects the most appropriate rendering backend based on dataset characteristics:
    /// - < 1K points: Skia (simple, fast)
    /// - 1K-10K points: Parallel (multi-threaded)
    /// - 10K-100K points: Parallel (optimized)
    /// - > 100K points: GPU/DataShader (hardware acceleration)
    ///
    /// If a backend was explicitly set with `.backend()`, that choice is respected.
    pub fn auto_optimize(self) -> Self {
        self.auto_optimize_with_extra_points(0)
    }

    /// Auto-optimize with additional pending points from PlotBuilder
    ///
    /// This internal method is used by PlotBuilder to include points from
    /// the current series that hasn't been finalized yet.
    pub(crate) fn auto_optimize_with_extra_points(mut self, extra_points: usize) -> Self {
        // If backend already explicitly set, respect that choice
        if self.render.backend.is_some() {
            self.render.auto_optimized = true;
            return self;
        }

        // Count total data points across all series
        let series_points: usize = self
            .series_mgr
            .series
            .iter()
            .map(|s| match &s.series_type {
                SeriesType::Line { x_data, .. } => x_data.len(),
                SeriesType::Scatter { x_data, .. } => x_data.len(),
                SeriesType::Bar { values, .. } => values.len(),
                SeriesType::Histogram { data, .. } => data.len(),
                SeriesType::BoxPlot { data, .. } => data.len(),
                SeriesType::ErrorBars { x_data, .. } => x_data.len(),
                SeriesType::ErrorBarsXY { x_data, .. } => x_data.len(),
                SeriesType::Heatmap { data } => data.n_rows * data.n_cols,
                SeriesType::Kde { data } => data.x.len(),
                SeriesType::Ecdf { data } => data.x.len(),
                SeriesType::Violin { data } => data.data.len(),
                SeriesType::Boxen { data } => data.boxes.len() * 4,
                SeriesType::Contour { data } => data.x.len() * data.y.len(),
                SeriesType::Pie { data } => data.values.len(),
                SeriesType::Radar { data } => data.series.iter().map(|s| s.values.len()).sum(),
                SeriesType::Polar { data } => data.points.len(),
            })
            .sum();

        let total_points = series_points + extra_points;

        // Select backend based on data size
        let selected_backend = if total_points < 1000 {
            BackendType::Skia
        } else if total_points < 100_000 {
            #[cfg(feature = "parallel")]
            {
                BackendType::Parallel
            }
            #[cfg(not(feature = "parallel"))]
            {
                BackendType::Skia
            }
        } else {
            // For very large datasets, prefer GPU if available, else DataShader
            #[cfg(feature = "gpu")]
            {
                BackendType::GPU
            }
            #[cfg(not(feature = "gpu"))]
            {
                BackendType::DataShader
            }
        };

        self.render.backend = Some(selected_backend);
        self.render.auto_optimized = true;
        self
    }

    /// Set backend explicitly (overrides auto-optimization)
    pub fn backend(mut self, backend: BackendType) -> Self {
        self.render.backend = Some(backend);
        self
    }

    /// Enable GPU acceleration for coordinate transformations
    ///
    /// When enabled, large datasets (>5K points) will use GPU compute shaders
    /// for coordinate transformation, providing significant speedups.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// Plot::new()
    ///     .gpu(true)
    ///     .line(&large_x, &large_y)
    ///     .save("plot.png")?;
    /// ```
    ///
    /// # Requirements
    ///
    /// - Requires the `gpu` feature to be enabled
    /// - Falls back to CPU if GPU is not available
    #[cfg(feature = "gpu")]
    pub fn gpu(mut self, enabled: bool) -> Self {
        self.render.enable_gpu = enabled;
        if enabled {
            self.render.backend = Some(BackendType::GPU);
        }
        self
    }

    /// Get the current backend name (for testing)
    pub fn get_backend_name(&self) -> &'static str {
        match self.render.backend {
            Some(BackendType::Skia) => "skia",
            Some(BackendType::Parallel) => "parallel",
            Some(BackendType::GPU) => "gpu",
            Some(BackendType::DataShader) => "datashader",
            None => "auto",
        }
    }

    /// Save the plot to a PNG file
    ///
    /// Renders the plot and saves it to the specified path.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .title("Saved Plot")
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .end_series()
    ///     .save("output.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn save<P: AsRef<Path>>(self, path: P) -> Result<()> {
        use crate::render::skia::SkiaRenderer;

        if let Some(err) = self.pending_ingestion_error() {
            return Err(err);
        }

        // Validate data before rendering
        for series in &self.series_mgr.series {
            match &series.series_type {
                SeriesType::Line { x_data, y_data }
                | SeriesType::Scatter { x_data, y_data }
                | SeriesType::ErrorBars { x_data, y_data, .. }
                | SeriesType::ErrorBarsXY { x_data, y_data, .. } => {
                    // Check for empty data
                    if x_data.is_empty() || y_data.is_empty() {
                        return Err(crate::core::PlottingError::EmptyDataSet);
                    }
                    // Check for mismatched data lengths
                    if x_data.len() != y_data.len() {
                        return Err(crate::core::PlottingError::DataLengthMismatch {
                            x_len: x_data.len(),
                            y_len: y_data.len(),
                            series_index: None,
                        });
                    }
                }
                SeriesType::Bar { categories, values } => {
                    // Check for empty data
                    if categories.is_empty() || values.is_empty() {
                        return Err(crate::core::PlottingError::EmptyDataSet);
                    }
                    // Check for mismatched data lengths
                    if categories.len() != values.len() {
                        return Err(crate::core::PlottingError::DataLengthMismatch {
                            x_len: categories.len(),
                            y_len: values.len(),
                            series_index: None,
                        });
                    }
                }
                SeriesType::Histogram { data, .. } => {
                    // Check for empty data
                    if data.is_empty() {
                        return Err(crate::core::PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::BoxPlot { data, .. } => {
                    if data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Heatmap { data } => {
                    if data.values.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Kde { data } => {
                    if data.x.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Ecdf { data } => {
                    if data.x.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Violin { data } => {
                    if data.data.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Boxen { data } => {
                    if data.boxes.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Contour { data } => {
                    if data.levels.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Pie { data } => {
                    if data.values.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Radar { data } => {
                    if data.series.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
                SeriesType::Polar { data } => {
                    if data.points.is_empty() {
                        return Err(PlottingError::EmptyDataSet);
                    }
                }
            }
        }

        // Create renderer and render the plot with DPI scaling
        let (scaled_width, scaled_height) = self.dpi_scaled_dimensions();
        let mut renderer =
            SkiaRenderer::new(scaled_width, scaled_height, self.display.theme.clone())?;
        renderer.set_text_engine_mode(self.display.text_engine);

        // Clear background
        renderer.clear();

        // Calculate data bounds first (needed for layout calculation)
        let mut x_min = f64::INFINITY;
        let mut x_max = f64::NEG_INFINITY;
        let mut y_min = f64::INFINITY;
        let mut y_max = f64::NEG_INFINITY;

        for series in &self.series_mgr.series {
            match &series.series_type {
                SeriesType::Line { x_data, y_data }
                | SeriesType::Scatter { x_data, y_data }
                | SeriesType::ErrorBars { x_data, y_data, .. }
                | SeriesType::ErrorBarsXY { x_data, y_data, .. } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    for (&x, &y) in x_data.iter().zip(y_data.iter()) {
                        x_min = x_min.min(x);
                        x_max = x_max.max(x);
                        y_min = y_min.min(y);
                        y_max = y_max.max(y);
                    }
                }
                SeriesType::Bar { values, .. } => {
                    let values = values.resolve(0.0);
                    // Add 0.5-unit padding on each side for bar charts (matplotlib-compatible)
                    let n_categories = values.len();
                    x_min = x_min.min(-0.5);
                    x_max = x_max.max(n_categories as f64 - 0.5);
                    for &value in values.iter() {
                        y_min = y_min.min(0.0).min(value);
                        y_max = y_max.max(value);
                    }
                }
                SeriesType::Histogram { data, config } => {
                    let data = data.resolve(0.0);
                    if let Ok(hist_data) =
                        crate::plots::histogram::calculate_histogram(&data, config)
                    {
                        if let (Some(&first), Some(&last)) =
                            (hist_data.bin_edges.first(), hist_data.bin_edges.last())
                        {
                            x_min = x_min.min(first);
                            x_max = x_max.max(last);
                        }
                        y_min = y_min.min(0.0);
                        if let Some(&max_count) = hist_data
                            .counts
                            .iter()
                            .max_by(|a, b| a.partial_cmp(b).unwrap())
                        {
                            y_max = y_max.max(max_count);
                        }
                    }
                }
                SeriesType::BoxPlot { data, .. } => {
                    let data = data.resolve(0.0);
                    x_min = x_min.min(0.0);
                    x_max = x_max.max(1.0);
                    for &value in &data {
                        if value.is_finite() {
                            y_min = y_min.min(value);
                            y_max = y_max.max(value);
                        }
                    }
                }
                SeriesType::Heatmap { data } => {
                    // Heatmap bounds: x from 0 to n_cols, y from 0 to n_rows
                    x_min = x_min.min(0.0);
                    x_max = x_max.max(data.n_cols as f64);
                    y_min = y_min.min(0.0);
                    y_max = y_max.max(data.n_rows as f64);
                }
                SeriesType::Kde { data } => {
                    // KDE bounds from x/y data
                    for (&x, &y) in data.x.iter().zip(data.y.iter()) {
                        x_min = x_min.min(x);
                        x_max = x_max.max(x);
                        y_min = y_min.min(y);
                        y_max = y_max.max(y);
                    }
                    // Include zero baseline for density plots
                    y_min = y_min.min(0.0);
                }
                SeriesType::Ecdf { data } => {
                    // ECDF bounds from x/y data
                    for (&x, &y) in data.x.iter().zip(data.y.iter()) {
                        x_min = x_min.min(x);
                        x_max = x_max.max(x);
                        y_min = y_min.min(y);
                        y_max = y_max.max(y);
                    }
                    // Include zero baseline for ECDF
                    y_min = y_min.min(0.0);
                }
                SeriesType::Violin { data } => {
                    // Violin bounds from data range
                    let (data_min, data_max) = data.range;
                    if data_min.is_finite() {
                        y_min = y_min.min(data_min);
                    }
                    if data_max.is_finite() {
                        y_max = y_max.max(data_max);
                    }
                    // X bounds for centered violin
                    x_min = x_min.min(0.0);
                    x_max = x_max.max(1.0);
                }
                SeriesType::Boxen { data } => {
                    // Boxen bounds from data range
                    let (data_min, data_max) = data.data_range;
                    if data_min.is_finite() {
                        y_min = y_min.min(data_min);
                    }
                    if data_max.is_finite() {
                        y_max = y_max.max(data_max);
                    }
                    // X bounds for centered boxen
                    x_min = x_min.min(0.0);
                    x_max = x_max.max(1.0);
                }
                SeriesType::Contour { data } => {
                    // Contour bounds from grid coordinates
                    for &x_val in &data.x {
                        if x_val.is_finite() {
                            x_min = x_min.min(x_val);
                            x_max = x_max.max(x_val);
                        }
                    }
                    for &y_val in &data.y {
                        if y_val.is_finite() {
                            y_min = y_min.min(y_val);
                            y_max = y_max.max(y_val);
                        }
                    }
                }
                SeriesType::Pie { .. } => {
                    // Pie charts use normalized 0-1 coordinate space
                    x_min = x_min.min(0.0);
                    x_max = x_max.max(1.0);
                    y_min = y_min.min(0.0);
                    y_max = y_max.max(1.0);
                }
                SeriesType::Radar { data } => {
                    // Radar charts use polygon coordinates
                    for series_data in &data.series {
                        for &(x_val, y_val) in &series_data.polygon {
                            if x_val.is_finite() {
                                x_min = x_min.min(x_val);
                                x_max = x_max.max(x_val);
                            }
                            if y_val.is_finite() {
                                y_min = y_min.min(y_val);
                                y_max = y_max.max(y_val);
                            }
                        }
                    }
                }
                SeriesType::Polar { data } => {
                    // Polar plots need symmetric space centered at origin
                    // Use label margin bounds to ensure all angle labels are visible
                    let label_margin = data.r_max * 1.5;
                    x_min = -label_margin;
                    x_max = label_margin;
                    y_min = -label_margin;
                    y_max = label_margin;
                }
            }
        }

        // Add padding to data bounds (skip for polar which has its own margins)
        let x_range = x_max - x_min;
        let y_range = y_max - y_min;
        x_min -= x_range * 0.05;
        x_max += x_range * 0.05;
        y_min -= y_range * 0.05;
        y_max += y_range * 0.05;

        // Extract bar chart categories if present (for categorical x-axis labels)
        let bar_categories: Option<Vec<String>> = self.series_mgr.series.iter().find_map(|s| {
            if let SeriesType::Bar { categories, .. } = &s.series_type {
                Some(categories.clone())
            } else {
                None
            }
        });

        // Extract violin categories and positions if present (for categorical x-axis labels)
        let violin_data: Vec<(String, f64)> = self
            .series_mgr
            .series
            .iter()
            .filter_map(|s| {
                if let SeriesType::Violin { data } = &s.series_type {
                    data.config
                        .category
                        .clone()
                        .map(|cat| (cat, data.config.x_position))
                } else {
                    None
                }
            })
            .collect();

        let (violin_categories, violin_positions): (Vec<String>, Vec<f64>) =
            violin_data.into_iter().unzip();

        // Check if this is a violin categorical plot (to use special rendering)
        let is_violin_categorical = !violin_categories.is_empty();

        // Use violin categories if bar_categories is not present and violin categories exist
        let bar_categories = bar_categories.or_else(|| {
            if is_violin_categorical {
                Some(violin_categories.clone())
            } else {
                None
            }
        });

        let dpi = self.display.dpi as f32;
        let dpi_scale = dpi / 100.0;
        renderer.set_dpi_scale(dpi_scale);
        let content = self.create_plot_content(y_min, y_max);
        let measured_dimensions = self.measure_layout_text(&renderer, &content, dpi)?;
        let measurements = measured_dimensions.as_ref();

        // Calculate plot area based on MarginConfig
        let (plot_area, layout_opt): (tiny_skia::Rect, Option<PlotLayout>) =
            match &self.display.config.margins {
                MarginConfig::ContentDriven {
                    edge_buffer,
                    center_plot,
                } => {
                    // Use content-driven layout calculator
                    let layout_config = LayoutConfig {
                        edge_buffer_pt: *edge_buffer,
                        center_plot: *center_plot,
                        ..Default::default()
                    };
                    let calculator = LayoutCalculator::new(layout_config);
                    let layout = calculator.compute(
                        (scaled_width, scaled_height),
                        &content,
                        &self.display.config.typography,
                        &self.display.config.spacing,
                        dpi,
                        measurements,
                    );
                    let skia_rect = tiny_skia::Rect::from_ltrb(
                        layout.plot_area.left,
                        layout.plot_area.top,
                        layout.plot_area.right,
                        layout.plot_area.bottom,
                    )
                    .ok_or(PlottingError::InvalidData {
                        message: "Invalid plot area from layout".to_string(),
                        position: None,
                    })?;
                    (skia_rect, Some(layout))
                }
                _ => {
                    // Always use LayoutCalculator for consistent plot area and title/label positioning
                    let layout_config = LayoutConfig::default();
                    let calculator = LayoutCalculator::new(layout_config);
                    let layout = calculator.compute(
                        (scaled_width, scaled_height),
                        &content,
                        &self.display.config.typography,
                        &self.display.config.spacing,
                        dpi,
                        measurements,
                    );
                    let skia_rect = tiny_skia::Rect::from_ltrb(
                        layout.plot_area.left,
                        layout.plot_area.top,
                        layout.plot_area.right,
                        layout.plot_area.bottom,
                    )
                    .ok_or(PlottingError::InvalidData {
                        message: "Invalid plot area from layout".to_string(),
                        position: None,
                    })?;
                    (skia_rect, Some(layout))
                }
            };

        // Generate major and minor ticks for axes using scale-aware tick generation
        let x_major_ticks = crate::axes::generate_ticks_for_scale(
            x_min,
            x_max,
            self.layout.tick_config.major_ticks_x,
            &self.layout.x_scale,
        );
        let y_major_ticks = crate::axes::generate_ticks_for_scale(
            y_min,
            y_max,
            self.layout.tick_config.major_ticks_y,
            &self.layout.y_scale,
        );

        // Generate minor ticks if configured (using log-aware minor ticks for log scales)
        let x_minor_ticks = if self.layout.tick_config.minor_ticks_x > 0 {
            match &self.layout.x_scale {
                AxisScale::Log => crate::axes::generate_log_minor_ticks(&x_major_ticks),
                _ => crate::render::skia::generate_minor_ticks(
                    &x_major_ticks,
                    self.layout.tick_config.minor_ticks_x,
                ),
            }
        } else {
            Vec::new()
        };
        let y_minor_ticks = if self.layout.tick_config.minor_ticks_y > 0 {
            match &self.layout.y_scale {
                AxisScale::Log => crate::axes::generate_log_minor_ticks(&y_major_ticks),
                _ => crate::render::skia::generate_minor_ticks(
                    &y_major_ticks,
                    self.layout.tick_config.minor_ticks_y,
                ),
            }
        } else {
            Vec::new()
        };

        // Combine ticks for rendering based on grid mode
        let x_ticks = match self.layout.tick_config.grid_mode {
            GridMode::MajorOnly => x_major_ticks.clone(),
            GridMode::MinorOnly => x_minor_ticks.clone(),
            GridMode::Both => {
                let mut combined = x_major_ticks.clone();
                combined.extend(x_minor_ticks.iter());
                combined.sort_by(|a, b| a.partial_cmp(b).unwrap());
                combined
            }
        };
        let y_ticks = match self.layout.tick_config.grid_mode {
            GridMode::MajorOnly => y_major_ticks.clone(),
            GridMode::MinorOnly => y_minor_ticks.clone(),
            GridMode::Both => {
                let mut combined = y_major_ticks.clone();
                combined.extend(y_minor_ticks.iter());
                combined.sort_by(|a, b| a.partial_cmp(b).unwrap());
                combined
            }
        };

        let x_tick_pixels: Vec<f32> = x_ticks
            .iter()
            .map(|&x| {
                crate::render::skia::map_data_to_pixels_scaled(
                    x,
                    0.0,
                    x_min,
                    x_max,
                    0.0,
                    1.0,
                    plot_area,
                    &self.layout.x_scale,
                    &AxisScale::Linear,
                )
                .0
            })
            .collect();
        let y_tick_pixels: Vec<f32> = y_ticks
            .iter()
            .map(|&y| {
                crate::render::skia::map_data_to_pixels_scaled(
                    0.0,
                    y,
                    0.0,
                    1.0,
                    y_min,
                    y_max,
                    plot_area,
                    &AxisScale::Linear,
                    &self.layout.y_scale,
                )
                .1
            })
            .collect();

        // Render grid if enabled - using unified GridStyle
        // Skip grid for non-Cartesian plots (Pie, Radar, Polar)
        if self.layout.grid_style.visible && self.needs_cartesian_axes() {
            let grid_color = self.layout.grid_style.effective_color();
            renderer.draw_grid(
                &x_tick_pixels,
                &y_tick_pixels,
                plot_area,
                grid_color,
                self.layout.grid_style.line_style.clone(),
                self.dpi_scaled_line_width(self.layout.grid_style.line_width),
            )?;
        }

        // Convert tick values to pixel positions for major and minor ticks (scale-aware)
        let x_major_tick_pixels: Vec<f32> = x_major_ticks
            .iter()
            .map(|&x| {
                crate::render::skia::map_data_to_pixels_scaled(
                    x,
                    0.0,
                    x_min,
                    x_max,
                    0.0,
                    1.0,
                    plot_area,
                    &self.layout.x_scale,
                    &AxisScale::Linear,
                )
                .0
            })
            .collect();
        let y_major_tick_pixels: Vec<f32> = y_major_ticks
            .iter()
            .map(|&y| {
                crate::render::skia::map_data_to_pixels_scaled(
                    0.0,
                    y,
                    0.0,
                    1.0,
                    y_min,
                    y_max,
                    plot_area,
                    &AxisScale::Linear,
                    &self.layout.y_scale,
                )
                .1
            })
            .collect();

        let x_minor_tick_pixels: Vec<f32> = x_minor_ticks
            .iter()
            .map(|&x| {
                crate::render::skia::map_data_to_pixels_scaled(
                    x,
                    0.0,
                    x_min,
                    x_max,
                    0.0,
                    1.0,
                    plot_area,
                    &self.layout.x_scale,
                    &AxisScale::Linear,
                )
                .0
            })
            .collect();
        let y_minor_tick_pixels: Vec<f32> = y_minor_ticks
            .iter()
            .map(|&y| {
                crate::render::skia::map_data_to_pixels_scaled(
                    0.0,
                    y,
                    0.0,
                    1.0,
                    y_min,
                    y_max,
                    plot_area,
                    &AxisScale::Linear,
                    &self.layout.y_scale,
                )
                .1
            })
            .collect();

        // Only draw axes for Cartesian plots (skip for Pie, Radar, etc.)
        let draw_axes = self.needs_cartesian_axes();

        if draw_axes {
            // Calculate DPI scale for tick rendering (base 100 DPI)
            let dpi_scale = dpi / 100.0;
            // Always draw axes with enhanced tick system
            renderer.draw_axes_with_config(
                plot_area,
                &x_major_tick_pixels,
                &y_major_tick_pixels,
                &x_minor_tick_pixels,
                &y_minor_tick_pixels,
                &self.layout.tick_config.direction,
                self.display.theme.foreground,
                dpi_scale,
            )?;
        }

        // Draw axis labels, tick values, and title using computed layout positions
        // Note: layout_opt is always Some since all render paths now compute layout
        let layout = layout_opt.expect("layout should always be computed");
        let tick_size_px = pt_to_px(self.display.config.typography.tick_size(), dpi);

        // Draw tick labels using layout positions (only for Cartesian plots)
        if draw_axes {
            // Use categorical labels for bar/violin charts, numeric for others
            if is_violin_categorical {
                // Violin plots use their own x-positions for category labels
                renderer.draw_axis_labels_at_categorical_violin(
                    &layout.plot_area,
                    &violin_categories,
                    &violin_positions,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    &y_major_ticks,
                    layout.xtick_baseline_y,
                    layout.ytick_right_x,
                    tick_size_px,
                    self.display.theme.foreground,
                    dpi,
                )?;
            } else if let Some(ref categories) = bar_categories {
                renderer.draw_axis_labels_at_categorical(
                    &layout.plot_area,
                    categories,
                    y_min,
                    y_max,
                    &y_major_ticks,
                    layout.xtick_baseline_y,
                    layout.ytick_right_x,
                    tick_size_px,
                    self.display.theme.foreground,
                    dpi,
                )?;
            } else {
                renderer.draw_axis_labels_at(
                    &layout.plot_area,
                    x_min,
                    x_max,
                    y_min,
                    y_max,
                    &x_major_ticks,
                    &y_major_ticks,
                    layout.xtick_baseline_y,
                    layout.ytick_right_x,
                    tick_size_px,
                    self.display.theme.foreground,
                    dpi,
                )?;
            }
        }

        // Draw title if present (resolve at t=0 for static render)
        if let Some(ref pos) = layout.title_pos {
            if let Some(ref title) = self.display.title {
                let title_str = title.resolve(0.0);
                renderer.draw_title_at(pos, &title_str, self.display.theme.foreground)?;
            }
        }

        // Draw x-axis label if present (only for Cartesian plots)
        if draw_axes {
            if let Some(ref pos) = layout.xlabel_pos {
                if let Some(ref xlabel) = self.display.xlabel {
                    let xlabel_str = xlabel.resolve(0.0);
                    renderer.draw_xlabel_at(pos, &xlabel_str, self.display.theme.foreground)?;
                }
            }

            // Draw y-axis label if present
            if let Some(ref pos) = layout.ylabel_pos {
                if let Some(ref ylabel) = self.display.ylabel {
                    let ylabel_str = ylabel.resolve(0.0);
                    renderer.draw_ylabel_at(pos, &ylabel_str, self.display.theme.foreground)?;
                }
            }
        }

        // Check if we should use DataShader for large datasets
        let total_points: usize = self
            .series_mgr
            .series
            .iter()
            .map(|series| match &series.series_type {
                SeriesType::Line { x_data, .. }
                | SeriesType::Scatter { x_data, .. }
                | SeriesType::ErrorBars { x_data, .. }
                | SeriesType::ErrorBarsXY { x_data, .. } => x_data.len(),
                SeriesType::Bar { categories, .. } => categories.len(),
                SeriesType::Histogram { data, .. } => data.len(),
                SeriesType::BoxPlot { data, .. } => data.len(),
                SeriesType::Heatmap { data } => data.n_rows * data.n_cols,
                SeriesType::Kde { data } => data.x.len(),
                SeriesType::Ecdf { data } => data.x.len(),
                SeriesType::Violin { data } => data.data.len(),
                SeriesType::Boxen { data } => data.boxes.len() * 4,
                SeriesType::Contour { data } => data.x.len() * data.y.len(),
                SeriesType::Pie { data } => data.values.len(),
                SeriesType::Radar { data } => data.series.iter().map(|s| s.values.len()).sum(),
                SeriesType::Polar { data } => data.points.len(),
            })
            .sum();

        const DATASHADER_THRESHOLD: usize = 100_000; // Activate DataShader for >100K points
        #[cfg(feature = "gpu")]
        const GPU_THRESHOLD: usize = 5_000; // Activate GPU for >5K points

        if total_points > DATASHADER_THRESHOLD {
            // Use DataShader for massive datasets - simplified version
            use crate::data::DataShader;

            for series in &self.series_mgr.series {
                match &series.series_type {
                    SeriesType::Scatter { x_data, y_data }
                    | SeriesType::Line { x_data, y_data } => {
                        let x_data = x_data.resolve(0.0);
                        let y_data = y_data.resolve(0.0);
                        let mut datashader = DataShader::with_canvas_size(
                            plot_area.width() as usize,
                            plot_area.height() as usize,
                        );

                        datashader.aggregate(&x_data, &y_data)?;
                        let image = datashader.render();

                        // Draw the DataShader result
                        renderer.draw_datashader_image(&image, plot_area)?;
                    }
                    SeriesType::Histogram { data, config } => {
                        let data = data.resolve(0.0);
                        // For histograms, calculate bins and use DataShader for high density
                        let hist_data = crate::plots::histogram::calculate_histogram(&data, config)
                            .map_err(|e| {
                                PlottingError::RenderError(format!(
                                    "Histogram calculation failed: {}",
                                    e
                                ))
                            })?;

                        // Convert histogram to x,y data for DataShader
                        let x_data: Vec<f64> = hist_data
                            .bin_edges
                            .windows(2)
                            .map(|w| (w[0] + w[1]) / 2.0)
                            .collect();
                        let y_data: Vec<f64> = hist_data.counts;

                        let mut datashader = DataShader::with_canvas_size(
                            plot_area.width() as usize,
                            plot_area.height() as usize,
                        );

                        datashader.aggregate(&x_data, &y_data)?;
                        let image = datashader.render();

                        // Draw the DataShader result
                        renderer.draw_datashader_image(&image, plot_area)?;
                    }
                    _ => {
                        // For other plot types, use normal rendering
                        self.render_series_normal(
                            series,
                            &mut renderer,
                            plot_area,
                            x_min,
                            x_max,
                            y_min,
                            y_max,
                        )?;
                    }
                }
            }
        } else {
            // Check if GPU rendering should be used for medium datasets
            #[cfg(feature = "gpu")]
            let use_gpu_rendering = self.render.enable_gpu && total_points >= GPU_THRESHOLD;

            #[cfg(feature = "gpu")]
            if use_gpu_rendering {
                // Initialize GPU renderer
                match pollster::block_on(GpuRenderer::new()) {
                    Ok(mut gpu_renderer) => {
                        log::info!(
                            "Using GPU rendering for {} points (threshold: {})",
                            total_points,
                            GPU_THRESHOLD
                        );
                        for series in &self.series_mgr.series {
                            self.render_series_gpu(
                                series,
                                &mut renderer,
                                &mut gpu_renderer,
                                plot_area,
                                x_min,
                                x_max,
                                y_min,
                                y_max,
                            )?;
                        }
                    }
                    Err(e) => {
                        log::warn!("GPU initialization failed, falling back to CPU: {}", e);
                        // Fall back to normal rendering
                        for series in &self.series_mgr.series {
                            self.render_series_normal(
                                series,
                                &mut renderer,
                                plot_area,
                                x_min,
                                x_max,
                                y_min,
                                y_max,
                            )?;
                        }
                    }
                }
            } else {
                // Use normal rendering for smaller datasets
                for series in &self.series_mgr.series {
                    self.render_series_normal(
                        series,
                        &mut renderer,
                        plot_area,
                        x_min,
                        x_max,
                        y_min,
                        y_max,
                    )?;
                }
            }

            #[cfg(not(feature = "gpu"))]
            {
                // Use normal rendering for smaller datasets (no GPU feature)
                for series in &self.series_mgr.series {
                    self.render_series_normal(
                        series,
                        &mut renderer,
                        plot_area,
                        x_min,
                        x_max,
                        y_min,
                        y_max,
                    )?;
                }
            }
        }

        // Draw annotations after data series but before legend
        if !self.annotations.is_empty() {
            renderer.draw_annotations(
                &self.annotations,
                plot_area,
                x_min,
                x_max,
                y_min,
                y_max,
                self.display.config.figure.dpi,
            )?;
        }

        // Collect legend items, including grouped-series collapse behavior.
        let legend_items = self.collect_legend_items();

        // Draw legend if there are labeled series and legend is enabled
        if !legend_items.is_empty() && self.layout.legend.enabled {
            // Convert old LegendConfig to new Legend type with DPI scaling
            let mut legend = self.layout.legend.to_legend();
            // Scale legend font size from points to pixels for proper DPI handling
            legend.font_size = pt_to_px(legend.font_size, self.display.config.figure.dpi);

            // Collect data bounding boxes for best position algorithm
            let data_bboxes: Vec<(f32, f32, f32, f32)> =
                if matches!(legend.position, LegendPosition::Best) {
                    let marker_radius = 4.0_f32; // Approximate marker radius in pixels
                    self.series_mgr
                        .series
                        .iter()
                        .flat_map(|series| {
                            match &series.series_type {
                                SeriesType::Line { x_data, y_data }
                                | SeriesType::Scatter { x_data, y_data }
                                | SeriesType::ErrorBars { x_data, y_data, .. }
                                | SeriesType::ErrorBarsXY { x_data, y_data, .. } => {
                                    let x_data = x_data.resolve(0.0);
                                    let y_data = y_data.resolve(0.0);
                                    x_data
                                        .iter()
                                        .zip(y_data.iter())
                                        .map(|(&x, &y)| {
                                            let (px, py) =
                                                crate::render::skia::map_data_to_pixels_scaled(
                                                    x,
                                                    y,
                                                    x_min,
                                                    x_max,
                                                    y_min,
                                                    y_max,
                                                    plot_area,
                                                    &self.layout.x_scale,
                                                    &self.layout.y_scale,
                                                );
                                            // Create bounding box around each point
                                            (
                                                px - marker_radius,
                                                py - marker_radius,
                                                px + marker_radius,
                                                py + marker_radius,
                                            )
                                        })
                                        .collect::<Vec<_>>()
                                }
                                _ => vec![], // Skip bar charts, histograms, etc. for now
                            }
                        })
                        .collect()
                } else {
                    vec![]
                };

            // Use new legend rendering with proper handles
            let bbox_slice = if data_bboxes.is_empty() {
                None
            } else {
                Some(data_bboxes.as_slice())
            };
            renderer.draw_legend_full(&legend_items, &legend, plot_area, bbox_slice)?;
        }

        // Save as PNG
        renderer.save_png(path)?;

        Ok(())
    }

    /// Save the plot to a PNG file with custom dimensions
    pub fn save_with_size<P: AsRef<Path>>(
        mut self,
        path: P,
        width: u32,
        height: u32,
    ) -> Result<()> {
        // Update dimensions
        self.display.dimensions = (width, height);
        self.save(path)
    }

    /// Export to SVG format
    ///
    /// Renders the plot to a vector SVG file with full visual fidelity.
    /// Includes axes, grid, tick marks, labels, legend, and all data series.
    pub fn export_svg<P: AsRef<Path>>(self, path: P) -> Result<()> {
        let svg_content = self.render_to_svg()?;
        std::fs::write(path, svg_content).map_err(PlottingError::IoError)?;
        Ok(())
    }

    /// Render the plot to an SVG string
    ///
    /// Returns the complete SVG content as a string. This can be saved to a file
    /// or converted to other formats like PDF.
    pub fn render_to_svg(&self) -> Result<String> {
        use crate::axes::TickLayout;
        use crate::export::SvgRenderer;
        use crate::render::skia::map_data_to_pixels;

        let (width, height) = self.display.dimensions;
        let width = width as f32;
        let height = height as f32;

        let mut svg = SvgRenderer::new(width, height);
        let dpi_scale = self.display.config.figure.dpi / 100.0;
        svg.set_dpi_scale(dpi_scale);
        svg.set_text_engine_mode(self.display.text_engine);

        // Calculate data bounds
        let (x_min, x_max, y_min, y_max) = self.calculate_data_bounds()?;

        // Use the same content-driven layout path as PNG rendering.
        let content = self.create_plot_content(y_min, y_max);
        let mut measurement_renderer = SkiaRenderer::new(
            self.display.dimensions.0,
            self.display.dimensions.1,
            self.display.theme.clone(),
        )?;
        measurement_renderer.set_text_engine_mode(self.display.text_engine);
        measurement_renderer.set_dpi_scale(dpi_scale);
        let measured_dimensions = self.measure_layout_text(
            &measurement_renderer,
            &content,
            self.display.config.figure.dpi,
        )?;
        let measurements = measured_dimensions.as_ref();

        let layout_config = match &self.display.config.margins {
            MarginConfig::ContentDriven {
                edge_buffer,
                center_plot,
            } => LayoutConfig {
                edge_buffer_pt: *edge_buffer,
                center_plot: *center_plot,
                ..Default::default()
            },
            _ => LayoutConfig::default(),
        };
        let calculator = LayoutCalculator::new(layout_config);
        let layout = calculator.compute(
            self.display.dimensions,
            &content,
            &self.display.config.typography,
            &self.display.config.spacing,
            self.display.config.figure.dpi,
            measurements,
        );
        let plot_left = layout.plot_area.left;
        let plot_right = layout.plot_area.right;
        let plot_top = layout.plot_area.top;
        let plot_bottom = layout.plot_area.bottom;
        let plot_width = layout.plot_area.width();
        let plot_height = layout.plot_area.height();

        // Create plot area rectangle for coordinate mapping
        let plot_area = tiny_skia::Rect::from_ltrb(plot_left, plot_top, plot_right, plot_bottom)
            .ok_or(PlottingError::InvalidData {
                message: "Invalid plot area from layout".to_string(),
                position: None,
            })?;

        // Draw background
        svg.draw_rectangle(0.0, 0.0, width, height, self.display.theme.background, true);

        // Check if we have a bar chart (need special X-axis handling)
        let bar_categories: Option<&Vec<String>> = self.series_mgr.series.iter().find_map(|s| {
            if let SeriesType::Bar { categories, .. } = &s.series_type {
                Some(categories)
            } else {
                None
            }
        });

        // Compute Y-axis tick layout (fix parameter order: pixel_top then pixel_bottom)
        let y_tick_layout = TickLayout::compute_y_axis(
            y_min,
            y_max,
            plot_top,
            plot_bottom,
            &self.layout.y_scale,
            6,
        );

        // Draw grid lines (only horizontal for bar charts) - using unified GridStyle
        // Skip grid for non-Cartesian plots (Pie, Radar, Polar)
        if self.layout.grid_style.visible && self.needs_cartesian_axes() {
            let grid_color = self.layout.grid_style.effective_color();
            let grid_width_px = self.line_width_px(self.layout.grid_style.line_width);
            if bar_categories.is_some() {
                // For bar charts, only draw horizontal grid lines
                svg.draw_grid(
                    &[], // no vertical grid lines for bar charts
                    &y_tick_layout.pixel_positions,
                    plot_left,
                    plot_right,
                    plot_top,
                    plot_bottom,
                    grid_color,
                    self.layout.grid_style.line_style.clone(),
                    grid_width_px,
                );
            } else {
                // For other charts, compute X-axis ticks and draw full grid
                let x_tick_layout = TickLayout::compute(
                    x_min,
                    x_max,
                    plot_left,
                    plot_right,
                    &self.layout.x_scale,
                    7,
                );
                svg.draw_grid(
                    &x_tick_layout.pixel_positions,
                    &y_tick_layout.pixel_positions,
                    plot_left,
                    plot_right,
                    plot_top,
                    plot_bottom,
                    grid_color,
                    self.layout.grid_style.line_style.clone(),
                    grid_width_px,
                );
            }
        }

        // Draw plot area border
        svg.draw_rectangle(
            plot_left,
            plot_top,
            plot_width,
            plot_height,
            self.display.theme.foreground,
            false,
        );

        let tick_size_px = pt_to_px(
            self.display.config.typography.tick_size(),
            self.display.config.figure.dpi,
        );

        // Draw axes and tick labels
        if let Some(categories) = bar_categories {
            // Bar chart: draw axes with category labels
            svg.draw_axes(
                plot_left,
                plot_right,
                plot_top,
                plot_bottom,
                &[], // no X ticks for bar chart
                &y_tick_layout.pixel_positions,
                self.display.theme.foreground,
                true,
            );

            // Draw Y-axis tick labels
            svg.draw_tick_labels(
                &[],
                &[],
                &y_tick_layout.pixel_positions,
                &y_tick_layout.labels,
                plot_left,
                plot_right,
                plot_top,
                plot_bottom,
                layout.xtick_baseline_y,
                layout.ytick_right_x,
                self.display.theme.foreground,
                tick_size_px,
            )?;

            // Draw category labels on X-axis
            let num_categories = categories.len();
            for (i, category) in categories.iter().enumerate() {
                let x = plot_left + (i as f32 + 0.5) * (plot_width / num_categories as f32);
                svg.draw_text_centered(
                    category,
                    x,
                    layout.xtick_baseline_y,
                    tick_size_px,
                    self.display.theme.foreground,
                )?;
            }
        } else {
            // Normal chart: draw axes with numeric labels
            let x_tick_layout =
                TickLayout::compute(x_min, x_max, plot_left, plot_right, &self.layout.x_scale, 7);
            svg.draw_axes(
                plot_left,
                plot_right,
                plot_top,
                plot_bottom,
                &x_tick_layout.pixel_positions,
                &y_tick_layout.pixel_positions,
                self.display.theme.foreground,
                true,
            );
            svg.draw_tick_labels(
                &x_tick_layout.pixel_positions,
                &x_tick_layout.labels,
                &y_tick_layout.pixel_positions,
                &y_tick_layout.labels,
                plot_left,
                plot_right,
                plot_top,
                plot_bottom,
                layout.xtick_baseline_y,
                layout.ytick_right_x,
                self.display.theme.foreground,
                tick_size_px,
            )?;
        }

        // Create clip path for data
        let clip_id = svg.add_clip_rect(plot_left, plot_top, plot_width, plot_height);
        svg.start_clip_group(&clip_id);

        // Collect legend items, including grouped-series collapse behavior.
        let legend_items = self.collect_legend_items();

        // Render each series
        for (idx, series) in self.series_mgr.series.iter().enumerate() {
            let default_color = self.display.theme.get_color(idx);
            let color = series.color.unwrap_or(default_color);
            let line_width = series.line_width.unwrap_or(self.display.theme.line_width);
            let line_style = series.line_style.clone().unwrap_or(LineStyle::Solid);

            match &series.series_type {
                SeriesType::Line { x_data, y_data } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    let points: Vec<(f32, f32)> = x_data
                        .iter()
                        .zip(y_data.iter())
                        .map(|(&x, &y)| {
                            map_data_to_pixels(x, y, x_min, x_max, y_min, y_max, plot_area)
                        })
                        .collect();

                    svg.draw_polyline(&points, color, line_width, line_style);
                }
                SeriesType::Scatter { x_data, y_data } => {
                    let x_data = x_data.resolve(0.0);
                    let y_data = y_data.resolve(0.0);
                    let marker_size = series.marker_size.unwrap_or(6.0);
                    for (&x, &y) in x_data.iter().zip(y_data.iter()) {
                        let (px, py) =
                            map_data_to_pixels(x, y, x_min, x_max, y_min, y_max, plot_area);
                        svg.draw_marker(px, py, marker_size, color);
                    }
                }
                SeriesType::Bar { categories, values } => {
                    let values = values.resolve(0.0);
                    let num_bars = categories.len();
                    let bar_width = plot_width / num_bars as f32 * 0.7;
                    let bar_gap = plot_width / num_bars as f32 * 0.15;

                    for (i, &value) in values.iter().enumerate() {
                        let bar_x = plot_left + (i as f32 + 0.5) * (plot_width / num_bars as f32)
                            - bar_width / 2.0;
                        let (_, py) =
                            map_data_to_pixels(0.0, value, x_min, x_max, y_min, y_max, plot_area);
                        let (_, py_zero) =
                            map_data_to_pixels(0.0, 0.0, x_min, x_max, y_min, y_max, plot_area);
                        let bar_height = (py - py_zero).abs();
                        let bar_y = py.min(py_zero);

                        svg.draw_rectangle(bar_x, bar_y, bar_width, bar_height, color, true);
                    }
                }
                SeriesType::Histogram { data: _, config: _ } => {
                    // Histogram rendering would need pre-computed bins/values
                    // For now, skip in SVG output - histograms are complex
                }
                _ => {
                    // Other series types rendered as scatter for now
                }
            }
        }

        svg.end_group(); // End clip group

        // Draw title/xlabel/ylabel using layout-computed positions.
        if let Some(ref pos) = layout.title_pos {
            if let Some(ref title) = self.display.title {
                let title_str = title.resolve(0.0);
                svg.draw_text_centered(
                    &title_str,
                    pos.x,
                    pos.y,
                    pos.size,
                    self.display.theme.foreground,
                )?;
            }
        }
        if let Some(ref pos) = layout.xlabel_pos {
            if let Some(ref xlabel) = self.display.xlabel {
                let xlabel_str = xlabel.resolve(0.0);
                svg.draw_text_centered(
                    &xlabel_str,
                    pos.x,
                    pos.y,
                    pos.size,
                    self.display.theme.foreground,
                )?;
            }
        }
        if let Some(ref pos) = layout.ylabel_pos {
            if let Some(ref ylabel) = self.display.ylabel {
                let ylabel_str = ylabel.resolve(0.0);
                svg.draw_text_rotated(
                    &ylabel_str,
                    pos.x,
                    pos.y,
                    pos.size,
                    self.display.theme.foreground,
                    -90.0,
                )?;
            }
        }

        // Draw legend if we have labeled series and legend is enabled
        if !legend_items.is_empty() && self.layout.legend.enabled {
            // Convert old LegendConfig to new Legend type
            let legend = self.layout.legend.to_legend();
            // Use new legend rendering with proper handles
            let plot_bounds = (plot_left, plot_top, plot_right, plot_bottom);
            svg.draw_legend_full(&legend_items, &legend, plot_bounds, None)?;
        }

        Ok(svg.to_svg_string())
    }

    /// Export to PDF format (requires `pdf` feature)
    ///
    /// Creates a vector-based PDF file with the plot. PDF export produces
    /// publication-quality output with text rendered as vectors.
    ///
    /// # Example
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .line(&[0.0, 1.0, 2.0], &[0.0, 1.0, 4.0])
    ///     .title("My Plot")
    ///     .save_pdf("plot.pdf")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    #[cfg(feature = "pdf")]
    pub fn save_pdf<P: AsRef<Path>>(self, path: P) -> Result<()> {
        self.save_pdf_with_size(path, None)
    }

    /// Export to PDF format with custom page size in millimeters
    ///
    /// Uses SVG → PDF pipeline for high-quality vector output with full visual fidelity.
    /// This includes grid lines, tick marks, rotated labels, and legends.
    ///
    /// # Arguments
    /// * `path` - Output file path
    /// * `size` - Optional (width_mm, height_mm). If None, uses 160x120mm.
    #[cfg(feature = "pdf")]
    pub fn save_pdf_with_size<P: AsRef<Path>>(
        mut self,
        path: P,
        size: Option<(f64, f64)>,
    ) -> Result<()> {
        use crate::export::svg_to_pdf::page_sizes;

        // Calculate pixel dimensions from mm (at 96 DPI)
        let (width_mm, height_mm) = size.unwrap_or(page_sizes::PLOT_DEFAULT);
        let width_px = page_sizes::mm_to_px(width_mm) as u32;
        let height_px = page_sizes::mm_to_px(height_mm) as u32;

        // Update plot dimensions
        self.display.dimensions = (width_px, height_px);

        // Render to SVG
        let svg_content = self.render_to_svg()?;

        // Convert SVG to PDF
        let pdf_data = crate::export::svg_to_pdf(&svg_content)?;

        // Write PDF to file
        std::fs::write(path, pdf_data).map_err(PlottingError::IoError)?;

        Ok(())
    }

    // ==========================================================================
    // Animation Methods (feature-gated)
    // ==========================================================================

    /// Render a single frame for animation capture
    ///
    /// Returns the raw RGB pixel data suitable for encoding.
    /// This is used internally by the animation recording system.
    ///
    /// # Arguments
    ///
    /// * `width` - Frame width in pixels
    /// * `height` - Frame height in pixels
    ///
    /// # Returns
    ///
    /// RGB pixel data as a `Vec<u8>` (width * height * 3 bytes)
    #[cfg(feature = "animation")]
    pub fn render_frame(&self, width: u32, height: u32) -> Result<Vec<u8>> {
        // Create a sized version of the plot
        let sized_plot = self.clone().size_px(width, height);

        // Render to RGBA image
        let image = sized_plot.render()?;
        let rgba_data = &image.pixels;

        // Convert RGBA to RGB
        let pixels = (width * height) as usize;
        let mut rgb_data = vec![0u8; pixels * 3];

        for i in 0..pixels {
            rgb_data[i * 3] = rgba_data[i * 4]; // R
            rgb_data[i * 3 + 1] = rgba_data[i * 4 + 1]; // G
            rgb_data[i * 3 + 2] = rgba_data[i * 4 + 2]; // B
        }

        Ok(rgb_data)
    }
}

impl Default for Plot {
    fn default() -> Self {
        Self::new()
    }
}

/// Builder for scoped grouped-series construction.
///
/// Created via [`Plot::group`], this builder applies shared style defaults
/// to all series added within the group.
#[derive(Clone, Debug)]
pub struct SeriesGroupBuilder {
    plot: Plot,
    style: builder::SeriesStyle,
    group_id: usize,
    resolved_auto_color: Option<Color>,
}

impl SeriesGroupBuilder {
    fn new(mut plot: Plot) -> Self {
        let group_id = plot.register_series_group();
        Self {
            plot,
            style: builder::SeriesStyle::default(),
            group_id,
            resolved_auto_color: None,
        }
    }

    fn finalize(self) -> Plot {
        self.plot
    }

    /// Set the label used for the single legend entry of this group.
    pub fn group_label<S: Into<String>>(mut self, label: S) -> Self {
        self.plot
            .set_series_group_label(self.group_id, label.into());
        self
    }

    /// Set shared color applied to all group member series.
    pub fn color(mut self, color: Color) -> Self {
        self.style.color = Some(color);
        self
    }

    /// Set shared line width applied to all group member series.
    pub fn line_width(mut self, width: f32) -> Self {
        self.style.line_width = Some(width.max(0.1));
        self
    }

    /// Set shared line style applied to all group member series.
    pub fn line_style(mut self, style: LineStyle) -> Self {
        self.style.line_style = Some(style);
        self
    }

    /// Set shared alpha/transparency applied to all group member series.
    pub fn alpha(mut self, alpha: f32) -> Self {
        self.style.alpha = Some(alpha.clamp(0.0, 1.0));
        self
    }

    /// Add a line series to the current group.
    pub fn line<X, Y>(mut self, x_data: &X, y_data: &Y) -> Self
    where
        X: NumericData1D,
        Y: NumericData1D,
    {
        let x_vec = match collect_numeric_data_1d(x_data, self.plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                self.plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let y_vec = match collect_numeric_data_1d(y_data, self.plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                self.plot.set_pending_ingestion_error(err);
                vec![]
            }
        };

        let mut style = self.style.clone();
        let mut consume_palette_index = true;
        if self.style.color.is_none() {
            if let Some(color) = self.resolved_auto_color {
                style.color = Some(color);
                consume_palette_index = false;
            }
        }

        self.plot = self.plot.add_line_series_grouped(
            PlotData::Static(x_vec),
            PlotData::Static(y_vec),
            &crate::plots::basic::LineConfig::default(),
            style,
            Some(self.group_id),
            consume_palette_index,
        );

        if self.style.color.is_none() && self.resolved_auto_color.is_none() {
            self.resolved_auto_color = self.plot.series_mgr.series.last().and_then(|s| s.color);
        }
        self
    }

    /// Add a scatter series to the current group.
    pub fn scatter<X, Y>(mut self, x_data: &X, y_data: &Y) -> Self
    where
        X: NumericData1D,
        Y: NumericData1D,
    {
        let x_vec = match collect_numeric_data_1d(x_data, self.plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                self.plot.set_pending_ingestion_error(err);
                vec![]
            }
        };
        let y_vec = match collect_numeric_data_1d(y_data, self.plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                self.plot.set_pending_ingestion_error(err);
                vec![]
            }
        };

        let mut style = self.style.clone();
        let mut consume_palette_index = true;
        if self.style.color.is_none() {
            if let Some(color) = self.resolved_auto_color {
                style.color = Some(color);
                consume_palette_index = false;
            }
        }

        self.plot = self.plot.add_scatter_series_grouped(
            PlotData::Static(x_vec),
            PlotData::Static(y_vec),
            &crate::plots::basic::ScatterConfig::default(),
            style,
            Some(self.group_id),
            consume_palette_index,
        );

        if self.style.color.is_none() && self.resolved_auto_color.is_none() {
            self.resolved_auto_color = self.plot.series_mgr.series.last().and_then(|s| s.color);
        }
        self
    }

    /// Add a bar series to the current group.
    pub fn bar<S, V>(mut self, categories: &[S], values: &V) -> Self
    where
        S: ToString,
        V: NumericData1D,
    {
        let cat_vec: Vec<String> = categories.iter().map(ToString::to_string).collect();
        let val_vec = match collect_numeric_data_1d(values, self.plot.null_policy) {
            Ok(values) => values,
            Err(err) => {
                self.plot.set_pending_ingestion_error(err);
                vec![]
            }
        };

        let mut style = self.style.clone();
        let mut consume_palette_index = true;
        if self.style.color.is_none() {
            if let Some(color) = self.resolved_auto_color {
                style.color = Some(color);
                consume_palette_index = false;
            }
        }

        self.plot = self.plot.add_bar_series_grouped(
            cat_vec,
            PlotData::Static(val_vec),
            &crate::plots::basic::BarConfig::default(),
            style,
            Some(self.group_id),
            consume_palette_index,
        );

        if self.style.color.is_none() && self.resolved_auto_color.is_none() {
            self.resolved_auto_color = self.plot.series_mgr.series.last().and_then(|s| s.color);
        }
        self
    }
}

/// Builder for configuring individual plot series
pub struct PlotSeriesBuilder {
    plot: Plot,
    series: PlotSeries,
}

impl PlotSeriesBuilder {
    fn new(plot: Plot, series: PlotSeries) -> Self {
        Self { plot, series }
    }

    /// Set series label for legend
    ///
    /// Labels identify this series in the plot legend. **Important**: Labels are only
    /// displayed when the legend is explicitly enabled using one of:
    /// - [`Plot::legend_best()`] - auto-positioned legend (matplotlib-style, recommended)
    /// - [`Plot::legend()`] - legacy positioning
    /// - [`Plot::legend_position()`] - explicit position control
    ///
    /// Without enabling the legend, labels are stored but not rendered.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// // Legend with auto-positioning (recommended)
    /// Plot::new()
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .label("Quadratic")
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 2.0, 3.0])
    ///     .label("Linear")
    ///     .end_series()
    ///     .legend_best()  // Required to display labels
    ///     .save("labeled.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn label<S: Into<String>>(mut self, label: S) -> Self {
        self.series.label = Some(label.into());
        self
    }

    /// Set series color
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .color(Color::RED)
    ///     .line(&[1.0, 2.0, 3.0], &[2.0, 4.0, 6.0])
    ///     .color(Color::from_hex("#00FF00").unwrap())
    ///     .end_series()
    ///     .save("colored_lines.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn color(mut self, color: Color) -> Self {
        self.series.color = Some(color);
        self
    }

    /// Set line width
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .line_width(3.0)  // Thick line
    ///     .line(&[1.0, 2.0, 3.0], &[0.5, 2.0, 4.5])
    ///     .line_width(1.0)  // Thin line
    ///     .save("line_widths.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn width(mut self, width: f32) -> Self {
        self.series.line_width = Some(width.max(0.1));
        self
    }

    /// Set line style
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .style(LineStyle::Dashed)
    ///     .line(&[1.0, 2.0, 3.0], &[2.0, 4.0, 6.0])
    ///     .style(LineStyle::Dotted)
    ///     .end_series()
    ///     .save("line_styles.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn style(mut self, style: LineStyle) -> Self {
        self.series.line_style = Some(style);
        self
    }

    /// Set marker style (for scatter plots)
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .marker(MarkerStyle::Circle)
    ///     .scatter(&[1.0, 2.0, 3.0], &[2.0, 4.0, 6.0])
    ///     .marker(MarkerStyle::Square)
    ///     .end_series()
    ///     .save("markers.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn marker(mut self, marker: MarkerStyle) -> Self {
        self.series.marker_style = Some(marker);
        self
    }

    /// Set marker size (for scatter plots)
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .marker_size(15.0)  // Large markers
    ///     .scatter(&[1.0, 2.0, 3.0], &[2.0, 4.0, 6.0])
    ///     .marker_size(5.0)   // Small markers
    ///     .end_series()
    ///     .save("marker_sizes.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn marker_size(mut self, size: f32) -> Self {
        self.series.marker_size = Some(size.max(0.1));
        self
    }

    /// Set transparency
    ///
    /// Values range from 0.0 (fully transparent) to 1.0 (fully opaque).
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// Plot::new()
    ///     .scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .alpha(0.5)  // Semi-transparent
    ///     .end_series()
    ///     .save("transparent.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn alpha(mut self, alpha: f32) -> Self {
        self.series.alpha = Some(alpha.clamp(0.0, 1.0));
        self
    }

    // ========== Error Bar Modifier Methods ==========

    /// Attach symmetric Y error bars to this series
    ///
    /// Error bars are drawn as vertical lines centered on each data point,
    /// extending `yerr` distance above and below.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    /// let y = vec![2.3, 3.5, 4.1, 5.8, 6.2];
    /// let yerr = vec![0.3, 0.4, 0.25, 0.5, 0.35];
    ///
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .with_yerr(&yerr)
    ///     .label("Measurement")
    ///     .end_series()
    ///     .legend_best()
    ///     .save("line_with_errors.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn with_yerr<E: NumericData1D>(mut self, errors: &E) -> Self {
        match collect_numeric_data_1d(errors, self.plot.null_policy) {
            Ok(values) => {
                self.series.y_errors = Some(ErrorValues::symmetric(values));
            }
            Err(err) => {
                self.plot.set_pending_ingestion_error(err);
            }
        }
        self
    }

    /// Attach symmetric X error bars to this series
    ///
    /// Error bars are drawn as horizontal lines centered on each data point,
    /// extending `xerr` distance left and right.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    /// let y = vec![2.3, 3.5, 4.1, 5.8, 6.2];
    /// let xerr = vec![0.1, 0.15, 0.1, 0.2, 0.15];
    ///
    /// Plot::new()
    ///     .scatter(&x, &y)
    ///     .with_xerr(&xerr)
    ///     .label("Data Points")
    ///     .end_series()
    ///     .save("scatter_with_xerr.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn with_xerr<E: NumericData1D>(mut self, errors: &E) -> Self {
        match collect_numeric_data_1d(errors, self.plot.null_policy) {
            Ok(values) => {
                self.series.x_errors = Some(ErrorValues::symmetric(values));
            }
            Err(err) => {
                self.plot.set_pending_ingestion_error(err);
            }
        }
        self
    }

    /// Attach asymmetric Y error bars to this series
    ///
    /// Error bars have different lengths above and below each point.
    ///
    /// # Arguments
    /// * `lower` - Error values extending below each point
    /// * `upper` - Error values extending above each point
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x = vec![1.0, 2.0, 3.0];
    /// let y = vec![5.0, 7.0, 6.0];
    /// let lower = vec![0.5, 0.3, 0.4];  // Extends down
    /// let upper = vec![0.8, 1.0, 0.6];  // Extends up
    ///
    /// Plot::new()
    ///     .line(&x, &y)
    ///     .with_yerr_asymmetric(&lower, &upper)
    ///     .label("Asymmetric Errors")
    ///     .end_series()
    ///     .save("asymmetric_errors.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn with_yerr_asymmetric<E1, E2>(mut self, lower: &E1, upper: &E2) -> Self
    where
        E1: NumericData1D,
        E2: NumericData1D,
    {
        let lower_values = collect_numeric_data_1d(lower, self.plot.null_policy);
        let upper_values = collect_numeric_data_1d(upper, self.plot.null_policy);
        match (lower_values, upper_values) {
            (Ok(lower), Ok(upper)) => {
                self.series.y_errors = Some(ErrorValues::asymmetric(lower, upper));
            }
            (Err(err), _) | (_, Err(err)) => {
                self.plot.set_pending_ingestion_error(err);
            }
        }
        self
    }

    /// Attach asymmetric X error bars to this series
    ///
    /// Error bars have different lengths left and right of each point.
    ///
    /// # Arguments
    /// * `left` - Error values extending left of each point
    /// * `right` - Error values extending right of each point
    pub fn with_xerr_asymmetric<E1, E2>(mut self, left: &E1, right: &E2) -> Self
    where
        E1: NumericData1D,
        E2: NumericData1D,
    {
        let left_values = collect_numeric_data_1d(left, self.plot.null_policy);
        let right_values = collect_numeric_data_1d(right, self.plot.null_policy);
        match (left_values, right_values) {
            (Ok(left), Ok(right)) => {
                self.series.x_errors = Some(ErrorValues::asymmetric(left, right));
            }
            (Err(err), _) | (_, Err(err)) => {
                self.plot.set_pending_ingestion_error(err);
            }
        }
        self
    }

    /// Configure error bar appearance
    ///
    /// Allows customization of cap size, line width, and color for attached error bars.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    /// use ruviz::plots::error::ErrorBarConfig;
    ///
    /// Plot::new()
    ///     .line(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0])
    ///     .with_yerr(&[0.3, 0.4, 0.35])
    ///     .error_config(ErrorBarConfig::default().cap_size(0.15).line_width(2.0))
    ///     .end_series()
    ///     .save("custom_errors.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn error_config(mut self, config: ErrorBarConfig) -> Self {
        self.series.error_config = Some(config);
        self
    }

    // ========== End Error Bar Modifier Methods ==========

    /// Finish configuring this series and return to the main Plot
    ///
    /// This consumes the builder and adds the series to the plot.
    ///
    /// **Deprecated**: Series finalize automatically via `Into<Plot>`.
    /// Use `.save()` directly or `.into()` for explicit conversion.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// // Preferred: Use .save() directly (auto-finalizes)
    /// Plot::new()
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .label("Series 1")
    ///     .color(Color::BLUE)
    ///     .title("My Plot")
    ///     .save("plot.png")?;
    ///
    /// // Or use .into() for explicit conversion
    /// let plot: Plot = Plot::new()
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .into();
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    #[deprecated(
        since = "0.9.0",
        note = "Not needed - series finalize automatically. Use .save() directly or .into() for explicit conversion."
    )]
    pub fn end_series(mut self) -> Plot {
        // Auto-assign color if none specified
        if self.series.color.is_none() {
            self.series.color = Some(
                self.plot
                    .display
                    .theme
                    .get_color(self.plot.series_mgr.auto_color_index),
            );
            self.plot.series_mgr.auto_color_index += 1;
        }

        self.plot.series_mgr.series.push(self.series);
        self.plot
    }
}

// Implement Deref so methods can be chained through the builder
impl std::ops::Deref for PlotSeriesBuilder {
    type Target = Plot;

    fn deref(&self) -> &Self::Target {
        &self.plot
    }
}

/// Enable implicit conversion from PlotSeriesBuilder to Plot
///
/// This implementation allows PlotSeriesBuilder to be converted to Plot
/// automatically when needed, eliminating the need for explicit `.end_series()` calls.
///
/// # Example
///
/// ```rust,no_run
/// use ruviz::prelude::*;
/// use ruviz::core::Result;
///
/// // Implicit conversion via .into()
/// let plot: Plot = Plot::new()
///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
///     .color(Color::BLUE)
///     .into();
///
/// // Implicit conversion in function parameters
/// fn save_plot(p: impl Into<Plot>) -> Result<()> {
///     p.into().save("plot.png")
/// }
/// ```
impl From<PlotSeriesBuilder> for Plot {
    fn from(builder: PlotSeriesBuilder) -> Plot {
        #[allow(deprecated)]
        builder.end_series()
    }
}

impl builder::IntoPlot for PlotSeriesBuilder {
    fn into_plot(self) -> Plot {
        self.into()
    }

    fn as_plot(&self) -> &Plot {
        &self.plot
    }
}

// Implement most Plot methods for PlotSeriesBuilder to allow chaining
#[allow(deprecated)]
impl PlotSeriesBuilder {
    /// Continue with a new line series
    pub fn line<X, Y>(self, x_data: &X, y_data: &Y) -> PlotBuilder<crate::plots::basic::LineConfig>
    where
        X: NumericData1D,
        Y: NumericData1D,
    {
        self.end_series().line(x_data, y_data)
    }

    /// Continue with a new scatter series
    pub fn scatter<X, Y>(
        self,
        x_data: &X,
        y_data: &Y,
    ) -> PlotBuilder<crate::plots::basic::ScatterConfig>
    where
        X: NumericData1D,
        Y: NumericData1D,
    {
        self.end_series().scatter(x_data, y_data)
    }

    /// Continue with a new bar series
    pub fn bar<S, V>(
        self,
        categories: &[S],
        values: &V,
    ) -> PlotBuilder<crate::plots::basic::BarConfig>
    where
        S: ToString,
        V: NumericData1D,
    {
        self.end_series().bar(categories, values)
    }

    /// Continue with a grouped-series scope after finalizing the current series.
    pub fn group<F>(self, f: F) -> Plot
    where
        F: FnOnce(SeriesGroupBuilder) -> SeriesGroupBuilder,
    {
        self.end_series().group(f)
    }

    /// Continue with a new streaming line series
    pub fn line_streaming(self, stream: &StreamingXY) -> PlotSeriesBuilder {
        self.end_series().line_streaming(stream)
    }

    /// Continue with a new streaming scatter series
    pub fn scatter_streaming(self, stream: &StreamingXY) -> PlotSeriesBuilder {
        self.end_series().scatter_streaming(stream)
    }

    /// Continue with a new error bar series (Y errors only)
    ///
    /// This enables chaining multiple error bar series without ownership issues.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    /// let y1 = vec![2.3, 3.5, 4.1, 5.8, 6.2];
    /// let yerr1 = vec![0.3, 0.4, 0.25, 0.5, 0.35];
    /// let y2 = vec![1.8, 2.9, 3.5, 4.2, 5.5];
    /// let yerr2 = vec![0.3, 0.4, 0.3, 0.5, 0.4];
    ///
    /// Plot::new()
    ///     .error_bars(&x, &y1, &yerr1)
    ///     .label("Dataset 1")
    ///     .error_bars(&x, &y2, &yerr2)  // Continuation - chains correctly
    ///     .label("Dataset 2")
    ///     .end_series()
    ///     .legend_best()
    ///     .save("multiple_errorbars.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn error_bars<X, Y, E>(self, x_data: &X, y_data: &Y, y_errors: &E) -> PlotSeriesBuilder
    where
        X: NumericData1D,
        Y: NumericData1D,
        E: NumericData1D,
    {
        self.end_series().error_bars(x_data, y_data, y_errors)
    }

    /// Continue with a new error bar series (both X and Y errors)
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// let x = vec![1.0, 2.0, 3.0];
    /// let y = vec![2.0, 4.0, 3.0];
    /// let xerr = vec![0.1, 0.15, 0.1];
    /// let yerr = vec![0.3, 0.4, 0.35];
    ///
    /// Plot::new()
    ///     .error_bars_xy(&x, &y, &xerr, &yerr)
    ///     .label("Full Uncertainty")
    ///     .end_series()
    ///     .save("xy_errors.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn error_bars_xy<X, Y, EX, EY>(
        self,
        x_data: &X,
        y_data: &Y,
        x_errors: &EX,
        y_errors: &EY,
    ) -> PlotSeriesBuilder
    where
        X: NumericData1D,
        Y: NumericData1D,
        EX: NumericData1D,
        EY: NumericData1D,
    {
        self.end_series()
            .error_bars_xy(x_data, y_data, x_errors, y_errors)
    }

    /// Set plot title
    pub fn title<S: Into<String>>(mut self, title: S) -> Self {
        self.plot.display.title = Some(data::PlotText::Static(title.into()));
        self
    }

    /// Set the plot title as a reactive signal.
    pub fn title_signal(mut self, title: impl Into<data::PlotText>) -> Self {
        self.plot.display.title = Some(title.into());
        self
    }

    /// Set X-axis label
    pub fn xlabel<S: Into<String>>(mut self, label: S) -> Self {
        self.plot.display.xlabel = Some(data::PlotText::Static(label.into()));
        self
    }

    /// Set X-axis label as a reactive signal.
    pub fn xlabel_signal(mut self, label: impl Into<data::PlotText>) -> Self {
        self.plot.display.xlabel = Some(label.into());
        self
    }

    /// Set Y-axis label
    pub fn ylabel<S: Into<String>>(mut self, label: S) -> Self {
        self.plot.display.ylabel = Some(data::PlotText::Static(label.into()));
        self
    }

    /// Set null handling policy for dataframe-backed numeric inputs.
    pub fn null_policy(mut self, policy: NullPolicy) -> Self {
        self.plot = self.plot.null_policy(policy);
        self
    }

    /// Set Y-axis label as a reactive signal.
    pub fn ylabel_signal(mut self, label: impl Into<data::PlotText>) -> Self {
        self.plot.display.ylabel = Some(label.into());
        self
    }

    /// Configure legend with position
    pub fn legend(mut self, position: Position) -> Self {
        self.plot.layout.legend.enabled = true;
        self.plot.layout.legend.position = position;
        self
    }

    /// Set legend font size
    pub fn legend_font_size(mut self, size: f32) -> Self {
        self.plot.layout.legend.font_size = Some(size);
        self
    }

    /// Set legend corner radius for rounded corners
    pub fn legend_corner_radius(mut self, radius: f32) -> Self {
        self.plot.layout.legend.corner_radius = Some(radius);
        self
    }

    /// Set number of legend columns
    pub fn legend_columns(mut self, columns: usize) -> Self {
        self.plot.layout.legend.columns = Some(columns.max(1));
        self
    }

    /// Enable/disable grid
    pub fn grid(mut self, enabled: bool) -> Self {
        self.plot.layout.grid_style.visible = enabled;
        self
    }

    /// Enable or disable Typst text rendering mode.
    pub fn typst(mut self, enabled: bool) -> Self {
        self.plot = self.plot.typst(enabled);
        self
    }

    /// Set DPI for export quality
    pub fn dpi(mut self, dpi: u32) -> Self {
        self.plot.display.config.figure.dpi = dpi.max(72) as f32;
        self.plot.display.dpi = dpi.max(72);
        // Update dimensions to reflect new DPI
        let (w, h) = self.plot.display.config.canvas_size();
        self.plot.display.dimensions = (w, h);
        self
    }

    /// Set maximum output resolution while preserving figure aspect ratio
    ///
    /// See [`Plot::max_resolution`] for details.
    pub fn max_resolution(mut self, max_width: u32, max_height: u32) -> Self {
        self.plot = self.plot.max_resolution(max_width, max_height);
        self
    }

    /// Render the plot
    pub fn render(self) -> Result<Image> {
        self.end_series().render()
    }

    /// Save the plot to file
    pub fn save<P: AsRef<Path>>(self, path: P) -> Result<()> {
        self.end_series().save(path)
    }

    /// Save the plot to file with custom dimensions
    pub fn save_with_size<P: AsRef<Path>>(
        mut self,
        path: P,
        width: u32,
        height: u32,
    ) -> Result<()> {
        self.plot.display.dimensions = (width, height);
        self.end_series().save(path)
    }

    /// Export to SVG
    pub fn export_svg<P: AsRef<Path>>(self, path: P) -> Result<()> {
        self.end_series().export_svg(path)
    }

    /// Render to SVG string
    pub fn render_to_svg(self) -> Result<String> {
        self.end_series().render_to_svg()
    }

    /// Export to PDF (requires `pdf` feature)
    #[cfg(feature = "pdf")]
    pub fn save_pdf<P: AsRef<Path>>(self, path: P) -> Result<()> {
        self.end_series().save_pdf(path)
    }

    /// Export to PDF with custom size (requires `pdf` feature)
    #[cfg(feature = "pdf")]
    pub fn save_pdf_with_size<P: AsRef<Path>>(
        self,
        path: P,
        size: Option<(f64, f64)>,
    ) -> Result<()> {
        self.end_series().save_pdf_with_size(path, size)
    }

    /// Automatically optimize backend selection (fluent API)
    /// Note: This ends the current series before optimizing
    pub fn auto_optimize(self) -> Plot {
        self.end_series().auto_optimize()
    }

    /// Set backend explicitly (fluent API)
    /// Note: This ends the current series before setting backend
    pub fn backend(self, backend: BackendType) -> Plot {
        self.end_series().backend(backend)
    }

    /// Enable GPU acceleration for coordinate transformations
    ///
    /// When enabled, large datasets (>5K points) will use GPU compute shaders
    /// for coordinate transformation, providing significant speedups.
    #[cfg(feature = "gpu")]
    pub fn gpu(self, enabled: bool) -> Plot {
        self.end_series().gpu(enabled)
    }

    /// Get current backend name (for testing)
    pub fn get_backend_name(&self) -> &'static str {
        self.plot.get_backend_name()
    }

    // ========== Annotation Methods for PlotSeriesBuilder ==========

    /// Add a text annotation at data coordinates
    pub fn text<S: Into<String>>(mut self, x: f64, y: f64, text: S) -> Self {
        self.plot.annotations.push(Annotation::text(x, y, text));
        self
    }

    /// Add a text annotation with custom styling
    pub fn text_styled<S: Into<String>>(
        mut self,
        x: f64,
        y: f64,
        text: S,
        style: TextStyle,
    ) -> Self {
        self.plot
            .annotations
            .push(Annotation::text_styled(x, y, text, style));
        self
    }

    /// Add an arrow annotation between two points
    pub fn arrow(mut self, x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
        self.plot
            .annotations
            .push(Annotation::arrow(x1, y1, x2, y2));
        self
    }

    /// Add an arrow annotation with custom styling
    pub fn arrow_styled(mut self, x1: f64, y1: f64, x2: f64, y2: f64, style: ArrowStyle) -> Self {
        self.plot
            .annotations
            .push(Annotation::arrow_styled(x1, y1, x2, y2, style));
        self
    }

    /// Add a horizontal reference line
    pub fn hline(mut self, y: f64) -> Self {
        self.plot.annotations.push(Annotation::hline(y));
        self
    }

    /// Add a horizontal reference line with custom styling
    pub fn hline_styled(mut self, y: f64, color: Color, width: f32, style: LineStyle) -> Self {
        self.plot
            .annotations
            .push(Annotation::hline_styled(y, color, width, style));
        self
    }

    /// Add a vertical reference line
    pub fn vline(mut self, x: f64) -> Self {
        self.plot.annotations.push(Annotation::vline(x));
        self
    }

    /// Add a vertical reference line with custom styling
    pub fn vline_styled(mut self, x: f64, color: Color, width: f32, style: LineStyle) -> Self {
        self.plot
            .annotations
            .push(Annotation::vline_styled(x, color, width, style));
        self
    }

    /// Add a rectangle annotation
    pub fn rect(mut self, x: f64, y: f64, width: f64, height: f64) -> Self {
        self.plot
            .annotations
            .push(Annotation::rectangle(x, y, width, height));
        self
    }

    /// Add a fill between two curves
    pub fn fill_between(mut self, x: &[f64], y1: &[f64], y2: &[f64]) -> Self {
        self.plot.annotations.push(Annotation::fill_between(
            x.to_vec(),
            y1.to_vec(),
            y2.to_vec(),
        ));
        self
    }

    /// Add a fill between a curve and a baseline
    pub fn fill_to_baseline(mut self, x: &[f64], y: &[f64], baseline: f64) -> Self {
        self.plot.annotations.push(Annotation::fill_to_baseline(
            x.to_vec(),
            y.to_vec(),
            baseline,
        ));
        self
    }

    /// Add a vertical span (shaded region)
    pub fn axvspan(mut self, x_min: f64, x_max: f64) -> Self {
        self.plot.annotations.push(Annotation::hspan(x_min, x_max));
        self
    }

    /// Add a horizontal span (shaded region)
    pub fn axhspan(mut self, y_min: f64, y_max: f64) -> Self {
        self.plot.annotations.push(Annotation::vspan(y_min, y_max));
        self
    }

    /// Add a generic annotation
    pub fn annotate(mut self, annotation: Annotation) -> Self {
        self.plot.annotations.push(annotation);
        self
    }

    // ========== Axis Scale Methods for PlotSeriesBuilder ==========

    /// Set X-axis scale type
    pub fn xscale(mut self, scale: AxisScale) -> Self {
        self.plot.layout.x_scale = scale;
        self
    }

    /// Set Y-axis scale type
    pub fn yscale(mut self, scale: AxisScale) -> Self {
        self.plot.layout.y_scale = scale;
        self
    }

    /// Set X-axis limits
    pub fn xlim(mut self, min: f64, max: f64) -> Self {
        if min < max && min.is_finite() && max.is_finite() {
            self.plot.layout.x_limits = Some((min, max));
        }
        self
    }

    /// Set Y-axis limits
    pub fn ylim(mut self, min: f64, max: f64) -> Self {
        if min < max && min.is_finite() && max.is_finite() {
            self.plot.layout.y_limits = Some((min, max));
        }
        self
    }
}

#[cfg(test)]
#[allow(deprecated)]
mod tests {
    use super::*;

    #[derive(Debug)]
    struct FailingIngestionData;

    impl crate::data::NumericData1D for FailingIngestionData {
        fn len(&self) -> usize {
            3
        }

        fn try_collect_f64_with_policy(
            &self,
            _null_policy: crate::data::NullPolicy,
        ) -> crate::core::Result<Vec<f64>> {
            Err(PlottingError::DataExtractionFailed {
                source: "test::failing-ingestion".to_string(),
                message: "forced ingestion failure".to_string(),
            })
        }
    }

    fn parse_svg_attr(line: &str, attr: &str) -> f32 {
        let marker = format!(r#"{}=""#, attr);
        let start = line
            .find(&marker)
            .unwrap_or_else(|| panic!("missing {} in line: {}", attr, line))
            + marker.len();
        let end = line[start..]
            .find('"')
            .unwrap_or_else(|| panic!("unterminated {} in line: {}", attr, line))
            + start;
        line[start..end]
            .parse::<f32>()
            .unwrap_or_else(|_| panic!("invalid {} value in line: {}", attr, line))
    }

    fn extract_svg_text_xy(svg: &str, text: &str) -> (f32, f32) {
        let marker = format!(">{}</text>", text);
        let line = svg
            .lines()
            .find(|line| line.contains(&marker))
            .unwrap_or_else(|| panic!("missing text node for {}", text));
        (parse_svg_attr(line, "x"), parse_svg_attr(line, "y"))
    }

    fn extract_svg_group_translate_xy(svg: &str, text: &str) -> (f32, f32) {
        let marker = format!(">{}</text>", text);
        let line = svg
            .lines()
            .find(|line| line.contains(&marker))
            .unwrap_or_else(|| panic!("missing grouped text node for {}", text));
        let transform_marker = r#"transform="translate("#;
        let start = line
            .find(transform_marker)
            .unwrap_or_else(|| panic!("missing translate transform for {}", text))
            + transform_marker.len();
        let end = line[start..]
            .find(')')
            .unwrap_or_else(|| panic!("unterminated translate transform for {}", text))
            + start;
        let coords = &line[start..end];
        let mut parts = coords.split(',');
        let x = parts
            .next()
            .unwrap_or_else(|| panic!("missing translate x for {}", text))
            .parse::<f32>()
            .unwrap_or_else(|_| panic!("invalid translate x for {}", text));
        let y = parts
            .next()
            .unwrap_or_else(|| panic!("missing translate y for {}", text))
            .parse::<f32>()
            .unwrap_or_else(|_| panic!("invalid translate y for {}", text));
        (x, y)
    }

    fn parse_svg_attr_pt(line: &str, attr: &str) -> f32 {
        let marker = format!(r#"{}=""#, attr);
        let start = line
            .find(&marker)
            .unwrap_or_else(|| panic!("missing {} in line: {}", attr, line))
            + marker.len();
        let end = line[start..]
            .find('"')
            .unwrap_or_else(|| panic!("unterminated {} in line: {}", attr, line))
            + start;
        let value = line[start..end].trim_end_matches("pt");
        value
            .parse::<f32>()
            .unwrap_or_else(|_| panic!("invalid {} value in line: {}", attr, line))
    }

    fn extract_typst_group_boxes(svg: &str) -> Vec<(f32, f32, f32, f32)> {
        svg.lines()
            .filter(|line| line.contains(r#"data-ruviz-text-engine="typst""#))
            .map(|line| {
                let transform_marker = r#"transform="translate("#;
                let start = line
                    .find(transform_marker)
                    .unwrap_or_else(|| panic!("missing translate transform in line: {}", line))
                    + transform_marker.len();
                let end = line[start..].find(')').unwrap_or_else(|| {
                    panic!("unterminated translate transform in line: {}", line)
                }) + start;
                let coords = &line[start..end];
                let mut parts = coords.split(',');
                let tx = parts
                    .next()
                    .unwrap_or_else(|| panic!("missing translate x in line: {}", line))
                    .parse::<f32>()
                    .unwrap_or_else(|_| panic!("invalid translate x in line: {}", line));
                let ty = parts
                    .next()
                    .unwrap_or_else(|| panic!("missing translate y in line: {}", line))
                    .parse::<f32>()
                    .unwrap_or_else(|_| panic!("invalid translate y in line: {}", line));
                let width = parse_svg_attr_pt(line, "width");
                let height = parse_svg_attr_pt(line, "height");
                (tx, ty, width, height)
            })
            .collect()
    }

    #[test]
    fn test_get_theme_method() {
        use crate::render::Theme;

        let plot = Plot::new();
        let theme = plot.get_theme();

        // Should return a valid theme (can't compare directly since Theme doesn't implement PartialEq)
        // Just ensure the method works without panicking

        // Test with custom theme - just ensure get/set works
        let custom_theme = Theme::dark();
        let plot = Plot::new().theme(custom_theme);
        let _retrieved_theme = plot.get_theme();
        // Test passes if no panic occurs
    }

    #[test]
    fn test_pending_ingestion_error_preserves_single_error_shape() {
        let bad = FailingIngestionData;
        let y = vec![1.0, 2.0, 3.0];

        let err = Plot::new().line(&bad, &y).render().unwrap_err();
        match err {
            PlottingError::DataExtractionFailed { source, message } => {
                assert_eq!(source, "test::failing-ingestion");
                assert_eq!(message, "forced ingestion failure");
            }
            other => panic!("expected DataExtractionFailed, got {other:?}"),
        }
    }

    #[test]
    fn test_pending_ingestion_error_reports_additional_failures() {
        let bad = FailingIngestionData;

        let err = Plot::new().line(&bad, &bad).render().unwrap_err();
        match err {
            PlottingError::DataExtractionFailed { source, message } => {
                assert_eq!(source, "ruviz::plot-ingestion");
                assert!(message.contains("forced ingestion failure"));
                assert!(message.contains("1 additional ingestion error"));
            }
            other => panic!("expected DataExtractionFailed, got {other:?}"),
        }
    }

    #[test]
    fn test_group_scopes_shared_style_and_does_not_leak() {
        let x = vec![0.0, 1.0, 2.0];
        let y1 = vec![0.0, 1.0, 2.0];
        let y2 = vec![0.0, 2.0, 4.0];
        let y3 = vec![0.0, 3.0, 6.0];

        let plot = Plot::new()
            .group(|g| {
                g.color(Color::RED)
                    .line_width(3.0)
                    .line_style(LineStyle::Dashed)
                    .alpha(0.35)
                    .line(&x, &y1)
                    .line(&x, &y2)
            })
            .line(&x, &y3)
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 3);
        assert_eq!(plot.series_mgr.series[0].color, Some(Color::RED));
        assert_eq!(plot.series_mgr.series[1].color, Some(Color::RED));
        assert_eq!(plot.series_mgr.series[0].line_width, Some(3.0));
        assert_eq!(plot.series_mgr.series[1].line_width, Some(3.0));
        assert!(matches!(
            plot.series_mgr.series[0].line_style,
            Some(LineStyle::Dashed)
        ));
        assert!(matches!(
            plot.series_mgr.series[1].line_style,
            Some(LineStyle::Dashed)
        ));
        assert_eq!(plot.series_mgr.series[0].alpha, Some(0.35));
        assert_eq!(plot.series_mgr.series[1].alpha, Some(0.35));

        // Outside the group, styles should fall back to normal per-series defaults.
        assert_ne!(plot.series_mgr.series[2].color, Some(Color::RED));
        assert_ne!(plot.series_mgr.series[2].line_width, Some(3.0));
        assert!(matches!(
            plot.series_mgr.series[2].line_style,
            Some(LineStyle::Solid)
        ));
        assert_eq!(plot.series_mgr.series[2].alpha, Some(1.0));
    }

    #[test]
    fn test_group_label_collapses_legend_to_single_item() {
        let x = vec![0.0, 1.0, 2.0];
        let y1 = vec![0.0, 1.0, 2.0];
        let y2 = vec![0.0, 2.0, 4.0];
        let y3 = vec![0.0, 3.0, 6.0];

        let plot = Plot::new()
            .group(|g| {
                g.group_label("Grouped")
                    .line_style(LineStyle::Dashed)
                    .line(&x, &y1)
                    .line(&x, &y2)
            })
            .line(&x, &y3)
            .label("Solo")
            .end_series();

        let legend_items = plot.collect_legend_items();
        assert_eq!(legend_items.len(), 2);
        assert!(legend_items.iter().any(|item| item.label == "Grouped"));
        assert!(legend_items.iter().any(|item| item.label == "Solo"));

        let grouped = legend_items
            .iter()
            .find(|item| item.label == "Grouped")
            .expect("group legend item should exist");
        assert!(matches!(grouped.item_type, LegendItemType::Line { .. }));
    }

    #[test]
    fn test_group_without_label_is_omitted_from_legend() {
        let x = vec![0.0, 1.0, 2.0];
        let y1 = vec![0.0, 1.0, 2.0];
        let y2 = vec![0.0, 2.0, 4.0];

        let plot = Plot::new().group(|g| g.line(&x, &y1).line(&x, &y2));
        let legend_items = plot.collect_legend_items();
        assert!(legend_items.is_empty());
    }

    #[test]
    fn test_group_without_color_uses_single_palette_color() {
        let x = vec![0.0, 1.0, 2.0];
        let y1 = vec![0.0, 1.0, 2.0];
        let y2 = vec![0.0, 2.0, 4.0];
        let y3 = vec![0.0, 3.0, 6.0];

        let plot = Plot::new()
            .group(|g| g.line(&x, &y1).line(&x, &y2))
            .line(&x, &y3)
            .end_series();
        assert_eq!(plot.series_mgr.series.len(), 3);
        assert_eq!(
            plot.series_mgr.series[0].color,
            plot.series_mgr.series[1].color
        );
        assert_ne!(
            plot.series_mgr.series[0].color,
            plot.series_mgr.series[2].color
        );
    }

    #[test]
    fn test_group_mixed_series_uses_first_member_legend_glyph() {
        let x = vec![0.0, 1.0, 2.0];
        let y1 = vec![0.0, 1.0, 2.0];
        let y2 = vec![0.0, 2.0, 4.0];

        let plot = Plot::new().group(|g| {
            g.group_label("Mixed")
                .scatter(&x, &y1)
                .line(&x, &y2)
                .line_style(LineStyle::Dashed)
        });

        let legend_items = plot.collect_legend_items();
        assert_eq!(legend_items.len(), 1);
        assert_eq!(legend_items[0].label, "Mixed");
        assert!(matches!(
            legend_items[0].item_type,
            LegendItemType::Scatter { .. }
        ));
    }

    #[test]
    fn test_render_to_renderer_basic() {
        use crate::render::{SkiaRenderer, Theme};

        let x_data = vec![1.0, 2.0, 3.0];
        let y_data = vec![2.0, 4.0, 3.0];

        let plot = Plot::new()
            .line(&x_data, &y_data)
            .title("Test Plot")
            .xlabel("X")
            .ylabel("Y")
            .end_series();

        let mut renderer = SkiaRenderer::new(400, 300, Theme::default()).unwrap();

        // Should render without error
        let result = plot.render_to_renderer(&mut renderer, 96.0);
        assert!(result.is_ok());
    }

    #[test]
    fn test_render_to_renderer_empty_series() {
        use crate::render::{SkiaRenderer, Theme};

        let plot = Plot::new().title("Empty Plot");
        let mut renderer = SkiaRenderer::new(400, 300, Theme::default()).unwrap();

        // Should fail with no data series
        let result = plot.render_to_renderer(&mut renderer, 96.0);
        assert!(result.is_err());
    }

    #[test]
    fn test_render_to_renderer_multiple_series() {
        use crate::render::{SkiaRenderer, Theme};

        let x1 = vec![1.0, 2.0, 3.0];
        let y1 = vec![2.0, 4.0, 3.0];
        let x2 = vec![1.5, 2.5, 3.5];
        let y2 = vec![1.0, 3.0, 2.0];

        let plot = Plot::new()
            .line(&x1, &y1)
            .label("Series 1")
            .line(&x2, &y2)
            .label("Series 2")
            .title("Multi-series Plot")
            .end_series();

        let mut renderer = SkiaRenderer::new(400, 300, Theme::default()).unwrap();

        // Should render multiple series without error
        let result = plot.render_to_renderer(&mut renderer, 96.0);
        assert!(result.is_ok());
    }

    #[test]
    fn test_render_to_renderer_dpi_scaling() {
        use crate::render::{SkiaRenderer, Theme};

        let x_data = vec![1.0, 2.0, 3.0];
        let y_data = vec![2.0, 4.0, 3.0];

        let plot = Plot::new()
            .line(&x_data, &y_data)
            .title("DPI Test")
            .end_series();

        let mut renderer = SkiaRenderer::new(400, 300, Theme::default()).unwrap();

        // Test different DPI values
        let result_96 = plot.clone().render_to_renderer(&mut renderer, 96.0);
        assert!(result_96.is_ok());

        let result_144 = plot.clone().render_to_renderer(&mut renderer, 144.0);
        assert!(result_144.is_ok());

        let result_300 = plot.render_to_renderer(&mut renderer, 300.0);
        assert!(result_300.is_ok());
    }

    #[test]
    fn test_render_to_svg_uses_layout_positions_for_title_and_labels() {
        use crate::render::{FontConfig, FontFamily, TextRenderer};

        let x_data = vec![0.0, 1.0, 2.0, 3.0];
        let y_data = vec![1.0, 3.0, 2.0, 4.0];
        let plot = Plot::new()
            .line(&x_data, &y_data)
            .title("SVG_LAYOUT_TITLE")
            .xlabel("SVG_LAYOUT_X")
            .ylabel("SVG_LAYOUT_Y")
            .end_series();

        let svg = plot.render_to_svg().expect("SVG render should succeed");

        let (_x_min, _x_max, y_min, y_max) = plot
            .calculate_data_bounds()
            .expect("data bounds should be available");
        let content = plot.create_plot_content(y_min, y_max);
        let layout_config = match &plot.display.config.margins {
            MarginConfig::ContentDriven {
                edge_buffer,
                center_plot,
            } => LayoutConfig {
                edge_buffer_pt: *edge_buffer,
                center_plot: *center_plot,
                ..Default::default()
            },
            _ => LayoutConfig::default(),
        };
        let calculator = LayoutCalculator::new(layout_config);
        let mut measurement_renderer = crate::render::SkiaRenderer::new(
            plot.display.dimensions.0,
            plot.display.dimensions.1,
            plot.display.theme.clone(),
        )
        .expect("measurement renderer");
        let dpi_scale = plot.display.config.figure.dpi / 100.0;
        measurement_renderer.set_dpi_scale(dpi_scale);
        measurement_renderer.set_text_engine_mode(plot.display.text_engine);
        let measured_dimensions = plot
            .measure_layout_text(
                &measurement_renderer,
                &content,
                plot.display.config.figure.dpi,
            )
            .expect("layout text measurements");
        let layout = calculator.compute(
            plot.display.dimensions,
            &content,
            &plot.display.config.typography,
            &plot.display.config.spacing,
            plot.display.config.figure.dpi,
            measured_dimensions.as_ref(),
        );

        let title_pos = layout.title_pos.expect("title position");
        let xlabel_pos = layout.xlabel_pos.expect("xlabel position");
        let ylabel_pos = layout.ylabel_pos.expect("ylabel position");
        let text_renderer = TextRenderer::new();
        let title_metrics = text_renderer
            .measure_text_placement(
                "SVG_LAYOUT_TITLE",
                &FontConfig::new(FontFamily::SansSerif, title_pos.size),
            )
            .expect("title metrics");
        let xlabel_metrics = text_renderer
            .measure_text_placement(
                "SVG_LAYOUT_X",
                &FontConfig::new(FontFamily::SansSerif, xlabel_pos.size),
            )
            .expect("xlabel metrics");

        let (title_x, title_y) = extract_svg_text_xy(&svg, "SVG_LAYOUT_TITLE");
        let (xlabel_x, xlabel_y) = extract_svg_text_xy(&svg, "SVG_LAYOUT_X");
        let (ylabel_x, ylabel_y) = extract_svg_group_translate_xy(&svg, "SVG_LAYOUT_Y");

        assert!(
            (title_x - title_pos.x).abs() <= 0.6
                && (title_y - (title_pos.y + title_metrics.baseline_from_top)).abs() <= 0.6,
            "title should follow layout position: svg=({}, {}), layout=({}, {})",
            title_x,
            title_y,
            title_pos.x,
            title_pos.y + title_metrics.baseline_from_top
        );
        assert!(
            (xlabel_x - xlabel_pos.x).abs() <= 0.6
                && (xlabel_y - (xlabel_pos.y + xlabel_metrics.baseline_from_top)).abs() <= 0.6,
            "xlabel should follow layout position: svg=({}, {}), layout=({}, {})",
            xlabel_x,
            xlabel_y,
            xlabel_pos.x,
            xlabel_pos.y + xlabel_metrics.baseline_from_top
        );
        assert!(
            (ylabel_x - ylabel_pos.x).abs() <= 0.6 && (ylabel_y - ylabel_pos.y).abs() <= 0.6,
            "ylabel should follow layout position: svg=({}, {}), layout=({}, {})",
            ylabel_x,
            ylabel_y,
            ylabel_pos.x,
            ylabel_pos.y
        );
    }

    #[cfg(feature = "typst-math")]
    #[test]
    fn test_render_to_svg_typst_uses_layout_anchor_contract() {
        let x_data = vec![0.0, 1.0, 2.0, 3.0];
        let y_data = vec![1.0, 3.0, 2.0, 4.0];
        let plot = Plot::new()
            .line(&x_data, &y_data)
            .title("SVG_LAYOUT_TITLE")
            .xlabel("SVG_LAYOUT_X")
            .ylabel("SVG_LAYOUT_Y")
            .typst(true)
            .end_series();

        let svg = plot.render_to_svg().expect("SVG render should succeed");

        let (_x_min, _x_max, y_min, y_max) = plot
            .calculate_data_bounds()
            .expect("data bounds should be available");
        let content = plot.create_plot_content(y_min, y_max);
        let layout_config = match &plot.display.config.margins {
            MarginConfig::ContentDriven {
                edge_buffer,
                center_plot,
            } => LayoutConfig {
                edge_buffer_pt: *edge_buffer,
                center_plot: *center_plot,
                ..Default::default()
            },
            _ => LayoutConfig::default(),
        };
        let calculator = LayoutCalculator::new(layout_config);
        let mut measurement_renderer = crate::render::SkiaRenderer::new(
            plot.display.dimensions.0,
            plot.display.dimensions.1,
            plot.display.theme.clone(),
        )
        .expect("measurement renderer");
        let dpi_scale = plot.display.config.figure.dpi / 100.0;
        measurement_renderer.set_dpi_scale(dpi_scale);
        measurement_renderer.set_text_engine_mode(plot.display.text_engine);
        let measured_dimensions = plot
            .measure_layout_text(
                &measurement_renderer,
                &content,
                plot.display.config.figure.dpi,
            )
            .expect("layout text measurements");
        let layout = calculator.compute(
            plot.display.dimensions,
            &content,
            &plot.display.config.typography,
            &plot.display.config.spacing,
            plot.display.config.figure.dpi,
            measured_dimensions.as_ref(),
        );

        let title_pos = layout.title_pos.expect("title position");
        let xlabel_pos = layout.xlabel_pos.expect("xlabel position");
        let ylabel_pos = layout.ylabel_pos.expect("ylabel position");

        let typst_groups = extract_typst_group_boxes(&svg);
        assert!(
            typst_groups.len() >= 3,
            "expected at least three typst text groups, found {}",
            typst_groups.len()
        );
        let n = typst_groups.len();
        let title = typst_groups[n - 3];
        let xlabel = typst_groups[n - 2];
        let ylabel = typst_groups[n - 1];

        let title_center_x = title.0 + title.2 / 2.0;
        let xlabel_center_x = xlabel.0 + xlabel.2 / 2.0;
        let ylabel_center_x = ylabel.0 + ylabel.2 / 2.0;
        let ylabel_center_y = ylabel.1 + ylabel.3 / 2.0;

        assert!(
            (title_center_x - title_pos.x).abs() <= 0.8 && (title.1 - title_pos.y).abs() <= 0.8,
            "typst title should follow top-center anchor: group=({}, {}, {}x{}), layout=({}, {})",
            title.0,
            title.1,
            title.2,
            title.3,
            title_pos.x,
            title_pos.y
        );
        assert!(
            (xlabel_center_x - xlabel_pos.x).abs() <= 0.8 && (xlabel.1 - xlabel_pos.y).abs() <= 0.8,
            "typst xlabel should follow top-center anchor: group=({}, {}, {}x{}), layout=({}, {})",
            xlabel.0,
            xlabel.1,
            xlabel.2,
            xlabel.3,
            xlabel_pos.x,
            xlabel_pos.y
        );
        assert!(
            (ylabel_center_x - ylabel_pos.x).abs() <= 0.8
                && (ylabel_center_y - ylabel_pos.y).abs() <= 0.8,
            "typst ylabel should follow center anchor: group=({}, {}, {}x{}), layout=({}, {})",
            ylabel.0,
            ylabel.1,
            ylabel.2,
            ylabel.3,
            ylabel_pos.x,
            ylabel_pos.y
        );
    }

    // ========== GPU Integration Tests ==========

    #[test]
    #[cfg(feature = "gpu")]
    fn test_gpu_method_sets_backend() {
        let plot = Plot::new().gpu(true);
        assert_eq!(plot.get_backend_name(), "gpu");
        assert!(plot.render.enable_gpu);
    }

    #[test]
    #[cfg(feature = "gpu")]
    fn test_gpu_method_disabled() {
        let plot = Plot::new().gpu(false);
        // When disabled, backend should not be set to GPU
        assert!(!plot.render.enable_gpu);
    }

    #[test]
    #[cfg(feature = "gpu")]
    fn test_gpu_threshold_constants() {
        // Verify threshold constants are reasonable
        const DATASHADER_THRESHOLD: usize = 100_000;
        const GPU_THRESHOLD: usize = 5_000;

        assert!(GPU_THRESHOLD < DATASHADER_THRESHOLD);
        assert!(GPU_THRESHOLD > 0);
    }

    #[test]
    #[cfg(feature = "gpu")]
    fn test_gpu_with_small_dataset() {
        // Small datasets should not trigger GPU even with gpu(true)
        let x_data: Vec<f64> = (0..100).map(|i| i as f64).collect();
        let y_data: Vec<f64> = x_data.iter().map(|x| x * x).collect();

        let plot = Plot::new()
            .gpu(true)
            .line(&x_data, &y_data)
            .title("Small Dataset GPU Test")
            .end_series();

        // Should succeed (will use CPU path due to small dataset)
        let result = plot.render();
        assert!(result.is_ok());
    }

    #[test]
    #[cfg(feature = "gpu")]
    fn test_gpu_with_medium_dataset() {
        // Medium datasets (>5K) should trigger GPU path
        let x_data: Vec<f64> = (0..6000).map(|i| i as f64 * 0.01).collect();
        let y_data: Vec<f64> = x_data.iter().map(|x| x.sin()).collect();

        let plot = Plot::new()
            .gpu(true)
            .line(&x_data, &y_data)
            .title("Medium Dataset GPU Test")
            .end_series();

        // Should succeed (GPU path if available, otherwise fallback to CPU)
        let result = plot.render();
        assert!(result.is_ok());
    }

    #[test]
    #[cfg(feature = "gpu")]
    fn test_gpu_scatter_plot() {
        // Test GPU with scatter plot
        let x_data: Vec<f64> = (0..5500).map(|i| i as f64 * 0.01).collect();
        let y_data: Vec<f64> = x_data.iter().map(|x| x.cos()).collect();

        let plot = Plot::new()
            .gpu(true)
            .scatter(&x_data, &y_data)
            .title("Scatter GPU Test")
            .end_series();

        let result = plot.render();
        assert!(result.is_ok());
    }

    #[test]
    #[cfg(feature = "gpu")]
    fn test_gpu_fallback_on_unsupported_series() {
        // Bar charts should fall back to normal rendering even with GPU enabled
        let categories = vec!["A", "B", "C", "D"];
        let values = vec![10.0, 20.0, 15.0, 25.0];

        let plot = Plot::new()
            .gpu(true)
            .bar(&categories, &values)
            .title("Bar Chart GPU Fallback")
            .end_series();

        let result = plot.render();
        assert!(result.is_ok());
    }

    #[test]
    #[cfg(feature = "gpu")]
    fn test_plot_series_builder_gpu_method() {
        let x_data: Vec<f64> = (0..100).map(|i| i as f64).collect();
        let y_data: Vec<f64> = x_data.iter().map(|x| x * 2.0).collect();

        // Test that gpu() works on PlotSeriesBuilder
        let plot = Plot::new().line(&x_data, &y_data).gpu(true);

        assert_eq!(plot.get_backend_name(), "gpu");
    }

    #[test]
    fn test_backend_selection_without_gpu_feature() {
        // Test that backend selection works when GPU feature is not enabled
        let plot = Plot::new().backend(BackendType::Parallel);
        assert_eq!(plot.get_backend_name(), "parallel");

        let plot2 = Plot::new().backend(BackendType::DataShader);
        assert_eq!(plot2.get_backend_name(), "datashader");
    }

    #[test]
    fn test_auto_backend_selection() {
        // Test auto-optimization selects appropriate backend
        let x_small: Vec<f64> = (0..100).map(|i| i as f64).collect();
        let y_small: Vec<f64> = x_small.iter().map(|x| x * x).collect();

        let plot = Plot::new().line(&x_small, &y_small).end_series();

        // auto_optimize consumes self and returns Self
        let plot = plot.auto_optimize();

        // Small dataset should use Skia
        let backend_name = plot.get_backend_name();
        assert_eq!(backend_name, "skia");
    }

    // ========================================================================
    // Streaming Data Tests
    // ========================================================================

    #[test]
    fn test_line_streaming_basic() {
        use crate::data::StreamingXY;

        let stream = StreamingXY::new(100);
        stream.push_many(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0), (3.0, 9.0)]);

        let plot = Plot::new()
            .line_streaming(&stream)
            .title("Streaming Line Plot")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 1);

        // Verify data was captured
        if let SeriesType::Line { x_data, y_data } = &plot.series_mgr.series[0].series_type {
            let x_resolved = x_data.resolve(0.0);
            let y_resolved = y_data.resolve(0.0);
            assert_eq!(x_resolved.len(), 4);
            assert_eq!(y_resolved.len(), 4);
            assert_eq!(x_resolved[0], 0.0);
            assert_eq!(y_resolved[3], 9.0);
        } else {
            panic!("Expected Line series type");
        }
    }

    #[test]
    fn test_scatter_streaming_basic() {
        use crate::data::StreamingXY;

        let stream = StreamingXY::new(100);
        stream.push_many(vec![(1.0, 10.0), (2.0, 20.0), (3.0, 30.0)]);

        let plot = Plot::new()
            .scatter_streaming(&stream)
            .title("Streaming Scatter")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 1);

        if let SeriesType::Scatter { x_data, y_data } = &plot.series_mgr.series[0].series_type {
            assert_eq!(x_data.len(), 3);
            assert_eq!(y_data.len(), 3);
        } else {
            panic!("Expected Scatter series type");
        }
    }

    #[test]
    fn test_streaming_marks_rendered() {
        use crate::data::StreamingXY;

        let stream = StreamingXY::new(100);
        stream.push_many(vec![(0.0, 0.0), (1.0, 1.0)]);

        assert_eq!(stream.appended_count(), 2);

        let _plot = Plot::new().line_streaming(&stream).end_series();

        // After line_streaming, buffer should be marked as rendered
        assert_eq!(stream.appended_count(), 0);
    }

    #[test]
    fn test_streaming_render_output() {
        use crate::data::StreamingXY;

        let stream = StreamingXY::new(100);
        stream.push_many(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)]);

        let plot = Plot::new()
            .line_streaming(&stream)
            .title("Streaming Test")
            .end_series();

        let result = plot.render();
        assert!(result.is_ok());
    }

    #[test]
    fn test_streaming_with_ring_buffer_wrap() {
        use crate::data::StreamingXY;

        // Small buffer that wraps
        let stream = StreamingXY::new(3);
        stream.push_many(vec![
            (0.0, 0.0),
            (1.0, 1.0),
            (2.0, 2.0),
            (3.0, 3.0),
            (4.0, 4.0),
        ]);

        // Buffer should only contain last 3 points
        assert_eq!(stream.len(), 3);

        let plot = Plot::new().line_streaming(&stream).end_series();

        if let SeriesType::Line { x_data, y_data: _ } = &plot.series_mgr.series[0].series_type {
            let x_resolved = x_data.resolve(0.0);
            assert_eq!(x_resolved.len(), 3);
            // Should be the last 3 values
            assert_eq!(x_resolved[0], 2.0);
            assert_eq!(x_resolved[1], 3.0);
            assert_eq!(x_resolved[2], 4.0);
        } else {
            panic!("Expected Line series type");
        }
    }

    #[test]
    fn test_streaming_empty_buffer() {
        use crate::data::StreamingXY;

        // Empty stream should still create a valid plot structure
        let stream = StreamingXY::new(100);

        let plot = Plot::new()
            .line_streaming(&stream)
            .title("Empty Stream")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 1);

        if let SeriesType::Line { x_data, y_data } = &plot.series_mgr.series[0].series_type {
            assert!(x_data.is_empty());
            assert!(y_data.is_empty());
        }

        // Note: Empty data may fail to render (no bounds can be computed)
        // This is expected behavior - we test that the plot structure is correct
        // A real application would check for empty data before rendering
    }

    #[test]
    fn test_streaming_multiple_series() {
        use crate::data::StreamingXY;

        let stream1 = StreamingXY::new(100);
        let stream2 = StreamingXY::new(100);

        stream1.push_many(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)]);
        stream2.push_many(vec![(0.0, 0.0), (1.0, 2.0), (2.0, 4.0)]);

        let plot = Plot::new()
            .line_streaming(&stream1)
            .label("Linear")
            .line_streaming(&stream2)
            .label("Quadratic")
            .title("Multiple Streaming Series")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 2);

        // First series
        if let SeriesType::Line { x_data, .. } = &plot.series_mgr.series[0].series_type {
            assert_eq!(x_data.len(), 3);
        }

        // Second series
        if let SeriesType::Line { x_data, .. } = &plot.series_mgr.series[1].series_type {
            assert_eq!(x_data.len(), 3);
        }

        let result = plot.render();
        assert!(result.is_ok());
    }

    #[test]
    fn test_streaming_mixed_with_static() {
        use crate::data::StreamingXY;

        let stream = StreamingXY::new(100);
        stream.push_many(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)]);

        // Mix streaming and static data
        let static_x = vec![0.0, 1.0, 2.0];
        let static_y = vec![0.0, 2.0, 4.0];

        let plot = Plot::new()
            .line_streaming(&stream)
            .label("Streaming")
            .line(&static_x, &static_y)
            .label("Static")
            .title("Mixed Data Sources")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 2);

        let result = plot.render();
        assert!(result.is_ok());
    }

    #[test]
    fn test_streaming_with_styling() {
        use crate::data::StreamingXY;

        let stream = StreamingXY::new(100);
        stream.push_many(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)]);

        let plot = Plot::new()
            .line_streaming(&stream)
            .color(Color::new(255, 0, 0))
            .width(3.0)
            .label("Styled Streaming")
            .title("Styled Streaming Plot")
            .xlabel("X Axis")
            .ylabel("Y Axis")
            .end_series();

        assert_eq!(plot.series_mgr.series[0].color, Some(Color::new(255, 0, 0)));
        assert_eq!(plot.series_mgr.series[0].line_width, Some(3.0));

        let result = plot.render();
        assert!(result.is_ok());
    }

    #[test]
    fn test_streaming_scatter_with_styling() {
        use crate::data::StreamingXY;

        let stream = StreamingXY::new(100);
        stream.push_many(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)]);

        let plot = Plot::new()
            .scatter_streaming(&stream)
            .color(Color::new(0, 255, 0))
            .marker_size(10.0)
            .end_series();

        assert_eq!(plot.series_mgr.series[0].color, Some(Color::new(0, 255, 0)));
        assert_eq!(plot.series_mgr.series[0].marker_size, Some(10.0));

        let result = plot.render();
        assert!(result.is_ok());
    }

    #[test]
    fn test_streaming_version_changes_on_data_update() {
        use crate::data::StreamingXY;

        let stream = StreamingXY::new(100);

        let v0 = stream.version();
        stream.push(1.0, 1.0);
        let v1 = stream.version();

        assert!(v1 > v0, "Version should increase after push");

        // Create plot (marks as rendered)
        let _plot = Plot::new().line_streaming(&stream).end_series();

        // Push more data
        stream.push(2.0, 2.0);
        let v2 = stream.version();

        assert!(v2 > v1, "Version should increase after second push");
    }

    // ========== Error Bar Modifier API Tests ==========

    #[test]
    fn test_with_yerr_symmetric() {
        let x = vec![1.0, 2.0, 3.0];
        let y = vec![2.0, 4.0, 3.0];
        let yerr = vec![0.3, 0.4, 0.25];

        let plot = Plot::new()
            .line(&x, &y)
            .with_yerr(&yerr)
            .label("Test")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 1);
        assert!(plot.series_mgr.series[0].y_errors.is_some());
        assert!(plot.series_mgr.series[0].x_errors.is_none());

        // Verify it's symmetric error values
        if let Some(ErrorValues::Symmetric(errs)) = &plot.series_mgr.series[0].y_errors {
            assert_eq!(errs.len(), 3);
            assert!((errs[0] - 0.3).abs() < 1e-10);
        } else {
            panic!("Expected symmetric error values");
        }
    }

    #[test]
    fn test_with_xerr_symmetric() {
        let x = vec![1.0, 2.0, 3.0];
        let y = vec![2.0, 4.0, 3.0];
        let xerr = vec![0.15, 0.2, 0.1];

        let plot = Plot::new()
            .scatter(&x, &y)
            .with_xerr(&xerr)
            .label("Test")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 1);
        assert!(plot.series_mgr.series[0].x_errors.is_some());
        assert!(plot.series_mgr.series[0].y_errors.is_none());

        if let Some(ErrorValues::Symmetric(errs)) = &plot.series_mgr.series[0].x_errors {
            assert_eq!(errs.len(), 3);
            assert!((errs[1] - 0.2).abs() < 1e-10);
        } else {
            panic!("Expected symmetric error values");
        }
    }

    #[test]
    fn test_with_yerr_asymmetric() {
        let x = vec![1.0, 2.0, 3.0];
        let y = vec![2.0, 4.0, 3.0];
        let lower = vec![0.2, 0.3, 0.2];
        let upper = vec![0.5, 0.6, 0.4];

        let plot = Plot::new()
            .line(&x, &y)
            .with_yerr_asymmetric(&lower, &upper)
            .label("Test")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 1);
        assert!(plot.series_mgr.series[0].y_errors.is_some());

        if let Some(ErrorValues::Asymmetric(lo, hi)) = &plot.series_mgr.series[0].y_errors {
            assert_eq!(lo.len(), 3);
            assert_eq!(hi.len(), 3);
            assert!((lo[0] - 0.2).abs() < 1e-10);
            assert!((hi[0] - 0.5).abs() < 1e-10);
        } else {
            panic!("Expected asymmetric error values");
        }
    }

    #[test]
    fn test_with_xerr_asymmetric() {
        let x = vec![1.0, 2.0, 3.0];
        let y = vec![2.0, 4.0, 3.0];
        let left = vec![0.1, 0.15, 0.1];
        let right = vec![0.2, 0.25, 0.2];

        let plot = Plot::new()
            .scatter(&x, &y)
            .with_xerr_asymmetric(&left, &right)
            .label("Test")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 1);
        assert!(plot.series_mgr.series[0].x_errors.is_some());

        if let Some(ErrorValues::Asymmetric(lo, hi)) = &plot.series_mgr.series[0].x_errors {
            assert_eq!(lo.len(), 3);
            assert_eq!(hi.len(), 3);
            assert!((lo[1] - 0.15).abs() < 1e-10);
            assert!((hi[1] - 0.25).abs() < 1e-10);
        } else {
            panic!("Expected asymmetric error values");
        }
    }

    #[test]
    fn test_error_config() {
        let x = vec![1.0, 2.0, 3.0];
        let y = vec![2.0, 4.0, 3.0];
        let yerr = vec![0.3, 0.4, 0.25];

        let config = ErrorBarConfig::default().cap_size(0.15).line_width(2.0);

        let plot = Plot::new()
            .line(&x, &y)
            .with_yerr(&yerr)
            .error_config(config)
            .label("Test")
            .end_series();

        assert!(plot.series_mgr.series[0].error_config.is_some());
        let cfg = plot.series_mgr.series[0].error_config.as_ref().unwrap();
        assert!((cfg.cap_size - 0.15).abs() < 1e-10);
        assert!((cfg.line_width - 2.0).abs() < 1e-10);
    }

    #[test]
    fn test_combined_xy_errors() {
        let x = vec![1.0, 2.0, 3.0];
        let y = vec![2.0, 4.0, 3.0];
        let xerr = vec![0.15, 0.2, 0.1];
        let yerr = vec![0.3, 0.4, 0.25];

        let plot = Plot::new()
            .scatter(&x, &y)
            .with_yerr(&yerr)
            .with_xerr(&xerr)
            .label("Test")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 1);
        assert!(plot.series_mgr.series[0].y_errors.is_some());
        assert!(plot.series_mgr.series[0].x_errors.is_some());
    }

    #[test]
    fn test_error_bars_continuation_method() {
        let x = vec![1.0, 2.0, 3.0];
        let y1 = vec![2.0, 4.0, 3.0];
        let y1_err = vec![0.3, 0.4, 0.25];
        let y2 = vec![1.5, 3.5, 2.5];
        let y2_err = vec![0.2, 0.3, 0.2];

        let plot = Plot::new()
            .error_bars(&x, &y1, &y1_err)
            .label("Series A")
            .error_bars(&x, &y2, &y2_err) // Continuation method
            .label("Series B")
            .end_series();

        assert_eq!(plot.series_mgr.series.len(), 2);
        assert!(matches!(
            &plot.series_mgr.series[0].series_type,
            SeriesType::ErrorBars { .. }
        ));
        assert!(matches!(
            &plot.series_mgr.series[1].series_type,
            SeriesType::ErrorBars { .. }
        ));
    }

    #[test]
    fn test_line_with_error_bars_renders() {
        use crate::render::{SkiaRenderer, Theme};

        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
        let yerr = vec![0.3, 0.4, 0.25, 0.5, 0.35];

        let plot = Plot::new()
            .line(&x, &y)
            .with_yerr(&yerr)
            .title("Line with Error Bars")
            .end_series();

        let mut renderer = SkiaRenderer::new(400, 300, Theme::default()).unwrap();
        let result = plot.render_to_renderer(&mut renderer, 96.0);
        assert!(result.is_ok());
    }

    #[test]
    fn test_scatter_with_xy_error_bars_renders() {
        use crate::render::{SkiaRenderer, Theme};

        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
        let xerr = vec![0.15, 0.2, 0.1, 0.15, 0.2];
        let yerr = vec![0.3, 0.4, 0.25, 0.5, 0.35];

        let plot = Plot::new()
            .scatter(&x, &y)
            .with_yerr(&yerr)
            .with_xerr(&xerr)
            .title("Scatter with XY Error Bars")
            .end_series();

        let mut renderer = SkiaRenderer::new(400, 300, Theme::default()).unwrap();
        let result = plot.render_to_renderer(&mut renderer, 96.0);
        assert!(result.is_ok());
    }

    #[test]
    fn test_multiple_series_with_different_errors() {
        use crate::render::{SkiaRenderer, Theme};

        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
        let y1 = vec![2.0, 4.0, 3.0, 5.0, 4.5];
        let y1_err = vec![0.3, 0.4, 0.25, 0.5, 0.35];
        let y2 = vec![1.5, 3.5, 2.5, 4.5, 4.0];
        let y2_err = vec![0.2, 0.3, 0.2, 0.4, 0.3];

        let plot = Plot::new()
            .line(&x, &y1)
            .with_yerr(&y1_err)
            .label("Series A")
            .scatter(&x, &y2)
            .with_yerr(&y2_err)
            .label("Series B")
            .title("Multiple Series with Error Bars")
            .end_series();

        let mut renderer = SkiaRenderer::new(400, 300, Theme::default()).unwrap();
        let result = plot.render_to_renderer(&mut renderer, 96.0);
        assert!(result.is_ok());
    }

    // ========== max_resolution Tests ==========

    #[test]
    fn test_max_resolution_height_constrained() {
        // Default 4:3 figure (6.4x4.8) fitting into 1920x1080 (16:9)
        // Should be height-constrained → 1440x1080
        let plot = Plot::new().max_resolution(1920, 1080);

        assert_eq!(plot.display.dimensions, (1440, 1080));
        // DPI should be 1080 / 4.8 = 225
        assert!((plot.display.config.figure.dpi - 225.0).abs() < 1.0);
    }

    #[test]
    fn test_max_resolution_width_constrained() {
        // 4:3 figure fitting into 800x800 (square)
        // Should be width-constrained → 800x600
        let plot = Plot::new().max_resolution(800, 800);

        assert_eq!(plot.display.dimensions, (800, 600));
        // DPI should be 800 / 6.4 = 125
        assert!((plot.display.config.figure.dpi - 125.0).abs() < 1.0);
    }

    #[test]
    fn test_max_resolution_exact_fit() {
        // 4:3 figure fitting into 1920x1440 (exactly 4:3)
        // Should fit exactly → 1920x1440
        let plot = Plot::new().max_resolution(1920, 1440);

        assert_eq!(plot.display.dimensions, (1920, 1440));
        // DPI should be 1920 / 6.4 = 300
        assert!((plot.display.config.figure.dpi - 300.0).abs() < 1.0);
    }

    #[test]
    fn test_max_resolution_custom_figure() {
        // 16:9 figure fitting into 1920x1080
        let plot = Plot::new().size(16.0, 9.0).max_resolution(1920, 1080);

        // Should fit exactly for 16:9
        assert_eq!(plot.display.dimensions, (1920, 1080));
        // DPI should be 1920 / 16.0 = 120
        assert!((plot.display.config.figure.dpi - 120.0).abs() < 1.0);
    }

    #[test]
    fn test_max_resolution_equivalent_to_dpi() {
        // max_resolution(1920, 1440) should produce same result as dpi(300)
        let plot_max_res = Plot::new().max_resolution(1920, 1440);
        let plot_dpi = Plot::new().dpi(300);

        assert_eq!(plot_max_res.display.dimensions, plot_dpi.display.dimensions);
        assert!(
            (plot_max_res.display.config.figure.dpi - plot_dpi.display.config.figure.dpi).abs()
                < 1.0
        );
    }

    // ========== IntoPlot Trait Tests ==========

    #[test]
    fn test_from_plot_series_builder_for_plot() {
        // Test From<PlotSeriesBuilder> for Plot
        let x_data = vec![1.0, 2.0, 3.0];
        let y_data = vec![2.0, 4.0, 3.0];

        let builder = Plot::new()
            .line(&x_data, &y_data)
            .color(crate::render::Color::RED)
            .label("Test Series");

        // Convert via .into()
        let plot: Plot = builder.into();

        // Verify the series was added
        assert_eq!(plot.series_mgr.series.len(), 1);
        assert_eq!(
            plot.series_mgr.series[0].label,
            Some("Test Series".to_string())
        );
    }

    #[test]
    fn test_into_plot_trait_for_plot() {
        use builder::IntoPlot;

        let plot = Plot::new().title("Test");
        let converted = plot.into_plot();

        // Verify title is set (PlotText doesn't impl PartialEq, so check via pattern match)
        match &converted.display.title {
            Some(data::PlotText::Static(s)) => assert_eq!(s, "Test"),
            _ => panic!("Expected Static PlotText with 'Test'"),
        }
    }

    #[test]
    fn test_into_plot_trait_for_plot_series_builder() {
        use builder::IntoPlot;

        let x_data = vec![1.0, 2.0, 3.0];
        let y_data = vec![2.0, 4.0, 3.0];

        let builder = Plot::new().line(&x_data, &y_data).label("Via IntoPlot");

        let plot = builder.into_plot();

        assert_eq!(plot.series_mgr.series.len(), 1);
        assert_eq!(
            plot.series_mgr.series[0].label,
            Some("Via IntoPlot".to_string())
        );
    }

    #[test]
    fn test_as_plot_for_plot_series_builder() {
        use builder::IntoPlot;

        let x_data = vec![1.0, 2.0, 3.0];
        let y_data = vec![2.0, 4.0, 3.0];

        let builder = Plot::new().title("Inspectable").line(&x_data, &y_data);

        // as_plot() allows inspection without consuming
        let plot_ref = builder.as_plot();
        match &plot_ref.display.title {
            Some(data::PlotText::Static(s)) => assert_eq!(s, "Inspectable"),
            _ => panic!("Expected Static PlotText with 'Inspectable'"),
        }

        // Builder is still usable after as_plot()
        let plot = builder.into_plot();
        assert_eq!(plot.series_mgr.series.len(), 1);
    }

    #[test]
    fn test_order_independent_method_chaining() {
        // Test that plot-level methods can be called in any order
        let x_data = vec![1.0, 2.0, 3.0];
        let y_data = vec![2.0, 4.0, 3.0];

        // Version 1: Plot config before series
        let plot1: Plot = Plot::new()
            .title("My Plot")
            .xlabel("X")
            .line(&x_data, &y_data)
            .into();

        // Version 2: Plot config after series
        let plot2: Plot = Plot::new()
            .line(&x_data, &y_data)
            .title("My Plot")
            .xlabel("X")
            .into();

        // Both should have the same configuration
        // Verify titles match (PlotText doesn't impl PartialEq)
        match (&plot1.display.title, &plot2.display.title) {
            (Some(data::PlotText::Static(s1)), Some(data::PlotText::Static(s2))) => {
                assert_eq!(s1, s2);
            }
            _ => panic!("Expected matching Static PlotText titles"),
        }
        match (&plot1.display.xlabel, &plot2.display.xlabel) {
            (Some(data::PlotText::Static(s1)), Some(data::PlotText::Static(s2))) => {
                assert_eq!(s1, s2);
            }
            _ => panic!("Expected matching Static PlotText xlabels"),
        }
        assert_eq!(plot1.series_mgr.series.len(), plot2.series_mgr.series.len());
    }

    #[test]
    fn test_generic_function_with_into_plot() {
        use builder::IntoPlot;

        // Function that accepts any IntoPlot type
        fn count_series(p: impl IntoPlot) -> usize {
            p.into_plot().series_mgr.series.len()
        }

        let x_data = vec![1.0, 2.0, 3.0];
        let y_data = vec![2.0, 4.0, 3.0];

        // Works with Plot
        assert_eq!(count_series(Plot::new()), 0);

        // Works with PlotSeriesBuilder
        let builder = Plot::new().line(&x_data, &y_data);
        assert_eq!(count_series(builder), 1);
    }

    #[test]
    fn test_implicit_conversion_in_function_param() {
        // Test that Into<Plot> works for function parameters
        fn accepts_into_plot(p: impl Into<Plot>) -> Plot {
            p.into()
        }

        let x_data = vec![1.0, 2.0, 3.0];
        let y_data = vec![2.0, 4.0, 3.0];

        let builder = Plot::new().line(&x_data, &y_data).label("Implicit");
        let plot = accepts_into_plot(builder);

        assert_eq!(plot.series_mgr.series.len(), 1);
        assert_eq!(
            plot.series_mgr.series[0].label,
            Some("Implicit".to_string())
        );
    }

    #[test]
    fn test_typst_toggle_mode_switch() {
        let plot = Plot::new().typst(true);
        assert_eq!(plot.display.text_engine, TextEngineMode::Typst);

        let plot = plot.typst(false);
        assert_eq!(plot.display.text_engine, TextEngineMode::Plain);
    }

    #[test]
    fn test_plot_builder_typst_forwarding() {
        let x = vec![0.0, 1.0, 2.0];
        let y = vec![1.0, 2.0, 3.0];

        let plot: Plot = Plot::new().line(&x, &y).typst(true).into();
        assert_eq!(plot.display.text_engine, TextEngineMode::Typst);
    }

    #[test]
    fn test_plot_series_builder_typst_forwarding() {
        let x = vec![0.0, 1.0, 2.0];
        let y = vec![1.0, 2.0, 3.0];

        let plot = Plot::new()
            .line(&x, &y)
            .label("Series")
            .typst(true)
            .end_series();
        assert_eq!(plot.display.text_engine, TextEngineMode::Typst);
    }

    #[cfg(not(feature = "typst-math"))]
    #[test]
    fn test_typst_feature_gate_error() {
        let x = vec![0.0, 1.0, 2.0];
        let y = vec![1.0, 2.0, 3.0];

        let result = Plot::new().line(&x, &y).title("Title").typst(true).render();

        assert!(matches!(
            result,
            Err(PlottingError::FeatureNotEnabled { ref feature, .. }) if feature == "typst-math"
        ));
    }

    #[cfg(feature = "typst-math")]
    #[test]
    fn test_invalid_typst_snippet_returns_typst_error() {
        let x = vec![0.0, 1.0, 2.0];
        let y = vec![1.0, 2.0, 3.0];

        let result = Plot::new()
            .line(&x, &y)
            .title("#let broken =")
            .typst(true)
            .render();

        assert!(matches!(result, Err(PlottingError::TypstError(_))));
    }
}
