summaryrefslogtreecommitdiff
path: root/silx/gui/data
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/data')
-rw-r--r--silx/gui/data/DataViewer.py38
-rw-r--r--silx/gui/data/DataViews.py337
-rw-r--r--silx/gui/data/Hdf5TableView.py68
-rw-r--r--silx/gui/data/NXdataWidgets.py82
-rw-r--r--silx/gui/data/_RecordPlot.py92
-rw-r--r--silx/gui/data/test/test_arraywidget.py4
6 files changed, 543 insertions, 78 deletions
diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py
index bad4362..2e51439 100644
--- a/silx/gui/data/DataViewer.py
+++ b/silx/gui/data/DataViewer.py
@@ -27,10 +27,12 @@ view from the ones provided by silx.
"""
from __future__ import division
-from silx.gui.data import DataViews
-from silx.gui.data.DataViews import _normalizeData
import logging
+import os.path
+import collections
from silx.gui import qt
+from silx.gui.data import DataViews
+from silx.gui.data.DataViews import _normalizeData
from silx.gui.utils import blockSignals
from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
@@ -43,6 +45,11 @@ __date__ = "12/02/2019"
_logger = logging.getLogger(__name__)
+DataSelection = collections.namedtuple("DataSelection",
+ ["filename", "datapath",
+ "slice", "permutation"])
+
+
class DataViewer(qt.QFrame):
"""Widget to display any kind of data
@@ -150,6 +157,7 @@ class DataViewer(qt.QFrame):
DataViews._Plot3dView,
DataViews._RawView,
DataViews._StackView,
+ DataViews._Plot2dRecordView,
]
views = []
for viewClass in viewClasses:
@@ -238,14 +246,39 @@ class DataViewer(qt.QFrame):
"""
if self.__useAxisSelection:
self.__displayedData = self.__numpySelection.selectedData()
+
+ permutation = self.__numpySelection.permutation()
+ normal = tuple(range(len(permutation)))
+ if permutation == normal:
+ permutation = None
+ slicing = self.__numpySelection.selection()
+ normal = tuple([slice(None)] * len(slicing))
+ if slicing == normal:
+ slicing = None
else:
self.__displayedData = self.__data
+ permutation = None
+ slicing = None
+
+ try:
+ filename = os.path.abspath(self.__data.file.filename)
+ except:
+ filename = None
+
+ try:
+ datapath = self.__data.name
+ except:
+ datapath = None
+
+ # FIXME: maybe use DataUrl, with added support of permutation
+ self.__displayedSelection = DataSelection(filename, datapath, slicing, permutation)
# TODO: would be good to avoid that, it should be synchonous
qt.QTimer.singleShot(10, self.__setDataInView)
def __setDataInView(self):
self.__currentView.setData(self.__displayedData)
+ self.__currentView.setDataSelection(self.__displayedSelection)
def setDisplayedView(self, view):
"""Set the displayed view.
@@ -468,6 +501,7 @@ class DataViewer(qt.QFrame):
self.__data = data
self._invalidateInfo()
self.__displayedData = None
+ self.__displayedSelection = None
self.__updateView()
self.__updateNumpySelectionAxis()
self.__updateDataInView()
diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py
index eb635c4..f3b02b9 100644
--- a/silx/gui/data/DataViews.py
+++ b/silx/gui/data/DataViews.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,6 +29,7 @@ from collections import OrderedDict
import logging
import numbers
import numpy
+import os
import silx.io
from silx.utils import deprecation
@@ -50,6 +51,7 @@ _logger = logging.getLogger(__name__)
# DataViewer modes
EMPTY_MODE = 0
PLOT1D_MODE = 10
+RECORD_PLOT_MODE = 15
IMAGE_MODE = 20
PLOT2D_MODE = 21
COMPLEX_IMAGE_MODE = 22
@@ -114,6 +116,7 @@ class DataInfo(object):
self.isRecord = False
self.hasNXdata = False
self.isInvalidNXdata = False
+ self.countNumericColumns = 0
self.shape = tuple()
self.dim = 0
self.size = 0
@@ -200,6 +203,12 @@ class DataInfo(object):
else:
self.size = 1
+ if hasattr(data, "dtype"):
+ if data.dtype.fields is not None:
+ for field in data.dtype.fields:
+ if numpy.issubdtype(data.dtype[field], numpy.number):
+ self.countNumericColumns += 1
+
def normalizeData(self, data):
"""Returns a normalized data if the embed a numpy or a dataset.
Else returns the data."""
@@ -223,6 +232,9 @@ class DataViewHooks(object):
"""Returns a color dialog for this view."""
return None
+ def viewWidgetCreated(self, view, plot):
+ """Called when the widget of the view was created"""
+ return
class DataView(object):
"""Holder for the data view."""
@@ -231,6 +243,12 @@ class DataView(object):
"""Priority returned when the requested data can't be displayed by the
view."""
+ TITLE_PATTERN = "{datapath}{slicing} {permuted}"
+ """Pattern used to format the title of the plot.
+
+ Supported fields: `{directory}`, `{filename}`, `{datapath}`, `{slicing}`, `{permuted}`.
+ """
+
def __init__(self, parent, modeId=None, icon=None, label=None):
"""Constructor
@@ -334,6 +352,9 @@ class DataView(object):
"""
if self.__widget is None:
self.__widget = self.createWidget(self.__parent)
+ hooks = self.getHooks()
+ if hooks is not None:
+ hooks.viewWidgetCreated(self, self.__widget)
return self.__widget
def createWidget(self, parent):
@@ -356,6 +377,70 @@ class DataView(object):
"""
return None
+ def __formatSlices(self, indices):
+ """Format an iterable of slice objects
+
+ :param indices: The slices to format
+ :type indices: Union[None,List[Union[slice,int]]]
+ :rtype: str
+ """
+ if indices is None:
+ return ''
+
+ def formatSlice(slice_):
+ start, stop, step = slice_.start, slice_.stop, slice_.step
+ string = ('' if start is None else str(start)) + ':'
+ if stop is not None:
+ string += str(stop)
+ if step not in (None, 1):
+ string += ':' + step
+ return string
+
+ return '[' + ', '.join(
+ formatSlice(index) if isinstance(index, slice) else str(index)
+ for index in indices) + ']'
+
+ def titleForSelection(self, selection):
+ """Build title from given selection information.
+
+ :param NamedTuple selection: Data selected
+ :rtype: str
+ """
+ if selection is None:
+ return None
+ else:
+ directory, filename = os.path.split(selection.filename)
+ try:
+ slicing = self.__formatSlices(selection.slice)
+ except Exception:
+ _logger.debug("Error while formatting slices", exc_info=True)
+ slicing = '[sliced]'
+
+ permuted = '(permuted)' if selection.permutation is not None else ''
+
+ try:
+ title = self.TITLE_PATTERN.format(
+ directory=directory,
+ filename=filename,
+ datapath=selection.datapath,
+ slicing=slicing,
+ permuted=permuted)
+ except Exception:
+ _logger.debug("Error while formatting title", exc_info=True)
+ title = selection.datapath + slicing
+
+ return title
+
+ def setDataSelection(self, selection):
+ """Set the data selection displayed by the view
+
+ If called, it have to be called directly after `setData`.
+
+ :param selection: Data selected
+ :type selection: NamedTuple
+ """
+ pass
+
def axesNames(self, data, info):
"""Returns names of the expected axes of the view, according to the
input data. A none value will disable the default axes selectior.
@@ -579,6 +664,11 @@ class SelectOneDataView(_CompositeDataView):
self.__updateDisplayedView()
self.__currentView.setData(data)
+ def setDataSelection(self, selection):
+ if self.__currentView is None:
+ return
+ self.__currentView.setDataSelection(selection)
+
def axesNames(self, data, info):
view = self.__getBestView(data, info)
self.__currentView = view
@@ -799,12 +889,18 @@ class _Plot1dView(DataView):
def setData(self, data):
data = self.normalizeData(data)
- self.getWidget().addCurve(legend="data",
- x=range(len(data)),
- y=data,
- resetzoom=self.__resetZoomNextTime)
+ plotWidget = self.getWidget()
+ legend = "data"
+ plotWidget.addCurve(legend=legend,
+ x=range(len(data)),
+ y=data,
+ resetzoom=self.__resetZoomNextTime)
+ plotWidget.setActiveCurve(legend)
self.__resetZoomNextTime = True
+ def setDataSelection(self, selection):
+ self.getWidget().setGraphTitle(self.titleForSelection(selection))
+
def axesNames(self, data, info):
return ["y"]
@@ -825,6 +921,107 @@ class _Plot1dView(DataView):
return 10
+class _Plot2dRecordView(DataView):
+ def __init__(self, parent):
+ super(_Plot2dRecordView, self).__init__(
+ parent=parent,
+ modeId=RECORD_PLOT_MODE,
+ label="Curve",
+ icon=icons.getQIcon("view-1d"))
+ self.__resetZoomNextTime = True
+ self._data = None
+ self._xAxisDropDown = None
+ self._yAxisDropDown = None
+ self.__fields = None
+
+ def createWidget(self, parent):
+ from ._RecordPlot import RecordPlot
+ return RecordPlot(parent=parent)
+
+ def clear(self):
+ self.getWidget().clear()
+ self.__resetZoomNextTime = True
+
+ def normalizeData(self, data):
+ data = DataView.normalizeData(self, data)
+ data = _normalizeComplex(data)
+ return data
+
+ def setData(self, data):
+ self._data = self.normalizeData(data)
+
+ all_fields = sorted(self._data.dtype.fields.items(), key=lambda e: e[1][1])
+ numeric_fields = [f[0] for f in all_fields if numpy.issubdtype(f[1][0], numpy.number)]
+ if numeric_fields == self.__fields: # Reuse previously selected fields
+ fieldNameX = self.getWidget().getXAxisFieldName()
+ fieldNameY = self.getWidget().getYAxisFieldName()
+ else:
+ self.__fields = numeric_fields
+
+ self.getWidget().setSelectableXAxisFieldNames(numeric_fields)
+ self.getWidget().setSelectableYAxisFieldNames(numeric_fields)
+ fieldNameX = None
+ fieldNameY = numeric_fields[0]
+
+ # If there is a field called time, use it for the x-axis by default
+ if "time" in numeric_fields:
+ fieldNameX = "time"
+ # Use the first field that is not "time" for the y-axis
+ if fieldNameY == "time" and len(numeric_fields) >= 2:
+ fieldNameY = numeric_fields[1]
+
+ self._plotData(fieldNameX, fieldNameY)
+
+ if not self._xAxisDropDown:
+ self._xAxisDropDown = self.getWidget().getAxesSelectionToolBar().getXAxisDropDown()
+ self._yAxisDropDown = self.getWidget().getAxesSelectionToolBar().getYAxisDropDown()
+ self._xAxisDropDown.activated.connect(self._onAxesSelectionChaned)
+ self._yAxisDropDown.activated.connect(self._onAxesSelectionChaned)
+
+ def setDataSelection(self, selection):
+ self.getWidget().setGraphTitle(self.titleForSelection(selection))
+
+ def _onAxesSelectionChaned(self):
+ fieldNameX = self._xAxisDropDown.currentData()
+ self._plotData(fieldNameX, self._yAxisDropDown.currentText())
+
+ def _plotData(self, fieldNameX, fieldNameY):
+ self.clear()
+ ydata = self._data[fieldNameY]
+ if fieldNameX is None:
+ xdata = numpy.arange(len(ydata))
+ else:
+ xdata = self._data[fieldNameX]
+ self.getWidget().addCurve(legend="data",
+ x=xdata,
+ y=ydata,
+ resetzoom=self.__resetZoomNextTime)
+ self.getWidget().setXAxisFieldName(fieldNameX)
+ self.getWidget().setYAxisFieldName(fieldNameY)
+ self.__resetZoomNextTime = True
+
+ def axesNames(self, data, info):
+ return ["data"]
+
+ def getDataPriority(self, data, info):
+ if info.size <= 0:
+ return DataView.UNSUPPORTED
+ if data is None or not info.isRecord:
+ return DataView.UNSUPPORTED
+ if info.dim < 1:
+ return DataView.UNSUPPORTED
+ if info.countNumericColumns < 2:
+ return DataView.UNSUPPORTED
+ if info.interpretation == "spectrum":
+ return 1000
+ if info.dim == 2 and info.shape[0] == 1:
+ return 210
+ if info.dim == 1:
+ return 40
+ else:
+ return 10
+
+
class _Plot2dView(DataView):
"""View displaying data using a 2d plot"""
@@ -863,6 +1060,9 @@ class _Plot2dView(DataView):
resetzoom=self.__resetZoomNextTime)
self.__resetZoomNextTime = False
+ def setDataSelection(self, selection):
+ self.getWidget().setGraphTitle(self.titleForSelection(selection))
+
def axesNames(self, data, info):
return ["y", "x"]
@@ -969,6 +1169,10 @@ class _ComplexImageView(DataView):
data = self.normalizeData(data)
self.getWidget().setData(data)
+ def setDataSelection(self, selection):
+ self.getWidget().getPlot().setGraphTitle(
+ self.titleForSelection(selection))
+
def axesNames(self, data, info):
return ["y", "x"]
@@ -1045,7 +1249,7 @@ class _StackView(DataView):
from silx.gui import plot
widget = plot.StackView(parent=parent)
widget.setColormap(self.defaultColormap())
- widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog())
widget.setKeepDataAspectRatio(True)
widget.setLabels(self.axesNames(None, None))
# hide default option panel
@@ -1068,6 +1272,11 @@ class _StackView(DataView):
self.getWidget().setColormap(self.defaultColormap())
self.__resetZoomNextTime = False
+ def setDataSelection(self, selection):
+ title = self.titleForSelection(selection)
+ self.getWidget().setTitleCallback(
+ lambda idx: "%s z=%d" % (title, idx))
+
def axesNames(self, data, info):
return ["depth", "y", "x"]
@@ -1337,12 +1546,26 @@ class _InvalidNXdataView(DataView):
return 100
-class _NXdataScalarView(DataView):
+class _NXdataBaseDataView(DataView):
+ """Base class for NXdata DataView"""
+
+ def __init__(self, *args, **kwargs):
+ DataView.__init__(self, *args, **kwargs)
+
+ def _updateColormap(self, nxdata):
+ """Update used colormap according to nxdata's SILX_style"""
+ cmap_norm = nxdata.plot_style.signal_scale_type
+ if cmap_norm is not None:
+ self.defaultColormap().setNormalization(
+ 'log' if cmap_norm == 'log' else 'linear')
+
+
+class _NXdataScalarView(_NXdataBaseDataView):
"""DataView using a table view for displaying NXdata scalars:
0-D signal or n-D signal with *@interpretation=scalar*"""
def __init__(self, parent):
- DataView.__init__(self, parent,
- modeId=NXDATA_SCALAR_MODE)
+ _NXdataBaseDataView.__init__(
+ self, parent, modeId=NXDATA_SCALAR_MODE)
def createWidget(self, parent):
from silx.gui.data.ArrayTableWidget import ArrayTableWidget
@@ -1375,7 +1598,7 @@ class _NXdataScalarView(DataView):
return DataView.UNSUPPORTED
-class _NXdataCurveView(DataView):
+class _NXdataCurveView(_NXdataBaseDataView):
"""DataView using a Plot1D for displaying NXdata curves:
1-D signal or n-D signal with *@interpretation=spectrum*.
@@ -1383,8 +1606,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,
- modeId=NXDATA_CURVE_MODE)
+ _NXdataBaseDataView.__init__(
+ self, parent, modeId=NXDATA_CURVE_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayCurvePlot
@@ -1422,7 +1645,9 @@ class _NXdataCurveView(DataView):
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])
+ title=nxd.title or signals_names[0],
+ xscale=nxd.plot_style.axes_scale_types[-1],
+ yscale=nxd.plot_style.signal_scale_type)
def getDataPriority(self, data, info):
data = self.normalizeData(data)
@@ -1432,16 +1657,19 @@ class _NXdataCurveView(DataView):
return DataView.UNSUPPORTED
-class _NXdataXYVScatterView(DataView):
+class _NXdataXYVScatterView(_NXdataBaseDataView):
"""DataView using a Plot1D for displaying NXdata 3D scatters as
a scatter of coloured points (1-D signal with 2 axes)"""
def __init__(self, parent):
- DataView.__init__(self, parent,
- modeId=NXDATA_XYVSCATTER_MODE)
+ _NXdataBaseDataView.__init__(
+ self, parent, modeId=NXDATA_XYVSCATTER_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import XYVScatterPlot
widget = XYVScatterPlot(parent)
+ widget.getScatterView().setColormap(self.defaultColormap())
+ widget.getScatterView().getScatterToolBar().getColormapAction().setColorDialog(
+ self.defaultColorDialog())
return widget
def axesNames(self, data, info):
@@ -1472,11 +1700,15 @@ class _NXdataXYVScatterView(DataView):
else:
y_errors = None
+ self._updateColormap(nxd)
+
self.getWidget().setScattersData(y_axis, x_axis, values=[nxd.signal] + nxd.auxiliary_signals,
yerror=y_errors, xerror=x_errors,
ylabel=y_label, xlabel=x_label,
title=nxd.title,
- scatter_titles=[nxd.signal_name] + nxd.auxiliary_signals_names)
+ scatter_titles=[nxd.signal_name] + nxd.auxiliary_signals_names,
+ xscale=nxd.plot_style.axes_scale_types[-2],
+ yscale=nxd.plot_style.axes_scale_types[-1])
def getDataPriority(self, data, info):
data = self.normalizeData(data)
@@ -1488,12 +1720,12 @@ class _NXdataXYVScatterView(DataView):
return DataView.UNSUPPORTED
-class _NXdataImageView(DataView):
+class _NXdataImageView(_NXdataBaseDataView):
"""DataView using a Plot2D for displaying NXdata images:
2-D signal or n-D signals with *@interpretation=image*."""
def __init__(self, parent):
- DataView.__init__(self, parent,
- modeId=NXDATA_IMAGE_MODE)
+ _NXdataBaseDataView.__init__(
+ self, parent, modeId=NXDATA_IMAGE_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayImagePlot
@@ -1514,17 +1746,21 @@ class _NXdataImageView(DataView):
nxd = nxdata.get_default(data, validate=False)
isRgba = nxd.interpretation == "rgba-image"
+ self._updateColormap(nxd)
+
# last two axes are Y & X
img_slicing = slice(-2, None) if not isRgba else slice(-3, -1)
y_axis, x_axis = nxd.axes[img_slicing]
y_label, x_label = nxd.axes_names[img_slicing]
+ y_scale, x_scale = nxd.plot_style.axes_scale_types[img_slicing]
self.getWidget().setImageData(
[nxd.signal] + nxd.auxiliary_signals,
x_axis=x_axis, y_axis=y_axis,
signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names,
xlabel=x_label, ylabel=y_label,
- title=nxd.title, isRgba=isRgba)
+ title=nxd.title, isRgba=isRgba,
+ xscale=x_scale, yscale=y_scale)
def getDataPriority(self, data, info):
data = self.normalizeData(data)
@@ -1536,12 +1772,12 @@ class _NXdataImageView(DataView):
return DataView.UNSUPPORTED
-class _NXdataComplexImageView(DataView):
+class _NXdataComplexImageView(_NXdataBaseDataView):
"""DataView using a ComplexImageView for displaying NXdata complex images:
2-D signal or n-D signals with *@interpretation=image*."""
def __init__(self, parent):
- DataView.__init__(self, parent,
- modeId=NXDATA_IMAGE_MODE)
+ _NXdataBaseDataView.__init__(
+ self, parent, modeId=NXDATA_IMAGE_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot
@@ -1556,6 +1792,8 @@ class _NXdataComplexImageView(DataView):
data = self.normalizeData(data)
nxd = nxdata.get_default(data, validate=False)
+ self._updateColormap(nxd)
+
# last two axes are Y & X
img_slicing = slice(-2, None)
y_axis, x_axis = nxd.axes[img_slicing]
@@ -1583,16 +1821,16 @@ class _NXdataComplexImageView(DataView):
return DataView.UNSUPPORTED
-class _NXdataStackView(DataView):
+class _NXdataStackView(_NXdataBaseDataView):
def __init__(self, parent):
- DataView.__init__(self, parent,
- modeId=NXDATA_STACK_MODE)
+ _NXdataBaseDataView.__init__(
+ self, parent, modeId=NXDATA_STACK_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayStackPlot
widget = ArrayStackPlot(parent)
widget.getStackView().setColormap(self.defaultColormap())
- widget.getStackView().getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.getStackView().getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog())
return widget
def axesNames(self, data, info):
@@ -1610,6 +1848,8 @@ class _NXdataStackView(DataView):
z_label, y_label, x_label = nxd.axes_names[-3:]
title = nxd.title or signal_name
+ self._updateColormap(nxd)
+
widget = self.getWidget()
widget.setStackData(
nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
@@ -1628,12 +1868,13 @@ class _NXdataStackView(DataView):
return DataView.UNSUPPORTED
-class _NXdataVolumeView(DataView):
+class _NXdataVolumeView(_NXdataBaseDataView):
def __init__(self, parent):
- DataView.__init__(self, parent,
- label="NXdata (3D)",
- icon=icons.getQIcon("view-nexus"),
- modeId=NXDATA_VOLUME_MODE)
+ _NXdataBaseDataView.__init__(
+ self, parent,
+ label="NXdata (3D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_MODE)
try:
import silx.gui.plot3d # noqa
except ImportError:
@@ -1642,7 +1883,7 @@ class _NXdataVolumeView(DataView):
raise
def normalizeData(self, data):
- data = DataView.normalizeData(self, data)
+ data = super(_NXdataVolumeView, self).normalizeData(data)
data = _normalizeComplex(data)
return data
@@ -1682,18 +1923,19 @@ class _NXdataVolumeView(DataView):
return DataView.UNSUPPORTED
-class _NXdataVolumeAsStackView(DataView):
+class _NXdataVolumeAsStackView(_NXdataBaseDataView):
def __init__(self, parent):
- DataView.__init__(self, parent,
- label="NXdata (2D)",
- icon=icons.getQIcon("view-nexus"),
- modeId=NXDATA_VOLUME_AS_STACK_MODE)
+ _NXdataBaseDataView.__init__(
+ self, parent,
+ label="NXdata (2D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_AS_STACK_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayStackPlot
widget = ArrayStackPlot(parent)
widget.getStackView().setColormap(self.defaultColormap())
- widget.getStackView().getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.getStackView().getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog())
return widget
def axesNames(self, data, info):
@@ -1711,6 +1953,8 @@ class _NXdataVolumeAsStackView(DataView):
z_label, y_label, x_label = nxd.axes_names[-3:]
title = nxd.title or signal_name
+ self._updateColormap(nxd)
+
widget = self.getWidget()
widget.setStackData(
nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
@@ -1730,12 +1974,13 @@ class _NXdataVolumeAsStackView(DataView):
return DataView.UNSUPPORTED
-class _NXdataComplexVolumeAsStackView(DataView):
+class _NXdataComplexVolumeAsStackView(_NXdataBaseDataView):
def __init__(self, parent):
- DataView.__init__(self, parent,
- label="NXdata (2D)",
- icon=icons.getQIcon("view-nexus"),
- modeId=NXDATA_VOLUME_AS_STACK_MODE)
+ _NXdataBaseDataView.__init__(
+ self, parent,
+ label="NXdata (2D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_AS_STACK_MODE)
self._is_complex_data = False
def createWidget(self, parent):
@@ -1759,6 +2004,8 @@ class _NXdataComplexVolumeAsStackView(DataView):
z_label, y_label, x_label = nxd.axes_names[-3:]
title = nxd.title or signal_name
+ self._updateColormap(nxd)
+
self.getWidget().setImageData(
[nxd.signal] + nxd.auxiliary_signals,
x_axis=x_axis, y_axis=y_axis,
diff --git a/silx/gui/data/Hdf5TableView.py b/silx/gui/data/Hdf5TableView.py
index d7c33f3..57d6f7b 100644
--- a/silx/gui/data/Hdf5TableView.py
+++ b/silx/gui/data/Hdf5TableView.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -37,6 +37,7 @@ import functools
import os.path
import logging
import h5py
+import numpy
from silx.gui import qt
import silx.io
@@ -265,7 +266,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
return cell.span()
elif role == self.IsHeaderRole:
return cell.isHeader()
- elif role == qt.Qt.DisplayRole:
+ elif role in (qt.Qt.DisplayRole, qt.Qt.EditRole):
value = cell.value()
if callable(value):
try:
@@ -287,12 +288,6 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
return cell.data(role)
return None
- def flags(self, index):
- """QAbstractTableModel method to inform the view whether data
- is editable or not.
- """
- return qt.QAbstractTableModel.flags(self, index)
-
def isSupportedObject(self, h5pyObject):
"""
Returns true if the provided object can be modelized using this model.
@@ -349,6 +344,16 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
shape = self.__hdf5Formatter.humanReadableShape(dataset)
return u"%s = %s" % (shape, size)
+ def __formatChunks(self, dataset):
+ """Format the shape"""
+ chunks = dataset.chunks
+ if chunks is None:
+ return ""
+ shape = " \u00D7 ".join([str(i) for i in chunks])
+ sizes = numpy.product(chunks)
+ text = "%s = %s" % (shape, sizes)
+ return text
+
def __initProperties(self):
"""Initialize the list of available properties according to the defined
h5py-like object."""
@@ -418,7 +423,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
if hasattr(obj, "shape"):
self.__data.addHeaderValueRow("shape", self.__formatShape)
if hasattr(obj, "chunks") and obj.chunks is not None:
- self.__data.addHeaderValueRow("chunks", lambda x: x.chunks)
+ self.__data.addHeaderValueRow("chunks", self.__formatChunks)
# relative to compression
# h5py expose compression, compression_opts but are not initialized
@@ -438,8 +443,8 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
self.__data.addRow(pos, hdf5id, name, options, availability)
for index in range(dcpl.get_nfilters()):
filterId, name, options = self.__getFilterInfo(obj, index)
- pos = _CellData(value=index)
- hdf5id = _CellData(value=filterId)
+ pos = _CellData(value=str(index))
+ hdf5id = _CellData(value=str(filterId))
name = _CellData(value=name)
options = _CellData(value=options)
availability = _CellFilterAvailableData(filterId=filterId)
@@ -517,12 +522,42 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
self.reset()
+class Hdf5TableItemDelegate(HierarchicalTableView.HierarchicalItemDelegate):
+ """Item delegate the :class:`Hdf5TableView` with read-only text editor"""
+
+ def createEditor(self, parent, option, index):
+ """See :meth:`QStyledItemDelegate.createEditor`"""
+ editor = super().createEditor(parent, option, index)
+ if isinstance(editor, qt.QLineEdit):
+ editor.setReadOnly(True)
+ editor.deselect()
+ editor.textChanged.connect(self.__textChanged, qt.Qt.QueuedConnection)
+ self.installEventFilter(editor)
+ return editor
+
+ def __textChanged(self, text):
+ sender = self.sender()
+ if sender is not None:
+ sender.deselect()
+
+ def eventFilter(self, watched, event):
+ eventType = event.type()
+ if eventType == qt.QEvent.FocusIn:
+ watched.selectAll()
+ qt.QTimer.singleShot(0, watched.selectAll)
+ elif eventType == qt.QEvent.FocusOut:
+ watched.deselect()
+ return super().eventFilter(watched, event)
+
+
class Hdf5TableView(HierarchicalTableView.HierarchicalTableView):
"""A widget to display metadata about a HDF5 node using a table."""
def __init__(self, parent=None):
super(Hdf5TableView, self).__init__(parent)
self.setModel(Hdf5TableModel(self))
+ self.setItemDelegate(Hdf5TableItemDelegate(self))
+ self.setSelectionMode(qt.QAbstractItemView.NoSelection)
def isSupportedData(self, data):
"""
@@ -538,7 +573,9 @@ class Hdf5TableView(HierarchicalTableView.HierarchicalTableView):
`silx.gui.hdf5.H5Node` which is needed to display some local path
information.
"""
- self.model().setObject(data)
+ model = self.model()
+
+ model.setObject(data)
header = self.horizontalHeader()
if qt.qVersion() < "5.0":
setResizeMode = header.setResizeMode
@@ -550,3 +587,10 @@ class Hdf5TableView(HierarchicalTableView.HierarchicalTableView):
setResizeMode(3, qt.QHeaderView.ResizeToContents)
setResizeMode(4, qt.QHeaderView.ResizeToContents)
header.setStretchLastSection(False)
+
+ for row in range(model.rowCount()):
+ for column in range(model.columnCount()):
+ index = model.index(row, column)
+ if (index.isValid() and index.data(
+ HierarchicalTableView.HierarchicalTableModel.IsHeaderRole) is False):
+ self.openPersistentEditor(index)
diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py
index c3aefd3..224f337 100644
--- a/silx/gui/data/NXdataWidgets.py
+++ b/silx/gui/data/NXdataWidgets.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -99,7 +99,8 @@ class ArrayCurvePlot(qt.QWidget):
def setCurvesData(self, ys, x=None,
yerror=None, xerror=None,
- ylabels=None, xlabel=None, title=None):
+ ylabels=None, xlabel=None, title=None,
+ xscale=None, yscale=None):
"""
:param List[ndarray] ys: List of arrays to be represented by the y (vertical) axis.
@@ -115,6 +116,8 @@ class ArrayCurvePlot(qt.QWidget):
:param str ylabels: Labels for each curve's Y axis
:param str xlabel: Label for X axis
:param str title: Graph title
+ :param str xscale: Scale of X axis in (None, 'linear', 'log')
+ :param str yscale: Scale of Y axis in (None, 'linear', 'log')
"""
self.__signals = ys
self.__signals_names = ylabels or (["Y"] * len(ys))
@@ -135,6 +138,12 @@ class ArrayCurvePlot(qt.QWidget):
self._selector.show()
self._plot.setGraphTitle(title or "")
+ if xscale is not None:
+ self._plot.getXAxis().setScale(
+ 'log' if xscale == 'log' else 'linear')
+ if yscale is not None:
+ self._plot.getYAxis().setScale(
+ 'log' if yscale == 'log' else 'linear')
self._updateCurve()
if not self.__selector_is_connected:
@@ -235,6 +244,13 @@ class XYVScatterPlot(qt.QWidget):
def _sliderIdxChanged(self, value):
self._updateScatter()
+ def getScatterView(self):
+ """Returns the :class:`ScatterView` used for the display
+
+ :rtype: ScatterView
+ """
+ return self._plot
+
def getPlot(self):
"""Returns the plot used for the display
@@ -245,7 +261,8 @@ class XYVScatterPlot(qt.QWidget):
def setScattersData(self, y, x, values,
yerror=None, xerror=None,
ylabel=None, xlabel=None,
- title="", scatter_titles=None):
+ title="", scatter_titles=None,
+ xscale=None, yscale=None):
"""
:param ndarray y: 1D array for y (vertical) coordinates.
@@ -260,6 +277,8 @@ class XYVScatterPlot(qt.QWidget):
:param str xlabel: Label for X axis
:param str title: Main graph title
:param List[str] scatter_titles: Subtitles (one per scatter)
+ :param str xscale: Scale of X axis in (None, 'linear', 'log')
+ :param str yscale: Scale of Y axis in (None, 'linear', 'log')
"""
self.__y_axis = y
self.__x_axis = x
@@ -281,6 +300,13 @@ class XYVScatterPlot(qt.QWidget):
self._slider.setValue(0)
self._slider.valueChanged[int].connect(self._sliderIdxChanged)
+ if xscale is not None:
+ self._plot.getXAxis().setScale(
+ 'log' if xscale == 'log' else 'linear')
+ if yscale is not None:
+ self._plot.getYAxis().setScale(
+ 'log' if yscale == 'log' else 'linear')
+
self._updateScatter()
def _updateScatter(self):
@@ -289,10 +315,13 @@ class XYVScatterPlot(qt.QWidget):
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
+ title = self.__graph_title # main NXdata @title
+ if len(self.__scatter_titles) > 1:
+ # Append dataset name only when there is many datasets
+ title += '\n' + self.__scatter_titles[idx]
+ else:
+ title = self.__scatter_titles[idx] # scatter dataset name
self._plot.setGraphTitle(title)
self._plot.setData(x, y, self.__values[idx],
@@ -374,7 +403,8 @@ class ArrayImagePlot(qt.QWidget):
x_axis=None, y_axis=None,
signals_names=None,
xlabel=None, ylabel=None,
- title=None, isRgba=False):
+ title=None, isRgba=False,
+ xscale=None, yscale=None):
"""
:param signals: list of n-D datasets, whose last 2 dimensions are used as the
@@ -390,6 +420,8 @@ class ArrayImagePlot(qt.QWidget):
:param ylabel: Label for Y axis
:param title: Graph title
:param isRgba: True if data is a 3D RGBA image
+ :param str xscale: Scale of X axis in (None, 'linear', 'log')
+ :param str yscale: Scale of Y axis in (None, 'linear', 'log')
"""
self._selector.selectionChanged.disconnect(self._updateImage)
self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged)
@@ -423,6 +455,7 @@ class ArrayImagePlot(qt.QWidget):
self._auxSigSlider.hide()
self._auxSigSlider.setValue(0)
+ self._axis_scales = xscale, yscale
self._updateImage()
self._plot.resetZoom()
@@ -473,10 +506,21 @@ class ArrayImagePlot(qt.QWidget):
origin = (xorigin, yorigin)
scale = (xscale, yscale)
+ self._plot.getXAxis().setScale('linear')
+ self._plot.getYAxis().setScale('linear')
self._plot.addImage(image, legend=legend,
origin=origin, scale=scale,
replace=True)
else:
+ xaxisscale, yaxisscale = self._axis_scales
+
+ if xaxisscale is not None:
+ self._plot.getXAxis().setScale(
+ 'log' if xaxisscale == 'log' else 'linear')
+ if yaxisscale is not None:
+ self._plot.getYAxis().setScale(
+ 'log' if yaxisscale == 'log' else 'linear')
+
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),
@@ -484,11 +528,13 @@ class ArrayImagePlot(qt.QWidget):
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]
+ title = self.__title
+ if len(self.__signals_names) > 1:
+ # Append dataset name only when there is many datasets
+ title += '\n' + self.__signals_names[auxSigIdx]
+ else:
+ title = self.__signals_names[auxSigIdx]
self._plot.setGraphTitle(title)
self._plot.getXAxis().setLabel(self.__x_axis_name)
self._plot.getYAxis().setLabel(self.__y_axis_name)
@@ -672,11 +718,13 @@ class ArrayComplexImagePlot(qt.QWidget):
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]
+ title = self.__title
+ if len(self.__signals_names) > 1:
+ # Append dataset name only when there is many datasets
+ title += '\n' + self.__signals_names[auxSigIdx]
+ else:
+ title = self.__signals_names[auxSigIdx]
self._plot.setGraphTitle(title)
self._plot.getXAxis().setLabel(self.__x_axis_name)
self._plot.getYAxis().setLabel(self.__y_axis_name)
@@ -785,8 +833,8 @@ class ArrayStackPlot(qt.QWidget):
self._stack_view.setGraphTitle(title or "")
# by default, the z axis is the image position (dimension not plotted)
- self._stack_view.getPlot().getXAxis().setLabel(self.__x_axis_name or "X")
- self._stack_view.getPlot().getYAxis().setLabel(self.__y_axis_name or "Y")
+ self._stack_view.getPlotWidget().getXAxis().setLabel(self.__x_axis_name or "X")
+ self._stack_view.getPlotWidget().getYAxis().setLabel(self.__y_axis_name or "Y")
self._updateStack()
diff --git a/silx/gui/data/_RecordPlot.py b/silx/gui/data/_RecordPlot.py
new file mode 100644
index 0000000..5be792f
--- /dev/null
+++ b/silx/gui/data/_RecordPlot.py
@@ -0,0 +1,92 @@
+from silx.gui.plot.PlotWindow import PlotWindow
+from silx.gui.plot.PlotWidget import PlotWidget
+from .. import qt
+
+
+class RecordPlot(PlotWindow):
+ def __init__(self, parent=None, backend=None):
+ super(RecordPlot, self).__init__(parent=parent, backend=backend,
+ resetzoom=True, autoScale=True,
+ logScale=True, grid=True,
+ curveStyle=True, colormap=False,
+ aspectRatio=False, yInverted=False,
+ copy=True, save=True, print_=True,
+ control=True, position=True,
+ roi=True, mask=False, fit=True)
+ if parent is None:
+ self.setWindowTitle('RecordPlot')
+ self._axesSelectionToolBar = AxesSelectionToolBar(parent=self, plot=self)
+ self.addToolBar(qt.Qt.BottomToolBarArea, self._axesSelectionToolBar)
+
+ def setXAxisFieldName(self, value):
+ """Set the current selected field for the X axis.
+
+ :param Union[str,None] value:
+ """
+ label = '' if value is None else value
+ index = self._axesSelectionToolBar.getXAxisDropDown().findData(value)
+
+ if index >= 0:
+ self.getXAxis().setLabel(label)
+ self._axesSelectionToolBar.getXAxisDropDown().setCurrentIndex(index)
+
+ def getXAxisFieldName(self):
+ """Returns currently selected field for the X axis or None.
+
+ rtype: Union[str,None]
+ """
+ return self._axesSelectionToolBar.getXAxisDropDown().currentData()
+
+ def setYAxisFieldName(self, value):
+ self.getYAxis().setLabel(value)
+ index = self._axesSelectionToolBar.getYAxisDropDown().findText(value)
+ if index >= 0:
+ self._axesSelectionToolBar.getYAxisDropDown().setCurrentIndex(index)
+
+ def getYAxisFieldName(self):
+ return self._axesSelectionToolBar.getYAxisDropDown().currentText()
+
+ def setSelectableXAxisFieldNames(self, fieldNames):
+ """Add list of field names to X axis
+
+ :param List[str] fieldNames:
+ """
+ comboBox = self._axesSelectionToolBar.getXAxisDropDown()
+ comboBox.clear()
+ comboBox.addItem('-', None)
+ comboBox.insertSeparator(1)
+ for name in fieldNames:
+ comboBox.addItem(name, name)
+
+ def setSelectableYAxisFieldNames(self, fieldNames):
+ self._axesSelectionToolBar.getYAxisDropDown().clear()
+ self._axesSelectionToolBar.getYAxisDropDown().addItems(fieldNames)
+
+ def getAxesSelectionToolBar(self):
+ return self._axesSelectionToolBar
+
+class AxesSelectionToolBar(qt.QToolBar):
+ def __init__(self, parent=None, plot=None, title='Plot Axes Selection'):
+ super(AxesSelectionToolBar, self).__init__(title, parent)
+
+ assert isinstance(plot, PlotWidget)
+
+ self.addWidget(qt.QLabel("Field selection: "))
+
+ self._labelXAxis = qt.QLabel(" X: ")
+ self.addWidget(self._labelXAxis)
+
+ self._selectXAxisDropDown = qt.QComboBox()
+ self.addWidget(self._selectXAxisDropDown)
+
+ self._labelYAxis = qt.QLabel(" Y: ")
+ self.addWidget(self._labelYAxis)
+
+ self._selectYAxisDropDown = qt.QComboBox()
+ self.addWidget(self._selectYAxisDropDown)
+
+ def getXAxisDropDown(self):
+ return self._selectXAxisDropDown
+
+ def getYAxisDropDown(self):
+ return self._selectYAxisDropDown \ No newline at end of file
diff --git a/silx/gui/data/test/test_arraywidget.py b/silx/gui/data/test/test_arraywidget.py
index 6bcbbd3..7785ac5 100644
--- a/silx/gui/data/test/test_arraywidget.py
+++ b/silx/gui/data/test/test_arraywidget.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -199,7 +199,7 @@ class TestH5pyArrayWidget(TestCaseQt):
# create an h5py file with a dataset
self.tempdir = tempfile.mkdtemp()
self.h5_fname = os.path.join(self.tempdir, "array.h5")
- h5f = h5py.File(self.h5_fname)
+ h5f = h5py.File(self.h5_fname, mode='w')
h5f["my_array"] = self.data
h5f["my_scalar"] = 3.14
h5f["my_1D_array"] = numpy.array(numpy.arange(1000))