use super::cache::{ChartCache, ChartDirtyFlags};
use super::data::downsample_data;
use super::rect::Rect;
use super::renderers::{
GPU_RENDER_THRESHOLD, GpuChartAreaRenderer, GpuChartBarRenderer, GpuChartLineRenderer,
GpuChartScatterRenderer,
};
use super::types::{AxisId, Chart, ChartType, DataPoint};
use astrelis_render::{GraphicsContext, Viewport, wgpu};
use std::sync::Arc;
#[cfg(feature = "chart-text")]
use super::text::ChartTextRenderer;
#[cfg(feature = "chart-text")]
use astrelis_text::FontSystem;
#[derive(Debug, Clone, Default)]
pub struct SeriesDirtyState {
pub last_rendered_count: usize,
pub total_pushed: u64,
pub needs_full_rebuild: bool,
pub dirty_range: Option<(usize, usize)>,
}
#[derive(Debug, Clone)]
pub struct PrepareResult {
pub updated: bool,
pub series_updates: Vec<SeriesUpdateInfo>,
}
#[derive(Debug, Clone)]
pub struct SeriesUpdateInfo {
pub index: usize,
pub full_rebuild: bool,
pub new_points: usize,
}
#[derive(Debug)]
pub struct StreamingChart {
chart: Chart,
cache: ChartCache,
series_dirty: Vec<SeriesDirtyState>,
auto_scroll_config: Vec<(AxisId, f64)>,
}
impl StreamingChart {
pub fn new(chart: Chart) -> Self {
let series_count = chart.series.len();
Self {
chart,
cache: ChartCache::new(),
series_dirty: vec![SeriesDirtyState::default(); series_count],
auto_scroll_config: Vec::new(),
}
}
pub fn chart(&self) -> &Chart {
&self.chart
}
pub fn chart_mut(&mut self) -> &mut Chart {
self.cache.invalidate();
&mut self.chart
}
pub fn cache(&self) -> &ChartCache {
&self.cache
}
pub fn cache_mut(&mut self) -> &mut ChartCache {
&mut self.cache
}
pub fn needs_rebuild(&self) -> bool {
self.cache.needs_rebuild()
}
pub fn prepare_render(&mut self, bounds: &Rect) {
if self.cache.needs_rebuild() {
self.cache.rebuild(&self.chart, bounds);
}
}
pub fn append_data(&mut self, series_idx: usize, points: &[DataPoint]) {
let old_len = self.chart.series_len(series_idx);
self.chart.append_data(series_idx, points);
let new_len = self.chart.series_len(series_idx);
if new_len > old_len {
self.cache.mark_data_appended(series_idx, new_len);
}
}
pub fn push_point(&mut self, series_idx: usize, point: DataPoint, max_points: Option<usize>) {
let old_len = self.chart.series_len(series_idx);
self.chart.push_point(series_idx, point, max_points);
let new_len = self.chart.series_len(series_idx);
if new_len <= old_len && max_points.is_some() {
self.cache.mark_data_changed();
} else {
self.cache.mark_data_appended(series_idx, new_len);
}
}
pub fn set_data(&mut self, series_idx: usize, data: Vec<DataPoint>) {
self.chart.set_data(series_idx, data);
self.cache.mark_data_changed();
}
pub fn clear_data(&mut self, series_idx: usize) {
self.chart.clear_data(series_idx);
self.cache.mark_data_changed();
}
pub fn mark_view_changed(&mut self) {
self.cache.mark_view_changed();
}
pub fn mark_style_changed(&mut self) {
self.cache.mark_style_changed();
}
pub fn mark_axes_changed(&mut self) {
self.cache.mark_axes_changed();
}
pub fn mark_bounds_changed(&mut self) {
self.cache.mark_bounds_changed();
}
pub fn dirty_flags(&self) -> ChartDirtyFlags {
self.cache.dirty_flags()
}
pub fn clear_dirty(&mut self) {
self.cache.clear_dirty();
}
pub fn push_time_point(&mut self, series_idx: usize, time: f64, value: f64) {
self.push_point(series_idx, DataPoint::new(time, value), None);
}
pub fn push_time_point_windowed(
&mut self,
series_idx: usize,
time: f64,
value: f64,
max_points: usize,
) {
self.push_point(series_idx, DataPoint::new(time, value), Some(max_points));
}
pub fn auto_scroll(&mut self, axis_id: AxisId, window_size: f64) {
self.auto_scroll_config.retain(|(id, _)| *id != axis_id);
self.auto_scroll_config.push((axis_id, window_size));
}
pub fn disable_auto_scroll(&mut self, axis_id: AxisId) {
self.auto_scroll_config.retain(|(id, _)| *id != axis_id);
}
pub fn apply_auto_scroll(&mut self) {
for (axis_id, window_size) in &self.auto_scroll_config {
let max_value = self.find_max_for_axis(*axis_id);
if let Some(max) = max_value {
if let Some(axis) = self.chart.get_axis_mut(*axis_id) {
axis.min = Some(max - window_size);
axis.max = Some(max);
}
self.cache.mark_view_changed();
}
}
}
fn find_max_for_axis(&self, axis_id: AxisId) -> Option<f64> {
let mut max = None;
for series in &self.chart.series {
let uses_axis = series.x_axis == axis_id || series.y_axis == axis_id;
if !uses_axis {
continue;
}
if let Some(last_point) = series.data.last() {
let value = if series.x_axis == axis_id {
last_point.x
} else {
last_point.y
};
max = Some(max.map_or(value, |m: f64| m.max(value)));
}
}
max
}
pub fn prepare_render_with_result(&mut self, bounds: &Rect) -> PrepareResult {
while self.series_dirty.len() < self.chart.series.len() {
self.series_dirty.push(SeriesDirtyState::default());
}
self.apply_auto_scroll();
let updated = self.cache.needs_rebuild();
let series_updates: Vec<SeriesUpdateInfo> = self
.chart
.series
.iter()
.enumerate()
.filter_map(|(idx, series)| {
let dirty_state = &self.series_dirty[idx];
if series.data.len() != dirty_state.last_rendered_count {
Some(SeriesUpdateInfo {
index: idx,
full_rebuild: dirty_state.needs_full_rebuild,
new_points: series
.data
.len()
.saturating_sub(dirty_state.last_rendered_count),
})
} else {
None
}
})
.collect();
if self.cache.needs_rebuild() {
self.cache.rebuild(&self.chart, bounds);
}
for (idx, series) in self.chart.series.iter().enumerate() {
if idx < self.series_dirty.len() {
self.series_dirty[idx].last_rendered_count = series.data.len();
self.series_dirty[idx].needs_full_rebuild = false;
self.series_dirty[idx].dirty_range = None;
}
}
PrepareResult {
updated,
series_updates,
}
}
pub fn get_display_data(&self, series_idx: usize, pixel_width: f32) -> Vec<DataPoint> {
let Some(series) = self.chart.series.get(series_idx) else {
return Vec::new();
};
let target_points = (pixel_width * 2.0) as usize;
if series.data.len() <= target_points {
series.data.clone()
} else {
downsample_data(&series.data, target_points)
}
}
pub fn get_all_display_data(&self, pixel_width: f32) -> Vec<Vec<DataPoint>> {
(0..self.chart.series.len())
.map(|idx| self.get_display_data(idx, pixel_width))
.collect()
}
pub fn statistics(&self) -> StreamingStatistics {
let total_points: usize = self.chart.series.iter().map(|s| s.data.len()).sum();
let series_counts: Vec<usize> = self.chart.series.iter().map(|s| s.data.len()).collect();
StreamingStatistics {
total_points,
series_counts,
cache_dirty: self.cache.needs_rebuild(),
auto_scroll_active: !self.auto_scroll_config.is_empty(),
}
}
}
#[derive(Debug, Clone)]
pub struct StreamingStatistics {
pub total_points: usize,
pub series_counts: Vec<usize>,
pub cache_dirty: bool,
pub auto_scroll_active: bool,
}
impl From<Chart> for StreamingChart {
fn from(chart: Chart) -> Self {
Self::new(chart)
}
}
impl std::ops::Deref for StreamingChart {
type Target = Chart;
fn deref(&self) -> &Self::Target {
&self.chart
}
}
#[derive(Debug, Clone, Copy)]
pub struct SlidingWindowConfig {
pub max_points: usize,
pub auto_scale: bool,
}
impl Default for SlidingWindowConfig {
fn default() -> Self {
Self {
max_points: 1000,
auto_scale: true,
}
}
}
impl SlidingWindowConfig {
pub fn new(max_points: usize) -> Self {
Self {
max_points,
auto_scale: true,
}
}
pub fn with_auto_scale(mut self, auto_scale: bool) -> Self {
self.auto_scale = auto_scale;
self
}
}
pub struct GpuStreamingChart {
streaming: StreamingChart,
line_renderer: GpuChartLineRenderer,
scatter_renderer: GpuChartScatterRenderer,
bar_renderer: GpuChartBarRenderer,
area_renderer: GpuChartAreaRenderer,
gpu_enabled: bool,
force_gpu: bool,
#[cfg(feature = "chart-text")]
text_renderer: Option<ChartTextRenderer>,
}
impl std::fmt::Debug for GpuStreamingChart {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("GpuStreamingChart");
s.field("streaming", &self.streaming)
.field("line_renderer", &self.line_renderer)
.field("scatter_renderer", &self.scatter_renderer)
.field("bar_renderer", &self.bar_renderer)
.field("area_renderer", &self.area_renderer)
.field("gpu_enabled", &self.gpu_enabled)
.field("force_gpu", &self.force_gpu);
#[cfg(feature = "chart-text")]
s.field("has_text_renderer", &self.text_renderer.is_some());
s.finish()
}
}
impl GpuStreamingChart {
pub fn new(
chart: Chart,
context: Arc<GraphicsContext>,
target_format: wgpu::TextureFormat,
) -> Self {
Self {
streaming: StreamingChart::new(chart),
line_renderer: GpuChartLineRenderer::new(context.clone(), target_format),
scatter_renderer: GpuChartScatterRenderer::new(context.clone(), target_format),
bar_renderer: GpuChartBarRenderer::new(context.clone(), target_format),
area_renderer: GpuChartAreaRenderer::new(context, target_format),
gpu_enabled: false,
force_gpu: false,
#[cfg(feature = "chart-text")]
text_renderer: None,
}
}
pub fn from_streaming(
streaming: StreamingChart,
context: Arc<GraphicsContext>,
target_format: wgpu::TextureFormat,
) -> Self {
Self {
streaming,
line_renderer: GpuChartLineRenderer::new(context.clone(), target_format),
scatter_renderer: GpuChartScatterRenderer::new(context.clone(), target_format),
bar_renderer: GpuChartBarRenderer::new(context.clone(), target_format),
area_renderer: GpuChartAreaRenderer::new(context, target_format),
gpu_enabled: false,
force_gpu: false,
#[cfg(feature = "chart-text")]
text_renderer: None,
}
}
#[cfg(feature = "chart-text")]
pub fn with_text(mut self, context: Arc<GraphicsContext>, font_system: FontSystem) -> Self {
self.text_renderer = Some(ChartTextRenderer::new(context, font_system));
self
}
#[cfg(feature = "chart-text")]
pub fn text_renderer(&self) -> Option<&ChartTextRenderer> {
self.text_renderer.as_ref()
}
#[cfg(feature = "chart-text")]
pub fn text_renderer_mut(&mut self) -> Option<&mut ChartTextRenderer> {
self.text_renderer.as_mut()
}
pub fn force_gpu_rendering(mut self, force: bool) -> Self {
self.force_gpu = force;
self
}
pub fn chart(&self) -> &Chart {
self.streaming.chart()
}
pub fn chart_mut(&mut self) -> &mut Chart {
self.streaming.chart_mut()
}
pub fn streaming(&self) -> &StreamingChart {
&self.streaming
}
pub fn streaming_mut(&mut self) -> &mut StreamingChart {
&mut self.streaming
}
pub fn line_renderer(&self) -> &GpuChartLineRenderer {
&self.line_renderer
}
pub fn line_renderer_mut(&mut self) -> &mut GpuChartLineRenderer {
&mut self.line_renderer
}
pub fn scatter_renderer(&self) -> &GpuChartScatterRenderer {
&self.scatter_renderer
}
pub fn bar_renderer(&self) -> &GpuChartBarRenderer {
&self.bar_renderer
}
pub fn area_renderer(&self) -> &GpuChartAreaRenderer {
&self.area_renderer
}
pub fn is_gpu_enabled(&self) -> bool {
self.gpu_enabled
}
pub fn append_data(&mut self, series_idx: usize, points: &[DataPoint]) {
self.streaming.append_data(series_idx, points);
self.mark_all_renderers_data_changed();
}
pub fn push_point(&mut self, series_idx: usize, point: DataPoint, max_points: Option<usize>) {
self.streaming.push_point(series_idx, point, max_points);
self.mark_all_renderers_data_changed();
}
pub fn set_data(&mut self, series_idx: usize, data: Vec<DataPoint>) {
self.streaming.set_data(series_idx, data);
self.mark_all_renderers_data_changed();
}
pub fn clear_data(&mut self, series_idx: usize) {
self.streaming.clear_data(series_idx);
self.mark_all_renderers_data_changed();
}
pub fn push_time_point(&mut self, series_idx: usize, time: f64, value: f64) {
self.streaming.push_time_point(series_idx, time, value);
self.mark_all_renderers_data_changed();
}
pub fn push_time_point_windowed(
&mut self,
series_idx: usize,
time: f64,
value: f64,
max_points: usize,
) {
self.streaming
.push_time_point_windowed(series_idx, time, value, max_points);
self.mark_all_renderers_data_changed();
}
fn mark_all_renderers_data_changed(&mut self) {
self.line_renderer.mark_data_changed();
self.scatter_renderer.mark_data_changed();
self.bar_renderer.mark_data_changed();
self.area_renderer.mark_data_changed();
}
pub fn auto_scroll(&mut self, axis_id: AxisId, window_size: f64) {
self.streaming.auto_scroll(axis_id, window_size);
}
pub fn disable_auto_scroll(&mut self, axis_id: AxisId) {
self.streaming.disable_auto_scroll(axis_id);
}
pub fn mark_view_changed(&mut self) {
self.streaming.mark_view_changed();
}
pub fn mark_style_changed(&mut self) {
self.streaming.mark_style_changed();
}
pub fn mark_bounds_changed(&mut self) {
self.streaming.mark_bounds_changed();
}
pub fn dirty_flags(&self) -> ChartDirtyFlags {
self.streaming.dirty_flags()
}
fn should_use_gpu(&self) -> bool {
if self.force_gpu {
return true;
}
self.streaming
.chart()
.series
.iter()
.any(|s| s.data.len() > GPU_RENDER_THRESHOLD)
}
pub fn prepare_render(&mut self, bounds: &Rect) {
self.streaming.prepare_render(bounds);
self.gpu_enabled = self.should_use_gpu();
if self.gpu_enabled {
self.prepare_gpu_renderer();
}
}
pub fn prepare_render_with_result(&mut self, bounds: &Rect) -> PrepareResult {
let result = self.streaming.prepare_render_with_result(bounds);
self.gpu_enabled = self.should_use_gpu();
if self.gpu_enabled {
self.prepare_gpu_renderer();
}
result
}
fn prepare_gpu_renderer(&mut self) {
let chart = self.streaming.chart();
match chart.chart_type {
ChartType::Line => {
self.line_renderer.prepare(chart);
}
ChartType::Scatter => {
self.scatter_renderer.prepare(chart);
}
ChartType::Bar => {
self.bar_renderer.prepare(chart);
}
ChartType::Area => {
self.area_renderer.prepare(chart);
}
}
}
pub fn render(
&self,
pass: &mut wgpu::RenderPass,
viewport: Viewport,
geometry_renderer: &mut crate::GeometryRenderer,
bounds: &Rect,
) {
use crate::chart::ChartRenderer;
let plot_area = bounds.inset(self.streaming.chart().padding);
let chart = self.streaming.chart();
{
let mut chart_renderer = ChartRenderer::new(geometry_renderer);
if self.gpu_enabled {
chart_renderer.draw_with_gpu_lines(chart, *bounds);
} else {
chart_renderer.draw(chart, *bounds);
}
chart_renderer.render(pass, viewport);
}
if self.gpu_enabled {
self.render_gpu_series(pass, viewport, &plot_area, chart);
}
}
#[cfg(feature = "chart-text")]
pub fn render_with_text(
&mut self,
pass: &mut wgpu::RenderPass,
viewport: Viewport,
geometry_renderer: &mut crate::GeometryRenderer,
bounds: &Rect,
) {
use crate::chart::ChartRenderer;
let chart = self.streaming.chart();
let plot_area = bounds.inset(chart.padding);
let text_margins = self
.text_renderer
.as_ref()
.map(|tr| tr.calculate_margins(chart))
.unwrap_or_default();
let adjusted_plot_area = Rect::new(
plot_area.x + text_margins.left,
plot_area.y + text_margins.top,
(plot_area.width - text_margins.left - text_margins.right).max(1.0),
(plot_area.height - text_margins.top - text_margins.bottom).max(1.0),
);
{
let mut chart_renderer = ChartRenderer::new(geometry_renderer);
if self.gpu_enabled {
chart_renderer.draw_with_gpu_lines(chart, *bounds);
} else {
chart_renderer.draw(chart, *bounds);
}
chart_renderer.render(pass, viewport);
}
if self.gpu_enabled {
self.render_gpu_series(pass, viewport, &adjusted_plot_area, chart);
}
if let Some(text_renderer) = &mut self.text_renderer {
text_renderer.set_viewport(viewport);
text_renderer.draw_title(chart, bounds);
text_renderer.draw_tick_labels(chart, &adjusted_plot_area);
text_renderer.draw_axis_labels(chart, &adjusted_plot_area);
text_renderer.draw_legend(chart, &adjusted_plot_area, geometry_renderer);
geometry_renderer.render(pass, viewport);
text_renderer.render(pass);
}
}
fn render_gpu_series(
&self,
pass: &mut wgpu::RenderPass,
viewport: Viewport,
plot_area: &Rect,
chart: &Chart,
) {
match chart.chart_type {
ChartType::Line => {
if self.line_renderer.segment_count() > 0 {
self.line_renderer.render(pass, viewport, plot_area, chart);
}
}
ChartType::Scatter => {
if self.scatter_renderer.point_count() > 0 {
self.scatter_renderer
.render(pass, viewport, plot_area, chart);
}
}
ChartType::Bar => {
if self.bar_renderer.quad_count() > 0 {
self.bar_renderer.render(pass, viewport, plot_area, chart);
}
}
ChartType::Area => {
if self.area_renderer.quad_count() > 0 || self.area_renderer.segment_count() > 0 {
self.area_renderer.render(pass, viewport, plot_area, chart);
}
}
}
}
pub fn statistics(&self) -> GpuStreamingStatistics {
let base = self.streaming.statistics();
let chart = self.streaming.chart();
let gpu_element_count = match chart.chart_type {
ChartType::Line => self.line_renderer.segment_count(),
ChartType::Scatter => self.scatter_renderer.point_count(),
ChartType::Bar => self.bar_renderer.quad_count(),
ChartType::Area => self.area_renderer.quad_count() + self.area_renderer.segment_count(),
};
GpuStreamingStatistics {
total_points: base.total_points,
series_counts: base.series_counts,
cache_dirty: base.cache_dirty,
auto_scroll_active: base.auto_scroll_active,
gpu_enabled: self.gpu_enabled,
gpu_segment_count: gpu_element_count,
}
}
pub fn get_display_data(&self, series_idx: usize, pixel_width: f32) -> Vec<DataPoint> {
self.streaming.get_display_data(series_idx, pixel_width)
}
}
impl std::ops::Deref for GpuStreamingChart {
type Target = Chart;
fn deref(&self) -> &Self::Target {
self.streaming.chart()
}
}
impl From<(Chart, Arc<GraphicsContext>, wgpu::TextureFormat)> for GpuStreamingChart {
fn from(
(chart, context, target_format): (Chart, Arc<GraphicsContext>, wgpu::TextureFormat),
) -> Self {
Self::new(chart, context, target_format)
}
}
#[derive(Debug, Clone)]
pub struct GpuStreamingStatistics {
pub total_points: usize,
pub series_counts: Vec<usize>,
pub cache_dirty: bool,
pub auto_scroll_active: bool,
pub gpu_enabled: bool,
pub gpu_segment_count: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chart::ChartBuilder;
#[test]
fn test_streaming_chart_append() {
let chart = ChartBuilder::line()
.add_series("Test", &[(0.0_f64, 1.0_f64)])
.build();
let mut streaming = StreamingChart::new(chart);
assert_eq!(streaming.chart.series_len(0), 1);
streaming.append_data(0, &[DataPoint::new(1.0, 2.0), DataPoint::new(2.0, 3.0)]);
assert_eq!(streaming.chart.series_len(0), 3);
assert!(
streaming
.cache
.dirty_flags()
.contains(ChartDirtyFlags::DATA_APPENDED)
);
}
#[test]
fn test_streaming_chart_sliding_window() {
let chart = ChartBuilder::line()
.add_series("Test", &[] as &[(f64, f64)])
.build();
let mut streaming = StreamingChart::new(chart);
for i in 0..5 {
streaming.push_point(0, DataPoint::new(i as f64, i as f64), Some(3));
}
assert_eq!(streaming.chart.series_len(0), 3);
assert!(
streaming
.cache
.dirty_flags()
.contains(ChartDirtyFlags::DATA_CHANGED)
);
}
}