embedded_charts/chart/
traits.rs

1//! Core traits for chart implementations.
2
3use crate::data::DataSeries;
4use crate::error::ChartResult;
5use embedded_graphics::{prelude::*, primitives::Rectangle};
6
7/// Main trait for all chart types
8pub trait Chart<C: PixelColor> {
9    /// The type of data this chart can render
10    type Data: DataSeries;
11    /// Configuration type for this chart
12    type Config;
13
14    /// Draw the chart to the target display
15    ///
16    /// # Arguments
17    /// * `data` - The data to render
18    /// * `config` - Chart configuration
19    /// * `viewport` - The area to draw the chart in
20    /// * `target` - The display target to draw to
21    fn draw<D>(
22        &self,
23        data: &Self::Data,
24        config: &Self::Config,
25        viewport: Rectangle,
26        target: &mut D,
27    ) -> ChartResult<()>
28    where
29        D: DrawTarget<Color = C>;
30
31    /// Get the data bounds for this chart
32    fn data_bounds(&self, _data: &Self::Data) -> ChartResult<()> {
33        // Default implementation - concrete charts should override this
34        // if they need specific bounds calculation
35        Ok(())
36    }
37}
38
39/// Trait for charts that support real-time data streaming
40#[cfg(feature = "animations")]
41pub trait StreamingChart<C: PixelColor>: Chart<C> {
42    /// The type of individual data points
43    type DataPoint: Copy + Clone;
44
45    /// Get the streaming animator for this chart
46    fn streaming_animator(&mut self) -> &mut crate::animation::StreamingAnimator<Self::DataPoint>;
47
48    /// Push a new data point to the chart
49    ///
50    /// # Arguments
51    /// * `point` - The new data point to add
52    fn push_data(&mut self, point: Self::DataPoint) -> ChartResult<()> {
53        self.streaming_animator().push_data(point);
54        Ok(())
55    }
56
57    /// Draw the chart with streaming data and interpolation
58    ///
59    /// # Arguments
60    /// * `config` - Chart configuration
61    /// * `viewport` - The area to draw the chart in
62    /// * `target` - The display target to draw to
63    /// * `interpolation_progress` - Progress for smooth interpolation (0-100)
64    fn draw_streaming<D>(
65        &self,
66        config: &Self::Config,
67        viewport: embedded_graphics::primitives::Rectangle,
68        target: &mut D,
69        interpolation_progress: crate::animation::Progress,
70    ) -> ChartResult<()>
71    where
72        D: embedded_graphics::draw_target::DrawTarget<Color = C>;
73
74    /// Enable or disable smooth interpolation for streaming data
75    ///
76    /// # Arguments
77    /// * `enabled` - Whether to enable smooth interpolation
78    fn set_smooth_interpolation(&mut self, enabled: bool) {
79        self.streaming_animator().set_smooth_interpolation(enabled);
80    }
81
82    /// Check if smooth interpolation is enabled
83    fn is_smooth_interpolation_enabled(&self) -> bool;
84}
85
86/// Trait for charts that can be animated using the new progress-based system
87#[cfg(feature = "animations")]
88pub trait AnimatedChart<C: PixelColor>: Chart<C> {
89    /// The type of data that can be animated
90    type AnimatedData: crate::animation::Interpolatable;
91
92    /// Draw the chart with animation at the specified progress
93    ///
94    /// # Arguments
95    /// * `data` - The data to render
96    /// * `config` - Chart configuration
97    /// * `viewport` - The area to draw the chart in
98    /// * `target` - The display target to draw to
99    /// * `progress` - Animation progress (0-100)
100    fn draw_animated<D>(
101        &self,
102        data: &Self::Data,
103        config: &Self::Config,
104        viewport: embedded_graphics::primitives::Rectangle,
105        target: &mut D,
106        progress: crate::animation::Progress,
107    ) -> ChartResult<()>
108    where
109        D: embedded_graphics::draw_target::DrawTarget<Color = C>;
110
111    /// Set up a transition animation between two data states
112    ///
113    /// # Arguments
114    /// * `from_data` - Starting data state
115    /// * `to_data` - Target data state
116    /// * `easing` - Easing function to use
117    ///
118    /// # Returns
119    /// A ChartAnimator that can be used to interpolate between states
120    fn create_transition_animator(
121        &self,
122        from_data: Self::AnimatedData,
123        to_data: Self::AnimatedData,
124        easing: crate::animation::EasingFunction,
125    ) -> crate::animation::ChartAnimator<Self::AnimatedData>;
126
127    /// Extract animatable data from the chart's data
128    ///
129    /// # Arguments
130    /// * `data` - The chart data to extract from
131    ///
132    /// # Returns
133    /// The animatable representation of the data
134    fn extract_animated_data(&self, data: &Self::Data) -> ChartResult<Self::AnimatedData>;
135}
136
137/// Trait for customizable chart styling
138pub trait StylableChart<C: PixelColor> {
139    /// Style configuration type
140    type Style;
141
142    /// Apply a style to the chart
143    ///
144    /// # Arguments
145    /// * `style` - The style configuration to apply
146    fn apply_style(&mut self, style: Self::Style);
147
148    /// Get the current style
149    fn style(&self) -> &Self::Style;
150}
151
152/// Trait for charts with configurable axes
153pub trait AxisChart<C: PixelColor>: Chart<C> {
154    /// X-axis type
155    type XAxis;
156    /// Y-axis type
157    type YAxis;
158
159    /// Set the X-axis configuration
160    ///
161    /// # Arguments
162    /// * `axis` - X-axis configuration
163    fn set_x_axis(&mut self, axis: Self::XAxis);
164
165    /// Set the Y-axis configuration
166    ///
167    /// # Arguments
168    /// * `axis` - Y-axis configuration
169    fn set_y_axis(&mut self, axis: Self::YAxis);
170
171    /// Get the X-axis configuration
172    fn x_axis(&self) -> ChartResult<&Self::XAxis>;
173
174    /// Get the Y-axis configuration
175    fn y_axis(&self) -> ChartResult<&Self::YAxis>;
176}
177
178/// Trait for charts that support legends
179pub trait LegendChart<C: PixelColor>: Chart<C> {
180    /// Legend configuration type
181    type Legend;
182
183    /// Set the legend configuration
184    ///
185    /// # Arguments
186    /// * `legend` - Legend configuration
187    fn set_legend(&mut self, legend: Option<Self::Legend>);
188
189    /// Get the legend configuration
190    fn legend(&self) -> Option<&Self::Legend>;
191
192    /// Calculate the space required for the legend
193    fn legend_size(&self) -> Size;
194}
195
196/// Builder trait for fluent chart construction
197pub trait ChartBuilder<C: PixelColor> {
198    /// The chart type this builder creates
199    type Chart: Chart<C>;
200    /// Error type for building operations
201    type Error;
202
203    /// Build the chart with current configuration
204    fn build(self) -> Result<Self::Chart, Self::Error>;
205}
206
207/// Trait for charts that can be rendered incrementally
208pub trait IncrementalChart<C: PixelColor>: Chart<C> {
209    /// Render only the changed portions of the chart
210    ///
211    /// # Arguments
212    /// * `data` - The data to render
213    /// * `config` - Chart configuration
214    /// * `viewport` - The area to draw the chart in
215    /// * `target` - The display target to draw to
216    /// * `dirty_region` - The region that needs to be redrawn
217    fn draw_incremental<D>(
218        &self,
219        data: &Self::Data,
220        config: &Self::Config,
221        viewport: Rectangle,
222        target: &mut D,
223        dirty_region: Rectangle,
224    ) -> ChartResult<()>
225    where
226        D: DrawTarget<Color = C>;
227
228    /// Mark a region as dirty (needing redraw)
229    ///
230    /// # Arguments
231    /// * `region` - The region to mark as dirty
232    fn mark_dirty(&mut self, region: Rectangle);
233
234    /// Get all dirty regions
235    fn dirty_regions(&self) -> &[Rectangle];
236
237    /// Clear all dirty regions
238    fn clear_dirty(&mut self);
239}
240
241/// Trait for charts that support interaction
242pub trait InteractiveChart<C: PixelColor>: Chart<C> {
243    /// Event type for interactions
244    type Event;
245    /// Response type for interactions
246    type Response;
247
248    /// Handle an interaction event
249    ///
250    /// # Arguments
251    /// * `event` - The interaction event
252    /// * `viewport` - The chart viewport
253    fn handle_event(
254        &mut self,
255        event: Self::Event,
256        viewport: Rectangle,
257    ) -> ChartResult<Self::Response>;
258
259    /// Check if a point is within an interactive area
260    ///
261    /// # Arguments
262    /// * `point` - The point to check
263    /// * `viewport` - The chart viewport
264    fn hit_test(&self, point: Point, viewport: Rectangle) -> Option<Self::Response>;
265}
266
267/// Trait for chart renderers that support animation frame rendering
268#[cfg(feature = "animations")]
269pub trait AnimationRenderer<C: PixelColor> {
270    /// Check if the renderer needs frame updates
271    fn needs_frame_update(&self) -> bool;
272
273    /// Get the current frame rate (FPS)
274    fn frame_rate(&self) -> u32;
275
276    /// Set the target frame rate
277    fn set_frame_rate(&mut self, fps: u32);
278}
279
280/// Common chart configuration
281#[derive(Debug, Clone)]
282pub struct ChartConfig<C: PixelColor> {
283    /// Chart title
284    pub title: Option<heapless::String<64>>,
285    /// Background color
286    pub background_color: Option<C>,
287    /// Chart margins
288    pub margins: Margins,
289    /// Whether to show grid lines
290    pub show_grid: bool,
291    /// Grid color
292    pub grid_color: Option<C>,
293}
294
295/// Chart margins configuration
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub struct Margins {
298    /// Top margin in pixels
299    pub top: u32,
300    /// Right margin in pixels
301    pub right: u32,
302    /// Bottom margin in pixels
303    pub bottom: u32,
304    /// Left margin in pixels
305    pub left: u32,
306}
307
308impl Margins {
309    /// Create new margins with the same value for all sides
310    pub const fn all(margin: u32) -> Self {
311        Self {
312            top: margin,
313            right: margin,
314            bottom: margin,
315            left: margin,
316        }
317    }
318
319    /// Create new margins with different horizontal and vertical values
320    pub const fn symmetric(horizontal: u32, vertical: u32) -> Self {
321        Self {
322            top: vertical,
323            right: horizontal,
324            bottom: vertical,
325            left: horizontal,
326        }
327    }
328
329    /// Create new margins with individual values
330    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
331        Self {
332            top,
333            right,
334            bottom,
335            left,
336        }
337    }
338
339    /// Get the total horizontal margin (left + right)
340    pub const fn horizontal(&self) -> u32 {
341        self.left + self.right
342    }
343
344    /// Get the total vertical margin (top + bottom)
345    pub const fn vertical(&self) -> u32 {
346        self.top + self.bottom
347    }
348
349    /// Apply margins to a rectangle, returning the inner area
350    pub fn apply_to(&self, rect: Rectangle) -> Rectangle {
351        let top_left = Point::new(
352            rect.top_left.x + self.left as i32,
353            rect.top_left.y + self.top as i32,
354        );
355        let size = Size::new(
356            rect.size.width.saturating_sub(self.horizontal()),
357            rect.size.height.saturating_sub(self.vertical()),
358        );
359        Rectangle::new(top_left, size)
360    }
361}
362
363impl Default for Margins {
364    fn default() -> Self {
365        Self::all(10)
366    }
367}
368
369impl<C: PixelColor> Default for ChartConfig<C> {
370    fn default() -> Self {
371        Self {
372            title: None,
373            background_color: None,
374            margins: Margins::default(),
375            show_grid: false,
376            grid_color: None,
377        }
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_margins_creation() {
387        let margins = Margins::all(5);
388        assert_eq!(margins.top, 5);
389        assert_eq!(margins.right, 5);
390        assert_eq!(margins.bottom, 5);
391        assert_eq!(margins.left, 5);
392    }
393
394    #[test]
395    fn test_margins_symmetric() {
396        let margins = Margins::symmetric(10, 20);
397        assert_eq!(margins.top, 20);
398        assert_eq!(margins.right, 10);
399        assert_eq!(margins.bottom, 20);
400        assert_eq!(margins.left, 10);
401    }
402
403    #[test]
404    fn test_margins_totals() {
405        let margins = Margins::new(5, 10, 15, 20);
406        assert_eq!(margins.horizontal(), 30);
407        assert_eq!(margins.vertical(), 20);
408    }
409
410    #[test]
411    fn test_margins_apply_to() {
412        let margins = Margins::all(10);
413        let rect = Rectangle::new(Point::new(0, 0), Size::new(100, 80));
414        let inner = margins.apply_to(rect);
415
416        assert_eq!(inner.top_left, Point::new(10, 10));
417        assert_eq!(inner.size, Size::new(80, 60));
418    }
419}