embedded_charts/chart/
curve.rs

1//! Smooth curve chart implementation with interpolation support.
2//!
3//! This module provides a specialized chart type for displaying smooth curves using
4//! various interpolation algorithms. It extends the basic line chart functionality
5//! with advanced curve generation capabilities.
6
7use crate::chart::line::{LineChart, LineChartBuilder, LineChartStyle, MarkerStyle};
8use crate::chart::traits::{Chart, ChartBuilder, ChartConfig};
9use crate::data::{DataPoint, DataSeries, Point2D};
10use crate::error::{ChartError, ChartResult};
11use crate::math::interpolation::{CurveInterpolator, InterpolationConfig, InterpolationType};
12use embedded_graphics::{draw_target::DrawTarget, prelude::*};
13use heapless::Vec;
14
15/// A smooth curve chart that uses interpolation to create fluid curves from discrete data points.
16///
17/// This chart type builds upon the LineChart foundation but adds sophisticated curve
18/// interpolation capabilities including cubic splines, Catmull-Rom curves, and Bezier curves.
19/// It automatically generates additional points between input data to create smooth, visually
20/// appealing curves.
21///
22/// # Features
23///
24/// - Multiple interpolation algorithms (cubic spline, Catmull-Rom, Bezier, linear)
25/// - Configurable curve smoothness and tension
26/// - Memory-efficient implementation suitable for embedded systems
27/// - Integration with existing chart styling and theming
28/// - Support for markers, area fills, and grid systems
29///
30/// # Examples
31///
32/// Basic smooth curve:
33/// ```rust
34/// use embedded_charts::prelude::*;
35/// use embedded_graphics::pixelcolor::Rgb565;
36///
37/// let chart = CurveChart::builder()
38///     .line_color(Rgb565::BLUE)
39///     .interpolation_type(InterpolationType::CubicSpline)
40///     .subdivisions(12)
41///     .build()?;
42/// # Ok::<(), embedded_charts::error::ChartError>(())
43/// ```
44///
45/// Artistic Bezier curves:
46/// ```rust
47/// use embedded_charts::prelude::*;
48/// use embedded_graphics::pixelcolor::Rgb565;
49///
50/// let chart = CurveChart::builder()
51///     .line_color(Rgb565::GREEN)
52///     .interpolation_type(InterpolationType::Bezier)
53///     .tension(0.8)
54///     .subdivisions(16)
55///     .with_markers(MarkerStyle::default())
56///     .build()?;
57/// # Ok::<(), embedded_charts::error::ChartError>(())
58/// ```
59#[derive(Debug)]
60pub struct CurveChart<C: PixelColor> {
61    /// Base line chart for rendering and styling
62    base_chart: LineChart<C>,
63    /// Interpolation configuration
64    interpolation_config: InterpolationConfig,
65}
66
67impl<C: PixelColor + 'static> CurveChart<C>
68where
69    C: From<embedded_graphics::pixelcolor::Rgb565>,
70{
71    /// Create a new curve chart with default settings.
72    ///
73    /// Creates a curve chart with:
74    /// - Cubic spline interpolation
75    /// - 8 subdivisions per segment
76    /// - Medium tension (0.5)
77    /// - Default line chart styling
78    pub fn new() -> Self {
79        Self {
80            base_chart: LineChart::new(),
81            interpolation_config: InterpolationConfig::default(),
82        }
83    }
84
85    /// Create a builder for configuring the curve chart.
86    pub fn builder() -> CurveChartBuilder<C> {
87        CurveChartBuilder::new()
88    }
89
90    /// Set the interpolation configuration.
91    ///
92    /// # Arguments
93    /// * `config` - The interpolation configuration to use
94    pub fn set_interpolation_config(&mut self, config: InterpolationConfig) {
95        self.interpolation_config = config;
96    }
97
98    /// Get the current interpolation configuration.
99    pub fn interpolation_config(&self) -> &InterpolationConfig {
100        &self.interpolation_config
101    }
102
103    /// Set the line style configuration.
104    pub fn set_style(&mut self, style: LineChartStyle<C>) {
105        self.base_chart.set_style(style);
106    }
107
108    /// Get the current line style configuration.
109    pub fn style(&self) -> &LineChartStyle<C> {
110        self.base_chart.style()
111    }
112
113    /// Set the chart configuration.
114    pub fn set_config(&mut self, config: ChartConfig<C>) {
115        self.base_chart.set_config(config);
116    }
117
118    /// Get the current chart configuration.
119    pub fn config(&self) -> &ChartConfig<C> {
120        self.base_chart.config()
121    }
122
123    /// Set the grid system for the chart.
124    pub fn set_grid(&mut self, grid: Option<crate::grid::GridSystem<C>>) {
125        self.base_chart.set_grid(grid);
126    }
127
128    /// Get the current grid system configuration.
129    pub fn grid(&self) -> Option<&crate::grid::GridSystem<C>> {
130        self.base_chart.grid()
131    }
132
133    /// Get access to the underlying line chart for advanced configuration.
134    pub fn base_chart(&self) -> &LineChart<C> {
135        &self.base_chart
136    }
137
138    /// Get mutable access to the underlying line chart.
139    pub fn base_chart_mut(&mut self) -> &mut LineChart<C> {
140        &mut self.base_chart
141    }
142
143    /// Generate interpolated curve points from input data.
144    fn interpolate_data(
145        &self,
146        data: &crate::data::series::StaticDataSeries<Point2D, 256>,
147    ) -> ChartResult<Vec<Point2D, 512>> {
148        // Convert data series to slice for interpolation
149        let mut points = Vec::<Point2D, 256>::new();
150        for point in data.iter() {
151            points.push(point).map_err(|_| ChartError::MemoryFull)?;
152        }
153
154        // Perform interpolation
155        CurveInterpolator::interpolate(&points, &self.interpolation_config)
156    }
157
158    /// Transform a data point to screen coordinates using the same logic as LineChart
159    fn transform_curve_point(
160        &self,
161        point: &Point2D,
162        data_bounds: &crate::data::DataBounds<f32, f32>,
163        viewport: embedded_graphics::primitives::Rectangle,
164    ) -> embedded_graphics::prelude::Point {
165        use crate::math::NumericConversion;
166
167        // Convert to math abstraction layer (same as LineChart)
168        let data_x = point.x.to_number();
169        let data_y = point.y.to_number();
170
171        // Use the same bounds as LineChart would
172        let min_x = data_bounds.min_x.to_number();
173        let max_x = data_bounds.max_x.to_number();
174        let min_y = data_bounds.min_y.to_number();
175        let max_y = data_bounds.max_y.to_number();
176
177        // Apply margins to get the actual drawing area (same as LineChart)
178        let draw_area = self.base_chart.config().margins.apply_to(viewport);
179
180        // Normalize to 0-1 range using math abstraction (same as LineChart)
181        let norm_x = if f32::from_number(max_x) > f32::from_number(min_x) {
182            let range_x = f32::from_number(max_x - min_x);
183            let offset_x = f32::from_number(data_x - min_x);
184            (offset_x / range_x).to_number()
185        } else {
186            0.5f32.to_number()
187        };
188
189        let norm_y = if f32::from_number(max_y) > f32::from_number(min_y) {
190            let range_y = f32::from_number(max_y - min_y);
191            let offset_y = f32::from_number(data_y - min_y);
192            (offset_y / range_y).to_number()
193        } else {
194            0.5f32.to_number()
195        };
196
197        // Transform to screen coordinates (Y is flipped) - same as LineChart
198        let norm_x_f32 = f32::from_number(norm_x);
199        let norm_y_f32 = f32::from_number(norm_y);
200
201        let screen_x =
202            draw_area.top_left.x + (norm_x_f32 * (draw_area.size.width as f32 - 1.0)) as i32;
203        let screen_y = draw_area.top_left.y + draw_area.size.height as i32
204            - 1
205            - (norm_y_f32 * (draw_area.size.height as f32 - 1.0)) as i32;
206
207        embedded_graphics::prelude::Point::new(screen_x, screen_y)
208    }
209}
210
211impl<C: PixelColor + 'static> Default for CurveChart<C>
212where
213    C: From<embedded_graphics::pixelcolor::Rgb565>,
214{
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220impl<C: PixelColor + 'static> Chart<C> for CurveChart<C>
221where
222    C: From<embedded_graphics::pixelcolor::Rgb565>,
223{
224    type Data = crate::data::series::StaticDataSeries<Point2D, 256>;
225    type Config = ChartConfig<C>;
226
227    fn draw<D>(
228        &self,
229        data: &Self::Data,
230        config: &Self::Config,
231        viewport: embedded_graphics::primitives::Rectangle,
232        target: &mut D,
233    ) -> ChartResult<()>
234    where
235        D: DrawTarget<Color = C>,
236        Self::Data: DataSeries,
237        <Self::Data as DataSeries>::Item: DataPoint,
238        <<Self::Data as DataSeries>::Item as DataPoint>::X: Into<f32> + Copy + PartialOrd,
239        <<Self::Data as DataSeries>::Item as DataPoint>::Y: Into<f32> + Copy + PartialOrd,
240    {
241        if data.is_empty() {
242            return Err(ChartError::InsufficientData);
243        }
244
245        // Handle case with only one point (can't interpolate)
246        if data.len() == 1 {
247            return self.base_chart.draw(data, config, viewport, target);
248        }
249
250        // Generate interpolated curve points
251        let interpolated_points = self.interpolate_data(data)?;
252
253        // Create a temporary data series with interpolated points
254        let mut curve_data = crate::data::series::StaticDataSeries::new();
255        for point in interpolated_points.iter() {
256            curve_data
257                .push(*point)
258                .map_err(|_| ChartError::MemoryFull)?;
259        }
260
261        // Save the original marker style and remove it temporarily
262        let original_markers = self.base_chart.style().markers;
263
264        // Create a temporary chart without markers for drawing the curve
265        let mut temp_chart = LineChart::builder()
266            .line_color(self.base_chart.style().line_color)
267            .line_width(self.base_chart.style().line_width)
268            .fill_area(
269                self.base_chart
270                    .style()
271                    .fill_color
272                    .unwrap_or(self.base_chart.style().line_color),
273            )
274            .smooth(false) // Already interpolated
275            .build()?;
276
277        if self.base_chart.style().fill_area {
278            if let Some(fill_color) = self.base_chart.style().fill_color {
279                temp_chart = LineChart::builder()
280                    .line_color(self.base_chart.style().line_color)
281                    .line_width(self.base_chart.style().line_width)
282                    .fill_area(fill_color)
283                    .smooth(false)
284                    .build()?;
285            }
286        } else {
287            temp_chart = LineChart::builder()
288                .line_color(self.base_chart.style().line_color)
289                .line_width(self.base_chart.style().line_width)
290                .smooth(false)
291                .build()?;
292        }
293
294        // Draw the smooth curve without markers
295        temp_chart.draw(&curve_data, config, viewport, target)?;
296
297        // Now draw markers at original data points manually
298        if let Some(marker_style) = original_markers {
299            if marker_style.visible {
300                use embedded_graphics::primitives::{Circle, PrimitiveStyle};
301
302                let data_bounds = data.bounds()?;
303
304                for original_point in data.iter() {
305                    // Convert to Point2D for transformation
306                    let point_2d = crate::data::Point2D::new(original_point.x, original_point.y);
307                    // Transform original data point to screen coordinates
308                    let screen_point =
309                        self.transform_curve_point(&point_2d, &data_bounds, viewport);
310
311                    // Draw marker
312                    let marker_primitive_style = PrimitiveStyle::with_fill(marker_style.color);
313                    let radius = marker_style.size / 2;
314
315                    Circle::new(
316                        embedded_graphics::prelude::Point::new(
317                            screen_point.x - radius as i32,
318                            screen_point.y - radius as i32,
319                        ),
320                        marker_style.size,
321                    )
322                    .into_styled(marker_primitive_style)
323                    .draw(target)
324                    .map_err(|_| ChartError::RenderingError)?;
325                }
326            }
327        }
328
329        Ok(())
330    }
331}
332
333/// Builder for curve charts with fluent configuration API.
334#[derive(Debug)]
335pub struct CurveChartBuilder<C: PixelColor> {
336    /// Base line chart builder
337    line_builder: LineChartBuilder<C>,
338    /// Interpolation configuration
339    interpolation_config: InterpolationConfig,
340}
341
342impl<C: PixelColor + 'static> CurveChartBuilder<C>
343where
344    C: From<embedded_graphics::pixelcolor::Rgb565>,
345{
346    /// Create a new curve chart builder.
347    pub fn new() -> Self {
348        Self {
349            line_builder: LineChartBuilder::new(),
350            interpolation_config: InterpolationConfig::default(),
351        }
352    }
353
354    /// Set the interpolation algorithm to use.
355    ///
356    /// # Arguments
357    /// * `interpolation_type` - The type of curve interpolation
358    pub fn interpolation_type(mut self, interpolation_type: InterpolationType) -> Self {
359        self.interpolation_config.interpolation_type = interpolation_type;
360        self
361    }
362
363    /// Set the number of subdivisions between data points.
364    ///
365    /// Higher values create smoother curves but require more memory and processing.
366    /// Recommended range: 4-20 subdivisions.
367    ///
368    /// # Arguments
369    /// * `subdivisions` - Number of interpolated points between each pair of data points
370    pub fn subdivisions(mut self, subdivisions: u32) -> Self {
371        self.interpolation_config.subdivisions = subdivisions.clamp(2, 32);
372        self
373    }
374
375    /// Set the curve tension for spline interpolation.
376    ///
377    /// # Arguments
378    /// * `tension` - Tension value (0.0 = loose curves, 1.0 = tight curves)
379    pub fn tension(mut self, tension: f32) -> Self {
380        self.interpolation_config.tension = tension.clamp(0.0, 1.0);
381        self
382    }
383
384    /// Enable closed curve (connect last point to first).
385    ///
386    /// # Arguments
387    /// * `closed` - Whether to create a closed curve
388    pub fn closed(mut self, closed: bool) -> Self {
389        self.interpolation_config.closed = closed;
390        self
391    }
392
393    /// Set the line color.
394    pub fn line_color(mut self, color: C) -> Self {
395        self.line_builder = self.line_builder.line_color(color);
396        self
397    }
398
399    /// Set the line width.
400    pub fn line_width(mut self, width: u32) -> Self {
401        self.line_builder = self.line_builder.line_width(width);
402        self
403    }
404
405    /// Enable area filling with the specified color.
406    pub fn fill_area(mut self, color: C) -> Self {
407        self.line_builder = self.line_builder.fill_area(color);
408        self
409    }
410
411    /// Add markers to original data points.
412    ///
413    /// Note: Markers are only placed at the original data points, not the interpolated points.
414    pub fn with_markers(mut self, marker_style: MarkerStyle<C>) -> Self {
415        self.line_builder = self.line_builder.with_markers(marker_style);
416        self
417    }
418
419    /// Set the chart title.
420    pub fn with_title(mut self, title: &str) -> Self {
421        self.line_builder = self.line_builder.with_title(title);
422        self
423    }
424
425    /// Set the background color.
426    pub fn background_color(mut self, color: C) -> Self {
427        self.line_builder = self.line_builder.background_color(color);
428        self
429    }
430
431    /// Set the chart margins.
432    pub fn margins(mut self, margins: crate::chart::traits::Margins) -> Self {
433        self.line_builder = self.line_builder.margins(margins);
434        self
435    }
436
437    /// Set the grid system.
438    pub fn with_grid(mut self, grid: crate::grid::GridSystem<C>) -> Self {
439        self.line_builder = self.line_builder.with_grid(grid);
440        self
441    }
442
443    /// Set the X-axis configuration.
444    pub fn with_x_axis(mut self, axis: crate::axes::LinearAxis<f32, C>) -> Self {
445        self.line_builder = self.line_builder.with_x_axis(axis);
446        self
447    }
448
449    /// Set the Y-axis configuration.
450    pub fn with_y_axis(mut self, axis: crate::axes::LinearAxis<f32, C>) -> Self {
451        self.line_builder = self.line_builder.with_y_axis(axis);
452        self
453    }
454
455    /// Build the curve chart.
456    pub fn build(self) -> ChartResult<CurveChart<C>> {
457        let base_chart = self.line_builder.build()?;
458
459        Ok(CurveChart {
460            base_chart,
461            interpolation_config: self.interpolation_config,
462        })
463    }
464}
465
466impl<C: PixelColor + 'static> Default for CurveChartBuilder<C>
467where
468    C: From<embedded_graphics::pixelcolor::Rgb565>,
469{
470    fn default() -> Self {
471        Self::new()
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use embedded_graphics::pixelcolor::Rgb565;
479
480    #[test]
481    fn test_curve_chart_creation() {
482        let chart: CurveChart<Rgb565> = CurveChart::new();
483        assert_eq!(
484            chart.interpolation_config().interpolation_type,
485            InterpolationType::CubicSpline
486        );
487        assert_eq!(chart.interpolation_config().subdivisions, 8);
488    }
489
490    #[test]
491    fn test_curve_chart_builder() {
492        let chart: CurveChart<Rgb565> = CurveChart::builder()
493            .line_color(Rgb565::RED)
494            .line_width(3)
495            .interpolation_type(InterpolationType::Bezier)
496            .subdivisions(12)
497            .tension(0.8)
498            .build()
499            .unwrap();
500
501        assert_eq!(chart.style().line_color, Rgb565::RED);
502        assert_eq!(chart.style().line_width, 3);
503        assert_eq!(
504            chart.interpolation_config().interpolation_type,
505            InterpolationType::Bezier
506        );
507        assert_eq!(chart.interpolation_config().subdivisions, 12);
508    }
509
510    #[test]
511    fn test_interpolation_config_clamping() {
512        let chart: CurveChart<Rgb565> = CurveChart::builder()
513            .subdivisions(100) // Should be clamped to 32
514            .tension(2.0) // Should be clamped to 1.0
515            .build()
516            .unwrap();
517
518        assert_eq!(chart.interpolation_config().subdivisions, 32);
519        assert_eq!(chart.interpolation_config().tension, 1.0);
520    }
521}