scatter3d-anywidget 0.1.4__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.4 → scatter3d_anywidget-0.1.6}/PKG-INFO +1 -1
- {scatter3d_anywidget-0.1.4 → scatter3d_anywidget-0.1.6}/pyproject.toml +1 -1
- {scatter3d_anywidget-0.1.4 → scatter3d_anywidget-0.1.6}/src/scatter3d/scatter3d.py +68 -23
- {scatter3d_anywidget-0.1.4 → scatter3d_anywidget-0.1.6}/src/scatter3d/static/scatter3d.js +2525 -2479
- scatter3d_anywidget-0.1.6/src/scatter3d/static/scatter3d.js.map +1 -0
- {scatter3d_anywidget-0.1.4 → scatter3d_anywidget-0.1.6}/src/scatter3d/widget_test.py +5 -6
- scatter3d_anywidget-0.1.4/src/scatter3d/static/scatter3d.js.map +0 -1
- {scatter3d_anywidget-0.1.4 → scatter3d_anywidget-0.1.6}/README.md +0 -0
- {scatter3d_anywidget-0.1.4 → scatter3d_anywidget-0.1.6}/src/scatter3d/__init__.py +0 -0
|
@@ -80,6 +80,7 @@ class Category:
|
|
|
80
80
|
label_list=None,
|
|
81
81
|
color_palette: dict[Any, tuple[float, float, float]] | None = None,
|
|
82
82
|
missing_color: tuple[float, float, float] = MISSING_COLOR,
|
|
83
|
+
editable: bool = True,
|
|
83
84
|
):
|
|
84
85
|
self._cb_id_gen = count(1)
|
|
85
86
|
self._callbacks: dict[int, weakref.ReferenceType] = {}
|
|
@@ -102,6 +103,8 @@ class Category:
|
|
|
102
103
|
_is_valid_color(missing_color)
|
|
103
104
|
self._missing_color = missing_color
|
|
104
105
|
|
|
106
|
+
self._editable = bool(editable)
|
|
107
|
+
|
|
105
108
|
def subscribe(self, cb: CategoryCallback) -> int:
|
|
106
109
|
cb_id = next(self._cb_id_gen)
|
|
107
110
|
try:
|
|
@@ -125,6 +128,10 @@ class Category:
|
|
|
125
128
|
for cb_id in dead:
|
|
126
129
|
self._callbacks.pop(cb_id, None)
|
|
127
130
|
|
|
131
|
+
@property
|
|
132
|
+
def editable(self) -> bool:
|
|
133
|
+
return self._editable
|
|
134
|
+
|
|
128
135
|
@staticmethod
|
|
129
136
|
def _get_unique_labels_in_values(values):
|
|
130
137
|
return values.drop_nulls().unique().to_list()
|
|
@@ -495,6 +502,11 @@ class Scatter3dWidget(anywidget.AnyWidget):
|
|
|
495
502
|
help="Desired widget height in CSS pixels. Frontend will clamp to notebook constraints.",
|
|
496
503
|
).tag(sync=True)
|
|
497
504
|
|
|
505
|
+
category_editable_t = traitlets.Bool(
|
|
506
|
+
default_value=True,
|
|
507
|
+
help="Whether the active category is editable (enables lasso UI).",
|
|
508
|
+
).tag(sync=True)
|
|
509
|
+
|
|
498
510
|
def __init__(
|
|
499
511
|
self,
|
|
500
512
|
xyz: numpy.ndarray,
|
|
@@ -507,6 +519,9 @@ class Scatter3dWidget(anywidget.AnyWidget):
|
|
|
507
519
|
super().__init__()
|
|
508
520
|
self._category_cb_id: int | None = None
|
|
509
521
|
|
|
522
|
+
# Guard to suppress trait observers during multi-traitlet sync bursts
|
|
523
|
+
self._syncing_category = False
|
|
524
|
+
|
|
510
525
|
if category is not None and xyz.shape[0] != category.num_values:
|
|
511
526
|
raise ValueError(
|
|
512
527
|
f"The number of points ({xyz.shape[0]}) should match "
|
|
@@ -628,13 +643,19 @@ class Scatter3dWidget(anywidget.AnyWidget):
|
|
|
628
643
|
|
|
629
644
|
@traitlets.observe("labels_t")
|
|
630
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
|
+
|
|
631
650
|
if self.interaction_mode_t == "lasso":
|
|
632
651
|
self._ensure_active_category_invariants()
|
|
633
652
|
elif self.active_category_t is not None and self.active_category_t not in (
|
|
634
653
|
change.get("new") or []
|
|
635
654
|
):
|
|
636
655
|
# in rotate mode, invalid active is a real error
|
|
637
|
-
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
|
+
)
|
|
638
659
|
|
|
639
660
|
@traitlets.observe("client_ready_t")
|
|
640
661
|
def _on_client_ready_t(self, change) -> None:
|
|
@@ -730,6 +751,10 @@ class Scatter3dWidget(anywidget.AnyWidget):
|
|
|
730
751
|
)
|
|
731
752
|
return v
|
|
732
753
|
|
|
754
|
+
@property
|
|
755
|
+
def active_category(self):
|
|
756
|
+
return self.active_category_t
|
|
757
|
+
|
|
733
758
|
@traitlets.observe("tooltip_request_t")
|
|
734
759
|
def _on_tooltip_request(self, change) -> None:
|
|
735
760
|
req = change["new"] or {}
|
|
@@ -839,34 +864,54 @@ class Scatter3dWidget(anywidget.AnyWidget):
|
|
|
839
864
|
if self._category is None:
|
|
840
865
|
raise RuntimeError("The category should be set")
|
|
841
866
|
|
|
842
|
-
|
|
867
|
+
self._syncing_category = True
|
|
868
|
+
try:
|
|
869
|
+
cat = self._category
|
|
843
870
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
871
|
+
# labels_t must be JSON-friendly; enforce str
|
|
872
|
+
labels = [str(lbl) for lbl in cat.label_list]
|
|
873
|
+
self.labels_t = labels
|
|
847
874
|
|
|
848
|
-
|
|
849
|
-
coded = cat.coded_values
|
|
850
|
-
if coded.shape[0] != self.num_points:
|
|
851
|
-
raise RuntimeError(
|
|
852
|
-
f"Category has {coded.shape[0]} values but xyz has {self.num_points} points"
|
|
853
|
-
)
|
|
854
|
-
self.coded_values_t = self._pack_u16_c(coded)
|
|
875
|
+
self.category_editable_t = cat.editable
|
|
855
876
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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")
|
|
860
881
|
|
|
861
|
-
|
|
862
|
-
|
|
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)
|
|
863
888
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
"Internal error: colors_t length must match labels_t length"
|
|
867
|
-
)
|
|
889
|
+
palette = cat.color_palette # label -> (r,g,b)
|
|
890
|
+
self.colors_t = [list(map(float, palette[lbl])) for lbl in cat.label_list]
|
|
868
891
|
|
|
869
|
-
|
|
892
|
+
self.missing_color_t = list(map(float, cat.missing_color))
|
|
893
|
+
|
|
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
|
+
)
|
|
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
|
+
finally:
|
|
914
|
+
self._syncing_category = False
|
|
870
915
|
|
|
871
916
|
def _get_category(self):
|
|
872
917
|
return self._category
|