diff options
Diffstat (limited to 'silx/gui/data')
-rw-r--r-- | silx/gui/data/ArrayTableModel.py | 670 | ||||
-rw-r--r-- | silx/gui/data/ArrayTableWidget.py | 492 | ||||
-rw-r--r-- | silx/gui/data/DataViewer.py | 593 | ||||
-rw-r--r-- | silx/gui/data/DataViewerFrame.py | 217 | ||||
-rw-r--r-- | silx/gui/data/DataViewerSelector.py | 175 | ||||
-rw-r--r-- | silx/gui/data/DataViews.py | 2059 | ||||
-rw-r--r-- | silx/gui/data/Hdf5TableView.py | 646 | ||||
-rw-r--r-- | silx/gui/data/HexaTableView.py | 286 | ||||
-rw-r--r-- | silx/gui/data/NXdataWidgets.py | 1081 | ||||
-rw-r--r-- | silx/gui/data/NumpyAxesSelector.py | 578 | ||||
-rw-r--r-- | silx/gui/data/RecordTableView.py | 447 | ||||
-rw-r--r-- | silx/gui/data/TextFormatter.py | 395 | ||||
-rw-r--r-- | silx/gui/data/_RecordPlot.py | 92 | ||||
-rw-r--r-- | silx/gui/data/_VolumeWindow.py | 148 | ||||
-rw-r--r-- | silx/gui/data/__init__.py | 35 | ||||
-rw-r--r-- | silx/gui/data/setup.py | 41 | ||||
-rw-r--r-- | silx/gui/data/test/__init__.py | 45 | ||||
-rw-r--r-- | silx/gui/data/test/test_arraywidget.py | 329 | ||||
-rw-r--r-- | silx/gui/data/test/test_dataviewer.py | 314 | ||||
-rw-r--r-- | silx/gui/data/test/test_numpyaxesselector.py | 161 | ||||
-rw-r--r-- | silx/gui/data/test/test_textformatter.py | 212 |
21 files changed, 0 insertions, 9016 deletions
diff --git a/silx/gui/data/ArrayTableModel.py b/silx/gui/data/ArrayTableModel.py deleted file mode 100644 index b7bd9c4..0000000 --- a/silx/gui/data/ArrayTableModel.py +++ /dev/null @@ -1,670 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -This module defines a data model for displaying and editing arrays of any -number of dimensions in a table view. -""" -from __future__ import division -import numpy -import logging -from silx.gui import qt -from silx.gui.data.TextFormatter import TextFormatter - -__authors__ = ["V.A. Sole"] -__license__ = "MIT" -__date__ = "27/09/2017" - - -_logger = logging.getLogger(__name__) - - -def _is_array(data): - """Return True if object implements all necessary attributes to be used - as a numpy array. - - :param object data: Array-like object (numpy array, h5py dataset...) - :return: boolean - """ - # add more required attribute if necessary - for attr in ("shape", "dtype"): - if not hasattr(data, attr): - return False - return True - - -class ArrayTableModel(qt.QAbstractTableModel): - """This data model provides access to 2D slices in a N-dimensional - array. - - A slice for a 3-D array is characterized by a perspective (the number of - the axis orthogonal to the slice) and an index at which the slice - intersects the orthogonal axis. - - In the n-D case, only slices parallel to the last two axes are handled. A - slice is therefore characterized by a list of indices locating the - slice on all the :math:`n - 2` orthogonal axes. - - :param parent: Parent QObject - :param data: Numpy array, or object implementing a similar interface - (e.g. h5py dataset) - :param str fmt: Format string for representing numerical values. - Default is ``"%g"``. - :param sequence[int] perspective: See documentation - of :meth:`setPerspective`. - """ - - MAX_NUMBER_OF_SECTIONS = 10e6 - """Maximum number of displayed rows and columns""" - - def __init__(self, parent=None, data=None, perspective=None): - qt.QAbstractTableModel.__init__(self, parent) - - self._array = None - """n-dimensional numpy array""" - - self._bgcolors = None - """(n+1)-dimensional numpy array containing RGB(A) color data - for the background color - """ - - self._fgcolors = None - """(n+1)-dimensional numpy array containing RGB(A) color data - for the foreground color - """ - - self._formatter = None - """Formatter for text representation of data""" - - formatter = TextFormatter(self) - formatter.setUseQuoteForText(False) - self.setFormatter(formatter) - - self._index = None - """This attribute stores the slice index, as a list of indices - where the frame intersects orthogonal axis.""" - - self._perspective = None - """Sequence of dimensions orthogonal to the frame to be viewed. - For an array with ``n`` dimensions, this is a sequence of ``n-2`` - integers. the first dimension is numbered ``0``. - By default, the data frames use the last two dimensions as their axes - and therefore the perspective is a sequence of the first ``n-2`` - dimensions. - For example, for a 5-D array, the default perspective is ``(0, 1, 2)`` - and the default frames axes are ``(3, 4)``.""" - - # set _data and _perspective - self.setArrayData(data, perspective=perspective) - - def _getRowDim(self): - """The row axis is the first axis parallel to the frames - (lowest dimension number) - - Return None for 0-D (scalar) or 1-D arrays - """ - n_dimensions = len(self._array.shape) - if n_dimensions < 2: - # scalar or 1D array: no row index - return None - # take all dimensions and remove the orthogonal ones - frame_axes = set(range(0, n_dimensions)) - set(self._perspective) - # sanity check - assert len(frame_axes) == 2 - return min(frame_axes) - - def _getColumnDim(self): - """The column axis is the second (highest dimension) axis parallel - to the frames - - Return None for 0-D (scalar) - """ - n_dimensions = len(self._array.shape) - if n_dimensions < 1: - # scalar: no column index - return None - frame_axes = set(range(0, n_dimensions)) - set(self._perspective) - # sanity check - assert (len(frame_axes) == 2) if n_dimensions > 1 else (len(frame_axes) == 1) - return max(frame_axes) - - def _getIndexTuple(self, table_row, table_col): - """Return the n-dimensional index of a value in the original array, - based on its row and column indices in the table view - - :param table_row: Row index (0-based) of a table cell - :param table_col: Column index (0-based) of a table cell - :return: Tuple of indices of the element in the numpy array - """ - row_dim = self._getRowDim() - col_dim = self._getColumnDim() - - # get indices on all orthogonal axes - selection = list(self._index) - # insert indices on parallel axes - if row_dim is not None: - selection.insert(row_dim, table_row) - if col_dim is not None: - selection.insert(col_dim, table_col) - return tuple(selection) - - # Methods to be implemented to subclass QAbstractTableModel - def rowCount(self, parent_idx=None): - """QAbstractTableModel method - Return number of rows to be displayed in table""" - row_dim = self._getRowDim() - if row_dim is None: - # 0-D and 1-D arrays - return 1 - return min(self._array.shape[row_dim], self.MAX_NUMBER_OF_SECTIONS) - - def columnCount(self, parent_idx=None): - """QAbstractTableModel method - Return number of columns to be displayed in table""" - col_dim = self._getColumnDim() - if col_dim is None: - # 0-D array - return 1 - return min(self._array.shape[col_dim], self.MAX_NUMBER_OF_SECTIONS) - - def __isClipped(self, orientation=qt.Qt.Vertical) -> bool: - """Returns whether or not array is clipped in a given orientation""" - if orientation == qt.Qt.Vertical: - dim = self._getRowDim() - else: - dim = self._getColumnDim() - return (dim is not None and - self._array.shape[dim] > self.MAX_NUMBER_OF_SECTIONS) - - def __isClippedIndex(self, index) -> bool: - """Returns whether or not index's cell represents clipped data.""" - if not index.isValid(): - return False - if index.row() == self.MAX_NUMBER_OF_SECTIONS - 2: - return self.__isClipped(qt.Qt.Vertical) - if index.column() == self.MAX_NUMBER_OF_SECTIONS - 2: - return self.__isClipped(qt.Qt.Horizontal) - return False - - def __clippedData(self, role=qt.Qt.DisplayRole): - """Return data for cells representing clipped data""" - if role == qt.Qt.DisplayRole: - return "..." - elif role == qt.Qt.ToolTipRole: - return "Dataset is too large: display is clipped" - else: - return None - - def data(self, index, role=qt.Qt.DisplayRole): - """QAbstractTableModel method to access data values - in the format ready to be displayed""" - if index.isValid(): - if self.__isClippedIndex(index): # Special displayed for clipped data - return self.__clippedData(role) - - row, column = index.row(), index.column() - - # When clipped, display last data of the array in last column of the table - if (self.__isClipped(qt.Qt.Vertical) and - row == self.MAX_NUMBER_OF_SECTIONS - 1): - row = self._array.shape[self._getRowDim()] - 1 - if (self.__isClipped(qt.Qt.Horizontal) and - column == self.MAX_NUMBER_OF_SECTIONS - 1): - column = self._array.shape[self._getColumnDim()] - 1 - - selection = self._getIndexTuple(row, column) - - if role == qt.Qt.DisplayRole: - return self._formatter.toString(self._array[selection], self._array.dtype) - - if role == qt.Qt.BackgroundRole and self._bgcolors is not None: - r, g, b = self._bgcolors[selection][0:3] - if self._bgcolors.shape[-1] == 3: - return qt.QColor(r, g, b) - if self._bgcolors.shape[-1] == 4: - a = self._bgcolors[selection][3] - return qt.QColor(r, g, b, a) - - if role == qt.Qt.ForegroundRole: - if self._fgcolors is not None: - r, g, b = self._fgcolors[selection][0:3] - if self._fgcolors.shape[-1] == 3: - return qt.QColor(r, g, b) - if self._fgcolors.shape[-1] == 4: - a = self._fgcolors[selection][3] - return qt.QColor(r, g, b, a) - - # no fg color given, use black or white - # based on luminosity threshold - elif self._bgcolors is not None: - r, g, b = self._bgcolors[selection][0:3] - lum = 0.21 * r + 0.72 * g + 0.07 * b - if lum < 128: - return qt.QColor(qt.Qt.white) - else: - return qt.QColor(qt.Qt.black) - - def headerData(self, section, orientation, role=qt.Qt.DisplayRole): - """QAbstractTableModel method - Return the 0-based row or column index, for display in the - horizontal and vertical headers""" - if self.__isClipped(orientation): # Header is clipped - if section == self.MAX_NUMBER_OF_SECTIONS - 2: - # Represent clipped data - return self.__clippedData(role) - - elif section == self.MAX_NUMBER_OF_SECTIONS - 1: - # Display last index from data not table - if role == qt.Qt.DisplayRole: - if orientation == qt.Qt.Vertical: - dim = self._getRowDim() - else: - dim = self._getColumnDim() - return str(self._array.shape[dim] - 1) - else: - return None - - if role == qt.Qt.DisplayRole: - return "%d" % section - return None - - def flags(self, index): - """QAbstractTableModel method to inform the view whether data - is editable or not.""" - if not self._editable or self.__isClippedIndex(index): - return qt.QAbstractTableModel.flags(self, index) - return qt.QAbstractTableModel.flags(self, index) | qt.Qt.ItemIsEditable - - def setData(self, index, value, role=None): - """QAbstractTableModel method to handle editing data. - Cast the new value into the same format as the array before editing - the array value.""" - if index.isValid() and role == qt.Qt.EditRole: - try: - # cast value to same type as array - v = numpy.array(value, dtype=self._array.dtype).item() - except ValueError: - return False - - selection = self._getIndexTuple(index.row(), - index.column()) - self._array[selection] = v - self.dataChanged.emit(index, index) - return True - else: - return False - - # Public methods - def setArrayData(self, data, copy=True, - perspective=None, editable=False): - """Set the data array and the viewing perspective. - - You can set ``copy=False`` if you need more performances, when dealing - with a large numpy array. In this case, a simple reference to the data - is used to access the data, rather than a copy of the array. - - .. warning:: - - Any change to the data model will affect your original data - array, when using a reference rather than a copy.. - - :param data: n-dimensional numpy array, or any object that can be - converted to a numpy array using ``numpy.array(data)`` (e.g. - a nested sequence). - :param bool copy: If *True* (default), a copy of the array is stored - and the original array is not modified if the table is edited. - If *False*, then the behavior depends on the data type: - if possible (if the original array is a proper numpy array) - a reference to the original array is used. - :param perspective: See documentation of :meth:`setPerspective`. - If None, the default perspective is the list of the first ``n-2`` - dimensions, to view frames parallel to the last two axes. - :param bool editable: Flag to enable editing data. Default *False*. - """ - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - if data is None: - # empty array - self._array = numpy.array([]) - elif copy: - # copy requested (default) - self._array = numpy.array(data, copy=True) - if hasattr(data, "dtype"): - # Avoid to lose the monkey-patched h5py dtype - self._array.dtype = data.dtype - elif not _is_array(data): - raise TypeError("data is not a proper array. Try setting" + - " copy=True to convert it into a numpy array" + - " (this will cause the data to be copied!)") - # # copy not requested, but necessary - # _logger.warning( - # "data is not an array-like object. " + - # "Data must be copied.") - # self._array = numpy.array(data, copy=True) - else: - # Copy explicitly disabled & data implements required attributes. - # We can use a reference. - self._array = data - - # reset colors to None if new data shape is inconsistent - valid_color_shapes = (self._array.shape + (3,), - self._array.shape + (4,)) - if self._bgcolors is not None: - if self._bgcolors.shape not in valid_color_shapes: - self._bgcolors = None - if self._fgcolors is not None: - if self._fgcolors.shape not in valid_color_shapes: - self._fgcolors = None - - self.setEditable(editable) - - self._index = [0 for _i in range((len(self._array.shape) - 2))] - self._perspective = tuple(perspective) if perspective is not None else\ - tuple(range(0, len(self._array.shape) - 2)) - - if qt.qVersion() > "4.6": - self.endResetModel() - - def setArrayColors(self, bgcolors=None, fgcolors=None): - """Set the colors for all table cells by passing an array - of RGB or RGBA values (integers between 0 and 255). - - The shape of the colors array must be consistent with the data shape. - - If the data array is n-dimensional, the colors array must be - (n+1)-dimensional, with the first n-dimensions identical to the data - array dimensions, and the last dimension length-3 (RGB) or - length-4 (RGBA). - - :param bgcolors: RGB or RGBA colors array, defining the background color - for each cell in the table. - :param fgcolors: RGB or RGBA colors array, defining the foreground color - (text color) for each cell in the table. - """ - # array must be RGB or RGBA - valid_shapes = (self._array.shape + (3,), self._array.shape + (4,)) - errmsg = "Inconsistent shape for color array, should be %s or %s" % valid_shapes - - if bgcolors is not None: - if not _is_array(bgcolors): - bgcolors = numpy.array(bgcolors) - assert bgcolors.shape in valid_shapes, errmsg - - self._bgcolors = bgcolors - - if fgcolors is not None: - if not _is_array(fgcolors): - fgcolors = numpy.array(fgcolors) - assert fgcolors.shape in valid_shapes, errmsg - - self._fgcolors = fgcolors - - def setEditable(self, editable): - """Set flags to make the data editable. - - .. warning:: - - If the data is a reference to a h5py dataset open in read-only - mode, setting *editable=True* will fail and print a warning. - - .. warning:: - - Making the data editable means that the underlying data structure - in this data model will be modified. - If the data is a reference to a public object (open with - ``copy=False``), this could have side effects. If it is a - reference to an HDF5 dataset, this means the file will be - modified. - - :param bool editable: Flag to enable editing data. - :return: True if setting desired flag succeeded, False if it failed. - """ - self._editable = editable - if hasattr(self._array, "file"): - if hasattr(self._array.file, "mode"): - if editable and self._array.file.mode == "r": - _logger.warning( - "Data is a HDF5 dataset open in read-only " + - "mode. Editing must be disabled.") - self._editable = False - return False - return True - - def getData(self, copy=True): - """Return a copy of the data array, or a reference to it - if *copy=False* is passed as parameter. - - In case the shape was modified, to convert 0-D or 1-D data - into 2-D data, the original shape is restored in the returned data. - - :param bool copy: If *True* (default), return a copy of the data. If - *False*, return a reference. - :return: numpy array of data, or reference to original data object - if *copy=False* - """ - data = self._array if not copy else numpy.array(self._array, copy=True) - return data - - def setFrameIndex(self, index): - """Set the active slice index. - - This method is only relevant to arrays with at least 3 dimensions. - - :param index: Index of the active slice in the array. - In the general n-D case, this is a sequence of :math:`n - 2` - indices where the slice intersects the respective orthogonal axes. - :raise IndexError: If any index in the index sequence is out of bound - on its respective axis. - """ - shape = self._array.shape - if len(shape) < 3: - # index is ignored - return - - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - if len(shape) == 3: - len_ = shape[self._perspective[0]] - # accept integers as index in the case of 3-D arrays - if not hasattr(index, "__len__"): - self._index = [index] - else: - self._index = index - if not 0 <= self._index[0] < len_: - raise ValueError("Index must be a positive integer " + - "lower than %d" % len_) - else: - # general n-D case - for i_, idx in enumerate(index): - if not 0 <= idx < shape[self._perspective[i_]]: - raise IndexError("Invalid index %d " % idx + - "not in range 0-%d" % (shape[i_] - 1)) - self._index = index - - if qt.qVersion() > "4.6": - self.endResetModel() - - def setFormatter(self, formatter): - """Set the formatter object to be used to display data from the model - - :param TextFormatter formatter: Formatter to use - """ - if formatter is self._formatter: - return - - if qt.qVersion() > "4.6": - self.beginResetModel() - - if self._formatter is not None: - self._formatter.formatChanged.disconnect(self.__formatChanged) - - self._formatter = formatter - if self._formatter is not None: - self._formatter.formatChanged.connect(self.__formatChanged) - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def getFormatter(self): - """Returns the text formatter used. - - :rtype: TextFormatter - """ - return self._formatter - - def __formatChanged(self): - """Called when the format changed. - """ - self.reset() - - def setPerspective(self, perspective): - """Set the perspective by defining a sequence listing all axes - orthogonal to the frame or 2-D slice to be visualized. - - Alternatively, you can use :meth:`setFrameAxes` for the complementary - approach of specifying the two axes parallel to the frame. - - In the 1-D or 2-D case, this parameter is irrelevant. - - In the 3-D case, if the unit vectors describing - your axes are :math:`\vec{x}, \vec{y}, \vec{z}`, a perspective of 0 - means you slices are parallel to :math:`\vec{y}\vec{z}`, 1 means they - are parallel to :math:`\vec{x}\vec{z}` and 2 means they - are parallel to :math:`\vec{x}\vec{y}`. - - In the n-D case, this parameter is a sequence of :math:`n-2` axes - numbers. - For instance if you want to display 2-D frames whose axes are the - second and third dimensions of a 5-D array, set the perspective to - ``(0, 3, 4)``. - - :param perspective: Sequence of dimensions/axes orthogonal to the - frames. - :raise: IndexError if any value in perspective is higher than the - number of dimensions minus one (first dimension is 0), or - if the number of values is different from the number of dimensions - minus two. - """ - n_dimensions = len(self._array.shape) - if n_dimensions < 3: - _logger.warning( - "perspective is not relevant for 1D and 2D arrays") - return - - if not hasattr(perspective, "__len__"): - # we can tolerate an integer for 3-D array - if n_dimensions == 3: - perspective = [perspective] - else: - raise ValueError("perspective must be a sequence of integers") - - # ensure unicity of dimensions in perspective - perspective = tuple(set(perspective)) - - if len(perspective) != n_dimensions - 2 or\ - min(perspective) < 0 or max(perspective) >= n_dimensions: - raise IndexError( - "Invalid perspective " + str(perspective) + - " for %d-D array " % n_dimensions + - "with shape " + str(self._array.shape)) - - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - self._perspective = perspective - - # reset index - self._index = [0 for _i in range(n_dimensions - 2)] - - if qt.qVersion() > "4.6": - self.endResetModel() - - def setFrameAxes(self, row_axis, col_axis): - """Set the perspective by specifying the two axes parallel to the frame - to be visualised. - - The complementary approach of defining the orthogonal axes can be used - with :meth:`setPerspective`. - - :param int row_axis: Index (0-based) of the first dimension used as a frame - axis - :param int col_axis: Index (0-based) of the 2nd dimension used as a frame - axis - :raise: IndexError if axes are invalid - """ - if row_axis > col_axis: - _logger.warning("The dimension of the row axis must be lower " + - "than the dimension of the column axis. Swapping.") - row_axis, col_axis = min(row_axis, col_axis), max(row_axis, col_axis) - - n_dimensions = len(self._array.shape) - if n_dimensions < 3: - _logger.warning( - "Frame axes cannot be changed for 1D and 2D arrays") - return - - perspective = tuple(set(range(0, n_dimensions)) - {row_axis, col_axis}) - - if len(perspective) != n_dimensions - 2 or\ - min(perspective) < 0 or max(perspective) >= n_dimensions: - raise IndexError( - "Invalid perspective " + str(perspective) + - " for %d-D array " % n_dimensions + - "with shape " + str(self._array.shape)) - - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - self._perspective = perspective - # reset index - self._index = [0 for _i in range(n_dimensions - 2)] - - if qt.qVersion() > "4.6": - self.endResetModel() - - -if __name__ == "__main__": - app = qt.QApplication([]) - w = qt.QTableView() - d = numpy.random.normal(0, 1, (5, 1000, 1000)) - for i in range(5): - d[i, :, :] += i * 10 - m = ArrayTableModel(data=d) - w.setModel(m) - m.setFrameIndex(3) - # m.setArrayData(numpy.ones((100,))) - w.show() - app.exec_() diff --git a/silx/gui/data/ArrayTableWidget.py b/silx/gui/data/ArrayTableWidget.py deleted file mode 100644 index cb8e915..0000000 --- a/silx/gui/data/ArrayTableWidget.py +++ /dev/null @@ -1,492 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines a widget designed to display data arrays with any -number of dimensions as 2D frames (images, slices) in a table view. -The dimensions not displayed in the table can be browsed using improved -sliders. - -The widget uses a TableView that relies on a custom abstract item -model: :class:`silx.gui.data.ArrayTableModel`. -""" -from __future__ import division -import sys - -from silx.gui import qt -from silx.gui.widgets.TableWidget import TableView -from .ArrayTableModel import ArrayTableModel -from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser - -__authors__ = ["V.A. Sole", "P. Knobel"] -__license__ = "MIT" -__date__ = "24/01/2017" - - -class AxesSelector(qt.QWidget): - """Widget with two combo-boxes to select two dimensions among - all possible dimensions of an n-dimensional array. - - The first combobox contains values from :math:`0` to :math:`n-2`. - - The choices in the 2nd CB depend on the value selected in the first one. - If the value selected in the first CB is :math:`m`, the second one lets you - select values from :math:`m+1` to :math:`n-1`. - - The two axes can be used to select the row axis and the column axis t - display a slice of the array data in a table view. - """ - sigDimensionsChanged = qt.Signal(int, int) - """Signal emitted whenever one of the comboboxes is changed. - The signal carries the two selected dimensions.""" - - def __init__(self, parent=None, n=None): - qt.QWidget.__init__(self, parent) - self.layout = qt.QHBoxLayout(self) - self.layout.setContentsMargins(0, 2, 0, 2) - self.layout.setSpacing(10) - - self.rowsCB = qt.QComboBox(self) - self.columnsCB = qt.QComboBox(self) - - self.layout.addWidget(qt.QLabel("Rows dimension", self)) - self.layout.addWidget(self.rowsCB) - self.layout.addWidget(qt.QLabel(" ", self)) - self.layout.addWidget(qt.QLabel("Columns dimension", self)) - self.layout.addWidget(self.columnsCB) - self.layout.addStretch(1) - - self._slotsAreConnected = False - if n is not None: - self.setNDimensions(n) - - def setNDimensions(self, n): - """Initialize combo-boxes depending on number of dimensions of array. - Initially, the rows dimension is the second-to-last one, and the - columns dimension is the last one. - - Link the CBs together. MAke them emit a signal when their value is - changed. - - :param int n: Number of dimensions of array - """ - # remember the number of dimensions and the rows dimension - self.n = n - self._rowsDim = n - 2 - - # ensure slots are disconnected before (re)initializing widget - if self._slotsAreConnected: - self.rowsCB.currentIndexChanged.disconnect(self._rowDimChanged) - self.columnsCB.currentIndexChanged.disconnect(self._colDimChanged) - - self._clear() - self.rowsCB.addItems([str(i) for i in range(n - 1)]) - self.rowsCB.setCurrentIndex(n - 2) - if n >= 1: - self.columnsCB.addItem(str(n - 1)) - self.columnsCB.setCurrentIndex(0) - - # reconnect slots - self.rowsCB.currentIndexChanged.connect(self._rowDimChanged) - self.columnsCB.currentIndexChanged.connect(self._colDimChanged) - self._slotsAreConnected = True - - # emit new dimensions - if n > 2: - self.sigDimensionsChanged.emit(n - 2, n - 1) - - def setDimensions(self, row_dim, col_dim): - """Set the rows and columns dimensions. - - The rows dimension must be lower than the columns dimension. - - :param int row_dim: Rows dimension - :param int col_dim: Columns dimension - """ - if row_dim >= col_dim: - raise IndexError("Row dimension must be lower than column dimension") - if not (0 <= row_dim < self.n - 1): - raise IndexError("Row dimension must be between 0 and %d" % (self.n - 2)) - if not (row_dim < col_dim <= self.n - 1): - raise IndexError("Col dimension must be between %d and %d" % (row_dim + 1, self.n - 1)) - - # set the rows dimension; this triggers an update of columnsCB - self.rowsCB.setCurrentIndex(row_dim) - # columnsCB first item is "row_dim + 1". So index of "col_dim" is - # col_dim - (row_dim + 1) - self.columnsCB.setCurrentIndex(col_dim - row_dim - 1) - - def getDimensions(self): - """Return a 2-tuple of the rows dimension and the columns dimension. - - :return: 2-tuple of axes numbers (row_dimension, col_dimension) - """ - return self._getRowDim(), self._getColDim() - - def _clear(self): - """Empty the combo-boxes""" - self.rowsCB.clear() - self.columnsCB.clear() - - def _getRowDim(self): - """Get rows dimension, selected in :attr:`rowsCB` - """ - # rows combobox contains elements "0", ..."n-2", - # so the selected dim is always equal to the index - return self.rowsCB.currentIndex() - - def _getColDim(self): - """Get columns dimension, selected in :attr:`columnsCB`""" - # columns combobox contains elements "row_dim+1", "row_dim+2", ..., "n-1" - # so the selected dim is equal to row_dim + 1 + index - return self._rowsDim + 1 + self.columnsCB.currentIndex() - - def _rowDimChanged(self): - """Update columns combobox when the rows dimension is changed. - - Emit :attr:`sigDimensionsChanged`""" - old_col_dim = self._getColDim() - new_row_dim = self._getRowDim() - - # clear cols CB - self.columnsCB.currentIndexChanged.disconnect(self._colDimChanged) - self.columnsCB.clear() - # refill cols CB - for i in range(new_row_dim + 1, self.n): - self.columnsCB.addItem(str(i)) - - # keep previous col dimension if possible - new_col_cb_idx = old_col_dim - (new_row_dim + 1) - if new_col_cb_idx < 0: - # if row_dim is now greater than the previous col_dim, - # we select a new col_dim = row_dim + 1 (first element in cols CB) - new_col_cb_idx = 0 - self.columnsCB.setCurrentIndex(new_col_cb_idx) - - # reconnect slot - self.columnsCB.currentIndexChanged.connect(self._colDimChanged) - - self._rowsDim = new_row_dim - - self.sigDimensionsChanged.emit(self._getRowDim(), self._getColDim()) - - def _colDimChanged(self): - """Emit :attr:`sigDimensionsChanged`""" - self.sigDimensionsChanged.emit(self._getRowDim(), self._getColDim()) - - -def _get_shape(array_like): - """Return shape of an array like object. - - In case the object is a nested sequence (list of lists, tuples...), - the size of each dimension is assumed to be uniform, and is deduced from - the length of the first sequence. - - :param array_like: Array like object:Â numpy array, hdf5 dataset, - multi-dimensional sequence - :return: Shape of array, as a tuple of integers - """ - if hasattr(array_like, "shape"): - return array_like.shape - - shape = [] - subsequence = array_like - while hasattr(subsequence, "__len__"): - shape.append(len(subsequence)) - subsequence = subsequence[0] - - return tuple(shape) - - -class ArrayTableWidget(qt.QWidget): - """This widget is designed to display data of 2D frames (images, slices) - in a table view. The widget can load any n-dimensional array, and display - any 2-D frame/slice in the array. - - The index of the dimensions orthogonal to the displayed frame can be set - interactively using a browser widget (sliders, buttons and text entries). - - To set the data, use :meth:`setArrayData`. - To select the perspective, use :meth:`setPerspective` or - use :meth:`setFrameAxes`. - To select the frame, use :meth:`setFrameIndex`. - - .. image:: img/ArrayTableWidget.png - """ - def __init__(self, parent=None): - """ - - :param parent: parent QWidget - :param labels: list of labels for each dimension of the array - """ - qt.QWidget.__init__(self, parent) - self.mainLayout = qt.QVBoxLayout(self) - self.mainLayout.setContentsMargins(0, 0, 0, 0) - self.mainLayout.setSpacing(0) - - self.browserContainer = qt.QWidget(self) - self.browserLayout = qt.QGridLayout(self.browserContainer) - self.browserLayout.setContentsMargins(0, 0, 0, 0) - self.browserLayout.setSpacing(0) - - self._dimensionLabelsText = [] - """List of text labels sorted in the increasing order of the dimension - they apply to.""" - self._browserLabels = [] - """List of QLabel widgets.""" - self._browserWidgets = [] - """List of HorizontalSliderWithBrowser widgets.""" - - self.axesSelector = AxesSelector(self) - - self.view = TableView(self) - - self.mainLayout.addWidget(self.browserContainer) - self.mainLayout.addWidget(self.axesSelector) - self.mainLayout.addWidget(self.view) - - self.model = ArrayTableModel(self) - self.view.setModel(self.model) - - def setArrayData(self, data, labels=None, copy=True, editable=False): - """Set the data array. Update frame browsers and labels. - - :param data: Numpy array or similar object (e.g. nested sequence, - h5py dataset...) - :param labels: list of labels for each dimension of the array, or - boolean ``True`` to use default labels ("dimension 0", - "dimension 1", ...). `None` to disable labels (default). - :param bool copy: If *True*, store a copy of *data* in the model. If - *False*, store a reference to *data* if possible (only possible if - *data* is a proper numpy array or an object that implements the - same methods). - :param bool editable: Flag to enable editing data. Default is *False* - """ - self._data_shape = _get_shape(data) - - n_widgets = len(self._browserWidgets) - n_dimensions = len(self._data_shape) - - # Reset text of labels - self._dimensionLabelsText = [] - for i in range(n_dimensions): - if labels in [True, 1]: - label_text = "Dimension %d" % i - elif labels is None or i >= len(labels): - label_text = "" - else: - label_text = labels[i] - self._dimensionLabelsText.append(label_text) - - # not enough widgets, create new ones (we need n_dim - 2) - for i in range(n_widgets, n_dimensions - 2): - browser = HorizontalSliderWithBrowser(self.browserContainer) - self.browserLayout.addWidget(browser, i, 1) - self._browserWidgets.append(browser) - browser.valueChanged.connect(self._browserSlot) - browser.setEnabled(False) - browser.hide() - - label = qt.QLabel(self.browserContainer) - self._browserLabels.append(label) - self.browserLayout.addWidget(label, i, 0) - label.hide() - - n_widgets = len(self._browserWidgets) - for i in range(n_widgets): - label = self._browserLabels[i] - browser = self._browserWidgets[i] - - if (i + 2) < n_dimensions: - label.setText(self._dimensionLabelsText[i]) - browser.setRange(0, self._data_shape[i] - 1) - browser.setEnabled(True) - browser.show() - if labels is not None: - label.show() - else: - label.hide() - else: - browser.setEnabled(False) - browser.hide() - label.hide() - - # set model - self.model.setArrayData(data, copy=copy, editable=editable) - # some linux distributions need this call - self.view.setModel(self.model) - if editable: - self.view.enableCut() - self.view.enablePaste() - - # initialize & connect axesSelector - self.axesSelector.setNDimensions(n_dimensions) - self.axesSelector.sigDimensionsChanged.connect(self.setFrameAxes) - - def setArrayColors(self, bgcolors=None, fgcolors=None): - """Set the colors for all table cells by passing an array - of RGB or RGBA values (integers between 0 and 255). - - The shape of the colors array must be consistent with the data shape. - - If the data array is n-dimensional, the colors array must be - (n+1)-dimensional, with the first n-dimensions identical to the data - array dimensions, and the last dimension length-3 (RGB) or - length-4 (RGBA). - - :param bgcolors: RGB or RGBA colors array, defining the background color - for each cell in the table. - :param fgcolors: RGB or RGBA colors array, defining the foreground color - (text color) for each cell in the table. - """ - self.model.setArrayColors(bgcolors, fgcolors) - - def displayAxesSelector(self, isVisible): - """Allow to display or hide the axes selector. - - :param bool isVisible: True to display the axes selector. - """ - self.axesSelector.setVisible(isVisible) - - def setFrameIndex(self, index): - """Set the active slice/image index in the n-dimensional array. - - A frame is a 2D array extracted from an array. This frame is - necessarily parallel to 2 axes, and orthogonal to all other axes. - - The index of a frame is a sequence of indices along the orthogonal - axes, where the frame intersects the respective axis. The indices - are listed in the same order as the corresponding dimensions of the - data array. - - For example, it the data array has 5 dimensions, and we are - considering frames whose parallel axes are the 2nd and 4th dimensions - of the array, the frame index will be a sequence of length 3 - corresponding to the indices where the frame intersects the 1st, 3rd - and 5th axes. - - :param index: Sequence of indices defining the active data slice in - a n-dimensional array. The sequence length is :math:`n-2` - :raise: IndexError if any index in the index sequence is out of bound - on its respective axis. - """ - self.model.setFrameIndex(index) - - def _resetBrowsers(self, perspective): - """Adjust limits for browsers based on the perspective and the - size of the corresponding dimensions. Reset the index to 0. - Update the dimension in the labels. - - :param perspective: Sequence of axes/dimensions numbers (0-based) - defining the axes orthogonal to the frame. - """ - # for 3D arrays we can accept an int rather than a 1-tuple - if not hasattr(perspective, "__len__"): - perspective = [perspective] - - # perspective must be sorted - perspective = sorted(perspective) - - n_dimensions = len(self._data_shape) - for i in range(n_dimensions - 2): - browser = self._browserWidgets[i] - label = self._browserLabels[i] - browser.setRange(0, self._data_shape[perspective[i]] - 1) - browser.setValue(0) - label.setText(self._dimensionLabelsText[perspective[i]]) - - def setPerspective(self, perspective): - """Set the *perspective* by specifying which axes are orthogonal - to the frame. - - For the opposite approach (defining parallel axes), use - :meth:`setFrameAxes` instead. - - :param perspective: Sequence of unique axes numbers (0-based) defining - the orthogonal axes. For a n-dimensional array, the sequence - length is :math:`n-2`. The order is of the sequence is not taken - into account (the dimensions are displayed in increasing order - in the widget). - """ - self.model.setPerspective(perspective) - self._resetBrowsers(perspective) - - def setFrameAxes(self, row_axis, col_axis): - """Set the *perspective* by specifying which axes are parallel - to the frame. - - For the opposite approach (defining orthogonal axes), use - :meth:`setPerspective` instead. - - :param int row_axis: Index (0-based) of the first dimension used as a frame - axis - :param int col_axis: Index (0-based) of the 2nd dimension used as a frame - axis - """ - self.model.setFrameAxes(row_axis, col_axis) - n_dimensions = len(self._data_shape) - perspective = tuple(set(range(0, n_dimensions)) - {row_axis, col_axis}) - self._resetBrowsers(perspective) - - def _browserSlot(self, value): - index = [] - for browser in self._browserWidgets: - if browser.isEnabled(): - index.append(browser.value()) - self.setFrameIndex(index) - self.view.reset() - - def getData(self, copy=True): - """Return a copy of the data array, or a reference to it if - *copy=False* is passed as parameter. - - :param bool copy: If *True* (default), return a copy of the data. If - *False*, return a reference. - :return: Numpy array of data, or reference to original data object - if *copy=False* - """ - return self.model.getData(copy=copy) - - -def main(): - import numpy - a = qt.QApplication([]) - d = numpy.random.normal(0, 1, (4, 5, 1000, 1000)) - for j in range(4): - for i in range(5): - d[j, i, :, :] += i + 10 * j - w = ArrayTableWidget() - if "2" in sys.argv: - print("sending a single image") - w.setArrayData(d[0, 0]) - elif "3" in sys.argv: - print("sending 5 images") - w.setArrayData(d[0]) - else: - print("sending 4 * 5 images ") - w.setArrayData(d, labels=True) - w.show() - a.exec_() - -if __name__ == "__main__": - main() diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py deleted file mode 100644 index 2e51439..0000000 --- a/silx/gui/data/DataViewer.py +++ /dev/null @@ -1,593 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines a widget designed to display data using the most adapted -view from the ones provided by silx. -""" -from __future__ import division - -import logging -import os.path -import collections -from silx.gui import qt -from silx.gui.data import DataViews -from silx.gui.data.DataViews import _normalizeData -from silx.gui.utils import blockSignals -from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector - - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/02/2019" - - -_logger = logging.getLogger(__name__) - - -DataSelection = collections.namedtuple("DataSelection", - ["filename", "datapath", - "slice", "permutation"]) - - -class DataViewer(qt.QFrame): - """Widget to display any kind of data - - .. image:: img/DataViewer.png - - The method :meth:`setData` allows to set any data to the widget. Mostly - `numpy.array` and `h5py.Dataset` are supported with adapted views. Other - data types are displayed using a text viewer. - - A default view is automatically selected when a data is set. The method - :meth:`setDisplayMode` allows to change the view. To have a graphical tool - to select the view, prefer using the widget :class:`DataViewerFrame`. - - The dimension of the input data and the expected dimension of the selected - view can differ. For example you can display an image (2D) from 4D - data. In this case a :class:`NumpyAxesSelector` is displayed to allow the - user to select the axis mapping and the slicing of other axes. - - .. code-block:: python - - import numpy - data = numpy.random.rand(500,500) - viewer = DataViewer() - viewer.setData(data) - viewer.setVisible(True) - """ - - displayedViewChanged = qt.Signal(object) - """Emitted when the displayed view changes""" - - dataChanged = qt.Signal() - """Emitted when the data changes""" - - currentAvailableViewsChanged = qt.Signal() - """Emitted when the current available views (which support the current - data) change""" - - def __init__(self, parent=None): - """Constructor - - :param QWidget parent: The parent of the widget - """ - super(DataViewer, self).__init__(parent) - - self.__stack = qt.QStackedWidget(self) - self.__numpySelection = NumpyAxesSelector(self) - self.__numpySelection.selectedAxisChanged.connect(self.__numpyAxisChanged) - self.__numpySelection.selectionChanged.connect(self.__numpySelectionChanged) - self.__numpySelection.customAxisChanged.connect(self.__numpyCustomAxisChanged) - - self.setLayout(qt.QVBoxLayout(self)) - self.layout().addWidget(self.__stack, 1) - - group = qt.QGroupBox(self) - group.setLayout(qt.QVBoxLayout()) - group.layout().addWidget(self.__numpySelection) - group.setTitle("Axis selection") - self.__axisSelection = group - - self.layout().addWidget(self.__axisSelection) - - self.__currentAvailableViews = [] - self.__currentView = None - self.__data = None - self.__info = None - self.__useAxisSelection = False - self.__userSelectedView = None - self.__hooks = None - - self.__views = [] - self.__index = {} - """store stack index for each views""" - - self._initializeViews() - - def _initializeViews(self): - """Inisialize the available views""" - views = self.createDefaultViews(self.__stack) - self.__views = list(views) - self.setDisplayMode(DataViews.EMPTY_MODE) - - def setGlobalHooks(self, hooks): - """Set a data view hooks for all the views - - :param DataViewHooks context: The hooks to use - """ - self.__hooks = hooks - for v in self.__views: - v.setHooks(hooks) - - def createDefaultViews(self, parent=None): - """Create and returns available views which can be displayed by default - by the data viewer. It is called internally by the widget. It can be - overwriten to provide a different set of viewers. - - :param QWidget parent: QWidget parent of the views - :rtype: List[silx.gui.data.DataViews.DataView] - """ - viewClasses = [ - DataViews._EmptyView, - DataViews._Hdf5View, - DataViews._NXdataView, - DataViews._Plot1dView, - DataViews._ImageView, - DataViews._Plot3dView, - DataViews._RawView, - DataViews._StackView, - DataViews._Plot2dRecordView, - ] - views = [] - for viewClass in viewClasses: - try: - view = viewClass(parent) - views.append(view) - except Exception: - _logger.warning("%s instantiation failed. View is ignored" % viewClass.__name__) - _logger.debug("Backtrace", exc_info=True) - - return views - - def clear(self): - """Clear the widget""" - self.setData(None) - - def normalizeData(self, data): - """Returns a normalized data if the embed a numpy or a dataset. - Else returns the data.""" - return _normalizeData(data) - - def __getStackIndex(self, view): - """Get the stack index containing the view. - - :param silx.gui.data.DataViews.DataView view: The view - """ - if view not in self.__index: - widget = view.getWidget() - index = self.__stack.addWidget(widget) - self.__index[view] = index - else: - index = self.__index[view] - return index - - def __clearCurrentView(self): - """Clear the current selected view""" - view = self.__currentView - if view is not None: - view.clear() - - def __numpyCustomAxisChanged(self, name, value): - view = self.__currentView - if view is not None: - view.setCustomAxisValue(name, value) - - def __updateNumpySelectionAxis(self): - """ - Update the numpy-selector according to the needed axis names - """ - with blockSignals(self.__numpySelection): - previousPermutation = self.__numpySelection.permutation() - previousSelection = self.__numpySelection.selection() - - self.__numpySelection.clear() - - info = self._getInfo() - axisNames = self.__currentView.axesNames(self.__data, info) - if (info.isArray and info.size != 0 and - self.__data is not None and axisNames is not None): - self.__useAxisSelection = True - self.__numpySelection.setAxisNames(axisNames) - self.__numpySelection.setCustomAxis( - self.__currentView.customAxisNames()) - data = self.normalizeData(self.__data) - self.__numpySelection.setData(data) - - # Try to restore previous permutation and selection - try: - self.__numpySelection.setSelection( - previousSelection, previousPermutation) - except ValueError as e: - _logger.info("Not restoring selection because: %s", e) - - if hasattr(data, "shape"): - isVisible = not (len(axisNames) == 1 and len(data.shape) == 1) - else: - isVisible = True - self.__axisSelection.setVisible(isVisible) - else: - self.__useAxisSelection = False - self.__axisSelection.setVisible(False) - - def __updateDataInView(self): - """ - Update the views using the current data - """ - if self.__useAxisSelection: - self.__displayedData = self.__numpySelection.selectedData() - - permutation = self.__numpySelection.permutation() - normal = tuple(range(len(permutation))) - if permutation == normal: - permutation = None - slicing = self.__numpySelection.selection() - normal = tuple([slice(None)] * len(slicing)) - if slicing == normal: - slicing = None - else: - self.__displayedData = self.__data - permutation = None - slicing = None - - try: - filename = os.path.abspath(self.__data.file.filename) - except: - filename = None - - try: - datapath = self.__data.name - except: - datapath = None - - #Â FIXME: maybe use DataUrl, with added support of permutation - self.__displayedSelection = DataSelection(filename, datapath, slicing, permutation) - - # TODO: would be good to avoid that, it should be synchonous - qt.QTimer.singleShot(10, self.__setDataInView) - - def __setDataInView(self): - self.__currentView.setData(self.__displayedData) - self.__currentView.setDataSelection(self.__displayedSelection) - - def setDisplayedView(self, view): - """Set the displayed view. - - Change the displayed view according to the view itself. - - :param silx.gui.data.DataViews.DataView view: The DataView to use to display the data - """ - self.__userSelectedView = view - self._setDisplayedView(view) - - def _setDisplayedView(self, view): - """Internal set of the displayed view. - - Change the displayed view according to the view itself. - - :param silx.gui.data.DataViews.DataView view: The DataView to use to display the data - """ - if self.__currentView is view: - return - self.__clearCurrentView() - self.__currentView = view - self.__updateNumpySelectionAxis() - self.__updateDataInView() - stackIndex = self.__getStackIndex(self.__currentView) - if self.__currentView is not None: - self.__currentView.select() - self.__stack.setCurrentIndex(stackIndex) - self.displayedViewChanged.emit(view) - - def getViewFromModeId(self, modeId): - """Returns the first available view which have the requested modeId. - Return None if modeId does not correspond to an existing view. - - :param int modeId: Requested mode id - :rtype: silx.gui.data.DataViews.DataView - """ - for view in self.__views: - if view.modeId() == modeId: - return view - return None - - def setDisplayMode(self, modeId): - """Set the displayed view using display mode. - - Change the displayed view according to the requested mode. - - :param int modeId: Display mode, one of - - - `DataViews.EMPTY_MODE`: display nothing - - `DataViews.PLOT1D_MODE`: display the data as a curve - - `DataViews.IMAGE_MODE`: display the data as an image - - `DataViews.PLOT3D_MODE`: display the data as an isosurface - - `DataViews.RAW_MODE`: display the data as a table - - `DataViews.STACK_MODE`: display the data as a stack of images - - `DataViews.HDF5_MODE`: display the data as a table of HDF5 info - - `DataViews.NXDATA_MODE`: display the data as NXdata - """ - try: - view = self.getViewFromModeId(modeId) - except KeyError: - raise ValueError("Display mode %s is unknown" % modeId) - self._setDisplayedView(view) - - def displayedView(self): - """Returns the current displayed view. - - :rtype: silx.gui.data.DataViews.DataView - """ - return self.__currentView - - def addView(self, view): - """Allow to add a view to the dataview. - - If the current data support this view, it will be displayed. - - :param DataView view: A dataview - """ - if self.__hooks is not None: - view.setHooks(self.__hooks) - self.__views.append(view) - # TODO It can be skipped if the view do not support the data - self.__updateAvailableViews() - - def removeView(self, view): - """Allow to remove a view which was available from the dataview. - - If the view was displayed, the widget will be updated. - - :param DataView view: A dataview - """ - self.__views.remove(view) - self.__stack.removeWidget(view.getWidget()) - # invalidate the full index. It will be updated as expected - self.__index = {} - - if self.__userSelectedView is view: - self.__userSelectedView = None - - if view is self.__currentView: - self.__updateView() - else: - # TODO It can be skipped if the view is not part of the - # available views - self.__updateAvailableViews() - - def __updateAvailableViews(self): - """ - Update available views from the current data. - """ - data = self.__data - info = self._getInfo() - # sort available views according to priority - views = [] - for v in self.__views: - views.extend(v.getMatchingViews(data, info)) - views = [(v.getCachedDataPriority(data, info), v) for v in views] - views = filter(lambda t: t[0] > DataViews.DataView.UNSUPPORTED, views) - views = sorted(views, reverse=True) - views = [v[1] for v in views] - - # store available views - self.__setCurrentAvailableViews(views) - - def __updateView(self): - """Display the data using the widget which fit the best""" - data = self.__data - - # update available views for this data - self.__updateAvailableViews() - available = self.__currentAvailableViews - - # display the view with the most priority (the default view) - view = self.getDefaultViewFromAvailableViews(data, available) - self.__clearCurrentView() - try: - self._setDisplayedView(view) - except Exception as e: - # in case there is a problem to read the data, try to use a safe - # view - view = self.getSafeViewFromAvailableViews(data, available) - self._setDisplayedView(view) - raise e - - def getSafeViewFromAvailableViews(self, data, available): - """Returns a view which is sure to display something without failing - on rendering. - - :param object data: data which will be displayed - :param List[view] available: List of available views, from highest - priority to lowest. - :rtype: DataView - """ - hdf5View = self.getViewFromModeId(DataViews.HDF5_MODE) - if hdf5View in available: - return hdf5View - return self.getViewFromModeId(DataViews.EMPTY_MODE) - - def getDefaultViewFromAvailableViews(self, data, available): - """Returns the default view which will be used according to available - views. - - :param object data: data which will be displayed - :param List[view] available: List of available views, from highest - priority to lowest. - :rtype: DataView - """ - if len(available) > 0: - # returns the view with the highest priority - if self.__userSelectedView in available: - return self.__userSelectedView - self.__userSelectedView = None - view = available[0] - else: - # else returns the empty view - view = self.getViewFromModeId(DataViews.EMPTY_MODE) - return view - - def __setCurrentAvailableViews(self, availableViews): - """Set the current available viewa - - :param List[DataView] availableViews: Current available viewa - """ - self.__currentAvailableViews = availableViews - self.currentAvailableViewsChanged.emit() - - def currentAvailableViews(self): - """Returns the list of available views for the current data - - :rtype: List[DataView] - """ - return self.__currentAvailableViews - - def getReachableViews(self): - """Returns the list of reachable views from the registred available - views. - - :rtype: List[DataView] - """ - views = [] - for v in self.availableViews(): - views.extend(v.getReachableViews()) - return views - - def availableViews(self): - """Returns the list of registered views - - :rtype: List[DataView] - """ - return self.__views - - def setData(self, data): - """Set the data to view. - - It mostly can be a h5py.Dataset or a numpy.ndarray. Other kind of - objects will be displayed as text rendering. - - :param numpy.ndarray data: The data. - """ - self.__data = data - self._invalidateInfo() - self.__displayedData = None - self.__displayedSelection = None - self.__updateView() - self.__updateNumpySelectionAxis() - self.__updateDataInView() - self.dataChanged.emit() - - def __numpyAxisChanged(self): - """ - Called when axis selection of the numpy-selector changed - """ - self.__clearCurrentView() - - def __numpySelectionChanged(self): - """ - Called when data selection of the numpy-selector changed - """ - self.__updateDataInView() - - def data(self): - """Returns the data""" - return self.__data - - def _invalidateInfo(self): - """Invalidate DataInfo cache.""" - self.__info = None - - def _getInfo(self): - """Returns the DataInfo of the current selected data. - - This value is cached. - - :rtype: DataInfo - """ - if self.__info is None: - self.__info = DataViews.DataInfo(self.__data) - return self.__info - - def displayMode(self): - """Returns the current display mode""" - return self.__currentView.modeId() - - def replaceView(self, modeId, newView): - """Replace one of the builtin data views with a custom view. - Return True in case of success, False in case of failure. - - .. note:: - - This method must be called just after instantiation, before - the viewer is used. - - :param int modeId: Unique mode ID identifying the DataView to - be replaced. One of: - - - `DataViews.EMPTY_MODE` - - `DataViews.PLOT1D_MODE` - - `DataViews.IMAGE_MODE` - - `DataViews.PLOT2D_MODE` - - `DataViews.COMPLEX_IMAGE_MODE` - - `DataViews.PLOT3D_MODE` - - `DataViews.RAW_MODE` - - `DataViews.STACK_MODE` - - `DataViews.HDF5_MODE` - - `DataViews.NXDATA_MODE` - - `DataViews.NXDATA_INVALID_MODE` - - `DataViews.NXDATA_SCALAR_MODE` - - `DataViews.NXDATA_CURVE_MODE` - - `DataViews.NXDATA_XYVSCATTER_MODE` - - `DataViews.NXDATA_IMAGE_MODE` - - `DataViews.NXDATA_STACK_MODE` - - :param DataViews.DataView newView: New data view - :return: True if replacement was successful, else False - """ - assert isinstance(newView, DataViews.DataView) - isReplaced = False - for idx, view in enumerate(self.__views): - if view.modeId() == modeId: - if self.__hooks is not None: - newView.setHooks(self.__hooks) - self.__views[idx] = newView - isReplaced = True - break - elif isinstance(view, DataViews.CompositeDataView): - isReplaced = view.replaceView(modeId, newView) - if isReplaced: - break - - if isReplaced: - self.__updateAvailableViews() - return isReplaced diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py deleted file mode 100644 index 9bfb95b..0000000 --- a/silx/gui/data/DataViewerFrame.py +++ /dev/null @@ -1,217 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module contains a DataViewer with a view selector. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/02/2019" - -from silx.gui import qt -from .DataViewer import DataViewer -from .DataViewerSelector import DataViewerSelector - - -class DataViewerFrame(qt.QWidget): - """ - A :class:`DataViewer` with a view selector. - - .. image:: img/DataViewerFrame.png - - This widget provides the same API as :class:`DataViewer`. Therefore, for more - documentation, take a look at the documentation of the class - :class:`DataViewer`. - - .. code-block:: python - - import numpy - data = numpy.random.rand(500,500) - viewer = DataViewerFrame() - viewer.setData(data) - viewer.setVisible(True) - - """ - - displayedViewChanged = qt.Signal(object) - """Emitted when the displayed view changes""" - - dataChanged = qt.Signal() - """Emitted when the data changes""" - - def __init__(self, parent=None): - """ - Constructor - - :param qt.QWidget parent: - """ - super(DataViewerFrame, self).__init__(parent) - - class _DataViewer(DataViewer): - """Overwrite methods to avoid to create views while the instance - is not created. `initializeViews` have to be called manually.""" - - def _initializeViews(self): - pass - - def initializeViews(self): - """Avoid to create views while the instance is not created.""" - super(_DataViewer, self)._initializeViews() - - def _createDefaultViews(self, parent): - """Expose the original `createDefaultViews` function""" - return super(_DataViewer, self).createDefaultViews() - - def createDefaultViews(self, parent=None): - """Allow the DataViewerFrame to override this function""" - return self.parent().createDefaultViews(parent) - - self.__dataViewer = _DataViewer(self) - # initialize views when `self.__dataViewer` is set - self.__dataViewer.initializeViews() - self.__dataViewer.setFrameShape(qt.QFrame.StyledPanel) - self.__dataViewer.setFrameShadow(qt.QFrame.Sunken) - self.__dataViewerSelector = DataViewerSelector(self, self.__dataViewer) - self.__dataViewerSelector.setFlat(True) - - layout = qt.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(self.__dataViewer, 1) - layout.addWidget(self.__dataViewerSelector) - self.setLayout(layout) - - self.__dataViewer.dataChanged.connect(self.__dataChanged) - self.__dataViewer.displayedViewChanged.connect(self.__displayedViewChanged) - - def __dataChanged(self): - """Called when the data is changed""" - self.dataChanged.emit() - - def __displayedViewChanged(self, view): - """Called when the displayed view changes""" - self.displayedViewChanged.emit(view) - - def setGlobalHooks(self, hooks): - """Set a data view hooks for all the views - - :param DataViewHooks context: The hooks to use - """ - self.__dataViewer.setGlobalHooks(hooks) - - def getReachableViews(self): - return self.__dataViewer.getReachableViews() - - def availableViews(self): - """Returns the list of registered views - - :rtype: List[DataView] - """ - return self.__dataViewer.availableViews() - - def currentAvailableViews(self): - """Returns the list of available views for the current data - - :rtype: List[DataView] - """ - return self.__dataViewer.currentAvailableViews() - - def createDefaultViews(self, parent=None): - """Create and returns available views which can be displayed by default - by the data viewer. It is called internally by the widget. It can be - overwriten to provide a different set of viewers. - - :param QWidget parent: QWidget parent of the views - :rtype: List[silx.gui.data.DataViews.DataView] - """ - return self.__dataViewer._createDefaultViews(parent) - - def addView(self, view): - """Allow to add a view to the dataview. - - If the current data support this view, it will be displayed. - - :param DataView view: A dataview - """ - return self.__dataViewer.addView(view) - - def removeView(self, view): - """Allow to remove a view which was available from the dataview. - - If the view was displayed, the widget will be updated. - - :param DataView view: A dataview - """ - return self.__dataViewer.removeView(view) - - def setData(self, data): - """Set the data to view. - - It mostly can be a h5py.Dataset or a numpy.ndarray. Other kind of - objects will be displayed as text rendering. - - :param numpy.ndarray data: The data. - """ - self.__dataViewer.setData(data) - - def data(self): - """Returns the data""" - return self.__dataViewer.data() - - def setDisplayedView(self, view): - self.__dataViewer.setDisplayedView(view) - - def displayedView(self): - return self.__dataViewer.displayedView() - - def displayMode(self): - return self.__dataViewer.displayMode() - - def setDisplayMode(self, modeId): - """Set the displayed view using display mode. - - Change the displayed view according to the requested mode. - - :param int modeId: Display mode, one of - - - `EMPTY_MODE`: display nothing - - `PLOT1D_MODE`: display the data as a curve - - `PLOT2D_MODE`: display the data as an image - - `TEXT_MODE`: display the data as a text - - `ARRAY_MODE`: display the data as a table - """ - return self.__dataViewer.setDisplayMode(modeId) - - def getViewFromModeId(self, modeId): - """See :meth:`DataViewer.getViewFromModeId`""" - return self.__dataViewer.getViewFromModeId(modeId) - - def replaceView(self, modeId, newView): - """Replace one of the builtin data views with a custom view. - See :meth:`DataViewer.replaceView` for more documentation. - - :param DataViews.DataView newView: New data view - :return: True if replacement was successful, else False - """ - return self.__dataViewer.replaceView(modeId, newView) diff --git a/silx/gui/data/DataViewerSelector.py b/silx/gui/data/DataViewerSelector.py deleted file mode 100644 index a1e9947..0000000 --- a/silx/gui/data/DataViewerSelector.py +++ /dev/null @@ -1,175 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines a widget to be able to select the available view -of the DataViewer. -""" -from __future__ import division - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/02/2019" - -import weakref -import functools -from silx.gui import qt -import silx.utils.weakref - - -class DataViewerSelector(qt.QWidget): - """Widget to be able to select a custom view from the DataViewer""" - - def __init__(self, parent=None, dataViewer=None): - """Constructor - - :param QWidget parent: The parent of the widget - :param DataViewer dataViewer: The connected `DataViewer` - """ - super(DataViewerSelector, self).__init__(parent) - - self.__group = None - self.__buttons = {} - self.__buttonLayout = None - self.__buttonDummy = None - self.__dataViewer = None - - # Create the fixed layout - self.setLayout(qt.QHBoxLayout()) - layout = self.layout() - layout.setContentsMargins(0, 0, 0, 0) - self.__buttonLayout = qt.QHBoxLayout() - self.__buttonLayout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(self.__buttonLayout) - layout.addStretch(1) - - if dataViewer is not None: - self.setDataViewer(dataViewer) - - def __updateButtons(self): - if self.__group is not None: - self.__group.deleteLater() - - # Clean up - for _, b in self.__buttons.items(): - b.deleteLater() - if self.__buttonDummy is not None: - self.__buttonDummy.deleteLater() - self.__buttonDummy = None - self.__buttons = {} - self.__buttonDummy = None - - self.__group = qt.QButtonGroup(self) - if self.__dataViewer is None: - return - - iconSize = qt.QSize(16, 16) - - for view in self.__dataViewer.getReachableViews(): - label = view.label() - icon = view.icon() - button = qt.QPushButton(label) - button.setIcon(icon) - button.setIconSize(iconSize) - button.setCheckable(True) - # the weak objects are needed to be able to destroy the widget safely - weakView = weakref.ref(view) - weakMethod = silx.utils.weakref.WeakMethodProxy(self.__setDisplayedView) - callback = functools.partial(weakMethod, weakView) - button.clicked.connect(callback) - self.__buttonLayout.addWidget(button) - self.__group.addButton(button) - self.__buttons[view] = button - - button = qt.QPushButton("Dummy") - button.setCheckable(True) - button.setVisible(False) - self.__buttonLayout.addWidget(button) - self.__group.addButton(button) - self.__buttonDummy = button - - self.__updateButtonsVisibility() - self.__displayedViewChanged(self.__dataViewer.displayedView()) - - def setDataViewer(self, dataViewer): - """Define the dataviewer connected to this status bar - - :param DataViewer dataViewer: The connected `DataViewer` - """ - if self.__dataViewer is dataViewer: - return - if self.__dataViewer is not None: - self.__dataViewer.dataChanged.disconnect(self.__updateButtonsVisibility) - self.__dataViewer.displayedViewChanged.disconnect(self.__displayedViewChanged) - self.__dataViewer = dataViewer - if self.__dataViewer is not None: - self.__dataViewer.dataChanged.connect(self.__updateButtonsVisibility) - self.__dataViewer.displayedViewChanged.connect(self.__displayedViewChanged) - self.__updateButtons() - - def setFlat(self, isFlat): - """Set the flat state of all the buttons. - - :param bool isFlat: True to display the buttons flatten. - """ - for b in self.__buttons.values(): - b.setFlat(isFlat) - self.__buttonDummy.setFlat(isFlat) - - def __displayedViewChanged(self, view): - """Called on displayed view changes""" - selectedButton = self.__buttons.get(view, self.__buttonDummy) - selectedButton.setChecked(True) - - def __setDisplayedView(self, refView, clickEvent=None): - """Display a data using the requested view - - :param DataView view: Requested view - :param clickEvent: Event sent by the clicked event - """ - if self.__dataViewer is None: - return - view = refView() - if view is None: - return - self.__dataViewer.setDisplayedView(view) - - def __checkAvailableButtons(self): - views = set(self.__dataViewer.getReachableViews()) - if views == set(self.__buttons.keys()): - return - # Recreate all the buttons - # TODO: We dont have to create everything again - # We expect the views stay quite stable - self.__updateButtons() - - def __updateButtonsVisibility(self): - """Called on data changed""" - if self.__dataViewer is None: - for b in self.__buttons.values(): - b.setVisible(False) - else: - self.__checkAvailableButtons() - availableViews = set(self.__dataViewer.currentAvailableViews()) - for view, button in self.__buttons.items(): - button.setVisible(view in availableViews) diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py deleted file mode 100644 index b18a813..0000000 --- a/silx/gui/data/DataViews.py +++ /dev/null @@ -1,2059 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines a views used by :class:`silx.gui.data.DataViewer`. -""" - -from collections import OrderedDict -import logging -import numbers -import numpy -import os - -import silx.io -from silx.utils import deprecation -from silx.gui import qt, icons -from silx.gui.data.TextFormatter import TextFormatter -from silx.io import nxdata -from silx.gui.hdf5 import H5Node -from silx.io.nxdata import get_attr_as_unicode -from silx.gui.colors import Colormap -from silx.gui.dialog.ColormapDialog import ColormapDialog - -__authors__ = ["V. Valls", "P. Knobel"] -__license__ = "MIT" -__date__ = "19/02/2019" - -_logger = logging.getLogger(__name__) - - -# DataViewer modes -EMPTY_MODE = 0 -PLOT1D_MODE = 10 -RECORD_PLOT_MODE = 15 -IMAGE_MODE = 20 -PLOT2D_MODE = 21 -COMPLEX_IMAGE_MODE = 22 -PLOT3D_MODE = 30 -RAW_MODE = 40 -RAW_ARRAY_MODE = 41 -RAW_RECORD_MODE = 42 -RAW_SCALAR_MODE = 43 -RAW_HEXA_MODE = 44 -STACK_MODE = 50 -HDF5_MODE = 60 -NXDATA_MODE = 70 -NXDATA_INVALID_MODE = 71 -NXDATA_SCALAR_MODE = 72 -NXDATA_CURVE_MODE = 73 -NXDATA_XYVSCATTER_MODE = 74 -NXDATA_IMAGE_MODE = 75 -NXDATA_STACK_MODE = 76 -NXDATA_VOLUME_MODE = 77 -NXDATA_VOLUME_AS_STACK_MODE = 78 - - -def _normalizeData(data): - """Returns a normalized data. - - If the data embed a numpy data or a dataset it is returned. - Else returns the input data.""" - if isinstance(data, H5Node): - if data.is_broken: - return None - return data.h5py_object - return data - - -def _normalizeComplex(data): - """Returns a normalized complex data. - - If the data is a numpy data with complex, returns the - absolute value. - Else returns the input data.""" - if hasattr(data, "dtype"): - isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating) - else: - isComplex = isinstance(data, numbers.Complex) - if isComplex: - data = numpy.absolute(data) - return data - - -class DataInfo(object): - """Store extracted information from a data""" - - def __init__(self, data): - self.__priorities = {} - data = self.normalizeData(data) - self.isArray = False - self.interpretation = None - self.isNumeric = False - self.isVoid = False - self.isComplex = False - self.isBoolean = False - self.isRecord = False - self.hasNXdata = False - self.isInvalidNXdata = False - self.countNumericColumns = 0 - self.shape = tuple() - self.dim = 0 - self.size = 0 - - if data is None: - return - - if silx.io.is_group(data): - nxd = nxdata.get_default(data) - nx_class = get_attr_as_unicode(data, "NX_class") - if nxd is not None: - self.hasNXdata = True - # can we plot it? - is_scalar = nxd.signal_is_0d or nxd.interpretation in ["scalar", "scaler"] - if not (is_scalar or nxd.is_curve or nxd.is_x_y_value_scatter or - nxd.is_image or nxd.is_stack): - # invalid: cannot be plotted by any widget - self.isInvalidNXdata = True - elif nx_class == "NXdata": - # group claiming to be NXdata could not be parsed - self.isInvalidNXdata = True - elif nx_class == "NXroot" or silx.io.is_file(data): - # root claiming to have a default entry - if "default" in data.attrs: - def_entry = data.attrs["default"] - if def_entry in data and "default" in data[def_entry].attrs: - # and entry claims to have default NXdata - self.isInvalidNXdata = True - elif "default" in data.attrs: - # group claiming to have a default NXdata could not be parsed - self.isInvalidNXdata = True - - if isinstance(data, numpy.ndarray): - self.isArray = True - elif silx.io.is_dataset(data) and data.shape != tuple(): - self.isArray = True - else: - self.isArray = False - - if silx.io.is_dataset(data): - if "interpretation" in data.attrs: - self.interpretation = get_attr_as_unicode(data, "interpretation") - else: - self.interpretation = None - elif self.hasNXdata: - self.interpretation = nxd.interpretation - else: - self.interpretation = None - - if hasattr(data, "dtype"): - if numpy.issubdtype(data.dtype, numpy.void): - # That's a real opaque type, else it is a structured type - self.isVoid = data.dtype.fields is None - self.isNumeric = numpy.issubdtype(data.dtype, numpy.number) - self.isRecord = data.dtype.fields is not None - self.isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating) - self.isBoolean = numpy.issubdtype(data.dtype, numpy.bool_) - elif self.hasNXdata: - self.isNumeric = numpy.issubdtype(nxd.signal.dtype, - numpy.number) - self.isComplex = numpy.issubdtype(nxd.signal.dtype, numpy.complexfloating) - self.isBoolean = numpy.issubdtype(nxd.signal.dtype, numpy.bool_) - else: - self.isNumeric = isinstance(data, numbers.Number) - self.isComplex = isinstance(data, numbers.Complex) - self.isBoolean = isinstance(data, bool) - self.isRecord = False - - if hasattr(data, "shape"): - self.shape = data.shape - elif self.hasNXdata: - self.shape = nxd.signal.shape - else: - self.shape = tuple() - if self.shape is not None: - self.dim = len(self.shape) - - if hasattr(data, "shape") and data.shape is None: - # This test is expected to avoid to fall done on the h5py issue - # https://github.com/h5py/h5py/issues/1044 - self.size = 0 - elif hasattr(data, "size"): - self.size = int(data.size) - else: - self.size = 1 - - if hasattr(data, "dtype"): - if data.dtype.fields is not None: - for field in data.dtype.fields: - if numpy.issubdtype(data.dtype[field], numpy.number): - self.countNumericColumns += 1 - - def normalizeData(self, data): - """Returns a normalized data if the embed a numpy or a dataset. - Else returns the data.""" - return _normalizeData(data) - - def cachePriority(self, view, priority): - self.__priorities[view] = priority - - def getPriority(self, view): - return self.__priorities[view] - - -class DataViewHooks(object): - """A set of hooks defined to custom the behaviour of the data views.""" - - def getColormap(self, view): - """Returns a colormap for this view.""" - return None - - def getColormapDialog(self, view): - """Returns a color dialog for this view.""" - return None - - def viewWidgetCreated(self, view, plot): - """Called when the widget of the view was created""" - return - -class DataView(object): - """Holder for the data view.""" - - UNSUPPORTED = -1 - """Priority returned when the requested data can't be displayed by the - view.""" - - TITLE_PATTERN = "{datapath}{slicing} {permuted}" - """Pattern used to format the title of the plot. - - Supported fields: `{directory}`, `{filename}`, `{datapath}`, `{slicing}`, `{permuted}`. - """ - - def __init__(self, parent, modeId=None, icon=None, label=None): - """Constructor - - :param qt.QWidget parent: Parent of the hold widget - """ - self.__parent = parent - self.__widget = None - self.__modeId = modeId - if label is None: - label = self.__class__.__name__ - self.__label = label - if icon is None: - icon = qt.QIcon() - self.__icon = icon - self.__hooks = None - - def getHooks(self): - """Returns the data viewer hooks used by this view. - - :rtype: DataViewHooks - """ - return self.__hooks - - def setHooks(self, hooks): - """Set the data view hooks to use with this view. - - :param DataViewHooks hooks: The data view hooks to use - """ - self.__hooks = hooks - - def defaultColormap(self): - """Returns a default colormap. - - :rtype: Colormap - """ - colormap = None - if self.__hooks is not None: - colormap = self.__hooks.getColormap(self) - if colormap is None: - colormap = Colormap(name="viridis") - return colormap - - def defaultColorDialog(self): - """Returns a default color dialog. - - :rtype: ColormapDialog - """ - dialog = None - if self.__hooks is not None: - dialog = self.__hooks.getColormapDialog(self) - if dialog is None: - dialog = ColormapDialog() - dialog.setModal(False) - return dialog - - def icon(self): - """Returns the default icon""" - return self.__icon - - def label(self): - """Returns the default label""" - return self.__label - - def modeId(self): - """Returns the mode id""" - return self.__modeId - - def normalizeData(self, data): - """Returns a normalized data if the embed a numpy or a dataset. - Else returns the data.""" - return _normalizeData(data) - - def customAxisNames(self): - """Returns names of axes which can be custom by the user and provided - to the view.""" - return [] - - def setCustomAxisValue(self, name, value): - """ - Set the value of a custom axis - - :param str name: Name of the custom axis - :param int value: Value of the custom axis - """ - pass - - def isWidgetInitialized(self): - """Returns true if the widget is already initialized. - """ - return self.__widget is not None - - def select(self): - """Called when the view is selected to display the data. - """ - return - - def getWidget(self): - """Returns the widget hold in the view and displaying the data. - - :returns: qt.QWidget - """ - if self.__widget is None: - self.__widget = self.createWidget(self.__parent) - hooks = self.getHooks() - if hooks is not None: - hooks.viewWidgetCreated(self, self.__widget) - return self.__widget - - def createWidget(self, parent): - """Create the the widget displaying the data - - :param qt.QWidget parent: Parent of the widget - :returns: qt.QWidget - """ - raise NotImplementedError() - - def clear(self): - """Clear the data from the view""" - return None - - def setData(self, data): - """Set the data displayed by the view - - :param data: Data to display - :type data: numpy.ndarray or h5py.Dataset - """ - return None - - def __formatSlices(self, indices): - """Format an iterable of slice objects - - :param indices: The slices to format - :type indices: Union[None,List[Union[slice,int]]] - :rtype: str - """ - if indices is None: - return '' - - def formatSlice(slice_): - start, stop, step = slice_.start, slice_.stop, slice_.step - string = ('' if start is None else str(start)) + ':' - if stop is not None: - string += str(stop) - if step not in (None, 1): - string += ':' + step - return string - - return '[' + ', '.join( - formatSlice(index) if isinstance(index, slice) else str(index) - for index in indices) + ']' - - def titleForSelection(self, selection): - """Build title from given selection information. - - :param NamedTuple selection: Data selected - :rtype: str - """ - if selection is None or selection.filename is None: - return None - else: - directory, filename = os.path.split(selection.filename) - try: - slicing = self.__formatSlices(selection.slice) - except Exception: - _logger.debug("Error while formatting slices", exc_info=True) - slicing = '[sliced]' - - permuted = '(permuted)' if selection.permutation is not None else '' - - try: - title = self.TITLE_PATTERN.format( - directory=directory, - filename=filename, - datapath=selection.datapath, - slicing=slicing, - permuted=permuted) - except Exception: - _logger.debug("Error while formatting title", exc_info=True) - title = selection.datapath + slicing - - return title - - def setDataSelection(self, selection): - """Set the data selection displayed by the view - - If called, it have to be called directly after `setData`. - - :param selection: Data selected - :type selection: NamedTuple - """ - pass - - def axesNames(self, data, info): - """Returns names of the expected axes of the view, according to the - input data. A none value will disable the default axes selectior. - - :param data: Data to display - :type data: numpy.ndarray or h5py.Dataset - :param DataInfo info: Pre-computed information on the data - :rtype: list[str] or None - """ - return [] - - def getReachableViews(self): - """Returns the views that can be returned by `getMatchingViews`. - - :param object data: Any object to be displayed - :param DataInfo info: Information cached about this data - :rtype: List[DataView] - """ - return [self] - - def getMatchingViews(self, data, info): - """Returns the views according to data and info from the data. - - :param object data: Any object to be displayed - :param DataInfo info: Information cached about this data - :rtype: List[DataView] - """ - priority = self.getCachedDataPriority(data, info) - if priority == DataView.UNSUPPORTED: - return [] - return [self] - - def getCachedDataPriority(self, data, info): - try: - priority = info.getPriority(self) - except KeyError: - priority = self.getDataPriority(data, info) - info.cachePriority(self, priority) - return priority - - def getDataPriority(self, data, info): - """ - Returns the priority of using this view according to a data. - - - `UNSUPPORTED` means this view can't display this data - - `1` means this view can display the data - - `100` means this view should be used for this data - - `1000` max value used by the views provided by silx - - ... - - :param object data: The data to check - :param DataInfo info: Pre-computed information on the data - :rtype: int - """ - return DataView.UNSUPPORTED - - def __lt__(self, other): - return str(self) < str(other) - - -class _CompositeDataView(DataView): - """Contains sub views""" - - def getViews(self): - """Returns the direct sub views registered in this view. - - :rtype: List[DataView] - """ - raise NotImplementedError() - - def getReachableViews(self): - """Returns all views that can be reachable at on point. - - This method return any sub view provided (recursivly). - - :rtype: List[DataView] - """ - raise NotImplementedError() - - def getMatchingViews(self, data, info): - """Returns sub views matching this data and info. - - This method return any sub view provided (recursivly). - - :param object data: Any object to be displayed - :param DataInfo info: Information cached about this data - :rtype: List[DataView] - """ - raise NotImplementedError() - - @deprecation.deprecated(replacement="getReachableViews", since_version="0.10") - def availableViews(self): - return self.getViews() - - def isSupportedData(self, data, info): - """If true, the composite view allow sub views to access to this data. - Else this this data is considered as not supported by any of sub views - (incliding this composite view). - - :param object data: Any object to be displayed - :param DataInfo info: Information cached about this data - :rtype: bool - """ - return True - - -class SelectOneDataView(_CompositeDataView): - """Data view which can display a data using different view according to - the kind of the data.""" - - def __init__(self, parent, modeId=None, icon=None, label=None): - """Constructor - - :param qt.QWidget parent: Parent of the hold widget - """ - super(SelectOneDataView, self).__init__(parent, modeId, icon, label) - self.__views = OrderedDict() - self.__currentView = None - - def setHooks(self, hooks): - """Set the data context to use with this view. - - :param DataViewHooks hooks: The data view hooks to use - """ - super(SelectOneDataView, self).setHooks(hooks) - if hooks is not None: - for v in self.__views: - v.setHooks(hooks) - - def addView(self, dataView): - """Add a new dataview to the available list.""" - hooks = self.getHooks() - if hooks is not None: - dataView.setHooks(hooks) - self.__views[dataView] = None - - def getReachableViews(self): - views = [] - addSelf = False - for v in self.__views: - if isinstance(v, SelectManyDataView): - views.extend(v.getReachableViews()) - else: - addSelf = True - if addSelf: - # Single views are hidden by this view - views.insert(0, self) - return views - - def getMatchingViews(self, data, info): - if not self.isSupportedData(data, info): - return [] - view = self.__getBestView(data, info) - if isinstance(view, SelectManyDataView): - return view.getMatchingViews(data, info) - else: - return [self] - - def getViews(self): - """Returns the list of registered views - - :rtype: List[DataView] - """ - return list(self.__views.keys()) - - def __getBestView(self, data, info): - """Returns the best view according to priorities.""" - if not self.isSupportedData(data, info): - return None - views = [(v.getCachedDataPriority(data, info), v) for v in self.__views.keys()] - views = filter(lambda t: t[0] > DataView.UNSUPPORTED, views) - views = sorted(views, key=lambda t: t[0], reverse=True) - - if len(views) == 0: - return None - elif views[0][0] == DataView.UNSUPPORTED: - return None - else: - return views[0][1] - - def customAxisNames(self): - if self.__currentView is None: - return - return self.__currentView.customAxisNames() - - def setCustomAxisValue(self, name, value): - if self.__currentView is None: - return - self.__currentView.setCustomAxisValue(name, value) - - def __updateDisplayedView(self): - widget = self.getWidget() - if self.__currentView is None: - return - - # load the widget if it is not yet done - index = self.__views[self.__currentView] - if index is None: - w = self.__currentView.getWidget() - index = widget.addWidget(w) - self.__views[self.__currentView] = index - if widget.currentIndex() != index: - widget.setCurrentIndex(index) - self.__currentView.select() - - def select(self): - self.__updateDisplayedView() - if self.__currentView is not None: - self.__currentView.select() - - def createWidget(self, parent): - return qt.QStackedWidget() - - def clear(self): - for v in self.__views.keys(): - v.clear() - - def setData(self, data): - if self.__currentView is None: - return - self.__updateDisplayedView() - self.__currentView.setData(data) - - def setDataSelection(self, selection): - if self.__currentView is None: - return - self.__currentView.setDataSelection(selection) - - def axesNames(self, data, info): - view = self.__getBestView(data, info) - self.__currentView = view - return view.axesNames(data, info) - - def getDataPriority(self, data, info): - view = self.__getBestView(data, info) - self.__currentView = view - if view is None: - return DataView.UNSUPPORTED - else: - return view.getCachedDataPriority(data, info) - - def replaceView(self, modeId, newView): - """Replace a data view with a custom view. - Return True in case of success, False in case of failure. - - .. note:: - - This method must be called just after instantiation, before - the viewer is used. - - :param int modeId: Unique mode ID identifying the DataView to - be replaced. - :param DataViews.DataView newView: New data view - :return: True if replacement was successful, else False - """ - oldView = None - for view in self.__views: - if view.modeId() == modeId: - oldView = view - break - elif isinstance(view, _CompositeDataView): - # recurse - hooks = self.getHooks() - if hooks is not None: - newView.setHooks(hooks) - if view.replaceView(modeId, newView): - return True - if oldView is None: - return False - - # replace oldView with new view in dict - self.__views = OrderedDict( - (newView, None) if view is oldView else (view, idx) for - view, idx in self.__views.items()) - return True - - -# NOTE: SelectOneDataView was introduced with silx 0.10 -CompositeDataView = SelectOneDataView - - -class SelectManyDataView(_CompositeDataView): - """Data view which can select a set of sub views according to - the kind of the data. - - This view itself is abstract and is not exposed. - """ - - def __init__(self, parent, views=None): - """Constructor - - :param qt.QWidget parent: Parent of the hold widget - """ - super(SelectManyDataView, self).__init__(parent, modeId=None, icon=None, label=None) - if views is None: - views = [] - self.__views = views - - def setHooks(self, hooks): - """Set the data context to use with this view. - - :param DataViewHooks hooks: The data view hooks to use - """ - super(SelectManyDataView, self).setHooks(hooks) - if hooks is not None: - for v in self.__views: - v.setHooks(hooks) - - def addView(self, dataView): - """Add a new dataview to the available list.""" - hooks = self.getHooks() - if hooks is not None: - dataView.setHooks(hooks) - self.__views.append(dataView) - - def getViews(self): - """Returns the list of registered views - - :rtype: List[DataView] - """ - return list(self.__views) - - def getReachableViews(self): - views = [] - for v in self.__views: - views.extend(v.getReachableViews()) - return views - - def getMatchingViews(self, data, info): - """Returns the views according to data and info from the data. - - :param object data: Any object to be displayed - :param DataInfo info: Information cached about this data - """ - if not self.isSupportedData(data, info): - return [] - views = [v for v in self.__views if v.getCachedDataPriority(data, info) != DataView.UNSUPPORTED] - return views - - def customAxisNames(self): - raise RuntimeError("Abstract view") - - def setCustomAxisValue(self, name, value): - raise RuntimeError("Abstract view") - - def select(self): - raise RuntimeError("Abstract view") - - def createWidget(self, parent): - raise RuntimeError("Abstract view") - - def clear(self): - for v in self.__views: - v.clear() - - def setData(self, data): - raise RuntimeError("Abstract view") - - def axesNames(self, data, info): - raise RuntimeError("Abstract view") - - def getDataPriority(self, data, info): - if not self.isSupportedData(data, info): - return DataView.UNSUPPORTED - priorities = [v.getCachedDataPriority(data, info) for v in self.__views] - priorities = [v for v in priorities if v != DataView.UNSUPPORTED] - priorities = sorted(priorities) - if len(priorities) == 0: - return DataView.UNSUPPORTED - return priorities[-1] - - def replaceView(self, modeId, newView): - """Replace a data view with a custom view. - Return True in case of success, False in case of failure. - - .. note:: - - This method must be called just after instantiation, before - the viewer is used. - - :param int modeId: Unique mode ID identifying the DataView to - be replaced. - :param DataViews.DataView newView: New data view - :return: True if replacement was successful, else False - """ - oldView = None - for iview, view in enumerate(self.__views): - if view.modeId() == modeId: - oldView = view - break - elif isinstance(view, CompositeDataView): - # recurse - hooks = self.getHooks() - if hooks is not None: - newView.setHooks(hooks) - if view.replaceView(modeId, newView): - return True - - if oldView is None: - return False - - # replace oldView with new view in dict - self.__views[iview] = newView - return True - - -class _EmptyView(DataView): - """Dummy view to display nothing""" - - def __init__(self, parent): - DataView.__init__(self, parent, modeId=EMPTY_MODE) - - def axesNames(self, data, info): - return None - - def createWidget(self, parent): - return qt.QLabel(parent) - - def getDataPriority(self, data, info): - return DataView.UNSUPPORTED - - -class _Plot1dView(DataView): - """View displaying data using a 1d plot""" - - def __init__(self, parent): - super(_Plot1dView, self).__init__( - parent=parent, - modeId=PLOT1D_MODE, - label="Curve", - icon=icons.getQIcon("view-1d")) - self.__resetZoomNextTime = True - - def createWidget(self, parent): - from silx.gui import plot - return plot.Plot1D(parent=parent) - - def clear(self): - self.getWidget().clear() - self.__resetZoomNextTime = True - - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - data = _normalizeComplex(data) - return data - - def setData(self, data): - data = self.normalizeData(data) - plotWidget = self.getWidget() - legend = "data" - plotWidget.addCurve(legend=legend, - x=range(len(data)), - y=data, - resetzoom=self.__resetZoomNextTime) - plotWidget.setActiveCurve(legend) - self.__resetZoomNextTime = True - - def setDataSelection(self, selection): - self.getWidget().setGraphTitle(self.titleForSelection(selection)) - - def axesNames(self, data, info): - return ["y"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isArray or not info.isNumeric: - return DataView.UNSUPPORTED - if info.dim < 1: - return DataView.UNSUPPORTED - if info.interpretation == "spectrum": - return 1000 - if info.dim == 2 and info.shape[0] == 1: - return 210 - if info.dim == 1: - return 100 - else: - return 10 - - -class _Plot2dRecordView(DataView): - def __init__(self, parent): - super(_Plot2dRecordView, self).__init__( - parent=parent, - modeId=RECORD_PLOT_MODE, - label="Curve", - icon=icons.getQIcon("view-1d")) - self.__resetZoomNextTime = True - self._data = None - self._xAxisDropDown = None - self._yAxisDropDown = None - self.__fields = None - - def createWidget(self, parent): - from ._RecordPlot import RecordPlot - return RecordPlot(parent=parent) - - def clear(self): - self.getWidget().clear() - self.__resetZoomNextTime = True - - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - data = _normalizeComplex(data) - return data - - def setData(self, data): - self._data = self.normalizeData(data) - - all_fields = sorted(self._data.dtype.fields.items(), key=lambda e: e[1][1]) - numeric_fields = [f[0] for f in all_fields if numpy.issubdtype(f[1][0], numpy.number)] - if numeric_fields == self.__fields: # Reuse previously selected fields - fieldNameX = self.getWidget().getXAxisFieldName() - fieldNameY = self.getWidget().getYAxisFieldName() - else: - self.__fields = numeric_fields - - self.getWidget().setSelectableXAxisFieldNames(numeric_fields) - self.getWidget().setSelectableYAxisFieldNames(numeric_fields) - fieldNameX = None - fieldNameY = numeric_fields[0] - - # If there is a field called time, use it for the x-axis by default - if "time" in numeric_fields: - fieldNameX = "time" - # Use the first field that is not "time" for the y-axis - if fieldNameY == "time" and len(numeric_fields) >= 2: - fieldNameY = numeric_fields[1] - - self._plotData(fieldNameX, fieldNameY) - - if not self._xAxisDropDown: - self._xAxisDropDown = self.getWidget().getAxesSelectionToolBar().getXAxisDropDown() - self._yAxisDropDown = self.getWidget().getAxesSelectionToolBar().getYAxisDropDown() - self._xAxisDropDown.activated.connect(self._onAxesSelectionChaned) - self._yAxisDropDown.activated.connect(self._onAxesSelectionChaned) - - def setDataSelection(self, selection): - self.getWidget().setGraphTitle(self.titleForSelection(selection)) - - def _onAxesSelectionChaned(self): - fieldNameX = self._xAxisDropDown.currentData() - self._plotData(fieldNameX, self._yAxisDropDown.currentText()) - - def _plotData(self, fieldNameX, fieldNameY): - self.clear() - ydata = self._data[fieldNameY] - if fieldNameX is None: - xdata = numpy.arange(len(ydata)) - else: - xdata = self._data[fieldNameX] - self.getWidget().addCurve(legend="data", - x=xdata, - y=ydata, - resetzoom=self.__resetZoomNextTime) - self.getWidget().setXAxisFieldName(fieldNameX) - self.getWidget().setYAxisFieldName(fieldNameY) - self.__resetZoomNextTime = True - - def axesNames(self, data, info): - return ["data"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isRecord: - return DataView.UNSUPPORTED - if info.dim < 1: - return DataView.UNSUPPORTED - if info.countNumericColumns < 2: - return DataView.UNSUPPORTED - if info.interpretation == "spectrum": - return 1000 - if info.dim == 2 and info.shape[0] == 1: - return 210 - if info.dim == 1: - return 40 - else: - return 10 - - -class _Plot2dView(DataView): - """View displaying data using a 2d plot""" - - def __init__(self, parent): - super(_Plot2dView, self).__init__( - parent=parent, - modeId=PLOT2D_MODE, - label="Image", - icon=icons.getQIcon("view-2d")) - self.__resetZoomNextTime = True - - def createWidget(self, parent): - from silx.gui import plot - widget = plot.Plot2D(parent=parent) - widget.setDefaultColormap(self.defaultColormap()) - widget.getColormapAction().setColorDialog(self.defaultColorDialog()) - widget.getIntensityHistogramAction().setVisible(True) - widget.setKeepDataAspectRatio(True) - widget.getXAxis().setLabel('X') - widget.getYAxis().setLabel('Y') - maskToolsWidget = widget.getMaskToolsDockWidget().widget() - maskToolsWidget.setItemMaskUpdated(True) - return widget - - def clear(self): - self.getWidget().clear() - self.__resetZoomNextTime = True - - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - data = _normalizeComplex(data) - return data - - def setData(self, data): - data = self.normalizeData(data) - self.getWidget().addImage(legend="data", - data=data, - resetzoom=self.__resetZoomNextTime) - self.__resetZoomNextTime = False - - def setDataSelection(self, selection): - self.getWidget().setGraphTitle(self.titleForSelection(selection)) - - def axesNames(self, data, info): - return ["y", "x"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if (data is None or - not info.isArray or - not (info.isNumeric or info.isBoolean)): - return DataView.UNSUPPORTED - if info.dim < 2: - return DataView.UNSUPPORTED - if info.interpretation == "image": - return 1000 - if info.dim == 2: - return 200 - else: - return 190 - - -class _Plot3dView(DataView): - """View displaying data using a 3d plot""" - - def __init__(self, parent): - super(_Plot3dView, self).__init__( - parent=parent, - modeId=PLOT3D_MODE, - label="Cube", - icon=icons.getQIcon("view-3d")) - try: - from ._VolumeWindow import VolumeWindow # noqa - except ImportError: - _logger.warning("3D visualization is not available") - _logger.debug("Backtrace", exc_info=True) - raise - self.__resetZoomNextTime = True - - def createWidget(self, parent): - from ._VolumeWindow import VolumeWindow - - plot = VolumeWindow(parent) - plot.setAxesLabels(*reversed(self.axesNames(None, None))) - return plot - - def clear(self): - self.getWidget().clear() - self.__resetZoomNextTime = True - - def setData(self, data): - data = self.normalizeData(data) - self.getWidget().setData(data) - self.__resetZoomNextTime = False - - def axesNames(self, data, info): - return ["z", "y", "x"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isArray or not info.isNumeric: - return DataView.UNSUPPORTED - if info.dim < 3: - return DataView.UNSUPPORTED - if min(data.shape) < 2: - return DataView.UNSUPPORTED - if info.dim == 3: - return 100 - else: - return 10 - - -class _ComplexImageView(DataView): - """View displaying data using a ComplexImageView""" - - def __init__(self, parent): - super(_ComplexImageView, self).__init__( - parent=parent, - modeId=COMPLEX_IMAGE_MODE, - label="Complex Image", - icon=icons.getQIcon("view-2d")) - - def createWidget(self, parent): - from silx.gui.plot.ComplexImageView import ComplexImageView - widget = ComplexImageView(parent=parent) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.ABSOLUTE) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.SQUARE_AMPLITUDE) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.REAL) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.IMAGINARY) - widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) - widget.getPlot().getIntensityHistogramAction().setVisible(True) - widget.getPlot().setKeepDataAspectRatio(True) - widget.getXAxis().setLabel('X') - widget.getYAxis().setLabel('Y') - maskToolsWidget = widget.getPlot().getMaskToolsDockWidget().widget() - maskToolsWidget.setItemMaskUpdated(True) - return widget - - def clear(self): - self.getWidget().setData(None) - - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - return data - - def setData(self, data): - data = self.normalizeData(data) - self.getWidget().setData(data) - - def setDataSelection(self, selection): - self.getWidget().getPlot().setGraphTitle( - self.titleForSelection(selection)) - - def axesNames(self, data, info): - return ["y", "x"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isArray or not info.isComplex: - return DataView.UNSUPPORTED - if info.dim < 2: - return DataView.UNSUPPORTED - if info.interpretation == "image": - return 1000 - if info.dim == 2: - return 200 - else: - return 190 - - -class _ArrayView(DataView): - """View displaying data using a 2d table""" - - def __init__(self, parent): - DataView.__init__(self, parent, modeId=RAW_ARRAY_MODE) - - def createWidget(self, parent): - from silx.gui.data.ArrayTableWidget import ArrayTableWidget - widget = ArrayTableWidget(parent) - widget.displayAxesSelector(False) - return widget - - def clear(self): - self.getWidget().setArrayData(numpy.array([[]])) - - def setData(self, data): - data = self.normalizeData(data) - self.getWidget().setArrayData(data) - - def axesNames(self, data, info): - return ["col", "row"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isArray or info.isRecord: - return DataView.UNSUPPORTED - if info.dim < 2: - return DataView.UNSUPPORTED - if info.interpretation in ["scalar", "scaler"]: - return 1000 - return 500 - - -class _StackView(DataView): - """View displaying data using a stack of images""" - - def __init__(self, parent): - super(_StackView, self).__init__( - parent=parent, - modeId=STACK_MODE, - label="Image stack", - icon=icons.getQIcon("view-2d-stack")) - self.__resetZoomNextTime = True - - def customAxisNames(self): - return ["depth"] - - def setCustomAxisValue(self, name, value): - if name == "depth": - self.getWidget().setFrameNumber(value) - else: - raise Exception("Unsupported axis") - - def createWidget(self, parent): - from silx.gui import plot - widget = plot.StackView(parent=parent) - widget.setColormap(self.defaultColormap()) - widget.getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog()) - widget.setKeepDataAspectRatio(True) - widget.setLabels(self.axesNames(None, None)) - # hide default option panel - widget.setOptionVisible(False) - maskToolWidget = widget.getPlotWidget().getMaskToolsDockWidget().widget() - maskToolWidget.setItemMaskUpdated(True) - return widget - - def clear(self): - self.getWidget().clear() - self.__resetZoomNextTime = True - - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - data = _normalizeComplex(data) - return data - - def setData(self, data): - data = self.normalizeData(data) - self.getWidget().setStack(stack=data, reset=self.__resetZoomNextTime) - # Override the colormap, while setStack overwrite it - self.getWidget().setColormap(self.defaultColormap()) - self.__resetZoomNextTime = False - - def setDataSelection(self, selection): - title = self.titleForSelection(selection) - self.getWidget().setTitleCallback( - lambda idx: "%s z=%d" % (title, idx)) - - def axesNames(self, data, info): - return ["depth", "y", "x"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isArray or not info.isNumeric: - return DataView.UNSUPPORTED - if info.dim < 3: - return DataView.UNSUPPORTED - if info.interpretation == "image": - return 500 - return 90 - - -class _ScalarView(DataView): - """View displaying data using text""" - - def __init__(self, parent): - DataView.__init__(self, parent, modeId=RAW_SCALAR_MODE) - - def createWidget(self, parent): - widget = qt.QTextEdit(parent) - widget.setTextInteractionFlags(qt.Qt.TextSelectableByMouse) - widget.setAlignment(qt.Qt.AlignLeft | qt.Qt.AlignTop) - self.__formatter = TextFormatter(parent) - return widget - - def clear(self): - self.getWidget().setText("") - - def setData(self, data): - d = self.normalizeData(data) - if silx.io.is_dataset(d): - d = d[()] - dtype = None - if data is not None: - if hasattr(data, "dtype"): - dtype = data.dtype - text = self.__formatter.toString(d, dtype) - self.getWidget().setText(text) - - def axesNames(self, data, info): - return [] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - data = self.normalizeData(data) - if info.shape is None: - return DataView.UNSUPPORTED - if data is None: - return DataView.UNSUPPORTED - if silx.io.is_group(data): - return DataView.UNSUPPORTED - return 2 - - -class _RecordView(DataView): - """View displaying data using text""" - - def __init__(self, parent): - DataView.__init__(self, parent, modeId=RAW_RECORD_MODE) - - def createWidget(self, parent): - from .RecordTableView import RecordTableView - widget = RecordTableView(parent) - widget.setWordWrap(False) - return widget - - def clear(self): - self.getWidget().setArrayData(None) - - def setData(self, data): - data = self.normalizeData(data) - widget = self.getWidget() - widget.setArrayData(data) - if len(data) < 100: - widget.resizeRowsToContents() - widget.resizeColumnsToContents() - - def axesNames(self, data, info): - return ["data"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if info.isRecord: - return 40 - if data is None or not info.isArray: - return DataView.UNSUPPORTED - if info.dim == 1: - if info.interpretation in ["scalar", "scaler"]: - return 1000 - if info.shape[0] == 1: - return 510 - return 500 - elif info.isRecord: - return 40 - return DataView.UNSUPPORTED - - -class _HexaView(DataView): - """View displaying data using text""" - - def __init__(self, parent): - DataView.__init__(self, parent, modeId=RAW_HEXA_MODE) - - def createWidget(self, parent): - from .HexaTableView import HexaTableView - widget = HexaTableView(parent) - return widget - - def clear(self): - self.getWidget().setArrayData(None) - - def setData(self, data): - data = self.normalizeData(data) - widget = self.getWidget() - widget.setArrayData(data) - - def axesNames(self, data, info): - return [] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if info.isVoid: - return 2000 - return DataView.UNSUPPORTED - - -class _Hdf5View(DataView): - """View displaying data using text""" - - def __init__(self, parent): - super(_Hdf5View, self).__init__( - parent=parent, - modeId=HDF5_MODE, - label="HDF5", - icon=icons.getQIcon("view-hdf5")) - - def createWidget(self, parent): - from .Hdf5TableView import Hdf5TableView - widget = Hdf5TableView(parent) - return widget - - def clear(self): - widget = self.getWidget() - widget.setData(None) - - def setData(self, data): - widget = self.getWidget() - widget.setData(data) - - def axesNames(self, data, info): - return None - - def getDataPriority(self, data, info): - widget = self.getWidget() - if widget.isSupportedData(data): - return 1 - else: - return DataView.UNSUPPORTED - - -class _RawView(CompositeDataView): - """View displaying data as raw data. - - This implementation use a 2d-array view, or a record array view, or a - raw text output. - """ - - def __init__(self, parent): - super(_RawView, self).__init__( - parent=parent, - modeId=RAW_MODE, - label="Raw", - icon=icons.getQIcon("view-raw")) - self.addView(_HexaView(parent)) - self.addView(_ScalarView(parent)) - self.addView(_ArrayView(parent)) - self.addView(_RecordView(parent)) - - -class _ImageView(CompositeDataView): - """View displaying data as 2D image - - It choose between Plot2D and ComplexImageView widgets - """ - - def __init__(self, parent): - super(_ImageView, self).__init__( - parent=parent, - modeId=IMAGE_MODE, - label="Image", - icon=icons.getQIcon("view-2d")) - self.addView(_ComplexImageView(parent)) - self.addView(_Plot2dView(parent)) - - -class _InvalidNXdataView(DataView): - """DataView showing a simple label with an error message - to inform that a group with @NX_class=NXdata cannot be - interpreted by any NXDataview.""" - def __init__(self, parent): - DataView.__init__(self, parent, - modeId=NXDATA_INVALID_MODE) - self._msg = "" - - def createWidget(self, parent): - widget = qt.QLabel(parent) - widget.setWordWrap(True) - widget.setStyleSheet("QLabel { color : red; }") - return widget - - def axesNames(self, data, info): - return [] - - def clear(self): - self.getWidget().setText("") - - def setData(self, data): - self.getWidget().setText(self._msg) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - - if not info.isInvalidNXdata: - return DataView.UNSUPPORTED - - if info.hasNXdata: - self._msg = "NXdata seems valid, but cannot be displayed " - self._msg += "by any existing plot widget." - else: - nx_class = get_attr_as_unicode(data, "NX_class") - if nx_class == "NXdata": - # invalid: could not even be parsed by NXdata - self._msg = "Group has @NX_class = NXdata, but could not be interpreted" - self._msg += " as valid NXdata." - elif nx_class == "NXroot" or silx.io.is_file(data): - default_entry = data[data.attrs["default"]] - default_nxdata_name = default_entry.attrs["default"] - self._msg = "NXroot group provides a @default attribute " - self._msg += "pointing to a NXentry which defines its own " - self._msg += "@default attribute, " - if default_nxdata_name not in default_entry: - self._msg += " but no corresponding NXdata group exists." - elif get_attr_as_unicode(default_entry[default_nxdata_name], - "NX_class") != "NXdata": - self._msg += " but the corresponding item is not a " - self._msg += "NXdata group." - else: - self._msg += " but the corresponding NXdata seems to be" - self._msg += " malformed." - else: - self._msg = "Group provides a @default attribute," - default_nxdata_name = data.attrs["default"] - if default_nxdata_name not in data: - self._msg += " but no corresponding NXdata group exists." - elif get_attr_as_unicode(data[default_nxdata_name], "NX_class") != "NXdata": - self._msg += " but the corresponding item is not a " - self._msg += "NXdata group." - else: - self._msg += " but the corresponding NXdata seems to be" - self._msg += " malformed." - return 100 - - -class _NXdataBaseDataView(DataView): - """Base class for NXdata DataView""" - - def __init__(self, *args, **kwargs): - DataView.__init__(self, *args, **kwargs) - - def _updateColormap(self, nxdata): - """Update used colormap according to nxdata's SILX_style""" - cmap_norm = nxdata.plot_style.signal_scale_type - if cmap_norm is not None: - self.defaultColormap().setNormalization( - 'log' if cmap_norm == 'log' else 'linear') - - -class _NXdataScalarView(_NXdataBaseDataView): - """DataView using a table view for displaying NXdata scalars: - 0-D signal or n-D signal with *@interpretation=scalar*""" - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_SCALAR_MODE) - - def createWidget(self, parent): - from silx.gui.data.ArrayTableWidget import ArrayTableWidget - widget = ArrayTableWidget(parent) - # widget.displayAxesSelector(False) - return widget - - def axesNames(self, data, info): - return ["col", "row"] - - def clear(self): - self.getWidget().setArrayData(numpy.array([[]]), - labels=True) - - def setData(self, data): - data = self.normalizeData(data) - # data could be a NXdata or an NXentry - nxd = nxdata.get_default(data, validate=False) - signal = nxd.signal - self.getWidget().setArrayData(signal, - labels=True) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - - if info.hasNXdata and not info.isInvalidNXdata: - nxd = nxdata.get_default(data, validate=False) - if nxd.signal_is_0d or nxd.interpretation in ["scalar", "scaler"]: - return 100 - return DataView.UNSUPPORTED - - -class _NXdataCurveView(_NXdataBaseDataView): - """DataView using a Plot1D for displaying NXdata curves: - 1-D signal or n-D signal with *@interpretation=spectrum*. - - It also handles basic scatter plots: - a 1-D signal with one axis whose values are not monotonically increasing. - """ - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_CURVE_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayCurvePlot - widget = ArrayCurvePlot(parent) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - signals_names = [nxd.signal_name] + nxd.auxiliary_signals_names - if nxd.axes_dataset_names[-1] is not None: - x_errors = nxd.get_axis_errors(nxd.axes_dataset_names[-1]) - else: - x_errors = None - - # this fix is necessary until the next release of PyMca (5.2.3 or 5.3.0) - # see https://github.com/vasole/pymca/issues/144 and https://github.com/vasole/pymca/pull/145 - if not hasattr(self.getWidget(), "setCurvesData") and \ - hasattr(self.getWidget(), "setCurveData"): - _logger.warning("Using deprecated ArrayCurvePlot API, " - "without support of auxiliary signals") - self.getWidget().setCurveData(nxd.signal, nxd.axes[-1], - yerror=nxd.errors, xerror=x_errors, - ylabel=nxd.signal_name, xlabel=nxd.axes_names[-1], - title=nxd.title or nxd.signal_name) - return - - self.getWidget().setCurvesData([nxd.signal] + nxd.auxiliary_signals, nxd.axes[-1], - yerror=nxd.errors, xerror=x_errors, - ylabels=signals_names, xlabel=nxd.axes_names[-1], - title=nxd.title or signals_names[0], - xscale=nxd.plot_style.axes_scale_types[-1], - yscale=nxd.plot_style.signal_scale_type) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_curve: - return 100 - return DataView.UNSUPPORTED - - -class _NXdataXYVScatterView(_NXdataBaseDataView): - """DataView using a Plot1D for displaying NXdata 3D scatters as - a scatter of coloured points (1-D signal with 2 axes)""" - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_XYVSCATTER_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import XYVScatterPlot - widget = XYVScatterPlot(parent) - widget.getScatterView().setColormap(self.defaultColormap()) - widget.getScatterView().getScatterToolBar().getColormapAction().setColorDialog( - self.defaultColorDialog()) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - - x_axis, y_axis = nxd.axes[-2:] - if x_axis is None: - x_axis = numpy.arange(nxd.signal.size) - if y_axis is None: - y_axis = numpy.arange(nxd.signal.size) - - x_label, y_label = nxd.axes_names[-2:] - if x_label is not None: - x_errors = nxd.get_axis_errors(x_label) - else: - x_errors = None - - if y_label is not None: - y_errors = nxd.get_axis_errors(y_label) - else: - y_errors = None - - self._updateColormap(nxd) - - self.getWidget().setScattersData(y_axis, x_axis, values=[nxd.signal] + nxd.auxiliary_signals, - yerror=y_errors, xerror=x_errors, - ylabel=y_label, xlabel=x_label, - title=nxd.title, - scatter_titles=[nxd.signal_name] + nxd.auxiliary_signals_names, - xscale=nxd.plot_style.axes_scale_types[-2], - yscale=nxd.plot_style.axes_scale_types[-1]) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_x_y_value_scatter: - # It have to be a little more than a NX curve priority - return 110 - - return DataView.UNSUPPORTED - - -class _NXdataImageView(_NXdataBaseDataView): - """DataView using a Plot2D for displaying NXdata images: - 2-D signal or n-D signals with *@interpretation=image*.""" - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_IMAGE_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayImagePlot - widget = ArrayImagePlot(parent) - widget.getPlot().setDefaultColormap(self.defaultColormap()) - widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - isRgba = nxd.interpretation == "rgba-image" - - self._updateColormap(nxd) - - # last two axes are Y & X - img_slicing = slice(-2, None) if not isRgba else slice(-3, -1) - y_axis, x_axis = nxd.axes[img_slicing] - y_label, x_label = nxd.axes_names[img_slicing] - y_scale, x_scale = nxd.plot_style.axes_scale_types[img_slicing] - - self.getWidget().setImageData( - [nxd.signal] + nxd.auxiliary_signals, - x_axis=x_axis, y_axis=y_axis, - signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names, - xlabel=x_label, ylabel=y_label, - title=nxd.title, isRgba=isRgba, - xscale=x_scale, yscale=y_scale) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_image: - return 100 - - return DataView.UNSUPPORTED - - -class _NXdataComplexImageView(_NXdataBaseDataView): - """DataView using a ComplexImageView for displaying NXdata complex images: - 2-D signal or n-D signals with *@interpretation=image*.""" - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_IMAGE_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot - widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap()) - widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) - return widget - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - - self._updateColormap(nxd) - - # last two axes are Y & X - img_slicing = slice(-2, None) - y_axis, x_axis = nxd.axes[img_slicing] - y_label, x_label = nxd.axes_names[img_slicing] - - self.getWidget().setImageData( - [nxd.signal] + nxd.auxiliary_signals, - x_axis=x_axis, y_axis=y_axis, - signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names, - xlabel=x_label, ylabel=y_label, - title=nxd.title) - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - - if info.hasNXdata and not info.isInvalidNXdata: - nxd = nxdata.get_default(data, validate=False) - if nxd.is_image and numpy.iscomplexobj(nxd.signal): - return 100 - - return DataView.UNSUPPORTED - - -class _NXdataStackView(_NXdataBaseDataView): - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_STACK_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayStackPlot - widget = ArrayStackPlot(parent) - widget.getStackView().setColormap(self.defaultColormap()) - widget.getStackView().getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog()) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - signal_name = nxd.signal_name - z_axis, y_axis, x_axis = nxd.axes[-3:] - z_label, y_label, x_label = nxd.axes_names[-3:] - title = nxd.title or signal_name - - self._updateColormap(nxd) - - widget = self.getWidget() - widget.setStackData( - nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis, - signal_name=signal_name, - xlabel=x_label, ylabel=y_label, zlabel=z_label, - title=title) - # Override the colormap, while setStack overwrite it - widget.getStackView().setColormap(self.defaultColormap()) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_stack: - return 100 - - return DataView.UNSUPPORTED - - -class _NXdataVolumeView(_NXdataBaseDataView): - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, - label="NXdata (3D)", - icon=icons.getQIcon("view-nexus"), - modeId=NXDATA_VOLUME_MODE) - try: - import silx.gui.plot3d # noqa - except ImportError: - _logger.warning("Plot3dView is not available") - _logger.debug("Backtrace", exc_info=True) - raise - - def normalizeData(self, data): - data = super(_NXdataVolumeView, self).normalizeData(data) - data = _normalizeComplex(data) - return data - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayVolumePlot - widget = ArrayVolumePlot(parent) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - signal_name = nxd.signal_name - z_axis, y_axis, x_axis = nxd.axes[-3:] - z_label, y_label, x_label = nxd.axes_names[-3:] - title = nxd.title or signal_name - - widget = self.getWidget() - widget.setData( - nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis, - signal_name=signal_name, - xlabel=x_label, ylabel=y_label, zlabel=z_label, - title=title) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_volume: - return 150 - - return DataView.UNSUPPORTED - - -class _NXdataVolumeAsStackView(_NXdataBaseDataView): - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, - label="NXdata (2D)", - icon=icons.getQIcon("view-nexus"), - modeId=NXDATA_VOLUME_AS_STACK_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayStackPlot - widget = ArrayStackPlot(parent) - widget.getStackView().setColormap(self.defaultColormap()) - widget.getStackView().getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog()) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - signal_name = nxd.signal_name - z_axis, y_axis, x_axis = nxd.axes[-3:] - z_label, y_label, x_label = nxd.axes_names[-3:] - title = nxd.title or signal_name - - self._updateColormap(nxd) - - widget = self.getWidget() - widget.setStackData( - nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis, - signal_name=signal_name, - xlabel=x_label, ylabel=y_label, zlabel=z_label, - title=title) - # Override the colormap, while setStack overwrite it - widget.getStackView().setColormap(self.defaultColormap()) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if info.isComplex: - return DataView.UNSUPPORTED - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_volume: - return 200 - - return DataView.UNSUPPORTED - -class _NXdataComplexVolumeAsStackView(_NXdataBaseDataView): - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, - label="NXdata (2D)", - icon=icons.getQIcon("view-nexus"), - modeId=NXDATA_VOLUME_AS_STACK_MODE) - self._is_complex_data = False - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot - widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap()) - widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - signal_name = nxd.signal_name - z_axis, y_axis, x_axis = nxd.axes[-3:] - z_label, y_label, x_label = nxd.axes_names[-3:] - title = nxd.title or signal_name - - self._updateColormap(nxd) - - self.getWidget().setImageData( - [nxd.signal] + nxd.auxiliary_signals, - x_axis=x_axis, y_axis=y_axis, - signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names, - xlabel=x_label, ylabel=y_label, title=nxd.title) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if not info.isComplex: - return DataView.UNSUPPORTED - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_volume: - return 200 - - return DataView.UNSUPPORTED - - -class _NXdataView(CompositeDataView): - """Composite view displaying NXdata groups using the most adequate - widget depending on the dimensionality.""" - def __init__(self, parent): - super(_NXdataView, self).__init__( - parent=parent, - label="NXdata", - modeId=NXDATA_MODE, - icon=icons.getQIcon("view-nexus")) - - self.addView(_InvalidNXdataView(parent)) - self.addView(_NXdataScalarView(parent)) - self.addView(_NXdataCurveView(parent)) - self.addView(_NXdataXYVScatterView(parent)) - self.addView(_NXdataComplexImageView(parent)) - self.addView(_NXdataImageView(parent)) - self.addView(_NXdataStackView(parent)) - - # The 3D view can be displayed using 2 ways - nx3dViews = SelectManyDataView(parent) - nx3dViews.addView(_NXdataVolumeAsStackView(parent)) - nx3dViews.addView(_NXdataComplexVolumeAsStackView(parent)) - try: - nx3dViews.addView(_NXdataVolumeView(parent)) - except Exception: - _logger.warning("NXdataVolumeView is not available") - _logger.debug("Backtrace", exc_info=True) - self.addView(nx3dViews) diff --git a/silx/gui/data/Hdf5TableView.py b/silx/gui/data/Hdf5TableView.py deleted file mode 100644 index 7749326..0000000 --- a/silx/gui/data/Hdf5TableView.py +++ /dev/null @@ -1,646 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -This module define model and widget to display 1D slices from numpy -array using compound data types or hdf5 databases. -""" -from __future__ import division - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/02/2019" - -import collections -import functools -import os.path -import logging -import h5py -import numpy - -from silx.gui import qt -import silx.io -from .TextFormatter import TextFormatter -import silx.gui.hdf5 -from silx.gui.widgets import HierarchicalTableView -from ..hdf5.Hdf5Formatter import Hdf5Formatter -from ..hdf5._utils import htmlFromDict - - -_logger = logging.getLogger(__name__) - - -class _CellData(object): - """Store a table item - """ - def __init__(self, value=None, isHeader=False, span=None, tooltip=None): - """ - Constructor - - :param str value: Label of this property - :param bool isHeader: True if the cell is an header - :param tuple span: Tuple of row, column span - """ - self.__value = value - self.__isHeader = isHeader - self.__span = span - self.__tooltip = tooltip - - def isHeader(self): - """Returns true if the property is a sub-header title. - - :rtype: bool - """ - return self.__isHeader - - def value(self): - """Returns the value of the item. - """ - return self.__value - - def span(self): - """Returns the span size of the cell. - - :rtype: tuple - """ - return self.__span - - def tooltip(self): - """Returns the tooltip of the item. - - :rtype: tuple - """ - return self.__tooltip - - def invalidateValue(self): - self.__value = None - - def invalidateToolTip(self): - self.__tooltip = None - - def data(self, role): - return None - - -class _TableData(object): - """Modelize a table with header, row and column span. - - It is mostly defined as a row based table. - """ - - def __init__(self, columnCount): - """Constructor. - - :param int columnCount: Define the number of column of the table - """ - self.__colCount = columnCount - self.__data = [] - - def rowCount(self): - """Returns the number of rows. - - :rtype: int - """ - return len(self.__data) - - def columnCount(self): - """Returns the number of columns. - - :rtype: int - """ - return self.__colCount - - def clear(self): - """Remove all the cells of the table""" - self.__data = [] - - def cellAt(self, row, column): - """Returns the cell at the row column location. Else None if there is - nothing. - - :rtype: _CellData - """ - if row < 0: - return None - if column < 0: - return None - if row >= len(self.__data): - return None - cells = self.__data[row] - if column >= len(cells): - return None - return cells[column] - - def addHeaderRow(self, headerLabel): - """Append the table with header on the full row. - - :param str headerLabel: label of the header. - """ - item = _CellData(value=headerLabel, isHeader=True, span=(1, self.__colCount)) - self.__data.append([item]) - - def addHeaderValueRow(self, headerLabel, value, tooltip=None): - """Append the table with a row using the first column as an header and - other cells as a single cell for the value. - - :param str headerLabel: label of the header. - :param object value: value to store. - """ - header = _CellData(value=headerLabel, isHeader=True) - value = _CellData(value=value, span=(1, self.__colCount), tooltip=tooltip) - self.__data.append([header, value]) - - def addRow(self, *args): - """Append the table with a row using arguments for each cells - - :param list[object] args: List of cell values for the row - """ - row = [] - for value in args: - if not isinstance(value, _CellData): - value = _CellData(value=value) - row.append(value) - self.__data.append(row) - - -class _CellFilterAvailableData(_CellData): - """Cell rendering for availability of a filter""" - - _states = { - True: ("Available", qt.QColor(0x000000), None, None), - False: ("Not available", qt.QColor(0xFFFFFF), qt.QColor(0xFF0000), - "You have to install this filter on your system to be able to read this dataset"), - "na": ("n.a.", qt.QColor(0x000000), None, - "This version of h5py/hdf5 is not able to display the information"), - } - - def __init__(self, filterId): - if h5py.version.hdf5_version_tuple >= (1, 10, 2): - # Previous versions only returns True if the filter was first used - # to decode a dataset - self.__availability = h5py.h5z.filter_avail(filterId) - else: - self.__availability = "na" - _CellData.__init__(self) - - def value(self): - state = self._states[self.__availability] - return state[0] - - def tooltip(self): - state = self._states[self.__availability] - return state[3] - - def data(self, role=qt.Qt.DisplayRole): - state = self._states[self.__availability] - if role == qt.Qt.TextColorRole: - return state[1] - elif role == qt.Qt.BackgroundColorRole: - return state[2] - else: - return None - - -class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): - """This data model provides access to HDF5 node content (File, Group, - Dataset). Main info, like name, file, attributes... are displayed - """ - - def __init__(self, parent=None, data=None): - """ - Constructor - - :param qt.QObject parent: Parent object - :param object data: An h5py-like object (file, group or dataset) - """ - super(Hdf5TableModel, self).__init__(parent) - - self.__obj = None - self.__data = _TableData(columnCount=5) - self.__formatter = None - self.__hdf5Formatter = Hdf5Formatter(self) - formatter = TextFormatter(self) - self.setFormatter(formatter) - self.setObject(data) - - def rowCount(self, parent_idx=None): - """Returns number of rows to be displayed in table""" - return self.__data.rowCount() - - def columnCount(self, parent_idx=None): - """Returns number of columns to be displayed in table""" - return self.__data.columnCount() - - def data(self, index, role=qt.Qt.DisplayRole): - """QAbstractTableModel method to access data values - in the format ready to be displayed""" - if not index.isValid(): - return None - - cell = self.__data.cellAt(index.row(), index.column()) - if cell is None: - return None - - if role == self.SpanRole: - return cell.span() - elif role == self.IsHeaderRole: - return cell.isHeader() - elif role in (qt.Qt.DisplayRole, qt.Qt.EditRole): - value = cell.value() - if callable(value): - try: - value = value(self.__obj) - except Exception: - cell.invalidateValue() - raise - return value - elif role == qt.Qt.ToolTipRole: - value = cell.tooltip() - if callable(value): - try: - value = value(self.__obj) - except Exception: - cell.invalidateToolTip() - raise - return value - else: - return cell.data(role) - return None - - def isSupportedObject(self, h5pyObject): - """ - Returns true if the provided object can be modelized using this model. - """ - isSupported = False - isSupported = isSupported or silx.io.is_group(h5pyObject) - isSupported = isSupported or silx.io.is_dataset(h5pyObject) - isSupported = isSupported or isinstance(h5pyObject, silx.gui.hdf5.H5Node) - return isSupported - - def setObject(self, h5pyObject): - """Set the h5py-like object exposed by the model - - :param h5pyObject: A h5py-like object. It can be a `h5py.Dataset`, - a `h5py.File`, a `h5py.Group`. It also can be a, - `silx.gui.hdf5.H5Node` which is needed to display some local path - information. - """ - if qt.qVersion() > "4.6": - self.beginResetModel() - - if h5pyObject is None or self.isSupportedObject(h5pyObject): - self.__obj = h5pyObject - else: - _logger.warning("Object class %s unsupported. Object ignored.", type(h5pyObject)) - self.__initProperties() - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def __formatHdf5Type(self, dataset): - """Format the HDF5 type""" - return self.__hdf5Formatter.humanReadableHdf5Type(dataset) - - def __attributeTooltip(self, attribute): - attributeDict = collections.OrderedDict() - if hasattr(attribute, "shape"): - attributeDict["Shape"] = self.__hdf5Formatter.humanReadableShape(attribute) - attributeDict["Data type"] = self.__hdf5Formatter.humanReadableType(attribute, full=True) - html = htmlFromDict(attributeDict, title="HDF5 Attribute") - return html - - def __formatDType(self, dataset): - """Format the numpy dtype""" - return self.__hdf5Formatter.humanReadableType(dataset, full=True) - - def __formatShape(self, dataset): - """Format the shape""" - if dataset.shape is None or len(dataset.shape) <= 1: - return self.__hdf5Formatter.humanReadableShape(dataset) - size = dataset.size - shape = self.__hdf5Formatter.humanReadableShape(dataset) - return u"%s = %s" % (shape, size) - - def __formatChunks(self, dataset): - """Format the shape""" - chunks = dataset.chunks - if chunks is None: - return "" - shape = " \u00D7 ".join([str(i) for i in chunks]) - sizes = numpy.product(chunks) - text = "%s = %s" % (shape, sizes) - return text - - def __initProperties(self): - """Initialize the list of available properties according to the defined - h5py-like object.""" - self.__data.clear() - if self.__obj is None: - return - - obj = self.__obj - - hdf5obj = obj - if isinstance(obj, silx.gui.hdf5.H5Node): - hdf5obj = obj.h5py_object - - if silx.io.is_file(hdf5obj): - objectType = "File" - elif silx.io.is_group(hdf5obj): - objectType = "Group" - elif silx.io.is_dataset(hdf5obj): - objectType = "Dataset" - else: - objectType = obj.__class__.__name__ - self.__data.addHeaderRow(headerLabel="HDF5 %s" % objectType) - - SEPARATOR = "::" - - self.__data.addHeaderRow(headerLabel="Path info") - showPhysicalLocation = True - if isinstance(obj, silx.gui.hdf5.H5Node): - # helpful informations if the object come from an HDF5 tree - self.__data.addHeaderValueRow("Basename", lambda x: x.local_basename) - self.__data.addHeaderValueRow("Name", lambda x: x.local_name) - local = lambda x: x.local_filename + SEPARATOR + x.local_name - self.__data.addHeaderValueRow("Local", local) - else: - # it's a real H5py object - self.__data.addHeaderValueRow("Basename", lambda x: os.path.basename(x.name)) - self.__data.addHeaderValueRow("Name", lambda x: x.name) - if obj.file is not None: - self.__data.addHeaderValueRow("File", lambda x: x.file.filename) - if hasattr(obj, "path"): - # That's a link - if hasattr(obj, "filename"): - # External link - link = lambda x: x.filename + SEPARATOR + x.path - else: - # Soft link - link = lambda x: x.path - self.__data.addHeaderValueRow("Link", link) - showPhysicalLocation = False - - # External data (nothing to do with external links) - nExtSources = 0 - firstExtSource = None - extType = None - if silx.io.is_dataset(hdf5obj): - if hasattr(hdf5obj, "is_virtual"): - if hdf5obj.is_virtual: - extSources = hdf5obj.virtual_sources() - if extSources: - firstExtSource = extSources[0].file_name + SEPARATOR + extSources[0].dset_name - extType = "Virtual" - nExtSources = len(extSources) - if hasattr(hdf5obj, "external"): - extSources = hdf5obj.external - if extSources: - firstExtSource = extSources[0][0] - extType = "Raw" - nExtSources = len(extSources) - - if showPhysicalLocation: - def _physical_location(x): - if isinstance(obj, silx.gui.hdf5.H5Node): - return x.physical_filename + SEPARATOR + x.physical_name - elif silx.io.is_file(obj): - return x.filename + SEPARATOR + x.name - elif obj.file is not None: - return x.file.filename + SEPARATOR + x.name - else: - # Guess it is a virtual node - return "No physical location" - - self.__data.addHeaderValueRow("Physical", _physical_location) - - if extType: - def _first_source(x): - # Absolute path - if os.path.isabs(firstExtSource): - return firstExtSource - - # Relative path with respect to the file directory - if isinstance(obj, silx.gui.hdf5.H5Node): - filename = x.physical_filename - elif silx.io.is_file(obj): - filename = x.filename - elif obj.file is not None: - filename = x.file.filename - else: - return firstExtSource - - if firstExtSource[0] == ".": - firstExtSource.pop(0) - return os.path.join(os.path.dirname(filename), firstExtSource) - - self.__data.addHeaderRow(headerLabel="External sources") - self.__data.addHeaderValueRow("Type", extType) - self.__data.addHeaderValueRow("Count", str(nExtSources)) - self.__data.addHeaderValueRow("First", _first_source) - - if hasattr(obj, "dtype"): - - self.__data.addHeaderRow(headerLabel="Data info") - - if hasattr(obj, "id") and hasattr(obj.id, "get_type"): - # display the HDF5 type - self.__data.addHeaderValueRow("HDF5 type", self.__formatHdf5Type) - self.__data.addHeaderValueRow("dtype", self.__formatDType) - if hasattr(obj, "shape"): - self.__data.addHeaderValueRow("shape", self.__formatShape) - if hasattr(obj, "chunks") and obj.chunks is not None: - self.__data.addHeaderValueRow("chunks", self.__formatChunks) - - # relative to compression - # h5py expose compression, compression_opts but are not initialized - # for external plugins, then we use id - # h5py also expose fletcher32 and shuffle attributes, but it is also - # part of the filters - if hasattr(obj, "shape") and hasattr(obj, "id"): - if hasattr(obj.id, "get_create_plist"): - dcpl = obj.id.get_create_plist() - if dcpl.get_nfilters() > 0: - self.__data.addHeaderRow(headerLabel="Compression info") - pos = _CellData(value="Position", isHeader=True) - hdf5id = _CellData(value="HDF5 ID", isHeader=True) - name = _CellData(value="Name", isHeader=True) - options = _CellData(value="Options", isHeader=True) - availability = _CellData(value="", isHeader=True) - self.__data.addRow(pos, hdf5id, name, options, availability) - for index in range(dcpl.get_nfilters()): - filterId, name, options = self.__getFilterInfo(obj, index) - pos = _CellData(value=str(index)) - hdf5id = _CellData(value=str(filterId)) - name = _CellData(value=name) - options = _CellData(value=options) - availability = _CellFilterAvailableData(filterId=filterId) - self.__data.addRow(pos, hdf5id, name, options, availability) - - if hasattr(obj, "attrs"): - if len(obj.attrs) > 0: - self.__data.addHeaderRow(headerLabel="Attributes") - for key in sorted(obj.attrs.keys()): - callback = lambda key, x: self.__formatter.toString(x.attrs[key]) - callbackTooltip = lambda key, x: self.__attributeTooltip(x.attrs[key]) - self.__data.addHeaderValueRow(headerLabel=key, - value=functools.partial(callback, key), - tooltip=functools.partial(callbackTooltip, key)) - - def __getFilterInfo(self, dataset, filterIndex): - """Get a tuple of readable info from dataset filters - - :param h5py.Dataset dataset: A h5py dataset - :param int filterId: - """ - try: - dcpl = dataset.id.get_create_plist() - info = dcpl.get_filter(filterIndex) - filterId, _flags, cdValues, name = info - name = self.__formatter.toString(name) - options = " ".join([self.__formatter.toString(i) for i in cdValues]) - return (filterId, name, options) - except Exception: - _logger.debug("Backtrace", exc_info=True) - return (None, None, None) - - def object(self): - """Returns the internal object modelized. - - :rtype: An h5py-like object - """ - return self.__obj - - def setFormatter(self, formatter): - """Set the formatter object to be used to display data from the model - - :param TextFormatter formatter: Formatter to use - """ - if formatter is self.__formatter: - return - - self.__hdf5Formatter.setTextFormatter(formatter) - - if qt.qVersion() > "4.6": - self.beginResetModel() - - if self.__formatter is not None: - self.__formatter.formatChanged.disconnect(self.__formatChanged) - - self.__formatter = formatter - if self.__formatter is not None: - self.__formatter.formatChanged.connect(self.__formatChanged) - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def getFormatter(self): - """Returns the text formatter used. - - :rtype: TextFormatter - """ - return self.__formatter - - def __formatChanged(self): - """Called when the format changed. - """ - self.reset() - - -class Hdf5TableItemDelegate(HierarchicalTableView.HierarchicalItemDelegate): - """Item delegate the :class:`Hdf5TableView` with read-only text editor""" - - def createEditor(self, parent, option, index): - """See :meth:`QStyledItemDelegate.createEditor`""" - editor = super().createEditor(parent, option, index) - if isinstance(editor, qt.QLineEdit): - editor.setReadOnly(True) - editor.deselect() - editor.textChanged.connect(self.__textChanged, qt.Qt.QueuedConnection) - self.installEventFilter(editor) - return editor - - def __textChanged(self, text): - sender = self.sender() - if sender is not None: - sender.deselect() - - def eventFilter(self, watched, event): - eventType = event.type() - if eventType == qt.QEvent.FocusIn: - watched.selectAll() - qt.QTimer.singleShot(0, watched.selectAll) - elif eventType == qt.QEvent.FocusOut: - watched.deselect() - return super().eventFilter(watched, event) - - -class Hdf5TableView(HierarchicalTableView.HierarchicalTableView): - """A widget to display metadata about a HDF5 node using a table.""" - - def __init__(self, parent=None): - super(Hdf5TableView, self).__init__(parent) - self.setModel(Hdf5TableModel(self)) - self.setItemDelegate(Hdf5TableItemDelegate(self)) - self.setSelectionMode(qt.QAbstractItemView.NoSelection) - - def isSupportedData(self, data): - """ - Returns true if the provided object can be modelized using this model. - """ - return self.model().isSupportedObject(data) - - def setData(self, data): - """Set the h5py-like object exposed by the model - - :param data: A h5py-like object. It can be a `h5py.Dataset`, - a `h5py.File`, a `h5py.Group`. It also can be a, - `silx.gui.hdf5.H5Node` which is needed to display some local path - information. - """ - model = self.model() - - model.setObject(data) - header = self.horizontalHeader() - if qt.qVersion() < "5.0": - setResizeMode = header.setResizeMode - else: - setResizeMode = header.setSectionResizeMode - setResizeMode(0, qt.QHeaderView.Fixed) - setResizeMode(1, qt.QHeaderView.ResizeToContents) - setResizeMode(2, qt.QHeaderView.Stretch) - setResizeMode(3, qt.QHeaderView.ResizeToContents) - setResizeMode(4, qt.QHeaderView.ResizeToContents) - header.setStretchLastSection(False) - - for row in range(model.rowCount()): - for column in range(model.columnCount()): - index = model.index(row, column) - if (index.isValid() and index.data( - HierarchicalTableView.HierarchicalTableModel.IsHeaderRole) is False): - self.openPersistentEditor(index) diff --git a/silx/gui/data/HexaTableView.py b/silx/gui/data/HexaTableView.py deleted file mode 100644 index 1617f0a..0000000 --- a/silx/gui/data/HexaTableView.py +++ /dev/null @@ -1,286 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -This module defines model and widget to display raw data using an -hexadecimal viewer. -""" -from __future__ import division - -import collections - -import numpy -import six - -from silx.gui import qt -import silx.io.utils -from silx.gui.widgets.TableWidget import CopySelectedCellsAction - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "23/05/2018" - - -class _VoidConnector(object): - """Byte connector to a numpy.void data. - - It uses a cache of 32 x 1KB and a direct read access API from HDF5. - """ - - def __init__(self, data): - self.__cache = collections.OrderedDict() - self.__len = data.itemsize - self.__data = data - - def __getBuffer(self, bufferId): - if bufferId not in self.__cache: - pos = bufferId << 10 - data = self.__data - if hasattr(data, "tobytes"): - data = data.tobytes()[pos:pos + 1024] - else: - # Old fashion - data = data.data[pos:pos + 1024] - - self.__cache[bufferId] = data - if len(self.__cache) > 32: - self.__cache.popitem() - else: - data = self.__cache[bufferId] - return data - - def __getitem__(self, pos): - """Returns the value of the byte at the given position. - - :param uint pos: Position of the byte - :rtype: int - """ - bufferId = pos >> 10 - bufferPos = pos & 0b1111111111 - data = self.__getBuffer(bufferId) - value = data[bufferPos] - if six.PY2: - return ord(value) - else: - return value - - def __len__(self): - """ - Returns the number of available bytes. - - :rtype: uint - """ - return self.__len - - -class HexaTableModel(qt.QAbstractTableModel): - """This data model provides access to a numpy void data. - - Bytes are displayed one by one as a hexadecimal viewer. - - The 16th first columns display bytes as hexadecimal, the last column - displays the same data as ASCII. - - :param qt.QObject parent: Parent object - :param data: A numpy array or a h5py dataset - """ - def __init__(self, parent=None, data=None): - qt.QAbstractTableModel.__init__(self, parent) - - self.__data = None - self.__connector = None - self.setArrayData(data) - - if hasattr(qt.QFontDatabase, "systemFont"): - self.__font = qt.QFontDatabase.systemFont(qt.QFontDatabase.FixedFont) - else: - self.__font = qt.QFont("Monospace") - self.__font.setStyleHint(qt.QFont.TypeWriter) - self.__palette = qt.QPalette() - - def rowCount(self, parent_idx=None): - """Returns number of rows to be displayed in table""" - if self.__connector is None: - return 0 - return ((len(self.__connector) - 1) >> 4) + 1 - - def columnCount(self, parent_idx=None): - """Returns number of columns to be displayed in table""" - return 0x10 + 1 - - def data(self, index, role=qt.Qt.DisplayRole): - """QAbstractTableModel method to access data values - in the format ready to be displayed""" - if not index.isValid(): - return None - - if self.__connector is None: - return None - - row = index.row() - column = index.column() - - if role == qt.Qt.DisplayRole: - if column == 0x10: - start = (row << 4) - text = "" - for i in range(0x10): - pos = start + i - if pos >= len(self.__connector): - break - value = self.__connector[pos] - if value > 0x20 and value < 0x7F: - text += chr(value) - else: - text += "." - return text - else: - pos = (row << 4) + column - if pos < len(self.__connector): - value = self.__connector[pos] - return "%02X" % value - else: - return "" - elif role == qt.Qt.FontRole: - return self.__font - - elif role == qt.Qt.BackgroundColorRole: - pos = (row << 4) + column - if column != 0x10 and pos >= len(self.__connector): - return self.__palette.color(qt.QPalette.Disabled, qt.QPalette.Background) - else: - return None - - return None - - def headerData(self, section, orientation, role=qt.Qt.DisplayRole): - """Returns the 0-based row or column index, for display in the - horizontal and vertical headers""" - if section == -1: - # PyQt4 send -1 when there is columns but no rows - return None - - if role == qt.Qt.DisplayRole: - if orientation == qt.Qt.Vertical: - return "%02X" % (section << 4) - if orientation == qt.Qt.Horizontal: - if section == 0x10: - return "ASCII" - else: - return "%02X" % section - elif role == qt.Qt.FontRole: - return self.__font - elif role == qt.Qt.TextAlignmentRole: - if orientation == qt.Qt.Vertical: - return qt.Qt.AlignRight - if orientation == qt.Qt.Horizontal: - if section == 0x10: - return qt.Qt.AlignLeft - else: - return qt.Qt.AlignCenter - return None - - def flags(self, index): - """QAbstractTableModel method to inform the view whether data - is editable or not. - """ - row = index.row() - column = index.column() - pos = (row << 4) + column - if column != 0x10 and pos >= len(self.__connector): - return qt.Qt.NoItemFlags - return qt.QAbstractTableModel.flags(self, index) - - def setArrayData(self, data): - """Set the data array. - - :param data: A numpy object or a dataset. - """ - if qt.qVersion() > "4.6": - self.beginResetModel() - - self.__connector = None - self.__data = data - if self.__data is not None: - if silx.io.utils.is_dataset(self.__data): - data = data[()] - elif isinstance(self.__data, numpy.ndarray): - data = data[()] - self.__connector = _VoidConnector(data) - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def arrayData(self): - """Returns the internal data. - - :rtype: numpy.ndarray of h5py.Dataset - """ - return self.__data - - -class HexaTableView(qt.QTableView): - """TableView using HexaTableModel as default model. - - It customs the column size to provide a better layout. - """ - def __init__(self, parent=None): - """ - Constructor - - :param qt.QWidget parent: parent QWidget - """ - qt.QTableView.__init__(self, parent) - - model = HexaTableModel(self) - self.setModel(model) - self._copyAction = CopySelectedCellsAction(self) - self.addAction(self._copyAction) - - def copy(self): - self._copyAction.trigger() - - def setArrayData(self, data): - """Set the data array. - - :param data: A numpy object or a dataset. - """ - self.model().setArrayData(data) - self.__fixHeader() - - def __fixHeader(self): - """Update the view according to the state of the auto-resize""" - header = self.horizontalHeader() - if qt.qVersion() < "5.0": - setResizeMode = header.setResizeMode - else: - setResizeMode = header.setSectionResizeMode - - header.setDefaultSectionSize(30) - header.setStretchLastSection(True) - for i in range(0x10): - setResizeMode(i, qt.QHeaderView.Fixed) - setResizeMode(0x10, qt.QHeaderView.Stretch) diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py deleted file mode 100644 index be7d0e3..0000000 --- a/silx/gui/data/NXdataWidgets.py +++ /dev/null @@ -1,1081 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines widgets used by _NXdataView. -""" -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "12/11/2018" - -import logging -import numpy - -from silx.gui import qt -from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector -from silx.gui.plot import Plot1D, Plot2D, StackView, ScatterView -from silx.gui.plot.ComplexImageView import ComplexImageView -from silx.gui.colors import Colormap -from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser - -from silx.math.calibration import ArrayCalibration, NoCalibration, LinearCalibration - - -_logger = logging.getLogger(__name__) - - -class ArrayCurvePlot(qt.QWidget): - """ - Widget for plotting a curve from a multi-dimensional signal array - and a 1D axis array. - - The signal array can have an arbitrary number of dimensions, the only - limitation being that the last dimension must have the same length as - the axis array. - - The widget provides sliders to select indices on the first (n - 1) - dimensions of the signal array, and buttons to add/replace selected - curves to the plot. - - This widget also handles simple 2D or 3D scatter plots (third dimension - displayed as colour of points). - """ - def __init__(self, parent=None): - """ - - :param parent: Parent QWidget - """ - super(ArrayCurvePlot, self).__init__(parent) - - self.__signals = None - self.__signals_names = None - self.__signal_errors = None - self.__axis = None - self.__axis_name = None - self.__x_axis_errors = None - self.__values = None - - self._plot = Plot1D(self) - - self._selector = NumpyAxesSelector(self) - self._selector.setNamedAxesSelectorVisibility(False) - self.__selector_is_connected = False - - self._plot.sigActiveCurveChanged.connect(self._setYLabelFromActiveLegend) - - layout = qt.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._plot) - layout.addWidget(self._selector) - - self.setLayout(layout) - - def getPlot(self): - """Returns the plot used for the display - - :rtype: Plot1D - """ - return self._plot - - def setCurvesData(self, ys, x=None, - yerror=None, xerror=None, - ylabels=None, xlabel=None, title=None, - xscale=None, yscale=None): - """ - - :param List[ndarray] ys: List of arrays to be represented by the y (vertical) axis. - It can be multiple n-D array whose last dimension must - have the same length as x (and values must be None) - :param ndarray x: 1-D dataset used as the curve's x values. If provided, - its lengths must be equal to the length of the last dimension of - ``y`` (and equal to the length of ``value``, for a scatter plot). - :param ndarray yerror: Single array of errors for y (same shape), or None. - There can only be one array, and it applies to the first/main y - (no y errors for auxiliary_signals curves). - :param ndarray xerror: 1-D dataset of errors for x, or None - :param str ylabels: Labels for each curve's Y axis - :param str xlabel: Label for X axis - :param str title: Graph title - :param str xscale: Scale of X axis in (None, 'linear', 'log') - :param str yscale: Scale of Y axis in (None, 'linear', 'log') - """ - self.__signals = ys - self.__signals_names = ylabels or (["Y"] * len(ys)) - self.__signal_errors = yerror - self.__axis = x - self.__axis_name = xlabel - self.__x_axis_errors = xerror - - if self.__selector_is_connected: - self._selector.selectionChanged.disconnect(self._updateCurve) - self.__selector_is_connected = False - self._selector.setData(ys[0]) - self._selector.setAxisNames(["Y"]) - - if len(ys[0].shape) < 2: - self._selector.hide() - else: - self._selector.show() - - self._plot.setGraphTitle(title or "") - if xscale is not None: - self._plot.getXAxis().setScale( - 'log' if xscale == 'log' else 'linear') - if yscale is not None: - self._plot.getYAxis().setScale( - 'log' if yscale == 'log' else 'linear') - self._updateCurve() - - if not self.__selector_is_connected: - self._selector.selectionChanged.connect(self._updateCurve) - self.__selector_is_connected = True - - def _updateCurve(self): - selection = self._selector.selection() - ys = [sig[selection] for sig in self.__signals] - y0 = ys[0] - len_y = len(y0) - x = self.__axis - if x is None: - x = numpy.arange(len_y) - elif numpy.isscalar(x) or len(x) == 1: - # constant axis - x = x * numpy.ones_like(y0) - elif len(x) == 2 and len_y != 2: - # linear calibration a + b * x - x = x[0] + x[1] * numpy.arange(len_y) - - self._plot.remove(kind=("curve",)) - - for i in range(len(self.__signals)): - legend = self.__signals_names[i] - - # errors only supported for primary signal in NXdata - y_errors = None - if i == 0 and self.__signal_errors is not None: - y_errors = self.__signal_errors[self._selector.selection()] - self._plot.addCurve(x, ys[i], legend=legend, - xerror=self.__x_axis_errors, - yerror=y_errors) - if i == 0: - self._plot.setActiveCurve(legend) - - self._plot.resetZoom() - self._plot.getXAxis().setLabel(self.__axis_name) - self._plot.getYAxis().setLabel(self.__signals_names[0]) - - def _setYLabelFromActiveLegend(self, previous_legend, new_legend): - for ylabel in self.__signals_names: - if new_legend is not None and new_legend == ylabel: - self._plot.getYAxis().setLabel(ylabel) - break - - def clear(self): - old = self._selector.blockSignals(True) - self._selector.clear() - self._selector.blockSignals(old) - self._plot.clear() - - -class XYVScatterPlot(qt.QWidget): - """ - Widget for plotting one or more scatters - (with identical x, y coordinates). - """ - def __init__(self, parent=None): - """ - - :param parent: Parent QWidget - """ - super(XYVScatterPlot, self).__init__(parent) - - self.__y_axis = None - """1D array""" - self.__y_axis_name = None - self.__values = None - """List of 1D arrays (for multiple scatters with identical - x, y coordinates)""" - - self.__x_axis = None - self.__x_axis_name = None - self.__x_axis_errors = None - self.__y_axis = None - self.__y_axis_name = None - self.__y_axis_errors = None - - self._plot = ScatterView(self) - self._plot.setColormap(Colormap(name="viridis", - vmin=None, vmax=None, - normalization=Colormap.LINEAR)) - - self._slider = HorizontalSliderWithBrowser(parent=self) - self._slider.setMinimum(0) - self._slider.setValue(0) - self._slider.valueChanged[int].connect(self._sliderIdxChanged) - self._slider.setToolTip("Select auxiliary signals") - - layout = qt.QGridLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._plot, 0, 0) - layout.addWidget(self._slider, 1, 0) - - self.setLayout(layout) - - def _sliderIdxChanged(self, value): - self._updateScatter() - - def getScatterView(self): - """Returns the :class:`ScatterView` used for the display - - :rtype: ScatterView - """ - return self._plot - - def getPlot(self): - """Returns the plot used for the display - - :rtype: PlotWidget - """ - return self._plot.getPlotWidget() - - def setScattersData(self, y, x, values, - yerror=None, xerror=None, - ylabel=None, xlabel=None, - title="", scatter_titles=None, - xscale=None, yscale=None): - """ - - :param ndarray y: 1D array for y (vertical) coordinates. - :param ndarray x: 1D array for x coordinates. - :param List[ndarray] values: List of 1D arrays of values. - This will be used to compute the color map and assign colors - to the points. There should be as many arrays in the list as - scatters to be represented. - :param ndarray yerror: 1D array of errors for y (same shape), or None. - :param ndarray xerror: 1D array of errors for x, or None - :param str ylabel: Label for Y axis - :param str xlabel: Label for X axis - :param str title: Main graph title - :param List[str] scatter_titles: Subtitles (one per scatter) - :param str xscale: Scale of X axis in (None, 'linear', 'log') - :param str yscale: Scale of Y axis in (None, 'linear', 'log') - """ - self.__y_axis = y - self.__x_axis = x - self.__x_axis_name = xlabel or "X" - self.__y_axis_name = ylabel or "Y" - self.__x_axis_errors = xerror - self.__y_axis_errors = yerror - self.__values = values - - self.__graph_title = title or "" - self.__scatter_titles = scatter_titles - - self._slider.valueChanged[int].disconnect(self._sliderIdxChanged) - self._slider.setMaximum(len(values) - 1) - if len(values) > 1: - self._slider.show() - else: - self._slider.hide() - self._slider.setValue(0) - self._slider.valueChanged[int].connect(self._sliderIdxChanged) - - if xscale is not None: - self._plot.getXAxis().setScale( - 'log' if xscale == 'log' else 'linear') - if yscale is not None: - self._plot.getYAxis().setScale( - 'log' if yscale == 'log' else 'linear') - - self._updateScatter() - - def _updateScatter(self): - x = self.__x_axis - y = self.__y_axis - - idx = self._slider.value() - - if self.__graph_title: - title = self.__graph_title # main NXdata @title - if len(self.__scatter_titles) > 1: - # Append dataset name only when there is many datasets - title += '\n' + self.__scatter_titles[idx] - else: - title = self.__scatter_titles[idx] # scatter dataset name - - self._plot.setGraphTitle(title) - self._plot.setData(x, y, self.__values[idx], - xerror=self.__x_axis_errors, - yerror=self.__y_axis_errors) - self._plot.resetZoom() - self._plot.getXAxis().setLabel(self.__x_axis_name) - self._plot.getYAxis().setLabel(self.__y_axis_name) - - def clear(self): - self._plot.getPlotWidget().clear() - - -class ArrayImagePlot(qt.QWidget): - """ - Widget for plotting an image from a multi-dimensional signal array - and two 1D axes array. - - The signal array can have an arbitrary number of dimensions, the only - limitation being that the last two dimensions must have the same length as - the axes arrays. - - Sliders are provided to select indices on the first (n - 2) dimensions of - the signal array, and the plot is updated to show the image corresponding - to the selection. - - If one or both of the axes does not have regularly spaced values, the - the image is plotted as a coloured scatter plot. - """ - def __init__(self, parent=None): - """ - - :param parent: Parent QWidget - """ - super(ArrayImagePlot, self).__init__(parent) - - self.__signals = None - self.__signals_names = None - self.__x_axis = None - self.__x_axis_name = None - self.__y_axis = None - self.__y_axis_name = None - - self._plot = Plot2D(self) - self._plot.setDefaultColormap(Colormap(name="viridis", - vmin=None, vmax=None, - normalization=Colormap.LINEAR)) - self._plot.getIntensityHistogramAction().setVisible(True) - self._plot.setKeepDataAspectRatio(True) - maskToolWidget = self._plot.getMaskToolsDockWidget().widget() - maskToolWidget.setItemMaskUpdated(True) - - # not closable - self._selector = NumpyAxesSelector(self) - self._selector.setNamedAxesSelectorVisibility(False) - self._selector.selectionChanged.connect(self._updateImage) - - self._auxSigSlider = HorizontalSliderWithBrowser(parent=self) - self._auxSigSlider.setMinimum(0) - self._auxSigSlider.setValue(0) - self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged) - self._auxSigSlider.setToolTip("Select auxiliary signals") - - layout = qt.QVBoxLayout() - layout.addWidget(self._plot) - layout.addWidget(self._selector) - layout.addWidget(self._auxSigSlider) - - self.setLayout(layout) - - def _sliderIdxChanged(self, value): - self._updateImage() - - def getPlot(self): - """Returns the plot used for the display - - :rtype: Plot2D - """ - return self._plot - - def setImageData(self, signals, - x_axis=None, y_axis=None, - signals_names=None, - xlabel=None, ylabel=None, - title=None, isRgba=False, - xscale=None, yscale=None): - """ - - :param signals: list of n-D datasets, whose last 2 dimensions are used as the - image's values, or list of 3D datasets interpreted as RGBA image. - :param x_axis: 1-D dataset used as the image's x coordinates. If - provided, its lengths must be equal to the length of the last - dimension of ``signal``. - :param y_axis: 1-D dataset used as the image's y. If provided, - its lengths must be equal to the length of the 2nd to last - dimension of ``signal``. - :param signals_names: Names for each image, used as subtitle and legend. - :param xlabel: Label for X axis - :param ylabel: Label for Y axis - :param title: Graph title - :param isRgba: True if data is a 3D RGBA image - :param str xscale: Scale of X axis in (None, 'linear', 'log') - :param str yscale: Scale of Y axis in (None, 'linear', 'log') - """ - self._selector.selectionChanged.disconnect(self._updateImage) - self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) - - self.__signals = signals - self.__signals_names = signals_names - self.__x_axis = x_axis - self.__x_axis_name = xlabel - self.__y_axis = y_axis - self.__y_axis_name = ylabel - self.__title = title - - self._selector.clear() - if not isRgba: - self._selector.setAxisNames(["Y", "X"]) - img_ndim = 2 - else: - self._selector.setAxisNames(["Y", "X", "RGB(A) channel"]) - img_ndim = 3 - self._selector.setData(signals[0]) - - if len(signals[0].shape) <= img_ndim: - self._selector.hide() - else: - self._selector.show() - - self._auxSigSlider.setMaximum(len(signals) - 1) - if len(signals) > 1: - self._auxSigSlider.show() - else: - self._auxSigSlider.hide() - self._auxSigSlider.setValue(0) - - self._axis_scales = xscale, yscale - self._updateImage() - self._plot.resetZoom() - - self._selector.selectionChanged.connect(self._updateImage) - self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) - - def _updateImage(self): - selection = self._selector.selection() - auxSigIdx = self._auxSigSlider.value() - - legend = self.__signals_names[auxSigIdx] - - images = [img[selection] for img in self.__signals] - image = images[auxSigIdx] - - x_axis = self.__x_axis - y_axis = self.__y_axis - - if x_axis is None and y_axis is None: - xcalib = NoCalibration() - ycalib = NoCalibration() - else: - if x_axis is None: - # no calibration - x_axis = numpy.arange(image.shape[1]) - elif numpy.isscalar(x_axis) or len(x_axis) == 1: - # constant axis - x_axis = x_axis * numpy.ones((image.shape[1], )) - elif len(x_axis) == 2: - # linear calibration - x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1] - - if y_axis is None: - y_axis = numpy.arange(image.shape[0]) - elif numpy.isscalar(y_axis) or len(y_axis) == 1: - y_axis = y_axis * numpy.ones((image.shape[0], )) - elif len(y_axis) == 2: - y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1] - - xcalib = ArrayCalibration(x_axis) - ycalib = ArrayCalibration(y_axis) - - self._plot.remove(kind=("scatter", "image",)) - if xcalib.is_affine() and ycalib.is_affine(): - # regular image - xorigin, xscale = xcalib(0), xcalib.get_slope() - yorigin, yscale = ycalib(0), ycalib.get_slope() - origin = (xorigin, yorigin) - scale = (xscale, yscale) - - self._plot.getXAxis().setScale('linear') - self._plot.getYAxis().setScale('linear') - self._plot.addImage(image, legend=legend, - origin=origin, scale=scale, - replace=True, resetzoom=False) - else: - xaxisscale, yaxisscale = self._axis_scales - - if xaxisscale is not None: - self._plot.getXAxis().setScale( - 'log' if xaxisscale == 'log' else 'linear') - if yaxisscale is not None: - self._plot.getYAxis().setScale( - 'log' if yaxisscale == 'log' else 'linear') - - scatterx, scattery = numpy.meshgrid(x_axis, y_axis) - # fixme: i don't think this can handle "irregular" RGBA images - self._plot.addScatter(numpy.ravel(scatterx), - numpy.ravel(scattery), - numpy.ravel(image), - legend=legend) - - if self.__title: - title = self.__title - if len(self.__signals_names) > 1: - # Append dataset name only when there is many datasets - title += '\n' + self.__signals_names[auxSigIdx] - else: - title = self.__signals_names[auxSigIdx] - self._plot.setGraphTitle(title) - self._plot.getXAxis().setLabel(self.__x_axis_name) - self._plot.getYAxis().setLabel(self.__y_axis_name) - - def clear(self): - old = self._selector.blockSignals(True) - self._selector.clear() - self._selector.blockSignals(old) - self._plot.clear() - - -class ArrayComplexImagePlot(qt.QWidget): - """ - Widget for plotting an image of complex from a multi-dimensional signal array - and two 1D axes array. - - The signal array can have an arbitrary number of dimensions, the only - limitation being that the last two dimensions must have the same length as - the axes arrays. - - Sliders are provided to select indices on the first (n - 2) dimensions of - the signal array, and the plot is updated to show the image corresponding - to the selection. - - If one or both of the axes does not have regularly spaced values, the - the image is plotted as a coloured scatter plot. - """ - def __init__(self, parent=None, colormap=None): - """ - - :param parent: Parent QWidget - """ - super(ArrayComplexImagePlot, self).__init__(parent) - - self.__signals = None - self.__signals_names = None - self.__x_axis = None - self.__x_axis_name = None - self.__y_axis = None - self.__y_axis_name = None - - self._plot = ComplexImageView(self) - if colormap is not None: - for mode in (ComplexImageView.ComplexMode.ABSOLUTE, - ComplexImageView.ComplexMode.SQUARE_AMPLITUDE, - ComplexImageView.ComplexMode.REAL, - ComplexImageView.ComplexMode.IMAGINARY): - self._plot.setColormap(colormap, mode) - - self._plot.getPlot().getIntensityHistogramAction().setVisible(True) - self._plot.setKeepDataAspectRatio(True) - maskToolWidget = self._plot.getPlot().getMaskToolsDockWidget().widget() - maskToolWidget.setItemMaskUpdated(True) - - # not closable - self._selector = NumpyAxesSelector(self) - self._selector.setNamedAxesSelectorVisibility(False) - self._selector.selectionChanged.connect(self._updateImage) - - self._auxSigSlider = HorizontalSliderWithBrowser(parent=self) - self._auxSigSlider.setMinimum(0) - self._auxSigSlider.setValue(0) - self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged) - self._auxSigSlider.setToolTip("Select auxiliary signals") - - layout = qt.QVBoxLayout() - layout.addWidget(self._plot) - layout.addWidget(self._selector) - layout.addWidget(self._auxSigSlider) - - self.setLayout(layout) - - def _sliderIdxChanged(self, value): - self._updateImage() - - def getPlot(self): - """Returns the plot used for the display - - :rtype: PlotWidget - """ - return self._plot.getPlot() - - def setImageData(self, signals, - x_axis=None, y_axis=None, - signals_names=None, - xlabel=None, ylabel=None, - title=None): - """ - - :param signals: list of n-D datasets, whose last 2 dimensions are used as the - image's values, or list of 3D datasets interpreted as RGBA image. - :param x_axis: 1-D dataset used as the image's x coordinates. If - provided, its lengths must be equal to the length of the last - dimension of ``signal``. - :param y_axis: 1-D dataset used as the image's y. If provided, - its lengths must be equal to the length of the 2nd to last - dimension of ``signal``. - :param signals_names: Names for each image, used as subtitle and legend. - :param xlabel: Label for X axis - :param ylabel: Label for Y axis - :param title: Graph title - """ - self._selector.selectionChanged.disconnect(self._updateImage) - self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) - - self.__signals = signals - self.__signals_names = signals_names - self.__x_axis = x_axis - self.__x_axis_name = xlabel - self.__y_axis = y_axis - self.__y_axis_name = ylabel - self.__title = title - - self._selector.clear() - self._selector.setAxisNames(["Y", "X"]) - self._selector.setData(signals[0]) - - if len(signals[0].shape) <= 2: - self._selector.hide() - else: - self._selector.show() - - self._auxSigSlider.setMaximum(len(signals) - 1) - if len(signals) > 1: - self._auxSigSlider.show() - else: - self._auxSigSlider.hide() - self._auxSigSlider.setValue(0) - - self._updateImage() - self._plot.getPlot().resetZoom() - - self._selector.selectionChanged.connect(self._updateImage) - self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) - - def _updateImage(self): - selection = self._selector.selection() - auxSigIdx = self._auxSigSlider.value() - - images = [img[selection] for img in self.__signals] - image = images[auxSigIdx] - - x_axis = self.__x_axis - y_axis = self.__y_axis - - if x_axis is None and y_axis is None: - xcalib = NoCalibration() - ycalib = NoCalibration() - else: - if x_axis is None: - # no calibration - x_axis = numpy.arange(image.shape[1]) - elif numpy.isscalar(x_axis) or len(x_axis) == 1: - # constant axis - x_axis = x_axis * numpy.ones((image.shape[1], )) - elif len(x_axis) == 2: - # linear calibration - x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1] - - if y_axis is None: - y_axis = numpy.arange(image.shape[0]) - elif numpy.isscalar(y_axis) or len(y_axis) == 1: - y_axis = y_axis * numpy.ones((image.shape[0], )) - elif len(y_axis) == 2: - y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1] - - xcalib = ArrayCalibration(x_axis) - ycalib = ArrayCalibration(y_axis) - - self._plot.setData(image) - if xcalib.is_affine(): - xorigin, xscale = xcalib(0), xcalib.get_slope() - else: - _logger.warning("Unsupported complex image X axis calibration") - xorigin, xscale = 0., 1. - - if ycalib.is_affine(): - yorigin, yscale = ycalib(0), ycalib.get_slope() - else: - _logger.warning("Unsupported complex image Y axis calibration") - yorigin, yscale = 0., 1. - - self._plot.setOrigin((xorigin, yorigin)) - self._plot.setScale((xscale, yscale)) - - if self.__title: - title = self.__title - if len(self.__signals_names) > 1: - # Append dataset name only when there is many datasets - title += '\n' + self.__signals_names[auxSigIdx] - else: - title = self.__signals_names[auxSigIdx] - self._plot.setGraphTitle(title) - self._plot.getXAxis().setLabel(self.__x_axis_name) - self._plot.getYAxis().setLabel(self.__y_axis_name) - - def clear(self): - old = self._selector.blockSignals(True) - self._selector.clear() - self._selector.blockSignals(old) - self._plot.setData(None) - - -class ArrayStackPlot(qt.QWidget): - """ - Widget for plotting a n-D array (n >= 3) as a stack of images. - Three axis arrays can be provided to calibrate the axes. - - The signal array can have an arbitrary number of dimensions, the only - limitation being that the last 3 dimensions must have the same length as - the axes arrays. - - Sliders are provided to select indices on the first (n - 3) dimensions of - the signal array, and the plot is updated to load the stack corresponding - to the selection. - """ - def __init__(self, parent=None): - """ - - :param parent: Parent QWidget - """ - super(ArrayStackPlot, self).__init__(parent) - - self.__signal = None - self.__signal_name = None - # the Z, Y, X axes apply to the last three dimensions of the signal - # (in that order) - self.__z_axis = None - self.__z_axis_name = None - self.__y_axis = None - self.__y_axis_name = None - self.__x_axis = None - self.__x_axis_name = None - - self._stack_view = StackView(self) - maskToolWidget = self._stack_view.getPlotWidget().getMaskToolsDockWidget().widget() - maskToolWidget.setItemMaskUpdated(True) - - self._hline = qt.QFrame(self) - self._hline.setFrameStyle(qt.QFrame.HLine) - self._hline.setFrameShadow(qt.QFrame.Sunken) - self._legend = qt.QLabel(self) - self._selector = NumpyAxesSelector(self) - self._selector.setNamedAxesSelectorVisibility(False) - self.__selector_is_connected = False - - layout = qt.QVBoxLayout() - layout.addWidget(self._stack_view) - layout.addWidget(self._hline) - layout.addWidget(self._legend) - layout.addWidget(self._selector) - - self.setLayout(layout) - - def getStackView(self): - """Returns the plot used for the display - - :rtype: StackView - """ - return self._stack_view - - def setStackData(self, signal, - x_axis=None, y_axis=None, z_axis=None, - signal_name=None, - xlabel=None, ylabel=None, zlabel=None, - title=None): - """ - - :param signal: n-D dataset, whose last 3 dimensions are used as the - 3D stack values. - :param x_axis: 1-D dataset used as the image's x coordinates. If - provided, its lengths must be equal to the length of the last - dimension of ``signal``. - :param y_axis: 1-D dataset used as the image's y. If provided, - its lengths must be equal to the length of the 2nd to last - dimension of ``signal``. - :param z_axis: 1-D dataset used as the image's z. If provided, - its lengths must be equal to the length of the 3rd to last - dimension of ``signal``. - :param signal_name: Label used in the legend - :param xlabel: Label for X axis - :param ylabel: Label for Y axis - :param zlabel: Label for Z axis - :param title: Graph title - """ - if self.__selector_is_connected: - self._selector.selectionChanged.disconnect(self._updateStack) - self.__selector_is_connected = False - - self.__signal = signal - self.__signal_name = signal_name or "" - self.__x_axis = x_axis - self.__x_axis_name = xlabel - self.__y_axis = y_axis - self.__y_axis_name = ylabel - self.__z_axis = z_axis - self.__z_axis_name = zlabel - - self._selector.setData(signal) - self._selector.setAxisNames(["Y", "X", "Z"]) - - self._stack_view.setGraphTitle(title or "") - # by default, the z axis is the image position (dimension not plotted) - self._stack_view.getPlotWidget().getXAxis().setLabel(self.__x_axis_name or "X") - self._stack_view.getPlotWidget().getYAxis().setLabel(self.__y_axis_name or "Y") - - self._updateStack() - - ndims = len(signal.shape) - self._stack_view.setFirstStackDimension(ndims - 3) - - # the legend label shows the selection slice producing the volume - # (only interesting for ndim > 3) - if ndims > 3: - self._selector.setVisible(True) - self._legend.setVisible(True) - self._hline.setVisible(True) - else: - self._selector.setVisible(False) - self._legend.setVisible(False) - self._hline.setVisible(False) - - if not self.__selector_is_connected: - self._selector.selectionChanged.connect(self._updateStack) - self.__selector_is_connected = True - - @staticmethod - def _get_origin_scale(axis): - """Assuming axis is a regularly spaced 1D array, - return a tuple (origin, scale) where: - - origin = axis[0] - - scale = (axis[n-1] - axis[0]) / (n -1) - :param axis: 1D numpy array - :return: Tuple (axis[0], (axis[-1] - axis[0]) / (len(axis) - 1)) - """ - return axis[0], (axis[-1] - axis[0]) / (len(axis) - 1) - - def _updateStack(self): - """Update displayed stack according to the current axes selector - data.""" - stk = self._selector.selectedData() - x_axis = self.__x_axis - y_axis = self.__y_axis - z_axis = self.__z_axis - - calibrations = [] - for axis in [z_axis, y_axis, x_axis]: - - if axis is None: - calibrations.append(NoCalibration()) - elif len(axis) == 2: - calibrations.append( - LinearCalibration(y_intercept=axis[0], - slope=axis[1])) - else: - calibrations.append(ArrayCalibration(axis)) - - legend = self.__signal_name + "[" - for sl in self._selector.selection(): - if sl == slice(None): - legend += ":, " - else: - legend += str(sl) + ", " - legend = legend[:-2] + "]" - self._legend.setText("Displayed data: " + legend) - - self._stack_view.setStack(stk, calibrations=calibrations) - self._stack_view.setLabels( - labels=[self.__z_axis_name, - self.__y_axis_name, - self.__x_axis_name]) - - def clear(self): - old = self._selector.blockSignals(True) - self._selector.clear() - self._selector.blockSignals(old) - self._stack_view.clear() - - -class ArrayVolumePlot(qt.QWidget): - """ - Widget for plotting a n-D array (n >= 3) as a 3D scalar field. - Three axis arrays can be provided to calibrate the axes. - - The signal array can have an arbitrary number of dimensions, the only - limitation being that the last 3 dimensions must have the same length as - the axes arrays. - - Sliders are provided to select indices on the first (n - 3) dimensions of - the signal array, and the plot is updated to load the stack corresponding - to the selection. - """ - def __init__(self, parent=None): - """ - - :param parent: Parent QWidget - """ - super(ArrayVolumePlot, self).__init__(parent) - - self.__signal = None - self.__signal_name = None - # the Z, Y, X axes apply to the last three dimensions of the signal - # (in that order) - self.__z_axis = None - self.__z_axis_name = None - self.__y_axis = None - self.__y_axis_name = None - self.__x_axis = None - self.__x_axis_name = None - - from ._VolumeWindow import VolumeWindow - - self._view = VolumeWindow(self) - - self._hline = qt.QFrame(self) - self._hline.setFrameStyle(qt.QFrame.HLine) - self._hline.setFrameShadow(qt.QFrame.Sunken) - self._legend = qt.QLabel(self) - self._selector = NumpyAxesSelector(self) - self._selector.setNamedAxesSelectorVisibility(False) - self.__selector_is_connected = False - - layout = qt.QVBoxLayout() - layout.addWidget(self._view) - layout.addWidget(self._hline) - layout.addWidget(self._legend) - layout.addWidget(self._selector) - - self.setLayout(layout) - - def getVolumeView(self): - """Returns the plot used for the display - - :rtype: SceneWindow - """ - return self._view - - def setData(self, signal, - x_axis=None, y_axis=None, z_axis=None, - signal_name=None, - xlabel=None, ylabel=None, zlabel=None, - title=None): - """ - - :param signal: n-D dataset, whose last 3 dimensions are used as the - 3D stack values. - :param x_axis: 1-D dataset used as the image's x coordinates. If - provided, its lengths must be equal to the length of the last - dimension of ``signal``. - :param y_axis: 1-D dataset used as the image's y. If provided, - its lengths must be equal to the length of the 2nd to last - dimension of ``signal``. - :param z_axis: 1-D dataset used as the image's z. If provided, - its lengths must be equal to the length of the 3rd to last - dimension of ``signal``. - :param signal_name: Label used in the legend - :param xlabel: Label for X axis - :param ylabel: Label for Y axis - :param zlabel: Label for Z axis - :param title: Graph title - """ - if self.__selector_is_connected: - self._selector.selectionChanged.disconnect(self._updateVolume) - self.__selector_is_connected = False - - self.__signal = signal - self.__signal_name = signal_name or "" - self.__x_axis = x_axis - self.__x_axis_name = xlabel - self.__y_axis = y_axis - self.__y_axis_name = ylabel - self.__z_axis = z_axis - self.__z_axis_name = zlabel - - self._selector.setData(signal) - self._selector.setAxisNames(["Y", "X", "Z"]) - - self._updateVolume() - - # the legend label shows the selection slice producing the volume - # (only interesting for ndim > 3) - if signal.ndim > 3: - self._selector.setVisible(True) - self._legend.setVisible(True) - self._hline.setVisible(True) - else: - self._selector.setVisible(False) - self._legend.setVisible(False) - self._hline.setVisible(False) - - if not self.__selector_is_connected: - self._selector.selectionChanged.connect(self._updateVolume) - self.__selector_is_connected = True - - def _updateVolume(self): - """Update displayed stack according to the current axes selector - data.""" - x_axis = self.__x_axis - y_axis = self.__y_axis - z_axis = self.__z_axis - - offset = [] - scale = [] - for axis in [x_axis, y_axis, z_axis]: - if axis is None: - calibration = NoCalibration() - elif len(axis) == 2: - calibration = LinearCalibration( - y_intercept=axis[0], slope=axis[1]) - else: - calibration = ArrayCalibration(axis) - if not calibration.is_affine(): - _logger.warning("Axis has not linear values, ignored") - offset.append(0.) - scale.append(1.) - else: - offset.append(calibration(0)) - scale.append(calibration.get_slope()) - - legend = self.__signal_name + "[" - for sl in self._selector.selection(): - if sl == slice(None): - legend += ":, " - else: - legend += str(sl) + ", " - legend = legend[:-2] + "]" - self._legend.setText("Displayed data: " + legend) - - # Update SceneWidget - data = self._selector.selectedData() - - volumeView = self.getVolumeView() - volumeView.setData(data, offset=offset, scale=scale) - volumeView.setAxesLabels( - self.__x_axis_name, self.__y_axis_name, self.__z_axis_name) - - def clear(self): - old = self._selector.blockSignals(True) - self._selector.clear() - self._selector.blockSignals(old) - self.getVolumeView().clear() diff --git a/silx/gui/data/NumpyAxesSelector.py b/silx/gui/data/NumpyAxesSelector.py deleted file mode 100644 index e6da0d4..0000000 --- a/silx/gui/data/NumpyAxesSelector.py +++ /dev/null @@ -1,578 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines a widget able to convert a numpy array from n-dimensions -to a numpy array with less dimensions. -""" -from __future__ import division - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "29/01/2018" - -import logging -import numpy -import functools -from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser -from silx.gui import qt -from silx.gui.utils import blockSignals -import silx.utils.weakref - - -_logger = logging.getLogger(__name__) - - -class _Axis(qt.QWidget): - """Widget displaying an axis. - - It allows to display and scroll in the axis, and provide a widget to - map the axis with a named axis (the one from the view). - """ - - valueChanged = qt.Signal(int) - """Emitted when the location on the axis change.""" - - axisNameChanged = qt.Signal(object) - """Emitted when the user change the name of the axis.""" - - def __init__(self, parent=None): - """Constructor - - :param parent: Parent of the widget - """ - super(_Axis, self).__init__(parent) - self.__axisNumber = None - self.__customAxisNames = set([]) - self.__label = qt.QLabel(self) - self.__axes = qt.QComboBox(self) - self.__axes.currentIndexChanged[int].connect(self.__axisMappingChanged) - self.__slider = HorizontalSliderWithBrowser(self) - self.__slider.valueChanged[int].connect(self.__sliderValueChanged) - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.__label) - layout.addWidget(self.__axes) - layout.addWidget(self.__slider, 10000) - layout.addStretch(1) - self.setLayout(layout) - - def slider(self): - """Returns the slider used to display axes location. - - :rtype: HorizontalSliderWithBrowser - """ - return self.__slider - - def setAxis(self, number, position, size): - """Set axis information. - - :param int number: The number of the axis (from the original numpy - array) - :param int position: The current position in the axis (for a slicing) - :param int size: The size of this axis (0..n) - """ - self.__label.setText("Dimension %s" % number) - self.__axisNumber = number - self.__slider.setMaximum(size - 1) - - def axisNumber(self): - """Returns the axis number. - - :rtype: int - """ - return self.__axisNumber - - def setAxisName(self, axisName): - """Set the current used axis name. - - If this name is not available an exception is raised. An empty string - means that no name is selected. - - :param str axisName: The new name of the axis - :raise ValueError: When the name is not available - """ - if axisName == "" and self.__axes.count() == 0: - self.__axes.setCurrentIndex(-1) - self.__updateSliderVisibility() - return - - for index in range(self.__axes.count()): - name = self.__axes.itemData(index) - if name == axisName: - self.__axes.setCurrentIndex(index) - self.__updateSliderVisibility() - return - raise ValueError("Axis name '%s' not found", axisName) - - def axisName(self): - """Returns the selected axis name. - - If no name is selected, an empty string is returned. - - :rtype: str - """ - index = self.__axes.currentIndex() - if index == -1: - return "" - return self.__axes.itemData(index) - - def setAxisNames(self, axesNames): - """Set the available list of names for the axis. - - :param List[str] axesNames: List of available names - """ - self.__axes.clear() - with blockSignals(self.__axes): - self.__axes.addItem(" ", "") - for axis in axesNames: - self.__axes.addItem(axis, axis) - - self.__updateSliderVisibility() - - def setCustomAxis(self, axesNames): - """Set the available list of named axis which can be set to a value. - - :param List[str] axesNames: List of customable axis names - """ - self.__customAxisNames = set(axesNames) - self.__updateSliderVisibility() - - def __axisMappingChanged(self, index): - """Called when the selected name change. - - :param int index: Selected index - """ - self.__updateSliderVisibility() - name = self.axisName() - self.axisNameChanged.emit(name) - - def __updateSliderVisibility(self): - """Update the visibility of the slider according to axis names and - customable axis names.""" - name = self.axisName() - isVisible = name == "" or name in self.__customAxisNames - self.__slider.setVisible(isVisible) - - def value(self): - """Returns the currently selected position in the axis. - - :rtype: int - """ - return self.__slider.value() - - def setValue(self, value): - """Set the currently selected position in the axis. - - :param int value: - """ - self.__slider.setValue(value) - - def __sliderValueChanged(self, value): - """Called when the selected position in the axis change. - - :param int value: Position of the axis - """ - self.valueChanged.emit(value) - - def setNamedAxisSelectorVisibility(self, visible): - """Hide or show the named axis combobox. - - If both the selector and the slider are hidden, hide the entire widget. - - :param visible: boolean - """ - self.__axes.setVisible(visible) - name = self.axisName() - self.setVisible(visible or name == "") - - -class NumpyAxesSelector(qt.QWidget): - """Widget to select a view from a numpy array. - - .. image:: img/NumpyAxesSelector.png - - The widget is set with an input data using :meth:`setData`, and a requested - output dimension using :meth:`setAxisNames`. - - Widgets are provided to selected expected input axis, and a slice on the - non-selected axis. - - The final selected array can be reached using the getter - :meth:`selectedData`, and the event `selectionChanged`. - - If the input data is a HDF5 Dataset, the selected output data will be a - new numpy array. - """ - - dataChanged = qt.Signal() - """Emitted when the input data change""" - - selectedAxisChanged = qt.Signal() - """Emitted when the selected axis change""" - - selectionChanged = qt.Signal() - """Emitted when the selected data change""" - - customAxisChanged = qt.Signal(str, int) - """Emitted when a custom axis change""" - - def __init__(self, parent=None): - """Constructor - - :param parent: Parent of the widget - """ - super(NumpyAxesSelector, self).__init__(parent) - - self.__data = None - self.__selectedData = None - self.__axis = [] - self.__axisNames = [] - self.__customAxisNames = set([]) - self.__namedAxesVisibility = True - layout = qt.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSizeConstraint(qt.QLayout.SetMinAndMaxSize) - self.setLayout(layout) - - def clear(self): - """Clear the widget.""" - self.setData(None) - - def setAxisNames(self, axesNames): - """Set the axis names of the output selected data. - - Axis names are defined from slower to faster axis. - - The size of the list will constrain the dimension of the resulting - array. - - :param List[str] axesNames: List of distinct strings identifying axis names - """ - self.__axisNames = list(axesNames) - assert len(set(self.__axisNames)) == len(self.__axisNames),\ - "Non-unique axes names: %s" % self.__axisNames - - delta = len(self.__axis) - len(self.__axisNames) - if delta < 0: - delta = 0 - for index, axis in enumerate(self.__axis): - with blockSignals(axis): - axis.setAxisNames(self.__axisNames) - if index >= delta and index - delta < len(self.__axisNames): - axis.setAxisName(self.__axisNames[index - delta]) - else: - axis.setAxisName("") - self.__updateSelectedData() - - def setCustomAxis(self, axesNames): - """Set the available list of named axis which can be set to a value. - - :param List[str] axesNames: List of customable axis names - """ - self.__customAxisNames = set(axesNames) - for axis in self.__axis: - axis.setCustomAxis(self.__customAxisNames) - - def setData(self, data): - """Set the input data unsed by the widget. - - :param numpy.ndarray data: The input data - """ - if self.__data is not None: - # clean up - for widget in self.__axis: - self.layout().removeWidget(widget) - widget.deleteLater() - self.__axis = [] - - self.__data = data - - if data is not None: - # create expected axes - dimensionNumber = len(data.shape) - delta = dimensionNumber - len(self.__axisNames) - for index in range(dimensionNumber): - axis = _Axis(self) - axis.setAxis(index, 0, data.shape[index]) - axis.setAxisNames(self.__axisNames) - axis.setCustomAxis(self.__customAxisNames) - if index >= delta and index - delta < len(self.__axisNames): - axis.setAxisName(self.__axisNames[index - delta]) - # this weak method was expected to be able to delete sub widget - callback = functools.partial(silx.utils.weakref.WeakMethodProxy(self.__axisValueChanged), axis) - axis.valueChanged.connect(callback) - # this weak method was expected to be able to delete sub widget - callback = functools.partial(silx.utils.weakref.WeakMethodProxy(self.__axisNameChanged), axis) - axis.axisNameChanged.connect(callback) - axis.setNamedAxisSelectorVisibility(self.__namedAxesVisibility) - self.layout().addWidget(axis) - self.__axis.append(axis) - self.__normalizeAxisGeometry() - - self.dataChanged.emit() - self.__updateSelectedData() - - def __normalizeAxisGeometry(self): - """Update axes geometry to align all axes components together.""" - if len(self.__axis) <= 0: - return - lineEditWidth = max([a.slider().lineEdit().minimumSize().width() for a in self.__axis]) - limitWidth = max([a.slider().limitWidget().minimumSizeHint().width() for a in self.__axis]) - for a in self.__axis: - a.slider().lineEdit().setFixedWidth(lineEditWidth) - a.slider().limitWidget().setFixedWidth(limitWidth) - - def __axisValueChanged(self, axis, value): - name = axis.axisName() - if name in self.__customAxisNames: - self.customAxisChanged.emit(name, value) - else: - self.__updateSelectedData() - - def __axisNameChanged(self, axis, name): - """Called when an axis name change. - - :param _Axis axis: The changed axis - :param str name: The new name of the axis - """ - names = [x.axisName() for x in self.__axis] - missingName = set(self.__axisNames) - set(names) - set("") - if len(missingName) == 0: - missingName = None - elif len(missingName) == 1: - missingName = list(missingName)[0] - else: - raise Exception("Unexpected state") - - axisChanged = True - - if axis.axisName() == "": - # set the removed label to another widget if it is possible - availableWidget = None - for widget in self.__axis: - if widget is axis: - continue - if widget.axisName() == "": - availableWidget = widget - break - if availableWidget is None: - # If there is no other solution we set the name at the same place - axisChanged = False - availableWidget = axis - with blockSignals(availableWidget): - availableWidget.setAxisName(missingName) - else: - # there is a duplicated name somewhere - # we swap it with the missing name or with nothing - dupWidget = None - for widget in self.__axis: - if widget is axis: - continue - if widget.axisName() == axis.axisName(): - dupWidget = widget - break - if missingName is None: - missingName = "" - with blockSignals(dupWidget): - dupWidget.setAxisName(missingName) - - if self.__data is None: - return - if axisChanged: - self.selectedAxisChanged.emit() - self.__updateSelectedData() - - def __updateSelectedData(self): - """Update the selected data according to the state of the widget. - - It fires a `selectionChanged` event. - """ - permutation = self.permutation() - - if self.__data is None or permutation is None: - # No data or not all the expected axes are there - if self.__selectedData is not None: - self.__selectedData = None - self.selectionChanged.emit() - return - - # get a view with few fixed dimensions - # with a h5py dataset, it create a copy - # TODO we can reuse the same memory in case of a copy - self.__selectedData = numpy.transpose(self.__data[self.selection()], permutation) - self.selectionChanged.emit() - - def data(self): - """Returns the input data. - - :rtype: Union[numpy.ndarray,None] - """ - if self.__data is None: - return None - else: - return numpy.array(self.__data, copy=False) - - def selectedData(self): - """Returns the output data. - - This is equivalent to:: - - numpy.transpose(self.data()[self.selection()], self.permutation()) - - :rtype: Union[numpy.ndarray,None] - """ - if self.__selectedData is None: - return None - else: - return numpy.array(self.__selectedData, copy=False) - - def permutation(self): - """Returns the axes permutation to convert data subset to selected data. - - If permutation cannot be computer, it returns None. - - :rtype: Union[List[int],None] - """ - if self.__data is None: - return None - else: - indices = [] - for name in self.__axisNames: - index = 0 - for axis in self.__axis: - if axis.axisName() == name: - indices.append(index) - break - if axis.axisName() != "": - index += 1 - else: - _logger.warning("No axis corresponding to: %s", name) - return None - return tuple(indices) - - def selection(self): - """Returns the selection tuple used to slice the data. - - :rtype: tuple - """ - if self.__data is None: - return tuple() - else: - return tuple([axis.value() if axis.axisName() == "" else slice(None) - for axis in self.__axis]) - - def setSelection(self, selection, permutation=None): - """Set the selection along each dimension. - - tuple returned by :meth:`selection` can be provided as input, - provided that it is for the same the number of axes and - the same number of dimensions of the data. - - :param List[Union[int,slice,None]] selection: - The selection tuple with as one element for each dimension of the data. - If an element is None, then the whole dimension is selected. - :param Union[List[int],None] permutation: - The data axes indices to transpose. - If not given, no permutation is applied - :raise ValueError: - When the selection does not match current data shape and number of axes. - """ - data_shape = self.__data.shape if self.__data is not None else () - - # Check selection - if len(selection) != len(data_shape): - raise ValueError( - "Selection length (%d) and data ndim (%d) mismatch" % - (len(selection), len(data_shape))) - - # Check selection type - selectedDataNDim = 0 - for element, size in zip(selection, data_shape): - if isinstance(element, int): - if not 0 <= element < size: - raise ValueError( - "Selected index (%d) outside data dimension range [0-%d]" % - (element, size)) - elif element is None or element == slice(None): - selectedDataNDim += 1 - else: - raise ValueError("Unsupported element in selection: %s" % element) - - ndim = len(self.__axisNames) - if selectedDataNDim != ndim: - raise ValueError( - "Selection dimensions (%d) and number of axes (%d) mismatch" % - (selectedDataNDim, ndim)) - - # check permutation - if permutation is None: - permutation = tuple(range(ndim)) - - if set(permutation) != set(range(ndim)): - raise ValueError( - "Error in provided permutation: " - "Wrong size, elements out of range or duplicates") - - inversePermutation = numpy.argsort(permutation) - - axisNameChanged = False - customValueChanged = [] - with blockSignals(*self.__axis): - index = 0 - for element, axis in zip(selection, self.__axis): - if isinstance(element, int): - name = "" - else: - name = self.__axisNames[inversePermutation[index]] - index += 1 - - if axis.axisName() != name: - axis.setAxisName(name) - axisNameChanged = True - - for element, axis in zip(selection, self.__axis): - value = element if isinstance(element, int) else 0 - if axis.value() != value: - axis.setValue(value) - - name = axis.axisName() - if name in self.__customAxisNames: - customValueChanged.append((name, value)) - - # Send signals that where disabled - if axisNameChanged: - self.selectedAxisChanged.emit() - for name, value in customValueChanged: - self.customAxisChanged.emit(name, value) - self.__updateSelectedData() - - def setNamedAxesSelectorVisibility(self, visible): - """Show or hide the combo-boxes allowing to map the plot axes - to the data dimension. - - :param visible: Boolean - """ - self.__namedAxesVisibility = visible - for axis in self.__axis: - axis.setNamedAxisSelectorVisibility(visible) diff --git a/silx/gui/data/RecordTableView.py b/silx/gui/data/RecordTableView.py deleted file mode 100644 index 2c0011a..0000000 --- a/silx/gui/data/RecordTableView.py +++ /dev/null @@ -1,447 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -This module define model and widget to display 1D slices from numpy -array using compound data types or hdf5 databases. -""" -from __future__ import division - -import itertools -import numpy -from silx.gui import qt -import silx.io -from .TextFormatter import TextFormatter -from silx.gui.widgets.TableWidget import CopySelectedCellsAction - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "29/08/2018" - - -class _MultiLineItem(qt.QItemDelegate): - """Draw a multiline text without hiding anything. - - The paint method display a cell without any wrap. And an editor is - available to scroll into the selected cell. - """ - - def __init__(self, parent=None): - """ - Constructor - - :param qt.QWidget parent: Parent of the widget - """ - qt.QItemDelegate.__init__(self, parent) - self.__textOptions = qt.QTextOption() - self.__textOptions.setFlags(qt.QTextOption.IncludeTrailingSpaces | - qt.QTextOption.ShowTabsAndSpaces) - self.__textOptions.setWrapMode(qt.QTextOption.NoWrap) - self.__textOptions.setAlignment(qt.Qt.AlignTop | qt.Qt.AlignLeft) - - def paint(self, painter, option, index): - """ - Write multiline text without using any wrap or any alignment according - to the cell size. - - :param qt.QPainter painter: Painter context used to displayed the cell - :param qt.QStyleOptionViewItem option: Control how the editor is shown - :param qt.QIndex index: Index of the data to display - """ - painter.save() - - # set colors - painter.setPen(qt.QPen(qt.Qt.NoPen)) - if option.state & qt.QStyle.State_Selected: - brush = option.palette.highlight() - painter.setBrush(brush) - else: - brush = index.data(qt.Qt.BackgroundRole) - if brush is None: - # default background color for a cell - brush = qt.Qt.white - painter.setBrush(brush) - painter.drawRect(option.rect) - - if index.isValid(): - if option.state & qt.QStyle.State_Selected: - brush = option.palette.highlightedText() - else: - brush = index.data(qt.Qt.ForegroundRole) - if brush is None: - brush = option.palette.text() - painter.setPen(qt.QPen(brush.color())) - text = index.data(qt.Qt.DisplayRole) - painter.drawText(qt.QRectF(option.rect), text, self.__textOptions) - - painter.restore() - - def createEditor(self, parent, option, index): - """ - Returns the widget used to edit the item specified by index for editing. - - We use it not to edit the content but to show the content with a - convenient scroll bar. - - :param qt.QWidget parent: Parent of the widget - :param qt.QStyleOptionViewItem option: Control how the editor is shown - :param qt.QIndex index: Index of the data to display - """ - if not index.isValid(): - return super(_MultiLineItem, self).createEditor(parent, option, index) - - editor = qt.QTextEdit(parent) - editor.setReadOnly(True) - return editor - - def setEditorData(self, editor, index): - """ - Read data from the model and feed the editor. - - :param qt.QWidget editor: Editor widget - :param qt.QIndex index: Index of the data to display - """ - text = index.model().data(index, qt.Qt.EditRole) - editor.setText(text) - - def updateEditorGeometry(self, editor, option, index): - """ - Update the geometry of the editor according to the changes of the view. - - :param qt.QWidget editor: Editor widget - :param qt.QStyleOptionViewItem option: Control how the editor is shown - :param qt.QIndex index: Index of the data to display - """ - editor.setGeometry(option.rect) - - -class RecordTableModel(qt.QAbstractTableModel): - """This data model provides access to 1D slices from numpy array using - compound data types or hdf5 databases. - - Each entries are displayed in a single row, and each columns contain a - specific field of the compound type. - - It also allows to display 1D arrays of simple data types. - array. - - :param qt.QObject parent: Parent object - :param numpy.ndarray data: A numpy array or a h5py dataset - """ - - MAX_NUMBER_OF_ROWS = 10e6 - """Maximum number of display values of the dataset""" - - def __init__(self, parent=None, data=None): - qt.QAbstractTableModel.__init__(self, parent) - - self.__data = None - self.__is_array = False - self.__fields = None - self.__formatter = None - self.__editFormatter = None - self.setFormatter(TextFormatter(self)) - - # set _data - self.setArrayData(data) - - # Methods to be implemented to subclass QAbstractTableModel - def rowCount(self, parent_idx=None): - """Returns number of rows to be displayed in table""" - if self.__data is None: - return 0 - elif not self.__is_array: - return 1 - else: - return min(len(self.__data), self.MAX_NUMBER_OF_ROWS) - - def columnCount(self, parent_idx=None): - """Returns number of columns to be displayed in table""" - if self.__fields is None: - return 1 - else: - return len(self.__fields) - - def __clippedData(self, role=qt.Qt.DisplayRole): - """Return data for cells representing clipped data""" - if role == qt.Qt.DisplayRole: - return "..." - elif role == qt.Qt.ToolTipRole: - return "Dataset is too large: display is clipped" - else: - return None - - def data(self, index, role=qt.Qt.DisplayRole): - """QAbstractTableModel method to access data values - in the format ready to be displayed""" - if not index.isValid(): - return None - - if self.__data is None: - return None - - # Special display of one before last data for clipped table - if self.__isClipped() and index.row() == self.rowCount() - 2: - return self.__clippedData(role) - - if self.__is_array: - row = index.row() - if row >= self.rowCount(): - return None - elif self.__isClipped() and row == self.rowCount() - 1: - # Clipped array, display last value at the end - data = self.__data[-1] - else: - data = self.__data[row] - else: - if index.row() > 0: - return None - data = self.__data - - if self.__fields is not None: - if index.column() >= len(self.__fields): - return None - key = self.__fields[index.column()][1] - data = data[key[0]] - if len(key) > 1: - data = data[key[1]] - - # no dtype in case of 1D array of unicode objects (#2093) - dtype = getattr(data, "dtype", None) - - if role == qt.Qt.DisplayRole: - return self.__formatter.toString(data, dtype=dtype) - elif role == qt.Qt.EditRole: - return self.__editFormatter.toString(data, dtype=dtype) - return None - - def headerData(self, section, orientation, role=qt.Qt.DisplayRole): - """Returns the 0-based row or column index, for display in the - horizontal and vertical headers""" - if section == -1: - # PyQt4 send -1 when there is columns but no rows - return None - - # Handle clipping of huge tables - if (self.__isClipped() and - orientation == qt.Qt.Vertical and - section == self.rowCount() - 2): - return self.__clippedData(role) - - if role == qt.Qt.DisplayRole: - if orientation == qt.Qt.Vertical: - if not self.__is_array: - return "Scalar" - elif section == self.MAX_NUMBER_OF_ROWS - 1: - return str(len(self.__data) - 1) - else: - return str(section) - if orientation == qt.Qt.Horizontal: - if self.__fields is None: - if section == 0: - return "Data" - else: - return None - else: - if section < len(self.__fields): - return self.__fields[section][0] - else: - return None - return None - - def flags(self, index): - """QAbstractTableModel method to inform the view whether data - is editable or not. - """ - return qt.QAbstractTableModel.flags(self, index) - - def __isClipped(self) -> bool: - """Returns whether the displayed array is clipped or not""" - return self.__data is not None and self.__is_array and len(self.__data) > self.MAX_NUMBER_OF_ROWS - - def setArrayData(self, data): - """Set the data array and the viewing perspective. - - You can set ``copy=False`` if you need more performances, when dealing - with a large numpy array. In this case, a simple reference to the data - is used to access the data, rather than a copy of the array. - - .. warning:: - - Any change to the data model will affect your original data - array, when using a reference rather than a copy.. - - :param data: 1D numpy array, or any object that can be - converted to a numpy array using ``numpy.array(data)`` (e.g. - a nested sequence). - """ - if qt.qVersion() > "4.6": - self.beginResetModel() - - self.__data = data - if isinstance(data, numpy.ndarray): - self.__is_array = True - elif silx.io.is_dataset(data) and data.shape != tuple(): - self.__is_array = True - else: - self.__is_array = False - - self.__fields = [] - if data is not None: - if data.dtype.fields is not None: - fields = sorted(data.dtype.fields.items(), key=lambda e: e[1][1]) - for name, (dtype, _index) in fields: - if dtype.shape != tuple(): - keys = itertools.product(*[range(x) for x in dtype.shape]) - for key in keys: - label = "%s%s" % (name, list(key)) - array_key = (name, key) - self.__fields.append((label, array_key)) - else: - self.__fields.append((name, (name,))) - else: - self.__fields = None - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def arrayData(self): - """Returns the internal data. - - :rtype: numpy.ndarray of h5py.Dataset - """ - return self.__data - - def setFormatter(self, formatter): - """Set the formatter object to be used to display data from the model - - :param TextFormatter formatter: Formatter to use - """ - if formatter is self.__formatter: - return - - if qt.qVersion() > "4.6": - self.beginResetModel() - - if self.__formatter is not None: - self.__formatter.formatChanged.disconnect(self.__formatChanged) - - self.__formatter = formatter - self.__editFormatter = TextFormatter(formatter) - self.__editFormatter.setUseQuoteForText(False) - - if self.__formatter is not None: - self.__formatter.formatChanged.connect(self.__formatChanged) - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def getFormatter(self): - """Returns the text formatter used. - - :rtype: TextFormatter - """ - return self.__formatter - - def __formatChanged(self): - """Called when the format changed. - """ - self.__editFormatter = TextFormatter(self, self.getFormatter()) - self.__editFormatter.setUseQuoteForText(False) - self.reset() - - -class _ShowEditorProxyModel(qt.QIdentityProxyModel): - """ - Allow to custom the flag edit of the model - """ - - def __init__(self, parent=None): - """ - Constructor - - :param qt.QObject arent: parent object - """ - super(_ShowEditorProxyModel, self).__init__(parent) - self.__forceEditable = False - - def flags(self, index): - flag = qt.QIdentityProxyModel.flags(self, index) - if self.__forceEditable: - flag = flag | qt.Qt.ItemIsEditable - return flag - - def forceCellEditor(self, show): - """ - Enable the editable flag to allow to display cell editor. - """ - if self.__forceEditable == show: - return - self.beginResetModel() - self.__forceEditable = show - self.endResetModel() - - -class RecordTableView(qt.QTableView): - """TableView using DatabaseTableModel as default model. - """ - def __init__(self, parent=None): - """ - Constructor - - :param qt.QWidget parent: parent QWidget - """ - qt.QTableView.__init__(self, parent) - - model = _ShowEditorProxyModel(self) - self._model = RecordTableModel() - model.setSourceModel(self._model) - self.setModel(model) - - self.__multilineView = _MultiLineItem(self) - self.setEditTriggers(qt.QAbstractItemView.AllEditTriggers) - self._copyAction = CopySelectedCellsAction(self) - self.addAction(self._copyAction) - - def copy(self): - self._copyAction.trigger() - - def setArrayData(self, data): - model = self.model() - sourceModel = model.sourceModel() - sourceModel.setArrayData(data) - - if data is not None: - if issubclass(data.dtype.type, (numpy.string_, numpy.unicode_)): - # TODO it would be nice to also fix fields - # but using it only for string array is already very useful - self.setItemDelegateForColumn(0, self.__multilineView) - model.forceCellEditor(True) - else: - self.setItemDelegateForColumn(0, None) - model.forceCellEditor(False) diff --git a/silx/gui/data/TextFormatter.py b/silx/gui/data/TextFormatter.py deleted file mode 100644 index 8fd7c7c..0000000 --- a/silx/gui/data/TextFormatter.py +++ /dev/null @@ -1,395 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This package provides a class sharred by widget from the -data module to format data as text in the same way.""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "24/07/2018" - -import logging -import numbers - -import numpy -import six - -from silx.gui import qt - -import h5py - - -_logger = logging.getLogger(__name__) - - -class TextFormatter(qt.QObject): - """Formatter to convert data to string. - - The method :meth:`toString` returns a formatted string from an input data - using parameters set to this object. - - It support most python and numpy data, expecting dictionary. Unsupported - data are displayed using the string representation of the object (`str`). - - It provides a set of parameters to custom the formatting of integer and - float values (:meth:`setIntegerFormat`, :meth:`setFloatFormat`). - - It also allows to custom the use of quotes to display text data - (:meth:`setUseQuoteForText`), and custom unit used to display imaginary - numbers (:meth:`setImaginaryUnit`). - - The object emit an event `formatChanged` every time a parametter is - changed. - """ - - formatChanged = qt.Signal() - """Emitted when properties of the formatter change.""" - - def __init__(self, parent=None, formatter=None): - """ - Constructor - - :param qt.QObject parent: Owner of the object - :param TextFormatter formatter: Instantiate this object from the - formatter - """ - qt.QObject.__init__(self, parent) - if formatter is not None: - self.__integerFormat = formatter.integerFormat() - self.__floatFormat = formatter.floatFormat() - self.__useQuoteForText = formatter.useQuoteForText() - self.__imaginaryUnit = formatter.imaginaryUnit() - self.__enumFormat = formatter.enumFormat() - else: - self.__integerFormat = "%d" - self.__floatFormat = "%g" - self.__useQuoteForText = True - self.__imaginaryUnit = u"j" - self.__enumFormat = u"%(name)s(%(value)d)" - - def integerFormat(self): - """Returns the format string controlling how the integer data - are formated by this object. - - This is the C-style format string used by python when formatting - strings with the modulus operator. - - :rtype: str - """ - return self.__integerFormat - - def setIntegerFormat(self, value): - """Set format string controlling how the integer data are - formated by this object. - - :param str value: Format string (e.g. "%d", "%i", "%08i"). - This is the C-style format string used by python when formatting - strings with the modulus operator. - """ - if self.__integerFormat == value: - return - self.__integerFormat = value - self.formatChanged.emit() - - def floatFormat(self): - """Returns the format string controlling how the floating-point data - are formated by this object. - - This is the C-style format string used by python when formatting - strings with the modulus operator. - - :rtype: str - """ - return self.__floatFormat - - def setFloatFormat(self, value): - """Set format string controlling how the floating-point data are - formated by this object. - - :param str value: Format string (e.g. "%.3f", "%d", "%-10.2f", - "%10.3e"). - This is the C-style format string used by python when formatting - strings with the modulus operator. - """ - if self.__floatFormat == value: - return - self.__floatFormat = value - self.formatChanged.emit() - - def useQuoteForText(self): - """Returns true if the string data are formatted using double quotes. - - Else, no quotes are used. - """ - return self.__integerFormat - - def setUseQuoteForText(self, useQuote): - """Set the use of quotes to delimit string data. - - :param bool useQuote: True to use quotes. - """ - if self.__useQuoteForText == useQuote: - return - self.__useQuoteForText = useQuote - self.formatChanged.emit() - - def imaginaryUnit(self): - """Returns the unit display for imaginary numbers. - - :rtype: str - """ - return self.__imaginaryUnit - - def setImaginaryUnit(self, imaginaryUnit): - """Set the unit display for imaginary numbers. - - :param str imaginaryUnit: Unit displayed after imaginary numbers - """ - if self.__imaginaryUnit == imaginaryUnit: - return - self.__imaginaryUnit = imaginaryUnit - self.formatChanged.emit() - - def setEnumFormat(self, value): - """Set format string controlling how the enum data are - formated by this object. - - :param str value: Format string (e.g. "%(name)s(%(value)d)"). - This is the C-style format string used by python when formatting - strings with the modulus operator. - """ - if self.__enumFormat == value: - return - self.__enumFormat = value - self.formatChanged.emit() - - def enumFormat(self): - """Returns the format string controlling how the enum data - are formated by this object. - - This is the C-style format string used by python when formatting - strings with the modulus operator. - - :rtype: str - """ - return self.__enumFormat - - def __formatText(self, text): - if self.__useQuoteForText: - text = "\"%s\"" % text.replace("\\", "\\\\").replace("\"", "\\\"") - return text - - def __formatBinary(self, data): - if isinstance(data, numpy.void): - if six.PY2: - data = [ord(d) for d in data.data] - else: - data = data.item() - if isinstance(data, numpy.ndarray): - # Before numpy 1.15.0 the item API was returning a numpy array - data = data.astype(numpy.uint8) - else: - # Now it is supposed to be a bytes type - pass - elif six.PY2: - data = [ord(d) for d in data] - # In python3 data is already a bytes array - data = ["\\x%02X" % d for d in data] - if self.__useQuoteForText: - return "b\"%s\"" % "".join(data) - else: - return "".join(data) - - def __formatSafeAscii(self, data): - if six.PY2: - data = [ord(d) for d in data] - data = [chr(d) if (d > 0x20 and d < 0x7F) else "\\x%02X" % d for d in data] - if self.__useQuoteForText: - data = [c if c != '"' else "\\" + c for c in data] - return "b\"%s\"" % "".join(data) - else: - return "".join(data) - - def __formatCharString(self, data): - """Format text of char. - - From the specifications we expect to have ASCII, but we also allow - CP1252 in some ceases as fallback. - - If no encoding fits, it will display a readable ASCII chars, with - escaped chars (using the python syntax) for non decoded characters. - - :param data: A binary string of char expected in ASCII - :rtype: str - """ - try: - text = "%s" % data.decode("ascii") - return self.__formatText(text) - except UnicodeDecodeError: - # Here we can spam errors, this is definitly a badly - # generated file - _logger.error("Invalid ASCII string %s.", data) - if data == b"\xB0": - _logger.error("Fallback using cp1252 encoding") - return self.__formatText(u"\u00B0") - return self.__formatSafeAscii(data) - - def __formatH5pyObject(self, data, dtype): - # That's an HDF5 object - ref = h5py.check_dtype(ref=dtype) - if ref is not None: - if bool(data): - return "REF" - else: - return "NULL_REF" - vlen = h5py.check_dtype(vlen=dtype) - if vlen is not None: - if vlen == six.text_type: - # HDF5 UTF8 - # With h5py>=3 reading dataset returns bytes - if isinstance(data, (bytes, numpy.bytes_)): - try: - data = data.decode("utf-8") - except UnicodeDecodeError: - self.__formatSafeAscii(data) - return self.__formatText(data) - elif vlen == six.binary_type: - # HDF5 ASCII - return self.__formatCharString(data) - elif isinstance(vlen, numpy.dtype): - return self.toString(data, vlen) - return None - - def toString(self, data, dtype=None): - """Format a data into a string using formatter options - - :param object data: Data to render - :param dtype: enforce a dtype (mostly used to remember the h5py dtype, - special h5py dtypes are not propagated from array to items) - :rtype: str - """ - if isinstance(data, tuple): - text = [self.toString(d) for d in data] - return "(" + " ".join(text) + ")" - elif isinstance(data, list): - text = [self.toString(d) for d in data] - return "[" + " ".join(text) + "]" - elif isinstance(data, numpy.ndarray): - if dtype is None: - dtype = data.dtype - if data.shape == (): - # it is a scaler - return self.toString(data[()], dtype) - else: - text = [self.toString(d, dtype) for d in data] - return "[" + " ".join(text) + "]" - if dtype is not None and dtype.kind == 'O': - text = self.__formatH5pyObject(data, dtype) - if text is not None: - return text - elif isinstance(data, numpy.void): - if dtype is None: - dtype = data.dtype - if dtype.fields is not None: - text = [] - for index, field in enumerate(dtype.fields.items()): - text.append(field[0] + ":" + self.toString(data[index], field[1][0])) - return "(" + " ".join(text) + ")" - return self.__formatBinary(data) - elif isinstance(data, (numpy.unicode_, six.text_type)): - return self.__formatText(data) - elif isinstance(data, (numpy.string_, six.binary_type)): - if dtype is None and hasattr(data, "dtype"): - dtype = data.dtype - if dtype is not None: - # Maybe a sub item from HDF5 - if dtype.kind == 'S': - return self.__formatCharString(data) - elif dtype.kind == 'O': - text = self.__formatH5pyObject(data, dtype) - if text is not None: - return text - try: - # Try ascii/utf-8 - text = "%s" % data.decode("utf-8") - return self.__formatText(text) - except UnicodeDecodeError: - pass - return self.__formatBinary(data) - elif isinstance(data, six.string_types): - text = "%s" % data - return self.__formatText(text) - elif isinstance(data, (numpy.integer)): - if dtype is None: - dtype = data.dtype - enumType = h5py.check_dtype(enum=dtype) - if enumType is not None: - for key, value in enumType.items(): - if value == data: - result = {} - result["name"] = key - result["value"] = data - return self.__enumFormat % result - return self.__integerFormat % data - elif isinstance(data, (numbers.Integral)): - return self.__integerFormat % data - elif isinstance(data, (numbers.Real, numpy.floating)): - # It have to be done before complex checking - return self.__floatFormat % data - elif isinstance(data, (numpy.complexfloating, numbers.Complex)): - text = "" - if data.real != 0: - text += self.__floatFormat % data.real - if data.real != 0 and data.imag != 0: - if data.imag < 0: - template = self.__floatFormat + " - " + self.__floatFormat + self.__imaginaryUnit - params = (data.real, -data.imag) - else: - template = self.__floatFormat + " + " + self.__floatFormat + self.__imaginaryUnit - params = (data.real, data.imag) - else: - if data.imag != 0: - template = self.__floatFormat + self.__imaginaryUnit - params = (data.imag) - else: - template = self.__floatFormat - params = (data.real) - return template % params - elif isinstance(data, h5py.h5r.Reference): - dtype = h5py.special_dtype(ref=h5py.Reference) - text = self.__formatH5pyObject(data, dtype) - return text - elif isinstance(data, h5py.h5r.RegionReference): - dtype = h5py.special_dtype(ref=h5py.RegionReference) - text = self.__formatH5pyObject(data, dtype) - return text - elif isinstance(data, numpy.object_) or dtype is not None: - if dtype is None: - dtype = data.dtype - text = self.__formatH5pyObject(data, dtype) - if text is not None: - return text - # That's a numpy object - return str(data) - return str(data) diff --git a/silx/gui/data/_RecordPlot.py b/silx/gui/data/_RecordPlot.py deleted file mode 100644 index 5be792f..0000000 --- a/silx/gui/data/_RecordPlot.py +++ /dev/null @@ -1,92 +0,0 @@ -from silx.gui.plot.PlotWindow import PlotWindow -from silx.gui.plot.PlotWidget import PlotWidget -from .. import qt - - -class RecordPlot(PlotWindow): - def __init__(self, parent=None, backend=None): - super(RecordPlot, self).__init__(parent=parent, backend=backend, - resetzoom=True, autoScale=True, - logScale=True, grid=True, - curveStyle=True, colormap=False, - aspectRatio=False, yInverted=False, - copy=True, save=True, print_=True, - control=True, position=True, - roi=True, mask=False, fit=True) - if parent is None: - self.setWindowTitle('RecordPlot') - self._axesSelectionToolBar = AxesSelectionToolBar(parent=self, plot=self) - self.addToolBar(qt.Qt.BottomToolBarArea, self._axesSelectionToolBar) - - def setXAxisFieldName(self, value): - """Set the current selected field for the X axis. - - :param Union[str,None] value: - """ - label = '' if value is None else value - index = self._axesSelectionToolBar.getXAxisDropDown().findData(value) - - if index >= 0: - self.getXAxis().setLabel(label) - self._axesSelectionToolBar.getXAxisDropDown().setCurrentIndex(index) - - def getXAxisFieldName(self): - """Returns currently selected field for the X axis or None. - - rtype: Union[str,None] - """ - return self._axesSelectionToolBar.getXAxisDropDown().currentData() - - def setYAxisFieldName(self, value): - self.getYAxis().setLabel(value) - index = self._axesSelectionToolBar.getYAxisDropDown().findText(value) - if index >= 0: - self._axesSelectionToolBar.getYAxisDropDown().setCurrentIndex(index) - - def getYAxisFieldName(self): - return self._axesSelectionToolBar.getYAxisDropDown().currentText() - - def setSelectableXAxisFieldNames(self, fieldNames): - """Add list of field names to X axis - - :param List[str] fieldNames: - """ - comboBox = self._axesSelectionToolBar.getXAxisDropDown() - comboBox.clear() - comboBox.addItem('-', None) - comboBox.insertSeparator(1) - for name in fieldNames: - comboBox.addItem(name, name) - - def setSelectableYAxisFieldNames(self, fieldNames): - self._axesSelectionToolBar.getYAxisDropDown().clear() - self._axesSelectionToolBar.getYAxisDropDown().addItems(fieldNames) - - def getAxesSelectionToolBar(self): - return self._axesSelectionToolBar - -class AxesSelectionToolBar(qt.QToolBar): - def __init__(self, parent=None, plot=None, title='Plot Axes Selection'): - super(AxesSelectionToolBar, self).__init__(title, parent) - - assert isinstance(plot, PlotWidget) - - self.addWidget(qt.QLabel("Field selection: ")) - - self._labelXAxis = qt.QLabel(" X: ") - self.addWidget(self._labelXAxis) - - self._selectXAxisDropDown = qt.QComboBox() - self.addWidget(self._selectXAxisDropDown) - - self._labelYAxis = qt.QLabel(" Y: ") - self.addWidget(self._labelYAxis) - - self._selectYAxisDropDown = qt.QComboBox() - self.addWidget(self._selectYAxisDropDown) - - def getXAxisDropDown(self): - return self._selectXAxisDropDown - - def getYAxisDropDown(self): - return self._selectYAxisDropDown
\ No newline at end of file diff --git a/silx/gui/data/_VolumeWindow.py b/silx/gui/data/_VolumeWindow.py deleted file mode 100644 index 03b6876..0000000 --- a/silx/gui/data/_VolumeWindow.py +++ /dev/null @@ -1,148 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides a widget to visualize 3D arrays""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "22/03/2019" - - -import numpy - -from .. import qt -from ..plot3d.SceneWindow import SceneWindow -from ..plot3d.items import ScalarField3D, ComplexField3D, ItemChangedType - - -class VolumeWindow(SceneWindow): - """Extends SceneWindow with a convenient API for 3D array - - :param QWidget: parent - """ - - def __init__(self, parent): - super(VolumeWindow, self).__init__(parent) - self.__firstData = True - # Hide global parameter dock - self.getGroupResetWidget().parent().setVisible(False) - - def setAxesLabels(self, xlabel=None, ylabel=None, zlabel=None): - """Set the text labels of the axes. - - :param Union[str,None] xlabel: Label of the X axis - :param Union[str,None] ylabel: Label of the Y axis - :param Union[str,None] zlabel: Label of the Z axis - """ - sceneWidget = self.getSceneWidget() - sceneWidget.getSceneGroup().setAxesLabels( - 'X' if xlabel is None else xlabel, - 'Y' if ylabel is None else ylabel, - 'Z' if zlabel is None else zlabel) - - def clear(self): - """Clear any currently displayed data""" - sceneWidget = self.getSceneWidget() - items = sceneWidget.getItems() - if (len(items) == 1 and - isinstance(items[0], (ScalarField3D, ComplexField3D))): - items[0].setData(None) - else: # Safety net - sceneWidget.clearItems() - - @staticmethod - def __computeIsolevel(data): - """Returns a suitable isolevel value for data - - :param numpy.ndarray data: - :rtype: float - """ - data = data[numpy.isfinite(data)] - if len(data) == 0: - return 0 - else: - return numpy.mean(data) + numpy.std(data) - - def setData(self, data, offset=(0., 0., 0.), scale=(1., 1., 1.)): - """Set the 3D array data to display. - - :param numpy.ndarray data: 3D array of float or complex - :param List[float] offset: (tx, ty, tz) coordinates of the origin - :param List[float] scale: (sx, sy, sz) scale for each dimension - """ - sceneWidget = self.getSceneWidget() - dataMaxCoords = numpy.array(list(reversed(data.shape))) - 1 - - previousItems = sceneWidget.getItems() - if (len(previousItems) == 1 and - isinstance(previousItems[0], (ScalarField3D, ComplexField3D)) and - numpy.iscomplexobj(data) == isinstance(previousItems[0], ComplexField3D)): - # Reuse existing volume item - volume = sceneWidget.getItems()[0] - volume.setData(data, copy=False) - # Make sure the plane goes through the dataset - for plane in volume.getCutPlanes(): - point = numpy.array(plane.getPoint()) - if numpy.any(point < (0, 0, 0)) or numpy.any(point > dataMaxCoords): - plane.setPoint(dataMaxCoords // 2) - else: - # Add a new volume - sceneWidget.clearItems() - volume = sceneWidget.addVolume(data, copy=False) - volume.setLabel('Volume') - for plane in volume.getCutPlanes(): - # Make plane going through the center of the data - plane.setPoint(dataMaxCoords // 2) - plane.setVisible(False) - plane.sigItemChanged.connect(self.__cutPlaneUpdated) - volume.addIsosurface(self.__computeIsolevel, '#FF0000FF') - - # Expand the parameter tree - model = self.getParamTreeView().model() - index = qt.QModelIndex() # Invalid index for top level - while 1: - rowCount = model.rowCount(parent=index) - if rowCount == 0: - break - index = model.index(rowCount - 1, 0, parent=index) - self.getParamTreeView().setExpanded(index, True) - if not index.isValid(): - break - - volume.setTranslation(*offset) - volume.setScale(*scale) - - if self.__firstData: # Only center for first dataset - self.__firstData = False - sceneWidget.centerScene() - - def __cutPlaneUpdated(self, event): - """Handle the change of visibility of the cut plane - - :param event: Kind of update - """ - if event == ItemChangedType.VISIBLE: - plane = self.sender() - if plane.isVisible(): - self.getSceneWidget().selection().setCurrentItem(plane) diff --git a/silx/gui/data/__init__.py b/silx/gui/data/__init__.py deleted file mode 100644 index 560062d..0000000 --- a/silx/gui/data/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This package provides a set of Qt widgets for displaying data arrays using -table views and plot widgets. - -.. note:: - - Widgets in this package may rely on additional dependencies that are - not mandatory for *silx*. - :class:`DataViewer.DataViewer` relies on :mod:`silx.gui.plot` which - depends on *matplotlib*. It also optionally depends on *PyOpenGL* for 3D - visualization. -""" diff --git a/silx/gui/data/setup.py b/silx/gui/data/setup.py deleted file mode 100644 index 23ccbdd..0000000 --- a/silx/gui/data/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "16/01/2017" - - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('data', parent_package, top_path) - config.add_subpackage('test') - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - setup(configuration=configuration) diff --git a/silx/gui/data/test/__init__.py b/silx/gui/data/test/__init__.py deleted file mode 100644 index 08c044b..0000000 --- a/silx/gui/data/test/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -import unittest - -from . import test_arraywidget -from . import test_numpyaxesselector -from . import test_dataviewer -from . import test_textformatter - -__authors__ = ["V. Valls", "P. Knobel"] -__license__ = "MIT" -__date__ = "24/01/2017" - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTests( - [test_arraywidget.suite(), - test_numpyaxesselector.suite(), - test_dataviewer.suite(), - test_textformatter.suite(), - ]) - return test_suite diff --git a/silx/gui/data/test/test_arraywidget.py b/silx/gui/data/test/test_arraywidget.py deleted file mode 100644 index 87081ed..0000000 --- a/silx/gui/data/test/test_arraywidget.py +++ /dev/null @@ -1,329 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "05/12/2016" - -import os -import tempfile -import unittest - -import numpy - -from silx.gui import qt -from silx.gui.data import ArrayTableWidget -from silx.gui.data.ArrayTableModel import ArrayTableModel -from silx.gui.utils.testutils import TestCaseQt - -import h5py - - -class TestArrayWidget(TestCaseQt): - """Basic test for ArrayTableWidget with a numpy array""" - def setUp(self): - super(TestArrayWidget, self).setUp() - self.aw = ArrayTableWidget.ArrayTableWidget() - - def tearDown(self): - del self.aw - super(TestArrayWidget, self).tearDown() - - def testShow(self): - """test for errors""" - self.aw.show() - self.qWaitForWindowExposed(self.aw) - - def testSetData0D(self): - a = 1 - self.aw.setArrayData(a) - b = self.aw.getData(copy=True) - - self.assertTrue(numpy.array_equal(a, b)) - - # scalar/0D data has no frame index - self.assertEqual(len(self.aw.model._index), 0) - # and no perspective - self.assertEqual(len(self.aw.model._perspective), 0) - - def testSetData1D(self): - a = [1, 2] - self.aw.setArrayData(a) - b = self.aw.getData(copy=True) - - self.assertTrue(numpy.array_equal(a, b)) - - # 1D data has no frame index - self.assertEqual(len(self.aw.model._index), 0) - # and no perspective - self.assertEqual(len(self.aw.model._perspective), 0) - - def testSetData4D(self): - a = numpy.reshape(numpy.linspace(0.213, 1.234, 1250), - (5, 5, 5, 10)) - self.aw.setArrayData(a) - - # default perspective (0, 1) - self.assertEqual(list(self.aw.model._perspective), - [0, 1]) - self.aw.setPerspective((1, 3)) - self.assertEqual(list(self.aw.model._perspective), - [1, 3]) - - b = self.aw.getData(copy=True) - self.assertTrue(numpy.array_equal(a, b)) - - # 4D data has a 2-tuple as frame index - self.assertEqual(len(self.aw.model._index), 2) - # default index is (0, 0) - self.assertEqual(list(self.aw.model._index), - [0, 0]) - self.aw.setFrameIndex((3, 1)) - - self.assertEqual(list(self.aw.model._index), - [3, 1]) - - def testColors(self): - a = numpy.arange(256, dtype=numpy.uint8) - self.aw.setArrayData(a) - - bgcolor = numpy.empty(a.shape + (3,), dtype=numpy.uint8) - # Black & white palette - bgcolor[..., 0] = a - bgcolor[..., 1] = a - bgcolor[..., 2] = a - - fgcolor = numpy.bitwise_xor(bgcolor, 255) - - self.aw.setArrayColors(bgcolor, fgcolor) - - # test colors are as expected in model - for i in range(256): - # all RGB channels for BG equal to data value - self.assertEqual( - self.aw.model.data(self.aw.model.index(0, i), - role=qt.Qt.BackgroundRole), - qt.QColor(i, i, i), - "Unexpected background color" - ) - - # all RGB channels for FG equal to XOR(data value, 255) - self.assertEqual( - self.aw.model.data(self.aw.model.index(0, i), - role=qt.Qt.ForegroundRole), - qt.QColor(i ^ 255, i ^ 255, i ^ 255), - "Unexpected text color" - ) - - # test colors are reset to None when a new data array is loaded - # with different shape - self.aw.setArrayData(numpy.arange(300)) - - for i in range(300): - # all RGB channels for BG equal to data value - self.assertIsNone( - self.aw.model.data(self.aw.model.index(0, i), - role=qt.Qt.BackgroundRole)) - - def testDefaultFlagNotEditable(self): - """editable should be False by default, in setArrayData""" - self.aw.setArrayData([[0]]) - idx = self.aw.model.createIndex(0, 0) - # model is editable - self.assertFalse( - self.aw.model.flags(idx) & qt.Qt.ItemIsEditable) - - def testFlagEditable(self): - self.aw.setArrayData([[0]], editable=True) - idx = self.aw.model.createIndex(0, 0) - # model is editable - self.assertTrue( - self.aw.model.flags(idx) & qt.Qt.ItemIsEditable) - - def testFlagNotEditable(self): - self.aw.setArrayData([[0]], editable=False) - idx = self.aw.model.createIndex(0, 0) - # model is editable - self.assertFalse( - self.aw.model.flags(idx) & qt.Qt.ItemIsEditable) - - def testReferenceReturned(self): - """when setting the data with copy=False and - retrieving it with getData(copy=False), we should recover - the same original object. - """ - # n-D (n >=2) - a0 = numpy.reshape(numpy.linspace(0.213, 1.234, 1000), - (10, 10, 10)) - self.aw.setArrayData(a0, copy=False) - a1 = self.aw.getData(copy=False) - - self.assertIs(a0, a1) - - # 1D - b0 = numpy.linspace(0.213, 1.234, 1000) - self.aw.setArrayData(b0, copy=False) - b1 = self.aw.getData(copy=False) - self.assertIs(b0, b1) - - def testClipping(self): - """Test clipping of large arrays""" - self.aw.show() - self.qWaitForWindowExposed(self.aw) - - data = numpy.arange(ArrayTableModel.MAX_NUMBER_OF_SECTIONS + 10) - - for shape in [(1, -1), (-1, 1)]: - with self.subTest(shape=shape): - self.aw.setArrayData(data.reshape(shape), editable=True) - self.qapp.processEvents() - - -class TestH5pyArrayWidget(TestCaseQt): - """Basic test for ArrayTableWidget with a dataset. - - Test flags, for dataset open in read-only or read-write modes""" - def setUp(self): - super(TestH5pyArrayWidget, self).setUp() - self.aw = ArrayTableWidget.ArrayTableWidget() - self.data = numpy.reshape(numpy.linspace(0.213, 1.234, 1000), - (10, 10, 10)) - # create an h5py file with a dataset - self.tempdir = tempfile.mkdtemp() - self.h5_fname = os.path.join(self.tempdir, "array.h5") - h5f = h5py.File(self.h5_fname, mode='w') - h5f["my_array"] = self.data - h5f["my_scalar"] = 3.14 - h5f["my_1D_array"] = numpy.array(numpy.arange(1000)) - h5f.close() - - def tearDown(self): - del self.aw - os.unlink(self.h5_fname) - os.rmdir(self.tempdir) - super(TestH5pyArrayWidget, self).tearDown() - - def testShow(self): - self.aw.show() - self.qWaitForWindowExposed(self.aw) - - def testReadOnly(self): - """Open H5 dataset in read-only mode, ensure the model is not editable.""" - h5f = h5py.File(self.h5_fname, "r") - a = h5f["my_array"] - # ArrayTableModel relies on following condition - self.assertTrue(a.file.mode == "r") - - self.aw.setArrayData(a, copy=False, editable=True) - - self.assertIsInstance(a, h5py.Dataset) # simple sanity check - # internal representation must be a reference to original data (copy=False) - self.assertIsInstance(self.aw.model._array, h5py.Dataset) - self.assertTrue(self.aw.model._array.file.mode == "r") - - b = self.aw.getData() - self.assertTrue(numpy.array_equal(self.data, b)) - - # model must have detected read-only dataset and disabled editing - self.assertFalse(self.aw.model._editable) - idx = self.aw.model.createIndex(0, 0) - self.assertFalse( - self.aw.model.flags(idx) & qt.Qt.ItemIsEditable) - - # force editing read-only datasets raises IOError - self.assertRaises(IOError, self.aw.model.setData, - idx, 123.4, role=qt.Qt.EditRole) - h5f.close() - - def testReadWrite(self): - h5f = h5py.File(self.h5_fname, "r+") - a = h5f["my_array"] - self.assertTrue(a.file.mode == "r+") - - self.aw.setArrayData(a, copy=False, editable=True) - b = self.aw.getData(copy=False) - self.assertTrue(numpy.array_equal(self.data, b)) - - idx = self.aw.model.createIndex(0, 0) - # model is editable - self.assertTrue( - self.aw.model.flags(idx) & qt.Qt.ItemIsEditable) - h5f.close() - - def testSetData0D(self): - h5f = h5py.File(self.h5_fname, "r+") - a = h5f["my_scalar"] - self.aw.setArrayData(a) - b = self.aw.getData(copy=True) - - self.assertTrue(numpy.array_equal(a, b)) - - h5f.close() - - def testSetData1D(self): - h5f = h5py.File(self.h5_fname, "r+") - a = h5f["my_1D_array"] - self.aw.setArrayData(a) - b = self.aw.getData(copy=True) - - self.assertTrue(numpy.array_equal(a, b)) - - h5f.close() - - def testReferenceReturned(self): - """when setting the data with copy=False and - retrieving it with getData(copy=False), we should recover - the same original object. - - This only works for array with at least 2D. For 1D and 0D - arrays, a view is created at some point, which in the case - of an hdf5 dataset creates a copy.""" - h5f = h5py.File(self.h5_fname, "r+") - - # n-D - a0 = h5f["my_array"] - self.aw.setArrayData(a0, copy=False) - a1 = self.aw.getData(copy=False) - self.assertIs(a0, a1) - - # 1D - b0 = h5f["my_1D_array"] - self.aw.setArrayData(b0, copy=False) - b1 = self.aw.getData(copy=False) - self.assertIs(b0, b1) - - h5f.close() - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestArrayWidget)) - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestH5pyArrayWidget)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py deleted file mode 100644 index dd01dd6..0000000 --- a/silx/gui/data/test/test_dataviewer.py +++ /dev/null @@ -1,314 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "19/02/2019" - -import os -import tempfile -import unittest -from contextlib import contextmanager - -import numpy -from ..DataViewer import DataViewer -from ..DataViews import DataView -from .. import DataViews - -from silx.gui import qt - -from silx.gui.data.DataViewerFrame import DataViewerFrame -from silx.gui.utils.testutils import SignalListener -from silx.gui.utils.testutils import TestCaseQt - -import h5py - - -class _DataViewMock(DataView): - """Dummy view to display nothing""" - - def __init__(self, parent): - DataView.__init__(self, parent) - - def axesNames(self, data, info): - return [] - - def createWidget(self, parent): - return qt.QLabel(parent) - - def getDataPriority(self, data, info): - return 0 - - -class AbstractDataViewerTests(TestCaseQt): - - def create_widget(self): - # Avoid to raise an error when testing the full module - self.skipTest("Not implemented") - - @contextmanager - def h5_temporary_file(self): - # create tmp file - fd, tmp_name = tempfile.mkstemp(suffix=".h5") - os.close(fd) - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - # create h5 data - h5file = h5py.File(tmp_name, "w") - h5file["data"] = data - yield h5file - # clean up - h5file.close() - os.unlink(tmp_name) - - def test_text_data(self): - data_list = ["aaa", int, 8, self] - widget = self.create_widget() - for data in data_list: - widget.setData(data) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - - def test_plot_1d_data(self): - data = numpy.arange(3 ** 1) - data.shape = [3] * 1 - widget = self.create_widget() - widget.setData(data) - availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - self.assertIn(DataViews.PLOT1D_MODE, availableModes) - - def test_image_data(self): - data = numpy.arange(3 ** 2) - data.shape = [3] * 2 - widget = self.create_widget() - widget.setData(data) - availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - self.assertIn(DataViews.IMAGE_MODE, availableModes) - - def test_image_bool(self): - data = numpy.zeros((10, 10), dtype=bool) - data[::2, ::2] = True - widget = self.create_widget() - widget.setData(data) - availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - self.assertIn(DataViews.IMAGE_MODE, availableModes) - - def test_image_complex_data(self): - data = numpy.arange(3 ** 2, dtype=numpy.complex64) - data.shape = [3] * 2 - widget = self.create_widget() - widget.setData(data) - availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - self.assertIn(DataViews.IMAGE_MODE, availableModes) - - def test_plot_3d_data(self): - data = numpy.arange(3 ** 3) - data.shape = [3] * 3 - widget = self.create_widget() - widget.setData(data) - availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - try: - import silx.gui.plot3d # noqa - self.assertIn(DataViews.PLOT3D_MODE, availableModes) - except ImportError: - self.assertIn(DataViews.STACK_MODE, availableModes) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - - def test_array_1d_data(self): - data = numpy.array(["aaa"] * (3 ** 1)) - data.shape = [3] * 1 - widget = self.create_widget() - widget.setData(data) - self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) - - def test_array_2d_data(self): - data = numpy.array(["aaa"] * (3 ** 2)) - data.shape = [3] * 2 - widget = self.create_widget() - widget.setData(data) - self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) - - def test_array_4d_data(self): - data = numpy.array(["aaa"] * (3 ** 4)) - data.shape = [3] * 4 - widget = self.create_widget() - widget.setData(data) - self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) - - def test_record_4d_data(self): - data = numpy.zeros(3 ** 4, dtype='3int8, float32, (2,3)float64') - data.shape = [3] * 4 - widget = self.create_widget() - widget.setData(data) - self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) - - def test_3d_h5_dataset(self): - with self.h5_temporary_file() as h5file: - dataset = h5file["data"] - widget = self.create_widget() - widget.setData(dataset) - - def test_data_event(self): - listener = SignalListener() - widget = self.create_widget() - widget.dataChanged.connect(listener) - widget.setData(10) - widget.setData(None) - self.assertEqual(listener.callCount(), 2) - - def test_display_mode_event(self): - listener = SignalListener() - widget = self.create_widget() - widget.displayedViewChanged.connect(listener) - widget.setData(10) - widget.setData(None) - modes = [v.modeId() for v in listener.arguments(argumentIndex=0)] - self.assertEqual(modes, [DataViews.RAW_MODE, DataViews.EMPTY_MODE]) - listener.clear() - - def test_change_display_mode(self): - data = numpy.arange(10 ** 4) - data.shape = [10] * 4 - widget = self.create_widget() - widget.setData(data) - widget.setDisplayMode(DataViews.PLOT1D_MODE) - self.assertEqual(widget.displayedView().modeId(), DataViews.PLOT1D_MODE) - widget.setDisplayMode(DataViews.IMAGE_MODE) - self.assertEqual(widget.displayedView().modeId(), DataViews.IMAGE_MODE) - widget.setDisplayMode(DataViews.RAW_MODE) - self.assertEqual(widget.displayedView().modeId(), DataViews.RAW_MODE) - widget.setDisplayMode(DataViews.EMPTY_MODE) - self.assertEqual(widget.displayedView().modeId(), DataViews.EMPTY_MODE) - - def test_create_default_views(self): - widget = self.create_widget() - views = widget.createDefaultViews() - self.assertTrue(len(views) > 0) - - def test_add_view(self): - widget = self.create_widget() - view = _DataViewMock(widget) - widget.addView(view) - self.assertTrue(view in widget.availableViews()) - self.assertTrue(view in widget.currentAvailableViews()) - - def test_remove_view(self): - widget = self.create_widget() - widget.setData("foobar") - view = widget.currentAvailableViews()[0] - widget.removeView(view) - self.assertTrue(view not in widget.availableViews()) - self.assertTrue(view not in widget.currentAvailableViews()) - - def test_replace_view(self): - widget = self.create_widget() - view = _DataViewMock(widget) - widget.replaceView(DataViews.RAW_MODE, - view) - self.assertIsNone(widget.getViewFromModeId(DataViews.RAW_MODE)) - self.assertTrue(view in widget.availableViews()) - self.assertTrue(view in widget.currentAvailableViews()) - - def test_replace_view_in_composite(self): - # replace a view that is a child of a composite view - widget = self.create_widget() - view = _DataViewMock(widget) - replaced = widget.replaceView(DataViews.NXDATA_INVALID_MODE, - view) - self.assertTrue(replaced) - nxdata_view = widget.getViewFromModeId(DataViews.NXDATA_MODE) - self.assertNotIn(DataViews.NXDATA_INVALID_MODE, - [v.modeId() for v in nxdata_view.getViews()]) - self.assertTrue(view in nxdata_view.getViews()) - - -class TestDataViewer(AbstractDataViewerTests): - def create_widget(self): - return DataViewer() - - -class TestDataViewerFrame(AbstractDataViewerTests): - def create_widget(self): - return DataViewerFrame() - - -class TestDataView(TestCaseQt): - - def createComplexData(self): - line = [1, 2j, 3 + 3j, 4] - image = [line, line, line, line] - cube = [image, image, image, image] - data = numpy.array(cube, dtype=numpy.complex64) - return data - - def createDataViewWithData(self, dataViewClass, data): - viewer = dataViewClass(None) - widget = viewer.getWidget() - viewer.setData(data) - return widget - - def testCurveWithComplex(self): - data = self.createComplexData() - dataViewClass = DataViews._Plot1dView - widget = self.createDataViewWithData(dataViewClass, data[0, 0]) - self.qWaitForWindowExposed(widget) - - def testImageWithComplex(self): - data = self.createComplexData() - dataViewClass = DataViews._Plot2dView - widget = self.createDataViewWithData(dataViewClass, data[0]) - self.qWaitForWindowExposed(widget) - - def testCubeWithComplex(self): - self.skipTest("OpenGL widget not yet tested") - try: - import silx.gui.plot3d # noqa - except ImportError: - self.skipTest("OpenGL not available") - data = self.createComplexData() - dataViewClass = DataViews._Plot3dView - widget = self.createDataViewWithData(dataViewClass, data) - self.qWaitForWindowExposed(widget) - - def testImageStackWithComplex(self): - data = self.createComplexData() - dataViewClass = DataViews._StackView - widget = self.createDataViewWithData(dataViewClass, data) - self.qWaitForWindowExposed(widget) - - -def suite(): - test_suite = unittest.TestSuite() - loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase - test_suite.addTest(loadTestsFromTestCase(TestDataViewer)) - test_suite.addTest(loadTestsFromTestCase(TestDataViewerFrame)) - test_suite.addTest(loadTestsFromTestCase(TestDataView)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/data/test/test_numpyaxesselector.py b/silx/gui/data/test/test_numpyaxesselector.py deleted file mode 100644 index d37cff7..0000000 --- a/silx/gui/data/test/test_numpyaxesselector.py +++ /dev/null @@ -1,161 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "29/01/2018" - -import os -import tempfile -import unittest -from contextlib import contextmanager - -import numpy - -from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector -from silx.gui.utils.testutils import SignalListener -from silx.gui.utils.testutils import TestCaseQt - -import h5py - - -class TestNumpyAxesSelector(TestCaseQt): - - def test_creation(self): - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - widget = NumpyAxesSelector() - widget.setVisible(True) - - def test_none(self): - data = numpy.arange(3 * 3 * 3) - widget = NumpyAxesSelector() - widget.setData(data) - widget.setData(None) - result = widget.selectedData() - self.assertIsNone(result) - - def test_output_samedim(self): - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - expectedResult = data - - widget = NumpyAxesSelector() - widget.setAxisNames(["x", "y", "z"]) - widget.setData(data) - result = widget.selectedData() - self.assertTrue(numpy.array_equal(result, expectedResult)) - - def test_output_moredim(self): - data = numpy.arange(3 * 3 * 3 * 3) - data.shape = 3, 3, 3, 3 - expectedResult = data - - widget = NumpyAxesSelector() - widget.setAxisNames(["x", "y", "z", "boum"]) - widget.setData(data[0]) - result = widget.selectedData() - self.assertIsNone(result) - widget.setData(data) - result = widget.selectedData() - self.assertTrue(numpy.array_equal(result, expectedResult)) - - def test_output_lessdim(self): - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - expectedResult = data[0] - - widget = NumpyAxesSelector() - widget.setAxisNames(["y", "x"]) - widget.setData(data) - result = widget.selectedData() - self.assertTrue(numpy.array_equal(result, expectedResult)) - - def test_output_1dim(self): - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - expectedResult = data[0, 0, 0] - - widget = NumpyAxesSelector() - widget.setData(data) - result = widget.selectedData() - self.assertTrue(numpy.array_equal(result, expectedResult)) - - @contextmanager - def h5_temporary_file(self): - # create tmp file - fd, tmp_name = tempfile.mkstemp(suffix=".h5") - os.close(fd) - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - # create h5 data - h5file = h5py.File(tmp_name, "w") - h5file["data"] = data - yield h5file - # clean up - h5file.close() - os.unlink(tmp_name) - - def test_h5py_dataset(self): - with self.h5_temporary_file() as h5file: - dataset = h5file["data"] - expectedResult = dataset[0] - - widget = NumpyAxesSelector() - widget.setData(dataset) - widget.setAxisNames(["y", "x"]) - result = widget.selectedData() - self.assertTrue(numpy.array_equal(result, expectedResult)) - - def test_data_event(self): - data = numpy.arange(3 * 3 * 3) - widget = NumpyAxesSelector() - listener = SignalListener() - widget.dataChanged.connect(listener) - widget.setData(data) - widget.setData(None) - self.assertEqual(listener.callCount(), 2) - - def test_selected_data_event(self): - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - widget = NumpyAxesSelector() - listener = SignalListener() - widget.selectionChanged.connect(listener) - widget.setData(data) - widget.setAxisNames(["x"]) - widget.setData(None) - self.assertEqual(listener.callCount(), 3) - listener.clear() - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestNumpyAxesSelector)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py deleted file mode 100644 index d3050bf..0000000 --- a/silx/gui/data/test/test_textformatter.py +++ /dev/null @@ -1,212 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/12/2017" - -import unittest -import shutil -import tempfile - -import numpy -import six - -from silx.gui.utils.testutils import TestCaseQt -from silx.gui.utils.testutils import SignalListener -from ..TextFormatter import TextFormatter -from silx.io.utils import h5py_read_dataset - -import h5py - - -class TestTextFormatter(TestCaseQt): - - def test_copy(self): - formatter = TextFormatter() - copy = TextFormatter(formatter=formatter) - self.assertIsNot(formatter, copy) - copy.setFloatFormat("%.3f") - self.assertEqual(formatter.integerFormat(), copy.integerFormat()) - self.assertNotEqual(formatter.floatFormat(), copy.floatFormat()) - self.assertEqual(formatter.useQuoteForText(), copy.useQuoteForText()) - self.assertEqual(formatter.imaginaryUnit(), copy.imaginaryUnit()) - - def test_event(self): - listener = SignalListener() - formatter = TextFormatter() - formatter.formatChanged.connect(listener) - formatter.setFloatFormat("%.3f") - formatter.setIntegerFormat("%03i") - formatter.setUseQuoteForText(False) - formatter.setImaginaryUnit("z") - self.assertEqual(listener.callCount(), 4) - - def test_int(self): - formatter = TextFormatter() - formatter.setIntegerFormat("%05i") - result = formatter.toString(512) - self.assertEqual(result, "00512") - - def test_float(self): - formatter = TextFormatter() - formatter.setFloatFormat("%.3f") - result = formatter.toString(1.3) - self.assertEqual(result, "1.300") - - def test_complex(self): - formatter = TextFormatter() - formatter.setFloatFormat("%.1f") - formatter.setImaginaryUnit("i") - result = formatter.toString(1.0 + 5j) - result = result.replace(" ", "") - self.assertEqual(result, "1.0+5.0i") - - def test_string(self): - formatter = TextFormatter() - formatter.setIntegerFormat("%.1f") - formatter.setImaginaryUnit("z") - result = formatter.toString("toto") - self.assertEqual(result, '"toto"') - - def test_numpy_void(self): - formatter = TextFormatter() - result = formatter.toString(numpy.void(b"\xFF")) - self.assertEqual(result, 'b"\\xFF"') - - def test_char_cp1252(self): - # degree character in cp1252 - formatter = TextFormatter() - result = formatter.toString(numpy.bytes_(b"\xB0")) - self.assertEqual(result, u'"\u00B0"') - - -class TestTextFormatterWithH5py(TestCaseQt): - - @classmethod - def setUpClass(cls): - super(TestTextFormatterWithH5py, cls).setUpClass() - - cls.tmpDirectory = tempfile.mkdtemp() - cls.h5File = h5py.File("%s/formatter.h5" % cls.tmpDirectory, mode="w") - cls.formatter = TextFormatter() - - @classmethod - def tearDownClass(cls): - super(TestTextFormatterWithH5py, cls).tearDownClass() - cls.h5File.close() - cls.h5File = None - shutil.rmtree(cls.tmpDirectory) - - def create_dataset(self, data, dtype=None): - testName = "%s" % self.id() - dataset = self.h5File.create_dataset(testName, data=data, dtype=dtype) - return dataset - - def read_dataset(self, d): - return self.formatter.toString(d[()], dtype=d.dtype) - - def testAscii(self): - d = self.create_dataset(data=b"abc") - result = self.read_dataset(d) - self.assertEqual(result, '"abc"') - - def testUnicode(self): - d = self.create_dataset(data=u"i\u2661cookies") - result = self.read_dataset(d) - self.assertEqual(len(result), 11) - self.assertEqual(result, u'"i\u2661cookies"') - - def testBadAscii(self): - d = self.create_dataset(data=b"\xF0\x9F\x92\x94") - result = self.read_dataset(d) - self.assertEqual(result, 'b"\\xF0\\x9F\\x92\\x94"') - - def testVoid(self): - d = self.create_dataset(data=numpy.void(b"abc\xF0")) - result = self.read_dataset(d) - self.assertEqual(result, 'b"\\x61\\x62\\x63\\xF0"') - - def testEnum(self): - dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42})) - d = numpy.array(42, dtype=dtype) - d = self.create_dataset(data=d) - result = self.read_dataset(d) - self.assertEqual(result, 'BLUE(42)') - - def testRef(self): - dtype = h5py.special_dtype(ref=h5py.Reference) - d = numpy.array(self.h5File.ref, dtype=dtype) - d = self.create_dataset(data=d) - result = self.read_dataset(d) - self.assertEqual(result, 'REF') - - def testArrayAscii(self): - d = self.create_dataset(data=[b"abc"]) - result = self.read_dataset(d) - self.assertEqual(result, '["abc"]') - - def testArrayUnicode(self): - dtype = h5py.special_dtype(vlen=six.text_type) - d = numpy.array([u"i\u2661cookies"], dtype=dtype) - d = self.create_dataset(data=d) - result = self.read_dataset(d) - self.assertEqual(len(result), 13) - self.assertEqual(result, u'["i\u2661cookies"]') - - def testArrayBadAscii(self): - d = self.create_dataset(data=[b"\xF0\x9F\x92\x94"]) - result = self.read_dataset(d) - self.assertEqual(result, '[b"\\xF0\\x9F\\x92\\x94"]') - - def testArrayVoid(self): - d = self.create_dataset(data=numpy.void([b"abc\xF0"])) - result = self.read_dataset(d) - self.assertEqual(result, '[b"\\x61\\x62\\x63\\xF0"]') - - def testArrayEnum(self): - dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42})) - d = numpy.array([42, 1, 100], dtype=dtype) - d = self.create_dataset(data=d) - result = self.read_dataset(d) - self.assertEqual(result, '[BLUE(42) GREEN(1) 100]') - - def testArrayRef(self): - dtype = h5py.special_dtype(ref=h5py.Reference) - d = numpy.array([self.h5File.ref, None], dtype=dtype) - d = self.create_dataset(data=d) - result = self.read_dataset(d) - self.assertEqual(result, '[REF NULL_REF]') - - -def suite(): - loadTests = unittest.defaultTestLoader.loadTestsFromTestCase - test_suite = unittest.TestSuite() - test_suite.addTest(loadTests(TestTextFormatter)) - test_suite.addTest(loadTests(TestTextFormatterWithH5py)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') |