embedded_charts/chart/
line.rs

1//! Line chart implementation with no_std compatibility.
2//!
3//! This module provides a comprehensive line chart implementation optimized for embedded systems.
4//! It supports multiple styling options, markers, area fills, and smooth curves while maintaining
5//! memory efficiency and performance suitable for resource-constrained environments.
6//!
7//! # Features
8//!
9//! - **Multi-series support**: Display multiple data series with different colors
10//! - **Marker customization**: Various shapes (circle, square, diamond, triangle) with configurable size and color
11//! - **Area filling**: Fill the area under the line with customizable colors
12//! - **Smooth curves**: Optional bezier curve smoothing for professional appearance
13//! - **Grid integration**: Support for both legacy and modern grid systems
14//! - **Axis integration**: Full support for linear axes with labels and ticks
15//! - **Animation support**: Real-time data streaming and smooth transitions (feature-gated)
16//! - **Memory efficient**: Static allocation with compile-time bounds
17//!
18//! # Basic Usage
19//!
20//! ```rust
21//! use embedded_charts::prelude::*;
22//! use embedded_graphics::pixelcolor::Rgb565;
23//!
24//! // Create sample data
25//! let mut data: StaticDataSeries<Point2D, 256> = StaticDataSeries::new();
26//! data.push(Point2D::new(0.0, 10.0))?;
27//! data.push(Point2D::new(1.0, 20.0))?;
28//! data.push(Point2D::new(2.0, 15.0))?;
29//!
30//! // Create a basic line chart
31//! let chart = LineChart::builder()
32//!     .line_color(Rgb565::BLUE)
33//!     .line_width(2)
34//!     .build()?;
35//!
36//! // Configure the chart
37//! let config: ChartConfig<Rgb565> = ChartConfig::default();
38//! let viewport = Rectangle::new(Point::zero(), Size::new(320, 240));
39//!
40//! // Render to display (display would be provided by your embedded target)  
41//! // chart.draw(&data, &config, viewport, &mut display)?;
42//! # Ok::<(), embedded_charts::error::ChartError>(())
43//! ```
44//!
45//! # Advanced Styling
46//!
47//! ```rust
48//! use embedded_charts::prelude::*;
49//! use embedded_graphics::pixelcolor::Rgb565;
50//!
51//! let chart = LineChart::builder()
52//!     .line_color(Rgb565::BLUE)
53//!     .line_width(3)
54//!     .fill_area(Rgb565::CSS_LIGHT_BLUE)
55//!     .with_markers(MarkerStyle {
56//!         shape: MarkerShape::Circle,
57//!         size: 8,
58//!         color: Rgb565::RED,
59//!         visible: true,
60//!     })
61//!     .smooth(true)
62//!     .build()?;
63//! # Ok::<(), embedded_charts::error::ChartError>(())
64//! ```
65//!
66//! # With Axes and Grid
67//!
68//! ```rust
69//! use embedded_charts::prelude::*;
70//! use embedded_graphics::pixelcolor::Rgb565;
71//!
72//! // Simple example with line chart styling
73//! let chart = LineChart::builder()
74//!     .line_color(Rgb565::BLUE)
75//!     .line_width(2)
76//!     .build()?;
77//!
78//! // Axes and grids can be configured through the chart config
79//! // This is a simplified example focusing on basic line chart usage
80//! # Ok::<(), embedded_charts::error::ChartError>(())
81//! ```
82
83use crate::axes::traits::Axis;
84use crate::chart::traits::AxisChart;
85use crate::chart::traits::{Chart, ChartBuilder, ChartConfig, Margins};
86use crate::data::{DataBounds, DataPoint, DataSeries};
87use crate::error::{ChartError, ChartResult};
88use crate::math::NumericConversion;
89use embedded_graphics::{
90    draw_target::DrawTarget,
91    prelude::*,
92    primitives::{Circle, Line, PrimitiveStyle, Rectangle},
93};
94
95/// Line chart implementation for displaying continuous data series.
96///
97/// A line chart connects data points with straight lines (or smooth curves when enabled),
98/// making it ideal for showing trends over time or continuous relationships between variables.
99/// This implementation is optimized for embedded systems with static memory allocation
100/// and efficient rendering.
101///
102/// # Features
103///
104/// - Configurable line styling (color, width, smoothing)
105/// - Optional markers at data points with various shapes
106/// - Area filling under the line
107/// - Integration with grid systems and axes
108/// - Support for animations and real-time data streaming
109///
110/// # Memory Usage
111///
112/// The line chart uses static allocation with a maximum of 256 data points per series.
113/// Additional memory is used for:
114/// - Screen coordinate transformation (256 points)
115/// - Area fill polygon vertices (258 points maximum)
116/// - Grid and axis rendering buffers
117///
118/// # Examples
119///
120/// Basic line chart:
121/// ```rust
122/// use embedded_charts::prelude::*;
123/// use embedded_graphics::pixelcolor::Rgb565;
124///
125/// let chart: LineChart<Rgb565> = LineChart::new();
126/// let mut data: StaticDataSeries<Point2D, 256> = StaticDataSeries::new();
127/// data.push(Point2D::new(0.0, 10.0))?;
128/// data.push(Point2D::new(1.0, 20.0))?;
129/// # Ok::<(), embedded_charts::error::DataError>(())
130/// ```
131///
132/// Styled line chart with markers:
133/// ```rust
134/// use embedded_charts::prelude::*;
135/// use embedded_graphics::pixelcolor::Rgb565;
136///
137/// let chart = LineChart::builder()
138///     .line_color(Rgb565::BLUE)
139///     .line_width(2)
140///     .with_markers(MarkerStyle {
141///         shape: MarkerShape::Circle,
142///         size: 6,
143///         color: Rgb565::RED,
144///         visible: true,
145///     })
146///     .build()?;
147/// # Ok::<(), embedded_charts::error::ChartError>(())
148/// ```
149#[derive(Debug)]
150pub struct LineChart<C: PixelColor> {
151    style: LineChartStyle<C>,
152    config: ChartConfig<C>,
153    grid: Option<crate::grid::GridSystem<C>>,
154    x_axis: Option<crate::axes::LinearAxis<f32, C>>,
155    y_axis: Option<crate::axes::LinearAxis<f32, C>>,
156}
157
158/// Style configuration for line charts.
159///
160/// This structure contains all visual styling options for line charts,
161/// including line appearance, markers, and area fills.
162///
163/// # Examples
164///
165/// ```rust
166/// use embedded_charts::prelude::*;
167/// use embedded_graphics::pixelcolor::Rgb565;
168///
169/// let style = LineChartStyle {
170///     line_color: Rgb565::BLUE,
171///     line_width: 2,
172///     fill_area: true,
173///     fill_color: Some(Rgb565::CSS_LIGHT_BLUE),
174///     markers: Some(MarkerStyle::default()),
175///     smooth: false,
176///     smooth_subdivisions: 8,
177/// };
178/// ```
179#[derive(Debug, Clone)]
180pub struct LineChartStyle<C: PixelColor> {
181    /// Color of the line connecting data points.
182    pub line_color: C,
183    /// Width of the line in pixels (recommended range: 1-10).
184    ///
185    /// Larger widths may impact performance on resource-constrained devices.
186    pub line_width: u32,
187    /// Whether to fill the area under the line.
188    ///
189    /// When enabled, creates a filled polygon from the line to the chart baseline.
190    pub fill_area: bool,
191    /// Fill color for the area under the line.
192    ///
193    /// Only used when `fill_area` is `true`. If `None`, no fill is drawn.
194    pub fill_color: Option<C>,
195    /// Marker style for data points.
196    ///
197    /// When `Some`, markers are drawn at each data point. When `None`, no markers are shown.
198    pub markers: Option<MarkerStyle<C>>,
199    /// Whether to smooth the line using interpolation.
200    ///
201    /// When enabled, creates smooth curves between data points instead of straight lines.
202    /// Uses Catmull-Rom spline interpolation for balanced smoothness and performance.
203    /// This feature may impact performance and is recommended for larger displays.
204    pub smooth: bool,
205    /// Number of subdivisions for smooth curves (only used when smooth = true)
206    pub smooth_subdivisions: u32,
207}
208
209/// Marker style configuration for data points.
210///
211/// Markers are visual indicators drawn at each data point to make individual
212/// values easier to identify. This is particularly useful for sparse data
213/// or when precise values need to be highlighted.
214///
215/// # Examples
216///
217/// ```rust
218/// use embedded_charts::prelude::*;
219/// use embedded_graphics::pixelcolor::Rgb565;
220///
221/// let marker_style = MarkerStyle {
222///     shape: MarkerShape::Circle,
223///     size: 8,
224///     color: Rgb565::RED,
225///     visible: true,
226/// };
227/// ```
228#[derive(Debug, Clone, Copy)]
229pub struct MarkerStyle<C: PixelColor> {
230    /// Shape of the marker.
231    pub shape: MarkerShape,
232    /// Size of the marker in pixels.
233    ///
234    /// This represents the diameter for circles or the side length for squares.
235    /// Recommended range: 4-16 pixels for optimal visibility.
236    pub size: u32,
237    /// Color of the marker.
238    pub color: C,
239    /// Whether markers should be visible.
240    ///
241    /// When `false`, markers are not drawn even if a `MarkerStyle` is provided.
242    pub visible: bool,
243}
244
245/// Available shapes for data point markers.
246///
247/// Each shape provides different visual characteristics:
248/// - `Circle`: Smooth, traditional marker shape
249/// - `Square`: Sharp, geometric appearance
250/// - `Diamond`: Distinctive diamond shape
251/// - `Triangle`: Directional appearance, good for indicating trends
252///
253/// # Performance Notes
254///
255/// - `Circle` and `Square` use embedded-graphics primitives (fastest)
256/// - `Diamond` and `Triangle` use custom rendering (slightly slower)
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum MarkerShape {
259    /// Circular marker - smooth and traditional appearance.
260    Circle,
261    /// Square marker - sharp, geometric appearance.
262    Square,
263    /// Diamond marker - distinctive diamond shape.
264    Diamond,
265    /// Triangle marker - directional appearance.
266    Triangle,
267}
268
269impl<C: PixelColor> LineChart<C>
270where
271    C: From<embedded_graphics::pixelcolor::Rgb565>,
272{
273    /// Create a new line chart with default styling.
274    ///
275    /// This creates a line chart with:
276    /// - Blue line color
277    /// - 1-pixel line width
278    /// - No area fill
279    /// - No markers
280    /// - No smoothing
281    /// - Default margins (10 pixels on all sides)
282    ///
283    /// # Examples
284    ///
285    /// ```rust
286    /// use embedded_charts::prelude::*;
287    /// use embedded_graphics::pixelcolor::Rgb565;
288    ///
289    /// let chart: LineChart<Rgb565> = LineChart::new();
290    /// ```
291    pub fn new() -> Self {
292        Self {
293            style: LineChartStyle::default(),
294            config: ChartConfig::default(),
295            grid: None,
296            x_axis: None,
297            y_axis: None,
298        }
299    }
300
301    /// Create a builder for configuring the line chart.
302    ///
303    /// The builder pattern provides a fluent interface for configuring
304    /// all aspects of the line chart before creation.
305    ///
306    /// # Examples
307    ///
308    /// ```rust
309    /// use embedded_charts::prelude::*;
310    /// use embedded_graphics::pixelcolor::Rgb565;
311    ///
312    /// let chart = LineChart::builder()
313    ///     .line_color(Rgb565::BLUE)
314    ///     .line_width(2)
315    ///     .with_markers(MarkerStyle::default())
316    ///     .build()?;
317    /// # Ok::<(), embedded_charts::error::ChartError>(())
318    /// ```
319    pub fn builder() -> LineChartBuilder<C> {
320        LineChartBuilder::new()
321    }
322
323    /// Set the line style configuration.
324    ///
325    /// This replaces the entire style configuration with the provided one.
326    /// Use the builder pattern for more granular control.
327    ///
328    /// # Arguments
329    ///
330    /// * `style` - The new line chart style configuration
331    ///
332    /// # Examples
333    ///
334    /// ```rust
335    /// use embedded_charts::prelude::*;
336    /// use embedded_graphics::pixelcolor::Rgb565;
337    ///
338    /// let mut chart = LineChart::new();
339    /// let style = LineChartStyle {
340    ///     line_color: Rgb565::RED,
341    ///     line_width: 3,
342    ///     fill_area: true,
343    ///     fill_color: Some(Rgb565::CSS_LIGHT_CORAL),
344    ///     markers: None,
345    ///     smooth: false,
346    ///     smooth_subdivisions: 8,
347    /// };
348    /// chart.set_style(style);
349    /// ```
350    pub fn set_style(&mut self, style: LineChartStyle<C>) {
351        self.style = style;
352    }
353
354    /// Get the current line style configuration.
355    ///
356    /// Returns a reference to the current style configuration,
357    /// allowing inspection of current settings.
358    ///
359    /// # Returns
360    ///
361    /// A reference to the current `LineChartStyle`
362    pub fn style(&self) -> &LineChartStyle<C> {
363        &self.style
364    }
365
366    /// Set the chart configuration.
367    ///
368    /// This includes general chart settings like title, background color,
369    /// margins, and grid visibility.
370    ///
371    /// # Arguments
372    ///
373    /// * `config` - The new chart configuration
374    pub fn set_config(&mut self, config: ChartConfig<C>) {
375        self.config = config;
376    }
377
378    /// Get the current chart configuration.
379    ///
380    /// # Returns
381    ///
382    /// A reference to the current `ChartConfig`
383    pub fn config(&self) -> &ChartConfig<C> {
384        &self.config
385    }
386
387    /// Set the grid system for the chart.
388    ///
389    /// The grid system draws background grid lines to help with data reading.
390    /// Pass `None` to disable the grid.
391    ///
392    /// # Arguments
393    ///
394    /// * `grid` - Optional grid system configuration
395    ///
396    /// # Examples
397    ///
398    /// ```rust
399    /// use embedded_charts::prelude::*;
400    /// use embedded_graphics::pixelcolor::Rgb565;
401    ///
402    /// let mut chart: LineChart<Rgb565> = LineChart::new();
403    /// // Grid configuration is simplified in this example
404    /// let grid: LinearGrid<Rgb565> = LinearGrid::new(GridOrientation::Horizontal, GridSpacing::DataUnits(10.0));
405    /// // chart.set_grid(Some(grid)); // Grid system API still evolving
406    /// # Ok::<(), embedded_charts::error::ChartError>(())
407    /// ```
408    pub fn set_grid(&mut self, grid: Option<crate::grid::GridSystem<C>>) {
409        self.grid = grid;
410    }
411
412    /// Get the current grid system configuration.
413    ///
414    /// # Returns
415    ///
416    /// An optional reference to the current grid system
417    pub fn grid(&self) -> Option<&crate::grid::GridSystem<C>> {
418        self.grid.as_ref()
419    }
420
421    /// Transform data coordinates to screen coordinates using math abstraction
422    fn transform_point<P>(
423        &self,
424        point: &P,
425        data_bounds: &DataBounds<P::X, P::Y>,
426        viewport: Rectangle,
427    ) -> Point
428    where
429        P: DataPoint,
430        P::X: NumericConversion<P::X> + Into<f32> + Copy,
431        P::Y: NumericConversion<P::Y> + Into<f32> + Copy,
432    {
433        // Convert to our math abstraction layer
434        let data_x = point.x().into().to_number();
435        let data_y = point.y().into().to_number();
436
437        // Use axis ranges if available, otherwise fall back to data bounds
438        let (min_x, max_x) = if let Some(ref x_axis) = self.x_axis {
439            let axis_min: f32 = x_axis.min();
440            let axis_max: f32 = x_axis.max();
441            (axis_min.to_number(), axis_max.to_number())
442        } else {
443            (
444                data_bounds.min_x.into().to_number(),
445                data_bounds.max_x.into().to_number(),
446            )
447        };
448
449        let (min_y, max_y) = if let Some(ref y_axis) = self.y_axis {
450            let axis_min: f32 = y_axis.min();
451            let axis_max: f32 = y_axis.max();
452            (axis_min.to_number(), axis_max.to_number())
453        } else {
454            (
455                data_bounds.min_y.into().to_number(),
456                data_bounds.max_y.into().to_number(),
457            )
458        };
459
460        // Apply margins to get the actual drawing area
461        let draw_area = self.config.margins.apply_to(viewport);
462
463        // Normalize to 0-1 range using math abstraction
464        let norm_x = if f32::from_number(max_x) > f32::from_number(min_x) {
465            let range_x = f32::from_number(max_x - min_x);
466            let offset_x = f32::from_number(data_x - min_x);
467            (offset_x / range_x).to_number()
468        } else {
469            0.5f32.to_number()
470        };
471
472        let norm_y = if f32::from_number(max_y) > f32::from_number(min_y) {
473            let range_y = f32::from_number(max_y - min_y);
474            let offset_y = f32::from_number(data_y - min_y);
475            (offset_y / range_y).to_number()
476        } else {
477            0.5f32.to_number()
478        };
479
480        // Transform to screen coordinates (Y is flipped)
481        let norm_x_f32 = f32::from_number(norm_x);
482        let norm_y_f32 = f32::from_number(norm_y);
483
484        let screen_x =
485            draw_area.top_left.x + (norm_x_f32 * (draw_area.size.width as f32 - 1.0)) as i32;
486        let screen_y = draw_area.top_left.y + draw_area.size.height as i32
487            - 1
488            - (norm_y_f32 * (draw_area.size.height as f32 - 1.0)) as i32;
489
490        Point::new(screen_x, screen_y)
491    }
492
493    /// Draw markers at data points
494    fn draw_markers<D>(
495        &self,
496        data: &crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>,
497        data_bounds: &DataBounds<f32, f32>,
498        viewport: Rectangle,
499        target: &mut D,
500    ) -> ChartResult<()>
501    where
502        D: DrawTarget<Color = C>,
503    {
504        if let Some(marker_style) = &self.style.markers {
505            if marker_style.visible {
506                for point in data.iter() {
507                    let screen_point = self.transform_point(&point, data_bounds, viewport);
508                    self.draw_marker(screen_point, marker_style, target)?;
509                }
510            }
511        }
512        Ok(())
513    }
514
515    /// Draw a single marker
516    fn draw_marker<D>(
517        &self,
518        center: Point,
519        marker_style: &MarkerStyle<C>,
520        target: &mut D,
521    ) -> ChartResult<()>
522    where
523        D: DrawTarget<Color = C>,
524    {
525        let style = PrimitiveStyle::with_fill(marker_style.color);
526        let radius = marker_style.size / 2;
527
528        match marker_style.shape {
529            MarkerShape::Circle => {
530                Circle::new(
531                    Point::new(center.x - radius as i32, center.y - radius as i32),
532                    marker_style.size,
533                )
534                .into_styled(style)
535                .draw(target)
536                .map_err(|_| ChartError::RenderingError)?;
537            }
538            MarkerShape::Square => {
539                Rectangle::new(
540                    Point::new(center.x - radius as i32, center.y - radius as i32),
541                    Size::new(marker_style.size, marker_style.size),
542                )
543                .into_styled(style)
544                .draw(target)
545                .map_err(|_| ChartError::RenderingError)?;
546            }
547            MarkerShape::Diamond => {
548                use crate::render::PrimitiveRenderer;
549                use crate::style::FillStyle;
550
551                let fill_style = FillStyle::solid(marker_style.color);
552                PrimitiveRenderer::draw_diamond(
553                    center,
554                    marker_style.size,
555                    None,
556                    Some(&fill_style),
557                    target,
558                )
559                .map_err(|_| ChartError::RenderingError)?;
560            }
561            MarkerShape::Triangle => {
562                use crate::render::PrimitiveRenderer;
563                use crate::style::FillStyle;
564
565                let fill_style = FillStyle::solid(marker_style.color);
566                let half_size = marker_style.size as i32 / 2;
567                let p1 = Point::new(center.x, center.y - half_size);
568                let p2 = Point::new(center.x - half_size, center.y + half_size);
569                let p3 = Point::new(center.x + half_size, center.y + half_size);
570
571                PrimitiveRenderer::draw_triangle(p1, p2, p3, None, Some(&fill_style), target)
572                    .map_err(|_| ChartError::RenderingError)?;
573            }
574        }
575
576        Ok(())
577    }
578
579    /// Draw area fill under the line
580    fn draw_area_fill<D>(
581        &self,
582        screen_points: &heapless::Vec<Point, 512>,
583        fill_color: C,
584        viewport: Rectangle,
585        _data_bounds: &DataBounds<f32, f32>,
586        target: &mut D,
587    ) -> ChartResult<()>
588    where
589        D: DrawTarget<Color = C>,
590    {
591        if screen_points.len() < 2 {
592            return Ok(());
593        }
594
595        // Get the chart area (with margins applied)
596        let chart_area = self.config.margins.apply_to(viewport);
597        let baseline_y = chart_area.top_left.y + chart_area.size.height as i32 - 1;
598
599        use embedded_graphics::primitives::{Line, PrimitiveStyle};
600        let line_style = PrimitiveStyle::with_stroke(fill_color, 1);
601
602        // Draw horizontal fill lines using scanline approach
603        let min_x = screen_points
604            .iter()
605            .map(|p| p.x)
606            .min()
607            .unwrap_or(chart_area.top_left.x);
608        let max_x = screen_points
609            .iter()
610            .map(|p| p.x)
611            .max()
612            .unwrap_or(chart_area.top_left.x);
613
614        // For each x position, find the curve y and draw a vertical line to baseline
615        for x in min_x..=max_x {
616            if x < chart_area.top_left.x
617                || x >= chart_area.top_left.x + chart_area.size.width as i32
618            {
619                continue;
620            }
621
622            // Find the y value on the curve at this x position
623            let mut curve_y = baseline_y;
624
625            // Linear interpolation between adjacent points
626            for window in screen_points.windows(2) {
627                if let [p1, p2] = window {
628                    if (p1.x <= x && x <= p2.x) || (p2.x <= x && x <= p1.x) {
629                        if p1.x == p2.x {
630                            curve_y = p1.y.min(p2.y);
631                        } else {
632                            let t = (x - p1.x) as f32 / (p2.x - p1.x) as f32;
633                            curve_y = (p1.y as f32 + t * (p2.y - p1.y) as f32) as i32;
634                        }
635                        break;
636                    }
637                }
638            }
639
640            // Clip curve_y to chart area
641            curve_y = curve_y.clamp(
642                chart_area.top_left.y,
643                chart_area.top_left.y + chart_area.size.height as i32 - 1,
644            );
645
646            // Draw vertical line from curve to baseline
647            if curve_y <= baseline_y {
648                let top_point = Point::new(x, curve_y);
649                let bottom_point = Point::new(x, baseline_y);
650
651                Line::new(top_point, bottom_point)
652                    .into_styled(line_style)
653                    .draw(target)
654                    .map_err(|_| ChartError::RenderingError)?;
655            }
656        }
657
658        Ok(())
659    }
660}
661
662impl<C: PixelColor> Default for LineChart<C>
663where
664    C: From<embedded_graphics::pixelcolor::Rgb565>,
665{
666    fn default() -> Self {
667        Self::new()
668    }
669}
670
671impl<C: PixelColor + 'static> Chart<C> for LineChart<C>
672where
673    C: From<embedded_graphics::pixelcolor::Rgb565>,
674{
675    type Data = crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>;
676    type Config = ChartConfig<C>;
677
678    fn draw<D>(
679        &self,
680        data: &Self::Data,
681        config: &Self::Config,
682        viewport: Rectangle,
683        target: &mut D,
684    ) -> ChartResult<()>
685    where
686        D: DrawTarget<Color = C>,
687        Self::Data: DataSeries,
688        <Self::Data as DataSeries>::Item: DataPoint,
689        <<Self::Data as DataSeries>::Item as DataPoint>::X: Into<f32> + Copy + PartialOrd,
690        <<Self::Data as DataSeries>::Item as DataPoint>::Y: Into<f32> + Copy + PartialOrd,
691    {
692        if data.is_empty() {
693            return Err(ChartError::InsufficientData);
694        }
695
696        // Calculate data bounds
697        let data_bounds = data.bounds()?;
698
699        // Draw background if specified
700        if let Some(bg_color) = config.background_color {
701            Rectangle::new(viewport.top_left, viewport.size)
702                .into_styled(PrimitiveStyle::with_fill(bg_color))
703                .draw(target)
704                .map_err(|_| ChartError::RenderingError)?;
705        }
706
707        // First, draw grid lines from axes (background layer)
708        {
709            let chart_area = config.margins.apply_to(viewport);
710
711            // Draw grid lines from X-axis
712            if let Some(ref x_axis) = self.x_axis {
713                x_axis.draw_grid_lines(chart_area, chart_area, target)?;
714            }
715
716            // Draw grid lines from Y-axis
717            if let Some(ref y_axis) = self.y_axis {
718                y_axis.draw_grid_lines(chart_area, chart_area, target)?;
719            }
720        }
721
722        // Draw grid if present (legacy grid system)
723        if let Some(ref grid) = self.grid {
724            let chart_area = config.margins.apply_to(viewport);
725            grid.draw(chart_area, target)?;
726        }
727
728        // Collect and potentially smooth the data points
729        let data_to_render = if self.style.smooth && data.len() > 2 {
730            // Create interpolated smooth curve
731            use crate::math::interpolation::{
732                CurveInterpolator, InterpolationConfig, InterpolationType,
733            };
734
735            let mut input_points = heapless::Vec::<crate::data::Point2D, 256>::new();
736            for point in data.iter() {
737                input_points
738                    .push(point)
739                    .map_err(|_| ChartError::MemoryFull)?;
740            }
741
742            let interpolation_config = InterpolationConfig {
743                interpolation_type: InterpolationType::CatmullRom,
744                subdivisions: self.style.smooth_subdivisions,
745                tension: 0.5,
746                closed: false,
747            };
748
749            let interpolated =
750                CurveInterpolator::interpolate(&input_points, &interpolation_config)?;
751
752            // Create a temporary data series with interpolated points
753            let mut smooth_data = crate::data::series::StaticDataSeries::new();
754            for point in interpolated.iter() {
755                smooth_data
756                    .push(*point)
757                    .map_err(|_| ChartError::MemoryFull)?;
758            }
759            smooth_data
760        } else {
761            // Use original data
762            data.clone()
763        };
764
765        // Transform data points to screen coordinates
766        let mut screen_points = heapless::Vec::<Point, 512>::new();
767        for point in data_to_render.iter() {
768            let screen_point = self.transform_point(&point, &data_bounds, viewport);
769            screen_points
770                .push(screen_point)
771                .map_err(|_| ChartError::MemoryFull)?;
772        }
773
774        // Draw area fill if enabled
775        if self.style.fill_area {
776            if let Some(fill_color) = self.style.fill_color {
777                self.draw_area_fill(&screen_points, fill_color, viewport, &data_bounds, target)?;
778            }
779        }
780
781        // Draw lines between consecutive points
782        let line_style = PrimitiveStyle::with_stroke(self.style.line_color, self.style.line_width);
783        for window in screen_points.windows(2) {
784            if let [p1, p2] = window {
785                Line::new(*p1, *p2)
786                    .into_styled(line_style)
787                    .draw(target)
788                    .map_err(|_| ChartError::RenderingError)?;
789            }
790        }
791
792        // Draw markers
793        self.draw_markers(data, &data_bounds, viewport, target)?;
794
795        // Finally, draw axis lines, ticks, and labels (foreground layer)
796        {
797            let chart_area = config.margins.apply_to(viewport);
798
799            // Draw X-axis (without grid lines)
800            if let Some(ref x_axis) = self.x_axis {
801                x_axis.draw_axis_only(chart_area, target)?;
802            }
803
804            // Draw Y-axis (without grid lines)
805            if let Some(ref y_axis) = self.y_axis {
806                y_axis.draw_axis_only(chart_area, target)?;
807            }
808        }
809
810        Ok(())
811    }
812}
813
814impl<C: PixelColor> Default for LineChartStyle<C>
815where
816    C: From<embedded_graphics::pixelcolor::Rgb565>,
817{
818    fn default() -> Self {
819        Self {
820            line_color: embedded_graphics::pixelcolor::Rgb565::BLUE.into(),
821            line_width: 1,
822            fill_area: false,
823            fill_color: None,
824            markers: None,
825            smooth: false,
826            smooth_subdivisions: 8,
827        }
828    }
829}
830
831impl<C: PixelColor> Default for MarkerStyle<C>
832where
833    C: From<embedded_graphics::pixelcolor::Rgb565>,
834{
835    fn default() -> Self {
836        Self {
837            shape: MarkerShape::Circle,
838            size: 4,
839            color: embedded_graphics::pixelcolor::Rgb565::RED.into(),
840            visible: true,
841        }
842    }
843}
844
845/// Builder for line charts
846#[derive(Debug)]
847pub struct LineChartBuilder<C: PixelColor> {
848    style: LineChartStyle<C>,
849    config: ChartConfig<C>,
850    grid: Option<crate::grid::GridSystem<C>>,
851    x_axis: Option<crate::axes::LinearAxis<f32, C>>,
852    y_axis: Option<crate::axes::LinearAxis<f32, C>>,
853}
854
855impl<C: PixelColor> LineChartBuilder<C>
856where
857    C: From<embedded_graphics::pixelcolor::Rgb565>,
858{
859    /// Create a new line chart builder
860    pub fn new() -> Self {
861        Self {
862            style: LineChartStyle::default(),
863            config: ChartConfig::default(),
864            grid: None,
865            x_axis: None,
866            y_axis: None,
867        }
868    }
869
870    /// Set the line color
871    pub fn line_color(mut self, color: C) -> Self {
872        self.style.line_color = color;
873        self
874    }
875
876    /// Set the line width
877    pub fn line_width(mut self, width: u32) -> Self {
878        self.style.line_width = width.clamp(1, 10);
879        self
880    }
881
882    /// Enable area filling with the specified color
883    pub fn fill_area(mut self, color: C) -> Self {
884        self.style.fill_area = true;
885        self.style.fill_color = Some(color);
886        self
887    }
888
889    /// Add markers to data points
890    pub fn with_markers(mut self, marker_style: MarkerStyle<C>) -> Self {
891        self.style.markers = Some(marker_style);
892        self
893    }
894
895    /// Set the chart title
896    pub fn with_title(mut self, title: &str) -> Self {
897        if let Ok(title_string) = heapless::String::try_from(title) {
898            self.config.title = Some(title_string);
899        }
900        self
901    }
902
903    /// Set the background color
904    pub fn background_color(mut self, color: C) -> Self {
905        self.config.background_color = Some(color);
906        self
907    }
908
909    /// Set the chart margins
910    pub fn margins(mut self, margins: Margins) -> Self {
911        self.config.margins = margins;
912        self
913    }
914
915    /// Enable smooth line rendering
916    pub fn smooth(mut self, smooth: bool) -> Self {
917        self.style.smooth = smooth;
918        self
919    }
920
921    /// Set the number of subdivisions for smooth curves
922    pub fn smooth_subdivisions(mut self, subdivisions: u32) -> Self {
923        self.style.smooth_subdivisions = subdivisions.clamp(2, 16);
924        self
925    }
926
927    /// Set the grid system
928    pub fn with_grid(mut self, grid: crate::grid::GridSystem<C>) -> Self {
929        self.grid = Some(grid);
930        self
931    }
932
933    /// Set the X-axis configuration
934    pub fn with_x_axis(mut self, axis: crate::axes::LinearAxis<f32, C>) -> Self {
935        self.x_axis = Some(axis);
936        self
937    }
938
939    /// Set the Y-axis configuration
940    pub fn with_y_axis(mut self, axis: crate::axes::LinearAxis<f32, C>) -> Self {
941        self.y_axis = Some(axis);
942        self
943    }
944}
945
946impl<C: PixelColor + 'static> ChartBuilder<C> for LineChartBuilder<C>
947where
948    C: From<embedded_graphics::pixelcolor::Rgb565>,
949{
950    type Chart = LineChart<C>;
951    type Error = ChartError;
952
953    fn build(self) -> Result<Self::Chart, Self::Error> {
954        Ok(LineChart {
955            style: self.style,
956            config: self.config,
957            grid: self.grid,
958            x_axis: self.x_axis,
959            y_axis: self.y_axis,
960        })
961    }
962}
963
964impl<C: PixelColor> Default for LineChartBuilder<C>
965where
966    C: From<embedded_graphics::pixelcolor::Rgb565>,
967{
968    fn default() -> Self {
969        Self::new()
970    }
971}
972
973#[cfg(test)]
974mod tests {
975    use super::*;
976    use embedded_graphics::pixelcolor::Rgb565;
977
978    #[test]
979    fn test_line_chart_creation() {
980        let chart: LineChart<Rgb565> = LineChart::new();
981        assert_eq!(chart.style().line_width, 1);
982    }
983
984    #[test]
985    fn test_line_chart_builder() {
986        let chart = LineChart::builder()
987            .line_color(Rgb565::RED)
988            .line_width(3)
989            .build()
990            .unwrap();
991
992        assert_eq!(chart.style().line_color, Rgb565::RED);
993        assert_eq!(chart.style().line_width, 3);
994    }
995
996    #[test]
997    fn test_marker_style() {
998        let marker = MarkerStyle {
999            shape: MarkerShape::Diamond,
1000            size: 8,
1001            color: Rgb565::GREEN,
1002            visible: true,
1003        };
1004
1005        assert_eq!(marker.shape, MarkerShape::Diamond);
1006        assert_eq!(marker.size, 8);
1007    }
1008}
1009
1010impl<C: PixelColor + 'static> AxisChart<C> for LineChart<C>
1011where
1012    C: From<embedded_graphics::pixelcolor::Rgb565>,
1013{
1014    type XAxis = crate::axes::LinearAxis<f32, C>;
1015    type YAxis = crate::axes::LinearAxis<f32, C>;
1016
1017    fn set_x_axis(&mut self, axis: crate::axes::LinearAxis<f32, C>) {
1018        self.x_axis = Some(axis);
1019    }
1020
1021    fn set_y_axis(&mut self, axis: crate::axes::LinearAxis<f32, C>) {
1022        self.y_axis = Some(axis);
1023    }
1024
1025    fn x_axis(&self) -> ChartResult<&crate::axes::LinearAxis<f32, C>> {
1026        self.x_axis.as_ref().ok_or(ChartError::InvalidConfiguration)
1027    }
1028
1029    fn y_axis(&self) -> ChartResult<&crate::axes::LinearAxis<f32, C>> {
1030        self.y_axis.as_ref().ok_or(ChartError::InvalidConfiguration)
1031    }
1032}
1033
1034/// Animated line chart that extends LineChart with animation capabilities
1035#[cfg(feature = "animations")]
1036#[derive(Debug)]
1037pub struct AnimatedLineChart<C: PixelColor> {
1038    /// Base line chart
1039    base_chart: LineChart<C>,
1040    /// Current animated data (interpolated values)
1041    current_data: Option<crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>>,
1042}
1043
1044#[cfg(feature = "animations")]
1045impl<C: PixelColor + 'static> AnimatedLineChart<C>
1046where
1047    C: From<embedded_graphics::pixelcolor::Rgb565>,
1048{
1049    /// Create a new animated line chart
1050    pub fn new() -> Self {
1051        Self {
1052            base_chart: LineChart::new(),
1053            current_data: None,
1054        }
1055    }
1056
1057    /// Create a builder for configuring the animated line chart
1058    pub fn builder() -> AnimatedLineChartBuilder<C> {
1059        AnimatedLineChartBuilder::new()
1060    }
1061
1062    /// Set the line style
1063    pub fn set_style(&mut self, style: LineChartStyle<C>) {
1064        self.base_chart.set_style(style);
1065    }
1066
1067    /// Get the current line style
1068    pub fn style(&self) -> &LineChartStyle<C> {
1069        self.base_chart.style()
1070    }
1071
1072    /// Set the chart configuration
1073    pub fn set_config(&mut self, config: ChartConfig<C>) {
1074        self.base_chart.set_config(config);
1075    }
1076
1077    /// Get the chart configuration
1078    pub fn config(&self) -> &ChartConfig<C> {
1079        self.base_chart.config()
1080    }
1081
1082    /// Set the grid system
1083    pub fn set_grid(&mut self, grid: Option<crate::grid::GridSystem<C>>) {
1084        self.base_chart.set_grid(grid);
1085    }
1086
1087    /// Get the grid system
1088    pub fn grid(&self) -> Option<&crate::grid::GridSystem<C>> {
1089        self.base_chart.grid()
1090    }
1091
1092    /// Set the current animated data for rendering
1093    pub fn set_animated_data(
1094        &mut self,
1095        data: Option<crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>>,
1096    ) {
1097        self.current_data = data;
1098    }
1099
1100    /// Get the current animated data
1101    pub fn animated_data(
1102        &self,
1103    ) -> Option<&crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>> {
1104        self.current_data.as_ref()
1105    }
1106
1107    /// Get access to the base chart for configuration
1108    pub fn base_chart(&self) -> &LineChart<C> {
1109        &self.base_chart
1110    }
1111
1112    /// Get mutable access to the base chart for configuration
1113    pub fn base_chart_mut(&mut self) -> &mut LineChart<C> {
1114        &mut self.base_chart
1115    }
1116
1117    /// Interpolate between two data series using a ChartAnimator
1118    pub fn interpolate_with_animator(
1119        animator: &crate::animation::ChartAnimator<
1120            crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>,
1121        >,
1122        progress: crate::animation::Progress,
1123    ) -> Option<crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>> {
1124        animator.value_at(progress)
1125    }
1126}
1127
1128#[cfg(feature = "animations")]
1129impl<C: PixelColor + 'static> Default for AnimatedLineChart<C>
1130where
1131    C: From<embedded_graphics::pixelcolor::Rgb565>,
1132{
1133    fn default() -> Self {
1134        Self::new()
1135    }
1136}
1137
1138#[cfg(feature = "animations")]
1139impl<C: PixelColor + 'static> Chart<C> for AnimatedLineChart<C>
1140where
1141    C: From<embedded_graphics::pixelcolor::Rgb565>,
1142{
1143    type Data = crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>;
1144    type Config = ChartConfig<C>;
1145
1146    fn draw<D>(
1147        &self,
1148        data: &Self::Data,
1149        config: &Self::Config,
1150        viewport: Rectangle,
1151        target: &mut D,
1152    ) -> ChartResult<()>
1153    where
1154        D: DrawTarget<Color = C>,
1155        Self::Data: crate::data::DataSeries,
1156        <Self::Data as crate::data::DataSeries>::Item: crate::data::DataPoint,
1157        <<Self::Data as crate::data::DataSeries>::Item as crate::data::DataPoint>::X:
1158            Into<f32> + Copy + PartialOrd,
1159        <<Self::Data as crate::data::DataSeries>::Item as crate::data::DataPoint>::Y:
1160            Into<f32> + Copy + PartialOrd,
1161    {
1162        // Use animated data if available, otherwise use provided data
1163        if let Some(ref animated_data) = self.current_data {
1164            self.base_chart
1165                .draw(animated_data, config, viewport, target)
1166        } else {
1167            self.base_chart.draw(data, config, viewport, target)
1168        }
1169    }
1170}
1171
1172#[cfg(feature = "animations")]
1173impl<C: PixelColor + 'static> crate::chart::traits::AnimatedChart<C> for AnimatedLineChart<C>
1174where
1175    C: From<embedded_graphics::pixelcolor::Rgb565>,
1176{
1177    type AnimatedData = crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>;
1178
1179    fn draw_animated<D>(
1180        &self,
1181        data: &Self::Data,
1182        config: &Self::Config,
1183        viewport: Rectangle,
1184        target: &mut D,
1185        _progress: crate::animation::Progress,
1186    ) -> ChartResult<()>
1187    where
1188        D: DrawTarget<Color = C>,
1189    {
1190        // Use the provided data which should already be interpolated by the caller
1191        self.base_chart.draw(data, config, viewport, target)
1192    }
1193
1194    fn create_transition_animator(
1195        &self,
1196        from_data: Self::AnimatedData,
1197        to_data: Self::AnimatedData,
1198        easing: crate::animation::EasingFunction,
1199    ) -> crate::animation::ChartAnimator<Self::AnimatedData> {
1200        crate::animation::ChartAnimator::new(from_data, to_data, easing)
1201    }
1202
1203    fn extract_animated_data(&self, data: &Self::Data) -> ChartResult<Self::AnimatedData> {
1204        // Clone the data series for animation
1205        Ok(data.clone())
1206    }
1207}
1208
1209/// Builder for animated line charts
1210#[cfg(feature = "animations")]
1211#[derive(Debug)]
1212pub struct AnimatedLineChartBuilder<C: PixelColor> {
1213    base_builder: LineChartBuilder<C>,
1214    frame_rate: u32,
1215}
1216
1217#[cfg(feature = "animations")]
1218impl<C: PixelColor + 'static> AnimatedLineChartBuilder<C>
1219where
1220    C: From<embedded_graphics::pixelcolor::Rgb565>,
1221{
1222    /// Create a new animated line chart builder
1223    pub fn new() -> Self {
1224        Self {
1225            base_builder: LineChartBuilder::new(),
1226            frame_rate: 60,
1227        }
1228    }
1229
1230    /// Set the target frame rate
1231    pub fn frame_rate(mut self, fps: u32) -> Self {
1232        self.frame_rate = fps.clamp(1, 120);
1233        self
1234    }
1235
1236    /// Set the line color
1237    pub fn line_color(mut self, color: C) -> Self {
1238        self.base_builder = self.base_builder.line_color(color);
1239        self
1240    }
1241
1242    /// Set the line width
1243    pub fn line_width(mut self, width: u32) -> Self {
1244        self.base_builder = self.base_builder.line_width(width);
1245        self
1246    }
1247
1248    /// Enable area fill with color
1249    pub fn fill_area(mut self, color: C) -> Self {
1250        self.base_builder = self.base_builder.fill_area(color);
1251        self
1252    }
1253
1254    /// Add markers to data points
1255    pub fn with_markers(mut self, marker_style: MarkerStyle<C>) -> Self {
1256        self.base_builder = self.base_builder.with_markers(marker_style);
1257        self
1258    }
1259
1260    /// Set the chart title
1261    pub fn with_title(mut self, title: &str) -> Self {
1262        self.base_builder = self.base_builder.with_title(title);
1263        self
1264    }
1265
1266    /// Set the background color
1267    pub fn background_color(mut self, color: C) -> Self {
1268        self.base_builder = self.base_builder.background_color(color);
1269        self
1270    }
1271
1272    /// Set chart margins
1273    pub fn margins(mut self, margins: Margins) -> Self {
1274        self.base_builder = self.base_builder.margins(margins);
1275        self
1276    }
1277
1278    /// Enable smooth lines
1279    pub fn smooth(mut self, smooth: bool) -> Self {
1280        self.base_builder = self.base_builder.smooth(smooth);
1281        self
1282    }
1283
1284    /// Set the number of subdivisions for smooth curves
1285    pub fn smooth_subdivisions(mut self, subdivisions: u32) -> Self {
1286        self.base_builder = self.base_builder.smooth_subdivisions(subdivisions);
1287        self
1288    }
1289
1290    /// Add grid system
1291    pub fn with_grid(mut self, grid: crate::grid::GridSystem<C>) -> Self {
1292        self.base_builder = self.base_builder.with_grid(grid);
1293        self
1294    }
1295
1296    /// Add X-axis
1297    pub fn with_x_axis(mut self, axis: crate::axes::LinearAxis<f32, C>) -> Self {
1298        self.base_builder = self.base_builder.with_x_axis(axis);
1299        self
1300    }
1301
1302    /// Add Y-axis
1303    pub fn with_y_axis(mut self, axis: crate::axes::LinearAxis<f32, C>) -> Self {
1304        self.base_builder = self.base_builder.with_y_axis(axis);
1305        self
1306    }
1307
1308    /// Build the animated line chart
1309    pub fn build(self) -> ChartResult<AnimatedLineChart<C>> {
1310        let base_chart = self.base_builder.build()?;
1311
1312        Ok(AnimatedLineChart {
1313            base_chart,
1314            current_data: None,
1315        })
1316    }
1317}
1318
1319#[cfg(feature = "animations")]
1320impl<C: PixelColor + 'static> Default for AnimatedLineChartBuilder<C>
1321where
1322    C: From<embedded_graphics::pixelcolor::Rgb565>,
1323{
1324    fn default() -> Self {
1325        Self::new()
1326    }
1327}