Skip to main content

astrelis_geometry/chart/
renderer.rs

1//! Chart rendering using the geometry renderer.
2//!
3//! This module provides two rendering paths:
4//! - **Tessellation path**: Uses Lyon for CPU tessellation (slower, but universal)
5//! - **GPU path**: Uses specialized renderers for instanced GPU rendering (faster for large datasets)
6//!
7//! The GPU path is automatically selected for charts with >500 data points per series.
8
9use super::rect::Rect;
10use super::renderers::GPU_RENDER_THRESHOLD;
11use super::renderers::GpuChartLineRenderer;
12use super::types::{
13    AxisId, AxisOrientation, AxisPosition, Chart, ChartType, DataPoint, FillRegionKind,
14};
15use crate::{GeometryRenderer, PathBuilder, ScissorRect, Stroke, Style};
16use astrelis_core::profiling::profile_scope;
17use astrelis_render::{Color, Viewport, wgpu};
18use glam::Vec2;
19
20/// Renders charts using a GeometryRenderer, with optional GPU acceleration.
21pub struct ChartRenderer<'a> {
22    geometry: &'a mut GeometryRenderer,
23    /// Optional GPU line renderer for accelerated line series rendering.
24    gpu_line_renderer: Option<&'a mut GpuChartLineRenderer>,
25}
26
27impl<'a> ChartRenderer<'a> {
28    /// Create a new chart renderer wrapping a geometry renderer.
29    pub fn new(geometry: &'a mut GeometryRenderer) -> Self {
30        Self {
31            geometry,
32            gpu_line_renderer: None,
33        }
34    }
35
36    /// Create a chart renderer with GPU acceleration for line series.
37    ///
38    /// When GPU acceleration is enabled and a line chart has more than
39    /// `GPU_RENDER_THRESHOLD` points, it will use GPU instancing for
40    /// much faster rendering.
41    pub fn with_gpu_line_renderer(
42        geometry: &'a mut GeometryRenderer,
43        gpu_line_renderer: &'a mut GpuChartLineRenderer,
44    ) -> Self {
45        Self {
46            geometry,
47            gpu_line_renderer: Some(gpu_line_renderer),
48        }
49    }
50
51    /// Render a chart within the given bounds.
52    ///
53    /// This method accumulates geometry commands. Call `render()` afterwards
54    /// to draw everything to a render pass.
55    ///
56    /// For GPU-accelerated line charts, use `draw_with_gpu_lines()` instead.
57    pub fn draw(&mut self, chart: &Chart, bounds: Rect) {
58        self.draw_internal(chart, bounds, false);
59    }
60
61    /// Render a chart with GPU-accelerated line series.
62    ///
63    /// This method accumulates non-line geometry commands. Large line series
64    /// (>500 points) will NOT be drawn - they should be rendered separately
65    /// via `GpuChartLineRenderer` for much better performance.
66    ///
67    /// Returns the plot area rectangle for use with GPU line rendering.
68    pub fn draw_with_gpu_lines(&mut self, chart: &Chart, bounds: Rect) -> Rect {
69        let plot_area = bounds.inset(chart.padding);
70        self.draw_internal(chart, bounds, true);
71        plot_area
72    }
73
74    /// Internal draw implementation with GPU line skip option.
75    fn draw_internal(&mut self, chart: &Chart, bounds: Rect, skip_gpu_lines: bool) {
76        profile_scope!("chart_draw");
77
78        // When skip_gpu_lines is true, we skip large line series (they'll be rendered via GPU).
79        // When false, we draw everything via tessellation.
80        let use_gpu_for_lines = skip_gpu_lines && chart.chart_type == ChartType::Line;
81
82        // Draw background
83        self.geometry
84            .draw_rect(bounds.position(), bounds.size(), chart.background_color);
85
86        // Calculate plot area (inside padding and axis labels)
87        let plot_area = bounds.inset(chart.padding);
88
89        // Draw grid lines (outside scissor so they extend to edges)
90        {
91            profile_scope!("draw_grid");
92            self.draw_grid(chart, &plot_area);
93        }
94
95        // Draw axes (outside scissor)
96        {
97            profile_scope!("draw_axes");
98            self.draw_all_axes(chart, &plot_area);
99        }
100
101        // Set scissor to clip data series to plot area
102        self.geometry.set_scissor(ScissorRect::from_f32(
103            plot_area.x,
104            plot_area.y,
105            plot_area.width,
106            plot_area.height,
107        ));
108
109        // Draw fill regions (clipped to plot area)
110        {
111            profile_scope!("draw_fill_regions");
112            self.draw_fill_regions(chart, &plot_area);
113        }
114
115        // Draw line annotations (clipped to plot area)
116        {
117            profile_scope!("draw_line_annotations");
118            self.draw_line_annotations(chart, &plot_area);
119        }
120
121        // Draw data series (clipped to plot area)
122        {
123            profile_scope!("draw_series");
124            match chart.chart_type {
125                ChartType::Line => {
126                    if use_gpu_for_lines {
127                        // Only draw small series via tessellation, skip large ones for GPU
128                        self.draw_line_series_tessellated_only(chart, &plot_area);
129                    } else {
130                        self.draw_line_series(chart, &plot_area);
131                    }
132                }
133                ChartType::Scatter => self.draw_scatter_series(chart, &plot_area),
134                ChartType::Bar => self.draw_bar_series(chart, &plot_area),
135                ChartType::Area => self.draw_area_series(chart, &plot_area),
136            }
137        }
138
139        // Draw crosshair if enabled and hovering (clipped to plot area)
140        if chart.show_crosshair {
141            profile_scope!("draw_crosshair");
142            self.draw_crosshair(chart, &plot_area);
143        }
144
145        // Reset scissor for any subsequent drawing
146        self.geometry.reset_scissor();
147    }
148
149    /// Draw only small line series via tessellation (for hybrid rendering).
150    fn draw_line_series_tessellated_only(&mut self, chart: &Chart, plot_area: &Rect) {
151        profile_scope!("draw_line_series_small");
152        for series in &chart.series {
153            // Skip large series - they'll be rendered via GPU
154            if series.data.len() > GPU_RENDER_THRESHOLD {
155                continue;
156            }
157
158            if series.data.len() < 2 {
159                continue;
160            }
161
162            self.draw_single_line_series_tessellated(chart, series, plot_area);
163        }
164    }
165
166    /// Draw a single line series using tessellation.
167    fn draw_single_line_series_tessellated(
168        &mut self,
169        chart: &Chart,
170        series: &super::types::Series,
171        plot_area: &Rect,
172    ) {
173        // Get visible X range with buffer for smooth scrolling
174        let (x_min, x_max) = chart.axis_range(series.x_axis);
175        let x_range = x_max - x_min;
176        let buffer = x_range * 0.1;
177        let visible_x_min = x_min - buffer;
178        let visible_x_max = x_max + buffer;
179
180        // Find visible point range using binary search
181        let (start_idx, end_idx) =
182            Self::find_visible_range(&series.data, visible_x_min, visible_x_max);
183
184        if end_idx <= start_idx + 1 {
185            return;
186        }
187
188        // Build path for the visible portion of the line
189        let mut builder = PathBuilder::new();
190        let first_point = self.data_to_pixel_with_axes(
191            chart,
192            plot_area,
193            series.data[start_idx].x,
194            series.data[start_idx].y,
195            series.x_axis,
196            series.y_axis,
197        );
198        builder.move_to(first_point);
199
200        for point in &series.data[start_idx + 1..end_idx] {
201            let pixel = self.data_to_pixel_with_axes(
202                chart,
203                plot_area,
204                point.x,
205                point.y,
206                series.x_axis,
207                series.y_axis,
208            );
209            builder.line_to(pixel);
210        }
211
212        let path = builder.build();
213        let stroke = Stroke::solid(series.style.color, series.style.line_width);
214        self.geometry.draw_path_stroke(&path, &stroke);
215
216        // Draw points if enabled
217        if let Some(point_style) = &series.style.point_style {
218            for point in &series.data[start_idx..end_idx] {
219                let pixel = self.data_to_pixel_with_axes(
220                    chart,
221                    plot_area,
222                    point.x,
223                    point.y,
224                    series.x_axis,
225                    series.y_axis,
226                );
227                self.geometry
228                    .draw_circle(pixel, point_style.size * 0.5, point_style.color);
229            }
230        }
231    }
232
233    /// Convert data coordinates to pixel coordinates using specific axes.
234    fn data_to_pixel_with_axes(
235        &self,
236        chart: &Chart,
237        plot_area: &Rect,
238        x: f64,
239        y: f64,
240        x_axis_id: AxisId,
241        y_axis_id: AxisId,
242    ) -> Vec2 {
243        let (x_min, x_max) = chart.axis_range(x_axis_id);
244        let (y_min, y_max) = chart.axis_range(y_axis_id);
245
246        let px = plot_area.x + ((x - x_min) / (x_max - x_min)) as f32 * plot_area.width;
247        // Y is inverted (0 at top in screen coords)
248        let py = plot_area.y + plot_area.height
249            - ((y - y_min) / (y_max - y_min)) as f32 * plot_area.height;
250
251        Vec2::new(px, py)
252    }
253
254    /// Convert pixel coordinates to data coordinates.
255    pub fn pixel_to_data(&self, chart: &Chart, plot_area: &Rect, pixel: Vec2) -> DataPoint {
256        let (x_min, x_max) = chart.x_range();
257        let (y_min, y_max) = chart.y_range();
258
259        let x = x_min + ((pixel.x - plot_area.x) / plot_area.width) as f64 * (x_max - x_min);
260        let y = y_max - ((pixel.y - plot_area.y) / plot_area.height) as f64 * (y_max - y_min);
261
262        DataPoint::new(x, y)
263    }
264
265    fn draw_fill_regions(&mut self, chart: &Chart, plot_area: &Rect) {
266        for region in &chart.fill_regions {
267            match &region.kind {
268                FillRegionKind::HorizontalBand { y_min, y_max } => {
269                    let (x_range_min, x_range_max) = chart.axis_range(region.x_axis);
270                    let top_left = self.data_to_pixel_with_axes(
271                        chart,
272                        plot_area,
273                        x_range_min,
274                        *y_max,
275                        region.x_axis,
276                        region.y_axis,
277                    );
278                    let bottom_right = self.data_to_pixel_with_axes(
279                        chart,
280                        plot_area,
281                        x_range_max,
282                        *y_min,
283                        region.x_axis,
284                        region.y_axis,
285                    );
286
287                    self.geometry.draw_rect(
288                        Vec2::new(top_left.x, top_left.y),
289                        Vec2::new(bottom_right.x - top_left.x, bottom_right.y - top_left.y),
290                        region.color,
291                    );
292                }
293                FillRegionKind::VerticalBand { x_min, x_max } => {
294                    let (y_range_min, y_range_max) = chart.axis_range(region.y_axis);
295                    let top_left = self.data_to_pixel_with_axes(
296                        chart,
297                        plot_area,
298                        *x_min,
299                        y_range_max,
300                        region.x_axis,
301                        region.y_axis,
302                    );
303                    let bottom_right = self.data_to_pixel_with_axes(
304                        chart,
305                        plot_area,
306                        *x_max,
307                        y_range_min,
308                        region.x_axis,
309                        region.y_axis,
310                    );
311
312                    self.geometry.draw_rect(
313                        Vec2::new(top_left.x, top_left.y),
314                        Vec2::new(bottom_right.x - top_left.x, bottom_right.y - top_left.y),
315                        region.color,
316                    );
317                }
318                FillRegionKind::BelowSeries {
319                    series_index,
320                    y_baseline,
321                } => {
322                    if let Some(series) = chart.series.get(*series_index) {
323                        if series.data.len() < 2 {
324                            continue;
325                        }
326
327                        let mut builder = PathBuilder::new();
328
329                        // Start at baseline
330                        let base_start = self.data_to_pixel_with_axes(
331                            chart,
332                            plot_area,
333                            series.data[0].x,
334                            *y_baseline,
335                            series.x_axis,
336                            series.y_axis,
337                        );
338                        builder.move_to(base_start);
339
340                        // Line up to first data point
341                        let first = self.data_to_pixel_with_axes(
342                            chart,
343                            plot_area,
344                            series.data[0].x,
345                            series.data[0].y,
346                            series.x_axis,
347                            series.y_axis,
348                        );
349                        builder.line_to(first);
350
351                        // Follow series
352                        for point in &series.data[1..] {
353                            let p = self.data_to_pixel_with_axes(
354                                chart,
355                                plot_area,
356                                point.x,
357                                point.y,
358                                series.x_axis,
359                                series.y_axis,
360                            );
361                            builder.line_to(p);
362                        }
363
364                        // Close to baseline
365                        let base_end = self.data_to_pixel_with_axes(
366                            chart,
367                            plot_area,
368                            series.data.last().unwrap().x,
369                            *y_baseline,
370                            series.x_axis,
371                            series.y_axis,
372                        );
373                        builder.line_to(base_end);
374                        builder.close();
375
376                        let path = builder.build();
377                        let style = Style::fill_color(region.color);
378                        self.geometry.draw_path(&path, &style);
379                    }
380                }
381                FillRegionKind::BetweenSeries {
382                    series_index_1,
383                    series_index_2,
384                } => {
385                    let series1 = chart.series.get(*series_index_1);
386                    let series2 = chart.series.get(*series_index_2);
387
388                    if let (Some(s1), Some(s2)) = (series1, series2) {
389                        if s1.data.is_empty() || s2.data.is_empty() {
390                            continue;
391                        }
392
393                        let mut builder = PathBuilder::new();
394
395                        // Forward along series 1
396                        let first = self.data_to_pixel_with_axes(
397                            chart,
398                            plot_area,
399                            s1.data[0].x,
400                            s1.data[0].y,
401                            s1.x_axis,
402                            s1.y_axis,
403                        );
404                        builder.move_to(first);
405
406                        for point in &s1.data[1..] {
407                            let p = self.data_to_pixel_with_axes(
408                                chart, plot_area, point.x, point.y, s1.x_axis, s1.y_axis,
409                            );
410                            builder.line_to(p);
411                        }
412
413                        // Backward along series 2
414                        for point in s2.data.iter().rev() {
415                            let p = self.data_to_pixel_with_axes(
416                                chart, plot_area, point.x, point.y, s2.x_axis, s2.y_axis,
417                            );
418                            builder.line_to(p);
419                        }
420
421                        builder.close();
422
423                        let path = builder.build();
424                        let style = Style::fill_color(region.color);
425                        self.geometry.draw_path(&path, &style);
426                    }
427                }
428                FillRegionKind::Rectangle {
429                    x_min,
430                    y_min,
431                    x_max,
432                    y_max,
433                } => {
434                    let top_left = self.data_to_pixel_with_axes(
435                        chart,
436                        plot_area,
437                        *x_min,
438                        *y_max,
439                        region.x_axis,
440                        region.y_axis,
441                    );
442                    let bottom_right = self.data_to_pixel_with_axes(
443                        chart,
444                        plot_area,
445                        *x_max,
446                        *y_min,
447                        region.x_axis,
448                        region.y_axis,
449                    );
450
451                    self.geometry.draw_rect(
452                        Vec2::new(top_left.x, top_left.y),
453                        Vec2::new(bottom_right.x - top_left.x, bottom_right.y - top_left.y),
454                        region.color,
455                    );
456                }
457                FillRegionKind::Polygon { points } => {
458                    if points.len() < 3 {
459                        continue;
460                    }
461
462                    let mut builder = PathBuilder::new();
463                    let first = self.data_to_pixel_with_axes(
464                        chart,
465                        plot_area,
466                        points[0].x,
467                        points[0].y,
468                        region.x_axis,
469                        region.y_axis,
470                    );
471                    builder.move_to(first);
472
473                    for point in &points[1..] {
474                        let p = self.data_to_pixel_with_axes(
475                            chart,
476                            plot_area,
477                            point.x,
478                            point.y,
479                            region.x_axis,
480                            region.y_axis,
481                        );
482                        builder.line_to(p);
483                    }
484                    builder.close();
485
486                    let path = builder.build();
487                    let style = Style::fill_color(region.color);
488                    self.geometry.draw_path(&path, &style);
489                }
490            }
491        }
492    }
493
494    fn draw_line_annotations(&mut self, chart: &Chart, plot_area: &Rect) {
495        for annotation in &chart.line_annotations {
496            let start = self.data_to_pixel_with_axes(
497                chart,
498                plot_area,
499                annotation.start.x,
500                annotation.start.y,
501                annotation.x_axis,
502                annotation.y_axis,
503            );
504            let end = self.data_to_pixel_with_axes(
505                chart,
506                plot_area,
507                annotation.end.x,
508                annotation.end.y,
509                annotation.x_axis,
510                annotation.y_axis,
511            );
512
513            self.geometry
514                .draw_line(start, end, annotation.width, annotation.color);
515        }
516    }
517
518    fn draw_crosshair(&mut self, chart: &Chart, plot_area: &Rect) {
519        if let Some((series_idx, point_idx)) = chart.interactive.hovered_point
520            && let Some(series) = chart.series.get(series_idx)
521            && let Some(point) = series.data.get(point_idx)
522        {
523            let pixel = self.data_to_pixel_with_axes(
524                chart,
525                plot_area,
526                point.x,
527                point.y,
528                series.x_axis,
529                series.y_axis,
530            );
531
532            let crosshair_color = Color::rgba(1.0, 1.0, 1.0, 0.5);
533
534            // Vertical line
535            self.geometry.draw_line(
536                Vec2::new(pixel.x, plot_area.y),
537                Vec2::new(pixel.x, plot_area.bottom()),
538                1.0,
539                crosshair_color,
540            );
541
542            // Horizontal line
543            self.geometry.draw_line(
544                Vec2::new(plot_area.x, pixel.y),
545                Vec2::new(plot_area.right(), pixel.y),
546                1.0,
547                crosshair_color,
548            );
549
550            // Highlight point
551            self.geometry.draw_circle(pixel, 6.0, series.style.color);
552        }
553    }
554
555    fn draw_grid(&mut self, chart: &Chart, plot_area: &Rect) {
556        // Draw grid for each axis
557        for axis in &chart.axes {
558            if !axis.grid_lines || !axis.visible {
559                continue;
560            }
561
562            let style = &axis.style;
563            let tick_count = axis.tick_count;
564
565            match axis.orientation {
566                AxisOrientation::Horizontal => {
567                    // Vertical grid lines
568                    for i in 0..=tick_count {
569                        let t = i as f32 / tick_count as f32;
570                        let x = plot_area.x + t * plot_area.width;
571                        self.geometry.draw_line(
572                            Vec2::new(x, plot_area.y),
573                            Vec2::new(x, plot_area.bottom()),
574                            style.grid_width,
575                            style.grid_color,
576                        );
577                    }
578                }
579                AxisOrientation::Vertical => {
580                    // Horizontal grid lines
581                    for i in 0..=tick_count {
582                        let t = i as f32 / tick_count as f32;
583                        let y = plot_area.y + t * plot_area.height;
584                        self.geometry.draw_line(
585                            Vec2::new(plot_area.x, y),
586                            Vec2::new(plot_area.right(), y),
587                            style.grid_width,
588                            style.grid_color,
589                        );
590                    }
591                }
592            }
593        }
594    }
595
596    fn draw_all_axes(&mut self, chart: &Chart, plot_area: &Rect) {
597        for axis in &chart.axes {
598            if !axis.visible {
599                continue;
600            }
601
602            let style = &axis.style;
603
604            match (axis.orientation, axis.position) {
605                (AxisOrientation::Horizontal, AxisPosition::Bottom) => {
606                    // X axis at bottom
607                    self.geometry.draw_line(
608                        Vec2::new(plot_area.x, plot_area.bottom()),
609                        Vec2::new(plot_area.right(), plot_area.bottom()),
610                        style.line_width,
611                        style.line_color,
612                    );
613
614                    // Ticks
615                    for i in 0..=axis.tick_count {
616                        let t = i as f32 / axis.tick_count as f32;
617                        let x = plot_area.x + t * plot_area.width;
618                        let y = plot_area.bottom();
619                        self.geometry.draw_line(
620                            Vec2::new(x, y),
621                            Vec2::new(x, y + style.tick_length),
622                            style.line_width,
623                            style.tick_color,
624                        );
625                    }
626                }
627                (AxisOrientation::Horizontal, AxisPosition::Top) => {
628                    // X axis at top
629                    self.geometry.draw_line(
630                        Vec2::new(plot_area.x, plot_area.y),
631                        Vec2::new(plot_area.right(), plot_area.y),
632                        style.line_width,
633                        style.line_color,
634                    );
635
636                    // Ticks
637                    for i in 0..=axis.tick_count {
638                        let t = i as f32 / axis.tick_count as f32;
639                        let x = plot_area.x + t * plot_area.width;
640                        let y = plot_area.y;
641                        self.geometry.draw_line(
642                            Vec2::new(x, y - style.tick_length),
643                            Vec2::new(x, y),
644                            style.line_width,
645                            style.tick_color,
646                        );
647                    }
648                }
649                (AxisOrientation::Vertical, AxisPosition::Left) => {
650                    // Y axis at left
651                    self.geometry.draw_line(
652                        Vec2::new(plot_area.x, plot_area.y),
653                        Vec2::new(plot_area.x, plot_area.bottom()),
654                        style.line_width,
655                        style.line_color,
656                    );
657
658                    // Ticks
659                    for i in 0..=axis.tick_count {
660                        let t = i as f32 / axis.tick_count as f32;
661                        let x = plot_area.x;
662                        let y = plot_area.y + t * plot_area.height;
663                        self.geometry.draw_line(
664                            Vec2::new(x - style.tick_length, y),
665                            Vec2::new(x, y),
666                            style.line_width,
667                            style.tick_color,
668                        );
669                    }
670                }
671                (AxisOrientation::Vertical, AxisPosition::Right) => {
672                    // Y axis at right
673                    self.geometry.draw_line(
674                        Vec2::new(plot_area.right(), plot_area.y),
675                        Vec2::new(plot_area.right(), plot_area.bottom()),
676                        style.line_width,
677                        style.line_color,
678                    );
679
680                    // Ticks
681                    for i in 0..=axis.tick_count {
682                        let t = i as f32 / axis.tick_count as f32;
683                        let x = plot_area.right();
684                        let y = plot_area.y + t * plot_area.height;
685                        self.geometry.draw_line(
686                            Vec2::new(x, y),
687                            Vec2::new(x + style.tick_length, y),
688                            style.line_width,
689                            style.tick_color,
690                        );
691                    }
692                }
693                _ => {}
694            }
695        }
696    }
697
698    fn draw_line_series(&mut self, chart: &Chart, plot_area: &Rect) {
699        profile_scope!("draw_line_series");
700        for series in &chart.series {
701            if series.data.len() < 2 {
702                continue;
703            }
704
705            // Get visible X range with buffer for smooth scrolling
706            let (x_min, x_max) = chart.axis_range(series.x_axis);
707            let x_range = x_max - x_min;
708            let buffer = x_range * 0.1; // 10% buffer on each side
709            let visible_x_min = x_min - buffer;
710            let visible_x_max = x_max + buffer;
711
712            // Find visible point range using binary search (assumes sorted X data)
713            let (start_idx, end_idx) =
714                Self::find_visible_range(&series.data, visible_x_min, visible_x_max);
715
716            tracing::trace!(
717                "Series '{}': rendering {} of {} points (indices {}..{})",
718                series.name,
719                end_idx - start_idx,
720                series.data.len(),
721                start_idx,
722                end_idx
723            );
724
725            // Need at least 2 points to draw a line
726            if end_idx <= start_idx + 1 {
727                continue;
728            }
729
730            // Build path for the visible portion of the line
731            let mut builder = PathBuilder::new();
732            let first_point = self.data_to_pixel_with_axes(
733                chart,
734                plot_area,
735                series.data[start_idx].x,
736                series.data[start_idx].y,
737                series.x_axis,
738                series.y_axis,
739            );
740            builder.move_to(first_point);
741
742            for point in &series.data[start_idx + 1..end_idx] {
743                let pixel = self.data_to_pixel_with_axes(
744                    chart,
745                    plot_area,
746                    point.x,
747                    point.y,
748                    series.x_axis,
749                    series.y_axis,
750                );
751                builder.line_to(pixel);
752            }
753
754            let path = builder.build();
755
756            // Draw the line
757            let stroke = Stroke::solid(series.style.color, series.style.line_width);
758            self.geometry.draw_path_stroke(&path, &stroke);
759
760            // Draw points if enabled (only visible ones)
761            if let Some(point_style) = &series.style.point_style {
762                for point in &series.data[start_idx..end_idx] {
763                    let pixel = self.data_to_pixel_with_axes(
764                        chart,
765                        plot_area,
766                        point.x,
767                        point.y,
768                        series.x_axis,
769                        series.y_axis,
770                    );
771                    self.geometry
772                        .draw_circle(pixel, point_style.size * 0.5, point_style.color);
773                }
774            }
775        }
776    }
777
778    /// Find the range of visible points using binary search.
779    /// Returns (start_idx, end_idx) where end_idx is exclusive.
780    /// Includes one extra point on each side for line continuity.
781    fn find_visible_range(data: &[DataPoint], x_min: f64, x_max: f64) -> (usize, usize) {
782        if data.is_empty() {
783            return (0, 0);
784        }
785
786        // Binary search for first point >= x_min
787        let start = data
788            .binary_search_by(|p| p.x.partial_cmp(&x_min).unwrap_or(std::cmp::Ordering::Equal))
789            .unwrap_or_else(|i| i);
790
791        // Binary search for first point > x_max
792        let end = data
793            .binary_search_by(|p| {
794                if p.x <= x_max {
795                    std::cmp::Ordering::Less
796                } else {
797                    std::cmp::Ordering::Greater
798                }
799            })
800            .unwrap_or_else(|i| i);
801
802        // Include one extra point on each side for line continuity
803        let start = start.saturating_sub(1);
804        let end = (end + 1).min(data.len());
805
806        (start, end)
807    }
808
809    fn draw_scatter_series(&mut self, chart: &Chart, plot_area: &Rect) {
810        let default_point_style = super::style::PointStyle::default();
811        for series in &chart.series {
812            let point_style = series
813                .style
814                .point_style
815                .as_ref()
816                .unwrap_or(&default_point_style);
817
818            // Get visible X range with buffer
819            let (x_min, x_max) = chart.axis_range(series.x_axis);
820            let x_range = x_max - x_min;
821            let buffer = x_range * 0.1;
822            let (start_idx, end_idx) =
823                Self::find_visible_range(&series.data, x_min - buffer, x_max + buffer);
824
825            for point in &series.data[start_idx..end_idx] {
826                let pixel = self.data_to_pixel_with_axes(
827                    chart,
828                    plot_area,
829                    point.x,
830                    point.y,
831                    series.x_axis,
832                    series.y_axis,
833                );
834                self.geometry
835                    .draw_circle(pixel, point_style.size * 0.5, series.style.color);
836            }
837        }
838    }
839
840    fn draw_bar_series(&mut self, chart: &Chart, plot_area: &Rect) {
841        let bar_width = chart.bar_config.bar_width;
842        let gap = chart.bar_config.gap;
843
844        let series_count = chart.series.len() as f32;
845        let total_width = bar_width * series_count + gap * (series_count - 1.0);
846
847        for (series_idx, series) in chart.series.iter().enumerate() {
848            let (y_min, _) = chart.axis_range(series.y_axis);
849            let offset = series_idx as f32 * (bar_width + gap) - total_width * 0.5;
850
851            // Get visible X range with buffer
852            let (x_min, x_max) = chart.axis_range(series.x_axis);
853            let x_range = x_max - x_min;
854            let buffer = x_range * 0.1;
855            let (start_idx, end_idx) =
856                Self::find_visible_range(&series.data, x_min - buffer, x_max + buffer);
857
858            for point in &series.data[start_idx..end_idx] {
859                let center_pixel = self.data_to_pixel_with_axes(
860                    chart,
861                    plot_area,
862                    point.x,
863                    point.y,
864                    series.x_axis,
865                    series.y_axis,
866                );
867                let base_pixel = self.data_to_pixel_with_axes(
868                    chart,
869                    plot_area,
870                    point.x,
871                    y_min,
872                    series.x_axis,
873                    series.y_axis,
874                );
875
876                let bar_x = center_pixel.x + offset;
877                let bar_height = (base_pixel.y - center_pixel.y).abs();
878                let bar_y = center_pixel.y.min(base_pixel.y);
879
880                self.geometry.draw_rect(
881                    Vec2::new(bar_x, bar_y),
882                    Vec2::new(bar_width, bar_height),
883                    series.style.color,
884                );
885            }
886        }
887    }
888
889    fn draw_area_series(&mut self, chart: &Chart, plot_area: &Rect) {
890        for series in &chart.series {
891            if series.data.len() < 2 {
892                continue;
893            }
894
895            let (y_min, _) = chart.axis_range(series.y_axis);
896
897            // Get visible X range with buffer for smooth scrolling
898            let (x_min, x_max) = chart.axis_range(series.x_axis);
899            let x_range = x_max - x_min;
900            let buffer = x_range * 0.1; // 10% buffer on each side
901            let visible_x_min = x_min - buffer;
902            let visible_x_max = x_max + buffer;
903
904            // Find visible point range using binary search
905            let (start_idx, end_idx) =
906                Self::find_visible_range(&series.data, visible_x_min, visible_x_max);
907
908            // Need at least 2 points to draw an area
909            if end_idx <= start_idx + 1 {
910                continue;
911            }
912
913            let visible_data = &series.data[start_idx..end_idx];
914
915            // Build filled path for visible portion
916            let mut builder = PathBuilder::new();
917
918            // Start at baseline
919            let first_x = visible_data[0].x;
920            let base_start = self.data_to_pixel_with_axes(
921                chart,
922                plot_area,
923                first_x,
924                y_min,
925                series.x_axis,
926                series.y_axis,
927            );
928            builder.move_to(base_start);
929
930            // Line to first data point
931            let first_point = self.data_to_pixel_with_axes(
932                chart,
933                plot_area,
934                first_x,
935                visible_data[0].y,
936                series.x_axis,
937                series.y_axis,
938            );
939            builder.line_to(first_point);
940
941            // Connect visible data points
942            for point in &visible_data[1..] {
943                let pixel = self.data_to_pixel_with_axes(
944                    chart,
945                    plot_area,
946                    point.x,
947                    point.y,
948                    series.x_axis,
949                    series.y_axis,
950                );
951                builder.line_to(pixel);
952            }
953
954            // Close to baseline
955            let last_x = visible_data.last().unwrap().x;
956            let base_end = self.data_to_pixel_with_axes(
957                chart,
958                plot_area,
959                last_x,
960                y_min,
961                series.x_axis,
962                series.y_axis,
963            );
964            builder.line_to(base_end);
965            builder.close();
966
967            let path = builder.build();
968
969            // Draw filled area with transparency
970            let fill_color = if let Some(fill) = &series.style.fill {
971                Color::rgba(fill.color.r, fill.color.g, fill.color.b, fill.opacity)
972            } else {
973                Color::rgba(
974                    series.style.color.r,
975                    series.style.color.g,
976                    series.style.color.b,
977                    0.3,
978                )
979            };
980
981            let style = Style::fill_color(fill_color);
982            self.geometry.draw_path(&path, &style);
983
984            // Draw line on top (only visible portion)
985            let mut builder = PathBuilder::new();
986            let first_point = self.data_to_pixel_with_axes(
987                chart,
988                plot_area,
989                visible_data[0].x,
990                visible_data[0].y,
991                series.x_axis,
992                series.y_axis,
993            );
994            builder.move_to(first_point);
995
996            for point in &visible_data[1..] {
997                let pixel = self.data_to_pixel_with_axes(
998                    chart,
999                    plot_area,
1000                    point.x,
1001                    point.y,
1002                    series.x_axis,
1003                    series.y_axis,
1004                );
1005                builder.line_to(pixel);
1006            }
1007
1008            let path = builder.build();
1009            let stroke = Stroke::solid(series.style.color, series.style.line_width);
1010            self.geometry.draw_path_stroke(&path, &stroke);
1011        }
1012    }
1013
1014    /// Render the accumulated draw commands.
1015    pub fn render(&mut self, pass: &mut wgpu::RenderPass, viewport: Viewport) {
1016        self.geometry.render(pass, viewport);
1017    }
1018
1019    /// Render with GPU-accelerated line series.
1020    ///
1021    /// Call this after `draw_with_gpu_lines()`. The GPU line renderer must
1022    /// have been prepared with `GpuChartLineRenderer::prepare()` before this call.
1023    ///
1024    /// # Arguments
1025    ///
1026    /// * `pass` - The render pass to draw into
1027    /// * `viewport` - The viewport for rendering
1028    /// * `chart` - The chart being rendered (for data ranges)
1029    /// * `plot_area` - The plot area returned by `draw_with_gpu_lines()`
1030    pub fn render_with_gpu_lines(
1031        &mut self,
1032        pass: &mut wgpu::RenderPass,
1033        viewport: Viewport,
1034        chart: &Chart,
1035        plot_area: &Rect,
1036    ) {
1037        profile_scope!("chart_render_gpu_lines");
1038
1039        // First render all non-line geometry
1040        self.geometry.render(pass, viewport);
1041
1042        // Then render GPU lines if available
1043        if let Some(gpu_renderer) = &self.gpu_line_renderer {
1044            profile_scope!("render_gpu_line_series");
1045            gpu_renderer.render(pass, viewport, plot_area, chart);
1046        }
1047    }
1048}
1049
1050/// Hit test result for chart interaction.
1051#[derive(Debug, Clone)]
1052pub struct HitTestResult {
1053    /// Series index
1054    pub series_index: usize,
1055    /// Point index within the series
1056    pub point_index: usize,
1057    /// Distance from the test point to the data point (in pixels)
1058    pub distance: f32,
1059    /// The data point
1060    pub data_point: DataPoint,
1061    /// The pixel position of the data point
1062    pub pixel_position: Vec2,
1063}
1064
1065impl ChartRenderer<'_> {
1066    /// Find the nearest data point to a pixel position.
1067    pub fn hit_test(
1068        &self,
1069        chart: &Chart,
1070        plot_area: &Rect,
1071        pixel: Vec2,
1072        max_distance: f32,
1073    ) -> Option<HitTestResult> {
1074        if !plot_area.contains(pixel) {
1075            return None;
1076        }
1077
1078        let mut best: Option<HitTestResult> = None;
1079
1080        for (series_idx, series) in chart.series.iter().enumerate() {
1081            for (point_idx, point) in series.data.iter().enumerate() {
1082                let point_pixel = self.data_to_pixel_with_axes(
1083                    chart,
1084                    plot_area,
1085                    point.x,
1086                    point.y,
1087                    series.x_axis,
1088                    series.y_axis,
1089                );
1090
1091                let dist = pixel.distance(point_pixel);
1092
1093                if dist <= max_distance && best.as_ref().is_none_or(|b| dist < b.distance) {
1094                    best = Some(HitTestResult {
1095                        series_index: series_idx,
1096                        point_index: point_idx,
1097                        distance: dist,
1098                        data_point: *point,
1099                        pixel_position: point_pixel,
1100                    });
1101                }
1102            }
1103        }
1104
1105        best
1106    }
1107}