scatter3d-anywidget 0.1.5__tar.gz → 0.1.6__tar.gz
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_anywidget-0.1.5 → scatter3d_anywidget-0.1.6}/PKG-INFO +1 -1
- {scatter3d_anywidget-0.1.5 → scatter3d_anywidget-0.1.6}/pyproject.toml +1 -1
- {scatter3d_anywidget-0.1.5 → scatter3d_anywidget-0.1.6}/src/scatter3d/scatter3d.py +50 -28
- {scatter3d_anywidget-0.1.5 → scatter3d_anywidget-0.1.6}/src/scatter3d/static/scatter3d.js +519 -496
- {scatter3d_anywidget-0.1.5 → scatter3d_anywidget-0.1.6}/src/scatter3d/static/scatter3d.js.map +1 -1
- {scatter3d_anywidget-0.1.5 → scatter3d_anywidget-0.1.6}/README.md +0 -0
- {scatter3d_anywidget-0.1.5 → scatter3d_anywidget-0.1.6}/src/scatter3d/__init__.py +0 -0
- {scatter3d_anywidget-0.1.5 → scatter3d_anywidget-0.1.6}/src/scatter3d/widget_test.py +0 -0
|
@@ -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
|
-
|
|
867
|
+
self._syncing_category = True
|
|
868
|
+
try:
|
|
869
|
+
cat = self._category
|
|
859
870
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
875
|
+
self.category_editable_t = cat.editable
|
|
865
876
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
-
|
|
880
|
-
|
|
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
|
-
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
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
|