from __future__ import annotations
import numpy as np
import pyqtgraph as pg
from typing import Any, Iterable
from pyqtgraph.Qt import QtCore, QtGui
from pyqtgraph.graphicsItems.ImageItem import ImageItem
from data_slicer.imageplot import TracedVariable
BASE_LINECOLOR = (255, 255, 0, 255)
BINLINES_LINECOLOR = (168, 168, 104, 255)
ORIENTLINES_LINECOLOR = (164, 37, 22, 255)
HOVER_COLOR = (195, 155, 0, 255)
BGR_COLOR = (64, 64, 64)
util_panel_style = """
QFrame{margin:5px; border:1px solid rgb(150,150,150);}
QLabel{color: rgb(246, 246, 246); border:1px solid rgb(64, 64, 64);}
QCheckBox{color: rgb(246, 246, 246);}
QTabWidget{background-color: rgb(64, 64, 64);}
"""
SIGNALS = 5
MY_CMAPS = True
DEFAULT_CMAP = "coolwarm"
bold_font = QtGui.QFont()
bold_font.setBold(True)
[docs]
class Sliders:
"""
Object defining draggable lines allowing user to choose exact position of
a slice.
"""
[docs]
def __init__(
self,
image_plot: ImagePlot,
pos: Iterable = (0, 0),
mainplot: bool = True,
orientation: str = "horizontal",
) -> None:
"""
Initialize two draggable sliders.
:param image_plot: :class:`ImagePlot` on which sliders are displayed
:param pos: initial position of the sliders
:param mainplot: if :py:obj:`True` register signals for both sliders.
Different behavior is expected for :class:`ImagePlot`\
s being horizontal and vertical cuts
:param orientation: relative orientation of the image, when `vertical`
the actual orientation of sliders is flipped.
Default is `horizontal`.
.. note::
Actually, it's easier to define sliders
in coordinate system of the data, where
horizontal always corresponds to slicing along
momentum.
"""
self.image_plot = image_plot
self.orientation = orientation
# Store the positions in TracedVariables
self.hpos = CustomTracedVariable(pos[1], name="hpos")
self.vpos = CustomTracedVariable(pos[0], name="vpos")
# Initialize the InfiniteLines
if orientation == "horizontal":
self.hline = pg.InfiniteLine(pos[1], movable=True, angle=0)
self.vline = pg.InfiniteLine(pos[0], movable=True, angle=90)
elif orientation == "vertical":
self.hline = pg.InfiniteLine(pos[1], movable=True, angle=90)
self.vline = pg.InfiniteLine(pos[0], movable=True, angle=0)
# Set the color
self.set_color(BASE_LINECOLOR, HOVER_COLOR)
# Register some callbacks depending on plot type
if mainplot:
self.hpos.sig_value_changed.connect(self.update_position_h)
self.vpos.sig_value_changed.connect(self.update_position_v)
self.hline.sigDragged.connect(self.on_dragged_h)
self.vline.sigDragged.connect(self.on_dragged_v)
else:
self.hpos.sig_value_changed.connect(self.update_position_h)
self.hline.sigDragged.connect(self.on_dragged_h)
[docs]
def add_to(self, widget: Any) -> None:
"""
Add these sliders to a :mod:`pyqtgraph` widget.
:param widget: widget to which sliders are added
"""
for line in [self.hline, self.vline]:
line.setZValue(1)
widget.addItem(line)
[docs]
def remove_from(self, widget: Any) -> None:
"""
Remove these sliders from a :mod:`pyqtgraph` widget.
:param widget: widget to which sliders are removed
"""
for line in [self.hline, self.vline]:
widget.removeItem(line)
[docs]
def set_color(
self, linecolor: Any = BASE_LINECOLOR, hover_color: Any = HOVER_COLOR
) -> None:
"""
Set the color and hover color of both sliders that make up. The
arguments can be any :mod:`pyqtgraph` compatible color specifiers.
:param linecolor: can be any :mod:`pyqtgraph` compatible color
specifiers
:param hover_color: can be any :mod:`pyqtgraph` compatible color
specifiers
"""
for line in [self.hline, self.vline]:
line.setPen(linecolor)
line.setHoverPen(hover_color)
[docs]
def set_movable(self, movable: bool = True) -> None:
"""
Define whether these sliders can be dragged by the mouse or not.
:param movable: if :py:obj:`True`, make sliders movable
"""
for line in [self.hline, self.vline]:
line.setMovable = movable
[docs]
def update_position_h(self) -> None:
"""
Update position of the horizontal slider.
"""
self.hline.setValue(self.hpos.get_value())
[docs]
def update_position_v(self) -> None:
"""
Update position of the vertical slider.
"""
self.vline.setValue(self.vpos.get_value())
[docs]
def on_dragged_h(self):
"""
Callback for dragging horizontal slider changing value of the
connected :class:`CustomTracedVariable`.
"""
self.hpos.set_value(self.hline.value())
# if it's an energy plot, and binning option is active,
# update also binning boundaries
if self.image_plot.binning:
pos = self.hline.value()
self.image_plot.left_line.setValue(pos - self.image_plot.width)
self.image_plot.right_line.setValue(pos + self.image_plot.width)
[docs]
def on_dragged_v(self) -> None:
"""
Callback for dragging vertical slider changing value of the connected
:class:`CustomTracedVariable`.
"""
self.vpos.set_value(self.vline.value())
[docs]
def set_bounds(self, xmin: int, xmax: int, ymin: int, ymax: int) -> None:
"""
Set boundaries within which sliders can be dragged.
:param xmin: lowest value along horizontal direction
:param xmax: highest value along horizontal direction
:param ymin: lowest value along vertical direction
:param ymax: highest value along vertical direction
"""
if self.orientation == "horizontal":
self.hline.setBounds([ymin, ymax])
self.vline.setBounds([xmin, xmax])
else:
self.vline.setBounds([ymin, ymax])
self.hline.setBounds([xmin, xmax])
[docs]
class ImagePlot(pg.PlotWidget):
"""
Object displaying 2D color-scaled data using different colormaps. It treats
data as a regular rectangular images, which allows for time efficient
slicing and updating displayed cuts.
Inherits from :class:`pyqtgraph.PlotWidget` which gives access to
additional *plotty* features like displaying custom scales instead of just
pixels.
"""
sig_image_changed = QtCore.Signal()
sig_axes_changed = QtCore.Signal()
sig_clicked = QtCore.Signal(object)
[docs]
def __init__(
self,
image: np.ndarray = None,
background: Any = BGR_COLOR,
name: str = None,
mainplot: bool = True,
orientation: str = "horizontal",
**kwargs: dict,
) -> None:
"""
Initialize color-scaled plot.
:param image: 2D array with data
:param background: color of the background, can be any pyqtgraph
compatible color specifier
:param mainplot: specifies behavior of the draggable sliders.
See :class:`Sliders` for more details
:param orientation: orientation of the image. Default is '`horizontal`'
:param kwargs: kwargs passed to parent :class:`pg.PlotWidget`
"""
super().__init__(background=background, **kwargs)
# Initialize instance variables
# np.array, raw image data
self.image_data = None
self.image_item = None
self.image_kwargs = {}
self.xlim = None
self.xlim_rescaled = None
self.ylim = None
self.ylim_rescaled = None
self.xscale = None
self.xscale_rescaled = None
self.yscale = None
self.yscale_rescaled = None
self.transform_factors = []
self.crosshair_cursor_visible = False
self.binning = False
self.orientation = orientation
if name is not None:
self.name = name
else:
self.name = "Unnamed"
self.orient()
if image is not None:
self.set_image(image)
# Initiliaze a sliders and add it to this widget
self.sliders = Sliders(self, mainplot=mainplot, orientation=orientation)
self.sliders.add_to(self)
self.pos = (self.sliders.vpos, self.sliders.hpos)
# Initialize range to [0, 1]x[0, 1]
self.set_bounds(0, 1, 0, 1)
# Disable mouse scrolling, panning and zooming for both axes
self.setMouseEnabled(False, False)
# Connect a slot (callback) to dragging and clicking events
self.sig_axes_changed.connect(
lambda: self.set_bounds(*[x for lst in self.get_limits() for x in lst])
)
self.sig_image_changed.connect(self.update_allowed_values)
# methods added to make crosshairs work
[docs]
def update_allowed_values(self) -> None:
"""
Update the allowed values of the sliders. This assumes that the
displayed image is in pixel coordinates and sets the allowed values
to the available pixels.
"""
[[xmin, xmax], [ymin, ymax]] = self.get_limits()
if self.orientation == "horizontal":
self.pos[0].set_allowed_values(np.arange(xmin, xmax + 1, 1))
self.pos[1].set_allowed_values(np.arange(ymin, ymax + 1, 1))
else:
self.pos[1].set_allowed_values(np.arange(xmin, xmax + 1, 1))
self.pos[0].set_allowed_values(np.arange(ymin, ymax + 1, 1))
[docs]
def set_bounds(self, xmin: int, xmax: int, ymin: int, ymax: int) -> None:
"""
Set both, the displayed area of the axes and the range in which
sliders can be dragged.
:param xmin: lowest value along horizontal direction
:param xmax: highest value along horizontal direction
:param ymin: lowest value along vertical direction
:param ymax: highest value along vertical direction
"""
self.setXRange(xmin, xmax, padding=0.0)
self.setYRange(ymin, ymax, padding=0.0)
self.sliders.set_bounds(xmin, xmax, ymin, ymax)
[docs]
def orient(self) -> None:
"""
Configure plot's layout depending on its orientation.
"""
if self.orientation == "horizontal":
# Show top and tight axes by default, but without tick labels
self.showAxis("top")
self.showAxis("right")
self.getAxis("top").setStyle(showValues=False)
self.getAxis("right").setStyle(showValues=False)
self.main_xaxis = "bottom"
self.main_xaxis_grid = (255, 1)
self.main_yaxis = "left"
self.main_yaxis_grid = (2, 0)
# moved here to get rid of warnings:
self.right_axis = "top"
self.secondary_axis = "right"
self.secondary_axis_grid = (2, 2)
self.angle = 0
self.slider_axis_index = 1
elif self.orientation == "vertical":
# Show top and tight axes by default, but without tick labels
self.showAxis("right")
self.showAxis("top")
self.getAxis("right").setStyle(showValues=False)
self.getAxis("top").setStyle(showValues=False)
self.main_xaxis = "left"
self.main_xaxis_grid = (255, 1)
self.main_yaxis = "bottom"
self.main_yaxis_grid = (2, 0)
# moved here to get rid of warnings:
self.right_axis = "right"
self.secondary_axis = "top"
self.secondary_axis_grid = (2, 2)
self.angle = 90
self.slider_axis_index = 1
[docs]
def remove_image(self) -> None:
"""
Remove currently displayed image.
"""
if self.image_item is not None:
self.removeItem(self.image_item)
self.image_item = None
[docs]
def set_image(
self, image: np.ndarray, emit: bool = True, *args: dict, **kwargs: dict
) -> None:
"""
Set/update displayed image. Also make sure the old one has been
removed.
:param image: 2D array with data
:param emit: if :py:obj:`True`, emmit signal that image has been
changed
:param args: additional arguments for a parent
:class:`~pyqtgraph.graphicsItems.ImageItem.ImageItem`
:param kwargs: additional keyword arguments for a parent
:class:`~pyqtgraph.graphicsItems.ImageItem.ImageItem`
"""
# Convert array to ImageItem
if isinstance(image, np.ndarray):
if 0 not in image.shape:
image_item = ImageItem(image, *args, **kwargs)
else:
return
else:
image_item = image
# Replace the image
self.remove_image()
self.image_item = image_item
self.image_data = image
self.addItem(image_item)
# Reset limits if necessary
if self.xscale is not None and self.yscale is not None:
axes_shape = (len(self.xscale), len(self.yscale))
if axes_shape != self.image_data.shape:
self.xlim = None
self.ylim = None
self._set_axes_scales(emit=emit)
if emit:
self.sig_image_changed.emit()
[docs]
def set_xscale(self, scale: np.ndarray) -> None:
"""
Set custom values of the ``xscale``.
:param scale: 1D array with axis values
"""
if self.orientation == "vertical":
self.yscale = scale
self.ylim = (scale[0], scale[-1])
else:
self.xscale = scale
self.xlim = (scale[0], scale[-1])
[docs]
def set_yscale(self, scale: np.ndarray) -> None:
"""
Set custom values of the ``yscale``.
:param scale: 1D array with axis values
"""
if self.orientation == "vertical":
self.xscale = scale
self.xlim = (scale[0], scale[-1])
else:
self.yscale = scale
self.ylim = (scale[0], scale[-1])
[docs]
def set_ticks(self, min_val: float, max_val: float, axis: str) -> None:
"""
Set customized axis' ticks to reflect the dimensions of the physical
data.
:param min_val: lowest axis' value
:param max_val: highest axis' value
:param axis: concerned axis, can be [`bottom`, `left`, *etc*]
"""
plotItem = self.plotItem
# Remove the old top-axis
plotItem.layout.removeItem(plotItem.getAxis(axis))
# Create the new axis and set its range
new_axis = pg.AxisItem(orientation=axis)
new_axis.setRange(min_val, max_val)
# Attach it internally to the plotItem and its layout (The arguments
plotItem.axes[axis]["item"] = new_axis
if axis == "bottom":
plotItem.layout.addItem(new_axis, *self.main_xaxis_grid)
else:
plotItem.layout.addItem(new_axis, *self.main_yaxis_grid)
def _set_axes_scales(self, emit: bool = False) -> None:
"""
Transform the image so that it matches the desired *x* and *y* scales.
:param emit: if :py:obj:`True` emmit signal that axes has changed
"""
# Get image dimensions and requested origin (x0,y0) and
# top right corner (x1, y1)
nx, ny = self.image_data.shape
[[x0, x1], [y0, y1]] = self.get_limits()
# Calculate the scaling factors
sx = (x1 - x0) / nx
sy = (y1 - y0) / ny
# Ensure nonzero
sx = 1 if sx == 0 else sx
sy = 1 if sy == 0 else sy
# Define a transformation matrix that scales and translates the image
# such that it appears at the coordinates that match our x and y axes.
transform = QtGui.QTransform()
transform.scale(sx, sy)
# Carry out the translation in scaled coordinates
transform.translate(x0 / sx, y0 / sy)
# Finally, apply the transformation to the imageItem
self.image_item.setTransform(transform)
if emit:
self.sig_axes_changed.emit()
[docs]
def set_secondary_axis(self, min_val: float, max_val: float) -> None:
"""
Set/update a second `x`-axis on the top.
:param min_val: lowest axis' value
:param max_val: highest axis' value
"""
# Get a handle on the underlying plotItem
plotItem = self.plotItem
# Remove the old top-axis
plotItem.layout.removeItem(plotItem.getAxis(self.secondary_axis))
# Create the new axis and set its range
new_axis = pg.AxisItem(orientation=self.secondary_axis)
new_axis.setRange(min_val, max_val)
# Attach it internally to the plotItem and its layout (The arguments
plotItem.axes[self.secondary_axis]["item"] = new_axis
plotItem.layout.addItem(new_axis, *self.secondary_axis_grid)
[docs]
def get_limits(self) -> list:
"""
Get limits of the image data.
:return: list of lists in a format: [[`x_min`, `x_max`],
[`y_min`, `y_max`]]
"""
# Default to current viewrange but try to get more accurate values
# if possible
if self.image_item is not None:
x, y = self.image_data.shape
else:
x, y = 1, 1
# Set the limits to image pixels if they are not defined
if self.xlim is None or self.ylim is None:
self.set_xscale(np.arange(0, x))
self.set_yscale(np.arange(0, y))
if self.orientation == "horizontal":
x_min, x_max = self.xlim
y_min, y_max = self.ylim
else:
x_min, x_max = self.ylim
y_min, y_max = self.xlim
return [[x_min, x_max], [y_min, y_max]]
[docs]
def fix_viewrange(self) -> None:
"""
Prevent zooming out by fixing the limits of the :class:`QViewBox`.
"""
[[x_min, x_max], [y_min, y_max]] = self.get_limits()
self.setLimits(
xMin=x_min,
xMax=x_max,
yMin=y_min,
yMax=y_max,
maxXRange=x_max - x_min,
maxYRange=y_max - y_min,
)
[docs]
def add_binning_lines(
self, pos: int, width: int, orientation: str = "horizontal"
) -> None:
"""
Add not-movable lines around the specified sliders. The lines indicate
integration area for a corresponding cut.
:param pos: position of the slider
:param width: number of left and right steps from the slider position
:param orientation: orientation of the slider
"""
# delete binning lines if exist
if orientation == "horizontal":
try:
self.removeItem(self.left_hor_line)
self.removeItem(self.right_hor_line)
except AttributeError:
pass
# add new binning lines
self.binning_hor = True
self.hor_width = width
self.left_hor_line = pg.InfiniteLine(pos - width, movable=False, angle=0)
self.right_hor_line = pg.InfiniteLine(pos + width, movable=False, angle=0)
self.left_hor_line.setPen(color=BINLINES_LINECOLOR, width=1)
self.right_hor_line.setPen(color=BINLINES_LINECOLOR, width=1)
self.addItem(self.left_hor_line)
self.addItem(self.right_hor_line)
elif orientation == "vertical":
try:
self.removeItem(self.left_ver_line)
self.removeItem(self.right_ver_line)
except AttributeError:
pass
# add new binning lines
self.binning_ver = True
self.ver_width = width
self.left_ver_line = pg.InfiniteLine(pos - width, movable=False, angle=90)
self.right_ver_line = pg.InfiniteLine(pos + width, movable=False, angle=90)
self.left_ver_line.setPen(color=BINLINES_LINECOLOR, width=1)
self.right_ver_line.setPen(color=BINLINES_LINECOLOR, width=1)
self.addItem(self.left_ver_line)
self.addItem(self.right_ver_line)
[docs]
def remove_binning_lines(self, orientation: str = "horizontal") -> None:
"""
Remove binning lines from the :class:`ImagePlot`.
:param orientation: orientation of the slider
"""
if orientation == "horizontal":
self.binning_hor = False
self.hor_width = 0
self.removeItem(self.left_hor_line)
self.removeItem(self.right_hor_line)
elif orientation == "vertical":
self.binning_ver = False
self.ver_width = 0
self.removeItem(self.left_ver_line)
self.removeItem(self.right_ver_line)
[docs]
def register_momentum_slider(self, traced_variable: TracedVariable) -> None:
"""
Register vertical slider draggable along momentum in the **cut** plots.
:param traced_variable: connected traced variable
"""
self.sliders.vpos = traced_variable
self.sliders.vpos.sig_value_changed.connect(self.set_position)
self.sliders.vpos.sig_allowed_values_changed.connect(
self.on_allowed_values_change
)
[docs]
def set_position(self) -> None:
"""
Set the position of the momentum sliders whenever the value of
connected :class:`CustomTracedVariable` has changed.
"""
if self.orientation == "horizontal":
new_pos = self.sliders.vpos.get_value()
self.sliders.vline.setValue(new_pos)
elif self.orientation == "vertical":
new_pos = self.sliders.vpos.get_value()
self.sliders.hline.setValue(new_pos)
[docs]
def on_allowed_values_change(self) -> None:
"""
Set new momentum slider bounds after changing allowed values of the
connected :class:`CustomTracedVariable`, *e.g.* after setting binning
lines.
"""
# If the allowed values were reset, just exit
if self.sliders.vpos.allowed_values is None:
return
lower = self.sliders.vpos.min_allowed
upper = self.sliders.vpos.max_allowed
self.sliders.vline.setBounds([lower, upper])
[docs]
class CurvePlot(pg.PlotWidget):
"""
Object displaying basic 1D curves with a draggable slider.
"""
hover_color = HOVER_COLOR
[docs]
def __init__(
self,
background: Any = BGR_COLOR,
name: str = None,
orientation: str = "horizontal",
slider_width: int = 1,
z_plot: bool = False,
**kwargs: dict,
) -> None:
"""
Initialize simple 1D plot panel.
:param background: color of the background, can be any pyqtgraph
compatible color specifier
:param orientation: orientation of the plot. Default is '`horizontal`'
:param slider_width: width of the draggable slider
:param z_plot: if :py:obj:`True`, plot is a main energy plot of the
:class:`~_3Dviewer.DataViewer3D` GUI
:param kwargs: kwargs passed to parent :class:`~pyqtgraph.PlotWidget`
"""
super().__init__(background=background, **kwargs)
# moved here to get rid of warnings:
self.right_axis = "top"
self.left_axis = "bottom"
self.secondary_axis = "right"
self.main_xaxis = "left"
self.main_xaxis_grid = (2, 0)
self.secondary_axis_grid = (2, 2)
self.angle = 0
self.slider_axis_index = 1
self.pos = None
self.left_line = None
self.right_line = None
self.width = 0
self.wheel_frames = None
self.cursor_color = None
self.pen_width = None
self.sp_EDC_pen = pg.mkPen("r")
# Whether to allow changing the sliders width with arrow keys
self.change_width_enabled = False
self.orientation = orientation
self.orient()
self.binning = False
self.z_plot = z_plot
if name is not None:
self.name = name
else:
self.name = "Unnamed"
# Hide the pyqtgraph auto-rescale button
self.getPlotItem().buttonsHidden = True
# Display the right (or top) axis without ticklabels
self.showAxis(self.right_axis)
self.getAxis(self.right_axis).setStyle(showValues=False)
# The position of the sliders is stored with a TracedVariable
initial_pos = 0
pos = CustomTracedVariable(initial_pos, name="pos")
self.register_traced_variable(pos)
# Set up the sliders
self.slider = pg.InfiniteLine(initial_pos, movable=True, angle=self.angle)
self.set_slider_pen(color=BASE_LINECOLOR, width=slider_width)
# Add a marker. Args are (style, position (from 0-1), size #NOTE
# seems broken
self.addItem(self.slider)
# Disable mouse scrolling, panning and zooming for both axes
self.setMouseEnabled(False, False)
# Initialize range to [0, 1]
self.set_bounds(initial_pos, initial_pos + 1)
# Connect a slot (callback) to dragging and clicking events
self.slider.sigDragged.connect(self.on_position_change)
[docs]
def get_data(self) -> tuple:
"""
Get the currently displayed data as a tuple of arrays.
:return: (`x_data`, `y_data`)
"""
pdi = self.listDataItems()[0]
return pdi.getData()
[docs]
def orient(self) -> None:
"""
Configure plot's layout depending on its orientation.
"""
if self.orientation == "horizontal":
self.right_axis = "right"
self.left_axis = "left"
self.secondary_axis = "top"
self.main_xaxis = "bottom"
self.main_xaxis_grid = (255, 1)
self.secondary_axis_grid = (1, 1)
self.angle = 90
self.slider_axis_index = 1
elif self.orientation == "vertical":
self.right_axis = "top"
self.left_axis = "bottom"
self.secondary_axis = "right"
self.main_xaxis = "left"
self.main_xaxis_grid = (2, 0)
self.secondary_axis_grid = (2, 2)
self.angle = 0
self.slider_axis_index = 1
[docs]
def register_traced_variable(self, traced_variable: TracedVariable) -> None:
"""
Register :class:`CustomTracedVariable` connected to the slider.
"""
self.pos = traced_variable
self.pos.sig_value_changed.connect(self.set_position)
self.pos.sig_allowed_values_changed.connect(self.on_allowed_values_change)
[docs]
def on_position_change(self) -> None:
"""
Callback for dragging slider changing value of the connected
:class:`CustomTracedVariable`.
"""
current_pos = self.slider.value()
# NOTE pos.set_value emits signal sig_value_changed which may lead to
# duplicate processing of the position change.
self.pos.set_value(current_pos)
# if it's an energy plot, and binning option is active,
# update also binning boundaries
if self.binning and self.z_plot:
z_pos = self.slider.value()
self.left_line.setValue(z_pos - self.width)
self.right_line.setValue(z_pos + self.width)
[docs]
def on_allowed_values_change(self) -> None:
"""
Set new momentum slider bounds after changing allowed values of the
connected :class:`CustomTracedVariable`, *e.g.* after setting binning
lines.
"""
# If the allowed values were reset, just exit
if self.pos.allowed_values is None:
return
lower = self.pos.min_allowed - self.width
upper = self.pos.max_allowed + self.width
self.set_bounds(lower, upper)
[docs]
def set_position(self) -> None:
"""
Set the position of the sliders whenever the value of connected
:class:`CustomTracedVariable` has changed.
"""
new_pos = self.pos.get_value()
self.slider.setValue(new_pos)
[docs]
def add_binning_lines(self, pos: int, width: int) -> None:
"""
Add not-movable lines around the specified sliders. The lines indicate
integration area for a corresponding cut.
:param pos: position of the slider
:param width: number of left and right steps from the slider position
"""
# delete binning lines if exist
try:
self.removeItem(self.left_line)
self.removeItem(self.right_line)
except AttributeError:
pass
# add new binning lines
self.binning = True
self.width = width
self.left_line = pg.InfiniteLine(pos - width, movable=False, angle=self.angle)
self.right_line = pg.InfiniteLine(pos + width, movable=False, angle=self.angle)
self.left_line.setPen(color=BINLINES_LINECOLOR, width=1)
self.right_line.setPen(color=BINLINES_LINECOLOR, width=1)
self.addItem(self.left_line)
self.addItem(self.right_line)
[docs]
def remove_binning_lines(self) -> None:
"""
Remove binning lines from the :class:`ImagePlot`.
"""
self.binning = False
self.width = 0
self.removeItem(self.left_line)
self.removeItem(self.right_line)
[docs]
def set_bounds(self, lower: int, upper: int) -> None:
"""
Set both, the displayed area of the axes and the range in which
sliders can be dragged.
:param lower: lowest value
:param upper: highest value
"""
if self.orientation == "horizontal":
self.setXRange(lower, upper, padding=0.0)
else:
self.setYRange(lower, upper, padding=0.0)
self.slider.setBounds([lower, upper])
[docs]
def set_secondary_axis(self, min_val: float, max_val: float) -> None:
"""
Set/update a second `x`-axis on the top.
:param min_val: lowest axis' value
:param max_val: highest axis' value
"""
# Get a handle on the underlying plotItem
plotItem = self.plotItem
# Remove the old top-axis
plotItem.layout.removeItem(plotItem.getAxis(self.secondary_axis))
# Create the new axis and set its range
new_axis = pg.AxisItem(orientation=self.secondary_axis)
new_axis.setRange(min_val, max_val)
# Attach it internally to the plotItem and its layout (The arguments
plotItem.axes[self.secondary_axis]["item"] = new_axis
plotItem.layout.addItem(new_axis, *self.secondary_axis_grid)
[docs]
def set_ticks(self, min_val: float, max_val: float, axis: str) -> None:
"""
Set customized axis' ticks to reflect the dimensions of the physical
data.
:param min_val: lowest axis' value
:param max_val: highest axis' value
:param axis: concerned axis, can be [`bottom`, `left`, *etc*]
"""
plotItem = self.plotItem
# Remove the old top-axis
plotItem.layout.removeItem(plotItem.getAxis(axis))
# Create the new axis and set its range
new_axis = pg.AxisItem(orientation=axis)
new_axis.setRange(min_val, max_val)
# Attach it internally to the plotItem and its layout (The arguments
plotItem.axes[axis]["item"] = new_axis
plotItem.layout.addItem(new_axis, *self.main_xaxis_grid)
[docs]
def set_slider_pen(
self, color: Any = None, width: int = None, hover_color: Any = None
) -> None:
"""
Set a color and thickness of the slider.
:param color: color of the slider, can be any pyqtgraph compatible
color specifier
:param width: width of the slider
:param hover_color: hover color of the slider, can be any pyqtgraph
compatible color specifier
"""
# Default to the current values if none are given
if color is None:
color = self.cursor_color
else:
self.cursor_color = color
if width is None:
width = self.pen_width
else:
self.pen_width = width
if hover_color is None:
hover_color = self.hover_color
else:
self.hover_color = hover_color
self.slider.setPen(color=color, width=width)
# Keep the hoverPen-size consistent
self.slider.setHoverPen(color=hover_color, width=width)
[docs]
class CustomTracedVariable(TracedVariable):
"""
Wrapper around :class:`data_slicer.imageplot.TracedVariable`
(see for more details).
In short, object taking care of traced variables using signalling
mechanism.
"""
[docs]
def __init__(self, value: Any = None, name: str = None) -> None:
"""
Initialize traced variable.
:param value: initial value of the variable
:param name: name of the variable
"""
super(CustomTracedVariable, self).__init__(value=value, name=name)
def _set_allowed_values(self, values: Any = None, binning: int = 0) -> None:
"""
Wrapper method to set values taking possible binning into account.
:param values: new allowed values within which `traced_variable`
can be changed
:param binning: number of applied bins, which additionally limit
allowed values from both sides
"""
if binning == 0:
values = np.array(values)
else:
values = np.array(values)[binning:-binning]
self.set_allowed_values(values)