summaryrefslogtreecommitdiff
path: root/silx/gui/data
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/data')
-rw-r--r--silx/gui/data/ArrayTableModel.py670
-rw-r--r--silx/gui/data/ArrayTableWidget.py492
-rw-r--r--silx/gui/data/DataViewer.py593
-rw-r--r--silx/gui/data/DataViewerFrame.py217
-rw-r--r--silx/gui/data/DataViewerSelector.py175
-rw-r--r--silx/gui/data/DataViews.py2059
-rw-r--r--silx/gui/data/Hdf5TableView.py646
-rw-r--r--silx/gui/data/HexaTableView.py286
-rw-r--r--silx/gui/data/NXdataWidgets.py1081
-rw-r--r--silx/gui/data/NumpyAxesSelector.py578
-rw-r--r--silx/gui/data/RecordTableView.py447
-rw-r--r--silx/gui/data/TextFormatter.py395
-rw-r--r--silx/gui/data/_RecordPlot.py92
-rw-r--r--silx/gui/data/_VolumeWindow.py148
-rw-r--r--silx/gui/data/__init__.py35
-rw-r--r--silx/gui/data/setup.py41
-rw-r--r--silx/gui/data/test/__init__.py45
-rw-r--r--silx/gui/data/test/test_arraywidget.py329
-rw-r--r--silx/gui/data/test/test_dataviewer.py314
-rw-r--r--silx/gui/data/test/test_numpyaxesselector.py161
-rw-r--r--silx/gui/data/test/test_textformatter.py212
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')