Source code for silx.gui.data.DataViewer

# 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"])


[docs]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)
[docs] 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)
[docs] 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
[docs] def clear(self): """Clear the widget""" self.setData(None)
[docs] 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)
[docs] 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)
[docs] 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
[docs] 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)
[docs] def displayedView(self): """Returns the current displayed view. :rtype: silx.gui.data.DataViews.DataView """ return self.__currentView
[docs] 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()
[docs] 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
[docs] 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)
[docs] 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()
[docs] def currentAvailableViews(self): """Returns the list of available views for the current data :rtype: List[DataView] """ return self.__currentAvailableViews
[docs] 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
[docs] def availableViews(self): """Returns the list of registered views :rtype: List[DataView] """ return self.__views
[docs] 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()
[docs] 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
[docs] def displayMode(self): """Returns the current display mode""" return self.__currentView.modeId()
[docs] 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