Skip to main content

astrelis_geometry/chart/
builder.rs

1//! Fluent chart builder API.
2//!
3//! Provides ergonomic builders for constructing charts with full configuration:
4//!
5//! # Example
6//!
7//! ```ignore
8//! let chart = ChartBuilder::new()
9//!     .title("Multi-Axis Chart")
10//!     .x_axis(|a| a
11//!         .label("Time (s)")
12//!         .range(0.0, 100.0)
13//!         .grid(|g| g
14//!             .major(|m| m.thickness(1.0).color(Color::GRAY))
15//!             .minor(|m| m.thickness(0.5).dotted())
16//!             .divisions(5)
17//!         )
18//!     )
19//!     .y_axis(|a| a
20//!         .label("Temperature")
21//!         .auto_range(0.1)
22//!     )
23//!     .series("Temperature", &temp_data)
24//!     .add_series("Pressure", |s| s
25//!         .data(&pressure_data)
26//!         .color(Color::ORANGE)
27//!         .dashed(5.0, 3.0)
28//!     )
29//!     .build();
30//! ```
31
32use super::grid::{DashPattern, GridConfig, GridLevel, GridSpacing};
33use super::style::{FillStyle, LineStyle, PointStyle, SeriesStyle, palette_color};
34use super::types::{
35    Axis, AxisId, AxisOrientation, AxisPosition, BarConfig, Chart, ChartTitle, ChartType,
36    DataPoint, FillRegion, LegendConfig, LegendPosition, LineAnnotation, Series, TextAnnotation,
37};
38use astrelis_render::Color;
39
40/// Builder for creating charts.
41#[derive(Debug)]
42pub struct ChartBuilder {
43    chart: Chart,
44    series_count: usize,
45}
46
47impl Default for ChartBuilder {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl ChartBuilder {
54    /// Create a new chart builder.
55    pub fn new() -> Self {
56        Self {
57            chart: Chart::default(),
58            series_count: 0,
59        }
60    }
61
62    /// Create a line chart builder.
63    pub fn line() -> Self {
64        let mut builder = Self::new();
65        builder.chart.chart_type = ChartType::Line;
66        builder
67    }
68
69    /// Create a bar chart builder.
70    pub fn bar() -> Self {
71        let mut builder = Self::new();
72        builder.chart.chart_type = ChartType::Bar;
73        builder
74    }
75
76    /// Create a scatter plot builder.
77    pub fn scatter() -> Self {
78        let mut builder = Self::new();
79        builder.chart.chart_type = ChartType::Scatter;
80        builder
81    }
82
83    /// Create an area chart builder.
84    pub fn area() -> Self {
85        let mut builder = Self::new();
86        builder.chart.chart_type = ChartType::Area;
87        builder
88    }
89
90    /// Set the chart title.
91    pub fn title(mut self, title: impl Into<String>) -> Self {
92        self.chart.title = Some(ChartTitle::new(title));
93        self
94    }
95
96    /// Set the chart title with full configuration.
97    pub fn title_config(mut self, title: ChartTitle) -> Self {
98        self.chart.title = Some(title);
99        self
100    }
101
102    /// Set the chart subtitle.
103    pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
104        self.chart.subtitle = Some(ChartTitle::new(subtitle).with_font_size(12.0));
105        self
106    }
107
108    /// Set the X axis label.
109    pub fn x_label(mut self, label: impl Into<String>) -> Self {
110        if let Some(axis) = self.chart.get_axis_mut(AxisId::X_PRIMARY) {
111            axis.label = Some(label.into());
112        }
113        self
114    }
115
116    /// Set the Y axis label.
117    pub fn y_label(mut self, label: impl Into<String>) -> Self {
118        if let Some(axis) = self.chart.get_axis_mut(AxisId::Y_PRIMARY) {
119            axis.label = Some(label.into());
120        }
121        self
122    }
123
124    /// Configure the primary X axis.
125    pub fn x_axis_config(mut self, axis: Axis) -> Self {
126        let mut axis = axis;
127        axis.id = AxisId::X_PRIMARY;
128        axis.orientation = AxisOrientation::Horizontal;
129        axis.position = AxisPosition::Bottom;
130        self.chart.set_axis(axis);
131        self
132    }
133
134    /// Configure the primary Y axis.
135    pub fn y_axis_config(mut self, axis: Axis) -> Self {
136        let mut axis = axis;
137        axis.id = AxisId::Y_PRIMARY;
138        axis.orientation = AxisOrientation::Vertical;
139        axis.position = AxisPosition::Left;
140        self.chart.set_axis(axis);
141        self
142    }
143
144    /// Add a secondary Y axis (right side).
145    pub fn secondary_y_axis(mut self, axis: Axis) -> Self {
146        let mut axis = axis;
147        axis.id = AxisId::Y_SECONDARY;
148        axis.orientation = AxisOrientation::Vertical;
149        axis.position = AxisPosition::Right;
150        self.chart.set_axis(axis);
151        self
152    }
153
154    /// Add a secondary X axis (top).
155    pub fn secondary_x_axis(mut self, axis: Axis) -> Self {
156        let mut axis = axis;
157        axis.id = AxisId::X_SECONDARY;
158        axis.orientation = AxisOrientation::Horizontal;
159        axis.position = AxisPosition::Top;
160        self.chart.set_axis(axis);
161        self
162    }
163
164    /// Add a custom axis.
165    pub fn add_axis(mut self, axis: Axis) -> Self {
166        self.chart.set_axis(axis);
167        self
168    }
169
170    /// Set the X axis range.
171    pub fn x_range(mut self, min: f64, max: f64) -> Self {
172        if let Some(axis) = self.chart.get_axis_mut(AxisId::X_PRIMARY) {
173            axis.min = Some(min);
174            axis.max = Some(max);
175        }
176        self
177    }
178
179    /// Set the Y axis range.
180    pub fn y_range(mut self, min: f64, max: f64) -> Self {
181        if let Some(axis) = self.chart.get_axis_mut(AxisId::Y_PRIMARY) {
182            axis.min = Some(min);
183            axis.max = Some(max);
184        }
185        self
186    }
187
188    /// Set the secondary Y axis range.
189    pub fn secondary_y_range(mut self, min: f64, max: f64) -> Self {
190        if let Some(axis) = self.chart.get_axis_mut(AxisId::Y_SECONDARY) {
191            axis.min = Some(min);
192            axis.max = Some(max);
193        } else {
194            // Create the axis if it doesn't exist
195            self.chart
196                .set_axis(Axis::y_secondary().with_range(min, max));
197        }
198        self
199    }
200
201    /// Add a data series.
202    pub fn add_series<T: Into<DataPoint> + Copy>(
203        mut self,
204        name: impl Into<String>,
205        data: &[T],
206    ) -> Self {
207        let color = palette_color(self.series_count);
208        let style = SeriesStyle::with_color(color);
209        self.chart
210            .series
211            .push(Series::from_tuples(name, data, style));
212        self.series_count += 1;
213        self
214    }
215
216    /// Add a data series with custom style.
217    pub fn add_series_styled<T: Into<DataPoint> + Copy>(
218        mut self,
219        name: impl Into<String>,
220        data: &[T],
221        style: SeriesStyle,
222    ) -> Self {
223        self.chart
224            .series
225            .push(Series::from_tuples(name, data, style));
226        self.series_count += 1;
227        self
228    }
229
230    /// Add a data series on the secondary Y axis.
231    pub fn add_series_secondary_y<T: Into<DataPoint> + Copy>(
232        mut self,
233        name: impl Into<String>,
234        data: &[T],
235    ) -> Self {
236        let color = palette_color(self.series_count);
237        let style = SeriesStyle::with_color(color);
238        let series = Series::from_tuples(name, data, style)
239            .with_axes(AxisId::X_PRIMARY, AxisId::Y_SECONDARY);
240        self.chart.series.push(series);
241        self.series_count += 1;
242        self
243    }
244
245    /// Add a data series with custom axes.
246    pub fn add_series_with_axes<T: Into<DataPoint> + Copy>(
247        mut self,
248        name: impl Into<String>,
249        data: &[T],
250        x_axis: AxisId,
251        y_axis: AxisId,
252    ) -> Self {
253        let color = palette_color(self.series_count);
254        let style = SeriesStyle::with_color(color);
255        let series = Series::from_tuples(name, data, style).with_axes(x_axis, y_axis);
256        self.chart.series.push(series);
257        self.series_count += 1;
258        self
259    }
260
261    /// Plot a mathematical function.
262    pub fn plot_function<F>(
263        mut self,
264        name: impl Into<String>,
265        f: F,
266        x_min: f64,
267        x_max: f64,
268        samples: usize,
269    ) -> Self
270    where
271        F: Fn(f64) -> f64,
272    {
273        let step = (x_max - x_min) / (samples - 1) as f64;
274        let data: Vec<DataPoint> = (0..samples)
275            .map(|i| {
276                let x = x_min + step * i as f64;
277                DataPoint::new(x, f(x))
278            })
279            .collect();
280
281        let color = palette_color(self.series_count);
282        let style = SeriesStyle::with_color(color);
283        self.chart.series.push(Series::new(name, data, style));
284        self.series_count += 1;
285        self
286    }
287
288    /// Add a text annotation.
289    pub fn add_text_annotation(mut self, annotation: TextAnnotation) -> Self {
290        self.chart.text_annotations.push(annotation);
291        self
292    }
293
294    /// Add a text at data coordinates.
295    pub fn add_text_at(mut self, text: impl Into<String>, x: f64, y: f64) -> Self {
296        self.chart
297            .text_annotations
298            .push(TextAnnotation::at_data(text, x, y));
299        self
300    }
301
302    /// Add a line annotation.
303    pub fn add_line_annotation(mut self, annotation: LineAnnotation) -> Self {
304        self.chart.line_annotations.push(annotation);
305        self
306    }
307
308    /// Add a horizontal reference line.
309    pub fn add_horizontal_line(mut self, y: f64, color: Color) -> Self {
310        let (x_min, x_max) = self.chart.x_range();
311        self.chart
312            .line_annotations
313            .push(LineAnnotation::horizontal(y, x_min, x_max).with_color(color));
314        self
315    }
316
317    /// Add a vertical reference line.
318    pub fn add_vertical_line(mut self, x: f64, color: Color) -> Self {
319        let (y_min, y_max) = self.chart.y_range();
320        self.chart
321            .line_annotations
322            .push(LineAnnotation::vertical(x, y_min, y_max).with_color(color));
323        self
324    }
325
326    /// Add a fill region.
327    pub fn add_fill_region(mut self, region: FillRegion) -> Self {
328        self.chart.fill_regions.push(region);
329        self
330    }
331
332    /// Add a horizontal band fill.
333    pub fn add_horizontal_band(mut self, y_min: f64, y_max: f64, color: Color) -> Self {
334        self.chart
335            .fill_regions
336            .push(FillRegion::horizontal_band(y_min, y_max, color));
337        self
338    }
339
340    /// Add a vertical band fill.
341    pub fn add_vertical_band(mut self, x_min: f64, x_max: f64, color: Color) -> Self {
342        self.chart
343            .fill_regions
344            .push(FillRegion::vertical_band(x_min, x_max, color));
345        self
346    }
347
348    /// Add fill below a series.
349    pub fn fill_below_series(mut self, series_index: usize, baseline: f64, color: Color) -> Self {
350        self.chart
351            .fill_regions
352            .push(FillRegion::below_series(series_index, baseline, color));
353        self
354    }
355
356    /// Add fill between two series.
357    pub fn fill_between_series(mut self, series1: usize, series2: usize, color: Color) -> Self {
358        self.chart
359            .fill_regions
360            .push(FillRegion::between_series(series1, series2, color));
361        self
362    }
363
364    /// Enable grid lines.
365    pub fn with_grid(mut self) -> Self {
366        for axis in &mut self.chart.axes {
367            axis.grid_lines = true;
368        }
369        self
370    }
371
372    /// Disable grid lines.
373    pub fn without_grid(mut self) -> Self {
374        for axis in &mut self.chart.axes {
375            axis.grid_lines = false;
376        }
377        self
378    }
379
380    /// Set legend position.
381    pub fn with_legend(mut self, position: LegendPosition) -> Self {
382        self.chart.legend = Some(LegendConfig {
383            position,
384            padding: 10.0,
385        });
386        self
387    }
388
389    /// Disable legend.
390    pub fn without_legend(mut self) -> Self {
391        self.chart.legend = None;
392        self
393    }
394
395    /// Set background color.
396    pub fn background(mut self, color: Color) -> Self {
397        self.chart.background_color = color;
398        self
399    }
400
401    /// Set padding around the chart area.
402    pub fn padding(mut self, padding: f32) -> Self {
403        self.chart.padding = padding;
404        self
405    }
406
407    /// Set bar chart configuration.
408    pub fn bar_config(mut self, config: BarConfig) -> Self {
409        self.chart.bar_config = config;
410        self
411    }
412
413    /// Enable interactivity (pan and zoom).
414    pub fn interactive(mut self, enabled: bool) -> Self {
415        self.chart.interactive.pan_enabled = enabled;
416        self.chart.interactive.zoom_enabled = enabled;
417        self
418    }
419
420    /// Enable crosshair on hover.
421    pub fn with_crosshair(mut self) -> Self {
422        self.chart.show_crosshair = true;
423        self
424    }
425
426    /// Enable tooltips on hover.
427    pub fn with_tooltips(mut self) -> Self {
428        self.chart.show_tooltips = true;
429        self
430    }
431
432    /// Disable tooltips.
433    pub fn without_tooltips(mut self) -> Self {
434        self.chart.show_tooltips = false;
435        self
436    }
437
438    /// Set zoom limits.
439    pub fn zoom_limits(mut self, min: f32, max: f32) -> Self {
440        self.chart.interactive.zoom_min = min;
441        self.chart.interactive.zoom_max = max;
442        self
443    }
444
445    /// Build the chart.
446    pub fn build(self) -> Chart {
447        self.chart
448    }
449
450    // =========================================================================
451    // Enhanced Builder API (closure-based configuration)
452    // =========================================================================
453
454    /// Configure the primary X axis using a closure.
455    ///
456    /// # Example
457    ///
458    /// ```ignore
459    /// chart.x_axis(|a| a
460    ///     .label("Time (s)")
461    ///     .range(0.0, 100.0)
462    ///     .grid(|g| g.major(|m| m.thickness(1.0)))
463    /// );
464    /// ```
465    pub fn x_axis<F>(self, f: F) -> Self
466    where
467        F: FnOnce(AxisBuilder) -> AxisBuilder,
468    {
469        let builder = AxisBuilder::new(AxisId::X_PRIMARY)
470            .orientation(AxisOrientation::Horizontal)
471            .position(AxisPosition::Bottom);
472        let configured = f(builder);
473        self.x_axis_config(configured.build())
474    }
475
476    /// Configure the primary Y axis using a closure.
477    pub fn y_axis<F>(self, f: F) -> Self
478    where
479        F: FnOnce(AxisBuilder) -> AxisBuilder,
480    {
481        let builder = AxisBuilder::new(AxisId::Y_PRIMARY)
482            .orientation(AxisOrientation::Vertical)
483            .position(AxisPosition::Left);
484        let configured = f(builder);
485        self.y_axis_config(configured.build())
486    }
487
488    /// Add a custom axis using a closure.
489    ///
490    /// # Example
491    ///
492    /// ```ignore
493    /// chart.add_custom_axis("pressure", |a| a
494    ///     .orientation(AxisOrientation::Vertical)
495    ///     .position(AxisPosition::Right)
496    ///     .label("Pressure (kPa)")
497    ///     .range(0.0, 200.0)
498    /// );
499    /// ```
500    pub fn add_custom_axis<F>(mut self, name: &str, f: F) -> Self
501    where
502        F: FnOnce(AxisBuilder) -> AxisBuilder,
503    {
504        let axis_id = AxisId::from_name(name);
505        let builder = AxisBuilder::new(axis_id).name(name);
506        let configured = f(builder);
507        self.chart.set_axis(configured.build());
508        self
509    }
510
511    /// Add a series using a closure for configuration.
512    ///
513    /// # Example
514    ///
515    /// ```ignore
516    /// chart.add_series_with("Temperature", |s| s
517    ///     .data(&data)
518    ///     .color(Color::RED)
519    ///     .dashed(5.0, 3.0)
520    ///     .markers(|m| m.circle().size(4.0))
521    /// );
522    /// ```
523    pub fn add_series_with<F>(mut self, name: impl Into<String>, f: F) -> Self
524    where
525        F: FnOnce(SeriesBuilder) -> SeriesBuilder,
526    {
527        let color = palette_color(self.series_count);
528        let builder = SeriesBuilder::new(name).color(color);
529        let configured = f(builder);
530        self.chart.series.push(configured.build());
531        self.series_count += 1;
532        self
533    }
534
535    /// Create a streaming series with a ring buffer.
536    ///
537    /// The series is created with an empty ring buffer of the specified capacity.
538    ///
539    /// # Example
540    ///
541    /// ```ignore
542    /// chart.streaming_series("Live Sensor", 10_000, |s| s
543    ///     .color(Color::BLUE)
544    ///     .fill_to_baseline(Color::rgba(0.0, 0.0, 1.0, 0.1))
545    /// );
546    /// ```
547    pub fn streaming_series<F>(mut self, name: impl Into<String>, capacity: usize, f: F) -> Self
548    where
549        F: FnOnce(SeriesBuilder) -> SeriesBuilder,
550    {
551        let color = palette_color(self.series_count);
552        let builder = SeriesBuilder::new(name).color(color).streaming(capacity);
553        let configured = f(builder);
554        self.chart.series.push(configured.build());
555        self.series_count += 1;
556        self
557    }
558}
559
560// =============================================================================
561// AxisBuilder
562// =============================================================================
563
564/// Builder for configuring chart axes.
565#[derive(Debug)]
566pub struct AxisBuilder {
567    axis: Axis,
568    grid_config: Option<GridConfig>,
569}
570
571impl AxisBuilder {
572    /// Create a new axis builder with the given ID.
573    pub fn new(id: AxisId) -> Self {
574        Self {
575            axis: Axis {
576                id,
577                ..Default::default()
578            },
579            grid_config: None,
580        }
581    }
582
583    /// Set the axis name (for custom axes).
584    pub fn name(self, name: impl Into<String>) -> Self {
585        // Store name in label for now (could add separate field later)
586        let _ = name.into();
587        self
588    }
589
590    /// Set the axis label.
591    pub fn label(mut self, label: impl Into<String>) -> Self {
592        self.axis.label = Some(label.into());
593        self
594    }
595
596    /// Set the axis range.
597    pub fn range(mut self, min: f64, max: f64) -> Self {
598        self.axis.min = Some(min);
599        self.axis.max = Some(max);
600        self
601    }
602
603    /// Enable auto-ranging with the specified padding.
604    pub fn auto_range(mut self, padding: f64) -> Self {
605        self.axis.min = None;
606        self.axis.max = None;
607        // Note: padding is stored elsewhere in enhanced axis
608        let _ = padding;
609        self
610    }
611
612    /// Set the axis orientation.
613    pub fn orientation(mut self, orientation: AxisOrientation) -> Self {
614        self.axis.orientation = orientation;
615        self
616    }
617
618    /// Set the axis position.
619    pub fn position(mut self, position: AxisPosition) -> Self {
620        self.axis.position = position;
621        self
622    }
623
624    /// Set the number of ticks.
625    pub fn ticks(mut self, count: usize) -> Self {
626        self.axis.tick_count = count;
627        self
628    }
629
630    /// Set custom tick values.
631    pub fn custom_ticks(mut self, ticks: Vec<(f64, String)>) -> Self {
632        self.axis.custom_ticks = Some(ticks);
633        self
634    }
635
636    /// Enable or disable grid lines.
637    pub fn show_grid(mut self, show: bool) -> Self {
638        self.axis.grid_lines = show;
639        self
640    }
641
642    /// Configure grid lines using a closure.
643    pub fn grid<F>(mut self, f: F) -> Self
644    where
645        F: FnOnce(GridBuilder) -> GridBuilder,
646    {
647        let builder = GridBuilder::new();
648        let configured = f(builder);
649        self.grid_config = Some(configured.build());
650        self.axis.grid_lines = true;
651        self
652    }
653
654    /// Set visibility.
655    pub fn visible(mut self, visible: bool) -> Self {
656        self.axis.visible = visible;
657        self
658    }
659
660    /// Build the axis.
661    pub fn build(self) -> Axis {
662        self.axis
663    }
664}
665
666// =============================================================================
667// GridBuilder
668// =============================================================================
669
670/// Builder for configuring grid lines.
671#[derive(Debug)]
672pub struct GridBuilder {
673    config: GridConfig,
674}
675
676impl GridBuilder {
677    /// Create a new grid builder.
678    pub fn new() -> Self {
679        Self {
680            config: GridConfig::default(),
681        }
682    }
683
684    /// Configure major grid lines.
685    pub fn major<F>(mut self, f: F) -> Self
686    where
687        F: FnOnce(GridLevelBuilder) -> GridLevelBuilder,
688    {
689        let builder = GridLevelBuilder::new(GridLevel::major());
690        let configured = f(builder);
691        self.config.major = configured.build();
692        self
693    }
694
695    /// Configure minor grid lines.
696    pub fn minor<F>(mut self, f: F) -> Self
697    where
698        F: FnOnce(GridLevelBuilder) -> GridLevelBuilder,
699    {
700        let builder = GridLevelBuilder::new(GridLevel::minor());
701        let configured = f(builder);
702        self.config.minor = Some(configured.build());
703        self
704    }
705
706    /// Configure tertiary grid lines.
707    pub fn tertiary<F>(mut self, f: F) -> Self
708    where
709        F: FnOnce(GridLevelBuilder) -> GridLevelBuilder,
710    {
711        let builder = GridLevelBuilder::new(GridLevel::tertiary());
712        let configured = f(builder);
713        self.config.tertiary = Some(configured.build());
714        self
715    }
716
717    /// Set the number of minor divisions between major lines.
718    pub fn divisions(mut self, count: usize) -> Self {
719        self.config.minor_divisions = count;
720        self
721    }
722
723    /// Set the grid spacing strategy.
724    pub fn spacing(mut self, spacing: GridSpacing) -> Self {
725        self.config.spacing = spacing;
726        self
727    }
728
729    /// Use auto spacing with the target count.
730    pub fn auto_spacing(mut self, count: usize) -> Self {
731        self.config.spacing = GridSpacing::auto(count);
732        self
733    }
734
735    /// Use fixed interval spacing.
736    pub fn fixed_spacing(mut self, interval: f64) -> Self {
737        self.config.spacing = GridSpacing::fixed(interval);
738        self
739    }
740
741    /// Build the grid configuration.
742    pub fn build(self) -> GridConfig {
743        self.config
744    }
745}
746
747impl Default for GridBuilder {
748    fn default() -> Self {
749        Self::new()
750    }
751}
752
753/// Builder for a single grid level.
754#[derive(Debug)]
755pub struct GridLevelBuilder {
756    level: GridLevel,
757}
758
759impl GridLevelBuilder {
760    /// Create a new grid level builder.
761    pub fn new(level: GridLevel) -> Self {
762        Self { level }
763    }
764
765    /// Set the line thickness.
766    pub fn thickness(mut self, thickness: f32) -> Self {
767        self.level.thickness = thickness;
768        self
769    }
770
771    /// Set the line color.
772    pub fn color(mut self, color: Color) -> Self {
773        self.level.color = color;
774        self
775    }
776
777    /// Set the dash pattern.
778    pub fn dash(mut self, dash: DashPattern) -> Self {
779        self.level.dash = dash;
780        self
781    }
782
783    /// Make this a dotted line.
784    pub fn dotted(mut self) -> Self {
785        self.level.dash = DashPattern::dotted(2.0);
786        self
787    }
788
789    /// Make this a dashed line.
790    pub fn dashed(mut self) -> Self {
791        self.level.dash = DashPattern::medium_dash();
792        self
793    }
794
795    /// Enable or disable this level.
796    pub fn enabled(mut self, enabled: bool) -> Self {
797        self.level.enabled = enabled;
798        self
799    }
800
801    /// Build the grid level.
802    pub fn build(self) -> GridLevel {
803        self.level
804    }
805}
806
807// =============================================================================
808// SeriesBuilder
809// =============================================================================
810
811/// Builder for configuring data series.
812#[derive(Debug)]
813pub struct SeriesBuilder {
814    name: String,
815    data: Vec<DataPoint>,
816    style: SeriesStyle,
817    x_axis: AxisId,
818    y_axis: AxisId,
819    is_streaming: bool,
820    streaming_capacity: usize,
821}
822
823impl SeriesBuilder {
824    /// Create a new series builder.
825    pub fn new(name: impl Into<String>) -> Self {
826        Self {
827            name: name.into(),
828            data: Vec::new(),
829            style: SeriesStyle::default(),
830            x_axis: AxisId::X_PRIMARY,
831            y_axis: AxisId::Y_PRIMARY,
832            is_streaming: false,
833            streaming_capacity: 1000,
834        }
835    }
836
837    /// Set the data points.
838    pub fn data<T: Into<DataPoint> + Copy>(mut self, data: &[T]) -> Self {
839        self.data = data.iter().map(|&d| d.into()).collect();
840        self
841    }
842
843    /// Set as a streaming series with ring buffer.
844    pub fn streaming(mut self, capacity: usize) -> Self {
845        self.is_streaming = true;
846        self.streaming_capacity = capacity;
847        self
848    }
849
850    /// Set the line color.
851    pub fn color(mut self, color: Color) -> Self {
852        self.style.color = color;
853        self
854    }
855
856    /// Set the line width.
857    pub fn line_width(mut self, width: f32) -> Self {
858        self.style.line_width = width;
859        self
860    }
861
862    /// Make this a dashed line.
863    pub fn dashed(mut self, dash_len: f32, gap_len: f32) -> Self {
864        self.style.line_style = LineStyle::Dashed;
865        let _ = (dash_len, gap_len); // Would be used with enhanced line config
866        self
867    }
868
869    /// Make this a dotted line.
870    pub fn dotted(mut self) -> Self {
871        self.style.line_style = LineStyle::Dotted;
872        self
873    }
874
875    /// Add markers using a closure.
876    pub fn markers<F>(mut self, f: F) -> Self
877    where
878        F: FnOnce(MarkerBuilder) -> MarkerBuilder,
879    {
880        let builder = MarkerBuilder::new();
881        let configured = f(builder);
882        self.style.point_style = Some(configured.build());
883        self
884    }
885
886    /// Add simple circle markers.
887    pub fn with_markers(mut self) -> Self {
888        self.style.point_style = Some(PointStyle {
889            size: 6.0,
890            shape: super::style::MarkerShape::Circle,
891            color: self.style.color,
892        });
893        self
894    }
895
896    /// Add fill to baseline.
897    pub fn fill_to_baseline(mut self, color: Color) -> Self {
898        self.style.fill = Some(FillStyle {
899            color,
900            opacity: color.a,
901        });
902        self
903    }
904
905    /// Set the X axis.
906    pub fn x_axis(mut self, axis: AxisId) -> Self {
907        self.x_axis = axis;
908        self
909    }
910
911    /// Set the Y axis.
912    pub fn y_axis(mut self, axis: AxisId) -> Self {
913        self.y_axis = axis;
914        self
915    }
916
917    /// Set both axes.
918    pub fn axes(mut self, x_axis: AxisId, y_axis: AxisId) -> Self {
919        self.x_axis = x_axis;
920        self.y_axis = y_axis;
921        self
922    }
923
924    /// Set z-order.
925    pub fn z_order(mut self, z_order: i32) -> Self {
926        self.style.z_order = z_order;
927        self
928    }
929
930    /// Set visibility.
931    pub fn visible(mut self, visible: bool) -> Self {
932        self.style.visible = visible;
933        self
934    }
935
936    /// Hide from legend.
937    pub fn hide_from_legend(mut self) -> Self {
938        self.style.show_in_legend = false;
939        self
940    }
941
942    /// Build the series.
943    pub fn build(self) -> Series {
944        Series {
945            name: self.name,
946            data: self.data,
947            style: self.style,
948            x_axis: self.x_axis,
949            y_axis: self.y_axis,
950        }
951    }
952}
953
954/// Builder for marker configuration.
955#[derive(Debug)]
956pub struct MarkerBuilder {
957    style: PointStyle,
958}
959
960impl MarkerBuilder {
961    /// Create a new marker builder.
962    pub fn new() -> Self {
963        Self {
964            style: PointStyle::default(),
965        }
966    }
967
968    /// Use circle markers.
969    pub fn circle(mut self) -> Self {
970        self.style.shape = super::style::MarkerShape::Circle;
971        self
972    }
973
974    /// Use square markers.
975    pub fn square(mut self) -> Self {
976        self.style.shape = super::style::MarkerShape::Square;
977        self
978    }
979
980    /// Use diamond markers.
981    pub fn diamond(mut self) -> Self {
982        self.style.shape = super::style::MarkerShape::Diamond;
983        self
984    }
985
986    /// Use triangle markers.
987    pub fn triangle(mut self) -> Self {
988        self.style.shape = super::style::MarkerShape::Triangle;
989        self
990    }
991
992    /// Set marker size.
993    pub fn size(mut self, size: f32) -> Self {
994        self.style.size = size;
995        self
996    }
997
998    /// Set marker color.
999    pub fn color(mut self, color: Color) -> Self {
1000        self.style.color = color;
1001        self
1002    }
1003
1004    /// Build the point style.
1005    pub fn build(self) -> PointStyle {
1006        self.style
1007    }
1008}
1009
1010impl Default for MarkerBuilder {
1011    fn default() -> Self {
1012        Self::new()
1013    }
1014}
1015
1016#[cfg(test)]
1017mod tests {
1018    use super::*;
1019
1020    #[test]
1021    fn test_line_chart_builder() {
1022        let chart = ChartBuilder::line()
1023            .title("Test Chart")
1024            .x_label("X")
1025            .y_label("Y")
1026            .add_series("Series 1", &[(0.0, 1.0), (1.0, 2.0), (2.0, 1.5)])
1027            .with_grid()
1028            .build();
1029
1030        assert_eq!(chart.chart_type, ChartType::Line);
1031        assert_eq!(
1032            chart.title.as_ref().map(|t| t.text.as_str()),
1033            Some("Test Chart")
1034        );
1035        assert_eq!(chart.series.len(), 1);
1036    }
1037
1038    #[test]
1039    fn test_function_plot() {
1040        let chart = ChartBuilder::line()
1041            .plot_function("sin(x)", |x| x.sin(), 0.0, std::f64::consts::TAU, 100)
1042            .build();
1043
1044        assert_eq!(chart.series.len(), 1);
1045        assert_eq!(chart.series[0].data.len(), 100);
1046    }
1047
1048    #[test]
1049    fn test_secondary_axis() {
1050        let chart = ChartBuilder::line()
1051            .add_series("Primary", &[(0.0, 1.0), (1.0, 2.0)])
1052            .secondary_y_axis(Axis::y_secondary().with_label("Secondary Y"))
1053            .secondary_y_range(0.0, 100.0)
1054            .add_series_secondary_y("Secondary", &[(0.0, 50.0), (1.0, 75.0)])
1055            .build();
1056
1057        assert_eq!(chart.series.len(), 2);
1058        assert_eq!(chart.series[1].y_axis, AxisId::Y_SECONDARY);
1059        assert!(chart.get_axis(AxisId::Y_SECONDARY).is_some());
1060    }
1061
1062    #[test]
1063    fn test_annotations() {
1064        let chart = ChartBuilder::line()
1065            .add_text_at("Peak", 1.0, 2.0)
1066            .add_horizontal_line(1.5, Color::RED)
1067            .add_horizontal_band(0.5, 1.0, Color::rgba(0.0, 1.0, 0.0, 0.2))
1068            .build();
1069
1070        assert_eq!(chart.text_annotations.len(), 1);
1071        assert_eq!(chart.line_annotations.len(), 1);
1072        assert_eq!(chart.fill_regions.len(), 1);
1073    }
1074}