datalab-platform 1.0.1__py3-none-any.whl → 1.0.2__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.
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.1"
27
+ __version__ = "1.0.2"
28
28
  __docurl__ = __homeurl__ = "https://datalab-platform.com/"
29
29
  __supporturl__ = "https://github.com/DataLab-Platform/DataLab/issues/new/choose"
30
30
 
@@ -39,11 +39,13 @@ def plotitem_to_singleroi(
39
39
  | AnnotatedRectangle
40
40
  | AnnotatedCircle
41
41
  | AnnotatedPolygon,
42
+ obj: SignalObj | ImageObj | None = None,
42
43
  ) -> SegmentROI | RectangularROI | CircularROI | PolygonalROI:
43
44
  """Create a single ROI from the given PlotPy item to integrate with DataLab
44
45
 
45
46
  Args:
46
47
  plot_item: The PlotPy item for which to create a single ROI
48
+ obj: Optional signal or image object for coordinate rounding
47
49
 
48
50
  Returns:
49
51
  A single ROI instance
@@ -66,7 +68,7 @@ def plotitem_to_singleroi(
66
68
  adapter = PolygonalROIPlotPyAdapter
67
69
  else:
68
70
  raise TypeError(f"Unsupported PlotPy item type: {type(plot_item)}")
69
- return adapter.from_plot_item(plot_item)
71
+ return adapter.from_plot_item(plot_item, obj)
70
72
 
71
73
 
72
74
  def singleroi_to_plotitem(
@@ -0,0 +1,157 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ ROI Coordinate Utilities
5
+ =========================
6
+
7
+ This module provides utility functions for rounding ROI coordinates to appropriate
8
+ precision based on the sampling characteristics of signals and images.
9
+
10
+ These functions are used when converting interactive PlotPy shapes to ROI objects
11
+ to ensure coordinates are displayed with reasonable precision.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import numpy as np
17
+ from sigima.objects import ImageObj, ROI1DParam, ROI2DParam, SignalObj
18
+
19
+
20
+ def round_signal_coords(
21
+ obj: SignalObj, coords: list[float], precision_factor: float = 0.1
22
+ ) -> list[float]:
23
+ """Round signal coordinates to appropriate precision based on sampling period.
24
+
25
+ Rounds to a fraction of the median sampling period to avoid excessive decimal
26
+ places while maintaining reasonable precision.
27
+
28
+ Args:
29
+ obj: signal object
30
+ coords: coordinates to round
31
+ precision_factor: fraction of sampling period to use as rounding precision.
32
+ Default is 0.1 (1/10th of sampling period).
33
+
34
+ Returns:
35
+ Rounded coordinates
36
+ """
37
+ if len(obj.x) < 2:
38
+ # Cannot compute sampling period, return coords as-is
39
+ return coords
40
+ # Compute median sampling period
41
+ sampling_period = float(np.median(np.diff(obj.x)))
42
+ if sampling_period == 0:
43
+ # Avoid division by zero for constant x arrays
44
+ return coords
45
+ # Round to specified fraction of sampling period
46
+ precision = sampling_period * precision_factor
47
+ # Determine number of decimal places
48
+ if precision > 0:
49
+ decimals = max(0, int(-np.floor(np.log10(precision))))
50
+ return [round(c, decimals) for c in coords]
51
+ return coords
52
+
53
+
54
+ def round_image_coords(
55
+ obj: ImageObj, coords: list[float], precision_factor: float = 0.1
56
+ ) -> list[float]:
57
+ """Round image coordinates to appropriate precision based on pixel spacing.
58
+
59
+ Rounds to a fraction of the pixel spacing to avoid excessive decimal places
60
+ while maintaining reasonable precision. Uses separate precision for X and Y.
61
+
62
+ Args:
63
+ obj: image object
64
+ coords: flat list of coordinates [x0, y0, x1, y1, ...] to round
65
+ precision_factor: fraction of pixel spacing to use as rounding precision.
66
+ Default is 0.1 (1/10th of pixel spacing).
67
+
68
+ Returns:
69
+ Rounded coordinates
70
+
71
+ Raises:
72
+ ValueError: if coords does not contain an even number of elements
73
+ """
74
+ if len(coords) % 2 != 0:
75
+ raise ValueError("coords must contain an even number of elements (x, y pairs).")
76
+ if len(coords) == 0:
77
+ return coords
78
+
79
+ rounded = list(coords)
80
+ if obj.is_uniform_coords:
81
+ # Use dx, dy for uniform coordinates
82
+ precision_x = abs(obj.dx) * precision_factor
83
+ precision_y = abs(obj.dy) * precision_factor
84
+ else:
85
+ # Compute average spacing for non-uniform coordinates
86
+ if len(obj.xcoords) > 1:
87
+ avg_dx = float(np.mean(np.abs(np.diff(obj.xcoords))))
88
+ precision_x = avg_dx * precision_factor
89
+ else:
90
+ precision_x = 0
91
+ if len(obj.ycoords) > 1:
92
+ avg_dy = float(np.mean(np.abs(np.diff(obj.ycoords))))
93
+ precision_y = avg_dy * precision_factor
94
+ else:
95
+ precision_y = 0
96
+
97
+ # Round X coordinates (even indices)
98
+ if precision_x > 0:
99
+ decimals_x = max(0, int(-np.floor(np.log10(precision_x))))
100
+ for i in range(0, len(rounded), 2):
101
+ rounded[i] = round(rounded[i], decimals_x)
102
+
103
+ # Round Y coordinates (odd indices)
104
+ if precision_y > 0:
105
+ decimals_y = max(0, int(-np.floor(np.log10(precision_y))))
106
+ for i in range(1, len(rounded), 2):
107
+ rounded[i] = round(rounded[i], decimals_y)
108
+
109
+ return rounded
110
+
111
+
112
+ def round_signal_roi_param(
113
+ obj: SignalObj, param: ROI1DParam, precision_factor: float = 0.1
114
+ ) -> None:
115
+ """Round signal ROI parameter coordinates in-place.
116
+
117
+ Args:
118
+ obj: signal object
119
+ param: ROI parameter to round (modified in-place)
120
+ precision_factor: fraction of sampling period to use as rounding precision
121
+ """
122
+ coords = round_signal_coords(obj, [param.xmin, param.xmax], precision_factor)
123
+ param.xmin, param.xmax = coords
124
+
125
+
126
+ def round_image_roi_param(
127
+ obj: ImageObj, param: ROI2DParam, precision_factor: float = 0.1
128
+ ) -> None:
129
+ """Round image ROI parameter coordinates in-place.
130
+
131
+ Args:
132
+ obj: image object
133
+ param: ROI parameter to round (modified in-place)
134
+ precision_factor: fraction of pixel spacing to use as rounding precision
135
+ """
136
+ if param.geometry == "rectangle":
137
+ # Round x0, y0, dx, dy
138
+ x0, y0, x1, y1 = param.x0, param.y0, param.x0 + param.dx, param.y0 + param.dy
139
+ coords = round_image_coords(obj, [x0, y0, x1, y1], precision_factor)
140
+ param.x0, param.y0 = coords[0], coords[1]
141
+ # Round dx and dy to avoid floating-point errors in subtraction
142
+ dx_dy_rounded = round_image_coords(
143
+ obj, [coords[2] - coords[0], coords[3] - coords[1]], precision_factor
144
+ )
145
+ param.dx = dx_dy_rounded[0]
146
+ param.dy = dx_dy_rounded[1]
147
+ elif param.geometry == "circle":
148
+ # Round xc, yc, r
149
+ coords = round_image_coords(obj, [param.xc, param.yc], precision_factor)
150
+ param.xc, param.yc = coords
151
+ # Round radius using X precision
152
+ r_rounded = round_image_coords(obj, [param.r, 0], precision_factor)[0]
153
+ param.r = r_rounded
154
+ elif param.geometry == "polygon":
155
+ # Round polygon points
156
+ rounded = round_image_coords(obj, param.points.tolist(), precision_factor)
157
+ param.points = np.array(rounded)
@@ -13,6 +13,7 @@ from plotpy.items import AnnotatedCircle, AnnotatedPolygon, AnnotatedRectangle
13
13
  from sigima.objects import CircularROI, ImageObj, ImageROI, PolygonalROI, RectangularROI
14
14
  from sigima.tools import coordinates
15
15
 
16
+ from datalab.adapters_plotpy.coordutils import round_image_coords
16
17
  from datalab.adapters_plotpy.roi.base import (
17
18
  BaseROIPlotPyAdapter,
18
19
  BaseSingleROIPlotPyAdapter,
@@ -64,14 +65,21 @@ class PolygonalROIPlotPyAdapter(
64
65
  return item
65
66
 
66
67
  @classmethod
67
- def from_plot_item(cls, item: AnnotatedPolygon) -> PolygonalROI:
68
+ def from_plot_item(
69
+ cls, item: AnnotatedPolygon, obj: ImageObj | None = None
70
+ ) -> PolygonalROI:
68
71
  """Create ROI from plot item
