From 328032e2317e3ac4859196bbf12bdb71795302fe Mon Sep 17 00:00:00 2001 From: Alexandre Marie Date: Tue, 21 Jul 2020 14:45:14 +0200 Subject: New upstream version 0.13.0+dfsg --- silx/gui/data/DataViewer.py | 38 +++- silx/gui/data/DataViews.py | 337 ++++++++++++++++++++++++++++----- silx/gui/data/Hdf5TableView.py | 68 +++++-- silx/gui/data/NXdataWidgets.py | 82 ++++++-- silx/gui/data/_RecordPlot.py | 92 +++++++++ silx/gui/data/test/test_arraywidget.py | 4 +- 6 files changed, 543 insertions(+), 78 deletions(-) create mode 100644 silx/gui/data/_RecordPlot.py (limited to 'silx/gui/data') 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)) -- cgit v1.2.3