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>,
224 style_version: Signal<u64>,
227 #[cfg(debug_assertions)]
233 debug_adapter: Option<Rc<dyn crate::debug_registry::ModelDebug>>,
234}
235
236pub struct ChartModel<T: 'static> {
242 inner: Rc<RefCell<ChartModelInner<T>>>,
243}
244
245impl<T: 'static> ChartModel<T> {
246 pub fn new() -> Self {
248 Self {
249 inner: Rc::new(RefCell::new(ChartModelInner {
250 arena: SlotMap::new(),
251 order: Vec::new(),
252 observers: Vec::new(),
253 next_observer_id: 1,
254 structure_version: Signal::new(0),
255 style_version: Signal::new(0),
256 #[cfg(debug_assertions)]
257 debug_adapter: None,
258 })),
259 }
260 }
261
262 pub fn from_series_vec(series: Vec<ChartSeries<T>>) -> Self {
266 let model = Self::new();
267 {
268 let mut guard = model.inner.borrow_mut();
269 for s in series {
270 let key = guard.arena.insert(SeriesEntry {
271 name: s.name,
272 color: s.color,
273 pattern: s.pattern,
274 visible: s.visible,
275 points: s.points,
276 });
277 let id = SeriesId::from_key(key);
278 guard.order.push(id);
279 }
280 }
281 model
282 }
283
284 pub fn from_points(points: Vec<ChartDatum<T>>) -> Self {
288 Self::from_series_vec(vec![ChartSeries::new(String::new()).data(points)])
289 }
290
291 pub fn only_series(&self) -> Option<SeriesId> {
293 let guard = self.inner.borrow();
294 if guard.order.len() == 1 {
295 Some(guard.order[0])
296 } else {
297 None
298 }
299 }
300
301 pub fn add_series(&self, name: impl Into<String>) -> SeriesId {
305 let (id, index) = {
306 let mut guard = self.inner.borrow_mut();
307 let key = guard.arena.insert(SeriesEntry {
308 name: name.into(),
309 color: None,
310 pattern: None,
311 visible: true,
312 points: Vec::new(),
313 });
314 let id = SeriesId::from_key(key);
315 let index = guard.order.len();
316 guard.order.push(id);
317 (id, index)
318 };
319 self.notify(ChartChange::SeriesInserted { index, series: id });
320 self.bump_structure();
321 id
322 }
323
324 pub fn insert_series(&self, index: usize, name: impl Into<String>) -> SeriesId {
329 let id = {
330 let mut guard = self.inner.borrow_mut();
331 let key = guard.arena.insert(SeriesEntry {
332 name: name.into(),
333 color: None,
334 pattern: None,
335 visible: true,
336 points: Vec::new(),
337 });
338 let id = SeriesId::from_key(key);
339 guard.order.insert(index, id);
340 id
341 };
342 self.notify(ChartChange::SeriesInserted { index, series: id });
343 self.bump_structure();
344 id
345 }
346
347 pub fn remove_series(&self, series: SeriesId) {
352 {
353 let mut guard = self.inner.borrow_mut();
354 guard.arena.remove(series.key()).expect("unknown SeriesId");
355 guard.order.retain(|&id| id != series);
356 }
357 self.notify(ChartChange::SeriesRemoved { series });
358 self.bump_structure();
359 }
360
361 pub fn rename_series(&self, series: SeriesId, name: impl Into<String>) {
367 let name = name.into();
368 let changed = {
369 let mut guard = self.inner.borrow_mut();
370 let entry = &mut guard.arena[series.key()];
371 if entry.name == name {
372 false
373 } else {
374 entry.name = name;
375 true
376 }
377 };
378 if !changed {
379 return;
380 }
381 self.notify(ChartChange::SeriesRenamed { series });
382 self.bump_structure();
383 }
384
385 pub fn set_series_color(&self, series: SeriesId, color: impl Into<ColorProp>) {
393 let color = color.into();
394 let changed = {
395 let mut guard = self.inner.borrow_mut();
396 let entry = &mut guard.arena[series.key()];
397 if entry
398 .color
399 .as_ref()
400 .is_some_and(|c| color_prop_eq(c, &color))
401 {
402 false
403 } else {
404 entry.color = Some(color);
405 true
406 }
407 };
408 if !changed {
409 return;
410 }
411 self.notify(ChartChange::SeriesColorChanged { series });
412 self.bump_style();
413 }
414
415 pub fn clear_series_color(&self, series: SeriesId) {
422 let changed = {
423 let mut guard = self.inner.borrow_mut();
424 let entry = &mut guard.arena[series.key()];
425 if entry.color.is_none() {
426 false
427 } else {
428 entry.color = None;
429 true
430 }
431 };
432 if !changed {
433 return;
434 }
435 self.notify(ChartChange::SeriesColorChanged { series });
436 self.bump_style();
437 }
438
439 pub fn set_series_pattern(&self, series: SeriesId, pattern: SeriesPattern) {
446 let changed = {
447 let mut guard = self.inner.borrow_mut();
448 let entry = &mut guard.arena[series.key()];
449 if entry.pattern == Some(pattern) {
450 false
451 } else {
452 entry.pattern = Some(pattern);
453 true
454 }
455 };
456 if !changed {
457 return;
458 }
459 self.notify(ChartChange::SeriesPatternChanged { series });
460 self.bump_style();
461 }
462
463 pub fn clear_series_pattern(&self, series: SeriesId) {
469 let changed = {
470 let mut guard = self.inner.borrow_mut();
471 let entry = &mut guard.arena[series.key()];
472 if entry.pattern.is_none() {
473 false
474 } else {
475 entry.pattern = None;
476 true
477 }
478 };
479 if !changed {
480 return;
481 }
482 self.notify(ChartChange::SeriesPatternChanged { series });
483 self.bump_style();
484 }
485
486 pub fn set_series_visible(&self, series: SeriesId, visible: bool) {
492 let changed = {
493 let mut guard = self.inner.borrow_mut();
494 let entry = &mut guard.arena[series.key()];
495 if entry.visible == visible {
496 false
497 } else {
498 entry.visible = visible;
499 true
500 }
501 };
502 if !changed {
503 return;
504 }
505 self.notify(ChartChange::SeriesVisibilityChanged { series });
506 self.bump_structure();
507 }
508
509 pub fn move_series(&self, series: SeriesId, to: usize) {
515 let from = {
516 let guard = self.inner.borrow();
517 guard
518 .order
519 .iter()
520 .position(|&id| id == series)
521 .expect("unknown SeriesId")
522 };
523 if from == to {
524 return;
525 }
526 {
527 let mut guard = self.inner.borrow_mut();
528 let id = guard.order.remove(from);
529 guard.order.insert(to, id);
530 }
531 self.notify(ChartChange::SeriesMoved { series, from, to });
532 self.bump_structure();
533 }
534
535 pub fn clear(&self) {
537 {
538 let mut guard = self.inner.borrow_mut();
539 guard.arena.clear();
540 guard.order.clear();
541 }
542 self.notify(ChartChange::Reset);
543 self.bump_structure();
544 }
545
546 pub fn push_point(&self, series: SeriesId, category: T, value: f32) {
553 let index = {
554 let mut guard = self.inner.borrow_mut();
555 let entry = &mut guard.arena[series.key()];
556 let index = entry.points.len();
557 entry.points.push(ChartDatum::new(category, value));
558 index
559 };
560 self.notify(ChartChange::PointsInserted {
561 series,
562 range: index..index + 1,
563 });
564 self.bump_structure();
565 }
566
567 pub fn insert_point(&self, series: SeriesId, index: usize, category: T, value: f32) {
572 {
573 let mut guard = self.inner.borrow_mut();
574 guard.arena[series.key()]
575 .points
576 .insert(index, ChartDatum::new(category, value));
577 }
578 self.notify(ChartChange::PointsInserted {
579 series,
580 range: index..index + 1,
581 });
582 self.bump_structure();
583 }
584
585 pub fn remove_point(&self, series: SeriesId, index: usize) -> ChartDatum<T> {
590 let datum = {
591 let mut guard = self.inner.borrow_mut();
592 guard.arena[series.key()].points.remove(index)
593 };
594 self.notify(ChartChange::PointsRemoved {
595 series,
596 range: index..index + 1,
597 });
598 self.bump_structure();
599 datum
600 }
601
602 pub fn update_point(&self, series: SeriesId, index: usize, category: T, value: f32) {
607 {
608 let mut guard = self.inner.borrow_mut();
609 guard.arena[series.key()].points[index] = ChartDatum::new(category, value);
610 }
611 self.notify(ChartChange::PointUpdated { series, index });
612 self.bump_structure();
613 }
614
615 pub fn replace_series_data(&self, series: SeriesId, points: Vec<ChartDatum<T>>) {
620 {
621 let mut guard = self.inner.borrow_mut();
622 guard.arena[series.key()].points = points;
623 }
624 self.notify(ChartChange::SeriesDataReplaced { series });
625 self.bump_structure();
626 }
627
628 pub fn series_count(&self) -> usize {
632 self.inner.borrow().order.len()
633 }
634
635 pub fn series_ids(&self) -> Vec<SeriesId> {
637 self.inner.borrow().order.clone()
638 }
639
640 pub fn series_id_at(&self, index: usize) -> Option<SeriesId> {
642 self.inner.borrow().order.get(index).copied()
643 }
644
645 pub fn series_index_of(&self, series: SeriesId) -> Option<usize> {
647 self.inner
648 .borrow()
649 .order
650 .iter()
651 .position(|&id| id == series)
652 }
653
654 pub fn point_count(&self, series: SeriesId) -> usize {
656 self.inner
657 .borrow()
658 .arena
659 .get(series.key())
660 .map(|e| e.points.len())
661 .unwrap_or(0)
662 }
663
664 pub fn with_series<R>(
667 &self,
668 series: SeriesId,
669 f: impl FnOnce(&str, Option<&ColorProp>, bool) -> R,
670 ) -> Option<R> {
671 let guard = self.inner.borrow();
672 guard
673 .arena
674 .get(series.key())
675 .map(|e| f(&e.name, e.color.as_ref(), e.visible))
676 }
677
678 pub fn with_point<R>(
681 &self,
682 series: SeriesId,
683 index: usize,
684 f: impl FnOnce(&ChartDatum<T>) -> R,
685 ) -> Option<R> {
686 let guard = self.inner.borrow();
687 guard
688 .arena
689 .get(series.key())
690 .and_then(|e| e.points.get(index))
691 .map(f)
692 }
693
694 pub fn with_series_view<R>(
697 &self,
698 series: SeriesId,
699 f: impl FnOnce(SeriesView<'_, T>) -> R,
700 ) -> Option<R> {
701 let guard = self.inner.borrow();
702 guard.arena.get(series.key()).map(|e| {
703 f(SeriesView {
704 id: series,
705 name: &e.name,
706 color: e.color.as_ref(),
707 pattern: e.pattern,
708 visible: e.visible,
709 points: &e.points,
710 })
711 })
712 }
713
714 pub fn with_all_series<R>(&self, f: impl FnOnce(&[SeriesView<'_, T>]) -> R) -> R {
716 let guard = self.inner.borrow();
717 let views: Vec<SeriesView<'_, T>> = guard
718 .order
719 .iter()
720 .filter_map(|&id| {
721 guard.arena.get(id.key()).map(|e| SeriesView {
722 id,
723 name: &e.name,
724 color: e.color.as_ref(),
725 pattern: e.pattern,
726 visible: e.visible,
727 points: &e.points,
728 })
729 })
730 .collect();
731 f(&views)
732 }
733
734 pub fn structure_version(&self) -> Signal<u64> {
745 self.inner.borrow().structure_version.clone()
746 }
747
748 pub fn style_version(&self) -> Signal<u64> {
753 self.inner.borrow().style_version.clone()
754 }
755
756 pub fn observe_changes(&self, f: impl Fn(&ChartChange) + 'static) -> ObserverHandle {
776 let mut guard = self.inner.borrow_mut();
777 let id = guard.next_observer_id;
778 guard.next_observer_id += 1;
779 guard.observers.push(ObserverEntry {
780 id,
781 callback: Rc::new(f),
782 });
783 let inner = self.inner.clone();
784 ObserverHandle::new(
785 self.inner.clone(),
786 id,
787 Rc::new(move |observer_id| {
788 inner.borrow_mut().observers.retain(|e| e.id != observer_id);
789 }),
790 )
791 }
792
793 fn notify(&self, change: ChartChange) {
794 let callbacks: Vec<Rc<dyn Fn(&ChartChange)>> = self
795 .inner
796 .borrow()
797 .observers
798 .iter()
799 .map(|e| e.callback.clone())
800 .collect();
801 for cb in &callbacks {
802 cb(&change);
803 }
804 }
805
806 fn bump_structure(&self) {
811 let sig = self.inner.borrow().structure_version.clone();
812 sig.set(sig.get().wrapping_add(1));
813 }
814
815 fn bump_style(&self) {
818 let sig = self.inner.borrow().style_version.clone();
819 sig.set(sig.get().wrapping_add(1));
820 }
821}
822
823impl<T: std::fmt::Debug + 'static> ChartModel<T> {
824 pub fn debug_named(self, _name: impl Into<String>) -> Self {
832 #[cfg(debug_assertions)]
833 {
834 let weak = Rc::downgrade(&self.inner);
835 let adapter: Rc<dyn crate::debug_registry::ModelDebug> =
836 Rc::new(ChartModelDebug::<T> { weak });
837 let name = _name.into();
838 crate::debug_registry::register(name, Rc::downgrade(&adapter));
839 self.inner.borrow_mut().debug_adapter = Some(adapter);
840 }
841 self
842 }
843}
844
845#[cfg(debug_assertions)]
846struct ChartModelDebug<T> {
847 weak: std::rc::Weak<RefCell<ChartModelInner<T>>>,
848}
849
850#[cfg(debug_assertions)]
851impl<T: std::fmt::Debug + 'static> crate::debug_registry::ModelDebug for ChartModelDebug<T> {
852 fn kind(&self) -> &'static str {
853 "ChartModel"
854 }
855 fn len(&self) -> usize {
856 self.weak
857 .upgrade()
858 .map(|inner| inner.borrow().arena.values().map(|e| e.points.len()).sum())
859 .unwrap_or(0)
860 }
861 fn debug_dump(&self, out: &mut dyn std::fmt::Write) {
862 let Some(inner) = self.weak.upgrade() else {
863 return;
864 };
865 let guard = inner.borrow();
866 for (i, &id) in guard.order.iter().enumerate() {
867 if let Some(e) = guard.arena.get(id.key()) {
868 let _ = writeln!(
869 out,
870 "[{}] {:?} ({} pts, visible={})",
871 i,
872 e.name,
873 e.points.len(),
874 e.visible
875 );
876 }
877 }
878 }
879}
880
881impl<T: 'static> Default for ChartModel<T> {
882 fn default() -> Self {
883 Self::new()
884 }
885}
886
887impl<T: 'static> Clone for ChartModel<T> {
888 fn clone(&self) -> Self {
889 Self {
890 inner: self.inner.clone(),
891 }
892 }
893}
894
895impl<T: std::fmt::Debug + 'static> std::fmt::Debug for ChartModel<T> {
896 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
897 let guard = self.inner.borrow();
898 f.debug_struct("ChartModel")
899 .field("series_count", &guard.order.len())
900 .finish()
901 }
902}
903
904#[cfg(test)]
905impl<T: 'static> ChartModel<T> {
906 pub(crate) fn observer_count(&self) -> usize {
909 self.inner.borrow().observers.len()
910 }
911}
912
913#[cfg(test)]
914mod tests {
915 use std::cell::Cell;
916
917 use super::*;
918
919 #[test]
920 fn datum_with_color_sets_a_per_point_override() {
921 use teksilo_core::color_prop::ColorProp;
922 use teksilo_tokens::SurfaceRole;
923 let plain = ChartDatum::new("Q1".to_string(), 5.0);
924 assert!(plain.color.is_none(), "a plain datum has no color override");
925 let colored = ChartDatum::new("Q1".to_string(), 5.0).with_color(SurfaceRole::StatusError);
926 assert!(matches!(
927 colored.color,
928 Some(ColorProp::SurfaceRole(SurfaceRole::StatusError))
929 ));
930 let _ = ChartDatum::new("Q2".to_string(), 1.0);
932 }
933
934 fn sample() -> (ChartModel<String>, SeriesId, SeriesId) {
935 let model = ChartModel::from_series_vec(vec![
936 ChartSeries::new("Revenue").data(vec![
937 ChartDatum::new("Q1".to_string(), 10.0),
938 ChartDatum::new("Q2".to_string(), 20.0),
939 ]),
940 ChartSeries::new("Costs").data(vec![ChartDatum::new("Q1".to_string(), 5.0)]),
941 ]);
942 let a = model.series_id_at(0).unwrap();
943 let b = model.series_id_at(1).unwrap();
944 (model, a, b)
945 }
946
947 #[test]
948 fn from_series_vec_builds_correctly() {
949 let (model, a, b) = sample();
950 assert_eq!(model.series_count(), 2);
951 assert_eq!(model.point_count(a), 2);
952 assert_eq!(model.point_count(b), 1);
953 assert_eq!(
954 model.with_series(a, |name, _, visible| (name.to_string(), visible)),
955 Some(("Revenue".to_string(), true))
956 );
957 }
958
959 #[test]
960 fn new_is_empty() {
961 let model: ChartModel<String> = ChartModel::new();
962 assert_eq!(model.series_count(), 0);
963 assert_eq!(model.only_series(), None);
964 }
965
966 #[test]
967 fn only_series_some_iff_exactly_one() {
968 let model: ChartModel<String> = ChartModel::new();
969 assert_eq!(model.only_series(), None);
970 let a = model.add_series("A");
971 assert_eq!(model.only_series(), Some(a));
972 model.add_series("B");
973 assert_eq!(model.only_series(), None);
974 }
975
976 #[test]
977 fn from_points_builds_single_anonymous_series() {
978 let model = ChartModel::from_points(vec![
979 ChartDatum::new("a".to_string(), 1.0),
980 ChartDatum::new("b".to_string(), 2.0),
981 ]);
982 assert_eq!(model.series_count(), 1);
983 let s = model.only_series().unwrap();
984 assert_eq!(model.point_count(s), 2);
985 assert_eq!(model.with_series(s, |_, _, visible| visible), Some(true));
986 }
987
988 fn track_changes(
989 model: &ChartModel<String>,
990 ) -> (Rc<RefCell<Vec<ChartChange>>>, ObserverHandle) {
991 let log: Rc<RefCell<Vec<ChartChange>>> = Rc::new(RefCell::new(Vec::new()));
992 let l = log.clone();
993 let handle = model.observe_changes(move |c| l.borrow_mut().push(c.clone()));
994 (log, handle)
995 }
996
997 #[test]
998 fn add_series_emits_inserted_and_bumps_structure() {
999 let model: ChartModel<String> = ChartModel::new();
1000 let structure = model.structure_version();
1001 let style = model.style_version();
1002 let (log, _handle) = track_changes(&model);
1003
1004 let s = model.add_series("A");
1005 assert_eq!(log.borrow().len(), 1);
1006 assert_eq!(
1007 log.borrow()[0],
1008 ChartChange::SeriesInserted {
1009 index: 0,
1010 series: s
1011 }
1012 );
1013 assert_eq!(structure.get(), 1);
1014 assert_eq!(style.get(), 0);
1015 }
1016
1017 #[test]
1018 fn observer_sees_pre_bump_structure_version_during_notify() {
1019 let model: ChartModel<String> = ChartModel::new();
1020 let structure = model.structure_version();
1021 let seen_during_callback: Rc<Cell<Option<u64>>> = Rc::new(Cell::new(None));
1022 let seen = seen_during_callback.clone();
1023 let sig = structure.clone();
1024 let _handle = model.observe_changes(move |_| seen.set(Some(sig.get())));
1025
1026 let before = structure.get();
1027 model.add_series("A");
1028 let after = structure.get();
1029
1030 assert_eq!(after, before + 1, "the mutation did bump the signal");
1031 assert_eq!(
1032 seen_during_callback.get(),
1033 Some(before),
1034 "notify runs before the version bump, so a ChartChange observer \
1035 reading structure_version() synchronously sees the pre-bump value"
1036 );
1037 }
1038
1039 #[test]
1040 fn observer_sees_pre_bump_style_version_during_notify() {
1041 let (model, a, _b) = sample();
1042 let style = model.style_version();
1043 let seen_during_callback: Rc<Cell<Option<u64>>> = Rc::new(Cell::new(None));
1044 let seen = seen_during_callback.clone();
1045 let sig = style.clone();
1046 let _handle = model.observe_changes(move |_| seen.set(Some(sig.get())));
1047
1048 let before = style.get();
1049 model.set_series_color(a, test_color());
1050 let after = style.get();
1051
1052 assert_eq!(after, before + 1);
1053 assert_eq!(seen_during_callback.get(), Some(before));
1054 }
1055
1056 #[test]
1057 fn insert_series_at_index() {
1058 let (model, a, b) = sample();
1059 let c = model.insert_series(1, "Middle");
1060 assert_eq!(model.series_ids(), vec![a, c, b]);
1061 }
1062
1063 #[test]
1064 fn remove_series_emits_removed_and_bumps_structure() {
1065 let (model, a, _b) = sample();
1066 let structure_before = model.structure_version().get();
1067 let (log, _handle) = track_changes(&model);
1068
1069 model.remove_series(a);
1070 assert_eq!(log.borrow().len(), 1);
1071 assert_eq!(log.borrow()[0], ChartChange::SeriesRemoved { series: a });
1072 assert_eq!(model.series_count(), 1);
1073 assert!(model.structure_version().get() > structure_before);
1074 }
1075
1076 #[test]
1077 fn rename_series_emits_renamed_and_bumps_structure() {
1078 let (model, a, _b) = sample();
1079 let style_before = model.style_version().get();
1080 let (log, _handle) = track_changes(&model);
1081
1082 model.rename_series(a, "New Name");
1083 assert_eq!(log.borrow().len(), 1);
1084 assert_eq!(log.borrow()[0], ChartChange::SeriesRenamed { series: a });
1085 assert_eq!(
1086 model.with_series(a, |name, _, _| name.to_string()),
1087 Some("New Name".to_string())
1088 );
1089 assert_eq!(
1090 model.style_version().get(),
1091 style_before,
1092 "renaming is not a style change"
1093 );
1094 }
1095
1096 #[test]
1097 fn rename_series_noop_does_not_notify() {
1098 let (model, a, _b) = sample();
1099 let structure_before = model.structure_version().get();
1100 let (log, _handle) = track_changes(&model);
1101
1102 model.rename_series(a, "Revenue"); assert_eq!(log.borrow().len(), 0);
1104 assert_eq!(model.structure_version().get(), structure_before);
1105 }
1106
1107 #[test]
1108 fn set_series_color_bumps_style_not_structure() {
1109 let (model, a, _b) = sample();
1110 let structure_before = model.structure_version().get();
1111 let style_before = model.style_version().get();
1112 let (log, _handle) = track_changes(&model);
1113
1114 model.set_series_color(a, test_color());
1115 assert_eq!(log.borrow().len(), 1);
1116 assert_eq!(
1117 log.borrow()[0],
1118 ChartChange::SeriesColorChanged { series: a }
1119 );
1120 assert_eq!(
1121 model.structure_version().get(),
1122 structure_before,
1123 "color change must not bump structure_version"
1124 );
1125 assert!(model.style_version().get() > style_before);
1126 assert!(model.with_series(a, |_, color, _| color.is_some()).unwrap());
1127 }
1128
1129 #[test]
1130 fn set_series_color_noop_does_not_notify() {
1131 let (model, a, _b) = sample();
1132 model.set_series_color(a, test_color());
1133 let structure_before = model.structure_version().get();
1134 let style_before = model.style_version().get();
1135 let (log, _handle) = track_changes(&model);
1136
1137 model.set_series_color(a, test_color()); assert_eq!(log.borrow().len(), 0);
1139 assert_eq!(model.structure_version().get(), structure_before);
1140 assert_eq!(model.style_version().get(), style_before);
1141 }
1142
1143 #[test]
1144 fn clear_series_color_bumps_style_and_clears() {
1145 let (model, a, _b) = sample();
1146 model.set_series_color(a, test_color());
1147 let style_before = model.style_version().get();
1148 let (log, _handle) = track_changes(&model);
1149
1150 model.clear_series_color(a);
1151 assert_eq!(log.borrow().len(), 1);
1152 assert_eq!(
1153 log.borrow()[0],
1154 ChartChange::SeriesColorChanged { series: a }
1155 );
1156 assert!(model.style_version().get() > style_before);
1157 assert!(!model.with_series(a, |_, color, _| color.is_some()).unwrap());
1158 }
1159
1160 #[test]
1161 fn clear_series_color_noop_does_not_notify_when_already_none() {
1162 let (model, a, _b) = sample();
1163 let structure_before = model.structure_version().get();
1165 let style_before = model.style_version().get();
1166 let (log, _handle) = track_changes(&model);
1167
1168 model.clear_series_color(a);
1169 assert_eq!(log.borrow().len(), 0);
1170 assert_eq!(model.structure_version().get(), structure_before);
1171 assert_eq!(model.style_version().get(), style_before);
1172 }
1173
1174 #[test]
1175 fn set_series_visible_bumps_structure() {
1176 let (model, a, _b) = sample();
1177 let structure_before = model.structure_version().get();
1178 let (log, _handle) = track_changes(&model);
1179
1180 model.set_series_visible(a, false);
1181 assert_eq!(log.borrow().len(), 1);
1182 assert_eq!(
1183 log.borrow()[0],
1184 ChartChange::SeriesVisibilityChanged { series: a }
1185 );
1186 assert!(model.structure_version().get() > structure_before);
1187 assert_eq!(model.with_series(a, |_, _, visible| visible), Some(false));
1188 }
1189
1190 #[test]
1191 fn set_series_visible_noop_does_not_notify() {
1192 let (model, a, _b) = sample();
1193 let structure_before = model.structure_version().get();
1194 let (log, _handle) = track_changes(&model);
1195
1196 model.set_series_visible(a, true); assert_eq!(log.borrow().len(), 0);
1198 assert_eq!(model.structure_version().get(), structure_before);
1199 }
1200
1201 #[test]
1202 fn move_series_emits_moved_and_bumps_structure() {
1203 let (model, a, b) = sample();
1204 let structure_before = model.structure_version().get();
1205 let (log, _handle) = track_changes(&model);
1206
1207 model.move_series(a, 1);
1208 assert_eq!(model.series_ids(), vec![b, a]);
1209 assert_eq!(log.borrow().len(), 1);
1210 assert_eq!(
1211 log.borrow()[0],
1212 ChartChange::SeriesMoved {
1213 series: a,
1214 from: 0,
1215 to: 1
1216 }
1217 );
1218 assert!(model.structure_version().get() > structure_before);
1219 }
1220
1221 #[test]
1222 fn move_series_noop_does_not_notify() {
1223 let (model, a, _b) = sample();
1224 let structure_before = model.structure_version().get();
1225 let (log, _handle) = track_changes(&model);
1226
1227 model.move_series(a, 0); assert_eq!(log.borrow().len(), 0);
1229 assert_eq!(model.structure_version().get(), structure_before);
1230 }
1231
1232 #[test]
1233 fn push_point_emits_inserted_and_bumps_structure() {
1234 let (model, a, _b) = sample();
1235 let structure_before = model.structure_version().get();
1236 let (log, _handle) = track_changes(&model);
1237
1238 model.push_point(a, "Q3".to_string(), 30.0);
1239 assert_eq!(model.point_count(a), 3);
1240 assert_eq!(log.borrow().len(), 1);
1241 assert_eq!(
1242 log.borrow()[0],
1243 ChartChange::PointsInserted {
1244 series: a,
1245 range: 2..3
1246 }
1247 );
1248 assert!(model.structure_version().get() > structure_before);
1249 }
1250
1251 #[test]
1252 fn insert_point_at_index() {
1253 let (model, a, _b) = sample();
1254 model.insert_point(a, 1, "Q1.5".to_string(), 15.0);
1255 assert_eq!(model.point_count(a), 3);
1256 assert_eq!(
1257 model.with_point(a, 1, |d| d.category.clone()),
1258 Some("Q1.5".to_string())
1259 );
1260 }
1261
1262 #[test]
1263 fn remove_point_emits_removed_and_returns_datum() {
1264 let (model, a, _b) = sample();
1265 let (log, _handle) = track_changes(&model);
1266
1267 let removed = model.remove_point(a, 0);
1268 assert_eq!(removed.category, "Q1");
1269 assert_eq!(model.point_count(a), 1);
1270 assert_eq!(log.borrow().len(), 1);
1271 assert_eq!(
1272 log.borrow()[0],
1273 ChartChange::PointsRemoved {
1274 series: a,
1275 range: 0..1
1276 }
1277 );
1278 }
1279
1280 #[test]
1281 fn update_point_bumps_structure_not_style() {
1282 let (model, a, _b) = sample();
1283 let structure_before = model.structure_version().get();
1284 let style_before = model.style_version().get();
1285 let (log, _handle) = track_changes(&model);
1286
1287 model.update_point(a, 0, "Q1-revised".to_string(), 99.0);
1288 assert_eq!(log.borrow().len(), 1);
1289 assert_eq!(
1290 log.borrow()[0],
1291 ChartChange::PointUpdated {
1292 series: a,
1293 index: 0
1294 }
1295 );
1296 assert!(model.structure_version().get() > structure_before);
1297 assert_eq!(model.style_version().get(), style_before);
1298 assert_eq!(model.with_point(a, 0, |d| d.value), Some(99.0));
1299 }
1300
1301 #[test]
1302 fn replace_series_data_emits_replaced() {
1303 let (model, a, _b) = sample();
1304 let (log, _handle) = track_changes(&model);
1305
1306 model.replace_series_data(a, vec![ChartDatum::new("X".to_string(), 1.0)]);
1307 assert_eq!(model.point_count(a), 1);
1308 assert_eq!(log.borrow().len(), 1);
1309 assert_eq!(
1310 log.borrow()[0],
1311 ChartChange::SeriesDataReplaced { series: a }
1312 );
1313 }
1314
1315 #[test]
1316 fn clear_emits_reset() {
1317 let (model, _a, _b) = sample();
1318 let (log, _handle) = track_changes(&model);
1319
1320 model.clear();
1321 assert_eq!(model.series_count(), 0);
1322 assert_eq!(log.borrow().len(), 1);
1323 assert_eq!(log.borrow()[0], ChartChange::Reset);
1324 }
1325
1326 #[test]
1327 fn observer_removed_on_handle_drop() {
1328 let model: ChartModel<String> = ChartModel::new();
1329 let count = Rc::new(Cell::new(0));
1330 let c = count.clone();
1331 let handle = model.observe_changes(move |_| c.set(c.get() + 1));
1332
1333 model.add_series("A");
1334 assert_eq!(count.get(), 1);
1335
1336 drop(handle);
1337 model.add_series("B");
1338 assert_eq!(count.get(), 1);
1339 }
1340
1341 #[test]
1342 fn multiple_observers() {
1343 let model: ChartModel<String> = ChartModel::new();
1344 let count = Rc::new(Cell::new(0));
1345 let c1 = count.clone();
1346 let c2 = count.clone();
1347 let _h1 = model.observe_changes(move |_| c1.set(c1.get() + 1));
1348 let _h2 = model.observe_changes(move |_| c2.set(c2.get() + 1));
1349
1350 model.add_series("A");
1351 assert_eq!(count.get(), 2);
1352 }
1353
1354 #[test]
1355 fn clone_shares_data_and_observers() {
1356 let (model, _a, _b) = sample();
1357 let clone = model.clone();
1358 let count = Rc::new(Cell::new(0));
1359 let c = count.clone();
1360 let _handle = model.observe_changes(move |_| c.set(c.get() + 1));
1361
1362 clone.add_series("New");
1363 assert_eq!(model.series_count(), 3);
1364 assert_eq!(count.get(), 1);
1365 }
1366
1367 #[test]
1368 fn with_all_series_returns_views_in_order() {
1369 let (model, a, b) = sample();
1370 let ids: Vec<SeriesId> =
1371 model.with_all_series(|views| views.iter().map(|v| v.id).collect());
1372 assert_eq!(ids, vec![a, b]);
1373 let names: Vec<String> =
1374 model.with_all_series(|views| views.iter().map(|v| v.name.to_string()).collect());
1375 assert_eq!(names, vec!["Revenue".to_string(), "Costs".to_string()]);
1376 }
1377
1378 #[test]
1379 fn with_point_out_of_bounds_returns_none() {
1380 let (model, a, _b) = sample();
1381 assert_eq!(model.with_point(a, 99, |d| d.value), None);
1382 }
1383
1384 fn stale_id(model: &ChartModel<String>) -> SeriesId {
1391 let ghost = model.add_series("Ghost");
1392 model.remove_series(ghost);
1393 ghost
1394 }
1395
1396 #[test]
1397 fn with_series_unknown_id_returns_none() {
1398 let (model, _a, _b) = sample();
1399 let ghost = stale_id(&model);
1400 assert_eq!(model.with_series(ghost, |_, _, _| ()), None);
1401 assert_eq!(model.with_point(ghost, 0, |d| d.value), None);
1402 }
1403
1404 #[test]
1405 #[should_panic(expected = "unknown SeriesId")]
1406 fn remove_series_unknown_id_panics() {
1407 let (model, _a, _b) = sample();
1408 let ghost = stale_id(&model);
1409 model.remove_series(ghost);
1410 }
1411
1412 #[test]
1413 #[should_panic]
1414 fn rename_series_unknown_id_panics() {
1415 let (model, _a, _b) = sample();
1416 let ghost = stale_id(&model);
1417 model.rename_series(ghost, "X");
1418 }
1419
1420 #[test]
1421 #[should_panic]
1422 fn push_point_unknown_series_panics() {
1423 let (model, _a, _b) = sample();
1424 let ghost = stale_id(&model);
1425 model.push_point(ghost, "X".to_string(), 1.0);
1426 }
1427
1428 #[test]
1429 #[should_panic(expected = "unknown SeriesId")]
1430 fn move_series_unknown_id_panics() {
1431 let (model, _a, _b) = sample();
1432 let ghost = stale_id(&model);
1433 model.move_series(ghost, 0);
1434 }
1435
1436 fn test_color() -> teksilo_tokens::Color {
1440 teksilo_tokens::Color::from_hex("#FF0000")
1441 }
1442}