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.assertEqual(result, None) 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.assertEqual(result, None) widget.setData(data) result = widget.selectedData() self.assertTrue(numpy.array_equal(result, expectedResult))
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_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_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))
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))
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_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))
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()
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) """ EMPTY_MODE = 0 PLOT1D_MODE = 10 PLOT2D_MODE = 20 PLOT3D_MODE = 30 RAW_MODE = 40 RAW_ARRAY_MODE = 41 RAW_RECORD_MODE = 42 RAW_SCALAR_MODE = 43 STACK_MODE = 50 HDF5_MODE = 60 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.__useAxisSelection = False self.__userSelectedView = 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(self.EMPTY_MODE) 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, ] 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 """ previous = self.__numpySelection.blockSignals(True) self.__numpySelection.clear() info = DataViews.DataInfo(self.__data) axisNames = self.__currentView.axesNames(self.__data, info) if info.isArray 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) 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) self.__numpySelection.blockSignals(previous) def __updateDataInView(self): """ Update the views using the current data """ if self.__useAxisSelection: self.__displayedData = self.__numpySelection.selectedData() else: self.__displayedData = self.__data qt.QTimer.singleShot(10, self.__setDataInView) def __setDataInView(self): self.__currentView.setData(self.__displayedData) 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. :param int modeId: Requested mode id :rtype: silx.gui.data.DataViews.DataView """ for view in self.__views: if view.modeId() == modeId: return view return view 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 - `PLOT3D_MODE`: display the data as an isosurface - `RAW_MODE`: display the data as a table - `STACK_MODE`: display the data as a stack of images - `HDF5_MODE`: display the data as a table """ 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 """ 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 # sort available views according to priority info = DataViews.DataInfo(data) priorities = [v.getDataPriority(data, info) for v in self.__views] views = zip(priorities, self.__views) views = filter(lambda t: t[0] > DataViews.DataView.UNSUPPORTED, views) views = sorted(views, reverse=True) # store available views if len(views) == 0: self.__setCurrentAvailableViews([]) available = [] else: available = [v[1] for v in views] self.__setCurrentAvailableViews(available) 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(DataViewer.HDF5_MODE) if hdf5View in available: return hdf5View return self.getViewFromModeId(DataViewer.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(DataViewer.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 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.__displayedData = 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 displayMode(self): """Returns the current display mode""" return self.__currentView.modeId()
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) 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.getPlot().getXAxis().setLabel(self.__x_axis_name or "X") self._stack_view.getPlot().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 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) 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 silx.gui.plot3d.ScalarFieldView import ScalarFieldView from silx.gui.plot3d import SFViewParamTree self._view = ScalarFieldView(self) def computeIsolevel(data): data = data[numpy.isfinite(data)] if len(data) == 0: return 0 else: return numpy.mean(data) + numpy.std(data) self._view.addIsosurface(computeIsolevel, '#FF0000FF') # Create a parameter tree for the scalar field view options = SFViewParamTree.TreeView(self._view) options.setSfView(self._view) # Add the parameter tree to the main window in a dock widget dock = qt.QDockWidget() dock.setWidget(options) self._view.addDockWidget(qt.Qt.RightDockWidgetArea, dock) 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: ScalarFieldView """ return self._view def normalizeComplexData(self, data): """ Converts a complex data array to its amplitude, if necessary. :param data: the data to normalize :return: """ 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 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 """ signal = self.normalizeComplexData(signal) 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._view.setAxesLabels(self.__x_axis_name or 'X', self.__y_axis_name or 'Y', self.__z_axis_name or '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.""" data = self._selector.selectedData() 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) self._view.setData(data, copy=False) self._view.setScale(*scale) self._view.setTranslation(*offset) self._view.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._view.setData(None)
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.__signal = None self.__signal_name = None self.__signal_errors = None self.__axis = None self.__axis_name = None self.__axis_errors = None self.__values = None self.__first_curve_added = False self._plot = Plot1D(self) self._plot.setDefaultColormap( # for scatters {"name": "viridis", "vmin": 0., "vmax": 1., # ignored (autoscale) but mandatory "normalization": "linear", "autoscale": True}) self.selectorDock = qt.QDockWidget("Data selector", self._plot) # not closable self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable | qt.QDockWidget.DockWidgetFloatable) self._selector = NumpyAxesSelector(self.selectorDock) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False self.selectorDock.setWidget(self._selector) self._plot.addTabbedDockWidget(self.selectorDock) layout = qt.QGridLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._plot, 0, 0) self.setLayout(layout) def setCurveData(self, y, x=None, values=None, yerror=None, xerror=None, ylabel=None, xlabel=None, title=None): """ :param y: dataset to be represented by the y (vertical) axis. For a scatter, this must be a 1D array and x and values must be 1-D arrays of the same size. In other cases, it can be a n-D array whose last dimension must have the same length as x (and values must be None) :param 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 values: Values, to be provided for a x-y-value scatter plot. This will be used to compute the color map and assign colors to the points. :param yerror: 1-D dataset of errors for y, or None :param xerror: 1-D dataset of errors for x, or None :param ylabel: Label for Y axis :param xlabel: Label for X axis :param title: Graph title """ self.__signal = y self.__signal_name = ylabel self.__signal_errors = yerror self.__axis = x self.__axis_name = xlabel self.__axis_errors = xerror self.__values = values if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateCurve) self.__selector_is_connected = False self._selector.setData(y) self._selector.setAxisNames([ylabel or "Y"]) if len(y.shape) < 2: self.selectorDock.hide() else: self.selectorDock.show() self._plot.setGraphTitle(title or "") self._plot.setGraphXLabel(self.__axis_name or "X") self._plot.setGraphYLabel(self.__signal_name or "Y") self._updateCurve() if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateCurve) self.__selector_is_connected = True def _updateCurve(self): y = self._selector.selectedData() 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(y) elif len(x) == 2 and len(y) != 2: # linear calibration a + b * x x = x[0] + x[1] * numpy.arange(len(y)) legend = self.__signal_name + "[" for sl in self._selector.selection(): if sl == slice(None): legend += ":, " else: legend += str(sl) + ", " legend = legend[:-2] + "]" if self.__signal_errors is not None: y_errors = self.__signal_errors[self._selector.selection()] else: y_errors = None self._plot.remove(kind=("curve", "scatter")) # values: x-y-v scatter if self.__values is not None: self._plot.addScatter(x, y, self.__values, legend=legend, xerror=self.__axis_errors, yerror=y_errors) # x monotonically increasing: curve elif numpy.all(numpy.diff(x) > 0): self._plot.addCurve(x, y, legend=legend, xerror=self.__axis_errors, yerror=y_errors) # scatter else: self._plot.addScatter(x, y, value=numpy.ones_like(y), legend=legend, xerror=self.__axis_errors, yerror=y_errors) self._plot.resetZoom() self._plot.setGraphXLabel(self.__axis_name) self._plot.setGraphYLabel(self.__signal_name) def clear(self): self._plot.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.__signal = None self.__signal_name = 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({ "name": "viridis", "vmin": 0., "vmax": 1., # ignored (autoscale) but mandatory "normalization": "linear", "autoscale": True }) self.selectorDock = qt.QDockWidget("Data selector", self._plot) # not closable self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable | qt.QDockWidget.DockWidgetFloatable) self._legend = qt.QLabel(self) self._selector = NumpyAxesSelector(self.selectorDock) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False layout = qt.QVBoxLayout() layout.addWidget(self._plot) layout.addWidget(self._legend) self.selectorDock.setWidget(self._selector) self._plot.addTabbedDockWidget(self.selectorDock) self.setLayout(layout) def setImageData(self, signal, x_axis=None, y_axis=None, signal_name=None, xlabel=None, ylabel=None, title=None): """ :param signal: n-D dataset, whose last 2 dimensions are used as the image's 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 signal_name: Label used in the legend :param xlabel: Label for X axis :param ylabel: Label for Y axis :param title: Graph title """ if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateImage) 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._selector.setData(signal) self._selector.setAxisNames([ylabel or "Y", xlabel or "X"]) if len(signal.shape) < 3: self.selectorDock.hide() else: self.selectorDock.show() self._plot.setGraphTitle(title or "") self._plot.setGraphXLabel(self.__x_axis_name or "X") self._plot.setGraphYLabel(self.__y_axis_name or "Y") self._updateImage() if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateImage) self.__selector_is_connected = True def _updateImage(self): 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) img = self._selector.selectedData() 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(img.shape[-1]) elif numpy.isscalar(x_axis) or len(x_axis) == 1: # constant axis x_axis = x_axis * numpy.ones((img.shape[-1], )) elif len(x_axis) == 2: # linear calibration x_axis = x_axis[0] * numpy.arange(img.shape[-1]) + x_axis[1] if y_axis is None: y_axis = numpy.arange(img.shape[-2]) elif numpy.isscalar(y_axis) or len(y_axis) == 1: y_axis = y_axis * numpy.ones((img.shape[-2], )) elif len(y_axis) == 2: y_axis = y_axis[0] * numpy.arange(img.shape[-2]) + 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.addImage(img, legend=legend, origin=origin, scale=scale) else: scatterx, scattery = numpy.meshgrid(x_axis, y_axis) self._plot.addScatter(numpy.ravel(scatterx), numpy.ravel(scattery), numpy.ravel(img), legend=legend) self._plot.setGraphXLabel(self.__x_axis_name) self._plot.setGraphYLabel(self.__y_axis_name) self._plot.resetZoom() def clear(self): self._plot.clear()
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) """ # TODO: Can be removed for silx 0.8 @classproperty @deprecation.deprecated(replacement="DataViews.EMPTY_MODE", since_version="0.7", skip_backtrace_count=2) def EMPTY_MODE(self): return DataViews.EMPTY_MODE # TODO: Can be removed for silx 0.8 @classproperty @deprecation.deprecated(replacement="DataViews.PLOT1D_MODE", since_version="0.7", skip_backtrace_count=2) def PLOT1D_MODE(self): return DataViews.PLOT1D_MODE # TODO: Can be removed for silx 0.8 @classproperty @deprecation.deprecated(replacement="DataViews.PLOT2D_MODE", since_version="0.7", skip_backtrace_count=2) def PLOT2D_MODE(self): return DataViews.PLOT2D_MODE # TODO: Can be removed for silx 0.8 @classproperty @deprecation.deprecated(replacement="DataViews.PLOT3D_MODE", since_version="0.7", skip_backtrace_count=2) def PLOT3D_MODE(self): return DataViews.PLOT3D_MODE # TODO: Can be removed for silx 0.8 @classproperty @deprecation.deprecated(replacement="DataViews.RAW_MODE", since_version="0.7", skip_backtrace_count=2) def RAW_MODE(self): return DataViews.RAW_MODE # TODO: Can be removed for silx 0.8 @classproperty @deprecation.deprecated(replacement="DataViews.RAW_ARRAY_MODE", since_version="0.7", skip_backtrace_count=2) def RAW_ARRAY_MODE(self): return DataViews.RAW_ARRAY_MODE # TODO: Can be removed for silx 0.8 @classproperty @deprecation.deprecated(replacement="DataViews.RAW_RECORD_MODE", since_version="0.7", skip_backtrace_count=2) def RAW_RECORD_MODE(self): return DataViews.RAW_RECORD_MODE # TODO: Can be removed for silx 0.8 @classproperty @deprecation.deprecated(replacement="DataViews.RAW_SCALAR_MODE", since_version="0.7", skip_backtrace_count=2) def RAW_SCALAR_MODE(self): return DataViews.RAW_SCALAR_MODE # TODO: Can be removed for silx 0.8 @classproperty @deprecation.deprecated(replacement="DataViews.STACK_MODE", since_version="0.7", skip_backtrace_count=2) def STACK_MODE(self): return DataViews.STACK_MODE # TODO: Can be removed for silx 0.8 @classproperty @deprecation.deprecated(replacement="DataViews.HDF5_MODE", since_version="0.7", skip_backtrace_count=2) def HDF5_MODE(self): return DataViews.HDF5_MODE 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, ] 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 """ previous = self.__numpySelection.blockSignals(True) 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) 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) self.__numpySelection.blockSignals(previous) def __updateDataInView(self): """ Update the views using the current data """ if self.__useAxisSelection: self.__displayedData = self.__numpySelection.selectedData() else: self.__displayedData = self.__data # 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) 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 priorities = [v.getDataPriority(data, info) for v in self.__views] views = zip(priorities, self.__views) views = filter(lambda t: t[0] > DataViews.DataView.UNSUPPORTED, views) views = sorted(views, reverse=True) # store available views if len(views) == 0: self.__setCurrentAvailableViews([]) available = [] else: available = [v[1] for v in views] self.__setCurrentAvailableViews(available) 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(DataViewer.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 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.__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
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