datalab-platform 1.0.3__py3-none-any.whl → 1.1.0__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.
Files changed (46) hide show
  1. datalab/__init__.py +1 -1
  2. datalab/config.py +4 -0
  3. datalab/control/baseproxy.py +160 -0
  4. datalab/control/remote.py +175 -1
  5. datalab/data/doc/DataLab_en.pdf +0 -0
  6. datalab/data/doc/DataLab_fr.pdf +0 -0
  7. datalab/data/icons/control/copy_connection_info.svg +11 -0
  8. datalab/data/icons/control/start_webapi_server.svg +19 -0
  9. datalab/data/icons/control/stop_webapi_server.svg +7 -0
  10. datalab/gui/docks.py +3 -2
  11. datalab/gui/main.py +221 -2
  12. datalab/gui/settings.py +10 -0
  13. datalab/gui/tour.py +2 -3
  14. datalab/locale/fr/LC_MESSAGES/datalab.mo +0 -0
  15. datalab/locale/fr/LC_MESSAGES/datalab.po +95 -1
  16. datalab/tests/__init__.py +32 -1
  17. datalab/tests/backbone/config_unit_test.py +1 -1
  18. datalab/tests/backbone/main_app_test.py +4 -0
  19. datalab/tests/backbone/memory_leak.py +1 -1
  20. datalab/tests/features/common/createobject_unit_test.py +1 -1
  21. datalab/tests/features/common/misc_app_test.py +5 -0
  22. datalab/tests/features/control/call_method_unit_test.py +104 -0
  23. datalab/tests/features/control/embedded1_unit_test.py +8 -0
  24. datalab/tests/features/control/remoteclient_app_test.py +39 -35
  25. datalab/tests/features/control/simpleclient_unit_test.py +7 -3
  26. datalab/tests/features/hdf5/h5browser2_unit.py +1 -1
  27. datalab/tests/features/image/background_dialog_test.py +2 -2
  28. datalab/tests/features/image/imagetools_unit_test.py +1 -1
  29. datalab/tests/features/signal/baseline_dialog_test.py +1 -1
  30. datalab/tests/webapi_test.py +395 -0
  31. datalab/webapi/__init__.py +95 -0
  32. datalab/webapi/actions.py +318 -0
  33. datalab/webapi/adapter.py +642 -0
  34. datalab/webapi/controller.py +379 -0
  35. datalab/webapi/routes.py +576 -0
  36. datalab/webapi/schema.py +198 -0
  37. datalab/webapi/serialization.py +388 -0
  38. datalab/widgets/status.py +61 -0
  39. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/METADATA +11 -7
  40. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/RECORD +46 -34
  41. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/WHEEL +1 -1
  42. /datalab/data/icons/{libre-gui-link.svg → control/libre-gui-link.svg} +0 -0
  43. /datalab/data/icons/{libre-gui-unlink.svg → control/libre-gui-unlink.svg} +0 -0
  44. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/entry_points.txt +0 -0
  45. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/licenses/LICENSE +0 -0
  46. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/top_level.txt +0 -0
datalab/__init__.py CHANGED
@@ -24,7 +24,7 @@ except RuntimeError:
24
24
  # this module is imported more than once, e.g. when running tests)
25
25
  pass
26
26
 
27
- __version__ = "1.0.3"
27
+ __version__ = "1.1.0"
28
28
  __docurl__ = __homeurl__ = "https://datalab-platform.com/"
29
29
  __supporturl__ = "https://github.com/DataLab-Platform/DataLab/issues/new/choose"
30
30
 
datalab/config.py CHANGED
@@ -135,6 +135,7 @@ class MainSection(conf.Section, metaclass=conf.SectionMeta):
135
135
  process_isolation_enabled = conf.Option()
136
136
  rpc_server_enabled = conf.Option()
137
137
  rpc_server_port = conf.Option()
138
+ webapi_localhost_no_token = conf.Option() # Allow localhost without token
138
139
  traceback_log_path = conf.ConfigPathOption()
139
140
  traceback_log_available = conf.Option()
140
141
  faulthandler_enabled = conf.Option()
@@ -406,6 +407,9 @@ def initialize():
406
407
  Conf.main.color_mode.get("auto")
407
408
  Conf.main.process_isolation_enabled.get(True)
408
409
  Conf.main.rpc_server_enabled.get(True)
410
+ Conf.main.webapi_localhost_no_token.get(
411
+ True
412
+ ) # Enabled by default (Web API is off by default)
409
413
  Conf.main.traceback_log_path.get(f".{APP_NAME}_traceback.log")
