1use std::cell::Cell;
44use std::cell::RefCell;
45use std::collections::HashMap;
46use std::rc::Rc;
47
48use teksilo_core::ObserverHandle;
49use teksilo_core::color_prop::ColorProp;
50
51use crate::chart_change::{ChartChange, SeriesId};
52use crate::chart_model::{ChartDatum, ChartModel};
53
54type SeriesIdsFn = Rc<dyn Fn() -> Vec<SeriesId>>;
55type PointCountFn = Rc<dyn Fn(SeriesId) -> usize>;
56type WithPointFn<T> = Rc<dyn Fn(SeriesId, usize, &dyn Fn(&ChartDatum<T>))>;
57type WithSeriesFn = Rc<dyn Fn(SeriesId, &dyn Fn(&str, Option<&ColorProp>, bool))>;
58type ObserveChartFn = Rc<dyn Fn(Box<dyn Fn(&ChartChange)>) -> ObserverHandle>;
59
60struct ObserverEntry {
61 id: u64,
62 callback: Rc<dyn Fn(&ChartChange)>,
63}
64
65struct ChartWindowInner<T: 'static> {
66 series_ids_fn: SeriesIdsFn,
67 point_count_fn: PointCountFn,
68 with_point_fn: WithPointFn<T>,
69 with_series_fn: WithSeriesFn,
70 window_size: usize,
71 starts: HashMap<SeriesId, usize>,
73 divergence: HashMap<SeriesId, usize>,
76 observers: Vec<ObserverEntry>,
77 next_observer_id: u64,
78 _upstream_handle: Option<ObserverHandle>,
79}
80
81impl<T: 'static> ChartWindowInner<T> {
82 fn start_for(&self, series: SeriesId) -> usize {
83 let total = (self.point_count_fn)(series);
84 total.saturating_sub(self.window_size)
85 }
86
87 fn rebuild_series(&mut self, series: SeriesId) {
88 let start = self.start_for(series);
89 self.starts.insert(series, start);
90 self.divergence.insert(series, 0);
91 }
92
93 fn rebuild_all(&mut self) {
94 self.starts.clear();
95 self.divergence.clear();
96 for series in (self.series_ids_fn)() {
97 self.rebuild_series(series);
98 }
99 }
100
101 fn snapshot_callbacks(&self) -> Vec<Rc<dyn Fn(&ChartChange)>> {
102 self.observers.iter().map(|e| e.callback.clone()).collect()
103 }
104}
105
106pub struct ChartWindow<T: 'static> {
110 inner: Rc<RefCell<ChartWindowInner<T>>>,
111}
112
113impl<T: 'static> ChartWindow<T> {
114 pub fn new(source: ChartModel<T>, window_size: usize) -> Self {
117 let series_ids_fn: SeriesIdsFn = {
118 let m = source.clone();
119 Rc::new(move || m.series_ids())
120 };
121 let point_count_fn: PointCountFn = {
122 let m = source.clone();
123 Rc::new(move |series| m.point_count(series))
124 };
125 let with_point_fn: WithPointFn<T> = {
126 let m = source.clone();
127 Rc::new(move |series, idx, f| {
128 m.with_point(series, idx, |d| f(d));
129 })
130 };
131 let with_series_fn: WithSeriesFn = {
132 let m = source.clone();
133 Rc::new(move |series, f| {
134 m.with_series(series, |name, color, visible| f(name, color, visible));
135 })
136 };
137 let observe_fn: ObserveChartFn = {
138 let m = source;
139 Rc::new(move |callback| m.observe_changes(move |change| callback(change)))
140 };
141
142 let inner = Rc::new(RefCell::new(ChartWindowInner {
143 series_ids_fn,
144 point_count_fn,
145 with_point_fn,
146 with_series_fn,
147 window_size,
148 starts: HashMap::new(),
149 divergence: HashMap::new(),
150 observers: Vec::new(),
151 next_observer_id: 1,
152 _upstream_handle: None,
153 }));
154
155 inner.borrow_mut().rebuild_all();
156
157 let weak = Rc::downgrade(&inner);
158 let upstream_handle = (observe_fn)(Box::new(move |change| {
159 if let Some(strong) = weak.upgrade() {
160 translate_and_notify(&strong, change);
161 }
162 }));
163 inner.borrow_mut()._upstream_handle = Some(upstream_handle);
164
165 Self { inner }
166 }
167
168 pub fn window_size(&self) -> usize {
170 self.inner.borrow().window_size
171 }
172
173 pub fn set_window_size(&self, window_size: usize) {
176 let callbacks = {
177 let mut guard = self.inner.borrow_mut();
178 guard.window_size = window_size;
179 guard.rebuild_all();
180 guard.snapshot_callbacks()
181 };
182 for cb in &callbacks {
183 cb(&ChartChange::Reset);
184 }
185 }
186
187 pub fn series_count(&self) -> usize {
189 (self.inner.borrow().series_ids_fn)().len()
190 }
191
192 pub fn series_ids(&self) -> Vec<SeriesId> {
194 (self.inner.borrow().series_ids_fn)()
195 }
196
197 pub fn point_count(&self, series: SeriesId) -> usize {
199 let guard = self.inner.borrow();
200 let total = (guard.point_count_fn)(series);
201 let start = guard
202 .starts
203 .get(&series)
204 .copied()
205 .unwrap_or_else(|| total.saturating_sub(guard.window_size));
206 total.saturating_sub(start)
207 }
208
209 pub fn with_series<R>(
212 &self,
213 series: SeriesId,
214 f: impl FnOnce(&str, Option<&ColorProp>, bool) -> R,
215 ) -> Option<R> {
216 let with_series_fn = self.inner.borrow().with_series_fn.clone();
217 let f_cell: Cell<Option<_>> = Cell::new(Some(f));
218 let slot: Cell<Option<R>> = Cell::new(None);
219 (with_series_fn)(series, &|name, color, visible| {
220 if let Some(f) = f_cell.take() {
221 slot.set(Some(f(name, color, visible)));
222 }
223 });
224 slot.into_inner()
225 }
226
227 pub fn with_point<R>(
230 &self,
231 series: SeriesId,
232 index: usize,
233 f: impl FnOnce(&ChartDatum<T>) -> R,
234 ) -> Option<R> {
235 let (with_point_fn, src_idx) = {
236 let guard = self.inner.borrow();
237 let total = (guard.point_count_fn)(series);
238 let start = guard
239 .starts
240 .get(&series)
241 .copied()
242 .unwrap_or_else(|| total.saturating_sub(guard.window_size));
243 let visible = total.saturating_sub(start);
244 if index >= visible {
249 return None;
250 }
251 (guard.with_point_fn.clone(), start + index)
252 };
253 let f_cell: Cell<Option<_>> = Cell::new(Some(f));
254 let slot: Cell<Option<R>> = Cell::new(None);
255 (with_point_fn)(series, src_idx, &|d: &ChartDatum<T>| {
256 if let Some(f) = f_cell.take() {
257 slot.set(Some(f(d)));
258 }
259 });
260 slot.into_inner()
261 }
262
263 pub fn observe_changes(&self, f: impl Fn(&ChartChange) + 'static) -> ObserverHandle {
266 let mut guard = self.inner.borrow_mut();
267 let id = guard.next_observer_id;
268 guard.next_observer_id += 1;
269 guard.observers.push(ObserverEntry {
270 id,
271 callback: Rc::new(f),
272 });
273 let inner = self.inner.clone();
274 ObserverHandle::new(
275 self.inner.clone(),
276 id,
277 Rc::new(move |observer_id| {
278 inner.borrow_mut().observers.retain(|e| e.id != observer_id);
279 }),
280 )
281 }
282
283 pub fn first_changed_index(&self, series: SeriesId) -> Option<usize> {
289 self.inner.borrow().divergence.get(&series).copied()
290 }
291}
292
293fn translate<T: 'static>(
296 inner: &mut ChartWindowInner<T>,
297 change: &ChartChange,
298) -> Vec<ChartChange> {
299 match change {
300 ChartChange::SeriesInserted { index, series } => {
301 let (index, series) = (*index, *series);
302 inner.starts.insert(series, 0);
303 inner.divergence.insert(series, 0);
304 vec![ChartChange::SeriesInserted { index, series }]
305 }
306 ChartChange::SeriesRemoved { series } => {
307 let series = *series;
308 inner.starts.remove(&series);
309 inner.divergence.remove(&series);
310 vec![ChartChange::SeriesRemoved { series }]
311 }
312 ChartChange::SeriesMoved { series, from, to } => vec![ChartChange::SeriesMoved {
313 series: *series,
314 from: *from,
315 to: *to,
316 }],
317 ChartChange::SeriesRenamed { series } => {
318 vec![ChartChange::SeriesRenamed { series: *series }]
319 }
320 ChartChange::SeriesColorChanged { series } => {
321 vec![ChartChange::SeriesColorChanged { series: *series }]
322 }
323 ChartChange::SeriesPatternChanged { series } => {
324 vec![ChartChange::SeriesPatternChanged { series: *series }]
325 }
326 ChartChange::SeriesVisibilityChanged { series } => {
327 vec![ChartChange::SeriesVisibilityChanged { series: *series }]
328 }
329 ChartChange::PointsInserted { series, range } => {
330 let series = *series;
331 let range = range.clone();
332 let window_size = inner.window_size;
333 let new_total = (inner.point_count_fn)(series);
334 let inserted = range.end - range.start;
335 let old_total = new_total - inserted;
336 let old_start = inner
337 .starts
338 .get(&series)
339 .copied()
340 .unwrap_or_else(|| old_total.saturating_sub(window_size));
341
342 if range.start != old_total {
343 inner.rebuild_series(series);
345 return vec![ChartChange::SeriesDataReplaced { series }];
346 }
347
348 let old_visible = old_total.saturating_sub(old_start);
358 let new_start = new_total.saturating_sub(window_size);
359 let new_visible = new_total.saturating_sub(new_start);
360 let dropped = new_start.saturating_sub(old_start).min(old_visible);
363 let remaining = old_visible - dropped;
364
365 let mut out = Vec::new();
366 if dropped > 0 {
367 out.push(ChartChange::PointsRemoved {
368 series,
369 range: 0..dropped,
370 });
371 }
372 if new_visible > remaining {
373 out.push(ChartChange::PointsInserted {
374 series,
375 range: remaining..new_visible,
376 });
377 }
378 inner
382 .divergence
383 .insert(series, if dropped > 0 { 0 } else { remaining });
384 inner.starts.insert(series, new_start);
385 out
386 }
387 ChartChange::PointsRemoved { series, range } => {
388 let series = *series;
389 let range = range.clone();
390 let window_size = inner.window_size;
391 let new_total = (inner.point_count_fn)(series);
392 let removed = range.end - range.start;
393 let old_total = new_total + removed;
394 let old_start = inner
395 .starts
396 .get(&series)
397 .copied()
398 .unwrap_or_else(|| old_total.saturating_sub(window_size));
399
400 if range.end != old_total {
401 inner.rebuild_series(series);
405 return vec![ChartChange::SeriesDataReplaced { series }];
406 }
407
408 let old_visible = old_total.saturating_sub(old_start);
417 let new_start = new_total.saturating_sub(window_size);
418 let new_visible = new_total.saturating_sub(new_start);
419 let survivors = new_total.saturating_sub(old_start).min(old_visible);
422 let removed_from_window = old_visible - survivors;
423 let revealed = old_start.saturating_sub(new_start);
424
425 let mut out = Vec::new();
426 if removed_from_window > 0 {
427 out.push(ChartChange::PointsRemoved {
428 series,
429 range: survivors..old_visible,
430 });
431 }
432 if revealed > 0 {
433 out.push(ChartChange::PointsInserted {
434 series,
435 range: 0..revealed,
436 });
437 }
438 inner.divergence.insert(
442 series,
443 if revealed > 0 {
444 0
445 } else {
446 survivors.min(new_visible)
447 },
448 );
449 inner.starts.insert(series, new_start);
450 out
451 }
452 ChartChange::PointUpdated { series, index } => {
453 let (series, index) = (*series, *index);
454 let start = inner.starts.get(&series).copied().unwrap_or(0);
455 if index >= start {
456 let local = index - start;
457 inner.divergence.insert(series, local);
458 vec![ChartChange::PointUpdated {
459 series,
460 index: local,
461 }]
462 } else {
463 vec![]
464 }
465 }
466 ChartChange::SeriesDataReplaced { series } => {
467 let series = *series;
468 inner.rebuild_series(series);
469 vec![ChartChange::SeriesDataReplaced { series }]
470 }
471 ChartChange::Reset => {
472 inner.rebuild_all();
473 vec![ChartChange::Reset]
474 }
475 }
476}
477
478fn translate_and_notify<T: 'static>(
479 inner: &Rc<RefCell<ChartWindowInner<T>>>,
480 change: &ChartChange,
481) {
482 let (changes, callbacks) = {
483 let mut guard = inner.borrow_mut();
484 let changes = translate(&mut guard, change);
485 (changes, guard.snapshot_callbacks())
486 };
487 for c in &changes {
488 for cb in &callbacks {
489 cb(c);
490 }
491 }
492}
493
494impl<T: 'static> Clone for ChartWindow<T> {
495 fn clone(&self) -> Self {
496 Self {
497 inner: self.inner.clone(),
498 }
499 }
500}
501
502impl<T: 'static> std::fmt::Debug for ChartWindow<T> {
503 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
504 let guard = self.inner.borrow();
505 f.debug_struct("ChartWindow")
506 .field("window_size", &guard.window_size)
507 .field("series_count", &(guard.series_ids_fn)().len())
508 .finish()
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 fn one_series(n: usize) -> (ChartModel<i32>, SeriesId) {
517 let model: ChartModel<i32> = ChartModel::new();
518 let s = model.add_series("s");
519 for i in 0..n {
520 model.push_point(s, i as i32, i as f32);
521 }
522 (model, s)
523 }
524
525 fn track(window: &ChartWindow<i32>) -> (Rc<RefCell<Vec<ChartChange>>>, ObserverHandle) {
526 let log: Rc<RefCell<Vec<ChartChange>>> = Rc::new(RefCell::new(Vec::new()));
527 let l = log.clone();
528 let handle = window.observe_changes(move |c| l.borrow_mut().push(c.clone()));
529 (log, handle)
530 }
531
532 #[test]
533 fn initial_window_shows_only_the_tail() {
534 let (_model, s) = one_series(5);
535 let window = ChartWindow::new(_model, 3);
536 assert_eq!(window.point_count(s), 3);
537 let vals: Vec<f32> = (0..3)
538 .map(|i| window.with_point(s, i, |d| d.value).unwrap())
539 .collect();
540 assert_eq!(vals, vec![2.0, 3.0, 4.0]);
541 }
542
543 #[test]
544 fn with_point_out_of_range_returns_none_without_overflow() {
545 let (model, s) = one_series(5);
550 let window = ChartWindow::new(model, 3); assert_eq!(window.with_point(s, usize::MAX, |d| d.value), None);
552 }
553
554 #[test]
555 fn full_window_shift_emits_removed_then_inserted() {
556 let (model, s) = one_series(3);
557 let window = ChartWindow::new(model.clone(), 3);
558 assert_eq!(window.point_count(s), 3);
559 let (log, _h) = track(&window);
560
561 model.push_point(s, 3, 3.0);
562
563 let entries = log.borrow();
564 assert_eq!(entries.len(), 2);
565 assert_eq!(
566 entries[0],
567 ChartChange::PointsRemoved {
568 series: s,
569 range: 0..1
570 }
571 );
572 assert_eq!(
573 entries[1],
574 ChartChange::PointsInserted {
575 series: s,
576 range: 2..3
577 }
578 );
579 drop(entries);
580
581 assert_eq!(window.point_count(s), 3);
582 let vals: Vec<f32> = (0..3)
583 .map(|i| window.with_point(s, i, |d| d.value).unwrap())
584 .collect();
585 assert_eq!(vals, vec![1.0, 2.0, 3.0]);
586 }
587
588 #[test]
589 fn partial_window_tail_append_emits_plain_inserted() {
590 let (model, s) = one_series(1);
591 let window = ChartWindow::new(model.clone(), 5);
592 let (log, _h) = track(&window);
593
594 model.push_point(s, 1, 1.0);
595
596 let entries = log.borrow();
597 assert_eq!(entries.len(), 1);
598 assert_eq!(
599 entries[0],
600 ChartChange::PointsInserted {
601 series: s,
602 range: 1..2
603 }
604 );
605 drop(entries);
606 assert_eq!(window.point_count(s), 2);
607 }
608
609 #[test]
610 fn mid_series_insert_falls_back_to_replace() {
611 let (model, s) = one_series(3);
612 let window = ChartWindow::new(model.clone(), 5);
613 let (log, _h) = track(&window);
614
615 model.insert_point(s, 1, 99, 99.0);
616
617 let entries = log.borrow();
618 assert_eq!(entries.len(), 1);
619 assert_eq!(entries[0], ChartChange::SeriesDataReplaced { series: s });
620 drop(entries);
621 assert_eq!(window.point_count(s), 4);
622 }
623
624 #[test]
625 fn front_removal_falls_back_to_replace() {
626 let (model, s) = one_series(3);
629 let window = ChartWindow::new(model.clone(), 5);
630 let (log, _h) = track(&window);
631
632 model.remove_point(s, 0);
633
634 let entries = log.borrow();
635 assert_eq!(entries.len(), 1);
636 assert_eq!(entries[0], ChartChange::SeriesDataReplaced { series: s });
637 drop(entries);
638 assert_eq!(window.point_count(s), 2);
639 }
640
641 #[test]
642 fn partial_window_tail_removal_emits_plain_removed() {
643 let (model, s) = one_series(5);
646 let window = ChartWindow::new(model.clone(), 10);
647 let (log, _h) = track(&window);
648
649 model.remove_point(s, 4); let entries = log.borrow();
652 assert_eq!(entries.len(), 1);
653 assert_eq!(
654 entries[0],
655 ChartChange::PointsRemoved {
656 series: s,
657 range: 4..5
658 }
659 );
660 drop(entries);
661 assert_eq!(window.point_count(s), 4);
662 let vals: Vec<f32> = (0..4)
663 .map(|i| window.with_point(s, i, |d| d.value).unwrap())
664 .collect();
665 assert_eq!(vals, vec![0.0, 1.0, 2.0, 3.0]);
666 }
667
668 #[test]
669 fn full_window_tail_removal_emits_removed_then_inserted() {
670 let (model, s) = one_series(5); let window = ChartWindow::new(model.clone(), 3);
674 assert_eq!(window.point_count(s), 3);
675 let (log, _h) = track(&window);
676
677 model.remove_point(s, 4); let entries = log.borrow();
680 assert_eq!(entries.len(), 2);
681 assert_eq!(
682 entries[0],
683 ChartChange::PointsRemoved {
684 series: s,
685 range: 2..3
686 }
687 );
688 assert_eq!(
689 entries[1],
690 ChartChange::PointsInserted {
691 series: s,
692 range: 0..1
693 }
694 );
695 drop(entries);
696
697 assert_eq!(window.point_count(s), 3);
698 let vals: Vec<f32> = (0..3)
699 .map(|i| window.with_point(s, i, |d| d.value).unwrap())
700 .collect();
701 assert_eq!(vals, vec![1.0, 2.0, 3.0]);
702 }
703
704 #[test]
705 fn update_in_and_out_of_window() {
706 let (model, s) = one_series(5);
707 let window = ChartWindow::new(model.clone(), 2); let (log, _h) = track(&window);
709
710 model.update_point(s, 4, 4, 40.0); model.update_point(s, 0, 0, 5.0); let entries = log.borrow();
714 assert_eq!(entries.len(), 1);
715 assert_eq!(
716 entries[0],
717 ChartChange::PointUpdated {
718 series: s,
719 index: 1
720 }
721 );
722 drop(entries);
723 assert_eq!(window.with_point(s, 1, |d| d.value), Some(40.0));
724 }
725
726 #[test]
727 fn reset_passes_through_and_rebuilds() {
728 let (model, s) = one_series(3);
729 let window = ChartWindow::new(model.clone(), 2);
730 let (log, _h) = track(&window);
731
732 model.clear();
733
734 assert_eq!(log.borrow().last(), Some(&ChartChange::Reset));
735 assert_eq!(window.point_count(s), 0);
736 }
737
738 #[test]
739 fn dropping_window_unregisters_upstream_observer() {
740 let model: ChartModel<i32> = ChartModel::new();
741 let before = model.observer_count();
742 let window = ChartWindow::new(model.clone(), 3);
743 assert_eq!(model.observer_count(), before + 1);
744 drop(window);
745 assert_eq!(model.observer_count(), before);
746 }
747
748 #[test]
749 fn set_window_size_rebuilds_and_emits_reset() {
750 let (model, s) = one_series(5);
751 let window = ChartWindow::new(model, 2);
752 assert_eq!(window.point_count(s), 2);
753 let (log, _h) = track(&window);
754
755 window.set_window_size(4);
756 assert_eq!(window.point_count(s), 4);
757 assert_eq!(log.borrow().last(), Some(&ChartChange::Reset));
758 }
759
760 #[test]
761 fn series_inserted_and_removed_pass_through() {
762 let model: ChartModel<i32> = ChartModel::new();
763 let window = ChartWindow::new(model.clone(), 3);
764 let (log, _h) = track(&window);
765
766 let s = model.add_series("new");
767 assert_eq!(
768 log.borrow().last(),
769 Some(&ChartChange::SeriesInserted {
770 index: 0,
771 series: s
772 })
773 );
774
775 model.remove_series(s);
776 assert_eq!(
777 log.borrow().last(),
778 Some(&ChartChange::SeriesRemoved { series: s })
779 );
780 assert_eq!(window.series_count(), 0);
781 }
782
783 #[test]
784 fn translate_multi_point_insert_not_full_to_overflow() {
785 let model: ChartModel<i32> = ChartModel::new();
799 let s = model.add_series("s");
800 model.push_point(s, 0, 0.0);
801 model.push_point(s, 1, 1.0); let series_ids_fn: SeriesIdsFn = {
804 let m = model.clone();
805 Rc::new(move || m.series_ids())
806 };
807 let point_count_fn: PointCountFn = {
808 let m = model.clone();
809 Rc::new(move |series| m.point_count(series))
810 };
811 let with_point_fn: WithPointFn<i32> = {
812 let m = model.clone();
813 Rc::new(move |series, idx, f| {
814 m.with_point(series, idx, |d| f(d));
815 })
816 };
817 let with_series_fn: WithSeriesFn = {
818 let m = model.clone();
819 Rc::new(move |series, f| {
820 m.with_series(series, |name, color, visible| f(name, color, visible));
821 })
822 };
823
824 let mut inner = ChartWindowInner {
825 series_ids_fn,
826 point_count_fn,
827 with_point_fn,
828 with_series_fn,
829 window_size: 3,
830 starts: HashMap::new(),
831 divergence: HashMap::new(),
832 observers: Vec::new(),
833 next_observer_id: 1,
834 _upstream_handle: None,
835 };
836 inner.rebuild_all(); model.push_point(s, 2, 2.0);
843 model.push_point(s, 3, 3.0);
844
845 let changes = translate(
846 &mut inner,
847 &ChartChange::PointsInserted {
848 series: s,
849 range: 2..4,
850 },
851 );
852
853 assert_eq!(
857 changes,
858 vec![
859 ChartChange::PointsRemoved {
860 series: s,
861 range: 0..1
862 },
863 ChartChange::PointsInserted {
864 series: s,
865 range: 1..3
866 },
867 ]
868 );
869 }
870
871 #[test]
872 fn translate_multi_point_tail_removal_from_full_to_not_full() {
873 let model: ChartModel<i32> = ChartModel::new();
883 let s = model.add_series("s");
884 for i in 0..5 {
885 model.push_point(s, i, i as f32);
886 }
887
888 let series_ids_fn: SeriesIdsFn = {
889 let m = model.clone();
890 Rc::new(move || m.series_ids())
891 };
892 let point_count_fn: PointCountFn = {
893 let m = model.clone();
894 Rc::new(move |series| m.point_count(series))
895 };
896 let with_point_fn: WithPointFn<i32> = {
897 let m = model.clone();
898 Rc::new(move |series, idx, f| {
899 m.with_point(series, idx, |d| f(d));
900 })
901 };
902 let with_series_fn: WithSeriesFn = {
903 let m = model.clone();
904 Rc::new(move |series, f| {
905 m.with_series(series, |name, color, visible| f(name, color, visible));
906 })
907 };
908
909 let mut inner = ChartWindowInner {
910 series_ids_fn,
911 point_count_fn,
912 with_point_fn,
913 with_series_fn,
914 window_size: 3,
915 starts: HashMap::new(),
916 divergence: HashMap::new(),
917 observers: Vec::new(),
918 next_observer_id: 1,
919 _upstream_handle: None,
920 };
921 inner.rebuild_all(); model.remove_point(s, 4);
928 model.remove_point(s, 3);
929 model.remove_point(s, 2);
930
931 let changes = translate(
932 &mut inner,
933 &ChartChange::PointsRemoved {
934 series: s,
935 range: 2..5,
936 },
937 );
938
939 assert_eq!(
943 changes,
944 vec![
945 ChartChange::PointsRemoved {
946 series: s,
947 range: 0..3
948 },
949 ChartChange::PointsInserted {
950 series: s,
951 range: 0..2
952 },
953 ]
954 );
955 }
956}