69
72
 
70
73
  Args:
71
74
  item: plot item
75
+ obj: image object for coordinate rounding (optional)
72
76
  """
77
+ coords = item.get_points().flatten().tolist()
78
+ # Round coordinates to appropriate precision
79
+ if obj is not None:
80
+ coords = round_image_coords(obj, coords)
73
81
  title = str(item.title().text())
74
- return PolygonalROI(item.get_points().flatten(), False, title)
82
+ return PolygonalROI(coords, False, title)
75
83
 
76
84
 
77
85
  class RectangularROIPlotPyAdapter(
@@ -116,15 +124,22 @@ class RectangularROIPlotPyAdapter(
116
124
  return item
117
125
 
118
126
  @classmethod
119
- def from_plot_item(cls, item: AnnotatedRectangle) -> RectangularROI:
127
+ def from_plot_item(
128
+ cls, item: AnnotatedRectangle, obj: ImageObj | None = None
129
+ ) -> RectangularROI:
120
130
  """Create ROI from plot item
121
131
 
122
132
  Args:
123
133
  item: plot item
134
+ obj: image object for coordinate rounding (optional)
124
135
  """
125
136
  rect = item.get_rect()
137
+ coords = RectangularROI.rect_to_coords(*rect)
138
+ # Round coordinates to appropriate precision
139
+ if obj is not None:
140
+ coords = round_image_coords(obj, coords)
126
141
  title = str(item.title().text())
127
- return RectangularROI(RectangularROI.rect_to_coords(*rect), False, title)
142
+ return RectangularROI(coords, False, title)
128
143
 
129
144
 
130
145
  class CircularROIPlotPyAdapter(
@@ -166,15 +181,29 @@ class CircularROIPlotPyAdapter(
166
181
  return item
167
182
 
168
183
  @classmethod
169
- def from_plot_item(cls, item: AnnotatedCircle) -> CircularROI:
184
+ def from_plot_item(
185
+ cls, item: AnnotatedCircle, obj: ImageObj | None = None
186
+ ) -> CircularROI:
170
187
  """Create ROI from plot item