410
414
  Conf.main.faulthandler_log_path.get(f".{APP_NAME}_faulthandler.log")
411
415
  Conf.main.available_memory_threshold.get(500)
@@ -127,6 +127,14 @@ class AbstractDLControl(abc.ABC):
127
127
  def reset_all(self) -> None:
128
128
  """Reset all application data"""
129
129
 
130
+ @abc.abstractmethod
131
+ def remove_object(self, force: bool = False) -> None:
132
+ """Remove current object from current panel.
133
+
134
+ Args:
135
+ force: if True, remove object without confirmation. Defaults to False.
136
+ """
137
+
130
138
  @abc.abstractmethod
131
139
  def toggle_auto_refresh(self, state: bool) -> None:
132
140
  """Toggle auto refresh state.
@@ -546,6 +554,85 @@ class AbstractDLControl(abc.ABC):
546
554
  ValueError: unknown function
547
555
  """
548
556
 
557
+ # =========================================================================
558
+ # Web API control methods
559
+ # =========================================================================
560
+
561
+ @abc.abstractmethod
562
+ def start_webapi_server(
563
+ self,
564
+ host: str | None = None,
565
+ port: int | None = None,
566
+ ) -> dict:
567
+ """Start the Web API server.
568
+
569
+ Args:
570
+ host: Host address to bind to. Defaults to "127.0.0.1".
571
+ port: Port number. Defaults to auto-detect available port.
572
+
573
+ Returns:
574
+ Dictionary with "url" and "token" keys.
575
+
576
+ Raises:
577
+ RuntimeError: If Web API deps not installed or server already running.
578
+ """
579
+
580
+ @abc.abstractmethod
581
+ def stop_webapi_server(self) -> None:
582
+ """Stop the Web API server."""
583
+
584
+ @abc.abstractmethod
585
+ def get_webapi_status(self) -> dict:
586
+ """Get Web API server status.
587
+
588
+ Returns:
589
+ Dictionary with "running", "url", and "token" keys.
590
+ """
591
+
592
+ @abc.abstractmethod
593
+ def call_method(
594
+ self,
595
+ method_name: str,
596
+ *args,
597
+ panel: str | None = None,
598
+ **kwargs,
599
+ ):
600
+ """Call a public method on a panel or main window.
601
+
602
+ This generic method allows calling any public method that is not explicitly
603
+ exposed in the proxy API. The method resolution follows this order:
604
+
605
+ 1. If panel is specified: call method on that specific panel
606
+ 2. If panel is None:
607
+ a. Try to call method on main window (DLMainWindow)
608
+ b. If not found, try to call method on current panel (BaseDataPanel)
609
+
610
+ This makes it convenient to call panel methods without specifying the panel
611
+ parameter when working on the current panel.
612
+
613
+ Args:
614
+ method_name: Name of the method to call
615
+ *args: Positional arguments to pass to the method
616
+ panel: Panel name ("signal", "image", or None for auto-detection).
617
+ Defaults to None.
618
+ **kwargs: Keyword arguments to pass to the method
619
+
620
+ Returns:
621
+ The return value of the called method
622
+
623
+ Raises:
624
+ AttributeError: If the method does not exist or is not public
625
+ ValueError: If the panel name is invalid
626
+
627
+ Examples:
628
+ >>> # Call remove_object on current panel (auto-detected)
629
+ >>> proxy.call_method("remove_object", force=True)
630
+ >>> # Call a signal panel method specifically
631
+ >>> proxy.call_method("delete_all_objects", panel="signal")
632
+ >>> # Call main window method
633
+ >>> proxy.call_method("raise_window")
634
+ """
635
+
549
636
 
550
637
  class BaseProxy(AbstractDLControl, metaclass=abc.ABCMeta):
551
638
  """Common base class for DataLab proxies
@@ -594,6 +681,14 @@ class BaseProxy(AbstractDLControl, metaclass=abc.ABCMeta):
594
681
  """Reset all application data"""
595
682
  self._datalab.reset_all()
596
683
 
684
+ def remove_object(self, force: bool = False) -> None:
685
+ """Remove current object from current panel.
686
+
687
+ Args:
688
+ force: if True, remove object without confirmation. Defaults to False.
689
+ """
690
+ self._datalab.remove_object(force)
691
+
597
692
  def toggle_auto_refresh(self, state: bool) -> None:
598
693
  """Toggle auto refresh state.
599
694
 
@@ -844,3 +939,68 @@ class BaseProxy(AbstractDLControl, metaclass=abc.ABCMeta):
844
939
  filename: Filename.
