Skip to main content

astrelis_geometry/chart/
streaming.rs

1//! Streaming chart utilities for live/real-time data visualization.
2//!
3//! This module provides helpers for efficiently updating charts with
4//! streaming data, such as sensor readings or real-time metrics.
5//!
6//! # Features
7//!
8//! - **RingBuffer integration**: Efficient O(1) push for streaming data
9//! - **Auto-scrolling**: Automatically adjust view window for live data
10//! - **LTTB downsampling**: Preserve visual features with reduced point count
11//! - **GPU acceleration**: Optional GPU-instanced line rendering for large datasets
12//!
13//! # Example
14//!
15//! ```ignore
16//! let mut streaming = StreamingChart::new(chart);
17//!
18//! // In update loop:
19//! streaming.push_time_point(0, timestamp, temperature);
20//! streaming.auto_scroll(AxisId::X_PRIMARY, 60.0); // Show last 60 seconds
21//!
22//! // Before rendering:
23//! let result = streaming.prepare_render(&bounds);
24//! let display_data = streaming.get_display_data(0, pixel_width);
25//! ```
26//!
27//! # GPU-Accelerated Streaming
28//!
29//! For high-performance streaming with large datasets:
30//!
31//! ```ignore
32//! use astrelis_render::GraphicsContext;
33//!
34//! let context: Arc<GraphicsContext> = /* ... */;
35//! let mut gpu_streaming = GpuStreamingChart::new(chart, context, surface_format);
36//!
37//! // Push data as usual
38//! gpu_streaming.push_time_point(0, timestamp, value);
39//!
40//! // Prepare GPU buffers (only rebuilds if data changed)
41//! gpu_streaming.prepare_render(&bounds);
42//!
43//! // In render pass:
44//! gpu_streaming.render(pass, viewport, &bounds);
45//! ```
46
47use super::cache::{ChartCache, ChartDirtyFlags};
48use super::data::downsample_data;
49use super::rect::Rect;
50use super::renderers::{
51    GPU_RENDER_THRESHOLD, GpuChartAreaRenderer, GpuChartBarRenderer, GpuChartLineRenderer,
52    GpuChartScatterRenderer,
53};
54use super::types::{AxisId, Chart, ChartType, DataPoint};
55use astrelis_render::{GraphicsContext, Viewport, wgpu};
56use std::sync::Arc;
57
58#[cfg(feature = "chart-text")]
59use super::text::ChartTextRenderer;
60
61#[cfg(feature = "chart-text")]
62use astrelis_text::FontSystem;
63
64/// Per-series dirty tracking for efficient partial updates.
65#[derive(Debug, Clone, Default)]
66pub struct SeriesDirtyState {
67    /// Number of points when last rendered.
68    pub last_rendered_count: usize,
69    /// Total points ever pushed (for ring buffer tracking).
70    pub total_pushed: u64,
71    /// Whether the series needs full rebuild.
72    pub needs_full_rebuild: bool,
73    /// Range of indices that need updating.
74    pub dirty_range: Option<(usize, usize)>,
75}
76
77/// Result of prepare_render operation.
78#[derive(Debug, Clone)]
79pub struct PrepareResult {
80    /// Whether any series was updated.
81    pub updated: bool,
82    /// Per-series update info.
83    pub series_updates: Vec<SeriesUpdateInfo>,
84}
85
86/// Information about a series update.
87#[derive(Debug, Clone)]
88pub struct SeriesUpdateInfo {
89    /// Series index.
90    pub index: usize,
91    /// Whether it was a full rebuild.
92    pub full_rebuild: bool,
93    /// Number of new points (for append).
94    pub new_points: usize,
95}
96
97/// A wrapper around Chart that automatically manages caching for live updates.
98///
99/// `StreamingChart` tracks dirty state and provides efficient methods for
100/// updating chart data in real-time scenarios.
101///
102/// # Example
103///
104/// ```ignore
105/// use astrelis_geometry::chart::*;
106///
107/// // Create a streaming chart for sensor data
108/// let chart = ChartBuilder::line()
109///     .add_series("Temperature", &[])
110///     .interactive(true)
111///     .build();
112///
113/// let mut streaming = StreamingChart::new(chart);
114///
115/// // In your update loop:
116/// streaming.push_point(0, DataPoint::new(timestamp, temperature), Some(1000));
117///
118/// // Before rendering:
119/// streaming.prepare_render(&bounds);
120///
121/// // Render using cached data
122/// chart_renderer.draw(streaming.chart(), bounds);
123/// ```
124#[derive(Debug)]
125pub struct StreamingChart {
126    chart: Chart,
127    cache: ChartCache,
128    /// Per-series dirty tracking for efficient updates.
129    series_dirty: Vec<SeriesDirtyState>,
130    /// Auto-scroll configuration per axis.
131    auto_scroll_config: Vec<(AxisId, f64)>,
132}
133
134impl StreamingChart {
135    /// Create a new streaming chart wrapper.
136    pub fn new(chart: Chart) -> Self {
137        let series_count = chart.series.len();
138        Self {
139            chart,
140            cache: ChartCache::new(),
141            series_dirty: vec![SeriesDirtyState::default(); series_count],
142            auto_scroll_config: Vec::new(),
143        }
144    }
145
146    /// Get a reference to the underlying chart.
147    pub fn chart(&self) -> &Chart {
148        &self.chart
149    }
150
151    /// Get a mutable reference to the underlying chart.
152    ///
153    /// Note: Direct mutations bypass dirty tracking. Prefer using the
154    /// streaming-specific methods when possible.
155    pub fn chart_mut(&mut self) -> &mut Chart {
156        self.cache.invalidate();
157        &mut self.chart
158    }
159
160    /// Get a reference to the cache.
161    pub fn cache(&self) -> &ChartCache {
162        &self.cache
163    }
164
165    /// Get a mutable reference to the cache.
166    pub fn cache_mut(&mut self) -> &mut ChartCache {
167        &mut self.cache
168    }
169
170    /// Check if the cache needs to be rebuilt before rendering.
171    pub fn needs_rebuild(&self) -> bool {
172        self.cache.needs_rebuild()
173    }
174
175    /// Prepare the chart for rendering by rebuilding the cache if needed.
176    pub fn prepare_render(&mut self, bounds: &Rect) {
177        if self.cache.needs_rebuild() {
178            self.cache.rebuild(&self.chart, bounds);
179        }
180    }
181
182    /// Append data points to a series.
183    ///
184    /// This method tracks the change for efficient partial cache updates.
185    pub fn append_data(&mut self, series_idx: usize, points: &[DataPoint]) {
186        let old_len = self.chart.series_len(series_idx);
187        self.chart.append_data(series_idx, points);
188        let new_len = self.chart.series_len(series_idx);
189
190        if new_len > old_len {
191            self.cache.mark_data_appended(series_idx, new_len);
192        }
193    }
194
195    /// Push a single point with optional sliding window.
196    ///
197    /// If `max_points` causes data to be removed, this marks as a full
198    /// data change. Otherwise, it marks as an append for partial updates.
199    pub fn push_point(&mut self, series_idx: usize, point: DataPoint, max_points: Option<usize>) {
200        let old_len = self.chart.series_len(series_idx);
201        self.chart.push_point(series_idx, point, max_points);
202        let new_len = self.chart.series_len(series_idx);
203
204        // If data was removed (sliding window), need full rebuild
205        if new_len <= old_len && max_points.is_some() {
206            self.cache.mark_data_changed();
207        } else {
208            self.cache.mark_data_appended(series_idx, new_len);
209        }
210    }
211
212    /// Replace all data in a series.
213    pub fn set_data(&mut self, series_idx: usize, data: Vec<DataPoint>) {
214        self.chart.set_data(series_idx, data);
215        self.cache.mark_data_changed();
216    }
217
218    /// Clear all data from a series.
219    pub fn clear_data(&mut self, series_idx: usize) {
220        self.chart.clear_data(series_idx);
221        self.cache.mark_data_changed();
222    }
223
224    /// Notify that the view has changed (pan/zoom).
225    ///
226    /// Call this after modifying `chart.interactive.pan_offset` or
227    /// `chart.interactive.zoom`.
228    pub fn mark_view_changed(&mut self) {
229        self.cache.mark_view_changed();
230    }
231
232    /// Notify that style has changed.
233    pub fn mark_style_changed(&mut self) {
234        self.cache.mark_style_changed();
235    }
236
237    /// Notify that axes have changed.
238    pub fn mark_axes_changed(&mut self) {
239        self.cache.mark_axes_changed();
240    }
241
242    /// Notify that bounds have changed.
243    pub fn mark_bounds_changed(&mut self) {
244        self.cache.mark_bounds_changed();
245    }
246
247    /// Get the current dirty flags.
248    pub fn dirty_flags(&self) -> ChartDirtyFlags {
249        self.cache.dirty_flags()
250    }
251
252    /// Manually clear dirty flags (normally done by prepare_render).
253    pub fn clear_dirty(&mut self) {
254        self.cache.clear_dirty();
255    }
256
257    // =========================================================================
258    // Enhanced Streaming API
259    // =========================================================================
260
261    /// Push a time/value point to a series.
262    ///
263    /// Convenient method for time series data.
264    pub fn push_time_point(&mut self, series_idx: usize, time: f64, value: f64) {
265        self.push_point(series_idx, DataPoint::new(time, value), None);
266    }
267
268    /// Push a time/value point with sliding window.
269    pub fn push_time_point_windowed(
270        &mut self,
271        series_idx: usize,
272        time: f64,
273        value: f64,
274        max_points: usize,
275    ) {
276        self.push_point(series_idx, DataPoint::new(time, value), Some(max_points));
277    }
278
279    /// Configure auto-scrolling for an axis.
280    ///
281    /// When enabled, the axis range will automatically shift to show
282    /// the most recent `window_size` units of data.
283    ///
284    /// # Example
285    ///
286    /// ```ignore
287    /// // Show the last 60 seconds of data
288    /// streaming.auto_scroll(AxisId::X_PRIMARY, 60.0);
289    /// ```
290    pub fn auto_scroll(&mut self, axis_id: AxisId, window_size: f64) {
291        // Remove existing config for this axis
292        self.auto_scroll_config.retain(|(id, _)| *id != axis_id);
293        self.auto_scroll_config.push((axis_id, window_size));
294    }
295
296    /// Disable auto-scrolling for an axis.
297    pub fn disable_auto_scroll(&mut self, axis_id: AxisId) {
298        self.auto_scroll_config.retain(|(id, _)| *id != axis_id);
299    }
300
301    /// Apply auto-scroll to update axis ranges.
302    ///
303    /// Call this after pushing data to adjust the view window.
304    pub fn apply_auto_scroll(&mut self) {
305        for (axis_id, window_size) in &self.auto_scroll_config {
306            // Find the maximum data value for this axis
307            let max_value = self.find_max_for_axis(*axis_id);
308
309            if let Some(max) = max_value {
310                // Update the axis range to show a window ending at max
311                if let Some(axis) = self.chart.get_axis_mut(*axis_id) {
312                    axis.min = Some(max - window_size);
313                    axis.max = Some(max);
314                }
315                self.cache.mark_view_changed();
316            }
317        }
318    }
319
320    /// Find the maximum data value for an axis across all series.
321    fn find_max_for_axis(&self, axis_id: AxisId) -> Option<f64> {
322        let mut max = None;
323
324        for series in &self.chart.series {
325            // Check if this series uses this axis
326            let uses_axis = series.x_axis == axis_id || series.y_axis == axis_id;
327            if !uses_axis {
328                continue;
329            }
330
331            if let Some(last_point) = series.data.last() {
332                let value = if series.x_axis == axis_id {
333                    last_point.x
334                } else {
335                    last_point.y
336                };
337
338                max = Some(max.map_or(value, |m: f64| m.max(value)));
339            }
340        }
341
342        max
343    }
344
345    /// Prepare for rendering and return detailed update information.
346    pub fn prepare_render_with_result(&mut self, bounds: &Rect) -> PrepareResult {
347        // Ensure series_dirty has correct size
348        while self.series_dirty.len() < self.chart.series.len() {
349            self.series_dirty.push(SeriesDirtyState::default());
350        }
351
352        // Apply auto-scroll first
353        self.apply_auto_scroll();
354
355        let updated = self.cache.needs_rebuild();
356
357        // Collect update info
358        let series_updates: Vec<SeriesUpdateInfo> = self
359            .chart
360            .series
361            .iter()
362            .enumerate()
363            .filter_map(|(idx, series)| {
364                let dirty_state = &self.series_dirty[idx];
365                if series.data.len() != dirty_state.last_rendered_count {
366                    Some(SeriesUpdateInfo {
367                        index: idx,
368                        full_rebuild: dirty_state.needs_full_rebuild,
369                        new_points: series
370                            .data
371                            .len()
372                            .saturating_sub(dirty_state.last_rendered_count),
373                    })
374                } else {
375                    None
376                }
377            })
378            .collect();
379
380        // Rebuild cache if needed
381        if self.cache.needs_rebuild() {
382            self.cache.rebuild(&self.chart, bounds);
383        }
384
385        // Update dirty state
386        for (idx, series) in self.chart.series.iter().enumerate() {
387            if idx < self.series_dirty.len() {
388                self.series_dirty[idx].last_rendered_count = series.data.len();
389                self.series_dirty[idx].needs_full_rebuild = false;
390                self.series_dirty[idx].dirty_range = None;
391            }
392        }
393
394        PrepareResult {
395            updated,
396            series_updates,
397        }
398    }
399
400    /// Get display data for a series, downsampled if necessary.
401    ///
402    /// Uses LTTB (Largest Triangle Three Buckets) algorithm to preserve
403    /// visual features while reducing point count for rendering.
404    ///
405    /// # Arguments
406    ///
407    /// * `series_idx` - Index of the series
408    /// * `pixel_width` - Width of the display area in pixels
409    ///
410    /// # Returns
411    ///
412    /// Downsampled data points optimized for the given pixel width.
413    /// If the data has fewer points than pixels, returns all points.
414    pub fn get_display_data(&self, series_idx: usize, pixel_width: f32) -> Vec<DataPoint> {
415        let Some(series) = self.chart.series.get(series_idx) else {
416            return Vec::new();
417        };
418
419        let target_points = (pixel_width * 2.0) as usize; // 2 points per pixel max
420
421        if series.data.len() <= target_points {
422            series.data.clone()
423        } else {
424            downsample_data(&series.data, target_points)
425        }
426    }
427
428    /// Get downsampled data for all series.
429    pub fn get_all_display_data(&self, pixel_width: f32) -> Vec<Vec<DataPoint>> {
430        (0..self.chart.series.len())
431            .map(|idx| self.get_display_data(idx, pixel_width))
432            .collect()
433    }
434
435    /// Get statistics about the streaming data.
436    pub fn statistics(&self) -> StreamingStatistics {
437        let total_points: usize = self.chart.series.iter().map(|s| s.data.len()).sum();
438        let series_counts: Vec<usize> = self.chart.series.iter().map(|s| s.data.len()).collect();
439
440        StreamingStatistics {
441            total_points,
442            series_counts,
443            cache_dirty: self.cache.needs_rebuild(),
444            auto_scroll_active: !self.auto_scroll_config.is_empty(),
445        }
446    }
447}
448
449/// Statistics about streaming chart data.
450#[derive(Debug, Clone)]
451pub struct StreamingStatistics {
452    /// Total data points across all series.
453    pub total_points: usize,
454    /// Data points per series.
455    pub series_counts: Vec<usize>,
456    /// Whether cache needs rebuild.
457    pub cache_dirty: bool,
458    /// Whether auto-scroll is active.
459    pub auto_scroll_active: bool,
460}
461
462impl From<Chart> for StreamingChart {
463    fn from(chart: Chart) -> Self {
464        Self::new(chart)
465    }
466}
467
468impl std::ops::Deref for StreamingChart {
469    type Target = Chart;
470
471    fn deref(&self) -> &Self::Target {
472        &self.chart
473    }
474}
475
476/// Configuration for a sliding window chart.
477#[derive(Debug, Clone, Copy)]
478pub struct SlidingWindowConfig {
479    /// Maximum number of points to keep per series.
480    pub max_points: usize,
481    /// Whether to auto-scale axes based on visible data.
482    pub auto_scale: bool,
483}
484
485impl Default for SlidingWindowConfig {
486    fn default() -> Self {
487        Self {
488            max_points: 1000,
489            auto_scale: true,
490        }
491    }
492}
493
494impl SlidingWindowConfig {
495    /// Create a new config with the specified max points.
496    pub fn new(max_points: usize) -> Self {
497        Self {
498            max_points,
499            auto_scale: true,
500        }
501    }
502
503    /// Set whether to auto-scale axes.
504    pub fn with_auto_scale(mut self, auto_scale: bool) -> Self {
505        self.auto_scale = auto_scale;
506        self
507    }
508}
509
510// =============================================================================
511// GPU-Accelerated Streaming Chart
512// =============================================================================
513
514/// GPU-accelerated streaming chart for high-performance live data visualization.
515///
516/// Combines `StreamingChart` functionality with GPU-accelerated renderers for
517/// efficient rendering of large datasets. The GPU path is automatically used
518/// when series exceed `GPU_RENDER_THRESHOLD` points.
519///
520/// Supports all chart types:
521/// - **Line**: GPU-instanced line segments via `GpuChartLineRenderer`
522/// - **Scatter**: GPU-instanced points via `GpuChartScatterRenderer`
523/// - **Bar**: GPU-instanced quads via `GpuChartBarRenderer`
524/// - **Area**: GPU-instanced fill + lines via `GpuChartAreaRenderer`
525///
526/// # Performance Characteristics
527///
528/// | Operation | CPU Path | GPU Path |
529/// |-----------|----------|----------|
530/// | Data append | O(n) tessellation | O(n) buffer upload (once) |
531/// | Pan/zoom | O(n) tessellation | O(1) uniform update |
532/// | Render | O(n) triangles | O(n) instances |
533///
534/// # Text Rendering (with `chart-text` feature)
535///
536/// When the `chart-text` feature is enabled, use `with_text()` to add text rendering:
537///
538/// ```ignore
539/// let font_system = FontSystem::with_system_fonts();
540/// let gpu_chart = GpuStreamingChart::new(chart, context.clone(), surface_format)
541///     .with_text(context.clone(), font_system);
542/// ```
543///
544/// # Example
545///
546/// ```ignore
547/// use astrelis_geometry::chart::*;
548/// use astrelis_render::GraphicsContext;
549/// use std::sync::Arc;
550///
551/// let context: Arc<GraphicsContext> = /* ... */;
552/// let surface_format = window.surface_format();
553/// let chart = ChartBuilder::line()
554///     .add_series("Sensor", &[])
555///     .interactive(true)
556///     .build();
557///
558/// let mut gpu_chart = GpuStreamingChart::new(chart, context, surface_format);
559///
560/// // In update loop:
561/// gpu_chart.push_time_point(0, time, value);
562/// gpu_chart.auto_scroll(AxisId::X_PRIMARY, 60.0);
563///
564/// // Before rendering:
565/// let bounds = Rect::new(0.0, 0.0, 800.0, 600.0);
566/// gpu_chart.prepare_render(&bounds);
567///
568/// // In render pass:
569/// gpu_chart.render(&mut pass, viewport, &bounds);
570/// ```
571pub struct GpuStreamingChart {
572    /// Underlying streaming chart.
573    streaming: StreamingChart,
574    /// GPU line renderer for accelerated line chart rendering.
575    line_renderer: GpuChartLineRenderer,
576    /// GPU scatter renderer for accelerated scatter chart rendering.
577    scatter_renderer: GpuChartScatterRenderer,
578    /// GPU bar renderer for accelerated bar chart rendering.
579    bar_renderer: GpuChartBarRenderer,
580    /// GPU area renderer for accelerated area chart rendering.
581    area_renderer: GpuChartAreaRenderer,
582    /// Whether GPU rendering is enabled (auto-detected based on data size).
583    gpu_enabled: bool,
584    /// Force GPU rendering regardless of data size.
585    force_gpu: bool,
586    /// Optional text renderer for titles, labels, and legends.
587    #[cfg(feature = "chart-text")]
588    text_renderer: Option<ChartTextRenderer>,
589}
590
591impl std::fmt::Debug for GpuStreamingChart {
592    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593        let mut s = f.debug_struct("GpuStreamingChart");
594        s.field("streaming", &self.streaming)
595            .field("line_renderer", &self.line_renderer)
596            .field("scatter_renderer", &self.scatter_renderer)
597            .field("bar_renderer", &self.bar_renderer)
598            .field("area_renderer", &self.area_renderer)
599            .field("gpu_enabled", &self.gpu_enabled)
600            .field("force_gpu", &self.force_gpu);
601        #[cfg(feature = "chart-text")]
602        s.field("has_text_renderer", &self.text_renderer.is_some());
603        s.finish()
604    }
605}
606
607impl GpuStreamingChart {
608    /// Create a new GPU-accelerated streaming chart.
609    ///
610    /// The `target_format` must match the render target this chart will draw into.
611    pub fn new(
612        chart: Chart,
613        context: Arc<GraphicsContext>,
614        target_format: wgpu::TextureFormat,
615    ) -> Self {
616        Self {
617            streaming: StreamingChart::new(chart),
618            line_renderer: GpuChartLineRenderer::new(context.clone(), target_format),
619            scatter_renderer: GpuChartScatterRenderer::new(context.clone(), target_format),
620            bar_renderer: GpuChartBarRenderer::new(context.clone(), target_format),
621            area_renderer: GpuChartAreaRenderer::new(context, target_format),
622            gpu_enabled: false,
623            force_gpu: false,
624            #[cfg(feature = "chart-text")]
625            text_renderer: None,
626        }
627    }
628
629    /// Create from an existing streaming chart.
630    ///
631    /// The `target_format` must match the render target this chart will draw into.
632    pub fn from_streaming(
633        streaming: StreamingChart,
634        context: Arc<GraphicsContext>,
635        target_format: wgpu::TextureFormat,
636    ) -> Self {
637        Self {
638            streaming,
639            line_renderer: GpuChartLineRenderer::new(context.clone(), target_format),
640            scatter_renderer: GpuChartScatterRenderer::new(context.clone(), target_format),
641            bar_renderer: GpuChartBarRenderer::new(context.clone(), target_format),
642            area_renderer: GpuChartAreaRenderer::new(context, target_format),
643            gpu_enabled: false,
644            force_gpu: false,
645            #[cfg(feature = "chart-text")]
646            text_renderer: None,
647        }
648    }
649
650    /// Enable text rendering for titles, labels, and legends.
651    ///
652    /// Requires the `chart-text` feature.
653    ///
654    /// # Example
655    ///
656    /// ```ignore
657    /// use astrelis_text::FontSystem;
658    ///
659    /// let font_system = FontSystem::with_system_fonts();
660    /// let gpu_chart = GpuStreamingChart::new(chart, context.clone(), surface_format)
661    ///     .with_text(context.clone(), font_system);
662    /// ```
663    #[cfg(feature = "chart-text")]
664    pub fn with_text(mut self, context: Arc<GraphicsContext>, font_system: FontSystem) -> Self {
665        self.text_renderer = Some(ChartTextRenderer::new(context, font_system));
666        self
667    }
668
669    /// Get a reference to the text renderer if enabled.
670    #[cfg(feature = "chart-text")]
671    pub fn text_renderer(&self) -> Option<&ChartTextRenderer> {
672        self.text_renderer.as_ref()
673    }
674
675    /// Get a mutable reference to the text renderer if enabled.
676    #[cfg(feature = "chart-text")]
677    pub fn text_renderer_mut(&mut self) -> Option<&mut ChartTextRenderer> {
678        self.text_renderer.as_mut()
679    }
680
681    /// Force GPU rendering regardless of data size.
682    ///
683    /// Useful for testing or when you know data will grow large.
684    pub fn force_gpu_rendering(mut self, force: bool) -> Self {
685        self.force_gpu = force;
686        self
687    }
688
689    /// Get a reference to the underlying chart.
690    pub fn chart(&self) -> &Chart {
691        self.streaming.chart()
692    }
693
694    /// Get a mutable reference to the underlying chart.
695    pub fn chart_mut(&mut self) -> &mut Chart {
696        self.streaming.chart_mut()
697    }
698
699    /// Get a reference to the underlying streaming chart.
700    pub fn streaming(&self) -> &StreamingChart {
701        &self.streaming
702    }
703
704    /// Get a mutable reference to the underlying streaming chart.
705    pub fn streaming_mut(&mut self) -> &mut StreamingChart {
706        &mut self.streaming
707    }
708
709    /// Get a reference to the GPU line renderer.
710    pub fn line_renderer(&self) -> &GpuChartLineRenderer {
711        &self.line_renderer
712    }
713
714    /// Get a mutable reference to the GPU line renderer.
715    pub fn line_renderer_mut(&mut self) -> &mut GpuChartLineRenderer {
716        &mut self.line_renderer
717    }
718
719    /// Get a reference to the GPU scatter renderer.
720    pub fn scatter_renderer(&self) -> &GpuChartScatterRenderer {
721        &self.scatter_renderer
722    }
723
724    /// Get a reference to the GPU bar renderer.
725    pub fn bar_renderer(&self) -> &GpuChartBarRenderer {
726        &self.bar_renderer
727    }
728
729    /// Get a reference to the GPU area renderer.
730    pub fn area_renderer(&self) -> &GpuChartAreaRenderer {
731        &self.area_renderer
732    }
733
734    /// Check if GPU rendering is currently enabled.
735    pub fn is_gpu_enabled(&self) -> bool {
736        self.gpu_enabled
737    }
738
739    // =========================================================================
740    // Data Operations (delegated to StreamingChart)
741    // =========================================================================
742
743    /// Append data points to a series.
744    pub fn append_data(&mut self, series_idx: usize, points: &[DataPoint]) {
745        self.streaming.append_data(series_idx, points);
746        self.mark_all_renderers_data_changed();
747    }
748
749    /// Push a single point with optional sliding window.
750    pub fn push_point(&mut self, series_idx: usize, point: DataPoint, max_points: Option<usize>) {
751        self.streaming.push_point(series_idx, point, max_points);
752        self.mark_all_renderers_data_changed();
753    }
754
755    /// Replace all data in a series.
756    pub fn set_data(&mut self, series_idx: usize, data: Vec<DataPoint>) {
757        self.streaming.set_data(series_idx, data);
758        self.mark_all_renderers_data_changed();
759    }
760
761    /// Clear all data from a series.
762    pub fn clear_data(&mut self, series_idx: usize) {
763        self.streaming.clear_data(series_idx);
764        self.mark_all_renderers_data_changed();
765    }
766
767    /// Push a time/value point to a series.
768    pub fn push_time_point(&mut self, series_idx: usize, time: f64, value: f64) {
769        self.streaming.push_time_point(series_idx, time, value);
770        self.mark_all_renderers_data_changed();
771    }
772
773    /// Push a time/value point with sliding window.
774    pub fn push_time_point_windowed(
775        &mut self,
776        series_idx: usize,
777        time: f64,
778        value: f64,
779        max_points: usize,
780    ) {
781        self.streaming
782            .push_time_point_windowed(series_idx, time, value, max_points);
783        self.mark_all_renderers_data_changed();
784    }
785
786    /// Mark all GPU renderers as needing data update.
787    fn mark_all_renderers_data_changed(&mut self) {
788        self.line_renderer.mark_data_changed();
789        self.scatter_renderer.mark_data_changed();
790        self.bar_renderer.mark_data_changed();
791        self.area_renderer.mark_data_changed();
792    }
793
794    /// Configure auto-scrolling for an axis.
795    pub fn auto_scroll(&mut self, axis_id: AxisId, window_size: f64) {
796        self.streaming.auto_scroll(axis_id, window_size);
797    }
798
799    /// Disable auto-scrolling for an axis.
800    pub fn disable_auto_scroll(&mut self, axis_id: AxisId) {
801        self.streaming.disable_auto_scroll(axis_id);
802    }
803
804    /// Notify that the view has changed (pan/zoom).
805    pub fn mark_view_changed(&mut self) {
806        self.streaming.mark_view_changed();
807    }
808
809    /// Notify that style has changed.
810    pub fn mark_style_changed(&mut self) {
811        self.streaming.mark_style_changed();
812    }
813
814    /// Notify that bounds have changed (widget resized).
815    pub fn mark_bounds_changed(&mut self) {
816        self.streaming.mark_bounds_changed();
817    }
818
819    /// Get the current dirty flags.
820    pub fn dirty_flags(&self) -> ChartDirtyFlags {
821        self.streaming.dirty_flags()
822    }
823
824    // =========================================================================
825    // Rendering
826    // =========================================================================
827
828    /// Check if GPU rendering should be used based on current data size.
829    fn should_use_gpu(&self) -> bool {
830        if self.force_gpu {
831            return true;
832        }
833
834        // Check if any series exceeds threshold
835        self.streaming
836            .chart()
837            .series
838            .iter()
839            .any(|s| s.data.len() > GPU_RENDER_THRESHOLD)
840    }
841
842    /// Prepare the chart for rendering.
843    ///
844    /// This prepares both the coordinate cache and GPU buffers as needed.
845    /// Call this once per frame before rendering.
846    pub fn prepare_render(&mut self, bounds: &Rect) {
847        // Prepare coordinate cache
848        self.streaming.prepare_render(bounds);
849
850        // Check if we should enable GPU rendering
851        self.gpu_enabled = self.should_use_gpu();
852
853        // Prepare the appropriate GPU renderer based on chart type
854        if self.gpu_enabled {
855            self.prepare_gpu_renderer();
856        }
857    }
858
859    /// Prepare with detailed result information.
860    pub fn prepare_render_with_result(&mut self, bounds: &Rect) -> PrepareResult {
861        let result = self.streaming.prepare_render_with_result(bounds);
862
863        // Check if we should enable GPU rendering
864        self.gpu_enabled = self.should_use_gpu();
865
866        // Prepare the appropriate GPU renderer based on chart type
867        if self.gpu_enabled {
868            self.prepare_gpu_renderer();
869        }
870
871        result
872    }
873
874    /// Prepare the appropriate GPU renderer based on chart type.
875    fn prepare_gpu_renderer(&mut self) {
876        let chart = self.streaming.chart();
877        match chart.chart_type {
878            ChartType::Line => {
879                self.line_renderer.prepare(chart);
880            }
881            ChartType::Scatter => {
882                self.scatter_renderer.prepare(chart);
883            }
884            ChartType::Bar => {
885                self.bar_renderer.prepare(chart);
886            }
887            ChartType::Area => {
888                self.area_renderer.prepare(chart);
889            }
890        }
891    }
892
893    /// Render the chart to a render pass.
894    ///
895    /// This uses GPU-accelerated rendering for large datasets and
896    /// falls back to tessellation for small datasets.
897    ///
898    /// # Arguments
899    ///
900    /// * `pass` - The render pass to draw into
901    /// * `viewport` - The viewport for rendering
902    /// * `geometry_renderer` - The geometry renderer for non-GPU elements
903    /// * `bounds` - The chart bounds
904    pub fn render(
905        &self,
906        pass: &mut wgpu::RenderPass,
907        viewport: Viewport,
908        geometry_renderer: &mut crate::GeometryRenderer,
909        bounds: &Rect,
910    ) {
911        use crate::chart::ChartRenderer;
912
913        // Render non-GPU elements via geometry renderer
914        let plot_area = bounds.inset(self.streaming.chart().padding);
915        let chart = self.streaming.chart();
916
917        {
918            let mut chart_renderer = ChartRenderer::new(geometry_renderer);
919            if self.gpu_enabled {
920                // Draw everything except the data series (which will be GPU rendered)
921                chart_renderer.draw_with_gpu_lines(chart, *bounds);
922            } else {
923                // Draw everything via tessellation
924                chart_renderer.draw(chart, *bounds);
925            }
926            chart_renderer.render(pass, viewport);
927        }
928
929        // Render GPU data series if enabled
930        if self.gpu_enabled {
931            self.render_gpu_series(pass, viewport, &plot_area, chart);
932        }
933    }
934
935    /// Render the chart with text labels to a render pass.
936    ///
937    /// This is an enhanced version of `render()` that also draws text elements
938    /// when the `chart-text` feature is enabled and a text renderer is configured.
939    ///
940    /// # Arguments
941    ///
942    /// * `pass` - The render pass to draw into
943    /// * `viewport` - The viewport for rendering
944    /// * `geometry_renderer` - The geometry renderer for non-GPU elements
945    /// * `bounds` - The chart bounds
946    #[cfg(feature = "chart-text")]
947    pub fn render_with_text(
948        &mut self,
949        pass: &mut wgpu::RenderPass,
950        viewport: Viewport,
951        geometry_renderer: &mut crate::GeometryRenderer,
952        bounds: &Rect,
953    ) {
954        use crate::chart::ChartRenderer;
955
956        let chart = self.streaming.chart();
957        let plot_area = bounds.inset(chart.padding);
958
959        // Calculate text margins if text renderer is available
960        let text_margins = self
961            .text_renderer
962            .as_ref()
963            .map(|tr| tr.calculate_margins(chart))
964            .unwrap_or_default();
965
966        // Adjust plot area for text margins
967        let adjusted_plot_area = Rect::new(
968            plot_area.x + text_margins.left,
969            plot_area.y + text_margins.top,
970            (plot_area.width - text_margins.left - text_margins.right).max(1.0),
971            (plot_area.height - text_margins.top - text_margins.bottom).max(1.0),
972        );
973
974        // Render chart geometry
975        {
976            let mut chart_renderer = ChartRenderer::new(geometry_renderer);
977            if self.gpu_enabled {
978                chart_renderer.draw_with_gpu_lines(chart, *bounds);
979            } else {
980                chart_renderer.draw(chart, *bounds);
981            }
982            chart_renderer.render(pass, viewport);
983        }
984
985        // Render GPU data series if enabled
986        if self.gpu_enabled {
987            self.render_gpu_series(pass, viewport, &adjusted_plot_area, chart);
988        }
989
990        // Render text elements
991        if let Some(text_renderer) = &mut self.text_renderer {
992            text_renderer.set_viewport(viewport);
993
994            // Draw title
995            text_renderer.draw_title(chart, bounds);
996
997            // Draw tick labels
998            text_renderer.draw_tick_labels(chart, &adjusted_plot_area);
999
1000            // Draw axis labels
1001            text_renderer.draw_axis_labels(chart, &adjusted_plot_area);
1002
1003            // Draw legend
1004            text_renderer.draw_legend(chart, &adjusted_plot_area, geometry_renderer);
1005
1006            // Render geometry for legend background
1007            geometry_renderer.render(pass, viewport);
1008
1009            // Render all text
1010            text_renderer.render(pass);
1011        }
1012    }
1013
1014    /// Render data series using the appropriate GPU renderer.
1015    fn render_gpu_series(
1016        &self,
1017        pass: &mut wgpu::RenderPass,
1018        viewport: Viewport,
1019        plot_area: &Rect,
1020        chart: &Chart,
1021    ) {
1022        match chart.chart_type {
1023            ChartType::Line => {
1024                if self.line_renderer.segment_count() > 0 {
1025                    self.line_renderer.render(pass, viewport, plot_area, chart);
1026                }
1027            }
1028            ChartType::Scatter => {
1029                if self.scatter_renderer.point_count() > 0 {
1030                    self.scatter_renderer
1031                        .render(pass, viewport, plot_area, chart);
1032                }
1033            }
1034            ChartType::Bar => {
1035                if self.bar_renderer.quad_count() > 0 {
1036                    self.bar_renderer.render(pass, viewport, plot_area, chart);
1037                }
1038            }
1039            ChartType::Area => {
1040                if self.area_renderer.quad_count() > 0 || self.area_renderer.segment_count() > 0 {
1041                    self.area_renderer.render(pass, viewport, plot_area, chart);
1042                }
1043            }
1044        }
1045    }
1046
1047    /// Get statistics about the streaming data.
1048    pub fn statistics(&self) -> GpuStreamingStatistics {
1049        let base = self.streaming.statistics();
1050        let chart = self.streaming.chart();
1051
1052        // Get GPU element count based on chart type
1053        let gpu_element_count = match chart.chart_type {
1054            ChartType::Line => self.line_renderer.segment_count(),
1055            ChartType::Scatter => self.scatter_renderer.point_count(),
1056            ChartType::Bar => self.bar_renderer.quad_count(),
1057            ChartType::Area => self.area_renderer.quad_count() + self.area_renderer.segment_count(),
1058        };
1059
1060        GpuStreamingStatistics {
1061            total_points: base.total_points,
1062            series_counts: base.series_counts,
1063            cache_dirty: base.cache_dirty,
1064            auto_scroll_active: base.auto_scroll_active,
1065            gpu_enabled: self.gpu_enabled,
1066            gpu_segment_count: gpu_element_count,
1067        }
1068    }
1069
1070    /// Get downsampled data for display (for external use like tooltips).
1071    pub fn get_display_data(&self, series_idx: usize, pixel_width: f32) -> Vec<DataPoint> {
1072        self.streaming.get_display_data(series_idx, pixel_width)
1073    }
1074}
1075
1076impl std::ops::Deref for GpuStreamingChart {
1077    type Target = Chart;
1078
1079    fn deref(&self) -> &Self::Target {
1080        self.streaming.chart()
1081    }
1082}
1083
1084impl From<(Chart, Arc<GraphicsContext>, wgpu::TextureFormat)> for GpuStreamingChart {
1085    fn from(
1086        (chart, context, target_format): (Chart, Arc<GraphicsContext>, wgpu::TextureFormat),
1087    ) -> Self {
1088        Self::new(chart, context, target_format)
1089    }
1090}
1091
1092/// Statistics about GPU streaming chart data.
1093#[derive(Debug, Clone)]
1094pub struct GpuStreamingStatistics {
1095    /// Total data points across all series.
1096    pub total_points: usize,
1097    /// Data points per series.
1098    pub series_counts: Vec<usize>,
1099    /// Whether cache needs rebuild.
1100    pub cache_dirty: bool,
1101    /// Whether auto-scroll is active.
1102    pub auto_scroll_active: bool,
1103    /// Whether GPU rendering is enabled.
1104    pub gpu_enabled: bool,
1105    /// Number of GPU line segments.
1106    pub gpu_segment_count: usize,
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111    use super::*;
1112    use crate::chart::ChartBuilder;
1113
1114    #[test]
1115    fn test_streaming_chart_append() {
1116        let chart = ChartBuilder::line()
1117            .add_series("Test", &[(0.0_f64, 1.0_f64)])
1118            .build();
1119
1120        let mut streaming = StreamingChart::new(chart);
1121
1122        assert_eq!(streaming.chart.series_len(0), 1);
1123
1124        streaming.append_data(0, &[DataPoint::new(1.0, 2.0), DataPoint::new(2.0, 3.0)]);
1125
1126        assert_eq!(streaming.chart.series_len(0), 3);
1127        assert!(
1128            streaming
1129                .cache
1130                .dirty_flags()
1131                .contains(ChartDirtyFlags::DATA_APPENDED)
1132        );
1133    }
1134
1135    #[test]
1136    fn test_streaming_chart_sliding_window() {
1137        let chart = ChartBuilder::line()
1138            .add_series("Test", &[] as &[(f64, f64)])
1139            .build();
1140
1141        let mut streaming = StreamingChart::new(chart);
1142
1143        // Add points up to the limit
1144        for i in 0..5 {
1145            streaming.push_point(0, DataPoint::new(i as f64, i as f64), Some(3));
1146        }
1147
1148        // Should only have last 3 points
1149        assert_eq!(streaming.chart.series_len(0), 3);
1150        // Sliding window should trigger full data change
1151        assert!(
1152            streaming
1153                .cache
1154                .dirty_flags()
1155                .contains(ChartDirtyFlags::DATA_CHANGED)
1156        );
1157    }
1158}