1use std::cell::RefCell;
45use std::rc::Rc;
46
47use slotmap::SlotMap;
48
49use teksilo_core::ObserverHandle;
50use teksilo_core::color_prop::ColorProp;
51use teksilo_core::signal::Signal;
52
53use crate::chart_change::{ChartChange, SeriesId};
54use crate::series_pattern::SeriesPattern;
55
56#[derive(Debug, Clone)]
59pub struct ChartDatum<T> {
60 pub category: T,
61 pub value: f32,
62 pub color: Option<ColorProp>,
63}
64
65impl<T> ChartDatum<T> {
66 pub fn new(category: T, value: f32) -> Self {
67 Self {
68 category,
69 value,
70 color: None,
71 }
72 }
73
74 pub fn with_color(mut self, color: impl Into<ColorProp>) -> Self {
77 self.color = Some(color.into());
78 self
79 }
80}
81
82pub struct ChartSeries<T> {
89 pub name: String,
90 pub color: Option<ColorProp>,
91 pub pattern: Option<SeriesPattern>,
98 pub visible: bool,
99 pub points: Vec<ChartDatum<T>>,
100}
101
102impl<T> std::fmt::Debug for ChartSeries<T>
103where
104 T: std::fmt::Debug,
105{
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 f.debug_struct("ChartSeries")
108 .field("name", &self.name)
109 .field("pattern", &self.pattern)
110 .field("visible", &self.visible)
111 .field("len", &self.points.len())
112 .finish()
113 }
114}
115
116impl<T> Clone for ChartSeries<T>
117where
118 T: Clone,
119{
120 fn clone(&self) -> Self {
121 Self {
122 name: self.name.clone(),
123 color: self.color.clone(),
124 pattern: self.pattern,
125 visible: self.visible,
126 points: self.points.clone(),
127 }
128 }
129}
130
131impl<T> ChartSeries<T> {
132 pub fn new(name: impl Into<String>) -> Self {
133 Self {
134 name: name.into(),
135 color: None,
136 pattern: None,
137 visible: true,
138 points: Vec::new(),
139 }
140 }
141
142 pub fn color(mut self, color: impl Into<ColorProp>) -> Self {
143 self.color = Some(color.into());
144 self
145 }
146
147 pub fn pattern(mut self, pattern: SeriesPattern) -> Self {
150 self.pattern = Some(pattern);
151 self
152 }
153
154 pub fn visibility(mut self, visible: bool) -> Self {
155 self.visible = visible;
156 self
157 }
158
159 pub fn push(&mut self, category: T, value: f32) {
160 self.points.push(ChartDatum::new(category, value));
161 }
162
163 pub fn data(mut self, points: Vec<ChartDatum<T>>) -> Self {
164 self.points = points;
165 self
166 }
167}
168
169pub struct SeriesView<'a, T> {
172 pub id: SeriesId,
173 pub name: &'a str,
174 pub color: Option<&'a ColorProp>,
175 pub pattern: Option<SeriesPattern>,
178 pub visible: bool,
179 pub points: &'a [ChartDatum<T>],
180}
181
182struct SeriesEntry<T> {
183 name: String,
184 color: Option<ColorProp>,
185 pattern: Option<SeriesPattern>,
186 visible: bool,
187 points: Vec<ChartDatum<T>>,
188}
189
190struct ObserverEntry {
191 id: u64,
192 callback: Rc<dyn Fn(&ChartChange)>,
193}
194
195fn color_prop_eq(a: &ColorProp, b: &ColorProp) -> bool {
203 match (a, b) {
204 (ColorProp::Static(x), ColorProp::Static(y)) => x == y,
205 (ColorProp::Bound(x), ColorProp::Bound(y)) => Signal::same(x, y),
206 (ColorProp::TextRole(x), ColorProp::TextRole(y)) => x == y,
207 (ColorProp::SurfaceRole(x), ColorProp::SurfaceRole(y)) => x == y,
208 (ColorProp::BorderRole(x), ColorProp::BorderRole(y)) => x == y,
209 (ColorProp::DynamicTextRole(x), ColorProp::DynamicTextRole(y)) => Signal::same(x, y),
210 (ColorProp::DynamicSurfaceRole(x), ColorProp::DynamicSurfaceRole(y)) => Signal::same(x, y),
211 (ColorProp::DynamicBorderRole(x), ColorProp::DynamicBorderRole(y)) => Signal::same(x, y),
212 _ => false,
213 }
214}
215
216struct ChartModelInner<T> {
217 arena: SlotMap<slotmap::DefaultKey, SeriesEntry<T>>,
218 order: Vec<SeriesId>,
219 observers: Vec<ObserverEntry>,
220 next_observer_id: u64,
221 structure_version: Signal<u64>,
223 style_version: Signal<u64>,
225 #[cfg(debug_assertions)]
231 debug_adapter: Option<Rc<dyn crate::debug_registry::ModelDebug>>,
232}
233
234pub struct ChartModel<T: 'static> {
240 inner: Rc<RefCell<ChartModelInner<T>>>,
241}
242
243impl<T: 'static> ChartModel<T> {
244 pub fn new() -> Self {
246 Self {
247 inner: Rc::new(RefCell::new(ChartModelInner {
248 arena: SlotMap::new(),
249 order: Vec::new(),
250 observers: Vec::new(),
251 next_observer_id: 1,
252 structure_version: Signal::new(0),
253 style_version: Signal::new(0),
254 #[cfg(debug_assertions)]
255 debug_adapter: None,
256 })),
257 }
258 }
259
260 pub fn from_series_vec(series: Vec<ChartSeries<T>>) -> Self {
264 let model = Self::new();
265 {
266 let mut guard = model.inner.borrow_mut();
267 for s in series {
268 let key = guard.arena.insert(SeriesEntry {
269 name: s.name,
270 color: s.color,
271 pattern: s.pattern,
272 visible: s.visible,
273 points: s.points,
274 });
275 let id = SeriesId::from_key(key);
276 guard.order.push(id);
277 }
278 }
279 model
280 }
281
282 pub fn from_points(points: Vec<ChartDatum<T>>) -> Self {
286 Self::from_series_vec(vec![ChartSeries::new(String::new()).data(points)])
287 }
288
289 pub fn only_series(&self) -> Option<SeriesId> {
291 let guard = self.inner.borrow();
292 if guard.order.len() == 1 {
293 Some(guard.order[0])
294 } else {
295 None
296 }
297 }
298
299 pub fn add_series(&self, name: impl Into<String>) -> SeriesId {
303 let (id, index) = {
304 let mut guard = self.inner.borrow_mut();
305 let key = guard.arena.insert(SeriesEntry {
306 name: name.into(),
307 color: None,
308 pattern: None,
309 visible: true,
310 points: Vec::new(),
311 });
312 let id = SeriesId::from_key(key);
313 let index = guard.order.len();
314 guard.order.push(id);
315 (id, index)
316 };
317 self.notify(ChartChange::SeriesInserted { index, series: id });
318 self.bump_structure();
319 id
320 }
321
322 pub fn insert_series(&self, index: usize, name: impl Into<String>) -> SeriesId {
327 let id = {
328 let mut guard = self.inner.borrow_mut();
329 let key = guard.arena.insert(SeriesEntry {
330 name: name.into(),
331 color: None,
332 pattern: None,
333 visible: true,
334 points: Vec::new(),
335 });
336 let id = SeriesId::from_key(key);
337 guard.order.insert(index, id);
338 id
339 };
340 self.notify(ChartChange::SeriesInserted { index, series: id });
341 self.bump_structure();
342 id
343 }
344
345 pub fn remove_series(&self, series: SeriesId) {
350 {
351 let mut guard = self.inner.borrow_mut();
352 guard.arena.remove(series.key()).expect("unknown SeriesId");
353 guard.order.retain(|&id| id != series);
354 }
355 self.notify(ChartChange::SeriesRemoved { series });
356 self.bump_structure();
357 }
358
359 pub fn rename_series(&self, series: SeriesId, name: impl Into<String>) {
365 let name = name.into();
366 let changed = {
367 let mut guard = self.inner.borrow_mut();
368 let entry = &mut guard.arena[series.key()];
369 if entry.name == name {
370 false
371 } else {
372 entry.name = name;
373 true
374 }
375 };
376 if !changed {
377 return;
378 }
379 self.notify(ChartChange::SeriesRenamed { series });
380 self.bump_structure();
381 }
382
383 pub fn set_series_color(&self, series: SeriesId, color: impl Into<ColorProp>) {
391 let color = color.into();
392 let changed = {
393 let mut guard = self.inner.borrow_mut();
394 let entry = &mut guard.arena[series.key()];
395 if entry
396 .color
397 .as_ref()
398 .is_some_and(|c| color_prop_eq(c, &color))
399 {
400 false
401 } else {
402 entry.color = Some(color);
403 true
404 }
405 };
406 if !changed {
407 return;
408 }
409 self.notify(ChartChange::SeriesColorChanged { series });
410 self.bump_style();
411 }
412
413 pub fn clear_series_color(&self, series: SeriesId) {
420 let changed = {
421 let mut guard = self.inner.borrow_mut();
422 let entry = &mut guard.arena[series.key()];
423 if entry.color.is_none() {
424 false
425 } else {
426 entry.color = None;
427 true
428 }
429 };
430 if !changed {
431 return;
432 }
433 self.notify(ChartChange::SeriesColorChanged { series });
434 self.bump_style();
435 }
436
437 pub fn set_series_pattern(&self, series: SeriesId, pattern: SeriesPattern) {
444 let changed = {
445 let mut guard = self.inner.borrow_mut();
446 let entry = &mut guard.arena[series.key()];
447 if entry.pattern == Some(pattern) {
448 false
449 } else {
450 entry.pattern = Some(pattern);
451 true
452 }
453 };
454 if !changed {
455 return;
456 }
457 self.notify(ChartChange::SeriesPatternChanged { series });
458 self.bump_style();
459 }
460
461 pub fn clear_series_pattern(&self, series: SeriesId) {
467 let changed = {
468 let mut guard = self.inner.borrow_mut();
469 let entry = &mut guard.arena[series.key()];
470 if entry.pattern.is_none() {
471 false
472 } else {
473 entry.pattern = None;
474 true
475 }
476 };
477 if !changed {
478 return;
479 }
480 self.notify(ChartChange::SeriesPatternChanged { series });
481 self.bump_style();
482 }
483
484 pub fn set_series_visible(&self, series: SeriesId, visible: bool) {
490 let changed = {
491 let mut guard = self.inner.borrow_mut();
492 let entry = &mut guard.arena[series.key()];
493 if entry.visible == visible {
494 false
495 } else {
496 entry.visible = visible;
497 true
498 }
499 };
500 if !changed {
501 return;
502 }
503 self.notify(ChartChange::SeriesVisibilityChanged { series });
504 self.bump_structure();
505 }
506
507 pub fn move_series(&self, series: SeriesId, to: usize) {
513 let from = {
514 let guard = self.inner.borrow();
515 guard
516 .order
517 .iter()
518 .position(|&id| id == series)
519 .expect("unknown SeriesId")
520 };
521 if from == to {
522 return;
523 }
524 {
525 let mut guard = self.inner.borrow_mut();
526 let id = guard.order.remove(from);
527 guard.order.insert(to, id);
528 }
529 self.notify(ChartChange::SeriesMoved { series, from, to });
530 self.bump_structure();
531 }
532
533 pub fn clear(&self) {
535 {
536 let mut guard = self.inner.borrow_mut();
537 guard.arena.clear();
538 guard.order.clear();
539 }
540 self.notify(ChartChange::Reset);
541 self.bump_structure();
542 }
543
544 pub fn push_point(&self, series: SeriesId, category: T, value: f32) {
551 let index = {
552 let mut guard = self.inner.borrow_mut();
553 let entry = &mut guard.arena[series.key()];
554 let index = entry.points.len();
555 entry.points.push(ChartDatum::new(category, value));
556 index
557 };
558 self.notify(ChartChange::PointsInserted {
559 series,
560 range: index..index + 1,
561 });
562 self.bump_structure();
563 }
564
565 pub fn insert_point(&self, series: SeriesId, index: usize, category: T, value: f32) {
570 {
571 let mut guard = self.inner.borrow_mut();
572 guard.arena[series.key()]
573 .points
574 .insert(index, ChartDatum::new(category, value));
575 }
576 self.notify(ChartChange::PointsInserted {
577 series,
578 range: index..index + 1,
579 });
580 self.bump_structure();
581 }
582
583 pub fn remove_point(&self, series: SeriesId, index: usize) -> ChartDatum<T> {
588 let datum = {
589 let mut guard = self.inner.borrow_mut();
590 guard.arena[series.key()].points.remove(index)
591 };
592 self.notify(ChartChange::PointsRemoved {
593 series,
594 range: index..index + 1,
595 });
596 self.bump_structure();
597 datum
598 }
599
600 pub fn update_point(&self, series: SeriesId, index: usize, category: T, value: f32) {
605 {
606 let mut guard = self.inner.borrow_mut();
607 guard.arena[series.key()].points[index] = ChartDatum::new(category, value);
608 }
609 self.notify(ChartChange::PointUpdated { series, index });
610 self.bump_structure();
611 }
612
613 pub fn replace_series_data(&self, series: SeriesId, points: Vec<ChartDatum<T>>) {
618 {
619 let mut guard = self.inner.borrow_mut();
620 guard.arena[series.key()].points = points;
621 }
622 self.notify(ChartChange::SeriesDataReplaced { series });
623 self.bump_structure();
624 }
625
626 pub fn series_count(&self) -> usize {
630 self.inner.borrow().order.len()
631 }
632
633 pub fn series_ids(&self) -> Vec<SeriesId> {
635 self.inner.borrow().order.clone()
636 }
637
638 pub fn series_id_at(&self, index: usize) -> Option<SeriesId> {
640 self.inner.borrow().order.get(index).copied()
641 }
642
643 pub fn series_index_of(&self, series: SeriesId) -> Option<usize> {
645 self.inner
646 .borrow()
647 .order
648 .iter()
649 .position(|&id| id == series)
650 }
651
652 pub fn point_count(&self, series: SeriesId) -> usize {
654 self.inner
655 .borrow()
656 .arena
657 .get(series.key())
658 .map(|e| e.points.len())
659 .unwrap_or(0)
660 }
661
662 pub fn with_series<R>(
665 &self,
666 series: SeriesId,
667 f: impl FnOnce(&str, Option<&ColorProp>, bool) -> R,
668 ) -> Option<R> {
669 let guard = self.inner.borrow();
670 guard
671 .arena
672 .get(series.key())
673 .map(|e| f(&e.name, e.color.as_ref(), e.visible))
674 }
675
676 pub fn with_point<R>(
679 &self,
680 series: SeriesId,
681 index: usize,
682 f: impl FnOnce(&ChartDatum<T>) -> R,
683 ) -> Option<R> {
684 let guard = self.inner.borrow();
685 guard
686 .arena
687 .get(series.key())
688 .and_then(|e| e.points.get(index))
689 .map(f)
690 }
691
692 pub fn with_series_view<R>(
695 &self,
696 series: SeriesId,
697 f: impl FnOnce(SeriesView<'_, T>) -> R,
698 ) -> Option<R> {
699 let guard = self.inner.borrow();
700 guard.arena.get(series.key()).map(|e| {
701 f(SeriesView {
702 id: series,
703 name: &e.name,
704 color: e.color.as_ref(),
705 pattern: e.pattern,
706 visible: e.visible,
707 points: &e.points,
708 })
709 })
710 }
711
712 pub fn with_all_series<R>(&self, f: impl FnOnce(&[SeriesView<'_, T>]) -> R) -> R {
714 let guard = self.inner.borrow();
715 let views: Vec<SeriesView<'_, T>> = guard
716 .order
717 .iter()
718 .filter_map(|&id| {
719 guard.arena.get(id.key()).map(|e| SeriesView {
720 id,
721 name: &e.name,
722 color: e.color.as_ref(),
723 pattern: e.pattern,
724 visible: e.visible,
725 points: &e.points,
726 })
727 })
728 .collect();
729 f(&views)
730 }
731
732 pub fn structure_version(&self) -> Signal<u64> {
743 self.inner.borrow().structure_version.clone()
744 }
745
746 pub fn style_version(&self) -> Signal<u64> {
750 self.inner.borrow().style_version.clone()
751 }
752
753 pub fn observe_changes(&self, f: impl Fn(&ChartChange) + 'static) -> ObserverHandle {
773 let mut guard = self.inner.borrow_mut();
774 let id = guard.next_observer_id;
775 guard.next_observer_id += 1;
776 guard.observers.push(ObserverEntry {
777 id,
778 callback: Rc::new(f),
779 });
780 let inner = self.inner.clone();
781 ObserverHandle::new(
782 self.inner.clone(),
783 id,
784 Rc::new(move |observer_id| {
785 inner.borrow_mut().observers.retain(|e| e.id != observer_id);
786 }),
787 )
788 }
789
790 fn notify(&self, change: ChartChange) {
791 let callbacks: Vec<Rc<dyn Fn(&ChartChange)>> = self
792 .inner
793 .borrow()
794 .observers
795 .iter()
796 .map(|e| e.callback.clone())
797 .collect();
798 for cb in &callbacks {
799 cb(&change);
800 }
801 }
802
803 fn bump_structure(&self) {
808 let sig = self.inner.borrow().structure_version.clone();
809 sig.set(sig.get().wrapping_add(1));
810 }
811
812 fn bump_style(&self) {
815 let sig = self.inner.borrow().style_version.clone();
816 sig.set(sig.get().wrapping_add(1));
817 }
818}
819
820impl<T: std::fmt::Debug + 'static> ChartModel<T> {
821 pub fn debug_named(self, _name: impl Into<String>) -> Self {
829 #[cfg(debug_assertions)]
830 {
831 let weak = Rc::downgrade(&self.inner);
832 let adapter: Rc<dyn crate::debug_registry::ModelDebug> =
833 Rc::new(ChartModelDebug::<T> { weak });
834 let name = _name.into();
835 crate::debug_registry::register(name, Rc::downgrade(&adapter));
836 self.inner.borrow_mut().debug_adapter = Some(adapter);
837 }
838 self
839 }
840}
841
842#[cfg(debug_assertions)]
843struct ChartModelDebug<T> {
844 weak: std::rc::Weak<RefCell<ChartModelInner<T>>>,
845}
846
847#[cfg(debug_assertions)]
848impl<T: std::fmt::Debug + 'static> crate::debug_registry::ModelDebug for ChartModelDebug<T> {
849 fn kind(&self) -> &'static str {
850 "ChartModel"
851 }
852 fn len(&self) -> usize {
853 self.weak
854 .upgrade()
855 .map(|inner| inner.borrow().arena.values().map(|e| e.points.len()).sum())
856 .unwrap_or(0)
857 }
858 fn debug_dump(&self, out: &mut dyn std::fmt::Write) {
859 let Some(inner) = self.weak.upgrade() else {
860 return;
861 };
862 let guard = inner.borrow();
863 for (i, &id) in guard.order.iter().enumerate() {
864 if let Some(e) = guard.arena.get(id.key()) {
865 let _ = writeln!(
866 out,
867 "[{}] {:?} ({} pts, visible={})",
868 i,
869 e.name,
870 e.points.len(),
871 e.visible
872 );
873 }
874 }
875 }
876}
877
878impl<T: 'static> Default for ChartModel<T> {
879 fn default() -> Self {
880 Self::new()
881 }
882}
883
884impl<T: 'static> Clone for ChartModel<T> {
885 fn clone(&self) -> Self {
886 Self {
887 inner: self.inner.clone(),
888 }
889 }
890}
891
892impl<T: std::fmt::Debug + 'static> std::fmt::Debug for ChartModel<T> {
893 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
894 let guard = self.inner.borrow();
895 f.debug_struct("ChartModel")
896 .field("series_count", &guard.order.len())
897 .finish()
898 }
899}
900
901#[cfg(test)]
902impl<T: 'static> ChartModel<T> {
903 pub(crate) fn observer_count(&self) -> usize {
906 self.inner.borrow().observers.len()
907 }
908}
909
910#[cfg(test)]
911mod tests {
912 use std::cell::Cell;
913
914 use super::*;
915
916 #[test]
917 fn datum_with_color_sets_a_per_point_override() {
918 use teksilo_core::color_prop::ColorProp;
919 use teksilo_tokens::SurfaceRole;
920 let plain = ChartDatum::new("Q1".to_string(), 5.0);
921 assert!(plain.color.is_none(), "a plain datum has no color override");
922 let colored = ChartDatum::new("Q1".to_string(), 5.0).with_color(SurfaceRole::StatusError);
923 assert!(matches!(
924 colored.color,
925 Some(ColorProp::SurfaceRole(SurfaceRole::StatusError))
926 ));
927 let _ = ChartDatum::new("Q2".to_string(), 1.0);
929 }
930
931 fn sample() -> (ChartModel<String>, SeriesId, SeriesId) {
932 let model = ChartModel::from_series_vec(vec![
933 ChartSeries::new("Revenue").data(vec![
934 ChartDatum::new("Q1".to_string(), 10.0),
935 ChartDatum::new("Q2".to_string(), 20.0),
936 ]),
937 ChartSeries::new("Costs").data(vec![ChartDatum::new("Q1".to_string(), 5.0)]),
938 ]);
939 let a = model.series_id_at(0).unwrap();
940 let b = model.series_id_at(1).unwrap();
941 (model, a, b)
942 }
943
944 #[test]
945 fn from_series_vec_builds_correctly() {
946 let (model, a, b) = sample();
947 assert_eq!(model.series_count(), 2);
948 assert_eq!(model.point_count(a), 2);
949 assert_eq!(model.point_count(b), 1);
950 assert_eq!(
951 model.with_series(a, |name, _, visible| (name.to_string(), visible)),
952 Some(("Revenue".to_string(), true))
953 );
954 }
955
956 #[test]
957 fn new_is_empty() {
958 let model: ChartModel<String> = ChartModel::new();
959 assert_eq!(model.series_count(), 0);
960 assert_eq!(model.only_series(), None);
961 }
962
963 #[test]
964 fn only_series_some_iff_exactly_one() {
965 let model: ChartModel<String> = ChartModel::new();
966 assert_eq!(model.only_series(), None);
967 let a = model.add_series("A");
968 assert_eq!(model.only_series(), Some(a));
969 model.add_series("B");
970 assert_eq!(model.only_series(), None);
971 }
972
973 #[test]
974 fn from_points_builds_single_anonymous_series() {
975 let model = ChartModel::from_points(vec![
976 ChartDatum::new("a".to_string(), 1.0),
977 ChartDatum::new("b".to_string(), 2.0),
978 ]);
979 assert_eq!(model.series_count(), 1);
980 let s = model.only_series().unwrap();
981 assert_eq!(model.point_count(s), 2);
982 assert_eq!(model.with_series(s, |_, _, visible| visible), Some(true));
983 }
984
985 fn track_changes(
986 model: &ChartModel<String>,
987 ) -> (Rc<RefCell<Vec<ChartChange>>>, ObserverHandle) {
988 let log: Rc<RefCell<Vec<ChartChange>>> = Rc::new(RefCell::new(Vec::new()));
989 let l = log.clone();
990 let handle = model.observe_changes(move |c| l.borrow_mut().push(c.clone()));
991 (log, handle)
992 }
993
994 #[test]
995 fn add_series_emits_inserted_and_bumps_structure() {
996 let model: ChartModel<String> = ChartModel::new();
997 let structure = model.structure_version();
998 let style = model.style_version();
999 let (log, _handle) = track_changes(&model);
1000
1001 let s = model.add_series("A");
1002 assert_eq!(log.borrow().len(), 1);
1003 assert_eq!(
1004 log.borrow()[0],
1005 ChartChange::SeriesInserted {
1006 index: 0,
1007 series: s
1008 }
1009 );
1010 assert_eq!(structure.get(), 1);
1011 assert_eq!(style.get(), 0);
1012 }
1013
1014 #[test]
1015 fn observer_sees_pre_bump_structure_version_during_notify() {
1016 let model: ChartModel<String> = ChartModel::new();
1017 let structure = model.structure_version();
1018 let seen_during_callback: Rc<Cell<Option<u64>>> = Rc::new(Cell::new(None));
1019 let seen = seen_during_callback.clone();
1020 let sig = structure.clone();
1021 let _handle = model.observe_changes(move |_| seen.set(Some(sig.get())));
1022
1023 let before = structure.get();
1024 model.add_series("A");
1025 let after = structure.get();
1026
1027 assert_eq!(after, before + 1, "the mutation did bump the signal");
1028 assert_eq!(
1029 seen_during_callback.get(),
1030 Some(before),
1031 "notify runs before the version bump, so a ChartChange observer \
1032 reading structure_version() synchronously sees the pre-bump value"
1033 );
1034 }
1035
1036 #[test]
1037 fn observer_sees_pre_bump_style_version_during_notify() {
1038 let (model, a, _b) = sample();
1039 let style = model.style_version();
1040 let seen_during_callback: Rc<Cell<Option<u64>>> = Rc::new(Cell::new(None));
1041 let seen = seen_during_callback.clone();
1042 let sig = style.clone();
1043 let _handle = model.observe_changes(move |_| seen.set(Some(sig.get())));
1044
1045 let before = style.get();
1046 model.set_series_color(a, test_color());
1047 let after = style.get();
1048
1049 assert_eq!(after, before + 1);
1050 assert_eq!(seen_during_callback.get(), Some(before));
1051 }
1052
1053 #[test]
1054 fn insert_series_at_index() {
1055 let (model, a, b) = sample();
1056 let c = model.insert_series(1, "Middle");
1057 assert_eq!(model.series_ids(), vec![a, c, b]);
1058 }
1059
1060 #[test]
1061 fn remove_series_emits_removed_and_bumps_structure() {
1062 let (model, a, _b) = sample();
1063 let structure_before = model.structure_version().get();
1064 let (log, _handle) = track_changes(&model);
1065
1066 model.remove_series(a);
1067 assert_eq!(log.borrow().len(), 1);
1068 assert_eq!(log.borrow()[0], ChartChange::SeriesRemoved { series: a });
1069 assert_eq!(model.series_count(), 1);
1070 assert!(model.structure_version().get() > structure_before);
1071 }
1072
1073 #[test]
1074 fn rename_series_emits_renamed_and_bumps_structure() {
1075 let (model, a, _b) = sample();
1076 let style_before = model.style_version().get();
1077 let (log, _handle) = track_changes(&model);
1078
1079 model.rename_series(a, "New Name");
1080 assert_eq!(log.borrow().len(), 1);
1081 assert_eq!(log.borrow()[0], ChartChange::SeriesRenamed { series: a });
1082 assert_eq!(
1083 model.with_series(a, |name, _, _| name.to_string()),
1084 Some("New Name".to_string())
1085 );
1086 assert_eq!(
1087 model.style_version().get(),
1088 style_before,
1089 "renaming is not a style change"
1090 );
1091 }
1092
1093 #[test]
1094 fn rename_series_noop_does_not_notify() {
1095 let (model, a, _b) = sample();
1096 let structure_before = model.structure_version().get();
1097 let (log, _handle) = track_changes(&model);
1098
1099 model.rename_series(a, "Revenue"); assert_eq!(log.borrow().len(), 0);
1101 assert_eq!(model.structure_version().get(), structure_before);
1102 }
1103
1104 #[test]
1105 fn set_series_color_bumps_style_not_structure() {
1106 let (model, a, _b) = sample();
1107 let structure_before = model.structure_version().get();
1108 let style_before = model.style_version().get();
1109 let (log, _handle) = track_changes(&model);
1110
1111 model.set_series_color(a, test_color());
1112 assert_eq!(log.borrow().len(), 1);
1113 assert_eq!(
1114 log.borrow()[0],
1115 ChartChange::SeriesColorChanged { series: a }
1116 );
1117 assert_eq!(
1118 model.structure_version().get(),
1119 structure_before,
1120 "color change must not bump structure_version"
1121 );
1122 assert!(model.style_version().get() > style_before);
1123 assert!(model.with_series(a, |_, color, _| color.is_some()).unwrap());
1124 }
1125
1126 #[test]
1127 fn set_series_color_noop_does_not_notify() {
1128 let (model, a, _b) = sample();
1129 model.set_series_color(a, test_color());
1130 let structure_before = model.structure_version().get();
1131 let style_before = model.style_version().get();
1132 let (log, _handle) = track_changes(&model);
1133
1134 model.set_series_color(a, test_color()); assert_eq!(log.borrow().len(), 0);
1136 assert_eq!(model.structure_version().get(), structure_before);
1137 assert_eq!(model.style_version().get(), style_before);
1138 }
1139
1140 #[test]
1141 fn clear_series_color_bumps_style_and_clears() {
1142 let (model, a, _b) = sample();
1143 model.set_series_color(a, test_color());
1144 let style_before = model.style_version().get();
1145 let (log, _handle) = track_changes(&model);
1146
1147 model.clear_series_color(a);
1148 assert_eq!(log.borrow().len(), 1);
1149 assert_eq!(
1150 log.borrow()[0],
1151 ChartChange::SeriesColorChanged { series: a }
1152 );
1153 assert!(model.style_version().get() > style_before);
1154 assert!(!model.with_series(a, |_, color, _| color.is_some()).unwrap());
1155 }
1156
1157 #[test]
1158 fn clear_series_color_noop_does_not_notify_when_already_none() {
1159 let (model, a, _b) = sample();
1160 let structure_before = model.structure_version().get();
1162 let style_before = model.style_version().get();
1163 let (log, _handle) = track_changes(&model);
1164
1165 model.clear_series_color(a);
1166 assert_eq!(log.borrow().len(), 0);
1167 assert_eq!(model.structure_version().get(), structure_before);
1168 assert_eq!(model.style_version().get(), style_before);
1169 }
1170
1171 #[test]
1172 fn set_series_visible_bumps_structure() {
1173 let (model, a, _b) = sample();
1174 let structure_before = model.structure_version().get();
1175 let (log, _handle) = track_changes(&model);
1176
1177 model.set_series_visible(a, false);
1178 assert_eq!(log.borrow().len(), 1);
1179 assert_eq!(
1180 log.borrow()[0],
1181 ChartChange::SeriesVisibilityChanged { series: a }
1182 );
1183 assert!(model.structure_version().get() > structure_before);
1184 assert_eq!(model.with_series(a, |_, _, visible| visible), Some(false));
1185 }
1186
1187 #[test]
1188 fn set_series_visible_noop_does_not_notify() {
1189 let (model, a, _b) = sample();
1190 let structure_before = model.structure_version().get();
1191 let (log, _handle) = track_changes(&model);
1192
1193 model.set_series_visible(a, true); assert_eq!(log.borrow().len(), 0);
1195 assert_eq!(model.structure_version().get(), structure_before);
1196 }
1197
1198 #[test]
1199 fn move_series_emits_moved_and_bumps_structure() {
1200 let (model, a, b) = sample();
1201 let structure_before = model.structure_version().get();
1202 let (log, _handle) = track_changes(&model);
1203
1204 model.move_series(a, 1);
1205 assert_eq!(model.series_ids(), vec![b, a]);
1206 assert_eq!(log.borrow().len(), 1);
1207 assert_eq!(
1208 log.borrow()[0],
1209 ChartChange::SeriesMoved {
1210 series: a,
1211 from: 0,
1212 to: 1
1213 }
1214 );
1215 assert!(model.structure_version().get() > structure_before);
1216 }
1217
1218 #[test]
1219 fn move_series_noop_does_not_notify() {
1220 let (model, a, _b) = sample();
1221 let structure_before = model.structure_version().get();
1222 let (log, _handle) = track_changes(&model);
1223
1224 model.move_series(a, 0); assert_eq!(log.borrow().len(), 0);
1226 assert_eq!(model.structure_version().get(), structure_before);
1227 }
1228
1229 #[test]
1230 fn push_point_emits_inserted_and_bumps_structure() {
1231 let (model, a, _b) = sample();
1232 let structure_before = model.structure_version().get();
1233 let (log, _handle) = track_changes(&model);
1234
1235 model.push_point(a, "Q3".to_string(), 30.0);
1236 assert_eq!(model.point_count(a), 3);
1237 assert_eq!(log.borrow().len(), 1);
1238 assert_eq!(
1239 log.borrow()[0],
1240 ChartChange::PointsInserted {
1241 series: a,
1242 range: 2..3
1243 }
1244 );
1245 assert!(model.structure_version().get() > structure_before);
1246 }
1247
1248 #[test]
1249 fn insert_point_at_index() {
1250 let (model, a, _b) = sample();
1251 model.insert_point(a, 1, "Q1.5".to_string(), 15.0);
1252 assert_eq!(model.point_count(a), 3);
1253 assert_eq!(
1254 model.with_point(a, 1, |d| d.category.clone()),
1255 Some("Q1.5".to_string())
1256 );
1257 }
1258
1259 #[test]
1260 fn remove_point_emits_removed_and_returns_datum() {
1261 let (model, a, _b) = sample();
1262 let (log, _handle) = track_changes(&model);
1263
1264 let removed = model.remove_point(a, 0);
1265 assert_eq!(removed.category, "Q1");
1266 assert_eq!(model.point_count(a), 1);
1267 assert_eq!(log.borrow().len(), 1);
1268 assert_eq!(
1269 log.borrow()[0],
1270 ChartChange::PointsRemoved {
1271 series: a,
1272 range: 0..1
1273 }
1274 );
1275 }
1276
1277 #[test]
1278 fn update_point_bumps_structure_not_style() {
1279 let (model, a, _b) = sample();
1280 let structure_before = model.structure_version().get();
1281 let style_before = model.style_version().get();
1282 let (log, _handle) = track_changes(&model);
1283
1284 model.update_point(a, 0, "Q1-revised".to_string(), 99.0);
1285 assert_eq!(log.borrow().len(), 1);
1286 assert_eq!(
1287 log.borrow()[0],
1288 ChartChange::PointUpdated {
1289 series: a,
1290 index: 0
1291 }
1292 );
1293 assert!(model.structure_version().get() > structure_before);
1294 assert_eq!(model.style_version().get(), style_before);
1295 assert_eq!(model.with_point(a, 0, |d| d.value), Some(99.0));
1296 }
1297
1298 #[test]
1299 fn replace_series_data_emits_replaced() {
1300 let (model, a, _b) = sample();
1301 let (log, _handle) = track_changes(&model);
1302
1303 model.replace_series_data(a, vec![ChartDatum::new("X".to_string(), 1.0)]);
1304 assert_eq!(model.point_count(a), 1);
1305 assert_eq!(log.borrow().len(), 1);
1306 assert_eq!(
1307 log.borrow()[0],
1308 ChartChange::SeriesDataReplaced { series: a }
1309 );
1310 }
1311
1312 #[test]
1313 fn clear_emits_reset() {
1314 let (model, _a, _b) = sample();
1315 let (log, _handle) = track_changes(&model);
1316
1317 model.clear();
1318 assert_eq!(model.series_count(), 0);
1319 assert_eq!(log.borrow().len(), 1);
1320 assert_eq!(log.borrow()[0], ChartChange::Reset);
1321 }
1322
1323 #[test]
1324 fn observer_removed_on_handle_drop() {
1325 let model: ChartModel<String> = ChartModel::new();
1326 let count = Rc::new(Cell::new(0));
1327 let c = count.clone();
1328 let handle = model.observe_changes(move |_| c.set(c.get() + 1));
1329
1330 model.add_series("A");
1331 assert_eq!(count.get(), 1);
1332
1333 drop(handle);
1334 model.add_series("B");
1335 assert_eq!(count.get(), 1);
1336 }
1337
1338 #[test]
1339 fn multiple_observers() {
1340 let model: ChartModel<String> = ChartModel::new();
1341 let count = Rc::new(Cell::new(0));
1342 let c1 = count.clone();
1343 let c2 = count.clone();
1344 let _h1 = model.observe_changes(move |_| c1.set(c1.get() + 1));
1345 let _h2 = model.observe_changes(move |_| c2.set(c2.get() + 1));
1346
1347 model.add_series("A");
1348 assert_eq!(count.get(), 2);
1349 }
1350
1351 #[test]
1352 fn clone_shares_data_and_observers() {
1353 let (model, _a, _b) = sample();
1354 let clone = model.clone();
1355 let count = Rc::new(Cell::new(0));
1356 let c = count.clone();
1357 let _handle = model.observe_changes(move |_| c.set(c.get() + 1));
1358
1359 clone.add_series("New");
1360 assert_eq!(model.series_count(), 3);
1361 assert_eq!(count.get(), 1);
1362 }
1363
1364 #[test]
1365 fn with_all_series_returns_views_in_order() {
1366 let (model, a, b) = sample();
1367 let ids: Vec<SeriesId> =
1368 model.with_all_series(|views| views.iter().map(|v| v.id).collect());
1369 assert_eq!(ids, vec![a, b]);
1370 let names: Vec<String> =
1371 model.with_all_series(|views| views.iter().map(|v| v.name.to_string()).collect());
1372 assert_eq!(names, vec!["Revenue".to_string(), "Costs".to_string()]);
1373 }
1374
1375 #[test]
1376 fn with_point_out_of_bounds_returns_none() {
1377 let (model, a, _b) = sample();
1378 assert_eq!(model.with_point(a, 99, |d| d.value), None);
1379 }
1380
1381 fn stale_id(model: &ChartModel<String>) -> SeriesId {
1388 let ghost = model.add_series("Ghost");
1389 model.remove_series(ghost);
1390 ghost
1391 }
1392
1393 #[test]
1394 fn with_series_unknown_id_returns_none() {
1395 let (model, _a, _b) = sample();
1396 let ghost = stale_id(&model);
1397 assert_eq!(model.with_series(ghost, |_, _, _| ()), None);
1398 assert_eq!(model.with_point(ghost, 0, |d| d.value), None);
1399 }
1400
1401 #[test]
1402 #[should_panic(expected = "unknown SeriesId")]
1403 fn remove_series_unknown_id_panics() {
1404 let (model, _a, _b) = sample();
1405 let ghost = stale_id(&model);
1406 model.remove_series(ghost);
1407 }
1408
1409 #[test]
1410 #[should_panic]
1411 fn rename_series_unknown_id_panics() {
1412 let (model, _a, _b) = sample();
1413 let ghost = stale_id(&model);
1414 model.rename_series(ghost, "X");
1415 }
1416
1417 #[test]
1418 #[should_panic]
1419 fn push_point_unknown_series_panics() {
1420 let (model, _a, _b) = sample();
1421 let ghost = stale_id(&model);
1422 model.push_point(ghost, "X".to_string(), 1.0);
1423 }
1424
1425 #[test]
1426 #[should_panic(expected = "unknown SeriesId")]
1427 fn move_series_unknown_id_panics() {
1428 let (model, _a, _b) = sample();
1429 let ghost = stale_id(&model);
1430 model.move_series(ghost, 0);
1431 }
1432
1433 fn test_color() -> teksilo_tokens::Color {
1437 teksilo_tokens::Color::from_hex("#FF0000")
1438 }
1439}