embedded_charts/axes/
builder.rs

1//! Builder pattern for axis configuration.
2
3use crate::axes::{
4    linear::LinearAxis,
5    style::AxisStyle,
6    ticks::{CustomTickGenerator, LinearTickGenerator},
7    traits::{AxisValue, TickGenerator},
8    AxisOrientation, AxisPosition,
9};
10use crate::error::ChartError;
11use embedded_graphics::prelude::*;
12
13/// Builder for creating linear axes with fluent configuration
14#[derive(Debug)]
15pub struct LinearAxisBuilder<T, C: PixelColor> {
16    min: Option<T>,
17    max: Option<T>,
18    orientation: AxisOrientation,
19    position: AxisPosition,
20    tick_generator: LinearTickGenerator,
21    style: AxisStyle<C>,
22    show_line: bool,
23    show_ticks: bool,
24    show_labels: bool,
25    show_grid: bool,
26}
27
28impl<T, C> LinearAxisBuilder<T, C>
29where
30    T: AxisValue,
31    C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>,
32{
33    /// Create a new linear axis builder
34    pub fn new(orientation: AxisOrientation, position: AxisPosition) -> Self {
35        Self {
36            min: None,
37            max: None,
38            orientation,
39            position,
40            tick_generator: LinearTickGenerator::new(5),
41            style: AxisStyle::new(),
42            show_line: true,
43            show_ticks: true,
44            show_labels: true,
45            show_grid: false,
46        }
47    }
48
49    /// Set the range of the axis
50    pub fn range(mut self, min: T, max: T) -> Self {
51        self.min = Some(min);
52        self.max = Some(max);
53        self
54    }
55
56    /// Set the minimum value
57    pub fn min(mut self, min: T) -> Self {
58        self.min = Some(min);
59        self
60    }
61
62    /// Set the maximum value
63    pub fn max(mut self, max: T) -> Self {
64        self.max = Some(max);
65        self
66    }
67
68    /// Set the number of ticks
69    pub fn tick_count(mut self, count: usize) -> Self {
70        <LinearTickGenerator as TickGenerator<T>>::set_preferred_tick_count(
71            &mut self.tick_generator,
72            count,
73        );
74        self
75    }
76
77    /// Enable minor ticks with the specified ratio
78    pub fn with_minor_ticks(mut self, ratio: usize) -> Self {
79        self.tick_generator = self.tick_generator.with_minor_ticks(ratio);
80        self
81    }
82
83    /// Disable minor ticks
84    pub fn without_minor_ticks(mut self) -> Self {
85        self.tick_generator = self.tick_generator.without_minor_ticks();
86        self
87    }
88
89    /// Set the axis style
90    pub fn style(mut self, style: AxisStyle<C>) -> Self {
91        self.style = style;
92        self
93    }
94
95    /// Use minimal styling for small displays
96    pub fn minimal_style(mut self) -> Self {
97        self.style = AxisStyle::minimal();
98        self
99    }
100
101    /// Use professional styling
102    pub fn professional_style(mut self) -> Self {
103        self.style = AxisStyle::professional();
104        self
105    }
106
107    /// Enable or disable the axis line
108    pub fn show_line(mut self, show: bool) -> Self {
109        self.show_line = show;
110        self
111    }
112
113    /// Enable or disable tick marks
114    pub fn show_ticks(mut self, show: bool) -> Self {
115        self.show_ticks = show;
116        self
117    }
118
119    /// Enable or disable labels
120    pub fn show_labels(mut self, show: bool) -> Self {
121        self.show_labels = show;
122        self
123    }
124
125    /// Enable or disable grid lines
126    pub fn show_grid(mut self, show: bool) -> Self {
127        self.show_grid = show;
128        self
129    }
130
131    /// Build the linear axis
132    pub fn build(self) -> Result<LinearAxis<T, C>, ChartError> {
133        let min = self.min.ok_or(ChartError::ConfigurationError)?;
134        let max = self.max.ok_or(ChartError::ConfigurationError)?;
135
136        if min.to_f32() >= max.to_f32() {
137            return Err(ChartError::ConfigurationError);
138        }
139
140        let axis = LinearAxis::new(min, max, self.orientation, self.position)
141            .with_tick_generator(self.tick_generator)
142            .with_style(self.style)
143            .show_line(self.show_line)
144            .show_ticks(self.show_ticks)
145            .show_labels(self.show_labels)
146            .show_grid(self.show_grid);
147
148        Ok(axis)
149    }
150}
151
152/// Builder for creating custom axes with manually specified ticks
153#[derive(Debug)]
154pub struct CustomAxisBuilder<T, C: PixelColor> {
155    min: Option<T>,
156    max: Option<T>,
157    orientation: AxisOrientation,
158    position: AxisPosition,
159    tick_generator: CustomTickGenerator<T>,
160    style: AxisStyle<C>,
161    show_line: bool,
162    show_ticks: bool,
163    show_labels: bool,
164    show_grid: bool,
165}
166
167impl<T, C> CustomAxisBuilder<T, C>
168where
169    T: AxisValue,
170    C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>,
171{
172    /// Create a new custom axis builder
173    pub fn new(orientation: AxisOrientation, position: AxisPosition) -> Self {
174        Self {
175            min: None,
176            max: None,
177            orientation,
178            position,
179            tick_generator: CustomTickGenerator::new(),
180            style: AxisStyle::new(),
181            show_line: true,
182            show_ticks: true,
183            show_labels: true,
184            show_grid: false,
185        }
186    }
187
188    /// Set the range of the axis
189    pub fn range(mut self, min: T, max: T) -> Self {
190        self.min = Some(min);
191        self.max = Some(max);
192        self
193    }
194
195    /// Add a major tick with a label
196    pub fn add_major_tick(mut self, value: T, label: &str) -> Self {
197        self.tick_generator = self.tick_generator.add_major_tick(value, label);
198        self
199    }
200
201    /// Add a minor tick
202    pub fn add_minor_tick(mut self, value: T) -> Self {
203        self.tick_generator = self.tick_generator.add_minor_tick(value);
204        self
205    }
206
207    /// Set the axis style
208    pub fn style(mut self, style: AxisStyle<C>) -> Self {
209        self.style = style;
210        self
211    }
212
213    /// Enable or disable the axis line
214    pub fn show_line(mut self, show: bool) -> Self {
215        self.show_line = show;
216        self
217    }
218
219    /// Enable or disable tick marks
220    pub fn show_ticks(mut self, show: bool) -> Self {
221        self.show_ticks = show;
222        self
223    }
224
225    /// Enable or disable labels
226    pub fn show_labels(mut self, show: bool) -> Self {
227        self.show_labels = show;
228        self
229    }
230
231    /// Enable or disable grid lines
232    pub fn show_grid(mut self, show: bool) -> Self {
233        self.show_grid = show;
234        self
235    }
236
237    /// Build the custom axis (returns a LinearAxis with custom tick generator)
238    pub fn build(self) -> Result<LinearAxis<T, C>, ChartError> {
239        let min = self.min.ok_or(ChartError::ConfigurationError)?;
240        let max = self.max.ok_or(ChartError::ConfigurationError)?;
241
242        if min.to_f32() >= max.to_f32() {
243            return Err(ChartError::ConfigurationError);
244        }
245
246        // Create a linear axis and replace its tick generator
247        let axis = LinearAxis::new(min, max, self.orientation, self.position)
248            .with_style(self.style)
249            .show_line(self.show_line)
250            .show_ticks(self.show_ticks)
251            .show_labels(self.show_labels)
252            .show_grid(self.show_grid);
253
254        // Note: In a full implementation, we'd need to modify LinearAxis to accept
255        // different tick generator types. For now, this is a simplified version.
256
257        Ok(axis)
258    }
259}
260
261/// Convenience functions for creating common axis configurations
262pub mod presets {
263    use super::*;
264    use embedded_graphics::pixelcolor::Rgb565;
265
266    /// Create a standard X-axis at the bottom
267    pub fn x_axis_bottom<T: AxisValue>(min: T, max: T) -> LinearAxisBuilder<T, Rgb565> {
268        LinearAxisBuilder::new(AxisOrientation::Horizontal, AxisPosition::Bottom).range(min, max)
269    }
270
271    /// Create a standard Y-axis on the left
272    pub fn y_axis_left<T: AxisValue>(min: T, max: T) -> LinearAxisBuilder<T, Rgb565> {
273        LinearAxisBuilder::new(AxisOrientation::Vertical, AxisPosition::Left).range(min, max)
274    }
275
276    /// Create a minimal X-axis for small displays
277    pub fn minimal_x_axis<T: AxisValue>(min: T, max: T) -> LinearAxisBuilder<T, Rgb565> {
278        x_axis_bottom(min, max)
279            .minimal_style()
280            .tick_count(3)
281            .without_minor_ticks()
282    }
283
284    /// Create a minimal Y-axis for small displays
285    pub fn minimal_y_axis<T: AxisValue>(min: T, max: T) -> LinearAxisBuilder<T, Rgb565> {
286        y_axis_left(min, max)
287            .minimal_style()
288            .tick_count(3)
289            .without_minor_ticks()
290    }
291
292    /// Create a professional X-axis with grid
293    pub fn professional_x_axis<T: AxisValue>(min: T, max: T) -> LinearAxisBuilder<T, Rgb565> {
294        x_axis_bottom(min, max)
295            .professional_style()
296            .show_grid(true)
297            .with_minor_ticks(4)
298    }
299
300    /// Create a professional Y-axis with grid
301    pub fn professional_y_axis<T: AxisValue>(min: T, max: T) -> LinearAxisBuilder<T, Rgb565> {
302        y_axis_left(min, max)
303            .professional_style()
304            .show_grid(true)
305            .with_minor_ticks(4)
306    }
307
308    /// Create a time-series X-axis (horizontal, bottom)
309    pub fn time_axis<T: AxisValue>(start: T, end: T) -> LinearAxisBuilder<T, Rgb565> {
310        x_axis_bottom(start, end).tick_count(6).show_grid(true)
311    }
312
313    /// Create a percentage Y-axis (0-100)
314    pub fn percentage_axis() -> LinearAxisBuilder<f32, Rgb565> {
315        y_axis_left(0.0, 100.0).tick_count(6).show_grid(true)
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use crate::axes::traits::Axis;
323    use embedded_graphics::pixelcolor::Rgb565;
324
325    #[test]
326    fn test_linear_axis_builder() {
327        let axis = LinearAxisBuilder::<f32, Rgb565>::new(
328            AxisOrientation::Horizontal,
329            AxisPosition::Bottom,
330        )
331        .range(0.0f32, 10.0f32)
332        .tick_count(5)
333        .show_grid(true)
334        .build()
335        .unwrap();
336
337        assert_eq!(axis.min(), 0.0);
338        assert_eq!(axis.max(), 10.0);
339        assert_eq!(axis.orientation(), AxisOrientation::Horizontal);
340    }
341
342    #[test]
343    fn test_builder_validation() {
344        // Missing range should fail
345        let result = LinearAxisBuilder::<f32, Rgb565>::new(
346            AxisOrientation::Horizontal,
347            AxisPosition::Bottom,
348        )
349        .tick_count(5)
350        .build();
351        assert!(result.is_err());
352
353        // Invalid range should fail
354        let result = LinearAxisBuilder::<f32, Rgb565>::new(
355            AxisOrientation::Horizontal,
356            AxisPosition::Bottom,
357        )
358        .range(10.0f32, 5.0f32)
359        .build();
360        assert!(result.is_err());
361    }
362
363    #[test]
364    fn test_custom_axis_builder() {
365        let axis =
366            CustomAxisBuilder::<f32, Rgb565>::new(AxisOrientation::Vertical, AxisPosition::Left)
367                .range(0.0f32, 100.0f32)
368                .add_major_tick(0.0, "0%")
369                .add_major_tick(50.0, "50%")
370                .add_major_tick(100.0, "100%")
371                .add_minor_tick(25.0)
372                .add_minor_tick(75.0)
373                .build()
374                .unwrap();
375
376        assert_eq!(axis.min(), 0.0);
377        assert_eq!(axis.max(), 100.0);
378    }
379
380    #[test]
381    fn test_preset_axes() {
382        let x_axis = presets::x_axis_bottom(0.0f32, 10.0f32).build().unwrap();
383        assert_eq!(x_axis.orientation(), AxisOrientation::Horizontal);
384        assert_eq!(x_axis.position(), AxisPosition::Bottom);
385
386        let y_axis = presets::y_axis_left(-5.0f32, 5.0f32).build().unwrap();
387        assert_eq!(y_axis.orientation(), AxisOrientation::Vertical);
388        assert_eq!(y_axis.position(), AxisPosition::Left);
389    }
390
391    #[test]
392    fn test_minimal_preset() {
393        let _axis: LinearAxis<f32, Rgb565> =
394            presets::minimal_x_axis(0.0f32, 100.0f32).build().unwrap();
395        // Note: Tick generator test commented out due to type inference issues
396        // assert_eq!(axis.tick_generator().preferred_tick_count(), 3);
397    }
398
399    #[test]
400    fn test_professional_preset() {
401        let _axis = presets::professional_y_axis(0.0f32, 1000.0f32)
402            .build()
403            .unwrap();
404        // Professional style should have grid enabled
405        // Note: We'd need to expose the config to test this properly
406    }
407}