845
940
  """
846
941
  return self._datalab.import_macro_from_file(filename)
942
+
943
+ def call_method(
944
+ self,
945
+ method_name: str,
946
+ *args,
947
+ panel: str | None = None,
948
+ **kwargs,
949
+ ):
950
+ """Call a public method on a panel or main window.
951
+
952
+ Method resolution order when panel is None:
953
+ 1. Try main window (DLMainWindow)
954
+ 2. If not found, try current panel (BaseDataPanel)
955
+
956
+ Args:
957
+ method_name: Name of the method to call
958
+ *args: Positional arguments to pass to the method
959
+ panel: Panel name ("signal", "image", or None for auto-detection).
960
+ Defaults to None.
961
+ **kwargs: Keyword arguments to pass to the method
962
+
963
+ Returns:
964
+ The return value of the called method
965
+
966
+ Raises:
967
+ AttributeError: If the method does not exist or is not public
968
+ ValueError: If the panel name is invalid
969
+ """
970
+ return self._datalab.call_method(method_name, *args, panel=panel, **kwargs)
971
+
972
+ # =========================================================================
973
+ # Web API control methods
974
+ # =========================================================================
975
+
976
+ def start_webapi_server(
977
+ self,
978
+ host: str | None = None,
979
+ port: int | None = None,
980
+ ) -> dict:
981
+ """Start the Web API server.
982
+
983
+ Args:
984
+ host: Host address to bind to. Defaults to "127.0.0.1".
985
+ port: Port number. Defaults to auto-detect available port.
986
+
987
+ Returns:
988
+ Dictionary with "url" and "token" keys.
989
+
990
+ Raises:
991
+ RuntimeError: If Web API dependencies not installed or server
992
+ already running.
993
+ """
994
+ return self._datalab.start_webapi_server(host, port)
995
+
996
+ def stop_webapi_server(self) -> None:
997
+ """Stop the Web API server."""
998
+ return self._datalab.stop_webapi_server()
999
+
1000
+ def get_webapi_status(self) -> dict:
1001
+ """Get Web API server status.
1002
+
1003
+ Returns:
1004
+ Dictionary with "running", "url", and "token" keys.
1005
+ """
1006
+ return self._datalab.get_webapi_status()
datalab/control/remote.py CHANGED
@@ -48,12 +48,26 @@ def remote_call(func: Callable) -> object:
48
48
  @functools.wraps(func)
49
49
  def method_wrapper(*args, **kwargs):
50
50
  """Decorator wrapper function"""
51
- self = args[0] # extracting 'self' from method arguments
51
+ self: RemoteServer = args[0] # extracting 'self' from method arguments
52
52
  self.is_ready = False
53
53
  output = func(*args, **kwargs)
54
54
  while not self.is_ready:
55
55
  QC.QCoreApplication.processEvents()
56
56
  time.sleep(0.05)
57
+ # Check if an exception was raised and stored by the slot
58
+ if self.exception is not None:
59
+ exc = self.exception
60
+ self.exception = None # Clear the exception
61
+ raise exc
62
+ # For methods that use signal/slot pattern (like call_method),
63
+ # they return self.result which is set by the slot. The function
64
+ # itself returns this value, but it's set asynchronously, so we
65
+ # need to return the value AFTER waiting, not the initial return value.
66
+ # Since we wait above, self.result should now be set by the slot.
67
+ # If the function returned self.result, use the updated value.
68
+ if output is None:
69
+ # Return self.result which should be set by now
70
+ return self.result
57
71
  return output
58
72
 
59
73
  return method_wrapper
@@ -81,6 +95,7 @@ class RemoteServer(QC.QThread):
81
95
  SIG_TOGGLE_AUTO_REFRESH = QC.Signal(bool)
82
96
  SIG_TOGGLE_SHOW_TITLES = QC.Signal(bool)
83
97
  SIG_RESET_ALL = QC.Signal()
98
+ SIG_REMOVE_OBJECT = QC.Signal(bool)
84
99
  SIG_SAVE_TO_H5 = QC.Signal(str)
85
100
  SIG_OPEN_H5 = QC.Signal(list, bool, bool)
86
101
  SIG_IMPORT_H5 = QC.Signal(str, bool)
@@ -90,6 +105,7 @@ class RemoteServer(QC.QThread):
90
105
  SIG_RUN_MACRO = QC.Signal(str)
91
106
  SIG_STOP_MACRO = QC.Signal(str)
92
107
  SIG_IMPORT_MACRO_FROM_FILE = QC.Signal(str)
108
+ SIG_CALL_METHOD = QC.Signal(str, list, object, dict)
93
109
 
94
110
  def __init__(self, win: DLMainWindow) -> None:
95
111
  QC.QThread.__init__(self)
@@ -97,6 +113,8 @@ class RemoteServer(QC.QThread):
97
113
  self.is_ready = True
98
114
  self.server: SimpleXMLRPCServer | None = None
99
115
  self.win = win
116
+ self.result = None
117
+ self.exception = None
100
118
  win.SIG_READY.connect(self.datalab_is_ready)
101
119
  win.SIG_CLOSING.connect(self.shutdown_server)
102
120
  self.SIG_CLOSE_APP.connect(win.close)
@@ -113,6 +131,7 @@ class RemoteServer(QC.QThread):
113
131
  self.SIG_TOGGLE_AUTO_REFRESH.connect(win.toggle_auto_refresh)
114
132
  self.SIG_TOGGLE_SHOW_TITLES.connect(win.toggle_show_titles)
115
133
  self.SIG_RESET_ALL.connect(win.reset_all)
134
+ self.SIG_REMOVE_OBJECT.connect(win.remove_object)
116
135
  self.SIG_SAVE_TO_H5.connect(win.save_to_h5_file)
117
136
  self.SIG_OPEN_H5.connect(win.open_h5_files)
118
137
  self.SIG_IMPORT_H5.connect(win.import_h5_file)
@@ -122,6 +141,7 @@ class RemoteServer(QC.QThread):
122
141
  self.SIG_RUN_MACRO.connect(win.run_macro)
123
142
  self.SIG_STOP_MACRO.connect(win.stop_macro)
124
143
  self.SIG_IMPORT_MACRO_FROM_FILE.connect(win.import_macro_from_file)
144
+ self.SIG_CALL_METHOD.connect(win.call_method_slot)
125
145
 
126
146
  def serve(self) -> None:
127
147
  """Start server and serve forever"""
@@ -234,6 +254,15 @@ class RemoteServer(QC.QThread):
234
254
  """Reset all application data"""
235
255
  self.SIG_RESET_ALL.emit()
236
256
 
257
+ @remote_call
258
+ def remove_object(self, force: bool = False) -> None:
259
+ """Remove current object from current panel.
260
+
261
+ Args:
262
+ force: if True, remove object without confirmation. Defaults to False.
263
+ """
264
+ self.SIG_REMOVE_OBJECT.emit(force)
265
+
237
266
  @remote_call
238
267
  def save_to_h5_file(self, filename: str) -> None:
239
268
  """Save to a DataLab HDF5 file.
