diff options
Diffstat (limited to 'silx/gui/data')
-rw-r--r-- | silx/gui/data/DataViewer.py | 153 | ||||
-rw-r--r-- | silx/gui/data/DataViewerFrame.py | 17 | ||||
-rw-r--r-- | silx/gui/data/DataViewerSelector.py | 40 | ||||
-rw-r--r-- | silx/gui/data/DataViews.py | 365 | ||||
-rw-r--r-- | silx/gui/data/Hdf5TableView.py | 35 | ||||
-rw-r--r-- | silx/gui/data/NXdataWidgets.py | 390 | ||||
-rw-r--r-- | silx/gui/data/NumpyAxesSelector.py | 24 | ||||
-rw-r--r-- | silx/gui/data/TextFormatter.py | 47 | ||||
-rw-r--r-- | silx/gui/data/test/test_dataviewer.py | 87 | ||||
-rw-r--r-- | silx/gui/data/test/test_numpyaxesselector.py | 16 | ||||
-rw-r--r-- | silx/gui/data/test/test_textformatter.py | 13 |
11 files changed, 882 insertions, 305 deletions
diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py index 750c654..5e0b25e 100644 --- a/silx/gui/data/DataViewer.py +++ b/silx/gui/data/DataViewer.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -32,10 +32,12 @@ from silx.gui.data.DataViews import _normalizeData import logging from silx.gui import qt from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector +from silx.utils import deprecation +from silx.utils.property import classproperty __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "03/10/2017" +__date__ = "26/02/2018" _logger = logging.getLogger(__name__) @@ -68,16 +70,65 @@ class DataViewer(qt.QFrame): 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 + # 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""" @@ -129,7 +180,7 @@ class DataViewer(qt.QFrame): """Inisialize the available views""" views = self.createDefaultViews(self.__stack) self.__views = list(views) - self.setDisplayMode(self.EMPTY_MODE) + self.setDisplayMode(DataViews.EMPTY_MODE) def createDefaultViews(self, parent=None): """Create and returns available views which can be displayed by default @@ -137,7 +188,7 @@ class DataViewer(qt.QFrame): overwriten to provide a different set of viewers. :param QWidget parent: QWidget parent of the views - :rtype: list[silx.gui.data.DataViews.DataView] + :rtype: List[silx.gui.data.DataViews.DataView] """ viewClasses = [ DataViews._EmptyView, @@ -262,6 +313,7 @@ class DataViewer(qt.QFrame): 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 @@ -269,7 +321,7 @@ class DataViewer(qt.QFrame): for view in self.__views: if view.modeId() == modeId: return view - return view + return None def setDisplayMode(self, modeId): """Set the displayed view using display mode. @@ -278,13 +330,14 @@ class DataViewer(qt.QFrame): :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 + - `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) @@ -377,21 +430,21 @@ class DataViewer(qt.QFrame): on rendering. :param object data: data which will be displayed - :param list[view] available: List of available views, from highest + :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) + 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 + :param List[view] available: List of available views, from highest priority to lowest. :rtype: DataView """ @@ -403,7 +456,7 @@ class DataViewer(qt.QFrame): view = available[0] else: # else returns the empty view - view = self.getViewFromModeId(DataViewer.EMPTY_MODE) + view = self.getViewFromModeId(DataViews.EMPTY_MODE) return view def __setCurrentAvailableViews(self, availableViews): @@ -462,3 +515,51 @@ class DataViewer(qt.QFrame): 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: + self.__views[idx] = newView + isReplaced = True + break + elif isinstance(view, DataViews.CompositeDataView): + isReplaced = view.replaceView(modeId, newView) + if isReplaced: + break + + if isReplaced: + self.__updateAvailableViews() + return isReplaced diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py index e050d4a..89a9992 100644 --- a/silx/gui/data/DataViewerFrame.py +++ b/silx/gui/data/DataViewerFrame.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -133,7 +133,7 @@ class DataViewerFrame(qt.QWidget): overwriten to provide a different set of viewers. :param QWidget parent: QWidget parent of the views - :rtype: list[silx.gui.data.DataViews.DataView] + :rtype: List[silx.gui.data.DataViews.DataView] """ return self.__dataViewer._createDefaultViews(parent) @@ -192,3 +192,16 @@ class DataViewerFrame(qt.QWidget): - `ARRAY_MODE`: display the data as a table """ return self.__dataViewer.setDisplayMode(modeId) + + def getViewFromModeId(self, modeId): + """See :meth:`DataViewer.getViewFromModeId`""" + return self.__dataViewer.getViewFromModeId(modeId) + + def replaceView(self, modeId, newView): + """Replace one of the builtin data views with a custom view. + See :meth:`DataViewer.replaceView` for more documentation. + + :param DataViews.DataView newView: New data view + :return: True if replacement was successful, else False + """ + return self.__dataViewer.replaceView(modeId, newView) diff --git a/silx/gui/data/DataViewerSelector.py b/silx/gui/data/DataViewerSelector.py index 32cc636..35bbe99 100644 --- a/silx/gui/data/DataViewerSelector.py +++ b/silx/gui/data/DataViewerSelector.py @@ -29,12 +29,11 @@ from __future__ import division __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "26/01/2017" +__date__ = "23/01/2018" import weakref import functools from silx.gui import qt -from silx.gui.data.DataViewer import DataViewer import silx.utils.weakref @@ -51,21 +50,36 @@ class DataViewerSelector(qt.QWidget): self.__group = None self.__buttons = {} + self.__buttonLayout = None self.__buttonDummy = None self.__dataViewer = None + # Create the fixed layout + self.setLayout(qt.QHBoxLayout()) + layout = self.layout() + layout.setContentsMargins(0, 0, 0, 0) + self.__buttonLayout = qt.QHBoxLayout() + self.__buttonLayout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(self.__buttonLayout) + layout.addStretch(1) + if dataViewer is not None: self.setDataViewer(dataViewer) def __updateButtons(self): if self.__group is not None: self.__group.deleteLater() + + # Clean up + for _, b in self.__buttons.items(): + b.deleteLater() + if self.__buttonDummy is not None: + self.__buttonDummy.deleteLater() + self.__buttonDummy = None self.__buttons = {} self.__buttonDummy = None self.__group = qt.QButtonGroup(self) - self.setLayout(qt.QHBoxLayout()) - self.layout().setContentsMargins(0, 0, 0, 0) if self.__dataViewer is None: return @@ -83,19 +97,17 @@ class DataViewerSelector(qt.QWidget): weakMethod = silx.utils.weakref.WeakMethodProxy(self.__setDisplayedView) callback = functools.partial(weakMethod, weakView) button.clicked.connect(callback) - self.layout().addWidget(button) + self.__buttonLayout.addWidget(button) self.__group.addButton(button) self.__buttons[view] = button button = qt.QPushButton("Dummy") button.setCheckable(True) button.setVisible(False) - self.layout().addWidget(button) + self.__buttonLayout.addWidget(button) self.__group.addButton(button) self.__buttonDummy = button - self.layout().addStretch(1) - self.__updateButtonsVisibility() self.__displayedViewChanged(self.__dataViewer.displayedView()) @@ -125,7 +137,7 @@ class DataViewerSelector(qt.QWidget): self.__buttonDummy.setFlat(isFlat) def __displayedViewChanged(self, view): - """Called on displayed view changeS""" + """Called on displayed view changes""" selectedButton = self.__buttons.get(view, self.__buttonDummy) selectedButton.setChecked(True) @@ -142,12 +154,22 @@ class DataViewerSelector(qt.QWidget): return self.__dataViewer.setDisplayedView(view) + def __checkAvailableButtons(self): + views = set(self.__dataViewer.availableViews()) + if views == set(self.__buttons.keys()): + return + # Recreate all the buttons + # TODO: We dont have to create everything again + # We expect the views stay quite stable + self.__updateButtons() + def __updateButtonsVisibility(self): """Called on data changed""" if self.__dataViewer is None: for b in self.__buttons.values(): b.setVisible(False) else: + self.__checkAvailableButtons() availableViews = set(self.__dataViewer.currentAvailableViews()) for view, button in self.__buttons.items(): button.setVisible(view in availableViews) diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py index 1ad997b..ef69441 100644 --- a/silx/gui/data/DataViews.py +++ b/silx/gui/data/DataViews.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -35,11 +35,13 @@ 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 NXdata, get_attr_as_string +from silx.io.nxdata import get_attr_as_string +from silx.gui.plot.Colormap import Colormap +from silx.gui.plot.actions.control import ColormapAction __authors__ = ["V. Valls", "P. Knobel"] __license__ = "MIT" -__date__ = "03/10/2017" +__date__ = "23/01/2018" _logger = logging.getLogger(__name__) @@ -47,7 +49,9 @@ _logger = logging.getLogger(__name__) # DataViewer modes EMPTY_MODE = 0 PLOT1D_MODE = 10 -PLOT2D_MODE = 20 +IMAGE_MODE = 20 +PLOT2D_MODE = 21 +COMPLEX_IMAGE_MODE = 22 PLOT3D_MODE = 30 RAW_MODE = 40 RAW_ARRAY_MODE = 41 @@ -56,6 +60,13 @@ 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 def _normalizeData(data): @@ -77,7 +88,7 @@ def _normalizeComplex(data): absolute value. Else returns the input data.""" if hasattr(data, "dtype"): - isComplex = numpy.issubdtype(data.dtype, numpy.complex) + isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating) else: isComplex = isinstance(data, numbers.Complex) if isComplex: @@ -97,7 +108,7 @@ class DataInfo(object): self.isComplex = False self.isBoolean = False self.isRecord = False - self.isNXdata = False + self.hasNXdata = False self.shape = tuple() self.dim = 0 self.size = 0 @@ -105,9 +116,10 @@ class DataInfo(object): if data is None: return - if silx.io.is_group(data) and nxdata.is_valid_nxdata(data): - self.isNXdata = True - nxd = nxdata.NXdata(data) + if silx.io.is_group(data): + nxd = nxdata.get_default(data) + if nxd is not None: + self.hasNXdata = True if isinstance(data, numpy.ndarray): self.isArray = True @@ -121,7 +133,7 @@ class DataInfo(object): self.interpretation = get_attr_as_string(data, "interpretation") else: self.interpretation = None - elif self.isNXdata: + elif self.hasNXdata: self.interpretation = nxd.interpretation else: self.interpretation = None @@ -132,12 +144,12 @@ class DataInfo(object): 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.complex) + self.isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating) self.isBoolean = numpy.issubdtype(data.dtype, numpy.bool_) - elif self.isNXdata: + elif self.hasNXdata: self.isNumeric = numpy.issubdtype(nxd.signal.dtype, numpy.number) - self.isComplex = numpy.issubdtype(nxd.signal.dtype, numpy.complex) + 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) @@ -147,7 +159,7 @@ class DataInfo(object): if hasattr(data, "shape"): self.shape = data.shape - elif self.isNXdata: + elif self.hasNXdata: self.shape = nxd.signal.shape else: self.shape = tuple() @@ -172,6 +184,12 @@ class DataView(object): """Priority returned when the requested data can't be displayed by the view.""" + _defaultColormap = None + """Store a default colormap shared with all the views""" + + _defaultColorDialog = None + """Store a default color dialog shared with all the views""" + def __init__(self, parent, modeId=None, icon=None, label=None): """Constructor @@ -187,6 +205,32 @@ class DataView(object): icon = qt.QIcon() self.__icon = icon + @staticmethod + def defaultColormap(): + """Returns a shared colormap as default for all the views. + + :rtype: Colormap + """ + if DataView._defaultColormap is None: + DataView._defaultColormap = Colormap(name="viridis") + return DataView._defaultColormap + + @staticmethod + def defaultColorDialog(): + """Returns a shared color dialog as default for all the views. + + :rtype: ColorDialog + """ + if DataView._defaultColorDialog is None: + DataView._defaultColorDialog = ColormapAction._createDialog(qt.QApplication.instance().activeWindow()) + return DataView._defaultColorDialog + + @staticmethod + def _cleanUpCache(): + """Clean up the cache. Needed for tests""" + DataView._defaultColormap = None + DataView._defaultColorDialog = None + def icon(self): """Returns the default icon""" return self.__icon @@ -305,6 +349,13 @@ class CompositeDataView(DataView): """Add a new dataview to the available list.""" self.__views[dataView] = None + def availableViews(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.""" views = [(v.getDataPriority(data, info), v) for v in self.__views.keys()] @@ -374,6 +425,38 @@ class CompositeDataView(DataView): else: return view.getDataPriority(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 + 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 + class _EmptyView(DataView): """Dummy view to display nothing""" @@ -457,6 +540,8 @@ class _Plot2dView(DataView): 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') @@ -582,13 +667,18 @@ class _ComplexImageView(DataView): def __init__(self, parent): super(_ComplexImageView, self).__init__( parent=parent, - modeId=PLOT2D_MODE, + 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.Mode.ABSOLUTE) + widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.SQUARE_AMPLITUDE) + widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.REAL) + widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.IMAGINARY) + widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) widget.getPlot().getIntensityHistogramAction().setVisible(True) widget.getPlot().setKeepDataAspectRatio(True) widget.getXAxis().setLabel('X') @@ -681,6 +771,8 @@ class _StackView(DataView): def createWidget(self, parent): from silx.gui import plot widget = plot.StackView(parent=parent) + widget.setColormap(self.defaultColormap()) + widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) widget.setKeepDataAspectRatio(True) widget.setLabels(self.axesNames(None, None)) # hide default option panel @@ -699,6 +791,8 @@ class _StackView(DataView): 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 axesNames(self, data, info): @@ -736,7 +830,11 @@ class _ScalarView(DataView): d = self.normalizeData(data) if silx.io.is_dataset(d): d = d[()] - text = self.__formatter.toString(d, data.dtype) + 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): @@ -891,18 +989,111 @@ class _ImageView(CompositeDataView): def __init__(self, parent): super(_ImageView, self).__init__( parent=parent, - modeId=PLOT2D_MODE, + 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 silx.io.is_group(data): + nxd = nxdata.get_default(data) + nx_class = get_attr_as_string(data, "NX_class") + + if nxd is None: + 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." + return 100 + elif nx_class == "NXentry": + if "default" not in data.attrs: + # no link to NXdata, no problem + return DataView.UNSUPPORTED + self._msg = "NXentry 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_string(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 + elif nx_class == "NXroot" or silx.io.is_file(data): + if "default" not in data.attrs: + # no link to NXentry, no problem + return DataView.UNSUPPORTED + default_entry_name = data.attrs["default"] + if default_entry_name not in data: + # this is a problem, but not NXdata related + return DataView.UNSUPPORTED + default_entry = data[default_entry_name] + if "default" not in default_entry.attrs: + # no NXdata specified, no problemo + return DataView.UNSUPPORTED + 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_string(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." + return 100 + else: + # Not pretending to be NXdata, no problem + return DataView.UNSUPPORTED + + 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 (I cannot imagine a case) + self._msg = "NXdata seems valid, but cannot be displayed " + self._msg += "by any existing plot widget." + return 100 + + return DataView.UNSUPPORTED + + class _NXdataScalarView(DataView): """DataView using a table view for displaying NXdata scalars: 0-D signal or n-D signal with *@interpretation=scalar*""" def __init__(self, parent): - DataView.__init__(self, parent) + DataView.__init__(self, parent, + modeId=NXDATA_SCALAR_MODE) def createWidget(self, parent): from silx.gui.data.ArrayTableWidget import ArrayTableWidget @@ -919,14 +1110,17 @@ class _NXdataScalarView(DataView): def setData(self, data): data = self.normalizeData(data) - signal = NXdata(data).signal + # data could be a NXdata or an NXentry + nxd = nxdata.get_default(data) + signal = nxd.signal self.getWidget().setArrayData(signal, labels=True) def getDataPriority(self, data, info): data = self.normalizeData(data) - if info.isNXdata: - nxd = NXdata(data) + + if info.hasNXdata: + nxd = nxdata.get_default(data) if nxd.signal_is_0d or nxd.interpretation in ["scalar", "scaler"]: return 100 return DataView.UNSUPPORTED @@ -940,7 +1134,8 @@ class _NXdataCurveView(DataView): a 1-D signal with one axis whose values are not monotonically increasing. """ def __init__(self, parent): - DataView.__init__(self, parent) + DataView.__init__(self, parent, + modeId=NXDATA_CURVE_MODE) def createWidget(self, parent): from silx.gui.data.NXdataWidgets import ArrayCurvePlot @@ -956,29 +1151,34 @@ class _NXdataCurveView(DataView): def setData(self, data): data = self.normalizeData(data) - nxd = NXdata(data) - signal_name = get_attr_as_string(data, "signal") - group_name = data.name + nxd = nxdata.get_default(data) + 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 - self.getWidget().setCurveData(nxd.signal, nxd.axes[-1], - yerror=nxd.errors, xerror=x_errors, - ylabel=signal_name, xlabel=nxd.axes_names[-1], - title="NXdata group " + group_name) + # 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]) def getDataPriority(self, data, info): data = self.normalizeData(data) - if info.isNXdata: - nxd = NXdata(data) - if nxd.is_x_y_value_scatter or nxd.is_unsupported_scatter: - return DataView.UNSUPPORTED - if nxd.signal_is_1d and \ - not nxd.interpretation in ["scalar", "scaler"]: - return 100 - if nxd.interpretation == "spectrum": + if info.hasNXdata: + if nxdata.get_default(data).is_curve: return 100 return DataView.UNSUPPORTED @@ -987,11 +1187,12 @@ class _NXdataXYVScatterView(DataView): """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): - DataView.__init__(self, parent) + DataView.__init__(self, parent, + modeId=NXDATA_XYVSCATTER_MODE) def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayCurvePlot - widget = ArrayCurvePlot(parent) + from silx.gui.data.NXdataWidgets import XYVScatterPlot + widget = XYVScatterPlot(parent) return widget def axesNames(self, data, info): @@ -1003,10 +1204,7 @@ class _NXdataXYVScatterView(DataView): def setData(self, data): data = self.normalizeData(data) - nxd = NXdata(data) - signal_name = get_attr_as_string(data, "signal") - # signal_errors = nx.errors # not supported - group_name = data.name + nxd = nxdata.get_default(data) x_axis, y_axis = nxd.axes[-2:] x_label, y_label = nxd.axes_names[-2:] @@ -1020,16 +1218,18 @@ class _NXdataXYVScatterView(DataView): else: y_errors = None - self.getWidget().setCurveData(y_axis, x_axis, values=nxd.signal, - yerror=y_errors, xerror=x_errors, - ylabel=signal_name, xlabel=x_label, - title="NXdata group " + group_name) + 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) def getDataPriority(self, data, info): data = self.normalizeData(data) - if info.isNXdata: - if NXdata(data).is_x_y_value_scatter: + if info.hasNXdata: + if nxdata.get_default(data).is_x_y_value_scatter: return 100 + return DataView.UNSUPPORTED @@ -1037,11 +1237,14 @@ class _NXdataImageView(DataView): """DataView using a Plot2D for displaying NXdata images: 2-D signal or n-D signals with *@interpretation=spectrum*.""" def __init__(self, parent): - DataView.__init__(self, parent) + DataView.__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): @@ -1053,36 +1256,41 @@ class _NXdataImageView(DataView): def setData(self, data): data = self.normalizeData(data) - nxd = NXdata(data) - signal_name = get_attr_as_string(data, "signal") - group_name = data.name - y_axis, x_axis = nxd.axes[-2:] - y_label, x_label = nxd.axes_names[-2:] + nxd = nxdata.get_default(data) + isRgba = nxd.interpretation == "rgba-image" + + # 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] self.getWidget().setImageData( - nxd.signal, x_axis=x_axis, y_axis=y_axis, - signal_name=signal_name, xlabel=x_label, ylabel=y_label, - title="NXdata group %s: %s" % (group_name, signal_name)) + [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) def getDataPriority(self, data, info): data = self.normalizeData(data) - if info.isNXdata: - nxd = NXdata(data) - if nxd.signal_is_2d: - if nxd.interpretation not in ["scalar", "spectrum", "scaler"]: - return 100 - if nxd.interpretation == "image": + + if info.hasNXdata: + if nxdata.get_default(data).is_image: return 100 + return DataView.UNSUPPORTED class _NXdataStackView(DataView): def __init__(self, parent): - DataView.__init__(self, parent) + DataView.__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().getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) return widget def axesNames(self, data, info): @@ -1094,26 +1302,27 @@ class _NXdataStackView(DataView): def setData(self, data): data = self.normalizeData(data) - nxd = NXdata(data) - signal_name = get_attr_as_string(data, "signal") - group_name = data.name + nxd = nxdata.get_default(data) + 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.getWidget().setStackData( + 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="NXdata group %s: %s" % (group_name, signal_name)) + 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.isNXdata: - nxd = NXdata(data) - if nxd.signal_ndim >= 3: - if nxd.interpretation not in ["scalar", "scaler", - "spectrum", "image"]: - return 100 + if info.hasNXdata: + if nxdata.get_default(data).is_stack: + return 100 + return DataView.UNSUPPORTED @@ -1124,8 +1333,10 @@ class _NXdataView(CompositeDataView): 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)) diff --git a/silx/gui/data/Hdf5TableView.py b/silx/gui/data/Hdf5TableView.py index ba737e3..e4a0747 100644 --- a/silx/gui/data/Hdf5TableView.py +++ b/silx/gui/data/Hdf5TableView.py @@ -30,7 +30,7 @@ from __future__ import division __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "29/09/2017" +__date__ = "10/10/2017" import functools import os.path @@ -330,7 +330,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): self.__data.addHeaderRow(headerLabel="Data info") - if h5py is not None and hasattr(obj, "id"): + if h5py is not None and hasattr(obj, "id") and hasattr(obj.id, "get_type"): # display the HDF5 type self.__data.addHeaderValueRow("HDF5 type", self.__formatHdf5Type) self.__data.addHeaderValueRow("dtype", self.__formatDType) @@ -345,21 +345,22 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): # h5py also expose fletcher32 and shuffle attributes, but it is also # part of the filters if hasattr(obj, "shape") and hasattr(obj, "id"): - dcpl = obj.id.get_create_plist() - if dcpl.get_nfilters() > 0: - self.__data.addHeaderRow(headerLabel="Compression info") - pos = _CellData(value="Position", isHeader=True) - hdf5id = _CellData(value="HDF5 ID", isHeader=True) - name = _CellData(value="Name", isHeader=True) - options = _CellData(value="Options", isHeader=True) - self.__data.addRow(pos, hdf5id, name, options) - for index in range(dcpl.get_nfilters()): - callback = lambda index, dataIndex, x: self.__get_filter_info(x, index)[dataIndex] - pos = _CellData(value=functools.partial(callback, index, 0)) - hdf5id = _CellData(value=functools.partial(callback, index, 1)) - name = _CellData(value=functools.partial(callback, index, 2)) - options = _CellData(value=functools.partial(callback, index, 3)) - self.__data.addRow(pos, hdf5id, name, options) + if hasattr(obj.id, "get_create_plist"): + dcpl = obj.id.get_create_plist() + if dcpl.get_nfilters() > 0: + self.__data.addHeaderRow(headerLabel="Compression info") + pos = _CellData(value="Position", isHeader=True) + hdf5id = _CellData(value="HDF5 ID", isHeader=True) + name = _CellData(value="Name", isHeader=True) + options = _CellData(value="Options", isHeader=True) + self.__data.addRow(pos, hdf5id, name, options) + for index in range(dcpl.get_nfilters()): + callback = lambda index, dataIndex, x: self.__get_filter_info(x, index)[dataIndex] + pos = _CellData(value=functools.partial(callback, index, 0)) + hdf5id = _CellData(value=functools.partial(callback, index, 1)) + name = _CellData(value=functools.partial(callback, index, 2)) + options = _CellData(value=functools.partial(callback, index, 3)) + self.__data.addRow(pos, hdf5id, name, options) if hasattr(obj, "attrs"): if len(obj.attrs) > 0: diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py index 7aaf3ad..ae2911d 100644 --- a/silx/gui/data/NXdataWidgets.py +++ b/silx/gui/data/NXdataWidgets.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017 European Synchrotron Radiation Facility +# Copyright (c) 2017-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,13 +26,15 @@ """ __authors__ = ["P. Knobel"] __license__ = "MIT" -__date__ = "27/06/2017" +__date__ = "20/12/2017" import numpy from silx.gui import qt from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector from silx.gui.plot import Plot1D, Plot2D, StackView +from silx.gui.plot.Colormap import Colormap +from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser from silx.math.calibration import ArrayCalibration, NoCalibration, LinearCalibration @@ -60,83 +62,79 @@ class ArrayCurvePlot(qt.QWidget): """ super(ArrayCurvePlot, self).__init__(parent) - self.__signal = None - self.__signal_name = None + self.__signals = None + self.__signals_names = None self.__signal_errors = None self.__axis = None self.__axis_name = None - self.__axis_errors = None + self.__x_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) + 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) + self._plot.sigActiveCurveChanged.connect(self._setYLabelFromActiveLegend) + layout = qt.QGridLayout() layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._plot, 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): + def getPlot(self): + """Returns the plot used for the display + + :rtype: Plot1D + """ + return self._plot + + def setCurvesData(self, ys, x=None, + yerror=None, xerror=None, + ylabels=None, xlabel=None, title=None): """ - :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 + :param List[ndarray] ys: List of arrays to be represented by the y (vertical) axis. + It can be multiple n-D array whose last dimension must have the same length as x (and values must be None) - :param x: 1-D dataset used as the curve's x values. If provided, + :param ndarray x: 1-D dataset used as the curve's x values. If provided, its lengths must be equal to the length of the last dimension of ``y`` (and equal to the length of ``value``, for a scatter plot). - :param 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 + :param ndarray yerror: Single array of errors for y (same shape), or None. + There can only be one array, and it applies to the first/main y + (no y errors for auxiliary_signals curves). + :param ndarray xerror: 1-D dataset of errors for x, or None + :param str ylabels: Labels for each curve's Y axis + :param str xlabel: Label for X axis + :param str title: Graph title """ - self.__signal = y - self.__signal_name = ylabel or "Y" + self.__signals = ys + self.__signals_names = ylabels or (["Y"] * len(ys)) self.__signal_errors = yerror self.__axis = x self.__axis_name = xlabel - self.__axis_errors = xerror - self.__values = values + self.__x_axis_errors = xerror 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"]) + self._selector.setData(ys[0]) + self._selector.setAxisNames(["Y"]) - if len(y.shape) < 2: + if len(ys[0].shape) < 2: self.selectorDock.hide() else: self.selectorDock.show() self._plot.setGraphTitle(title or "") - self._plot.getXAxis().setLabel(self.__axis_name or "X") - self._plot.getYAxis().setLabel(self.__signal_name) self._updateCurve() if not self.__selector_is_connected: @@ -144,52 +142,165 @@ class ArrayCurvePlot(qt.QWidget): self.__selector_is_connected = True def _updateCurve(self): - y = self._selector.selectedData() + selection = self._selector.selection() + ys = [sig[selection] for sig in self.__signals] + y0 = ys[0] + len_y = len(y0) x = self.__axis if x is None: - x = numpy.arange(len(y)) + 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: + x = x * numpy.ones_like(y0) + elif len(x) == 2 and len_y != 2: # linear calibration a + b * x - x = x[0] + x[1] * numpy.arange(len(y)) - 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 + x = x[0] + x[1] * numpy.arange(len_y) - self._plot.remove(kind=("curve", "scatter")) + self._plot.remove(kind=("curve",)) - # 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) + for i in range(len(self.__signals)): + legend = self.__signals_names[i] - # x monotonically increasing or decreasiing: curve - elif numpy.all(numpy.diff(x) > 0) or numpy.all(numpy.diff(x) < 0): - self._plot.addCurve(x, y, legend=legend, - xerror=self.__axis_errors, + # errors only supported for primary signal in NXdata + y_errors = None + if i == 0 and self.__signal_errors is not None: + y_errors = self.__signal_errors[self._selector.selection()] + self._plot.addCurve(x, ys[i], legend=legend, + xerror=self.__x_axis_errors, yerror=y_errors) + if i == 0: + self._plot.setActiveCurve(legend) - # 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.getXAxis().setLabel(self.__axis_name) - self._plot.getYAxis().setLabel(self.__signal_name) + self._plot.getYAxis().setLabel(self.__signals_names[0]) + + def _setYLabelFromActiveLegend(self, previous_legend, new_legend): + for ylabel in self.__signals_names: + if new_legend is not None and new_legend == ylabel: + self._plot.getYAxis().setLabel(ylabel) + break + + def clear(self): + self._plot.clear() + + +class XYVScatterPlot(qt.QWidget): + """ + Widget for plotting one or more scatters + (with identical x, y coordinates). + """ + def __init__(self, parent=None): + """ + + :param parent: Parent QWidget + """ + super(XYVScatterPlot, self).__init__(parent) + + self.__y_axis = None + """1D array""" + self.__y_axis_name = None + self.__values = None + """List of 1D arrays (for multiple scatters with identical + x, y coordinates)""" + + self.__x_axis = None + self.__x_axis_name = None + self.__x_axis_errors = None + self.__y_axis = None + self.__y_axis_name = None + self.__y_axis_errors = None + + self._plot = Plot1D(self) + self._plot.setDefaultColormap(Colormap(name="viridis", + vmin=None, vmax=None, + normalization=Colormap.LINEAR)) + + self._slider = HorizontalSliderWithBrowser(parent=self) + self._slider.setMinimum(0) + self._slider.setValue(0) + self._slider.valueChanged[int].connect(self._sliderIdxChanged) + self._slider.setToolTip("Select auxiliary signals") + + layout = qt.QGridLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self._plot, 0, 0) + layout.addWidget(self._slider, 1, 0) + + self.setLayout(layout) + + def _sliderIdxChanged(self, value): + self._updateScatter() + + def getPlot(self): + """Returns the plot used for the display + + :rtype: Plot1D + """ + return self._plot + + def setScattersData(self, y, x, values, + yerror=None, xerror=None, + ylabel=None, xlabel=None, + title="", scatter_titles=None): + """ + + :param ndarray y: 1D array for y (vertical) coordinates. + :param ndarray x: 1D array for x coordinates. + :param List[ndarray] values: List of 1D arrays of values. + This will be used to compute the color map and assign colors + to the points. There should be as many arrays in the list as + scatters to be represented. + :param ndarray yerror: 1D array of errors for y (same shape), or None. + :param ndarray xerror: 1D array of errors for x, or None + :param str ylabel: Label for Y axis + :param str xlabel: Label for X axis + :param str title: Main graph title + :param List[str] scatter_titles: Subtitles (one per scatter) + """ + self.__y_axis = y + self.__x_axis = x + self.__x_axis_name = xlabel or "X" + self.__y_axis_name = ylabel or "Y" + self.__x_axis_errors = xerror + self.__y_axis_errors = yerror + self.__values = values + + self.__graph_title = title or "" + self.__scatter_titles = scatter_titles + + self._slider.valueChanged[int].disconnect(self._sliderIdxChanged) + self._slider.setMaximum(len(values) - 1) + if len(values) > 1: + self._slider.show() + else: + self._slider.hide() + self._slider.setValue(0) + self._slider.valueChanged[int].connect(self._sliderIdxChanged) + + self._updateScatter() + + def _updateScatter(self): + x = self.__x_axis + y = self.__y_axis + + self._plot.remove(kind=("scatter", )) + + idx = self._slider.value() + + title = "" + if self.__graph_title: + title += self.__graph_title + "\n" # main NXdata @title + title += self.__scatter_titles[idx] # scatter dataset name + + self._plot.setGraphTitle(title) + self._plot.addScatter(x, y, self.__values[idx], + legend="scatter%d" % idx, + xerror=self.__x_axis_errors, + yerror=self.__y_axis_errors) + self._plot.resetZoom() + self._plot.getXAxis().setLabel(self.__x_axis_name) + self._plot.getYAxis().setLabel(self.__y_axis_name) def clear(self): self._plot.clear() @@ -218,97 +329,117 @@ class ArrayImagePlot(qt.QWidget): """ super(ArrayImagePlot, self).__init__(parent) - self.__signal = None - self.__signal_name = None + self.__signals = None + self.__signals_names = None self.__x_axis = None self.__x_axis_name = None self.__y_axis = None self.__y_axis_name = None self._plot = Plot2D(self) - self._plot.setDefaultColormap( - {"name": "viridis", - "vmin": 0., "vmax": 1., # ignored (autoscale) but mandatory - "normalization": "linear", - "autoscale": True}) + self._plot.setDefaultColormap(Colormap(name="viridis", + vmin=None, vmax=None, + normalization=Colormap.LINEAR)) 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 + self._selector.selectionChanged.connect(self._updateImage) + + self._auxSigSlider = HorizontalSliderWithBrowser(parent=self) + self._auxSigSlider.setMinimum(0) + self._auxSigSlider.setValue(0) + self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged) + self._auxSigSlider.setToolTip("Select auxiliary signals") layout = qt.QVBoxLayout() layout.addWidget(self._plot) - layout.addWidget(self._legend) + layout.addWidget(self._auxSigSlider) self.selectorDock.setWidget(self._selector) self._plot.addTabbedDockWidget(self.selectorDock) self.setLayout(layout) - def setImageData(self, signal, + def _sliderIdxChanged(self, value): + self._updateImage() + + def getPlot(self): + """Returns the plot used for the display + + :rtype: Plot2D + """ + return self._plot + + def setImageData(self, signals, x_axis=None, y_axis=None, - signal_name=None, + signals_names=None, xlabel=None, ylabel=None, - title=None): + title=None, isRgba=False): """ - :param signal: n-D dataset, whose last 2 dimensions are used as the - image's values. + :param signals: list of n-D datasets, whose last 2 dimensions are used as the + image's values, or list of 3D datasets interpreted as RGBA image. :param x_axis: 1-D dataset used as the image's x coordinates. If provided, its lengths must be equal to the length of the last dimension of ``signal``. :param y_axis: 1-D dataset used as the image's y. If provided, its lengths must be equal to the length of the 2nd to last dimension of ``signal``. - :param signal_name: Label used in the legend + :param signals_names: Names for each image, used as subtitle and legend. :param xlabel: Label for X axis :param ylabel: Label for Y axis :param title: Graph title + :param isRgba: True if data is a 3D RGBA image """ - if self.__selector_is_connected: - self._selector.selectionChanged.disconnect(self._updateImage) - self.__selector_is_connected = False + self._selector.selectionChanged.disconnect(self._updateImage) + self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) - self.__signal = signal - self.__signal_name = signal_name or "" + self.__signals = signals + self.__signals_names = signals_names self.__x_axis = x_axis self.__x_axis_name = xlabel self.__y_axis = y_axis self.__y_axis_name = ylabel + self.__title = title - self._selector.setData(signal) - self._selector.setAxisNames([ylabel or "Y", xlabel or "X"]) + self._selector.clear() + if not isRgba: + self._selector.setAxisNames(["Y", "X"]) + img_ndim = 2 + else: + self._selector.setAxisNames(["Y", "X", "RGB(A) channel"]) + img_ndim = 3 + self._selector.setData(signals[0]) - if len(signal.shape) < 3: + if len(signals[0].shape) <= img_ndim: self.selectorDock.hide() else: self.selectorDock.show() - self._plot.setGraphTitle(title or "") - self._plot.getXAxis().setLabel(self.__x_axis_name or "X") - self._plot.getYAxis().setLabel(self.__y_axis_name or "Y") + self._auxSigSlider.setMaximum(len(signals) - 1) + if len(signals) > 1: + self._auxSigSlider.show() + else: + self._auxSigSlider.hide() + self._auxSigSlider.setValue(0) self._updateImage() - if not self.__selector_is_connected: - self._selector.selectionChanged.connect(self._updateImage) - self.__selector_is_connected = True + self._selector.selectionChanged.connect(self._updateImage) + self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) 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) + selection = self._selector.selection() + auxSigIdx = self._auxSigSlider.value() + + legend = self.__signals_names[auxSigIdx] + + images = [img[selection] for img in self.__signals] + image = images[auxSigIdx] - img = self._selector.selectedData() x_axis = self.__x_axis y_axis = self.__y_axis @@ -318,25 +449,25 @@ class ArrayImagePlot(qt.QWidget): else: if x_axis is None: # no calibration - x_axis = numpy.arange(img.shape[-1]) + x_axis = numpy.arange(image.shape[1]) elif numpy.isscalar(x_axis) or len(x_axis) == 1: # constant axis - x_axis = x_axis * numpy.ones((img.shape[-1], )) + x_axis = x_axis * numpy.ones((image.shape[1], )) elif len(x_axis) == 2: # linear calibration - x_axis = x_axis[0] * numpy.arange(img.shape[-1]) + x_axis[1] + x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1] if y_axis is None: - y_axis = numpy.arange(img.shape[-2]) + y_axis = numpy.arange(image.shape[0]) elif numpy.isscalar(y_axis) or len(y_axis) == 1: - y_axis = y_axis * numpy.ones((img.shape[-2], )) + y_axis = y_axis * numpy.ones((image.shape[0], )) elif len(y_axis) == 2: - y_axis = y_axis[0] * numpy.arange(img.shape[-2]) + y_axis[1] + y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1] xcalib = ArrayCalibration(x_axis) ycalib = ArrayCalibration(y_axis) - self._plot.remove(kind=("scatter", "image")) + self._plot.remove(kind=("scatter", "image",)) if xcalib.is_affine() and ycalib.is_affine(): # regular image xorigin, xscale = xcalib(0), xcalib.get_slope() @@ -344,14 +475,22 @@ class ArrayImagePlot(qt.QWidget): origin = (xorigin, yorigin) scale = (xscale, yscale) - self._plot.addImage(img, legend=legend, + self._plot.addImage(image, legend=legend, origin=origin, scale=scale) else: scatterx, scattery = numpy.meshgrid(x_axis, y_axis) + # fixme: i don't think this can handle "irregular" RGBA images self._plot.addScatter(numpy.ravel(scatterx), numpy.ravel(scattery), - numpy.ravel(img), + numpy.ravel(image), legend=legend) + + title = "" + if self.__title: + title += self.__title + if not title.strip().endswith(self.__signals_names[auxSigIdx]): + title += "\n" + self.__signals_names[auxSigIdx] + self._plot.setGraphTitle(title) self._plot.getXAxis().setLabel(self.__x_axis_name) self._plot.getYAxis().setLabel(self.__y_axis_name) self._plot.resetZoom() @@ -408,6 +547,13 @@ class ArrayStackPlot(qt.QWidget): 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, @@ -446,7 +592,7 @@ class ArrayStackPlot(qt.QWidget): self.__z_axis_name = zlabel self._selector.setData(signal) - self._selector.setAxisNames([ylabel or "Y", xlabel or "X", zlabel or "Z"]) + self._selector.setAxisNames(["Y", "X", "Z"]) self._stack_view.setGraphTitle(title or "") # by default, the z axis is the image position (dimension not plotted) diff --git a/silx/gui/data/NumpyAxesSelector.py b/silx/gui/data/NumpyAxesSelector.py index f4641da..4530aa9 100644 --- a/silx/gui/data/NumpyAxesSelector.py +++ b/silx/gui/data/NumpyAxesSelector.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -29,7 +29,7 @@ from __future__ import division __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "16/01/2017" +__date__ = "29/01/2018" import numpy import functools @@ -133,7 +133,7 @@ class _Axis(qt.QWidget): def setAxisNames(self, axesNames): """Set the available list of names for the axis. - :param list[str] axesNames: List of available names + :param List[str] axesNames: List of available names """ self.__axes.clear() previous = self.__axes.blockSignals(True) @@ -146,7 +146,7 @@ class _Axis(qt.QWidget): def setCustomAxis(self, axesNames): """Set the available list of named axis which can be set to a value. - :param list[str] axesNames: List of customable axis names + :param List[str] axesNames: List of customable axis names """ self.__customAxisNames = set(axesNames) self.__updateSliderVisibility() @@ -258,9 +258,12 @@ class NumpyAxesSelector(qt.QWidget): The size of the list will constrain the dimension of the resulting array. - :param list[str] axesNames: List of string identifying axis names + :param List[str] axesNames: List of distinct strings identifying axis names """ self.__axisNames = list(axesNames) + assert len(set(self.__axisNames)) == len(self.__axisNames),\ + "Non-unique axes names: %s" % self.__axisNames + delta = len(self.__axis) - len(self.__axisNames) if delta < 0: delta = 0 @@ -277,7 +280,7 @@ class NumpyAxesSelector(qt.QWidget): def setCustomAxis(self, axesNames): """Set the available list of named axis which can be set to a value. - :param list[str] axesNames: List of customable axis names + :param List[str] axesNames: List of customable axis names """ self.__customAxisNames = set(axesNames) for axis in self.__axis: @@ -415,13 +418,20 @@ class NumpyAxesSelector(qt.QWidget): else: selection.append(slice(None)) axisNames.append(name) - self.__selection = tuple(selection) # get a view with few fixed dimensions # with a h5py dataset, it create a copy # TODO we can reuse the same memory in case of a copy view = self.__data[self.__selection] + if set(self.__axisNames) - set(axisNames) != set([]): + # Not all the expected axis are there + if self.__selectedData is not None: + self.__selectedData = None + self.__selection = tuple() + self.selectionChanged.emit() + return + # order axis as expected source = [] destination = [] diff --git a/silx/gui/data/TextFormatter.py b/silx/gui/data/TextFormatter.py index 37e1f48..332625c 100644 --- a/silx/gui/data/TextFormatter.py +++ b/silx/gui/data/TextFormatter.py @@ -27,12 +27,13 @@ data module to format data as text in the same way.""" __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "27/09/2017" +__date__ = "13/12/2017" import numpy import numbers from silx.third_party import six from silx.gui import qt +import logging try: import h5py @@ -40,6 +41,9 @@ except ImportError: h5py = None +_logger = logging.getLogger(__name__) + + class TextFormatter(qt.QObject): """Formatter to convert data to string. @@ -203,8 +207,9 @@ class TextFormatter(qt.QObject): data = [ord(d) for d in data.item()] else: data = data.item().astype(numpy.uint8) - else: + elif six.PY2: data = [ord(d) for d in data] + # In python3 data is already a bytes array data = ["\\x%02X" % d for d in data] if self.__useQuoteForText: return "b\"%s\"" % "".join(data) @@ -221,6 +226,30 @@ class TextFormatter(qt.QObject): else: return "".join(data) + def __formatCharString(self, data): + """Format text of char. + + From the specifications we expect to have ASCII, but we also allow + CP1252 in some ceases as fallback. + + If no encoding fits, it will display a readable ASCII chars, with + escaped chars (using the python syntax) for non decoded characters. + + :param data: A binary string of char expected in ASCII + :rtype: str + """ + try: + text = "%s" % data.decode("ascii") + return self.__formatText(text) + except UnicodeDecodeError: + # Here we can spam errors, this is definitly a badly + # generated file + _logger.error("Invalid ASCII string %s.", data) + if data == b"\xB0": + _logger.error("Fallback using cp1252 encoding") + return self.__formatText(u"\u00B0") + return self.__formatSafeAscii(data) + def __formatH5pyObject(self, data, dtype): # That's an HDF5 object ref = h5py.check_dtype(ref=dtype) @@ -236,11 +265,7 @@ class TextFormatter(qt.QObject): return self.__formatText(data) elif vlen == six.binary_type: # HDF5 ASCII - try: - text = "%s" % data.decode("ascii") - return self.__formatText(text) - except UnicodeDecodeError: - return self.__formatSafeAscii(data) + return self.__formatCharString(data) return None def toString(self, data, dtype=None): @@ -276,14 +301,12 @@ class TextFormatter(qt.QObject): elif isinstance(data, (numpy.unicode_, six.text_type)): return self.__formatText(data) elif isinstance(data, (numpy.string_, six.binary_type)): + if dtype is None and hasattr(data, "dtype"): + dtype = data.dtype if dtype is not None: # Maybe a sub item from HDF5 if dtype.kind == 'S': - try: - text = "%s" % data.decode("ascii") - return self.__formatText(text) - except UnicodeDecodeError: - return self.__formatSafeAscii(data) + return self.__formatCharString(data) elif dtype.kind == 'O': if h5py is not None: text = self.__formatH5pyObject(data, dtype) diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py index dd3114a..274df92 100644 --- a/silx/gui/data/test/test_dataviewer.py +++ b/silx/gui/data/test/test_dataviewer.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -24,7 +24,7 @@ # ###########################################################################*/ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "22/08/2017" +__date__ = "22/02/2018" import os import tempfile @@ -67,7 +67,8 @@ class _DataViewMock(DataView): class AbstractDataViewerTests(TestCaseQt): def create_widget(self): - raise NotImplementedError() + # Avoid to raise an error when testing the full module + self.skipTest("Not implemented") @contextmanager def h5_temporary_file(self): @@ -89,7 +90,7 @@ class AbstractDataViewerTests(TestCaseQt): widget = self.create_widget() for data in data_list: widget.setData(data) - self.assertEqual(DataViewer.RAW_MODE, widget.displayMode()) + self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) def test_plot_1d_data(self): data = numpy.arange(3 ** 1) @@ -97,35 +98,35 @@ class AbstractDataViewerTests(TestCaseQt): widget = self.create_widget() widget.setData(data) availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViewer.RAW_MODE, widget.displayMode()) - self.assertIn(DataViewer.PLOT1D_MODE, availableModes) + self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) + self.assertIn(DataViews.PLOT1D_MODE, availableModes) - def test_plot_2d_data(self): + def test_image_data(self): data = numpy.arange(3 ** 2) data.shape = [3] * 2 widget = self.create_widget() widget.setData(data) availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViewer.RAW_MODE, widget.displayMode()) - self.assertIn(DataViewer.PLOT2D_MODE, availableModes) + self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) + self.assertIn(DataViews.IMAGE_MODE, availableModes) - def test_plot_2d_bool(self): + def test_image_bool(self): data = numpy.zeros((10, 10), dtype=numpy.bool) data[::2, ::2] = True widget = self.create_widget() widget.setData(data) availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViewer.RAW_MODE, widget.displayMode()) - self.assertIn(DataViewer.PLOT2D_MODE, availableModes) + self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) + self.assertIn(DataViews.IMAGE_MODE, availableModes) - def test_plot_2d_complex_data(self): + def test_image_complex_data(self): data = numpy.arange(3 ** 2, dtype=numpy.complex) data.shape = [3] * 2 widget = self.create_widget() widget.setData(data) availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViewer.RAW_MODE, widget.displayMode()) - self.assertIn(DataViewer.PLOT2D_MODE, availableModes) + self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) + self.assertIn(DataViews.IMAGE_MODE, availableModes) def test_plot_3d_data(self): data = numpy.arange(3 ** 3) @@ -135,38 +136,38 @@ class AbstractDataViewerTests(TestCaseQt): availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) try: import silx.gui.plot3d # noqa - self.assertIn(DataViewer.PLOT3D_MODE, availableModes) + self.assertIn(DataViews.PLOT3D_MODE, availableModes) except ImportError: - self.assertIn(DataViewer.STACK_MODE, availableModes) - self.assertEqual(DataViewer.RAW_MODE, widget.displayMode()) + self.assertIn(DataViews.STACK_MODE, availableModes) + self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) def test_array_1d_data(self): data = numpy.array(["aaa"] * (3 ** 1)) data.shape = [3] * 1 widget = self.create_widget() widget.setData(data) - self.assertEqual(DataViewer.RAW_MODE, widget.displayedView().modeId()) + self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) def test_array_2d_data(self): data = numpy.array(["aaa"] * (3 ** 2)) data.shape = [3] * 2 widget = self.create_widget() widget.setData(data) - self.assertEqual(DataViewer.RAW_MODE, widget.displayedView().modeId()) + self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) def test_array_4d_data(self): data = numpy.array(["aaa"] * (3 ** 4)) data.shape = [3] * 4 widget = self.create_widget() widget.setData(data) - self.assertEqual(DataViewer.RAW_MODE, widget.displayedView().modeId()) + self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) def test_record_4d_data(self): data = numpy.zeros(3 ** 4, dtype='3int8, float32, (2,3)float64') data.shape = [3] * 4 widget = self.create_widget() widget.setData(data) - self.assertEqual(DataViewer.RAW_MODE, widget.displayedView().modeId()) + self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) def test_3d_h5_dataset(self): if h5py is None: @@ -191,7 +192,7 @@ class AbstractDataViewerTests(TestCaseQt): widget.setData(10) widget.setData(None) modes = [v.modeId() for v in listener.arguments(argumentIndex=0)] - self.assertEquals(modes, [DataViewer.RAW_MODE, DataViewer.EMPTY_MODE]) + self.assertEquals(modes, [DataViews.RAW_MODE, DataViews.EMPTY_MODE]) listener.clear() def test_change_display_mode(self): @@ -199,14 +200,15 @@ class AbstractDataViewerTests(TestCaseQt): data.shape = [10] * 4 widget = self.create_widget() widget.setData(data) - widget.setDisplayMode(DataViewer.PLOT1D_MODE) - self.assertEquals(widget.displayedView().modeId(), DataViewer.PLOT1D_MODE) - widget.setDisplayMode(DataViewer.PLOT2D_MODE) - self.assertEquals(widget.displayedView().modeId(), DataViewer.PLOT2D_MODE) - widget.setDisplayMode(DataViewer.RAW_MODE) - self.assertEquals(widget.displayedView().modeId(), DataViewer.RAW_MODE) - widget.setDisplayMode(DataViewer.EMPTY_MODE) - self.assertEquals(widget.displayedView().modeId(), DataViewer.EMPTY_MODE) + widget.setDisplayMode(DataViews.PLOT1D_MODE) + self.assertEquals(widget.displayedView().modeId(), DataViews.PLOT1D_MODE) + widget.setDisplayMode(DataViews.IMAGE_MODE) + self.assertEquals(widget.displayedView().modeId(), DataViews.IMAGE_MODE) + widget.setDisplayMode(DataViews.RAW_MODE) + self.assertEquals(widget.displayedView().modeId(), DataViews.RAW_MODE) + widget.setDisplayMode(DataViews.EMPTY_MODE) + self.assertEquals(widget.displayedView().modeId(), DataViews.EMPTY_MODE) + DataView._cleanUpCache() def test_create_default_views(self): widget = self.create_widget() @@ -228,6 +230,26 @@ class AbstractDataViewerTests(TestCaseQt): self.assertTrue(view not in widget.availableViews()) self.assertTrue(view not in widget.currentAvailableViews()) + def test_replace_view(self): + widget = self.create_widget() + view = _DataViewMock(widget) + widget.replaceView(DataViews.RAW_MODE, + view) + self.assertIsNone(widget.getViewFromModeId(DataViews.RAW_MODE)) + self.assertTrue(view in widget.availableViews()) + self.assertTrue(view in widget.currentAvailableViews()) + + def test_replace_view_in_composite(self): + # replace a view that is a child of a composite view + widget = self.create_widget() + view = _DataViewMock(widget) + widget.replaceView(DataViews.NXDATA_INVALID_MODE, + view) + nxdata_view = widget.getViewFromModeId(DataViews.NXDATA_MODE) + self.assertNotIn(DataViews.NXDATA_INVALID_MODE, + [v.modeId() for v in nxdata_view.availableViews()]) + self.assertTrue(view in nxdata_view.availableViews()) + class TestDataViewer(AbstractDataViewerTests): def create_widget(self): @@ -265,6 +287,7 @@ class TestDataView(TestCaseQt): dataViewClass = DataViews._Plot2dView widget = self.createDataViewWithData(dataViewClass, data[0]) self.qWaitForWindowExposed(widget) + DataView._cleanUpCache() def testCubeWithComplex(self): self.skipTest("OpenGL widget not yet tested") @@ -276,12 +299,14 @@ class TestDataView(TestCaseQt): dataViewClass = DataViews._Plot3dView widget = self.createDataViewWithData(dataViewClass, data) self.qWaitForWindowExposed(widget) + DataView._cleanUpCache() def testImageStackWithComplex(self): data = self.createComplexData() dataViewClass = DataViews._StackView widget = self.createDataViewWithData(dataViewClass, data) self.qWaitForWindowExposed(widget) + DataView._cleanUpCache() def suite(): diff --git a/silx/gui/data/test/test_numpyaxesselector.py b/silx/gui/data/test/test_numpyaxesselector.py index cc15f83..6ce5119 100644 --- a/silx/gui/data/test/test_numpyaxesselector.py +++ b/silx/gui/data/test/test_numpyaxesselector.py @@ -24,7 +24,7 @@ # ###########################################################################*/ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "15/12/2016" +__date__ = "29/01/2018" import os import tempfile @@ -70,6 +70,20 @@ class TestNumpyAxesSelector(TestCaseQt): 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_output_lessdim(self): data = numpy.arange(3 * 3 * 3) data.shape = 3, 3, 3 diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py index 2a7a66b..06a29ba 100644 --- a/silx/gui/data/test/test_textformatter.py +++ b/silx/gui/data/test/test_textformatter.py @@ -24,7 +24,7 @@ # ###########################################################################*/ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "27/09/2017" +__date__ = "12/12/2017" import unittest import shutil @@ -91,6 +91,17 @@ class TestTextFormatter(TestCaseQt): result = formatter.toString("toto") self.assertEquals(result, '"toto"') + def test_numpy_void(self): + formatter = TextFormatter() + result = formatter.toString(numpy.void(b"\xFF")) + self.assertEquals(result, 'b"\\xFF"') + + def test_char_cp1252(self): + # degree character in cp1252 + formatter = TextFormatter() + result = formatter.toString(numpy.bytes_(b"\xB0")) + self.assertEquals(result, u'"\u00B0"') + class TestTextFormatterWithH5py(TestCaseQt): |