scatter3d-anywidget 0.1.6__py3-none-any.whl → 0.1.8__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
@@ -632,12 +632,13 @@ class Scatter3dWidget(anywidget.AnyWidget):
632
632
 
633
633
  @traitlets.observe("active_category_t")
634
634
  def _on_active_category_t(self, change) -> None:
635
- if self.interaction_mode_t == "lasso" and change.get("new") is None:
636
- # NOTE: traitlets bypasses @validate(...) when allow_none=True.
637
- # In lasso mode, clearing is a hard error (no silent fallback).
635
+ old = change.get("old")
636
+ new = change.get("new")
637
+
638
+ # Keep your existing policy/logic:
639
+ if self.interaction_mode_t == "lasso" and new is None:
638
640
  old = change.get("old")
639
641
  if old is not None:
640
- # restore previous valid value to keep state consistent
641
642
  self.set_trait("active_category_t", old)
642
643
  raise traitlets.TraitError("active_category_t cannot be None in lasso mode")
643
644
 
@@ -647,15 +648,14 @@ class Scatter3dWidget(anywidget.AnyWidget):
647
648
  if getattr(self, "_syncing_category", False):
648
649
  return
649
650
 
650
- if self.interaction_mode_t == "lasso":
651
- self._ensure_active_category_invariants()
652
- elif self.active_category_t is not None and self.active_category_t not in (
651
+ # After our new policy, we should never be in lasso mode during a category switch,
652
+ # and active_category_t should be None. Still, be defensive: never crash.
653
+ if self.active_category_t is not None and self.active_category_t not in (
653
654
  change.get("new") or []
654
655
  ):
655
- # in rotate mode, invalid active is a real error
656
- raise RuntimeError(
657
- f"active_category_t={self.active_category_t!r} is not present in labels_t after labels update"
658
- )
656
+ self.active_category_t = None
657
+ if self.interactive_ready_t:
658
+ self.send_state("active_category_t")
659
659
 
660
660
  @traitlets.observe("client_ready_t")
661
661
  def _on_client_ready_t(self, change) -> None:
@@ -744,11 +744,9 @@ class Scatter3dWidget(anywidget.AnyWidget):
744
744
  )
745
745
  return v
746
746
 
747
- # rotate mode
747
+ # rotate mode: do not crash on stale frontend values
748
748
  if labels and v not in labels:
749
- raise traitlets.TraitError(
750
- f"active_category_t={v!r} is not present in labels_t"
751
- )
749
+ return None
752
750
  return v
753
751
 
754
752
  @property
@@ -874,11 +872,7 @@ class Scatter3dWidget(anywidget.AnyWidget):
874
872
 
875
873
  self.category_editable_t = cat.editable
876
874
 
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")
881
-
875
+ # coded values: uint16 bytes, length N
882
876
  coded = cat.coded_values
883
877
  if coded.shape[0] != self.num_points:
884
878
  raise RuntimeError(
@@ -886,30 +880,17 @@ class Scatter3dWidget(anywidget.AnyWidget):
886
880
  )
887
881
  self.coded_values_t = self._pack_u16_c(coded)
888
882
 
883
+ # colors aligned with labels order
889
884
  palette = cat.color_palette # label -> (r,g,b)
890
885
  self.colors_t = [list(map(float, palette[lbl])) for lbl in cat.label_list]
891
886
 
887
+ # missing color
892
888
  self.missing_color_t = list(map(float, cat.missing_color))
893
889
 
894
890
  if len(self.colors_t) != len(self.labels_t):
895
891
  raise RuntimeError(
896
892
  "Internal error: colors_t length must match labels_t length"
897
893
  )
898
-
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
894
  finally:
914
895
  self._syncing_category = False
915
896
 
@@ -917,6 +898,11 @@ class Scatter3dWidget(anywidget.AnyWidget):
917
898
  return self._category
918
899
 
919
900
  def _set_category(self, category: Category) -> None:
901
+ # Idempotence: marimo may re-run cells and re-assign the same Category.
902
+ # That must be a no-op (must not clear active category, mode, etc).
903
+ if category is self._category:
904
+ return
905
+
920
906
  if self._xyz is not None and category.num_values != self.num_points:
921
907
  raise ValueError(
922
908
  f"The number of values in the category ({category.num_values}) "
@@ -926,6 +912,19 @@ class Scatter3dWidget(anywidget.AnyWidget):
926
912
  self._category.unsubscribe(self._category_cb_id)
927
913
 
928
914
  self._category = category
915
+
916
+ # POLICY: changing the category object resets interaction to rotate and clears active.
917
+ # This must NOT run on Category mutations (coded values edits, palette tweaks, etc.).
918
+ if self.interaction_mode_t != "rotate":
919
+ self.interaction_mode_t = "rotate"
920
+ if self.interactive_ready_t:
921
+ self.send_state("interaction_mode_t")
922
+
923
+ if self.active_category_t is not None:
924
+ self.active_category_t = None
925
+ if self.interactive_ready_t:
926
+ self.send_state("active_category_t")
927
+
929
928
  # Subscribe to new category
930
929
  self._category_cb_id = category.subscribe(self._on_category_changed)
931
930
  self._sync_traitlets_from_category()
@@ -22450,6 +22450,7 @@ function wg(i) {
22450
22450
  _ !== null && d === _ ? e.set(ne.activeCategory, null) : e.set(ne.activeCategory, d), r("activeCategory");
22451
22451
  }), e.on(`change:${ne.labels}`, u), e.on(`change:${ne.colors}`, u), e.on(`change:${ne.activeCategory}`, u), e.on(`change:${ne.interactionMode}`, u), e.on(`change:${ne.legendSide}`, u), e.on(`change:${ne.legendDock}`, u), {
22452
22452
  refreshFromModel: s,
22453
+ scheduleRefresh: h,
22453
22454
  dispose: () => {
22454
22455
  e.off(`change:${ne.labels}`, u), e.off(`change:${ne.colors}`, u), e.off(`change:${ne.activeCategory}`, u), e.off(`change:${ne.interactionMode}`, u), e.off(`change:${ne.legendSide}`, u), e.off(`change:${ne.legendDock}`, u);
22455
22456
  }
@@ -22700,7 +22701,7 @@ function Vg(i, e, t) {
22700
22701
  view: F,
22701
22702
  transportReady: R
22702
22703
  });
22703
- M.refreshFromModel();
22704
+ M.scheduleRefresh();
22704
22705
  function S() {
22705
22706
  const re = r.get(ne.interactionMode) === "lasso" ? "lasso" : "rotate";
22706
22707
  if (re === "rotate")
@@ -22739,7 +22740,7 @@ function Vg(i, e, t) {
22739
22740
  }), B = fg({
22740
22741
  model: r,
22741
22742
  three: h,
22742
- refreshLegendUI: M.refreshFromModel,
22743
+ refreshLegendUI: M.scheduleRefresh,
22743
22744
  onTooltipResponseChange: z.onTooltipResponseChange
22744
22745
  });
22745
22746
  r.set(ne.clientReady, !0);