summaryrefslogtreecommitdiff
path: root/silx/gui/data
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/data')
-rw-r--r--silx/gui/data/DataViewer.py153
-rw-r--r--silx/gui/data/DataViewerFrame.py17
-rw-r--r--silx/gui/data/DataViewerSelector.py40
-rw-r--r--silx/gui/data/DataViews.py365
-rw-r--r--silx/gui/data/Hdf5TableView.py35
-rw-r--r--silx/gui/data/NXdataWidgets.py390
-rw-r--r--silx/gui/data/NumpyAxesSelector.py24
-rw-r--r--silx/gui/data/TextFormatter.py47
-rw-r--r--silx/gui/data/test/test_dataviewer.py87
-rw-r--r--silx/gui/data/test/test_numpyaxesselector.py16
-rw-r--r--silx/gui/data/test/test_textformatter.py13
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):