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