summaryrefslogtreecommitdiff
path: root/silx/gui/data
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2019-05-28 08:16:16 +0200
committerPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2019-05-28 08:16:16 +0200
commita763e5d1b3921b3194f3d4e94ab9de3fbe08bbdd (patch)
tree45d462ed36a5522e9f3b9fde6c4ec4918c2ae8e3 /silx/gui/data
parentcebdc9244c019224846cb8d2668080fe386a6adc (diff)
New upstream version 0.10.1+dfsg
Diffstat (limited to 'silx/gui/data')
-rw-r--r--silx/gui/data/DataViewer.py92
-rw-r--r--silx/gui/data/DataViewerFrame.py5
-rw-r--r--silx/gui/data/DataViewerSelector.py6
-rw-r--r--silx/gui/data/DataViews.py499
-rw-r--r--silx/gui/data/Hdf5TableView.py13
-rw-r--r--silx/gui/data/HexaTableView.py8
-rw-r--r--silx/gui/data/NXdataWidgets.py439
-rw-r--r--silx/gui/data/TextFormatter.py50
-rw-r--r--silx/gui/data/test/test_arraywidget.py6
-rw-r--r--silx/gui/data/test/test_dataviewer.py14
-rw-r--r--silx/gui/data/test/test_numpyaxesselector.py7
-rw-r--r--silx/gui/data/test/test_textformatter.py12
12 files changed, 959 insertions, 192 deletions
diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py
index 4db2863..b33a931 100644
--- a/silx/gui/data/DataViewer.py
+++ b/silx/gui/data/DataViewer.py
@@ -32,12 +32,10 @@ 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__ = "24/04/2018"
+__date__ = "12/02/2019"
_logger = logging.getLogger(__name__)
@@ -70,66 +68,6 @@ class DataViewer(qt.QFrame):
viewer.setVisible(True)
"""
- # 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"""
@@ -288,6 +226,7 @@ class DataViewer(qt.QFrame):
else:
self.__displayedData = self.__data
+ # TODO: would be good to avoid that, it should be synchonous
qt.QTimer.singleShot(10, self.__setDataInView)
def __setDataInView(self):
@@ -405,18 +344,16 @@ class DataViewer(qt.QFrame):
data = self.__data
info = self._getInfo()
# sort available views according to priority
- priorities = [v.getDataPriority(data, info) for v in self.__views]
- views = zip(priorities, self.__views)
+ views = []
+ for v in self.__views:
+ views.extend(v.getMatchingViews(data, info))
+ views = [(v.getCachedDataPriority(data, info), v) for v in views]
views = filter(lambda t: t[0] > DataViews.DataView.UNSUPPORTED, views)
views = sorted(views, reverse=True)
+ views = [v[1] for v in views]
# store available views
- if len(views) == 0:
- self.__setCurrentAvailableViews([])
- available = []
- else:
- available = [v[1] for v in views]
- self.__setCurrentAvailableViews(available)
+ self.__setCurrentAvailableViews(views)
def __updateView(self):
"""Display the data using the widget which fit the best"""
@@ -447,7 +384,7 @@ class DataViewer(qt.QFrame):
priority to lowest.
:rtype: DataView
"""
- hdf5View = self.getViewFromModeId(DataViewer.HDF5_MODE)
+ hdf5View = self.getViewFromModeId(DataViews.HDF5_MODE)
if hdf5View in available:
return hdf5View
return self.getViewFromModeId(DataViews.EMPTY_MODE)
@@ -487,6 +424,17 @@ class DataViewer(qt.QFrame):
"""
return self.__currentAvailableViews
+ def getReachableViews(self):
+ """Returns the list of reachable views from the registred available
+ views.
+
+ :rtype: List[DataView]
+ """
+ views = []
+ for v in self.availableViews():
+ views.extend(v.getReachableViews())
+ return views
+
def availableViews(self):
"""Returns the list of registered views
diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py
index 4e6d2e8..9bfb95b 100644
--- a/silx/gui/data/DataViewerFrame.py
+++ b/silx/gui/data/DataViewerFrame.py
@@ -27,7 +27,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "24/04/2018"
+__date__ = "12/02/2019"
from silx.gui import qt
from .DataViewer import DataViewer
@@ -120,6 +120,9 @@ class DataViewerFrame(qt.QWidget):
"""
self.__dataViewer.setGlobalHooks(hooks)
+ def getReachableViews(self):
+ return self.__dataViewer.getReachableViews()
+
def availableViews(self):
"""Returns the list of registered views
diff --git a/silx/gui/data/DataViewerSelector.py b/silx/gui/data/DataViewerSelector.py
index 35bbe99..a1e9947 100644
--- a/silx/gui/data/DataViewerSelector.py
+++ b/silx/gui/data/DataViewerSelector.py
@@ -29,7 +29,7 @@ from __future__ import division
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "23/01/2018"
+__date__ = "12/02/2019"
import weakref
import functools
@@ -85,7 +85,7 @@ class DataViewerSelector(qt.QWidget):
iconSize = qt.QSize(16, 16)
- for view in self.__dataViewer.availableViews():
+ for view in self.__dataViewer.getReachableViews():
label = view.label()
icon = view.icon()
button = qt.QPushButton(label)
@@ -155,7 +155,7 @@ class DataViewerSelector(qt.QWidget):
self.__dataViewer.setDisplayedView(view)
def __checkAvailableButtons(self):
- views = set(self.__dataViewer.availableViews())
+ views = set(self.__dataViewer.getReachableViews())
if views == set(self.__buttons.keys()):
return
# Recreate all the buttons
diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py
index 2291e87..6575d0d 100644
--- a/silx/gui/data/DataViews.py
+++ b/silx/gui/data/DataViews.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,6 +31,7 @@ import numbers
import numpy
import silx.io
+from silx.utils import deprecation
from silx.gui import qt, icons
from silx.gui.data.TextFormatter import TextFormatter
from silx.io import nxdata
@@ -41,7 +42,7 @@ from silx.gui.dialog.ColormapDialog import ColormapDialog
__authors__ = ["V. Valls", "P. Knobel"]
__license__ = "MIT"
-__date__ = "23/05/2018"
+__date__ = "19/02/2019"
_logger = logging.getLogger(__name__)
@@ -67,6 +68,8 @@ NXDATA_CURVE_MODE = 73
NXDATA_XYVSCATTER_MODE = 74
NXDATA_IMAGE_MODE = 75
NXDATA_STACK_MODE = 76
+NXDATA_VOLUME_MODE = 77
+NXDATA_VOLUME_AS_STACK_MODE = 78
def _normalizeData(data):
@@ -100,6 +103,7 @@ class DataInfo(object):
"""Store extracted information from a data"""
def __init__(self, data):
+ self.__priorities = {}
data = self.normalizeData(data)
self.isArray = False
self.interpretation = None
@@ -131,9 +135,6 @@ class DataInfo(object):
elif nx_class == "NXdata":
# group claiming to be NXdata could not be parsed
self.isInvalidNXdata = True
- elif nx_class == "NXentry" and "default" in data.attrs:
- # entry claiming to have a default NXdata could not be parsed
- self.isInvalidNXdata = True
elif nx_class == "NXroot" or silx.io.is_file(data):
# root claiming to have a default entry
if "default" in data.attrs:
@@ -141,6 +142,9 @@ class DataInfo(object):
if def_entry in data and "default" in data[def_entry].attrs:
# and entry claims to have default NXdata
self.isInvalidNXdata = True
+ elif "default" in data.attrs:
+ # group claiming to have a default NXdata could not be parsed
+ self.isInvalidNXdata = True
if isinstance(data, numpy.ndarray):
self.isArray = True
@@ -201,6 +205,12 @@ class DataInfo(object):
Else returns the data."""
return _normalizeData(data)
+ def cachePriority(self, view, priority):
+ self.__priorities[view] = priority
+
+ def getPriority(self, view):
+ return self.__priorities[view]
+
class DataViewHooks(object):
"""A set of hooks defined to custom the behaviour of the data views."""
@@ -357,6 +367,35 @@ class DataView(object):
"""
return []
+ def getReachableViews(self):
+ """Returns the views that can be returned by `getMatchingViews`.
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: List[DataView]
+ """
+ return [self]
+
+ def getMatchingViews(self, data, info):
+ """Returns the views according to data and info from the data.
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: List[DataView]
+ """
+ priority = self.getCachedDataPriority(data, info)
+ if priority == DataView.UNSUPPORTED:
+ return []
+ return [self]
+
+ def getCachedDataPriority(self, data, info):
+ try:
+ priority = info.getPriority(self)
+ except KeyError:
+ priority = self.getDataPriority(data, info)
+ info.cachePriority(self, priority)
+ return priority
+
def getDataPriority(self, data, info):
"""
Returns the priority of using this view according to a data.
@@ -377,7 +416,53 @@ class DataView(object):
return str(self) < str(other)
-class CompositeDataView(DataView):
+class _CompositeDataView(DataView):
+ """Contains sub views"""
+
+ def getViews(self):
+ """Returns the direct sub views registered in this view.
+
+ :rtype: List[DataView]
+ """
+ raise NotImplementedError()
+
+ def getReachableViews(self):
+ """Returns all views that can be reachable at on point.
+
+ This method return any sub view provided (recursivly).
+
+ :rtype: List[DataView]
+ """
+ raise NotImplementedError()
+
+ def getMatchingViews(self, data, info):
+ """Returns sub views matching this data and info.
+
+ This method return any sub view provided (recursivly).
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: List[DataView]
+ """
+ raise NotImplementedError()
+
+ @deprecation.deprecated(replacement="getReachableViews", since_version="0.10")
+ def availableViews(self):
+ return self.getViews()
+
+ def isSupportedData(self, data, info):
+ """If true, the composite view allow sub views to access to this data.
+ Else this this data is considered as not supported by any of sub views
+ (incliding this composite view).
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: bool
+ """
+ return True
+
+
+class SelectOneDataView(_CompositeDataView):
"""Data view which can display a data using different view according to
the kind of the data."""
@@ -386,7 +471,7 @@ class CompositeDataView(DataView):
:param qt.QWidget parent: Parent of the hold widget
"""
- super(CompositeDataView, self).__init__(parent, modeId, icon, label)
+ super(SelectOneDataView, self).__init__(parent, modeId, icon, label)
self.__views = OrderedDict()
self.__currentView = None
@@ -395,7 +480,7 @@ class CompositeDataView(DataView):
:param DataViewHooks hooks: The data view hooks to use
"""
- super(CompositeDataView, self).setHooks(hooks)
+ super(SelectOneDataView, self).setHooks(hooks)
if hooks is not None:
for v in self.__views:
v.setHooks(hooks)
@@ -407,16 +492,40 @@ class CompositeDataView(DataView):
dataView.setHooks(hooks)
self.__views[dataView] = None
- def availableViews(self):
+ def getReachableViews(self):
+ views = []
+ addSelf = False
+ for v in self.__views:
+ if isinstance(v, SelectManyDataView):
+ views.extend(v.getReachableViews())
+ else:
+ addSelf = True
+ if addSelf:
+ # Single views are hidden by this view
+ views.insert(0, self)
+ return views
+
+ def getMatchingViews(self, data, info):
+ if not self.isSupportedData(data, info):
+ return []
+ view = self.__getBestView(data, info)
+ if isinstance(view, SelectManyDataView):
+ return view.getMatchingViews(data, info)
+ else:
+ return [self]
+
+ def getViews(self):
"""Returns the list of registered views
:rtype: List[DataView]
"""
return list(self.__views.keys())
- def getBestView(self, data, info):
+ def __getBestView(self, data, info):
"""Returns the best view according to priorities."""
- views = [(v.getDataPriority(data, info), v) for v in self.__views.keys()]
+ if not self.isSupportedData(data, info):
+ return None
+ views = [(v.getCachedDataPriority(data, info), v) for v in self.__views.keys()]
views = filter(lambda t: t[0] > DataView.UNSUPPORTED, views)
views = sorted(views, key=lambda t: t[0], reverse=True)
@@ -471,17 +580,17 @@ class CompositeDataView(DataView):
self.__currentView.setData(data)
def axesNames(self, data, info):
- view = self.getBestView(data, info)
+ view = self.__getBestView(data, info)
self.__currentView = view
return view.axesNames(data, info)
def getDataPriority(self, data, info):
- view = self.getBestView(data, info)
+ view = self.__getBestView(data, info)
self.__currentView = view
if view is None:
return DataView.UNSUPPORTED
else:
- return view.getDataPriority(data, info)
+ return view.getCachedDataPriority(data, info)
def replaceView(self, modeId, newView):
"""Replace a data view with a custom view.
@@ -502,7 +611,7 @@ class CompositeDataView(DataView):
if view.modeId() == modeId:
oldView = view
break
- elif isinstance(view, CompositeDataView):
+ elif isinstance(view, _CompositeDataView):
# recurse
hooks = self.getHooks()
if hooks is not None:
@@ -519,6 +628,135 @@ class CompositeDataView(DataView):
return True
+# NOTE: SelectOneDataView was introduced with silx 0.10
+CompositeDataView = SelectOneDataView
+
+
+class SelectManyDataView(_CompositeDataView):
+ """Data view which can select a set of sub views according to
+ the kind of the data.
+
+ This view itself is abstract and is not exposed.
+ """
+
+ def __init__(self, parent, views=None):
+ """Constructor
+
+ :param qt.QWidget parent: Parent of the hold widget
+ """
+ super(SelectManyDataView, self).__init__(parent, modeId=None, icon=None, label=None)
+ if views is None:
+ views = []
+ self.__views = views
+
+ def setHooks(self, hooks):
+ """Set the data context to use with this view.
+
+ :param DataViewHooks hooks: The data view hooks to use
+ """
+ super(SelectManyDataView, self).setHooks(hooks)
+ if hooks is not None:
+ for v in self.__views:
+ v.setHooks(hooks)
+
+ def addView(self, dataView):
+ """Add a new dataview to the available list."""
+ hooks = self.getHooks()
+ if hooks is not None:
+ dataView.setHooks(hooks)
+ self.__views.append(dataView)
+
+ def getViews(self):
+ """Returns the list of registered views
+
+ :rtype: List[DataView]
+ """
+ return list(self.__views)
+
+ def getReachableViews(self):
+ views = []
+ for v in self.__views:
+ views.extend(v.getReachableViews())
+ return views
+
+ def getMatchingViews(self, data, info):
+ """Returns the views according to data and info from the data.
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ """
+ if not self.isSupportedData(data, info):
+ return []
+ views = [v for v in self.__views if v.getCachedDataPriority(data, info) != DataView.UNSUPPORTED]
+ return views
+
+ def customAxisNames(self):
+ raise RuntimeError("Abstract view")
+
+ def setCustomAxisValue(self, name, value):
+ raise RuntimeError("Abstract view")
+
+ def select(self):
+ raise RuntimeError("Abstract view")
+
+ def createWidget(self, parent):
+ raise RuntimeError("Abstract view")
+
+ def clear(self):
+ for v in self.__views:
+ v.clear()
+
+ def setData(self, data):
+ raise RuntimeError("Abstract view")
+
+ def axesNames(self, data, info):
+ raise RuntimeError("Abstract view")
+
+ def getDataPriority(self, data, info):
+ if not self.isSupportedData(data, info):
+ return DataView.UNSUPPORTED
+ priorities = [v.getCachedDataPriority(data, info) for v in self.__views]
+ priorities = [v for v in priorities if v != DataView.UNSUPPORTED]
+ priorities = sorted(priorities)
+ if len(priorities) == 0:
+ return DataView.UNSUPPORTED
+ return priorities[-1]
+
+ def replaceView(self, modeId, newView):
+ """Replace a data view with a custom view.
+ Return True in case of success, False in case of failure.
+
+ .. note::
+
+ This method must be called just after instantiation, before
+ the viewer is used.
+
+ :param int modeId: Unique mode ID identifying the DataView to
+ be replaced.
+ :param DataViews.DataView newView: New data view
+ :return: True if replacement was successful, else False
+ """
+ oldView = None
+ for iview, view in enumerate(self.__views):
+ if view.modeId() == modeId:
+ oldView = view
+ break
+ elif isinstance(view, CompositeDataView):
+ # recurse
+ hooks = self.getHooks()
+ if hooks is not None:
+ newView.setHooks(hooks)
+ if view.replaceView(modeId, newView):
+ return True
+
+ if oldView is None:
+ return False
+
+ # replace oldView with new view in dict
+ self.__views[iview] = newView
+ return True
+
+
class _EmptyView(DataView):
"""Dummy view to display nothing"""
@@ -1096,17 +1334,6 @@ class _InvalidNXdataView(DataView):
# invalid: could not even be parsed by NXdata
self._msg = "Group has @NX_class = NXdata, but could not be interpreted"
self._msg += " as valid NXdata."
- elif nx_class == "NXentry":
- 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_unicode(data[default_nxdata_name], "NX_class") != "NXdata":
- self._msg += " but the corresponding item is not a "
- self._msg += "NXdata group."
- else:
- self._msg += " but the corresponding NXdata seems to be"
- self._msg += " malformed."
elif nx_class == "NXroot" or silx.io.is_file(data):
default_entry = data[data.attrs["default"]]
default_nxdata_name = default_entry.attrs["default"]
@@ -1122,6 +1349,17 @@ class _InvalidNXdataView(DataView):
else:
self._msg += " but the corresponding NXdata seems to be"
self._msg += " malformed."
+ else:
+ self._msg = "Group provides a @default attribute,"
+ default_nxdata_name = data.attrs["default"]
+ if default_nxdata_name not in data:
+ self._msg += " but no corresponding NXdata group exists."
+ elif get_attr_as_unicode(data[default_nxdata_name], "NX_class") != "NXdata":
+ self._msg += " but the corresponding item is not a "
+ self._msg += "NXdata group."
+ else:
+ self._msg += " but the corresponding NXdata seems to be"
+ self._msg += " malformed."
return 100
@@ -1277,7 +1515,7 @@ class _NXdataXYVScatterView(DataView):
class _NXdataImageView(DataView):
"""DataView using a Plot2D for displaying NXdata images:
- 2-D signal or n-D signals with *@interpretation=spectrum*."""
+ 2-D signal or n-D signals with *@interpretation=image*."""
def __init__(self, parent):
DataView.__init__(self, parent,
modeId=NXDATA_IMAGE_MODE)
@@ -1323,6 +1561,53 @@ class _NXdataImageView(DataView):
return DataView.UNSUPPORTED
+class _NXdataComplexImageView(DataView):
+ """DataView using a ComplexImageView for displaying NXdata complex images:
+ 2-D signal or n-D signals with *@interpretation=image*."""
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ modeId=NXDATA_IMAGE_MODE)
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot
+ widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap())
+ widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ return widget
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+
+ # last two axes are Y & X
+ img_slicing = slice(-2, None)
+ y_axis, x_axis = nxd.axes[img_slicing]
+ y_label, x_label = nxd.axes_names[img_slicing]
+
+ self.getWidget().setImageData(
+ [nxd.signal] + nxd.auxiliary_signals,
+ x_axis=x_axis, y_axis=y_axis,
+ signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names,
+ xlabel=x_label, ylabel=y_label,
+ title=nxd.title)
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+
+ if info.hasNXdata and not info.isInvalidNXdata:
+ nxd = nxdata.get_default(data, validate=False)
+ if nxd.is_image and numpy.iscomplexobj(nxd.signal):
+ return 100
+
+ return DataView.UNSUPPORTED
+
+
class _NXdataStackView(DataView):
def __init__(self, parent):
DataView.__init__(self, parent,
@@ -1368,6 +1653,154 @@ class _NXdataStackView(DataView):
return DataView.UNSUPPORTED
+class _NXdataVolumeView(DataView):
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ label="NXdata (3D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_MODE)
+ try:
+ import silx.gui.plot3d # noqa
+ except ImportError:
+ _logger.warning("Plot3dView is not available")
+ _logger.debug("Backtrace", exc_info=True)
+ raise
+
+ def normalizeData(self, data):
+ data = DataView.normalizeData(self, data)
+ data = _normalizeComplex(data)
+ return data
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayVolumePlot
+ widget = ArrayVolumePlot(parent)
+ return widget
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+ signal_name = nxd.signal_name
+ z_axis, y_axis, x_axis = nxd.axes[-3:]
+ z_label, y_label, x_label = nxd.axes_names[-3:]
+ title = nxd.title or signal_name
+
+ widget = self.getWidget()
+ widget.setData(
+ nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
+ signal_name=signal_name,
+ xlabel=x_label, ylabel=y_label, zlabel=z_label,
+ title=title)
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+ if info.hasNXdata and not info.isInvalidNXdata:
+ if nxdata.get_default(data, validate=False).is_volume:
+ return 150
+
+ return DataView.UNSUPPORTED
+
+
+class _NXdataVolumeAsStackView(DataView):
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ label="NXdata (2D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_AS_STACK_MODE)
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayStackPlot
+ widget = ArrayStackPlot(parent)
+ widget.getStackView().setColormap(self.defaultColormap())
+ widget.getStackView().getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ return widget
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+ signal_name = nxd.signal_name
+ z_axis, y_axis, x_axis = nxd.axes[-3:]
+ z_label, y_label, x_label = nxd.axes_names[-3:]
+ title = nxd.title or signal_name
+
+ widget = self.getWidget()
+ widget.setStackData(
+ nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
+ signal_name=signal_name,
+ xlabel=x_label, ylabel=y_label, zlabel=z_label,
+ title=title)
+ # Override the colormap, while setStack overwrite it
+ widget.getStackView().setColormap(self.defaultColormap())
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+ if info.isComplex:
+ return DataView.UNSUPPORTED
+ if info.hasNXdata and not info.isInvalidNXdata:
+ if nxdata.get_default(data, validate=False).is_volume:
+ return 200
+
+ return DataView.UNSUPPORTED
+
+class _NXdataComplexVolumeAsStackView(DataView):
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ label="NXdata (2D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_AS_STACK_MODE)
+ self._is_complex_data = False
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot
+ widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap())
+ widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ return widget
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+ signal_name = nxd.signal_name
+ z_axis, y_axis, x_axis = nxd.axes[-3:]
+ z_label, y_label, x_label = nxd.axes_names[-3:]
+ title = nxd.title or signal_name
+
+ self.getWidget().setImageData(
+ [nxd.signal] + nxd.auxiliary_signals,
+ x_axis=x_axis, y_axis=y_axis,
+ signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names,
+ xlabel=x_label, ylabel=y_label, title=nxd.title)
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+ if not info.isComplex:
+ return DataView.UNSUPPORTED
+ if info.hasNXdata and not info.isInvalidNXdata:
+ if nxdata.get_default(data, validate=False).is_volume:
+ return 200
+
+ return DataView.UNSUPPORTED
+
+
class _NXdataView(CompositeDataView):
"""Composite view displaying NXdata groups using the most adequate
widget depending on the dimensionality."""
@@ -1382,5 +1815,17 @@ class _NXdataView(CompositeDataView):
self.addView(_NXdataScalarView(parent))
self.addView(_NXdataCurveView(parent))
self.addView(_NXdataXYVScatterView(parent))
+ self.addView(_NXdataComplexImageView(parent))
self.addView(_NXdataImageView(parent))
self.addView(_NXdataStackView(parent))
+
+ # The 3D view can be displayed using 2 ways
+ nx3dViews = SelectManyDataView(parent)
+ nx3dViews.addView(_NXdataVolumeAsStackView(parent))
+ nx3dViews.addView(_NXdataComplexVolumeAsStackView(parent))
+ try:
+ nx3dViews.addView(_NXdataVolumeView(parent))
+ except Exception:
+ _logger.warning("NXdataVolumeView is not available")
+ _logger.debug("Backtrace", exc_info=True)
+ self.addView(nx3dViews)
diff --git a/silx/gui/data/Hdf5TableView.py b/silx/gui/data/Hdf5TableView.py
index 9e28fbf..d7c33f3 100644
--- a/silx/gui/data/Hdf5TableView.py
+++ b/silx/gui/data/Hdf5TableView.py
@@ -30,12 +30,14 @@ from __future__ import division
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "05/07/2018"
+__date__ = "12/02/2019"
import collections
import functools
import os.path
import logging
+import h5py
+
from silx.gui import qt
import silx.io
from .TextFormatter import TextFormatter
@@ -44,11 +46,6 @@ from silx.gui.widgets import HierarchicalTableView
from ..hdf5.Hdf5Formatter import Hdf5Formatter
from ..hdf5._utils import htmlFromDict
-try:
- import h5py
-except ImportError:
- h5py = None
-
_logger = logging.getLogger(__name__)
@@ -198,11 +195,9 @@ class _CellFilterAvailableData(_CellData):
}
def __init__(self, filterId):
- import h5py.version
if h5py.version.hdf5_version_tuple >= (1, 10, 2):
# Previous versions only returns True if the filter was first used
# to decode a dataset
- import h5py.h5z
self.__availability = h5py.h5z.filter_avail(filterId)
else:
self.__availability = "na"
@@ -416,7 +411,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
self.__data.addHeaderRow(headerLabel="Data info")
- if h5py is not None and hasattr(obj, "id") and hasattr(obj.id, "get_type"):
+ if 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)
diff --git a/silx/gui/data/HexaTableView.py b/silx/gui/data/HexaTableView.py
index c86c0af..1617f0a 100644
--- a/silx/gui/data/HexaTableView.py
+++ b/silx/gui/data/HexaTableView.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
@@ -28,11 +28,13 @@ hexadecimal viewer.
"""
from __future__ import division
-import numpy
import collections
+
+import numpy
+import six
+
from silx.gui import qt
import silx.io.utils
-from silx.third_party import six
from silx.gui.widgets.TableWidget import CopySelectedCellsAction
__authors__ = ["V. Valls"]
diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py
index f7c479d..e5a2550 100644
--- a/silx/gui/data/NXdataWidgets.py
+++ b/silx/gui/data/NXdataWidgets.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,19 +26,25 @@
"""
__authors__ = ["P. Knobel"]
__license__ = "MIT"
-__date__ = "10/10/2018"
+__date__ = "12/11/2018"
+import logging
+import numbers
import numpy
from silx.gui import qt
from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
from silx.gui.plot import Plot1D, Plot2D, StackView, ScatterView
+from silx.gui.plot.ComplexImageView import ComplexImageView
from silx.gui.colors import Colormap
from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
from silx.math.calibration import ArrayCalibration, NoCalibration, LinearCalibration
+_logger = logging.getLogger(__name__)
+
+
class ArrayCurvePlot(qt.QWidget):
"""
Widget for plotting a curve from a multi-dimensional signal array
@@ -72,21 +78,16 @@ class ArrayCurvePlot(qt.QWidget):
self._plot = Plot1D(self)
- self.selectorDock = qt.QDockWidget("Data selector", self._plot)
- # not closable
- self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable |
- qt.QDockWidget.DockWidgetFloatable)
- self._selector = NumpyAxesSelector(self.selectorDock)
+ self._selector = NumpyAxesSelector(self)
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 = qt.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self._plot, 0, 0)
+ layout.addWidget(self._plot)
+ layout.addWidget(self._selector)
self.setLayout(layout)
@@ -130,9 +131,9 @@ class ArrayCurvePlot(qt.QWidget):
self._selector.setAxisNames(["Y"])
if len(ys[0].shape) < 2:
- self.selectorDock.hide()
+ self._selector.hide()
else:
- self.selectorDock.show()
+ self._selector.show()
self._plot.setGraphTitle(title or "")
self._updateCurve()
@@ -182,6 +183,9 @@ class ArrayCurvePlot(qt.QWidget):
break
def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
self._plot.clear()
@@ -339,11 +343,8 @@ class ArrayImagePlot(qt.QWidget):
normalization=Colormap.LINEAR))
self._plot.getIntensityHistogramAction().setVisible(True)
- self.selectorDock = qt.QDockWidget("Data selector", self._plot)
# not closable
- self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable |
- qt.QDockWidget.DockWidgetFloatable)
- self._selector = NumpyAxesSelector(self.selectorDock)
+ self._selector = NumpyAxesSelector(self)
self._selector.setNamedAxesSelectorVisibility(False)
self._selector.selectionChanged.connect(self._updateImage)
@@ -355,9 +356,8 @@ class ArrayImagePlot(qt.QWidget):
layout = qt.QVBoxLayout()
layout.addWidget(self._plot)
+ layout.addWidget(self._selector)
layout.addWidget(self._auxSigSlider)
- self.selectorDock.setWidget(self._selector)
- self._plot.addTabbedDockWidget(self.selectorDock)
self.setLayout(layout)
@@ -413,9 +413,9 @@ class ArrayImagePlot(qt.QWidget):
self._selector.setData(signals[0])
if len(signals[0].shape) <= img_ndim:
- self.selectorDock.hide()
+ self._selector.hide()
else:
- self.selectorDock.show()
+ self._selector.show()
self._auxSigSlider.setMaximum(len(signals) - 1)
if len(signals) > 1:
@@ -425,6 +425,7 @@ class ArrayImagePlot(qt.QWidget):
self._auxSigSlider.setValue(0)
self._updateImage()
+ self._plot.resetZoom()
self._selector.selectionChanged.connect(self._updateImage)
self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged)
@@ -492,12 +493,202 @@ class ArrayImagePlot(qt.QWidget):
self._plot.setGraphTitle(title)
self._plot.getXAxis().setLabel(self.__x_axis_name)
self._plot.getYAxis().setLabel(self.__y_axis_name)
- self._plot.resetZoom()
def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
self._plot.clear()
+class ArrayComplexImagePlot(qt.QWidget):
+ """
+ Widget for plotting an image of complex from a multi-dimensional signal array
+ and two 1D axes array.
+
+ The signal array can have an arbitrary number of dimensions, the only
+ limitation being that the last two dimensions must have the same length as
+ the axes arrays.
+
+ Sliders are provided to select indices on the first (n - 2) dimensions of
+ the signal array, and the plot is updated to show the image corresponding
+ to the selection.
+
+ If one or both of the axes does not have regularly spaced values, the
+ the image is plotted as a coloured scatter plot.
+ """
+ def __init__(self, parent=None, colormap=None):
+ """
+
+ :param parent: Parent QWidget
+ """
+ super(ArrayComplexImagePlot, self).__init__(parent)
+
+ 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 = ComplexImageView(self)
+ if colormap is not None:
+ for mode in (ComplexImageView.Mode.ABSOLUTE,
+ ComplexImageView.Mode.SQUARE_AMPLITUDE,
+ ComplexImageView.Mode.REAL,
+ ComplexImageView.Mode.IMAGINARY):
+ self._plot.setColormap(colormap, mode)
+
+ self._plot.getPlot().getIntensityHistogramAction().setVisible(True)
+ self._plot.setKeepDataAspectRatio(True)
+
+ # not closable
+ self._selector = NumpyAxesSelector(self)
+ self._selector.setNamedAxesSelectorVisibility(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._selector)
+ layout.addWidget(self._auxSigSlider)
+
+ self.setLayout(layout)
+
+ def _sliderIdxChanged(self, value):
+ self._updateImage()
+
+ def getPlot(self):
+ """Returns the plot used for the display
+
+ :rtype: PlotWidget
+ """
+ return self._plot.getPlot()
+
+ def setImageData(self, signals,
+ x_axis=None, y_axis=None,
+ signals_names=None,
+ xlabel=None, ylabel=None,
+ title=None):
+ """
+
+ :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 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
+ """
+ self._selector.selectionChanged.disconnect(self._updateImage)
+ self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged)
+
+ 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.clear()
+ self._selector.setAxisNames(["Y", "X"])
+ self._selector.setData(signals[0])
+
+ if len(signals[0].shape) <= 2:
+ self._selector.hide()
+ else:
+ self._selector.show()
+
+ self._auxSigSlider.setMaximum(len(signals) - 1)
+ if len(signals) > 1:
+ self._auxSigSlider.show()
+ else:
+ self._auxSigSlider.hide()
+ self._auxSigSlider.setValue(0)
+
+ self._updateImage()
+ self._plot.getPlot().resetZoom()
+
+ self._selector.selectionChanged.connect(self._updateImage)
+ self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged)
+
+ def _updateImage(self):
+ selection = self._selector.selection()
+ auxSigIdx = self._auxSigSlider.value()
+
+ images = [img[selection] for img in self.__signals]
+ image = images[auxSigIdx]
+
+ x_axis = self.__x_axis
+ y_axis = self.__y_axis
+
+ if x_axis is None and y_axis is None:
+ xcalib = NoCalibration()
+ ycalib = NoCalibration()
+ else:
+ if x_axis is None:
+ # no calibration
+ 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((image.shape[1], ))
+ elif len(x_axis) == 2:
+ # linear calibration
+ x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1]
+
+ if y_axis is None:
+ y_axis = numpy.arange(image.shape[0])
+ elif numpy.isscalar(y_axis) or len(y_axis) == 1:
+ y_axis = y_axis * numpy.ones((image.shape[0], ))
+ elif len(y_axis) == 2:
+ y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1]
+
+ xcalib = ArrayCalibration(x_axis)
+ ycalib = ArrayCalibration(y_axis)
+
+ self._plot.setData(image)
+ if xcalib.is_affine():
+ xorigin, xscale = xcalib(0), xcalib.get_slope()
+ else:
+ _logger.warning("Unsupported complex image X axis calibration")
+ xorigin, xscale = 0., 1.
+
+ if ycalib.is_affine():
+ yorigin, yscale = ycalib(0), ycalib.get_slope()
+ else:
+ _logger.warning("Unsupported complex image Y axis calibration")
+ yorigin, yscale = 0., 1.
+
+ self._plot.setOrigin((xorigin, yorigin))
+ self._plot.setScale((xscale, yscale))
+
+ 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)
+
+ def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
+ self._plot.setData(None)
+
+
class ArrayStackPlot(qt.QWidget):
"""
Widget for plotting a n-D array (n >= 3) as a stack of images.
@@ -665,4 +856,208 @@ class ArrayStackPlot(qt.QWidget):
self.__x_axis_name])
def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
self._stack_view.clear()
+
+
+class ArrayVolumePlot(qt.QWidget):
+ """
+ Widget for plotting a n-D array (n >= 3) as a 3D scalar field.
+ Three axis arrays can be provided to calibrate the axes.
+
+ The signal array can have an arbitrary number of dimensions, the only
+ limitation being that the last 3 dimensions must have the same length as
+ the axes arrays.
+
+ Sliders are provided to select indices on the first (n - 3) dimensions of
+ the signal array, and the plot is updated to load the stack corresponding
+ to the selection.
+ """
+ def __init__(self, parent=None):
+ """
+
+ :param parent: Parent QWidget
+ """
+ super(ArrayVolumePlot, self).__init__(parent)
+
+ self.__signal = None
+ self.__signal_name = None
+ # the Z, Y, X axes apply to the last three dimensions of the signal
+ # (in that order)
+ self.__z_axis = None
+ self.__z_axis_name = None
+ self.__y_axis = None
+ self.__y_axis_name = None
+ self.__x_axis = None
+ self.__x_axis_name = None
+
+ from silx.gui.plot3d.ScalarFieldView import ScalarFieldView
+ from silx.gui.plot3d import SFViewParamTree
+
+ self._view = ScalarFieldView(self)
+
+ def computeIsolevel(data):
+ data = data[numpy.isfinite(data)]
+ if len(data) == 0:
+ return 0
+ else:
+ return numpy.mean(data) + numpy.std(data)
+
+ self._view.addIsosurface(computeIsolevel, '#FF0000FF')
+
+ # Create a parameter tree for the scalar field view
+ options = SFViewParamTree.TreeView(self._view)
+ options.setSfView(self._view)
+
+ # Add the parameter tree to the main window in a dock widget
+ dock = qt.QDockWidget()
+ dock.setWidget(options)
+ self._view.addDockWidget(qt.Qt.RightDockWidgetArea, dock)
+
+ self._hline = qt.QFrame(self)
+ self._hline.setFrameStyle(qt.QFrame.HLine)
+ self._hline.setFrameShadow(qt.QFrame.Sunken)
+ self._legend = qt.QLabel(self)
+ self._selector = NumpyAxesSelector(self)
+ self._selector.setNamedAxesSelectorVisibility(False)
+ self.__selector_is_connected = False
+
+ layout = qt.QVBoxLayout()
+ layout.addWidget(self._view)
+ layout.addWidget(self._hline)
+ layout.addWidget(self._legend)
+ layout.addWidget(self._selector)
+
+ self.setLayout(layout)
+
+ def getVolumeView(self):
+ """Returns the plot used for the display
+
+ :rtype: ScalarFieldView
+ """
+ return self._view
+
+ def normalizeComplexData(self, data):
+ """
+ Converts a complex data array to its amplitude, if necessary.
+ :param data: the data to normalize
+ :return:
+ """
+ if hasattr(data, "dtype"):
+ isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating)
+ else:
+ isComplex = isinstance(data, numbers.Complex)
+ if isComplex:
+ data = numpy.absolute(data)
+ return data
+
+ def setData(self, signal,
+ x_axis=None, y_axis=None, z_axis=None,
+ signal_name=None,
+ xlabel=None, ylabel=None, zlabel=None,
+ title=None):
+ """
+
+ :param signal: n-D dataset, whose last 3 dimensions are used as the
+ 3D stack values.
+ :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 z_axis: 1-D dataset used as the image's z. If provided,
+ its lengths must be equal to the length of the 3rd to last
+ dimension of ``signal``.
+ :param signal_name: Label used in the legend
+ :param xlabel: Label for X axis
+ :param ylabel: Label for Y axis
+ :param zlabel: Label for Z axis
+ :param title: Graph title
+ """
+ signal = self.normalizeComplexData(signal)
+ if self.__selector_is_connected:
+ self._selector.selectionChanged.disconnect(self._updateVolume)
+ self.__selector_is_connected = False
+
+ self.__signal = signal
+ self.__signal_name = signal_name or ""
+ self.__x_axis = x_axis
+ self.__x_axis_name = xlabel
+ self.__y_axis = y_axis
+ self.__y_axis_name = ylabel
+ self.__z_axis = z_axis
+ self.__z_axis_name = zlabel
+
+ self._selector.setData(signal)
+ self._selector.setAxisNames(["Y", "X", "Z"])
+
+ self._view.setAxesLabels(self.__x_axis_name or 'X',
+ self.__y_axis_name or 'Y',
+ self.__z_axis_name or 'Z')
+ self._updateVolume()
+
+ # the legend label shows the selection slice producing the volume
+ # (only interesting for ndim > 3)
+ if signal.ndim > 3:
+ self._selector.setVisible(True)
+ self._legend.setVisible(True)
+ self._hline.setVisible(True)
+ else:
+ self._selector.setVisible(False)
+ self._legend.setVisible(False)
+ self._hline.setVisible(False)
+
+ if not self.__selector_is_connected:
+ self._selector.selectionChanged.connect(self._updateVolume)
+ self.__selector_is_connected = True
+
+ def _updateVolume(self):
+ """Update displayed stack according to the current axes selector
+ data."""
+ data = self._selector.selectedData()
+ x_axis = self.__x_axis
+ y_axis = self.__y_axis
+ z_axis = self.__z_axis
+
+ offset = []
+ scale = []
+ for axis in [x_axis, y_axis, z_axis]:
+ if axis is None:
+ calibration = NoCalibration()
+ elif len(axis) == 2:
+ calibration = LinearCalibration(
+ y_intercept=axis[0], slope=axis[1])
+ else:
+ calibration = ArrayCalibration(axis)
+ if not calibration.is_affine():
+ _logger.warning("Axis has not linear values, ignored")
+ offset.append(0.)
+ scale.append(1.)
+ else:
+ offset.append(calibration(0))
+ scale.append(calibration.get_slope())
+
+ 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)
+
+ self._view.setData(data, copy=False)
+ self._view.setScale(*scale)
+ self._view.setTranslation(*offset)
+ self._view.setAxesLabels(self.__x_axis_name,
+ self.__y_axis_name,
+ self.__z_axis_name)
+
+ def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
+ self._view.setData(None)
diff --git a/silx/gui/data/TextFormatter.py b/silx/gui/data/TextFormatter.py
index 1401634..98c37d7 100644
--- a/silx/gui/data/TextFormatter.py
+++ b/silx/gui/data/TextFormatter.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
@@ -29,16 +29,15 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "24/07/2018"
-import numpy
+import logging
import numbers
-from silx.third_party import six
+
+import numpy
+import six
+
from silx.gui import qt
-import logging
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
_logger = logging.getLogger(__name__)
@@ -322,10 +321,9 @@ class TextFormatter(qt.QObject):
if dtype.kind == 'S':
return self.__formatCharString(data)
elif dtype.kind == 'O':
- if h5py is not None:
- text = self.__formatH5pyObject(data, dtype)
- if text is not None:
- return text
+ text = self.__formatH5pyObject(data, dtype)
+ if text is not None:
+ return text
try:
# Try ascii/utf-8
text = "%s" % data.decode("utf-8")
@@ -339,15 +337,14 @@ class TextFormatter(qt.QObject):
elif isinstance(data, (numpy.integer)):
if dtype is None:
dtype = data.dtype
- if h5py is not None:
- enumType = h5py.check_dtype(enum=dtype)
- if enumType is not None:
- for key, value in enumType.items():
- if value == data:
- result = {}
- result["name"] = key
- result["value"] = data
- return self.__enumFormat % result
+ enumType = h5py.check_dtype(enum=dtype)
+ if enumType is not None:
+ for key, value in enumType.items():
+ if value == data:
+ result = {}
+ result["name"] = key
+ result["value"] = data
+ return self.__enumFormat % result
return self.__integerFormat % data
elif isinstance(data, (numbers.Integral)):
return self.__integerFormat % data
@@ -373,21 +370,20 @@ class TextFormatter(qt.QObject):
template = self.__floatFormat
params = (data.real)
return template % params
- elif h5py is not None and isinstance(data, h5py.h5r.Reference):
+ elif isinstance(data, h5py.h5r.Reference):
dtype = h5py.special_dtype(ref=h5py.Reference)
text = self.__formatH5pyObject(data, dtype)
return text
- elif h5py is not None and isinstance(data, h5py.h5r.RegionReference):
+ elif isinstance(data, h5py.h5r.RegionReference):
dtype = h5py.special_dtype(ref=h5py.RegionReference)
text = self.__formatH5pyObject(data, dtype)
return text
elif isinstance(data, numpy.object_) or dtype is not None:
if dtype is None:
dtype = data.dtype
- if h5py is not None:
- text = self.__formatH5pyObject(data, dtype)
- if text is not None:
- return text
+ text = self.__formatH5pyObject(data, dtype)
+ if text is not None:
+ return text
# That's a numpy object
return str(data)
return str(data)
diff --git a/silx/gui/data/test/test_arraywidget.py b/silx/gui/data/test/test_arraywidget.py
index 50ffc84..6bcbbd3 100644
--- a/silx/gui/data/test/test_arraywidget.py
+++ b/silx/gui/data/test/test_arraywidget.py
@@ -36,10 +36,7 @@ from silx.gui import qt
from silx.gui.data import ArrayTableWidget
from silx.gui.utils.testutils import TestCaseQt
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class TestArrayWidget(TestCaseQt):
@@ -190,7 +187,6 @@ class TestArrayWidget(TestCaseQt):
self.assertIs(b0, b1)
-@unittest.skipIf(h5py is None, "Could not import h5py")
class TestH5pyArrayWidget(TestCaseQt):
"""Basic test for ArrayTableWidget with a dataset.
diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py
index a681f33..dc6fee8 100644
--- a/silx/gui/data/test/test_dataviewer.py
+++ b/silx/gui/data/test/test_dataviewer.py
@@ -24,7 +24,7 @@
# ###########################################################################*/
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "23/04/2018"
+__date__ = "19/02/2019"
import os
import tempfile
@@ -42,10 +42,7 @@ from silx.gui.data.DataViewerFrame import DataViewerFrame
from silx.gui.utils.testutils import SignalListener
from silx.gui.utils.testutils import TestCaseQt
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class _DataViewMock(DataView):
@@ -170,8 +167,6 @@ class AbstractDataViewerTests(TestCaseQt):
self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId())
def test_3d_h5_dataset(self):
- if h5py is None:
- self.skipTest("h5py library is not available")
with self.h5_temporary_file() as h5file:
dataset = h5file["data"]
widget = self.create_widget()
@@ -242,8 +237,9 @@ class AbstractDataViewerTests(TestCaseQt):
# 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)
+ replaced = widget.replaceView(DataViews.NXDATA_INVALID_MODE,
+ view)
+ self.assertTrue(replaced)
nxdata_view = widget.getViewFromModeId(DataViews.NXDATA_MODE)
self.assertNotIn(DataViews.NXDATA_INVALID_MODE,
[v.modeId() for v in nxdata_view.availableViews()])
diff --git a/silx/gui/data/test/test_numpyaxesselector.py b/silx/gui/data/test/test_numpyaxesselector.py
index 6b7b58c..df11c1a 100644
--- a/silx/gui/data/test/test_numpyaxesselector.py
+++ b/silx/gui/data/test/test_numpyaxesselector.py
@@ -37,10 +37,7 @@ from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
from silx.gui.utils.testutils import SignalListener
from silx.gui.utils.testutils import TestCaseQt
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class TestNumpyAxesSelector(TestCaseQt):
@@ -121,8 +118,6 @@ class TestNumpyAxesSelector(TestCaseQt):
os.unlink(tmp_name)
def test_h5py_dataset(self):
- if h5py is None:
- self.skipTest("h5py library is not available")
with self.h5_temporary_file() as h5file:
dataset = h5file["data"]
expectedResult = dataset[0]
diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py
index 850aa00..935344a 100644
--- a/silx/gui/data/test/test_textformatter.py
+++ b/silx/gui/data/test/test_textformatter.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,17 +29,15 @@ __date__ = "12/12/2017"
import unittest
import shutil
import tempfile
+
import numpy
+import six
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.utils.testutils import SignalListener
from ..TextFormatter import TextFormatter
-from silx.third_party import six
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class TestTextFormatter(TestCaseQt):
@@ -108,8 +106,6 @@ class TestTextFormatterWithH5py(TestCaseQt):
@classmethod
def setUpClass(cls):
super(TestTextFormatterWithH5py, cls).setUpClass()
- if h5py is None:
- raise unittest.SkipTest("h5py is not available")
cls.tmpDirectory = tempfile.mkdtemp()
cls.h5File = h5py.File("%s/formatter.h5" % cls.tmpDirectory, mode="w")