diff options
Diffstat (limited to 'silx/gui/data/DataViews.py')
-rw-r--r-- | silx/gui/data/DataViews.py | 365 |
1 files changed, 288 insertions, 77 deletions
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)) |