diff options
Diffstat (limited to 'silx/gui/plot/actions')
-rw-r--r-- | silx/gui/plot/actions/PlotAction.py | 3 | ||||
-rw-r--r-- | silx/gui/plot/actions/__init__.py | 12 | ||||
-rw-r--r-- | silx/gui/plot/actions/control.py | 140 | ||||
-rw-r--r-- | silx/gui/plot/actions/fit.py | 4 | ||||
-rw-r--r-- | silx/gui/plot/actions/histogram.py | 4 | ||||
-rw-r--r-- | silx/gui/plot/actions/io.py | 133 | ||||
-rw-r--r-- | silx/gui/plot/actions/medfilt.py | 8 |
7 files changed, 227 insertions, 77 deletions
diff --git a/silx/gui/plot/actions/PlotAction.py b/silx/gui/plot/actions/PlotAction.py index 6eb9ba3..2983775 100644 --- a/silx/gui/plot/actions/PlotAction.py +++ b/silx/gui/plot/actions/PlotAction.py @@ -32,10 +32,9 @@ from __future__ import division __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" -__date__ = "20/04/2017" +__date__ = "03/01/2018" -from collections import OrderedDict import weakref from silx.gui import icons from silx.gui import qt diff --git a/silx/gui/plot/actions/__init__.py b/silx/gui/plot/actions/__init__.py index 73829cd..930c728 100644 --- a/silx/gui/plot/actions/__init__.py +++ b/silx/gui/plot/actions/__init__.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017 European Synchrotron Radiation Facility +# Copyright (c) 2017-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,10 +22,14 @@ # THE SOFTWARE. # # ###########################################################################*/ -"""This package provides a set of QActions to use with :class:`PlotWidget` +"""This package provides a set of QAction to use with +:class:`~silx.gui.plot.PlotWidget` -It also contains the :class:'.PlotAction' (Base class for QAction that operates -on a PlotWidget) +Those actions are useful to add menu items or toolbar items +that interact with a :class:`~silx.gui.plot.PlotWidget`. + +It provides a base class used to define new plot actions: +:class:`~silx.gui.plot.actions.PlotAction`. """ __authors__ = ["H. Payno"] diff --git a/silx/gui/plot/actions/control.py b/silx/gui/plot/actions/control.py index 23e710e..ac6dc2f 100644 --- a/silx/gui/plot/actions/control.py +++ b/silx/gui/plot/actions/control.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2017 European Synchrotron Radiation Facility +# Copyright (c) 2004-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -50,11 +50,10 @@ from __future__ import division __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" -__date__ = "27/06/2017" +__date__ = "15/02/2018" from . import PlotAction import logging -import numpy from silx.gui.plot import items from silx.gui.plot.ColormapDialog import ColormapDialog from silx.gui.plot._utils import applyZoomToPlot as _applyZoomToPlot @@ -327,67 +326,112 @@ class ColormapAction(PlotAction): plot, icon='colormap', text='Colormap', tooltip="Change colormap", triggered=self._actionTriggered, - checkable=False, parent=parent) + checkable=True, parent=parent) + self.plot.sigActiveImageChanged.connect(self._updateColormap) + + def setColorDialog(self, colorDialog): + """Set a specific color dialog instead of using the default dialog.""" + assert(colorDialog is not None) + assert(self._dialog is None) + self._dialog = colorDialog + self._dialog.visibleChanged.connect(self._dialogVisibleChanged) + self.setChecked(self._dialog.isVisible()) + + @staticmethod + def _createDialog(parent): + """Create the dialog if not already existing + + :parent QWidget parent: Parent of the new colormap + :rtype: ColormapDialog + """ + dialog = ColormapDialog(parent=parent) + dialog.setModal(False) + return dialog def _actionTriggered(self, checked=False): """Create a cmap dialog and update active image and default cmap.""" - # Create the dialog if not already existing if self._dialog is None: - self._dialog = ColormapDialog() + self._dialog = self._createDialog(self.plot) + self._dialog.visibleChanged.connect(self._dialogVisibleChanged) + + # Run the dialog listening to colormap change + if checked is True: + self._dialog.show() + self._updateColormap() + else: + self._dialog.hide() + + def _dialogVisibleChanged(self, isVisible): + self.setChecked(isVisible) + def _updateColormap(self): + if self._dialog is None: + return image = self.plot.getActiveImage() - if not isinstance(image, items.ColormapMixIn): - # No active image or active image is RGBA, - # set dialog from default info - colormap = self.plot.getDefaultColormap() - self._dialog.setHistogram() # Reset histogram and range if any + if isinstance(image, items.ImageComplexData): + # Specific init for complex images + colormap = image.getColormap() - else: + mode = image.getVisualizationMode() + if mode in (items.ImageComplexData.Mode.AMPLITUDE_PHASE, + items.ImageComplexData.Mode.LOG10_AMPLITUDE_PHASE): + data = image.getData( + copy=False, mode=items.ImageComplexData.Mode.PHASE) + else: + data = image.getData(copy=False) + + # Set histogram and range if any + self._dialog.setData(data) + + elif isinstance(image, items.ColormapMixIn): # Set dialog from active image colormap = image.getColormap() - data = image.getData(copy=False) + # Set histogram and range if any + self._dialog.setData(data) - goodData = data[numpy.isfinite(data)] - if goodData.size > 0: - dataMin = goodData.min() - dataMax = goodData.max() - else: - qt.QMessageBox.warning( - None, "No Data", - "Image data does not contain any real value") - dataMin, dataMax = 1., 10. - - self._dialog.setHistogram() # Reset histogram if any - self._dialog.setDataRange(dataMin, dataMax) - # The histogram should be done in a worker thread - # hist, bin_edges = numpy.histogram(goodData, bins=256) - # self._dialog.setHistogram(hist, bin_edges) - - self._dialog.setColormap(name=colormap.getName(), - normalization=colormap.getNormalization(), - autoscale=colormap.isAutoscale(), - vmin=colormap.getVMin(), - vmax=colormap.getVMax(), - colors=colormap.getColormapLUT()) + else: + # No active image or active image is RGBA, + # set dialog from default info + colormap = self.plot.getDefaultColormap() + # Reset histogram and range if any + self._dialog.setData(None) - # Run the dialog listening to colormap change - self._dialog.sigColormapChanged.connect(self._colormapChanged) - result = self._dialog.exec_() - self._dialog.sigColormapChanged.disconnect(self._colormapChanged) + self._dialog.setColormap(colormap) - if not result: # Restore the previous colormap - self._colormapChanged(colormap) - def _colormapChanged(self, colormap): - # Update default colormap - self.plot.setDefaultColormap(colormap) +class ColorBarAction(PlotAction): + """QAction opening the ColorBarWidget of the specified plot. + + :param plot: :class:`.PlotWidget` instance on which to operate + :param parent: See :class:`QAction` + """ + def __init__(self, plot, parent=None): + self._dialog = None # To store an instance of ColormapDialog + super(ColorBarAction, self).__init__( + plot, icon='colorbar', text='Colorbar', + tooltip="Show/Hide the colorbar", + triggered=self._actionTriggered, + checkable=True, parent=parent) + colorBarWidget = self.plot.getColorBarWidget() + old = self.blockSignals(True) + self.setChecked(colorBarWidget.isVisibleTo(self.plot)) + self.blockSignals(old) + colorBarWidget.sigVisibleChanged.connect(self._widgetVisibleChanged) + + def _widgetVisibleChanged(self, isVisible): + """Callback when the colorbar `visible` property change.""" + if self.isChecked() == isVisible: + return + self.setChecked(isVisible) - # Update active image colormap - activeImage = self.plot.getActiveImage() - if isinstance(activeImage, items.ColormapMixIn): - activeImage.setColormap(colormap) + def _actionTriggered(self, checked=False): + """Create a cmap dialog and update active image and default cmap.""" + colorBarWidget = self.plot.getColorBarWidget() + if not colorBarWidget.isHidden() == checked: + return + self.plot.getColorBarWidget().setVisible(checked) class KeepAspectRatioAction(PlotAction): diff --git a/silx/gui/plot/actions/fit.py b/silx/gui/plot/actions/fit.py index d7256ab..5ca649c 100644 --- a/silx/gui/plot/actions/fit.py +++ b/silx/gui/plot/actions/fit.py @@ -36,7 +36,7 @@ from __future__ import division __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" -__date__ = "28/06/2017" +__date__ = "03/01/2018" from . import PlotAction import logging @@ -111,7 +111,7 @@ class FitAction(PlotAction): if histo is None and curve is None: # ambiguous case, we need to ask which plot item to fit - isd = ItemsSelectionDialog(plot=self.plot) + isd = ItemsSelectionDialog(parent=self.plot, plot=self.plot) isd.setWindowTitle("Select item to be fitted") isd.setItemsSelectionMode(qt.QTableWidget.SingleSelection) isd.setAvailableKinds(["curve", "histogram"]) diff --git a/silx/gui/plot/actions/histogram.py b/silx/gui/plot/actions/histogram.py index a4a91e9..40ef873 100644 --- a/silx/gui/plot/actions/histogram.py +++ b/silx/gui/plot/actions/histogram.py @@ -39,6 +39,7 @@ __license__ = "MIT" from . import PlotAction from silx.math.histogram import Histogramnd +from silx.math.combo import min_max import numpy import logging from silx.gui import qt @@ -107,8 +108,7 @@ class PixelIntensitiesHistoAction(PlotAction): image[:, :, 1] * 0.587 + image[:, :, 2] * 0.114) - xmin = numpy.nanmin(image) - xmax = numpy.nanmax(image) + xmin, xmax = min_max(image, min_positive=False, finite=True) nbins = min(1024, int(numpy.sqrt(image.size))) data_range = xmin, xmax diff --git a/silx/gui/plot/actions/io.py b/silx/gui/plot/actions/io.py index 50410e3..d6d5909 100644 --- a/silx/gui/plot/actions/io.py +++ b/silx/gui/plot/actions/io.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2017 European Synchrotron Radiation Facility +# Copyright (c) 2004-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -37,10 +37,11 @@ from __future__ import division __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" -__date__ = "27/06/2017" +__date__ = "02/02/2018" from . import PlotAction from silx.io.utils import save1D, savespec +from silx.io.nxdata import save_NXdata import logging import sys from collections import OrderedDict @@ -59,6 +60,10 @@ else: _logger = logging.getLogger(__name__) +_NEXUS_HDF5_EXT = [".nx5", ".nxs", ".hdf", ".hdf5", ".cxi", ".h5"] +_NEXUS_HDF5_EXT_STR = ' '.join(['*' + ext for ext in _NEXUS_HDF5_EXT]) + + class SaveAction(PlotAction): """QAction for saving Plot content. @@ -89,12 +94,15 @@ class SaveAction(PlotAction): ('Curve as OMNIC CSV (*.csv)', {'fmt': '%.7E', 'delimiter': ',', 'header': False}), ('Curve as SpecFile (*.dat)', - {'fmt': '%.7g', 'delimiter': '', 'header': False}) + {'fmt': '%.10g', 'delimiter': '', 'header': False}) )) CURVE_FILTER_NPY = 'Curve as NumPy binary file (*.npy)' - CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [CURVE_FILTER_NPY] + CURVE_FILTER_NXDATA = 'Curve as NXdata (%s)' % _NEXUS_HDF5_EXT_STR + + CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [CURVE_FILTER_NPY, + CURVE_FILTER_NXDATA] ALL_CURVES_FILTERS = ("All curves as SpecFile (*.dat)", ) @@ -107,6 +115,7 @@ class SaveAction(PlotAction): IMAGE_FILTER_CSV_TAB = 'Image data as tab-separated CSV (*.csv)' IMAGE_FILTER_RGB_PNG = 'Image as PNG (*.png)' IMAGE_FILTER_RGB_TIFF = 'Image as TIFF (*.tif)' + IMAGE_FILTER_NXDATA = 'Image as NXdata (%s)' % _NEXUS_HDF5_EXT_STR IMAGE_FILTERS = (IMAGE_FILTER_EDF, IMAGE_FILTER_TIFF, IMAGE_FILTER_NUMPY, @@ -115,7 +124,11 @@ class SaveAction(PlotAction): IMAGE_FILTER_CSV_SEMICOLON, IMAGE_FILTER_CSV_TAB, IMAGE_FILTER_RGB_PNG, - IMAGE_FILTER_RGB_TIFF) + IMAGE_FILTER_RGB_TIFF, + IMAGE_FILTER_NXDATA) + + SCATTER_FILTER_NXDATA = 'Scatter as NXdata (%s)' % _NEXUS_HDF5_EXT_STR + SCATTER_FILTERS = (SCATTER_FILTER_NXDATA, ) def __init__(self, plot, parent=None): super(SaveAction, self).__init__( @@ -183,7 +196,7 @@ class SaveAction(PlotAction): csvdelim = filter_['delimiter'] autoheader = filter_['header'] else: - # .npy + # .npy or nxdata fmt, csvdelim, autoheader = ("", "", False) # If curve has no associated label, get the default from the plot @@ -194,6 +207,19 @@ class SaveAction(PlotAction): if ylabel is None: ylabel = self.plot.getYAxis().getLabel() + if nameFilter == self.CURVE_FILTER_NXDATA: + return save_NXdata( + filename, + signal=curve.getYData(copy=False), + axes=[curve.getXData(copy=False)], + signal_name="y", + axes_names=["x"], + signal_long_name=ylabel, + axes_long_names=[xlabel], + signal_errors=curve.getYErrorData(copy=False), + axes_errors=[curve.getXErrorData(copy=True)], + title=self.plot.getGraphTitle()) + try: save1D(filename, curve.getXData(copy=False), @@ -226,11 +252,13 @@ class SaveAction(PlotAction): curve = curves[0] scanno = 1 try: + xlabel = curve.getXLabel() or self.plot.getGraphXLabel() + ylabel = curve.getYLabel() or self.plot.getGraphYLabel(curve.getYAxis()) specfile = savespec(filename, curve.getXData(copy=False), curve.getYData(copy=False), - curve.getXLabel(), - curve.getYLabel(), + xlabel, + ylabel, fmt="%.7g", scan_number=1, mode="w", write_file_header=True, close_file=False) @@ -241,12 +269,14 @@ class SaveAction(PlotAction): for curve in curves[1:]: try: scanno += 1 + xlabel = curve.getXLabel() or self.plot.getGraphXLabel() + ylabel = curve.getYLabel() or self.plot.getGraphYLabel(curve.getYAxis()) specfile = savespec(specfile, curve.getXData(copy=False), curve.getYData(copy=False), - curve.getXLabel(), - curve.getYLabel(), - fmt="%.7g", scan_number=scanno, mode="w", + xlabel, + ylabel, + fmt="%.7g", scan_number=scanno, write_file_header=False, close_file=False) except IOError: @@ -294,6 +324,24 @@ class SaveAction(PlotAction): return False return True + elif nameFilter == self.IMAGE_FILTER_NXDATA: + xorigin, yorigin = image.getOrigin() + xscale, yscale = image.getScale() + xaxis = xorigin + xscale * numpy.arange(data.shape[1]) + yaxis = yorigin + yscale * numpy.arange(data.shape[0]) + xlabel = image.getXLabel() or self.plot.getGraphXLabel() + ylabel = image.getYLabel() or self.plot.getGraphYLabel() + interpretation = "image" if len(data.shape) == 2 else "rgba-image" + + return save_NXdata(filename, + signal=data, + axes=[yaxis, xaxis], + signal_name="image", + axes_names=["y", "x"], + axes_long_names=[ylabel, xlabel], + title=self.plot.getGraphTitle(), + interpretation=interpretation) + elif nameFilter in (self.IMAGE_FILTER_ASCII, self.IMAGE_FILTER_CSV_COMMA, self.IMAGE_FILTER_CSV_SEMICOLON, @@ -343,6 +391,45 @@ class SaveAction(PlotAction): return False + def _saveScatter(self, filename, nameFilter): + """Save an image from the plot. + + :param str filename: The name of the file to write + :param str nameFilter: The selected name filter + :return: False if format is not supported or save failed, + True otherwise. + """ + if nameFilter not in self.SCATTER_FILTERS: + return False + + if nameFilter == self.SCATTER_FILTER_NXDATA: + scatter = self.plot.getScatter() + # TODO: we could get all scatters on this plot and concatenate their (x, y, values) + x = scatter.getXData(copy=False) + y = scatter.getYData(copy=False) + z = scatter.getValueData(copy=False) + + xerror = scatter.getXErrorData(copy=False) + if isinstance(xerror, float): + xerror = xerror * numpy.ones(x.shape, dtype=numpy.float32) + + yerror = scatter.getYErrorData(copy=False) + if isinstance(yerror, float): + yerror = yerror * numpy.ones(x.shape, dtype=numpy.float32) + + xlabel = self.plot.getGraphXLabel() + ylabel = self.plot.getGraphYLabel() + + return save_NXdata( + filename, + signal=z, + axes=[x, y], + signal_name="values", + axes_names=["x", "y"], + axes_long_names=[xlabel, ylabel], + axes_errors=[xerror, yerror], + title=self.plot.getGraphTitle()) + def _actionTriggered(self, checked=False): """Handle save action.""" # Set-up filters @@ -359,6 +446,11 @@ class SaveAction(PlotAction): if len(self.plot.getAllCurves()) > 1: filters.extend(self.ALL_CURVES_FILTERS) + # Add scatter filters if there is a scatter + # todo: CSV + if self.plot.getScatter() is not None: + filters.extend(self.SCATTER_FILTERS) + filters.extend(self.SNAPSHOT_FILTERS) # Create and run File dialog @@ -378,10 +470,19 @@ class SaveAction(PlotAction): dialog.close() # Forces the filename extension to match the chosen filter - extension = nameFilter.split()[-1][2:-1] - if (len(filename) <= len(extension) or - filename[-len(extension):].lower() != extension.lower()): - filename += extension + if "NXdata" in nameFilter: + has_allowed_ext = False + for ext in _NEXUS_HDF5_EXT: + if (len(filename) > len(ext) and + filename[-len(ext):].lower() == ext.lower()): + has_allowed_ext = True + if not has_allowed_ext: + filename += ".h5" + else: + default_extension = nameFilter.split()[-1][2:-1] + if (len(filename) <= len(default_extension) or + filename[-len(default_extension):].lower() != default_extension.lower()): + filename += default_extension # Handle save if nameFilter in self.SNAPSHOT_FILTERS: @@ -392,6 +493,8 @@ class SaveAction(PlotAction): return self._saveCurves(filename, nameFilter) elif nameFilter in self.IMAGE_FILTERS: return self._saveImage(filename, nameFilter) + elif nameFilter in self.SCATTER_FILTERS: + return self._saveScatter(filename, nameFilter) else: _logger.warning('Unsupported file filter: %s', nameFilter) return False diff --git a/silx/gui/plot/actions/medfilt.py b/silx/gui/plot/actions/medfilt.py index 3305d1b..4284a8b 100644 --- a/silx/gui/plot/actions/medfilt.py +++ b/silx/gui/plot/actions/medfilt.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2017 European Synchrotron Radiation Facility +# Copyright (c) 2004-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -39,7 +39,7 @@ from __future__ import division __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" -__date__ = "24/05/2017" +__date__ = "03/01/2018" from . import PlotAction from silx.gui.widgets.MedianFilterDialog import MedianFilterDialog @@ -67,7 +67,7 @@ class MedianFilterAction(PlotAction): self._originalImage = None self._legend = None self._filteredImage = None - self._popup = MedianFilterDialog(parent=None) + self._popup = MedianFilterDialog(parent=plot) self._popup.sigFilterOptChanged.connect(self._updateFilter) self.plot.sigActiveImageChanged.connect(self._updateActiveImage) self._updateActiveImage() @@ -101,7 +101,7 @@ class MedianFilterAction(PlotAction): self.plot.sigActiveImageChanged.connect(self._updateActiveImage) def _computeFilteredImage(self, kernelWidth, conditional): - raise NotImplemented('MedianFilterAction is a an abstract class') + raise NotImplementedError('MedianFilterAction is a an abstract class') def getFilteredImage(self): """ |