scatter3d-anywidget 0.1.5__py3-none-any.whl → 0.1.7__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 +63 -42
- scatter3d/static/scatter3d.js +519 -496
- scatter3d/static/scatter3d.js.map +1 -1
- scatter3d/widget_test.py +26 -5
- {scatter3d_anywidget-0.1.5.dist-info → scatter3d_anywidget-0.1.7.dist-info}/METADATA +1 -1
- scatter3d_anywidget-0.1.7.dist-info/RECORD +8 -0
- {scatter3d_anywidget-0.1.5.dist-info → scatter3d_anywidget-0.1.7.dist-info}/WHEEL +1 -1
- scatter3d_anywidget-0.1.5.dist-info/RECORD +0 -8
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 "
|
|
@@ -629,24 +632,30 @@ class Scatter3dWidget(anywidget.AnyWidget):
|
|
|
629
632
|
|
|
630
633
|
@traitlets.observe("active_category_t")
|
|
631
634
|
def _on_active_category_t(self, change) -> None:
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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:
|
|
635
640
|
old = change.get("old")
|
|
636
641
|
if old is not None:
|
|
637
|
-
# restore previous valid value to keep state consistent
|
|
638
642
|
self.set_trait("active_category_t", old)
|
|
639
643
|
raise traitlets.TraitError("active_category_t cannot be None in lasso mode")
|
|
640
644
|
|
|
641
645
|
@traitlets.observe("labels_t")
|
|
642
646
|
def _on_labels_t(self, change) -> None:
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
647
|
+
# During category sync we temporarily allow intermediate inconsistent states.
|
|
648
|
+
if getattr(self, "_syncing_category", False):
|
|
649
|
+
return
|
|
650
|
+
|
|
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 (
|
|
646
654
|
change.get("new") or []
|
|
647
655
|
):
|
|
648
|
-
|
|
649
|
-
|
|
656
|
+
self.active_category_t = None
|
|
657
|
+
if self.interactive_ready_t:
|
|
658
|
+
self.send_state("active_category_t")
|
|
650
659
|
|
|
651
660
|
@traitlets.observe("client_ready_t")
|
|
652
661
|
def _on_client_ready_t(self, change) -> None:
|
|
@@ -735,11 +744,9 @@ class Scatter3dWidget(anywidget.AnyWidget):
|
|
|
735
744
|
)
|
|
736
745
|
return v
|
|
737
746
|
|
|
738
|
-
# rotate mode
|
|
747
|
+
# rotate mode: do not crash on stale frontend values
|
|
739
748
|
if labels and v not in labels:
|
|
740
|
-
|
|
741
|
-
f"active_category_t={v!r} is not present in labels_t"
|
|
742
|
-
)
|
|
749
|
+
return None
|
|
743
750
|
return v
|
|
744
751
|
|
|
745
752
|
@property
|
|
@@ -855,46 +862,47 @@ class Scatter3dWidget(anywidget.AnyWidget):
|
|
|
855
862
|
if self._category is None:
|
|
856
863
|
raise RuntimeError("The category should be set")
|
|
857
864
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
labels = [str(lbl) for lbl in cat.label_list]
|
|
862
|
-
self.labels_t = labels
|
|
865
|
+
self._syncing_category = True
|
|
866
|
+
try:
|
|
867
|
+
cat = self._category
|
|
863
868
|
|
|
864
|
-
|
|
869
|
+
# labels_t must be JSON-friendly; enforce str
|
|
870
|
+
labels = [str(lbl) for lbl in cat.label_list]
|
|
871
|
+
self.labels_t = labels
|
|
865
872
|
|
|
866
|
-
|
|
867
|
-
self.interaction_mode_t = "rotate"
|
|
868
|
-
if self.interactive_ready_t:
|
|
869
|
-
self.send_state("interaction_mode_t")
|
|
870
|
-
|
|
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)
|
|
873
|
+
self.category_editable_t = cat.editable
|
|
878
874
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
875
|
+
# coded values: uint16 bytes, length N
|
|
876
|
+
coded = cat.coded_values
|
|
877
|
+
if coded.shape[0] != self.num_points:
|
|
878
|
+
raise RuntimeError(
|
|
879
|
+
f"Category has {coded.shape[0]} values but xyz has {self.num_points} points"
|
|
880
|
+
)
|
|
881
|
+
self.coded_values_t = self._pack_u16_c(coded)
|
|
883
882
|
|
|
884
|
-
|
|
885
|
-
|
|
883
|
+
# colors aligned with labels order
|
|
884
|
+
palette = cat.color_palette # label -> (r,g,b)
|
|
885
|
+
self.colors_t = [list(map(float, palette[lbl])) for lbl in cat.label_list]
|
|
886
886
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
"Internal error: colors_t length must match labels_t length"
|
|
890
|
-
)
|
|
887
|
+
# missing color
|
|
888
|
+
self.missing_color_t = list(map(float, cat.missing_color))
|
|
891
889
|
|
|
892
|
-
|
|
890
|
+
if len(self.colors_t) != len(self.labels_t):
|
|
891
|
+
raise RuntimeError(
|
|
892
|
+
"Internal error: colors_t length must match labels_t length"
|
|
893
|
+
)
|
|
894
|
+
finally:
|
|
895
|
+
self._syncing_category = False
|
|
893
896
|
|
|
894
897
|
def _get_category(self):
|
|
895
898
|
return self._category
|
|
896
899
|
|
|
897
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
|
+
|
|
898
906
|
if self._xyz is not None and category.num_values != self.num_points:
|
|
899
907
|
raise ValueError(
|
|
900
908
|
f"The number of values in the category ({category.num_values}) "
|
|
@@ -904,6 +912,19 @@ class Scatter3dWidget(anywidget.AnyWidget):
|
|
|
904
912
|
self._category.unsubscribe(self._category_cb_id)
|
|
905
913
|
|
|
906
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
|
+
|
|
907
928
|
# Subscribe to new category
|
|
908
929
|
self._category_cb_id = category.subscribe(self._on_category_changed)
|
|
909
930
|
self._sync_traitlets_from_category()
|