diff options
Diffstat (limited to 'src/silx/gui/data/DataViews.py')
-rw-r--r-- | src/silx/gui/data/DataViews.py | 2059 |
1 files changed, 2059 insertions, 0 deletions
diff --git a/src/silx/gui/data/DataViews.py b/src/silx/gui/data/DataViews.py new file mode 100644 index 0000000..b18a813 --- /dev/null +++ b/src/silx/gui/data/DataViews.py @@ -0,0 +1,2059 @@ +# 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) |