171
188
 
172
189
  Args:
173
190
  item: plot item
191
+ obj: image object for coordinate rounding (optional)
174
192
  """
175
193
  rect = item.get_rect()
194
+ coords = CircularROI.rect_to_coords(*rect)
195
+ # Round coordinates to appropriate precision
196
+ # For circular ROI: [xc, yc, r] - round center (xc, yc) as pair, then radius
197
+ if obj is not None:
198
+ xc, yc, r = coords
199
+ # Round center coordinates
200
+ xc_rounded, yc_rounded = round_image_coords(obj, [xc, yc])
201
+ # Round radius using average of X and Y precision
202
+ # For radius, we use the X precision (could also average X and Y)
203
+ r_rounded = round_image_coords(obj, [r, 0])[0]
204
+ coords = [xc_rounded, yc_rounded, r_rounded]
176
205
  title = str(item.title().text())
177
- return CircularROI(CircularROI.rect_to_coords(*rect), False, title)
206
+ return CircularROI(coords, False, title)
178
207
 
179
208
 
180
209
  class ImageROIPlotPyAdapter(BaseROIPlotPyAdapter[ImageROI]):
@@ -11,6 +11,7 @@ from plotpy.builder import make
11
11
  from plotpy.items import AnnotatedXRange
12
12
  from sigima.objects import SegmentROI, SignalObj, SignalROI
13
13
 
14
+ from datalab.adapters_plotpy.coordutils import round_signal_coords
14
15
  from datalab.adapters_plotpy.roi.base import (
15
16
  BaseROIPlotPyAdapter,
16
17
  BaseSingleROIPlotPyAdapter,
@@ -36,11 +37,14 @@ class SegmentROIPlotPyAdapter(BaseSingleROIPlotPyAdapter[SegmentROI, AnnotatedXR
36
37
  return item
37
38
 
38
39
  @classmethod
39
- def from_plot_item(cls, item: AnnotatedXRange) -> SegmentROI:
40
+ def from_plot_item(
41
+ cls, item: AnnotatedXRange, obj: SignalObj | None = None
42
+ ) -> SegmentROI:
40
43
  """Create ROI from plot item