@@ -659,6 +688,83 @@ class RemoteServer(QC.QThread):
659
688
  """
660
689
  self.SIG_IMPORT_MACRO_FROM_FILE.emit(filename)
661
690
 
691
+ @remote_call
692
+ def call_method(
693
+ self,
694
+ method_name: str,
695
+ call_params: dict,
696
+ ):
697
+ """Call a public method on a panel or main window.
698
+
699
+ Method resolution order when panel is None:
700
+ 1. Try main window (DLMainWindow)
701
+ 2. If not found, try current panel (BaseDataPanel)
702
+
703
+ Args:
704
+ method_name: Name of the method to call
705
+ call_params: Dictionary with keys 'args' (list), 'panel' (str|None),
706
+ 'kwargs' (dict). Defaults to empty for missing keys.
707
+
708
+ Returns:
709
+ The return value of the called method
710
+
711
+ Raises:
712
+ AttributeError: If the method does not exist or is not public
713
+ ValueError: If the panel name is invalid
714
+
715
+ """
716
+ args = call_params.get("args", [])
717
+ panel = call_params.get("panel")
718
+ kwargs = call_params.get("kwargs", {})
719
+ self.result = None # Initialize result
720
+ self.SIG_CALL_METHOD.emit(method_name, args, panel, kwargs)
721
+ # The decorator waits for is_ready, then this returns self.result
722
+ # which was set by call_method_slot
723
+ return self.result
724
+
725
+ # =========================================================================
726
+ # Web API control methods
727
+ # =========================================================================
728
+
729
+ @remote_call
730
+ def start_webapi_server(
731
+ self,
732
+ host: str | None = None,
733
+ port: int | None = None,
734
+ ) -> dict:
735
+ """Start the Web API server.
736
+
737
+ Args:
738
+ host: Host address to bind to. Defaults to "127.0.0.1".
739
+ port: Port number. Defaults to auto-detect available port.
740
+
741
+ Returns:
742
+ Dictionary with "url" and "token" keys.
743
+
744
+ Raises:
745
+ RuntimeError: If Web API dependencies not installed or server
746
+ already running.
747
+ """
748
+ # Delegate to main window method which is @remote_controlled
749
+ # This ensures SIG_READY is emitted for the @remote_call decorator
750
+ return self.win.start_webapi_server(host, port)
751
+
752
+ @remote_call
753
+ def stop_webapi_server(self) -> None:
754
+ """Stop the Web API server."""
755
+ # Delegate to main window method which is @remote_controlled
756
+ return self.win.stop_webapi_server()
757
+
758
+ @remote_call
759
+ def get_webapi_status(self) -> dict:
760
+ """Get Web API server status.
761
+
762
+ Returns:
763
+ Dictionary with "running", "url", and "token" keys.
764
+ """
765
+ # Delegate to main window method which is @remote_controlled
766
+ return self.win.get_webapi_status()
767
+
662
768
 
663
769
  RemoteServer.check_remote_functions()
664
770
 
@@ -1038,3 +1144,71 @@ class RemoteClient(BaseProxy):
1038
1144
  items_json = items_to_json(items)
1039
1145
  if items_json is not None:
1040
1146
  self._datalab.add_annotations_from_items(items_json, refresh_plot, panel)
1147
+
1148
+ def call_method(
1149
+ self,
1150
+ method_name: str,
1151
+ *args,
1152
+ panel: str | None = None,
1153
+ **kwargs,
1154
+ ):
1155
+ """Call a public method on a panel or main window.
1156
+
1157
+ Method resolution order when panel is None:
1158
+ 1. Try main window (DLMainWindow)
1159
+ 2. If not found, try current panel (BaseDataPanel)
1160
+
1161
+ Args:
1162
+ method_name: Name of the method to call
1163
+ *args: Positional arguments to pass to the method
1164
+ panel: Panel name ("signal", "image", or None for auto-detection).
1165
+ Defaults to None.
1166
+ **kwargs: Keyword arguments to pass to the method
1167
+
1168
+ Returns:
1169
+ The return value of the called method
1170
+
1171
+ Raises:
1172
+ AttributeError: If the method does not exist or is not public
1173
+ ValueError: If the panel name is invalid
1174
+ """
1175
+ # Convert args/kwargs to single dict for XML-RPC serialization
1176
+ # This avoids XML-RPC signature mismatch issues with default parameters
1177
+ call_params = {
1178
+ "args": list(args) if args else [],
1179
+ "panel": panel,
1180
+ "kwargs": dict(kwargs) if kwargs else {},
1181
+ }
1182
+ return self._datalab.call_method(method_name, call_params)
1183
+
1184
+ # === WebAPI Server Control Methods ===
1185
+
1186
+ def start_webapi_server(
1187
+ self, host: str = "127.0.0.1", port: int = 8080
1188
+ ) -> dict[str, str | int]:
1189
+ """Start the WebAPI server.
1190
+
1191
+ Args:
1192
+ host: Host address to bind to. Defaults to "127.0.0.1".
1193
+ port: Port number. Defaults to 8080.
1194
+
1195
+ Returns:
1196
+ Dictionary with server info including 'url' and 'token'.
1197
+ """
1198
+ return self._datalab.start_webapi_server(host, port)
1199
+
1200
+ def stop_webapi_server(self) -> bool:
1201
+ """Stop the WebAPI server.
1202
+
1203
+ Returns:
1204
+ True if server was stopped, False if it wasn't running.
1205
+ """
1206
+ return self._datalab.stop_webapi_server()
1207
+
1208
+ def get_webapi_status(self) -> dict[str, str | int | bool]:
1209
+ """Get the current status of the WebAPI server.
1210
+
1211
+ Returns:
1212
+ Dictionary with status info including 'running', 'url', and 'port'.
1213
+ """
1214
+ return self._datalab.get_webapi_status()
Binary file
Binary file
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
2
+ <g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)">
3
+ <path d="M 44.092 7.316 c 0.294 -1.329 0.228 -2.79 -0.865 -4.437 C 42.093 1.171 40.271 -0.001 38.221 0 c -3.32 0.002 -6.012 2.695 -6.012 6.016 c 0 0.448 0.057 0.881 0.15 1.3 H 26.79 c -1.013 0 -1.835 0.821 -1.835 1.835 v 4.243 c 0 1.997 1.619 3.617 3.617 3.617 h 19.306 c 1.997 0 3.617 -1.619 3.617 -3.617 V 9.151 c 0 -1.013 -0.821 -1.835 -1.835 -1.835 H 44.092 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(25,118,210); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
4
+ <path d="M 76.203 90 h -34.86 c -2.298 0 -4.16 -1.863 -4.16 -4.16 V 36.921 c 0 -2.298 1.863 -4.16 4.16 -4.16 h 34.86 c 2.298 0 4.16 1.863 4.16 4.16 V 85.84 C 80.363 88.137 78.501 90 76.203 90 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(216,230,239); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
5
+ <path d="M 37.183 36.921 c 0 -2.298 1.863 -4.16 4.16 -4.16 h 25.471 v -17.72 c 0 -2.233 -1.811 -4.044 -4.044 -4.044 H 51.495 v 2.397 c 0 1.997 -1.619 3.617 -3.617 3.617 H 28.572 c -1.997 0 -3.617 -1.619 -3.617 -3.617 v -2.397 H 13.681 c -2.233 0 -4.044 1.811 -4.044 4.044 v 63.662 c 0 2.233 1.811 4.044 4.044 4.044 h 23.502 V 36.921 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(33,150,243); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
6
+ <path d="M 71.325 50.294 H 46.221 c -0.552 0 -1 -0.447 -1 -1 s 0.448 -1 1 -1 h 25.104 c 0.553 0 1 0.447 1 1 S 71.878 50.294 71.325 50.294 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(33,150,243); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
7
+ <path d="M 71.325 62.381 H 46.221 c -0.552 0 -1 -0.447 -1 -1 s 0.448 -1 1 -1 h 25.104 c 0.553 0 1 0.447 1 1 S 71.878 62.381 71.325 62.381 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(33,150,243); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
8
+ <line x1="-6.509" y1="0" x2="6.509" y2="0" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(33,150,243); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
9
+ <path d="M 59.238 74.468 H 46.221 c -0.552 0 -1 -0.447 -1 -1 s 0.448 -1 1 -1 h 13.018 c 0.553 0 1 0.447 1 1 S 59.791 74.468 59.238 74.468 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(33,150,243); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
10
+ </g>
11
+ </svg>
@@ -0,0 +1,19 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
2
+ <g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)">
3
+ <path d="M 2.653 89.174 c -0.256 0 -0.512 -0.098 -0.707 -0.293 c -0.391 -0.391 -0.391 -1.023 0 -1.414 l 40.243 -40.243 c 0.391 -0.391 1.023 -0.391 1.414 0 s 0.391 1.023 0 1.414 L 3.36 88.881 C 3.164 89.076 2.908 89.174 2.653 89.174 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
4
+ <path d="M 79.421 42.886 c -0.42 0 -0.83 0.04 -1.234 0.098 c -0.471 -2.513 -2.028 -4.641 -4.165 -5.88 c -2.078 2.778 -4.36 5.624 -6.854 8.539 l 0.246 9.709 h 10.621 h 9.12 c 0.573 -1.155 0.903 -2.452 0.903 -3.828 C 88.058 46.753 84.191 42.886 79.421 42.886 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
5
+ <path d="M 23.801 25.879 c 1.726 -1.726 4.122 -2.689 6.557 -2.619 l 15.765 0.4 c 2.773 -2.373 5.481 -4.546 8.131 -6.543 c -2.083 -3.049 -5.585 -5.052 -9.557 -5.052 c -0.563 0 -1.111 0.054 -1.653 0.132 C 42.041 6.83 37.34 2.767 31.683 2.767 c -5.657 0 -10.358 4.063 -11.362 9.428 c -0.541 -0.078 -1.09 -0.132 -1.653 -0.132 c -6.389 0 -11.568 5.179 -11.568 11.568 c 0 1.844 0.443 3.581 1.21 5.128 h 12.611 L 23.801 25.879 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
6
+ <path d="M 66.501 30.498 c -1.764 0 -3.423 -0.687 -4.67 -1.935 c -1.248 -1.248 -1.936 -2.906 -1.936 -4.67 c 0 -1.765 0.688 -3.423 1.936 -4.671 c 1.247 -1.248 2.906 -1.935 4.67 -1.935 c 1.765 0 3.424 0.687 4.671 1.935 c 1.248 1.248 1.935 2.906 1.935 4.671 c 0 1.764 -0.687 3.423 -1.935 4.67 l 0 0 C 69.925 29.811 68.265 30.498 66.501 30.498 z M 66.501 19.287 c -1.229 0 -2.386 0.479 -3.256 1.349 c -0.87 0.87 -1.35 2.026 -1.35 3.257 c 0 1.23 0.479 2.386 1.35 3.256 c 0.87 0.87 2.026 1.349 3.256 1.349 c 1.23 0 2.387 -0.479 3.257 -1.349 l 0 0 c 0.87 -0.87 1.349 -2.026 1.349 -3.256 c 0 -1.23 -0.479 -2.387 -1.349 -3.257 C 68.887 19.766 67.731 19.287 66.501 19.287 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
7
+ <path d="M 89.987 4.147 c 0.084 -0.903 -0.235 -1.789 -0.877 -2.431 c -0.642 -0.642 -1.534 -0.961 -2.431 -0.877 C 75.05 1.928 61.171 9.362 45.271 22.986 L 29.41 22.584 c -2.377 -0.071 -4.688 0.867 -6.362 2.541 l -7.246 7.246 c -0.804 0.804 -1.11 1.941 -0.817 3.04 c 0.293 1.099 1.123 1.933 2.221 2.231 l 11.27 3.056 l 7.056 7.055 c 0.391 0.391 1.023 0.391 1.414 0 s 0.391 -1.023 0 -1.414 l -6.547 -6.547 C 52.91 16.665 71.904 4.232 86.866 2.831 c 0.309 -0.024 0.611 0.081 0.83 0.3 s 0.328 0.522 0.299 0.83 c -1.4 14.961 -13.835 33.956 -36.961 56.467 l -6.548 -6.548 c -0.391 -0.391 -1.023 -0.391 -1.414 0 s -0.391 1.023 0 1.414 l 7.057 7.056 l 3.056 11.269 c 0.297 1.099 1.132 1.929 2.231 2.222 c 0.274 0.073 0.551 0.109 0.825 0.109 c 0.82 -0.001 1.611 -0.323 2.214 -0.927 l 7.245 -7.245 c 1.676 -1.676 2.603 -3.994 2.542 -6.362 L 67.84 45.555 C 81.463 29.655 88.897 15.776 89.987 4.147 z M 28.69 38.684 l -10.96 -2.972 c -0.579 -0.157 -0.76 -0.623 -0.812 -0.815 c -0.051 -0.193 -0.125 -0.688 0.299 -1.111 l 7.246 -7.246 c 1.29 -1.289 3.08 -2 4.897 -1.956 l 13.674 0.346 C 38.417 29.005 33.633 33.593 28.69 38.684 z M 66.242 61.467 c 0.047 1.822 -0.666 3.607 -1.956 4.897 l -7.245 7.245 c -0.424 0.424 -0.917 0.35 -1.11 0.299 c -0.193 -0.052 -0.659 -0.233 -0.816 -0.813 l -2.972 -10.959 c 5.09 -4.943 9.679 -9.726 13.754 -14.343 L 66.242 61.467 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
8
+ <path d="M 25.031 78.36 c -0.256 0 -0.512 -0.098 -0.707 -0.293 c -0.391 -0.391 -0.391 -1.023 0 -1.414 L 40.67 60.306 c 0.391 -0.391 1.023 -0.391 1.414 0 s 0.391 1.023 0 1.414 L 25.738 78.067 C 25.542 78.262 25.286 78.36 25.031 78.36 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
9
+ <path d="M 8.92 70.356 c -0.256 0 -0.512 -0.098 -0.707 -0.293 c -0.391 -0.391 -0.391 -1.023 0 -1.414 l 19.811 -19.811 c 0.391 -0.391 1.023 -0.391 1.414 0 s 0.391 1.023 0 1.414 L 9.627 70.063 C 9.431 70.259 9.176 70.356 8.92 70.356 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
10
+ <path d="M 35.868 79.835 c -0.256 0 -0.512 -0.098 -0.707 -0.293 c -0.391 -0.391 -0.391 -1.023 0 -1.414 l 8.834 -8.835 c 0.391 -0.391 1.023 -0.391 1.414 0 s 0.391 1.023 0 1.414 l -8.834 8.835 C 36.38 79.737 36.124 79.835 35.868 79.835 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
11
+ <path d="M 13.346 53.618 c -0.256 0 -0.512 -0.098 -0.707 -0.293 c -0.391 -0.391 -0.391 -1.023 0 -1.414 l 7.224 -7.223 c 0.391 -0.391 1.023 -0.391 1.414 0 s 0.391 1.023 0 1.415 l -7.224 7.223 C 13.858 53.52 13.602 53.618 13.346 53.618 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
12
+ <path d="M 19.829 85.689 c -0.552 0 -1 -0.447 -1 -1 v -4.256 c 0 -0.553 0.448 -1 1 -1 s 1 0.447 1 1 v 4.256 C 20.829 85.242 20.382 85.689 19.829 85.689 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
13
+ <path d="M 21.957 83.561 h -4.255 c -0.552 0 -1 -0.447 -1 -1 s 0.448 -1 1 -1 h 4.255 c 0.552 0 1 0.447 1 1 S 22.509 83.561 21.957 83.561 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
14
+ <path d="M 8.137 60.955 c -0.552 0 -1 -0.447 -1 -1 v -4.256 c 0 -0.553 0.448 -1 1 -1 s 1 0.447 1 1 v 4.256 C 9.137 60.508 8.69 60.955 8.137 60.955 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
15
+ <path d="M 10.265 58.827 H 6.01 c -0.552 0 -1 -0.447 -1 -1 s 0.448 -1 1 -1 h 4.255 c 0.552 0 1 0.447 1 1 S 10.817 58.827 10.265 58.827 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
16
+ <path d="M 3.194 78.277 c -0.853 0 -1.655 -0.333 -2.259 -0.937 C 0.332 76.738 0 75.935 0 75.083 s 0.332 -1.655 0.935 -2.259 c 1.246 -1.244 3.272 -1.244 4.517 0 c 0.604 0.604 0.936 1.406 0.936 2.259 s -0.332 1.654 -0.935 2.258 l 0 0 C 4.849 77.944 4.047 78.277 3.194 78.277 z M 3.194 73.889 c -0.306 0 -0.611 0.116 -0.844 0.349 C 2.124 74.464 2 74.763 2 75.083 c 0 0.318 0.124 0.618 0.35 0.844 c 0.451 0.451 1.238 0.451 1.688 0 h 0 c 0.226 -0.226 0.35 -0.525 0.35 -0.844 c 0 -0.319 -0.124 -0.619 -0.35 -0.845 C 3.805 74.006 3.499 73.889 3.194 73.889 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
17
+ <path d="M 30.658 87.236 c -0.818 0 -1.636 -0.312 -2.258 -0.934 c -0.604 -0.603 -0.936 -1.405 -0.936 -2.259 c 0 -0.853 0.332 -1.655 0.936 -2.258 c 1.245 -1.244 3.271 -1.244 4.517 0 c 0.604 0.603 0.936 1.405 0.936 2.258 c 0 0.854 -0.332 1.656 -0.936 2.259 C 32.294 86.925 31.476 87.236 30.658 87.236 z M 30.658 82.851 c -0.306 0 -0.611 0.116 -0.844 0.349 c -0.226 0.226 -0.35 0.525 -0.35 0.844 c 0 0.319 0.124 0.619 0.35 0.845 c 0.466 0.465 1.223 0.465 1.688 0 c 0.226 -0.226 0.35 -0.525 0.35 -0.845 c 0 -0.318 -0.124 -0.618 -0.35 -0.844 C 31.27 82.968 30.964 82.851 30.658 82.851 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,204,29); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
18
+ </g>
19
+ </svg>
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
2
+ <g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)">
3
+ <circle cx="45" cy="45" r="45" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(230,62,50); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
4
+ <path d="M 45 32 c -1.104 0 -2 -0.896 -2 -2 V 18.595 c 0 -1.104 0.896 -2 2 -2 s 2 0.896 2 2 V 30 C 47 31.104 46.104 32 45 32 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
5
+ <path d="M 45 71 c -14.336 0 -26 -11.663 -26 -26 c 0 -9.587 5.251 -18.366 13.704 -22.912 c 0.973 -0.525 2.186 -0.158 2.708 0.814 c 0.523 0.973 0.159 2.186 -0.814 2.708 C 27.444 29.458 23 36.888 23 45 c 0 12.131 9.869 22 22 22 s 22 -9.869 22 -22 c 0 -8.112 -4.444 -15.542 -11.599 -19.389 c -0.973 -0.523 -1.337 -1.736 -0.814 -2.708 c 0.522 -0.972 1.733 -1.339 2.709 -0.814 C 65.749 26.634 71 35.414 71 45 C 71 59.337 59.337 71 45 71 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
6
+ </g>
7
+ </svg>
datalab/gui/docks.py CHANGED
@@ -213,8 +213,9 @@ def get_more_image_stats(
213
213
  if xunit and zunit:
214
214
  densityfmt += " " + zunit + "/" + xunit + "²"
215
215
  info = info + f"<br>ρ = {densityfmt % density}"
216
-
217
- c_i, c_j = measure.centroid(data)
216
+ # Convert data (ndarray) to a simple array to compute centroid with the new
217
+ # einsum optimisation introduce in numpy 2.4.0 and scikit-image 0.26.0
218
+ c_i, c_j = measure.centroid(np.array(data))
218
219
  c_x, c_y = item.get_plot_coordinates(c_j + ix0, c_i + iy0)
219
220
  info += "<br>" + "<br>".join(
220
221
  [