scatter3d-anywidget 0.1.5__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
scatter3d/scatter3d.py CHANGED
@@ -519,6 +519,9 @@ class Scatter3dWidget(anywidget.AnyWidget):
519
519
  super().__init__()
520
520
  self._category_cb_id: int | None = None
521
521
 
522
+ # Guard to suppress trait observers during multi-traitlet sync bursts
523
+ self._syncing_category = False
524
+
522
525
  if category is not None and xyz.shape[0] != category.num_values:
523
526
  raise ValueError(
524
527
  f"The number of points ({xyz.shape[0]}) should match "
@@ -640,13 +643,19 @@ class Scatter3dWidget(anywidget.AnyWidget):
640
643
 
641
644
  @traitlets.observe("labels_t")
642
645
  def _on_labels_t(self, change) -> None:
646
+ # During category sync we temporarily allow intermediate inconsistent states.
647
+ if getattr(self, "_syncing_category", False):
648
+ return
649
+
643
650
  if self.interaction_mode_t == "lasso":
644
651
  self._ensure_active_category_invariants()
645
652
  elif self.active_category_t is not None and self.active_category_t not in (
646
653
  change.get("new") or []
647
654
  ):
648
655
  # in rotate mode, invalid active is a real error
649
- raise RuntimeError(...)
656
+ raise RuntimeError(
657
+ f"active_category_t={self.active_category_t!r} is not present in labels_t after labels update"
658
+ )
650
659
 
651
660
  @traitlets.observe("client_ready_t")
652
661
  def _on_client_ready_t(self, change) -> None:
@@ -855,41 +864,54 @@ class Scatter3dWidget(anywidget.AnyWidget):
855
864
  if self._category is None:
856
865
  raise RuntimeError("The category should be set")
857
866
 
858
- cat = self._category
867
+ self._syncing_category = True
868
+ try:
869
+ cat = self._category
859
870
 
860
- # labels_t must be JSON-friendly; enforce str
861
- labels = [str(lbl) for lbl in cat.label_list]
862
- self.labels_t = labels
871
+ # labels_t must be JSON-friendly; enforce str
872
+ labels = [str(lbl) for lbl in cat.label_list]
873
+ self.labels_t = labels
863
874
 
864
- self.category_editable_t = cat.editable
875
+ self.category_editable_t = cat.editable
865
876
 
866
- if not self.category_editable_t and self.interaction_mode_t == "lasso":
867
- self.interaction_mode_t = "rotate"
868
- if self.interactive_ready_t:
869
- self.send_state("interaction_mode_t")
877
+ if not self.category_editable_t and self.interaction_mode_t == "lasso":
878
+ self.interaction_mode_t = "rotate"
879
+ if self.interactive_ready_t:
880
+ self.send_state("interaction_mode_t")
870
881
 
871
- # coded values: uint16 bytes, length N
872
- coded = cat.coded_values
873
- if coded.shape[0] != self.num_points:
874
- raise RuntimeError(
875
- f"Category has {coded.shape[0]} values but xyz has {self.num_points} points"
876
- )
877
- self.coded_values_t = self._pack_u16_c(coded)
882
+ coded = cat.coded_values
883
+ if coded.shape[0] != self.num_points:
884
+ raise RuntimeError(
885
+ f"Category has {coded.shape[0]} values but xyz has {self.num_points} points"
886
+ )
887
+ self.coded_values_t = self._pack_u16_c(coded)
878
888
 
879
- # colors aligned with labels order
880
- # Category stores palette keyed by original labels; we reconstruct in label_list order.
881
- palette = cat.color_palette # label -> (r,g,b)
882
- self.colors_t = [list(map(float, palette[lbl])) for lbl in cat.label_list]
889
+ palette = cat.color_palette # label -> (r,g,b)
890
+ self.colors_t = [list(map(float, palette[lbl])) for lbl in cat.label_list]
883
891
 
884
- # missing color
885
- self.missing_color_t = list(map(float, cat.missing_color))
892
+ self.missing_color_t = list(map(float, cat.missing_color))
886
893
 
887
- if len(self.colors_t) != len(self.labels_t):
888
- raise RuntimeError(
889
- "Internal error: colors_t length must match labels_t length"
890
- )
894
+ if len(self.colors_t) != len(self.labels_t):
895
+ raise RuntimeError(
896
+ "Internal error: colors_t length must match labels_t length"
897
+ )
891
898
 
892
- self._ensure_active_category_invariants()
899
+ # Now that labels/colors are consistent, enforce a stable policy for active category.
900
+ if self.interaction_mode_t == "rotate":
901
+ if (
902
+ self.active_category_t is not None
903
+ and self.active_category_t not in self.labels_t
904
+ ):
905
+ # Professional + predictable: clear invalid selection on category switch.
906
+ self.active_category_t = None
907
+ if self.interactive_ready_t:
908
+ self.send_state("active_category_t")
909
+ else:
910
+ # lasso mode keeps existing behavior: ensure a valid active label exists.
911
+ self._ensure_active_category_invariants()
912
+
913
+ finally:
914
+ self._syncing_category = False
893
915
 
894
916
  def _get_category(self):
895
917
  return self._category