def prepare_data(self, data, axes=3 * [None]): """ Load the specified data and prepare the corresponding z range. Then display the newly loaded data. *Parameters* ==== ================================================================== data 3d array; the data to display axes len(3) list or array of 1d-arrays or None; the units along the x, y and z axes respectively. If any of those is *None*, pixels are used. ==== ================================================================== """ logger.debug('prepare_data()') self.data = TracedVariable(data, name='data') self.axes = np.array(axes) # Retain a copy of the original data and axes so that we can reset later # NOTE: this effectively doubles the used memory! self.original_data = copy(self.data.get_value()) self.original_axes = copy(self.axes) self.prepare_axes() self.on_z_dim_change() # Connect signal handling so changes in data are immediately reflected self.z.sig_value_changed.connect( \ lambda : self.main_window.update_main_plot(emit=False)) self.data.sig_value_changed.connect(self.on_data_change) self.main_window.update_main_plot() self.main_window.set_axes()
def __init__(self, *args, data=None, **kwargs): """ Set up the :class: `GLViewWidget <pyqtgraph.opengl.GLViewWidget>` as this widget's central widget. """ super().__init__(*args, **kwargs) # Initialize instance variables self.data = TracedVariable(None, name='data') self.cmap = load_cmap(DEFAULT_CMAP) self.lut = self.cmap.getLookupTable() self.gloptions = 'translucent' # Create a GLViewWidget and put it into the layout of this view self.layout = QtGui.QGridLayout() self.setLayout(self.layout) self.glview = gl.GLViewWidget() # Add the scalebar self._initialize_sub_widgets() # Put all widgets in place self.align() # Add some coordinate axes to the view self.coordinate_axes = gl.GLAxisItem() self.glview.addItem(self.coordinate_axes) self.set_coordinate_axes(True) # Try to set the default data if data is not None: self.set_data(data)
def __init__(self, pos=(0,0)) : # Store the positions in TracedVariables self.hpos = TracedVariable(pos[1], name='hpos') self.vpos = TracedVariable(pos[0], name='vpos') # Initialize the InfiniteLines self.hline = pg.InfiniteLine(pos[1], movable=True, angle=0) self.vline = pg.InfiniteLine(pos[0], movable=True, angle=90) # Set the color self.set_color(BASE_LINECOLOR, HOVER_COLOR) # Register some callbacks 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)
def __init__(self, image=None, parent=None, background='default', name=None, **kwargs) : """ Allows setting of the image upon initialization. **Parameters** ========== ============================================================ image np.ndarray or pyqtgraph.ImageItem instance; the image to be displayed. parent QtWidget instance; parent widget of this widget. background str; confer PyQt documentation name str; allows giving a name for debug purposes ========== ============================================================ """ # Initialize instance variables # np.array, raw image data self.image_data = None # pg.ImageItem of *image_data* self.image_item = None self.image_kwargs = {} self.xlim = None self.ylim = None self.xscale = None self.yscale = None self.transform_factors = [] self.transposed = TracedVariable(False, name='transposed') self.crosshair_cursor_visible = False super().__init__(parent=parent, background=background, viewBox=DSViewBox(imageplot=self), **kwargs) self.name = name # Show top and tight axes by default, but without ticklabels self.showAxis('top') self.showAxis('right') self.getAxis('top').setStyle(showValues=False) self.getAxis('right').setStyle(showValues=False) if image is not None : self.set_image(image) self.sig_axes_changed.connect(self.fix_viewrange)
def __init__(self, pos=(0, 0)): # Store the positions in TracedVariables self.hpos = TracedVariable(pos[1], name='hpos') self.vpos = TracedVariable(pos[0], name='vpos') # Initialize the InfiniteLines self.hline = pg.InfiniteLine(pos[1], movable=True, angle=0) self.vline = pg.InfiniteLine(pos[0], movable=True, angle=90) # Set the color for line in [self.hline, self.vline]: line.setPen((255, 255, 0, 255)) line.setHoverPen(HOVER_COLOR) # Register some callbacks 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)
class Crosshair(): """ Crosshair made up of two InfiniteLines. """ def __init__(self, pos=(0, 0)): # Store the positions in TracedVariables self.hpos = TracedVariable(pos[1], name='hpos') self.vpos = TracedVariable(pos[0], name='vpos') # Initialize the InfiniteLines self.hline = pg.InfiniteLine(pos[1], movable=True, angle=0) self.vline = pg.InfiniteLine(pos[0], movable=True, angle=90) # Set the color for line in [self.hline, self.vline]: line.setPen((255, 255, 0, 255)) line.setHoverPen(HOVER_COLOR) # Register some callbacks 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) def add_to(self, widget): """ Add this crosshair to a Qt widget. """ for line in [self.hline, self.vline]: line.setZValue(1) widget.addItem(line) def update_position_h(self): """ Callback for the :signal: `sig_value_changed <data_slicer.utilities.TracedVariable.sig_value_changed>`. Whenever the value of this TracedVariable is updated (possibly from outside this Crosshair object), put the crosshair to the appropriate position. """ self.hline.setValue(self.hpos.get_value()) def update_position_v(self): """ Confer update_position_h. """ self.vline.setValue(self.vpos.get_value()) def on_dragged_h(self): """ Callback for dragging of InfiniteLines. Their visual position should be reflected in the TracedVariables self.hpos and self.vpos. """ self.hpos.set_value(self.hline.value()) def on_dragged_v(self): """ Callback for dragging of InfiniteLines. Their visual position should be reflected in the TracedVariables self.hpos and self.vpos. """ self.vpos.set_value(self.vline.value()) def set_bounds(self, xmin, xmax, ymin, ymax): """ Set the area in which the infinitelines can be dragged. """ self.hline.setBounds([ymin, ymax]) self.vline.setBounds([xmin, xmax])
def __init__(self, parent=None, background='default', name=None, orientation='vertical', slider_width=1, **kwargs): """ Initialize the slider and set up the visual tweaks to make a PlotWidget look more like a scalebar. =========== ============================================================ parent QtWidget instance; parent widget of this widget background str; confer PyQt documentation name str; allows giving a name for debug purposes orientation str, `horizontal` or `vertical`; orientation of the cursor =========== ============================================================ """ super().__init__(parent=parent, background=background, **kwargs) if orientation not in ['horizontal', 'vertical']: raise ValueError('Only `horizontal` or `vertical` are allowed for ' 'orientation.') self.orientation = orientation self.orientate() if name is not None: self.name = name # 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 slider is stored with a TracedVariable initial_pos = 0 pos = TracedVariable(initial_pos, name='pos') self.register_traced_variable(pos) # Set up the slider self.slider_width = TracedVariable(slider_width, name='{}.slider_width'.format( self.name)) self.slider = pg.InfiniteLine(initial_pos, movable=True, angle=self.angle) self.set_slider_pen(color=(255, 255, 0, 255), width=slider_width) # Add a marker. Args are (style, position (from 0-1), size #NOTE # seems broken #self.slider.addMarker('o', 0.5, 10) 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)
class CursorPlot(pg.PlotWidget): """ Implements a simple, draggable scalebar represented by a line (:class: `InfiniteLine <pyqtgraph.InfiniteLine>) on an axis (:class: `PlotWidget <pyqtgraph.PlotWidget>). The current position of the slider is tracked with the :class: `TracedVariable <data_slicer.utilities.TracedVariable>` self.pos and its width with the `TracedVariable` self.slider_width. """ name = 'Unnamed' hover_color = HOVER_COLOR # Whether to allow changing the slider width with arrow keys change_width_enabled = False def __init__(self, parent=None, background='default', name=None, orientation='vertical', slider_width=1, **kwargs): """ Initialize the slider and set up the visual tweaks to make a PlotWidget look more like a scalebar. =========== ============================================================ parent QtWidget instance; parent widget of this widget background str; confer PyQt documentation name str; allows giving a name for debug purposes orientation str, `horizontal` or `vertical`; orientation of the cursor =========== ============================================================ """ super().__init__(parent=parent, background=background, **kwargs) if orientation not in ['horizontal', 'vertical']: raise ValueError('Only `horizontal` or `vertical` are allowed for ' 'orientation.') self.orientation = orientation self.orientate() if name is not None: self.name = name # 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 slider is stored with a TracedVariable initial_pos = 0 pos = TracedVariable(initial_pos, name='pos') self.register_traced_variable(pos) # Set up the slider self.slider_width = TracedVariable(slider_width, name='{}.slider_width'.format( self.name)) self.slider = pg.InfiniteLine(initial_pos, movable=True, angle=self.angle) self.set_slider_pen(color=(255, 255, 0, 255), width=slider_width) # Add a marker. Args are (style, position (from 0-1), size #NOTE # seems broken #self.slider.addMarker('o', 0.5, 10) 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) # sigMouseReleased seems to not work (maybe because sigDragged is used) #self.sigMouseReleased.connect(self.onClick) # The inherited mouseReleaseEvent is probably used for sigDragged # already. Anyhow, overwriting it here leads to inconsistent behaviour. #self.mouseReleaseEvent = self.onClick def orientate(self): """ Define all aspects that are dependent on the orientation. """ if self.orientation == 'vertical': self.right_axis = 'right' self.secondary_axis = 'top' self.secondary_axis_grid = (1, 1) self.angle = 90 self.slider_axis_index = 0 else: self.right_axis = 'top' self.secondary_axis = 'right' self.secondary_axis_grid = (2, 2) self.angle = 0 self.slider_axis_index = 1 def register_traced_variable(self, traced_variable): """ Set self.pos to the given TracedVariable instance and connect the relevant slots to the signals. This can be used to share a TracedVariable among widgets. """ 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) def on_position_change(self): """ Callback for the :signal: `sigDragged <pyqtgraph.InfiniteLine.sigDragged>`. Set the value of the TracedVariable instance self.pos to the current slider position. """ 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) def on_allowed_values_change(self): """ Callback for the :signal: `sig_allowed_values_changed <pyqtgraph.utilities.TracedVariable.sig_allowed_values_changed>`. With a change of the allowed values in the TracedVariable, we should update our bounds accordingly. The number of allowed values can also give us a hint for a reasonable maximal width for the slider. """ # If the allowed values were reset, just exit if self.pos.allowed_values is None: return lower = self.pos.min_allowed upper = self.pos.max_allowed self.set_bounds(lower, upper) # Define a max width of the slider and the resulting set of allowed # widths max_width = int(len(self.pos.allowed_values) / 2) allowed_widths = [2 * i + 1 for i in range(max_width + 1)] self.slider_width.set_allowed_values(allowed_widths) def set_position(self): """ Callback for the :signal: `sig_value_changed <data_slicer.utilities.TracedVariable.sig_value_changed>`. Whenever the value of this TracedVariable is updated (possibly from outside this Scalebar object), put the slider to the appropriate position. """ new_pos = self.pos.get_value() self.slider.setValue(new_pos) def set_bounds(self, lower, upper): """ Set both, the displayed area of the axis as well as the the range in which the slider (InfiniteLine) can be dragged to the interval [lower, upper]. """ if self.orientation == 'vertical': self.setXRange(lower, upper, padding=0.01) else: self.setYRange(lower, upper, padding=0.01) self.slider.setBounds([lower, upper]) # When the bounds update, the mousewheelspeed should change accordingly # TODO This should be in a slot to self.pos.sig_value_changed now self.wheel_frames = 1 # Ensure wheel_frames is at least as big as a step in the allowed # values. NOTE This assumes allowed_values to be evenly spaced. av = self.pos.allowed_values if av is not None and self.wheel_frames <= 1: self.wheel_frames = av[1] - av[0] def set_secondary_axis(self, min_val, max_val): """ Create (or replace) a second x-axis on the top which ranges from :param: `min_val` to :param: `max_val`. This is the right axis in case of the horizontal orientation. """ # 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 # `*(1, 1)` or `*(2, 2)` refers to the axis' position in the GridLayout) plotItem.axes[self.secondary_axis]['item'] = new_axis plotItem.layout.addItem(new_axis, *self.secondary_axis_grid) def set_slider_pen(self, color=None, width=None, hover_color=None): """ Define the color and thickness of the slider (`InfiniteLine object <pyqtgraph.InfiniteLine>`) and store these attribute in :attr: `self.slider_width` and :attr: `self.cursor_color`). """ # 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.slider_width.get_value() 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) def increase_width(self, step=1): """ Increase (or decrease) `self.slider_width` by `step` units of odd numbers (such that the line always has a well defined center at the value it is positioned at). """ old_width = self.slider_width.get_value() new_width = old_width + 2 * step if new_width < 0: new_width = 1 self.slider_width.set_value(new_width) # Convert width in steps to width in pixels dmin, dmax = self.viewRange()[self.slider_axis_index] pmax = self.rect().getRect()[self.slider_axis_index + 2] pixel_per_step = pmax / (dmax - dmin) pen_width = new_width * pixel_per_step self.set_slider_pen(width=pen_width) def increase_pos(self, step=1): """ Increase (or decrease) `self.pos` by a reasonable amount. I.e. move `step` steps along the list of allowed values. """ allowed_values = self.pos.allowed_values old_index = indexof(self.pos.get_value(), allowed_values) new_index = int((old_index + step) % len(allowed_values)) new_value = allowed_values[int(new_index)] self.pos.set_value(new_value) def keyPressEvent(self, event): """ Define responses to keyboard interactions. """ key = event.key() logger.debug('{}.keyPressEvent(): key={}'.format(self.name, key)) if key == qt.QtCore.Qt.Key_Right: self.increase_pos(1) elif key == qt.QtCore.Qt.Key_Left: self.increase_pos(-1) elif self.change_width_enabled and key == qt.QtCore.Qt.Key_Up: self.increase_width(1) elif self.change_width_enabled and key == qt.QtCore.Qt.Key_Down: self.increase_width(-1) else: event.ignore() return # If any if-statement matched, we accept the event event.accept() def wheelEvent(self, event): """ Override of the Qt wheelEvent method. Fired on mousewheel scrolling inside the widget. """ # Get the relevant coordinate of the mouseWheel scroll delta = event.angleDelta().y() logger.debug('<{}>wheelEvent(); delta = {}'.format(self.name, delta)) if delta > 0: sign = 1 elif delta < 0: sign = -1 else: # It seems that in some cases delta==0 sign = 0 increment = sign * self.wheel_frames logger.debug('<{}>wheelEvent(); increment = {}'.format( self.name, increment)) self.increase_pos(increment)
import logging from data_slicer.utilities import TracedVariable from data_slicer import set_up_logging print('Initializing TracedVariable') tv1 = TracedVariable(5, name='tv1') print('Setting value') tv1.set_value(3) print('Reading value') foo = tv1.get_value() print('Initializing second TracedVariable') tv2 = TracedVariable(5, name='tv2') print('Setting value') tv2.set_value(3) print('Reading value') foo = tv2.get_value()
class ThreeDWidget(QtGui.QWidget): """ A widget that contains a :class:`GLViewWidget <pyqtgraph.opengl.GLViewWidget>` that allows displaying 2D colormeshes in a three dimensional scene. This class mostly functions as a base class for more refined variations. """ def __init__(self, *args, data=None, **kwargs): """ Set up the :class: `GLViewWidget <pyqtgraph.opengl.GLViewWidget>` as this widget's central widget. """ super().__init__(*args, **kwargs) # Initialize instance variables self.data = TracedVariable(None, name='data') self.cmap = load_cmap(DEFAULT_CMAP) self.lut = self.cmap.getLookupTable() self.gloptions = 'translucent' # Create a GLViewWidget and put it into the layout of this view self.layout = QtGui.QGridLayout() self.setLayout(self.layout) self.glview = gl.GLViewWidget() # Add the scalebar self._initialize_sub_widgets() # Put all widgets in place self.align() # Add some coordinate axes to the view self.coordinate_axes = gl.GLAxisItem() self.glview.addItem(self.coordinate_axes) self.set_coordinate_axes(True) # Try to set the default data if data is not None: self.set_data(data) def _initialize_sub_widgets(self): """ Create the slider subwidget. This method exists so it can be overridden by subclasses. """ self.slider_xy = Scalebar(name='xy-slider') self.slider_xy.pos.sig_value_changed.connect(self.update_xy) self.slider_xy.add_text('xy plane', relpos=(0.5, 0.5)) def align(self): """ Put all sub-widgets and elements into the layout. """ l = self.layout l.addWidget(self.glview, 0, 0, 5, 5) l.addWidget(self.slider_xy, 4, 2, 1, 1) def set_coordinate_axes(self, on=True): """ Turn the visibility of the coordinate axes on or off. **Parameters** == ==================================================================== on bool or one of ('on', 1); if not `True` or any of the values stated, turn the axes off. Otherwise turn them on. == ==================================================================== """ if on in [True, 'on', 1]: logger.debug('Turning coordinate axes ON') self.coordinate_axes.show() else: logger.debug('Turning coordinate axes OFF') self.coordinate_axes.hide() def set_data(self, data): """ Set this widget's data in a :class:`TracedVariable <data_slicer.utilities.TracedVariable>` instance to allow direct updates whenever the data changes. **Parameters** ==== ================================================================== data np.array of shape (x, y, z); the data cube to be displayed. ==== ================================================================== """ self.data.set_value(data) self.levels = [data.min(), data.max()] self.xscale, self.yscale, self.zscale = [1 / s for s in data.shape] self._update_sliders() self._initialize_planes() def _initialize_planes(self): """ This wrapper exists to be overwritten by subclasses. """ self.initialize_xy() def _update_sliders(self): """ Update the allowed values for of the plane slider(s). """ data = self.data.get_value() self.slider_xy.pos.set_allowed_values(range(data.shape[2])) def get_slice(self, d, i, integrate=0, silent=True): """ Wrap :func:`make_slice <data_slicer.utilities.make_slice>` to create slices out of this widget's `self.data`. Confer respective documentation for details. """ return make_slice(self.data.get_value(), d, i, integrate=integrate, silent=silent) def get_xy_slice(self, i, integrate=0): """ Shorthand to get an xy slice, i.e. *d=2*. """ self.old_z = i return self.get_slice(2, i, integrate) def make_texture(self, cut): """ Wrapper for :func:`makeRGBA <pyqtgraph.makeRGBA>`.""" return pg.makeRGBA(cut, levels=self.levels, lut=self.lut)[0] def initialize_xy(self): """ Create the xy plane. """ # Get out if no data is present if self.data.get_value() is None: return # Remove any old planes if hasattr(self, 'xy'): self.glview.removeItem(self.xy) # Get the data and texture to create the GLImageItem object cut = self.get_xy_slice(self.slider_xy.pos.get_value()) texture = self.make_texture(cut) self.xy = gl.GLImageItem(texture, glOptions=self.gloptions) # Scale and translate to origin and add to glview self.xy.scale(self.xscale, self.yscale, 1) self.xy.translate(T, T, T) # Put to position in accordance with slider self.old_z = 0 #self.slider_xy.pos.get_value() self.update_xy() # Add to GLView self.glview.addItem(self.xy) def update_xy(self): """ Update both texture and position of the xy plane. """ if not hasattr(self, 'xy'): return # Get the current position z = self.slider_xy.pos.get_value() # Translate self.xy.translate(0, 0, self.zscale * (z - self.old_z)) # Update the texture (this needs to happen after the translation # because self.get_xy_slice updates the value of self.old_z) cut = self.get_xy_slice(z) texture = self.make_texture(cut) self.xy.setData(texture) def set_cmap(self, cmap): """ Change the used colormap to a :class:`ds_cmap <data_slicer.cmaps.ds_cmap>` instance. """ if isinstance(cmap, str): self.cmap = load_cmap(cmap) elif isinstance(cmap, ds_cmap): self.cmap = cmap else: raise TypeError('*cmap* has to be a valid colormap name or a ' '*ds_cmap* instance') # Update the necessary elements self.lut = self.cmap.getLookupTable() self._on_cmap_change() def _on_cmap_change(self): """ Update all elements affected by the cmap change. """ self.update_xy()
class PITDataHandler(): """ Object that keeps track of a set of 3D data and allows manipulations on it. In a Model-View-Controller framework this could be seen as the Model, while :class: `MainWindow <data_slicer.pit.MainWindow>` would be the View part. """ # np.array that contains the 3D data data = None axes = np.array([[0, 1], [0, 1], [0, 1]]) # Indices of *data* that are displayed in the main plot displayed_axes = (0, 1) # Index along the z axis at which to produce a slice z = TracedVariable(0, name='z') # Number of slices to integrate along z # integrate_z = TracedVariable(value=0, name='integrate_z') # How often we have rolled the axes from the original setup _roll_state = 0 def __init__(self, main_window): self.main_window = main_window def get_data(self): """ Convenience `getter` method. Allows writing `self.get_data()` instead of ``self.data.get_value()``. """ return self.data.get_value() def set_data(self, data): """ Convenience `setter` method. Allows writing `self.set_data(d)` instead of ``self.data.set_value(d)``. """ self.data.set_value(data) def prepare_data(self, data, axes=3 * [None]): """ Load the specified data and prepare the corresponding z range. Then display the newly loaded data. *Parameters* ==== ================================================================== data 3d array; the data to display axes len(3) list or array of 1d-arrays or None; the units along the x, y and z axes respectively. If any of those is *None*, pixels are used. ==== ================================================================== """ logger.debug('prepare_data()') self.data = TracedVariable(data, name='data') self.axes = np.array(axes) # Retain a copy of the original data and axes so that we can reset later # NOTE: this effectively doubles the used memory! self.original_data = copy(self.data.get_value()) self.original_axes = copy(self.axes) self.prepare_axes() self.on_z_dim_change() # Connect signal handling so changes in data are immediately reflected self.z.sig_value_changed.connect( \ lambda : self.main_window.update_main_plot(emit=False)) self.data.sig_value_changed.connect(self.on_data_change) self.main_window.update_main_plot() self.main_window.set_axes() def load(self, filename): """ Alias to :func: `open <data_slicer.pit.PITDataHandler.open>`. """ self.open(filename) def open(self, filename): """ Open a file that's readable by :module: `dataloading <data_slicer.dataloading>`. """ D = dl.load_data(filename) self.prepare_data(D.data, D.axes) def update_z_range(self): """ When new data is loaded or the axes are rolled, the limits and allowed values along the z dimension change. """ # Determine the new ranges for z self.zmin = 0 self.zmax = self.get_data().shape[2] - 1 self.z.set_allowed_values(range(self.zmin, self.zmax + 1)) # self.z.set_value(self.zmin) def reset_data(self): """ Put all data and metadata into its original state, as if it was just loaded from file. """ logger.debug('reset_data()') self.set_data(copy(self.original_data)) self.axes = copy(self.original_axes) self.prepare_axes() # Roll back to the view we had before reset_data was called self._roll_axes(self._roll_state, update=False) def prepare_axes(self): """ Create a list containing the three original x-, y- and z-axes and replace *None* with the amount of pixels along the given axis. """ shapes = self.data.get_value().shape # Avoid undefined axes scales and replace them with len(1) sequences for i, axis in enumerate(self.axes): if axis is None: self.axes[i] = np.arange(shapes[i]) def on_data_change(self): """ Update self.main_window.image_data and replot. """ logger.debug('on_data_change()') self.update_image_data() self.main_window.redraw_plots() # Also need to recalculate the intensity plot self.on_z_dim_change() def on_z_dim_change(self): """ Called when either completely new data is loaded or the dimension from which we look at the data changed (e.g. through :func: `roll_axes <data_slicer.pit.PITDataHandler.roll_axes>`). Update the z range and the integrated intensity plot. """ logger.debug('on_z_dim_change()') self.update_z_range() # Get a shorthand for the integrated intensity plot ip = self.main_window.integrated_plot # Remove the old integrated intensity curve try: old = ip.listDataItems()[0] ip.removeItem(old) except IndexError: pass # Calculate the integrated intensity and plot it self.calculate_integrated_intensity() ip.plot(self.integrated) # Also display the actual data values in the top axis zscale = self.axes[2] zmin = zscale[0] zmax = zscale[-1] ip.set_secondary_axis(zmin, zmax) def calculate_integrated_intensity(self): self.integrated = self.get_data().sum(0).sum(0) def update_image_data(self): """ Get the right (possibly integrated) slice out of *self.data*, apply postprocessings and store it in *self.image_data*. Skip this if the z value happens to be out of range, which can happen if the image data changes and the z scale hasn't been updated yet. """ logger.debug('update_image_data()') z = self.z.get_value() integrate_z = \ int(self.main_window.integrated_plot.slider_width.get_value()/2) data = self.get_data() try: self.main_window.image_data = make_slice(data, dim=2, index=z, integrate=integrate_z) except IndexError: logger.debug( ('update_image_data(): z index {} out of range for ' 'data of length {}.').format(z, self.image_data.shape[0])) def roll_axes(self, i=1): """ Change the way we look at the data cube. While initially we see an Y vs. X slice in the main plot, roll it to Z vs. Y. A second call would roll it to X vs. Z and, finally, a third call brings us back to the original situation. *Parameters* = ===================================================================== i int; Number of dimensions to roll. = ===================================================================== """ self._roll_axes(i, update=True) def _roll_axes(self, i=1, update=True): """ Backend for :func: `roll_axes <arpys.pit.PITDataHandler.roll_axes>` that allows suppressing updating the roll-state, which is useful for :func: `reset_data <arpys.pit.PITDataHandler.reset_data>`. """ logger.debug('roll_axes()') data = self.get_data() res = np.roll([0, 1, 2], i) self.axes = np.roll(self.axes, -i) self.set_data(np.moveaxis(data, [0, 1, 2], res)) # Setting the data triggers a call to self.redraw_plots() self.on_z_dim_change() self.main_window.set_axes() if update: self._roll_state = (self._roll_state + i) % NDIM def lineplot(self, plot='main', dim=0, ax=None, n=10, offset=0.2, lw=0.5, color='k', label_fmt='{:.2f}', n_ticks=5, **getlines_kwargs): """ Create a matplotlib figure with *n* lines extracted out of one of the visible plots. The lines are normalized to their global maximum and shifted from each other by *offset*. See :func: `get_lines <data_slicer.utilities.get_lines>` for more options on the extraction of the lines. This wraps the :class: `ImagePlot <data_slicer.imageplot.ImagePlot>`'s lineplot method. *Parameters* =============== ======================================================= plot str; either "main" or "cut", specifies from which plot to extract the lines. dim int; either 0 or 1, specifies in which direction to take the lines. ax matplotlib.axes.Axes; the axes in which to plot. If *None*, create a new figure with a fresh axes. n int; number of lines to extract. offset float; spacing between neighboring lines. lw float; linewidth of the plotted lines. color any color argument understood by matplotlib; color of the plotted lines. label_fmt str; a format string for the ticklabels. n_ticks int; number of ticks to print. getlines_kwargs other kwargs are passed to :func: `get_lines <data_slicer.utilities.get_lines>` =============== ======================================================= *Returns* =========== =========================================================== lines2ds list of Line2D objects; the drawn lines. xticks list of float; locations of the 0 intensity value of each line. xtickvalues list of float; if *momenta* were supplied, corresponding xtick values in units of *momenta*. Otherwise this is just a copy of *xticks*. xticklabels list of str; *xtickvalues* formatted according to *label_fmt*. =========== =========================================================== """ # Get the specified data if plot == 'main': imageplot = self.main_window.main_plot elif plot == 'cut': imageplot = self.main_window.cut_plot else: raise ValueError('*plot* should be one of ("main", "cut").') # Create a mpl axis object if none was given if ax is None: fig, ax = plt.subplots(1) return imageplot.lineplot(ax=ax, dim=dim, n=n, offset=offset, lw=lw, color=color, label_fmt=label_fmt, n_ticks=n_ticks, **getlines_kwargs) def plot_all_slices(self, dim=2, integrate=0, zs=None, labels='default', max_ppf=16, max_nfigs=2): """ Wrapper for :func: `plot_cuts <data_slicer.utilities.plot_cuts>`. Plot all (or only the ones specified by `zs`) slices along dimension `dim` on separate suplots onto matplotlib figures. *Parameters* ========= ============================================================ dim int; one of (0,1,2). Dimension along which to take the cuts. integrate int or 'full'; number of slices to integrate around each extracted cut. If 'full', take the maximum number possible, depending on *zs* and whether the number of cuts is reduced due to otherwise exceeding *max_nfigs*. zs 1D np.array; selection of indices along dimension `dim`. Only the given indices will be plotted. labels 1D array/list of length z. Optional labels to assign to the different cuts. By default the values of the respective axis are used. Set to *None* to suppress labels. max_ppf int; maximum number of *p*lots *p*er *f*igure. max_nfigs int; maximum number of figures that are created. If more would be necessary to display all plots, a warning is issued and only every N'th plot is created, where N is chosen such that the whole 'range' of plots is represented on the figures. ========= ============================================================ """ data = self.get_data() if labels == 'default': # Use the values of the respective axis as default labels labels = self.axes[dim] # The default values for the colormap are taken from the main_window # settings gamma = self.main_window.gamma vmax = self.main_window.vmax * data.max() cmap = convert_ds_to_matplotlib(self.main_window.cmap, self.main_window.cmap_name) plot_cuts(data, dim=dim, integrate=integrate, zs=zs, labels=labels, cmap=cmap, vmax=vmax, gamma=gamma, max_ppf=max_ppf, max_nfigs=max_nfigs)
class ImagePlot(pg.PlotWidget) : """ A PlotWidget which mostly contains a single 2D image (intensity distribution) or a 3D array (distribution of RGB values) as well as all the nice pyqtgraph axes panning/rescaling/zooming functionality. In addition, this allows one to use custom axes scales as opposed to being limited to pixel coordinates. **Signals** ================= ========================================================= sig_image_changed emitted whenever the image is updated sig_axes_changed emitted when the axes are updated sig_clicked emitted when user clicks inside the imageplot ================= ========================================================= """ sig_image_changed = qt.QtCore.Signal() sig_axes_changed = qt.QtCore.Signal() sig_clicked = qt.QtCore.Signal(object) def __init__(self, image=None, parent=None, background='default', name=None, **kwargs) : """ Allows setting of the image upon initialization. **Parameters** ========== ============================================================ image np.ndarray or pyqtgraph.ImageItem instance; the image to be displayed. parent QtWidget instance; parent widget of this widget. background str; confer PyQt documentation name str; allows giving a name for debug purposes ========== ============================================================ """ # Initialize instance variables # np.array, raw image data self.image_data = None # pg.ImageItem of *image_data* self.image_item = None self.image_kwargs = {} self.xlim = None self.ylim = None self.xscale = None self.yscale = None self.transform_factors = [] self.transposed = TracedVariable(False, name='transposed') self.crosshair_cursor_visible = False super().__init__(parent=parent, background=background, viewBox=DSViewBox(imageplot=self), **kwargs) self.name = name # Show top and tight axes by default, but without ticklabels self.showAxis('top') self.showAxis('right') self.getAxis('top').setStyle(showValues=False) self.getAxis('right').setStyle(showValues=False) if image is not None : self.set_image(image) self.sig_axes_changed.connect(self.fix_viewrange) def show_cursor(self, show=True) : """ Toggle whether or not to show a crosshair cursor that tracks the mouse movement. """ if show : crosshair_cursor = Crosshair() crosshair_cursor.set_movable(False) crosshair_cursor.set_color((255, 255, 255, 255), (255, 255, 255, 255)) crosshair_cursor.add_to(self) self.scene().sigMouseMoved.connect(self.on_mouse_move) self.crosshair_cursor = crosshair_cursor else : try : self.scene().sigMouseMoved.disconnect(self.on_mouse_move) except TypeError : pass try : self.crosshair_cursor.remove_from(self) except AttributeError : pass self.crosshair_cursor_visible = show self.plotItem.vb.menu.toggle_cursor.setChecked(show) def toggle_cursor(self) : """ Change the visibility of the crosshair cursor. """ self.show_cursor(not self.crosshair_cursor_visible) def on_mouse_move(self, pos) : """ Slot for mouse movement over the plot. Calculate the mouse position in data coordinates and move the crosshair_cursor there. **Parameters** === =================================================================== pos QPointF object; x and y position of the mouse as returned by :signal:`sigMouseMoved <data_slicer.imageplot.ImagePlot.sigMouseMoved>`. === =================================================================== """ if self.plotItem.sceneBoundingRect().contains(pos) : data_point = self.plotItem.vb.mapSceneToView(pos) self.crosshair_cursor.move_to((data_point.x(), data_point.y())) def mousePressEvent(self, event) : """ Figure out where the click happened in data coordinates and make the position available through the signal :signal:`sig_clicked <data_slicer.imageplot.ImagePlot.sig_clicked>`. """ if event.button() == qt.QtCore.Qt.LeftButton : vb = self.plotItem.vb last_click = vb.mapToView(vb.mapFromScene(event.localPos())) message = 'Last click at ( {:.4f} | {:.4f} )' self.sig_clicked.emit(message.format(last_click.x(), last_click.y())) super().mousePressEvent(event) def remove_image(self) : """ Removes the current image using the parent's :meth:`removeItem pyqtgraph.PlotWidget.removeItem` function. """ if self.image_item is not None : self.removeItem(self.image_item) self.image_item = None def set_image(self, image, emit=True, *args, **kwargs) : """ Expects either np.arrays or pg.ImageItems as input and sets them correctly to this PlotWidget's Image with `addItem`. Also makes sure there is only one Image by deleting the previous image. Emits :signal:`sig_image_changed` **Parameters** ======== ============================================================== image np.ndarray or pyqtgraph.ImageItem instance; the image to be displayed. emit bool; whether or not to emit :signal:`sig_image_changed` (kw)args positional and keyword arguments that are passed on to :class:`pyqtgraph.ImageItem` ======== ============================================================== """ # Convert array to ImageItem if isinstance(image, ndarray) : if 0 not in image.shape : image_item = ImageItem(image, *args, **kwargs) else : logger.debug(('<{}>.set_image(): image.shape is {}. Not ' 'setting image.').format(self.name, image.shape)) return else : image_item = image # Throw an exception if image is not an ImageItem if not isinstance(image_item, ImageItem) : message = '''`image` should be a np.array or pg.ImageItem instance, not {}'''.format(type(image)) raise TypeError(message) # Transpose if necessary if self.transposed.get_value() : image_item = ImageItem(image_item.image.T, *args, **kwargs) # Replace the image self.remove_image() self.image_item = image_item self.image_data = image_item.image logger.debug('<{}>Setting image.'.format(self.name)) 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 : logger.info('<{}>Emitting sig_image_changed.'.format(self.name)) self.sig_image_changed.emit() def set_xscale(self, xscale, update=False) : """ Set the xscale of the plot. *xscale* is an array of the length ``len(self.image_item.shape[0])``. """ if self.transposed.get_value() : self._set_yscale(xscale, update) else : self._set_xscale(xscale, update) def set_yscale(self, yscale, update=False) : """ Set the yscale of the plot. *yscale* is an array of the length ``len(self.image_item.image.shape[1])``. """ if self.transposed.get_value() : self._set_xscale(yscale, update) else : self._set_yscale(yscale, update) def _set_xscale(self, xscale, update=False, force=False) : """ Set the scale of the horizontal axis of the plot. *force* can be used to bypass the length checking. """ # Sanity check if not force and self.image_item is not None and \ len(xscale) != self.image_item.image.shape[0] : raise TypeError('Shape of xscale does not match data dimensions.') self.xscale = xscale # 'Autoscale' the image to the xscale self.xlim = (xscale[0], xscale[-1]) if update : self._set_axes_scales(emit=True) def _set_yscale(self, yscale, update=False, force=False) : """ Set the scale of the vertical axis of the plot. *force* can be used to bypass the length checking. """ # Sanity check if not force and self.image_item is not None and \ len(yscale) != self.image_item.image.shape[1] : raise TypeError('Shape of yscale does not match data dimensions.') self.yscale = yscale # 'Autoscale' the image to the xscale self.ylim = (yscale[0], yscale[-1]) if update : self._set_axes_scales(emit=True) def transpose(self) : """ Transpose the image, i.e. swap the x- and y-axes. """ self.transposed.set_value(not self.transposed.get_value()) # Swap the scales new_xscale = self.yscale new_yscale = self.xscale self._set_xscale(new_xscale, force=True) self._set_yscale(new_yscale, force=True) # Update the image if not self.transposed.get_value() : # Take care of the back-transposition here self.set_image(self.image_item.image.T, lut=self.image_item.lut) else : self.set_image(self.image_item, lut=self.image_item.lut) def set_xlabel(self, label) : """ Shorthand for setting this plot's x axis label. """ axis = self.getAxis('bottom') axis.setLabel(label) def set_ylabel(self, label) : """ Shorthand for setting this plot's y axis label. """ axis = self.getAxis('left') axis.setLabel(label) def _set_axes_scales(self, emit=False) : """ Transform the image such that it matches the desired x and y scales. """ # Get image dimensions and requested origin (x0,y0) and top right # corner (x1, y1) nx, ny = self.image_item.image.shape logger.debug(('<{}>_set_axes_scales(): self.image_item.image.shape={}' + ' x {}').format(self.name, nx, ny)) [[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 = qt.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) self._update_transform_factors() if emit : logger.info('<{}>Emitting sig_axes_changed.'.format(self.name)) self.sig_axes_changed.emit() def get_limits(self) : """ Return ``[[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_item.image.shape else : x, y = 1, 1 # Set the limits to image pixels if they are not defined if self.xlim is None : self.set_xscale(arange(0, x)) x_min, x_max = self.xlim if self.ylim is None : self.set_yscale(arange(0, y)) y_min, y_max = self.ylim logger.debug(('<{}>get_limits(): [[x_min, x_max], [y_min, y_max]] = ' + '[[{}, {}], [{}, {}]]').format(self.name, x_min, x_max, y_min, y_max)) return [[x_min, x_max], [y_min, y_max]] def fix_viewrange(self) : """ Prevent zooming out by fixing the limits of the ViewBox. """ logger.debug('<{}>fix_viewrange().'.format(self.name)) [[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) def release_viewrange(self) : """ Undo the effects of :meth:`fix_viewrange <data_slicer.imageplot.ImagePlot.fix_viewrange>` """ logger.debug('<{}>release_viewrange().'.format(self.name)) self.setLimits(xMin=-inf, xMax=inf, yMin=-inf, yMax=inf, maxXRange=inf, maxYRange=inf) def _update_transform_factors(self) : """ Create a copy of the parameters that are necessary to reproduce the current transform. This is necessary e.g. for the calculation of the transform in :meth:`rotate <data_slicer.imageplot.ImagePlot.rotate>`. """ transform = self.image_item.transform() dx = transform.dx() dy = transform.dy() sx = transform.m11() sy = transform.m22() wx = self.image_item.width() wy = self.image_item.height() self.transform_factors = [dx, dy, sx, sy, wx, wy] def rotate(self, alpha=0) : """ Rotate the image_item by the given angle *alpha* (in degrees). """ # Get the details of the current transformation if self.transform_factors == [] : self._update_transform_factors() dx, dy, sx, sy, wx, wy = self.transform_factors # Build the transformation anew, adding a rotation # Remember that the order in which transformations are applied is # reverted to how they are added in the code, i.e. last transform # added in the code will come first (this is the reason we have to # completely rebuild the transformation instead of just adding a # rotation...) transform = self.image_item.transform() transform.reset() transform.translate(dx, dy) transform.translate(wx/2*sx, wy/2*sy) transform.rotate(alpha) transform.scale(sx, sy) transform.translate(-wx/2, -wy/2) self.release_viewrange() self.image_item.setTransform(transform) def mpl_export(self, *args, figsize=(5,5), title='', xlabel='', ylabel='', dpi=300) : """ Export the content of this plot to a png image using matplotlib. The resulting image will have a white background and black ticklabes and should therefore be more readable than pyqtgraph's native plot export options. **Parameters** ======= =============================================================== figsize tuple of float; (height, width) of figure in inches title str; figure title xlabel str; x axis label ylabel str; y axis label dpi int; png resolution in pixels per inch args positional arguments are absorbed and discarded (necessary to connect this method to signal handling) ======= =============================================================== """ logger.debug('<ImagePlot.mpl_export()>') # Show the dialog with some options dialog = MPLExportDialog(self, parent=self) # ok_button = qt.QtGui.QPushButton('Done', dialog) if not dialog.exec_() : return # Replot to update the figure dialog.plot_preview() # Get a filename first fd = qt.QtGui.QFileDialog() filename = fd.getSaveFileName()[0] if not filename : return logger.debug('Outfilename: {}'.format(filename)) # Update figure size before saving width, height = [float(box.text()) for box in [dialog.box_width, dialog.box_height]] dialog.figure.set_figwidth(width) dialog.figure.set_figheight(height) dialog.figure.savefig(filename, dpi=dpi) def lineplot(self, ax, dim=0, n=10, offset=0.2, lw=0.5, color='k', label_fmt='{:.2f}', n_ticks=5, **getlines_kwargs) : """ Create a matplotlib plot with *n* lines extracted out of one of the visible plots. The lines are normalized to their global maximum and shifted from each other by *offset*. See :func:`get_lines <data_slicer.utilities.get_lines>` for more options on the extraction of the lines. **Parameters** =============== ======================================================= plot str; either "main" or "cut", specifies from which plot to extract the lines. dim int; either 0 or 1, specifies in which direction to take the lines. ax matplotlib.axes.Axes; the axes in which to plot. n int; number of lines to extract. offset float; spacing between neighboring lines. lw float; linewidth of the plotted lines. color any color argument understood by matplotlib; color of the plotted lines. label_fmt str; a format string for the ticklabels. n_ticks int; number of ticks to print. getlines_kwargs other kwargs are passed to :func:`get_lines <data_slicer.utilities.get_lines>` =============== ======================================================= **Returns** =========== =========================================================== lines2ds list of Line2D objects; the drawn lines. xticks list of float; locations of the 0 intensity value of each line. xtickvalues list of float; if *momenta* were supplied, corresponding xtick values in units of *momenta*. Otherwise this is just a copy of *xticks*. xticklabels list of str; *xtickvalues* formatted according to *label_fmt*. =========== =========================================================== """ data = self.image_data # Get the right axes axes = [self.xscale, self.yscale] for i,scale in enumerate(axes) : # Deal with *None* if scale is None : axes[i] = np.arange(data.shape[i]) if dim==0 : data = data.T elif dim==1 : axes = axes[::-1] else : raise ValueError('*dim* should be one of (0, 1).') # Get the lines with an utility function lines, indices = get_lines(data, n, offset=offset, **getlines_kwargs) # Plot the lines line2ds = [] for line in lines : line2d = ax.plot(line, axes[0], lw=lw, color=color)[0] line2ds.append(line2d) # Create tick positions and labels xticks = [i*offset for i in range(n)] xtickvalues = axes[i][indices] xticklabels = [label_fmt.format(x) for x in xtickvalues] # Only render *n_ticks* ticks nth = int(n/n_ticks) ax.set_xticks(xticks[::nth]) ax.set_xticklabels(xticklabels[::nth]) return line2ds, xticks, xtickvalues, xticklabels
class Crosshair() : """ Crosshair made up of two InfiniteLines. """ def __init__(self, pos=(0,0)) : # Store the positions in TracedVariables self.hpos = TracedVariable(pos[1], name='hpos') self.vpos = TracedVariable(pos[0], name='vpos') # Initialize the InfiniteLines self.hline = pg.InfiniteLine(pos[1], movable=True, angle=0) self.vline = pg.InfiniteLine(pos[0], movable=True, angle=90) # Set the color self.set_color(BASE_LINECOLOR, HOVER_COLOR) # Register some callbacks 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) def add_to(self, widget) : """ Add this crosshair to a Qt widget. """ for line in [self.hline, self.vline] : line.setZValue(1) widget.addItem(line) def remove_from(self, widget) : """ Remove this crosshair from a pyqtgraph widget. """ for line in [self.hline, self.vline] : widget.removeItem(line) def set_color(self, linecolor=BASE_LINECOLOR, hover_color=HOVER_COLOR) : """ Set the color and hover color of both InfiniteLines that make up the crosshair. The arguments can be any pyqtgraph compatible color specifiers. """ for line in [self.hline, self.vline] : line.setPen(linecolor) line.setHoverPen(hover_color) def set_movable(self, movable=True) : """ Set whether or not this crosshair can be dragged by the mouse. """ for line in [self.hline, self.vline] : line.setMovable = movable def move_to(self, pos) : """ **Parameters** === =================================================================== pos 2-tuple; x and y coordinates of the desired location of the crosshair in data coordinates. === =================================================================== """ self.hpos.set_value(pos[1]) self.vpos.set_value(pos[0]) def update_position_h(self) : """ Callback for the :signal:`sig_value_changed <data_slicer.utilities.TracedVariable.sig_value_changed>`. Whenever the value of this TracedVariable is updated (possibly from outside this Crosshair object), put the crosshair to the appropriate position. """ self.hline.setValue(self.hpos.get_value()) def update_position_v(self) : """ Confer update_position_h. """ self.vline.setValue(self.vpos.get_value()) def on_dragged_h(self) : """ Callback for dragging of InfiniteLines. Their visual position should be reflected in the TracedVariables self.hpos and self.vpos. """ self.hpos.set_value(self.hline.value()) def on_dragged_v(self) : """ Callback for dragging of InfiniteLines. Their visual position should be reflected in the TracedVariables self.hpos and self.vpos. """ self.vpos.set_value(self.vline.value()) def set_bounds(self, xmin, xmax, ymin, ymax) : """ Set the area in which the infinitelines can be dragged. """ self.hline.setBounds([ymin, ymax]) self.vline.setBounds([xmin, xmax])
import logging from data_slicer import set_up_logging from data_slicer.utilities import TracedVariable # Set up logging #logger = logging.getLogger('ds') #logger.setLevel(logging.DEBUG) #console_handler = logging.StreamHandler() #console_handler.setLevel(logging.DEBUG) #formatter = logging.Formatter('[%(levelname)s][%(name)s]%(message)s') #console_handler.setFormatter(formatter) #logger.addHandler(console_handler) #logger.propagate = False tv = TracedVariable(value=5) tv.get_value() tv.set_value(8) tv.get_value()