41
44
 
42
45
  Args:
43
46
  item: plot item
47
+ obj: signal object for coordinate rounding (optional)
44
48
 
45
49
  Returns:
46
50
  ROI
@@ -48,6 +52,9 @@ class SegmentROIPlotPyAdapter(BaseSingleROIPlotPyAdapter[SegmentROI, AnnotatedXR
48
52
  if not isinstance(item, AnnotatedXRange):
49
53
  raise TypeError("Invalid plot item type")
50
54
  coords = sorted(item.get_range())
55
+ # Round coordinates to appropriate precision
56
+ if obj is not None:
57
+ coords = round_signal_coords(obj, coords)
51
58
  title = str(item.title().text())
52
59
  return SegmentROI(coords, False, title)
53
60
 
datalab/config.py CHANGED
@@ -301,6 +301,7 @@ class ViewSection(conf.Section, metaclass=conf.SectionMeta):
301
301
  ima_def_interpolation = conf.Option()
302
302
  ima_def_alpha = conf.Option()
303
303
  ima_def_alpha_function = conf.Option()
304
+ ima_def_keep_lut_range = conf.Option()
304
305
 
305
306
  # Annotated shape and marker visualization settings for signals
306
307
  sig_shape_param = conf.DataSetOption()
@@ -459,6 +460,7 @@ def initialize():
459
460
  Conf.view.ima_def_interpolation.get(5)
460
461
  Conf.view.ima_def_alpha.get(1.0)
461
462
  Conf.view.ima_def_alpha_function.get(LUTAlpha.NONE.value)
463
+ Conf.view.ima_def_keep_lut_range.get(False)
462
464
 
463
465
  # Datetime format strings: % must be escaped as %% for ConfigParser
464
466
  Conf.view.sig_datetime_format_s.get("%%H:%%M:%%S")
Binary file
Binary file
@@ -1269,7 +1269,9 @@ class SignalActionHandler(BaseActionHandler):
1269
1269
  with self.new_menu(_("Axis transformation")):
1270
1270
  self.action_for("transpose")
1271
1271
  self.action_for("reverse_x")
1272
- self.action_for("to_cartesian")
1272
+ self.action_for("replace_x_by_other_y")
1273
+ self.action_for("xy_mode")
1274
+ self.action_for("to_cartesian", separator=True)
1273
1275
  self.action_for("to_polar")
1274
1276
  with self.new_menu(_("Frequency filters"), icon_name="highpass.svg"):
1275
1277
  self.action_for("lowpass")
@@ -1364,7 +1366,6 @@ class SignalActionHandler(BaseActionHandler):
1364
1366
  separator=True,
1365
1367
  tip=_("Compute all stability features"),
1366
1368
  )
1367
- self.action_for("xy_mode", separator=True)
1368
1369
 
1369
1370
  # MARK: ANALYSIS
1370
1371
  with self.new_category(ActionCategory.ANALYSIS):
@@ -93,6 +93,7 @@ print("All done!")
93
93
  self.set_code(self.MACRO_SAMPLE)
94
94
  self.editor.modificationChanged.connect(self.modification_changed)
95
95
  self.process = None
96
+ self.__last_exit_code = None
96
97
 
97
98
  @property
98
99
  def title(self) -> str:
@@ -259,7 +260,14 @@ print("All done!")
259
260
  self.process = QC.QProcess()
260
261
  code = self.get_code().replace('"', "'")
261
262
  datalab_path = osp.abspath(osp.join(osp.dirname(datalab.__file__), os.pardir))
262
- code = f"import sys; sys.path.append(r'{datalab_path}'){os.linesep}{code}"
263
+ # Reconfigure stdout/stderr to use UTF-8 encoding to avoid UnicodeEncodeError
264
+ # on Windows with locales that don't support all Unicode characters
265
+ # (e.g., cp1252)
266
+ code = (
267
+ f"import sys; sys.path.append(r'{datalab_path}'); "
268
+ f"sys.stdout.reconfigure(encoding='utf-8'); "
269
+ f"sys.stderr.reconfigure(encoding='utf-8'){os.linesep}{code}"
270
+ )
263
271
  env = QC.QProcessEnvironment()
264
272
  env.insert(execenv.XMLRPCPORT_ENV, str(execenv.xmlrpcport))
265
273
  sysenv = env.systemEnvironment()
@@ -305,6 +313,15 @@ print("All done!")
305
313
  exit_code: Exit code
306
314
  exit_status: Exit status
307
315
  """
316
+ self.__last_exit_code = exit_code
308
317
  self.print(_("# <== '%s' macro has finished") % self.title, eol_before=False)
309
318
  self.FINISHED.emit()
310
319
  self.process = None
320
+
321
+ def get_exit_code(self) -> int | None:
322
+ """Return last exit code of the macro process
323
+
324
+ Returns:
325
+ Last exit code or None if process has not finished yet
326
+ """
327
+ return self.__last_exit_code
datalab/gui/main.py CHANGED
@@ -1617,6 +1617,8 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
1617
1617
  )
1618
1618
  if answer == QW.QMessageBox.Yes:
1619
1619
  reset_all = True
1620
+ elif answer == QW.QMessageBox.No:
1621
+ reset_all = False
1620
1622
  elif answer == QW.QMessageBox.Ignore:
1621
1623
  Conf.io.h5_clear_workspace_ask.set(False)
1622
1624
  if h5files is None:
datalab/gui/newobject.py CHANGED
@@ -122,6 +122,13 @@ def create_signal_gui(
122
122
  param = NewSignalParam()
123
123
  edit = True # Default to editing if no parameters provided
124
124
 
125
+ # CustomSignalParam requires edit mode to initialize the xyarray.
126
+ # Without this, if edit=False (the default in new_object), the setup_array
127
+ # call would be skipped, leaving xyarray as None, which would cause an
128
+ # AttributeError when trying to access param.xyarray.T later.
129
+ if isinstance(param, OrigCustomSignalParam):
130
+ edit = True
131
+
125
132
  if isinstance(param, OrigCustomSignalParam) and edit:
126
133
  p_init = NewSignalParam(_("Custom signal"))
127
134
  p_init.size = 10 # Set smaller default size for initial input
datalab/gui/panel/base.py CHANGED
@@ -216,6 +216,13 @@ class ObjectProp(QW.QWidget):
216
216
  self.analysis_parameters.setReadOnly(True)
217
217
  self.analysis_parameters.setFont(font)
218
218
 
219
+ # Track newly created objects to show Creation tab only once
220
+ self._newly_created_obj_uuid: str | None = None
221
+ # Track when analysis results were just computed
222
+ self._fresh_analysis_obj_uuid: str | None = None
223
+ # Track when object was just processed (1-to-1)
224
+ self._fresh_processing_obj_uuid: str | None = None
225
+
219
226
  self.tabwidget.addTab(
220
227
  self.processing_history, get_icon("history.svg"), _("History")
221
228
  )
@@ -370,11 +377,16 @@ class ObjectProp(QW.QWidget):
370
377
  self.properties.get()
371
378
  self.properties.apply_button.setEnabled(False)
372
379
 
373
- def update_properties_from(self, obj: SignalObj | ImageObj | None = None) -> None:
380
+ def update_properties_from(
381
+ self,
382
+ obj: SignalObj | ImageObj | None = None,
383
+ force_tab: Literal["creation", "processing", "analysis", None] | None = None,
384
+ ) -> None:
374
385
  """Update properties panel (properties, creation, processing) from object.
375
386
 
376
387
  Args:
377
388
  obj: Signal or Image object
389
+ force_tab: Force a specific tab to be current
378
390
  """
379
391
  self.properties.setDisabled(obj is None)
380
392
  if obj is None:
@@ -411,30 +423,56 @@ class ObjectProp(QW.QWidget):
411
423
  self.processing_scroll = None
412
424
 
413
425
  # Setup Creation and Processing tabs (if applicable)
414
- has_creation_tab = has_processing_tab = False
426
+ has_creation_tab = False
427
+ has_processing_tab = False
415
428
  if obj is not None:
416
429
  has_creation_tab = self.setup_creation_tab(obj)
417
- has_processing_tab = self.setup_processing_tab(obj)
430
+ has_processing_tab = self.setup_processing_tab(obj) # Processing tab setup
418
431
 
419
432
  # Trigger visibility update for History and Analysis parameters tabs
420
433
  # (will be called via textChanged signals, but we call explicitly
421
434
  # here to ensure initial state is correct)
422
435
  self._update_tab_visibility()
423
436
 
424
- # Handle priority regarding the tab to set as current:
425
- # 1. Analysis parameters if content exists
426
- # 2. Creation tab if it exists
427
- # 3. Processing tab if it exists
428
- # 4. Properties tab
429
- if has_analysis_parameters:
430
- self.tabwidget.setCurrentWidget(self.analysis_parameters)
431
- elif has_creation_tab:
437
+ # Determine which tab to show based on force_tab parameter:
438
+ # - If force_tab="creation" and Creation tab exists, show it
439
+ # - If force_tab="processing" and Processing tab exists, show it
440
+ # - If force_tab="analysis" and Analysis tab has content, show it
441
+ # - Otherwise, always show Properties tab (default behavior)
442
+ if force_tab == "creation" and has_creation_tab:
432
443
  self.tabwidget.setCurrentWidget(self.creation_scroll)
433
- elif has_processing_tab:
444
+ elif force_tab == "processing" and has_processing_tab:
434
445
  self.tabwidget.setCurrentWidget(self.processing_scroll)
446
+ elif force_tab == "analysis" and has_analysis_parameters:
447
+ self.tabwidget.setCurrentWidget(self.analysis_parameters)
435
448
  else:
449
+ # Default: always show Properties tab when switching objects
436
450
  self.tabwidget.setCurrentWidget(self.properties)
437
451
 
452
+ def mark_as_newly_created(self, obj: SignalObj | ImageObj) -> None:
453
+ """Mark object to show Creation tab on next selection.
454
+
455
+ Args:
456
+ obj: Object to mark
457
+ """
458
+ self._newly_created_obj_uuid = get_uuid(obj)
459
+
460
+ def mark_as_freshly_processed(self, obj: SignalObj | ImageObj) -> None:
461
+ """Mark object to show Processing tab on next selection.
462
+
463
+ Args:
464
+ obj: Object to mark
465
+ """
466
+ self._fresh_processing_obj_uuid = get_uuid(obj)
467
+
468
+ def mark_as_fresh_analysis(self, obj: SignalObj | ImageObj) -> None:
469
+ """Mark object to show Analysis tab on next selection.
470
+
471
+ Args:
472
+ obj: Object to mark
473
+ """
474
+ self._fresh_analysis_obj_uuid = get_uuid(obj)
475
+
438
476
  def get_changed_properties(self) -> dict[str, Any]:
439
477
  """Get dictionary of properties that have changed from original values.
440
478
 
@@ -1490,6 +1528,16 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
1490
1528
  obj.check_data()
1491
1529
  self.objmodel.add_object(obj, group_id)
1492
1530
 
1531
+ # Mark this object as newly created to show Creation tab on first selection
1532
+ # BUT: Don't overwrite if this object is already marked as freshly processed
1533
+ # or has fresh analysis results (those take precedence)
1534
+ obj_uuid = get_uuid(obj)
1535
+ if (
1536
+ obj_uuid != self.objprop._fresh_processing_obj_uuid
1537
+ and obj_uuid != self.objprop._fresh_analysis_obj_uuid
1538
+ ):
1539
+ self.objprop.mark_as_newly_created(obj)
1540
+
1493
1541
  # Block signals to avoid updating the plot (unnecessary refresh)
1494
1542
  self.objview.blockSignals(True)
1495
1543
  self.objview.add_object_item(obj, group_id, set_current=set_current)
@@ -2419,7 +2467,26 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
2419
2467
  """
2420
2468
  selected_objects = self.objview.get_sel_objects(include_groups=True)
2421
2469
  selected_groups = self.objview.get_sel_groups()
2422
- self.objprop.update_properties_from(self.objview.get_current_object())
2470
+
2471
+ # Determine which tab to show based on object state
2472
+ current_obj = self.objview.get_current_object()
2473
+ force_tab = None
2474
+ if current_obj is not None:
2475
+ obj_uuid = get_uuid(current_obj)
2476
+ # Show Creation tab for newly created objects (only once)
2477
+ if obj_uuid == self.objprop._newly_created_obj_uuid:
2478
+ force_tab = "creation"
2479
+ self.objprop._newly_created_obj_uuid = None
2480
+ # Show Processing tab for freshly processed objects (only once)
2481
+ elif obj_uuid == self.objprop._fresh_processing_obj_uuid:
2482
+ force_tab = "processing"
2483
+ self.objprop._fresh_processing_obj_uuid = None
2484
+ # Show Analysis tab for objects with fresh analysis results
2485
+ elif obj_uuid == self.objprop._fresh_analysis_obj_uuid:
2486
+ force_tab = "analysis"
2487
+ self.objprop._fresh_analysis_obj_uuid = None
2488
+
2489
+ self.objprop.update_properties_from(current_obj, force_tab=force_tab)
2423
2490
  self.acthandler.selected_objects_changed(selected_groups, selected_objects)
2424
2491
  self.refresh_plot("selected", update_items, False)
2425
2492
 
@@ -55,7 +55,16 @@ if TYPE_CHECKING:
55
55
 
56
56
 
57
57
  def calc_data_hash(obj: SignalObj | ImageObj) -> str:
58
- """Calculate a hash for a SignalObj | ImageObj object's data"""
58
+ """Calculate a hash for a SignalObj | ImageObj object's data
59
+
60
+ For signals, this includes both X and Y data to detect axis changes.
61
+ For images, this includes only the Z data.
62
+ """
63
+ if isinstance(obj, SignalObj):
64
+ # For signals, hash both X and Y data to detect axis changes
65
+ # (e.g., when xmin/xmax is modified without changing Y values)
66
+ return hashlib.sha1(np.ascontiguousarray(obj.xydata)).hexdigest()
67
+ # For images, hash only the image data
59
68
  return hashlib.sha1(np.ascontiguousarray(obj.data)).hexdigest()
60
69
 
61
70
 
@@ -42,6 +42,7 @@ from datalab.adapters_metadata import (
42
42
  TableAdapter,
43
43
  show_resultdata,
44
44
  )
45
+ from datalab.adapters_plotpy import coordutils
45
46
  from datalab.config import Conf, _
46
47
  from datalab.gui.processor.catcher import CompOut, wng_err_func
47
48
  from datalab.objectmodel import get_short_id, get_uuid, patch_title_with_ids
@@ -194,6 +195,23 @@ def insert_processing_parameters(
194
195
  obj.set_metadata_option(PROCESSING_PARAMETERS_OPTION, pp.to_dict())
195
196
 
196
197
 
198
+ def run_with_env(func: Callable, args: tuple, env_json: str) -> CompOut:
199
+ """Wrapper to apply environment config before calling func
200
+
201
+ Args:
202
+ func: function to call
203
+ args: function arguments
204
+ env_json: JSON string with environment configuration
205
+
206
+ Returns:
207
+ Computation output object containing the result, error message,
208
+ or warning message.
209
+ """
210
+ sigima_options.set_env(env_json)
211
+ sigima_options.ensure_loaded_from_env() # recharge depuis l'env
212
+ return wng_err_func(func, args)
213
+
214
+
197
215
  # Enable multiprocessing support for Windows, with frozen executable (e.g. PyInstaller)
198
216
  multiprocessing.freeze_support()
199
217
 
@@ -220,22 +238,6 @@ COMPUTATION_TIP = _(
220
238
  POOL: Pool | None = None
221
239
 
222
240
 
223
- def run_with_env(func: Callable, args: tuple, env_json: str) -> CompOut:
224
- """Wrapper to apply environment config before calling func
225
-
226
- Args:
227
- func: function to call
228
- args: function arguments
229
-
230
- Returns:
231
- Computation output object containing the result, error message,
232
- or warning message.
233
- """
234
- sigima_options.set_env(env_json)
235
- sigima_options.ensure_loaded_from_env() # recharge depuis l'env
236
- return wng_err_func(func, args)
237
-
238
-
239
241
  class WorkerState(Enum):
240
242
  """Worker states for computation lifecycle."""
241
243
 
@@ -1121,6 +1123,9 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
1121
1123
  )
1122
1124
  insert_processing_parameters(new_obj, pp)
1123
1125
 
1126
+ # Mark object as freshly processed to show Processing tab
1127
+ self.panel.objprop.mark_as_freshly_processed(new_obj)
1128
+
1124
1129
  new_gid = None
1125
1130
  if grps:
1126
1131
  # If groups are selected, then it means that there is no
@@ -1416,6 +1421,8 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
1416
1421
  rdata.append(adapter, obj)
1417
1422
 
1418
1423
  if obj is current_obj:
1424
+ # Mark object as having fresh analysis results to show Analysis tab
1425
+ self.panel.objprop.mark_as_fresh_analysis(obj)
1419
1426
  self.panel.selection_changed(update_items=True)
1420
1427
  else:
1421
1428
  self.panel.refresh_plot(get_uuid(obj), True, False)
@@ -2390,6 +2397,12 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
2390
2397
  obj = self.panel.objview.get_sel_objects()[0]
2391
2398
  assert obj.roi is not None, _("No ROI selected for editing.")
2392
2399
  params = obj.roi.to_params(obj)
2400
+ # Round coordinates to appropriate precision before displaying
2401
+ for param in params:
2402
+ if isinstance(obj, SignalObj):
2403
+ coordutils.round_signal_roi_param(obj, param)
2404
+ elif isinstance(obj, ImageObj):
2405
+ coordutils.round_image_roi_param(obj, param)
2393
2406
  group = gds.DataSetGroup(params, title=_("Regions of Interest"))
2394
2407
  if group.edit(parent=self.mainwindow):
2395
2408
  edited_roi = obj.roi.__class__.from_params(obj, params)