mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 11:11:49 +02:00
fix(crosshair): label decimal precision is dynamically scaled with the plot zoom; API of all affected widgets adjusted; option added to PlotBase; closes #637
This commit is contained in:
@ -1221,6 +1221,20 @@ class Image(RPCBase):
|
|||||||
Set auto range for the y-axis.
|
Set auto range for the y-axis.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@minimal_crosshair_precision.setter
|
||||||
|
@rpc_call
|
||||||
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def color_map(self) -> "str":
|
def color_map(self) -> "str":
|
||||||
@ -2350,6 +2364,20 @@ class MultiWaveform(RPCBase):
|
|||||||
The font size of the legend font.
|
The font size of the legend font.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@minimal_crosshair_precision.setter
|
||||||
|
@rpc_call
|
||||||
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def highlighted_index(self):
|
def highlighted_index(self):
|
||||||
@ -3315,6 +3343,20 @@ class ScatterWaveform(RPCBase):
|
|||||||
The font size of the legend font.
|
The font size of the legend font.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@minimal_crosshair_precision.setter
|
||||||
|
@rpc_call
|
||||||
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def main_curve(self) -> "ScatterCurve":
|
def main_curve(self) -> "ScatterCurve":
|
||||||
@ -3789,6 +3831,20 @@ class Waveform(RPCBase):
|
|||||||
The font size of the legend font.
|
The font size of the legend font.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@minimal_crosshair_precision.setter
|
||||||
|
@rpc_call
|
||||||
|
def minimal_crosshair_precision(self) -> "int":
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def curves(self) -> "list[Curve]":
|
def curves(self) -> "list[Curve]":
|
||||||
|
@ -34,13 +34,21 @@ class Crosshair(QObject):
|
|||||||
coordinatesChanged2D = Signal(tuple)
|
coordinatesChanged2D = Signal(tuple)
|
||||||
coordinatesClicked2D = Signal(tuple)
|
coordinatesClicked2D = Signal(tuple)
|
||||||
|
|
||||||
def __init__(self, plot_item: pg.PlotItem, precision: int = 3, parent=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
plot_item: pg.PlotItem,
|
||||||
|
precision: int | None = None,
|
||||||
|
*,
|
||||||
|
min_precision: int = 2,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Crosshair for 1D and 2D plots.
|
Crosshair for 1D and 2D plots.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
plot_item (pyqtgraph.PlotItem): The plot item to which the crosshair will be attached.
|
plot_item (pyqtgraph.PlotItem): The plot item to which the crosshair will be attached.
|
||||||
precision (int, optional): Number of decimal places to round the coordinates to. Defaults to None.
|
precision (int | None, optional): Fixed number of decimal places to display. If *None*, precision is chosen dynamically from the current view range.
|
||||||
|
min_precision (int, optional): The lower bound (in decimal places) used when dynamic precision is enabled. Defaults to 2.
|
||||||
parent (QObject, optional): Parent object for the QObject. Defaults to None.
|
parent (QObject, optional): Parent object for the QObject. Defaults to None.
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -48,7 +56,9 @@ class Crosshair(QObject):
|
|||||||
self.is_log_x = None
|
self.is_log_x = None
|
||||||
self.is_derivative = None
|
self.is_derivative = None
|
||||||
self.plot_item = plot_item
|
self.plot_item = plot_item
|
||||||
self.precision = precision
|
self._precision = precision
|
||||||
|
self._min_precision = max(0, int(min_precision)) # ensure non‑negative
|
||||||
|
|
||||||
self.v_line = pg.InfiniteLine(angle=90, movable=False)
|
self.v_line = pg.InfiniteLine(angle=90, movable=False)
|
||||||
self.v_line.skip_auto_range = True
|
self.v_line.skip_auto_range = True
|
||||||
self.h_line = pg.InfiniteLine(angle=0, movable=False)
|
self.h_line = pg.InfiniteLine(angle=0, movable=False)
|
||||||
@ -93,6 +103,56 @@ class Crosshair(QObject):
|
|||||||
|
|
||||||
self._connect_to_theme_change()
|
self._connect_to_theme_change()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def precision(self) -> int | None:
|
||||||
|
"""Fixed number of decimals; ``None`` enables dynamic mode."""
|
||||||
|
return self._precision
|
||||||
|
|
||||||
|
@precision.setter
|
||||||
|
def precision(self, value: int | None):
|
||||||
|
"""
|
||||||
|
Set the fixed number of decimals to display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(int | None): The number of decimals to display. If `None`, dynamic precision is used based on the view range.
|
||||||
|
"""
|
||||||
|
self._precision = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_precision(self) -> int:
|
||||||
|
"""Lower bound on decimals when dynamic precision is used."""
|
||||||
|
return self._min_precision
|
||||||
|
|
||||||
|
@min_precision.setter
|
||||||
|
def min_precision(self, value: int):
|
||||||
|
"""
|
||||||
|
Set the lower bound on decimals when dynamic precision is used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(int): The minimum number of decimals to display. Must be non-negative.
|
||||||
|
"""
|
||||||
|
self._min_precision = max(0, int(value))
|
||||||
|
|
||||||
|
def _current_precision(self) -> int:
|
||||||
|
"""
|
||||||
|
Get the current precision based on the view range or fixed precision.
|
||||||
|
"""
|
||||||
|
if self._precision is not None:
|
||||||
|
return self._precision
|
||||||
|
|
||||||
|
# Dynamically choose precision from the smaller visible span
|
||||||
|
view_range = self.plot_item.vb.viewRange()
|
||||||
|
x_span = abs(view_range[0][1] - view_range[0][0])
|
||||||
|
y_span = abs(view_range[1][1] - view_range[1][0])
|
||||||
|
|
||||||
|
# Ignore zero spans that can appear during initialisation
|
||||||
|
spans = [s for s in (x_span, y_span) if s > 0]
|
||||||
|
span = min(spans) if spans else 1.0
|
||||||
|
|
||||||
|
exponent = np.floor(np.log10(span)) # order of magnitude
|
||||||
|
decimals = max(0, int(-exponent) + 1)
|
||||||
|
return max(self._min_precision, decimals)
|
||||||
|
|
||||||
def _connect_to_theme_change(self):
|
def _connect_to_theme_change(self):
|
||||||
"""Connect to the theme change signal."""
|
"""Connect to the theme change signal."""
|
||||||
qapp = QApplication.instance()
|
qapp = QApplication.instance()
|
||||||
@ -324,6 +384,7 @@ class Crosshair(QObject):
|
|||||||
# not sure how we got here, but just to be safe...
|
# not sure how we got here, but just to be safe...
|
||||||
return
|
return
|
||||||
|
|
||||||
|
precision = self._current_precision()
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if isinstance(item, pg.PlotDataItem):
|
if isinstance(item, pg.PlotDataItem):
|
||||||
name = item.name() or str(id(item))
|
name = item.name() or str(id(item))
|
||||||
@ -334,8 +395,8 @@ class Crosshair(QObject):
|
|||||||
x_snapped_scaled, y_snapped_scaled = self.scale_emitted_coordinates(x, y)
|
x_snapped_scaled, y_snapped_scaled = self.scale_emitted_coordinates(x, y)
|
||||||
coordinate_to_emit = (
|
coordinate_to_emit = (
|
||||||
name,
|
name,
|
||||||
round(x_snapped_scaled, self.precision),
|
round(x_snapped_scaled, precision),
|
||||||
round(y_snapped_scaled, self.precision),
|
round(y_snapped_scaled, precision),
|
||||||
)
|
)
|
||||||
self.coordinatesChanged1D.emit(coordinate_to_emit)
|
self.coordinatesChanged1D.emit(coordinate_to_emit)
|
||||||
elif isinstance(item, pg.ImageItem):
|
elif isinstance(item, pg.ImageItem):
|
||||||
@ -380,6 +441,7 @@ class Crosshair(QObject):
|
|||||||
# not sure how we got here, but just to be safe...
|
# not sure how we got here, but just to be safe...
|
||||||
return
|
return
|
||||||
|
|
||||||
|
precision = self._current_precision()
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if isinstance(item, pg.PlotDataItem):
|
if isinstance(item, pg.PlotDataItem):
|
||||||
name = item.name() or str(id(item))
|
name = item.name() or str(id(item))
|
||||||
@ -391,8 +453,8 @@ class Crosshair(QObject):
|
|||||||
x_snapped_scaled, y_snapped_scaled = self.scale_emitted_coordinates(x, y)
|
x_snapped_scaled, y_snapped_scaled = self.scale_emitted_coordinates(x, y)
|
||||||
coordinate_to_emit = (
|
coordinate_to_emit = (
|
||||||
name,
|
name,
|
||||||
round(x_snapped_scaled, self.precision),
|
round(x_snapped_scaled, precision),
|
||||||
round(y_snapped_scaled, self.precision),
|
round(y_snapped_scaled, precision),
|
||||||
)
|
)
|
||||||
self.coordinatesClicked1D.emit(coordinate_to_emit)
|
self.coordinatesClicked1D.emit(coordinate_to_emit)
|
||||||
elif isinstance(item, pg.ImageItem):
|
elif isinstance(item, pg.ImageItem):
|
||||||
@ -443,7 +505,8 @@ class Crosshair(QObject):
|
|||||||
"""
|
"""
|
||||||
x, y = pos
|
x, y = pos
|
||||||
x_scaled, y_scaled = self.scale_emitted_coordinates(x, y)
|
x_scaled, y_scaled = self.scale_emitted_coordinates(x, y)
|
||||||
text = f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})"
|
precision = self._current_precision()
|
||||||
|
text = f"({x_scaled:.{precision}f}, {y_scaled:.{precision}f})"
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if isinstance(item, pg.ImageItem):
|
if isinstance(item, pg.ImageItem):
|
||||||
image = item.image
|
image = item.image
|
||||||
@ -452,7 +515,7 @@ class Crosshair(QObject):
|
|||||||
ix = int(np.clip(x, 0, image.shape[0] - 1))
|
ix = int(np.clip(x, 0, image.shape[0] - 1))
|
||||||
iy = int(np.clip(y, 0, image.shape[1] - 1))
|
iy = int(np.clip(y, 0, image.shape[1] - 1))
|
||||||
intensity = image[ix, iy]
|
intensity = image[ix, iy]
|
||||||
text += f"\nIntensity: {intensity:.{self.precision}g}"
|
text += f"\nIntensity: {intensity:.{precision}f}"
|
||||||
break
|
break
|
||||||
# Update coordinate label
|
# Update coordinate label
|
||||||
self.coord_label.setText(text)
|
self.coord_label.setText(text)
|
||||||
|
@ -88,6 +88,8 @@ class Image(PlotBase):
|
|||||||
"auto_range_x.setter",
|
"auto_range_x.setter",
|
||||||
"auto_range_y",
|
"auto_range_y",
|
||||||
"auto_range_y.setter",
|
"auto_range_y.setter",
|
||||||
|
"minimal_crosshair_precision",
|
||||||
|
"minimal_crosshair_precision.setter",
|
||||||
# ImageView Specific Settings
|
# ImageView Specific Settings
|
||||||
"color_map",
|
"color_map",
|
||||||
"color_map.setter",
|
"color_map.setter",
|
||||||
|
@ -91,6 +91,8 @@ class MultiWaveform(PlotBase):
|
|||||||
"y_log.setter",
|
"y_log.setter",
|
||||||
"legend_label_size",
|
"legend_label_size",
|
||||||
"legend_label_size.setter",
|
"legend_label_size.setter",
|
||||||
|
"minimal_crosshair_precision",
|
||||||
|
"minimal_crosshair_precision.setter",
|
||||||
# MultiWaveform Specific RPC Access
|
# MultiWaveform Specific RPC Access
|
||||||
"highlighted_index",
|
"highlighted_index",
|
||||||
"highlighted_index.setter",
|
"highlighted_index.setter",
|
||||||
|
@ -116,6 +116,7 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
self._user_y_label = ""
|
self._user_y_label = ""
|
||||||
self._y_label_suffix = ""
|
self._y_label_suffix = ""
|
||||||
self._y_axis_units = ""
|
self._y_axis_units = ""
|
||||||
|
self._minimal_crosshair_precision = 3
|
||||||
|
|
||||||
# Plot Indicator Items
|
# Plot Indicator Items
|
||||||
self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
|
self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
|
||||||
@ -978,7 +979,9 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
def hook_crosshair(self) -> None:
|
def hook_crosshair(self) -> None:
|
||||||
"""Hook the crosshair to all plots."""
|
"""Hook the crosshair to all plots."""
|
||||||
if self.crosshair is None:
|
if self.crosshair is None:
|
||||||
self.crosshair = Crosshair(self.plot_item, precision=3)
|
self.crosshair = Crosshair(
|
||||||
|
self.plot_item, min_precision=self._minimal_crosshair_precision
|
||||||
|
)
|
||||||
self.crosshair.crosshairChanged.connect(self.crosshair_position_changed)
|
self.crosshair.crosshairChanged.connect(self.crosshair_position_changed)
|
||||||
self.crosshair.crosshairClicked.connect(self.crosshair_position_clicked)
|
self.crosshair.crosshairClicked.connect(self.crosshair_position_clicked)
|
||||||
self.crosshair.coordinatesChanged1D.connect(self.crosshair_coordinates_changed)
|
self.crosshair.coordinatesChanged1D.connect(self.crosshair_coordinates_changed)
|
||||||
@ -1006,6 +1009,29 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
|
|
||||||
self.unhook_crosshair()
|
self.unhook_crosshair()
|
||||||
|
|
||||||
|
@SafeProperty(
|
||||||
|
int, doc="Minimum decimal places for crosshair when dynamic precision is enabled."
|
||||||
|
)
|
||||||
|
def minimal_crosshair_precision(self) -> int:
|
||||||
|
"""
|
||||||
|
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
"""
|
||||||
|
return self._minimal_crosshair_precision
|
||||||
|
|
||||||
|
@minimal_crosshair_precision.setter
|
||||||
|
def minimal_crosshair_precision(self, value: int):
|
||||||
|
"""
|
||||||
|
Set the minimum decimal places for crosshair when dynamic precision is enabled.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(int): The minimum decimal places to set.
|
||||||
|
"""
|
||||||
|
value_int = max(0, int(value))
|
||||||
|
self._minimal_crosshair_precision = value_int
|
||||||
|
if self.crosshair is not None:
|
||||||
|
self.crosshair.min_precision = value_int
|
||||||
|
self.property_changed.emit("minimal_crosshair_precision", value_int)
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Reset the plot widget."""
|
"""Reset the plot widget."""
|
||||||
|
@ -82,6 +82,8 @@ class ScatterWaveform(PlotBase):
|
|||||||
"y_log.setter",
|
"y_log.setter",
|
||||||
"legend_label_size",
|
"legend_label_size",
|
||||||
"legend_label_size.setter",
|
"legend_label_size.setter",
|
||||||
|
"minimal_crosshair_precision",
|
||||||
|
"minimal_crosshair_precision.setter",
|
||||||
# Scatter Waveform Specific RPC Access
|
# Scatter Waveform Specific RPC Access
|
||||||
"main_curve",
|
"main_curve",
|
||||||
"color_map",
|
"color_map",
|
||||||
|
@ -60,6 +60,7 @@ class AxisSettings(SettingWidget):
|
|||||||
self.ui.y_grid,
|
self.ui.y_grid,
|
||||||
self.ui.inner_axes,
|
self.ui.inner_axes,
|
||||||
self.ui.outer_axes,
|
self.ui.outer_axes,
|
||||||
|
self.ui.minimal_crosshair_precision,
|
||||||
]:
|
]:
|
||||||
WidgetIO.connect_widget_change_signal(widget, self.set_property)
|
WidgetIO.connect_widget_change_signal(widget, self.set_property)
|
||||||
|
|
||||||
@ -121,6 +122,7 @@ class AxisSettings(SettingWidget):
|
|||||||
self.ui.y_max,
|
self.ui.y_max,
|
||||||
self.ui.y_log,
|
self.ui.y_log,
|
||||||
self.ui.y_grid,
|
self.ui.y_grid,
|
||||||
|
self.ui.minimal_crosshair_precision,
|
||||||
]:
|
]:
|
||||||
property_name = widget.objectName()
|
property_name = widget.objectName()
|
||||||
value = getattr(self.target_widget, property_name)
|
value = getattr(self.target_widget, property_name)
|
||||||
@ -144,6 +146,7 @@ class AxisSettings(SettingWidget):
|
|||||||
self.ui.y_grid,
|
self.ui.y_grid,
|
||||||
self.ui.outer_axes,
|
self.ui.outer_axes,
|
||||||
self.ui.inner_axes,
|
self.ui.inner_axes,
|
||||||
|
self.ui.minimal_crosshair_precision,
|
||||||
]:
|
]:
|
||||||
property_name = widget.objectName()
|
property_name = widget.objectName()
|
||||||
value = WidgetIO.get_value(widget)
|
value = WidgetIO.get_value(widget)
|
||||||
|
@ -14,97 +14,6 @@
|
|||||||
<string>Form</string>
|
<string>Form</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Inner Axes</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="ToggleSwitch" name="inner_axes"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="2">
|
|
||||||
<widget class="QLabel" name="label_outer_axes">
|
|
||||||
<property name="text">
|
|
||||||
<string>Outer Axes</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="3">
|
|
||||||
<widget class="ToggleSwitch" name="outer_axes">
|
|
||||||
<property name="checked" stdset="0">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0" colspan="2">
|
|
||||||
<widget class="QGroupBox" name="x_axis_box">
|
|
||||||
<property name="title">
|
|
||||||
<string>X Axis</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QGridLayout" name="gridLayout_4">
|
|
||||||
<item row="5" column="0">
|
|
||||||
<widget class="QLabel" name="x_grid_label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Grid</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="2">
|
|
||||||
<widget class="ToggleSwitch" name="x_log">
|
|
||||||
<property name="checked" stdset="0">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QLabel" name="x_max_label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Max</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="2">
|
|
||||||
<widget class="ToggleSwitch" name="x_grid">
|
|
||||||
<property name="checked" stdset="0">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0">
|
|
||||||
<widget class="QLabel" name="x_scale_label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Log</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0" colspan="2">
|
|
||||||
<widget class="QLabel" name="x_min_label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Min</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="x_label_label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Label</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="2">
|
|
||||||
<widget class="QLineEdit" name="x_label"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="2">
|
|
||||||
<widget class="BECSpinBox" name="x_min"/>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="2">
|
|
||||||
<widget class="BECSpinBox" name="x_max"/>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="2" colspan="2">
|
<item row="2" column="2" colspan="2">
|
||||||
<widget class="QGroupBox" name="y_axis_box">
|
<widget class="QGroupBox" name="y_axis_box">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
@ -179,6 +88,87 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="3">
|
||||||
|
<widget class="ToggleSwitch" name="outer_axes">
|
||||||
|
<property name="checked" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Inner Axes</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QGroupBox" name="x_axis_box">
|
||||||
|
<property name="title">
|
||||||
|
<string>X Axis</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_4">
|
||||||
|
<item row="5" column="0">
|
||||||
|
<widget class="QLabel" name="x_grid_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Grid</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="2">
|
||||||
|
<widget class="ToggleSwitch" name="x_log">
|
||||||
|
<property name="checked" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="x_max_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="2">
|
||||||
|
<widget class="ToggleSwitch" name="x_grid">
|
||||||
|
<property name="checked" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="x_scale_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Log</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="x_min_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Min</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="x_label_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Label</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QLineEdit" name="x_label"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="BECSpinBox" name="x_min"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="BECSpinBox" name="x_max"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="0" column="0" colspan="4">
|
<item row="0" column="0" colspan="4">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<item>
|
<item>
|
||||||
@ -191,8 +181,41 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="title"/>
|
<widget class="QLineEdit" name="title"/>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Precision</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="minimal_crosshair_precision">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Minimal Crosshair Precision</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>3</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QLabel" name="label_outer_axes">
|
||||||
|
<property name="text">
|
||||||
|
<string>Outer Axes</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="ToggleSwitch" name="inner_axes"/>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
@ -6,15 +6,84 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>241</width>
|
<width>250</width>
|
||||||
<height>526</height>
|
<height>612</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Form</string>
|
<string>Form</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item row="4" column="0" colspan="2">
|
<item>
|
||||||
|
<widget class="QGroupBox" name="general_box">
|
||||||
|
<property name="title">
|
||||||
|
<string>General</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
|
<item row="4" column="0">
|
||||||
|
<widget class="QLabel" name="label_outer_axes">
|
||||||
|
<property name="text">
|
||||||
|
<string>Outer Axes</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="1">
|
||||||
|
<widget class="ToggleSwitch" name="outer_axes">
|
||||||
|
<property name="checked" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="title"/>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Inner Axes</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="plot_title_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Plot Title</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="ToggleSwitch" name="inner_axes"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Minimal Crosshair Precision</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Precision</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QSpinBox" name="minimal_crosshair_precision">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Minimal Crosshair Precision</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>3</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
<widget class="QGroupBox" name="x_axis_box">
|
<widget class="QGroupBox" name="x_axis_box">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>X Axis</string>
|
<string>X Axis</string>
|
||||||
@ -81,28 +150,7 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0" colspan="2">
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="plot_title_label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Plot Title</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLineEdit" name="title"/>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QLabel" name="label_outer_axes">
|
|
||||||
<property name="text">
|
|
||||||
<string>Outer Axes</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="0" colspan="2">
|
|
||||||
<widget class="QGroupBox" name="y_axis_box">
|
<widget class="QGroupBox" name="y_axis_box">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Y Axis</string>
|
<string>Y Axis</string>
|
||||||
@ -169,23 +217,6 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="ToggleSwitch" name="outer_axes">
|
|
||||||
<property name="checked" stdset="0">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Inner Axes</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="ToggleSwitch" name="inner_axes"/>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
@ -86,6 +86,8 @@ class Waveform(PlotBase):
|
|||||||
"y_log.setter",
|
"y_log.setter",
|
||||||
"legend_label_size",
|
"legend_label_size",
|
||||||
"legend_label_size.setter",
|
"legend_label_size.setter",
|
||||||
|
"minimal_crosshair_precision",
|
||||||
|
"minimal_crosshair_precision.setter",
|
||||||
# Waveform Specific RPC Access
|
# Waveform Specific RPC Access
|
||||||
"curves",
|
"curves",
|
||||||
"x_mode",
|
"x_mode",
|
||||||
|
@ -236,7 +236,7 @@ def test_update_coord_label_1D(plot_widget_with_crosshair):
|
|||||||
# Provide a test position
|
# Provide a test position
|
||||||
pos = (10, 20)
|
pos = (10, 20)
|
||||||
crosshair.update_coord_label(pos)
|
crosshair.update_coord_label(pos)
|
||||||
expected_text = f"({10:.3g}, {20:.3g})"
|
expected_text = f"({10:.3f}, {20:.3f})"
|
||||||
# Verify that the coordinate label shows only the 1D coordinates (no intensity line)
|
# Verify that the coordinate label shows only the 1D coordinates (no intensity line)
|
||||||
assert crosshair.coord_label.toPlainText() == expected_text
|
assert crosshair.coord_label.toPlainText() == expected_text
|
||||||
label_pos = crosshair.coord_label.pos()
|
label_pos = crosshair.coord_label.pos()
|
||||||
@ -260,10 +260,54 @@ def test_update_coord_label_2D(image_widget_with_crosshair):
|
|||||||
ix = int(np.clip(0.5, 0, known_image.shape[0] - 1)) # 0
|
ix = int(np.clip(0.5, 0, known_image.shape[0] - 1)) # 0
|
||||||
iy = int(np.clip(1.2, 0, known_image.shape[1] - 1)) # 1
|
iy = int(np.clip(1.2, 0, known_image.shape[1] - 1)) # 1
|
||||||
intensity = known_image[ix, iy] # Expected: 20
|
intensity = known_image[ix, iy] # Expected: 20
|
||||||
expected_text = f"({0.5:.3g}, {1.2:.3g})\nIntensity: {intensity:.3g}"
|
expected_text = f"({0.5:.3f}, {1.2:.3f})\nIntensity: {intensity:.3f}"
|
||||||
|
|
||||||
assert crosshair.coord_label.toPlainText() == expected_text
|
assert crosshair.coord_label.toPlainText() == expected_text
|
||||||
label_pos = crosshair.coord_label.pos()
|
label_pos = crosshair.coord_label.pos()
|
||||||
assert np.isclose(label_pos.x(), 0.5)
|
assert np.isclose(label_pos.x(), 0.5)
|
||||||
assert np.isclose(label_pos.y(), 1.2)
|
assert np.isclose(label_pos.y(), 1.2)
|
||||||
assert crosshair.coord_label.isVisible()
|
assert crosshair.coord_label.isVisible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_crosshair_precision_properties(plot_widget_with_crosshair):
|
||||||
|
"""
|
||||||
|
Ensure Crosshair.precision and Crosshair.min_precision behave correctly
|
||||||
|
and that _current_precision() reflects changes immediately.
|
||||||
|
"""
|
||||||
|
crosshair, plot_item = plot_widget_with_crosshair
|
||||||
|
|
||||||
|
assert crosshair.precision == 3
|
||||||
|
assert crosshair._current_precision() == 3
|
||||||
|
|
||||||
|
crosshair.precision = None
|
||||||
|
plot_item.vb.setXRange(0, 1_000, padding=0)
|
||||||
|
plot_item.vb.setYRange(0, 1_000, padding=0)
|
||||||
|
assert crosshair._current_precision() == crosshair.min_precision == 2 # default floor
|
||||||
|
|
||||||
|
crosshair.min_precision = 5
|
||||||
|
assert crosshair._current_precision() == 5
|
||||||
|
|
||||||
|
crosshair.precision = 1
|
||||||
|
assert crosshair._current_precision() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_crosshair_precision_properties_image(image_widget_with_crosshair):
|
||||||
|
"""
|
||||||
|
The same precision/min_precision behaviour must apply for crosshairs attached
|
||||||
|
to ImageItem-based plots.
|
||||||
|
"""
|
||||||
|
crosshair, plot_item = image_widget_with_crosshair
|
||||||
|
|
||||||
|
assert crosshair.precision == 3
|
||||||
|
assert crosshair._current_precision() == 3
|
||||||
|
|
||||||
|
crosshair.precision = None
|
||||||
|
plot_item.vb.setXRange(0, 1_000, padding=0)
|
||||||
|
plot_item.vb.setYRange(0, 1_000, padding=0)
|
||||||
|
assert crosshair._current_precision() == crosshair.min_precision == 2
|
||||||
|
|
||||||
|
crosshair.min_precision = 6
|
||||||
|
assert crosshair._current_precision() == 6
|
||||||
|
|
||||||
|
crosshair.precision = 2
|
||||||
|
assert crosshair._current_precision() == 2
|
||||||
|
@ -349,3 +349,39 @@ def test_enable_fps_monitor_property(qtbot, mocked_client):
|
|||||||
|
|
||||||
pb.enable_fps_monitor = False
|
pb.enable_fps_monitor = False
|
||||||
assert pb.fps_monitor is None
|
assert pb.fps_monitor is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_minimal_crosshair_precision_default(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
By default PlotBase should expose a floor of 3 decimals, with no crosshair yet.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
assert pb.minimal_crosshair_precision == 3
|
||||||
|
assert pb.crosshair is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_minimal_crosshair_precision_before_hook(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
If the floor is changed before hook_crosshair(), the new crosshair must pick it up.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
pb.minimal_crosshair_precision = 5
|
||||||
|
pb.hook_crosshair()
|
||||||
|
assert pb.crosshair is not None
|
||||||
|
assert pb.crosshair.min_precision == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_minimal_crosshair_precision_after_hook(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Changing the floor after the crosshair exists should update it immediately
|
||||||
|
and emit the property_changed signal.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
pb.hook_crosshair()
|
||||||
|
assert pb.crosshair is not None
|
||||||
|
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig:
|
||||||
|
pb.minimal_crosshair_precision = 1
|
||||||
|
|
||||||
|
assert sig.args == ["minimal_crosshair_precision", 1]
|
||||||
|
assert pb.crosshair.min_precision == 1
|
||||||
|
Reference in New Issue
Block a user