1use super::rect::Rect;
7use super::types::{AxisId, Chart, DataPoint};
8use glam::Vec2;
9
10bitflags::bitflags! {
11 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13 pub struct ChartDirtyFlags: u16 {
14 const DATA_CHANGED = 0b0000_0001;
16 const DATA_APPENDED = 0b0000_0010;
18 const VIEW_CHANGED = 0b0000_0100;
20 const STYLE_CHANGED = 0b0000_1000;
22 const AXES_CHANGED = 0b0001_0000;
24 const BOUNDS_CHANGED = 0b0010_0000;
26 }
27}
28
29impl ChartDirtyFlags {
30 pub fn needs_cache_rebuild(&self) -> bool {
32 self.intersects(
33 Self::DATA_CHANGED | Self::VIEW_CHANGED | Self::AXES_CHANGED | Self::BOUNDS_CHANGED,
34 )
35 }
36
37 pub fn is_append_only(&self) -> bool {
39 self.contains(Self::DATA_APPENDED) && !self.contains(Self::DATA_CHANGED)
40 }
41
42 pub fn is_style_only(&self) -> bool {
44 *self == Self::STYLE_CHANGED
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct SeriesPixelCache {
51 pub positions: Vec<Vec2>,
53 pub x_axis: AxisId,
55 pub y_axis: AxisId,
57 pub x_range: (f64, f64),
59 pub y_range: (f64, f64),
61 pub data_count: usize,
63}
64
65impl SeriesPixelCache {
66 pub fn new(x_axis: AxisId, y_axis: AxisId) -> Self {
68 Self {
69 positions: Vec::new(),
70 x_axis,
71 y_axis,
72 x_range: (0.0, 1.0),
73 y_range: (0.0, 1.0),
74 data_count: 0,
75 }
76 }
77
78 pub fn is_valid(&self, x_range: (f64, f64), y_range: (f64, f64), data_count: usize) -> bool {
80 self.x_range == x_range && self.y_range == y_range && self.data_count == data_count
81 }
82}
83
84#[derive(Debug, Clone)]
89pub struct ChartCache {
90 series_caches: Vec<SeriesPixelCache>,
92 spatial_index: Option<SpatialIndex>,
94 data_version: u64,
96 view_version: u64,
98 last_bounds: Option<Rect>,
100 dirty_flags: ChartDirtyFlags,
102}
103
104impl Default for ChartCache {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl ChartCache {
111 pub fn new() -> Self {
113 Self {
114 series_caches: Vec::new(),
115 spatial_index: None,
116 data_version: 0,
117 view_version: 0,
118 last_bounds: None,
119 dirty_flags: ChartDirtyFlags::all(),
120 }
121 }
122
123 pub fn dirty_flags(&self) -> ChartDirtyFlags {
125 self.dirty_flags
126 }
127
128 pub fn invalidate(&mut self) {
130 self.dirty_flags = ChartDirtyFlags::all();
131 }
132
133 pub fn mark_data_changed(&mut self) {
135 self.dirty_flags.insert(ChartDirtyFlags::DATA_CHANGED);
136 self.data_version = self.data_version.wrapping_add(1);
137 }
138
139 pub fn mark_data_appended(&mut self, series_idx: usize, new_count: usize) {
141 if series_idx < self.series_caches.len() {
142 let cache = &self.series_caches[series_idx];
144 if cache.data_count < new_count {
145 self.dirty_flags.insert(ChartDirtyFlags::DATA_APPENDED);
146 }
147 } else {
148 self.dirty_flags.insert(ChartDirtyFlags::DATA_CHANGED);
150 }
151 }
152
153 pub fn mark_view_changed(&mut self) {
155 self.dirty_flags.insert(ChartDirtyFlags::VIEW_CHANGED);
156 self.view_version = self.view_version.wrapping_add(1);
157 }
158
159 pub fn mark_style_changed(&mut self) {
161 self.dirty_flags.insert(ChartDirtyFlags::STYLE_CHANGED);
162 }
163
164 pub fn mark_axes_changed(&mut self) {
166 self.dirty_flags.insert(ChartDirtyFlags::AXES_CHANGED);
167 }
168
169 pub fn mark_bounds_changed(&mut self) {
171 self.dirty_flags.insert(ChartDirtyFlags::BOUNDS_CHANGED);
172 }
173
174 pub fn clear_dirty(&mut self) {
176 self.dirty_flags = ChartDirtyFlags::empty();
177 }
178
179 pub fn needs_rebuild(&self) -> bool {
181 self.dirty_flags.needs_cache_rebuild()
182 }
183
184 pub fn rebuild(&mut self, chart: &Chart, bounds: &Rect) {
186 let plot_area = bounds.inset(chart.padding);
187
188 if self.dirty_flags.is_append_only() && self.last_bounds == Some(*bounds) {
190 self.partial_update(chart, &plot_area);
191 } else {
192 self.full_rebuild(chart, &plot_area);
193 }
194
195 self.last_bounds = Some(*bounds);
196 self.clear_dirty();
197 }
198
199 fn full_rebuild(&mut self, chart: &Chart, plot_area: &Rect) {
201 self.series_caches.resize_with(chart.series.len(), || {
203 SeriesPixelCache::new(AxisId::X_PRIMARY, AxisId::Y_PRIMARY)
204 });
205
206 for (series_idx, series) in chart.series.iter().enumerate() {
208 let x_range = chart.axis_range(series.x_axis);
209 let y_range = chart.axis_range(series.y_axis);
210
211 let cache = &mut self.series_caches[series_idx];
212 cache.x_axis = series.x_axis;
213 cache.y_axis = series.y_axis;
214 cache.x_range = x_range;
215 cache.y_range = y_range;
216 cache.data_count = series.data.len();
217
218 cache.positions.clear();
220 cache.positions.reserve(series.data.len());
221
222 for point in &series.data {
223 let pixel = data_to_pixel(point, plot_area, x_range, y_range);
224 cache.positions.push(pixel);
225 }
226 }
227
228 self.rebuild_spatial_index(plot_area);
230 }
231
232 fn partial_update(&mut self, chart: &Chart, plot_area: &Rect) {
234 for (series_idx, series) in chart.series.iter().enumerate() {
235 if series_idx >= self.series_caches.len() {
236 self.series_caches
238 .push(SeriesPixelCache::new(series.x_axis, series.y_axis));
239 }
240
241 let cache = &mut self.series_caches[series_idx];
242 let x_range = chart.axis_range(series.x_axis);
243 let y_range = chart.axis_range(series.y_axis);
244
245 if cache.x_range != x_range || cache.y_range != y_range {
247 cache.x_range = x_range;
249 cache.y_range = y_range;
250 cache.positions.clear();
251 cache.positions.reserve(series.data.len());
252 for point in &series.data {
253 let pixel = data_to_pixel(point, plot_area, x_range, y_range);
254 cache.positions.push(pixel);
255 }
256 } else if series.data.len() > cache.data_count {
257 cache
259 .positions
260 .reserve(series.data.len() - cache.data_count);
261 for point in &series.data[cache.data_count..] {
262 let pixel = data_to_pixel(point, plot_area, x_range, y_range);
263 cache.positions.push(pixel);
264 }
265 }
266
267 cache.data_count = series.data.len();
268 }
269
270 self.rebuild_spatial_index(plot_area);
272 }
273
274 fn rebuild_spatial_index(&mut self, plot_area: &Rect) {
276 let mut index = SpatialIndex::new(*plot_area, 32, 32);
277
278 for (series_idx, cache) in self.series_caches.iter().enumerate() {
279 for (point_idx, &pos) in cache.positions.iter().enumerate() {
280 index.insert(pos, series_idx, point_idx);
281 }
282 }
283
284 self.spatial_index = Some(index);
285 }
286
287 pub fn series_positions(&self, series_idx: usize) -> Option<&[Vec2]> {
289 self.series_caches
290 .get(series_idx)
291 .map(|c| c.positions.as_slice())
292 }
293
294 pub fn spatial_index(&self) -> Option<&SpatialIndex> {
296 self.spatial_index.as_ref()
297 }
298
299 pub fn hit_test(
301 &self,
302 chart: &Chart,
303 pixel: Vec2,
304 max_distance: f32,
305 ) -> Option<CacheHitResult> {
306 let index = self.spatial_index.as_ref()?;
307
308 let mut best: Option<CacheHitResult> = None;
309
310 for (series_idx, point_idx) in index.query_near(pixel, max_distance) {
311 if let Some(cache) = self.series_caches.get(series_idx)
312 && let Some(&point_pixel) = cache.positions.get(point_idx)
313 {
314 let dist = pixel.distance(point_pixel);
315 if dist <= max_distance && best.as_ref().is_none_or(|b| dist < b.distance) {
316 let data_point = chart
317 .series
318 .get(series_idx)
319 .and_then(|s| s.data.get(point_idx))
320 .copied();
321
322 if let Some(data_point) = data_point {
323 best = Some(CacheHitResult {
324 series_index: series_idx,
325 point_index: point_idx,
326 distance: dist,
327 data_point,
328 pixel_position: point_pixel,
329 });
330 }
331 }
332 }
333 }
334
335 best
336 }
337}
338
339#[derive(Debug, Clone)]
341pub struct CacheHitResult {
342 pub series_index: usize,
344 pub point_index: usize,
346 pub distance: f32,
348 pub data_point: DataPoint,
350 pub pixel_position: Vec2,
352}
353
354#[derive(Debug, Clone)]
359pub struct SpatialIndex {
360 cells: Vec<Vec<(usize, usize)>>,
362 cols: usize,
364 rows: usize,
366 bounds: Rect,
368 cell_width: f32,
370 cell_height: f32,
372}
373
374impl SpatialIndex {
375 pub fn new(bounds: Rect, cols: usize, rows: usize) -> Self {
377 let cols = cols.max(1);
378 let rows = rows.max(1);
379
380 Self {
381 cells: vec![Vec::new(); cols * rows],
382 cols,
383 rows,
384 bounds,
385 cell_width: bounds.width / cols as f32,
386 cell_height: bounds.height / rows as f32,
387 }
388 }
389
390 pub fn insert(&mut self, pos: Vec2, series_idx: usize, point_idx: usize) {
392 if let Some(cell_idx) = self.cell_index(pos) {
393 self.cells[cell_idx].push((series_idx, point_idx));
394 }
395 }
396
397 pub fn clear(&mut self) {
399 for cell in &mut self.cells {
400 cell.clear();
401 }
402 }
403
404 fn cell_index(&self, pos: Vec2) -> Option<usize> {
406 if !self.bounds.contains(pos) {
407 return None;
408 }
409
410 let col = ((pos.x - self.bounds.x) / self.cell_width) as usize;
411 let row = ((pos.y - self.bounds.y) / self.cell_height) as usize;
412
413 let col = col.min(self.cols - 1);
414 let row = row.min(self.rows - 1);
415
416 Some(row * self.cols + col)
417 }
418
419 fn cell_coords(&self, pos: Vec2) -> Option<(usize, usize)> {
421 if !self.bounds.contains(pos) {
422 let x = pos.x.clamp(self.bounds.x, self.bounds.right());
424 let y = pos.y.clamp(self.bounds.y, self.bounds.bottom());
425
426 let col = ((x - self.bounds.x) / self.cell_width) as usize;
427 let row = ((y - self.bounds.y) / self.cell_height) as usize;
428
429 return Some((col.min(self.cols - 1), row.min(self.rows - 1)));
430 }
431
432 let col = ((pos.x - self.bounds.x) / self.cell_width) as usize;
433 let row = ((pos.y - self.bounds.y) / self.cell_height) as usize;
434
435 Some((col.min(self.cols - 1), row.min(self.rows - 1)))
436 }
437
438 pub fn query_near(&self, pos: Vec2, radius: f32) -> impl Iterator<Item = (usize, usize)> + '_ {
442 let (center_col, center_row) = self.cell_coords(pos).unwrap_or((0, 0));
444
445 let cells_x = (radius / self.cell_width).ceil() as usize + 1;
446 let cells_y = (radius / self.cell_height).ceil() as usize + 1;
447
448 let min_col = center_col.saturating_sub(cells_x);
449 let max_col = (center_col + cells_x).min(self.cols - 1);
450 let min_row = center_row.saturating_sub(cells_y);
451 let max_row = (center_row + cells_y).min(self.rows - 1);
452
453 SpatialQueryIter {
455 index: self,
456 min_col,
457 max_col,
458 min_row,
459 max_row,
460 current_col: min_col,
461 current_row: min_row,
462 current_entry: 0,
463 }
464 }
465
466 pub fn len(&self) -> usize {
468 self.cells.iter().map(|c| c.len()).sum()
469 }
470
471 pub fn is_empty(&self) -> bool {
473 self.cells.iter().all(|c| c.is_empty())
474 }
475}
476
477struct SpatialQueryIter<'a> {
479 index: &'a SpatialIndex,
480 min_col: usize,
481 max_col: usize,
482 #[allow(dead_code)] min_row: usize,
484 max_row: usize,
485 current_col: usize,
486 current_row: usize,
487 current_entry: usize,
488}
489
490impl Iterator for SpatialQueryIter<'_> {
491 type Item = (usize, usize);
492
493 fn next(&mut self) -> Option<Self::Item> {
494 loop {
495 if self.current_row > self.max_row {
496 return None;
497 }
498
499 let cell_idx = self.current_row * self.index.cols + self.current_col;
500 let cell = &self.index.cells[cell_idx];
501
502 if self.current_entry < cell.len() {
503 let result = cell[self.current_entry];
504 self.current_entry += 1;
505 return Some(result);
506 }
507
508 self.current_entry = 0;
510 self.current_col += 1;
511
512 if self.current_col > self.max_col {
513 self.current_col = self.min_col;
514 self.current_row += 1;
515 }
516 }
517 }
518}
519
520fn data_to_pixel(
522 point: &DataPoint,
523 plot_area: &Rect,
524 x_range: (f64, f64),
525 y_range: (f64, f64),
526) -> Vec2 {
527 let (x_min, x_max) = x_range;
528 let (y_min, y_max) = y_range;
529
530 let px = plot_area.x + ((point.x - x_min) / (x_max - x_min)) as f32 * plot_area.width;
531 let py = plot_area.y + plot_area.height
533 - ((point.y - y_min) / (y_max - y_min)) as f32 * plot_area.height;
534
535 Vec2::new(px, py)
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 #[test]
543 fn test_dirty_flags() {
544 let mut flags = ChartDirtyFlags::empty();
545 assert!(!flags.needs_cache_rebuild());
546
547 flags.insert(ChartDirtyFlags::STYLE_CHANGED);
548 assert!(!flags.needs_cache_rebuild());
549 assert!(flags.is_style_only());
550
551 flags.insert(ChartDirtyFlags::DATA_CHANGED);
552 assert!(flags.needs_cache_rebuild());
553 assert!(!flags.is_style_only());
554 }
555
556 #[test]
557 fn test_spatial_index() {
558 let bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
559 let mut index = SpatialIndex::new(bounds, 10, 10);
560
561 index.insert(Vec2::new(15.0, 15.0), 0, 0);
563 index.insert(Vec2::new(25.0, 25.0), 0, 1);
564 index.insert(Vec2::new(85.0, 85.0), 1, 0);
565
566 assert_eq!(index.len(), 3);
567
568 let results: Vec<_> = index.query_near(Vec2::new(15.0, 15.0), 20.0).collect();
570 assert!(results.contains(&(0, 0)));
571 assert!(results.contains(&(0, 1)));
572 assert!(!results.contains(&(1, 0)));
573 }
574}