summaryrefslogtreecommitdiff
path: root/silx/gui/plot/actions
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2018-03-04 10:20:27 +0100
committerPicca Frédéric-Emmanuel <picca@debian.org>2018-03-04 10:20:27 +0100
commit270d5ddc31c26b62379e3caa9044dd75ccc71847 (patch)
tree55c5bfc851dfce7172d335cd2405b214323e3caf /silx/gui/plot/actions
parente19c96eff0c310c06c4f268c8b80cb33bd08996f (diff)
New upstream version 0.7.0+dfsg
Diffstat (limited to 'silx/gui/plot/actions')
-rw-r--r--silx/gui/plot/actions/PlotAction.py3
-rw-r--r--silx/gui/plot/actions/__init__.py12
-rw-r--r--silx/gui/plot/actions/control.py140
-rw-r--r--silx/gui/plot/actions/fit.py4
-rw-r--r--silx/gui/plot/actions/histogram.py4
-rw-r--r--silx/gui/plot/actions/io.py133
-rw-r--r--silx/gui/plot/actions/medfilt.py8
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):
"""