summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot')
-rw-r--r--src/silx/gui/plot/AlphaSlider.py30
-rw-r--r--src/silx/gui/plot/ColorBar.py216
-rw-r--r--src/silx/gui/plot/Colormap.py41
-rw-r--r--src/silx/gui/plot/ColormapDialog.py40
-rw-r--r--src/silx/gui/plot/Colors.py87
-rw-r--r--src/silx/gui/plot/CompareImages.py1040
-rw-r--r--src/silx/gui/plot/ComplexImageView.py117
-rw-r--r--src/silx/gui/plot/CurvesROIWidget.py474
-rw-r--r--src/silx/gui/plot/ImageStack.py348
-rw-r--r--src/silx/gui/plot/ImageView.py290
-rw-r--r--src/silx/gui/plot/Interaction.py51
-rw-r--r--src/silx/gui/plot/ItemsSelectionDialog.py46
-rwxr-xr-xsrc/silx/gui/plot/LegendSelector.py505
-rw-r--r--src/silx/gui/plot/LimitsHistory.py4
-rw-r--r--src/silx/gui/plot/MaskToolsWidget.py267
-rw-r--r--src/silx/gui/plot/PlotActions.py66
-rw-r--r--src/silx/gui/plot/PlotEvents.py185
-rw-r--r--src/silx/gui/plot/PlotInteraction.py1034
-rw-r--r--src/silx/gui/plot/PlotToolButtons.py163
-rwxr-xr-xsrc/silx/gui/plot/PlotWidget.py1609
-rw-r--r--src/silx/gui/plot/PlotWindow.py278
-rw-r--r--src/silx/gui/plot/PrintPreviewToolButton.py106
-rw-r--r--src/silx/gui/plot/Profile.py175
-rw-r--r--src/silx/gui/plot/ProfileMainWindow.py109
-rw-r--r--src/silx/gui/plot/ROIStatsWidget.py182
-rw-r--r--src/silx/gui/plot/ScatterMaskToolsWidget.py207
-rw-r--r--src/silx/gui/plot/ScatterView.py77
-rw-r--r--src/silx/gui/plot/StackView.py389
-rw-r--r--src/silx/gui/plot/StatsWidget.py319
-rw-r--r--src/silx/gui/plot/_BaseMaskToolsWidget.py340
-rw-r--r--src/silx/gui/plot/__init__.py12
-rw-r--r--src/silx/gui/plot/_utils/__init__.py27
-rw-r--r--src/silx/gui/plot/_utils/delaunay.py60
-rw-r--r--src/silx/gui/plot/_utils/dtime_ticklayout.py162
-rw-r--r--src/silx/gui/plot/_utils/panzoom.py136
-rw-r--r--src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py1
-rw-r--r--src/silx/gui/plot/_utils/test/test_ticklayout.py19
-rw-r--r--src/silx/gui/plot/_utils/ticklayout.py16
-rw-r--r--src/silx/gui/plot/actions/PlotAction.py24
-rw-r--r--src/silx/gui/plot/actions/PlotToolAction.py30
-rwxr-xr-xsrc/silx/gui/plot/actions/control.py321
-rw-r--r--src/silx/gui/plot/actions/fit.py94
-rw-r--r--src/silx/gui/plot/actions/histogram.py131
-rw-r--r--src/silx/gui/plot/actions/io.py426
-rw-r--r--src/silx/gui/plot/actions/medfilt.py46
-rw-r--r--src/silx/gui/plot/actions/mode.py80
-rwxr-xr-xsrc/silx/gui/plot/backends/BackendBase.py137
-rwxr-xr-xsrc/silx/gui/plot/backends/BackendMatplotlib.py862
-rwxr-xr-xsrc/silx/gui/plot/backends/BackendOpenGL.py964
-rw-r--r--src/silx/gui/plot/backends/glutils/GLPlotCurve.py806
-rw-r--r--src/silx/gui/plot/backends/glutils/GLPlotFrame.py681
-rw-r--r--src/silx/gui/plot/backends/glutils/GLPlotImage.py357
-rw-r--r--src/silx/gui/plot/backends/glutils/GLPlotItem.py9
-rw-r--r--src/silx/gui/plot/backends/glutils/GLPlotTriangles.py45
-rw-r--r--src/silx/gui/plot/backends/glutils/GLSupport.py109
-rw-r--r--src/silx/gui/plot/backends/glutils/GLText.py207
-rw-r--r--src/silx/gui/plot/backends/glutils/GLTexture.py209
-rw-r--r--src/silx/gui/plot/backends/glutils/PlotImageFile.py99
-rw-r--r--src/silx/gui/plot/items/__init__.py44
-rw-r--r--src/silx/gui/plot/items/_arc_roi.py256
-rw-r--r--src/silx/gui/plot/items/_band_roi.py18
-rw-r--r--src/silx/gui/plot/items/_roi_base.py168
-rw-r--r--src/silx/gui/plot/items/axis.py88
-rw-r--r--src/silx/gui/plot/items/complex.py65
-rw-r--r--src/silx/gui/plot/items/core.py409
-rw-r--r--src/silx/gui/plot/items/curve.py209
-rw-r--r--src/silx/gui/plot/items/histogram.py139
-rw-r--r--src/silx/gui/plot/items/image.py165
-rw-r--r--src/silx/gui/plot/items/image_aggregated.py30
-rwxr-xr-xsrc/silx/gui/plot/items/marker.py95
-rw-r--r--src/silx/gui/plot/items/roi.py320
-rw-r--r--src/silx/gui/plot/items/scatter.py464
-rw-r--r--src/silx/gui/plot/items/shape.py99
-rw-r--r--src/silx/gui/plot/matplotlib/Colormap.py248
-rw-r--r--src/silx/gui/plot/stats/stats.py242
-rw-r--r--src/silx/gui/plot/stats/statshandler.py51
-rw-r--r--src/silx/gui/plot/test/conftest.py (renamed from src/silx/gui/plot/PlotTools.py)25
-rw-r--r--src/silx/gui/plot/test/testAlphaSlider.py40
-rw-r--r--src/silx/gui/plot/test/testAxis.py147
-rw-r--r--src/silx/gui/plot/test/testColorBar.py146
-rw-r--r--src/silx/gui/plot/test/testCompareImages.py271
-rw-r--r--src/silx/gui/plot/test/testComplexImageView.py3
-rw-r--r--src/silx/gui/plot/test/testCurvesROIWidget.py254
-rw-r--r--src/silx/gui/plot/test/testImageStack.py113
-rw-r--r--src/silx/gui/plot/test/testImageView.py56
-rw-r--r--src/silx/gui/plot/test/testInteraction.py32
-rw-r--r--src/silx/gui/plot/test/testItem.py382
-rw-r--r--src/silx/gui/plot/test/testLegendSelector.py50
-rw-r--r--src/silx/gui/plot/test/testMaskToolsWidget.py122
-rw-r--r--src/silx/gui/plot/test/testPixelIntensityHistoAction.py27
-rw-r--r--src/silx/gui/plot/test/testPlotActions.py41
-rw-r--r--src/silx/gui/plot/test/testPlotInteraction.py163
-rwxr-xr-xsrc/silx/gui/plot/test/testPlotWidget.py1579
-rwxr-xr-xsrc/silx/gui/plot/test/testPlotWidgetActiveItem.py416
-rw-r--r--src/silx/gui/plot/test/testPlotWidgetDataMargins.py135
-rw-r--r--src/silx/gui/plot/test/testPlotWidgetNoBackend.py528
-rw-r--r--src/silx/gui/plot/test/testPlotWindow.py38
-rw-r--r--src/silx/gui/plot/test/testRoiStatsWidget.py178
-rw-r--r--src/silx/gui/plot/test/testSaveAction.py60
-rw-r--r--src/silx/gui/plot/test/testScatterMaskToolsWidget.py124
-rw-r--r--src/silx/gui/plot/test/testScatterView.py20
-rw-r--r--src/silx/gui/plot/test/testStackView.py159
-rw-r--r--src/silx/gui/plot/test/testStats.py701
-rw-r--r--src/silx/gui/plot/test/testUtilsAxis.py77
-rw-r--r--src/silx/gui/plot/test/utils.py2
-rw-r--r--src/silx/gui/plot/tools/CurveLegendsWidget.py21
-rw-r--r--src/silx/gui/plot/tools/LimitsToolBar.py22
-rw-r--r--src/silx/gui/plot/tools/PlotToolButton.py92
-rw-r--r--src/silx/gui/plot/tools/PositionInfo.py88
-rw-r--r--src/silx/gui/plot/tools/RadarView.py53
-rw-r--r--src/silx/gui/plot/tools/RulerToolButton.py183
-rw-r--r--src/silx/gui/plot/tools/compare/__init__.py (renamed from src/silx/gui/plot/matplotlib/__init__.py)17
-rw-r--r--src/silx/gui/plot/tools/compare/core.py198
-rw-r--r--src/silx/gui/plot/tools/compare/profile.py173
-rw-r--r--src/silx/gui/plot/tools/compare/statusbar.py218
-rw-r--r--src/silx/gui/plot/tools/compare/toolbar.py390
-rw-r--r--src/silx/gui/plot/tools/menus.py93
-rw-r--r--src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py13
-rw-r--r--src/silx/gui/plot/tools/profile/core.py313
-rw-r--r--src/silx/gui/plot/tools/profile/editors.py28
-rw-r--r--src/silx/gui/plot/tools/profile/manager.py175
-rw-r--r--src/silx/gui/plot/tools/profile/rois.py246
-rw-r--r--src/silx/gui/plot/tools/profile/toolbar.py3
-rw-r--r--src/silx/gui/plot/tools/roi.py380
-rw-r--r--src/silx/gui/plot/tools/test/testCurveLegendsWidget.py31
-rw-r--r--src/silx/gui/plot/tools/test/testProfile.py155
-rw-r--r--src/silx/gui/plot/tools/test/testRoiCore.py (renamed from src/silx/gui/plot/tools/test/testROI.py)333
-rw-r--r--src/silx/gui/plot/tools/test/testRoiItems.py313
-rw-r--r--src/silx/gui/plot/tools/test/testScatterProfileToolBar.py18
-rw-r--r--src/silx/gui/plot/tools/test/testTools.py37
-rw-r--r--src/silx/gui/plot/tools/toolbars.py80
-rw-r--r--src/silx/gui/plot/utils/axis.py31
-rw-r--r--src/silx/gui/plot/utils/intersections.py26
133 files changed, 15757 insertions, 11837 deletions
diff --git a/src/silx/gui/plot/AlphaSlider.py b/src/silx/gui/plot/AlphaSlider.py
index 486ca6f..8a0a711 100644
--- a/src/silx/gui/plot/AlphaSlider.py
+++ b/src/silx/gui/plot/AlphaSlider.py
@@ -96,6 +96,7 @@ class BaseAlphaSlider(qt.QSlider):
You must subclass this class and implement :meth:`getItem`.
"""
+
sigAlphaChanged = qt.Signal(float)
"""Emits the alpha value when the slider's value changes,
as a float between 0. and 1."""
@@ -119,7 +120,7 @@ class BaseAlphaSlider(qt.QSlider):
self.setEnabled(False)
else:
alpha = self.getItem().getAlpha()
- self.setValue(round(255*alpha))
+ self.setValue(round(255 * alpha))
self.valueChanged.connect(self._valueChanged)
@@ -132,8 +133,8 @@ class BaseAlphaSlider(qt.QSlider):
:rtype: :class:`silx.plot.items.Item`
"""
raise NotImplementedError(
- "BaseAlphaSlider must be subclassed to " +
- "implement getItem()")
+ "BaseAlphaSlider must be subclassed to " + "implement getItem()"
+ )
def getAlpha(self):
"""Get the opacity, as a float between 0. and 1.
@@ -141,15 +142,14 @@ class BaseAlphaSlider(qt.QSlider):
:return: Alpha value in [0., 1.]
:rtype: float
"""
- return self.value() / 255.
+ return self.value() / 255.0
def _valueChanged(self, value):
self._updateItem()
- self.sigAlphaChanged.emit(value / 255.)
+ self.sigAlphaChanged.emit(value / 255.0)
def _updateItem(self):
- """Update the item's alpha channel.
- """
+ """Update the item's alpha channel."""
item = self.getItem()
if item is not None:
item.setAlpha(self.getAlpha())
@@ -164,6 +164,7 @@ class ActiveImageAlphaSlider(BaseAlphaSlider):
See documentation of :class:`BaseAlphaSlider`
"""
+
def __init__(self, parent=None, plot=None):
"""
@@ -203,8 +204,8 @@ class NamedItemAlphaSlider(BaseAlphaSlider):
:param str legend: Legend of item whose transparency is to be
controlled.
"""
- def __init__(self, parent=None, plot=None,
- kind=None, legend=None):
+
+ def __init__(self, parent=None, plot=None, kind=None, legend=None):
self._item_legend = legend
self._item_kind = kind
@@ -234,8 +235,7 @@ class NamedItemAlphaSlider(BaseAlphaSlider):
:rtype: subclass of :class:`silx.gui.plot.items.Item`"""
if self._item_legend is None or self._item_kind is None:
return None
- return self.plot._getItem(kind=self._item_kind,
- legend=self._item_legend)
+ return self.plot._getItem(kind=self._item_kind, legend=self._item_legend)
def setLegend(self, legend):
"""Associate a different item (of the same kind) to the slider.
@@ -280,9 +280,9 @@ class NamedImageAlphaSlider(NamedItemAlphaSlider):
:param str legend: Legend of image whose transparency is to be
controlled.
"""
+
def __init__(self, parent=None, plot=None, legend=None):
- NamedItemAlphaSlider.__init__(self, parent, plot,
- kind="image", legend=legend)
+ NamedItemAlphaSlider.__init__(self, parent, plot, kind="image", legend=legend)
class NamedScatterAlphaSlider(NamedItemAlphaSlider):
@@ -294,6 +294,6 @@ class NamedScatterAlphaSlider(NamedItemAlphaSlider):
:param str legend: Legend of scatter whose transparency is to be
controlled.
"""
+
def __init__(self, parent=None, plot=None, legend=None):
- NamedItemAlphaSlider.__init__(self, parent, plot,
- kind="scatter", legend=legend)
+ NamedItemAlphaSlider.__init__(self, parent, plot, kind="scatter", legend=legend)
diff --git a/src/silx/gui/plot/ColorBar.py b/src/silx/gui/plot/ColorBar.py
index 247da07..ee31f25 100644
--- a/src/silx/gui/plot/ColorBar.py
+++ b/src/silx/gui/plot/ColorBar.py
@@ -69,6 +69,7 @@ class ColorBarWidget(qt.QWidget):
:param plot: PlotWidget the colorbar is attached to (optional)
:param str legend: the label to set to the colorbar
"""
+
sigVisibleChanged = qt.Signal(bool)
"""Emitted when the property `visible` have changed."""
@@ -88,12 +89,11 @@ class ColorBarWidget(qt.QWidget):
self.setLayout(qt.QHBoxLayout())
# create color scale widget
- self._colorScale = ColorScaleBar(parent=self,
- colormap=None)
+ self._colorScale = ColorScaleBar(parent=self, colormap=None)
self.layout().addWidget(self._colorScale)
# legend (is the right group)
- self.legend = _VerticalLegend('', self)
+ self.legend = _VerticalLegend("", self)
self.layout().addWidget(self.legend)
self.layout().setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
@@ -118,10 +118,8 @@ class ColorBarWidget(qt.QWidget):
self._isConnected = False
plot = self.getPlot()
if plot is not None and qt_inspect.isValid(plot):
- plot.sigActiveImageChanged.disconnect(
- self._activeImageChanged)
- plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChanged)
+ plot.sigActiveImageChanged.disconnect(self._activeImageChanged)
+ plot.sigActiveScatterChanged.disconnect(self._activeScatterChanged)
plot.sigPlotSignal.disconnect(self._defaultColormapChanged)
def _connectPlot(self):
@@ -129,8 +127,7 @@ class ColorBarWidget(qt.QWidget):
plot = self.getPlot()
if plot is not None and not self._isConnected:
activeImageLegend = plot.getActiveImage(just_legend=True)
- activeScatterLegend = plot._getActiveItem(
- kind='scatter', just_legend=True)
+ activeScatterLegend = plot.getActiveScatter(just_legend=True)
if activeImageLegend is None and activeScatterLegend is None:
# Show plot default colormap
self._syncWithDefaultColormap()
@@ -170,8 +167,7 @@ class ColorBarWidget(qt.QWidget):
The data to display or item, needed if the colormap require an autoscale
"""
self._data = data
- self.getColorScaleBar().setColormap(colormap=colormap,
- data=data)
+ self.getColorScaleBar().setColormap(colormap=colormap, data=data)
if self._colormap is not None:
self._colormap.sigChanged.disconnect(self._colormapHasChanged)
self._colormap = colormap
@@ -179,11 +175,9 @@ class ColorBarWidget(qt.QWidget):
self._colormap.sigChanged.connect(self._colormapHasChanged)
def _colormapHasChanged(self):
- """handler of the Colormap.sigChanged signal
- """
+ """handler of the Colormap.sigChanged signal"""
assert self._colormap is not None
- self.setColormap(colormap=self._colormap,
- data=self._data)
+ self.setColormap(colormap=self._colormap, data=self._data)
def setLegend(self, legend):
"""Set the legend displayed along the colorbar
@@ -220,18 +214,16 @@ class ColorBarWidget(qt.QWidget):
return
# Sync with active scatter
- scatter = plot._getActiveItem(kind='scatter')
+ scatter = plot.getActiveScatter()
- self.setColormap(colormap=scatter.getColormap(),
- data=scatter)
+ self.setColormap(colormap=scatter.getColormap(), data=scatter)
def _activeImageChanged(self, previous, legend):
"""Handle plot active image changed"""
plot = self.getPlot()
if legend is None: # No active image, try with active scatter
- activeScatterLegend = plot._getActiveItem(
- kind='scatter', just_legend=True)
+ activeScatterLegend = plot.getActiveScatter(just_legend=True)
# No more active image, use active scatter if any
self._activeScatterChanged(None, activeScatterLegend)
else:
@@ -251,11 +243,13 @@ class ColorBarWidget(qt.QWidget):
def _defaultColormapChanged(self, event):
"""Handle plot default colormap changed"""
- if event['event'] == 'defaultColormapChanged':
+ if event["event"] == "defaultColormapChanged":
plot = self.getPlot()
- if (plot is not None and
- plot.getActiveImage() is None and
- plot._getActiveItem(kind='scatter') is None):
+ if (
+ plot is not None
+ and plot.getActiveImage() is None
+ and plot.getActiveScatter() is None
+ ):
# No active item, take default colormap update into account
self._syncWithDefaultColormap()
@@ -272,8 +266,8 @@ class ColorBarWidget(qt.QWidget):
class _VerticalLegend(qt.QLabel):
- """Display vertically the given text
- """
+ """Display vertically the given text"""
+
def __init__(self, text, parent=None):
"""
@@ -333,8 +327,7 @@ class ColorScaleBar(qt.QWidget):
"""The tick bar need a margin to display all labels at the correct place.
So the ColorScale should have the same margin in order for both to fit"""
- def __init__(self, parent=None, colormap=None, data=None,
- displayTicksValues=True):
+ def __init__(self, parent=None, colormap=None, data=None, displayTicksValues=True):
super(ColorScaleBar, self).__init__(parent)
self.minVal = None
@@ -345,10 +338,9 @@ class ColorScaleBar(qt.QWidget):
self.setLayout(qt.QGridLayout())
# create the left side group (ColorScale)
- self.colorScale = _ColorScale(colormap=colormap,
- data=data,
- parent=self,
- margin=ColorScaleBar._TEXT_MARGIN)
+ self.colorScale = _ColorScale(
+ colormap=colormap, data=data, parent=self, margin=ColorScaleBar._TEXT_MARGIN
+ )
if colormap:
vmin, vmax = colormap.getColormapRange(data)
normalizer = colormap._getNormalizer()
@@ -356,12 +348,14 @@ class ColorScaleBar(qt.QWidget):
vmin, vmax = colors.DEFAULT_MIN_LIN, colors.DEFAULT_MAX_LIN
normalizer = None
- self.tickbar = _TickBar(vmin=vmin,
- vmax=vmax,
- normalizer=normalizer,
- parent=self,
- displayValues=displayTicksValues,
- margin=ColorScaleBar._TEXT_MARGIN)
+ self.tickbar = _TickBar(
+ vmin=vmin,
+ vmax=vmax,
+ normalizer=normalizer,
+ parent=self,
+ displayValues=displayTicksValues,
+ margin=ColorScaleBar._TEXT_MARGIN,
+ )
self.layout().addWidget(self.tickbar, 1, 0, 1, 1, qt.Qt.AlignRight)
self.layout().addWidget(self.colorScale, 1, 1, qt.Qt.AlignLeft)
@@ -421,9 +415,7 @@ class ColorScaleBar(qt.QWidget):
vmin, vmax = None, None
normalizer = None
- self.tickbar.update(vmin=vmin,
- vmax=vmax,
- normalizer=normalizer)
+ self.tickbar.update(vmin=vmin, vmax=vmax, normalizer=normalizer)
self._setMinMaxLabels(vmin, vmax)
def setMinMaxVisible(self, val=True):
@@ -438,24 +430,24 @@ class ColorScaleBar(qt.QWidget):
"""Update the min and max label if we are in the case of the
configuration 'minMaxValueOnly'"""
if self.minVal is None:
- text, tooltip = '', ''
+ text, tooltip = "", ""
else:
if self.minVal == 0 or 0 <= numpy.log10(abs(self.minVal)) < 7:
- text = '%.7g' % self.minVal
+ text = "%.7g" % self.minVal
else:
- text = '%.2e' % self.minVal
+ text = "%.2e" % self.minVal
tooltip = repr(self.minVal)
self._minLabel.setText(text)
self._minLabel.setToolTip(tooltip)
if self.maxVal is None:
- text, tooltip = '', ''
+ text, tooltip = "", ""
else:
if self.maxVal == 0 or 0 <= numpy.log10(abs(self.maxVal)) < 7:
- text = '%.7g' % self.maxVal
+ text = "%.7g" % self.maxVal
else:
- text = '%.2e' % self.maxVal
+ text = "%.2e" % self.maxVal
tooltip = repr(self.maxVal)
self._maxLabel.setText(text)
@@ -561,7 +553,7 @@ class _ColorScale(qt.QWidget):
if colormap is None:
return
- indices = numpy.linspace(0., 1., self._NB_CONTROL_POINTS)
+ indices = numpy.linspace(0.0, 1.0, self._NB_CONTROL_POINTS)
colors = colormap.getNColors(nbColors=self._NB_CONTROL_POINTS)
self._gradient = qt.QLinearGradient(0, 1, 0, 0)
self._gradient.setCoordinateMode(qt.QGradient.StretchToDeviceMode)
@@ -574,30 +566,39 @@ class _ColorScale(qt.QWidget):
painter = qt.QPainter(self)
if self.getColormap() is not None:
painter.setBrush(self._gradient)
- penColor = self.palette().color(qt.QPalette.Active,
- qt.QPalette.WindowText)
+ penColor = self.palette().color(qt.QPalette.Active, qt.QPalette.WindowText)
else:
- penColor = self.palette().color(qt.QPalette.Disabled,
- qt.QPalette.WindowText)
+ penColor = self.palette().color(
+ qt.QPalette.Disabled, qt.QPalette.WindowText
+ )
painter.setPen(penColor)
- painter.drawRect(qt.QRect(
- 0,
- self.margin,
- self.width() - 1,
- self.height() - 2 * self.margin - 1))
+ painter.drawRect(
+ qt.QRect(
+ 0, self.margin, self.width() - 1, self.height() - 2 * self.margin - 1
+ )
+ )
def mouseMoveEvent(self, event):
- tooltip = str(self.getValueFromRelativePosition(
- self._getRelativePosition(qt.getMouseEventPosition(event)[1])))
- qt.QToolTip.showText(event.globalPos(), tooltip, self)
+ tooltip = str(
+ self.getValueFromRelativePosition(
+ self._getRelativePosition(qt.getMouseEventPosition(event)[1])
+ )
+ )
+ if qt.BINDING == "PyQt5":
+ position = event.globalPos()
+ else: # Qt6
+ position = event.globalPosition().toPoint()
+ qt.QToolTip.showText(position, tooltip, self)
super(_ColorScale, self).mouseMoveEvent(event)
def _getRelativePosition(self, yPixel):
- """yPixel : pixel position into _ColorScale widget reference
- """
+ """yPixel : pixel position into _ColorScale widget reference"""
# widgets are bottom-top referencial but we display in top-bottom referential
- return 1. - (yPixel - self.margin) / float(self.height() - 2 * self.margin)
+ height = float(self.height() - 2 * self.margin)
+ if height == 0:
+ return 0.0
+ return 1.0 - (yPixel - self.margin) / height
def getValueFromRelativePosition(self, value):
"""Return the value in the colorMap from a relative position in the
@@ -610,12 +611,15 @@ class _ColorScale(qt.QWidget):
if colormap is None:
return
- value = numpy.clip(value, 0., 1.)
+ value = numpy.clip(value, 0.0, 1.0)
normalizer = colormap._getNormalizer()
- normMin, normMax = normalizer.apply([self.vmin, self.vmax], self.vmin, self.vmax)
+ normMin, normMax = normalizer.apply(
+ [self.vmin, self.vmax], self.vmin, self.vmax
+ )
return normalizer.revert(
- normMin + (normMax - normMin) * value, self.vmin, self.vmax)
+ normMin + (normMax - normMin) * value, self.vmin, self.vmax
+ )
def setMargin(self, margin):
"""Define the margin to fit with a TickBar object.
@@ -651,6 +655,7 @@ class _TickBar(qt.QWidget):
number of ticks from the tick density.
:param int margin: margin to set on the top and bottom
"""
+
_WIDTH_DISP_VAL = 45
"""widget width when displayed with ticks labels"""
_WIDTH_NO_DISP_VAL = 10
@@ -662,8 +667,16 @@ class _TickBar(qt.QWidget):
DEFAULT_TICK_DENSITY = 0.015
- def __init__(self, vmin, vmax, normalizer, parent=None, displayValues=True,
- nticks=None, margin=5):
+ def __init__(
+ self,
+ vmin,
+ vmax,
+ normalizer,
+ parent=None,
+ displayValues=True,
+ nticks=None,
+ margin=5,
+ ):
super(_TickBar, self).__init__(parent)
self.margin = margin
self._nticks = None
@@ -722,7 +735,7 @@ class _TickBar(qt.QWidget):
(nticks=None) then you can specify a ticks density to be displayed.
"""
if density < 0.0:
- raise ValueError('Density should be a positive value')
+ raise ValueError("Density should be a positive value")
self.ticksDensity = density
def computeTicks(self):
@@ -752,14 +765,16 @@ class _TickBar(qt.QWidget):
def _computeTicksLog(self, nticks):
logMin = numpy.log10(self._vmin)
logMax = numpy.log10(self._vmax)
- lowBound, highBound, spacing, self._nfrac = ticklayout.niceNumbersForLog10(logMin,
- logMax,
- nticks)
- self.ticks = numpy.power(10., numpy.arange(lowBound, highBound, spacing))
+ lowBound, highBound, spacing, self._nfrac = ticklayout.niceNumbersForLog10(
+ logMin, logMax, nticks
+ )
+ self.ticks = numpy.power(10.0, numpy.arange(lowBound, highBound, spacing))
if spacing == 1:
- self.subTicks = ticklayout.computeLogSubTicks(ticks=self.ticks,
- lowBound=numpy.power(10., lowBound),
- highBound=numpy.power(10., highBound))
+ self.subTicks = ticklayout.computeLogSubTicks(
+ ticks=self.ticks,
+ lowBound=numpy.power(10.0, lowBound),
+ highBound=numpy.power(10.0, highBound),
+ )
else:
self.subTicks = []
@@ -768,9 +783,9 @@ class _TickBar(qt.QWidget):
self.computeTicks()
def _computeTicksLin(self, nticks):
- _min, _max, _spacing, self._nfrac = ticklayout.niceNumbers(self._vmin,
- self._vmax,
- nticks)
+ _min, _max, _spacing, self._nfrac = ticklayout.niceNumbers(
+ self._vmin, self._vmax, nticks
+ )
self.ticks = numpy.arange(_min, _max, _spacing)
self.subTicks = []
@@ -793,19 +808,18 @@ class _TickBar(qt.QWidget):
self._paintTick(val, painter, majorTick=False)
def _getRelativePosition(self, val):
- """Return the relative position of val according to min and max value
- """
+ """Return the relative position of val according to min and max value"""
if self._normalizer is None:
- return 0.
+ return 0.0
normMin, normMax, normVal = self._normalizer.apply(
- [self._vmin, self._vmax, val],
- self._vmin,
- self._vmax)
+ [self._vmin, self._vmax, val], self._vmin, self._vmax
+ )
if normMin == normMax:
- return 0.
- else:
- return 1. - (normVal - normMin) / (normMax - normMin)
+ return 0.0
+ if not numpy.isfinite(normVal):
+ return 0.0
+ return 1.0 - (normVal - normMin) / (normMax - normMin)
def _paintTick(self, val, painter, majorTick=True):
"""
@@ -821,14 +835,14 @@ class _TickBar(qt.QWidget):
if majorTick is False:
lineWidth /= 2
- painter.drawLine(qt.QLine(int(self.width() - lineWidth),
- height,
- self.width(),
- height))
+ painter.drawLine(
+ qt.QLine(int(self.width() - lineWidth), height, self.width(), height)
+ )
if self.displayValues and majorTick is True:
- painter.drawText(qt.QPoint(0, int(height + fm.height() / 2)),
- self.form.format(val))
+ painter.drawText(
+ qt.QPoint(0, int(height + fm.height() / 2)), self.form.format(val)
+ )
def setDisplayType(self, disType):
"""Set the type of display we want to set for ticks labels
@@ -841,8 +855,10 @@ class _TickBar(qt.QWidget):
- 'e' for scientific display
- None to let the _TickBar guess the best display for this kind of data.
"""
- if disType not in (None, 'std', 'e'):
- raise ValueError("display type not recognized, value should be in (None, 'std', 'e'")
+ if disType not in (None, "std", "e"):
+ raise ValueError(
+ "display type not recognized, value should be in (None, 'std', 'e'"
+ )
self._forcedDisplayType = disType
def _getStandardFormat(self):
@@ -851,12 +867,14 @@ class _TickBar(qt.QWidget):
def _getFormat(self, font):
if self._forcedDisplayType is None:
return self._guessType(font)
- elif self._forcedDisplayType == 'std':
+ elif self._forcedDisplayType == "std":
return self._getStandardFormat()
- elif self._forcedDisplayType == 'e':
+ elif self._forcedDisplayType == "e":
return self._getScientificForm()
else:
- err = 'Forced type for display %s is not recognized' % self._forcedDisplayType
+ err = (
+ "Forced type for display %s is not recognized" % self._forcedDisplayType
+ )
raise ValueError(err)
def _getScientificForm(self):
diff --git a/src/silx/gui/plot/Colormap.py b/src/silx/gui/plot/Colormap.py
deleted file mode 100644
index 8eaee84..0000000
--- a/src/silx/gui/plot/Colormap.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# /*##########################################################################
-#
-# Copyright (c) 2015-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
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""Deprecated module providing the Colormap object
-"""
-
-__authors__ = ["T. Vincent", "H.Payno"]
-__license__ = "MIT"
-__date__ = "27/11/2020"
-
-import silx.utils.deprecation
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.Colormap",
- reason="moved",
- replacement="silx.gui.colors.Colormap",
- since_version="0.8.0",
- only_once=True,
- skip_backtrace_count=1)
-
-from ..colors import * # noqa
diff --git a/src/silx/gui/plot/ColormapDialog.py b/src/silx/gui/plot/ColormapDialog.py
deleted file mode 100644
index 0c0df2c..0000000
--- a/src/silx/gui/plot/ColormapDialog.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# /*##########################################################################
-#
-# 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
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""Deprecated module providing ColormapDialog."""
-
-__authors__ = ["T. Vincent", "H.Payno"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-import silx.utils.deprecation
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.ColormapDialog",
- reason="moved",
- replacement="silx.gui.dialog.ColormapDialog",
- since_version="0.8.0",
- only_once=True,
- skip_backtrace_count=1)
-
-from ..dialog.ColormapDialog import * # noqa
diff --git a/src/silx/gui/plot/Colors.py b/src/silx/gui/plot/Colors.py
deleted file mode 100644
index 34ee815..0000000
--- a/src/silx/gui/plot/Colors.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# /*##########################################################################
-#
-# Copyright (c) 2004-2017 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
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""Color conversion function, color dictionary and colormap tools."""
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "14/06/2018"
-
-import silx.utils.deprecation
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.Colors",
- reason="moved",
- replacement="silx.gui.colors",
- since_version="0.8.0",
- only_once=True,
- skip_backtrace_count=1)
-
-from ..colors import * # noqa
-
-
-@silx.utils.deprecation.deprecated(replacement='silx.gui.colors.Colormap.applyColormap')
-def applyColormapToData(data,
- name='gray',
- normalization='linear',
- autoscale=True,
- vmin=0.,
- vmax=1.,
- colors=None):
- """Apply a colormap to the data and returns the RGBA image
-
- This supports data of any dimensions (not only of dimension 2).
- The returned array will have one more dimension (with 4 entries)
- than the input data to store the RGBA channels
- corresponding to each bin in the array.
-
- :param numpy.ndarray data: The data to convert.
- :param str name: Name of the colormap (default: 'gray').
- :param str normalization: Colormap mapping: 'linear' or 'log'.
- :param bool autoscale: Whether to use data min/max (True, default)
- or [vmin, vmax] range (False).
- :param float vmin: The minimum value of the range to use if
- 'autoscale' is False.
- :param float vmax: The maximum value of the range to use if
- 'autoscale' is False.
- :param numpy.ndarray colors: Only used if name is None.
- Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
- :return: The computed RGBA image
- :rtype: numpy.ndarray of uint8
- """
- colormap = Colormap(name=name,
- normalization=normalization,
- vmin=vmin,
- vmax=vmax,
- colors=colors)
- return colormap.applyToData(data)
-
-
-@silx.utils.deprecation.deprecated(replacement='silx.gui.colors.Colormap.getSupportedColormaps')
-def getSupportedColormaps():
- """Get the supported colormap names as a tuple of str.
-
- The list should at least contain and start by:
- ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
- """
- return Colormap.getSupportedColormaps()
diff --git a/src/silx/gui/plot/CompareImages.py b/src/silx/gui/plot/CompareImages.py
index 80e0db3..3823ae2 100644
--- a/src/silx/gui/plot/CompareImages.py
+++ b/src/silx/gui/plot/CompareImages.py
@@ -29,505 +29,30 @@ __license__ = "MIT"
__date__ = "23/07/2018"
-import enum
import logging
import numpy
-import weakref
-import collections
import math
import silx.image.bilinear
from silx.gui import qt
from silx.gui import plot
-from silx.gui import icons
from silx.gui.colors import Colormap
from silx.gui.plot import tools
+from silx.utils.deprecation import deprecated_warning
from silx.utils.weakref import WeakMethodProxy
+from silx.gui.plot.items import Scatter
+from silx.math.colormap import normalize
-_logger = logging.getLogger(__name__)
-
-from silx.opencl import ocl
-if ocl is not None:
- try:
- from silx.opencl import sift
- except ImportError:
- # sift module is not available (e.g., in official Debian packages)
- sift = None
-else: # No OpenCL device or no pyopencl
- sift = None
-
-
-@enum.unique
-class VisualizationMode(enum.Enum):
- """Enum for each visualization mode available."""
- ONLY_A = 'a'
- ONLY_B = 'b'
- VERTICAL_LINE = 'vline'
- HORIZONTAL_LINE = 'hline'
- COMPOSITE_RED_BLUE_GRAY = "rbgchannel"
- COMPOSITE_RED_BLUE_GRAY_NEG = "rbgnegchannel"
- COMPOSITE_A_MINUS_B = "aminusb"
-
-
-@enum.unique
-class AlignmentMode(enum.Enum):
- """Enum for each alignment mode available."""
- ORIGIN = 'origin'
- CENTER = 'center'
- STRETCH = 'stretch'
- AUTO = 'auto'
-
-
-AffineTransformation = collections.namedtuple("AffineTransformation",
- ["tx", "ty", "sx", "sy", "rot"])
-"""Contains a 2D affine transformation: translation, scale and rotation"""
-
-
-class CompareImagesToolBar(qt.QToolBar):
- """ToolBar containing specific tools to custom the configuration of a
- :class:`CompareImages` widget
-
- Use :meth:`setCompareWidget` to connect this toolbar to a specific
- :class:`CompareImages` widget.
-
- :param Union[qt.QWidget,None] parent: Parent of this widget.
- """
- def __init__(self, parent=None):
- qt.QToolBar.__init__(self, parent)
-
- self.__compareWidget = None
-
- menu = qt.QMenu(self)
- self.__visualizationToolButton = qt.QToolButton(self)
- self.__visualizationToolButton.setMenu(menu)
- self.__visualizationToolButton.setPopupMode(qt.QToolButton.InstantPopup)
- self.addWidget(self.__visualizationToolButton)
- self.__visualizationGroup = qt.QActionGroup(self)
- self.__visualizationGroup.setExclusive(True)
- self.__visualizationGroup.triggered.connect(self.__visualizationModeChanged)
-
- icon = icons.getQIcon("compare-mode-a")
- action = qt.QAction(icon, "Display the first image only", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_A))
- action.setProperty("mode", VisualizationMode.ONLY_A)
- menu.addAction(action)
- self.__aModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-b")
- action = qt.QAction(icon, "Display the second image only", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_B))
- action.setProperty("mode", VisualizationMode.ONLY_B)
- menu.addAction(action)
- self.__bModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-vline")
- action = qt.QAction(icon, "Vertical compare mode", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_V))
- action.setProperty("mode", VisualizationMode.VERTICAL_LINE)
- menu.addAction(action)
- self.__vlineModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-hline")
- action = qt.QAction(icon, "Horizontal compare mode", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_H))
- action.setProperty("mode", VisualizationMode.HORIZONTAL_LINE)
- menu.addAction(action)
- self.__hlineModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-rb-channel")
- action = qt.QAction(icon, "Blue/red compare mode (additive mode)", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_C))
- action.setProperty("mode", VisualizationMode.COMPOSITE_RED_BLUE_GRAY)
- menu.addAction(action)
- self.__brChannelModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-rbneg-channel")
- action = qt.QAction(icon, "Yellow/cyan compare mode (subtractive mode)", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_W))
- action.setProperty("mode", VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG)
- menu.addAction(action)
- self.__ycChannelModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-a-minus-b")
- action = qt.QAction(icon, "Raw A minus B compare mode", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_W))
- action.setProperty("mode", VisualizationMode.COMPOSITE_A_MINUS_B)
- menu.addAction(action)
- self.__ycChannelModeAction = action
- self.__visualizationGroup.addAction(action)
-
- menu = qt.QMenu(self)
- self.__alignmentToolButton = qt.QToolButton(self)
- self.__alignmentToolButton.setMenu(menu)
- self.__alignmentToolButton.setPopupMode(qt.QToolButton.InstantPopup)
- self.addWidget(self.__alignmentToolButton)
- self.__alignmentGroup = qt.QActionGroup(self)
- self.__alignmentGroup.setExclusive(True)
- self.__alignmentGroup.triggered.connect(self.__alignmentModeChanged)
-
- icon = icons.getQIcon("compare-align-origin")
- action = qt.QAction(icon, "Align images on their upper-left pixel", self)
- action.setProperty("mode", AlignmentMode.ORIGIN)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__originAlignAction = action
- menu.addAction(action)
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-align-center")
- action = qt.QAction(icon, "Center images", self)
- action.setProperty("mode", AlignmentMode.CENTER)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__centerAlignAction = action
- menu.addAction(action)
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-align-stretch")
- action = qt.QAction(icon, "Stretch the second image on the first one", self)
- action.setProperty("mode", AlignmentMode.STRETCH)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__stretchAlignAction = action
- menu.addAction(action)
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-align-auto")
- action = qt.QAction(icon, "Auto-alignment of the second image", self)
- action.setProperty("mode", AlignmentMode.AUTO)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__autoAlignAction = action
- menu.addAction(action)
- if sift is None:
- action.setEnabled(False)
- action.setToolTip("Sift module is not available")
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-keypoints")
- action = qt.QAction(icon, "Display/hide alignment keypoints", self)
- action.setCheckable(True)
- action.triggered.connect(self.__keypointVisibilityChanged)
- self.addAction(action)
- self.__displayKeypoints = action
-
- def setCompareWidget(self, widget):
- """
- Connect this tool bar to a specific :class:`CompareImages` widget.
-
- :param Union[None,CompareImages] widget: The widget to connect with.
- """
- compareWidget = self.getCompareWidget()
- if compareWidget is not None:
- compareWidget.sigConfigurationChanged.disconnect(self.__updateSelectedActions)
- compareWidget = widget
- if compareWidget is None:
- self.__compareWidget = None
- else:
- self.__compareWidget = weakref.ref(compareWidget)
- if compareWidget is not None:
- widget.sigConfigurationChanged.connect(self.__updateSelectedActions)
- self.__updateSelectedActions()
-
- def getCompareWidget(self):
- """Returns the connected widget.
-
- :rtype: CompareImages
- """
- if self.__compareWidget is None:
- return None
- else:
- return self.__compareWidget()
-
- def __updateSelectedActions(self):
- """
- Update the state of this tool bar according to the state of the
- connected :class:`CompareImages` widget.
- """
- widget = self.getCompareWidget()
- if widget is None:
- return
-
- mode = widget.getVisualizationMode()
- action = None
- for a in self.__visualizationGroup.actions():
- actionMode = a.property("mode")
- if mode == actionMode:
- action = a
- break
- old = self.__visualizationGroup.blockSignals(True)
- if action is not None:
- # Check this action
- action.setChecked(True)
- else:
- action = self.__visualizationGroup.checkedAction()
- if action is not None:
- # Uncheck this action
- action.setChecked(False)
- self.__updateVisualizationMenu()
- self.__visualizationGroup.blockSignals(old)
-
- mode = widget.getAlignmentMode()
- action = None
- for a in self.__alignmentGroup.actions():
- actionMode = a.property("mode")
- if mode == actionMode:
- action = a
- break
- old = self.__alignmentGroup.blockSignals(True)
- if action is not None:
- # Check this action
- action.setChecked(True)
- else:
- action = self.__alignmentGroup.checkedAction()
- if action is not None:
- # Uncheck this action
- action.setChecked(False)
- self.__updateAlignmentMenu()
- self.__alignmentGroup.blockSignals(old)
-
- def __visualizationModeChanged(self, selectedAction):
- """Called when user requesting changes of the visualization mode.
- """
- self.__updateVisualizationMenu()
- widget = self.getCompareWidget()
- if widget is not None:
- mode = selectedAction.property("mode")
- widget.setVisualizationMode(mode)
-
- def __updateVisualizationMenu(self):
- """Update the state of the action containing visualization menu.
- """
- selectedAction = self.__visualizationGroup.checkedAction()
- if selectedAction is not None:
- self.__visualizationToolButton.setText(selectedAction.text())
- self.__visualizationToolButton.setIcon(selectedAction.icon())
- self.__visualizationToolButton.setToolTip(selectedAction.toolTip())
- else:
- self.__visualizationToolButton.setText("")
- self.__visualizationToolButton.setIcon(qt.QIcon())
- self.__visualizationToolButton.setToolTip("")
-
- def __alignmentModeChanged(self, selectedAction):
- """Called when user requesting changes of the alignment mode.
- """
- self.__updateAlignmentMenu()
- widget = self.getCompareWidget()
- if widget is not None:
- mode = selectedAction.property("mode")
- widget.setAlignmentMode(mode)
-
- def __updateAlignmentMenu(self):
- """Update the state of the action containing alignment menu.
- """
- selectedAction = self.__alignmentGroup.checkedAction()
- if selectedAction is not None:
- self.__alignmentToolButton.setText(selectedAction.text())
- self.__alignmentToolButton.setIcon(selectedAction.icon())
- self.__alignmentToolButton.setToolTip(selectedAction.toolTip())
- else:
- self.__alignmentToolButton.setText("")
- self.__alignmentToolButton.setIcon(qt.QIcon())
- self.__alignmentToolButton.setToolTip("")
-
- def __keypointVisibilityChanged(self):
- """Called when action managing keypoints visibility changes"""
- widget = self.getCompareWidget()
- if widget is not None:
- keypointsVisible = self.__displayKeypoints.isChecked()
- widget.setKeypointsVisible(keypointsVisible)
+from .tools.compare.core import sift
+from .tools.compare.core import VisualizationMode
+from .tools.compare.core import AlignmentMode
+from .tools.compare.core import AffineTransformation
+from .tools.compare.toolbar import CompareImagesToolBar
+from .tools.compare.statusbar import CompareImagesStatusBar
+from .tools.compare.core import _CompareImageItem
-class CompareImagesStatusBar(qt.QStatusBar):
- """StatusBar containing specific information contained in a
- :class:`CompareImages` widget
-
- Use :meth:`setCompareWidget` to connect this toolbar to a specific
- :class:`CompareImages` widget.
-
- :param Union[qt.QWidget,None] parent: Parent of this widget.
- """
- def __init__(self, parent=None):
- qt.QStatusBar.__init__(self, parent)
- self.setSizeGripEnabled(False)
- self.layout().setSpacing(0)
- self.__compareWidget = None
- self._label1 = qt.QLabel(self)
- self._label1.setFrameShape(qt.QFrame.WinPanel)
- self._label1.setFrameShadow(qt.QFrame.Sunken)
- self._label2 = qt.QLabel(self)
- self._label2.setFrameShape(qt.QFrame.WinPanel)
- self._label2.setFrameShadow(qt.QFrame.Sunken)
- self._transform = qt.QLabel(self)
- self._transform.setFrameShape(qt.QFrame.WinPanel)
- self._transform.setFrameShadow(qt.QFrame.Sunken)
- self.addWidget(self._label1)
- self.addWidget(self._label2)
- self.addWidget(self._transform)
- self._pos = None
- self._updateStatusBar()
-
- def setCompareWidget(self, widget):
- """
- Connect this tool bar to a specific :class:`CompareImages` widget.
-
- :param Union[None,CompareImages] widget: The widget to connect with.
- """
- compareWidget = self.getCompareWidget()
- if compareWidget is not None:
- compareWidget.getPlot().sigPlotSignal.disconnect(self.__plotSignalReceived)
- compareWidget.sigConfigurationChanged.disconnect(self.__dataChanged)
- compareWidget = widget
- if compareWidget is None:
- self.__compareWidget = None
- else:
- self.__compareWidget = weakref.ref(compareWidget)
- if compareWidget is not None:
- compareWidget.getPlot().sigPlotSignal.connect(self.__plotSignalReceived)
- compareWidget.sigConfigurationChanged.connect(self.__dataChanged)
-
- def getCompareWidget(self):
- """Returns the connected widget.
-
- :rtype: CompareImages
- """
- if self.__compareWidget is None:
- return None
- else:
- return self.__compareWidget()
-
- def __plotSignalReceived(self, event):
- """Called when old style signals at emmited from the plot."""
- if event["event"] == "mouseMoved":
- x, y = event["x"], event["y"]
- self.__mouseMoved(x, y)
-
- def __mouseMoved(self, x, y):
- """Called when mouse move over the plot."""
- self._pos = x, y
- self._updateStatusBar()
-
- def __dataChanged(self):
- """Called when internal data from the connected widget changes."""
- self._updateStatusBar()
-
- def _formatData(self, data):
- """Format pixel of an image.
-
- It supports intensity, RGB, and RGBA.
-
- :param Union[int,float,numpy.ndarray,str]: Value of a pixel
- :rtype: str
- """
- if data is None:
- return "No data"
- if isinstance(data, (int, numpy.integer)):
- return "%d" % data
- if isinstance(data, (float, numpy.floating)):
- return "%f" % data
- if isinstance(data, numpy.ndarray):
- # RGBA value
- if data.shape == (3,):
- return "R:%d G:%d B:%d" % (data[0], data[1], data[2])
- elif data.shape == (4,):
- return "R:%d G:%d B:%d A:%d" % (data[0], data[1], data[2], data[3])
- _logger.debug("Unsupported data format %s. Cast it to string.", type(data))
- return str(data)
-
- def _updateStatusBar(self):
- """Update the content of the status bar"""
- widget = self.getCompareWidget()
- if widget is None:
- self._label1.setText("Image1: NA")
- self._label2.setText("Image2: NA")
- self._transform.setVisible(False)
- else:
- transform = widget.getTransformation()
- self._transform.setVisible(transform is not None)
- if transform is not None:
- has_notable_translation = not numpy.isclose(transform.tx, 0.0, atol=0.01) \
- or not numpy.isclose(transform.ty, 0.0, atol=0.01)
- has_notable_scale = not numpy.isclose(transform.sx, 1.0, atol=0.01) \
- or not numpy.isclose(transform.sy, 1.0, atol=0.01)
- has_notable_rotation = not numpy.isclose(transform.rot, 0.0, atol=0.01)
-
- strings = []
- if has_notable_translation:
- strings.append("Translation")
- if has_notable_scale:
- strings.append("Scale")
- if has_notable_rotation:
- strings.append("Rotation")
- if strings == []:
- has_translation = not numpy.isclose(transform.tx, 0.0) \
- or not numpy.isclose(transform.ty, 0.0)
- has_scale = not numpy.isclose(transform.sx, 1.0) \
- or not numpy.isclose(transform.sy, 1.0)
- has_rotation = not numpy.isclose(transform.rot, 0.0)
- if has_translation or has_scale or has_rotation:
- text = "No big changes"
- else:
- text = "No changes"
- else:
- text = "+".join(strings)
- self._transform.setText("Align: " + text)
-
- strings = []
- if not numpy.isclose(transform.ty, 0.0):
- strings.append("Translation x: %0.3fpx" % transform.tx)
- if not numpy.isclose(transform.ty, 0.0):
- strings.append("Translation y: %0.3fpx" % transform.ty)
- if not numpy.isclose(transform.sx, 1.0):
- strings.append("Scale x: %0.3f" % transform.sx)
- if not numpy.isclose(transform.sy, 1.0):
- strings.append("Scale y: %0.3f" % transform.sy)
- if not numpy.isclose(transform.rot, 0.0):
- strings.append("Rotation: %0.3fdeg" % (transform.rot * 180 / numpy.pi))
- if strings == []:
- text = "No transformation"
- else:
- text = "\n".join(strings)
- self._transform.setToolTip(text)
-
- if self._pos is None:
- self._label1.setText("Image1: NA")
- self._label2.setText("Image2: NA")
- else:
- data1, data2 = widget.getRawPixelData(self._pos[0], self._pos[1])
- if isinstance(data1, str):
- self._label1.setToolTip(data1)
- text1 = "NA"
- else:
- self._label1.setToolTip("")
- text1 = self._formatData(data1)
- if isinstance(data2, str):
- self._label2.setToolTip(data2)
- text2 = "NA"
- else:
- self._label2.setToolTip("")
- text2 = self._formatData(data2)
- self._label1.setText("Image1: %s" % text1)
- self._label2.setText("Image2: %s" % text2)
+_logger = logging.getLogger(__name__)
class CompareImages(qt.QMainWindow):
@@ -550,22 +75,28 @@ class CompareImages(qt.QMainWindow):
sigConfigurationChanged = qt.Signal()
"""Emitted when the configuration of the widget (visualization mode,
- alignement mode...) have changed."""
+ alignment mode...) have changed."""
def __init__(self, parent=None, backend=None):
qt.QMainWindow.__init__(self, parent)
self._resetZoomActive = True
self._colormap = Colormap()
"""Colormap shared by all modes, except the compose images (rgb image)"""
- self._colormapKeyPoints = Colormap('spring')
+ self._colormapKeyPoints = Colormap("spring")
"""Colormap used for sift keypoints"""
+ self._colormap.sigChanged.connect(self.__colormapChanged)
+
if parent is None:
- self.setWindowTitle('Compare images')
+ self.setWindowTitle("Compare images")
else:
self.setWindowFlags(qt.Qt.Widget)
self.__transformation = None
+ self.__item = _CompareImageItem()
+ self.__item.setName("_virtual")
+ self.__item.setColormap(self._colormap)
+
self.__raw1 = None
self.__raw2 = None
self.__data1 = None
@@ -574,35 +105,44 @@ class CompareImages(qt.QMainWindow):
self.__plot = plot.PlotWidget(parent=self, backend=backend)
self.__plot.setDefaultColormap(self._colormap)
- self.__plot.getXAxis().setLabel('Columns')
- self.__plot.getYAxis().setLabel('Rows')
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
+ self.__plot.getXAxis().setLabel("Columns")
+ self.__plot.getYAxis().setLabel("Rows")
+ if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward":
self.__plot.getYAxis().setInverted(True)
+ self.__plot.addItem(self.__item)
+ self.__plot.setActiveImage(self.__item)
self.__plot.setKeepDataAspectRatio(True)
self.__plot.sigPlotSignal.connect(self.__plotSlot)
self.__plot.setAxesDisplayed(False)
+ self.__scatter = Scatter()
+ self.__scatter.setZValue(1)
+ self.__scatter.setColormap(self._colormapKeyPoints)
+ self.__plot.addItem(self.__scatter)
+
self.setCentralWidget(self.__plot)
legend = VisualizationMode.VERTICAL_LINE.name
self.__plot.addXMarker(
- 0,
- legend=legend,
- text='',
- draggable=True,
- color='blue',
- constraint=WeakMethodProxy(self.__separatorConstraint))
+ 0,
+ legend=legend,
+ text="",
+ draggable=True,
+ color="blue",
+ constraint=WeakMethodProxy(self.__separatorConstraint),
+ )
self.__vline = self.__plot._getMarker(legend)
legend = VisualizationMode.HORIZONTAL_LINE.name
self.__plot.addYMarker(
- 0,
- legend=legend,
- text='',
- draggable=True,
- color='blue',
- constraint=WeakMethodProxy(self.__separatorConstraint))
+ 0,
+ legend=legend,
+ text="",
+ draggable=True,
+ color="blue",
+ constraint=WeakMethodProxy(self.__separatorConstraint),
+ )
self.__hline = self.__plot._getMarker(legend)
# default values
@@ -630,6 +170,26 @@ class CompareImages(qt.QMainWindow):
if self._statusBar is not None:
self.setStatusBar(self._statusBar)
+ def __getSealedColormap(self):
+ vrange = self._colormap.getColormapRange(
+ self.__item.getColormappedData(copy=False)
+ )
+ sealed = self._colormap.copy()
+ sealed.setVRange(*vrange)
+ return sealed
+
+ def __colormapChanged(self):
+ sealed = self.__getSealedColormap()
+ if self.__image1 is not None:
+ if self.__getImageMode(self.__image1.getData(copy=False)) == "intensity":
+ self.__image1.setColormap(sealed)
+ if self.__image2 is not None:
+ if self.__getImageMode(self.__image2.getData(copy=False)) == "intensity":
+ self.__image2.setColormap(sealed)
+
+ if "COMPOSITE" in self.__visualizationMode.name:
+ self.__updateData()
+
def _createStatusBar(self, plot):
self._statusBar = CompareImagesStatusBar(self)
self._statusBar.setCompareWidget(self)
@@ -644,6 +204,9 @@ class CompareImages(qt.QMainWindow):
toolBar.setCompareWidget(self)
self._compareToolBar = toolBar
+ def _getVirtualPlotItem(self):
+ return self.__item
+
def getPlot(self):
"""Returns the plot which is used to display the images.
@@ -676,10 +239,15 @@ class CompareImages(qt.QMainWindow):
It also could be a string containing information is some cases.
:rtype: Tuple(Union[int,float,numpy.ndarray,str],Union[int,float,numpy.ndarray,str])
"""
- data2 = None
alignmentMode = self.__alignmentMode
raw1, raw2 = self.__raw1, self.__raw2
- if alignmentMode == AlignmentMode.ORIGIN:
+
+ if raw1 is None or raw2 is None:
+ x1 = x
+ y1 = y
+ x2 = x
+ y2 = y
+ elif alignmentMode == AlignmentMode.ORIGIN:
x1 = x
y1 = y
x2 = x
@@ -700,22 +268,29 @@ class CompareImages(qt.QMainWindow):
x1 = x
y1 = y
# Not implemented
- data2 = "Not implemented with sift"
+ x2 = -1
+ y2 = -1
else:
- assert(False)
+ assert False
x1, y1 = int(x1), int(y1)
- if raw1 is None or y1 < 0 or y1 >= raw1.shape[0] or x1 < 0 or x1 >= raw1.shape[1]:
- data1 = None
+ x2, y2 = int(x2), int(y2)
+
+ if raw1 is None:
+ data1 = "No image A"
+ elif y1 < 0 or y1 >= raw1.shape[0] or x1 < 0 or x1 >= raw1.shape[1]:
+ data1 = ""
else:
data1 = raw1[y1, x1]
- if data2 is None:
- x2, y2 = int(x2), int(y2)
- if raw2 is None or y2 < 0 or y2 >= raw2.shape[0] or x2 < 0 or x2 >= raw2.shape[1]:
- data2 = None
- else:
- data2 = raw2[y2, x2]
+ if raw2 is None:
+ data2 = "No image B"
+ elif alignmentMode == AlignmentMode.AUTO:
+ data2 = "Not implemented with sift"
+ elif y2 < 0 or y2 >= raw2.shape[0] or x2 < 0 or x2 >= raw2.shape[1]:
+ data2 = None
+ else:
+ data2 = raw2[y2, x2]
return data1, data2
@@ -726,20 +301,31 @@ class CompareImages(qt.QMainWindow):
"""
if self.__visualizationMode == mode:
return
- previousMode = self.getVisualizationMode()
self.__visualizationMode = mode
- mode = self.getVisualizationMode()
+ self.__item.setVizualisationMode(mode)
self.__vline.setVisible(mode == VisualizationMode.VERTICAL_LINE)
self.__hline.setVisible(mode == VisualizationMode.HORIZONTAL_LINE)
- visModeRawDisplay = (VisualizationMode.ONLY_A,
- VisualizationMode.ONLY_B,
- VisualizationMode.VERTICAL_LINE,
- VisualizationMode.HORIZONTAL_LINE)
- updateColormap = not(previousMode in visModeRawDisplay and
- mode in visModeRawDisplay)
- self.__updateData(updateColormap=updateColormap)
+ self.__updateData()
self.sigConfigurationChanged.emit()
+ def centerLines(self):
+ """Center the line used to compare the 2 images."""
+ if self.__image1 is None:
+ return
+ data_range = self.__plot.getDataRange()
+
+ if data_range[0] is not None:
+ cx = (data_range[0][0] + data_range[0][1]) * 0.5
+ else:
+ cx = 0
+ if data_range[1] is not None:
+ cy = (data_range[1][0] + data_range[1][1]) * 0.5
+ else:
+ cy = 0
+ self.__vline.setPosition(cx, cy)
+ self.__hline.setPosition(cx, cy)
+ self.__updateSeparators()
+
def getVisualizationMode(self):
"""Returns the current interaction mode."""
return self.__visualizationMode
@@ -752,13 +338,17 @@ class CompareImages(qt.QMainWindow):
if self.__alignmentMode == mode:
return
self.__alignmentMode = mode
- self.__updateData(updateColormap=False)
+ self.__updateData()
self.sigConfigurationChanged.emit()
def getAlignmentMode(self):
"""Returns the current selected alignemnt mode."""
return self.__alignmentMode
+ def getKeypointsVisible(self):
+ """Returns true if the keypoints are displayed"""
+ return self.__keypointsVisible
+
def setKeypointsVisible(self, isVisible):
"""Set keypoints visibility.
@@ -776,16 +366,16 @@ class CompareImages(qt.QMainWindow):
def __plotSlot(self, event):
"""Handle events from the plot"""
- if event['event'] in ('markerMoving', 'markerMoved'):
+ if event["event"] in ("markerMoving", "markerMoved"):
mode = self.getVisualizationMode()
legend = mode.name
- if event['label'] == legend:
+ if event["label"] == legend:
if mode == VisualizationMode.VERTICAL_LINE:
- value = int(float(str(event['xdata'])))
+ value = int(float(str(event["xdata"])))
elif mode == VisualizationMode.HORIZONTAL_LINE:
- value = int(float(str(event['ydata'])))
+ value = int(float(str(event["ydata"])))
else:
- assert(False)
+ assert False
if self.__previousSeparatorPosition != value:
self.__separatorMoved(value)
self.__previousSeparatorPosition = value
@@ -807,8 +397,7 @@ class CompareImages(qt.QMainWindow):
return x, y
def __updateSeparators(self):
- """Redraw images according to the current state of the separators.
- """
+ """Redraw images according to the current state of the separators."""
mode = self.getVisualizationMode()
if mode == VisualizationMode.VERTICAL_LINE:
pos = self.__vline.getXPosition()
@@ -820,7 +409,8 @@ class CompareImages(qt.QMainWindow):
self.__previousSeparatorPosition = pos
else:
self.__image1.setOrigin((0, 0))
- self.__image2.setOrigin((0, 0))
+ if self.__image2 is not None:
+ self.__image2.setOrigin((0, 0))
def __separatorMoved(self, pos):
"""Called when vertical or horizontal separators have moved.
@@ -840,8 +430,9 @@ class CompareImages(qt.QMainWindow):
data1 = self.__data1[:, 0:pos]
data2 = self.__data2[:, pos:]
self.__image1.setData(data1, copy=False)
- self.__image2.setData(data2, copy=False)
- self.__image2.setOrigin((pos, 0))
+ if self.__image2 is not None:
+ self.__image2.setData(data2, copy=False)
+ self.__image2.setOrigin((pos, 0))
elif mode == VisualizationMode.HORIZONTAL_LINE:
pos = int(pos)
if pos <= 0:
@@ -851,150 +442,209 @@ class CompareImages(qt.QMainWindow):
data1 = self.__data1[0:pos, :]
data2 = self.__data2[pos:, :]
self.__image1.setData(data1, copy=False)
- self.__image2.setData(data2, copy=False)
- self.__image2.setOrigin((0, pos))
+ if self.__image2 is not None:
+ self.__image2.setData(data2, copy=False)
+ self.__image2.setOrigin((0, pos))
else:
- assert(False)
+ assert False
- def setData(self, image1, image2, updateColormap=True):
+ def clear(self):
+ self.setData(None, None)
+
+ def setData(self, image1, image2, updateColormap="deprecated"):
"""Set images to compare.
Images can contains floating-point or integer values, or RGB and RGBA
values, but should have comparable intensities.
RGB and RGBA images are provided as an array as `[width,height,channels]`
- of usigned integer 8-bits or floating-points between 0.0 to 1.0.
+ of unsigned integer 8-bits or floating-points between 0.0 to 1.0.
:param numpy.ndarray image1: The first image
:param numpy.ndarray image2: The second image
"""
+ if updateColormap != "deprecated":
+ deprecated_warning(
+ "Argument", "setData's updateColormap argument", since_version="2.0.0"
+ )
+
self.__raw1 = image1
self.__raw2 = image2
- self.__updateData(updateColormap=updateColormap)
+ self.__updateData()
if self.isAutoResetZoom():
self.__plot.resetZoom()
- def setImage1(self, image1, updateColormap=True):
+ def setImage1(self, image1, updateColormap="deprecated"):
"""Set image1 to be compared.
Images can contains floating-point or integer values, or RGB and RGBA
values, but should have comparable intensities.
RGB and RGBA images are provided as an array as `[width,height,channels]`
- of usigned integer 8-bits or floating-points between 0.0 to 1.0.
+ of unsigned integer 8-bits or floating-points between 0.0 to 1.0.
:param numpy.ndarray image1: The first image
"""
+ if updateColormap != "deprecated":
+ deprecated_warning(
+ "Argument", "setImage1's updateColormap argument", since_version="2.0.0"
+ )
+
self.__raw1 = image1
- self.__updateData(updateColormap=updateColormap)
+ self.__updateData()
if self.isAutoResetZoom():
self.__plot.resetZoom()
- def setImage2(self, image2, updateColormap=True):
+ def setImage2(self, image2, updateColormap="deprecated"):
"""Set image2 to be compared.
Images can contains floating-point or integer values, or RGB and RGBA
values, but should have comparable intensities.
RGB and RGBA images are provided as an array as `[width,height,channels]`
- of usigned integer 8-bits or floating-points between 0.0 to 1.0.
+ of unsigned integer 8-bits or floating-points between 0.0 to 1.0.
:param numpy.ndarray image2: The second image
"""
+ if updateColormap != "deprecated":
+ deprecated_warning(
+ "Argument", "setImage2's updateColormap argument", since_version="2.0.0"
+ )
+
self.__raw2 = image2
- self.__updateData(updateColormap=updateColormap)
+ self.__updateData()
if self.isAutoResetZoom():
self.__plot.resetZoom()
def __updateKeyPoints(self):
- """Update the displayed keypoints using cached keypoints.
- """
- if self.__keypointsVisible:
+ """Update the displayed keypoints using cached keypoints."""
+ if self.__keypointsVisible and self.__matching_keypoints:
data = self.__matching_keypoints
else:
data = [], [], []
- self.__plot.addScatter(x=data[0],
- y=data[1],
- z=1,
- value=data[2],
- colormap=self._colormapKeyPoints,
- legend="keypoints")
-
- def __updateData(self, updateColormap):
+ self.__scatter.setData(x=data[0], y=data[1], value=data[2])
+
+ def __updateData(self):
"""Compute aligned image when the alignment mode changes.
This function cache input images which are used when
vertical/horizontal separators moves.
"""
raw1, raw2 = self.__raw1, self.__raw2
- if raw1 is None or raw2 is None:
- return
alignmentMode = self.getAlignmentMode()
self.__transformation = None
- if alignmentMode == AlignmentMode.ORIGIN:
- yy = max(raw1.shape[0], raw2.shape[0])
- xx = max(raw1.shape[1], raw2.shape[1])
- size = yy, xx
- data1 = self.__createMarginImage(raw1, size, transparent=True)
- data2 = self.__createMarginImage(raw2, size, transparent=True)
- self.__matching_keypoints = [0.0], [0.0], [1.0]
- elif alignmentMode == AlignmentMode.CENTER:
- yy = max(raw1.shape[0], raw2.shape[0])
- xx = max(raw1.shape[1], raw2.shape[1])
- size = yy, xx
- data1 = self.__createMarginImage(raw1, size, transparent=True, center=True)
- data2 = self.__createMarginImage(raw2, size, transparent=True, center=True)
- self.__matching_keypoints = ([data1.shape[1] // 2],
- [data1.shape[0] // 2],
- [1.0])
- elif alignmentMode == AlignmentMode.STRETCH:
- data1 = raw1
- data2 = self.__rescaleImage(raw2, data1.shape)
- self.__matching_keypoints = ([0, data1.shape[1], data1.shape[1], 0],
- [0, 0, data1.shape[0], data1.shape[0]],
- [1.0, 1.0, 1.0, 1.0])
- elif alignmentMode == AlignmentMode.AUTO:
- # TODO: sift implementation do not support RGBA images
- yy = max(raw1.shape[0], raw2.shape[0])
- xx = max(raw1.shape[1], raw2.shape[1])
- size = yy, xx
- data1 = self.__createMarginImage(raw1, size)
- data2 = self.__createMarginImage(raw2, size)
- self.__matching_keypoints = [0.0], [0.0], [1.0]
- try:
- data1, data2 = self.__createSiftData(data1, data2)
- if data2 is None:
- raise ValueError("Unexpected None value")
- except Exception as e:
- # TODO: Display it on the GUI
- _logger.error(e)
- self.__setDefaultAlignmentMode()
- return
+ if raw1 is None or raw2 is None:
+ # No need to realign the 2 images
+ # But create a dummy image when there is None for simplification
+ if raw1 is None:
+ data1 = numpy.empty((0, 0))
+ else:
+ data1 = raw1
+ if raw2 is None:
+ data2 = numpy.empty((0, 0))
+ else:
+ data2 = raw2
+ self.__matching_keypoints = None
else:
- assert(False)
+ if alignmentMode == AlignmentMode.ORIGIN:
+ yy = max(raw1.shape[0], raw2.shape[0])
+ xx = max(raw1.shape[1], raw2.shape[1])
+ size = yy, xx
+ data1 = self.__createMarginImage(raw1, size, transparent=True)
+ data2 = self.__createMarginImage(raw2, size, transparent=True)
+ self.__matching_keypoints = [0.0], [0.0], [1.0]
+ elif alignmentMode == AlignmentMode.CENTER:
+ yy = max(raw1.shape[0], raw2.shape[0])
+ xx = max(raw1.shape[1], raw2.shape[1])
+ size = yy, xx
+ data1 = self.__createMarginImage(
+ raw1, size, transparent=True, center=True
+ )
+ data2 = self.__createMarginImage(
+ raw2, size, transparent=True, center=True
+ )
+ self.__matching_keypoints = (
+ [data1.shape[1] // 2],
+ [data1.shape[0] // 2],
+ [1.0],
+ )
+ elif alignmentMode == AlignmentMode.STRETCH:
+ data1 = raw1
+ data2 = self.__rescaleImage(raw2, data1.shape)
+ self.__matching_keypoints = (
+ [0, data1.shape[1], data1.shape[1], 0],
+ [0, 0, data1.shape[0], data1.shape[0]],
+ [1.0, 1.0, 1.0, 1.0],
+ )
+ elif alignmentMode == AlignmentMode.AUTO:
+ # TODO: sift implementation do not support RGBA images
+ yy = max(raw1.shape[0], raw2.shape[0])
+ xx = max(raw1.shape[1], raw2.shape[1])
+ size = yy, xx
+ data1 = self.__createMarginImage(raw1, size)
+ data2 = self.__createMarginImage(raw2, size)
+ self.__matching_keypoints = [0.0], [0.0], [1.0]
+ try:
+ data1, data2 = self.__createSiftData(data1, data2)
+ if data2 is None:
+ raise ValueError("Unexpected None value")
+ except Exception as e:
+ # TODO: Display it on the GUI
+ _logger.error(e)
+ self.__setDefaultAlignmentMode()
+ return
+ else:
+ assert False
+
+ self.__item.setImageData1(data1)
+ self.__item.setImageData2(data2)
mode = self.getVisualizationMode()
if mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG:
- data1 = self.__composeImage(data1, data2, mode)
- data2 = numpy.empty((0, 0))
+ data1 = self.__composeRgbImage(data1, data2, mode)
+ data2 = None
elif mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY:
- data1 = self.__composeImage(data1, data2, mode)
- data2 = numpy.empty((0, 0))
+ data1 = self.__composeRgbImage(data1, data2, mode)
+ data2 = None
elif mode == VisualizationMode.COMPOSITE_A_MINUS_B:
- data1 = self.__composeImage(data1, data2, mode)
- data2 = numpy.empty((0, 0))
+ data1 = self.__composeAMinusBImage(data1, data2)
+ data2 = None
elif mode == VisualizationMode.ONLY_A:
- data2 = numpy.empty((0, 0))
+ data2 = None
elif mode == VisualizationMode.ONLY_B:
data1 = numpy.empty((0, 0))
self.__data1, self.__data2 = data1, data2
- self.__plot.addImage(data1, z=0, legend="image1", resetzoom=False)
- self.__plot.addImage(data2, z=0, legend="image2", resetzoom=False)
+
+ colormap = self.__getSealedColormap()
+ mode1 = self.__getImageMode(self.__data1)
+ if mode1 == "intensity":
+ colormap1 = colormap
+ else:
+ colormap1 = None
+ self.__plot.addImage(
+ data1, z=0, legend="image1", resetzoom=False, colormap=colormap1
+ )
self.__image1 = self.__plot.getImage("image1")
- self.__image2 = self.__plot.getImage("image2")
+
+ if data2 is not None:
+ mode2 = self.__getImageMode(data2)
+ if mode2 == "intensity":
+ colormap2 = colormap
+ else:
+ colormap2 = None
+ self.__plot.addImage(
+ data2, z=0, legend="image2", resetzoom=False, colormap=colormap2
+ )
+ self.__image2 = self.__plot.getImage("image2")
+ self.__image2.setVisible(True)
+ else:
+ if self.__image2 is not None:
+ self.__image2.setVisible(False)
+ self.__image2 = None
+ self.__data2 = numpy.empty((0, 0))
self.__updateKeyPoints()
# Set the separator into the middle
@@ -1004,27 +654,6 @@ class CompareImages(qt.QMainWindow):
value = self.__data1.shape[0] // 2
self.__hline.setPosition(0, value)
self.__updateSeparators()
- if updateColormap:
- self.__updateColormap()
-
- def __updateColormap(self):
- # TODO: The colormap histogram will still be wrong
- mode1 = self.__getImageMode(self.__data1)
- mode2 = self.__getImageMode(self.__data2)
- if mode1 == "intensity" and mode1 == mode2:
- if self.__data1.size == 0:
- vmin = self.__data2.min()
- vmax = self.__data2.max()
- elif self.__data2.size == 0:
- vmin = self.__data1.min()
- vmax = self.__data1.max()
- else:
- vmin = min(self.__data1.min(), self.__data2.min())
- vmax = max(self.__data1.max(), self.__data2.max())
- colormap = self.getColormap()
- colormap.setVRange(vmin=vmin, vmax=vmax)
- self.__image1.setColormap(colormap)
- self.__image2.setColormap(colormap)
def __getImageMode(self, image):
"""Returns a value identifying the way the image is stored in the
@@ -1060,62 +689,117 @@ class CompareImages(qt.QMainWindow):
data[:, :, c] = self.__rescaleArray(image[:, :, c], shape)
return data
- def __composeImage(self, data1, data2, mode):
+ def __composeRgbImage(self, data1, data2, mode):
"""Returns an RBG image containing composition of data1 and data2 in 2
different channels
+ A data image of a size of 0 is considered as missing. This does not
+ interrupt the processing.
+
:param numpy.ndarray data1: First image
:param numpy.ndarray data1: Second image
:param VisualizationMode mode: Composition mode.
:rtype: numpy.ndarray
"""
- assert(data1.shape[0:2] == data2.shape[0:2])
- if mode == VisualizationMode.COMPOSITE_A_MINUS_B:
- # TODO: this calculation has no interest of generating a 'composed'
- # rgb image, this could be moved in an other function or doc
- # should be modified
- _type = data1.dtype
- result = data1.astype(numpy.float64) - data2.astype(numpy.float64)
- return result
- mode1 = self.__getImageMode(data1)
- if mode1 in ["rgb", "rgba"]:
- intensity1 = self.__luminosityImage(data1)
- vmin1, vmax1 = 0.0, 1.0
+ if data1.size != 0 and data2.size != 0:
+ assert data1.shape[0:2] == data2.shape[0:2]
+
+ sealed = self.__getSealedColormap()
+ vmin, vmax = sealed.getVRange()
+
+ if data1.size == 0:
+ intensity1 = numpy.zeros(data2.shape[0:2])
else:
- intensity1 = data1
- vmin1, vmax1 = data1.min(), data1.max()
+ mode1 = self.__getImageMode(data1)
+ if mode1 in ["rgb", "rgba"]:
+ intensity1 = self.__luminosityImage(data1)
+ else:
+ intensity1 = data1
- mode2 = self.__getImageMode(data2)
- if mode2 in ["rgb", "rgba"]:
- intensity2 = self.__luminosityImage(data2)
- vmin2, vmax2 = 0.0, 1.0
+ if data2.size == 0:
+ intensity2 = numpy.zeros(data1.shape[0:2])
else:
- intensity2 = data2
- vmin2, vmax2 = data2.min(), data2.max()
+ mode2 = self.__getImageMode(data2)
+ if mode2 in ["rgb", "rgba"]:
+ intensity2 = self.__luminosityImage(data2)
+ else:
+ intensity2 = data2
- vmin, vmax = min(vmin1, vmin2) * 1.0, max(vmax1, vmax2) * 1.0
- shape = data1.shape
+ shape = intensity1.shape
result = numpy.empty((shape[0], shape[1], 3), dtype=numpy.uint8)
- a = (intensity1 - vmin) * (1.0 / (vmax - vmin)) * 255.0
- b = (intensity2 - vmin) * (1.0 / (vmax - vmin)) * 255.0
+ a, _, _ = normalize(
+ intensity1,
+ norm=sealed.getNormalization(),
+ autoscale=sealed.getAutoscaleMode(),
+ vmin=sealed.getVMin(),
+ vmax=sealed.getVMax(),
+ gamma=sealed.getGammaNormalizationParameter(),
+ )
+ b, _, _ = normalize(
+ intensity2,
+ norm=sealed.getNormalization(),
+ autoscale=sealed.getAutoscaleMode(),
+ vmin=sealed.getVMin(),
+ vmax=sealed.getVMax(),
+ gamma=sealed.getGammaNormalizationParameter(),
+ )
if mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY:
result[:, :, 0] = a
- result[:, :, 1] = (a + b) / 2
+ result[:, :, 1] = a // 2 + b // 2
result[:, :, 2] = b
elif mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG:
result[:, :, 0] = 255 - b
- result[:, :, 1] = 255 - (a + b) / 2
+ result[:, :, 1] = 255 - (a // 2 + b // 2)
result[:, :, 2] = 255 - a
return result
- def __luminosityImage(self, image):
+ def __composeAMinusBImage(self, data1, data2):
+ """Returns an intensity image containing the composition of `A-B`.
+
+ A data image of a size of 0 is considered as missing. This does not
+ interrupt the processing.
+
+ :param numpy.ndarray data1: First image
+ :param numpy.ndarray data1: Second image
+ :rtype: numpy.ndarray
+ """
+ if data1.size != 0 and data2.size != 0:
+ assert data1.shape[0:2] == data2.shape[0:2]
+
+ data1 = self.__asIntensityImage(data1)
+ data2 = self.__asIntensityImage(data2)
+ if data1.size == 0:
+ result = data2
+ elif data2.size == 0:
+ result = data1
+ else:
+ result = data1.astype(numpy.float32) - data2.astype(numpy.float32)
+ return result
+
+ def __asIntensityImage(self, image: numpy.ndarray):
+ """Returns an intensity image.
+
+ If the image use a single channel, it will be returned as it is.
+
+ If the image is an RBG(A) image, the luminosity (0..1) is extracted and
+ returned. The alpha channel is ignored.
+
+ :rtype: numpy.ndarray
+ """
+ mode = self.__getImageMode(image)
+ if mode in ["rgb", "rgba"]:
+ return self.__luminosityImage(image)
+ return image
+
+ def __luminosityImage(self, image: numpy.ndarray):
"""Returns the luminosity channel from an RBG(A) image.
+
The alpha channel is ignored.
:rtype: numpy.ndarray
"""
mode = self.__getImageMode(image)
- assert(mode in ["rgb", "rgba"])
+ assert mode in ["rgb", "rgba"]
is_uint8 = image.dtype.type == numpy.uint8
# luminosity
image = 0.21 * image[..., 0] + 0.72 * image[..., 1] + 0.07 * image[..., 2]
@@ -1128,8 +812,10 @@ class CompareImages(qt.QMainWindow):
:rtype: numpy.ndarray
"""
- y, x = numpy.ogrid[:shape[0], :shape[1]]
- y, x = y * 1.0 * (image.shape[0] - 1) / (shape[0] - 1), x * 1.0 * (image.shape[1] - 1) / (shape[1] - 1)
+ y, x = numpy.ogrid[: shape[0], : shape[1]]
+ y, x = y * 1.0 * (image.shape[0] - 1) / (shape[0] - 1), x * 1.0 * (
+ image.shape[1] - 1
+ ) / (shape[1] - 1)
b = silx.image.bilinear.BilinearImage(image)
# TODO: could be optimized using strides
x2d = numpy.zeros_like(y) + x
@@ -1142,8 +828,8 @@ class CompareImages(qt.QMainWindow):
:rtype: numpy.ndarray
"""
- assert(image.shape[0] <= size[0])
- assert(image.shape[1] <= size[1])
+ assert image.shape[0] <= size[0]
+ assert image.shape[1] <= size[1]
if image.shape == size:
return image
mode = self.__getImageMode(image)
@@ -1156,7 +842,7 @@ class CompareImages(qt.QMainWindow):
if mode == "intensity":
data = numpy.zeros(size, dtype=image.dtype)
- data[pos0:pos0 + image.shape[0], pos1:pos1 + image.shape[1]] = image
+ data[pos0 : pos0 + image.shape[0], pos1 : pos1 + image.shape[1]] = image
# TODO: It is maybe possible to put NaN on the margin
else:
if transparent:
@@ -1164,9 +850,13 @@ class CompareImages(qt.QMainWindow):
else:
data = numpy.zeros((size[0], size[1], 3), dtype=numpy.uint8)
depth = min(data.shape[2], image.shape[2])
- data[pos0:pos0 + image.shape[0], pos1:pos1 + image.shape[1], 0:depth] = image[:, :, 0:depth]
+ data[
+ pos0 : pos0 + image.shape[0], pos1 : pos1 + image.shape[1], 0:depth
+ ] = image[:, :, 0:depth]
if transparent and depth == 3:
- data[pos0:pos0 + image.shape[0], pos1:pos1 + image.shape[1], 3] = 255
+ data[
+ pos0 : pos0 + image.shape[0], pos1 : pos1 + image.shape[1], 3
+ ] = 255
return data
def __toAffineTransformation(self, sift_result):
@@ -1190,7 +880,7 @@ class CompareImages(qt.QMainWindow):
return AffineTransformation(tx, ty, sx, sy, rot)
def getTransformation(self):
- """Retuns the affine transformation applied to the second image to align
+ """Returns the affine transformation applied to the second image to align
it to the first image.
This result is only valid for sift alignment.
@@ -1219,9 +909,11 @@ class CompareImages(qt.QMainWindow):
_logger.info("Number of Keypoints within image 1: %i" % keypoints.size)
_logger.info(" within image 2: %i" % second_keypoints.size)
- self.__matching_keypoints = (match[:].x[:, 0],
- match[:].y[:, 0],
- match[:].scale[:, 0])
+ self.__matching_keypoints = (
+ match[:].x[:, 0],
+ match[:].y[:, 0],
+ match[:].scale[:, 0],
+ )
matching_keypoints = match.shape[0]
_logger.info("Matching keypoints: %i" % matching_keypoints)
if matching_keypoints == 0:
@@ -1241,6 +933,10 @@ class CompareImages(qt.QMainWindow):
self.__transformation = self.__toAffineTransformation(result)
return data1, data2
+ def resetZoom(self, dataMargins=None):
+ """Reset the plot limits to the bounds of the data and redraw the plot."""
+ self.__plot.resetZoom(dataMargins)
+
def setAutoResetZoom(self, activate=True):
"""
diff --git a/src/silx/gui/plot/ComplexImageView.py b/src/silx/gui/plot/ComplexImageView.py
index 7febd19..654a1c1 100644
--- a/src/silx/gui/plot/ComplexImageView.py
+++ b/src/silx/gui/plot/ComplexImageView.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -33,10 +33,8 @@ __date__ = "24/04/2018"
import logging
-import collections
import numpy
-from ...utils.deprecation import deprecated
from .. import qt, icons
from .PlotWindow import Plot2D
from . import items
@@ -48,6 +46,7 @@ _logger = logging.getLogger(__name__)
# Widgets
+
class _AmplitudeRangeDialog(qt.QDialog):
"""QDialog asking for the amplitude range to display."""
@@ -57,12 +56,9 @@ class _AmplitudeRangeDialog(qt.QDialog):
It provides the new range as a 2-tuple: (max, delta)
"""
- def __init__(self,
- parent=None,
- amplitudeRange=None,
- displayedRange=(None, 2)):
+ def __init__(self, parent=None, amplitudeRange=None, displayedRange=(None, 2)):
super(_AmplitudeRangeDialog, self).__init__(parent)
- self.setWindowTitle('Set Displayed Amplitude Range')
+ self.setWindowTitle("Set Displayed Amplitude Range")
if amplitudeRange is not None:
amplitudeRange = min(amplitudeRange), max(amplitudeRange)
@@ -74,25 +70,24 @@ class _AmplitudeRangeDialog(qt.QDialog):
if self._amplitudeRange is not None:
min_, max_ = self._amplitudeRange
- layout.addRow(
- qt.QLabel('Data Amplitude Range: [%g, %g]' % (min_, max_)))
+ layout.addRow(qt.QLabel("Data Amplitude Range: [%g, %g]" % (min_, max_)))
self._maxLineEdit = FloatEdit(parent=self)
- self._maxLineEdit.validator().setBottom(0.)
+ self._maxLineEdit.validator().setBottom(0.0)
self._maxLineEdit.setAlignment(qt.Qt.AlignRight)
self._maxLineEdit.editingFinished.connect(self._rangeUpdated)
- layout.addRow('Displayed Max.:', self._maxLineEdit)
+ layout.addRow("Displayed Max.:", self._maxLineEdit)
- self._autoscale = qt.QCheckBox('autoscale')
+ self._autoscale = qt.QCheckBox("autoscale")
self._autoscale.toggled.connect(self._autoscaleCheckBoxToggled)
- layout.addRow('', self._autoscale)
+ layout.addRow("", self._autoscale)
self._deltaLineEdit = FloatEdit(parent=self)
- self._deltaLineEdit.validator().setBottom(1.)
+ self._deltaLineEdit.validator().setBottom(1.0)
self._deltaLineEdit.setAlignment(qt.Qt.AlignRight)
self._deltaLineEdit.editingFinished.connect(self._rangeUpdated)
- layout.addRow('Displayed delta (log10 unit):', self._deltaLineEdit)
+ layout.addRow("Displayed delta (log10 unit):", self._deltaLineEdit)
buttons = qt.QDialogButtonBox(self)
buttons.addButton(qt.QDialogButtonBox.Ok)
@@ -107,8 +102,7 @@ class _AmplitudeRangeDialog(qt.QDialog):
self.rejected.connect(self._handleRejected)
def _resetDialogToDefault(self):
- """Set Widgets of the dialog from range information
- """
+ """Set Widgets of the dialog from range information"""
max_, delta = self._defaultDisplayedRange
if max_ is not None: # Not in autoscale
@@ -116,7 +110,7 @@ class _AmplitudeRangeDialog(qt.QDialog):
elif self._amplitudeRange is not None: # Autoscale with data
displayedMax = self._amplitudeRange[1]
else: # Autoscale without data
- displayedMax = ''
+ displayedMax = ""
if displayedMax == "":
self._maxLineEdit.setText("")
else:
@@ -149,7 +143,7 @@ class _AmplitudeRangeDialog(qt.QDialog):
"""Handle autoscale checkbox state changes"""
if checked: # Use default values
if self._amplitudeRange is None:
- max_ = ''
+ max_ = ""
else:
max_ = self._amplitudeRange[1]
if max_ == "":
@@ -167,21 +161,31 @@ class _ComplexDataToolButton(qt.QToolButton):
:param plot: The :class:`ComplexImageView` to control
"""
- _MODES = collections.OrderedDict([
- (ImageComplexData.ComplexMode.ABSOLUTE, ('math-amplitude', 'Amplitude')),
- (ImageComplexData.ComplexMode.SQUARE_AMPLITUDE,
- ('math-square-amplitude', 'Square amplitude')),
- (ImageComplexData.ComplexMode.PHASE, ('math-phase', 'Phase')),
- (ImageComplexData.ComplexMode.REAL, ('math-real', 'Real part')),
- (ImageComplexData.ComplexMode.IMAGINARY,
- ('math-imaginary', 'Imaginary part')),
- (ImageComplexData.ComplexMode.AMPLITUDE_PHASE,
- ('math-phase-color', 'Amplitude and Phase')),
- (ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE,
- ('math-phase-color-log', 'Log10(Amp.) and Phase'))
- ])
-
- _RANGE_DIALOG_TEXT = 'Set Amplitude Range...'
+ _MODES = dict(
+ [
+ (ImageComplexData.ComplexMode.ABSOLUTE, ("math-amplitude", "Amplitude")),
+ (
+ ImageComplexData.ComplexMode.SQUARE_AMPLITUDE,
+ ("math-square-amplitude", "Square amplitude"),
+ ),
+ (ImageComplexData.ComplexMode.PHASE, ("math-phase", "Phase")),
+ (ImageComplexData.ComplexMode.REAL, ("math-real", "Real part")),
+ (
+ ImageComplexData.ComplexMode.IMAGINARY,
+ ("math-imaginary", "Imaginary part"),
+ ),
+ (
+ ImageComplexData.ComplexMode.AMPLITUDE_PHASE,
+ ("math-phase-color", "Amplitude and Phase"),
+ ),
+ (
+ ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE,
+ ("math-phase-color-log", "Log10(Amp.) and Phase"),
+ ),
+ ]
+ )
+
+ _RANGE_DIALOG_TEXT = "Set Amplitude Range..."
def __init__(self, parent=None, plot=None):
super(_ComplexDataToolButton, self).__init__(parent=parent)
@@ -207,16 +211,16 @@ class _ComplexDataToolButton(qt.QToolButton):
self.setPopupMode(qt.QToolButton.InstantPopup)
self._modeChanged(self._plot2DComplex.getComplexMode())
- self._plot2DComplex.sigVisualizationModeChanged.connect(
- self._modeChanged)
+ self._plot2DComplex.sigVisualizationModeChanged.connect(self._modeChanged)
def _modeChanged(self, mode):
"""Handle change of visualization modes"""
icon, text = self._MODES[mode]
self.setIcon(icons.getQIcon(icon))
- self.setToolTip('Display the ' + text.lower())
+ self.setToolTip("Display the " + text.lower())
self._rangeDialogAction.setEnabled(
- mode == ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE)
+ mode == ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE
+ )
def _triggered(self, action):
"""Handle triggering of menu actions"""
@@ -236,7 +240,8 @@ class _ComplexDataToolButton(qt.QToolButton):
dialog = _AmplitudeRangeDialog(
parent=self,
amplitudeRange=dataRange,
- displayedRange=self._plot2DComplex._getAmplitudeRangeInfo())
+ displayedRange=self._plot2DComplex._getAmplitudeRangeInfo(),
+ )
dialog.sigRangeChanged.connect(self._rangeChanged)
dialog.exec()
dialog.sigRangeChanged.disconnect(self._rangeChanged)
@@ -272,7 +277,7 @@ class ComplexImageView(qt.QWidget):
def __init__(self, parent=None):
super(ComplexImageView, self).__init__(parent)
if parent is None:
- self.setWindowTitle('ComplexImageView')
+ self.setWindowTitle("ComplexImageView")
self._plot2D = Plot2D(self)
@@ -284,14 +289,13 @@ class ComplexImageView(qt.QWidget):
# Create and add image to the plot
self._plotImage = ImageComplexData()
- self._plotImage.setName('__ComplexImageView__complex_image__')
+ self._plotImage.setName("__ComplexImageView__complex_image__")
self._plotImage.sigItemChanged.connect(self._itemChanged)
self._plot2D.addItem(self._plotImage)
- self._plot2D.setActiveImage(self._plotImage.getName())
+ self._plot2D.setActiveImage(self._plotImage)
- toolBar = qt.QToolBar('Complex', self)
- toolBar.addWidget(
- _ComplexDataToolButton(parent=self, plot=self))
+ toolBar = qt.QToolBar("Complex", self)
+ toolBar.addWidget(_ComplexDataToolButton(parent=self, plot=self))
self._plot2D.insertToolBar(self._plot2D.getProfileToolbar(), toolBar)
@@ -344,8 +348,10 @@ class ComplexImageView(qt.QWidget):
:rtype: numpy.ndarray of float with 2 dims or RGBA image (uint8).
"""
mode = self.getComplexMode()
- if mode in (self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
+ if mode in (
+ self.ComplexMode.AMPLITUDE_PHASE,
+ self.ComplexMode.LOG10_AMPLITUDE_PHASE,
+ ):
return self._plotImage.getRgbaImageData(copy=copy)
else:
return self._plotImage.getData(copy=copy)
@@ -354,19 +360,6 @@ class ComplexImageView(qt.QWidget):
Mode = ComplexMode
- @classmethod
- @deprecated(replacement='supportedComplexModes', since_version='0.11.0')
- def getSupportedVisualizationModes(cls):
- return cls.supportedComplexModes()
-
- @deprecated(replacement='setComplexMode', since_version='0.11.0')
- def setVisualizationMode(self, mode):
- return self.setComplexMode(mode)
-
- @deprecated(replacement='getComplexMode', since_version='0.11.0')
- def getVisualizationMode(self):
- return self.getComplexMode()
-
# Image item proxy
@staticmethod
@@ -490,7 +483,7 @@ class ComplexImageView(qt.QWidget):
:rtype: :class:`.items.Axis`
"""
- return self.getPlot().getYAxis(axis='left')
+ return self.getPlot().getYAxis(axis="left")
def getGraphTitle(self):
"""Return the plot main title as a str."""
diff --git a/src/silx/gui/plot/CurvesROIWidget.py b/src/silx/gui/plot/CurvesROIWidget.py
index f0cc7f3..bd47da0 100644
--- a/src/silx/gui/plot/CurvesROIWidget.py
+++ b/src/silx/gui/plot/CurvesROIWidget.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -32,14 +32,12 @@ __authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
__license__ = "MIT"
__date__ = "13/03/2018"
-from collections import OrderedDict
import logging
import os
import sys
import functools
import numpy
from silx.io import dictdump
-from silx.utils import deprecation
from silx.utils.weakref import WeakMethodProxy
from silx.utils.proxy import docstring
from .. import icons, qt
@@ -107,8 +105,7 @@ class CurvesROIWidget(qt.QWidget):
layout.addWidget(self.headerLabel)
widgetAllCheckbox = qt.QWidget(parent=self)
- self._showAllCheckBox = qt.QCheckBox("show all ROI",
- parent=widgetAllCheckbox)
+ self._showAllCheckBox = qt.QCheckBox("show all ROI", parent=widgetAllCheckbox)
widgetAllCheckbox.setLayout(qt.QHBoxLayout())
spacer = qt.QWidget(parent=widgetAllCheckbox)
spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
@@ -132,14 +129,15 @@ class CurvesROIWidget(qt.QWidget):
self.addButton = qt.QPushButton(hbox)
self.addButton.setText("Add ROI")
- self.addButton.setToolTip('Create a new ROI')
+ self.addButton.setToolTip("Create a new ROI")
self.delButton = qt.QPushButton(hbox)
self.delButton.setText("Delete ROI")
- self.addButton.setToolTip('Remove the selected ROI')
+ self.addButton.setToolTip("Remove the selected ROI")
self.resetButton = qt.QPushButton(hbox)
self.resetButton.setText("Reset")
- self.addButton.setToolTip('Clear all created ROIs. We only let the '
- 'default ROI')
+ self.addButton.setToolTip(
+ "Clear all created ROIs. We only let the " "default ROI"
+ )
hboxlayout.addWidget(self.addButton)
hboxlayout.addWidget(self.delButton)
@@ -149,10 +147,10 @@ class CurvesROIWidget(qt.QWidget):
self.loadButton = qt.QPushButton(hbox)
self.loadButton.setText("Load")
- self.loadButton.setToolTip('Load ROIs from a .ini file')
+ self.loadButton.setToolTip("Load ROIs from a .ini file")
self.saveButton = qt.QPushButton(hbox)
self.saveButton.setText("Save")
- self.loadButton.setToolTip('Save ROIs to a .ini file')
+ self.loadButton.setToolTip("Save ROIs to a .ini file")
hboxlayout.addWidget(self.loadButton)
hboxlayout.addWidget(self.saveButton)
layout.setStretchFactor(self.headerLabel, 0)
@@ -210,6 +208,7 @@ class CurvesROIWidget(qt.QWidget):
def _add(self):
"""Add button clicked handler"""
+
def getNextRoiName():
rois = self.roiTable.getRois(order=None)
roisNames = []
@@ -224,6 +223,7 @@ class CurvesROIWidget(qt.QWidget):
i += 1
newroi = "newroi %d" % i
return newroi
+
roi = ROI(name=getNextRoiName())
if roi.getName() == "ICR":
@@ -242,9 +242,9 @@ class CurvesROIWidget(qt.QWidget):
# back compatibility pymca roi signals
ddict = {}
- ddict['event'] = "AddROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
+ ddict["event"] = "AddROI"
+ ddict["roilist"] = self.roiTable.roidict.values()
+ ddict["roidict"] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
# end back compatibility pymca roi signals
@@ -254,9 +254,9 @@ class CurvesROIWidget(qt.QWidget):
# back compatibility pymca roi signals
ddict = {}
- ddict['event'] = "DelROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
+ ddict["event"] = "DelROI"
+ ddict["roilist"] = self.roiTable.roidict.values()
+ ddict["roidict"] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
# end back compatibility pymca roi signals
@@ -269,17 +269,16 @@ class CurvesROIWidget(qt.QWidget):
# back compatibility pymca roi signals
ddict = {}
- ddict['event'] = "ResetROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
+ ddict["event"] = "ResetROI"
+ ddict["roilist"] = self.roiTable.roidict.values()
+ ddict["roidict"] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
# end back compatibility pymca roi signals
def _load(self):
"""Load button clicked handler"""
dialog = qt.QFileDialog(self)
- dialog.setNameFilters(
- ['INI File *.ini', 'JSON File *.json', 'All *.*'])
+ dialog.setNameFilters(["INI File *.ini", "JSON File *.json", "All *.*"])
dialog.setFileMode(qt.QFileDialog.ExistingFile)
dialog.setDirectory(self.roiFileDir)
if not dialog.exec():
@@ -295,9 +294,9 @@ class CurvesROIWidget(qt.QWidget):
# back compatibility pymca roi signals
ddict = {}
- ddict['event'] = "LoadROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
+ ddict["event"] = "LoadROI"
+ ddict["roilist"] = self.roiTable.roidict.values()
+ ddict["roidict"] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
# end back compatibility pymca roi signals
@@ -311,7 +310,7 @@ class CurvesROIWidget(qt.QWidget):
def _save(self):
"""Save button clicked handler"""
dialog = qt.QFileDialog(self)
- dialog.setNameFilters(['INI File *.ini', 'JSON File *.json'])
+ dialog.setNameFilters(["INI File *.ini", "JSON File *.json"])
dialog.setFileMode(qt.QFileDialog.AnyFile)
dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
dialog.setDirectory(self.roiFileDir)
@@ -320,7 +319,7 @@ class CurvesROIWidget(qt.QWidget):
return
outputFile = dialog.selectedFiles()[0]
- extension = '.' + dialog.selectedNameFilter().split('.')[-1]
+ extension = "." + dialog.selectedNameFilter().split(".")[-1]
dialog.close()
if not outputFile.endswith(extension):
@@ -345,16 +344,10 @@ class CurvesROIWidget(qt.QWidget):
"""
self.roiTable.save(filename)
- def setHeader(self, text='ROIs'):
+ def setHeader(self, text="ROIs"):
"""Set the header text of this widget"""
self.headerLabel.setText("<b>%s<\b>" % text)
- @deprecation.deprecated(replacement="calculateRois",
- reason="CamelCase convention",
- since_version="0.7")
- def calculateROIs(self, *args, **kw):
- self.calculateRois(*args, **kw)
-
def calculateRois(self, roiList=None, roiDict=None):
"""Compute ROI information"""
return self.roiTable.calculateRois()
@@ -367,7 +360,7 @@ class CurvesROIWidget(qt.QWidget):
plot = self.getPlotWidget()
curves = () if plot is None else plot.getAllCurves()
if not curves:
- return 1.0, 1.0, 100., 100.
+ return 1.0, 1.0, 100.0, 100.0
xmin, ymin = None, None
xmax, ymax = None, None
@@ -420,12 +413,12 @@ class CurvesROIWidget(qt.QWidget):
def _emitCurrentROISignal(self):
ddict = {}
- ddict['event'] = "currentROISignal"
+ ddict["event"] = "currentROISignal"
if self.roiTable.activeRoi is not None:
- ddict['ROI'] = self.roiTable.activeRoi.toDict()
- ddict['current'] = self.roiTable.activeRoi.getName()
+ ddict["ROI"] = self.roiTable.activeRoi.toDict()
+ ddict["current"] = self.roiTable.activeRoi.getName()
else:
- ddict['current'] = None
+ ddict["current"] = None
if self.__lastSigROISignal != ddict:
self.__lastSigROISignal = ddict
@@ -440,13 +433,14 @@ class _FloatItem(qt.QTableWidgetItem):
"""
Simple QTableWidgetItem overloading the < operator to deal with ordering
"""
+
def __init__(self):
qt.QTableWidgetItem.__init__(self, type=qt.QTableWidgetItem.Type)
def __lt__(self, other):
- if self.text() in ('', ROITable.INFO_NOT_FOUND):
+ if self.text() in ("", ROITable.INFO_NOT_FOUND):
return False
- if other.text() in ('', ROITable.INFO_NOT_FOUND):
+ if other.text() in ("", ROITable.INFO_NOT_FOUND):
return True
return float(self.text()) < float(other.text())
@@ -464,21 +458,23 @@ class ROITable(TableWidget):
"""Signal emitted when the active roi changed or when the value of the
active roi are changing"""
- COLUMNS_INDEX = OrderedDict([
- ('ID', 0),
- ('ROI', 1),
- ('Type', 2),
- ('From', 3),
- ('To', 4),
- ('Raw Counts', 5),
- ('Net Counts', 6),
- ('Raw Area', 7),
- ('Net Area', 8),
- ])
+ COLUMNS_INDEX = dict(
+ [
+ ("ID", 0),
+ ("ROI", 1),
+ ("Type", 2),
+ ("From", 3),
+ ("To", 4),
+ ("Raw Counts", 5),
+ ("Net Counts", 6),
+ ("Raw Area", 7),
+ ("Net Area", 8),
+ ]
+ )
COLUMNS = list(COLUMNS_INDEX.keys())
- INFO_NOT_FOUND = '????????'
+ INFO_NOT_FOUND = "????????"
def __init__(self, parent=None, plot=None, rois=None):
super(ROITable, self).__init__(parent)
@@ -529,26 +525,32 @@ class ROITable(TableWidget):
header = self.horizontalHeader()
header.setSectionResizeMode(qt.QHeaderView.ResizeToContents)
self.sortByColumn(0, qt.Qt.AscendingOrder)
- self.hideColumn(self.COLUMNS_INDEX['ID'])
+ self.hideColumn(self.COLUMNS_INDEX["ID"])
def setPlot(self, plot):
self.clear()
self.plot = plot
def __setTooltip(self):
- self.horizontalHeaderItem(self.COLUMNS_INDEX['ROI']).setToolTip(
- 'Region of interest identifier')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['Type']).setToolTip(
- 'Type of the ROI')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['From']).setToolTip(
- 'X-value of the min point')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['To']).setToolTip(
- 'X-value of the max point')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['Raw Counts']).setToolTip(
- 'Estimation of the integral between y=0 and the selected curve')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['Net Counts']).setToolTip(
- 'Estimation of the integral between the segment [maxPt, minPt] '
- 'and the selected curve')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["ROI"]).setToolTip(
+ "Region of interest identifier"
+ )
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["Type"]).setToolTip(
+ "Type of the ROI"
+ )
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["From"]).setToolTip(
+ "X-value of the min point"
+ )
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["To"]).setToolTip(
+ "X-value of the max point"
+ )
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["Raw Counts"]).setToolTip(
+ "Estimation of the integral between y=0 and the selected curve"
+ )
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["Net Counts"]).setToolTip(
+ "Estimation of the integral between the segment [maxPt, minPt] "
+ "and the selected curve"
+ )
def setRois(self, rois, order=None):
"""Set the ROIs by providing a dictionary of ROI information.
@@ -565,7 +567,7 @@ class ROITable(TableWidget):
:param str order: Field used for ordering the ROIs.
One of "from", "to", "type".
None (default) for no ordering, or same order as specified
- in parameter ``roidict`` if provided as an OrderedDict.
+ in parameter ``rois`` if provided as a dict.
"""
assert order in [None, "from", "to", "type"]
self.clear()
@@ -576,7 +578,7 @@ class ROITable(TableWidget):
if isinstance(roi, ROI):
_roi = roi
else:
- roi['name'] = roiName
+ roi["name"] = roiName
_roi = ROI._fromDict(roi)
self.addRoi(_roi)
else:
@@ -591,12 +593,11 @@ class ROITable(TableWidget):
:param :class:`ROI` roi: roi to add to the table
"""
assert isinstance(roi, ROI)
- self._getItem(name='ID', row=None, roi=roi)
+ self._getItem(name="ID", row=None, roi=roi)
self._roiDict[roi.getID()] = roi
self._markersHandler.add(roi, _RoiMarkerHandler(roi, self.plot))
self._updateRoiInfo(roi.getID())
- callback = functools.partial(WeakMethodProxy(self._updateRoiInfo),
- roi.getID())
+ callback = functools.partial(WeakMethodProxy(self._updateRoiInfo), roi.getID())
roi.sigChanged.connect(callback)
# set it as the active one
self.setActiveRoi(roi)
@@ -609,7 +610,7 @@ class ROITable(TableWidget):
if item:
return item
else:
- if name == 'ID':
+ if name == "ID":
assert roi
if roi.getID() in self._roiToItems:
return self._roiToItems[roi.getID()]
@@ -617,41 +618,47 @@ class ROITable(TableWidget):
# create a new row
row = self.rowCount()
self.setRowCount(self.rowCount() + 1)
- item = qt.QTableWidgetItem(str(roi.getID()),
- type=qt.QTableWidgetItem.Type)
+ item = qt.QTableWidgetItem(
+ str(roi.getID()), type=qt.QTableWidgetItem.Type
+ )
self._roiToItems[roi.getID()] = item
- elif name == 'ROI':
- item = qt.QTableWidgetItem(roi.getName() if roi else '',
- type=qt.QTableWidgetItem.Type)
- if roi.getName().upper() in ('ICR', 'DEFAULT'):
+ elif name == "ROI":
+ item = qt.QTableWidgetItem(
+ roi.getName() if roi else "", type=qt.QTableWidgetItem.Type
+ )
+ if roi.getName().upper() in ("ICR", "DEFAULT"):
item.setFlags(qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled)
else:
- item.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsEditable)
- elif name == 'Type':
+ item.setFlags(
+ qt.Qt.ItemIsSelectable
+ | qt.Qt.ItemIsEnabled
+ | qt.Qt.ItemIsEditable
+ )
+ elif name == "Type":
item = qt.QTableWidgetItem(type=qt.QTableWidgetItem.Type)
item.setFlags((qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled))
- elif name in ('To', 'From'):
+ elif name in ("To", "From"):
item = _FloatItem()
- if roi.getName().upper() in ('ICR', 'DEFAULT'):
+ if roi.getName().upper() in ("ICR", "DEFAULT"):
item.setFlags(qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled)
else:
- item.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsEditable)
- elif name in ('Raw Counts', 'Net Counts', 'Raw Area', 'Net Area'):
+ item.setFlags(
+ qt.Qt.ItemIsSelectable
+ | qt.Qt.ItemIsEnabled
+ | qt.Qt.ItemIsEditable
+ )
+ elif name in ("Raw Counts", "Net Counts", "Raw Area", "Net Area"):
item = _FloatItem()
item.setFlags((qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled))
else:
- raise ValueError('item type not recognized')
+ raise ValueError("item type not recognized")
self.setItem(row, self.COLUMNS_INDEX[name], item)
return item
def _itemChanged(self, item):
def getRoi():
- IDItem = self.item(item.row(), self.COLUMNS_INDEX['ID'])
+ IDItem = self.item(item.row(), self.COLUMNS_INDEX["ID"])
assert IDItem
id = int(IDItem.text())
assert id in self._roiDict
@@ -663,21 +670,21 @@ class ROITable(TableWidget):
self.activeROIChanged.emit()
self._userIsEditingRoi = True
- if item.column() in (self.COLUMNS_INDEX['To'], self.COLUMNS_INDEX['From']):
+ if item.column() in (self.COLUMNS_INDEX["To"], self.COLUMNS_INDEX["From"]):
roi = getRoi()
- if item.text() not in ('', self.INFO_NOT_FOUND):
+ if item.text() not in ("", self.INFO_NOT_FOUND):
try:
value = float(item.text())
except ValueError:
value = 0
changed = False
- if item.column() == self.COLUMNS_INDEX['To']:
+ if item.column() == self.COLUMNS_INDEX["To"]:
if value != roi.getTo():
roi.setTo(value)
changed = True
else:
- assert(item.column() == self.COLUMNS_INDEX['From'])
+ assert item.column() == self.COLUMNS_INDEX["From"]
if value != roi.getFrom():
roi.setFrom(value)
changed = True
@@ -685,7 +692,7 @@ class ROITable(TableWidget):
self._updateMarker(roi.getName())
signalChanged(roi)
- if item.column() is self.COLUMNS_INDEX['ROI']:
+ if item.column() is self.COLUMNS_INDEX["ROI"]:
roi = getRoi()
if roi.getName() != item.text():
roi.setName(item.text())
@@ -705,7 +712,7 @@ class ROITable(TableWidget):
roiToRm = set()
for item in activeItems:
row = item.row()
- itemID = self.item(row, self.COLUMNS_INDEX['ID'])
+ itemID = self.item(row, self.COLUMNS_INDEX["ID"])
roiToRm.add(self._roiDict[int(itemID.text())])
[self.removeROI(roi) for roi in roiToRm]
self.blockSignals(old)
@@ -726,8 +733,9 @@ class ROITable(TableWidget):
del self._roiDict[roi.getID()]
self._markersHandler.remove(roi)
- callback = functools.partial(WeakMethodProxy(self._updateRoiInfo),
- roi.getID())
+ callback = functools.partial(
+ WeakMethodProxy(self._updateRoiInfo), roi.getID()
+ )
roi.sigChanged.connect(callback)
def setActiveRoi(self, roi):
@@ -769,42 +777,42 @@ class ROITable(TableWidget):
roi.setTo(max)
roi.blockSignals(False)
- itemID = self._getItem(name='ID', roi=roi, row=None)
- itemName = self._getItem(name='ROI', row=itemID.row(), roi=roi)
+ itemID = self._getItem(name="ID", roi=roi, row=None)
+ itemName = self._getItem(name="ROI", row=itemID.row(), roi=roi)
itemName.setText(roi.getName())
- itemType = self._getItem(name='Type', row=itemID.row(), roi=roi)
+ itemType = self._getItem(name="Type", row=itemID.row(), roi=roi)
itemType.setText(roi.getType() or self.INFO_NOT_FOUND)
- itemFrom = self._getItem(name='From', row=itemID.row(), roi=roi)
- fromdata = str(roi.getFrom()) if roi.getFrom() is not None else self.INFO_NOT_FOUND
+ itemFrom = self._getItem(name="From", row=itemID.row(), roi=roi)
+ fromdata = (
+ str(roi.getFrom()) if roi.getFrom() is not None else self.INFO_NOT_FOUND
+ )
itemFrom.setText(fromdata)
- itemTo = self._getItem(name='To', row=itemID.row(), roi=roi)
+ itemTo = self._getItem(name="To", row=itemID.row(), roi=roi)
todata = str(roi.getTo()) if roi.getTo() is not None else self.INFO_NOT_FOUND
itemTo.setText(todata)
rawCounts, netCounts = roi.computeRawAndNetCounts(
- curve=self.plot.getActiveCurve(just_legend=False))
- itemRawCounts = self._getItem(name='Raw Counts', row=itemID.row(),
- roi=roi)
+ curve=self.plot.getActiveCurve(just_legend=False)
+ )
+ itemRawCounts = self._getItem(name="Raw Counts", row=itemID.row(), roi=roi)
rawCounts = str(rawCounts) if rawCounts is not None else self.INFO_NOT_FOUND
itemRawCounts.setText(rawCounts)
- itemNetCounts = self._getItem(name='Net Counts', row=itemID.row(),
- roi=roi)
+ itemNetCounts = self._getItem(name="Net Counts", row=itemID.row(), roi=roi)
netCounts = str(netCounts) if netCounts is not None else self.INFO_NOT_FOUND
itemNetCounts.setText(netCounts)
rawArea, netArea = roi.computeRawAndNetArea(
- curve=self.plot.getActiveCurve(just_legend=False))
- itemRawArea = self._getItem(name='Raw Area', row=itemID.row(),
- roi=roi)
+ curve=self.plot.getActiveCurve(just_legend=False)
+ )
+ itemRawArea = self._getItem(name="Raw Area", row=itemID.row(), roi=roi)
rawArea = str(rawArea) if rawArea is not None else self.INFO_NOT_FOUND
itemRawArea.setText(rawArea)
- itemNetArea = self._getItem(name='Net Area', row=itemID.row(),
- roi=roi)
+ itemNetArea = self._getItem(name="Net Area", row=itemID.row(), roi=roi)
netArea = str(netArea) if netArea is not None else self.INFO_NOT_FOUND
itemNetArea.setText(netArea)
@@ -813,49 +821,23 @@ class ROITable(TableWidget):
def currentChanged(self, current, previous):
if previous and current.row() != previous.row() and current.row() >= 0:
- roiItem = self.item(current.row(),
- self.COLUMNS_INDEX['ID'])
+ roiItem = self.item(current.row(), self.COLUMNS_INDEX["ID"])
assert roiItem
self.setActiveRoi(self._roiDict[int(roiItem.text())])
self._markersHandler.updateAllMarkers()
qt.QTableWidget.currentChanged(self, current, previous)
- @deprecation.deprecated(reason="Removed",
- replacement="roidict and roidict.values()",
- since_version="0.10.0")
- def getROIListAndDict(self):
- """
-
- :return: the list of roi objects and the dictionary of roi name to roi
- object.
- """
- roidict = self._roiDict
- return list(roidict.values()), roidict
-
- def calculateRois(self, roiList=None, roiDict=None):
- """
- Update values of all registred rois (raw and net counts in particular)
-
- :param roiList: deprecated parameter
- :param roiDict: deprecated parameter
- """
- if roiDict:
- deprecation.deprecated_warning(name='roiDict', type_='Parameter',
- reason='Unused parameter',
- since_version="0.10.0")
- if roiList:
- deprecation.deprecated_warning(name='roiList', type_='Parameter',
- reason='Unused parameter',
- since_version="0.10.0")
-
+ def calculateRois(self):
+ """Update values of all registred rois (raw and net counts in particular)"""
for roiID in self._roiDict:
self._updateRoiInfo(roiID)
def _updateMarker(self, roiID):
"""Make sure the marker of the given roi name is updated"""
- if self._showAllMarkers or (self.activeRoi
- and self.activeRoi.getName() == roiID):
+ if self._showAllMarkers or (
+ self.activeRoi and self.activeRoi.getName() == roiID
+ ):
self._updateMarkers()
def _updateMarkers(self):
@@ -865,7 +847,9 @@ class ROITable(TableWidget):
if not self.activeRoi or not self.plot:
return
assert isinstance(self.activeRoi, ROI)
- markerHandler = self._markersHandler.getMarkerHandler(self.activeRoi.getID())
+ markerHandler = self._markersHandler.getMarkerHandler(
+ self.activeRoi.getID()
+ )
if markerHandler is not None:
markerHandler.updateMarkers()
@@ -884,12 +868,16 @@ class ROITable(TableWidget):
if order is None or order.lower() == "none":
ordered_roilist = list(self._roiDict.values())
- res = OrderedDict([(roi.getName(), self._roiDict[roi.getID()]) for roi in ordered_roilist])
+ res = dict(
+ [(roi.getName(), self._roiDict[roi.getID()]) for roi in ordered_roilist]
+ )
else:
assert order in ["from", "to", "type", "netcounts", "rawcounts"]
- ordered_roilist = sorted(self._roiDict.keys(),
- key=lambda roi_id: self._roiDict[roi_id].get(order))
- res = OrderedDict([(roi.getName(), self._roiDict[id]) for id in ordered_roilist])
+ ordered_roilist = sorted(
+ self._roiDict.keys(),
+ key=lambda roi_id: self._roiDict[roi_id].get(order),
+ )
+ res = dict([(roi.getName(), self._roiDict[id]) for id in ordered_roilist])
return res
@@ -904,7 +892,7 @@ class ROITable(TableWidget):
for roiID, roi in self._roiDict.items():
roilist.append(roi.toDict())
roidict[roi.getName()] = roi.toDict()
- datadict = {'ROI': {'roilist': roilist, 'roidict': roidict}}
+ datadict = {"ROI": {"roilist": roilist, "roidict": roidict}}
dictdump.dump(datadict, filename)
def load(self, filename):
@@ -917,9 +905,9 @@ class ROITable(TableWidget):
rois = []
# Remove rawcounts and netcounts from ROIs
- for roiDict in roisDict['ROI']['roidict'].values():
- roiDict.pop('rawcounts', None)
- roiDict.pop('netcounts', None)
+ for roiDict in roisDict["ROI"]["roidict"].values():
+ roiDict.pop("rawcounts", None)
+ roiDict.pop("netcounts", None)
rois.append(ROI._fromDict(roiDict))
self.setRois(rois)
@@ -946,14 +934,13 @@ class ROITable(TableWidget):
def _handleROIMarkerEvent(self, ddict):
"""Handle plot signals related to marker events."""
- if ddict['event'] == 'markerMoved':
- label = ddict['label']
+ if ddict["event"] == "markerMoved":
+ label = ddict["label"]
roiID = self._markersHandler.getRoiID(markerID=label)
if roiID is not None:
# avoid several emission of sigROISignal
old = self.blockSignals(True)
- self._markersHandler.changePosition(markerID=label,
- x=ddict['x'])
+ self._markersHandler.changePosition(markerID=label, x=ddict["x"])
self.blockSignals(old)
self._updateRoiInfo(roiID)
@@ -994,11 +981,11 @@ class ROITable(TableWidget):
should be visible.
"""
if visible is True:
- self.showColumn(self.COLUMNS_INDEX['Raw Counts'])
- self.showColumn(self.COLUMNS_INDEX['Net Counts'])
+ self.showColumn(self.COLUMNS_INDEX["Raw Counts"])
+ self.showColumn(self.COLUMNS_INDEX["Net Counts"])
else:
- self.hideColumn(self.COLUMNS_INDEX['Raw Counts'])
- self.hideColumn(self.COLUMNS_INDEX['Net Counts'])
+ self.hideColumn(self.COLUMNS_INDEX["Raw Counts"])
+ self.hideColumn(self.COLUMNS_INDEX["Net Counts"])
def setAreaVisible(self, visible):
"""
@@ -1008,11 +995,11 @@ class ROITable(TableWidget):
should be visible.
"""
if visible is True:
- self.showColumn(self.COLUMNS_INDEX['Raw Area'])
- self.showColumn(self.COLUMNS_INDEX['Net Area'])
+ self.showColumn(self.COLUMNS_INDEX["Raw Area"])
+ self.showColumn(self.COLUMNS_INDEX["Net Area"])
else:
- self.hideColumn(self.COLUMNS_INDEX['Raw Area'])
- self.hideColumn(self.COLUMNS_INDEX['Net Area'])
+ self.hideColumn(self.COLUMNS_INDEX["Raw Area"])
+ self.hideColumn(self.COLUMNS_INDEX["Net Area"])
def fillFromROIDict(self, roilist=(), roidict=None, currentroi=None):
"""
@@ -1073,7 +1060,7 @@ class ROI(_RegionOfInterestBase):
self._fromdata = fromdata
self._todata = todata
- self._type = type_ or 'Default'
+ self._type = type_ or "Default"
self.sigItemChanged.connect(self.__itemChanged)
@@ -1150,27 +1137,27 @@ class ROI(_RegionOfInterestBase):
:return: dict containing the roi parameters
"""
ddict = {
- 'type': self._type,
- 'name': self.getName(),
- 'from': self._fromdata,
- 'to': self._todata,
+ "type": self._type,
+ "name": self.getName(),
+ "from": self._fromdata,
+ "to": self._todata,
}
- if hasattr(self, '_extraInfo'):
+ if hasattr(self, "_extraInfo"):
ddict.update(self._extraInfo)
return ddict
@staticmethod
def _fromDict(dic):
- assert 'name' in dic
- roi = ROI(name=dic['name'])
+ assert "name" in dic
+ roi = ROI(name=dic["name"])
roi._extraInfo = {}
for key in dic:
- if key == 'from':
- roi.setFrom(dic['from'])
- elif key == 'to':
- roi.setTo(dic['to'])
- elif key == 'type':
- roi.setType(dic['type'])
+ if key == "from":
+ roi.setFrom(dic["from"])
+ elif key == "to":
+ roi.setTo(dic["to"])
+ elif key == "type":
+ roi.setType(dic["type"])
else:
roi._extraInfo[key] = dic[key]
@@ -1181,7 +1168,7 @@ class ROI(_RegionOfInterestBase):
:return: True if the ROI is the `ICR`
"""
- return self.getName() == 'ICR'
+ return self.getName() == "ICR"
def computeRawAndNetCounts(self, curve):
"""Compute the Raw and net counts in the ROI for the given curve.
@@ -1206,8 +1193,7 @@ class ROI(_RegionOfInterestBase):
x = curve.getXData(copy=False)
y = curve.getYData(copy=False)
- idx = numpy.nonzero((self._fromdata <= x) &
- (x <= self._todata))[0]
+ idx = numpy.nonzero((self._fromdata <= x) & (x <= self._todata))[0]
if len(idx):
xw = x[idx]
yw = y[idx]
@@ -1215,10 +1201,9 @@ class ROI(_RegionOfInterestBase):
deltaX = xw[-1] - xw[0]
deltaY = yw[-1] - yw[0]
if deltaX > 0.0:
- slope = (deltaY / deltaX)
+ slope = deltaY / deltaX
background = yw[0] + slope * (xw - xw[0])
- netCounts = (rawCounts -
- background.sum(dtype=numpy.float64))
+ netCounts = rawCounts - background.sum(dtype=numpy.float64)
else:
netCounts = 0.0
else:
@@ -1274,6 +1259,7 @@ class _RoiMarkerManager(object):
"""
Deal with all the ROI markers
"""
+
def __init__(self):
self._roiMarkerHandlers = {}
self._middleROIMarkerFlag = False
@@ -1293,7 +1279,7 @@ class _RoiMarkerManager(object):
assert isinstance(roi, ROI)
assert isinstance(markersHandler, _RoiMarkerHandler)
if roi.getID() in self._roiMarkerHandlers:
- raise ValueError('roi with the same ID already existing')
+ raise ValueError("roi with the same ID already existing")
else:
self._roiMarkerHandlers[roi.getID()] = markersHandler
@@ -1323,25 +1309,30 @@ class _RoiMarkerManager(object):
def changePosition(self, markerID, x):
markerHandler = self.getMarker(markerID)
if markerHandler is None:
- raise ValueError('Marker %s not register' % markerID)
+ raise ValueError("Marker %s not register" % markerID)
markerHandler.changePosition(markerID=markerID, x=x)
def updateMarker(self, markerID):
markerHandler = self.getMarker(markerID)
if markerHandler is None:
- raise ValueError('Marker %s not register' % markerID)
+ raise ValueError("Marker %s not register" % markerID)
roiID = self.getRoiID(markerID)
- visible = (self._activeRoi and self._activeRoi.getID() == roiID) or self._showAllMarkers is True
+ visible = (
+ self._activeRoi and self._activeRoi.getID() == roiID
+ ) or self._showAllMarkers is True
markerHandler.setVisible(visible)
markerHandler.updateAllMarkers()
def updateRoiMarkers(self, roiID):
if roiID in self._roiMarkerHandlers:
- visible = ((self._activeRoi and self._activeRoi.getID() == roiID)
- or self._showAllMarkers is True)
+ visible = (
+ self._activeRoi and self._activeRoi.getID() == roiID
+ ) or self._showAllMarkers is True
_roi = self._roiMarkerHandlers[roiID]._roi()
if _roi and not _roi.isICR():
- self._roiMarkerHandlers[roiID].showMiddleMarker(self._middleROIMarkerFlag)
+ self._roiMarkerHandlers[roiID].showMiddleMarker(
+ self._middleROIMarkerFlag
+ )
self._roiMarkerHandlers[roiID].setVisible(visible)
self._roiMarkerHandlers[roiID].updateMarkers()
@@ -1372,8 +1363,11 @@ class _RoiMarkerManager(object):
def getVisibleRois(self):
res = {}
for roiID, roiHandler in self._roiMarkerHandlers.items():
- markers = (roiHandler.getMarker('min'), roiHandler.getMarker('max'),
- roiHandler.getMarker('middle'))
+ markers = (
+ roiHandler.getMarker("min"),
+ roiHandler.getMarker("max"),
+ roiHandler.getMarker("middle"),
+ )
for marker in markers:
if marker.isVisible():
if roiID not in res:
@@ -1384,6 +1378,7 @@ class _RoiMarkerManager(object):
class _RoiMarkerHandler(object):
"""Used to deal with ROI markers used in ROITable"""
+
def __init__(self, roi, plot):
assert roi and isinstance(roi, ROI)
assert plot
@@ -1391,7 +1386,7 @@ class _RoiMarkerHandler(object):
self._roi = weakref.ref(roi)
self._plot = weakref.ref(plot)
self._draggable = False if roi.isICR() else True
- self._color = 'black' if roi.isICR() else 'blue'
+ self._color = "black" if roi.isICR() else "blue"
self._displayMidMarker = False
self._visible = True
@@ -1405,9 +1400,9 @@ class _RoiMarkerHandler(object):
def clear(self):
if self.plot and self.roi:
- self.plot.removeMarker(self._markerID('min'))
- self.plot.removeMarker(self._markerID('max'))
- self.plot.removeMarker(self._markerID('middle'))
+ self.plot.removeMarker(self._markerID("min"))
+ self.plot.removeMarker(self._markerID("max"))
+ self.plot.removeMarker(self._markerID("middle"))
@property
def roi(self):
@@ -1423,7 +1418,7 @@ class _RoiMarkerHandler(object):
_logger.warning("ROI is not draggable. Won't display middle marker")
return
self._displayMidMarker = visible
- self.getMarker('middle').setVisible(self._displayMidMarker)
+ self.getMarker("middle").setVisible(self._displayMidMarker)
def updateMarkers(self):
if self.roi is None:
@@ -1433,54 +1428,56 @@ class _RoiMarkerHandler(object):
self._updateMiddleMarkerPos()
def _updateMinMarkerPos(self):
- self.getMarker('min').setPosition(x=self.roi.getFrom(), y=None)
- self.getMarker('min').setVisible(self._visible)
+ self.getMarker("min").setPosition(x=self.roi.getFrom(), y=None)
+ self.getMarker("min").setVisible(self._visible)
def _updateMaxMarkerPos(self):
- self.getMarker('max').setPosition(x=self.roi.getTo(), y=None)
- self.getMarker('max').setVisible(self._visible)
+ self.getMarker("max").setPosition(x=self.roi.getTo(), y=None)
+ self.getMarker("max").setVisible(self._visible)
def _updateMiddleMarkerPos(self):
- self.getMarker('middle').setPosition(x=self.roi.getMiddle(), y=None)
- self.getMarker('middle').setVisible(self._displayMidMarker and self._visible)
+ self.getMarker("middle").setPosition(x=self.roi.getMiddle(), y=None)
+ self.getMarker("middle").setVisible(self._displayMidMarker and self._visible)
def getMarker(self, markerType):
if self.plot is None:
return None
- assert markerType in ('min', 'max', 'middle')
+ assert markerType in ("min", "max", "middle")
if self.plot._getMarker(self._markerID(markerType)) is None:
assert self.roi
- if markerType == 'min':
+ if markerType == "min":
val = self.roi.getFrom()
- elif markerType == 'max':
+ elif markerType == "max":
val = self.roi.getTo()
else:
val = self.roi.getMiddle()
_color = self._color
- if markerType == 'middle':
- _color = 'yellow'
- self.plot.addXMarker(val,
- legend=self._markerID(markerType),
- text=self.getMarkerName(markerType),
- color=_color,
- draggable=self.draggable)
+ if markerType == "middle":
+ _color = "yellow"
+ self.plot.addXMarker(
+ val,
+ legend=self._markerID(markerType),
+ text=self.getMarkerName(markerType),
+ color=_color,
+ draggable=self.draggable,
+ )
return self.plot._getMarker(self._markerID(markerType))
def _markerID(self, markerType):
- assert markerType in ('min', 'max', 'middle')
+ assert markerType in ("min", "max", "middle")
assert self.roi
- return '_'.join((str(self.roi.getID()), markerType))
+ return "_".join((str(self.roi.getID()), markerType))
def getMarkerName(self, markerType):
- assert markerType in ('min', 'max', 'middle')
+ assert markerType in ("min", "max", "middle")
assert self.roi
- return ' '.join((self.roi.getName(), markerType))
+ return " ".join((self.roi.getName(), markerType))
def updateTexts(self):
- self.getMarker('min').setText(self.getMarkerName('min'))
- self.getMarker('max').setText(self.getMarkerName('max'))
- self.getMarker('middle').setText(self.getMarkerName('middle'))
+ self.getMarker("min").setText(self.getMarkerName("min"))
+ self.getMarker("max").setText(self.getMarkerName("max"))
+ self.getMarker("middle").setText(self.getMarkerName("middle"))
def changePosition(self, markerID, x):
assert self.hasMarker(markerID)
@@ -1488,10 +1485,10 @@ class _RoiMarkerHandler(object):
assert markerType is not None
if self.roi is None:
return
- if markerType == 'min':
+ if markerType == "min":
self.roi.setFrom(x)
self._updateMiddleMarkerPos()
- elif markerType == 'max':
+ elif markerType == "max":
self.roi.setTo(x)
self._updateMiddleMarkerPos()
else:
@@ -1502,17 +1499,19 @@ class _RoiMarkerHandler(object):
self._updateMaxMarkerPos()
def hasMarker(self, marker):
- return marker in (self._markerID('min'),
- self._markerID('max'),
- self._markerID('middle'))
+ return marker in (
+ self._markerID("min"),
+ self._markerID("max"),
+ self._markerID("middle"),
+ )
def _getMarkerType(self, markerID):
- if markerID.endswith('_min'):
- return 'min'
- elif markerID.endswith('_max'):
- return 'max'
- elif markerID.endswith('_middle'):
- return 'middle'
+ if markerID.endswith("_min"):
+ return "min"
+ elif markerID.endswith("_max"):
+ return "max"
+ elif markerID.endswith("_middle"):
+ return "middle"
else:
return None
@@ -1526,6 +1525,7 @@ class CurvesROIDockWidget(qt.QDockWidget):
:param plot: :class:`.PlotWindow` instance on which to operate
:param name: See :class:`QDockWidget`
"""
+
sigROISignal = qt.Signal(object)
"""Deprecated signal for backward compatibility with silx < 0.7.
Prefer connecting directly to :attr:`CurvesRoiWidget.sigRoiSignal`
@@ -1564,7 +1564,7 @@ class CurvesROIDockWidget(qt.QDockWidget):
See :class:`QMainWindow`.
"""
action = super(CurvesROIDockWidget, self).toggleViewAction()
- action.setIcon(icons.getQIcon('plot-roi'))
+ action.setIcon(icons.getQIcon("plot-roi"))
return action
@property
diff --git a/src/silx/gui/plot/ImageStack.py b/src/silx/gui/plot/ImageStack.py
index e2bed9d..175d6e4 100644
--- a/src/silx/gui/plot/ImageStack.py
+++ b/src/silx/gui/plot/ImageStack.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2020-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2020-2023 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
@@ -23,118 +23,35 @@
# ###########################################################################*/
"""Image stack view with data prefetch capabilty."""
+from __future__ import annotations
+
__authors__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "04/03/2019"
-from silx.gui import icons, qt
+from silx.gui import qt
from silx.gui.plot import Plot2D
-from silx.gui.utils import concurrent
from silx.io.url import DataUrl
from silx.io.utils import get_data
-from collections import OrderedDict
from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
-import time
-import threading
+from silx.gui.widgets.UrlList import UrlList
+from silx.gui.utils import blockSignals
+from silx.utils.deprecation import deprecated
+
import typing
import logging
+from silx.gui.widgets.WaitingOverlay import WaitingOverlay
+from collections.abc import Iterable
_logger = logging.getLogger(__name__)
-class _PlotWithWaitingLabel(qt.QWidget):
- """Image plot widget with an overlay 'waiting' status.
- """
-
- class AnimationThread(threading.Thread):
- def __init__(self, label):
- self.running = True
- self._label = label
- self.animated_icon = icons.getWaitIcon()
- self.animated_icon.register(self._label)
- super(_PlotWithWaitingLabel.AnimationThread, self).__init__()
-
- def run(self):
- while self.running:
- time.sleep(0.05)
- icon = self.animated_icon.currentIcon()
- self.future_result = concurrent.submitToQtMainThread(
- self._label.setPixmap, icon.pixmap(30, state=qt.QIcon.On))
-
- def stop(self):
- """Stop the update thread"""
- if self.running:
- self.animated_icon.unregister(self._label)
- self.running = False
- self.join(2)
-
- def __init__(self, parent):
- super(_PlotWithWaitingLabel, self).__init__(parent=parent)
- self._autoResetZoom = True
- layout = qt.QStackedLayout(self)
- layout.setStackingMode(qt.QStackedLayout.StackAll)
-
- self._waiting_label = qt.QLabel(parent=self)
- self._waiting_label.setAlignment(qt.Qt.AlignHCenter | qt.Qt.AlignVCenter)
- layout.addWidget(self._waiting_label)
-
- self._plot = Plot2D(parent=self)
- layout.addWidget(self._plot)
-
- self.updateThread = _PlotWithWaitingLabel.AnimationThread(self._waiting_label)
- self.updateThread.start()
-
- def close(self) -> bool:
- super(_PlotWithWaitingLabel, self).close()
- self.stopUpdateThread()
-
- def stopUpdateThread(self):
- self.updateThread.stop()
-
- def setAutoResetZoom(self, reset):
- """
- Should we reset the zoom when adding an image (eq. when browsing)
-
- :param bool reset:
- """
- self._autoResetZoom = reset
- if self._autoResetZoom:
- self._plot.resetZoom()
-
- def isAutoResetZoom(self):
- """
-
- :return: True if a reset is done when the image change
- :rtype: bool
- """
- return self._autoResetZoom
-
- def setWaiting(self, activate=True):
- if activate is True:
- self._plot.clear()
- self._waiting_label.show()
- else:
- self._waiting_label.hide()
-
- def setData(self, data):
- self.setWaiting(activate=False)
- self._plot.addImage(data=data, resetzoom=self._autoResetZoom)
-
- def clear(self):
- self._plot.clear()
- self.setWaiting(False)
-
- def getPlotWidget(self):
- return self._plot
-
-
class _HorizontalSlider(HorizontalSliderWithBrowser):
-
sigCurrentUrlIndexChanged = qt.Signal(int)
def __init__(self, parent):
- super(_HorizontalSlider, self).__init__(parent=parent)
+ super().__init__(parent=parent)
# connect signal / slot
self.valueChanged.connect(self._urlChanged)
@@ -146,67 +63,23 @@ class _HorizontalSlider(HorizontalSliderWithBrowser):
self.sigCurrentUrlIndexChanged.emit(value)
-class UrlList(qt.QWidget):
- """List of URLs the user to select an URL"""
-
- sigCurrentUrlChanged = qt.Signal(str)
- """Signal emitted when the active/current url change"""
-
- def __init__(self, parent=None):
- super(UrlList, self).__init__(parent)
- self.setLayout(qt.QVBoxLayout())
- self.layout().setSpacing(0)
- self.layout().setContentsMargins(0, 0, 0, 0)
- self._listWidget = qt.QListWidget(parent=self)
- self.layout().addWidget(self._listWidget)
-
- # connect signal / Slot
- self._listWidget.currentItemChanged.connect(self._notifyCurrentUrlChanged)
-
- # expose API
- self.currentItem = self._listWidget.currentItem
-
- def setUrls(self, urls: list) -> None:
- url_names = []
- [url_names.append(url.path()) for url in urls]
- self._listWidget.addItems(url_names)
-
- def _notifyCurrentUrlChanged(self, current, previous):
- if current is None:
- pass
- else:
- self.sigCurrentUrlChanged.emit(current.text())
-
- def setUrl(self, url: DataUrl) -> None:
- assert isinstance(url, DataUrl)
- sel_items = self._listWidget.findItems(url.path(), qt.Qt.MatchExactly)
- if sel_items is None:
- _logger.warning(url.path(), ' is not registered in the list.')
- elif len(sel_items) > 0:
- item = sel_items[0]
- self._listWidget.setCurrentItem(item)
- self.sigCurrentUrlChanged.emit(item.text())
-
- def clear(self):
- self._listWidget.clear()
-
-
class _ToggleableUrlSelectionTable(qt.QWidget):
-
_BUTTON_ICON = qt.QStyle.SP_ToolBarHorizontalExtensionButton # noqa
sigCurrentUrlChanged = qt.Signal(str)
"""Signal emitted when the active/current url change"""
+ sigUrlRemoved = qt.Signal(str)
+
def __init__(self, parent=None) -> None:
- qt.QWidget.__init__(self, parent)
+ super().__init__(parent)
self.setLayout(qt.QGridLayout())
self._toggleButton = qt.QPushButton(parent=self)
self.layout().addWidget(self._toggleButton, 0, 2, 1, 1)
- self._toggleButton.setSizePolicy(qt.QSizePolicy.Fixed,
- qt.QSizePolicy.Fixed)
+ self._toggleButton.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
self._urlsTable = UrlList(parent=self)
+
self.layout().addWidget(self._urlsTable, 1, 1, 1, 2)
# set up
@@ -214,12 +87,8 @@ class _ToggleableUrlSelectionTable(qt.QWidget):
# Signal / slot connection
self._toggleButton.clicked.connect(self.toggleUrlSelectionTable)
- self._urlsTable.sigCurrentUrlChanged.connect(self._propagateSignal)
-
- # expose API
- self.setUrls = self._urlsTable.setUrls
- self.setUrl = self._urlsTable.setUrl
- self.currentItem = self._urlsTable.currentItem
+ self._urlsTable.sigCurrentUrlChanged.connect(self.sigCurrentUrlChanged)
+ self._urlsTable.sigUrlRemoved.connect(self.sigUrlRemoved)
def toggleUrlSelectionTable(self):
visible = not self.urlSelectionTableIsVisible()
@@ -236,21 +105,36 @@ class _ToggleableUrlSelectionTable(qt.QWidget):
self._toggleButton.setIcon(icon)
def urlSelectionTableIsVisible(self):
- return self._urlsTable.isVisible()
-
- def _propagateSignal(self, url):
- self.sigCurrentUrlChanged.emit(url)
+ return self._urlsTable.isVisibleTo(self)
def clear(self):
self._urlsTable.clear()
+ # expose UrlList API
+ @deprecated(replacement="addUrls", since_version="2.0")
+ def setUrls(self, urls: Iterable[DataUrl]):
+ self._urlsTable.addUrls(urls=urls)
+
+ def addUrls(self, urls: Iterable[DataUrl]):
+ self._urlsTable.addUrls(urls=urls)
+
+ def setUrl(self, url: typing.Optional[DataUrl]):
+ self._urlsTable.setUrl(url=url)
+
+ def removeUrl(self, url: str):
+ self._urlsTable.removeUrl(url)
+
+ def currentItem(self):
+ return self._urlsTable.currentItem()
+
class UrlLoader(qt.QThread):
"""
Thread use to load DataUrl
"""
+
def __init__(self, parent, url):
- super(UrlLoader, self).__init__(parent=parent)
+ super().__init__(parent=parent)
assert isinstance(url, DataUrl)
self.url = url
self.data = None
@@ -277,17 +161,21 @@ class ImageStack(qt.QMainWindow):
"""Signal emitted when the current url change"""
def __init__(self, parent=None) -> None:
- super(ImageStack, self).__init__(parent)
+ super().__init__(parent)
self.__n_prefetch = ImageStack.N_PRELOAD
self._loadingThreads = []
self.setWindowFlags(qt.Qt.Widget)
self._current_url = None
self._url_loader = UrlLoader
"class to instantiate for loading urls"
+ self._autoResetZoom = True
# main widget
- self._plot = _PlotWithWaitingLabel(parent=self)
+ self._plot = Plot2D(parent=self)
self._plot.setAttribute(qt.Qt.WA_DeleteOnClose, True)
+ self._waitingOverlay = WaitingOverlay(self._plot)
+ self._waitingOverlay.setIconSize(qt.QSize(30, 30))
+ self._waitingOverlay.hide()
self.setWindowTitle("Image stack")
self.setCentralWidget(self._plot)
@@ -308,12 +196,14 @@ class ImageStack(qt.QMainWindow):
# connect signal / slot
self._urlsTable.sigCurrentUrlChanged.connect(self.setCurrentUrl)
+ self._urlsTable.sigUrlRemoved.connect(self.removeUrl)
self._slider.sigCurrentUrlIndexChanged.connect(self.setCurrentUrlIndex)
def close(self) -> bool:
self._freeLoadingThreads()
+ self._waitingOverlay.close()
self._plot.close()
- super(ImageStack, self).close()
+ super().close()
def setUrlLoaderClass(self, urlLoader: typing.Type[UrlLoader]) -> None:
"""
@@ -346,14 +236,14 @@ class ImageStack(qt.QMainWindow):
:return: PlotWidget contained in this window
:rtype: Plot2D
"""
- return self._plot.getPlotWidget()
+ return self._plot
def reset(self) -> None:
"""Clear the plot and remove any link to url"""
self._freeLoadingThreads()
self._urls = None
self._urlIndexes = None
- self._urlData = OrderedDict({})
+ self._urlData = {}
self._current_url = None
self._plot.clear()
self._urlsTable.clear()
@@ -396,7 +286,8 @@ class ImageStack(qt.QMainWindow):
if url in self._urlIndexes:
self._urlData[url] = sender.data
if self.getCurrentUrl().path() == url:
- self._plot.setData(self._urlData[url])
+ self._waitingOverlay.setVisible(False)
+ self._plot.addImage(self._urlData[url], resetzoom=self._autoResetZoom)
if sender in self._loadingThreads:
self._loadingThreads.remove(sender)
self.sigLoaded.emit(url)
@@ -421,6 +312,29 @@ class ImageStack(qt.QMainWindow):
"""
return self.__n_prefetch
+ def setUrlsEditable(self, editable: bool):
+ self._urlsTable._urlsTable.setEditable(editable)
+ if editable:
+ selection_mode = qt.QAbstractItemView.ExtendedSelection
+ else:
+ selection_mode = qt.QAbstractItemView.SingleSelection
+ self._urlsTable._urlsTable.setSelectionMode(selection_mode)
+
+ @staticmethod
+ def createUrlIndexes(urls: tuple):
+ indexes = {}
+ for index, url in enumerate(urls):
+ assert isinstance(
+ url, DataUrl
+ ), f"url is expected to be a DataUrl. Get {type(url)}"
+ indexes[index] = url
+ return indexes
+
+ def _resetSlider(self):
+ with blockSignals(self._slider):
+ self._slider.setMinimum(0)
+ self._slider.setMaximum(len(self._urls) - 1)
+
def setUrls(self, urls: list) -> None:
"""list of urls within an index. Warning: urls should contain an image
compatible with the silx.gui.plot.Plot class
@@ -429,26 +343,16 @@ class ImageStack(qt.QMainWindow):
(position in the stack), value is the DataUrl
:type: list
"""
- def createUrlIndexes():
- indexes = OrderedDict()
- for index, url in enumerate(urls):
- indexes[index] = url
- return indexes
-
- urls_with_indexes = createUrlIndexes()
+ urls_with_indexes = self.createUrlIndexes(urls=urls)
urlsToIndex = self._urlsToIndex(urls_with_indexes)
self.reset()
self._urls = urls_with_indexes
self._urlIndexes = urlsToIndex
- old_url_table = self._urlsTable.blockSignals(True)
- self._urlsTable.setUrls(urls=list(self._urls.values()))
- self._urlsTable.blockSignals(old_url_table)
+ with blockSignals(self._urlsTable):
+ self._urlsTable.addUrls(urls=list(self._urls.values()))
- old_slider = self._slider.blockSignals(True)
- self._slider.setMinimum(0)
- self._slider.setMaximum(len(self._urls) - 1)
- self._slider.blockSignals(old_slider)
+ self._resetSlider()
if self.getCurrentUrl() in self._urls:
self.setCurrentUrl(self.getCurrentUrl())
@@ -457,6 +361,35 @@ class ImageStack(qt.QMainWindow):
first_url = self._urls[list(self._urls.keys())[0]]
self.setCurrentUrl(first_url)
+ def removeUrl(self, url: str) -> None:
+ """
+ Remove provided URL from the table
+
+ :param url: URL as str
+ """
+ # remove the given urls from self._urls and self._urlIndexes
+ if not isinstance(url, str):
+ raise TypeError("url is expected to be the str representation of the url")
+
+ # try to get reset the url displayed
+ current_url = self.getCurrentUrl()
+ with blockSignals(self._urlsTable):
+ self._urlsTable.removeUrl(url)
+ # update urls
+ urls_with_indexes = self.createUrlIndexes(
+ filter(
+ lambda a: a.path() != url,
+ self._urls.values(),
+ )
+ )
+ urlsToIndex = self._urlsToIndex(urls_with_indexes)
+ self._urls = urls_with_indexes
+ self._urlIndexes = urlsToIndex
+ self._resetSlider()
+
+ if current_url != url:
+ self.setCurrentUrl(current_url)
+
def getUrls(self) -> tuple:
"""
@@ -555,41 +488,46 @@ class ImageStack(qt.QMainWindow):
if self._urls is None:
return
elif index >= len(self._urls):
- raise ValueError('requested index out of bounds')
+ raise ValueError("requested index out of bounds")
else:
return self.setCurrentUrl(self._urls[index])
- def setCurrentUrl(self, url: typing.Union[DataUrl, str]) -> None:
+ def setCurrentUrl(self, url: typing.Optional[typing.Union[DataUrl, str]]) -> None:
"""
Define the url to be displayed
:param url: url to be displayed
:type: DataUrl
+ :raises KeyError: raised if the url is not know
"""
- assert isinstance(url, (DataUrl, str))
- if isinstance(url, str):
+ assert isinstance(url, (DataUrl, str, type(None)))
+ if url == "":
+ url = None
+ elif isinstance(url, str):
url = DataUrl(path=url)
- if url != self._current_url:
+ if url is not None and url != self._current_url:
self._current_url = url
self.sigCurrentUrlChanged.emit(url.path())
- old_url_table = self._urlsTable.blockSignals(True)
- old_slider = self._slider.blockSignals(True)
-
- self._urlsTable.setUrl(url)
- self._slider.setUrlIndex(self._urlIndexes[url.path()])
- if self._current_url is None:
- self._plot.clear()
- else:
- if self._current_url.path() in self._urlData:
- self._plot.setData(self._urlData[url.path()])
- else:
- self._load(url)
- self._notifyLoading()
- self._preFetch(self._getNNextUrls(self.__n_prefetch, url))
- self._preFetch(self._getNPreviousUrls(self.__n_prefetch, url))
- self._urlsTable.blockSignals(old_url_table)
- self._slider.blockSignals(old_slider)
+ with blockSignals(self._urlsTable):
+ with blockSignals(self._slider):
+ self._urlsTable.setUrl(url)
+ if url is not None:
+ self._slider.setUrlIndex(self._urlIndexes[url.path()])
+ if self._current_url is None:
+ self._plot.clear()
+ else:
+ if self._current_url.path() in self._urlData:
+ self._waitingOverlay.setVisible(False)
+ self._plot.addImage(
+ self._urlData[url.path()], resetzoom=self._autoResetZoom
+ )
+ else:
+ self._plot.clear()
+ self._load(url)
+ self._waitingOverlay.setVisible(True)
+ self._preFetch(self._getNNextUrls(self.__n_prefetch, url))
+ self._preFetch(self._getNPreviousUrls(self.__n_prefetch, url))
def getCurrentUrl(self) -> typing.Union[None, DataUrl]:
"""
@@ -618,17 +556,15 @@ class ImageStack(qt.QMainWindow):
res[url.path()] = index
return res
- def _notifyLoading(self):
- """display a simple image of loading..."""
- self._plot.setWaiting(activate=True)
-
def setAutoResetZoom(self, reset):
"""
Should we reset the zoom when adding an image (eq. when browsing)
:param bool reset:
"""
- self._plot.setAutoResetZoom(reset)
+ self._autoResetZoom = reset
+ if self._autoResetZoom:
+ self._plot.resetZoom()
def isAutoResetZoom(self) -> bool:
"""
@@ -636,4 +572,12 @@ class ImageStack(qt.QMainWindow):
:return: True if a reset is done when the image change
:rtype: bool
"""
- return self._plot.isAutoResetZoom()
+ return self._autoResetZoom
+
+ def getWaiterOverlay(self):
+ """
+
+ :return: Return the instance of `WaitingOverlay` used to display if processing or not
+ :rtype: WaitingOverlay
+ """
+ return self._waitingOverlay
diff --git a/src/silx/gui/plot/ImageView.py b/src/silx/gui/plot/ImageView.py
index a451b2d..eaca42b 100644
--- a/src/silx/gui/plot/ImageView.py
+++ b/src/silx/gui/plot/ImageView.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2015-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2023 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
@@ -63,19 +63,27 @@ from .tools.RadarView import RadarView
from .utils.axis import SyncAxes
from ..utils import blockSignals
from . import _utils
-from .tools.profile import manager
from .tools.profile import rois
from .actions import PlotAction
_logger = logging.getLogger(__name__)
-ProfileSumResult = collections.namedtuple("ProfileResult",
- ["dataXRange", "dataYRange",
- 'histoH', 'histoHRange',
- 'histoV', 'histoVRange',
- "xCoords", "xData",
- "yCoords", "yData"])
+ProfileSumResult = collections.namedtuple(
+ "ProfileResult",
+ [
+ "dataXRange",
+ "dataYRange",
+ "histoH",
+ "histoHRange",
+ "histoV",
+ "histoVRange",
+ "xCoords",
+ "xData",
+ "yCoords",
+ "yData",
+ ],
+)
def computeProfileSumOnRange(imageItem, xRange, yRange, cache=None):
@@ -103,8 +111,7 @@ def computeProfileSumOnRange(imageItem, xRange, yRange, cache=None):
yMin = int((yMin - origin[1]) / scale[1])
yMax = int((yMax - origin[1]) / scale[1])
- if (xMin >= width or xMax < 0 or
- yMin >= height or yMax < 0):
+ if xMin >= width or xMax < 0 or yMin >= height or yMax < 0:
return None
# The image is at least partly in the plot area
@@ -115,14 +122,15 @@ def computeProfileSumOnRange(imageItem, xRange, yRange, cache=None):
subsetYMax = (height if yMax >= height else yMax) + 1
if cache is not None:
- if ((subsetXMin, subsetXMax) == cache.dataXRange and
- (subsetYMin, subsetYMax) == cache.dataYRange):
+ if (subsetXMin, subsetXMax) == cache.dataXRange and (
+ subsetYMin,
+ subsetYMax,
+ ) == cache.dataYRange:
# The visible area of data is the same
return cache
# Rebuild histograms for visible area
- visibleData = data[subsetYMin:subsetYMax,
- subsetXMin:subsetXMax]
+ visibleData = data[subsetYMin:subsetYMax, subsetXMin:subsetXMax]
histoHVisibleData = numpy.nansum(visibleData, axis=0)
histoVVisibleData = numpy.nansum(visibleData, axis=1)
histoHMin = numpy.nanmin(histoHVisibleData)
@@ -151,7 +159,8 @@ def computeProfileSumOnRange(imageItem, xRange, yRange, cache=None):
xCoords=xCoords,
xData=xData,
yCoords=yCoords,
- yData=yData)
+ yData=yData,
+ )
return result
@@ -177,8 +186,8 @@ class _SideHistogram(PlotWidget):
def _plotEvents(self, eventDict):
"""Callback for horizontal histogram plot events."""
- if eventDict['event'] == 'mouseMoved':
- self.sigMouseMoved.emit(eventDict['x'], eventDict['y'])
+ if eventDict["event"] == "mouseMoved":
+ self.sigMouseMoved.emit(eventDict["x"], eventDict["y"])
def setProfileColor(self, color):
self._color = color
@@ -218,13 +227,13 @@ class _SideHistogram(PlotWidget):
profileSum = self.__profileSum
try:
- self.removeCurve('profile')
+ self.removeCurve("profile")
except Exception:
pass
if profileSum is None:
try:
- self.removeCurve('profilesum')
+ self.removeCurve("profilesum")
except Exception:
pass
return
@@ -236,13 +245,17 @@ class _SideHistogram(PlotWidget):
else:
assert False
- self.addCurve(xx, yy,
- xlabel='', ylabel='',
- legend="profilesum",
- color=self._color,
- linestyle='-',
- selectable=False,
- resetzoom=False)
+ self.addCurve(
+ xx,
+ yy,
+ xlabel="",
+ ylabel="",
+ legend="profilesum",
+ color=self._color,
+ linestyle="-",
+ selectable=False,
+ resetzoom=False,
+ )
self.__updateLimits()
@@ -254,13 +267,13 @@ class _SideHistogram(PlotWidget):
profile = self.__profile
try:
- self.removeCurve('profilesum')
+ self.removeCurve("profilesum")
except Exception:
pass
if profile is None:
try:
- self.removeCurve('profile')
+ self.removeCurve("profile")
except Exception:
pass
self.setProfileSum(self.__profileSum)
@@ -273,11 +286,7 @@ class _SideHistogram(PlotWidget):
else:
assert False
- self.addCurve(xx,
- yy,
- legend="profile",
- color=self._roiColor,
- resetzoom=False)
+ self.addCurve(xx, yy, legend="profile", color=self._roiColor, resetzoom=False)
self.__updateLimits()
@@ -299,9 +308,13 @@ class _SideHistogram(PlotWidget):
# Tune the result using the data margins
margins = self.getDataMargins()
if self._direction == qt.Qt.Horizontal:
- _, _, vMin, vMax = _utils.addMarginsToLimits(margins, False, False, 0, 0, vMin, vMax)
+ _, _, vMin, vMax = _utils.addMarginsToLimits(
+ margins, False, False, 0, 0, vMin, vMax
+ )
elif self._direction == qt.Qt.Vertical:
- vMin, vMax, _, _ = _utils.addMarginsToLimits(margins, False, False, vMin, vMax, 0, 0)
+ vMin, vMax, _, _ = _utils.addMarginsToLimits(
+ margins, False, False, vMin, vMax, 0, 0
+ )
else:
assert False
@@ -325,10 +338,14 @@ class ShowSideHistogramsAction(PlotAction):
def __init__(self, plot, parent=None):
super(ShowSideHistogramsAction, self).__init__(
- plot, icon='side-histograms', text='Show/hide side histograms',
- tooltip='Show/hide side histogram',
+ plot,
+ icon="side-histograms",
+ text="Show/hide side histograms",
+ tooltip="Show/hide side histogram",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
def _actionTriggered(self, checked=False):
if self.plot.isSideHistogramDisplayed() != checked:
@@ -349,25 +366,33 @@ class AggregationModeAction(qt.QWidgetAction):
filterAction.setText("No filter")
filterAction.setCheckable(True)
filterAction.setChecked(True)
- filterAction.setProperty("aggregation", items.ImageDataAggregated.Aggregation.NONE)
+ filterAction.setProperty(
+ "aggregation", items.ImageDataAggregated.Aggregation.NONE
+ )
densityNoFilterAction = filterAction
filterAction = qt.QAction(self)
filterAction.setText("Max filter")
filterAction.setCheckable(True)
- filterAction.setProperty("aggregation", items.ImageDataAggregated.Aggregation.MAX)
+ filterAction.setProperty(
+ "aggregation", items.ImageDataAggregated.Aggregation.MAX
+ )
densityMaxFilterAction = filterAction
filterAction = qt.QAction(self)
filterAction.setText("Mean filter")
filterAction.setCheckable(True)
- filterAction.setProperty("aggregation", items.ImageDataAggregated.Aggregation.MEAN)
+ filterAction.setProperty(
+ "aggregation", items.ImageDataAggregated.Aggregation.MEAN
+ )
densityMeanFilterAction = filterAction
filterAction = qt.QAction(self)
filterAction.setText("Min filter")
filterAction.setCheckable(True)
- filterAction.setProperty("aggregation", items.ImageDataAggregated.Aggregation.MIN)
+ filterAction.setProperty(
+ "aggregation", items.ImageDataAggregated.Aggregation.MIN
+ )
densityMinFilterAction = filterAction
densityGroup = qt.QActionGroup(self)
@@ -428,7 +453,7 @@ class ImageView(PlotWindow):
:type backend: str or :class:`BackendBase.BackendBase`
"""
- HISTOGRAMS_COLOR = 'blue'
+ HISTOGRAMS_COLOR = "blue"
"""Color to use for the side histograms."""
HISTOGRAMS_HEIGHT = 200
@@ -452,26 +477,37 @@ class ImageView(PlotWindow):
class ProfileWindowBehavior(Enum):
"""ImageView's profile window behavior options"""
- POPUP = 'popup'
+ POPUP = "popup"
"""All profiles are displayed in pop-up windows"""
- EMBEDDED = 'embedded'
+ EMBEDDED = "embedded"
"""Horizontal, vertical and cross profiles are displayed in
sides widgets, others are displayed in pop-up windows.
"""
def __init__(self, parent=None, backend=None):
- self._imageLegend = '__ImageView__image' + str(id(self))
+ self._imageLegend = "__ImageView__image" + str(id(self))
self._cache = None # Store currently visible data information
- super(ImageView, self).__init__(parent=parent, backend=backend,
- resetzoom=True, autoScale=False,
- logScale=False, grid=False,
- curveStyle=False, colormap=True,
- aspectRatio=True, yInverted=True,
- copy=True, save=True, print_=True,
- control=False, position=False,
- roi=False, mask=True)
+ super(ImageView, self).__init__(
+ parent=parent,
+ backend=backend,
+ resetzoom=True,
+ autoScale=False,
+ logScale=False,
+ grid=False,
+ curveStyle=False,
+ colormap=True,
+ aspectRatio=True,
+ yInverted=True,
+ copy=True,
+ save=True,
+ print_=True,
+ control=False,
+ position=False,
+ roi=False,
+ mask=True,
+ )
# Enable mask synchronisation to use it in profiles
maskToolsWidget = self.getMaskToolsDockWidget().widget()
@@ -481,12 +517,14 @@ class ImageView(PlotWindow):
self.__showSideHistogramsAction.setChecked(True)
self.__aggregationModeAction = AggregationModeAction(self)
- self.__aggregationModeAction.sigAggregationModeChanged.connect(self._aggregationModeChanged)
+ self.__aggregationModeAction.sigAggregationModeChanged.connect(
+ self._aggregationModeChanged
+ )
if parent is None:
- self.setWindowTitle('ImageView')
+ self.setWindowTitle("ImageView")
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
+ if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward":
self.getYAxis().setInverted(True)
self._initWidgets(backend)
@@ -501,26 +539,32 @@ class ImageView(PlotWindow):
def _initWidgets(self, backend):
"""Set-up layout and plots."""
- self._histoHPlot = _SideHistogram(backend=backend, parent=self, direction=qt.Qt.Horizontal)
+ self._histoHPlot = _SideHistogram(
+ backend=backend, parent=self, direction=qt.Qt.Horizontal
+ )
widgetHandle = self._histoHPlot.getWidgetHandle()
widgetHandle.setMinimumHeight(self.HISTOGRAMS_HEIGHT)
widgetHandle.setMaximumHeight(self.HISTOGRAMS_HEIGHT)
- self._histoHPlot.setInteractiveMode('zoom')
- self._histoHPlot.setDataMargins(0., 0., 0.1, 0.1)
+ self._histoHPlot.setInteractiveMode("zoom")
+ self._histoHPlot.setDataMargins(0.0, 0.0, 0.1, 0.1)
self._histoHPlot.sigMouseMoved.connect(self._mouseMovedOnHistoH)
self._histoHPlot.setProfileColor(self.HISTOGRAMS_COLOR)
- self._histoVPlot = _SideHistogram(backend=backend, parent=self, direction=qt.Qt.Vertical)
+ self._histoVPlot = _SideHistogram(
+ backend=backend, parent=self, direction=qt.Qt.Vertical
+ )
widgetHandle = self._histoVPlot.getWidgetHandle()
widgetHandle.setMinimumWidth(self.HISTOGRAMS_HEIGHT)
widgetHandle.setMaximumWidth(self.HISTOGRAMS_HEIGHT)
- self._histoVPlot.setInteractiveMode('zoom')
- self._histoVPlot.setDataMargins(0.1, 0.1, 0., 0.)
+ # Trick to align the histogram to the main plot
+ self._histoVPlot.setGraphTitle(" ")
+ self._histoVPlot.setInteractiveMode("zoom")
+ self._histoVPlot.setDataMargins(0.1, 0.1, 0.0, 0.0)
self._histoVPlot.sigMouseMoved.connect(self._mouseMovedOnHistoV)
self._histoVPlot.setProfileColor(self.HISTOGRAMS_COLOR)
self.setPanWithArrowKeys(True)
- self.setInteractiveMode('zoom') # Color set in setColormap
+ self.setInteractiveMode("zoom") # Color set in setColormap
self.sigPlotSignal.connect(self._imagePlotCB)
self.sigActiveImageChanged.connect(self._activeImageChangedSlot)
@@ -604,7 +648,7 @@ class ImageView(PlotWindow):
def isSideHistogramDisplayed(self):
"""True if the side histograms are displayed"""
- return self._histoHPlot.isVisible()
+ return self._histoHPlot.isVisibleTo(self)
def _updateHistograms(self):
"""Update histograms content using current active image."""
@@ -625,7 +669,7 @@ class ImageView(PlotWindow):
def _imagePlotCB(self, eventDict):
"""Callback for imageView plot events."""
- if eventDict['event'] == 'mouseMoved':
+ if eventDict["event"] == "mouseMoved":
activeImage = self.getActiveImage()
if activeImage is not None:
data = activeImage.getData(copy=False)
@@ -634,16 +678,14 @@ class ImageView(PlotWindow):
# Get corresponding coordinate in image
origin = activeImage.getOrigin()
scale = activeImage.getScale()
- if (eventDict['x'] >= origin[0] and
- eventDict['y'] >= origin[1]):
- x = int((eventDict['x'] - origin[0]) / scale[0])
- y = int((eventDict['y'] - origin[1]) / scale[1])
+ if eventDict["x"] >= origin[0] and eventDict["y"] >= origin[1]:
+ x = int((eventDict["x"] - origin[0]) / scale[0])
+ y = int((eventDict["y"] - origin[1]) / scale[1])
if x >= 0 and x < width and y >= 0 and y < height:
- self.valueChanged.emit(float(x), float(y),
- data[y][x])
+ self.valueChanged.emit(float(x), float(y), data[y][x])
- elif eventDict['event'] == 'limitsChanged':
+ elif eventDict["event"] == "limitsChanged":
self._updateHistograms()
def _mouseMovedOnHistoH(self, x, y):
@@ -663,9 +705,10 @@ class ImageView(PlotWindow):
column = int((x - minValue) / xScale)
if column >= 0 and column < data.shape[0]:
self.valueChanged.emit(
- float('nan'),
+ float("nan"),
float(column + self._cache.dataXRange[0]),
- data[column])
+ data[column],
+ )
def _mouseMovedOnHistoV(self, x, y):
if self._cache is None:
@@ -684,9 +727,8 @@ class ImageView(PlotWindow):
row = int((y - minValue) / yScale)
if row >= 0 and row < data.shape[0]:
self.valueChanged.emit(
- float(row + self._cache.dataYRange[0]),
- float('nan'),
- data[row])
+ float(row + self._cache.dataYRange[0]), float("nan"), data[row]
+ )
def _activeImageChangedSlot(self, previous, legend):
"""Handle Plot active image change.
@@ -733,7 +775,7 @@ class ImageView(PlotWindow):
return self.__profileWindowBehavior
def getProfileToolBar(self):
- """"Returns profile tools attached to this plot.
+ """Returns profile tools attached to this plot.
:rtype: silx.gui.plot.PlotTools.ProfileToolBar
"""
@@ -757,18 +799,20 @@ class ImageView(PlotWindow):
:return: The histogram and its extent as a dict or None.
:rtype: dict
"""
- assert axis in ('x', 'y')
+ assert axis in ("x", "y")
if self._cache is None:
return None
else:
- if axis == 'x':
+ if axis == "x":
return dict(
data=numpy.array(self._cache.histoH, copy=True),
- extent=self._cache.dataXRange)
+ extent=self._cache.dataXRange,
+ )
else:
return dict(
data=numpy.array(self._cache.histoV, copy=True),
- extent=(self._cache.dataYRange))
+ extent=(self._cache.dataYRange),
+ )
def radarView(self):
"""Get the lower right radarView widget."""
@@ -795,8 +839,15 @@ class ImageView(PlotWindow):
"""
return self.getDefaultColormap()
- def setColormap(self, colormap=None, normalization=None,
- autoscale=None, vmin=None, vmax=None, colors=None):
+ def setColormap(
+ self,
+ colormap=None,
+ normalization=None,
+ autoscale=None,
+ vmin=None,
+ vmax=None,
+ colors=None,
+ ):
"""Set the default colormap and update active image.
Parameters that are not provided are taken from the current colormap.
@@ -868,10 +919,17 @@ class ImageView(PlotWindow):
cmap.setColormapLUT(colors)
cursorColor = cursorColorForColormap(cmap.getName())
- self.setInteractiveMode('zoom', color=cursorColor)
-
- def setImage(self, image, origin=(0, 0), scale=(1., 1.),
- copy=True, reset=None, resetzoom=True):
+ self.setInteractiveMode("zoom", color=cursorColor)
+
+ def setImage(
+ self,
+ image,
+ origin=(0, 0),
+ scale=(1.0, 1.0),
+ copy=True,
+ reset=None,
+ resetzoom=True,
+ ):
"""Set the image to display.
:param image: A 2D array representing the image or None to empty plot.
@@ -901,12 +959,12 @@ class ImageView(PlotWindow):
assert scale[1] > 0
if image is None:
- self.remove(self._imageLegend, kind='image')
+ self.remove(self._imageLegend, kind="image")
return
- data = numpy.array(image, order='C', copy=copy)
+ data = numpy.array(image, order="C", copy=copy)
if data.size == 0:
- self.remove(self._imageLegend, kind='image')
+ self.remove(self._imageLegend, kind="image")
return
assert data.ndim == 2 or (data.ndim == 3 and data.shape[2] in (3, 4))
@@ -917,11 +975,14 @@ class ImageView(PlotWindow):
aggregation = items.ImageDataAggregated.Aggregation.NONE
if aggregation is items.ImageDataAggregated.Aggregation.NONE:
- self.addImage(data,
- legend=self._imageLegend,
- origin=origin, scale=scale,
- colormap=self.getColormap(),
- resetzoom=False)
+ self.addImage(
+ data,
+ legend=self._imageLegend,
+ origin=origin,
+ scale=scale,
+ colormap=self.getColormap(),
+ resetzoom=False,
+ )
else:
item = self._getItem("image", self._imageLegend)
if isinstance(item, items.ImageDataAggregated):
@@ -954,31 +1015,33 @@ class ImageView(PlotWindow):
# ImageViewMainWindow #########################################################
+
class ImageViewMainWindow(ImageView):
""":class:`ImageView` with additional toolbars
Adds extra toolbar and a status bar to :class:`ImageView`.
"""
+
def __init__(self, parent=None, backend=None):
self._dataInfo = None
super(ImageViewMainWindow, self).__init__(parent, backend)
self.setWindowFlags(qt.Qt.Window)
- self.getXAxis().setLabel('X')
- self.getYAxis().setLabel('Y')
- self.setGraphTitle('Image')
+ self.getXAxis().setLabel("X")
+ self.getYAxis().setLabel("Y")
+ self.setGraphTitle("Image")
# Add toolbars and status bar
self.addToolBar(qt.Qt.BottomToolBarArea, LimitsToolBar(plot=self))
- menu = self.menuBar().addMenu('File')
+ menu = self.menuBar().addMenu("File")
menu.addAction(self.getOutputToolBar().getSaveAction())
menu.addAction(self.getOutputToolBar().getPrintAction())
menu.addSeparator()
- action = menu.addAction('Quit')
+ action = menu.addAction("Quit")
action.triggered[bool].connect(qt.QApplication.instance().quit)
- menu = self.menuBar().addMenu('Edit')
+ menu = self.menuBar().addMenu("Edit")
menu.addAction(self.getOutputToolBar().getCopyAction())
menu.addSeparator()
menu.addAction(self.getResetZoomAction())
@@ -987,7 +1050,7 @@ class ImageViewMainWindow(ImageView):
menu.addAction(actions.control.YAxisInvertedAction(self, self))
menu.addAction(self.getShowSideHistogramsAction())
- self.__profileMenu = self.menuBar().addMenu('Profile')
+ self.__profileMenu = self.menuBar().addMenu("Profile")
self.__updateProfileMenu()
# Connect to ImageView's signal
@@ -1007,7 +1070,12 @@ class ImageViewMainWindow(ImageView):
try:
if isinstance(value, numpy.ndarray):
if len(value) == 4:
- return "RGBA: %.3g, %.3g, %.3g, %.3g" % (value[0], value[1], value[2], value[3])
+ return "RGBA: %.3g, %.3g, %.3g, %.3g" % (
+ value[0],
+ value[1],
+ value[2],
+ value[3],
+ )
elif len(value) == 3:
return "RGB: %.3g, %.3g, %.3g" % (value[0], value[1], value[2])
else:
@@ -1020,14 +1088,14 @@ class ImageViewMainWindow(ImageView):
def _statusBarSlot(self, row, column, value):
"""Update status bar with coordinates/value from plots."""
if numpy.isnan(row):
- msg = 'Column: %d, Sum: %g' % (int(column), value)
+ msg = "Column: %d, Sum: %g" % (int(column), value)
elif numpy.isnan(column):
- msg = 'Row: %d, Sum: %g' % (int(row), value)
+ msg = "Row: %d, Sum: %g" % (int(row), value)
else:
msg_value = self._formatValueToString(value)
- msg = 'Position: (%d, %d), %s' % (int(row), int(column), msg_value)
+ msg = "Position: (%d, %d), %s" % (int(row), int(column), msg_value)
if self._dataInfo is not None:
- msg = self._dataInfo + ', ' + msg
+ msg = self._dataInfo + ", " + msg
self.statusBar().showMessage(msg)
@@ -1038,10 +1106,10 @@ class ImageViewMainWindow(ImageView):
@docstring(ImageView)
def setImage(self, image, *args, **kwargs):
- if hasattr(image, 'dtype') and hasattr(image, 'shape'):
+ if hasattr(image, "dtype") and hasattr(image, "shape"):
assert image.ndim == 2 or (image.ndim == 3 and image.shape[2] in (3, 4))
height, width = image.shape[0:2]
- dataInfo = 'Data: %dx%d (%s)' % (width, height, str(image.dtype))
+ dataInfo = "Data: %dx%d (%s)" % (width, height, str(image.dtype))
else:
dataInfo = None
diff --git a/src/silx/gui/plot/Interaction.py b/src/silx/gui/plot/Interaction.py
index 053fbe5..2d8bf63 100644
--- a/src/silx/gui/plot/Interaction.py
+++ b/src/silx/gui/plot/Interaction.py
@@ -84,6 +84,7 @@ import weakref
# state machine ###############################################################
+
class State(object):
"""Base class for the states of a state machine.
@@ -142,6 +143,7 @@ class State(object):
"""
pass
+
class StateMachine(object):
"""State machine controller.
@@ -184,7 +186,7 @@ class StateMachine(object):
:param str eventName: Name of the event to handle
:returns: The return value of the handler or None
"""
- handlerName = 'on' + eventName[0].upper() + eventName[1:]
+ handlerName = "on" + eventName[0].upper() + eventName[1:]
try:
handler = getattr(self.state, handlerName)
except AttributeError:
@@ -204,13 +206,13 @@ class StateMachine(object):
# clickOrDrag #################################################################
-LEFT_BTN = 'left'
+LEFT_BTN = "left"
"""Left mouse button."""
-RIGHT_BTN = 'right'
+RIGHT_BTN = "right"
"""Right mouse button."""
-MIDDLE_BTN = 'middle'
+MIDDLE_BTN = "middle"
"""Middle mouse button."""
@@ -224,15 +226,15 @@ class ClickOrDrag(StateMachine):
:param Set[str] dragButtons: Set of buttons that provides drag interaction
"""
- DRAG_THRESHOLD_SQUARE_DIST = 5 ** 2
+ DRAG_THRESHOLD_SQUARE_DIST = 5**2
class Idle(State):
def onPress(self, x, y, btn):
if btn in self.machine.dragButtons:
- self.goto('clickOrDrag', x, y, btn)
+ self.goto("clickOrDrag", x, y, btn)
return True
elif btn in self.machine.clickButtons:
- self.goto('click', x, y, btn)
+ self.goto("click", x, y, btn)
return True
class Click(State):
@@ -244,12 +246,12 @@ class ClickOrDrag(StateMachine):
dx2 = (x - self.initPos[0]) ** 2
dy2 = (y - self.initPos[1]) ** 2
if (dx2 + dy2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
- self.goto('idle')
+ self.goto("idle")
def onRelease(self, x, y, btn):
if btn == self.button:
self.machine.click(x, y, btn)
- self.goto('idle')
+ self.goto("idle")
class ClickOrDrag(State):
def enterState(self, x, y, btn):
@@ -260,13 +262,13 @@ class ClickOrDrag(StateMachine):
dx2 = (x - self.initPos[0]) ** 2
dy2 = (y - self.initPos[1]) ** 2
if (dx2 + dy2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
- self.goto('drag', self.initPos, (x, y), self.button)
+ self.goto("drag", self.initPos, (x, y), self.button)
def onRelease(self, x, y, btn):
if btn == self.button:
if btn in self.machine.clickButtons:
self.machine.click(x, y, btn)
- self.goto('idle')
+ self.goto("idle")
class Drag(State):
def enterState(self, initPos, curPos, btn):
@@ -281,26 +283,27 @@ class ClickOrDrag(StateMachine):
def onRelease(self, x, y, btn):
if btn == self.button:
self.machine.endDrag(self.initPos, (x, y), btn)
- self.goto('idle')
+ self.goto("idle")
- def __init__(self,
- clickButtons=(LEFT_BTN, RIGHT_BTN),
- dragButtons=(LEFT_BTN,)):
+ def __init__(self, clickButtons=(LEFT_BTN, RIGHT_BTN), dragButtons=(LEFT_BTN,)):
states = {
- 'idle': self.Idle,
- 'click': self.Click,
- 'clickOrDrag': self.ClickOrDrag,
- 'drag': self.Drag
+ "idle": self.Idle,
+ "click": self.Click,
+ "clickOrDrag": self.ClickOrDrag,
+ "drag": self.Drag,
}
self.__clickButtons = set(clickButtons)
self.__dragButtons = set(dragButtons)
- super(ClickOrDrag, self).__init__(states, 'idle')
+ super(ClickOrDrag, self).__init__(states, "idle")
- clickButtons = property(lambda self: self.__clickButtons,
- doc="Buttons with click interaction (Set[int])")
+ clickButtons = property(
+ lambda self: self.__clickButtons,
+ doc="Buttons with click interaction (Set[int])",
+ )
- dragButtons = property(lambda self: self.__dragButtons,
- doc="Buttons with drag interaction (Set[int])")
+ dragButtons = property(
+ lambda self: self.__dragButtons, doc="Buttons with drag interaction (Set[int])"
+ )
def click(self, x, y, btn):
"""Called upon a button supporting click.
diff --git a/src/silx/gui/plot/ItemsSelectionDialog.py b/src/silx/gui/plot/ItemsSelectionDialog.py
index c303c6b..b4e4f9e 100644
--- a/src/silx/gui/plot/ItemsSelectionDialog.py
+++ b/src/silx/gui/plot/ItemsSelectionDialog.py
@@ -43,6 +43,7 @@ class KindsSelector(qt.QListWidget):
"""List widget allowing to select plot item kinds
("curve", "scatter", "image"...)
"""
+
sigSelectedKindsChanged = qt.Signal(list)
def __init__(self, parent=None, kinds=None):
@@ -87,8 +88,10 @@ class KindsSelector(qt.QListWidget):
def selectAll(self):
"""Select all available kinds."""
- if self.selectionMode() in [qt.QAbstractItemView.SingleSelection,
- qt.QAbstractItemView.NoSelection]:
+ if self.selectionMode() in [
+ qt.QAbstractItemView.SingleSelection,
+ qt.QAbstractItemView.NoSelection,
+ ]:
raise RuntimeError("selectAll requires a multiple selection mode")
for i in range(self.count()):
self.item(i).setSelected(True)
@@ -102,6 +105,7 @@ class PlotItemsSelector(qt.QTableWidget):
You can be warned of selection changes by listening to signal
:attr:`itemSelectionChanged`.
"""
+
def __init__(self, parent=None, plot=None):
if plot is None or not isinstance(plot, PlotWidget):
raise AttributeError("parameter plot is required")
@@ -131,8 +135,9 @@ class PlotItemsSelector(qt.QTableWidget):
:param list[str] kinds: Sequence of kinds
"""
if not set(kinds) <= set(PlotWidget.ITEM_KINDS):
- raise KeyError("Illegal plot item kinds: %s" %
- set(kinds) - set(PlotWidget.ITEM_KINDS))
+ raise KeyError(
+ "Illegal plot item kinds: %s" % set(kinds) - set(PlotWidget.ITEM_KINDS)
+ )
self.plot_item_kinds = kinds
self.updatePlotItems()
@@ -199,6 +204,7 @@ class ItemsSelectionDialog(qt.QDialog):
else:
print("Selection cancelled")
"""
+
def __init__(self, parent=None, plot=None):
if plot is None or not isinstance(plot, PlotWidget):
raise AttributeError("parameter plot is required")
@@ -211,7 +217,8 @@ class ItemsSelectionDialog(qt.QDialog):
self.kind_selector = KindsSelector(self)
self.kind_selector.setToolTip(
- "select one or more item kinds to show them in the item list")
+ "select one or more item kinds to show them in the item list"
+ )
self.item_selector = PlotItemsSelector(self, plot)
self.item_selector.setToolTip("select items")
@@ -261,25 +268,26 @@ class ItemsSelectionDialog(qt.QDialog):
:param mode: One of :class:`QTableWidget` selection modes
"""
if mode == self.item_selector.SingleSelection:
- self.item_selector.setToolTip(
- "Select one item by clicking on it.")
+ self.item_selector.setToolTip("Select one item by clicking on it.")
elif mode == self.item_selector.MultiSelection:
self.item_selector.setToolTip(
- "Select one or more items by clicking with the left mouse"
- " button.\nYou can unselect items by clicking them again.\n"
- "Multiple items can be toggled by dragging the mouse over them.")
+ "Select one or more items by clicking with the left mouse"
+ " button.\nYou can unselect items by clicking them again.\n"
+ "Multiple items can be toggled by dragging the mouse over them."
+ )
elif mode == self.item_selector.ExtendedSelection:
self.item_selector.setToolTip(
- "Select one or more items. You can select multiple items "
- "by keeping the Ctrl key pushed when clicking.\nYou can "
- "select a range of items by clicking on the first and "
- "last while keeping the Shift key pushed.")
+ "Select one or more items. You can select multiple items "
+ "by keeping the Ctrl key pushed when clicking.\nYou can "
+ "select a range of items by clicking on the first and "
+ "last while keeping the Shift key pushed."
+ )
elif mode == self.item_selector.ContiguousSelection:
self.item_selector.setToolTip(
- "Select one item by clicking on it. If you press the Shift"
- " key while clicking on a second item,\nall items between "
- "the two will be selected.")
+ "Select one item by clicking on it. If you press the Shift"
+ " key while clicking on a second item,\nall items between "
+ "the two will be selected."
+ )
elif mode == self.item_selector.NoSelection:
- raise ValueError("The NoSelection mode is not allowed "
- "in this context.")
+ raise ValueError("The NoSelection mode is not allowed " "in this context.")
self.item_selector.setSelectionMode(mode)
diff --git a/src/silx/gui/plot/LegendSelector.py b/src/silx/gui/plot/LegendSelector.py
index 4d8ebe9..22348fb 100755
--- a/src/silx/gui/plot/LegendSelector.py
+++ b/src/silx/gui/plot/LegendSelector.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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,6 +39,7 @@ import numpy
from .. import qt, colors
from ..widgets.LegendIconWidget import LegendIconWidget
from . import items
+from ...utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
@@ -86,11 +87,10 @@ class LegendIcon(LegendIconWidget):
self._update()
def _update(self):
- """Update widget according to current curve state.
- """
+ """Update widget according to current curve state."""
curve = self.getCurve()
if curve is None:
- _logger.error('Curve no more exists')
+ _logger.error("Curve no more exists")
self.setEnabled(False)
return
@@ -104,11 +104,10 @@ class LegendIcon(LegendIconWidget):
color = style.getColor()
if numpy.array(color, copy=False).ndim != 1:
# array of colors, use transparent black
- color = 0., 0., 0., 0.
+ color = 0.0, 0.0, 0.0, 0.0
color = colors.rgba(color) # Make sure it is float in [0, 1]
alpha = curve.getAlpha()
- color = qt.QColor.fromRgbF(
- color[0], color[1], color[2], color[3] * alpha)
+ color = qt.QColor.fromRgbF(color[0], color[1], color[2], color[3] * alpha)
self.setLineColor(color)
self.setSymbolColor(color)
self.update() # TODO this should not be needed
@@ -118,15 +117,17 @@ class LegendIcon(LegendIconWidget):
:param event: Kind of change
"""
- if event in (items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SYMBOL,
- items.ItemChangedType.SYMBOL_SIZE,
- items.ItemChangedType.LINE_WIDTH,
- items.ItemChangedType.LINE_STYLE,
- items.ItemChangedType.COLOR,
- items.ItemChangedType.ALPHA,
- items.ItemChangedType.HIGHLIGHTED,
- items.ItemChangedType.HIGHLIGHTED_STYLE):
+ if event in (
+ items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.SYMBOL,
+ items.ItemChangedType.SYMBOL_SIZE,
+ items.ItemChangedType.LINE_WIDTH,
+ items.ItemChangedType.LINE_STYLE,
+ items.ItemChangedType.COLOR,
+ items.ItemChangedType.ALPHA,
+ items.ItemChangedType.HIGHLIGHTED,
+ items.ItemChangedType.HIGHLIGHTED_STYLE,
+ ):
self._update()
@@ -142,12 +143,14 @@ class LegendModel(qt.QAbstractListModel):
- symbol
- visibility of the symbols
"""
+
iconColorRole = qt.Qt.UserRole + 0
iconLineWidthRole = qt.Qt.UserRole + 1
iconLineStyleRole = qt.Qt.UserRole + 2
showLineRole = qt.Qt.UserRole + 3
iconSymbolRole = qt.Qt.UserRole + 4
showSymbolRole = qt.Qt.UserRole + 5
+ itemRole = qt.Qt.UserRole + 6
def __init__(self, legendList=None, parent=None):
super(LegendModel, self).__init__(parent)
@@ -159,16 +162,14 @@ class LegendModel(qt.QAbstractListModel):
def __getitem__(self, idx):
if idx >= len(self.legendList):
- raise IndexError('list index out of range')
+ raise IndexError("list index out of range")
return self.legendList[idx]
def rowCount(self, modelIndex=None):
return len(self.legendList)
def flags(self, index):
- return (qt.Qt.ItemIsEditable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsSelectable)
+ return qt.Qt.ItemIsEditable | qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable
def data(self, modelIndex, role):
if modelIndex.isValid:
@@ -176,7 +177,7 @@ class LegendModel(qt.QAbstractListModel):
else:
return None
if idx >= len(self.legendList):
- raise IndexError('list index out of range')
+ raise IndexError("list index out of range")
item = self.legendList[idx]
isActive = item[1].get("active", False)
@@ -186,7 +187,7 @@ class LegendModel(qt.QAbstractListModel):
return legend
elif role == qt.Qt.SizeHintRole:
# size = qt.QSize(200,50)
- _logger.warning('LegendModel -- size hint role not implemented')
+ _logger.warning("LegendModel -- size hint role not implemented")
return qt.QSize()
elif role == qt.Qt.TextAlignmentRole:
alignment = qt.Qt.AlignVCenter | qt.Qt.AlignLeft
@@ -194,7 +195,7 @@ class LegendModel(qt.QAbstractListModel):
elif role == qt.Qt.BackgroundRole:
# Background color, must be QBrush
if isActive:
- brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.Highlight)
+ brush = self._palette.brush(qt.QPalette.Active, qt.QPalette.Highlight)
elif idx % 2:
brush = qt.QBrush(qt.QColor(240, 240, 240))
else:
@@ -203,28 +204,32 @@ class LegendModel(qt.QAbstractListModel):
elif role == qt.Qt.ForegroundRole:
# ForegroundRole color, must be QBrush
if isActive:
- brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.HighlightedText)
+ brush = self._palette.brush(
+ qt.QPalette.Active, qt.QPalette.HighlightedText
+ )
else:
- brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.WindowText)
+ brush = self._palette.brush(qt.QPalette.Active, qt.QPalette.WindowText)
return brush
elif role == qt.Qt.CheckStateRole:
return bool(item[2]) # item[2] == True
elif role == qt.Qt.ToolTipRole or role == qt.Qt.StatusTipRole:
- return ''
+ return ""
elif role == self.iconColorRole:
- return item[1]['color']
+ return item[1]["color"]
elif role == self.iconLineWidthRole:
- return item[1]['linewidth']
+ return item[1]["linewidth"]
elif role == self.iconLineStyleRole:
- return item[1]['linestyle']
+ return item[1]["linestyle"]
elif role == self.iconSymbolRole:
- return item[1]['symbol']
+ return item[1]["symbol"]
elif role == self.showLineRole:
return item[3]
elif role == self.showSymbolRole:
return item[4]
+ elif role == self.itemRole:
+ return item[5]
else:
- _logger.info('Unkown role requested: %s', str(role))
+ _logger.info("Unkown role requested: %s", str(role))
return None
def setData(self, modelIndex, value, role):
@@ -234,8 +239,7 @@ class LegendModel(qt.QAbstractListModel):
return None
if idx >= len(self.legendList):
# raise IndexError('list index out of range')
- _logger.warning(
- 'setData -- List index out of range, idx: %d', idx)
+ _logger.warning("setData -- List index out of range, idx: %d", idx)
return None
item = self.legendList[idx]
@@ -244,22 +248,25 @@ class LegendModel(qt.QAbstractListModel):
# Set legend
item[0] = str(value)
elif role == self.iconColorRole:
- item[1]['color'] = qt.QColor(value)
+ item[1]["color"] = qt.QColor(value)
elif role == self.iconLineWidthRole:
- item[1]['linewidth'] = int(value)
+ item[1]["linewidth"] = int(value)
elif role == self.iconLineStyleRole:
- item[1]['linestyle'] = str(value)
+ item[1]["linestyle"] = value
elif role == self.iconSymbolRole:
- item[1]['symbol'] = str(value)
+ item[1]["symbol"] = str(value)
elif role == qt.Qt.CheckStateRole:
item[2] = value
elif role == self.showLineRole:
item[3] = value
elif role == self.showSymbolRole:
item[4] = value
+ elif role == self.itemRole:
+ item[5] = value
except ValueError:
- _logger.warning('Conversion failed:\n\tvalue: %s\n\trole: %s',
- str(value), str(role))
+ _logger.warning(
+ "Conversion failed:\n\tvalue: %s\n\trole: %s", str(value), str(role)
+ )
# Can that be right? Read docs again..
self.dataChanged.emit(modelIndex, modelIndex)
return True
@@ -272,44 +279,45 @@ class LegendModel(qt.QAbstractListModel):
"""
modelIndex = self.createIndex(row, 0)
count = len(llist)
- super(LegendModel, self).beginInsertRows(modelIndex,
- row,
- row + count)
+ super(LegendModel, self).beginInsertRows(modelIndex, row, row + count)
head = self.legendList[0:row]
tail = self.legendList[row:]
new = []
- for (legend, icon) in llist:
- linestyle = icon.get('linestyle', None)
+ for legend, icon in llist:
+ linestyle = icon.get("linestyle", None)
if LegendIconWidget.isEmptyLineStyle(linestyle):
# Curve had no line, give it one and hide it
# So when toggle line, it will display a solid line
showLine = False
- icon['linestyle'] = '-'
+ icon["linestyle"] = "-"
else:
showLine = True
- symbol = icon.get('symbol', None)
+ symbol = icon.get("symbol", None)
if LegendIconWidget.isEmptySymbol(symbol):
# Curve had no symbol, give it one and hide it
# So when toggle symbol, it will display 'o'
showSymbol = False
- icon['symbol'] = 'o'
+ icon["symbol"] = "o"
else:
showSymbol = True
- selected = icon.get('selected', True)
- item = [legend,
- icon,
- selected,
- showLine,
- showSymbol]
+ selected = icon.get("selected", True)
+ item = [
+ legend,
+ icon,
+ selected,
+ showLine,
+ showSymbol,
+ icon.get("item", None),
+ ]
new.append(item)
self.legendList = head + new + tail
super(LegendModel, self).endInsertRows()
return True
def insertRows(self, row, count, modelIndex=qt.QModelIndex()):
- raise NotImplementedError('Use LegendModel.insertLegendList instead')
+ raise NotImplementedError("Use LegendModel.insertLegendList instead")
def removeRow(self, row):
return self.removeRows(row, 1)
@@ -320,14 +328,13 @@ class LegendModel(qt.QAbstractListModel):
# Nothing to do..
return True
if row < 0 or row >= length:
- raise IndexError('Index out of range -- ' +
- 'idx: %d, len: %d' % (row, length))
+ raise IndexError(
+ "Index out of range -- " + "idx: %d, len: %d" % (row, length)
+ )
if count == 0:
return False
- super(LegendModel, self).beginRemoveRows(modelIndex,
- row,
- row + count)
- del(self.legendList[row:row + count])
+ super(LegendModel, self).beginRemoveRows(modelIndex, row, row + count)
+ del self.legendList[row : row + count]
super(LegendModel, self).endRemoveRows()
return True
@@ -338,8 +345,7 @@ class LegendModel(qt.QAbstractListModel):
:type editor: QWidget
"""
if event not in self.eventList:
- raise ValueError('setEditor -- Event must be in %s' %
- str(self.eventList))
+ raise ValueError("setEditor -- Event must be in %s" % str(self.eventList))
self.editorDict[event] = editor
@@ -380,12 +386,11 @@ class LegendListItemWidget(qt.QItemDelegate):
iconSize = self.icon.sizeHint()
# Calculate icon position
x = rect.left() + 2
- y = rect.top() + int(.5 * (rect.height() - iconSize.height()))
+ y = rect.top() + int(0.5 * (rect.height() - iconSize.height()))
iconRect = qt.QRect(qt.QPoint(x, y), iconSize)
# Calculate label rectangle
- legendSize = qt.QSize(rect.width() - iconSize.width() - 30,
- rect.height())
+ legendSize = qt.QSize(rect.width() - iconSize.width() - 30, rect.height())
# Calculate label position
x = rect.left() + iconRect.width()
y = rect.top()
@@ -443,8 +448,7 @@ class LegendListItemWidget(qt.QItemDelegate):
else:
checkState = qt.Qt.Unchecked
- self.drawCheck(
- painter, qt.QStyleOptionViewItem(), chBoxRect, checkState)
+ self.drawCheck(painter, qt.QStyleOptionViewItem(), chBoxRect, checkState)
painter.restore()
@@ -453,7 +457,11 @@ class LegendListItemWidget(qt.QItemDelegate):
# Mouse events are sent to editorEvent()
# even if they don't start editing of the item.
if event.button() == qt.Qt.RightButton and self.contextMenu:
- self.contextMenu.exec(event.globalPos(), modelIndex)
+ if qt.BINDING == "PyQt5":
+ position = event.globalPos()
+ else: # Qt6
+ position = event.globalPosition().toPoint()
+ self.contextMenu.exec(position, modelIndex)
return True
elif event.button() == qt.Qt.LeftButton:
# Check if checkbox was clicked
@@ -461,26 +469,29 @@ class LegendListItemWidget(qt.QItemDelegate):
cbRect = self.cbDict[idx]
if cbRect.contains(event.pos()):
# Toggle checkbox
- model.setData(modelIndex,
- not modelIndex.data(qt.Qt.CheckStateRole),
- qt.Qt.CheckStateRole)
+ model.setData(
+ modelIndex,
+ not modelIndex.data(qt.Qt.CheckStateRole),
+ qt.Qt.CheckStateRole,
+ )
event.ignore()
return True
else:
return super(LegendListItemWidget, self).editorEvent(
- event, model, option, modelIndex)
+ event, model, option, modelIndex
+ )
def createEditor(self, parent, option, idx):
- _logger.info('### Editor request ###')
+ _logger.info("### Editor request ###")
def sizeHint(self, option, idx):
# return qt.QSize(68,24)
iconSize = self.icon.sizeHint()
legendSize = self.legend.sizeHint()
checkboxSize = self.checkbox.sizeHint()
- height = max([iconSize.height(),
- legendSize.height(),
- checkboxSize.height()]) + 4
+ height = (
+ max([iconSize.height(), legendSize.height(), checkboxSize.height()]) + 4
+ )
width = iconSize.width() + legendSize.width() + checkboxSize.width()
return qt.QSize(width, height)
@@ -491,9 +502,9 @@ class LegendListView(qt.QListView):
sigLegendSignal = qt.Signal(object)
"""Signal emitting a dict when an action is triggered by the user."""
- __mouseClickedEvent = 'mouseClicked'
- __checkBoxClickedEvent = 'checkBoxClicked'
- __legendClickedEvent = 'legendClicked'
+ __mouseClickedEvent = "mouseClicked"
+ __checkBoxClickedEvent = "checkBoxClicked"
+ __legendClickedEvent = "legendClicked"
def __init__(self, parent=None, model=None, contextMenu=None):
super(LegendListView, self).__init__(parent)
@@ -539,47 +550,55 @@ class LegendListView(qt.QListView):
model.setData(modelIndex, new_legend, qt.Qt.DisplayRole)
color = modelIndex.data(LegendModel.iconColorRole)
- new_color = icon.get('color', None)
+ new_color = icon.get("color", None)
if new_color != color:
model.setData(modelIndex, new_color, LegendModel.iconColorRole)
linewidth = modelIndex.data(LegendModel.iconLineWidthRole)
- new_linewidth = icon.get('linewidth', 1.0)
+ new_linewidth = icon.get("linewidth", 1.0)
if new_linewidth != linewidth:
- model.setData(modelIndex, new_linewidth, LegendModel.iconLineWidthRole)
+ model.setData(
+ modelIndex, new_linewidth, LegendModel.iconLineWidthRole
+ )
linestyle = modelIndex.data(LegendModel.iconLineStyleRole)
- new_linestyle = icon.get('linestyle', None)
+ new_linestyle = icon.get("linestyle", None)
visible = not LegendIconWidget.isEmptyLineStyle(new_linestyle)
model.setData(modelIndex, visible, LegendModel.showLineRole)
if new_linestyle != linestyle:
- model.setData(modelIndex, new_linestyle, LegendModel.iconLineStyleRole)
+ model.setData(
+ modelIndex, new_linestyle, LegendModel.iconLineStyleRole
+ )
symbol = modelIndex.data(LegendModel.iconSymbolRole)
- new_symbol = icon.get('symbol', None)
+ new_symbol = icon.get("symbol", None)
visible = not LegendIconWidget.isEmptySymbol(new_symbol)
model.setData(modelIndex, visible, LegendModel.showSymbolRole)
if new_symbol != symbol:
model.setData(modelIndex, new_symbol, LegendModel.iconSymbolRole)
selected = modelIndex.data(qt.Qt.CheckStateRole)
- new_selected = icon.get('selected', True)
+ new_selected = icon.get("selected", True)
if new_selected != selected:
model.setData(modelIndex, new_selected, qt.Qt.CheckStateRole)
- _logger.debug('LegendListView.setLegendList(legendList) finished')
+
+ item = modelIndex.data(LegendModel.itemRole)
+ newItem = icon.get("item", None)
+ if item is not newItem:
+ model.setData(modelIndex, newItem, LegendModel.itemRole)
+ _logger.debug("LegendListView.setLegendList(legendList) finished")
def clear(self):
model = self.model()
model.removeRows(0, model.rowCount())
- _logger.debug('LegendListView.clear() finished')
+ _logger.debug("LegendListView.clear() finished")
def setContextMenu(self, contextMenu=None):
delegate = self.itemDelegate()
if isinstance(delegate, LegendListItemWidget) and self.model():
if contextMenu is None:
delegate.contextMenu = LegendListContextMenu(self.model())
- delegate.contextMenu.sigContextMenu.connect(
- self._contextMenuSlot)
+ delegate.contextMenu.sigContextMenu.connect(self._contextMenuSlot)
else:
delegate.contextMenu = contextMenu
@@ -632,12 +651,11 @@ class LegendListView(qt.QListView):
:param QModelIndex modelIndex: index of the clicked item
"""
- _logger.debug('self._handleMouseClick called')
- if self.__lastButton not in [qt.Qt.LeftButton,
- qt.Qt.RightButton]:
+ _logger.debug("self._handleMouseClick called")
+ if self.__lastButton not in [qt.Qt.LeftButton, qt.Qt.RightButton]:
return
if not modelIndex.isValid():
- _logger.debug('_handleMouseClick -- Invalid QModelIndex')
+ _logger.debug("_handleMouseClick -- Invalid QModelIndex")
return
# model = self.model()
idx = modelIndex.row()
@@ -653,30 +671,29 @@ class LegendListView(qt.QListView):
# TODO: Check for doubleclicks on legend/icon and spawn editors
ddict = {
- 'legend': str(modelIndex.data(qt.Qt.DisplayRole)),
- 'icon': {
- 'linewidth': str(modelIndex.data(
- LegendModel.iconLineWidthRole)),
- 'linestyle': str(modelIndex.data(
- LegendModel.iconLineStyleRole)),
- 'symbol': str(modelIndex.data(LegendModel.iconSymbolRole))
+ "legend": str(modelIndex.data(qt.Qt.DisplayRole)),
+ "icon": {
+ "linewidth": str(modelIndex.data(LegendModel.iconLineWidthRole)),
+ "linestyle": modelIndex.data(LegendModel.iconLineStyleRole),
+ "symbol": str(modelIndex.data(LegendModel.iconSymbolRole)),
},
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data())
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
}
if self.__lastButton == qt.Qt.RightButton:
- _logger.debug('Right clicked')
- ddict['button'] = "right"
- ddict['event'] = self.__mouseClickedEvent
+ _logger.debug("Right clicked")
+ ddict["button"] = "right"
+ ddict["event"] = self.__mouseClickedEvent
elif cbClicked:
- _logger.debug('CheckBox clicked')
- ddict['button'] = "left"
- ddict['event'] = self.__checkBoxClickedEvent
+ _logger.debug("CheckBox clicked")
+ ddict["button"] = "left"
+ ddict["event"] = self.__checkBoxClickedEvent
else:
- _logger.debug('Legend clicked')
- ddict['button'] = "left"
- ddict['event'] = self.__legendClickedEvent
- _logger.debug(' idx: %d\n ddict: %s', idx, str(ddict))
+ _logger.debug("Legend clicked")
+ ddict["button"] = "left"
+ ddict["event"] = self.__legendClickedEvent
+ _logger.debug(" idx: %d\n ddict: %s", idx, str(ddict))
self.sigLegendSignal.emit(ddict)
@@ -690,29 +707,26 @@ class LegendListContextMenu(qt.QMenu):
super(LegendListContextMenu, self).__init__(parent=None)
self.model = model
- self.addAction('Set Active', self.setActiveAction)
- self.addAction('Map to left', self.mapToLeftAction)
- self.addAction('Map to right', self.mapToRightAction)
+ self.addAction("Set Active", self.setActiveAction)
+ self.addAction("Map to left", self.mapToLeftAction)
+ self.addAction("Map to right", self.mapToRightAction)
- self._pointsAction = self.addAction(
- 'Points', self.togglePointsAction)
+ self._pointsAction = self.addAction("Points", self.togglePointsAction)
self._pointsAction.setCheckable(True)
- self._linesAction = self.addAction('Lines', self.toggleLinesAction)
+ self._linesAction = self.addAction("Lines", self.toggleLinesAction)
self._linesAction.setCheckable(True)
- self.addAction('Remove curve', self.removeItemAction)
- self.addAction('Rename curve', self.renameItemAction)
+ self.addAction("Remove curve", self.removeItemAction)
+ self.addAction("Rename curve", self.renameItemAction)
def exec(self, pos, idx):
self.__currentIdx = idx
# Set checkable action state
modelIndex = self.currentIdx()
- self._pointsAction.setChecked(
- modelIndex.data(LegendModel.showSymbolRole))
- self._linesAction.setChecked(
- modelIndex.data(LegendModel.showLineRole))
+ self._pointsAction.setChecked(modelIndex.data(LegendModel.showSymbolRole))
+ self._linesAction.setChecked(modelIndex.data(LegendModel.showLineRole))
super(LegendListContextMenu, self).popup(pos)
@@ -723,55 +737,59 @@ class LegendListContextMenu(qt.QMenu):
return self.__currentIdx
def mapToLeftAction(self):
- _logger.debug('LegendListContextMenu.mapToLeftAction called')
+ _logger.debug("LegendListContextMenu.mapToLeftAction called")
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "mapToLeft"
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "event": "mapToLeft",
}
self.sigContextMenu.emit(ddict)
def mapToRightAction(self):
- _logger.debug('LegendListContextMenu.mapToRightAction called')
+ _logger.debug("LegendListContextMenu.mapToRightAction called")
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "mapToRight"
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "event": "mapToRight",
}
self.sigContextMenu.emit(ddict)
def removeItemAction(self):
- _logger.debug('LegendListContextMenu.removeCurveAction called')
+ _logger.debug("LegendListContextMenu.removeCurveAction called")
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "removeCurve"
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "event": "removeCurve",
}
self.model.removeRow(modelIndex.row())
self.sigContextMenu.emit(ddict)
def renameItemAction(self):
- _logger.debug('LegendListContextMenu.renameCurveAction called')
+ _logger.debug("LegendListContextMenu.renameCurveAction called")
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "renameCurve"
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "event": "renameCurve",
}
self.sigContextMenu.emit(ddict)
@@ -779,17 +797,18 @@ class LegendListContextMenu(qt.QMenu):
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "type": str(modelIndex.data()),
}
linestyle = modelIndex.data(LegendModel.iconLineStyleRole)
visible = not modelIndex.data(LegendModel.showLineRole)
- _logger.debug('toggleLinesAction -- lines visible: %s', str(visible))
- ddict['event'] = "toggleLine"
- ddict['line'] = visible
- ddict['linestyle'] = linestyle if visible else ''
+ _logger.debug("toggleLinesAction -- lines visible: %s", str(visible))
+ ddict["event"] = "toggleLine"
+ ddict["line"] = visible
+ ddict["linestyle"] = linestyle if visible else ""
self.model.setData(modelIndex, visible, LegendModel.showLineRole)
self.sigContextMenu.emit(ddict)
@@ -797,33 +816,34 @@ class LegendListContextMenu(qt.QMenu):
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
}
flag = modelIndex.data(LegendModel.showSymbolRole)
symbol = modelIndex.data(LegendModel.iconSymbolRole)
visible = not flag or LegendIconWidget.isEmptySymbol(symbol)
- _logger.debug(
- 'togglePointsAction -- Symbols visible: %s', str(visible))
+ _logger.debug("togglePointsAction -- Symbols visible: %s", str(visible))
- ddict['event'] = "togglePoints"
- ddict['points'] = visible
- ddict['symbol'] = symbol if visible else ''
+ ddict["event"] = "togglePoints"
+ ddict["points"] = visible
+ ddict["symbol"] = symbol if visible else ""
self.model.setData(modelIndex, visible, LegendModel.showSymbolRole)
self.sigContextMenu.emit(ddict)
def setActiveAction(self):
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
- _logger.debug('setActiveAction -- active curve: %s', legend)
+ _logger.debug("setActiveAction -- active curve: %s", legend)
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "setActiveCurve",
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "event": "setActiveCurve",
}
self.sigContextMenu.emit(ddict)
@@ -842,10 +862,10 @@ class RenameCurveDialog(qt.QDialog):
self.hboxLayout = qt.QHBoxLayout(self.hbox)
self.hboxLayout.addStretch(1)
self.okButton = qt.QPushButton(self.hbox)
- self.okButton.setText('OK')
+ self.okButton.setText("OK")
self.hboxLayout.addWidget(self.okButton)
self.cancelButton = qt.QPushButton(self.hbox)
- self.cancelButton.setText('Cancel')
+ self.cancelButton.setText("Cancel")
self.hboxLayout.addWidget(self.cancelButton)
self.hboxLayout.addStretch(1)
layout.addWidget(self.lineEdit)
@@ -895,8 +915,7 @@ class LegendsDockWidget(qt.QDockWidget):
self.layout().setContentsMargins(0, 0, 0, 0)
self.setWidget(self._legendWidget)
- self.visibilityChanged.connect(
- self._visibilityChangedHandler)
+ self.visibilityChanged.connect(self._visibilityChangedHandler)
self._legendWidget.sigLegendSignal.connect(self._legendSignalHandler)
@@ -905,6 +924,7 @@ class LegendsDockWidget(qt.QDockWidget):
"""The :class:`.PlotWindow` this widget is attached to."""
return self._plotRef()
+ @deprecated(reason="No longer needed", since_version="2.0.0")
def renameCurve(self, oldLegend, newLegend):
"""Change the name of a curve using remove and addCurve
@@ -913,88 +933,77 @@ class LegendsDockWidget(qt.QDockWidget):
"""
is_active = self.plot.getActiveCurve(just_legend=True) == oldLegend
curve = self.plot.getCurve(oldLegend)
- self.plot.remove(oldLegend, kind='curve')
- self.plot.addCurve(curve.getXData(copy=False),
- curve.getYData(copy=False),
- legend=newLegend,
- info=curve.getInfo(),
- color=curve.getColor(),
- symbol=curve.getSymbol(),
- linewidth=curve.getLineWidth(),
- linestyle=curve.getLineStyle(),
- xlabel=curve.getXLabel(),
- ylabel=curve.getYLabel(),
- xerror=curve.getXErrorData(copy=False),
- yerror=curve.getYErrorData(copy=False),
- z=curve.getZValue(),
- selectable=curve.isSelectable(),
- fill=curve.isFill(),
- resetzoom=False)
+ self.plot.remove(oldLegend, kind="curve")
+ self.plot.addCurve(
+ curve.getXData(copy=False),
+ curve.getYData(copy=False),
+ legend=newLegend,
+ info=curve.getInfo(),
+ color=curve.getColor(),
+ symbol=curve.getSymbol(),
+ linewidth=curve.getLineWidth(),
+ linestyle=curve.getLineStyle(),
+ xlabel=curve.getXLabel(),
+ ylabel=curve.getYLabel(),
+ xerror=curve.getXErrorData(copy=False),
+ yerror=curve.getYErrorData(copy=False),
+ z=curve.getZValue(),
+ selectable=curve.isSelectable(),
+ fill=curve.isFill(),
+ resetzoom=False,
+ )
if is_active:
self.plot.setActiveCurve(newLegend)
def _legendSignalHandler(self, ddict):
"""Handles events from the LegendListView signal"""
_logger.debug("Legend signal ddict = %s", str(ddict))
+ # If item is not provided, retrieve it from its legend
+ curve = ddict.get("item", None)
+ if curve is None:
+ curve = self.plot.getCurve(ddict["legend"])
- if ddict['event'] == "legendClicked":
- if ddict['button'] == "left":
- self.plot.setActiveCurve(ddict['legend'])
+ if ddict["event"] == "legendClicked":
+ if ddict["button"] == "left":
+ self.plot.setActiveCurve(curve)
- elif ddict['event'] == "removeCurve":
- self.plot.removeCurve(ddict['legend'])
+ elif ddict["event"] == "removeCurve":
+ self.plot.removeItem(curve)
- elif ddict['event'] == "renameCurve":
+ elif ddict["event"] == "renameCurve":
curveList = self.plot.getAllCurves(just_legend=True)
- oldLegend = ddict['legend']
+ oldLegend = ddict["legend"]
dialog = RenameCurveDialog(self.plot, oldLegend, curveList)
ret = dialog.exec()
if ret:
newLegend = dialog.getText()
- self.renameCurve(oldLegend, newLegend)
-
- elif ddict['event'] == "setActiveCurve":
- self.plot.setActiveCurve(ddict['legend'])
-
- elif ddict['event'] == "checkBoxClicked":
- self.plot.hideCurve(ddict['legend'], not ddict['selected'])
-
- elif ddict['event'] in ["mapToRight", "mapToLeft"]:
- legend = ddict['legend']
- curve = self.plot.getCurve(legend)
- yaxis = 'right' if ddict['event'] == 'mapToRight' else 'left'
- self.plot.addCurve(x=curve.getXData(copy=False),
- y=curve.getYData(copy=False),
- legend=curve.getName(),
- info=curve.getInfo(),
- yaxis=yaxis)
-
- elif ddict['event'] == "togglePoints":
- legend = ddict['legend']
- curve = self.plot.getCurve(legend)
- symbol = ddict['symbol'] if ddict['points'] else ''
- self.plot.addCurve(x=curve.getXData(copy=False),
- y=curve.getYData(copy=False),
- legend=curve.getName(),
- info=curve.getInfo(),
- symbol=symbol)
-
- elif ddict['event'] == "toggleLine":
- legend = ddict['legend']
- curve = self.plot.getCurve(legend)
- linestyle = ddict['linestyle'] if ddict['line'] else ''
- self.plot.addCurve(x=curve.getXData(copy=False),
- y=curve.getYData(copy=False),
- legend=curve.getName(),
- info=curve.getInfo(),
- linestyle=linestyle)
+ wasActive = self.plot.getActiveCurve() is curve
+ self.plot.removeItem(curve)
+ curve.setName(newLegend)
+ self.plot.addItem(curve)
+ if wasActive:
+ self.plot.setActiveCurve(curve)
+
+ elif ddict["event"] == "setActiveCurve":
+ self.plot.setActiveCurve(curve)
+
+ elif ddict["event"] == "checkBoxClicked":
+ curve.setVisible(ddict["selected"])
+
+ elif ddict["event"] in ["mapToRight", "mapToLeft"]:
+ curve.setYAxis("right" if ddict["event"] == "mapToRight" else "left")
+
+ elif ddict["event"] == "togglePoints":
+ curve.setSymbol(ddict["symbol"] if ddict["points"] else "")
+
+ elif ddict["event"] == "toggleLine":
+ curve.setLineStyle(ddict["linestyle"] if ddict["line"] else "")
else:
- _logger.debug("unhandled event %s", str(ddict['event']))
+ _logger.debug("unhandled event %s", str(ddict["event"]))
def updateLegends(self, *args):
- """Sync the LegendSelector widget displayed info with the plot.
- """
+ """Sync the LegendSelector widget displayed info with the plot."""
legendList = []
for curve in self.plot.getAllCurves(withhidden=True):
legend = curve.getName()
@@ -1004,15 +1013,17 @@ class LegendsDockWidget(qt.QDockWidget):
color = style.getColor()
if numpy.array(color, copy=False).ndim != 1:
# array of colors, use transparent black
- color = 0., 0., 0., 0.
+ color = 0.0, 0.0, 0.0, 0.0
curveInfo = {
- 'color': qt.QColor.fromRgbF(*color),
- 'linewidth': style.getLineWidth(),
- 'linestyle': style.getLineStyle(),
- 'symbol': style.getSymbol(),
- 'selected': not self.plot.isCurveHidden(legend),
- 'active': isActive}
+ "color": qt.QColor.fromRgbF(*color),
+ "linewidth": style.getLineWidth(),
+ "linestyle": style.getLineStyle(),
+ "symbol": style.getSymbol(),
+ "selected": not self.plot.isCurveHidden(legend),
+ "active": isActive,
+ "item": curve,
+ }
legendList.append((legend, curveInfo))
self._legendWidget.setLegendList(legendList)
diff --git a/src/silx/gui/plot/LimitsHistory.py b/src/silx/gui/plot/LimitsHistory.py
index 7215e37..f4e0afc 100644
--- a/src/silx/gui/plot/LimitsHistory.py
+++ b/src/silx/gui/plot/LimitsHistory.py
@@ -55,8 +55,8 @@ class LimitsHistory(qt.QObject):
"""Append current limits to the history."""
plot = self.parent()
xmin, xmax = plot.getXAxis().getLimits()
- ymin, ymax = plot.getYAxis(axis='left').getLimits()
- y2min, y2max = plot.getYAxis(axis='right').getLimits()
+ ymin, ymax = plot.getYAxis(axis="left").getLimits()
+ y2min, y2max = plot.getYAxis(axis="right").getLimits()
self._history.append((xmin, xmax, ymin, ymax, y2min, y2max))
def pop(self):
diff --git a/src/silx/gui/plot/MaskToolsWidget.py b/src/silx/gui/plot/MaskToolsWidget.py
index 327cdd6..40b2717 100644
--- a/src/silx/gui/plot/MaskToolsWidget.py
+++ b/src/silx/gui/plot/MaskToolsWidget.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -38,9 +38,12 @@ import os
import sys
import numpy
import logging
-import collections
import h5py
+import fabio
+from fabio.edfimage import EdfImage
+from fabio.TiffIO import TiffIO
+
from silx.image import shapes
from silx.io.utils import NEXUS_HDF5_EXT, is_dataset
from silx.gui.dialog.DatasetDialog import DatasetDialog
@@ -51,14 +54,10 @@ from ..colors import cursorColorForColormap, rgba
from .. import qt
from ..utils import LockReentrant
-from silx.third_party.EdfFile import EdfFile
-from silx.third_party.TiffIO import TiffIO
-
-import fabio
_logger = logging.getLogger(__name__)
-_HDF5_EXT_STR = ' '.join(['*' + ext for ext in NEXUS_HDF5_EXT])
+_HDF5_EXT_STR = " ".join(["*" + ext for ext in NEXUS_HDF5_EXT])
def _selectDataset(filename, mode=DatasetDialog.SaveMode):
@@ -110,16 +109,17 @@ class ImageMask(BaseMask):
or 'msk' (if FabIO is installed)
:raise Exception: Raised if the file writing fail
"""
- if kind == 'edf':
- edfFile = EdfFile(filename, access="w+")
- header = {"program_name": "silx-mask", "masked_value": "nonzero"}
- edfFile.WriteImage(header, self.getMask(copy=False), Append=0)
+ if kind == "edf":
+ EdfImage(
+ data=self.getMask(),
+ header={"program_name": "silx-mask", "masked_value": "nonzero"},
+ ).write(filename)
- elif kind == 'tif':
- tiffFile = TiffIO(filename, mode='w')
- tiffFile.writeImage(self.getMask(copy=False), software='silx')
+ elif kind == "tif":
+ tiffFile = TiffIO(filename, mode="w")
+ tiffFile.writeImage(self.getMask(copy=False), software="silx")
- elif kind == 'npy':
+ elif kind == "npy":
try:
numpy.save(filename, self.getMask(copy=False))
except IOError:
@@ -128,7 +128,7 @@ class ImageMask(BaseMask):
elif ("." + kind) in NEXUS_HDF5_EXT:
self._saveToHdf5(filename, self.getMask(copy=False))
- elif kind == 'msk':
+ elif kind == "msk":
try:
data = self.getMask(copy=False)
image = fabio.fabioimage.FabioImage(data=data)
@@ -159,10 +159,11 @@ class ImageMask(BaseMask):
existing_ds = h5f.get(dataPath)
if existing_ds is not None:
reply = qt.QMessageBox.question(
- None,
- "Confirm overwrite",
- "Do you want to overwrite an existing dataset?",
- qt.QMessageBox.Yes | qt.QMessageBox.No)
+ None,
+ "Confirm overwrite",
+ "Do you want to overwrite an existing dataset?",
+ qt.QMessageBox.Yes | qt.QMessageBox.No,
+ )
if reply != qt.QMessageBox.Yes:
return False
del h5f[dataPath]
@@ -186,10 +187,11 @@ class ImageMask(BaseMask):
assert 0 < level < 256
if row + height <= 0 or col + width <= 0:
return # Rectangle outside image, avoid negative indices
- selection = self._mask[max(0, row):row + height + 1,
- max(0, col):col + width + 1]
+ selection = self._mask[
+ max(0, row) : row + height + 1, max(0, col) : col + width + 1
+ ]
if mask:
- selection[:,:] = level
+ selection[:, :] = level
else:
selection[selection == level] = 0
self._notify()
@@ -205,8 +207,7 @@ class ImageMask(BaseMask):
if mask:
self._mask[fill != 0] = level
else:
- self._mask[numpy.logical_and(fill != 0,
- self._mask == level)] = 0
+ self._mask[numpy.logical_and(fill != 0, self._mask == level)] = 0
self._notify()
def updatePoints(self, level, rows, cols, mask=True):
@@ -221,8 +222,8 @@ class ImageMask(BaseMask):
"""
valid = numpy.logical_and(
numpy.logical_and(rows >= 0, cols >= 0),
- numpy.logical_and(rows < self._mask.shape[0],
- cols < self._mask.shape[1]))
+ numpy.logical_and(rows < self._mask.shape[0], cols < self._mask.shape[1]),
+ )
rows, cols = rows[valid], cols[valid]
if mask:
@@ -278,10 +279,9 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_maxLevelNumber = 255
def __init__(self, parent=None, plot=None):
- super(MaskToolsWidget, self).__init__(parent, plot,
- mask=ImageMask())
- self._origin = (0., 0.) # Mask origin in plot
- self._scale = (1., 1.) # Mask scale in plot
+ super(MaskToolsWidget, self).__init__(parent, plot, mask=ImageMask())
+ self._origin = (0.0, 0.0) # Mask origin in plot
+ self._scale = (1.0, 1.0) # Mask scale in plot
self._z = 1 # Mask layer in plot
self._data = numpy.zeros((0, 0), dtype=numpy.uint8) # Store image
@@ -336,11 +336,11 @@ class MaskToolsWidget(BaseMaskToolsWidget):
mask = numpy.array(mask, copy=False, dtype=numpy.uint8)
if len(mask.shape) != 2:
- _logger.error('Not an image, shape: %d', len(mask.shape))
+ _logger.error("Not an image, shape: %d", len(mask.shape))
return None
# Handle mask with single level
- if self.multipleMasks() == 'single':
+ if self.multipleMasks() == "single":
mask = numpy.array(mask != 0, dtype=numpy.uint8)
# if mask has not changed, do nothing
@@ -352,15 +352,17 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._mask.commit()
return mask.shape
else:
- _logger.warning('Mask has not the same size as current image.'
- ' Mask will be cropped or padded to fit image'
- ' dimensions. %s != %s',
- str(mask.shape), str(self._data.shape))
- resizedMask = numpy.zeros(self._data.shape[0:2],
- dtype=numpy.uint8)
+ _logger.warning(
+ "Mask has not the same size as current image."
+ " Mask will be cropped or padded to fit image"
+ " dimensions. %s != %s",
+ str(mask.shape),
+ str(self._data.shape),
+ )
+ resizedMask = numpy.zeros(self._data.shape[0:2], dtype=numpy.uint8)
height = min(self._data.shape[0], mask.shape[0])
width = min(self._data.shape[1], mask.shape[1])
- resizedMask[:height,:width] = mask[:height,:width]
+ resizedMask[:height, :width] = mask[:height, :width]
self._mask.setMask(resizedMask, copy=False)
self._mask.commit()
return resizedMask.shape
@@ -387,12 +389,13 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self.plot.addItem(maskItem)
elif self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
+ self.plot.remove(self._maskName, kind="image")
def showEvent(self, event):
try:
self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChangedAfterCare)
+ self._activeImageChangedAfterCare
+ )
except (RuntimeError, TypeError):
pass
@@ -402,8 +405,7 @@ class MaskToolsWidget(BaseMaskToolsWidget):
def hideEvent(self, event):
try:
- self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChanged)
+ self.plot.sigActiveImageChanged.disconnect(self._activeImageChanged)
except (RuntimeError, TypeError):
pass
@@ -424,11 +426,10 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._mask.reset()
if self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
+ self.plot.remove(self._maskName, kind="image")
elif self.getSelectionMask(copy=False) is not None:
- self.plot.sigActiveImageChanged.connect(
- self._activeImageChangedAfterCare)
+ self.plot.sigActiveImageChanged.connect(self._activeImageChangedAfterCare)
def _activeImageChanged(self, previous, current):
"""Reacts upon active image change.
@@ -448,10 +449,9 @@ class MaskToolsWidget(BaseMaskToolsWidget):
"""
if isinstance(image, items.ColormapMixIn):
colormap = image.getColormap()
- self._defaultOverlayColor = rgba(
- cursorColorForColormap(colormap['name']))
+ self._defaultOverlayColor = rgba(cursorColorForColormap(colormap["name"]))
else:
- self._defaultOverlayColor = rgba('black')
+ self._defaultOverlayColor = rgba("black")
def _activeImageChangedAfterCare(self, *args):
"""Check synchro of active image and mask when mask widget is hidden.
@@ -467,15 +467,17 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._mask.reset()
if self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
+ self.plot.remove(self._maskName, kind="image")
self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChangedAfterCare)
+ self._activeImageChangedAfterCare
+ )
else:
self._setOverlayColorForImage(activeImage)
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
+ self._setMaskColors(
+ self.levelSpinBox.value(),
+ self.transparencySlider.value() / self.transparencySlider.maximum(),
+ )
self._origin = activeImage.getOrigin()
self._scale = activeImage.getScale()
@@ -484,10 +486,11 @@ class MaskToolsWidget(BaseMaskToolsWidget):
if self._data.shape[:2] != self._mask.getMask(copy=False).shape:
# Image has not the same size, remove mask and stop listening
if self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
+ self.plot.remove(self._maskName, kind="image")
self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChangedAfterCare)
+ self._activeImageChangedAfterCare
+ )
else:
# Refresh in case origin, scale, z changed
self._mask.setDataItem(activeImage)
@@ -519,11 +522,9 @@ class MaskToolsWidget(BaseMaskToolsWidget):
if self.isItemMaskUpdated():
if image.getMaskData(copy=False) is None:
# Image item has no mask: use current mask from the tool
- image.setMaskData(
- self.getSelectionMask(copy=False), copy=True)
+ image.setMaskData(self.getSelectionMask(copy=False), copy=True)
else: # Image item has a mask: set it in tool
- self.setSelectionMask(
- image.getMaskData(copy=False), copy=True)
+ self.setSelectionMask(image.getMaskData(copy=False), copy=True)
self._mask.resetHistory()
self.__imageUpdated()
if self.isVisible():
@@ -536,17 +537,21 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_logger.error("Mask is not attached to an image")
return
- if event in (items.ItemChangedType.COLORMAP,
- items.ItemChangedType.DATA,
- items.ItemChangedType.POSITION,
- items.ItemChangedType.SCALE,
- items.ItemChangedType.VISIBLE,
- items.ItemChangedType.ZVALUE):
+ if event in (
+ items.ItemChangedType.COLORMAP,
+ items.ItemChangedType.DATA,
+ items.ItemChangedType.POSITION,
+ items.ItemChangedType.SCALE,
+ items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.ZVALUE,
+ ):
self.__imageUpdated()
- elif (event == items.ItemChangedType.MASK and
- self.isItemMaskUpdated() and
- not self.__itemMaskUpdatedLock.locked()):
+ elif (
+ event == items.ItemChangedType.MASK
+ and self.isItemMaskUpdated()
+ and not self.__itemMaskUpdatedLock.locked()
+ ):
# Update mask from the image item unless mask tool is updating it
self.setSelectionMask(image.getMaskData(copy=False), copy=True)
@@ -559,9 +564,10 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._setOverlayColorForImage(image)
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
+ self._setMaskColors(
+ self.levelSpinBox.value(),
+ self.transparencySlider.value() / self.transparencySlider.maximum(),
+ )
self._origin = image.getOrigin()
self._scale = image.getScale()
@@ -602,26 +608,11 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_logger.error("Can't load filename '%s'", filename)
_logger.debug("Backtrace", exc_info=True)
raise RuntimeError('File "%s" is not a numpy file.', filename)
- elif extension in ["tif", "tiff"]:
- try:
- image = TiffIO(filename, mode="r")
- mask = image.getImage(0)
- except Exception as e:
- _logger.error("Can't load filename %s", filename)
- _logger.debug("Backtrace", exc_info=True)
- raise e
- elif extension == "edf":
- try:
- mask = EdfFile(filename, access='r').GetData(0)
- except Exception as e:
- _logger.error("Can't load filename %s", filename)
- _logger.debug("Backtrace", exc_info=True)
- raise e
- elif extension == "msk":
+ elif extension in ("edf", "msk", "tif", "tiff"):
try:
mask = fabio.open(filename).data
except Exception as e:
- _logger.error("Can't load fit2d mask file")
+ _logger.error(f"Can't load filename {filename}")
_logger.debug("Backtrace", exc_info=True)
raise e
elif ("." + extension) in NEXUS_HDF5_EXT:
@@ -636,7 +627,7 @@ class MaskToolsWidget(BaseMaskToolsWidget):
if effectiveMaskShape is None:
return
if mask.shape != effectiveMaskShape:
- msg = 'Mask was resized from %s to %s'
+ msg = "Mask was resized from %s to %s"
msg = msg % (str(mask.shape), str(effectiveMaskShape))
raise RuntimeWarning(msg)
@@ -646,7 +637,7 @@ class MaskToolsWidget(BaseMaskToolsWidget):
dialog.setWindowTitle("Load Mask")
dialog.setModal(1)
- extensions = collections.OrderedDict()
+ extensions = {}
extensions["EDF files"] = "*.edf"
extensions["TIFF files"] = "*.tif *.tiff"
extensions["NumPy binary files"] = "*.npy"
@@ -714,15 +705,15 @@ class MaskToolsWidget(BaseMaskToolsWidget):
dialog.setWindowTitle("Save Mask")
dialog.setOption(qt.QFileDialog.DontUseNativeDialog)
dialog.setModal(1)
- hdf5Filter = 'HDF5 (%s)' % _HDF5_EXT_STR
+ hdf5Filter = "HDF5 (%s)" % _HDF5_EXT_STR
filters = [
- 'EDF (*.edf)',
- 'TIFF (*.tif)',
- 'NumPy binary file (*.npy)',
+ "EDF (*.edf)",
+ "TIFF (*.tif)",
+ "NumPy binary file (*.npy)",
hdf5Filter,
# Fit2D mask is displayed anyway fabio is here or not
# to show to the user that the option exists
- 'Fit2D mask (*.msk)',
+ "Fit2D mask (*.msk)",
]
dialog.setNameFilters(filters)
dialog.setFileMode(qt.QFileDialog.AnyFile)
@@ -749,8 +740,10 @@ class MaskToolsWidget(BaseMaskToolsWidget):
if "HDF5" in nameFilter:
has_allowed_ext = False
for ext in NEXUS_HDF5_EXT:
- if (len(filename) > len(ext) and
- filename[-len(ext):].lower() == ext.lower()):
+ if (
+ len(filename) > len(ext)
+ and filename[-len(ext) :].lower() == ext.lower()
+ ):
has_allowed_ext = True
extension = ext
if not has_allowed_ext:
@@ -774,8 +767,7 @@ class MaskToolsWidget(BaseMaskToolsWidget):
strerror = e.strerror
else:
strerror = sys.exc_info()[1]
- msg.setText("Cannot save.\n"
- "Input Output Error: %s" % strerror)
+ msg.setText("Cannot save.\n" "Input Output Error: %s" % strerror)
msg.exec()
return
@@ -803,8 +795,10 @@ class MaskToolsWidget(BaseMaskToolsWidget):
def _plotDrawEvent(self, event):
"""Handle draw events from the plot"""
- if (self._drawingMode is None or
- event['event'] not in ('drawingProgress', 'drawingFinished')):
+ if self._drawingMode is None or event["event"] not in (
+ "drawingProgress",
+ "drawingFinished",
+ ):
return
if not len(self._data):
@@ -812,56 +806,54 @@ class MaskToolsWidget(BaseMaskToolsWidget):
level = self.levelSpinBox.value()
- if self._drawingMode == 'rectangle':
- if event['event'] == 'drawingFinished':
+ if self._drawingMode == "rectangle":
+ if event["event"] == "drawingFinished":
# Convert from plot to array coords
doMask = self._isMasking()
ox, oy = self._origin
sx, sy = self._scale
- height = int(abs(event['height'] / sy))
- width = int(abs(event['width'] / sx))
+ height = int(abs(event["height"] / sy))
+ width = int(abs(event["width"] / sx))
- row = int((event['y'] - oy) / sy)
+ row = int((event["y"] - oy) / sy)
if sy < 0:
row -= height
- col = int((event['x'] - ox) / sx)
+ col = int((event["x"] - ox) / sx)
if sx < 0:
col -= width
self._mask.updateRectangle(
- level,
- row=row,
- col=col,
- height=height,
- width=width,
- mask=doMask)
+ level, row=row, col=col, height=height, width=width, mask=doMask
+ )
self._mask.commit()
- elif self._drawingMode == 'ellipse':
- if event['event'] == 'drawingFinished':
+ elif self._drawingMode == "ellipse":
+ if event["event"] == "drawingFinished":
doMask = self._isMasking()
# Convert from plot to array coords
- center = (event['points'][0] - self._origin) / self._scale
- size = event['points'][1] / self._scale
+ center = (event["points"][0] - self._origin) / self._scale
+ size = event["points"][1] / self._scale
center = center.astype(numpy.int64) # (row, col)
- self._mask.updateEllipse(level, center[1], center[0], size[1], size[0], doMask)
+ self._mask.updateEllipse(
+ level, center[1], center[0], size[1], size[0], doMask
+ )
self._mask.commit()
- elif self._drawingMode == 'polygon':
- if event['event'] == 'drawingFinished':
+ elif self._drawingMode == "polygon":
+ if event["event"] == "drawingFinished":
doMask = self._isMasking()
# Convert from plot to array coords
- vertices = (event['points'] - self._origin) / self._scale
+ vertices = (event["points"] - self._origin) / self._scale
vertices = vertices.astype(numpy.int64)[:, (1, 0)] # (row, col)
self._mask.updatePolygon(level, vertices, doMask)
self._mask.commit()
- elif self._drawingMode == 'pencil':
+ elif self._drawingMode == "pencil":
doMask = self._isMasking()
# convert from plot to array coords
- col, row = (event['points'][-1] - self._origin) / self._scale
+ col, row = (event["points"][-1] - self._origin) / self._scale
col, row = int(col), int(row)
brushSize = self._getPencilWidth()
@@ -870,15 +862,18 @@ class MaskToolsWidget(BaseMaskToolsWidget):
# Draw the line
self._mask.updateLine(
level,
- self._lastPencilPos[0], self._lastPencilPos[1],
- row, col,
+ self._lastPencilPos[0],
+ self._lastPencilPos[1],
+ row,
+ col,
brushSize,
- doMask)
+ doMask,
+ )
# Draw the very first, or last point
- self._mask.updateDisk(level, row, col, brushSize / 2., doMask)
+ self._mask.updateDisk(level, row, col, brushSize / 2.0, doMask)
- if event['event'] == 'drawingFinished':
+ if event["event"] == "drawingFinished":
self._mask.commit()
self._lastPencilPos = None
else:
@@ -889,15 +884,17 @@ class MaskToolsWidget(BaseMaskToolsWidget):
def _loadRangeFromColormapTriggered(self):
"""Set range from active image colormap range"""
activeImage = self.plot.getActiveImage()
- if (isinstance(activeImage, items.ColormapMixIn) and
- activeImage.getName() != self._maskName):
+ if (
+ isinstance(activeImage, items.ColormapMixIn)
+ and activeImage.getName() != self._maskName
+ ):
# Update thresholds according to colormap
colormap = activeImage.getColormap()
- if colormap['autoscale']:
+ if colormap["autoscale"]:
min_ = numpy.nanmin(activeImage.getData(copy=False))
max_ = numpy.nanmax(activeImage.getData(copy=False))
else:
- min_, max_ = colormap['vmin'], colormap['vmax']
+ min_, max_ = colormap["vmin"], colormap["vmax"]
self.minLineEdit.setText(str(min_))
self.maxLineEdit.setText(str(max_))
@@ -912,6 +909,6 @@ class MaskToolsDockWidget(BaseMaskToolsDockWidget):
:paran str name: The title of this widget
"""
- def __init__(self, parent=None, plot=None, name='Mask'):
+ def __init__(self, parent=None, plot=None, name="Mask"):
widget = MaskToolsWidget(plot=plot)
super(MaskToolsDockWidget, self).__init__(parent, name, widget)
diff --git a/src/silx/gui/plot/PlotActions.py b/src/silx/gui/plot/PlotActions.py
deleted file mode 100644
index f32be3c..0000000
--- a/src/silx/gui/plot/PlotActions.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# /*##########################################################################
-#
-# Copyright (c) 2004-2017 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
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""Depracted module linking old PlotAction with the actions.xxx"""
-
-
-__author__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "01/06/2017"
-
-from silx.utils.deprecation import deprecated_warning
-
-deprecated_warning(type_='module',
- name=__file__,
- reason='PlotActions refactoring',
- replacement='plot.actions',
- since_version='0.6')
-
-from .actions import PlotAction
-
-from .actions.io import CopyAction
-from .actions.io import PrintAction
-from .actions.io import SaveAction
-
-from .actions.control import ColormapAction
-from .actions.control import CrosshairAction
-from .actions.control import CurveStyleAction
-from .actions.control import GridAction
-from .actions.control import KeepAspectRatioAction
-from .actions.control import PanWithArrowKeysAction
-from .actions.control import ResetZoomAction
-from .actions.control import XAxisAutoScaleAction
-from .actions.control import XAxisLogarithmicAction
-from .actions.control import YAxisAutoScaleAction
-from .actions.control import YAxisLogarithmicAction
-from .actions.control import YAxisInvertedAction
-from .actions.control import ZoomInAction
-from .actions.control import ZoomOutAction
-
-from .actions.medfilt import MedianFilter1DAction
-from .actions.medfilt import MedianFilter2DAction
-from .actions.medfilt import MedianFilterAction
-
-from .actions.histogram import PixelIntensitiesHistoAction
-
-from .actions.fit import FitAction
diff --git a/src/silx/gui/plot/PlotEvents.py b/src/silx/gui/plot/PlotEvents.py
index be875d7..b4cbe30 100644
--- a/src/silx/gui/plot/PlotEvents.py
+++ b/src/silx/gui/plot/PlotEvents.py
@@ -33,60 +33,71 @@ import numpy as np
def prepareDrawingSignal(event, type_, points, parameters=None):
"""See Plot documentation for content of events"""
- assert event in ('drawingProgress', 'drawingFinished')
+ assert event in ("drawingProgress", "drawingFinished")
if parameters is None:
parameters = {}
eventDict = {}
- eventDict['event'] = event
- eventDict['type'] = type_
+ eventDict["event"] = event
+ eventDict["type"] = type_
points = np.array(points, dtype=np.float32)
points.shape = -1, 2
- eventDict['points'] = points
- eventDict['xdata'] = points[:, 0]
- eventDict['ydata'] = points[:, 1]
- if type_ in ('rectangle',):
- eventDict['x'] = eventDict['xdata'].min()
- eventDict['y'] = eventDict['ydata'].min()
- eventDict['width'] = eventDict['xdata'].max() - eventDict['x']
- eventDict['height'] = eventDict['ydata'].max() - eventDict['y']
- eventDict['parameters'] = parameters.copy()
+ eventDict["points"] = points
+ eventDict["xdata"] = points[:, 0]
+ eventDict["ydata"] = points[:, 1]
+ if type_ in ("rectangle",):
+ eventDict["x"] = eventDict["xdata"].min()
+ eventDict["y"] = eventDict["ydata"].min()
+ eventDict["width"] = eventDict["xdata"].max() - eventDict["x"]
+ eventDict["height"] = eventDict["ydata"].max() - eventDict["y"]
+ eventDict["parameters"] = parameters.copy()
return eventDict
def prepareMouseSignal(eventType, button, xData, yData, xPixel, yPixel):
"""See Plot documentation for content of events"""
- assert eventType in ('mouseMoved', 'mouseClicked', 'mouseDoubleClicked')
- assert button in (None, 'left', 'middle', 'right')
+ assert eventType in ("mouseMoved", "mouseClicked", "mouseDoubleClicked")
+ assert button in (None, "left", "middle", "right")
- return {'event': eventType,
- 'x': xData,
- 'y': yData,
- 'xpixel': xPixel,
- 'ypixel': yPixel,
- 'button': button}
+ return {
+ "event": eventType,
+ "x": xData,
+ "y": yData,
+ "xpixel": xPixel,
+ "ypixel": yPixel,
+ "button": button,
+ }
def prepareHoverSignal(label, type_, posData, posPixel, draggable, selectable):
"""See Plot documentation for content of events"""
- return {'event': 'hover',
- 'label': label,
- 'type': type_,
- 'x': posData[0],
- 'y': posData[1],
- 'xpixel': posPixel[0],
- 'ypixel': posPixel[1],
- 'draggable': draggable,
- 'selectable': selectable}
-
-
-def prepareMarkerSignal(eventType, button, label, type_,
- draggable, selectable,
- posDataMarker,
- posPixelCursor=None, posDataCursor=None):
+ return {
+ "event": "hover",
+ "label": label,
+ "type": type_,
+ "x": posData[0],
+ "y": posData[1],
+ "xpixel": posPixel[0],
+ "ypixel": posPixel[1],
+ "draggable": draggable,
+ "selectable": selectable,
+ }
+
+
+def prepareMarkerSignal(
+ eventType,
+ button,
+ label,
+ type_,
+ draggable,
+ selectable,
+ posDataMarker,
+ posPixelCursor=None,
+ posDataCursor=None,
+):
"""See Plot documentation for content of events"""
- if eventType == 'markerClicked':
+ if eventType == "markerClicked":
assert posPixelCursor is not None
assert posDataCursor is None
@@ -96,11 +107,11 @@ def prepareMarkerSignal(eventType, button, label, type_,
if hasattr(posDataCursor[1], "__len__"):
posDataCursor[1] = posDataCursor[1][-1]
- elif eventType == 'markerMoving':
+ elif eventType == "markerMoving":
assert posPixelCursor is not None
assert posDataCursor is not None
- elif eventType == 'markerMoved':
+ elif eventType == "markerMoved":
assert posPixelCursor is None
assert posDataCursor is None
@@ -108,58 +119,66 @@ def prepareMarkerSignal(eventType, button, label, type_,
else:
raise NotImplementedError("Unknown event type {0}".format(eventType))
- eventDict = {'event': eventType,
- 'button': button,
- 'label': label,
- 'type': type_,
- 'x': posDataCursor[0],
- 'y': posDataCursor[1],
- 'xdata': posDataMarker[0],
- 'ydata': posDataMarker[1],
- 'draggable': draggable,
- 'selectable': selectable}
-
- if eventType in ('markerMoving', 'markerClicked'):
- eventDict['xpixel'] = posPixelCursor[0]
- eventDict['ypixel'] = posPixelCursor[1]
+ eventDict = {
+ "event": eventType,
+ "button": button,
+ "label": label,
+ "type": type_,
+ "x": posDataCursor[0],
+ "y": posDataCursor[1],
+ "xdata": posDataMarker[0],
+ "ydata": posDataMarker[1],
+ "draggable": draggable,
+ "selectable": selectable,
+ }
+
+ if eventType in ("markerMoving", "markerClicked"):
+ eventDict["xpixel"] = posPixelCursor[0]
+ eventDict["ypixel"] = posPixelCursor[1]
return eventDict
-def prepareImageSignal(button, label, type_, col, row,
- x, y, xPixel, yPixel):
+def prepareImageSignal(button, item, col, row, x, y, xPixel, yPixel):
"""See Plot documentation for content of events"""
- return {'event': 'imageClicked',
- 'button': button,
- 'label': label,
- 'type': type_,
- 'col': col,
- 'row': row,
- 'x': x,
- 'y': y,
- 'xpixel': xPixel,
- 'ypixel': yPixel}
-
-
-def prepareCurveSignal(button, label, type_, xData, yData,
- x, y, xPixel, yPixel):
+ return {
+ "event": "imageClicked",
+ "button": button,
+ "item": item,
+ "label": item.getName(),
+ "type": "image",
+ "col": col,
+ "row": row,
+ "x": x,
+ "y": y,
+ "xpixel": xPixel,
+ "ypixel": yPixel,
+ }
+
+
+def prepareCurveSignal(button, item, xData, yData, x, y, xPixel, yPixel):
"""See Plot documentation for content of events"""
- return {'event': 'curveClicked',
- 'button': button,
- 'label': label,
- 'type': type_,
- 'xdata': xData,
- 'ydata': yData,
- 'x': x,
- 'y': y,
- 'xpixel': xPixel,
- 'ypixel': yPixel}
+ return {
+ "event": "curveClicked",
+ "button": button,
+ "item": item,
+ "label": item.getName(),
+ "type": "curve",
+ "xdata": xData,
+ "ydata": yData,
+ "x": x,
+ "y": y,
+ "xpixel": xPixel,
+ "ypixel": yPixel,
+ }
def prepareLimitsChangedSignal(sourceObj, xRange, yRange, y2Range):
"""See Plot documentation for content of events"""
- return {'event': 'limitsChanged',
- 'source': id(sourceObj),
- 'xdata': xRange,
- 'ydata': yRange,
- 'y2data': y2Range}
+ return {
+ "event": "limitsChanged",
+ "source": id(sourceObj),
+ "xdata": xRange,
+ "ydata": yRange,
+ "y2data": y2Range,
+ }
diff --git a/src/silx/gui/plot/PlotInteraction.py b/src/silx/gui/plot/PlotInteraction.py
index c4d64a5..d19bb6d 100644
--- a/src/silx/gui/plot/PlotInteraction.py
+++ b/src/silx/gui/plot/PlotInteraction.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2014-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 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
@@ -23,6 +23,8 @@
# ###########################################################################*/
"""Implementation of the interaction for the :class:`Plot`."""
+from __future__ import annotations
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/02/2019"
@@ -32,30 +34,53 @@ import math
import numpy
import time
import weakref
+from typing import NamedTuple, Optional
+from silx.gui import qt
from .. import colors
-from .. import qt
from . import items
-from .Interaction import (ClickOrDrag, LEFT_BTN, RIGHT_BTN, MIDDLE_BTN,
- State, StateMachine)
-from .PlotEvents import (prepareCurveSignal, prepareDrawingSignal,
- prepareHoverSignal, prepareImageSignal,
- prepareMarkerSignal, prepareMouseSignal)
-
-from .backends.BackendBase import (CURSOR_POINTING, CURSOR_SIZE_HOR,
- CURSOR_SIZE_VER, CURSOR_SIZE_ALL)
-
-from ._utils import (FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX,
- applyZoomToPlot)
+from .Interaction import (
+ ClickOrDrag,
+ LEFT_BTN,
+ RIGHT_BTN,
+ MIDDLE_BTN,
+ State,
+ StateMachine,
+)
+from .PlotEvents import (
+ prepareCurveSignal,
+ prepareDrawingSignal,
+ prepareHoverSignal,
+ prepareImageSignal,
+ prepareMarkerSignal,
+ prepareMouseSignal,
+)
+
+from .backends.BackendBase import (
+ CURSOR_POINTING,
+ CURSOR_SIZE_HOR,
+ CURSOR_SIZE_VER,
+ CURSOR_SIZE_ALL,
+)
+
+from ._utils import (
+ FLOAT32_SAFE_MIN,
+ FLOAT32_MINPOS,
+ FLOAT32_SAFE_MAX,
+ applyZoomToPlot,
+ EnabledAxes,
+)
# Base class ##################################################################
+
class _PlotInteraction(object):
"""Base class for interaction handler.
It provides a weakref to the plot and methods to set/reset overlay.
"""
+
def __init__(self, plot):
"""Init.
@@ -71,7 +96,7 @@ class _PlotInteraction(object):
assert plot is not None
return plot
- def setSelectionArea(self, points, fill, color, name='', shape='polygon'):
+ def setSelectionArea(self, points, fill, color, name="", shape="polygon"):
"""Set a polygon selection area overlaid on the plot.
Multiple simultaneous areas are supported through the name parameter.
@@ -83,7 +108,7 @@ class _PlotInteraction(object):
:param name: The key associated with this selection area
:param str shape: Shape of the area in 'polygon', 'polylines'
"""
- assert shape in ('polygon', 'polylines')
+ assert shape in ("polygon", "polylines")
if color is None:
return
@@ -91,9 +116,9 @@ class _PlotInteraction(object):
points = numpy.asarray(points)
# TODO Not very nice, but as is for now
- legend = '__SELECTION_AREA__' + name
+ legend = "__SELECTION_AREA__" + name
- fill = fill != 'none' # TODO not very nice either
+ fill = fill != "none" # TODO not very nice either
greyed = colors.greyed(color)[0]
if greyed < 0.5:
@@ -101,36 +126,39 @@ class _PlotInteraction(object):
else:
color2 = "black"
- self.plot.addShape(points[:, 0], points[:, 1], legend=legend,
- replace=False,
- shape=shape, fill=fill,
- color=color, linebgcolor=color2, linestyle="--",
- overlay=True)
+ self.plot.addShape(
+ points[:, 0],
+ points[:, 1],
+ legend=legend,
+ replace=False,
+ shape=shape,
+ fill=fill,
+ color=color,
+ gapcolor=color2,
+ linestyle="--",
+ overlay=True,
+ )
self._selectionAreas.add(legend)
def resetSelectionArea(self):
"""Remove all selection areas set by setSelectionArea."""
for legend in self._selectionAreas:
- self.plot.remove(legend, kind='item')
+ self.plot.remove(legend, kind="item")
self._selectionAreas = set()
# Zoom/Pan ####################################################################
-class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
- """:class:`ClickOrDrag` state machine with zooming on mouse wheel.
+
+class _PlotInteractionWithClickEvents(ClickOrDrag, _PlotInteraction):
+ """:class:`ClickOrDrag` state machine emitting click and double click events.
Base class for :class:`Pan` and :class:`Zoom`
"""
_DOUBLE_CLICK_TIMEOUT = 0.4
- class Idle(ClickOrDrag.Idle):
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.machine.plot, scaleF, (x, y))
-
def click(self, x, y, btn):
"""Handle clicks by sending events
@@ -144,18 +172,19 @@ class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
# Signal mouse double clicked event first
if (time.time() - lastClickTime) <= self._DOUBLE_CLICK_TIMEOUT:
# Use position of first click
- eventDict = prepareMouseSignal('mouseDoubleClicked', 'left',
- *lastClickPos)
+ eventDict = prepareMouseSignal(
+ "mouseDoubleClicked", "left", *lastClickPos
+ )
self.plot.notify(**eventDict)
- self._lastClick = 0., None
+ self._lastClick = 0.0, None
else:
# Signal mouse clicked event
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- eventDict = prepareMouseSignal('mouseClicked', 'left',
- dataPos[0], dataPos[1],
- x, y)
+ eventDict = prepareMouseSignal(
+ "mouseClicked", "left", dataPos[0], dataPos[1], x, y
+ )
self.plot.notify(**eventDict)
self._lastClick = time.time(), (dataPos[0], dataPos[1], x, y)
@@ -164,9 +193,9 @@ class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
# Signal mouse clicked event
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- eventDict = prepareMouseSignal('mouseClicked', 'right',
- dataPos[0], dataPos[1],
- x, y)
+ eventDict = prepareMouseSignal(
+ "mouseClicked", "right", dataPos[0], dataPos[1], x, y
+ )
self.plot.notify(**eventDict)
def __init__(self, plot, **kwargs):
@@ -174,7 +203,7 @@ class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
:param plot: The plot to apply modifications to.
"""
- self._lastClick = 0., None
+ self._lastClick = 0.0, None
_PlotInteraction.__init__(self, plot)
ClickOrDrag.__init__(self, **kwargs)
@@ -182,12 +211,13 @@ class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
# Pan #########################################################################
-class Pan(_ZoomOnWheel):
+
+class Pan(_PlotInteractionWithClickEvents):
"""Pan plot content and zoom on wheel state machine."""
def _pixelToData(self, x, y):
xData, yData = self.plot.pixelToData(x, y)
- _, y2Data = self.plot.pixelToData(x, y, axis='right')
+ _, y2Data = self.plot.pixelToData(x, y, axis="right")
return xData, yData, y2Data
def beginDrag(self, x, y, btn):
@@ -199,13 +229,13 @@ class Pan(_ZoomOnWheel):
xMin, xMax = self.plot.getXAxis().getLimits()
yMin, yMax = self.plot.getYAxis().getLimits()
- y2Min, y2Max = self.plot.getYAxis(axis='right').getLimits()
+ y2Min, y2Max = self.plot.getYAxis(axis="right").getLimits()
if self.plot.getXAxis()._isLogarithmic():
try:
dx = math.log10(xData) - math.log10(lastX)
- newXMin = pow(10., (math.log10(xMin) - dx))
- newXMax = pow(10., (math.log10(xMax) - dx))
+ newXMin = pow(10.0, (math.log10(xMin) - dx))
+ newXMax = pow(10.0, (math.log10(xMax) - dx))
except (ValueError, OverflowError):
newXMin, newXMax = xMin, xMax
@@ -223,19 +253,23 @@ class Pan(_ZoomOnWheel):
if self.plot.getYAxis()._isLogarithmic():
try:
dy = math.log10(yData) - math.log10(lastY)
- newYMin = pow(10., math.log10(yMin) - dy)
- newYMax = pow(10., math.log10(yMax) - dy)
+ newYMin = pow(10.0, math.log10(yMin) - dy)
+ newYMax = pow(10.0, math.log10(yMax) - dy)
dy2 = math.log10(y2Data) - math.log10(lastY2)
- newY2Min = pow(10., math.log10(y2Min) - dy2)
- newY2Max = pow(10., math.log10(y2Max) - dy2)
+ newY2Min = pow(10.0, math.log10(y2Min) - dy2)
+ newY2Max = pow(10.0, math.log10(y2Max) - dy2)
except (ValueError, OverflowError):
newYMin, newYMax = yMin, yMax
newY2Min, newY2Max = y2Min, y2Max
# Makes sure y and y2 stays in positive float32 range
- if (newYMin < FLOAT32_MINPOS or newYMax > FLOAT32_SAFE_MAX or
- newY2Min < FLOAT32_MINPOS or newY2Max > FLOAT32_SAFE_MAX):
+ if (
+ newYMin < FLOAT32_MINPOS
+ or newYMax > FLOAT32_SAFE_MAX
+ or newY2Min < FLOAT32_MINPOS
+ or newY2Max > FLOAT32_SAFE_MAX
+ ):
newYMin, newYMax = yMin, yMax
newY2Min, newY2Max = y2Min, y2Max
else:
@@ -245,16 +279,16 @@ class Pan(_ZoomOnWheel):
newY2Min, newY2Max = y2Min - dy2, y2Max - dy2
# Makes sure y and y2 stays in float32 range
- if (newYMin < FLOAT32_SAFE_MIN or
- newYMax > FLOAT32_SAFE_MAX or
- newY2Min < FLOAT32_SAFE_MIN or
- newY2Max > FLOAT32_SAFE_MAX):
+ if (
+ newYMin < FLOAT32_SAFE_MIN
+ or newYMax > FLOAT32_SAFE_MAX
+ or newY2Min < FLOAT32_SAFE_MIN
+ or newY2Max > FLOAT32_SAFE_MAX
+ ):
newYMin, newYMax = yMin, yMax
newY2Min, newY2Max = y2Min, y2Max
- self.plot.setLimits(newXMin, newXMax,
- newYMin, newYMax,
- newY2Min, newY2Max)
+ self.plot.setLimits(newXMin, newXMax, newYMin, newYMax, newY2Min, newY2Max)
self._previousDataPos = self._pixelToData(x, y)
@@ -267,7 +301,17 @@ class Pan(_ZoomOnWheel):
# Zoom ########################################################################
-class Zoom(_ZoomOnWheel):
+
+class AxesExtent(NamedTuple):
+ xmin: float
+ xmax: float
+ ymin: float
+ ymax: float
+ y2min: float
+ y2max: float
+
+
+class Zoom(_PlotInteractionWithClickEvents):
"""Zoom-in/out state machine.
Zoom-in on selected area, zoom-out on right click,
@@ -278,34 +322,67 @@ class Zoom(_ZoomOnWheel):
def __init__(self, plot, color):
self.color = color
+ self.enabledAxes = EnabledAxes()
super(Zoom, self).__init__(plot)
self.plot.getLimitsHistory().clear()
- def _areaWithAspectRatio(self, x0, y0, x1, y1):
- _plotLeft, _plotTop, plotW, plotH = self.plot.getPlotBoundsInPixels()
-
- areaX0, areaY0, areaX1, areaY1 = x0, y0, x1, y1
-
- if plotH != 0.:
- plotRatio = plotW / float(plotH)
- width, height = math.fabs(x1 - x0), math.fabs(y1 - y0)
-
- if height != 0. and width != 0.:
- if width / height > plotRatio:
- areaHeight = width / plotRatio
- areaX0, areaX1 = x0, x1
+ def _getAxesExtent(
+ self,
+ x0: float,
+ y0: float,
+ x1: float,
+ y1: float,
+ enabledAxes: Optional[EnabledAxes] = None,
+ ) -> AxesExtent:
+ """Convert selection coordinates (pixels) to axes coordinates (data)
+
+ This takes into account axes selected for zoom and aspect ratio.
+ """
+ if enabledAxes is None:
+ enabledAxes = self.enabledAxes
+
+ y2_0, y2_1 = y0, y1
+ left, top, width, height = self.plot.getPlotBoundsInPixels()
+
+ if not all(enabledAxes) and not self.plot.isKeepDataAspectRatio():
+ # Handle axes disabled for zoom if plot is not keeping aspec ratio
+ if not enabledAxes.xaxis:
+ x0, x1 = left, left + width
+ if not enabledAxes.yaxis:
+ y0, y1 = top, top + height
+ if not enabledAxes.y2axis:
+ y2_0, y2_1 = top, top + height
+
+ if self.plot.isKeepDataAspectRatio() and height != 0 and width != 0:
+ ratio = width / height
+ xextent, yextent = math.fabs(x1 - x0), math.fabs(y1 - y0)
+ if xextent != 0 and yextent != 0:
+ if xextent / yextent > ratio:
+ areaHeight = xextent / ratio
center = 0.5 * (y0 + y1)
- areaY0 = center - numpy.sign(y1 - y0) * 0.5 * areaHeight
- areaY1 = center + numpy.sign(y1 - y0) * 0.5 * areaHeight
+ y0 = center - numpy.sign(y1 - y0) * 0.5 * areaHeight
+ y1 = center + numpy.sign(y1 - y0) * 0.5 * areaHeight
else:
- areaWidth = height * plotRatio
- areaY0, areaY1 = y0, y1
+ areaWidth = yextent * ratio
center = 0.5 * (x0 + x1)
- areaX0 = center - numpy.sign(x1 - x0) * 0.5 * areaWidth
- areaX1 = center + numpy.sign(x1 - x0) * 0.5 * areaWidth
+ x0 = center - numpy.sign(x1 - x0) * 0.5 * areaWidth
+ x1 = center + numpy.sign(x1 - x0) * 0.5 * areaWidth
- return areaX0, areaY0, areaX1, areaY1
+ # Convert to data space
+ x0, y0 = self.plot.pixelToData(x0, y0, check=False)
+ x1, y1 = self.plot.pixelToData(x1, y1, check=False)
+ y2_0 = self.plot.pixelToData(None, y2_0, axis="right", check=False)[1]
+ y2_1 = self.plot.pixelToData(None, y2_1, axis="right", check=False)[1]
+
+ return AxesExtent(
+ min(x0, x1),
+ max(x0, x1),
+ min(y0, y1),
+ max(y0, y1),
+ min(y2_0, y2_1),
+ max(y2_0, y2_1),
+ )
def beginDrag(self, x, y, btn):
dataPos = self.plot.pixelToData(x, y)
@@ -319,66 +396,54 @@ class Zoom(_ZoomOnWheel):
dataPos = self.plot.pixelToData(x1, y1)
assert dataPos is not None
- if self.plot.isKeepDataAspectRatio():
- area = self._areaWithAspectRatio(self.x0, self.y0, x1, y1)
- areaX0, areaY0, areaX1, areaY1 = area
- areaPoints = ((areaX0, areaY0),
- (areaX1, areaY0),
- (areaX1, areaY1),
- (areaX0, areaY1))
- areaPoints = numpy.array([self.plot.pixelToData(
- x, y, check=False) for (x, y) in areaPoints])
-
- if self.color != 'video inverted':
+ if self.plot.isKeepDataAspectRatio() or not all(self.enabledAxes):
+ # Patch enabledAxes to display the right Y axis area on the left Y axis
+ # since the selection area is always displayed on the left Y axis
+ isY2Visible = self.plot.getYAxis("right").isVisible()
+ areaZoomEnabledAxes = EnabledAxes(
+ self.enabledAxes.xaxis,
+ self.enabledAxes.yaxis and (not isY2Visible or self.enabledAxes.y2axis),
+ self.enabledAxes.y2axis,
+ )
+ extents = self._getAxesExtent(self.x0, self.y0, x1, y1, areaZoomEnabledAxes)
+ areaCorners = (
+ (extents.xmin, extents.ymin),
+ (extents.xmax, extents.ymin),
+ (extents.xmax, extents.ymax),
+ (extents.xmin, extents.ymax),
+ )
+
+ if self.color != "video inverted":
areaColor = list(self.color)
areaColor[3] *= 0.25
else:
- areaColor = [1., 1., 1., 1.]
+ areaColor = [1.0, 1.0, 1.0, 1.0]
- self.setSelectionArea(areaPoints,
- fill='none',
- color=areaColor,
- name="zoomedArea")
+ self.setSelectionArea(
+ areaCorners, fill="none", color=areaColor, name="zoomedArea"
+ )
- corners = ((self.x0, self.y0),
- (self.x0, y1),
- (x1, y1),
- (x1, self.y0))
- corners = numpy.array([self.plot.pixelToData(x, y, check=False)
- for (x, y) in corners])
+ corners = ((self.x0, self.y0), (self.x0, y1), (x1, y1), (x1, self.y0))
+ corners = numpy.array(
+ [self.plot.pixelToData(x, y, check=False) for (x, y) in corners]
+ )
- self.setSelectionArea(corners, fill='none', color=self.color)
+ self.setSelectionArea(corners, fill="none", color=self.color)
def _zoom(self, x0, y0, x1, y1):
- """Zoom to the rectangle view x0,y0 x1,y1.
- """
- startPos = x0, y0
- endPos = x1, y1
-
+ """Zoom to the rectangle view x0,y0 x1,y1."""
# Store current zoom state in stack
self.plot.getLimitsHistory().push()
- if self.plot.isKeepDataAspectRatio():
- x0, y0, x1, y1 = self._areaWithAspectRatio(x0, y0, x1, y1)
-
- # Convert to data space and set limits
- x0, y0 = self.plot.pixelToData(x0, y0, check=False)
-
- dataPos = self.plot.pixelToData(
- startPos[0], startPos[1], axis="right", check=False)
- y2_0 = dataPos[1]
-
- x1, y1 = self.plot.pixelToData(x1, y1, check=False)
-
- dataPos = self.plot.pixelToData(
- endPos[0], endPos[1], axis="right", check=False)
- y2_1 = dataPos[1]
-
- xMin, xMax = min(x0, x1), max(x0, x1)
- yMin, yMax = min(y0, y1), max(y0, y1)
- y2Min, y2Max = min(y2_0, y2_1), max(y2_0, y2_1)
-
- self.plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
+ extents = self._getAxesExtent(x0, y0, x1, y1)
+ self.plot.setLimits(
+ extents.xmin,
+ extents.xmax,
+ extents.ymin,
+ extents.ymax,
+ extents.y2min,
+ extents.y2max,
+ )
def endDrag(self, startPos, endPos, btn):
x0, y0 = startPos
@@ -391,12 +456,13 @@ class Zoom(_ZoomOnWheel):
self.resetSelectionArea()
def cancel(self):
- if isinstance(self.state, self.states['drag']):
+ if isinstance(self.state, self.states["drag"]):
self.resetSelectionArea()
# Select ######################################################################
+
class Select(StateMachine, _PlotInteraction):
"""Base class for drawing selection areas."""
@@ -412,13 +478,9 @@ class Select(StateMachine, _PlotInteraction):
self.parameters = parameters
StateMachine.__init__(self, states, state)
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.plot, scaleF, (x, y))
-
@property
def color(self):
- return self.parameters.get('color', None)
+ return self.parameters.get("color", None)
class SelectPolygon(Select):
@@ -429,7 +491,7 @@ class SelectPolygon(Select):
class Idle(State):
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
- self.goto('select', x, y)
+ self.goto("select", x, y)
return True
class Select(State):
@@ -446,25 +508,28 @@ class SelectPolygon(Select):
x, y = self.machine.plot.dataToPixel(*self._firstPos, check=False)
offset = self.machine.getDragThreshold()
- points = [(x - offset, y - offset),
- (x - offset, y + offset),
- (x + offset, y + offset),
- (x + offset, y - offset)]
- points = [self.machine.plot.pixelToData(xpix, ypix, check=False)
- for xpix, ypix in points]
- self.machine.setSelectionArea(points, fill=None,
- color=self.machine.color,
- name='first_point')
+ points = [
+ (x - offset, y - offset),
+ (x - offset, y + offset),
+ (x + offset, y + offset),
+ (x + offset, y - offset),
+ ]
+ points = [
+ self.machine.plot.pixelToData(xpix, ypix, check=False)
+ for xpix, ypix in points
+ ]
+ self.machine.setSelectionArea(
+ points, fill=None, color=self.machine.color, name="first_point"
+ )
def updateSelectionArea(self):
"""Update drawing selection area using self.points"""
- self.machine.setSelectionArea(self.points,
- fill='hatch',
- color=self.machine.color)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'polygon',
- self.points,
- self.machine.parameters)
+ self.machine.setSelectionArea(
+ self.points, fill="hatch", color=self.machine.color
+ )
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "polygon", self.points, self.machine.parameters
+ )
self.machine.plot.notify(**eventDict)
def validate(self):
@@ -478,12 +543,11 @@ class SelectPolygon(Select):
def closePolygon(self):
self.machine.resetSelectionArea()
self.points[-1] = self.points[0]
- eventDict = prepareDrawingSignal('drawingFinished',
- 'polygon',
- self.points,
- self.machine.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "polygon", self.points, self.machine.parameters
+ )
self.machine.plot.notify(**eventDict)
- self.goto('idle')
+ self.goto("idle")
def onWheel(self, x, y, angle):
self.machine.onWheel(x, y, angle)
@@ -493,8 +557,7 @@ class SelectPolygon(Select):
if btn == LEFT_BTN:
# checking if the position is close to the first point
# if yes : closing the "loop"
- firstPos = self.machine.plot.dataToPixel(*self._firstPos,
- check=False)
+ firstPos = self.machine.plot.dataToPixel(*self._firstPos, check=False)
dx, dy = abs(firstPos[0] - x), abs(firstPos[1] - y)
threshold = self.machine.getDragThreshold()
@@ -516,8 +579,9 @@ class SelectPolygon(Select):
# in Idle state, but with a slightly different position that
# the mouse press. So we had the two first vertices that were
# almost identical.
- previousPos = self.machine.plot.dataToPixel(*self.points[-2],
- check=False)
+ previousPos = self.machine.plot.dataToPixel(
+ *self.points[-2], check=False
+ )
dx, dy = abs(previousPos[0] - x), abs(previousPos[1] - y)
if dx >= threshold or dy >= threshold:
self.points.append(dataPos)
@@ -528,8 +592,7 @@ class SelectPolygon(Select):
return False
def onMove(self, x, y):
- firstPos = self.machine.plot.dataToPixel(*self._firstPos,
- check=False)
+ firstPos = self.machine.plot.dataToPixel(*self._firstPos, check=False)
dx, dy = abs(firstPos[0] - x), abs(firstPos[1] - y)
threshold = self.machine.getDragThreshold()
@@ -542,15 +605,11 @@ class SelectPolygon(Select):
self.updateSelectionArea()
def __init__(self, plot, parameters):
- states = {
- 'idle': SelectPolygon.Idle,
- 'select': SelectPolygon.Select
- }
- super(SelectPolygon, self).__init__(plot, parameters,
- states, 'idle')
+ states = {"idle": SelectPolygon.Idle, "select": SelectPolygon.Select}
+ super(SelectPolygon, self).__init__(plot, parameters, states, "idle")
def cancel(self):
- if isinstance(self.state, self.states['select']):
+ if isinstance(self.state, self.states["select"]):
self.resetSelectionArea()
def getDragThreshold(self):
@@ -564,10 +623,11 @@ class SelectPolygon(Select):
class Select2Points(Select):
"""Base class for drawing selection based on 2 input points."""
+
class Idle(State):
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
- self.goto('start', x, y)
+ self.goto("start", x, y)
return True
class Start(State):
@@ -575,11 +635,11 @@ class Select2Points(Select):
self.machine.beginSelect(x, y)
def onMove(self, x, y):
- self.goto('select', x, y)
+ self.goto("select", x, y)
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
- self.goto('select', x, y)
+ self.goto("select", x, y)
return True
class Select(State):
@@ -592,16 +652,15 @@ class Select2Points(Select):
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
self.machine.endSelect(x, y)
- self.goto('idle')
+ self.goto("idle")
def __init__(self, plot, parameters):
states = {
- 'idle': Select2Points.Idle,
- 'start': Select2Points.Start,
- 'select': Select2Points.Select
+ "idle": Select2Points.Idle,
+ "start": Select2Points.Start,
+ "select": Select2Points.Select,
}
- super(Select2Points, self).__init__(plot, parameters,
- states, 'idle')
+ super(Select2Points, self).__init__(plot, parameters, states, "idle")
def beginSelect(self, x, y):
pass
@@ -616,12 +675,13 @@ class Select2Points(Select):
pass
def cancel(self):
- if isinstance(self.state, self.states['select']):
+ if isinstance(self.state, self.states["select"]):
self.cancelSelect()
class SelectEllipse(Select2Points):
"""Drawing ellipse selection area state machine."""
+
def beginSelect(self, x, y):
self.center = self.plot.pixelToData(x, y)
assert self.center is not None
@@ -667,21 +727,23 @@ class SelectEllipse(Select2Points):
width, height = self._getEllipseSize(dataPos)
# Circle used for circle preview
- nbpoints = 27.
+ nbpoints = 27.0
angles = numpy.arange(nbpoints) * numpy.pi * 2.0 / nbpoints
- circleShape = numpy.array((numpy.cos(angles) * width,
- numpy.sin(angles) * height)).T
+ circleShape = numpy.array(
+ (numpy.cos(angles) * width, numpy.sin(angles) * height)
+ ).T
circleShape += numpy.array(self.center)
- self.setSelectionArea(circleShape,
- shape="polygon",
- fill='hatch',
- color=self.color)
+ self.setSelectionArea(
+ circleShape, shape="polygon", fill="hatch", color=self.color
+ )
- eventDict = prepareDrawingSignal('drawingProgress',
- 'ellipse',
- (self.center, (width, height)),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingProgress",
+ "ellipse",
+ (self.center, (width, height)),
+ self.parameters,
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
@@ -691,10 +753,12 @@ class SelectEllipse(Select2Points):
assert dataPos is not None
width, height = self._getEllipseSize(dataPos)
- eventDict = prepareDrawingSignal('drawingFinished',
- 'ellipse',
- (self.center, (width, height)),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished",
+ "ellipse",
+ (self.center, (width, height)),
+ self.parameters,
+ )
self.plot.notify(**eventDict)
def cancelSelect(self):
@@ -703,6 +767,7 @@ class SelectEllipse(Select2Points):
class SelectRectangle(Select2Points):
"""Drawing rectangle selection area state machine."""
+
def beginSelect(self, x, y):
self.startPt = self.plot.pixelToData(x, y)
assert self.startPt is not None
@@ -711,17 +776,20 @@ class SelectRectangle(Select2Points):
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- self.setSelectionArea((self.startPt,
- (self.startPt[0], dataPos[1]),
- dataPos,
- (dataPos[0], self.startPt[1])),
- fill='hatch',
- color=self.color)
-
- eventDict = prepareDrawingSignal('drawingProgress',
- 'rectangle',
- (self.startPt, dataPos),
- self.parameters)
+ self.setSelectionArea(
+ (
+ self.startPt,
+ (self.startPt[0], dataPos[1]),
+ dataPos,
+ (dataPos[0], self.startPt[1]),
+ ),
+ fill="hatch",
+ color=self.color,
+ )
+
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "rectangle", (self.startPt, dataPos), self.parameters
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
@@ -730,10 +798,9 @@ class SelectRectangle(Select2Points):
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- eventDict = prepareDrawingSignal('drawingFinished',
- 'rectangle',
- (self.startPt, dataPos),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "rectangle", (self.startPt, dataPos), self.parameters
+ )
self.plot.notify(**eventDict)
def cancelSelect(self):
@@ -742,6 +809,7 @@ class SelectRectangle(Select2Points):
class SelectLine(Select2Points):
"""Drawing line selection area state machine."""
+
def beginSelect(self, x, y):
self.startPt = self.plot.pixelToData(x, y)
assert self.startPt is not None
@@ -750,14 +818,11 @@ class SelectLine(Select2Points):
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- self.setSelectionArea((self.startPt, dataPos),
- fill='hatch',
- color=self.color)
+ self.setSelectionArea((self.startPt, dataPos), fill="hatch", color=self.color)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'line',
- (self.startPt, dataPos),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "line", (self.startPt, dataPos), self.parameters
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
@@ -766,10 +831,9 @@ class SelectLine(Select2Points):
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- eventDict = prepareDrawingSignal('drawingFinished',
- 'line',
- (self.startPt, dataPos),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "line", (self.startPt, dataPos), self.parameters
+ )
self.plot.notify(**eventDict)
def cancelSelect(self):
@@ -778,10 +842,11 @@ class SelectLine(Select2Points):
class Select1Point(Select):
"""Base class for drawing selection area based on one input point."""
+
class Idle(State):
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
- self.goto('select', x, y)
+ self.goto("select", x, y)
return True
class Select(State):
@@ -794,18 +859,15 @@ class Select1Point(Select):
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
self.machine.endSelect(x, y)
- self.goto('idle')
+ self.goto("idle")
def onWheel(self, x, y, angle):
self.machine.onWheel(x, y, angle) # Call select default wheel
self.machine.select(x, y)
def __init__(self, plot, parameters):
- states = {
- 'idle': Select1Point.Idle,
- 'select': Select1Point.Select
- }
- super(Select1Point, self).__init__(plot, parameters, states, 'idle')
+ states = {"idle": Select1Point.Idle, "select": Select1Point.Select}
+ super(Select1Point, self).__init__(plot, parameters, states, "idle")
def select(self, x, y):
pass
@@ -817,12 +879,13 @@ class Select1Point(Select):
pass
def cancel(self):
- if isinstance(self.state, self.states['select']):
+ if isinstance(self.state, self.states["select"]):
self.cancelSelect()
class SelectHLine(Select1Point):
"""Drawing a horizontal line selection area state machine."""
+
def _hLine(self, y):
"""Return points in data coords of the segment visible in the plot.
@@ -836,21 +899,19 @@ class SelectHLine(Select1Point):
def select(self, x, y):
points = self._hLine(y)
- self.setSelectionArea(points, fill='hatch', color=self.color)
+ self.setSelectionArea(points, fill="hatch", color=self.color)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'hline',
- points,
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "hline", points, self.parameters
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
self.resetSelectionArea()
- eventDict = prepareDrawingSignal('drawingFinished',
- 'hline',
- self._hLine(y),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "hline", self._hLine(y), self.parameters
+ )
self.plot.notify(**eventDict)
def cancelSelect(self):
@@ -859,6 +920,7 @@ class SelectHLine(Select1Point):
class SelectVLine(Select1Point):
"""Drawing a vertical line selection area state machine."""
+
def _vLine(self, x):
"""Return points in data coords of the segment visible in the plot.
@@ -872,21 +934,19 @@ class SelectVLine(Select1Point):
def select(self, x, y):
points = self._vLine(x)
- self.setSelectionArea(points, fill='hatch', color=self.color)
+ self.setSelectionArea(points, fill="hatch", color=self.color)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'vline',
- points,
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "vline", points, self.parameters
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
self.resetSelectionArea()
- eventDict = prepareDrawingSignal('drawingFinished',
- 'vline',
- self._vLine(x),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "vline", self._vLine(x), self.parameters
+ )
self.plot.notify(**eventDict)
def cancelSelect(self):
@@ -901,7 +961,7 @@ class DrawFreeHand(Select):
class Idle(State):
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
- self.goto('select', x, y)
+ self.goto("select", x, y)
return True
def onMove(self, x, y):
@@ -924,7 +984,7 @@ class DrawFreeHand(Select):
if self.__isOut:
self.machine.resetSelectionArea()
self.machine.endSelect(x, y)
- self.goto('idle')
+ self.goto("idle")
def onEnter(self):
self.__isOut = False
@@ -934,20 +994,16 @@ class DrawFreeHand(Select):
def __init__(self, plot, parameters):
# Circle used for pencil preview
- angle = numpy.arange(13.) * numpy.pi * 2.0 / 13.
- size = parameters.get('width', 1.) * 0.5
- self._circle = size * numpy.array((numpy.cos(angle),
- numpy.sin(angle))).T
+ angle = numpy.arange(13.0) * numpy.pi * 2.0 / 13.0
+ size = parameters.get("width", 1.0) * 0.5
+ self._circle = size * numpy.array((numpy.cos(angle), numpy.sin(angle))).T
- states = {
- 'idle': DrawFreeHand.Idle,
- 'select': DrawFreeHand.Select
- }
- super(DrawFreeHand, self).__init__(plot, parameters, states, 'idle')
+ states = {"idle": DrawFreeHand.Idle, "select": DrawFreeHand.Select}
+ super(DrawFreeHand, self).__init__(plot, parameters, states, "idle")
@property
def width(self):
- return self.parameters.get('width', None)
+ return self.parameters.get("width", None)
def setFirstPoint(self, x, y):
self._points = []
@@ -959,7 +1015,7 @@ class DrawFreeHand(Select):
polygon = center + self._circle
- self.setSelectionArea(polygon, fill='none', color=self.color)
+ self.setSelectionArea(polygon, fill="none", color=self.color)
def select(self, x, y):
pos = self.plot.pixelToData(x, y, check=False)
@@ -968,10 +1024,9 @@ class DrawFreeHand(Select):
# Skip same points
return
self._points.append(pos)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'polylines',
- self._points,
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "polylines", self._points, self.parameters
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
@@ -981,10 +1036,9 @@ class DrawFreeHand(Select):
# Append if different
self._points.append(pos)
- eventDict = prepareDrawingSignal('drawingFinished',
- 'polylines',
- self._points,
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "polylines", self._points, self.parameters
+ )
self.plot.notify(**eventDict)
self._points = None
@@ -1010,13 +1064,9 @@ class SelectFreeLine(ClickOrDrag, _PlotInteraction):
_PlotInteraction.__init__(self, plot)
self.parameters = parameters
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.plot, scaleF, (x, y))
-
@property
def color(self):
- return self.parameters.get('color', None)
+ return self.parameters.get("color", None)
def click(self, x, y, btn):
if btn == LEFT_BTN:
@@ -1045,21 +1095,24 @@ class SelectFreeLine(ClickOrDrag, _PlotInteraction):
if isNewPoint or isLast:
eventDict = prepareDrawingSignal(
- 'drawingFinished' if isLast else 'drawingProgress',
- 'polylines',
+ "drawingFinished" if isLast else "drawingProgress",
+ "polylines",
self._points,
- self.parameters)
+ self.parameters,
+ )
self.plot.notify(**eventDict)
if not isLast:
- self.setSelectionArea(self._points, fill='none', color=self.color,
- shape='polylines')
+ self.setSelectionArea(
+ self._points, fill="none", color=self.color, shape="polylines"
+ )
else:
self.cancel()
# ItemInteraction #############################################################
+
class ItemsInteraction(ClickOrDrag, _PlotInteraction):
"""Interaction with items (markers, curves and images).
@@ -1073,9 +1126,12 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
super(ItemsInteraction.Idle, self).__init__(*args, **kw)
self._hoverMarker = None
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.machine.plot, scaleF, (x, y))
+ def enterState(self):
+ widget = self.machine.plot.getWidgetHandle()
+ if widget is None or not widget.isVisible():
+ return
+ position = widget.mapFromGlobal(qt.QCursor.pos())
+ self.onMove(position.x(), position.y())
def onMove(self, x, y):
marker = self.machine.plot._getMarkerAt(x, y)
@@ -1084,30 +1140,18 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
dataPos = self.machine.plot.pixelToData(x, y)
assert dataPos is not None
eventDict = prepareHoverSignal(
- marker.getName(), 'marker',
- dataPos, (x, y),
+ marker.getName(),
+ "marker",
+ dataPos,
+ (x, y),
marker.isDraggable(),
- marker.isSelectable())
+ marker.isSelectable(),
+ )
self.machine.plot.notify(**eventDict)
if marker != self._hoverMarker:
self._hoverMarker = marker
-
- if marker is None:
- self.machine.plot.setGraphCursorShape()
-
- elif marker.isDraggable():
- if isinstance(marker, items.YMarker):
- self.machine.plot.setGraphCursorShape(CURSOR_SIZE_VER)
- elif isinstance(marker, items.XMarker):
- self.machine.plot.setGraphCursorShape(CURSOR_SIZE_HOR)
- else:
- self.machine.plot.setGraphCursorShape(CURSOR_SIZE_ALL)
-
- elif marker.isSelectable():
- self.machine.plot.setGraphCursorShape(CURSOR_POINTING)
- else:
- self.machine.plot.setGraphCursorShape()
+ self.machine._setCursorForMarker(marker)
return True
@@ -1115,9 +1159,30 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
self._pan = Pan(plot)
_PlotInteraction.__init__(self, plot)
- ClickOrDrag.__init__(self,
- clickButtons=(LEFT_BTN, RIGHT_BTN),
- dragButtons=(LEFT_BTN, MIDDLE_BTN))
+ ClickOrDrag.__init__(
+ self, clickButtons=(LEFT_BTN, RIGHT_BTN), dragButtons=(LEFT_BTN, MIDDLE_BTN)
+ )
+
+ def _setCursorForMarker(self, marker: Optional[items.MarkerBase] = None):
+ """Set mouse cursor for given marker"""
+ if marker is None:
+ cursor = None
+
+ elif marker.isDraggable():
+ if isinstance(marker, items.YMarker):
+ cursor = CURSOR_SIZE_VER
+ elif isinstance(marker, items.XMarker):
+ cursor = CURSOR_SIZE_HOR
+ else:
+ cursor = CURSOR_SIZE_ALL
+
+ elif marker.isSelectable():
+ cursor = CURSOR_POINTING
+
+ else:
+ cursor = None
+
+ self.plot.setGraphCursorShape(cursor)
def click(self, x, y, btn):
"""Handle mouse click
@@ -1130,9 +1195,9 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
# Signal mouse clicked event
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- eventDict = prepareMouseSignal('mouseClicked', btn,
- dataPos[0], dataPos[1],
- x, y)
+ eventDict = prepareMouseSignal(
+ "mouseClicked", btn, dataPos[0], dataPos[1], x, y
+ )
self.plot.notify(**eventDict)
eventDict = self._handleClick(x, y, btn)
@@ -1163,14 +1228,17 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
if yData is None:
yData = [0, 1]
- eventDict = prepareMarkerSignal('markerClicked',
- 'left',
- item.getName(),
- 'marker',
- item.isDraggable(),
- item.isSelectable(),
- (xData, yData),
- (x, y), None)
+ eventDict = prepareMarkerSignal(
+ "markerClicked",
+ "left",
+ item.getName(),
+ "marker",
+ item.isDraggable(),
+ item.isSelectable(),
+ (xData, yData),
+ (x, y),
+ None,
+ )
return eventDict
elif isinstance(item, items.Curve):
@@ -1181,13 +1249,16 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
yData = item.getYData(copy=False)
indices = result.getIndices(copy=False)
- eventDict = prepareCurveSignal('left',
- item.getName(),
- 'curve',
- xData[indices],
- yData[indices],
- dataPos[0], dataPos[1],
- x, y)
+ eventDict = prepareCurveSignal(
+ "left",
+ item,
+ xData[indices],
+ yData[indices],
+ dataPos[0],
+ dataPos[1],
+ x,
+ y,
+ )
return eventDict
elif isinstance(item, items.ImageBase):
@@ -1196,12 +1267,9 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
indices = result.getIndices(copy=False)
row, column = indices[0][0], indices[1][0]
- eventDict = prepareImageSignal('left',
- item.getName(),
- 'image',
- column, row,
- dataPos[0], dataPos[1],
- x, y)
+ eventDict = prepareImageSignal(
+ "left", item, column, row, dataPos[0], dataPos[1], x, y
+ )
return eventDict
return None
@@ -1218,24 +1286,26 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
posDataCursor = self.plot.pixelToData(x, y)
assert posDataCursor is not None
- eventDict = prepareMarkerSignal(eventType,
- 'left',
- marker.getName(),
- 'marker',
- marker.isDraggable(),
- marker.isSelectable(),
- (xData, yData),
- (x, y),
- posDataCursor)
+ eventDict = prepareMarkerSignal(
+ eventType,
+ "left",
+ marker.getName(),
+ "marker",
+ marker.isDraggable(),
+ marker.isSelectable(),
+ (xData, yData),
+ (x, y),
+ posDataCursor,
+ )
self.plot.notify(**eventDict)
@staticmethod
def __isDraggableItem(item):
return isinstance(item, items.DraggableMixIn) and item.isDraggable()
- def __terminateDrag(self):
+ def __terminateDrag(self, x, y):
"""Finalize a drag operation by reseting to initial state"""
- self.plot.setGraphCursorShape()
+ self._setCursorForMarker(self.plot._getMarkerAt(x, y))
self.draggedItemRef = None
def beginDrag(self, x, y, btn):
@@ -1256,11 +1326,11 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
self.draggedItemRef = None if item is None else weakref.ref(item)
if item is None:
- self.__terminateDrag()
+ self.__terminateDrag(x, y)
return False
if isinstance(item, items.MarkerBase):
- self._signalMarkerMovingEvent('markerMoving', item, x, y)
+ self._signalMarkerMovingEvent("markerMoving", item, x, y)
item._startDrag()
return True
@@ -1278,7 +1348,7 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
item.drag(self._lastPos, dataPos)
if isinstance(item, items.MarkerBase):
- self._signalMarkerMovingEvent('markerMoving', item, x, y)
+ self._signalMarkerMovingEvent("markerMoving", item, x, y)
self._lastPos = dataPos
elif btn == MIDDLE_BTN:
@@ -1290,46 +1360,52 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
if isinstance(item, items.MarkerBase):
posData = list(item.getPosition())
if posData[0] is None:
- posData[0] = 1.
+ posData[0] = 1.0
if posData[1] is None:
- posData[1] = 1.
+ posData[1] = 1.0
eventDict = prepareMarkerSignal(
- 'markerMoved',
- 'left',
+ "markerMoved",
+ "left",
item.getLegend(),
- 'marker',
+ "marker",
item.isDraggable(),
item.isSelectable(),
- posData)
+ posData,
+ )
self.plot.notify(**eventDict)
item._endDrag()
- self.__terminateDrag()
+ self.__terminateDrag(*endPos)
elif btn == MIDDLE_BTN:
self._pan.endDrag(startPos, endPos, btn)
def cancel(self):
self._pan.cancel()
- self.__terminateDrag()
+ widget = self.plot.getWidgetHandle()
+ if widget is None or not widget.isVisible():
+ return
+ position = widget.mapFromGlobal(qt.QCursor.pos())
+ self.__terminateDrag(position.x(), position.y())
class ItemsInteractionForCombo(ItemsInteraction):
- """Interaction with items to combine through :class:`FocusManager`.
- """
+ """Interaction with items to combine through :class:`FocusManager`."""
class Idle(ItemsInteraction.Idle):
@staticmethod
def __isItemSelectableOrDraggable(item):
- return (item.isSelectable() or (
- isinstance(item, items.DraggableMixIn) and item.isDraggable()))
+ return item.isSelectable() or (
+ isinstance(item, items.DraggableMixIn) and item.isDraggable()
+ )
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
result = self.machine.plot._pickTopMost(
- x, y, self.__isItemSelectableOrDraggable)
+ x, y, self.__isItemSelectableOrDraggable
+ )
if result is not None: # Request focus and handle interaction
- self.goto('clickOrDrag', x, y, btn)
+ self.goto("clickOrDrag", x, y, btn)
return True
else: # Do not request focus
return False
@@ -1339,19 +1415,21 @@ class ItemsInteractionForCombo(ItemsInteraction):
# FocusManager ################################################################
+
class FocusManager(StateMachine):
"""Manages focus across multiple event handlers
On press an event handler can acquire focus.
By default it looses focus when all buttons are released.
"""
+
class Idle(State):
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
for eventHandler in self.machine.eventHandlers:
- requestFocus = eventHandler.handleEvent('press', x, y, btn)
+ requestFocus = eventHandler.handleEvent("press", x, y, btn)
if requestFocus:
- self.goto('focus', eventHandler, btn)
+ self.goto("focus", eventHandler, btn)
break
def _processEvent(self, *args):
@@ -1361,14 +1439,14 @@ class FocusManager(StateMachine):
break
def onMove(self, x, y):
- self._processEvent('move', x, y)
+ self._processEvent("move", x, y)
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
- self._processEvent('release', x, y, btn)
+ self._processEvent("release", x, y, btn)
def onWheel(self, x, y, angle):
- self._processEvent('wheel', x, y, angle)
+ self._processEvent("wheel", x, y, angle)
class Focus(State):
def enterState(self, eventHandler, btn):
@@ -1377,34 +1455,31 @@ class FocusManager(StateMachine):
def validate(self):
self.eventHandler.validate()
- self.goto('idle')
+ self.goto("idle")
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
self.focusBtns.add(btn)
- self.eventHandler.handleEvent('press', x, y, btn)
+ self.eventHandler.handleEvent("press", x, y, btn)
def onMove(self, x, y):
- self.eventHandler.handleEvent('move', x, y)
+ self.eventHandler.handleEvent("move", x, y)
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
self.focusBtns.discard(btn)
- requestFocus = self.eventHandler.handleEvent('release', x, y, btn)
+ requestFocus = self.eventHandler.handleEvent("release", x, y, btn)
if len(self.focusBtns) == 0 and not requestFocus:
- self.goto('idle')
+ self.goto("idle")
def onWheel(self, x, y, angleInDegrees):
- self.eventHandler.handleEvent('wheel', x, y, angleInDegrees)
+ self.eventHandler.handleEvent("wheel", x, y, angleInDegrees)
def __init__(self, eventHandlers=()):
self.eventHandlers = list(eventHandlers)
- states = {
- 'idle': FocusManager.Idle,
- 'focus': FocusManager.Focus
- }
- super(FocusManager, self).__init__(states, 'idle')
+ states = {"idle": FocusManager.Idle, "focus": FocusManager.Focus}
+ super(FocusManager, self).__init__(states, "idle")
def cancel(self):
for handler in self.eventHandlers:
@@ -1428,6 +1503,15 @@ class ZoomAndSelect(ItemsInteraction):
"""Color of the zoom area"""
return self._zoom.color
+ @property
+ def zoomEnabledAxes(self) -> EnabledAxes:
+ """Whether or not to apply zoom for each axis"""
+ return self._zoom.enabledAxes
+
+ @zoomEnabledAxes.setter
+ def zoomEnabledAxes(self, enabledAxes: EnabledAxes):
+ self._zoom.enabledAxes = enabledAxes
+
def click(self, x, y, btn):
"""Handle mouse click
@@ -1442,9 +1526,9 @@ class ZoomAndSelect(ItemsInteraction):
# Signal mouse clicked event
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- clickedEventDict = prepareMouseSignal('mouseClicked', btn,
- dataPos[0], dataPos[1],
- x, y)
+ clickedEventDict = prepareMouseSignal(
+ "mouseClicked", btn, dataPos[0], dataPos[1], x, y
+ )
self.plot.notify(**clickedEventDict)
self.plot.notify(**eventDict)
@@ -1513,9 +1597,9 @@ class PanAndSelect(ItemsInteraction):
# Signal mouse clicked event
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- clickedEventDict = prepareMouseSignal('mouseClicked', btn,
- dataPos[0], dataPos[1],
- x, y)
+ clickedEventDict = prepareMouseSignal(
+ "mouseClicked", btn, dataPos[0], dataPos[1], x, y
+ )
self.plot.notify(**clickedEventDict)
self.plot.notify(**eventDict)
@@ -1563,15 +1647,15 @@ class PanAndSelect(ItemsInteraction):
# Mapping of draw modes: event handler
_DRAW_MODES = {
- 'polygon': SelectPolygon,
- 'rectangle': SelectRectangle,
- 'ellipse': SelectEllipse,
- 'line': SelectLine,
- 'vline': SelectVLine,
- 'hline': SelectHLine,
- 'polylines': SelectFreeLine,
- 'pencil': DrawFreeHand,
- }
+ "polygon": SelectPolygon,
+ "rectangle": SelectRectangle,
+ "ellipse": SelectEllipse,
+ "line": SelectLine,
+ "vline": SelectVLine,
+ "hline": SelectHLine,
+ "polylines": SelectFreeLine,
+ "pencil": DrawFreeHand,
+}
class DrawMode(FocusManager):
@@ -1580,19 +1664,22 @@ class DrawMode(FocusManager):
def __init__(self, plot, shape, label, color, width):
eventHandlerClass = _DRAW_MODES[shape]
parameters = {
- 'shape': shape,
- 'label': label,
- 'color': color,
- 'width': width,
- }
- super().__init__((
- Pan(plot, clickButtons=(), dragButtons=(MIDDLE_BTN,)),
- eventHandlerClass(plot, parameters)))
+ "shape": shape,
+ "label": label,
+ "color": color,
+ "width": width,
+ }
+ super().__init__(
+ (
+ Pan(plot, clickButtons=(), dragButtons=(MIDDLE_BTN,)),
+ eventHandlerClass(plot, parameters),
+ )
+ )
def getDescription(self):
"""Returns the dict describing this interactive mode"""
params = self.eventHandlers[1].parameters.copy()
- params['mode'] = 'draw'
+ params["mode"] = "draw"
return params
@@ -1604,27 +1691,27 @@ class DrawSelectMode(FocusManager):
self._pan = Pan(plot)
self._panStart = None
parameters = {
- 'shape': shape,
- 'label': label,
- 'color': color,
- 'width': width,
- }
- super().__init__((
- ItemsInteractionForCombo(plot),
- eventHandlerClass(plot, parameters)))
+ "shape": shape,
+ "label": label,
+ "color": color,
+ "width": width,
+ }
+ super().__init__(
+ (ItemsInteractionForCombo(plot), eventHandlerClass(plot, parameters))
+ )
def handleEvent(self, eventName, *args, **kwargs):
# Hack to add pan interaction to select-draw
# See issue Refactor PlotWidget interaction #3292
- if eventName == 'press' and args[2] == MIDDLE_BTN:
+ if eventName == "press" and args[2] == MIDDLE_BTN:
self._panStart = args[:2]
self._pan.beginDrag(*args)
return # Consume middle click events
- elif eventName == 'release' and args[2] == MIDDLE_BTN:
+ elif eventName == "release" and args[2] == MIDDLE_BTN:
self._panStart = None
self._pan.endDrag(self._panStart, args[:2], MIDDLE_BTN)
return # Consume middle click events
- elif self._panStart is not None and eventName == 'move':
+ elif self._panStart is not None and eventName == "move":
x, y = args[:2]
self._pan.drag(x, y, MIDDLE_BTN)
@@ -1633,67 +1720,94 @@ class DrawSelectMode(FocusManager):
def getDescription(self):
"""Returns the dict describing this interactive mode"""
params = self.eventHandlers[1].parameters.copy()
- params['mode'] = 'select-draw'
+ params["mode"] = "select-draw"
return params
-class PlotInteraction(object):
- """Proxy to currently use state machine for interaction.
-
- This allows to switch interactive mode.
+class PlotInteraction(qt.QObject):
+ """PlotWidget user interaction handler.
- :param plot: The :class:`Plot` to apply interaction to
+ :param plot: The :class:`PlotWidget` to apply interaction to
"""
+ sigChanged = qt.Signal()
+ """Signal emitted when the interaction configuration has changed"""
+
_DRAW_MODES = {
- 'polygon': SelectPolygon,
- 'rectangle': SelectRectangle,
- 'ellipse': SelectEllipse,
- 'line': SelectLine,
- 'vline': SelectVLine,
- 'hline': SelectHLine,
- 'polylines': SelectFreeLine,
- 'pencil': DrawFreeHand,
+ "polygon": SelectPolygon,
+ "rectangle": SelectRectangle,
+ "ellipse": SelectEllipse,
+ "line": SelectLine,
+ "vline": SelectVLine,
+ "hline": SelectHLine,
+ "polylines": SelectFreeLine,
+ "pencil": DrawFreeHand,
}
- def __init__(self, plot):
- self._plot = weakref.ref(plot) # Avoid cyclic-ref
-
- self.zoomOnWheel = True
- """True to enable zoom on wheel, False otherwise."""
+ def __init__(self, parent):
+ super().__init__(parent)
+ self.__zoomOnWheel = True
+ self.__zoomEnabledAxes = EnabledAxes()
# Default event handler
- self._eventHandler = ItemsInteraction(plot)
+ self._eventHandler = ItemsInteraction(parent)
+
+ def isZoomOnWheelEnabled(self) -> bool:
+ """Returns whether or not wheel interaction triggers zoom"""
+ return self.__zoomOnWheel
+
+ def setZoomOnWheelEnabled(self, enabled: bool):
+ """Toggle zoom on wheel interaction"""
+ if enabled != self.__zoomOnWheel:
+ self.__zoomOnWheel = enabled
+ self.sigChanged.emit()
- def getInteractiveMode(self):
+ def setZoomEnabledAxes(self, xaxis: bool, yaxis: bool, y2axis: bool):
+ """Toggle zoom interaction for each axis
+
+ This is taken into account only if the plot does not keep aspect ratio.
+ """
+ zoomEnabledAxes = EnabledAxes(xaxis, yaxis, y2axis)
+ if zoomEnabledAxes != self.__zoomEnabledAxes:
+ self.__zoomEnabledAxes = zoomEnabledAxes
+ if isinstance(self._eventHandler, ZoomAndSelect):
+ self._eventHandler.zoomEnabledAxes = zoomEnabledAxes
+ self.sigChanged.emit()
+
+ def getZoomEnabledAxes(self) -> EnabledAxes:
+ """Returns axes for which zoom is enabled"""
+ return self.__zoomEnabledAxes
+
+ def _getInteractiveMode(self):
"""Returns the current interactive mode as a dict.
The returned dict contains at least the key 'mode'.
Mode can be: 'draw', 'pan', 'select', 'select-draw', 'zoom'.
It can also contains extra keys (e.g., 'color') specific to a mode
- as provided to :meth:`setInteractiveMode`.
+ as provided to :meth:`_setInteractiveMode`.
"""
if isinstance(self._eventHandler, ZoomAndSelect):
- return {'mode': 'zoom', 'color': self._eventHandler.color}
+ return {"mode": "zoom", "color": self._eventHandler.color}
elif isinstance(self._eventHandler, (DrawMode, DrawSelectMode)):
return self._eventHandler.getDescription()
elif isinstance(self._eventHandler, PanAndSelect):
- return {'mode': 'pan'}
+ return {"mode": "pan"}
else:
- return {'mode': 'select'}
+ return {"mode": "select"}
- def validate(self):
+ def _validate(self):
"""Validate the current interaction if possible
If was designed to close the polygon interaction.
"""
self._eventHandler.validate()
- def setInteractiveMode(self, mode, color='black',
- shape='polygon', label=None, width=None):
+ def _setInteractiveMode(
+ self, mode, color="black", shape="polygon", label=None, width=None
+ ):
"""Switch the interactive mode.
:param str mode: The name of the interactive mode.
@@ -1710,36 +1824,62 @@ class PlotInteraction(object):
:param str label: Only for 'draw' mode.
:param float width: Width of the pencil. Only for draw pencil mode.
"""
- assert mode in ('draw', 'pan', 'select', 'select-draw', 'zoom')
+ assert mode in ("draw", "pan", "select", "select-draw", "zoom")
- plot = self._plot()
- assert plot is not None
+ plotWidget = self.parent()
+ assert plotWidget is not None
- if isinstance(color, numpy.ndarray) or color not in (None, 'video inverted'):
+ if isinstance(color, numpy.ndarray) or color not in (None, "video inverted"):
color = colors.rgba(color)
- if mode in ('draw', 'select-draw'):
+ if mode in ("draw", "select-draw"):
self._eventHandler.cancel()
- handlerClass = DrawMode if mode == 'draw' else DrawSelectMode
- self._eventHandler = handlerClass(plot, shape, label, color, width)
+ handlerClass = DrawMode if mode == "draw" else DrawSelectMode
+ self._eventHandler = handlerClass(plotWidget, shape, label, color, width)
- elif mode == 'pan':
+ elif mode == "pan":
# Ignores color, shape and label
self._eventHandler.cancel()
- self._eventHandler = PanAndSelect(plot)
+ self._eventHandler = PanAndSelect(plotWidget)
- elif mode == 'zoom':
+ elif mode == "zoom":
# Ignores shape and label
self._eventHandler.cancel()
- self._eventHandler = ZoomAndSelect(plot, color)
+ self._eventHandler = ZoomAndSelect(plotWidget, color)
+ self._eventHandler.zoomEnabledAxes = self.getZoomEnabledAxes()
else: # Default mode: interaction with plot objects
# Ignores color, shape and label
self._eventHandler.cancel()
- self._eventHandler = ItemsInteraction(plot)
+ self._eventHandler = ItemsInteraction(plotWidget)
+
+ self.sigChanged.emit()
def handleEvent(self, event, *args, **kwargs):
"""Forward event to current interactive mode state machine."""
- if not self.zoomOnWheel and event == 'wheel':
- return # Discard wheel events
+ if event == "wheel": # Handle wheel events directly
+ self._onWheel(*args, **kwargs)
+ return
+
self._eventHandler.handleEvent(event, *args, **kwargs)
+
+ def _onWheel(self, x: float, y: float, angle: float):
+ """Handle wheel events"""
+ if not self.isZoomOnWheelEnabled():
+ return
+
+ plotWidget = self.parent()
+ if plotWidget is None:
+ return
+
+ # All axes are enabled if keep aspect ratio is on
+ enabledAxes = (
+ EnabledAxes()
+ if plotWidget.isKeepDataAspectRatio()
+ else self.getZoomEnabledAxes()
+ )
+ if enabledAxes.isDisabled():
+ return
+
+ scale = 1.1 if angle > 0 else 1.0 / 1.1
+ applyZoomToPlot(plotWidget, scale, (x, y), enabledAxes)
diff --git a/src/silx/gui/plot/PlotToolButtons.py b/src/silx/gui/plot/PlotToolButtons.py
index a810ce1..e132877 100644
--- a/src/silx/gui/plot/PlotToolButtons.py
+++ b/src/silx/gui/plot/PlotToolButtons.py
@@ -29,6 +29,7 @@ The following QToolButton are available:
- :class:`.AspectToolButton`
- :class:`.YAxisOriginToolButton`
- :class:`.ProfileToolButton`
+- :class:`.RulerToolButton`
- :class:`.SymbolToolButton`
"""
@@ -40,11 +41,11 @@ __date__ = "27/06/2017"
import functools
import logging
-import weakref
from .. import icons
from .. import qt
from ... import config
+from .tools.PlotToolButton import PlotToolButton
from .items import SymbolMixIn, Scatter
@@ -52,58 +53,6 @@ from .items import SymbolMixIn, Scatter
_logger = logging.getLogger(__name__)
-class PlotToolButton(qt.QToolButton):
- """A QToolButton connected to a :class:`~silx.gui.plot.PlotWidget`.
- """
-
- def __init__(self, parent=None, plot=None):
- super(PlotToolButton, self).__init__(parent)
- self._plotRef = None
- if plot is not None:
- self.setPlot(plot)
-
- def plot(self):
- """
- Returns the plot connected to the widget.
- """
- return None if self._plotRef is None else self._plotRef()
-
- def setPlot(self, plot):
- """
- Set the plot connected to the widget
-
- :param plot: :class:`.PlotWidget` instance on which to operate.
- """
- previousPlot = self.plot()
-
- if previousPlot is plot:
- return
- if previousPlot is not None:
- self._disconnectPlot(previousPlot)
-
- if plot is None:
- self._plotRef = None
- else:
- self._plotRef = weakref.ref(plot)
- self._connectPlot(plot)
-
- def _connectPlot(self, plot):
- """
- Called when the plot is connected to the widget
-
- :param plot: :class:`.PlotWidget` instance
- """
- pass
-
- def _disconnectPlot(self, plot):
- """
- Called when the plot is disconnected from the widget
-
- :param plot: :class:`.PlotWidget` instance
- """
- pass
-
-
class AspectToolButton(PlotToolButton):
"""Tool button to switch keep aspect ratio of a plot"""
@@ -114,11 +63,11 @@ class AspectToolButton(PlotToolButton):
if self.STATE is None:
self.STATE = {}
# dont keep ratio
- self.STATE[False, "icon"] = icons.getQIcon('shape-ellipse-solid')
+ self.STATE[False, "icon"] = icons.getQIcon("shape-ellipse-solid")
self.STATE[False, "state"] = "Aspect ratio is not kept"
self.STATE[False, "action"] = "Do no keep data aspect ratio"
# keep ratio
- self.STATE[True, "icon"] = icons.getQIcon('shape-circle-solid')
+ self.STATE[True, "icon"] = icons.getQIcon("shape-circle-solid")
self.STATE[True, "state"] = "Aspect ratio is kept"
self.STATE[True, "action"] = "Keep data aspect ratio"
@@ -166,7 +115,10 @@ class AspectToolButton(PlotToolButton):
def _keepDataAspectRatioChanged(self, aspectRatio):
"""Handle Plot set keep aspect ratio signal"""
- icon, toolTip = self.STATE[aspectRatio, "icon"], self.STATE[aspectRatio, "state"]
+ icon, toolTip = (
+ self.STATE[aspectRatio, "icon"],
+ self.STATE[aspectRatio, "state"],
+ )
self.setIcon(icon)
self.setToolTip(toolTip)
@@ -181,11 +133,11 @@ class YAxisOriginToolButton(PlotToolButton):
if self.STATE is None:
self.STATE = {}
# is down
- self.STATE[False, "icon"] = icons.getQIcon('plot-ydown')
+ self.STATE[False, "icon"] = icons.getQIcon("plot-ydown")
self.STATE[False, "state"] = "Y-axis is oriented downward"
self.STATE[False, "action"] = "Orient Y-axis downward"
# keep ration
- self.STATE[True, "icon"] = icons.getQIcon('plot-yup')
+ self.STATE[True, "icon"] = icons.getQIcon("plot-yup")
self.STATE[True, "state"] = "Y-axis is oriented upward"
self.STATE[True, "action"] = "Orient Y-axis upward"
@@ -242,28 +194,29 @@ class YAxisOriginToolButton(PlotToolButton):
class ProfileOptionToolButton(PlotToolButton):
"""Button to define option on the profile"""
+
sigMethodChanged = qt.Signal(str)
-
+
def __init__(self, parent=None, plot=None):
PlotToolButton.__init__(self, parent=parent, plot=plot)
self.STATE = {}
# is down
- self.STATE['sum', "icon"] = icons.getQIcon('math-sigma')
- self.STATE['sum', "state"] = "Compute profile sum"
- self.STATE['sum', "action"] = "Compute profile sum"
+ self.STATE["sum", "icon"] = icons.getQIcon("math-sigma")
+ self.STATE["sum", "state"] = "Compute profile sum"
+ self.STATE["sum", "action"] = "Compute profile sum"
# keep ration
- self.STATE['mean', "icon"] = icons.getQIcon('math-mean')
- self.STATE['mean', "state"] = "Compute profile mean"
- self.STATE['mean', "action"] = "Compute profile mean"
+ self.STATE["mean", "icon"] = icons.getQIcon("math-mean")
+ self.STATE["mean", "state"] = "Compute profile mean"
+ self.STATE["mean", "action"] = "Compute profile mean"
- self.sumAction = self._createAction('sum')
+ self.sumAction = self._createAction("sum")
self.sumAction.triggered.connect(self.setSum)
self.sumAction.setIconVisibleInMenu(True)
self.sumAction.setCheckable(True)
self.sumAction.setChecked(True)
- self.meanAction = self._createAction('mean')
+ self.meanAction = self._createAction("mean")
self.meanAction.triggered.connect(self.setMean)
self.meanAction.setIconVisibleInMenu(True)
self.meanAction.setCheckable(True)
@@ -273,7 +226,7 @@ class ProfileOptionToolButton(PlotToolButton):
menu.addAction(self.meanAction)
self.setMenu(menu)
self.setPopupMode(qt.QToolButton.InstantPopup)
- self._method = 'mean'
+ self._method = "mean"
self._update()
def _createAction(self, method):
@@ -282,7 +235,7 @@ class ProfileOptionToolButton(PlotToolButton):
return qt.QAction(icon, text, self)
def setSum(self):
- self.setMethod('sum')
+ self.setMethod("sum")
def _update(self):
icon = self.STATE[self._method, "icon"]
@@ -293,7 +246,7 @@ class ProfileOptionToolButton(PlotToolButton):
self.meanAction.setChecked(self._method == "mean")
def setMean(self):
- self.setMethod('mean')
+ self.setMethod("mean")
def setMethod(self, method):
"""Set the method to use.
@@ -301,13 +254,12 @@ class ProfileOptionToolButton(PlotToolButton):
:param str method: Either 'sum' or 'mean'
"""
if method != self._method:
- if method in ('sum', 'mean'):
+ if method in ("sum", "mean"):
self._method = method
self.sigMethodChanged.emit(self._method)
self._update()
else:
- _logger.warning(
- "Unsupported method '%s'. Setting ignored.", method)
+ _logger.warning("Unsupported method '%s'. Setting ignored.", method)
def getMethod(self):
"""Returns the current method in use (See :meth:`setMethod`).
@@ -320,6 +272,7 @@ class ProfileOptionToolButton(PlotToolButton):
class ProfileToolButton(PlotToolButton):
"""Button used in Profile3DToolbar to switch between 2D profile
and 1D profile."""
+
STATE = None
"""Lazy loaded states used to feed ProfileToolButton"""
@@ -328,12 +281,16 @@ class ProfileToolButton(PlotToolButton):
def __init__(self, parent=None, plot=None):
if self.STATE is None:
self.STATE = {
- (1, "icon"): icons.getQIcon('profile1D'),
+ (1, "icon"): icons.getQIcon("profile1D"),
(1, "state"): "1D profile is computed on visible image",
(1, "action"): "1D profile on visible image",
- (2, "icon"): icons.getQIcon('profile2D'),
- (2, "state"): "2D profile is computed, one 1D profile for each image in the stack",
- (2, "action"): "2D profile on image stack"}
+ (2, "icon"): icons.getQIcon("profile2D"),
+ (
+ 2,
+ "state",
+ ): "2D profile is computed, one 1D profile for each image in the stack",
+ (2, "action"): "2D profile on image stack",
+ }
# Compute 1D profile
# Compute 2D profile
@@ -359,7 +316,7 @@ class ProfileToolButton(PlotToolButton):
menu.addAction(profile2DAction)
self.setMenu(menu)
self.setPopupMode(qt.QToolButton.InstantPopup)
- menu.setTitle('Select profile dimension')
+ menu.setTitle("Select profile dimension")
self.computeProfileIn1D()
def _createAction(self, profileDimension):
@@ -431,12 +388,12 @@ class _SymbolToolButtonBase(PlotToolButton):
:param QMenu menu:
"""
- for marker, name in zip(SymbolMixIn.getSupportedSymbols(),
- SymbolMixIn.getSupportedSymbolNames()):
+ for marker, name in zip(
+ SymbolMixIn.getSupportedSymbols(), SymbolMixIn.getSupportedSymbolNames()
+ ):
action = qt.QAction(name, menu)
action.setCheckable(False)
- action.triggered.connect(
- functools.partial(self._markerChanged, marker))
+ action.triggered.connect(functools.partial(self._markerChanged, marker))
menu.addAction(action)
def _sizeChanged(self, value):
@@ -476,8 +433,8 @@ class SymbolToolButton(_SymbolToolButtonBase):
def __init__(self, parent=None, plot=None):
super(SymbolToolButton, self).__init__(parent=parent, plot=plot)
- self.setToolTip('Set symbol size and marker')
- self.setIcon(icons.getQIcon('plot-symbols'))
+ self.setToolTip("Set symbol size and marker")
+ self.setIcon(icons.getQIcon("plot-symbols"))
menu = qt.QMenu(self)
self._addSizeSliderToMenu(menu)
@@ -496,12 +453,10 @@ class ScatterVisualizationToolButton(_SymbolToolButtonBase):
"""
def __init__(self, parent=None, plot=None):
- super(ScatterVisualizationToolButton, self).__init__(
- parent=parent, plot=plot)
+ super(ScatterVisualizationToolButton, self).__init__(parent=parent, plot=plot)
- self.setToolTip(
- 'Set scatter visualization mode, symbol marker and size')
- self.setIcon(icons.getQIcon('eye'))
+ self.setToolTip("Set scatter visualization mode, symbol marker and size")
+ self.setIcon(icons.getQIcon("eye"))
menu = qt.QMenu(self)
@@ -513,26 +468,33 @@ class ScatterVisualizationToolButton(_SymbolToolButtonBase):
action = qt.QAction(name, menu)
action.setCheckable(False)
action.triggered.connect(
- functools.partial(self._visualizationChanged, mode, None))
+ functools.partial(self._visualizationChanged, mode, None)
+ )
menu.addAction(action)
if Scatter.Visualization.BINNED_STATISTIC in Scatter.supportedVisualizations():
reductions = Scatter.supportedVisualizationParameterValues(
- Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION)
+ Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ )
if reductions:
- submenu = menu.addMenu('Binned Statistic')
+ submenu = menu.addMenu("Binned Statistic")
for reduction in reductions:
name = reduction.capitalize()
action = qt.QAction(name, menu)
action.setCheckable(False)
- action.triggered.connect(functools.partial(
- self._visualizationChanged,
- Scatter.Visualization.BINNED_STATISTIC,
- {Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION: reduction}))
+ action.triggered.connect(
+ functools.partial(
+ self._visualizationChanged,
+ Scatter.Visualization.BINNED_STATISTIC,
+ {
+ Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION: reduction
+ },
+ )
+ )
submenu.addAction(action)
submenu.addSeparator()
- binsmenu = submenu.addMenu('N Bins')
+ binsmenu = submenu.addMenu("N Bins")
slider = qt.QSlider(qt.Qt.Horizontal)
slider.setRange(10, 1000)
@@ -545,10 +507,10 @@ class ScatterVisualizationToolButton(_SymbolToolButtonBase):
menu.addSeparator()
- submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol")
+ submenu = menu.addMenu(icons.getQIcon("plot-symbols"), "Symbol")
self._addSymbolsToMenu(submenu)
- submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol Size")
+ submenu = menu.addMenu(icons.getQIcon("plot-symbols"), "Symbol Size")
self._addSizeSliderToMenu(submenu)
self.setMenu(menu)
@@ -587,5 +549,6 @@ class ScatterVisualizationToolButton(_SymbolToolButtonBase):
if isinstance(item, Scatter):
item.setVisualizationParameter(
Scatter.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- (value, value))
+ (value, value),
+ )
item.setVisualization(Scatter.Visualization.BINNED_STATISTIC)
diff --git a/src/silx/gui/plot/PlotWidget.py b/src/silx/gui/plot/PlotWidget.py
index f07ef30..a01ca48 100755
--- a/src/silx/gui/plot/PlotWidget.py
+++ b/src/silx/gui/plot/PlotWidget.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -25,6 +25,8 @@
The :class:`PlotWidget` implements the plot API initially provided in PyMca.
"""
+from __future__ import annotations
+
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "21/12/2018"
@@ -34,20 +36,20 @@ import logging
_logger = logging.getLogger(__name__)
-from collections import OrderedDict, namedtuple
+from collections import namedtuple
+from collections.abc import Sequence
from contextlib import contextmanager
+from typing import Optional, Union
import datetime as dt
import itertools
import numbers
-import typing
import warnings
import numpy
import silx
from silx.utils.weakref import WeakMethodProxy
-from silx.utils.property import classproperty
-from silx.utils.deprecation import deprecated, deprecated_warning
+
try:
# Import matplotlib now to init matplotlib our way
import silx.gui.utils.matplotlib # noqa
@@ -68,17 +70,13 @@ from .items.axis import TickMode # noqa
from .. import qt
from ._utils.panzoom import ViewConstraints
from ...gui.plot._utils.dtime_ticklayout import timestamp
+from ...utils.deprecation import deprecated_warning
-
-_COLORDICT = colors.COLORDICT
-_COLORLIST = silx.config.DEFAULT_PLOT_CURVE_COLORS
-
"""
Object returned when requesting the data range.
"""
-_PlotDataRange = namedtuple('PlotDataRange',
- ['x', 'y', 'yright'])
+_PlotDataRange = namedtuple("PlotDataRange", ["x", "y", "yright"])
class _PlotWidgetSelection(qt.QObject):
@@ -104,10 +102,14 @@ class _PlotWidgetSelection(qt.QObject):
# Init history
self.__history = [ # Store active items from most recent to oldest
- item for item in (parent.getActiveCurve(),
- parent.getActiveImage(),
- parent.getActiveScatter())
- if item is not None]
+ item
+ for item in (
+ parent.getActiveCurve(),
+ parent.getActiveImage(),
+ parent.getActiveScatter(),
+ )
+ if item is not None
+ ]
self.__current = self.__mostRecentActiveItem()
@@ -115,11 +117,11 @@ class _PlotWidgetSelection(qt.QObject):
parent.sigActiveCurveChanged.connect(self._activeCurveChanged)
parent.sigActiveScatterChanged.connect(self._activeScatterChanged)
- def __mostRecentActiveItem(self) -> typing.Optional[items.Item]:
+ def __mostRecentActiveItem(self) -> Optional[items.Item]:
"""Returns most recent active item."""
return self.__history[0] if len(self.__history) >= 1 else None
- def getSelectedItems(self) -> typing.Tuple[items.Item]:
+ def getSelectedItems(self) -> tuple[items.Item]:
"""Returns the list of currently selected items in the :class:`PlotWidget`.
The list is given from most recently current item to oldest one."""
@@ -136,11 +138,11 @@ class _PlotWidgetSelection(qt.QObject):
return active
- def getCurrentItem(self) -> typing.Optional[items.Item]:
- """Returns the current item in the :class:`PlotWidget` or None. """
+ def getCurrentItem(self) -> Optional[items.Item]:
+ """Returns the current item in the :class:`PlotWidget` or None."""
return self.__current
- def setCurrentItem(self, item: typing.Optional[items.Item]):
+ def setCurrentItem(self, item: Optional[items.Item]):
"""Set the current item in the :class:`PlotWidget`.
:param item:
@@ -166,20 +168,21 @@ class _PlotWidgetSelection(qt.QObject):
elif isinstance(item, items.Item):
plot = self.parent()
if plot is None or item.getPlot() is not plot:
- raise ValueError(
- "Item is not in the PlotWidget: %s" % str(item))
+ raise ValueError("Item is not in the PlotWidget: %s" % str(item))
self.__current = item
kind = plot._itemKind(item)
# Clean-up history to be safe
- self.__history = [item for item in self.__history
- if PlotWidget._itemKind(item) != kind]
+ self.__history = [
+ item for item in self.__history if PlotWidget._itemKind(item) != kind
+ ]
# Sync active item if needed
- if (kind in plot._ACTIVE_ITEM_KINDS and
- item is not plot._getActiveItem(kind)):
- plot._setActiveItem(kind, item.getName())
+ if kind in plot._ACTIVE_ITEM_KINDS and item is not plot._getActiveItem(
+ kind
+ ):
+ plot._setActiveItem(kind, item)
else:
raise ValueError("Not an Item: %s" % str(item))
@@ -188,10 +191,9 @@ class _PlotWidgetSelection(qt.QObject):
if previousSelected != self.getSelectedItems():
self.sigSelectedItemsChanged.emit()
- def __activeItemChanged(self,
- kind: str,
- previous: typing.Optional[str],
- legend: typing.Optional[str]):
+ def __activeItemChanged(
+ self, kind: str, previous: Optional[str], legend: Optional[str]
+ ):
"""Set current item from kind and legend"""
if previous == legend:
return # No-op for update of item
@@ -203,8 +205,9 @@ class _PlotWidgetSelection(qt.QObject):
previousSelected = self.getSelectedItems()
# Remove items of this kind from the history
- self.__history = [item for item in self.__history
- if PlotWidget._itemKind(item) != kind]
+ self.__history = [
+ item for item in self.__history if PlotWidget._itemKind(item) != kind
+ ]
# Retrieve current item
if legend is None: # Use most recent active item
@@ -230,15 +233,15 @@ class _PlotWidgetSelection(qt.QObject):
def _activeImageChanged(self, previous, current):
"""Handle active image change"""
- self.__activeItemChanged('image', previous, current)
+ self.__activeItemChanged("image", previous, current)
def _activeCurveChanged(self, previous, current):
"""Handle active curve change"""
- self.__activeItemChanged('curve', previous, current)
+ self.__activeItemChanged("curve", previous, current)
def _activeScatterChanged(self, previous, current):
"""Handle active scatter change"""
- self.__activeItemChanged('scatter', previous, current)
+ self.__activeItemChanged("scatter", previous, current)
class PlotWidget(qt.QMainWindow):
@@ -260,15 +263,10 @@ class PlotWidget(qt.QMainWindow):
:type backend: str or :class:`BackendBase.BackendBase`
"""
- # TODO: Can be removed for silx 0.10
- @classproperty
- @deprecated(replacement="silx.config.DEFAULT_PLOT_BACKEND", since_version="0.8", skip_backtrace_count=2)
- def DEFAULT_BACKEND(self):
- """Class attribute setting the default backend for all instances."""
- return silx.config.DEFAULT_PLOT_BACKEND
-
- colorList = _COLORLIST
- colorDict = _COLORDICT
+ # The following 2 class attributes are no longer used
+ # but there is no way to warn about deprecation
+ colorList = silx.config.DEFAULT_PLOT_CURVE_COLORS
+ colorDict = colors.COLORDICT
sigPlotSignal = qt.Signal(object)
"""Signal for all events of the plot.
@@ -368,6 +366,9 @@ class PlotWidget(qt.QMainWindow):
It provides the menu which will be displayed.
"""
+ sigBackendChanged = qt.Signal()
+ """Signal emitted when the backend have changed."""
+
def __init__(self, parent=None, backend=None):
self._autoreplot = False
self._dirty = False
@@ -382,7 +383,7 @@ class PlotWidget(qt.QMainWindow):
# behave as a widget
self.setWindowFlags(qt.Qt.Widget)
else:
- self.setWindowTitle('PlotWidget')
+ self.setWindowTitle("PlotWidget")
# Init the backend
self._backend = self.__getBackendClass(backend)(self, self)
@@ -390,25 +391,28 @@ class PlotWidget(qt.QMainWindow):
self.setCallback() # set _callback
# Items handling
- self._content = OrderedDict()
- self._contentToUpdate = [] # Used as an OrderedSet
+ self.__items = []
+ self.__itemsToUpdate = [] # Used as an OrderedSet
+ self.__activeItems = {"curve": None, "image": None, "scatter": None}
self._dataRange = None
# line types
- self._styleList = ['-', '--', '-.', ':']
+ self._defaultColors = None
+ self._styleList = ["-", "--", "-.", ":"]
self._colorIndex = 0
self._styleIndex = 0
self._activeCurveSelectionMode = "atmostone"
- self._activeCurveStyle = CurveStyle(color='#000000')
- self._activeLegend = {'curve': None, 'image': None,
- 'scatter': None}
+ self._activeCurveStyle = CurveStyle(
+ color=silx.config.DEFAULT_PLOT_ACTIVE_CURVE_COLOR,
+ linewidth=silx.config.DEFAULT_PLOT_ACTIVE_CURVE_LINEWIDTH,
+ )
# plot colors (updated later to sync backend)
- self._foregroundColor = 0., 0., 0., 1.
- self._gridColor = .7, .7, .7, 1.
- self._backgroundColor = 1., 1., 1., 1.
+ self._foregroundColor = 0.0, 0.0, 0.0, 1.0
+ self._gridColor = 0.7, 0.7, 0.7, 1.0
+ self._backgroundColor = 1.0, 1.0, 1.0, 1.0
self._dataBackgroundColor = None
# default properties
@@ -419,18 +423,18 @@ class PlotWidget(qt.QMainWindow):
self._yRightAxis = items.YRightAxis(self, self._yAxis)
self._grid = None
- self._graphTitle = ''
- self.__graphCursorShape = 'default'
+ self._graphTitle = ""
+ self.__graphCursorShape = "default"
# Set axes margins
self.__axesDisplayed = True
- self.__axesMargins = 0., 0., 0., 0.
- self.setAxesMargins(.15, .1, .1, .15)
+ self.__axesMargins = 0.0, 0.0, 0.0, 0.0
+ self.setAxesMargins(0.15, 0.1, 0.1, 0.15)
self.setGraphTitle()
self.setGraphXLabel()
self.setGraphYLabel()
- self.setGraphYLabel('', axis='right')
+ self.setGraphYLabel("", axis="right")
self.setDefaultColormap() # Init default colormap
@@ -440,12 +444,14 @@ class PlotWidget(qt.QMainWindow):
self._limitsHistory = LimitsHistory(self)
self._eventHandler = PlotInteraction.PlotInteraction(self)
- self._eventHandler.setInteractiveMode('zoom', color=(0., 0., 0., 1.))
+ self._eventHandler._setInteractiveMode("zoom", color=(0.0, 0.0, 0.0, 1.0))
+ self._eventHandler.sigChanged.connect(self.__interactionChanged)
+ self.__isInteractionSignalForwarded = True
self._previousDefaultMode = "zoom", True
self._pressedButtons = [] # Currently pressed mouse buttons
- self._defaultDataMargins = (0., 0., 0., 0.)
+ self._defaultDataMargins = (0.0, 0.0, 0.0, 0.0)
# Only activate autoreplot at the end
# This avoids errors when loaded in Qt designer
@@ -462,9 +468,9 @@ class PlotWidget(qt.QMainWindow):
self.setFocus(qt.Qt.OtherFocusReason)
# Set default limits
- self.setGraphXLimits(0., 100.)
- self.setGraphYLimits(0., 100., axis='right')
- self.setGraphYLimits(0., 100., axis='left')
+ self.setGraphXLimits(0.0, 100.0)
+ self.setGraphYLimits(0.0, 100.0, axis="right")
+ self.setGraphYLimits(0.0, 100.0, axis="left")
# Sync backend colors with default ones
self._foregroundColorsUpdated()
@@ -492,30 +498,32 @@ class PlotWidget(qt.QMainWindow):
elif isinstance(backend, str):
backend = backend.lower()
- if backend in ('matplotlib', 'mpl'):
+ if backend in ("matplotlib", "mpl"):
try:
- from .backends.BackendMatplotlib import \
- BackendMatplotlibQt as backendClass
+ from .backends.BackendMatplotlib import (
+ BackendMatplotlibQt as backendClass,
+ )
except ImportError:
_logger.debug("Backtrace", exc_info=True)
raise RuntimeError("matplotlib backend is not available")
- elif backend in ('gl', 'opengl'):
+ elif backend in ("gl", "opengl"):
from ..utils.glutils import isOpenGLAvailable
+
checkOpenGL = isOpenGLAvailable(version=(2, 1), runtimeCheck=False)
if not checkOpenGL:
_logger.debug("OpenGL check failed")
raise RuntimeError(
- "OpenGL backend is not available: %s" % checkOpenGL.error)
+ "OpenGL backend is not available: %s" % checkOpenGL.error
+ )
try:
- from .backends.BackendOpenGL import \
- BackendOpenGL as backendClass
+ from .backends.BackendOpenGL import BackendOpenGL as backendClass
except ImportError:
_logger.debug("Backtrace", exc_info=True)
raise RuntimeError("OpenGL backend is not available")
- elif backend == 'none':
+ elif backend == "none":
from .backends.BackendBase import BackendBase as backendClass
else:
@@ -540,20 +548,6 @@ class PlotWidget(qt.QMainWindow):
self.__selection = _PlotWidgetSelection(parent=self)
return self.__selection
- # TODO: Can be removed for silx 0.10
- @staticmethod
- @deprecated(replacement="silx.config.DEFAULT_PLOT_BACKEND", since_version="0.8", skip_backtrace_count=2)
- def setDefaultBackend(backend):
- """Set system wide default plot backend.
-
- .. versionadded:: 0.6
-
- :param backend: The backend to use, in:
- 'matplotlib' (default), 'mpl', 'opengl', 'gl', 'none'
- or a :class:`BackendBase.BackendBase` class
- """
- silx.config.DEFAULT_PLOT_BACKEND = backend
-
def setBackend(self, backend):
"""Set the backend to use for rendering.
@@ -576,8 +570,8 @@ class PlotWidget(qt.QMainWindow):
# First save state that is stored in the backend
xaxis = self.getXAxis()
xmin, xmax = xaxis.getLimits()
- ymin, ymax = self.getYAxis(axis='left').getLimits()
- y2min, y2max = self.getYAxis(axis='right').getLimits()
+ ymin, ymax = self.getYAxis(axis="left").getLimits()
+ y2min, y2max = self.getYAxis(axis="right").getLimits()
isKeepDataAspectRatio = self.isKeepDataAspectRatio()
xTimeZone = xaxis.getTimeZone()
isXAxisTimeSeries = xaxis.getTickMode() == TickMode.TIME_SERIES
@@ -606,7 +600,7 @@ class PlotWidget(qt.QMainWindow):
self._backend.setGraphCursorShape(self.getGraphCursorShape())
crosshairConfig = self.getGraphCursor()
if crosshairConfig is None:
- self._backend.setGraphCursor(False, 'black', 1, '-')
+ self._backend.setGraphCursor(False, "black", 1, "-")
else:
self._backend.setGraphCursor(True, *crosshairConfig)
@@ -615,21 +609,21 @@ class PlotWidget(qt.QMainWindow):
if self.isAxesDisplayed():
self._backend.setAxesMargins(*self.getAxesMargins())
else:
- self._backend.setAxesMargins(0., 0., 0., 0.)
+ self._backend.setAxesMargins(0.0, 0.0, 0.0, 0.0)
# Set axes
xaxis = self.getXAxis()
self._backend.setGraphXLabel(xaxis.getLabel())
self._backend.setXAxisTimeZone(xTimeZone)
self._backend.setXAxisTimeSeries(isXAxisTimeSeries)
- self._backend.setXAxisLogarithmic(
- xaxis.getScale() == items.Axis.LOGARITHMIC)
+ self._backend.setXAxisLogarithmic(xaxis.getScale() == items.Axis.LOGARITHMIC)
- for axis in ('left', 'right'):
+ for axis in ("left", "right"):
self._backend.setGraphYLabel(self.getYAxis(axis).getLabel(), axis)
self._backend.setYAxisInverted(isYAxisInverted)
self._backend.setYAxisLogarithmic(
- self.getYAxis().getScale() == items.Axis.LOGARITHMIC)
+ self.getYAxis().getScale() == items.Axis.LOGARITHMIC
+ )
# Finally restore aspect ratio and limits
self._backend.setKeepDataAspectRatio(isKeepDataAspectRatio)
@@ -639,6 +633,8 @@ class PlotWidget(qt.QMainWindow):
for item in self.getItems():
item._updated()
+ self.sigBackendChanged.emit()
+
def getBackend(self):
"""Returns the backend currently used by :class:`PlotWidget`.
@@ -665,12 +661,16 @@ class PlotWidget(qt.QMainWindow):
"""Override QWidget.contextMenuEvent to implement the context menu"""
menu = qt.QMenu(self)
from .actions.control import ZoomBackAction # Avoid cyclic import
+
zoomBackAction = ZoomBackAction(plot=self, parent=menu)
menu.addAction(zoomBackAction)
mode = self.getInteractiveMode()
if "shape" in mode and mode["shape"] == "polygon":
- from .actions.control import ClosePolygonInteractionAction # Avoid cyclic import
+ from .actions.control import (
+ ClosePolygonInteractionAction,
+ ) # Avoid cyclic import
+
action = ClosePolygonInteractionAction(plot=self, parent=menu)
menu.addAction(action)
@@ -691,7 +691,7 @@ class PlotWidget(qt.QMainWindow):
wasDirty = self._dirty
if not self._dirty and overlayOnly:
- self._dirty = 'overlay'
+ self._dirty = "overlay"
else:
self._dirty = True
@@ -704,8 +704,7 @@ class PlotWidget(qt.QMainWindow):
gridColor = self._foregroundColor
else:
gridColor = self._gridColor
- self._backend.setForegroundColors(
- self._foregroundColor, gridColor)
+ self._backend.setForegroundColors(self._foregroundColor, gridColor)
self._setDirtyPlot()
def getForegroundColor(self):
@@ -759,8 +758,7 @@ class PlotWidget(qt.QMainWindow):
dataBGColor = self._backgroundColor
else:
dataBGColor = self._dataBackgroundColor
- self._backend.setBackgroundColors(
- self._backgroundColor, dataBGColor)
+ self._backend.setBackgroundColors(self._backgroundColor, dataBGColor)
self._setDirtyPlot()
def getBackgroundColor(self):
@@ -829,7 +827,14 @@ class PlotWidget(qt.QMainWindow):
def hideEvent(self, event):
super(PlotWidget, self).hideEvent(event)
- self.sigVisibilityChanged.emit(False)
+ if qt.BINDING == "PySide6":
+ # Workaround RuntimeError: The SignalInstance object was already deleted
+ try:
+ self.sigVisibilityChanged.emit(False)
+ except RuntimeError as e:
+ _logger.error(f"Exception occured: {e}")
+ else:
+ self.sigVisibilityChanged.emit(False)
def _invalidateDataRange(self):
"""
@@ -842,42 +847,43 @@ class PlotWidget(qt.QMainWindow):
"""
Recomputes the range of the data displayed on this PlotWidget.
"""
- xMin = yMinLeft = yMinRight = float('nan')
- xMax = yMaxLeft = yMaxRight = float('nan')
+ xMin = yMinLeft = yMinRight = float("nan")
+ xMax = yMaxLeft = yMaxRight = float("nan")
for item in self.getItems():
if item.isVisible():
bounds = item.getBounds()
if bounds is not None:
with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
+ warnings.simplefilter("ignore", category=RuntimeWarning)
# Ignore All-NaN slice encountered
xMin = numpy.nanmin([xMin, bounds[0]])
xMax = numpy.nanmax([xMax, bounds[1]])
# Take care of right axis
- if (isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right'):
+ if (
+ isinstance(item, items.YAxisMixIn)
+ and item.getYAxis() == "right"
+ ):
with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
+ warnings.simplefilter("ignore", category=RuntimeWarning)
# Ignore All-NaN slice encountered
yMinRight = numpy.nanmin([yMinRight, bounds[2]])
yMaxRight = numpy.nanmax([yMaxRight, bounds[3]])
else:
with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
+ warnings.simplefilter("ignore", category=RuntimeWarning)
# Ignore All-NaN slice encountered
yMinLeft = numpy.nanmin([yMinLeft, bounds[2]])
yMaxLeft = numpy.nanmax([yMaxLeft, bounds[3]])
def lGetRange(x, y):
return None if numpy.isnan(x) and numpy.isnan(y) else (x, y)
+
xRange = lGetRange(xMin, xMax)
yLeftRange = lGetRange(yMinLeft, yMaxLeft)
yRightRange = lGetRange(yMinRight, yMaxRight)
- self._dataRange = _PlotDataRange(x=xRange,
- y=yLeftRange,
- yright=yRightRange)
+ self._dataRange = _PlotDataRange(x=xRange, y=yLeftRange, yright=yRightRange)
def getDataRange(self):
"""
@@ -895,19 +901,19 @@ class PlotWidget(qt.QMainWindow):
# Content management
_KIND_TO_CLASSES = {
- 'curve': (items.Curve,),
- 'image': (items.ImageBase,),
- 'scatter': (items.Scatter,),
- 'marker': (items.MarkerBase,),
- 'item': (
+ "curve": (items.Curve,),
+ "image": (items.ImageBase,),
+ "scatter": (items.Scatter,),
+ "marker": (items.MarkerBase,),
+ "item": (
items.Line,
items.Shape,
items.BoundingRect,
items.XAxisExtent,
items.YAxisExtent,
),
- 'histogram': (items.Histogram,),
- }
+ "histogram": (items.Histogram,),
+ }
"""Mapping kind to item classes of this kind"""
@classmethod
@@ -920,11 +926,15 @@ class PlotWidget(qt.QMainWindow):
for kind, itemClasses in cls._KIND_TO_CLASSES.items():
if isinstance(item, itemClasses):
return kind
- raise ValueError('Unsupported item type %s' % type(item))
+ return "other"
def _notifyContentChanged(self, item):
- self.notify('contentChanged', action='add',
- kind=self._itemKind(item), legend=item.getName())
+ self.notify(
+ "contentChanged",
+ action="add",
+ kind=self._itemKind(item),
+ legend=item.getName(),
+ )
def _itemRequiresUpdate(self, item):
"""Called by items in the plot for asynchronous update
@@ -933,34 +943,25 @@ class PlotWidget(qt.QMainWindow):
"""
assert item.getPlot() == self
# Put item at the end of the list
- if item in self._contentToUpdate:
- self._contentToUpdate.remove(item)
- self._contentToUpdate.append(item)
+ if item in self.__itemsToUpdate:
+ self.__itemsToUpdate.remove(item)
+ self.__itemsToUpdate.append(item)
self._setDirtyPlot(overlayOnly=item.isOverlay())
- def addItem(self, item=None, *args, **kwargs):
+ def addItem(self, item):
"""Add an item to the plot content.
:param ~silx.gui.plot.items.Item item: The item to add.
:raises ValueError: If item is already in the plot.
"""
if not isinstance(item, items.Item):
- deprecated_warning(
- 'Function',
- 'addItem',
- replacement='addShape',
- since_version='0.13')
- if item is None and not args: # Only kwargs
- return self.addShape(**kwargs)
- else:
- return self.addShape(item, *args, **kwargs)
+ raise ValueError(f"argument must be a subclass of Item")
- assert not args and not kwargs
if item in self.getItems():
- raise ValueError('Item already in the plot')
+ raise ValueError("Item already in the plot")
# Add item to plot
- self._content[(item.getName(), self._itemKind(item))] = item
+ self.__items.append(item)
item._setPlot(self)
self._itemRequiresUpdate(item)
if isinstance(item, items.DATA_ITEMS):
@@ -975,19 +976,11 @@ class PlotWidget(qt.QMainWindow):
:param ~silx.gui.plot.items.Item item: Item to remove from the plot.
:raises ValueError: If item is not in the plot.
"""
- if not isinstance(item, items.Item): # Previous method usage
- deprecated_warning(
- 'Function',
- 'removeItem',
- replacement='remove(legend, kind="item")',
- since_version='0.13')
- if item is None:
- return
- self.remove(item, kind='item')
- return
+ if not isinstance(item, items.Item):
+ raise ValueError("argument must be an Item")
if item not in self.getItems():
- raise ValueError('Item not in the plot')
+ raise ValueError("Item not in the plot")
self.sigItemAboutToBeRemoved.emit(item)
@@ -999,9 +992,9 @@ class PlotWidget(qt.QMainWindow):
self._setActiveItem(kind, None)
# Remove item from plot
- self._content.pop((item.getName(), kind))
- if item in self._contentToUpdate:
- self._contentToUpdate.remove(item)
+ self.__items.remove(item)
+ if item in self.__itemsToUpdate:
+ self.__itemsToUpdate.remove(item)
if item.isVisible():
self._setDirtyPlot(overlayOnly=item.isOverlay())
if item.getBounds() is not None:
@@ -1009,14 +1002,12 @@ class PlotWidget(qt.QMainWindow):
item._removeBackendRenderer(self._backend)
item._setPlot(None)
- if (kind == 'curve' and not self.getAllCurves(just_legend=True,
- withhidden=True)):
+ if kind == "curve" and not self.getAllCurves(just_legend=True, withhidden=True):
self._resetColorAndStyle()
self.sigItemRemoved.emit(item)
- self.notify('contentChanged', action='remove',
- kind=kind, legend=item.getName())
+ self.notify("contentChanged", action="remove", kind=kind, legend=item.getName())
def discardItem(self, item) -> bool:
"""Remove the item from the plot.
@@ -1033,20 +1024,12 @@ class PlotWidget(qt.QMainWindow):
else:
return True
- @deprecated(replacement='addItem', since_version='0.13')
- def _add(self, item):
- return self.addItem(item)
-
- @deprecated(replacement='removeItem', since_version='0.13')
- def _remove(self, item):
- return self.removeItem(item)
-
def getItems(self):
"""Returns the list of items in the plot
:rtype: List[silx.gui.plot.items.Item]
"""
- return tuple(self._content.values())
+ return tuple(self.__items)
@contextmanager
def _muteActiveItemChangedSignal(self):
@@ -1064,15 +1047,30 @@ class PlotWidget(qt.QMainWindow):
# Store used value.
# This value is used when curve is updated either internally or by user.
- def addCurve(self, x, y, legend=None, info=None,
- replace=False,
- color=None, symbol=None,
- linewidth=None, linestyle=None,
- xlabel=None, ylabel=None, yaxis=None,
- xerror=None, yerror=None, z=None, selectable=None,
- fill=None, resetzoom=True,
- histogram=None, copy=True,
- baseline=None):
+ def addCurve(
+ self,
+ x,
+ y,
+ legend=None,
+ info=None,
+ replace=False,
+ color=None,
+ symbol=None,
+ linewidth=None,
+ linestyle=None,
+ xlabel=None,
+ ylabel=None,
+ yaxis=None,
+ xerror=None,
+ yerror=None,
+ z=None,
+ selectable=None,
+ fill=None,
+ resetzoom=True,
+ histogram=None,
+ copy=True,
+ baseline=None,
+ ):
"""Add a 1D curve given by x an y to the graph.
Curves are uniquely identified by their legend.
@@ -1155,18 +1153,19 @@ class PlotWidget(qt.QMainWindow):
False to use provided arrays.
:param baseline: curve baseline
:type: Union[None,float,numpy.ndarray]
- :returns: The key string identify this curve
+ :returns: The curve item
"""
# This is an histogram, use addHistogram
if histogram is not None:
- histoLegend = self.addHistogram(histogram=y,
- edges=x,
- legend=legend,
- color=color,
- fill=fill,
- align=histogram,
- copy=copy)
- histo = self.getHistogram(histoLegend)
+ histo = self.addHistogram(
+ histogram=y,
+ edges=x,
+ legend=legend,
+ color=color,
+ fill=fill,
+ align=histogram,
+ copy=copy,
+ )
histo.setInfo(info)
if linewidth is not None:
@@ -1174,25 +1173,21 @@ class PlotWidget(qt.QMainWindow):
if linestyle is not None:
histo.setLineStyle(linestyle)
if xlabel is not None:
- _logger.warning(
- 'addCurve: Histogram does not support xlabel argument')
+ _logger.warning("addCurve: Histogram does not support xlabel argument")
if ylabel is not None:
- _logger.warning(
- 'addCurve: Histogram does not support ylabel argument')
+ _logger.warning("addCurve: Histogram does not support ylabel argument")
if yaxis is not None:
histo.setYAxis(yaxis)
if z is not None:
histo.setZValue(z)
if selectable is not None:
_logger.warning(
- 'addCurve: Histogram does not support selectable argument')
-
- return
+ "addCurve: Histogram does not support selectable argument"
+ )
- legend = 'Unnamed curve 1.1' if legend is None else str(legend)
+ return histo
- # Check if curve was previously active
- wasActive = self.getActiveCurve(just_legend=True) == legend
+ legend = "Unnamed curve 1.1" if legend is None else str(legend)
if replace:
self._resetColorAndStyle()
@@ -1217,7 +1212,11 @@ class PlotWidget(qt.QMainWindow):
# Override previous/default values with provided ones
curve.setInfo(info)
if color is not None:
- curve.setColor(color)
+ curve.setColor(
+ colors.rgba(color, colors=self.getDefaultColors())
+ if isinstance(color, str)
+ else color
+ )
if symbol is not None:
curve.setSymbol(symbol)
if linewidth is not None:
@@ -1264,14 +1263,13 @@ class PlotWidget(qt.QMainWindow):
else:
self._notifyContentChanged(curve)
- if wasActive:
- self.setActiveCurve(curve.getName())
- elif self.getActiveCurveSelectionMode() == "legacy":
- if self.getActiveCurve(just_legend=True) is None:
- if len(self.getAllCurves(just_legend=True,
- withhidden=False)) == 1:
- if curve.isVisible():
- self.setActiveCurve(curve.getName())
+ if curve is self.getActiveCurve() or (
+ self.getActiveCurveSelectionMode() == "legacy"
+ and self.getActiveCurve() is None
+ and len(self.getAllCurves(just_legend=True, withhidden=False)) == 1
+ and curve.isVisible()
+ ):
+ self.setActiveCurve(curve)
if resetzoom:
# We ask for a zoom reset in order to handle the plot scaling
@@ -1279,19 +1277,21 @@ class PlotWidget(qt.QMainWindow):
# axes has to be set to off.
self.resetZoom()
- return legend
-
- def addHistogram(self,
- histogram,
- edges,
- legend=None,
- color=None,
- fill=None,
- align='center',
- resetzoom=True,
- copy=True,
- z=None,
- baseline=None):
+ return curve
+
+ def addHistogram(
+ self,
+ histogram,
+ edges,
+ legend=None,
+ color=None,
+ fill=None,
+ align="center",
+ resetzoom=True,
+ copy=True,
+ z=None,
+ baseline=None,
+ ):
"""Add an histogram to the graph.
This is NOT computing the histogram, this method takes as parameter
@@ -1325,9 +1325,9 @@ class PlotWidget(qt.QMainWindow):
:param int z: Layer on which to draw the histogram
:param baseline: histogram baseline
:type: Union[None,float,numpy.ndarray]
- :returns: The key string identify this histogram
+ :returns: The histogram item
"""
- legend = 'Unnamed histogram' if legend is None else str(legend)
+ legend = "Unnamed histogram" if legend is None else str(legend)
# Create/Update histogram object
histo = self.getHistogram(legend)
@@ -1341,15 +1341,20 @@ class PlotWidget(qt.QMainWindow):
# Override previous/default values with provided ones
if color is not None:
- histo.setColor(color)
+ histo.setColor(
+ colors.rgba(color, colors=self.getDefaultColors())
+ if isinstance(color, str)
+ else color
+ )
if fill is not None:
histo.setFill(fill)
if z is not None:
histo.setZValue(z=z)
# Set histogram data
- histo.setData(histogram=histogram, edges=edges, baseline=baseline,
- align=align, copy=copy)
+ histo.setData(
+ histogram=histogram, edges=edges, baseline=baseline, align=align, copy=copy
+ )
if mustBeAdded:
self.addItem(histo)
@@ -1362,16 +1367,26 @@ class PlotWidget(qt.QMainWindow):
# axes has to be set to off.
self.resetZoom()
- return legend
-
- def addImage(self, data, legend=None, info=None,
- replace=False,
- z=None,
- selectable=None, draggable=None,
- colormap=None, pixmap=None,
- xlabel=None, ylabel=None,
- origin=None, scale=None,
- resetzoom=True, copy=True):
+ return histo
+
+ def addImage(
+ self,
+ data,
+ legend=None,
+ info=None,
+ replace=False,
+ z=None,
+ selectable=None,
+ draggable=None,
+ colormap=None,
+ pixmap=None,
+ xlabel=None,
+ ylabel=None,
+ origin=None,
+ scale=None,
+ resetzoom=True,
+ copy=True,
+ ):
"""Add a 2D dataset or an image to the plot.
It displays either an array of data using a colormap or a RGB(A) image.
@@ -1421,13 +1436,10 @@ class PlotWidget(qt.QMainWindow):
:param bool resetzoom: True (the default) to reset the zoom.
:param bool copy: True make a copy of the data (default),
False to use provided arrays.
- :returns: The key string identify this image
+ :returns: The image item
"""
legend = "Unnamed Image 1.1" if legend is None else str(legend)
- # Check if image was previously active
- wasActive = self.getActiveImage(just_legend=True) == legend
-
data = numpy.array(data, copy=False)
assert data.ndim in (2, 3)
@@ -1480,7 +1492,8 @@ class PlotWidget(qt.QMainWindow):
else: # RGB(A) image
if pixmap is not None:
_logger.warning(
- 'addImage: pixmap argument ignored when data is RGB(A)')
+ "addImage: pixmap argument ignored when data is RGB(A)"
+ )
image.setData(data, copy=copy)
if replace:
@@ -1493,8 +1506,8 @@ class PlotWidget(qt.QMainWindow):
else:
self._notifyContentChanged(image)
- if len(self.getAllImages()) == 1 or wasActive:
- self.setActiveImage(legend)
+ if len(self.getAllImages()) == 1 or image is self.getActiveImage():
+ self.setActiveImage(image)
if resetzoom:
# We ask for a zoom reset in order to handle the plot scaling
@@ -1502,11 +1515,22 @@ class PlotWidget(qt.QMainWindow):
# axes has to be set to off.
self.resetZoom()
- return legend
-
- def addScatter(self, x, y, value, legend=None, colormap=None,
- info=None, symbol=None, xerror=None, yerror=None,
- z=None, copy=True):
+ return image
+
+ def addScatter(
+ self,
+ x,
+ y,
+ value,
+ legend=None,
+ colormap=None,
+ info=None,
+ symbol=None,
+ xerror=None,
+ yerror=None,
+ z=None,
+ copy=True,
+ ):
"""Add a (x, y, value) scatter to the graph.
Scatters are uniquely identified by their legend.
@@ -1549,16 +1573,12 @@ class PlotWidget(qt.QMainWindow):
:param bool copy: True make a copy of the data (default),
False to use provided arrays.
- :returns: The key string identify this scatter
+ :returns: The scatter item
"""
- legend = 'Unnamed scatter 1.1' if legend is None else str(legend)
-
- # Check if scatter was previously active
- wasActive = self._getActiveItem(kind='scatter',
- just_legend=True) == legend
+ legend = "Unnamed scatter 1.1" if legend is None else str(legend)
# Create/Update curve object
- scatter = self._getItem(kind='scatter', legend=legend)
+ scatter = self._getItem(kind="scatter", legend=legend)
mustBeAdded = scatter is None
if scatter is None:
# No previous scatter, create a default one and add it to the plot
@@ -1600,18 +1620,33 @@ class PlotWidget(qt.QMainWindow):
else:
self._notifyContentChanged(scatter)
- scatters = [item for item in self.getItems()
- if isinstance(item, items.Scatter) and item.isVisible()]
- if len(scatters) == 1 or wasActive:
- self._setActiveItem('scatter', scatter.getName())
-
- return legend
-
- def addShape(self, xdata, ydata, legend=None, info=None,
- replace=False,
- shape="polygon", color='black', fill=True,
- overlay=False, z=None, linestyle="-", linewidth=1.0,
- linebgcolor=None):
+ scatters = [
+ item
+ for item in self.getItems()
+ if isinstance(item, items.Scatter) and item.isVisible()
+ ]
+ if len(scatters) == 1 or scatter is self.getActiveScatter():
+ self.setActiveScatter(scatter)
+
+ return scatter
+
+ def addShape(
+ self,
+ xdata,
+ ydata,
+ legend=None,
+ info=None,
+ replace=False,
+ shape="polygon",
+ color="black",
+ fill=True,
+ overlay=False,
+ z=None,
+ linestyle="-",
+ linewidth=1.0,
+ linebgcolor="deprecated",
+ gapcolor=None,
+ ):
"""Add an item (i.e. a shape) to the plot.
Items are uniquely identified by their legend.
@@ -1624,7 +1659,8 @@ class PlotWidget(qt.QMainWindow):
:param numpy.ndarray ydata: The Y coords of the points of the shape
:param str legend: The legend to be associated to the item
:param info: User-defined information associated to the item
- :param bool replace: True (default) to delete already existing images
+ :param bool replace: True to delete already existing items
+ (the default is False)
:param str shape: Type of item to be drawn in
hline, polygon (the default), rectangle, vline,
polylines
@@ -1646,9 +1682,9 @@ class PlotWidget(qt.QMainWindow):
- ':' dotted line
:param float linewidth: Width of the line.
Only relevant for line markers where X or Y is None.
- :param str linebgcolor: Background color of the line, e.g., 'blue', 'b',
+ :param str gapcolor: Gap color of the line, e.g., 'blue', 'b',
'#FF0000'. It is used to draw dotted line using a second color.
- :returns: The key string identify this item
+ :returns: The shape item
"""
# expected to receive the same parameters as the signal
@@ -1657,9 +1693,9 @@ class PlotWidget(qt.QMainWindow):
z = int(z) if z is not None else 2
if replace:
- self.remove(kind='item')
+ self.remove(kind="item")
else:
- self.remove(legend, kind='item')
+ self.remove(legend, kind="item")
item = items.Shape(shape)
item.setName(legend)
@@ -1671,19 +1707,31 @@ class PlotWidget(qt.QMainWindow):
item.setPoints(numpy.array((xdata, ydata)).T)
item.setLineStyle(linestyle)
item.setLineWidth(linewidth)
- item.setLineBgColor(linebgcolor)
+ if linebgcolor != "deprecated":
+ deprecated_warning(
+ type_="Argument",
+ name="linebgcolor",
+ replacement="gapcolor",
+ since_version="2.0.0",
+ )
+ gapcolor = linebgcolor if gapcolor is None else gapcolor
+ item.setLineGapColor(gapcolor)
self.addItem(item)
- return legend
-
- def addXMarker(self, x, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- yaxis='left'):
+ return item
+
+ def addXMarker(
+ self,
+ x,
+ legend=None,
+ text=None,
+ color=None,
+ selectable=False,
+ draggable=False,
+ constraint=None,
+ yaxis="left",
+ ):
"""Add a vertical line marker to the plot.
Markers are uniquely identified by their legend.
@@ -1710,22 +1758,32 @@ class PlotWidget(qt.QMainWindow):
the current cursor position in the plot as input
and that returns the filtered coordinates.
:param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
- :return: The key string identify this marker
- """
- return self._addMarker(x=x, y=None, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=None, constraint=constraint,
- yaxis=yaxis)
-
- def addYMarker(self, y,
- legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- yaxis='left'):
+ :return: The marker item
+ """
+ return self._addMarker(
+ x=x,
+ y=None,
+ legend=legend,
+ text=text,
+ color=color,
+ selectable=selectable,
+ draggable=draggable,
+ symbol=None,
+ constraint=constraint,
+ yaxis=yaxis,
+ )
+
+ def addYMarker(
+ self,
+ y,
+ legend=None,
+ text=None,
+ color=None,
+ selectable=False,
+ draggable=False,
+ constraint=None,
+ yaxis="left",
+ ):
"""Add a horizontal line marker to the plot.
Markers are uniquely identified by their legend.
@@ -1752,22 +1810,34 @@ class PlotWidget(qt.QMainWindow):
the current cursor position in the plot as input
and that returns the filtered coordinates.
:param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
- :return: The key string identify this marker
- """
- return self._addMarker(x=None, y=y, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=None, constraint=constraint,
- yaxis=yaxis)
-
- def addMarker(self, x, y, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- symbol='+',
- constraint=None,
- yaxis='left'):
+ :return: The marker item
+ """
+ return self._addMarker(
+ x=None,
+ y=y,
+ legend=legend,
+ text=text,
+ color=color,
+ selectable=selectable,
+ draggable=draggable,
+ symbol=None,
+ constraint=constraint,
+ yaxis=yaxis,
+ )
+
+ def addMarker(
+ self,
+ x,
+ y,
+ legend=None,
+ text=None,
+ color=None,
+ selectable=False,
+ draggable=False,
+ symbol="+",
+ constraint=None,
+ yaxis="left",
+ ):
"""Add a point marker to the plot.
Markers are uniquely identified by their legend.
@@ -1806,7 +1876,7 @@ class PlotWidget(qt.QMainWindow):
the current cursor position in the plot as input
and that returns the filtered coordinates.
:param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
- :return: The key string identify this marker
+ :return: The marker item
"""
if x is None:
xmin, xmax = self._xAxis.getLimits()
@@ -1816,17 +1886,32 @@ class PlotWidget(qt.QMainWindow):
ymin, ymax = self._yAxis.getLimits()
y = 0.5 * (ymax + ymin)
- return self._addMarker(x=x, y=y, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=symbol, constraint=constraint,
- yaxis=yaxis)
-
- def _addMarker(self, x, y, legend,
- text, color,
- selectable, draggable,
- symbol, constraint,
- yaxis=None):
+ return self._addMarker(
+ x=x,
+ y=y,
+ legend=legend,
+ text=text,
+ color=color,
+ selectable=selectable,
+ draggable=draggable,
+ symbol=symbol,
+ constraint=constraint,
+ yaxis=yaxis,
+ )
+
+ def _addMarker(
+ self,
+ x,
+ y,
+ legend,
+ text,
+ color,
+ selectable,
+ draggable,
+ symbol,
+ constraint,
+ yaxis=None,
+ ):
"""Common method for adding point, vline and hline marker.
See :meth:`addMarker` for argument documentation.
@@ -1834,8 +1919,11 @@ class PlotWidget(qt.QMainWindow):
assert (x, y) != (None, None)
if legend is None: # Find an unused legend
- markerLegends = [item.getName() for item in self.getItems()
- if isinstance(item, items.MarkerBase)]
+ markerLegends = [
+ item.getName()
+ for item in self.getItems()
+ if isinstance(item, items.MarkerBase)
+ ]
for index in itertools.count():
legend = "Unnamed Marker %d" % index
if legend not in markerLegends:
@@ -1852,8 +1940,9 @@ class PlotWidget(qt.QMainWindow):
# Create/Update marker object
marker = self._getMarker(legend)
if marker is not None and not isinstance(marker, markerClass):
- _logger.warning('Adding marker with same legend'
- ' but different type replaces it')
+ _logger.warning(
+ "Adding marker with same legend" " but different type replaces it"
+ )
self.removeItem(marker)
marker = None
@@ -1886,7 +1975,7 @@ class PlotWidget(qt.QMainWindow):
else:
self._notifyContentChanged(marker)
- return legend
+ return marker
# Hide
@@ -1896,7 +1985,7 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend key identifying the curve
:return: True if the associated curve is hidden, False otherwise
"""
- curve = self._getItem('curve', legend)
+ curve = self._getItem("curve", legend)
return curve is not None and not curve.isVisible()
def hideCurve(self, legend, flag=True):
@@ -1907,9 +1996,9 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend associated to the curve to be hidden
:param bool flag: True (default) to hide the curve, False to show it
"""
- curve = self._getItem('curve', legend)
+ curve = self._getItem("curve", legend)
if curve is None:
- _logger.warning('Curve not in plot: %s', legend)
+ _logger.warning("Curve not in plot: %s", legend)
return
isVisible = not flag
@@ -1918,13 +2007,17 @@ class PlotWidget(qt.QMainWindow):
# Remove
- ITEM_KINDS = 'curve', 'image', 'scatter', 'item', 'marker', 'histogram'
+ ITEM_KINDS = "curve", "image", "scatter", "item", "marker", "histogram"
"""List of supported kind of items in the plot."""
- _ACTIVE_ITEM_KINDS = 'curve', 'scatter', 'image'
+ _ACTIVE_ITEM_KINDS = "curve", "scatter", "image"
"""List of item's kind which have a active item."""
- def remove(self, legend=None, kind=ITEM_KINDS):
+ def remove(
+ self,
+ legend: str | items.Item | None = None,
+ kind: str | Sequence[str] = ITEM_KINDS,
+ ):
"""Remove one or all element(s) of the given legend and kind.
Examples:
@@ -1938,14 +2031,17 @@ class PlotWidget(qt.QMainWindow):
- ``remove('myImage')`` removes elements (for instance curve, image,
item and marker) with legend 'myImage'.
- :param str legend: The legend associated to the element to remove,
- or None to remove
- :param kind: The kind of elements to remove from the plot.
+ :param legend:
+ The legend of the item to remove or the item itself.
+ If None all items of given kind are removed.
+ :param kind: The kind of items to remove from the plot.
See :attr:`ITEM_KINDS`.
By default, it removes all kind of elements.
- :type kind: str or tuple of str to specify multiple kinds.
"""
- if kind == 'all': # Replace all by tuple of all kinds
+ if isinstance(legend, items.Item):
+ return self.removeItem(legend)
+
+ if kind == "all": # Replace all by tuple of all kinds
kind = self.ITEM_KINDS
if kind in self.ITEM_KINDS: # Kind is a str, make it a tuple
@@ -1958,8 +2054,10 @@ class PlotWidget(qt.QMainWindow):
# Clear each given kind
for aKind in kind:
for item in self.getItems():
- if (isinstance(item, self._KIND_TO_CLASSES[aKind]) and
- item.getPlot() is self): # Make sure item is still in the plot
+ if (
+ isinstance(item, self._KIND_TO_CLASSES[aKind])
+ and item.getPlot() is self
+ ): # Make sure item is still in the plot
self.removeItem(item)
else: # This is removing a single element
@@ -1969,32 +2067,41 @@ class PlotWidget(qt.QMainWindow):
if item is not None:
self.removeItem(item)
- def removeCurve(self, legend):
+ def removeCurve(self, legend: str | items.Curve | None):
"""Remove the curve associated to legend from the graph.
- :param str legend: The legend associated to the curve to be deleted
+ :param legend:
+ The legend of the curve to be deleted or the curve item
"""
if legend is None:
return
- self.remove(legend, kind='curve')
+ if isinstance(legend, items.Item):
+ return self.removeItem(legend)
+ self.remove(legend, kind="curve")
- def removeImage(self, legend):
+ def removeImage(self, legend: str | items.ImageBase | None):
"""Remove the image associated to legend from the graph.
- :param str legend: The legend associated to the image to be deleted
+ :param legend:
+ The legend of the image to be deleted or the image item
"""
if legend is None:
return
- self.remove(legend, kind='image')
+ if isinstance(legend, items.Item):
+ return self.removeItem(legend)
+ self.remove(legend, kind="image")
- def removeMarker(self, legend):
+ def removeMarker(self, legend: str | items.Marker | None):
"""Remove the marker associated to legend from the graph.
- :param str legend: The legend associated to the marker to be deleted
+ :param legend:
+ The legend of the marker to be deleted or the marker item
"""
if legend is None:
return
- self.remove(legend, kind='marker')
+ if isinstance(legend, items.Item):
+ return self.removeItem(legend)
+ self.remove(legend, kind="marker")
# Clear
@@ -2006,19 +2113,19 @@ class PlotWidget(qt.QMainWindow):
def clearCurves(self):
"""Remove all the curves from the plot."""
- self.remove(kind='curve')
+ self.remove(kind="curve")
def clearImages(self):
"""Remove all the images from the plot."""
- self.remove(kind='image')
+ self.remove(kind="image")
def clearItems(self):
- """Remove all the items from the plot. """
- self.remove(kind='item')
+ """Remove all the items from the plot."""
+ self.remove(kind="item")
def clearMarkers(self):
"""Remove all the markers from the plot."""
- self.remove(kind='marker')
+ self.remove(kind="marker")
# Interaction
@@ -2032,8 +2139,7 @@ class PlotWidget(qt.QMainWindow):
"""
return self._cursorConfiguration
- def setGraphCursor(self, flag=False, color='black',
- linewidth=1, linestyle='-'):
+ def setGraphCursor(self, flag=False, color="black", linewidth=1, linestyle="-"):
"""Toggle the display of a crosshair cursor and set its attributes.
:param bool flag: Toggle the display of a crosshair cursor.
@@ -2057,11 +2163,11 @@ class PlotWidget(qt.QMainWindow):
else:
self._cursorConfiguration = None
- self._backend.setGraphCursor(flag=flag, color=color,
- linewidth=linewidth, linestyle=linestyle)
+ self._backend.setGraphCursor(
+ flag=flag, color=color, linewidth=linewidth, linestyle=linestyle
+ )
self._setDirtyPlot()
- self.notify('setGraphCursor',
- state=self._cursorConfiguration is not None)
+ self.notify("setGraphCursor", state=self._cursorConfiguration is not None)
def pan(self, direction, factor=0.1):
"""Pan the graph in the given direction by the given factor.
@@ -2072,20 +2178,21 @@ class PlotWidget(qt.QMainWindow):
:param float factor: Proportion of the range used to pan the graph.
Must be strictly positive.
"""
- assert direction in ('up', 'down', 'left', 'right')
- assert factor > 0.
+ assert direction in ("up", "down", "left", "right")
+ assert factor > 0.0
- if direction in ('left', 'right'):
- xFactor = factor if direction == 'right' else - factor
+ if direction in ("left", "right"):
+ xFactor = factor if direction == "right" else -factor
xMin, xMax = self._xAxis.getLimits()
- xMin, xMax = _utils.applyPan(xMin, xMax, xFactor,
- self._xAxis.getScale() == self._xAxis.LOGARITHMIC)
+ xMin, xMax = _utils.applyPan(
+ xMin, xMax, xFactor, self._xAxis.getScale() == self._xAxis.LOGARITHMIC
+ )
self._xAxis.setLimits(xMin, xMax)
else: # direction in ('up', 'down')
- sign = -1. if self._yAxis.isInverted() else 1.
- yFactor = sign * (factor if direction == 'up' else -factor)
+ sign = -1.0 if self._yAxis.isInverted() else 1.0
+ yFactor = sign * (factor if direction == "up" else -factor)
yMin, yMax = self._yAxis.getLimits()
yIsLog = self._yAxis.getScale() == self._yAxis.LOGARITHMIC
@@ -2104,7 +2211,7 @@ class PlotWidget(qt.QMainWindow):
:rtype: bool
"""
- return self.getActiveCurveSelectionMode() != 'none'
+ return self.getActiveCurveSelectionMode() != "none"
def setActiveCurveHandling(self, flag=True):
"""Enable/Disable active curve selection.
@@ -2112,7 +2219,7 @@ class PlotWidget(qt.QMainWindow):
:param bool flag: True to enable 'atmostone' active curve selection,
False to disable active curve selection.
"""
- self.setActiveCurveSelectionMode('atmostone' if flag else 'none')
+ self.setActiveCurveSelectionMode("atmostone" if flag else "none")
def getActiveCurveStyle(self):
"""Returns the current style applied to active curve
@@ -2121,12 +2228,9 @@ class PlotWidget(qt.QMainWindow):
"""
return self._activeCurveStyle
- def setActiveCurveStyle(self,
- color=None,
- linewidth=None,
- linestyle=None,
- symbol=None,
- symbolsize=None):
+ def setActiveCurveStyle(
+ self, color=None, linewidth=None, linestyle=None, symbol=None, symbolsize=None
+ ):
"""Set the style of active curve
:param color: Color
@@ -2135,36 +2239,17 @@ class PlotWidget(qt.QMainWindow):
:param Union[str,None] symbol: Symbol of the markers
:param Union[float,None] symbolsize: Size of the symbols
"""
- self._activeCurveStyle = CurveStyle(color=color,
- linewidth=linewidth,
- linestyle=linestyle,
- symbol=symbol,
- symbolsize=symbolsize)
+ self._activeCurveStyle = CurveStyle(
+ color=color,
+ linewidth=linewidth,
+ linestyle=linestyle,
+ symbol=symbol,
+ symbolsize=symbolsize,
+ )
curve = self.getActiveCurve()
if curve is not None:
curve.setHighlightedStyle(self.getActiveCurveStyle())
- @deprecated(replacement="getActiveCurveStyle", since_version="0.9")
- def getActiveCurveColor(self):
- """Get the color used to display the currently active curve.
-
- See :meth:`setActiveCurveColor`.
- """
- return self._activeCurveStyle.getColor()
-
- @deprecated(replacement="setActiveCurveStyle", since_version="0.9")
- def setActiveCurveColor(self, color="#000000"):
- """Set the color to use to display the currently active curve.
-
- :param str color: Color of the active curve,
- e.g., 'blue', 'b', '#FF0000' (Default: 'black')
- """
- if color is None:
- color = "black"
- if color in self.colorDict:
- color = self.colorDict[color]
- self.setActiveCurveStyle(color=color)
-
def getActiveCurve(self, just_legend=False):
"""Return the currently active curve.
@@ -2180,7 +2265,7 @@ class PlotWidget(qt.QMainWindow):
if not self.isActiveCurveHandling():
return None
- return self._getActiveItem(kind='curve', just_legend=just_legend)
+ return self._getActiveItem(kind="curve", just_legend=just_legend)
def setActiveCurve(self, legend):
"""Make the curve associated to legend the active curve.
@@ -2193,10 +2278,11 @@ class PlotWidget(qt.QMainWindow):
return
if legend is None and self.getActiveCurveSelectionMode() == "legacy":
_logger.info(
- 'setActiveCurve(None) ignored due to active curve selection mode')
+ "setActiveCurve(None) ignored due to active curve selection mode"
+ )
return
- return self._setActiveItem(kind='curve', legend=legend)
+ return self._setActiveItem(kind="curve", item=legend)
def setActiveCurveSelectionMode(self, mode):
"""Sets the current selection mode.
@@ -2204,17 +2290,16 @@ class PlotWidget(qt.QMainWindow):
:param str mode: The active curve selection mode to use.
It can be: 'legacy', 'atmostone' or 'none'.
"""
- assert mode in ('legacy', 'atmostone', 'none')
+ assert mode in ("legacy", "atmostone", "none")
if mode != self._activeCurveSelectionMode:
self._activeCurveSelectionMode = mode
- if mode == 'none': # reset active curve
- self._setActiveItem(kind='curve', legend=None)
+ if mode == "none": # reset active curve
+ self._setActiveItem(kind="curve", item=None)
- elif mode == 'legacy' and self.getActiveCurve() is None:
+ elif mode == "legacy" and self.getActiveCurve() is None:
# Select an active curve
- curves = self.getAllCurves(just_legend=False,
- withhidden=False)
+ curves = self.getAllCurves(just_legend=False, withhidden=False)
if len(curves) == 1:
if curves[0].isVisible():
self.setActiveCurve(curves[0].getName())
@@ -2240,7 +2325,7 @@ class PlotWidget(qt.QMainWindow):
:rtype: str, :class:`.items.ImageData`, :class:`.items.ImageRgba`
or None
"""
- return self._getActiveItem(kind='image', just_legend=just_legend)
+ return self._getActiveItem(kind="image", just_legend=just_legend)
def setActiveImage(self, legend):
"""Make the image associated to legend the active image.
@@ -2248,7 +2333,7 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend associated to the image
or None to have no active image.
"""
- return self._setActiveItem(kind='image', legend=legend)
+ return self._setActiveItem(kind="image", item=legend)
def getActiveScatter(self, just_legend=False):
"""Returns the currently active scatter.
@@ -2261,7 +2346,7 @@ class PlotWidget(qt.QMainWindow):
:return: Active scatter's legend or corresponding scatter object
:rtype: str, :class:`.items.Scatter` or None
"""
- return self._getActiveItem(kind='scatter', just_legend=just_legend)
+ return self._getActiveItem(kind="scatter", just_legend=just_legend)
def setActiveScatter(self, legend):
"""Make the scatter associated to legend the active scatter.
@@ -2269,78 +2354,79 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend associated to the scatter
or None to have no active scatter.
"""
- return self._setActiveItem(kind='scatter', legend=legend)
+ return self._setActiveItem(kind="scatter", item=legend)
- def _getActiveItem(self, kind, just_legend=False):
- """Return the currently active item of that kind if any
+ def _getActiveItem(
+ self,
+ kind: str | None,
+ just_legend: bool = False,
+ ) -> items.Curve | items.Scatter | items.ImageBase | None:
+ """Return the currently active item of given kind if any.
- :param str kind: Type of item: 'curve', 'scatter' or 'image'
- :param bool just_legend: True to get the legend,
- False (default) to get the item
- :return: legend or item or None if no active item
+ :param kind: Type of item: 'curve', 'scatter' or 'image'
+ :param just_legend:
+ True to get the item's legend, False (the default) to get the item
"""
assert kind in self._ACTIVE_ITEM_KINDS
+ item = self.__activeItems[kind]
+ if item is not None and just_legend:
+ return item.getName()
+ return item
- if self._activeLegend[kind] is None:
- return None
+ def _setActiveItem(
+ self,
+ kind: str,
+ item: items.Curve | items.ImageBase | items.Scatter | str | None,
+ ) -> str | None:
+ """Make the given item active.
+
+ Note: There is one active item per "kind" of item.
+ """
+ assert kind in self._ACTIVE_ITEM_KINDS
- item = self._getItem(kind, self._activeLegend[kind])
if item is None:
- return None
+ legend = None
+ elif isinstance(item, items.Item):
+ legend = item.getName()
+ else:
+ legend = str(item)
+ item = self._getItem(kind, legend)
+ if item is None:
+ _logger.warning("This %s does not exist: %s", kind, legend)
- return item.getName() if just_legend else item
+ oldActiveItem = self._getActiveItem(kind=kind)
- def _setActiveItem(self, kind, legend):
- """Make the curve associated to legend the active curve.
+ if oldActiveItem is None and item is None:
+ return None
- :param str kind: Type of item: 'curve' or 'image'
- :param legend: The legend associated to the curve
- or None to have no active curve.
- :type legend: str or None
- """
- assert kind in self._ACTIVE_ITEM_KINDS
+ if oldActiveItem is not None:
+ # Stop listening previous active item
+ oldActiveItem.sigItemChanged.disconnect(self._activeItemChanged)
+ # Curve specific: Reset highlight of previous active curve
+ if kind == "curve":
+ oldActiveItem.setHighlighted(False)
+
+ self.__activeItems[kind] = item
xLabel = None
yLabel = None
yRightLabel = None
- oldActiveItem = self._getActiveItem(kind=kind)
-
- if oldActiveItem is not None: # Stop listening previous active image
- oldActiveItem.sigItemChanged.disconnect(self._activeItemChanged)
+ if item is not None:
+ # Curve specific: handle highlight
+ if kind == "curve":
+ item.setHighlightedStyle(self.getActiveCurveStyle())
+ item.setHighlighted(True)
- # Curve specific: Reset highlight of previous active curve
- if kind == 'curve' and oldActiveItem is not None:
- oldActiveItem.setHighlighted(False)
+ if isinstance(item, items.LabelsMixIn):
+ xLabel = item.getXLabel()
+ if isinstance(item, items.YAxisMixIn) and item.getYAxis() == "right":
+ yRightLabel = item.getYLabel()
+ else:
+ yLabel = item.getYLabel()
- if legend is None:
- self._activeLegend[kind] = None
- else:
- legend = str(legend)
- item = self._getItem(kind, legend)
- if item is None:
- _logger.warning("This %s does not exist: %s", kind, legend)
- self._activeLegend[kind] = None
- else:
- self._activeLegend[kind] = legend
-
- # Curve specific: handle highlight
- if kind == 'curve':
- item.setHighlightedStyle(self.getActiveCurveStyle())
- item.setHighlighted(True)
-
- if isinstance(item, items.LabelsMixIn):
- if item.getXLabel() is not None:
- xLabel = item.getXLabel()
- if item.getYLabel() is not None:
- if (isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right'):
- yRightLabel = item.getYLabel()
- else:
- yLabel = item.getYLabel()
-
- # Start listening new active item
- item.sigItemChanged.connect(self._activeItemChanged)
+ # Start listening new active item
+ item.sigItemChanged.connect(self._activeItemChanged)
# Store current labels and update plot
self._xAxis._setCurrentLabel(xLabel)
@@ -2349,19 +2435,13 @@ class PlotWidget(qt.QMainWindow):
self._setDirtyPlot()
- activeLegend = self._activeLegend[kind]
- if oldActiveItem is not None or activeLegend is not None:
- if oldActiveItem is None:
- oldActiveLegend = None
- else:
- oldActiveLegend = oldActiveItem.getName()
- self.notify(
- 'active' + kind[0].upper() + kind[1:] + 'Changed',
- updated=oldActiveLegend != activeLegend,
- previous=oldActiveLegend,
- legend=activeLegend)
-
- return activeLegend
+ self.notify(
+ f"active{kind.capitalize()}Changed",
+ updated=oldActiveItem is not item,
+ previous=None if oldActiveItem is None else oldActiveItem.getName(),
+ legend=legend,
+ )
+ return legend
def _activeItemChanged(self, type_):
"""Listen for active item changed signal and broadcast signal
@@ -2373,10 +2453,11 @@ class PlotWidget(qt.QMainWindow):
if item is not None:
kind = self._itemKind(item)
self.notify(
- 'active' + kind[0].upper() + kind[1:] + 'Changed',
+ "active" + kind[0].upper() + kind[1:] + "Changed",
updated=False,
previous=item.getName(),
- legend=item.getName())
+ legend=item.getName(),
+ )
# Getters
@@ -2396,24 +2477,29 @@ class PlotWidget(qt.QMainWindow):
:return: list of curves' legend or :class:`.items.Curve`
:rtype: list of str or list of :class:`.items.Curve`
"""
- curves = [item for item in self.getItems() if
- isinstance(item, items.Curve) and
- (withhidden or item.isVisible())]
+ curves = [
+ item
+ for item in self.getItems()
+ if isinstance(item, items.Curve) and (withhidden or item.isVisible())
+ ]
return [curve.getName() for curve in curves] if just_legend else curves
- def getCurve(self, legend=None):
+ def getCurve(self, legend: str | items.Curve | None = None) -> items.Curve:
"""Get the object describing a specific curve.
It returns None in case no matching curve is found.
- :param str legend:
+ :param legend:
The legend identifying the curve.
If not provided or None (the default), the active curve is returned
or if there is no active curve, the latest updated curve that is
not hidden is returned if there are curves in the plot.
:return: None or :class:`.items.Curve` object
"""
- return self._getItem(kind='curve', legend=legend)
+ if isinstance(legend, items.Curve):
+ _logger.warning("getCurve call not needed: legend is already an item")
+ return legend
+ return self._getItem(kind="curve", legend=legend)
def getAllImages(self, just_legend=False):
"""Returns all images legend or objects.
@@ -2430,83 +2516,62 @@ class PlotWidget(qt.QMainWindow):
:return: list of images' legend or :class:`.items.ImageBase`
:rtype: list of str or list of :class:`.items.ImageBase`
"""
- images = [item for item in self.getItems()
- if isinstance(item, items.ImageBase)]
+ images = [item for item in self.getItems() if isinstance(item, items.ImageBase)]
return [image.getName() for image in images] if just_legend else images
- def getImage(self, legend=None):
+ def getImage(self, legend: str | items.ImageBase | None = None) -> items.ImageBase:
"""Get the object describing a specific image.
It returns None in case no matching image is found.
- :param str legend:
+ :param legend:
The legend identifying the image.
If not provided or None (the default), the active image is returned
or if there is no active image, the latest updated image
is returned if there are images in the plot.
:return: None or :class:`.items.ImageBase` object
"""
- return self._getItem(kind='image', legend=legend)
+ if isinstance(legend, items.ImageBase):
+ _logger.warning("getImage call not needed: legend is already an item")
+ return legend
+ return self._getItem(kind="image", legend=legend)
- def getScatter(self, legend=None):
+ def getScatter(self, legend: str | items.Scatter | None = None) -> items.Scatter:
"""Get the object describing a specific scatter.
It returns None in case no matching scatter is found.
- :param str legend:
+ :param legend:
The legend identifying the scatter.
If not provided or None (the default), the active scatter is
returned or if there is no active scatter, the latest updated
scatter is returned if there are scatters in the plot.
:return: None or :class:`.items.Scatter` object
"""
- return self._getItem(kind='scatter', legend=legend)
+ if isinstance(legend, items.Scatter):
+ _logger.warning("getScatter call not needed: legend is already an item")
+ return legend
+ return self._getItem(kind="scatter", legend=legend)
- def getHistogram(self, legend=None):
+ def getHistogram(
+ self, legend: str | items.Histogram | None = None
+ ) -> items.Histogram:
"""Get the object describing a specific histogram.
It returns None in case no matching histogram is found.
- :param str legend:
+ :param legend:
The legend identifying the histogram.
If not provided or None (the default), the latest updated scatter
is returned if there are histograms in the plot.
:return: None or :class:`.items.Histogram` object
"""
- return self._getItem(kind='histogram', legend=legend)
-
- @deprecated(replacement='getItems', since_version='0.13')
- def _getItems(self, kind=ITEM_KINDS, just_legend=False, withhidden=False):
- """Retrieve all items of a kind in the plot
-
- :param kind: The kind of elements to retrieve from the plot.
- See :attr:`ITEM_KINDS`.
- By default, it removes all kind of elements.
- :type kind: str or tuple of str to specify multiple kinds.
- :param str kind: Type of item: 'curve' or 'image'
- :param bool just_legend: True to get the legend of the curves,
- False (the default) to get the curves' data
- and info.
- :param bool withhidden: False (default) to skip hidden curves.
- :return: list of legends or item objects
- """
- if kind == 'all': # Replace all by tuple of all kinds
- kind = self.ITEM_KINDS
-
- if kind in self.ITEM_KINDS: # Kind is a str, make it a tuple
- kind = (kind,)
-
- for aKind in kind:
- assert aKind in self.ITEM_KINDS
-
- output = []
- for item in self.getItems():
- type_ = self._itemKind(item)
- if type_ in kind and (withhidden or item.isVisible()):
- output.append(item.getName() if just_legend else item)
- return output
+ if isinstance(legend, items.Histogram):
+ _logger.warning("getHistogram call not needed: legend is already an item")
+ return legend
+ return self._getItem(kind="histogram", legend=legend)
- def _getItem(self, kind, legend=None):
+ def _getItem(self, kind, legend=None) -> items.Item:
"""Get an item from the plot: either an image or a curve.
Returns None if no match found.
@@ -2517,20 +2582,30 @@ class PlotWidget(qt.QMainWindow):
None to get active or last item
:return: Object describing the item or None
"""
+ if isinstance(legend, items.Item):
+ _logger.warning("_getItem call not needed: legend is already an item")
+ return legend
+
assert kind in self.ITEM_KINDS
if legend is not None:
- return self._content.get((legend, kind), None)
- else:
- if kind in self._ACTIVE_ITEM_KINDS:
- item = self._getActiveItem(kind=kind)
- if item is not None: # Return active item if available
+ for item in self.getItems():
+ if item.getName() == legend and kind == self._itemKind(item):
return item
- # Return last visible item if any
- itemClasses = self._KIND_TO_CLASSES[kind]
- allItems = [item for item in self.getItems()
- if isinstance(item, itemClasses) and item.isVisible()]
- return allItems[-1] if allItems else None
+ return None # No item found
+
+ if kind in self._ACTIVE_ITEM_KINDS:
+ item = self._getActiveItem(kind=kind)
+ if item is not None: # Return active item if available
+ return item
+ # Return last visible item if any
+ itemClasses = self._KIND_TO_CLASSES[kind]
+ allItems = [
+ item
+ for item in self.getItems()
+ if isinstance(item, itemClasses) and item.isVisible()
+ ]
+ return allItems[-1] if allItems else None
# Limits
@@ -2545,7 +2620,8 @@ class PlotWidget(qt.QMainWindow):
for axis, limits in zip(axes, ranges):
axis.sigLimitsChanged.emit(*limits)
event = PlotEvents.prepareLimitsChangedSignal(
- id(self.getWidgetHandle()), xRange, yRange, y2Range)
+ id(self.getWidgetHandle()), xRange, yRange, y2Range
+ )
self.notify(**event)
def getLimitsHistory(self):
@@ -2567,18 +2643,18 @@ class PlotWidget(qt.QMainWindow):
"""
self._xAxis.setLimits(xmin, xmax)
- def getGraphYLimits(self, axis='left'):
+ def getGraphYLimits(self, axis="left"):
"""Get the graph Y limits.
:param str axis: The axis for which to get the limits:
Either 'left' or 'right'
:return: Minimum and maximum values of the X axis
"""
- assert axis in ('left', 'right')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ assert axis in ("left", "right")
+ yAxis = self._yAxis if axis == "left" else self._yRightAxis
return yAxis.getLimits()
- def setGraphYLimits(self, ymin, ymax, axis='left'):
+ def setGraphYLimits(self, ymin, ymax, axis="left"):
"""Set the graph Y limits.
:param float ymin: minimum bottom axis value
@@ -2586,40 +2662,80 @@ class PlotWidget(qt.QMainWindow):
:param str axis: The axis for which to get the limits:
Either 'left' or 'right'
"""
- assert axis in ('left', 'right')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ assert axis in ("left", "right")
+ yAxis = self._yAxis if axis == "left" else self._yRightAxis
return yAxis.setLimits(ymin, ymax)
- def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
+ def setLimits(
+ self,
+ xmin: float,
+ xmax: float,
+ ymin: float,
+ ymax: float,
+ y2min: Optional[float] = None,
+ y2max: Optional[float] = None,
+ margins: Union[bool, tuple[float, float, float, float]] = False,
+ ):
"""Set the limits of the X and Y axes at once.
If y2min or y2max is None, the right Y axis limits are not updated.
- :param float xmin: minimum bottom axis value
- :param float xmax: maximum bottom axis value
- :param float ymin: minimum left axis value
- :param float ymax: maximum left axis value
- :param float y2min: minimum right axis value or None (the default)
- :param float y2max: maximum right axis value or None (the default)
- """
- # Deal with incorrect values
- axis = self.getXAxis()
- xmin, xmax = axis._checkLimits(xmin, xmax)
- axis = self.getYAxis()
- ymin, ymax = axis._checkLimits(ymin, ymax)
-
- if y2min is None or y2max is None:
- # if one limit is None, both are ignored
- y2min, y2max = None, None
- else:
- axis = self.getYAxis(axis="right")
- y2min, y2max = axis._checkLimits(y2min, y2max)
+ :param xmin: minimum bottom axis value
+ :param xmax: maximum bottom axis value
+ :param ymin: minimum left axis value
+ :param ymax: maximum left axis value
+ :param y2min: minimum right axis value or None (the default)
+ :param y2max: maximum right axis value or None (the default)
+ :param margins:
+ Data margins to add to the limits or a boolean telling
+ whether or not to add margins from :meth:`getDataMargins`.
+ """
+ limits = [
+ *self.getXAxis()._checkLimits(xmin, xmax),
+ *self.getYAxis()._checkLimits(ymin, ymax),
+ ]
+
+ # Only consider y2 axis if both limits are not None
+ if None not in (y2min, y2max):
+ limits.extend(self.getYAxis(axis="right")._checkLimits(y2min, y2max))
+
+ if margins: # Add margins around limits inside the plot area
+ limits = list(
+ _utils.addMarginsToLimits(
+ self.getDataMargins() if margins is True else margins,
+ self.getXAxis()._isLogarithmic(),
+ self.getYAxis()._isLogarithmic(),
+ *limits,
+ )
+ )
+
+ if self.isKeepDataAspectRatio():
+ # Use limits with margins to keep ratio
+ xmin, xmax, ymin, ymax = limits[:4]
+
+ # Compute bbox wth figure aspect ratio
+ plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+ if plotWidth > 0 and plotHeight > 0:
+ plotRatio = plotHeight / plotWidth
+ dataRatio = (ymax - ymin) / (xmax - xmin)
+ if dataRatio < plotRatio:
+ # Increase y range
+ ycenter = 0.5 * (ymax + ymin)
+ yrange = (xmax - xmin) * plotRatio
+ limits[2] = ycenter - 0.5 * yrange
+ limits[3] = ycenter + 0.5 * yrange
+
+ elif dataRatio > plotRatio:
+ # Increase x range
+ xcenter = 0.5 * (xmax + xmin)
+ xrange_ = (ymax - ymin) / plotRatio
+ limits[0] = xcenter - 0.5 * xrange_
+ limits[1] = xcenter + 0.5 * xrange_
if self._viewConstrains:
- view = self._viewConstrains.normalize(xmin, xmax, ymin, ymax)
- xmin, xmax, ymin, ymax = view
+ limits[:4] = self._viewConstrains.normalize(*limits[:4])
- self._backend.setLimits(xmin, xmax, ymin, ymax, y2min, y2max)
+ self._backend.setLimits(*limits)
self._setDirtyPlot()
self._notifyLimitsChanged()
@@ -2661,16 +2777,16 @@ class PlotWidget(qt.QMainWindow):
"""
self._xAxis.setLabel(label)
- def getGraphYLabel(self, axis='left'):
+ def getGraphYLabel(self, axis="left"):
"""Return the current Y axis label as a str.
:param str axis: The Y axis for which to get the label (left or right)
"""
- assert axis in ('left', 'right')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ assert axis in ("left", "right")
+ yAxis = self._yAxis if axis == "left" else self._yRightAxis
return yAxis.getLabel()
- def setGraphYLabel(self, label="Y", axis='left'):
+ def setGraphYLabel(self, label="Y", axis="left"):
"""Set the plot Y axis label.
The provided label can be temporarily replaced by the Y label of the
@@ -2679,8 +2795,8 @@ class PlotWidget(qt.QMainWindow):
:param str label: The Y axis label (default: 'Y')
:param str axis: The Y axis for which to set the label (left or right)
"""
- assert axis in ('left', 'right')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ assert axis in ("left", "right")
+ yAxis = self._yAxis if axis == "left" else self._yRightAxis
return yAxis.setLabel(label)
# Axes
@@ -2703,7 +2819,7 @@ class PlotWidget(qt.QMainWindow):
('left' or 'right').
:rtype: :class:`.items.Axis`
"""
- assert(axis in ["left", "right"])
+ assert axis in ["left", "right"]
return self._yAxis if axis == "left" else self._yRightAxis
def setAxesDisplayed(self, displayed: bool):
@@ -2717,7 +2833,7 @@ class PlotWidget(qt.QMainWindow):
if displayed:
self._backend.setAxesMargins(*self.__axesMargins)
else:
- self._backend.setAxesMargins(0., 0., 0., 0.)
+ self._backend.setAxesMargins(0.0, 0.0, 0.0, 0.0)
self._setDirtyPlot()
self._sigAxesVisibilityChanged.emit(displayed)
@@ -2728,8 +2844,7 @@ class PlotWidget(qt.QMainWindow):
"""
return self.__axesDisplayed
- def setAxesMargins(
- self, left: float, top: float, right: float, bottom: float):
+ def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
"""Set ratios of margins surrounding data plot area.
All ratios must be within [0., 1.].
@@ -2742,9 +2857,9 @@ class PlotWidget(qt.QMainWindow):
:raises ValueError:
"""
for value in (left, top, right, bottom):
- if value < 0. or value > 1.:
+ if value < 0.0 or value > 1.0:
raise ValueError("Margin ratios must be within [0., 1.]")
- if left + right >= 1. or top + bottom >= 1.:
+ if left + right >= 1.0 or top + bottom >= 1.0:
raise ValueError("Sum of ratios of opposed sides >= 1")
margins = left, top, right, bottom
@@ -2835,7 +2950,7 @@ class PlotWidget(qt.QMainWindow):
self._backend.setKeepDataAspectRatio(flag=flag)
self._setDirtyPlot()
self._forceResetZoom()
- self.notify('setKeepDataAspectRatio', state=flag)
+ self.notify("setKeepDataAspectRatio", state=flag)
def getGraphGrid(self):
"""Return the current grid mode, either None, 'major' or 'both'.
@@ -2852,15 +2967,15 @@ class PlotWidget(qt.QMainWindow):
'both' for grid on both major and minor ticks.
:type which: str of bool
"""
- assert which in (None, True, False, 'both', 'major')
+ assert which in (None, True, False, "both", "major")
if not which:
which = None
elif which is True:
- which = 'major'
+ which = "major"
self._grid = which
self._backend.setGraphGrid(which)
self._setDirtyPlot()
- self.notify('setGraphGrid', which=str(which))
+ self.notify("setGraphGrid", which=str(which))
# Defaults
@@ -2876,7 +2991,7 @@ class PlotWidget(qt.QMainWindow):
:param bool flag: True to use 'o' as the default curve symbol,
False to use no symbol.
"""
- self._defaultPlotPoints = silx.config.DEFAULT_PLOT_SYMBOL if flag else ''
+ self._defaultPlotPoints = silx.config.DEFAULT_PLOT_SYMBOL if flag else ""
# Reset symbol of all curves
curves = self.getAllCurves(just_legend=False, withhidden=True)
@@ -2897,7 +3012,7 @@ class PlotWidget(qt.QMainWindow):
"""
self._plotLines = bool(flag)
- linestyle = '-' if self._plotLines else ' '
+ linestyle = "-" if self._plotLines else " "
# Reset linestyle of all curves
curves = self.getAllCurves(withhidden=True)
@@ -2927,16 +3042,18 @@ class PlotWidget(qt.QMainWindow):
autoscale gray colormap.
"""
if colormap is None:
- colormap = Colormap(name=silx.config.DEFAULT_COLORMAP_NAME,
- normalization='linear',
- vmin=None,
- vmax=None)
+ colormap = Colormap(
+ name=silx.config.DEFAULT_COLORMAP_NAME,
+ normalization="linear",
+ vmin=None,
+ vmax=None,
+ )
if isinstance(colormap, dict):
self._defaultColormap = Colormap._fromDict(colormap)
else:
assert isinstance(colormap, Colormap)
self._defaultColormap = colormap
- self.notify('defaultColormapChanged')
+ self.notify("defaultColormapChanged")
@staticmethod
def getSupportedColormaps():
@@ -2948,17 +3065,35 @@ class PlotWidget(qt.QMainWindow):
"""
return Colormap.getSupportedColormaps()
+ def setDefaultColors(self, colors: Optional[Tuple[str, ...]]):
+ """Set the list of colors to use as default for curves and histograms.
+
+ Set to None to use `silx.config.DEFAULT_PLOT_CURVE_COLORS`.
+ """
+ self._defaultColors = None if colors is None else tuple(colors)
+ self._resetColorAndStyle()
+
+ def getDefaultColors(self) -> Tuple[str, ...]:
+ """Returns the list of default colors for curves and histograms"""
+ if self._defaultColors is None:
+ return tuple(silx.config.DEFAULT_PLOT_CURVE_COLORS)
+ return self._defaultColors
+
def _resetColorAndStyle(self):
self._colorIndex = 0
self._styleIndex = 0
- def _getColorAndStyle(self):
- color = self.colorList[self._colorIndex]
+ def _getColorAndStyle(self) -> Tuple[str, str]:
+ defaultColors = self.getDefaultColors()
+ if self._colorIndex >= len(defaultColors): # Handle list length updated
+ self._colorIndex = 0
+
+ color = defaultColors[self._colorIndex]
style = self._styleList[self._styleIndex]
# Loop over color and then styles
self._colorIndex += 1
- if self._colorIndex >= len(self.colorList):
+ if self._colorIndex >= len(defaultColors):
self._colorIndex = 0
self._styleIndex = (self._styleIndex + 1) % len(self._styleList)
@@ -2967,7 +3102,7 @@ class PlotWidget(qt.QMainWindow):
color, style = self._getColorAndStyle()
if not self._plotLines:
- style = ' '
+ style = " "
return color, style
@@ -2990,32 +3125,30 @@ class PlotWidget(qt.QMainWindow):
:param kwargs: The information of the event.
"""
eventDict = kwargs.copy()
- eventDict['event'] = event
+ eventDict["event"] = event
self.sigPlotSignal.emit(eventDict)
- if event == 'setKeepDataAspectRatio':
- self.sigSetKeepDataAspectRatio.emit(kwargs['state'])
- elif event == 'setGraphGrid':
- self.sigSetGraphGrid.emit(kwargs['which'])
- elif event == 'setGraphCursor':
- self.sigSetGraphCursor.emit(kwargs['state'])
- elif event == 'contentChanged':
+ if event == "setKeepDataAspectRatio":
+ self.sigSetKeepDataAspectRatio.emit(kwargs["state"])
+ elif event == "setGraphGrid":
+ self.sigSetGraphGrid.emit(kwargs["which"])
+ elif event == "setGraphCursor":
+ self.sigSetGraphCursor.emit(kwargs["state"])
+ elif event == "contentChanged":
self.sigContentChanged.emit(
- kwargs['action'], kwargs['kind'], kwargs['legend'])
- elif event == 'activeCurveChanged':
- self.sigActiveCurveChanged.emit(
- kwargs['previous'], kwargs['legend'])
- elif event == 'activeImageChanged':
- self.sigActiveImageChanged.emit(
- kwargs['previous'], kwargs['legend'])
- elif event == 'activeScatterChanged':
- self.sigActiveScatterChanged.emit(
- kwargs['previous'], kwargs['legend'])
- elif event == 'interactiveModeChanged':
- self.sigInteractiveModeChanged.emit(kwargs['source'])
+ kwargs["action"], kwargs["kind"], kwargs["legend"]
+ )
+ elif event == "activeCurveChanged":
+ self.sigActiveCurveChanged.emit(kwargs["previous"], kwargs["legend"])
+ elif event == "activeImageChanged":
+ self.sigActiveImageChanged.emit(kwargs["previous"], kwargs["legend"])
+ elif event == "activeScatterChanged":
+ self.sigActiveScatterChanged.emit(kwargs["previous"], kwargs["legend"])
+ elif event == "interactiveModeChanged":
+ self.sigInteractiveModeChanged.emit(kwargs["source"])
eventDict = kwargs.copy()
- eventDict['event'] = event
+ eventDict["event"] = event
self._callback(eventDict)
def setCallback(self, callbackFunction=None):
@@ -3045,11 +3178,11 @@ class PlotWidget(qt.QMainWindow):
ddict = {}
_logger.debug("Received dict keys = %s", str(ddict.keys()))
_logger.debug(str(ddict))
- if ddict['event'] in ["legendClicked", "curveClicked"]:
- if ddict['button'] == "left":
- self.setActiveCurve(ddict['label'])
- qt.QToolTip.showText(self.cursor().pos(), ddict['label'])
- elif ddict['event'] == 'mouseClicked' and ddict['button'] == 'left':
+ if ddict["event"] == "curveClicked":
+ if ddict["button"] == "left":
+ self.setActiveCurve(ddict["item"])
+ qt.QToolTip.showText(self.cursor().pos(), ddict["label"])
+ elif ddict["event"] == "mouseClicked" and ddict["button"] == "left":
self.setActiveCurve(None)
def saveGraph(self, filename, fileFormat=None, dpi=None):
@@ -3066,42 +3199,51 @@ class PlotWidget(qt.QMainWindow):
:return: False if cannot save the plot, True otherwise
"""
if fileFormat is None:
- if not hasattr(filename, 'lower'):
- _logger.warning(
- 'saveGraph cancelled, cannot define file format.')
+ if not hasattr(filename, "lower"):
+ _logger.warning("saveGraph cancelled, cannot define file format.")
return False
else:
fileFormat = (filename.split(".")[-1]).lower()
- supportedFormats = ("png", "svg", "pdf", "ps", "eps",
- "tif", "tiff", "jpeg", "jpg")
+ supportedFormats = (
+ "png",
+ "svg",
+ "pdf",
+ "ps",
+ "eps",
+ "tif",
+ "tiff",
+ "jpeg",
+ "jpg",
+ )
if fileFormat not in supportedFormats:
- _logger.warning('Unsupported format %s', fileFormat)
+ _logger.warning("Unsupported format %s", fileFormat)
return False
else:
- self._backend.saveGraph(filename,
- fileFormat=fileFormat,
- dpi=dpi)
+ self._backend.saveGraph(filename, fileFormat=fileFormat, dpi=dpi)
return True
- def getDataMargins(self):
+ def getDataMargins(self) -> tuple[float, float, float, float]:
"""Get the default data margin ratios, see :meth:`setDataMargins`.
:return: The margin ratios for each side (xMin, xMax, yMin, yMax).
- :rtype: A 4-tuple of floats.
"""
return self._defaultDataMargins
- def setDataMargins(self, xMinMargin=0., xMaxMargin=0.,
- yMinMargin=0., yMaxMargin=0.):
+ def setDataMargins(
+ self,
+ xMinMargin: float = 0.0,
+ xMaxMargin: float = 0.0,
+ yMinMargin: float = 0.0,
+ yMaxMargin: float = 0.0,
+ ):
"""Set the default data margins to use in :meth:`resetZoom`.
- Set the default ratios of margins (as floats) to add around the data
+ Set the default ratios of margins to add around the data
inside the plot area for each side.
"""
- self._defaultDataMargins = (xMinMargin, xMaxMargin,
- yMinMargin, yMaxMargin)
+ self._defaultDataMargins = (xMinMargin, xMaxMargin, yMinMargin, yMaxMargin)
def getAutoReplot(self):
"""Return True if replot is automatically handled, False otherwise.
@@ -3133,10 +3275,10 @@ class PlotWidget(qt.QMainWindow):
It is in charge of performing required PlotWidget operations
"""
- for item in self._contentToUpdate:
+ for item in self.__itemsToUpdate:
item._update(self._backend)
- self._contentToUpdate = []
+ self.__itemsToUpdate = []
yield
self._dirty = False # reset dirty flag
@@ -3144,7 +3286,10 @@ class PlotWidget(qt.QMainWindow):
"""Request to draw the plot."""
self._backend.replot()
- def _forceResetZoom(self, dataMargins=None):
+ def _forceResetZoom(
+ self,
+ dataMargins: Optional[tuple[float, float, float, float]] = None,
+ ):
"""Reset the plot limits to the bounds of the data and redraw the plot.
This method forces a reset zoom and does not check axis autoscale.
@@ -3155,55 +3300,30 @@ class PlotWidget(qt.QMainWindow):
data (xMin, xMax, yMin and yMax limits).
For log scale, extra margins are applied in log10 of the data.
- :param dataMargins: Ratios of margins to add around the data inside
- the plot area for each side (default: no margins).
- :type dataMargins: A 4-tuple of float as (xMin, xMax, yMin, yMax).
+ :param dataMargins:
+ Ratios of margins to add around the data inside the plot area for each side.
+ If None (the default), use margins from :meth:`getDataMargins`.
"""
- if dataMargins is None:
- dataMargins = self._defaultDataMargins
-
# Get data range
ranges = self.getDataRange()
- xmin, xmax = (1., 100.) if ranges.x is None else ranges.x
- ymin, ymax = (1., 100.) if ranges.y is None else ranges.y
+ xmin, xmax = (1.0, 100.0) if ranges.x is None else ranges.x
+ ymin, ymax = (1.0, 100.0) if ranges.y is None else ranges.y
if ranges.yright is None:
- ymin2, ymax2 = ymin, ymax
+ y2min, y2max = ymin, ymax
else:
- ymin2, ymax2 = ranges.yright
+ y2min, y2max = ranges.yright
if ranges.y is None:
ymin, ymax = ranges.yright
- # Add margins around data inside the plot area
- newLimits = list(_utils.addMarginsToLimits(
- dataMargins,
- self._xAxis._isLogarithmic(),
- self._yAxis._isLogarithmic(),
- xmin, xmax, ymin, ymax, ymin2, ymax2))
-
- if self.isKeepDataAspectRatio():
- # Use limits with margins to keep ratio
- xmin, xmax, ymin, ymax = newLimits[:4]
-
- # Compute bbox wth figure aspect ratio
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
- if plotWidth > 0 and plotHeight > 0:
- plotRatio = plotHeight / plotWidth
- dataRatio = (ymax - ymin) / (xmax - xmin)
- if dataRatio < plotRatio:
- # Increase y range
- ycenter = 0.5 * (ymax + ymin)
- yrange = (xmax - xmin) * plotRatio
- newLimits[2] = ycenter - 0.5 * yrange
- newLimits[3] = ycenter + 0.5 * yrange
-
- elif dataRatio > plotRatio:
- # Increase x range
- xcenter = 0.5 * (xmax + xmin)
- xrange_ = (ymax - ymin) / plotRatio
- newLimits[0] = xcenter - 0.5 * xrange_
- newLimits[1] = xcenter + 0.5 * xrange_
-
- self.setLimits(*newLimits)
+ self.setLimits(
+ xmin,
+ xmax,
+ ymin,
+ ymax,
+ y2min,
+ y2max,
+ margins=dataMargins if dataMargins is not None else True,
+ )
def resetZoom(self, dataMargins=None):
"""Reset the plot limits to the bounds of the data and redraw the plot.
@@ -3233,7 +3353,9 @@ class PlotWidget(qt.QMainWindow):
# This avoids issues with toggling log scale with matplotlib 2.1.0
if self._xAxis.getScale() == self._xAxis.LOGARITHMIC and xLimits[0] <= 0:
xAuto = True
- if self._yAxis.getScale() == self._yAxis.LOGARITHMIC and (yLimits[0] <= 0 or y2Limits[0] <= 0):
+ if self._yAxis.getScale() == self._yAxis.LOGARITHMIC and (
+ yLimits[0] <= 0 or y2Limits[0] <= 0
+ ):
yAuto = True
if not xAuto and not yAuto:
@@ -3246,14 +3368,15 @@ class PlotWidget(qt.QMainWindow):
self.setGraphXLimits(*xLimits)
elif xAuto and not yAuto:
if y2Limits is not None:
- self.setGraphYLimits(
- y2Limits[0], y2Limits[1], axis='right')
+ self.setGraphYLimits(y2Limits[0], y2Limits[1], axis="right")
if yLimits is not None:
- self.setGraphYLimits(yLimits[0], yLimits[1], axis='left')
+ self.setGraphYLimits(yLimits[0], yLimits[1], axis="left")
- if (xLimits != self._xAxis.getLimits() or
- yLimits != self._yAxis.getLimits() or
- y2Limits != self._yRightAxis.getLimits()):
+ if (
+ xLimits != self._xAxis.getLimits()
+ or yLimits != self._yAxis.getLimits()
+ or y2Limits != self._yRightAxis.getLimits()
+ ):
self._notifyLimitsChanged()
# Coord conversion
@@ -3296,7 +3419,7 @@ class PlotWidget(qt.QMainWindow):
if check:
isOutside = numpy.logical_or(
numpy.logical_or(x > xmax, x < xmin),
- numpy.logical_or(y > ymax, y < ymin)
+ numpy.logical_or(y > ymax, y < ymin),
)
if numpy.any(isOutside):
@@ -3337,7 +3460,8 @@ class PlotWidget(qt.QMainWindow):
left, top, width, height = self.getPlotBoundsInPixels()
isOutside = numpy.logical_or(
numpy.logical_or(x < left, x > left + width),
- numpy.logical_or(y < top, y > top + height))
+ numpy.logical_or(y < top, y > top + height),
+ )
if numpy.any(isOutside):
return None
@@ -3367,14 +3491,6 @@ class PlotWidget(qt.QMainWindow):
self.__graphCursorShape = cursor
self._backend.setGraphCursorShape(cursor)
- @deprecated(replacement='getItems', since_version='0.13')
- def _getAllMarkers(self, just_legend=False):
- markers = [item for item in self.getItems() if isinstance(item, items.MarkerBase)]
- if just_legend:
- return [marker.getName() for marker in markers]
- else:
- return markers
-
def _getMarkerAt(self, x, y):
"""Return the most interactive marker at a location, else None
@@ -3382,10 +3498,13 @@ class PlotWidget(qt.QMainWindow):
:param float y: Y position in pixels
:rtype: None of marker object
"""
+
def checkDraggable(item):
return isinstance(item, items.MarkerBase) and item.isDraggable()
+
def checkSelectable(item):
return isinstance(item, items.MarkerBase) and item.isSelectable()
+
def check(item):
return isinstance(item, items.MarkerBase)
@@ -3405,7 +3524,7 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend of the marker to retrieve
:rtype: None of marker object
"""
- return self._getItem(kind='marker', legend=legend)
+ return self._getItem(kind="marker", legend=legend)
def pickItems(self, x, y, condition=None):
"""Generator of picked items in the plot at given position.
@@ -3420,7 +3539,9 @@ class PlotWidget(qt.QMainWindow):
:return: Iterable of :class:`PickingResult` objects at picked position.
Items are ordered from front to back.
"""
- for item in reversed(self._backend.getItemsFromBackToFront(condition=condition)):
+ for item in reversed(
+ self._backend.getItemsFromBackToFront(condition=condition)
+ ):
result = item.pick(x, y)
if result is not None:
yield result
@@ -3466,7 +3587,7 @@ class PlotWidget(qt.QMainWindow):
"""
if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
self._pressedButtons.append(btn)
- self._eventHandler.handleEvent('press', xPixel, yPixel, btn)
+ self._eventHandler.handleEvent("press", xPixel, yPixel, btn)
def onMouseMove(self, xPixel, yPixel):
"""Handle mouse move event.
@@ -3479,8 +3600,7 @@ class PlotWidget(qt.QMainWindow):
if self._cursorInPlot != isCursorInPlot:
self._cursorInPlot = isCursorInPlot
- self._eventHandler.handleEvent(
- 'enter' if self._cursorInPlot else 'leave')
+ self._eventHandler.handleEvent("enter" if self._cursorInPlot else "leave")
if isCursorInPlot:
# Signal mouse move event
@@ -3489,12 +3609,13 @@ class PlotWidget(qt.QMainWindow):
btn = self._pressedButtons[-1] if self._pressedButtons else None
event = PlotEvents.prepareMouseSignal(
- 'mouseMoved', btn, dataPos[0], dataPos[1], xPixel, yPixel)
+ "mouseMoved", btn, dataPos[0], dataPos[1], xPixel, yPixel
+ )
self.notify(**event)
# Either button was pressed in the plot or cursor is in the plot
if isCursorInPlot or self._pressedButtons:
- self._eventHandler.handleEvent('move', inXPixel, inYPixel)
+ self._eventHandler.handleEvent("move", inXPixel, inYPixel)
def onMouseRelease(self, xPixel, yPixel, btn):
"""Handle mouse release event.
@@ -3509,7 +3630,7 @@ class PlotWidget(qt.QMainWindow):
pass
else:
xPixel, yPixel = self._isPositionInPlotArea(xPixel, yPixel)
- self._eventHandler.handleEvent('release', xPixel, yPixel, btn)
+ self._eventHandler.handleEvent("release", xPixel, yPixel, btn)
def onMouseWheel(self, xPixel, yPixel, angleInDegrees):
"""Handle mouse wheel event.
@@ -3521,17 +3642,25 @@ class PlotWidget(qt.QMainWindow):
negative for movement toward the user.
"""
if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
- self._eventHandler.handleEvent(
- 'wheel', xPixel, yPixel, angleInDegrees)
+ self._eventHandler.handleEvent("wheel", xPixel, yPixel, angleInDegrees)
def onMouseLeaveWidget(self):
"""Handle mouse leave widget event."""
if self._cursorInPlot:
self._cursorInPlot = False
- self._eventHandler.handleEvent('leave')
+ self._eventHandler.handleEvent("leave")
# Interaction modes #
+ def interaction(self) -> PlotInteraction:
+ """Returns the interaction handler for this PlotWidget"""
+ return self._eventHandler
+
+ def __interactionChanged(self):
+ """Handle PlotInteraction updates"""
+ if self.__isInteractionSignalForwarded:
+ self.sigInteractiveModeChanged.emit(None)
+
def getInteractiveMode(self):
"""Returns the current interactive mode as a dict.
@@ -3540,7 +3669,7 @@ class PlotWidget(qt.QMainWindow):
It can also contains extra keys (e.g., 'color') specific to a mode
as provided to :meth:`setInteractiveMode`.
"""
- return self._eventHandler.getInteractiveMode()
+ return self.interaction()._getInteractiveMode()
def resetInteractiveMode(self):
"""Reset the interactive mode to use the previous basic interactive
@@ -3551,36 +3680,47 @@ class PlotWidget(qt.QMainWindow):
mode, zoomOnWheel = self._previousDefaultMode
self.setInteractiveMode(mode=mode, zoomOnWheel=zoomOnWheel)
- def setInteractiveMode(self, mode, color='black',
- shape='polygon', label=None,
- zoomOnWheel=True, source=None, width=None):
+ def setInteractiveMode(
+ self,
+ mode: str,
+ color: Union[str, Sequence[numbers.Real]] = "black",
+ shape: str = "polygon",
+ label: Optional[str] = None,
+ zoomOnWheel: bool = True,
+ source=None,
+ width: Optional[float] = None,
+ ):
"""Switch the interactive mode.
- :param str mode: The name of the interactive mode.
- In 'draw', 'pan', 'select', 'select-draw', 'zoom'.
+ :param mode: The name of the interactive mode.
+ In 'draw', 'pan', 'select', 'select-draw', 'zoom'.
:param color: Only for 'draw' and 'zoom' modes.
Color to use for drawing selection area. Default black.
:type color: Color description: The name as a str or
a tuple of 4 floats.
- :param str shape: Only for 'draw' mode. The kind of shape to draw.
- In 'polygon', 'rectangle', 'line', 'vline', 'hline',
- 'freeline'.
- Default is 'polygon'.
- :param str label: Only for 'draw' mode, sent in drawing events.
- :param bool zoomOnWheel: Toggle zoom on wheel support
+ :param shape: Only for 'draw' mode. The kind of shape to draw.
+ In 'polygon', 'rectangle', 'line', 'vline', 'hline',
+ 'freeline'.
+ Default is 'polygon'.
+ :param label: Only for 'draw' mode, sent in drawing events.
+ :param zoomOnWheel: Toggle zoom on wheel support
:param source: A user-defined object (typically the caller object)
that will be send in the interactiveModeChanged event,
to identify which object required a mode change.
Default: None
- :param float width: Width of the pencil. Only for draw pencil mode.
+ :param width: Width of the pencil. Only for draw pencil mode.
"""
- self._eventHandler.setInteractiveMode(mode, color, shape, label, width)
- self._eventHandler.zoomOnWheel = zoomOnWheel
+ self.__isInteractionSignalForwarded = False
+ try:
+ self._eventHandler._setInteractiveMode(mode, color, shape, label, width)
+ self._eventHandler.setZoomOnWheelEnabled(zoomOnWheel)
+ finally:
+ self.__isInteractionSignalForwarded = True
+
if mode in ["pan", "zoom"]:
self._previousDefaultMode = mode, zoomOnWheel
- self.notify(
- 'interactiveModeChanged', source=source)
+ self.notify("interactiveModeChanged", source=source)
# Panning with arrow keys
@@ -3613,10 +3753,10 @@ class PlotWidget(qt.QMainWindow):
# Dict to convert Qt arrow key code to direction str.
_ARROWS_TO_PAN_DIRECTION = {
- qt.Qt.Key_Left: 'left',
- qt.Qt.Key_Right: 'right',
- qt.Qt.Key_Up: 'up',
- qt.Qt.Key_Down: 'down'
+ qt.Qt.Key_Left: "left",
+ qt.Qt.Key_Right: "right",
+ qt.Qt.Key_Up: "up",
+ qt.Qt.Key_Down: "down",
}
def __simulateMouseMove(self):
@@ -3626,7 +3766,8 @@ class PlotWidget(qt.QMainWindow):
qt.QPointF(self.getWidgetHandle().mapFromGlobal(qt.QCursor.pos())),
qt.Qt.NoButton,
qapp.mouseButtons(),
- qapp.keyboardModifiers())
+ qapp.keyboardModifiers(),
+ )
qapp.sendEvent(self.getWidgetHandle(), event)
def keyPressEvent(self, event):
diff --git a/src/silx/gui/plot/PlotWindow.py b/src/silx/gui/plot/PlotWindow.py
index e8da174..9aa8c78 100644
--- a/src/silx/gui/plot/PlotWindow.py
+++ b/src/silx/gui/plot/PlotWindow.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -30,16 +30,12 @@ __authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "12/04/2019"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
import logging
import weakref
import silx
from silx.utils.weakref import WeakMethodProxy
-from silx.utils.deprecation import deprecated
from silx.utils.proxy import docstring
from . import PlotWidget
@@ -57,6 +53,7 @@ from .CurvesROIWidget import CurvesROIDockWidget
from .MaskToolsWidget import MaskToolsDockWidget
from .StatsWidget import BasicStatsWidget
from .ColorBar import ColorBarWidget
+
try:
from ..console import IPythonDockWidget
except ImportError:
@@ -103,16 +100,30 @@ class PlotWindow(PlotWidget):
:param bool fit: Toggle visibilty of fit action.
"""
- def __init__(self, parent=None, backend=None,
- resetzoom=True, autoScale=True, logScale=True, grid=True,
- curveStyle=True, colormap=True,
- aspectRatio=True, yInverted=True,
- copy=True, save=True, print_=True,
- control=False, position=False,
- roi=True, mask=True, fit=False):
+ def __init__(
+ self,
+ parent=None,
+ backend=None,
+ resetzoom=True,
+ autoScale=True,
+ logScale=True,
+ grid=True,
+ curveStyle=True,
+ colormap=True,
+ aspectRatio=True,
+ yInverted=True,
+ copy=True,
+ save=True,
+ print_=True,
+ control=False,
+ position=False,
+ roi=True,
+ mask=True,
+ fit=False,
+ ):
super(PlotWindow, self).__init__(parent=parent, backend=backend)
if parent is None:
- self.setWindowTitle('PlotWindow')
+ self.setWindowTitle("PlotWindow")
self._dockWidgets = []
@@ -131,63 +142,80 @@ class PlotWindow(PlotWidget):
self.group.setExclusive(False)
self.resetZoomAction = self.group.addAction(
- actions.control.ResetZoomAction(self, parent=self))
+ actions.control.ResetZoomAction(self, parent=self)
+ )
self.resetZoomAction.setVisible(resetzoom)
self.addAction(self.resetZoomAction)
- self.zoomInAction = actions.control.ZoomInAction(self, parent=self)
+ self.zoomInAction = self.group.addAction(
+ actions.control.ZoomInAction(self, parent=self)
+ )
+ self.zoomInAction.setVisible(False)
self.addAction(self.zoomInAction)
- self.zoomOutAction = actions.control.ZoomOutAction(self, parent=self)
+ self.zoomOutAction = self.group.addAction(
+ actions.control.ZoomOutAction(self, parent=self)
+ )
+ self.zoomOutAction.setVisible(False)
self.addAction(self.zoomOutAction)
self.xAxisAutoScaleAction = self.group.addAction(
- actions.control.XAxisAutoScaleAction(self, parent=self))
+ actions.control.XAxisAutoScaleAction(self, parent=self)
+ )
self.xAxisAutoScaleAction.setVisible(autoScale)
self.addAction(self.xAxisAutoScaleAction)
self.yAxisAutoScaleAction = self.group.addAction(
- actions.control.YAxisAutoScaleAction(self, parent=self))
+ actions.control.YAxisAutoScaleAction(self, parent=self)
+ )
self.yAxisAutoScaleAction.setVisible(autoScale)
self.addAction(self.yAxisAutoScaleAction)
self.xAxisLogarithmicAction = self.group.addAction(
- actions.control.XAxisLogarithmicAction(self, parent=self))
+ actions.control.XAxisLogarithmicAction(self, parent=self)
+ )
self.xAxisLogarithmicAction.setVisible(logScale)
self.addAction(self.xAxisLogarithmicAction)
self.yAxisLogarithmicAction = self.group.addAction(
- actions.control.YAxisLogarithmicAction(self, parent=self))
+ actions.control.YAxisLogarithmicAction(self, parent=self)
+ )
self.yAxisLogarithmicAction.setVisible(logScale)
self.addAction(self.yAxisLogarithmicAction)
self.gridAction = self.group.addAction(
- actions.control.GridAction(self, gridMode='both', parent=self))
+ actions.control.GridAction(self, gridMode="both", parent=self)
+ )
self.gridAction.setVisible(grid)
self.addAction(self.gridAction)
self.curveStyleAction = self.group.addAction(
- actions.control.CurveStyleAction(self, parent=self))
+ actions.control.CurveStyleAction(self, parent=self)
+ )
self.curveStyleAction.setVisible(curveStyle)
self.addAction(self.curveStyleAction)
self.colormapAction = self.group.addAction(
- actions.control.ColormapAction(self, parent=self))
+ actions.control.ColormapAction(self, parent=self)
+ )
self.colormapAction.setVisible(colormap)
self.addAction(self.colormapAction)
self.colorbarAction = self.group.addAction(
- actions_control.ColorBarAction(self, parent=self))
+ actions_control.ColorBarAction(self, parent=self)
+ )
self.colorbarAction.setVisible(False)
self.addAction(self.colorbarAction)
self._colorbar.setVisible(False)
self.keepDataAspectRatioButton = PlotToolButtons.AspectToolButton(
- parent=self, plot=self)
+ parent=self, plot=self
+ )
self.keepDataAspectRatioButton.setVisible(aspectRatio)
self.yAxisInvertedButton = PlotToolButtons.YAxisOriginToolButton(
- parent=self, plot=self)
+ parent=self, plot=self
+ )
self.yAxisInvertedButton.setVisible(yInverted)
self.group.addAction(self.getRoiAction())
@@ -197,15 +225,18 @@ class PlotWindow(PlotWidget):
self.getMaskAction().setVisible(mask)
self._intensityHistoAction = self.group.addAction(
- actions_histogram.PixelIntensitiesHistoAction(self, parent=self))
+ actions_histogram.PixelIntensitiesHistoAction(self, parent=self)
+ )
self._intensityHistoAction.setVisible(False)
self._medianFilter2DAction = self.group.addAction(
- actions_medfilt.MedianFilter2DAction(self, parent=self))
+ actions_medfilt.MedianFilter2DAction(self, parent=self)
+ )
self._medianFilter2DAction.setVisible(False)
self._medianFilter1DAction = self.group.addAction(
- actions_medfilt.MedianFilter1DAction(self, parent=self))
+ actions_medfilt.MedianFilter1DAction(self, parent=self)
+ )
self._medianFilter1DAction.setVisible(False)
self.fitAction = self.group.addAction(actions_fit.FitAction(self, parent=self))
@@ -239,24 +270,25 @@ class PlotWindow(PlotWidget):
converters = position
else:
converters = None
- self._positionWidget = tools.PositionInfo(
- plot=self, converters=converters)
+ self._positionWidget = tools.PositionInfo(plot=self, converters=converters)
# Set a snapping mode that is consistent with legacy one
self._positionWidget.setSnappingMode(
- tools.PositionInfo.SNAPPING_CROSSHAIR |
- tools.PositionInfo.SNAPPING_ACTIVE_ONLY |
- tools.PositionInfo.SNAPPING_SYMBOLS_ONLY |
- tools.PositionInfo.SNAPPING_CURVE |
- tools.PositionInfo.SNAPPING_SCATTER)
+ tools.PositionInfo.SNAPPING_CROSSHAIR
+ | tools.PositionInfo.SNAPPING_ACTIVE_ONLY
+ | tools.PositionInfo.SNAPPING_SYMBOLS_ONLY
+ | tools.PositionInfo.SNAPPING_CURVE
+ | tools.PositionInfo.SNAPPING_SCATTER
+ )
self.__setCentralWidget()
# Creating the toolbar also create actions for toolbuttons
self._interactiveModeToolBar = tools.InteractiveModeToolBar(
- parent=self, plot=self)
+ parent=self, plot=self
+ )
self.addToolBar(self._interactiveModeToolBar)
- self._toolbar = self._createToolBar(title='Plot', parent=self)
+ self._toolbar = self._createToolBar(title="Plot", parent=self)
self.addToolBar(self._toolbar)
self._outputToolBar = tools.OutputToolBar(parent=self, plot=self)
@@ -352,11 +384,6 @@ class PlotWindow(PlotWidget):
"""
return self._outputToolBar
- @property
- @deprecated(replacement="getPositionInfoWidget()", since_version="0.8.0")
- def positionWidget(self):
- return self.getPositionInfoWidget()
-
def getPositionInfoWidget(self):
"""Returns the widget displaying current cursor position information
@@ -395,12 +422,12 @@ class PlotWindow(PlotWidget):
banner = "The variable 'plt' is available. Use the 'whos' "
banner += "and 'help(plt)' commands for more information.\n\n"
self._consoleDockWidget = IPythonDockWidget(
- available_vars=available_vars,
- custom_banner=banner,
- parent=self)
+ available_vars=available_vars, custom_banner=banner, parent=self
+ )
self.addTabbedDockWidget(self._consoleDockWidget)
self._consoleDockWidget.toggleViewAction().toggled.connect(
- self._consoleDockWidgetToggled)
+ self._consoleDockWidgetToggled
+ )
self._consoleDockWidget.setVisible(isChecked)
@@ -440,15 +467,14 @@ class PlotWindow(PlotWidget):
elif obj is self.yAxisInvertedButton:
self.yAxisInvertedAction = toolbar.addWidget(obj)
else:
- raise RuntimeError()
+ raise RuntimeError("unknow action to be defined")
return toolbar
def toolBar(self):
- """Return a QToolBar from the QAction of the PlotWindow.
- """
+ """Return a QToolBar from the QAction of the PlotWindow."""
return self._toolbar
- def menu(self, title='Plot', parent=None):
+ def menu(self, title="Plot", parent=None):
"""Return a QMenu from the QAction of the PlotWindow.
:param str title: The title of the QMenu
@@ -495,8 +521,7 @@ class PlotWindow(PlotWidget):
self.addDockWidget(area, dock_widget)
else:
# Other dock widgets are added as tabs to the same widget area
- self.tabifyDockWidget(self._dockWidgets[0],
- dock_widget)
+ self.tabifyDockWidget(self._dockWidgets[0], dock_widget)
def removeDockWidget(self, dockwidget):
"""Removes the *dockwidget* from the main window layout and hides it.
@@ -521,8 +546,7 @@ class PlotWindow(PlotWidget):
"""
if visible:
dockWidget = self.sender()
- dockWidget.visibilityChanged.disconnect(
- self._handleFirstDockWidgetShow)
+ dockWidget.visibilityChanged.disconnect(self._handleFirstDockWidgetShow)
self.addTabbedDockWidget(dockWidget)
def _handleDockWidgetViewActionTriggered(self, checked):
@@ -551,9 +575,11 @@ class PlotWindow(PlotWidget):
self._legendsDockWidget = LegendsDockWidget(plot=self)
self._legendsDockWidget.hide()
self._legendsDockWidget.toggleViewAction().triggered.connect(
- self._handleDockWidgetViewActionTriggered)
+ self._handleDockWidgetViewActionTriggered
+ )
self._legendsDockWidget.visibilityChanged.connect(
- self._handleFirstDockWidgetShow)
+ self._handleFirstDockWidgetShow
+ )
return self._legendsDockWidget
def getCurvesRoiDockWidget(self):
@@ -561,12 +587,15 @@ class PlotWindow(PlotWidget):
# (still used internally for lazy loading)
if self._curvesROIDockWidget is None:
self._curvesROIDockWidget = CurvesROIDockWidget(
- plot=self, name='Regions Of Interest')
+ plot=self, name="Regions Of Interest"
+ )
self._curvesROIDockWidget.hide()
self._curvesROIDockWidget.toggleViewAction().triggered.connect(
- self._handleDockWidgetViewActionTriggered)
+ self._handleDockWidgetViewActionTriggered
+ )
self._curvesROIDockWidget.visibilityChanged.connect(
- self._handleFirstDockWidgetShow)
+ self._handleFirstDockWidgetShow
+ )
return self._curvesROIDockWidget
def getCurvesRoiWidget(self):
@@ -583,13 +612,14 @@ class PlotWindow(PlotWidget):
def getMaskToolsDockWidget(self):
"""DockWidget with image mask panel (lazy-loaded)."""
if self._maskToolsDockWidget is None:
- self._maskToolsDockWidget = MaskToolsDockWidget(
- plot=self, name='Mask')
+ self._maskToolsDockWidget = MaskToolsDockWidget(plot=self, name="Mask")
self._maskToolsDockWidget.hide()
self._maskToolsDockWidget.toggleViewAction().triggered.connect(
- self._handleDockWidgetViewActionTriggered)
+ self._handleDockWidgetViewActionTriggered
+ )
self._maskToolsDockWidget.visibilityChanged.connect(
- self._handleFirstDockWidgetShow)
+ self._handleFirstDockWidgetShow
+ )
return self._maskToolsDockWidget
def getStatsWidget(self):
@@ -605,23 +635,14 @@ class PlotWindow(PlotWidget):
self._statsDockWidget.setWidget(statsWidget)
self._statsDockWidget.hide()
self._statsDockWidget.toggleViewAction().triggered.connect(
- self._handleDockWidgetViewActionTriggered)
+ self._handleDockWidgetViewActionTriggered
+ )
self._statsDockWidget.visibilityChanged.connect(
- self._handleFirstDockWidgetShow)
+ self._handleFirstDockWidgetShow
+ )
return self._statsDockWidget.widget()
# getters for actions
- @property
- @deprecated(replacement="getInteractiveModeToolBar().getZoomModeAction()",
- since_version="0.8.0")
- def zoomModeAction(self):
- return self.getInteractiveModeToolBar().getZoomModeAction()
-
- @property
- @deprecated(replacement="getInteractiveModeToolBar().getPanModeAction()",
- since_version="0.8.0")
- def panModeAction(self):
- return self.getInteractiveModeToolBar().getPanModeAction()
def getConsoleAction(self):
"""QAction handling the IPython console activation.
@@ -634,7 +655,7 @@ class PlotWindow(PlotWidget):
:rtype: QAction
"""
if self._consoleAction is None:
- self._consoleAction = qt.QAction('Console', self)
+ self._consoleAction = qt.QAction("Console", self)
self._consoleAction.setCheckable(True)
if IPythonDockWidget is not None:
self._consoleAction.toggled.connect(self._toggleConsoleVisibility)
@@ -650,7 +671,7 @@ class PlotWindow(PlotWidget):
:rtype: actions.PlotAction
"""
if self._crosshairAction is None:
- self._crosshairAction = actions.control.CrosshairAction(self, color='red')
+ self._crosshairAction = actions.control.CrosshairAction(self, color="red")
return self._crosshairAction
def getMaskAction(self):
@@ -854,22 +875,36 @@ class Plot1D(PlotWindow):
"""
def __init__(self, parent=None, backend=None):
- super(Plot1D, 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)
+ super(Plot1D, 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('Plot1D')
- self.getXAxis().setLabel('X')
- self.getYAxis().setLabel('Y')
+ self.setWindowTitle("Plot1D")
+ self.getXAxis().setLabel("X")
+ self.getYAxis().setLabel("Y")
action = self.getFitAction()
action.setXRangeUpdatedOnZoom(True)
action.setFittedItemUpdatedFromActiveCurve(True)
+ self.getInteractiveModeToolBar().getZoomModeAction().setAxesMenuEnabled(True)
+
class Plot2D(PlotWindow):
"""PlotWindow with a toolbar specific for images.
@@ -885,26 +920,37 @@ class Plot2D(PlotWindow):
def __init__(self, parent=None, backend=None):
# List of information to display at the bottom of the plot
posInfo = [
- ('X', lambda x, y: x),
- ('Y', lambda x, y: y),
- ('Data', WeakMethodProxy(self._getImageValue)),
- ('Dims', WeakMethodProxy(self._getImageDims)),
+ ("X", lambda x, y: x),
+ ("Y", lambda x, y: y),
+ ("Data", WeakMethodProxy(self._getImageValue)),
+ ("Dims", WeakMethodProxy(self._getImageDims)),
]
- super(Plot2D, self).__init__(parent=parent, backend=backend,
- resetzoom=True, autoScale=False,
- logScale=False, grid=False,
- curveStyle=False, colormap=True,
- aspectRatio=True, yInverted=True,
- copy=True, save=True, print_=True,
- control=False, position=posInfo,
- roi=False, mask=True)
+ super(Plot2D, self).__init__(
+ parent=parent,
+ backend=backend,
+ resetzoom=True,
+ autoScale=False,
+ logScale=False,
+ grid=False,
+ curveStyle=False,
+ colormap=True,
+ aspectRatio=True,
+ yInverted=True,
+ copy=True,
+ save=True,
+ print_=True,
+ control=False,
+ position=posInfo,
+ roi=False,
+ mask=True,
+ )
if parent is None:
- self.setWindowTitle('Plot2D')
- self.getXAxis().setLabel('Columns')
- self.getYAxis().setLabel('Rows')
+ self.setWindowTitle("Plot2D")
+ self.getXAxis().setLabel("Columns")
+ self.getYAxis().setLabel("Rows")
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
+ if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward":
self.getYAxis().setInverted(True)
self.profile = ProfileToolBar(plot=self)
@@ -959,8 +1005,9 @@ class Plot2D(PlotWindow):
"""
pickedMask = None
for picked in self.pickItems(
- *self.dataToPixel(x, y, check=False),
- lambda item: isinstance(item, items.ImageBase)):
+ *self.dataToPixel(x, y, check=False),
+ lambda item: isinstance(item, items.ImageBase),
+ ):
if isinstance(picked.getItem(), items.MaskImageData):
if pickedMask is None: # Use top-most if many masks
pickedMask = picked
@@ -980,16 +1027,15 @@ class Plot2D(PlotWindow):
return value, "Masked"
return value
- return '-' # No image picked
+ return "-" # No image picked
def _getImageDims(self, *args):
activeImage = self.getActiveImage()
- if (activeImage is not None and
- activeImage.getData(copy=False) is not None):
+ if activeImage is not None and activeImage.getData(copy=False) is not None:
dims = activeImage.getData(copy=False).shape[1::-1]
- return 'x'.join(str(dim) for dim in dims)
+ return "x".join(str(dim) for dim in dims)
else:
- return '-'
+ return "-"
def getProfileToolbar(self):
"""Profile tools attached to this plot
@@ -998,10 +1044,6 @@ class Plot2D(PlotWindow):
"""
return self.profile
- @deprecated(replacement="getProfilePlot", since_version="0.5.0")
- def getProfileWindow(self):
- return self.getProfilePlot()
-
def getProfilePlot(self):
"""Return plot window used to display profile curve.
diff --git a/src/silx/gui/plot/PrintPreviewToolButton.py b/src/silx/gui/plot/PrintPreviewToolButton.py
index 9069ac3..0812420 100644
--- a/src/silx/gui/plot/PrintPreviewToolButton.py
+++ b/src/silx/gui/plot/PrintPreviewToolButton.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -109,7 +109,6 @@ from .. import icons
from . import PlotWidget
from ..widgets.PrintPreview import PrintPreviewDialog, SingletonPrintPreviewDialog
from ..widgets.PrintGeometryDialog import PrintGeometryDialog
-from silx.utils.deprecation import deprecated
__authors__ = ["P. Knobel"]
__license__ = "MIT"
@@ -126,6 +125,7 @@ class PrintPreviewToolButton(qt.QToolButton):
:param parent: See :class:`QAction`
:param plot: :class:`.PlotWidget` instance on which to operate
"""
+
def __init__(self, parent=None, plot=None):
super(PrintPreviewToolButton, self).__init__(parent)
@@ -133,17 +133,19 @@ class PrintPreviewToolButton(qt.QToolButton):
raise TypeError("plot parameter must be a PlotWidget")
self._plot = plot
- self.setIcon(icons.getQIcon('document-print'))
+ self.setIcon(icons.getQIcon("document-print"))
printGeomAction = qt.QAction("Print geometry", self)
- printGeomAction.setToolTip("Define a print geometry prior to sending "
- "the plot to the print preview dialog")
- printGeomAction.setIcon(icons.getQIcon('shape-rectangle'))
+ printGeomAction.setToolTip(
+ "Define a print geometry prior to sending "
+ "the plot to the print preview dialog"
+ )
+ printGeomAction.setIcon(icons.getQIcon("shape-rectangle"))
printGeomAction.triggered.connect(self._setPrintConfiguration)
printPreviewAction = qt.QAction("Print preview", self)
printPreviewAction.setToolTip("Send plot to the print preview dialog")
- printPreviewAction.setIcon(icons.getQIcon('document-print'))
+ printPreviewAction.setIcon(icons.getQIcon("document-print"))
printPreviewAction.triggered.connect(self._plotToPrintPreview)
menu = qt.QMenu(self)
@@ -155,12 +157,14 @@ class PrintPreviewToolButton(qt.QToolButton):
self._printPreviewDialog = None
self._printConfigurationDialog = None
- self._printGeometry = {"xOffset": 0.1,
- "yOffset": 0.1,
- "width": 0.9,
- "height": 0.9,
- "units": "page",
- "keepAspectRatio": True}
+ self._printGeometry = {
+ "xOffset": 0.1,
+ "yOffset": 0.1,
+ "width": 0.9,
+ "height": 0.9,
+ "units": "page",
+ "keepAspectRatio": True,
+ }
@property
def printPreviewDialog(self):
@@ -189,12 +193,6 @@ class PrintPreviewToolButton(qt.QToolButton):
"""
return None, None
- @property
- @deprecated(since_version="0.10",
- replacement="getPlot()")
- def plot(self):
- return self._plot
-
def getPlot(self):
"""Return the :class:`.PlotWidget` associated with this tool button.
@@ -212,19 +210,23 @@ class PrintPreviewToolButton(qt.QToolButton):
if qt.HAS_SVG:
svgRenderer, viewBox = self._getSvgRendererAndViewbox()
- self.printPreviewDialog.addSvgItem(svgRenderer,
- title=self.getTitle(),
- comment=comment,
- commentPosition=commentPosition,
- viewBox=viewBox,
- keepRatio=self._printGeometry["keepAspectRatio"])
+ self.printPreviewDialog.addSvgItem(
+ svgRenderer,
+ title=self.getTitle(),
+ comment=comment,
+ commentPosition=commentPosition,
+ viewBox=viewBox,
+ keepRatio=self._printGeometry["keepAspectRatio"],
+ )
else:
_logger.warning("Missing QtSvg library, using a raster image")
pixmap = self._plot.centralWidget().grab()
- self.printPreviewDialog.addPixmap(pixmap,
- title=self.getTitle(),
- comment=comment,
- commentPosition=commentPosition)
+ self.printPreviewDialog.addPixmap(
+ pixmap,
+ title=self.getTitle(),
+ comment=comment,
+ commentPosition=commentPosition,
+ )
self.printPreviewDialog.show()
self.printPreviewDialog.raise_()
@@ -236,8 +238,7 @@ class PrintPreviewToolButton(qt.QToolButton):
and to the geometry configuration (width, height, ratio) specified
by the user."""
imgData = StringIO()
- assert self._plot.saveGraph(imgData, fileFormat="svg"), \
- "Unable to save graph"
+ assert self._plot.saveGraph(imgData, fileFormat="svg"), "Unable to save graph"
imgData.flush()
imgData.seek(0)
svgData = imgData.read()
@@ -261,8 +262,7 @@ class PrintPreviewToolButton(qt.QToolButton):
return svgRenderer, viewbox
def _getViewBox(self):
- """
- """
+ """ """
printer = self.printPreviewDialog.printer
dpix = printer.logicalDpiX()
dpiy = printer.logicalDpiY()
@@ -270,23 +270,23 @@ class PrintPreviewToolButton(qt.QToolButton):
availableHeight = printer.height()
config = self._printGeometry
- width = config['width']
- height = config['height']
- xOffset = config['xOffset']
- yOffset = config['yOffset']
- units = config['units']
- keepAspectRatio = config['keepAspectRatio']
+ width = config["width"]
+ height = config["height"]
+ xOffset = config["xOffset"]
+ yOffset = config["yOffset"]
+ units = config["units"]
+ keepAspectRatio = config["keepAspectRatio"]
aspectRatio = self._getPlotAspectRatio()
# convert the offsets to dots
- if units.lower() in ['inch', 'inches']:
+ if units.lower() in ["inch", "inches"]:
xOffset = xOffset * dpix
yOffset = yOffset * dpiy
if width is not None:
width = width * dpix
if height is not None:
height = height * dpiy
- elif units.lower() in ['cm', 'centimeters']:
+ elif units.lower() in ["cm", "centimeters"]:
xOffset = (xOffset / 2.54) * dpix
yOffset = (yOffset / 2.54) * dpiy
if width is not None:
@@ -307,13 +307,17 @@ class PrintPreviewToolButton(qt.QToolButton):
if width is not None:
if (availableWidth + 0.1) < width:
- txt = "Available width %f is less than requested width %f" % \
- (availableWidth, width)
+ txt = "Available width %f is less than requested width %f" % (
+ availableWidth,
+ width,
+ )
raise ValueError(txt)
if height is not None:
if (availableHeight + 0.1) < height:
- txt = "Available height %f is less than requested height %f" % \
- (availableHeight, height)
+ txt = "Available height %f is less than requested height %f" % (
+ availableHeight,
+ height,
+ )
raise ValueError(txt)
if keepAspectRatio:
@@ -328,10 +332,7 @@ class PrintPreviewToolButton(qt.QToolButton):
bodyWidth = width or availableWidth
bodyHeight = height or availableHeight
- return qt.QRectF(xOffset,
- yOffset,
- bodyWidth,
- bodyHeight)
+ return qt.QRectF(xOffset, yOffset, bodyWidth, bodyHeight)
def _setPrintConfiguration(self):
"""Open a dialog to prompt the user to adjust print
@@ -357,6 +358,7 @@ class SingletonPrintPreviewToolButton(PrintPreviewToolButton):
This allows for several plots to send their content to the
same print page, and for users to arrange them."""
+
def __init__(self, parent=None, plot=None):
PrintPreviewToolButton.__init__(self, parent, plot)
@@ -367,14 +369,14 @@ class SingletonPrintPreviewToolButton(PrintPreviewToolButton):
return self._printPreviewDialog
-if __name__ == '__main__':
+if __name__ == "__main__":
import numpy
+
app = qt.QApplication([])
pw = PlotWidget()
toolbar = qt.QToolBar(pw)
- toolbutton = PrintPreviewToolButton(parent=toolbar,
- plot=pw)
+ toolbutton = PrintPreviewToolButton(parent=toolbar, plot=pw)
pw.addToolBar(toolbar)
toolbar.addWidget(toolbutton)
pw.show()
diff --git a/src/silx/gui/plot/Profile.py b/src/silx/gui/plot/Profile.py
index bf793c8..f89f780 100644
--- a/src/silx/gui/plot/Profile.py
+++ b/src/silx/gui/plot/Profile.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -34,27 +34,18 @@ import weakref
from .. import qt
from . import actions
-from .tools.profile import core
from .tools.profile import manager
from .tools.profile import rois
from silx.gui.widgets.MultiModeAction import MultiModeAction
-from silx.utils.deprecation import deprecated
-from silx.utils.deprecation import deprecated_warning
from .tools import roi as roi_mdl
from silx.gui.plot import items
-@deprecated(replacement="silx.gui.plot.tools.profile.createProfile", since_version="0.13.0")
-def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
- return core.createProfile(roiInfo, currentData, origin,
- scale, lineWidth, method)
-
-
class _CustomProfileManager(manager.ProfileManager):
"""This custom profile manager uses a single predefined profile window
if it is specified. Else the behavior is the same as the default
- ProfileManager """
+ ProfileManager"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -78,7 +69,10 @@ class _CustomProfileManager(manager.ProfileManager):
self.__profileWindow = profileWindow
def createProfileWindow(self, plot, roi):
- for roiClass, specializedProfileWindow in self.__specializedProfileWindows.items():
+ for (
+ roiClass,
+ specializedProfileWindow,
+ ) in self.__specializedProfileWindows.items():
if isinstance(roi, roiClass):
return specializedProfileWindow
@@ -121,23 +115,13 @@ class ProfileToolBar(qt.QToolBar):
:param plot: :class:`PlotWindow` instance on which to operate.
:param profileWindow: Plot widget instance where to
display the profile curve or None to create one.
- :param str title: See :class:`QToolBar`.
:param parent: See :class:`QToolBar`.
"""
- def __init__(self, parent=None, plot=None, profileWindow=None,
- title=None):
- super(ProfileToolBar, self).__init__(title, parent)
+ def __init__(self, parent=None, plot=None, profileWindow=None):
+ super(ProfileToolBar, self).__init__(parent)
assert plot is not None
- if title is not None:
- deprecated_warning("Attribute",
- name="title",
- reason="removed",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
-
self._plotRef = weakref.ref(plot)
# If a profileWindow is defined,
@@ -185,22 +169,27 @@ class ProfileToolBar(qt.QToolBar):
return _CustomProfileManager(parent, plot)
def _createProfileActions(self):
- self.hLineAction = self._manager.createProfileAction(rois.ProfileImageHorizontalLineROI, self)
- self.vLineAction = self._manager.createProfileAction(rois.ProfileImageVerticalLineROI, self)
- self.lineAction = self._manager.createProfileAction(rois.ProfileImageLineROI, self)
- self.freeLineAction = self._manager.createProfileAction(rois.ProfileImageDirectedLineROI, self)
- self.crossAction = self._manager.createProfileAction(rois.ProfileImageCrossROI, self)
+ self.hLineAction = self._manager.createProfileAction(
+ rois.ProfileImageHorizontalLineROI, self
+ )
+ self.vLineAction = self._manager.createProfileAction(
+ rois.ProfileImageVerticalLineROI, self
+ )
+ self.lineAction = self._manager.createProfileAction(
+ rois.ProfileImageLineROI, self
+ )
+ self.freeLineAction = self._manager.createProfileAction(
+ rois.ProfileImageDirectedLineROI, self
+ )
+ self.crossAction = self._manager.createProfileAction(
+ rois.ProfileImageCrossROI, self
+ )
self.clearAction = self._manager.createClearAction(self)
def getPlotWidget(self):
"""The :class:`.PlotWidget` associated to the toolbar."""
return self._plotRef()
- @property
- @deprecated(since_version="0.13.0", replacement="getPlotWidget()")
- def plot(self):
- return self.getPlotWidget()
-
def _setRoiActionEnabled(self, itemKind, enabled):
for action in self.__multiAction.getMenu().actions():
if not isinstance(action, roi_mdl.CreateRoiModeAction):
@@ -221,16 +210,6 @@ class ProfileToolBar(qt.QToolBar):
enabled = image.getData(copy=False).size > 0
self._setRoiActionEnabled(type(image), enabled)
- @property
- @deprecated(since_version="0.6.0")
- def browseAction(self):
- return self._browseAction
-
- @property
- @deprecated(replacement="getProfilePlot", since_version="0.5.0")
- def profileWindow(self):
- return self.getProfilePlot()
-
def getProfileManager(self):
"""Return the manager of the profiles.
@@ -238,114 +217,38 @@ class ProfileToolBar(qt.QToolBar):
"""
return self._manager
- @deprecated(since_version="0.13.0")
- def getProfilePlot(self):
- """Return plot widget in which the profile curve or the
- profile image is plotted.
- """
- window = self.getProfileMainWindow()
- if window is None:
- return None
- return window.getCurrentPlotWidget()
-
- @deprecated(replacement="getProfileManager().getCurrentRoi().getProfileWindow()", since_version="0.13.0")
- def getProfileMainWindow(self):
- """Return window containing the profile curve widget.
-
- This can return None if no profile was computed.
- """
- roi = self._manager.getCurrentRoi()
- if roi is None:
- return None
- return roi.getProfileWindow()
-
- @property
- @deprecated(since_version="0.13.0")
- def overlayColor(self):
- """This method does nothing anymore. But could be implemented if needed.
-
- It was used to set color to use for the ROI.
-
- If set to None (the default), the overlay color is adapted to the
- active image colormap and changes if the active image colormap changes.
- """
- pass
-
- @overlayColor.setter
- @deprecated(since_version="0.13.0")
- def overlayColor(self, color):
- """This method does nothing anymore. But could be implemented if needed.
- """
- pass
-
def clearProfile(self):
"""Remove profile curve and profile area."""
self._manager.clearProfile()
- @deprecated(since_version="0.13.0")
- def updateProfile(self):
- """This method does nothing anymore. But could be implemented if needed.
-
- It was used to update the displayed profile and profile ROI.
-
- This uses the current active image of the plot and the current ROI.
- """
- pass
-
- @deprecated(replacement="clearProfile()", since_version="0.13.0")
- def hideProfileWindow(self):
- """Hide profile window.
- """
- self.clearProfile()
-
- @deprecated(since_version="0.13.0")
- def setProfileMethod(self, method):
- assert method in ('sum', 'mean')
- roi = self._manager.getCurrentRoi()
- if roi is None:
- raise RuntimeError("No profile ROI selected")
- roi.setProfileMethod(method)
-
- @deprecated(since_version="0.13.0")
- def getProfileMethod(self):
- roi = self._manager.getCurrentRoi()
- if roi is None:
- raise RuntimeError("No profile ROI selected")
- return roi.getProfileMethod()
-
- @deprecated(since_version="0.13.0")
- def getProfileOptionToolAction(self):
- return self._editor
-
class Profile3DToolBar(ProfileToolBar):
- def __init__(self, parent=None, stackview=None,
- title=None):
+ def __init__(self, parent=None, stackview=None):
"""QToolBar providing profile tools for an image or a stack of images.
:param parent: the parent QWidget
:param stackview: :class:`StackView` instance on which to operate.
- :param str title: See :class:`QToolBar`.
:param parent: See :class:`QToolBar`.
"""
# TODO: add param profileWindow (specify the plot used for profiles)
- super(Profile3DToolBar, self).__init__(parent=parent,
- plot=stackview.getPlotWidget())
-
- if title is not None:
- deprecated_warning("Attribute",
- name="title",
- reason="removed",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
+ super(Profile3DToolBar, self).__init__(
+ parent=parent, plot=stackview.getPlotWidget()
+ )
self.stackView = stackview
""":class:`StackView` instance"""
def _createProfileActions(self):
- self.hLineAction = self._manager.createProfileAction(rois.ProfileImageStackHorizontalLineROI, self)
- self.vLineAction = self._manager.createProfileAction(rois.ProfileImageStackVerticalLineROI, self)
- self.lineAction = self._manager.createProfileAction(rois.ProfileImageStackLineROI, self)
- self.crossAction = self._manager.createProfileAction(rois.ProfileImageStackCrossROI, self)
+ self.hLineAction = self._manager.createProfileAction(
+ rois.ProfileImageStackHorizontalLineROI, self
+ )
+ self.vLineAction = self._manager.createProfileAction(
+ rois.ProfileImageStackVerticalLineROI, self
+ )
+ self.lineAction = self._manager.createProfileAction(
+ rois.ProfileImageStackLineROI, self
+ )
+ self.crossAction = self._manager.createProfileAction(
+ rois.ProfileImageStackCrossROI, self
+ )
self.clearAction = self._manager.createClearAction(self)
diff --git a/src/silx/gui/plot/ProfileMainWindow.py b/src/silx/gui/plot/ProfileMainWindow.py
deleted file mode 100644
index 09a5b41..0000000
--- a/src/silx/gui/plot/ProfileMainWindow.py
+++ /dev/null
@@ -1,109 +0,0 @@
-# /*##########################################################################
-#
-# 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
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module contains a QMainWindow class used to display profile plots.
-"""
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "21/02/2017"
-
-import silx.utils.deprecation
-from silx.gui import qt
-from .tools.profile.manager import ProfileWindow
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.ProfileMainWindow",
- reason="moved",
- replacement="silx.gui.plot.tools.profile.manager.ProfileWindow",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
-
-class ProfileMainWindow(ProfileWindow):
- """QMainWindow providing 2 plot widgets specialized in
- 1D and 2D plotting, with different toolbars.
-
- Only one of the plots is visible at any given time.
-
- :param qt.QWidget parent: The parent of this widget or None (default).
- :param Union[str,Class] backend: The backend to use, in:
- 'matplotlib' (default), 'mpl', 'opengl', 'gl', 'none'
- or a :class:`BackendBase.BackendBase` class
- """
-
- sigProfileDimensionsChanged = qt.Signal(int)
- """This signal is emitted when :meth:`setProfileDimensions` is called.
- It carries the number of dimensions for the profile data (1 or 2).
- It can be used to be notified that the profile plot widget has changed.
-
- Note: This signal should be removed.
- """
-
- sigProfileMethodChanged = qt.Signal(str)
- """Emitted when the method to compute the profile changed (for now can be
- sum or mean)
-
- Note: This signal should be removed.
- """
-
- def __init__(self, parent=None, backend=None):
- ProfileWindow.__init__(self, parent=parent, backend=backend)
- # by default, profile is assumed to be a 1D curve
- self._profileType = None
-
- def setProfileType(self, profileType):
- """Set which profile plot widget (1D or 2D) is to be used
-
- Note: This method should be removed.
-
- :param str profileType: Type of profile data,
- "1D" for a curve or "2D" for an image
- """
- self._profileType = profileType
- if self._profileType == "1D":
- self._showPlot1D()
- elif self._profileType == "2D":
- self._showPlot2D()
- else:
- raise ValueError("Profile type must be '1D' or '2D'")
- self.sigProfileDimensionsChanged.emit(profileType)
-
- def getPlot(self):
- """Return the profile plot widget which is currently in use.
- This can be the 2D profile plot or the 1D profile plot.
-
- Note: This method should be removed.
- """
- return self.getCurrentPlotWidget()
-
- def setProfileMethod(self, method):
- """
- Note: This method should be removed.
-
- :param str method: method to manage the 'width' in the profile
- (computing mean or sum).
- """
- assert method in ('sum', 'mean')
- self._method = method
- self.sigProfileMethodChanged.emit(self._method)
diff --git a/src/silx/gui/plot/ROIStatsWidget.py b/src/silx/gui/plot/ROIStatsWidget.py
index 732c60f..36f3391 100644
--- a/src/silx/gui/plot/ROIStatsWidget.py
+++ b/src/silx/gui/plot/ROIStatsWidget.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -34,8 +34,8 @@ __date__ = "22/07/2019"
from contextlib import contextmanager
from silx.gui import qt
from silx.gui import icons
-from silx.gui.plot.StatsWidget import _StatsWidgetBase, StatsTable, _Container
-from silx.gui.plot.StatsWidget import UpdateModeWidget, UpdateMode
+from silx.gui.plot.StatsWidget import _StatsWidgetBase, _Container
+from silx.gui.plot.StatsWidget import UpdateMode
from silx.gui.widgets.TableWidget import TableWidget
from silx.gui.plot.items.roi import RegionOfInterest
from silx.gui.plot import items as plotitems
@@ -43,7 +43,6 @@ from silx.gui.plot.items.core import ItemChangedType
from silx.gui.plot3d import items as plot3ditems
from silx.gui.plot.CurvesROIWidget import ROI
from silx.gui.plot import stats as statsmdl
-from collections import OrderedDict
from silx.utils.proxy import docstring
import silx.gui.plot.items.marker
import silx.gui.plot.items.shape
@@ -57,7 +56,8 @@ class _GetROIItemCoupleDialog(qt.QDialog):
"""
Dialog used to know which plot item and which roi he wants
"""
- _COMPATIBLE_KINDS = ('curve', 'image', 'scatter', 'histogram')
+
+ _COMPATIBLE_KINDS = ("curve", "image", "scatter", "histogram")
def __init__(self, parent=None, plot=None, rois=None):
qt.QDialog.__init__(self, parent=parent)
@@ -92,13 +92,15 @@ class _GetROIItemCoupleDialog(qt.QDialog):
def _getCompatibleRois(self, kind):
"""Return compatible rois for the given item kind"""
+
def is_compatible(roi, kind):
if isinstance(roi, RegionOfInterest):
- return kind in ('image', 'scatter')
+ return kind in ("image", "scatter")
elif isinstance(roi, ROI):
- return kind in ('curve', 'histogram')
+ return kind in ("curve", "histogram")
else:
- raise ValueError('kind not managed')
+ raise ValueError("kind not managed")
+
return list(filter(lambda x: is_compatible(x, kind), self._rois))
def exec(self):
@@ -114,6 +116,7 @@ class _GetROIItemCoupleDialog(qt.QDialog):
self._kind_name_to_item = {}
# key is (kind, legend name) value is item
for kind in _GetROIItemCoupleDialog._COMPATIBLE_KINDS:
+
def getItems(kind):
output = []
for item in self._plot.getItems():
@@ -135,7 +138,7 @@ class _GetROIItemCoupleDialog(qt.QDialog):
# filter roi according to kinds
if len(self._valid_kinds) == 0:
- _logger.warning('no couple item/roi detected for displaying stats')
+ _logger.warning("no couple item/roi detected for displaying stats")
return self.reject()
for kind in self._valid_kinds:
@@ -173,10 +176,11 @@ class ROIStatsItemHelper(object):
Display on one row statistics regarding the couple
(Item (plot item) / roi).
- :param Item plot_item: item for which we want statistics
+ :param Item plot_item: item for which we want statistics
:param Union[ROI,RegionOfInterest]: region of interest to use for
statistics.
"""
+
def __init__(self, plot_item, roi):
self._plot_item = plot_item
self._roi = roi
@@ -192,7 +196,7 @@ class ROIStatsItemHelper(object):
elif isinstance(self._roi, RegionOfInterest):
return self._roi.getName()
else:
- raise TypeError('Unmanaged roi type')
+ raise TypeError("Unmanaged roi type")
@property
def roi_kind(self):
@@ -203,19 +207,21 @@ class ROIStatsItemHelper(object):
def item_kind(self):
"""item kind"""
if isinstance(self._plot_item, plotitems.Curve):
- return 'curve'
+ return "curve"
elif isinstance(self._plot_item, plotitems.ImageData):
- return 'image'
+ return "image"
elif isinstance(self._plot_item, plotitems.Scatter):
- return 'scatter'
+ return "scatter"
elif isinstance(self._plot_item, plotitems.Histogram):
- return 'histogram'
- elif isinstance(self._plot_item, (plot3ditems.ImageData,
- plot3ditems.ScalarField3D)):
- return 'image'
- elif isinstance(self._plot_item, (plot3ditems.Scatter2D,
- plot3ditems.Scatter3D)):
- return 'scatter'
+ return "histogram"
+ elif isinstance(
+ self._plot_item, (plot3ditems.ImageData, plot3ditems.ScalarField3D)
+ ):
+ return "image"
+ elif isinstance(
+ self._plot_item, (plot3ditems.Scatter2D, plot3ditems.Scatter3D)
+ ):
+ return "scatter"
@property
def item_legend(self):
@@ -224,27 +230,28 @@ class ROIStatsItemHelper(object):
def id_key(self):
"""unique key to represent the couple (item, roi)"""
- return (self.item_kind(), self.item_legend, self.roi_kind,
- self.roi_name())
+ return (self.item_kind(), self.item_legend, self.roi_kind, self.roi_name())
class _StatsROITable(_StatsWidgetBase, TableWidget):
"""
Table sued to display some statistics regarding a couple (item/roi)
"""
- _LEGEND_HEADER_DATA = 'legend'
- _KIND_HEADER_DATA = 'kind'
+ _LEGEND_HEADER_DATA = "legend"
+
+ _KIND_HEADER_DATA = "kind"
- _ROI_HEADER_DATA = 'roi'
+ _ROI_HEADER_DATA = "roi"
sigUpdateModeChanged = qt.Signal(object)
"""Signal emitted when the update mode changed"""
def __init__(self, parent, plot):
TableWidget.__init__(self, parent)
- _StatsWidgetBase.__init__(self, statsOnVisibleData=False,
- displayOnlyActItem=False)
+ _StatsWidgetBase.__init__(
+ self, statsOnVisibleData=False, displayOnlyActItem=False
+ )
self.__region_edition_callback = {}
"""We need to keep trace of the roi signals connection because
the roi emits the sigChanged during roi edition"""
@@ -284,8 +291,8 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
def _addItem(self, item):
"""
Add a _RoiStatsItemWidget item to the table.
-
- :param item:
+
+ :param item:
:return: True if successfully added.
"""
if not isinstance(item, ROIStatsItemHelper):
@@ -307,7 +314,8 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
tableItems = [
qt.QTableWidgetItem(), # Legend
qt.QTableWidgetItem(), # Kind
- qt.QTableWidgetItem()] # roi
+ qt.QTableWidgetItem(),
+ ] # roi
for column in range(3, self.columnCount()):
header = self.horizontalHeaderItem(column)
@@ -334,8 +342,7 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
row = self.rowCount() - 1
for column, tableItem in enumerate(tableItems):
tableItem.setData(qt.Qt.UserRole, _Container(item))
- tableItem.setFlags(
- qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
+ tableItem.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
self.setItem(row, column, tableItem)
# Update table items content
@@ -344,8 +351,9 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
# Listen for item changes
# Using queued connection to avoid issue with sender
# being that of the signal calling the signal
- item._plot_item.sigItemChanged.connect(self._plotItemChanged,
- qt.Qt.QueuedConnection)
+ item._plot_item.sigItemChanged.connect(
+ self._plotItemChanged, qt.Qt.QueuedConnection
+ )
return True
def _removeAllItems(self):
@@ -369,7 +377,9 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
_StatsWidgetBase.setStats(self, statsHandler)
self.setRowCount(0)
- self.setColumnCount(len(self._statsHandler.stats) + 3) # + legend, kind and roi # noqa
+ self.setColumnCount(
+ len(self._statsHandler.stats) + 3
+ ) # + legend, kind and roi # noqa
for index, stat in enumerate(self._statsHandler.stats.values()):
headerItem = qt.QTableWidgetItem(stat.name.capitalize())
@@ -407,10 +417,14 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
statsHandler = self.getStatsHandler()
if statsHandler is not None:
- stats = statsHandler.calculate(plotItem, plot,
- onlimits=self._statsOnVisibleData,
- roi=roi, data_changed=data_changed,
- roi_changed=roi_changed)
+ stats = statsHandler.calculate(
+ plotItem,
+ plot,
+ onlimits=self._statsOnVisibleData,
+ roi=roi,
+ data_changed=data_changed,
+ roi_changed=roi_changed,
+ )
else:
stats = {}
@@ -428,7 +442,7 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
value = stats.get(name)
if value is None:
_logger.error("Value not found for: %s", name)
- tableItem.setText('-')
+ tableItem.setText("-")
else:
tableItem.setText(str(value))
@@ -473,9 +487,9 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
:param item: The plot item
:return: An ordered dict of column name to QTableWidgetItem mapping
for the given plot item.
- :rtype: OrderedDict
+ :rtype: dict
"""
- result = OrderedDict()
+ result = {}
row = self._itemToRow(item)
if row is not None:
for column in range(self.columnCount()):
@@ -519,15 +533,21 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
# item connection within sigRegionChanged should only be
# stopped during the region edition
self.__region_edition_callback[item._roi] = functools.partial(
- self._updateAllStats, False, True)
- item._roi.sigRegionChanged.connect(self.__region_edition_callback[item._roi])
- item._roi.sigEditingStarted.connect(functools.partial(
- self._startFiltering, item._roi))
- item._roi.sigEditingFinished.connect(functools.partial(
- self._endFiltering, item._roi))
+ self._updateAllStats, False, True
+ )
+ item._roi.sigRegionChanged.connect(
+ self.__region_edition_callback[item._roi]
+ )
+ item._roi.sigEditingStarted.connect(
+ functools.partial(self._startFiltering, item._roi)
+ )
+ item._roi.sigEditingFinished.connect(
+ functools.partial(self._endFiltering, item._roi)
+ )
else:
- item._roi.sigChanged.connect(functools.partial(
- self._updateAllStats, False, True))
+ item._roi.sigChanged.connect(
+ functools.partial(self._updateAllStats, False, True)
+ )
self.__roiToItems[item._roi].add(item)
def _startFiltering(self, roi):
@@ -541,10 +561,12 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
if roi in self.__roiToItems:
del self.__roiToItems[roi]
if isinstance(roi, RegionOfInterest):
- roi.sigRegionEditionStarted.disconnect(functools.partial(
- self._startFiltering, roi))
- roi.sigRegionEditionFinished.disconnect(functools.partial(
- self._startFiltering, roi))
+ roi.sigRegionEditionStarted.disconnect(
+ functools.partial(self._startFiltering, roi)
+ )
+ roi.sigRegionEditionFinished.disconnect(
+ functools.partial(self._startFiltering, roi)
+ )
try:
roi.sigRegionChanged.disconnect(self._updateAllStats)
except:
@@ -575,11 +597,13 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
self.setRowHidden(row_index, not item.isVisible())
def _removeItem(self, itemKey):
- if isinstance(itemKey, (silx.gui.plot.items.marker.Marker,
- silx.gui.plot.items.shape.Shape)):
+ if isinstance(
+ itemKey,
+ (silx.gui.plot.items.marker.Marker, silx.gui.plot.items.shape.Shape),
+ ):
return
if itemKey not in self._items:
- _logger.warning('key not recognized. Won\'t remove any item')
+ _logger.warning("key not recognized. Won't remove any item")
return
item = self._items[itemKey]
row = self._itemToRow(item)
@@ -597,16 +621,20 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
:param bool is_request: True if come from a manual request
"""
- if (self.getUpdateMode() is UpdateMode.MANUAL and
- not is_request and not roi_changed):
+ if (
+ self.getUpdateMode() is UpdateMode.MANUAL
+ and not is_request
+ and not roi_changed
+ ):
return
with self._disableSorting():
for row in range(self.rowCount()):
tableItem = self.item(row, 0)
item = self._tableItemToItem(tableItem)
- self._updateStats(item, roi_changed=roi_changed,
- data_changed=is_request)
+ self._updateStats(
+ item, roi_changed=roi_changed, data_changed=is_request
+ )
def _plotCurrentChanged(self, *args):
pass
@@ -624,7 +652,10 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
"""return the plotItem fitting the requirement kind, legend.
This information is enough to be sure it is unique (in the widget)"""
for plotItem in self.__plotItemToItems:
- if legend == plotItem.getLegend() and self._plotWrapper.getKind(plotItem) == kind:
+ if (
+ legend == plotItem.getLegend()
+ and self._plotWrapper.getKind(plotItem) == kind
+ ):
return plotItem
return None
@@ -668,12 +699,12 @@ class ROIStatsWidget(qt.QMainWindow):
qt.QMainWindow.__init__(self, parent)
toolbar = qt.QToolBar(self)
- icon = icons.getQIcon('add')
+ icon = icons.getQIcon("add")
self._rois = list(rois) if rois is not None else []
- self._addAction = qt.QAction(icon, 'add item/roi', toolbar)
+ self._addAction = qt.QAction(icon, "add item/roi", toolbar)
self._addAction.triggered.connect(self._addRoiStatsItem)
- icon = icons.getQIcon('rm')
- self._removeAction = qt.QAction(icon, 'remove item/roi', toolbar)
+ icon = icons.getQIcon("rm")
+ self._removeAction = qt.QAction(icon, "remove item/roi", toolbar)
self._removeAction.triggered.connect(self._removeCurrentRow)
toolbar.addAction(self._addAction)
@@ -717,15 +748,14 @@ class ROIStatsWidget(qt.QMainWindow):
@docstring(_StatsROITable)
def getStatsHandler(self):
"""
-
- :return:
+
+ :return:
"""
return self._statsROITable.getStatsHandler()
def _addRoiStatsItem(self):
"""Ask the user what couple ROI / item he want to display"""
- dialog = _GetROIItemCoupleDialog(parent=self, plot=self._plot,
- rois=self._rois)
+ dialog = _GetROIItemCoupleDialog(parent=self, plot=self._plot, rois=self._rois)
if dialog.exec():
self.addItem(roi=dialog.getROI(), plotItem=dialog.getItem())
@@ -755,7 +785,7 @@ class ROIStatsWidget(qt.QMainWindow):
def _removeCurrentRow(self):
def is1DKind(kind):
- if kind in ('curve', 'histogram', 'scatter'):
+ if kind in ("curve", "histogram", "scatter"):
return True
else:
return False
@@ -768,12 +798,10 @@ class ROIStatsWidget(qt.QMainWindow):
roi_kind = ROI if is1DKind(item_kind) else RegionOfInterest
roi = self._statsROITable._getRoi(kind=roi_kind, name=roi_name)
if roi is None:
- _logger.warning('failed to retrieve the roi you want to remove')
+ _logger.warning("failed to retrieve the roi you want to remove")
return False
- plot_item = self._statsROITable._getPlotItem(kind=item_kind,
- legend=item_legend)
+ plot_item = self._statsROITable._getPlotItem(kind=item_kind, legend=item_legend)
if plot_item is None:
- _logger.warning('failed to retrieve the plot item you want to'
- 'remove')
+ _logger.warning("failed to retrieve the plot item you want to" "remove")
return False
return self.removeItem(plotItem=plot_item, roi=roi)
diff --git a/src/silx/gui/plot/ScatterMaskToolsWidget.py b/src/silx/gui/plot/ScatterMaskToolsWidget.py
index 5c82fcf..300f3a6 100644
--- a/src/silx/gui/plot/ScatterMaskToolsWidget.py
+++ b/src/silx/gui/plot/ScatterMaskToolsWidget.py
@@ -54,8 +54,8 @@ _logger = logging.getLogger(__name__)
class ScatterMask(BaseMask):
- """A 1D mask for scatter data.
- """
+ """A 1D mask for scatter data."""
+
def __init__(self, scatter=None):
"""
@@ -76,7 +76,7 @@ class ScatterMask(BaseMask):
return self._dataItem.getValueData(copy=False)
def save(self, filename, kind):
- if kind == 'npy':
+ if kind == "npy":
try:
numpy.save(filename, self.getMask(copy=False))
except IOError:
@@ -116,8 +116,9 @@ class ScatterMask(BaseMask):
x, y = self._getXY()
# TODO: this could be optimized if necessary
- indices_in_polygon = [idx for idx in range(len(x)) if
- polygon.is_inside(y[idx], x[idx])]
+ indices_in_polygon = [
+ idx for idx in range(len(x)) if polygon.is_inside(y[idx], x[idx])
+ ]
self.updatePoints(level, indices_in_polygon, mask)
@@ -131,10 +132,7 @@ class ScatterMask(BaseMask):
:param float width:
:param bool mask: True to mask (default), False to unmask.
"""
- vertices = [(y, x),
- (y + height, x),
- (y + height, x + width),
- (y, x + width)]
+ vertices = [(y, x), (y + height, x), (y + height, x + width), (y, x + width)]
self.updatePolygon(level, vertices, mask)
def updateDisk(self, level, cy, cx, radius, mask=True):
@@ -147,7 +145,7 @@ class ScatterMask(BaseMask):
:param bool mask: True to mask (default), False to unmask.
"""
x, y = self._getXY()
- stencil = (y - cy)**2 + (x - cx)**2 < radius**2
+ stencil = (y - cy) ** 2 + (x - cx) ** 2 < radius**2
self.updateStencil(level, stencil, mask)
def updateEllipse(self, level, crow, ccol, radius_r, radius_c, mask=True):
@@ -160,8 +158,12 @@ class ScatterMask(BaseMask):
:param float radius_c: Radius of the ellipse in the column
:param bool mask: True to mask (default), False to unmask.
"""
+
def is_inside(px, py):
- return (px - ccol)**2 / radius_c**2 + (py - crow)**2 / radius_r**2 <= 1.0
+ return (px - ccol) ** 2 / radius_c**2 + (
+ py - crow
+ ) ** 2 / radius_r**2 <= 1.0
+
x, y = self._getXY()
indices_inside = [idx for idx in range(len(x)) if is_inside(x[idx], y[idx])]
self.updatePoints(level, indices_inside, mask)
@@ -180,13 +182,15 @@ class ScatterMask(BaseMask):
"""
# theta is the angle between the horizontal and the line
theta = math.atan((y1 - y0) / (x1 - x0)) if x1 - x0 else 0
- w_over_2_sin_theta = width / 2. * math.sin(theta)
- w_over_2_cos_theta = width / 2. * math.cos(theta)
-
- vertices = [(y0 - w_over_2_cos_theta, x0 + w_over_2_sin_theta),
- (y0 + w_over_2_cos_theta, x0 - w_over_2_sin_theta),
- (y1 + w_over_2_cos_theta, x1 - w_over_2_sin_theta),
- (y1 - w_over_2_cos_theta, x1 + w_over_2_sin_theta)]
+ w_over_2_sin_theta = width / 2.0 * math.sin(theta)
+ w_over_2_cos_theta = width / 2.0 * math.cos(theta)
+
+ vertices = [
+ (y0 - w_over_2_cos_theta, x0 + w_over_2_sin_theta),
+ (y0 + w_over_2_cos_theta, x0 - w_over_2_sin_theta),
+ (y1 + w_over_2_cos_theta, x1 - w_over_2_sin_theta),
+ (y1 - w_over_2_cos_theta, x1 + w_over_2_sin_theta),
+ ]
self.updatePolygon(level, vertices, mask)
@@ -196,8 +200,7 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
:class:`PlotWidget`."""
def __init__(self, parent=None, plot=None):
- super(ScatterMaskToolsWidget, self).__init__(parent, plot,
- mask=ScatterMask())
+ super(ScatterMaskToolsWidget, self).__init__(parent, plot, mask=ScatterMask())
self._z = 2 # Mask layer in plot
self._data_scatter = None
"""plot Scatter item for data"""
@@ -223,7 +226,7 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
"""
if self._data_scatter is None:
# this can happen if the mask tools widget has never been shown
- self._data_scatter = self.plot._getActiveItem(kind="scatter")
+ self._data_scatter = self.plot.getActiveScatter()
if self._data_scatter is None:
return None
self._adjustColorAndBrushSize(self._data_scatter)
@@ -234,8 +237,10 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
mask = numpy.array(mask, copy=False, dtype=numpy.uint8)
- if self._data_scatter.getXData(copy=False).shape == (0,) \
- or mask.shape == self._data_scatter.getXData(copy=False).shape:
+ if (
+ self._data_scatter.getXData(copy=False).shape == (0,)
+ or mask.shape == self._data_scatter.getXData(copy=False).shape
+ ):
self._mask.setMask(mask, copy=copy)
self._mask.commit()
return mask.shape
@@ -248,25 +253,28 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
"""Update mask image in plot"""
mask = self.getSelectionMask(copy=False)
if mask is not None:
- self.plot.addScatter(self._data_scatter.getXData(),
- self._data_scatter.getYData(),
- mask,
- legend=self._maskName,
- colormap=self._colormap,
- z=self._z)
- self._mask_scatter = self.plot._getItem(kind="scatter",
- legend=self._maskName)
- self._mask_scatter.setSymbolSize(
- self._data_scatter.getSymbolSize() + 2.0)
+ self.plot.addScatter(
+ self._data_scatter.getXData(),
+ self._data_scatter.getYData(),
+ mask,
+ legend=self._maskName,
+ colormap=self._colormap,
+ z=self._z,
+ )
+ self._mask_scatter = self.plot._getItem(
+ kind="scatter", legend=self._maskName
+ )
+ self._mask_scatter.setSymbolSize(self._data_scatter.getSymbolSize() + 2.0)
self._mask_scatter.sigItemChanged.connect(self.__maskScatterChanged)
- elif self.plot._getItem(kind="scatter",
- legend=self._maskName) is not None:
- self.plot.remove(self._maskName, kind='scatter')
+ elif self.plot._getItem(kind="scatter", legend=self._maskName) is not None:
+ self.plot.remove(self._maskName, kind="scatter")
def __maskScatterChanged(self, event):
"""Handles update of mask scatter"""
- if (event is ItemChangedType.VISUALIZATION_MODE and
- self._mask_scatter is not None):
+ if (
+ event is ItemChangedType.VISUALIZATION_MODE
+ and self._mask_scatter is not None
+ ):
self._mask_scatter.setVisualization(Scatter.Visualization.POINTS)
# track widget visibility and plot active image changes
@@ -274,10 +282,11 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
def showEvent(self, event):
try:
self.plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChangedAfterCare)
+ self._activeScatterChangedAfterCare
+ )
except (RuntimeError, TypeError):
pass
- self._activeScatterChanged(None, None) # Init mask + enable/disable widget
+ self._activeScatterChanged(None, None) # Init mask + enable/disable widget
self.plot.sigActiveScatterChanged.connect(self._activeScatterChanged)
def hideEvent(self, event):
@@ -294,14 +303,16 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
if self.getSelectionMask(copy=False) is not None:
self.plot.sigActiveScatterChanged.connect(
- self._activeScatterChangedAfterCare)
+ self._activeScatterChangedAfterCare
+ )
def _adjustColorAndBrushSize(self, activeScatter):
colormap = activeScatter.getColormap()
- self._defaultOverlayColor = rgba(cursorColorForColormap(colormap['name']))
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
+ self._defaultOverlayColor = rgba(cursorColorForColormap(colormap["name"]))
+ self._setMaskColors(
+ self.levelSpinBox.value(),
+ self.transparencySlider.value() / self.transparencySlider.maximum(),
+ )
self._z = activeScatter.getZValue() + 1
self._data_scatter = activeScatter
@@ -323,25 +334,30 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
removed, otherwise it is adjusted to z.
"""
# check that content changed was the active scatter
- activeScatter = self.plot._getActiveItem(kind="scatter")
+ activeScatter = self.plot.getActiveScatter()
if activeScatter is None or activeScatter.getName() == self._maskName:
# No active scatter or active scatter is the mask...
self.plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChangedAfterCare)
+ self._activeScatterChangedAfterCare
+ )
self._data_extent = None
self._data_scatter = None
else:
self._adjustColorAndBrushSize(activeScatter)
- if self._data_scatter.getXData(copy=False).shape != self._mask.getMask(copy=False).shape:
+ if (
+ self._data_scatter.getXData(copy=False).shape
+ != self._mask.getMask(copy=False).shape
+ ):
# scatter has not the same size, remove mask and stop listening
if self.plot._getItem(kind="scatter", legend=self._maskName):
- self.plot.remove(self._maskName, kind='scatter')
+ self.plot.remove(self._maskName, kind="scatter")
self.plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChangedAfterCare)
+ self._activeScatterChangedAfterCare
+ )
self._data_extent = None
self._data_scatter = None
@@ -352,7 +368,7 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
def _activeScatterChanged(self, previous, next):
"""Update widget and mask according to active scatter changes"""
- activeScatter = self.plot._getActiveItem(kind="scatter")
+ activeScatter = self.plot.getActiveScatter()
if activeScatter is None or activeScatter.getName() == self._maskName:
# No active scatter or active scatter is the mask...
@@ -368,7 +384,10 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
self._adjustColorAndBrushSize(activeScatter)
self._mask.setDataItem(self._data_scatter)
- if self._data_scatter.getXData(copy=False).shape != self._mask.getMask(copy=False).shape:
+ if (
+ self._data_scatter.getXData(copy=False).shape
+ != self._mask.getMask(copy=False).shape
+ ):
self._mask.reset(self._data_scatter.getXData(copy=False).shape)
self._mask.commit()
else:
@@ -395,16 +414,14 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
except IOError:
_logger.error("Can't load filename '%s'", filename)
_logger.debug("Backtrace", exc_info=True)
- raise RuntimeError('File "%s" is not a numpy file.',
- filename)
+ raise RuntimeError('File "%s" is not a numpy file.', filename)
elif extension in ["txt", "csv"]:
try:
mask = numpy.loadtxt(filename)
except IOError:
_logger.error("Can't load filename '%s'", filename)
_logger.debug("Backtrace", exc_info=True)
- raise RuntimeError('File "%s" is not a numpy txt file.',
- filename)
+ raise RuntimeError('File "%s" is not a numpy txt file.', filename)
else:
msg = "Extension '%s' is not supported."
raise RuntimeError(msg % extension)
@@ -417,8 +434,8 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
dialog.setWindowTitle("Load Mask")
dialog.setModal(1)
filters = [
- 'NumPy binary file (*.npy)',
- 'CSV text file (*.csv)',
+ "NumPy binary file (*.npy)",
+ "CSV text file (*.csv)",
]
dialog.setNameFilters(filters)
dialog.setFileMode(qt.QFileDialog.ExistingFile)
@@ -454,8 +471,8 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
dialog.setWindowTitle("Save Mask")
dialog.setModal(1)
filters = [
- 'NumPy binary file (*.npy)',
- 'CSV text file (*.csv)',
+ "NumPy binary file (*.npy)",
+ "CSV text file (*.csv)",
]
dialog.setNameFilters(filters)
dialog.setFileMode(qt.QFileDialog.AnyFile)
@@ -485,8 +502,7 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
strerror = e.strerror
else:
strerror = sys.exc_info()[1]
- msg.setText("Cannot save.\n"
- "Input Output Error: %s" % strerror)
+ msg.setText("Cannot save.\n" "Input Output Error: %s" % strerror)
msg.exec()
return
@@ -509,8 +525,7 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
def resetSelectionMask(self):
"""Reset the mask"""
- self._mask.reset(
- shape=self._data_scatter.getXData(copy=False).shape)
+ self._mask.reset(shape=self._data_scatter.getXData(copy=False).shape)
self._mask.commit()
def _getPencilWidth(self):
@@ -525,8 +540,10 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
def _plotDrawEvent(self, event):
"""Handle draw events from the plot"""
- if (self._drawingMode is None or
- event['event'] not in ('drawingProgress', 'drawingFinished')):
+ if self._drawingMode is None or event["event"] not in (
+ "drawingProgress",
+ "drawingFinished",
+ ):
return
if not len(self._data_scatter.getXData(copy=False)):
@@ -534,40 +551,42 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
level = self.levelSpinBox.value()
- if self._drawingMode == 'rectangle':
- if event['event'] == 'drawingFinished':
+ if self._drawingMode == "rectangle":
+ if event["event"] == "drawingFinished":
doMask = self._isMasking()
self._mask.updateRectangle(
level,
- y=event['y'],
- x=event['x'],
- height=abs(event['height']),
- width=abs(event['width']),
- mask=doMask)
+ y=event["y"],
+ x=event["x"],
+ height=abs(event["height"]),
+ width=abs(event["width"]),
+ mask=doMask,
+ )
self._mask.commit()
- elif self._drawingMode == 'ellipse':
- if event['event'] == 'drawingFinished':
+ elif self._drawingMode == "ellipse":
+ if event["event"] == "drawingFinished":
doMask = self._isMasking()
- center = event['points'][0]
- size = event['points'][1]
- self._mask.updateEllipse(level, center[1], center[0],
- size[1], size[0], doMask)
+ center = event["points"][0]
+ size = event["points"][1]
+ self._mask.updateEllipse(
+ level, center[1], center[0], size[1], size[0], doMask
+ )
self._mask.commit()
- elif self._drawingMode == 'polygon':
- if event['event'] == 'drawingFinished':
+ elif self._drawingMode == "polygon":
+ if event["event"] == "drawingFinished":
doMask = self._isMasking()
- vertices = event['points']
+ vertices = event["points"]
vertices = vertices[:, (1, 0)] # (y, x)
self._mask.updatePolygon(level, vertices, doMask)
self._mask.commit()
- elif self._drawingMode == 'pencil':
+ elif self._drawingMode == "pencil":
doMask = self._isMasking()
# convert from plot to array coords
- x, y = event['points'][-1]
+ x, y = event["points"][-1]
brushSize = self._getPencilWidth()
@@ -576,15 +595,18 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
# Draw the line
self._mask.updateLine(
level,
- self._lastPencilPos[0], self._lastPencilPos[1],
- y, x,
+ self._lastPencilPos[0],
+ self._lastPencilPos[1],
+ y,
+ x,
brushSize,
- doMask)
+ doMask,
+ )
# Draw the very first, or last point
- self._mask.updateDisk(level, y, x, brushSize / 2., doMask)
+ self._mask.updateDisk(level, y, x, brushSize / 2.0, doMask)
- if event['event'] == 'drawingFinished':
+ if event["event"] == "drawingFinished":
self._mask.commit()
self._lastPencilPos = None
else:
@@ -597,11 +619,11 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
if self._data_scatter is not None:
# Update thresholds according to colormap
colormap = self._data_scatter.getColormap()
- if colormap['autoscale']:
+ if colormap["autoscale"]:
min_ = numpy.nanmin(self._data_scatter.getValueData(copy=False))
max_ = numpy.nanmax(self._data_scatter.getValueData(copy=False))
else:
- min_, max_ = colormap['vmin'], colormap['vmax']
+ min_, max_ = colormap["vmin"], colormap["vmax"]
self.minLineEdit.setText(str(min_))
self.maxLineEdit.setText(str(max_))
@@ -615,6 +637,7 @@ class ScatterMaskToolsDockWidget(BaseMaskToolsDockWidget):
:param plot: The PlotWidget this widget is operating on
:paran str name: The title of this widget
"""
- def __init__(self, parent=None, plot=None, name='Mask'):
+
+ def __init__(self, parent=None, plot=None, name="Mask"):
widget = ScatterMaskToolsWidget(plot=plot)
super(ScatterMaskToolsDockWidget, self).__init__(parent, name, widget)
diff --git a/src/silx/gui/plot/ScatterView.py b/src/silx/gui/plot/ScatterView.py
index abacbef..06475e3 100644
--- a/src/silx/gui/plot/ScatterView.py
+++ b/src/silx/gui/plot/ScatterView.py
@@ -63,7 +63,7 @@ class ScatterView(qt.QMainWindow):
:type backend: Union[str,~silx.gui.plot.backends.BackendBase.BackendBase]
"""
- _SCATTER_LEGEND = ' '
+ _SCATTER_LEGEND = " "
"""Legend used for the scatter item"""
def __init__(self, parent=None, backend=None):
@@ -72,7 +72,7 @@ class ScatterView(qt.QMainWindow):
# behave as a widget
self.setWindowFlags(qt.Qt.Widget)
else:
- self.setWindowTitle('ScatterView')
+ self.setWindowTitle("ScatterView")
# Create plot widget
plot = PlotWidget(parent=self, backend=backend)
@@ -93,10 +93,13 @@ class ScatterView(qt.QMainWindow):
self.__pickingCache = None
self._positionInfo = tools.PositionInfo(
plot=plot,
- converters=(('X', WeakMethodProxy(self._getPickedX)),
- ('Y', WeakMethodProxy(self._getPickedY)),
- ('Data', WeakMethodProxy(self._getPickedValue)),
- ('Index', WeakMethodProxy(self._getPickedIndex))))
+ converters=(
+ ("X", WeakMethodProxy(self._getPickedX)),
+ ("Y", WeakMethodProxy(self._getPickedY)),
+ ("Data", WeakMethodProxy(self._getPickedValue)),
+ ("Index", WeakMethodProxy(self._getPickedIndex)),
+ ),
+ )
# Combine plot, position info and colorbar into central widget
gridLayout = qt.QGridLayout()
@@ -114,23 +117,25 @@ class ScatterView(qt.QMainWindow):
# Create mask tool dock widget
self._maskToolsWidget = ScatterMaskToolsWidget(parent=self, plot=plot)
self._maskDock = BoxLayoutDockWidget()
- self._maskDock.setWindowTitle('Scatter Mask')
+ self._maskDock.setWindowTitle("Scatter Mask")
self._maskDock.setWidget(self._maskToolsWidget)
self._maskDock.setVisible(False)
self.addDockWidget(qt.Qt.BottomDockWidgetArea, self._maskDock)
self._maskAction = self._maskDock.toggleViewAction()
- self._maskAction.setIcon(icons.getQIcon('image-mask'))
+ self._maskAction.setIcon(icons.getQIcon("image-mask"))
self._maskAction.setToolTip("Display/hide mask tools")
- self._intensityHistoAction = actions_histogram.PixelIntensitiesHistoAction(plot=plot, parent=self)
+ self._intensityHistoAction = actions_histogram.PixelIntensitiesHistoAction(
+ plot=plot, parent=self
+ )
# Create toolbars
self._interactiveModeToolBar = tools.InteractiveModeToolBar(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
- self._scatterToolBar = tools.ScatterToolBar(
- parent=self, plot=plot)
+ self._scatterToolBar = tools.ScatterToolBar(parent=self, plot=plot)
self._scatterToolBar.addAction(self._maskAction)
self._scatterToolBar.addAction(self._intensityHistoAction)
@@ -139,15 +144,16 @@ class ScatterView(qt.QMainWindow):
self._outputToolBar = tools.OutputToolBar(parent=self, plot=plot)
# Activate shortcuts in PlotWindow widget:
- for toolbar in (self._interactiveModeToolBar,
- self._scatterToolBar,
- self._profileToolBar,
- self._outputToolBar):
+ for toolbar in (
+ self._interactiveModeToolBar,
+ self._scatterToolBar,
+ self._profileToolBar,
+ self._outputToolBar,
+ ):
self.addToolBar(toolbar)
for action in toolbar.actions():
self.addAction(action)
-
def __createEmptyScatter(self):
"""Create an empty scatter item that is used to display the data
@@ -155,8 +161,7 @@ class ScatterView(qt.QMainWindow):
"""
plot = self.getPlotWidget()
plot.addScatter(x=(), y=(), value=(), legend=self._SCATTER_LEGEND)
- scatter = plot._getItem(
- kind='scatter', legend=self._SCATTER_LEGEND)
+ scatter = plot._getItem(kind="scatter", legend=self._SCATTER_LEGEND)
# Profile is not selectable,
# so it does not interfere with profile interaction
scatter._setSelectable(False)
@@ -180,16 +185,24 @@ class ScatterView(qt.QMainWindow):
if pixelPos is not None:
# Start from top-most item
result = plot._pickTopMost(
- pixelPos[0], pixelPos[1],
- lambda item: isinstance(item, items.Scatter))
+ pixelPos[0],
+ pixelPos[1],
+ lambda item: isinstance(item, items.Scatter),
+ )
if result is not None:
item = result.getItem()
- if item.getVisualization() is items.Scatter.Visualization.BINNED_STATISTIC:
+ if (
+ item.getVisualization()
+ is items.Scatter.Visualization.BINNED_STATISTIC
+ ):
# Get highest index of closest points
selected = result.getIndices(copy=False)[::-1]
- dataIndex = selected[numpy.argmin(
- (item.getXData(copy=False)[selected] - x)**2 +
- (item.getYData(copy=False)[selected] - y)**2)]
+ dataIndex = selected[
+ numpy.argmin(
+ (item.getXData(copy=False)[selected] - x) ** 2
+ + (item.getYData(copy=False)[selected] - y) ** 2
+ )
+ ]
else:
# Get last index
# with matplotlib it should be the top-most point
@@ -198,7 +211,8 @@ class ScatterView(qt.QMainWindow):
dataIndex,
item.getXData(copy=False)[dataIndex],
item.getYData(copy=False)[dataIndex],
- item.getValueData(copy=False)[dataIndex])
+ item.getValueData(copy=False)[dataIndex],
+ )
return self.__pickingCache
@@ -210,7 +224,7 @@ class ScatterView(qt.QMainWindow):
:return: The data index at that point or '-'
"""
picking = self._pickScatterData(x, y)
- return '-' if picking is None else picking[0]
+ return "-" if picking is None else picking[0]
def _getPickedX(self, x, y):
"""Returns X position snapped to scatter plot when close enough
@@ -240,7 +254,7 @@ class ScatterView(qt.QMainWindow):
:return: The data value at that point or '-'
"""
picking = self._pickScatterData(x, y)
- return '-' if picking is None else picking[3]
+ return "-" if picking is None else picking[3]
def _mouseInPlotArea(self, x, y):
"""Clip mouse coordinates to plot area coordinates
@@ -344,7 +358,7 @@ class ScatterView(qt.QMainWindow):
:param yerror: Values with the uncertainties on the y values
:type yerror: A float, or a numpy.ndarray of float32. See xerror.
:param alpha: Values with the transparency (between 0 and 1)
- :type alpha: A float, or a numpy.ndarray of float32
+ :type alpha: A float, or a numpy.ndarray of float32
:param bool copy: True make a copy of the data (default),
False to use provided arrays.
"""
@@ -353,7 +367,8 @@ class ScatterView(qt.QMainWindow):
value = () if value is None else value
self.getScatterItem().setData(
- x=x, y=y, value=value, xerror=xerror, yerror=yerror, alpha=alpha, copy=copy)
+ x=x, y=y, value=value, xerror=xerror, yerror=yerror, alpha=alpha, copy=copy
+ )
@docstring(items.Scatter)
def getData(self, *args, **kwargs):
@@ -367,7 +382,7 @@ class ScatterView(qt.QMainWindow):
:rtype: ~silx.gui.plot.items.Scatter
"""
plot = self.getPlotWidget()
- scatter = plot._getItem(kind='scatter', legend=self._SCATTER_LEGEND)
+ scatter = plot._getItem(kind="scatter", legend=self._SCATTER_LEGEND)
if scatter is None: # Resilient to call to PlotWidget API (e.g., clear)
scatter = self.__createEmptyScatter()
return scatter
diff --git a/src/silx/gui/plot/StackView.py b/src/silx/gui/plot/StackView.py
index 5101f87..36560fd 100644
--- a/src/silx/gui/plot/StackView.py
+++ b/src/silx/gui/plot/StackView.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2016-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -56,7 +56,7 @@ Example::
sv = StackViewMainWindow()
- sv.setColormap("jet", autoscale=True)
+ sv.setColormap("viridis", vmin=-4, vmax=4)
sv.setStack(mystack)
sv.setLabels(["1st dim (0-99)", "2nd dim (0-199)",
"3rd dim (0-299)"])
@@ -84,15 +84,11 @@ from .tools import LimitsToolBar
from .Profile import Profile3DToolBar
from ..widgets.FrameBrowser import HorizontalSliderWithBrowser
-from silx.gui.plot.actions import control as actions_control
from silx.gui.plot.actions import io as silx_io
from silx.io.nxdata import save_NXdata
from silx.utils.array_like import DatasetView, ListOfImages
from silx.math import calibration
-from silx.utils.deprecation import deprecated_warning
-from silx.utils.deprecation import deprecated
-import h5py
from silx.io.utils import is_dataset
_logger = logging.getLogger(__name__)
@@ -130,6 +126,7 @@ class StackView(qt.QMainWindow):
See :class:`silx.gui.plot.PlotTools.PositionInfo`.
:param bool mask: Toggle visibilty of mask action.
"""
+
# Qt signals
valueChanged = qt.Signal(object, object, object)
"""Signals that the data value under the cursor has changed.
@@ -163,20 +160,34 @@ class StackView(qt.QMainWindow):
This signal provides the current frame number.
"""
- IMAGE_STACK_FILTER_NXDATA = 'Stack of images as NXdata (%s)' % silx_io._NEXUS_HDF5_EXT_STR
-
+ IMAGE_STACK_FILTER_NXDATA = (
+ "Stack of images as NXdata (%s)" % silx_io._NEXUS_HDF5_EXT_STR
+ )
- def __init__(self, parent=None, resetzoom=True, backend=None,
- autoScale=False, logScale=False, grid=False,
- colormap=True, aspectRatio=True, yinverted=True,
- copy=True, save=True, print_=True, control=False,
- position=None, mask=True):
+ def __init__(
+ self,
+ parent=None,
+ resetzoom=True,
+ backend=None,
+ autoScale=False,
+ logScale=False,
+ grid=False,
+ colormap=True,
+ aspectRatio=True,
+ yinverted=True,
+ copy=True,
+ save=True,
+ print_=True,
+ control=False,
+ position=None,
+ mask=True,
+ ):
qt.QMainWindow.__init__(self, parent)
if parent is not None:
# behave as a widget
self.setWindowFlags(qt.Qt.Widget)
else:
- self.setWindowTitle('StackView')
+ self.setWindowTitle("StackView")
self._stack = None
"""Loaded stack, as a 3D array, a 3D dataset or a list of 2D arrays."""
@@ -188,14 +199,10 @@ class StackView(qt.QMainWindow):
self._stackItem = ImageStack()
"""Hold the item displaying the stack"""
- imageLegend = '__StackView__image' + str(id(self))
+ imageLegend = "__StackView__image" + str(id(self))
self._stackItem.setName(imageLegend)
- self.__autoscaleCmap = False
- """Flag to disable/enable colormap auto-scaling
- based on the min/max values of the entire 3D volume"""
- self.__dimensionsLabels = ["Dimension 0", "Dimension 1",
- "Dimension 2"]
+ self.__dimensionsLabels = ["Dimension 0", "Dimension 1", "Dimension 2"]
"""These labels are displayed on the X and Y axes.
:meth:`setLabels` updates this attribute."""
@@ -206,39 +213,56 @@ class StackView(qt.QMainWindow):
"""Function returning the plot title based on the frame index.
It can be set to a custom function using :meth:`setTitleCallback`"""
- self.calibrations3D = (calibration.NoCalibration(),
- calibration.NoCalibration(),
- calibration.NoCalibration())
+ self.calibrations3D = (
+ calibration.NoCalibration(),
+ calibration.NoCalibration(),
+ calibration.NoCalibration(),
+ )
central_widget = qt.QWidget(self)
- self._plot = PlotWindow(parent=central_widget, backend=backend,
- resetzoom=resetzoom, autoScale=autoScale,
- logScale=logScale, grid=grid,
- curveStyle=False, colormap=colormap,
- aspectRatio=aspectRatio, yInverted=yinverted,
- copy=copy, save=save, print_=print_,
- control=control, position=position,
- roi=False, mask=mask)
+ self._plot = PlotWindow(
+ parent=central_widget,
+ backend=backend,
+ resetzoom=resetzoom,
+ autoScale=autoScale,
+ logScale=logScale,
+ grid=grid,
+ curveStyle=False,
+ colormap=colormap,
+ aspectRatio=aspectRatio,
+ yInverted=yinverted,
+ copy=copy,
+ save=save,
+ print_=print_,
+ control=control,
+ position=position,
+ roi=False,
+ mask=mask,
+ )
self._plot.addItem(self._stackItem)
self._plot.getIntensityHistogramAction().setVisible(True)
self.sigInteractiveModeChanged = self._plot.sigInteractiveModeChanged
self.sigActiveImageChanged = self._plot.sigActiveImageChanged
self.sigPlotSignal = self._plot.sigPlotSignal
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
+ if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward":
self._plot.getYAxis().setInverted(True)
self._plot.getColorBarAction().setVisible(True)
self._plot.getColorBarWidget().setVisible(True)
- self._profileToolBar = Profile3DToolBar(parent=self._plot,
- stackview=self)
+ self._profileToolBar = Profile3DToolBar(parent=self._plot, stackview=self)
self._plot.addToolBar(self._profileToolBar)
- self._plot.getXAxis().setLabel('Columns')
- self._plot.getYAxis().setLabel('Rows')
+ self._plot.getXAxis().setLabel("Columns")
+ self._plot.getYAxis().setLabel("Rows")
self._plot.sigPlotSignal.connect(self._plotCallback)
- self._plot.getSaveAction().setFileFilter('image', self.IMAGE_STACK_FILTER_NXDATA, func=self._saveImageStack, appendToFile=True)
+ self._plot.getSaveAction().setFileFilter(
+ "image",
+ self.IMAGE_STACK_FILTER_NXDATA,
+ func=self._saveImageStack,
+ appendToFile=True,
+ )
self.__planeSelection = PlanesWidget(self._plot)
self.__planeSelection.sigPlaneSelectionChanged.connect(self.setPerspective)
@@ -262,7 +286,8 @@ class StackView(qt.QMainWindow):
# clear profile lines when the perspective changes (plane browsed changed)
self.__planeSelection.sigPlaneSelectionChanged.connect(
- self._profileToolBar.clearProfile)
+ self._profileToolBar.clearProfile
+ )
def _saveImageStack(self, plot, filename, nameFilter):
"""Save all images from the stack into a volume.
@@ -274,21 +299,25 @@ class StackView(qt.QMainWindow):
:raises: ValueError if nameFilter is invalid
"""
if not nameFilter == self.IMAGE_STACK_FILTER_NXDATA:
- raise ValueError('Wrong callback')
- entryPath = silx_io.SaveAction._selectWriteableOutputGroup(filename, parent=self)
+ raise ValueError("Wrong callback")
+ entryPath = silx_io.SaveAction._selectWriteableOutputGroup(
+ filename, parent=self
+ )
if entryPath is None:
return False
- return save_NXdata(filename,
- nxentry_name=entryPath,
- signal=self.getStack(copy=False, returnNumpyArray=True)[0],
- signal_name="image_stack")
+ return save_NXdata(
+ filename,
+ nxentry_name=entryPath,
+ signal=self.getStack(copy=False, returnNumpyArray=True)[0],
+ signal_name="image_stack",
+ )
def _plotCallback(self, eventDict):
"""Callback for plot events.
Emit :attr:`valueChanged` signal, with (x, y, value) tuple of the
cursor location in the plot."""
- if eventDict['event'] == 'mouseMoved':
+ if eventDict["event"] == "mouseMoved":
activeImage = self.getActiveImage()
if activeImage is not None:
data = activeImage.getData()
@@ -297,15 +326,13 @@ class StackView(qt.QMainWindow):
# Get corresponding coordinate in image
origin = activeImage.getOrigin()
scale = activeImage.getScale()
- x = int((eventDict['x'] - origin[0]) / scale[0])
- y = int((eventDict['y'] - origin[1]) / scale[1])
+ x = int((eventDict["x"] - origin[0]) / scale[0])
+ y = int((eventDict["y"] - origin[1]) / scale[1])
if 0 <= x < width and 0 <= y < height:
- self.valueChanged.emit(float(x), float(y),
- data[y][x])
+ self.valueChanged.emit(float(x), float(y), data[y][x])
else:
- self.valueChanged.emit(float(x), float(y),
- None)
+ self.valueChanged.emit(float(x), float(y), None)
def getPerspective(self):
"""Returns the index of the dimension the stack is browsed with
@@ -329,8 +356,7 @@ class StackView(qt.QMainWindow):
return
else:
if perspective > 2 or perspective < 0:
- raise ValueError(
- "Perspective must be 0, 1 or 2, not %s" % perspective)
+ raise ValueError("Perspective must be 0, 1 or 2, not %s" % perspective)
self._perspective = int(perspective)
self.__createTransposedView()
@@ -338,20 +364,29 @@ class StackView(qt.QMainWindow):
self._plot.resetZoom()
self.__updatePlotLabels()
self._updateTitle()
- self._browser_label.setText("Image index (Dim%d):" %
- (self._first_stack_dimension + perspective))
+ self._browser_label.setText(
+ "Image index (Dim%d):" % (self._first_stack_dimension + perspective)
+ )
self.sigPlaneSelectionChanged.emit(perspective)
- self.sigStackChanged.emit(self._stack.size if
- self._stack is not None else 0)
- self.__planeSelection.sigPlaneSelectionChanged.disconnect(self.setPerspective)
+ self.sigStackChanged.emit(
+ self._stack.size if self._stack is not None else 0
+ )
+ self.__planeSelection.sigPlaneSelectionChanged.disconnect(
+ self.setPerspective
+ )
self.__planeSelection.setPerspective(self._perspective)
self.__planeSelection.sigPlaneSelectionChanged.connect(self.setPerspective)
def __updatePlotLabels(self):
"""Update plot axes labels depending on perspective"""
- y, x = (1, 2) if self._perspective == 0 else \
- (0, 2) if self._perspective == 1 else (0, 1)
+ y, x = (
+ (1, 2)
+ if self._perspective == 0
+ else (0, 2)
+ if self._perspective == 1
+ else (0, 1)
+ )
self.setGraphXLabel(self.__dimensionsLabels[x])
self.setGraphYLabel(self.__dimensionsLabels[y])
@@ -409,9 +444,11 @@ class StackView(qt.QMainWindow):
See setStack for parameter documentation
"""
if calibrations is None:
- self.calibrations3D = (calibration.NoCalibration(),
- calibration.NoCalibration(),
- calibration.NoCalibration())
+ self.calibrations3D = (
+ calibration.NoCalibration(),
+ calibration.NoCalibration(),
+ calibration.NoCalibration(),
+ )
else:
self.calibrations3D = []
for i, calib in enumerate(calibrations):
@@ -420,17 +457,20 @@ class StackView(qt.QMainWindow):
elif calib is None:
calib = calibration.NoCalibration()
elif not isinstance(calib, calibration.AbstractCalibration):
- raise TypeError("calibration must be a 2-tuple, None or" +
- " an instance of an AbstractCalibration " +
- "subclass")
+ raise TypeError(
+ "calibration must be a 2-tuple, None or"
+ + " an instance of an AbstractCalibration "
+ + "subclass"
+ )
elif not calib.is_affine():
_logger.warning(
- "Calibration for dimension %d is not linear, "
- "it will be ignored for scaling the graph axes.",
- i)
+ "Calibration for dimension %d is not linear, "
+ "it will be ignored for scaling the graph axes.",
+ i,
+ )
self.calibrations3D.append(calib)
- def getCalibrations(self, order='array'):
+ def getCalibrations(self, order="array"):
"""Returns currently used calibrations for each axis
Returned calibrations might differ from the ones that were set as
@@ -442,7 +482,7 @@ class StackView(qt.QMainWindow):
:return: Calibrations ordered depending on order
:rtype: List[~silx.math.calibration.AbstractCalibration]
"""
- assert order in ('array', 'axes')
+ assert order in ("array", "axes")
calibs = []
# filter out non-linear calibration for graph axes
@@ -451,11 +491,13 @@ class StackView(qt.QMainWindow):
calib = calibration.NoCalibration()
calibs.append(calib)
- if order == 'axes': # Move 'z' axis to the end
+ if order == "axes": # Move 'z' axis to the end
xy_dims = [d for d in (0, 1, 2) if d != self._perspective]
- calibs = [calibs[max(xy_dims)],
- calibs[min(xy_dims)],
- calibs[self._perspective]]
+ calibs = [
+ calibs[max(xy_dims)],
+ calibs[min(xy_dims)],
+ calibs[self._perspective],
+ ]
return tuple(calibs)
@@ -463,14 +505,14 @@ class StackView(qt.QMainWindow):
"""
:return: 2-tuple (XScale, YScale) for current image view
"""
- xcalib, ycalib, _zcalib = self.getCalibrations(order='axes')
+ xcalib, ycalib, _zcalib = self.getCalibrations(order="axes")
return xcalib.get_slope(), ycalib.get_slope()
def _getImageOrigin(self):
"""
:return: 2-tuple (XOrigin, YOrigin) for current image view
"""
- xcalib, ycalib, _zcalib = self.getCalibrations(order='axes')
+ xcalib, ycalib, _zcalib = self.getCalibrations(order="axes")
return xcalib(0), ycalib(0)
def _getImageZ(self, index):
@@ -478,7 +520,7 @@ class StackView(qt.QMainWindow):
:param idx: 0-based image index in the stack
:return: calibrated Z value corresponding to the image idx
"""
- _xcalib, _ycalib, zcalib = self.getCalibrations(order='axes')
+ _xcalib, _ycalib, zcalib = self.getCalibrations(order="axes")
return zcalib(index)
def _updateTitle(self):
@@ -525,8 +567,8 @@ class StackView(qt.QMainWindow):
assert len(img.shape) == 2
except AssertionError:
raise ValueError(
- "Stack must be a 3D array/dataset or a list of " +
- "2D arrays.")
+ "Stack must be a 3D array/dataset or a list of " + "2D arrays."
+ )
stack = ListOfImages(stack)
assert len(stack.shape) == 3, "data must be 3D"
@@ -539,9 +581,6 @@ class StackView(qt.QMainWindow):
perspective_changed = True
self.setPerspective(perspective)
- if self.__autoscaleCmap:
- self.scaleColormapRangeToStack()
-
# init plot
self._stackItem.setStackData(self.__transposed_view, 0, copy=False)
self._stackItem.setColormap(self.getColormap())
@@ -554,7 +593,7 @@ class StackView(qt.QMainWindow):
if exists is None:
self._plot.addItem(self._stackItem)
- self._plot.setActiveImage(self._stackItem.getName())
+ self._plot.setActiveImage(self._stackItem)
self.__updatePlotLabels()
self._updateTitle()
@@ -564,7 +603,7 @@ class StackView(qt.QMainWindow):
# enable and init browser
self._browser.setEnabled(True)
- if not perspective_changed: # avoid double signal (see self.setPerspective)
+ if not perspective_changed: # avoid double signal (see self.setPerspective)
self.sigStackChanged.emit(stack.size)
def getStack(self, copy=True, returnNumpyArray=False):
@@ -590,15 +629,15 @@ class StackView(qt.QMainWindow):
colormap = image.getColormap()
params = {
- 'info': image.getInfo(),
- 'origin': image.getOrigin(),
- 'scale': image.getScale(),
- 'z': image.getZValue(),
- 'selectable': image.isSelectable(),
- 'draggable': image.isDraggable(),
- 'colormap': colormap,
- 'xlabel': image.getXLabel(),
- 'ylabel': image.getYLabel(),
+ "info": image.getInfo(),
+ "origin": image.getOrigin(),
+ "scale": image.getScale(),
+ "z": image.getZValue(),
+ "selectable": image.isSelectable(),
+ "draggable": image.isDraggable(),
+ "colormap": colormap,
+ "xlabel": image.getXLabel(),
+ "ylabel": image.getYLabel(),
}
if returnNumpyArray or copy:
return numpy.array(self._stack, copy=copy), params
@@ -641,15 +680,15 @@ class StackView(qt.QMainWindow):
colormap = None
params = {
- 'info': image.getInfo(),
- 'origin': image.getOrigin(),
- 'scale': image.getScale(),
- 'z': image.getZValue(),
- 'selectable': image.isSelectable(),
- 'draggable': image.isDraggable(),
- 'colormap': colormap,
- 'xlabel': image.getXLabel(),
- 'ylabel': image.getYLabel(),
+ "info": image.getInfo(),
+ "origin": image.getOrigin(),
+ "scale": image.getScale(),
+ "z": image.getZValue(),
+ "selectable": image.isSelectable(),
+ "draggable": image.isDraggable(),
+ "colormap": colormap,
+ "xlabel": image.getXLabel(),
+ "ylabel": image.getYLabel(),
}
if returnNumpyArray or copy:
return numpy.array(self.__transposed_view, copy=copy), params
@@ -718,8 +757,8 @@ class StackView(qt.QMainWindow):
def clear(self):
"""Clear the widget:
- - clear the plot
- - clear the loaded data volume
+ - clear the plot
+ - clear the loaded data volume
"""
self._stack = None
self.__transposed_view = None
@@ -742,9 +781,11 @@ class StackView(qt.QMainWindow):
of the data volumes.
"""
- default_labels = ["Dimension %d" % self._first_stack_dimension,
- "Dimension %d" % (self._first_stack_dimension + 1),
- "Dimension %d" % (self._first_stack_dimension + 2)]
+ default_labels = [
+ "Dimension %d" % self._first_stack_dimension,
+ "Dimension %d" % (self._first_stack_dimension + 1),
+ "Dimension %d" % (self._first_stack_dimension + 2),
+ ]
if labels is None:
new_labels = default_labels
else:
@@ -791,8 +832,9 @@ class StackView(qt.QMainWindow):
vmin, vmax = colormap.getColormapRange(data=stack[0])
colormap.setVRange(vmin=vmin, vmax=vmax)
- def setColormap(self, colormap=None, normalization=None,
- autoscale=None, vmin=None, vmax=None, colors=None):
+ def setColormap(
+ self, colormap=None, normalization=None, vmin=None, vmax=None, colors=None
+ ):
"""Set the colormap and update active image.
Parameters that are not provided are taken from the current colormap.
@@ -818,59 +860,33 @@ class StackView(qt.QMainWindow):
Or a :class`.Colormap` object.
:type colormap: dict or str.
:param str normalization: Colormap mapping: 'linear' or 'log'.
- :param bool autoscale: Whether to use autoscale or [vmin, vmax] range.
- Default value of autoscale is False. This option is not compatible
- with h5py datasets.
- :param float vmin: The minimum value of the range to use if
- 'autoscale' is False.
- :param float vmax: The maximum value of the range to use if
- 'autoscale' is False.
+ :param float vmin: The minimum value of the range to use.
+ :param float vmax: The maximum value of the range to use.
:param numpy.ndarray colors: Only used if name is None.
Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
"""
# if is a colormap object or a dictionary
if isinstance(colormap, Colormap) or isinstance(colormap, dict):
# Support colormap parameter as a dict
- errmsg = "If colormap is provided as a Colormap object, all other parameters"
+ errmsg = (
+ "If colormap is provided as a Colormap object, all other parameters"
+ )
errmsg += " must not be specified when calling setColormap"
assert normalization is None, errmsg
- assert autoscale is None, errmsg
assert vmin is None, errmsg
assert vmax is None, errmsg
assert colors is None, errmsg
- if isinstance(colormap, dict):
- reason = 'colormap parameter should now be an object'
- replacement = 'Colormap()'
- since_version = '0.6'
- deprecated_warning(type_='function',
- name='setColormap',
- reason=reason,
- replacement=replacement,
- since_version=since_version)
- _colormap = Colormap._fromDict(colormap)
- else:
- _colormap = colormap
+ _colormap = colormap
else:
- norm = normalization if normalization is not None else 'linear'
- name = colormap if colormap is not None else 'gray'
- _colormap = Colormap(name=name,
- normalization=norm,
- vmin=vmin,
- vmax=vmax,
- colors=colors)
-
- if autoscale is not None:
- deprecated_warning(
- type_='function',
- name='setColormap',
- reason='autoscale argument is replaced by a method',
- replacement='scaleColormapRangeToStack',
- since_version='0.14')
- self.__autoscaleCmap = bool(autoscale)
+ norm = normalization if normalization is not None else "linear"
+ name = colormap if colormap is not None else "gray"
+ _colormap = Colormap(
+ name=name, normalization=norm, vmin=vmin, vmax=vmax, colors=colors
+ )
cursorColor = cursorColorForColormap(_colormap.getName())
- self._plot.setInteractiveMode('zoom', color=cursorColor)
+ self._plot.setInteractiveMode("zoom", color=cursorColor)
self._plot.setDefaultColormap(_colormap)
@@ -879,16 +895,6 @@ class StackView(qt.QMainWindow):
if isinstance(activeImage, items.ColormapMixIn):
activeImage.setColormap(self.getColormap())
- if self.__autoscaleCmap:
- # scaleColormapRangeToStack needs to be called **after**
- # setDefaultColormap so getColormap returns the right colormap
- self.scaleColormapRangeToStack()
-
-
- @deprecated(replacement="getPlotWidget", since_version="0.13")
- def getPlot(self):
- return self.getPlotWidget()
-
def getPlotWidget(self):
"""Return the :class:`PlotWidget`.
@@ -912,13 +918,11 @@ class StackView(qt.QMainWindow):
# proxies to PlotWidget or PlotWindow methods
def getProfileToolbar(self):
- """Profile tools attached to this plot
- """
+ """Profile tools attached to this plot"""
return self._profileToolBar
def getGraphTitle(self):
- """Return the plot main title as a str.
- """
+ """Return the plot main title as a str."""
return self._plot.getGraphTitle()
def setGraphTitle(self, title=""):
@@ -929,8 +933,7 @@ class StackView(qt.QMainWindow):
return self._plot.setGraphTitle(title)
def getGraphXLabel(self):
- """Return the current horizontal axis label as a str.
- """
+ """Return the current horizontal axis label as a str."""
return self._plot.getXAxis().getLabel()
def setGraphXLabel(self, label=None):
@@ -942,14 +945,14 @@ class StackView(qt.QMainWindow):
label = self.__dimensionsLabels[1 if self._perspective == 2 else 2]
self._plot.getXAxis().setLabel(label)
- def getGraphYLabel(self, axis='left'):
+ def getGraphYLabel(self, axis="left"):
"""Return the current vertical axis label as a str.
:param str axis: The Y axis for which to get the label (left or right)
"""
return self._plot.getYAxis().getLabel(axis)
- def setGraphYLabel(self, label=None, axis='left'):
+ def setGraphYLabel(self, label=None, axis="left"):
"""Set the vertical axis label on the plot.
:param str label: The Y axis label
@@ -1033,8 +1036,7 @@ class StackView(qt.QMainWindow):
# kind of private methods, but needed by Profile
def getActiveImage(self, just_legend=False):
- """Returns the stack image object.
- """
+ """Returns the stack image object."""
if just_legend:
return self._stackItem.getName()
return self._stackItem
@@ -1049,8 +1051,7 @@ class StackView(qt.QMainWindow):
"""
return self._plot.getColorBarAction()
- def remove(self, legend=None,
- kind=('curve', 'image', 'item', 'marker')):
+ def remove(self, legend=None, kind=("curve", "image", "item", "marker")):
"""See :meth:`Plot.Plot.remove`"""
self._plot.remove(legend, kind)
@@ -1060,10 +1061,6 @@ class StackView(qt.QMainWindow):
"""
self._plot.setInteractiveMode(*args, **kwargs)
- @deprecated(replacement="addShape", since_version="0.13")
- def addItem(self, *args, **kwargs):
- self.addShape(*args, **kwargs)
-
def addShape(self, *args, **kwargs):
"""
See :meth:`Plot.Plot.addShape`
@@ -1076,6 +1073,7 @@ class PlanesWidget(qt.QWidget):
:param parent: the parent QWidget
"""
+
sigPlaneSelectionChanged = qt.Signal(int)
def __init__(self, parent):
@@ -1098,7 +1096,8 @@ class PlanesWidget(qt.QWidget):
self.qcbAxisSelection = qt.QComboBox(self)
self._setCBChoices(first_stack_dimension=0)
self.qcbAxisSelection.currentIndexChanged[int].connect(
- self.__planeSelectionChanged)
+ self.__planeSelectionChanged
+ )
layout0.addWidget(self.qcbAxisSelection)
@@ -1117,12 +1116,12 @@ class PlanesWidget(qt.QWidget):
def _setCBChoices(self, first_stack_dimension):
self.qcbAxisSelection.clear()
- dim1dim2 = 'Dim%d-Dim%d' % (first_stack_dimension + 1,
- first_stack_dimension + 2)
- dim0dim2 = 'Dim%d-Dim%d' % (first_stack_dimension,
- first_stack_dimension + 2)
- dim0dim1 = 'Dim%d-Dim%d' % (first_stack_dimension,
- first_stack_dimension + 1)
+ dim1dim2 = "Dim%d-Dim%d" % (
+ first_stack_dimension + 1,
+ first_stack_dimension + 2,
+ )
+ dim0dim2 = "Dim%d-Dim%d" % (first_stack_dimension, first_stack_dimension + 2)
+ dim0dim1 = "Dim%d-Dim%d" % (first_stack_dimension, first_stack_dimension + 1)
self.qcbAxisSelection.addItem(icons.getQIcon("cube-front"), dim1dim2)
self.qcbAxisSelection.addItem(icons.getQIcon("cube-bottom"), dim0dim2)
@@ -1160,25 +1159,25 @@ class StackViewMainWindow(StackView):
:param QWidget parent: Parent widget, or None
"""
+
def __init__(self, parent=None):
self._dataInfo = None
super(StackViewMainWindow, self).__init__(parent)
self.setWindowFlags(qt.Qt.Window)
# Add toolbars and status bar
- self.addToolBar(qt.Qt.BottomToolBarArea,
- LimitsToolBar(plot=self._plot))
+ self.addToolBar(qt.Qt.BottomToolBarArea, LimitsToolBar(plot=self._plot))
self.statusBar()
- menu = self.menuBar().addMenu('File')
+ menu = self.menuBar().addMenu("File")
menu.addAction(self._plot.getOutputToolBar().getSaveAction())
menu.addAction(self._plot.getOutputToolBar().getPrintAction())
menu.addSeparator()
- action = menu.addAction('Quit')
+ action = menu.addAction("Quit")
action.triggered[bool].connect(qt.QApplication.instance().quit)
- menu = self.menuBar().addMenu('Edit')
+ menu = self.menuBar().addMenu("Edit")
menu.addAction(self._plot.getOutputToolBar().getCopyAction())
menu.addSeparator()
menu.addAction(self._plot.getResetZoomAction())
@@ -1188,7 +1187,7 @@ class StackViewMainWindow(StackView):
menu.addAction(actions.control.KeepAspectRatioAction(self._plot, self))
menu.addAction(actions.control.YAxisInvertedAction(self._plot, self))
- menu = self.menuBar().addMenu('Profile')
+ menu = self.menuBar().addMenu("Profile")
profileToolBar = self._profileToolBar
menu.addAction(profileToolBar.hLineAction)
menu.addAction(profileToolBar.vLineAction)
@@ -1218,11 +1217,11 @@ class StackViewMainWindow(StackView):
elif self._perspective == 2:
dim0, dim1, dim2 = int(y), int(x), img_idx
- msg = 'Position: (%d, %d, %d)' % (dim0, dim1, dim2)
+ msg = "Position: (%d, %d, %d)" % (dim0, dim1, dim2)
if value is not None:
- msg += ', Value: %g' % value
+ msg += ", Value: %g" % value
if self._dataInfo is not None:
- msg = self._dataInfo + ', ' + msg
+ msg = self._dataInfo + ", " + msg
self.statusBar().showMessage(msg)
@@ -1231,11 +1230,15 @@ class StackViewMainWindow(StackView):
See :meth:`StackView.setStack` for details.
"""
- if hasattr(stack, 'dtype') and hasattr(stack, 'shape'):
+ if hasattr(stack, "dtype") and hasattr(stack, "shape"):
assert len(stack.shape) == 3
nframes, height, width = stack.shape
- self._dataInfo = 'Data: %dx%dx%d (%s)' % (nframes, height, width,
- str(stack.dtype))
+ self._dataInfo = "Data: %dx%dx%d (%s)" % (
+ nframes,
+ height,
+ width,
+ str(stack.dtype),
+ )
self.statusBar().showMessage(self._dataInfo)
else:
self._dataInfo = None
diff --git a/src/silx/gui/plot/StatsWidget.py b/src/silx/gui/plot/StatsWidget.py
index b23946f..0c37f52 100644
--- a/src/silx/gui/plot/StatsWidget.py
+++ b/src/silx/gui/plot/StatsWidget.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -30,7 +30,6 @@ __license__ = "MIT"
__date__ = "24/07/2018"
-from collections import OrderedDict
from contextlib import contextmanager
import logging
import weakref
@@ -55,8 +54,8 @@ _logger = logging.getLogger(__name__)
@enum.unique
class UpdateMode(_Enum):
- AUTO = 'auto'
- MANUAL = 'manual'
+ AUTO = "auto"
+ MANUAL = "manual"
# Helper class to handle specific calls to PlotWidget and SceneWidget
@@ -126,7 +125,7 @@ class _Wrapper(qt.QObject):
:param item:
:rtype: str
"""
- return ''
+ return ""
def getKind(self, item):
"""Returns the kind of an item or None if not supported
@@ -164,18 +163,18 @@ class _PlotWidgetWrapper(_Wrapper):
self.sigCurrentChanged.emit(item)
def _activeCurveChanged(self, previous, current):
- self._activeChanged(kind='curve')
+ self._activeChanged(kind="curve")
def _activeImageChanged(self, previous, current):
- self._activeChanged(kind='image')
+ self._activeChanged(kind="image")
def _activeScatterChanged(self, previous, current):
- self._activeChanged(kind='scatter')
+ self._activeChanged(kind="scatter")
def _limitsChanged(self, event):
"""Handle change of plot area limits."""
- if event['event'] == 'limitsChanged':
- self.sigVisibleDataChanged.emit()
+ if event["event"] == "limitsChanged":
+ self.sigVisibleDataChanged.emit()
def getItems(self):
plot = self.getPlot()
@@ -200,20 +199,20 @@ class _PlotWidgetWrapper(_Wrapper):
kind = self.getKind(item)
if kind in plot._ACTIVE_ITEM_KINDS:
if plot._getActiveItem(kind) != item:
- plot._setActiveItem(kind, item.getName())
+ plot._setActiveItem(kind, item)
def getLabel(self, item):
return item.getName()
def getKind(self, item):
if isinstance(item, plotitems.Curve):
- return 'curve'
+ return "curve"
elif isinstance(item, plotitems.ImageData):
- return 'image'
+ return "image"
elif isinstance(item, plotitems.Scatter):
- return 'scatter'
+ return "scatter"
elif isinstance(item, plotitems.Histogram):
- return 'histogram'
+ return "histogram"
else:
return None
@@ -259,12 +258,10 @@ class _SceneWidgetWrapper(_Wrapper):
def getKind(self, item):
from ..plot3d import items as plot3ditems
- if isinstance(item, (plot3ditems.ImageData,
- plot3ditems.ScalarField3D)):
- return 'image'
- elif isinstance(item, (plot3ditems.Scatter2D,
- plot3ditems.Scatter3D)):
- return 'scatter'
+ if isinstance(item, (plot3ditems.ImageData, plot3ditems.ScalarField3D)):
+ return "image"
+ elif isinstance(item, (plot3ditems.Scatter2D, plot3ditems.Scatter3D)):
+ return "scatter"
else:
return None
@@ -306,10 +303,10 @@ class _ScalarFieldViewWrapper(_Wrapper):
pass
def getLabel(self, item):
- return 'Data'
+ return "Data"
def getKind(self, item):
- return 'image'
+ return "image"
class _Container(object):
@@ -319,6 +316,7 @@ class _Container(object):
:param QObject obj:
"""
+
def __init__(self, obj):
self._obj = obj
@@ -383,7 +381,10 @@ class _StatsWidgetBase(object):
else: # Expect a ScalarFieldView
self._plotWrapper = _ScalarFieldViewWrapper(plot)
else:
- _logger.warning('OpenGL not installed, %s not managed' % ('SceneWidget qnd ScalarFieldView'))
+ _logger.warning(
+ "OpenGL not installed, %s not managed"
+ % ("SceneWidget qnd ScalarFieldView")
+ )
self._dealWithPlotConnection(create=True)
def setStats(self, statsHandler):
@@ -422,16 +423,19 @@ class _StatsWidgetBase(object):
connections = [] # List of (signal, slot) to connect/disconnect
if self._statsOnVisibleData:
connections.append(
- (self._plotWrapper.sigVisibleDataChanged, self._updateAllStats))
+ (self._plotWrapper.sigVisibleDataChanged, self._updateAllStats)
+ )
if self._displayOnlyActItem:
connections.append(
- (self._plotWrapper.sigCurrentChanged, self._updateCurrentItem))
+ (self._plotWrapper.sigCurrentChanged, self._updateCurrentItem)
+ )
else:
connections += [
(self._plotWrapper.sigItemAdded, self._addItem),
(self._plotWrapper.sigItemRemoved, self._removeItem),
- (self._plotWrapper.sigCurrentChanged, self._plotCurrentChanged)]
+ (self._plotWrapper.sigCurrentChanged, self._plotCurrentChanged),
+ ]
for signal, slot in connections:
if create:
@@ -441,12 +445,12 @@ class _StatsWidgetBase(object):
def _updateItemObserve(self, *args):
"""Reload table depending on mode"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _updateCurrentItem(self, *args):
"""specific callback for the sigCurrentChanged and with the
_displayOnlyActItem option."""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _updateStats(self, item, data_changed=False, roi_changed=False):
"""Update displayed information for given plot item
@@ -455,11 +459,11 @@ class _StatsWidgetBase(object):
:param bool data_changed: is the item data changed.
:param bool roi_changed: is the associated roi changed.
"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _updateAllStats(self):
"""Update stats for all rows in the table"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def setDisplayOnlyActiveItem(self, displayOnlyActItem):
"""Toggle display off all items or only the active/selected one
@@ -494,21 +498,21 @@ class _StatsWidgetBase(object):
:returns: True if the item is added to the widget.
:rtype: bool
"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _removeItem(self, item):
"""Remove table items corresponding to given plot item from the table.
:param item: The plot item
"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _plotCurrentChanged(self, current):
"""Handle change of current item and update selection in table
:param current:
"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def clear(self):
"""clear GUI"""
@@ -562,16 +566,17 @@ class StatsTable(_StatsWidgetBase, TableWidget):
:class:`PlotWidget` or :class:`SceneWidget` instance on which to operate
"""
- _LEGEND_HEADER_DATA = 'legend'
- _KIND_HEADER_DATA = 'kind'
+ _LEGEND_HEADER_DATA = "legend"
+ _KIND_HEADER_DATA = "kind"
sigUpdateModeChanged = qt.Signal(object)
"""Signal emitted when the update mode changed"""
def __init__(self, parent=None, plot=None):
TableWidget.__init__(self, parent)
- _StatsWidgetBase.__init__(self, statsOnVisibleData=False,
- displayOnlyActItem=False)
+ _StatsWidgetBase.__init__(
+ self, statsOnVisibleData=False, displayOnlyActItem=False
+ )
# Init for _displayOnlyActItem == False
assert self._displayOnlyActItem is False
@@ -669,7 +674,15 @@ class StatsTable(_StatsWidgetBase, TableWidget):
If exists, update it only when we are in 'auto' mode"""
if self.getUpdateMode() is UpdateMode.MANUAL:
# when sigCurrentChanged is giving the current item
- if len(args) > 0 and isinstance(args[0], (plotitems.Curve, plotitems.Histogram, plotitems.ImageData, plotitems.Scatter)):
+ if len(args) > 0 and isinstance(
+ args[0],
+ (
+ plotitems.Curve,
+ plotitems.Histogram,
+ plotitems.ImageData,
+ plotitems.Scatter,
+ ),
+ ):
item = args[0]
tableItems = self._itemToTableItems(item)
# if the table does not exists yet
@@ -722,9 +735,9 @@ class StatsTable(_StatsWidgetBase, TableWidget):
:param item: The plot item
:return: An ordered dict of column name to QTableWidgetItem mapping
for the given plot item.
- :rtype: OrderedDict
+ :rtype: dict
"""
- result = OrderedDict()
+ result = {}
row = self._itemToRow(item)
if row is not None:
for column in range(self.columnCount()):
@@ -776,9 +789,7 @@ class StatsTable(_StatsWidgetBase, TableWidget):
return False
# Prepare table items
- tableItems = [
- qt.QTableWidgetItem(), # Legend
- qt.QTableWidgetItem()] # Kind
+ tableItems = [qt.QTableWidgetItem(), qt.QTableWidgetItem()] # Legend # Kind
for column in range(2, self.columnCount()):
header = self.horizontalHeaderItem(column)
@@ -805,8 +816,7 @@ class StatsTable(_StatsWidgetBase, TableWidget):
row = self.rowCount() - 1
for column, tableItem in enumerate(tableItems):
tableItem.setData(qt.Qt.UserRole, _Container(item))
- tableItem.setFlags(
- qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
+ tableItem.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
self.setItem(row, column, tableItem)
# Update table items content
@@ -815,8 +825,7 @@ class StatsTable(_StatsWidgetBase, TableWidget):
# Listen for item changes
# Using queued connection to avoid issue with sender
# being that of the signal calling the signal
- item.sigItemChanged.connect(self._plotItemChanged,
- qt.Qt.QueuedConnection)
+ item.sigItemChanged.connect(self._plotItemChanged, qt.Qt.QueuedConnection)
return True
@@ -871,8 +880,12 @@ class StatsTable(_StatsWidgetBase, TableWidget):
else:
roi_changed = False
stats = statsHandler.calculate(
- item, plot, self._statsOnVisibleData,
- data_changed=data_changed, roi_changed=roi_changed)
+ item,
+ plot,
+ self._statsOnVisibleData,
+ data_changed=data_changed,
+ roi_changed=roi_changed,
+ )
else:
stats = {}
@@ -887,7 +900,7 @@ class StatsTable(_StatsWidgetBase, TableWidget):
value = stats.get(name)
if value is None:
_logger.error("Value not found for: %s", name)
- tableItem.setText('-')
+ tableItem.setText("-")
else:
tableItem.setText(str(value))
@@ -943,6 +956,7 @@ class StatsTable(_StatsWidgetBase, TableWidget):
class UpdateModeWidget(qt.QWidget):
"""Widget used to select the mode of update"""
+
sigUpdateModeChanged = qt.Signal(object)
"""signal emitted when the mode for update changed"""
sigUpdateRequested = qt.Signal()
@@ -954,22 +968,22 @@ class UpdateModeWidget(qt.QWidget):
self._buttonGrp = qt.QButtonGroup(parent=self)
self._buttonGrp.setExclusive(True)
- spacer = qt.QSpacerItem(20, 20,
- qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Minimum)
+ spacer = qt.QSpacerItem(
+ 20, 20, qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum
+ )
self.layout().addItem(spacer)
- self._autoRB = qt.QRadioButton('auto', parent=self)
+ self._autoRB = qt.QRadioButton("auto", parent=self)
self.layout().addWidget(self._autoRB)
self._buttonGrp.addButton(self._autoRB)
- self._manualRB = qt.QRadioButton('manual', parent=self)
+ self._manualRB = qt.QRadioButton("manual", parent=self)
self.layout().addWidget(self._manualRB)
self._buttonGrp.addButton(self._manualRB)
self._manualRB.setChecked(True)
- refresh_icon = icons.getQIcon('view-refresh')
- self._updatePB = qt.QPushButton(refresh_icon, '', parent=self)
+ refresh_icon = icons.getQIcon("view-refresh")
+ self._updatePB = qt.QPushButton(refresh_icon, "", parent=self)
self.layout().addWidget(self._updatePB)
# connect signal / SLOT
@@ -1006,7 +1020,7 @@ class UpdateModeWidget(qt.QWidget):
if not self._manualRB.isChecked():
self._manualRB.setChecked(True)
else:
- raise ValueError('mode', mode, 'is not recognized')
+ raise ValueError("mode", mode, "is not recognized")
def getUpdateMode(self):
"""Returns update mode (See :meth:`setUpdateMode`).
@@ -1031,7 +1045,6 @@ class UpdateModeWidget(qt.QWidget):
class _OptionsWidget(qt.QToolBar):
-
def __init__(self, parent=None, updateMode=None, displayOnlyActItem=False):
assert updateMode is not None
qt.QToolBar.__init__(self, parent)
@@ -1055,12 +1068,14 @@ class _OptionsWidget(qt.QToolBar):
action = qt.QAction(self)
action.setIcon(icons.getQIcon("stats-visible-data"))
action.setText("Use the visible data range")
- action.setToolTip("Use the visible data range.<br/>"
- "If activated the data is filtered to only use"
- "visible data of the plot."
- "The filtering is a data sub-sampling."
- "No interpolation is made to fit data to"
- "boundaries.")
+ action.setToolTip(
+ "Use the visible data range.<br/>"
+ "If activated the data is filtered to only use"
+ "visible data of the plot."
+ "The filtering is a data sub-sampling."
+ "No interpolation is made to fit data to"
+ "boundaries."
+ )
action.setCheckable(True)
self.__useVisibleData = action
@@ -1156,7 +1171,7 @@ class StatsWidget(qt.QWidget):
It Provides the visibility of the widget.
"""
- NUMBER_FORMAT = '{0:.3f}'
+ NUMBER_FORMAT = "{0:.3f}"
def __init__(self, parent=None, plot=None, stats=None):
qt.QWidget.__init__(self, parent)
@@ -1172,15 +1187,15 @@ class StatsWidget(qt.QWidget):
self.layout().addWidget(self._statsTable)
old = self._statsTable.blockSignals(True)
- self._options.itemSelection.triggered.connect(
- self._optSelectionChanged)
- self._options.dataRangeSelection.triggered.connect(
- self._optDataRangeChanged)
+ self._options.itemSelection.triggered.connect(self._optSelectionChanged)
+ self._options.dataRangeSelection.triggered.connect(self._optDataRangeChanged)
self._optDataRangeChanged()
self._statsTable.blockSignals(old)
self._statsTable.sigUpdateModeChanged.connect(self._options._setUpdateMode)
- callback = functools.partial(self._getStatsTable()._updateAllStats, is_request=True)
+ callback = functools.partial(
+ self._getStatsTable()._updateAllStats, is_request=True
+ )
self._options.sigUpdateStats.connect(callback)
def _getStatsTable(self):
@@ -1199,12 +1214,12 @@ class StatsWidget(qt.QWidget):
qt.QWidget.hideEvent(self, event)
def _optSelectionChanged(self, action=None):
- self._getStatsTable().setDisplayOnlyActiveItem(
- self._options.isActiveItemMode())
+ self._getStatsTable().setDisplayOnlyActiveItem(self._options.isActiveItemMode())
def _optDataRangeChanged(self, action=None):
self._getStatsTable().setStatsOnVisibleData(
- self._options.isVisibleDataRangeMode())
+ self._options.isVisibleDataRangeMode()
+ )
# Proxy methods
@@ -1215,7 +1230,8 @@ class StatsWidget(qt.QWidget):
@docstring(StatsTable)
def setPlot(self, plot):
self._options.setVisibleDataRangeModeEnabled(
- plot is None or isinstance(plot, PlotWidget))
+ plot is None or isinstance(plot, PlotWidget)
+ )
return self._getStatsTable().setPlot(plot=plot)
@docstring(StatsTable)
@@ -1229,7 +1245,8 @@ class StatsWidget(qt.QWidget):
self._options.setDisplayActiveItems(displayOnlyActItem)
self._options.blockSignals(old)
return self._getStatsTable().setDisplayOnlyActiveItem(
- displayOnlyActItem=displayOnlyActItem)
+ displayOnlyActItem=displayOnlyActItem
+ )
@docstring(StatsTable)
def setStatsOnVisibleData(self, b):
@@ -1244,15 +1261,17 @@ class StatsWidget(qt.QWidget):
self._statsTable.setUpdateMode(mode)
-DEFAULT_STATS = StatsHandler((
- (statsmdl.StatMin(), StatFormatter()),
- statsmdl.StatCoordMin(),
- (statsmdl.StatMax(), StatFormatter()),
- statsmdl.StatCoordMax(),
- statsmdl.StatCOM(),
- (('mean', numpy.mean), StatFormatter()),
- (('std', numpy.std), StatFormatter()),
-))
+DEFAULT_STATS = StatsHandler(
+ (
+ (statsmdl.StatMin(), StatFormatter()),
+ statsmdl.StatCoordMin(),
+ (statsmdl.StatMax(), StatFormatter()),
+ statsmdl.StatCoordMax(),
+ statsmdl.StatCOM(),
+ (("mean", numpy.mean), StatFormatter()),
+ (("std", numpy.std), StatFormatter()),
+ )
+)
class BasicStatsWidget(StatsWidget):
@@ -1282,9 +1301,9 @@ class BasicStatsWidget(StatsWidget):
widget = BasicStatsWidget(plot=plot)
widget.show()
"""
+
def __init__(self, parent=None, plot=None):
- StatsWidget.__init__(self, parent=parent, plot=plot,
- stats=DEFAULT_STATS)
+ StatsWidget.__init__(self, parent=parent, plot=plot, stats=DEFAULT_STATS)
class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
@@ -1306,8 +1325,9 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
sigUpdateModeChanged = qt.Signal(object)
"""Signal emitted when the update mode changed"""
- def __init__(self, parent=None, plot=None, kind='curve', stats=None,
- statsOnVisibleData=False):
+ def __init__(
+ self, parent=None, plot=None, kind="curve", stats=None, statsOnVisibleData=False
+ ):
self._item_kind = kind
"""The item displayed"""
self._statQlineEdit = {}
@@ -1315,9 +1335,9 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
self._n_statistics_per_line = 4
"""number of statistics displayed per line in the grid layout"""
qt.QWidget.__init__(self, parent)
- _StatsWidgetBase.__init__(self,
- statsOnVisibleData=statsOnVisibleData,
- displayOnlyActItem=True)
+ _StatsWidgetBase.__init__(
+ self, statsOnVisibleData=statsOnVisibleData, displayOnlyActItem=True
+ )
self.setLayout(self._createLayout())
self.setPlot(plot)
if stats is not None:
@@ -1336,8 +1356,8 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
widget = qt.QWidget(parent=self)
parent = widget
- qLabel = qt.QLabel(statistic.name + ':', parent=parent)
- qLineEdit = qt.QLineEdit('', parent=parent)
+ qLabel = qt.QLabel(statistic.name + ":", parent=parent)
+ qLineEdit = qt.QLineEdit("", parent=parent)
qLineEdit.setReadOnly(True)
self._addStatsWidgetsToLayout(qLabel=qLabel, qLineEdit=qLineEdit)
@@ -1353,7 +1373,7 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
self._updateAllStats()
def _addStatsWidgetsToLayout(self, qLabel, qLineEdit):
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def setStats(self, statsHandler):
"""Set which stats to display and the associated formatting.
@@ -1379,6 +1399,7 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
def kind_filter(_item):
return self._plotWrapper.getKind(_item) == self.getKind()
+
items = list(filter(kind_filter, _items))
assert len(items) in (0, 1)
if len(items) == 1:
@@ -1402,15 +1423,13 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
def _setItem(self, item, data_changed=True):
if item is None:
for stat_name, stat_widget in self._statQlineEdit.items():
- stat_widget.setText('')
- elif (self._statsHandler is not None and len(
- self._statsHandler.stats) > 0):
+ stat_widget.setText("")
+ elif self._statsHandler is not None and len(self._statsHandler.stats) > 0:
plot = self.getPlot()
if plot is not None:
- statsValDict = self._statsHandler.calculate(item,
- plot,
- self._statsOnVisibleData,
- data_changed=data_changed)
+ statsValDict = self._statsHandler.calculate(
+ item, plot, self._statsOnVisibleData, data_changed=data_changed
+ )
for statName, statVal in list(statsValDict.items()):
self._statQlineEdit[statName].setText(statVal)
@@ -1422,6 +1441,7 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
def kind_filter(_item):
return self._plotWrapper.getKind(_item) == self.getKind()
+
items = list(filter(kind_filter, _items))
assert len(items) in (0, 1)
_item = items[0] if len(items) == 1 else None
@@ -1432,27 +1452,38 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
def _createLayout(self):
"""create an instance of the main QLayout"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _addItem(self, item):
- raise NotImplementedError('Display only the active item')
+ raise NotImplementedError("Display only the active item")
def _removeItem(self, item):
- raise NotImplementedError('Display only the active item')
+ raise NotImplementedError("Display only the active item")
def _plotCurrentChanged(self, current):
- raise NotImplementedError('Display only the active item')
+ raise NotImplementedError("Display only the active item")
def _updateModeHasChanged(self):
self.sigUpdateModeChanged.emit(self._updateMode)
class _BasicLineStatsWidget(_BaseLineStatsWidget):
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False):
- _BaseLineStatsWidget.__init__(self, parent=parent, kind=kind,
- plot=plot, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
+ def __init__(
+ self,
+ parent=None,
+ plot=None,
+ kind="curve",
+ stats=DEFAULT_STATS,
+ statsOnVisibleData=False,
+ ):
+ _BaseLineStatsWidget.__init__(
+ self,
+ parent=parent,
+ kind=kind,
+ plot=plot,
+ stats=stats,
+ statsOnVisibleData=statsOnVisibleData,
+ )
def _createLayout(self):
return FlowLayout()
@@ -1488,15 +1519,26 @@ class BasicLineStatsWidget(qt.QWidget):
:param bool statsOnVisibleData: compute statistics for the whole data or
only visible ones.
"""
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False):
+
+ def __init__(
+ self,
+ parent=None,
+ plot=None,
+ kind="curve",
+ stats=DEFAULT_STATS,
+ statsOnVisibleData=False,
+ ):
qt.QWidget.__init__(self, parent)
self.setLayout(qt.QHBoxLayout())
self.layout().setSpacing(0)
self.layout().setContentsMargins(0, 0, 0, 0)
- self._lineStatsWidget = _BasicLineStatsWidget(parent=self, plot=plot,
- kind=kind, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
+ self._lineStatsWidget = _BasicLineStatsWidget(
+ parent=self,
+ plot=plot,
+ kind=kind,
+ stats=stats,
+ statsOnVisibleData=statsOnVisibleData,
+ )
self.layout().addWidget(self._lineStatsWidget)
self._options = UpdateModeWidget()
@@ -1548,12 +1590,23 @@ class BasicLineStatsWidget(qt.QWidget):
class _BasicGridStatsWidget(_BaseLineStatsWidget):
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False,
- statsPerLine=4):
- _BaseLineStatsWidget.__init__(self, parent=parent, kind=kind,
- plot=plot, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
+ def __init__(
+ self,
+ parent=None,
+ plot=None,
+ kind="curve",
+ stats=DEFAULT_STATS,
+ statsOnVisibleData=False,
+ statsPerLine=4,
+ ):
+ _BaseLineStatsWidget.__init__(
+ self,
+ parent=parent,
+ kind=kind,
+ plot=plot,
+ stats=stats,
+ statsOnVisibleData=statsOnVisibleData,
+ )
self._n_statistics_per_line = statsPerLine
def _addStatsWidgetsToLayout(self, qLabel, qLineEdit):
@@ -1597,8 +1650,14 @@ class BasicGridStatsWidget(qt.QWidget):
widget.show()
"""
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False):
+ def __init__(
+ self,
+ parent=None,
+ plot=None,
+ kind="curve",
+ stats=DEFAULT_STATS,
+ statsOnVisibleData=False,
+ ):
qt.QWidget.__init__(self, parent)
self.setLayout(qt.QVBoxLayout())
self.layout().setSpacing(0)
@@ -1608,9 +1667,13 @@ class BasicGridStatsWidget(qt.QWidget):
self._options.showRadioButtons(False)
self.layout().addWidget(self._options)
- self._lineStatsWidget = _BasicGridStatsWidget(parent=self, plot=plot,
- kind=kind, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
+ self._lineStatsWidget = _BasicGridStatsWidget(
+ parent=self,
+ plot=plot,
+ kind=kind,
+ stats=stats,
+ statsOnVisibleData=statsOnVisibleData,
+ )
self.layout().addWidget(self._lineStatsWidget)
# tune options
diff --git a/src/silx/gui/plot/_BaseMaskToolsWidget.py b/src/silx/gui/plot/_BaseMaskToolsWidget.py
index 1673137..6b98289 100644
--- a/src/silx/gui/plot/_BaseMaskToolsWidget.py
+++ b/src/silx/gui/plot/_BaseMaskToolsWidget.py
@@ -134,7 +134,7 @@ class BaseMask(qt.QObject):
:param bool copy: True (the default) to copy the array,
False to use it as is if possible.
"""
- self._mask = numpy.array(mask, copy=copy, order='C', dtype=numpy.uint8)
+ self._mask = numpy.array(mask, copy=copy, order="C", dtype=numpy.uint8)
self._notify()
# History control
@@ -147,8 +147,11 @@ class BaseMask(qt.QObject):
def commit(self):
"""Append the current mask to history if changed"""
- if (not self._history or self._redo or
- not numpy.array_equal(self._mask, self._history[-1])):
+ if (
+ not self._history
+ or self._redo
+ or not numpy.array_equal(self._mask, self._history[-1])
+ ):
if self._redo:
self._redo = [] # Reset redo as a new action as been performed
self.sigRedoable[bool].emit(False)
@@ -222,7 +225,7 @@ class BaseMask(qt.QObject):
if shape is None:
# assume dimensionality never changes
shape = (0,) * len(self._mask.shape) # empty array
- shapeChanged = (shape != self._mask.shape)
+ shapeChanged = shape != self._mask.shape
self._mask = numpy.zeros(shape, dtype=numpy.uint8)
if shapeChanged:
self.resetHistory()
@@ -263,9 +266,7 @@ class BaseMask(qt.QObject):
:param float threshold: Threshold
:param bool mask: True to mask (default), False to unmask.
"""
- self.updateStencil(level,
- self.getDataValues() < threshold,
- mask)
+ self.updateStencil(level, self.getDataValues() < threshold, mask)
def updateBetweenThresholds(self, level, min_, max_, mask=True):
"""Mask/unmask all points whose values are in a range.
@@ -275,8 +276,9 @@ class BaseMask(qt.QObject):
:param float max_: Upper threshold
:param bool mask: True to mask (default), False to unmask.
"""
- stencil = numpy.logical_and(min_ <= self.getDataValues(),
- self.getDataValues() <= max_)
+ stencil = numpy.logical_and(
+ min_ <= self.getDataValues(), self.getDataValues() <= max_
+ )
self.updateStencil(level, stencil, mask)
def updateAboveThreshold(self, level, threshold, mask=True):
@@ -286,9 +288,7 @@ class BaseMask(qt.QObject):
:param float threshold: Threshold.
:param bool mask: True to mask (default), False to unmask.
"""
- self.updateStencil(level,
- self.getDataValues() > threshold,
- mask)
+ self.updateStencil(level, self.getDataValues() > threshold, mask)
def updateNotFinite(self, level, mask=True):
"""Mask/unmask all points whose values are not finite.
@@ -296,9 +296,9 @@ class BaseMask(qt.QObject):
:param int level: Mask level to update.
:param bool mask: True to mask (default), False to unmask.
"""
- self.updateStencil(level,
- numpy.logical_not(numpy.isfinite(self.getDataValues())),
- mask)
+ self.updateStencil(
+ level, numpy.logical_not(numpy.isfinite(self.getDataValues())), mask
+ )
# Drawing operations:
def updateRectangle(self, level, row, col, height, width, mask=True):
@@ -390,18 +390,20 @@ class BaseMaskToolsWidget(qt.QWidget):
# register if the user as force a color for the corresponding mask level
self._defaultColors = numpy.ones((self._maxLevelNumber + 1), dtype=bool)
# overlays colors set by the user
- self._overlayColors = numpy.zeros((self._maxLevelNumber + 1, 3), dtype=numpy.float32)
+ self._overlayColors = numpy.zeros(
+ (self._maxLevelNumber + 1, 3), dtype=numpy.float32
+ )
# as parent have to be the first argument of the widget to fit
# QtDesigner need but here plot can't be None by default.
assert plot is not None
self._plotRef = weakref.ref(plot)
- self._maskName = '__MASK_TOOLS_%d' % id(self) # Legend of the mask
+ self._maskName = "__MASK_TOOLS_%d" % id(self) # Legend of the mask
- self._colormap = Colormap(normalization='linear',
- vmin=0,
- vmax=self._maxLevelNumber)
- self._defaultOverlayColor = rgba('gray') # Color of the mask
+ self._colormap = Colormap(
+ normalization="linear", vmin=0, vmax=self._maxLevelNumber
+ )
+ self._defaultOverlayColor = rgba("gray") # Color of the mask
self._setMaskColors(1, 0.5) # Set the colormap LUT
if not isinstance(mask, BaseMask):
@@ -413,11 +415,10 @@ class BaseMaskToolsWidget(qt.QWidget):
self._drawingMode = None # Store current drawing mode
self._lastPencilPos = None
- self._multipleMasks = 'exclusive'
+ self._multipleMasks = "exclusive"
self._maskFileDir = qt.QDir.current().absolutePath()
- self.plot.sigInteractiveModeChanged.connect(
- self._interactiveModeChanged)
+ self.plot.sigInteractiveModeChanged.connect(self._interactiveModeChanged)
self._initWidgets()
@@ -470,11 +471,11 @@ class BaseMaskToolsWidget(qt.QWidget):
:param str mode: The mode to use
"""
- assert mode in ('exclusive', 'single')
+ assert mode in ("exclusive", "single")
if mode != self._multipleMasks:
self._multipleMasks = mode
- self._levelWidget.setVisible(self._multipleMasks != 'single')
- self._clearAllBtn.setVisible(self._multipleMasks != 'single')
+ self._levelWidget.setVisible(self._multipleMasks != "single")
+ self._clearAllBtn.setVisible(self._multipleMasks != "single")
def setMaskFileDirectory(self, path):
"""Set the default directory to use by load/save GUI tools
@@ -505,7 +506,8 @@ class BaseMaskToolsWidget(qt.QWidget):
plot = self._plotRef()
if plot is None:
raise RuntimeError(
- 'Mask widget attached to a PlotWidget that no longer exists')
+ "Mask widget attached to a PlotWidget that no longer exists"
+ )
return plot
def setDirection(self, direction=qt.QBoxLayout.LeftToRight):
@@ -534,7 +536,7 @@ class BaseMaskToolsWidget(qt.QWidget):
False for no trailing stretch
:return: A QWidget with a QHBoxLayout
"""
- stretch = kwargs.get('stretch', True)
+ stretch = kwargs.get("stretch", True)
layout = qt.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
@@ -547,20 +549,27 @@ class BaseMaskToolsWidget(qt.QWidget):
return widget
def _initTransparencyWidget(self):
- """ Init the mask transparency widget """
+ """Init the mask transparency widget"""
transparencyWidget = qt.QWidget(parent=self)
grid = qt.QGridLayout()
grid.setContentsMargins(0, 0, 0, 0)
- self.transparencySlider = qt.QSlider(qt.Qt.Horizontal, parent=transparencyWidget)
+ self.transparencySlider = qt.QSlider(
+ qt.Qt.Horizontal, parent=transparencyWidget
+ )
self.transparencySlider.setRange(3, 10)
self.transparencySlider.setValue(8)
- self.transparencySlider.setToolTip(
- 'Set the transparency of the mask display')
+ self.transparencySlider.setToolTip("Set the transparency of the mask display")
self.transparencySlider.valueChanged.connect(self._updateColors)
- grid.addWidget(qt.QLabel('Display:', parent=transparencyWidget), 0, 0)
+ grid.addWidget(qt.QLabel("Display:", parent=transparencyWidget), 0, 0)
grid.addWidget(self.transparencySlider, 0, 1, 1, 3)
- grid.addWidget(qt.QLabel('<small><b>Transparent</b></small>', parent=transparencyWidget), 1, 1)
- grid.addWidget(qt.QLabel('<small><b>Opaque</b></small>', parent=transparencyWidget), 1, 3)
+ grid.addWidget(
+ qt.QLabel("<small><b>Transparent</b></small>", parent=transparencyWidget),
+ 1,
+ 1,
+ )
+ grid.addWidget(
+ qt.QLabel("<small><b>Opaque</b></small>", parent=transparencyWidget), 1, 3
+ )
transparencyWidget.setLayout(grid)
return transparencyWidget
@@ -571,11 +580,13 @@ class BaseMaskToolsWidget(qt.QWidget):
self.levelSpinBox = qt.QSpinBox()
self.levelSpinBox.setRange(1, self._maxLevelNumber)
self.levelSpinBox.setToolTip(
- 'Choose which mask level is edited.\n'
- 'A mask can have up to 255 non-overlapping levels.')
+ "Choose which mask level is edited.\n"
+ "A mask can have up to 255 non-overlapping levels."
+ )
self.levelSpinBox.valueChanged[int].connect(self._updateColors)
- self._levelWidget = self._hboxWidget(qt.QLabel('Mask level:'),
- self.levelSpinBox)
+ self._levelWidget = self._hboxWidget(
+ qt.QLabel("Mask level:"), self.levelSpinBox
+ )
# Transparency
self._transparencyWidget = self._initTransparencyWidget()
@@ -593,62 +604,66 @@ class BaseMaskToolsWidget(qt.QWidget):
return qt.QIcon()
undoAction = qt.QAction(self)
- undoAction.setText('Undo')
+ undoAction.setText("Undo")
icon = getIcon("edit-undo", qt.QStyle.SP_ArrowBack)
undoAction.setIcon(icon)
undoAction.setShortcut(qt.QKeySequence.Undo)
- undoAction.setToolTip('Undo last mask change <b>%s</b>' %
- undoAction.shortcut().toString())
+ undoAction.setToolTip(
+ "Undo last mask change <b>%s</b>" % undoAction.shortcut().toString()
+ )
self._mask.sigUndoable.connect(undoAction.setEnabled)
undoAction.triggered.connect(self._mask.undo)
redoAction = qt.QAction(self)
- redoAction.setText('Redo')
+ redoAction.setText("Redo")
icon = getIcon("edit-redo", qt.QStyle.SP_ArrowForward)
redoAction.setIcon(icon)
redoAction.setShortcut(qt.QKeySequence.Redo)
- redoAction.setToolTip('Redo last undone mask change <b>%s</b>' %
- redoAction.shortcut().toString())
+ redoAction.setToolTip(
+ "Redo last undone mask change <b>%s</b>" % redoAction.shortcut().toString()
+ )
self._mask.sigRedoable.connect(redoAction.setEnabled)
redoAction.triggered.connect(self._mask.redo)
loadAction = qt.QAction(self)
- loadAction.setText('Load...')
+ loadAction.setText("Load...")
icon = icons.getQIcon("document-open")
loadAction.setIcon(icon)
- loadAction.setToolTip('Load mask from file')
+ loadAction.setToolTip("Load mask from file")
loadAction.triggered.connect(self._loadMask)
saveAction = qt.QAction(self)
- saveAction.setText('Save...')
+ saveAction.setText("Save...")
icon = icons.getQIcon("document-save")
saveAction.setIcon(icon)
- saveAction.setToolTip('Save mask to file')
+ saveAction.setToolTip("Save mask to file")
saveAction.triggered.connect(self._saveMask)
invertAction = qt.QAction(self)
- invertAction.setText('Invert')
+ invertAction.setText("Invert")
icon = icons.getQIcon("mask-invert")
invertAction.setIcon(icon)
invertAction.setShortcut(qt.QKeySequence(qt.Qt.CTRL | qt.Qt.Key_I))
- invertAction.setToolTip('Invert current mask <b>%s</b>' %
- invertAction.shortcut().toString())
+ invertAction.setToolTip(
+ "Invert current mask <b>%s</b>" % invertAction.shortcut().toString()
+ )
invertAction.triggered.connect(self._handleInvertMask)
clearAction = qt.QAction(self)
- clearAction.setText('Clear')
+ clearAction.setText("Clear")
icon = icons.getQIcon("mask-clear")
clearAction.setIcon(icon)
clearAction.setShortcut(qt.QKeySequence.Delete)
- clearAction.setToolTip('Clear current mask level <b>%s</b>' %
- clearAction.shortcut().toString())
+ clearAction.setToolTip(
+ "Clear current mask level <b>%s</b>" % clearAction.shortcut().toString()
+ )
clearAction.triggered.connect(self._handleClearMask)
clearAllAction = qt.QAction(self)
- clearAllAction.setText('Clear all')
+ clearAllAction.setText("Clear all")
icon = icons.getQIcon("mask-clear-all")
clearAllAction.setIcon(icon)
- clearAllAction.setToolTip('Clear all mask levels')
+ clearAllAction.setToolTip("Clear all mask levels")
clearAllAction.triggered.connect(self.resetSelectionMask)
# Buttons group
@@ -657,9 +672,17 @@ class BaseMaskToolsWidget(qt.QWidget):
margin2 = qt.QWidget(self)
margin2.setMinimumWidth(6)
- actions = (loadAction, saveAction, margin1,
- undoAction, redoAction, margin2,
- invertAction, clearAction, clearAllAction)
+ actions = (
+ loadAction,
+ saveAction,
+ margin1,
+ undoAction,
+ redoAction,
+ margin2,
+ invertAction,
+ clearAction,
+ clearAllAction,
+ )
widgets = []
for action in actions:
if isinstance(action, qt.QWidget):
@@ -679,7 +702,7 @@ class BaseMaskToolsWidget(qt.QWidget):
layout.addWidget(self._transparencyWidget)
layout.addStretch(1)
- maskGroup = qt.QGroupBox('Mask')
+ maskGroup = qt.QGroupBox("Mask")
maskGroup.setLayout(layout)
return maskGroup
@@ -695,44 +718,46 @@ class BaseMaskToolsWidget(qt.QWidget):
self.addAction(self.browseAction)
# Draw tools
- self.rectAction = qt.QAction(icons.getQIcon('shape-rectangle'),
- 'Rectangle selection',
- self)
+ self.rectAction = qt.QAction(
+ icons.getQIcon("shape-rectangle"), "Rectangle selection", self
+ )
self.rectAction.setToolTip(
- 'Rectangle selection tool: (Un)Mask a rectangular region <b>R</b>')
+ "Rectangle selection tool: (Un)Mask a rectangular region <b>R</b>"
+ )
self.rectAction.setShortcut(qt.QKeySequence(qt.Qt.Key_R))
self.rectAction.setCheckable(True)
self.rectAction.triggered.connect(self._activeRectMode)
self.addAction(self.rectAction)
- self.ellipseAction = qt.QAction(icons.getQIcon('shape-ellipse'),
- 'Circle selection',
- self)
+ self.ellipseAction = qt.QAction(
+ icons.getQIcon("shape-ellipse"), "Circle selection", self
+ )
self.ellipseAction.setToolTip(
- 'Rectangle selection tool: (Un)Mask a circle region <b>R</b>')
+ "Rectangle selection tool: (Un)Mask a circle region <b>R</b>"
+ )
self.ellipseAction.setShortcut(qt.QKeySequence(qt.Qt.Key_R))
self.ellipseAction.setCheckable(True)
self.ellipseAction.triggered.connect(self._activeEllipseMode)
self.addAction(self.ellipseAction)
- self.polygonAction = qt.QAction(icons.getQIcon('shape-polygon'),
- 'Polygon selection',
- self)
+ self.polygonAction = qt.QAction(
+ icons.getQIcon("shape-polygon"), "Polygon selection", self
+ )
self.polygonAction.setShortcut(qt.QKeySequence(qt.Qt.Key_S))
self.polygonAction.setToolTip(
- 'Polygon selection tool: (Un)Mask a polygonal region <b>S</b><br>'
- 'Left-click to place new polygon corners<br>'
- 'Left-click on first corner to close the polygon')
+ "Polygon selection tool: (Un)Mask a polygonal region <b>S</b><br>"
+ "Left-click to place new polygon corners<br>"
+ "Left-click on first corner to close the polygon"
+ )
self.polygonAction.setCheckable(True)
self.polygonAction.triggered.connect(self._activePolygonMode)
self.addAction(self.polygonAction)
- self.pencilAction = qt.QAction(icons.getQIcon('draw-pencil'),
- 'Pencil tool',
- self)
+ self.pencilAction = qt.QAction(
+ icons.getQIcon("draw-pencil"), "Pencil tool", self
+ )
self.pencilAction.setShortcut(qt.QKeySequence(qt.Qt.Key_P))
- self.pencilAction.setToolTip(
- 'Pencil tool: (Un)Mask using a pencil <b>P</b>')
+ self.pencilAction.setToolTip("Pencil tool: (Un)Mask using a pencil <b>P</b>")
self.pencilAction.setCheckable(True)
self.pencilAction.triggered.connect(self._activePencilMode)
self.addAction(self.pencilAction)
@@ -744,8 +769,13 @@ class BaseMaskToolsWidget(qt.QWidget):
self.drawActionGroup.addAction(self.polygonAction)
self.drawActionGroup.addAction(self.pencilAction)
- actions = (self.browseAction, self.rectAction, self.ellipseAction,
- self.polygonAction, self.pencilAction)
+ actions = (
+ self.browseAction,
+ self.rectAction,
+ self.ellipseAction,
+ self.polygonAction,
+ self.pencilAction,
+ )
drawButtons = []
for action in actions:
btn = qt.QToolButton()
@@ -755,14 +785,16 @@ class BaseMaskToolsWidget(qt.QWidget):
layout.addWidget(container)
# Mask/Unmask radio buttons
- maskRadioBtn = qt.QRadioButton('Mask')
+ maskRadioBtn = qt.QRadioButton("Mask")
maskRadioBtn.setToolTip(
- 'Drawing masks with current level. Press <b>Ctrl</b> to unmask')
+ "Drawing masks with current level. Press <b>Ctrl</b> to unmask"
+ )
maskRadioBtn.setChecked(True)
- unmaskRadioBtn = qt.QRadioButton('Unmask')
+ unmaskRadioBtn = qt.QRadioButton("Unmask")
unmaskRadioBtn.setToolTip(
- 'Drawing unmasks with current level. Press <b>Ctrl</b> to mask')
+ "Drawing unmasks with current level. Press <b>Ctrl</b> to mask"
+ )
self.maskStateGroup = qt.QButtonGroup()
self.maskStateGroup.addButton(maskRadioBtn, 1)
@@ -780,7 +812,7 @@ class BaseMaskToolsWidget(qt.QWidget):
layout.addStretch(1)
- drawGroup = qt.QGroupBox('Draw tools')
+ drawGroup = qt.QGroupBox("Draw tools")
drawGroup.setLayout(layout)
return drawGroup
@@ -797,7 +829,7 @@ class BaseMaskToolsWidget(qt.QWidget):
self.pencilSlider.setRange(1, 50)
self.pencilSlider.setToolTip(pencilToolTip)
- pencilLabel = qt.QLabel('Pencil size:', parent=pencilSetting)
+ pencilLabel = qt.QLabel("Pencil size:", parent=pencilSetting)
layout = qt.QGridLayout()
layout.addWidget(pencilLabel, 0, 0)
@@ -813,26 +845,29 @@ class BaseMaskToolsWidget(qt.QWidget):
def _initThresholdGroupBox(self):
"""Init thresholding widgets"""
- self.belowThresholdAction = qt.QAction(icons.getQIcon('plot-roi-below'),
- 'Mask below threshold',
- self)
+ self.belowThresholdAction = qt.QAction(
+ icons.getQIcon("plot-roi-below"), "Mask below threshold", self
+ )
self.belowThresholdAction.setToolTip(
- 'Mask image where values are below given threshold')
+ "Mask image where values are below given threshold"
+ )
self.belowThresholdAction.setCheckable(True)
self.belowThresholdAction.setChecked(True)
- self.betweenThresholdAction = qt.QAction(icons.getQIcon('plot-roi-between'),
- 'Mask within range',
- self)
+ self.betweenThresholdAction = qt.QAction(
+ icons.getQIcon("plot-roi-between"), "Mask within range", self
+ )
self.betweenThresholdAction.setToolTip(
- 'Mask image where values are within given range')
+ "Mask image where values are within given range"
+ )
self.betweenThresholdAction.setCheckable(True)
- self.aboveThresholdAction = qt.QAction(icons.getQIcon('plot-roi-above'),
- 'Mask above threshold',
- self)
+ self.aboveThresholdAction = qt.QAction(
+ icons.getQIcon("plot-roi-above"), "Mask above threshold", self
+ )
self.aboveThresholdAction.setToolTip(
- 'Mask image where values are above given threshold')
+ "Mask image where values are above given threshold"
+ )
self.aboveThresholdAction.setCheckable(True)
self.thresholdActionGroup = qt.QActionGroup(self)
@@ -840,17 +875,18 @@ class BaseMaskToolsWidget(qt.QWidget):
self.thresholdActionGroup.addAction(self.belowThresholdAction)
self.thresholdActionGroup.addAction(self.betweenThresholdAction)
self.thresholdActionGroup.addAction(self.aboveThresholdAction)
- self.thresholdActionGroup.triggered.connect(
- self._thresholdActionGroupTriggered)
+ self.thresholdActionGroup.triggered.connect(self._thresholdActionGroupTriggered)
- self.loadColormapRangeAction = qt.QAction(icons.getQIcon('view-refresh'),
- 'Set min-max from colormap',
- self)
+ self.loadColormapRangeAction = qt.QAction(
+ icons.getQIcon("view-refresh"), "Set min-max from colormap", self
+ )
self.loadColormapRangeAction.setToolTip(
- 'Set min and max values from current colormap range')
+ "Set min and max values from current colormap range"
+ )
self.loadColormapRangeAction.setCheckable(False)
self.loadColormapRangeAction.triggered.connect(
- self._loadRangeFromColormapTriggered)
+ self._loadRangeFromColormapTriggered
+ )
widgets = []
for action in self.thresholdActionGroup.actions():
@@ -859,8 +895,7 @@ class BaseMaskToolsWidget(qt.QWidget):
widgets.append(btn)
spacer = qt.QWidget(parent=self)
- spacer.setSizePolicy(qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Preferred)
+ spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)
widgets.append(spacer)
loadColormapRangeBtn = qt.QToolButton()
@@ -882,7 +917,7 @@ class BaseMaskToolsWidget(qt.QWidget):
config.addWidget(self.maxLineLabel, 1, 0)
config.addWidget(self.maxLineEdit, 1, 1)
- self.applyMaskBtn = qt.QPushButton('Apply mask')
+ self.applyMaskBtn = qt.QPushButton("Apply mask")
self.applyMaskBtn.clicked.connect(self._maskBtnClicked)
layout = qt.QVBoxLayout()
@@ -891,7 +926,7 @@ class BaseMaskToolsWidget(qt.QWidget):
layout.addWidget(self.applyMaskBtn)
layout.addStretch(1)
- self.thresholdGroup = qt.QGroupBox('Threshold')
+ self.thresholdGroup = qt.QGroupBox("Threshold")
self.thresholdGroup.setLayout(layout)
# Init widget state
@@ -903,21 +938,23 @@ class BaseMaskToolsWidget(qt.QWidget):
def _initOtherToolsGroupBox(self):
layout = qt.QVBoxLayout()
- self.maskNanBtn = qt.QPushButton('Mask not finite values')
- self.maskNanBtn.setToolTip('Mask Not a Number and infinite values')
+ self.maskNanBtn = qt.QPushButton("Mask not finite values")
+ self.maskNanBtn.setToolTip("Mask Not a Number and infinite values")
self.maskNanBtn.clicked.connect(self._maskNotFiniteBtnClicked)
layout.addWidget(self.maskNanBtn)
layout.addStretch(1)
- self.otherToolGroup = qt.QGroupBox('Other tools')
+ self.otherToolGroup = qt.QGroupBox("Other tools")
self.otherToolGroup.setLayout(layout)
return self.otherToolGroup
def changeEvent(self, event):
"""Reset drawing action when disabling widget"""
- if (event.type() == qt.QEvent.EnabledChange and
- not self.isEnabled() and
- self.drawActionGroup.checkedAction()):
+ if (
+ event.type() == qt.QEvent.EnabledChange
+ and not self.isEnabled()
+ and self.drawActionGroup.checkedAction()
+ ):
# Disable drawing tool by reseting interaction to pan or zoom
self.plot.resetInteractiveMode()
@@ -952,20 +989,20 @@ class BaseMaskToolsWidget(qt.QWidget):
colors = numpy.empty((self._maxLevelNumber + 1, 4), dtype=numpy.float32)
# Set color
- colors[:,:3] = self._defaultOverlayColor[:3]
+ colors[:, :3] = self._defaultOverlayColor[:3]
# check if some colors has been directly set by the user
mask = numpy.equal(self._defaultColors, False)
- colors[mask,:3] = self._overlayColors[mask,:3]
+ colors[mask, :3] = self._overlayColors[mask, :3]
# Set alpha
- colors[:, -1] = alpha / 2.
+ colors[:, -1] = alpha / 2.0
# Set highlighted level color
colors[level, 3] = alpha
# Set no mask level
- colors[0] = (0., 0., 0., 0.)
+ colors[0] = (0.0, 0.0, 0.0, 0.0)
self._colormap.setColormapLUT(colors)
@@ -1007,14 +1044,14 @@ class BaseMaskToolsWidget(qt.QWidget):
def _updateColors(self, *args):
"""Rebuild mask colormap when selected level or transparency change"""
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
+ self._setMaskColors(
+ self.levelSpinBox.value(),
+ self.transparencySlider.value() / self.transparencySlider.maximum(),
+ )
self._updatePlotMask()
self._updateInteractiveMode()
def _pencilWidthChanged(self, width):
-
old = self.pencilSpinBox.blockSignals(True)
try:
self.pencilSpinBox.setValue(width)
@@ -1032,13 +1069,13 @@ class BaseMaskToolsWidget(qt.QWidget):
"""Update the current mode to the same if some cached data have to be
updated. It is the case for the color for example.
"""
- if self._drawingMode == 'rectangle':
+ if self._drawingMode == "rectangle":
self._activeRectMode()
- elif self._drawingMode == 'ellipse':
+ elif self._drawingMode == "ellipse":
self._activeEllipseMode()
- elif self._drawingMode == 'polygon':
+ elif self._drawingMode == "polygon":
self._activePolygonMode()
- elif self._drawingMode == 'pencil':
+ elif self._drawingMode == "pencil":
self._activePencilMode()
def _handleClearMask(self):
@@ -1075,30 +1112,30 @@ class BaseMaskToolsWidget(qt.QWidget):
def _activeRectMode(self):
"""Handle rect action mode triggering"""
self._releaseDrawingMode()
- self._drawingMode = 'rectangle'
+ self._drawingMode = "rectangle"
self.plot.sigPlotSignal.connect(self._plotDrawEvent)
color = self.getCurrentMaskColor()
self.plot.setInteractiveMode(
- 'draw', shape='rectangle', source=self, color=color)
+ "draw", shape="rectangle", source=self, color=color
+ )
self._updateDrawingModeWidgets()
def _activeEllipseMode(self):
"""Handle circle action mode triggering"""
self._releaseDrawingMode()
- self._drawingMode = 'ellipse'
+ self._drawingMode = "ellipse"
self.plot.sigPlotSignal.connect(self._plotDrawEvent)
color = self.getCurrentMaskColor()
- self.plot.setInteractiveMode(
- 'draw', shape='ellipse', source=self, color=color)
+ self.plot.setInteractiveMode("draw", shape="ellipse", source=self, color=color)
self._updateDrawingModeWidgets()
def _activePolygonMode(self):
"""Handle polygon action mode triggering"""
self._releaseDrawingMode()
- self._drawingMode = 'polygon'
+ self._drawingMode = "polygon"
self.plot.sigPlotSignal.connect(self._plotDrawEvent)
color = self.getCurrentMaskColor()
- self.plot.setInteractiveMode('draw', shape='polygon', source=self, color=color)
+ self.plot.setInteractiveMode("draw", shape="polygon", source=self, color=color)
self._updateDrawingModeWidgets()
def _getPencilWidth(self):
@@ -1111,17 +1148,18 @@ class BaseMaskToolsWidget(qt.QWidget):
def _activePencilMode(self):
"""Handle pencil action mode triggering"""
self._releaseDrawingMode()
- self._drawingMode = 'pencil'
+ self._drawingMode = "pencil"
self.plot.sigPlotSignal.connect(self._plotDrawEvent)
color = self.getCurrentMaskColor()
width = self._getPencilWidth()
self.plot.setInteractiveMode(
- 'draw', shape='pencil', source=self, color=color, width=width)
+ "draw", shape="pencil", source=self, color=color, width=width
+ )
self._updateDrawingModeWidgets()
def _updateDrawingModeWidgets(self):
self.maskStateWidget.setVisible(self._drawingMode is not None)
- self.pencilSetting.setVisible(self._drawingMode == 'pencil')
+ self.pencilSetting.setVisible(self._drawingMode == "pencil")
# Handle plot drawing events
@@ -1131,7 +1169,7 @@ class BaseMaskToolsWidget(qt.QWidget):
:rtype: bool"""
# First draw event, use current modifiers for all draw sequence
- doMask = (self.maskStateGroup.checkedId() == 1)
+ doMask = self.maskStateGroup.checkedId() == 1
if qt.QApplication.keyboardModifiers() & qt.Qt.ControlModifier:
doMask = not doMask
return doMask
@@ -1163,29 +1201,29 @@ class BaseMaskToolsWidget(qt.QWidget):
def _maskBtnClicked(self):
if self.belowThresholdAction.isChecked():
if self.minLineEdit.text():
- self._mask.updateBelowThreshold(self.levelSpinBox.value(),
- self.minLineEdit.value())
+ self._mask.updateBelowThreshold(
+ self.levelSpinBox.value(), self.minLineEdit.value()
+ )
self._mask.commit()
elif self.betweenThresholdAction.isChecked():
if self.minLineEdit.text() and self.maxLineEdit.text():
min_ = self.minLineEdit.value()
max_ = self.maxLineEdit.value()
- self._mask.updateBetweenThresholds(self.levelSpinBox.value(),
- min_, max_)
+ self._mask.updateBetweenThresholds(
+ self.levelSpinBox.value(), min_, max_
+ )
self._mask.commit()
elif self.aboveThresholdAction.isChecked():
if self.maxLineEdit.text():
max_ = float(self.maxLineEdit.value())
- self._mask.updateAboveThreshold(self.levelSpinBox.value(),
- max_)
+ self._mask.updateAboveThreshold(self.levelSpinBox.value(), max_)
self._mask.commit()
def _maskNotFiniteBtnClicked(self):
"""Handle not finite mask button clicked: mask NaNs and inf"""
- self._mask.updateNotFinite(
- self.levelSpinBox.value())
+ self._mask.updateNotFinite(self.levelSpinBox.value())
self._mask.commit()
@@ -1201,7 +1239,7 @@ class BaseMaskToolsDockWidget(qt.QDockWidget):
sigMaskChanged = qt.Signal()
- def __init__(self, parent=None, name='Mask', widget=None):
+ def __init__(self, parent=None, name="Mask", widget=None):
super(BaseMaskToolsDockWidget, self).__init__(parent)
self.setWindowTitle(name)
@@ -1255,7 +1293,7 @@ class BaseMaskToolsDockWidget(qt.QDockWidget):
See :class:`QMainWindow`.
"""
action = super(BaseMaskToolsDockWidget, self).toggleViewAction()
- action.setIcon(icons.getQIcon('image-mask'))
+ action.setIcon(icons.getQIcon("image-mask"))
action.setToolTip("Display/hide mask tools")
return action
diff --git a/src/silx/gui/plot/__init__.py b/src/silx/gui/plot/__init__.py
index 129c4de..2a1587f 100644
--- a/src/silx/gui/plot/__init__.py
+++ b/src/silx/gui/plot/__init__.py
@@ -66,5 +66,13 @@ from .ImageView import ImageView # noqa
from .StackView import StackView # noqa
from .ScatterView import ScatterView # noqa
-__all__ = ['ImageView', 'PlotWidget', 'PlotWindow', 'Plot1D', 'Plot2D',
- 'StackView', 'ScatterView', 'TickMode']
+__all__ = [
+ "ImageView",
+ "PlotWidget",
+ "PlotWindow",
+ "Plot1D",
+ "Plot2D",
+ "StackView",
+ "ScatterView",
+ "TickMode",
+]
diff --git a/src/silx/gui/plot/_utils/__init__.py b/src/silx/gui/plot/_utils/__init__.py
index 39fa7e4..3075007 100644
--- a/src/silx/gui/plot/_utils/__init__.py
+++ b/src/silx/gui/plot/_utils/__init__.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -31,11 +31,12 @@ __date__ = "21/03/2017"
import numpy
from .panzoom import FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX
-from .panzoom import applyZoomToPlot, applyPan, checkAxisLimits
+from .panzoom import applyZoomToPlot, applyPan, checkAxisLimits, EnabledAxes
-def addMarginsToLimits(margins, isXLog, isYLog,
- xMin, xMax, yMin, yMax, y2Min=None, y2Max=None):
+def addMarginsToLimits(
+ margins, isXLog, isYLog, xMin, xMax, yMin, yMax, y2Min=None, y2Max=None
+):
"""Returns updated limits by extending them with margins.
:param margins: The ratio of the margins to add or None for no margins.
@@ -55,35 +56,35 @@ def addMarginsToLimits(margins, isXLog, isYLog,
xMin -= xMinMargin * xRange
xMax += xMaxMargin * xRange
- elif xMin > 0. and xMax > 0.: # Log scale
+ elif xMin > 0.0 and xMax > 0.0: # Log scale
# Do not apply margins if limits < 0
xMinLog, xMaxLog = numpy.log10(xMin), numpy.log10(xMax)
xRangeLog = xMaxLog - xMinLog
- xMin = pow(10., xMinLog - xMinMargin * xRangeLog)
- xMax = pow(10., xMaxLog + xMaxMargin * xRangeLog)
+ xMin = pow(10.0, xMinLog - xMinMargin * xRangeLog)
+ xMax = pow(10.0, xMaxLog + xMaxMargin * xRangeLog)
if not isYLog:
yRange = yMax - yMin
yMin -= yMinMargin * yRange
yMax += yMaxMargin * yRange
- elif yMin > 0. and yMax > 0.: # Log scale
+ elif yMin > 0.0 and yMax > 0.0: # Log scale
# Do not apply margins if limits < 0
yMinLog, yMaxLog = numpy.log10(yMin), numpy.log10(yMax)
yRangeLog = yMaxLog - yMinLog
- yMin = pow(10., yMinLog - yMinMargin * yRangeLog)
- yMax = pow(10., yMaxLog + yMaxMargin * yRangeLog)
+ yMin = pow(10.0, yMinLog - yMinMargin * yRangeLog)
+ yMax = pow(10.0, yMaxLog + yMaxMargin * yRangeLog)
if y2Min is not None and y2Max is not None:
if not isYLog:
yRange = y2Max - y2Min
y2Min -= yMinMargin * yRange
y2Max += yMaxMargin * yRange
- elif y2Min > 0. and y2Max > 0.: # Log scale
+ elif y2Min > 0.0 and y2Max > 0.0: # Log scale
# Do not apply margins if limits < 0
yMinLog, yMaxLog = numpy.log10(y2Min), numpy.log10(y2Max)
yRangeLog = yMaxLog - yMinLog
- y2Min = pow(10., yMinLog - yMinMargin * yRangeLog)
- y2Max = pow(10., yMaxLog + yMaxMargin * yRangeLog)
+ y2Min = pow(10.0, yMinLog - yMinMargin * yRangeLog)
+ y2Max = pow(10.0, yMaxLog + yMaxMargin * yRangeLog)
if y2Min is None or y2Max is None:
return xMin, xMax, yMin, yMax
diff --git a/src/silx/gui/plot/_utils/delaunay.py b/src/silx/gui/plot/_utils/delaunay.py
deleted file mode 100644
index 48b0db7..0000000
--- a/src/silx/gui/plot/_utils/delaunay.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# /*##########################################################################
-#
-# Copyright (c) 2019 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
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""Wrapper over Delaunay implementation"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "02/05/2019"
-
-
-import logging
-import sys
-
-import numpy
-
-
-_logger = logging.getLogger(__name__)
-
-
-def delaunay(x, y):
- """Returns Delaunay instance for x, y points
-
- :param numpy.ndarray x:
- :param numpy.ndarray y:
- :rtype: Union[None,scipy.spatial.Delaunay]
- """
- # Lazy-loading of Delaunay
- try:
- from scipy.spatial import Delaunay as _Delaunay
- except ImportError: # Fallback using local Delaunay
- from silx.third_party.scipy_spatial import Delaunay as _Delaunay
-
- points = numpy.array((x, y)).T
- try:
- delaunay = _Delaunay(points)
- except (RuntimeError, ValueError):
- _logger.debug("Delaunay tesselation failed: %s", sys.exc_info()[1])
- delaunay = None
-
- return delaunay
diff --git a/src/silx/gui/plot/_utils/dtime_ticklayout.py b/src/silx/gui/plot/_utils/dtime_ticklayout.py
index 3c355d7..ba0fda7 100644
--- a/src/silx/gui/plot/_utils/dtime_ticklayout.py
+++ b/src/silx/gui/plot/_utils/dtime_ticklayout.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2014-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 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
@@ -21,6 +21,8 @@
# THE SOFTWARE.
#
# ###########################################################################*/
+from __future__ import annotations
+
"""This module implements date-time labels layout on graph axes."""
__authors__ = ["P. Kenter"]
@@ -28,6 +30,7 @@ __license__ = "MIT"
__date__ = "04/04/2018"
+from collections.abc import Sequence
import datetime as dt
import enum
import logging
@@ -48,14 +51,15 @@ SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE
SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
SECONDS_PER_YEAR = 365.25 * SECONDS_PER_DAY
-SECONDS_PER_MONTH_AVERAGE = SECONDS_PER_YEAR / 12 # Seconds per average month
+SECONDS_PER_MONTH_AVERAGE = SECONDS_PER_YEAR / 12 # Seconds per average month
# No dt.timezone in Python 2.7 so we use dateutil.tz.tzutc
_EPOCH = dt.datetime(1970, 1, 1, tzinfo=dateutil.tz.tzutc())
+
def timestamp(dtObj):
- """ Returns POSIX timestamp of a datetime objects.
+ """Returns POSIX timestamp of a datetime objects.
If the dtObj object has a timestamp() method (python 3.3), this is
used. Otherwise (e.g. python 2.7) it is calculated here.
@@ -73,9 +77,22 @@ def timestamp(dtObj):
else:
# Back ported from Python 3.5
if dtObj.tzinfo is None:
- return time.mktime((dtObj.year, dtObj.month, dtObj.day,
- dtObj.hour, dtObj.minute, dtObj.second,
- -1, -1, -1)) + dtObj.microsecond / 1e6
+ return (
+ time.mktime(
+ (
+ dtObj.year,
+ dtObj.month,
+ dtObj.day,
+ dtObj.hour,
+ dtObj.minute,
+ dtObj.second,
+ -1,
+ -1,
+ -1,
+ )
+ )
+ + dtObj.microsecond / 1e6
+ )
else:
return (dtObj - _EPOCH).total_seconds()
@@ -92,7 +109,7 @@ class DtUnit(enum.Enum):
def getDateElement(dateTime, unit):
- """ Picks the date element with the unit from the dateTime
+ """Picks the date element with the unit from the dateTime
E.g. getDateElement(datetime(1970, 5, 6), DtUnit.Day) will return 6
@@ -118,7 +135,7 @@ def getDateElement(dateTime, unit):
def setDateElement(dateTime, value, unit):
- """ Returns a copy of dateTime with the tickStep unit set to value
+ """Returns a copy of dateTime with the tickStep unit set to value
:param datetime.datetime: date time object
:param int value: value to set
@@ -126,8 +143,9 @@ def setDateElement(dateTime, value, unit):
:return: datetime.datetime
"""
intValue = int(value)
- _logger.debug("setDateElement({}, {} (int={}), {})"
- .format(dateTime, value, intValue, unit))
+ _logger.debug(
+ "setDateElement({}, {} (int={}), {})".format(dateTime, value, intValue, unit)
+ )
year = dateTime.year
month = dateTime.month
@@ -154,16 +172,19 @@ def setDateElement(dateTime, value, unit):
else:
raise ValueError("Unexpected DtUnit: {}".format(unit))
- _logger.debug("creating date time {}"
- .format((year, month, day, hour, minute, second, microsecond)))
-
- return dt.datetime(year, month, day, hour, minute, second, microsecond,
- tzinfo=dateTime.tzinfo)
+ _logger.debug(
+ "creating date time {}".format(
+ (year, month, day, hour, minute, second, microsecond)
+ )
+ )
+ return dt.datetime(
+ year, month, day, hour, minute, second, microsecond, tzinfo=dateTime.tzinfo
+ )
def roundToElement(dateTime, unit):
- """ Returns a copy of dateTime rounded to given unit
+ """Returns a copy of dateTime rounded to given unit
:param datetime.datetime: date time object
:param DtUnit unit: unit
@@ -178,7 +199,7 @@ def roundToElement(dateTime, unit):
microsecond = dateTime.microsecond
if unit.value < DtUnit.YEARS.value:
- pass # Never round years
+ pass # Never round years
if unit.value < DtUnit.MONTHS.value:
month = 1
if unit.value < DtUnit.DAYS.value:
@@ -192,14 +213,15 @@ def roundToElement(dateTime, unit):
if unit.value < DtUnit.MICRO_SECONDS.value:
microsecond = 0
- result = dt.datetime(year, month, day, hour, minute, second, microsecond,
- tzinfo=dateTime.tzinfo)
+ result = dt.datetime(
+ year, month, day, hour, minute, second, microsecond, tzinfo=dateTime.tzinfo
+ )
return result
def addValueToDate(dateTime, value, unit):
- """ Adds a value with unit to a dateTime.
+ """Adds a value with unit to a dateTime.
Uses dateutil.relativedelta.relativedelta from the standard library to do
the actual math. This function doesn't allow for fractional month or years,
@@ -211,13 +233,13 @@ def addValueToDate(dateTime, value, unit):
:return:
:raises ValueError: unit is unsupported or result is out of datetime bounds
"""
- #logger.debug("addValueToDate({}, {}, {})".format(dateTime, value, unit))
+ # logger.debug("addValueToDate({}, {}, {})".format(dateTime, value, unit))
if unit == DtUnit.YEARS:
- intValue = int(value) # floats not implemented in relativeDelta(years)
+ intValue = int(value) # floats not implemented in relativeDelta(years)
return dateTime + relativedelta(years=intValue)
elif unit == DtUnit.MONTHS:
- intValue = int(value) # floats not implemented in relativeDelta(mohths)
+ intValue = int(value) # floats not implemented in relativeDelta(mohths)
return dateTime + relativedelta(months=intValue)
elif unit == DtUnit.DAYS:
return dateTime + relativedelta(days=value)
@@ -234,7 +256,7 @@ def addValueToDate(dateTime, value, unit):
def bestUnit(durationInSeconds):
- """ Gets the best tick spacing given a duration in seconds.
+ """Gets the best tick spacing given a duration in seconds.
:param durationInSeconds: time span duration in seconds
:return: DtUnit enumeration.
@@ -264,8 +286,7 @@ def bestUnit(durationInSeconds):
elif durationInSeconds > 1 * 2:
return (durationInSeconds, DtUnit.SECONDS)
else:
- return (durationInSeconds * MICROSECONDS_PER_SECOND,
- DtUnit.MICRO_SECONDS)
+ return (durationInSeconds * MICROSECONDS_PER_SECOND, DtUnit.MICRO_SECONDS)
NICE_DATE_VALUES = {
@@ -275,12 +296,12 @@ NICE_DATE_VALUES = {
DtUnit.HOURS: [1, 2, 3, 4, 6, 12],
DtUnit.MINUTES: [1, 2, 3, 5, 10, 15, 30],
DtUnit.SECONDS: [1, 2, 3, 5, 10, 15, 30],
- DtUnit.MICRO_SECONDS : [1.0, 2.0, 5.0, 10.0], # floats for microsec
+ DtUnit.MICRO_SECONDS: [1.0, 2.0, 3.0, 4.0, 5.0, 10.0], # floats for microsec
}
def bestFormatString(spacing, unit):
- """ Finds the best format string given the spacing and DtUnit.
+ """Finds the best format string given the spacing and DtUnit.
If the spacing is a fractional number < 1 the format string will take this
into account
@@ -310,8 +331,31 @@ def bestFormatString(spacing, unit):
raise ValueError("Unexpected DtUnit: {}".format(unit))
+def formatDatetimes(
+ datetimes: Sequence[dt.datetime], spacing: int | None, unit: DtUnit | None
+) -> dict[dt.datetime, str]:
+ """Returns formatted string for each datetime according to tick spacing and time unit"""
+ if spacing is None or unit is None:
+ # Locator has no spacing or units yet: Use elaborate fmtString
+ return {
+ datetime: datetime.strftime("Y-%m-%d %H:%M:%S") for datetime in datetimes
+ }
+
+ formatString = bestFormatString(spacing, unit)
+ if unit != DtUnit.MICRO_SECONDS:
+ return {datetime: datetime.strftime(formatString) for datetime in datetimes}
+
+ # For microseconds: Strip leading/trailing zeros
+ texts = tuple(datetime.strftime(formatString) for datetime in datetimes)
+ nzeros = min(len(text) - len(text.rstrip("0")) for text in texts)
+ return {
+ datetime: text[0 if text[0] != "0" else 1 : -min(nzeros, 5)]
+ for datetime, text in zip(datetimes, texts)
+ }
+
+
def niceDateTimeElement(value, unit, isRound=False):
- """ Uses the Nice Numbers algorithm to determine a nice value.
+ """Uses the Nice Numbers algorithm to determine a nice value.
The fractions are optimized for the unit of the date element.
"""
@@ -326,10 +370,8 @@ def niceDateTimeElement(value, unit, isRound=False):
def findStartDate(dMin, dMax, nTicks):
- """ Rounds a date down to the nearest nice number of ticks
- """
- assert dMax >= dMin, \
- "dMin ({}) should come before dMax ({})".format(dMin, dMax)
+ """Rounds a date down to the nearest nice number of ticks"""
+ assert dMax >= dMin, "dMin ({}) should come before dMax ({})".format(dMin, dMax)
if dMin == dMax:
# Fallback when range is smaller than microsecond resolution
@@ -337,34 +379,42 @@ def findStartDate(dMin, dMax, nTicks):
delta = dMax - dMin
lengthSec = delta.total_seconds()
- _logger.debug("findStartDate: {}, {} (duration = {} sec, {} days)"
- .format(dMin, dMax, lengthSec, lengthSec / SECONDS_PER_DAY))
+ _logger.debug(
+ "findStartDate: {}, {} (duration = {} sec, {} days)".format(
+ dMin, dMax, lengthSec, lengthSec / SECONDS_PER_DAY
+ )
+ )
length, unit = bestUnit(lengthSec)
niceLength = niceDateTimeElement(length, unit)
- _logger.debug("Length: {:8.3f} {} (nice = {})"
- .format(length, unit.name, niceLength))
+ _logger.debug(
+ "Length: {:8.3f} {} (nice = {})".format(length, unit.name, niceLength)
+ )
niceSpacing = niceDateTimeElement(niceLength / nTicks, unit, isRound=True)
- _logger.debug("Spacing: {:8.3f} {} (nice = {})"
- .format(niceLength / nTicks, unit.name, niceSpacing))
+ _logger.debug(
+ "Spacing: {:8.3f} {} (nice = {})".format(
+ niceLength / nTicks, unit.name, niceSpacing
+ )
+ )
dVal = getDateElement(dMin, unit)
- if unit == DtUnit.MONTHS: # TODO: better rounding?
- niceVal = math.floor((dVal-1) / niceSpacing) * niceSpacing + 1
+ if unit == DtUnit.MONTHS: # TODO: better rounding?
+ niceVal = math.floor((dVal - 1) / niceSpacing) * niceSpacing + 1
elif unit == DtUnit.DAYS:
- niceVal = math.floor((dVal-1) / niceSpacing) * niceSpacing + 1
+ niceVal = math.floor((dVal - 1) / niceSpacing) * niceSpacing + 1
else:
niceVal = math.floor(dVal / niceSpacing) * niceSpacing
if unit == DtUnit.YEARS and niceVal <= dt.MINYEAR:
niceVal = max(1, niceSpacing)
- _logger.debug("StartValue: dVal = {}, niceVal: {} ({})"
- .format(dVal, niceVal, unit.name))
+ _logger.debug(
+ "StartValue: dVal = {}, niceVal: {} ({})".format(dVal, niceVal, unit.name)
+ )
startDate = roundToElement(dMin, unit)
startDate = setDateElement(startDate, niceVal, unit)
@@ -372,8 +422,8 @@ def findStartDate(dMin, dMax, nTicks):
return startDate, niceSpacing, unit
-def dateRange(dMin, dMax, step, unit, includeFirstBeyond = False):
- """ Generates a range of dates
+def dateRange(dMin, dMax, step, unit, includeFirstBeyond=False):
+ """Generates a range of dates
:param datetime dMin: start date
:param datetime dMax: end date
@@ -384,8 +434,7 @@ def dateRange(dMin, dMax, step, unit, includeFirstBeyond = False):
datetime will always be smaller than dMax.
:return:
"""
- if (unit == DtUnit.YEARS or unit == DtUnit.MONTHS or
- unit == DtUnit.MICRO_SECONDS):
+ if unit == DtUnit.YEARS or unit == DtUnit.MONTHS or unit == DtUnit.MICRO_SECONDS:
# No support for fractional month or year and resolution is microsecond
# In those cases, make sure the step is at least 1
step = max(1, step)
@@ -404,7 +453,6 @@ def dateRange(dMin, dMax, step, unit, includeFirstBeyond = False):
yield dateTime
-
def calcTicks(dMin, dMax, nTicks):
"""Returns tick positions.
@@ -414,27 +462,19 @@ def calcTicks(dMin, dMax, nTicks):
ticks may differ.
:returns: (list of datetimes, DtUnit) tuple
"""
- _logger.debug("Calc calcTicks({}, {}, nTicks={})"
- .format(dMin, dMax, nTicks))
+ _logger.debug("Calc calcTicks({}, {}, nTicks={})".format(dMin, dMax, nTicks))
startDate, niceSpacing, unit = findStartDate(dMin, dMax, nTicks)
result = []
- for d in dateRange(startDate, dMax, niceSpacing, unit,
- includeFirstBeyond=True):
+ for d in dateRange(startDate, dMax, niceSpacing, unit, includeFirstBeyond=True):
result.append(d)
return result, niceSpacing, unit
def calcTicksAdaptive(dMin, dMax, axisLength, tickDensity):
- """ Calls calcTicks with a variable number of ticks, depending on axisLength
- """
+ """Calls calcTicks with a variable number of ticks, depending on axisLength"""
# At least 2 ticks
nticks = max(2, int(round(tickDensity * axisLength)))
- return calcTicks(dMin, dMax, nticks)
-
-
-
-
-
+ return calcTicks(dMin, dMax, nticks)
diff --git a/src/silx/gui/plot/_utils/panzoom.py b/src/silx/gui/plot/_utils/panzoom.py
index 8592ad0..cac591d 100644
--- a/src/silx/gui/plot/_utils/panzoom.py
+++ b/src/silx/gui/plot/_utils/panzoom.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -23,6 +23,8 @@
# ###########################################################################*/
"""Functions to apply pan and zoom on a Plot"""
+from __future__ import annotations
+
__authors__ = ["T. Vincent", "V. Valls"]
__license__ = "MIT"
__date__ = "08/08/2017"
@@ -30,6 +32,7 @@ __date__ = "08/08/2017"
import logging
import math
+from typing import NamedTuple
import numpy
@@ -46,11 +49,11 @@ FLOAT32_SAFE_MAX = 1e37
# TODO double support
-def checkAxisLimits(vmin, vmax, isLog: bool=False, name: str=""):
+def checkAxisLimits(vmin: float, vmax: float, isLog: bool = False, name: str = ""):
"""Makes sure axis range is not empty and within supported range.
- :param float vmin: Min axis value
- :param float vmax: Max axis value
+ :param vmin: Min axis value
+ :param vmax: Max axis value
:return: (min, max) making sure min < max
:rtype: 2-tuple of float
"""
@@ -59,11 +62,11 @@ def checkAxisLimits(vmin, vmax, isLog: bool=False, name: str=""):
vmin = numpy.clip(vmin, min_, FLOAT32_SAFE_MAX)
if vmax < vmin:
- _logger.debug('%s axis: max < min, inverting limits.', name)
+ _logger.debug("%s axis: max < min, inverting limits.", name)
vmin, vmax = vmax, vmin
elif vmax == vmin:
- _logger.debug('%s axis: max == min, expanding limits.', name)
- if vmin == 0.:
+ _logger.debug("%s axis: max == min, expanding limits.", name)
+ if vmin == 0.0:
vmin, vmax = -0.1, 0.1
elif vmin < 0:
vmax *= 0.9
@@ -75,26 +78,27 @@ def checkAxisLimits(vmin, vmax, isLog: bool=False, name: str=""):
return vmin, vmax
-def scale1DRange(min_, max_, center, scale, isLog):
+def scale1DRange(
+ min_: float, max_: float, center: float, scale: float, isLog: bool
+) -> tuple[float, float]:
"""Scale a 1D range given a scale factor and an center point.
Keeps the values in a smaller range than float32.
- :param float min_: The current min value of the range.
- :param float max_: The current max value of the range.
- :param float center: The center of the zoom (i.e., invariant point).
- :param float scale: The scale to use for zoom
- :param bool isLog: Whether using log scale or not.
- :return: The zoomed range.
- :rtype: tuple of 2 floats: (min, max)
+ :param min_: The current min value of the range.
+ :param max_: The current max value of the range.
+ :param center: The center of the zoom (i.e., invariant point).
+ :param scale: The scale to use for zoom
+ :param isLog: Whether using log scale or not.
+ :return: The zoomed range (min, max)
"""
if isLog:
# Min and center can be < 0 when
# autoscale is off and switch to log scale
# max_ < 0 should not happen
- min_ = numpy.log10(min_) if min_ > 0. else FLOAT32_MINPOS
- center = numpy.log10(center) if center > 0. else FLOAT32_MINPOS
- max_ = numpy.log10(max_) if max_ > 0. else FLOAT32_MINPOS
+ min_ = numpy.log10(min_) if min_ > 0.0 else FLOAT32_MINPOS
+ center = numpy.log10(center) if center > 0.0 else FLOAT32_MINPOS
+ max_ = numpy.log10(max_) if max_ > 0.0 else FLOAT32_MINPOS
if min_ == max_:
return min_, max_
@@ -102,12 +106,12 @@ def scale1DRange(min_, max_, center, scale, isLog):
offset = (center - min_) / (max_ - min_)
range_ = (max_ - min_) / scale
newMin = center - offset * range_
- newMax = center + (1. - offset) * range_
+ newMax = center + (1.0 - offset) * range_
if isLog:
# No overflow as exponent is log10 of a float32
- newMin = pow(10., newMin)
- newMax = pow(10., newMax)
+ newMin = pow(10.0, newMin)
+ newMax = pow(10.0, newMax)
newMin = numpy.clip(newMin, FLOAT32_MINPOS, FLOAT32_SAFE_MAX)
newMax = numpy.clip(newMax, FLOAT32_MINPOS, FLOAT32_SAFE_MAX)
else:
@@ -116,16 +120,34 @@ def scale1DRange(min_, max_, center, scale, isLog):
return newMin, newMax
-def applyZoomToPlot(plot, scaleF, center=None):
+class EnabledAxes(NamedTuple):
+ """Toggle zoom for each axis"""
+
+ xaxis: bool = True
+ yaxis: bool = True
+ y2axis: bool = True
+
+ def isDisabled(self) -> bool:
+ """True only if all axes are disabled"""
+ return not (self.xaxis or self.yaxis or self.y2axis)
+
+
+def applyZoomToPlot(
+ plot,
+ scale: float,
+ center: tuple[float, float] = None,
+ enabled: EnabledAxes = EnabledAxes(),
+):
"""Zoom in/out plot given a scale and a center point.
:param plot: The plot on which to apply zoom.
- :param float scaleF: Scale factor of zoom.
+ :param scale: Scale factor of zoom.
:param center: (x, y) coords in pixel coordinates of the zoom center.
- :type center: 2-tuple of float
+ :param enabled: Toggle zoom for each axis independently
"""
xMin, xMax = plot.getXAxis().getLimits()
yMin, yMax = plot.getYAxis().getLimits()
+ y2Min, y2Max = plot.getYAxis(axis="right").getLimits()
if center is None:
left, top, width, height = plot.getPlotBoundsInPixels()
@@ -136,18 +158,23 @@ def applyZoomToPlot(plot, scaleF, center=None):
dataCenterPos = plot.pixelToData(cx, cy)
assert dataCenterPos is not None
- xMin, xMax = scale1DRange(xMin, xMax, dataCenterPos[0], scaleF,
- plot.getXAxis()._isLogarithmic())
+ if enabled.xaxis:
+ xMin, xMax = scale1DRange(
+ xMin, xMax, dataCenterPos[0], scale, plot.getXAxis()._isLogarithmic()
+ )
- yMin, yMax = scale1DRange(yMin, yMax, dataCenterPos[1], scaleF,
- plot.getYAxis()._isLogarithmic())
+ if enabled.yaxis:
+ yMin, yMax = scale1DRange(
+ yMin, yMax, dataCenterPos[1], scale, plot.getYAxis()._isLogarithmic()
+ )
- dataPos = plot.pixelToData(cx, cy, axis="right")
- assert dataPos is not None
- y2Center = dataPos[1]
- y2Min, y2Max = plot.getYAxis(axis="right").getLimits()
- y2Min, y2Max = scale1DRange(y2Min, y2Max, y2Center, scaleF,
- plot.getYAxis()._isLogarithmic())
+ if enabled.y2axis:
+ dataPos = plot.pixelToData(cx, cy, axis="right")
+ assert dataPos is not None
+ y2Center = dataPos[1]
+ y2Min, y2Max = scale1DRange(
+ y2Min, y2Max, y2Center, scale, plot.getYAxis()._isLogarithmic()
+ )
plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
@@ -166,15 +193,15 @@ def applyPan(min_, max_, panFactor, isLog10):
:return: New min and max value with pan applied.
:rtype: 2-tuple of float.
"""
- if isLog10 and min_ > 0.:
+ if isLog10 and min_ > 0.0:
# Negative range and log scale can happen with matplotlib
logMin, logMax = math.log10(min_), math.log10(max_)
logOffset = panFactor * (logMax - logMin)
- newMin = pow(10., logMin + logOffset)
- newMax = pow(10., logMax + logOffset)
+ newMin = pow(10.0, logMin + logOffset)
+ newMax = pow(10.0, logMax + logOffset)
# Takes care of out-of-range values
- if newMin > 0. and newMax < float('inf'):
+ if newMin > 0.0 and newMax < float("inf"):
min_, max_ = newMin, newMax
else:
@@ -182,13 +209,14 @@ def applyPan(min_, max_, panFactor, isLog10):
newMin, newMax = min_ + offset, max_ + offset
# Takes care of out-of-range values
- if newMin > - float('inf') and newMax < float('inf'):
+ if newMin > -float("inf") and newMax < float("inf"):
min_, max_ = newMin, newMax
return min_, max_
class _Unset(object):
"""To be able to have distinction between None and unset"""
+
pass
@@ -203,10 +231,17 @@ class ViewConstraints(object):
self._minRange = [None, None]
self._maxRange = [None, None]
- def update(self, xMin=_Unset, xMax=_Unset,
- yMin=_Unset, yMax=_Unset,
- minXRange=_Unset, maxXRange=_Unset,
- minYRange=_Unset, maxYRange=_Unset):
+ def update(
+ self,
+ xMin=_Unset,
+ xMax=_Unset,
+ yMin=_Unset,
+ yMax=_Unset,
+ minXRange=_Unset,
+ maxXRange=_Unset,
+ minYRange=_Unset,
+ maxYRange=_Unset,
+ ):
"""
Update the constraints managed by the object
@@ -238,7 +273,6 @@ class ViewConstraints(object):
maxPos = [xMax, yMax]
for axis in range(2):
-
value = minPos[axis]
if value is not _Unset and value != self._min[axis]:
self._min[axis] = value
@@ -262,7 +296,11 @@ class ViewConstraints(object):
# Sanity checks
for axis in range(2):
- if self._maxRange[axis] is not None and self._min[axis] is not None and self._max[axis] is not None:
+ if (
+ self._maxRange[axis] is not None
+ and self._min[axis] is not None
+ and self._max[axis] is not None
+ ):
# max range cannot be larger than bounds
diff = self._max[axis] - self._min[axis]
self._maxRange[axis] = min(self._maxRange[axis], diff)
@@ -298,8 +336,12 @@ class ViewConstraints(object):
viewRange[axis][1] += delta * 0.5
# clamp min and max positions
- outMin = self._min[axis] is not None and viewRange[axis][0] < self._min[axis]
- outMax = self._max[axis] is not None and viewRange[axis][1] > self._max[axis]
+ outMin = (
+ self._min[axis] is not None and viewRange[axis][0] < self._min[axis]
+ )
+ outMax = (
+ self._max[axis] is not None and viewRange[axis][1] > self._max[axis]
+ )
if outMin and outMax:
if allow_scaling:
diff --git a/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py b/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py
index 87c0742..adcb9c9 100644
--- a/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py
+++ b/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py
@@ -57,7 +57,6 @@ def testNoCrash():
value = 100e-6 # Start at 100 micro sec range.
while value <= 200 * SECONDS_PER_YEAR:
-
d2 = d1 + dt.timedelta(microseconds=value * 1e6) # end date range
for numTicks in range(2, 12):
diff --git a/src/silx/gui/plot/_utils/test/test_ticklayout.py b/src/silx/gui/plot/_utils/test/test_ticklayout.py
index 8388c7e..1413563 100644
--- a/src/silx/gui/plot/_utils/test/test_ticklayout.py
+++ b/src/silx/gui/plot/_utils/test/test_ticklayout.py
@@ -27,7 +27,6 @@ __license__ = "MIT"
__date__ = "17/01/2018"
-import unittest
import numpy
from silx.utils.testutils import ParametricTestCase
@@ -41,10 +40,10 @@ class TestTickLayout(ParametricTestCase):
def testTicks(self):
"""Test of :func:`ticks`"""
tests = { # (vmin, vmax): ref_ticks
- (1., 1.): (1.,),
+ (1.0, 1.0): (1.0,),
(0.5, 10.5): (2.0, 4.0, 6.0, 8.0, 10.0),
- (0.001, 0.005): (0.001, 0.002, 0.003, 0.004, 0.005)
- }
+ (0.001, 0.005): (0.001, 0.002, 0.003, 0.004, 0.005),
+ }
for (vmin, vmax), ref_ticks in tests.items():
with self.subTest(vmin=vmin, vmax=vmax):
@@ -55,9 +54,9 @@ class TestTickLayout(ParametricTestCase):
"""Minimalistic tests of :func:`niceNumbers`"""
tests = { # (vmin, vmax): ref_ticks
(0.5, 10.5): (0.0, 12.0, 2.0, 0),
- (10000., 10000.5): (10000.0, 10000.5, 0.1, 1),
- (0.001, 0.005): (0.001, 0.005, 0.001, 3)
- }
+ (10000.0, 10000.5): (10000.0, 10000.5, 0.1, 1),
+ (0.001, 0.005): (0.001, 0.005, 0.001, 3),
+ }
for (vmin, vmax), ref_ticks in tests.items():
with self.subTest(vmin=vmin, vmax=vmax):
@@ -67,9 +66,9 @@ class TestTickLayout(ParametricTestCase):
def testNiceNumbersLog(self):
"""Minimalistic tests of :func:`niceNumbersForLog10`"""
tests = { # (log10(min), log10(max): ref_ticks
- (0., 3.): (0, 3, 1, 0),
- (-3., 3): (-3, 3, 1, 0),
- (-32., 0.): (-36, 0, 6, 0)
+ (0.0, 3.0): (0, 3, 1, 0),
+ (-3.0, 3): (-3, 3, 1, 0),
+ (-32.0, 0.0): (-36, 0, 6, 0),
}
for (vmin, vmax), ref_ticks in tests.items():
diff --git a/src/silx/gui/plot/_utils/ticklayout.py b/src/silx/gui/plot/_utils/ticklayout.py
index 4266be0..3678270 100644
--- a/src/silx/gui/plot/_utils/ticklayout.py
+++ b/src/silx/gui/plot/_utils/ticklayout.py
@@ -33,6 +33,7 @@ import math
# utils #######################################################################
+
def numberOfDigits(tickSpacing):
"""Returns the number of digits to display for text label.
@@ -76,7 +77,7 @@ def numberOfDigits(tickSpacing):
def niceNumGeneric(value, niceFractions=None, isRound=False):
- """ A more generic implementation of the _niceNum function
+ """A more generic implementation of the _niceNum function
Allows the user to specify the fractions instead of using a hardcoded
list of [1, 2, 5, 10.0].
@@ -85,15 +86,15 @@ def niceNumGeneric(value, niceFractions=None, isRound=False):
return value
if niceFractions is None: # Use default values
- niceFractions = 1., 2., 5., 10.
- roundFractions = (1.5, 3., 7., 10.) if isRound else niceFractions
+ niceFractions = 1.0, 2.0, 5.0, 10.0
+ roundFractions = (1.5, 3.0, 7.0, 10.0) if isRound else niceFractions
else:
roundFractions = list(niceFractions)
if isRound:
# Take the average with the next element. The last remains the same.
for i in range(len(roundFractions) - 1):
- roundFractions[i] = (niceFractions[i] + niceFractions[i+1]) / 2
+ roundFractions[i] = (niceFractions[i] + niceFractions[i + 1]) / 2
highest = niceFractions[-1]
value = float(value)
@@ -133,7 +134,7 @@ def niceNumbers(vMin, vMax, nTicks=5):
def _frange(start, stop, step):
"""range for float (including stop)."""
- assert step >= 0.
+ assert step >= 0.0
while start <= stop:
yield start
start += step
@@ -166,7 +167,7 @@ def ticks(vMin, vMax, nbTicks=5):
nfrac = numberOfDigits(vMax - vMin)
# Generate labels
- format_ = '%g' if nfrac == 0 else '%.{}f'.format(nfrac)
+ format_ = "%g" if nfrac == 0 else "%.{}f".format(nfrac)
labels = [format_ % tick for tick in positions]
return positions, labels
@@ -194,6 +195,7 @@ def niceNumbersAdaptative(vMin, vMax, axisLength, tickDensity):
# Nice Numbers for log scale ##################################################
+
def niceNumbersForLog10(minLog, maxLog, nTicks=5):
"""Return tick positions for logarithmic scale
@@ -209,7 +211,7 @@ def niceNumbersForLog10(minLog, maxLog, nTicks=5):
rangelog = graphmaxlog - graphminlog
if rangelog <= nTicks:
- spacing = 1.
+ spacing = 1.0
else:
spacing = math.floor(rangelog / nTicks)
diff --git a/src/silx/gui/plot/actions/PlotAction.py b/src/silx/gui/plot/actions/PlotAction.py
index de041dc..9341bdd 100644
--- a/src/silx/gui/plot/actions/PlotAction.py
+++ b/src/silx/gui/plot/actions/PlotAction.py
@@ -31,26 +31,36 @@ __license__ = "MIT"
__date__ = "03/01/2018"
+from typing import Callable, Optional, Union
import weakref
from silx.gui import icons
from silx.gui import qt
+from silx.gui.plot import PlotWidget
class PlotAction(qt.QAction):
"""Base class for QAction that operates on a PlotWidget.
:param plot: :class:`.PlotWidget` instance on which to operate.
- :param icon: QIcon or str name of icon to use
- :param str text: The name of this action to be used for menu label
- :param str tooltip: The text of the tooltip
+ :param icon: QIcon or name of icon to use
+ :param text: The name of this action to be used for menu label
+ :param tooltip: The text of the tooltip
:param triggered: The callback to connect to the action's triggered
- signal or None for no callback.
- :param bool checkable: True for checkable action, False otherwise (default)
+ signal. None for no callback (default)
+ :param checkable: True for checkable action, False otherwise (default)
:param parent: See :class:`QAction`.
"""
- def __init__(self, plot, icon, text, tooltip=None,
- triggered=None, checkable=False, parent=None):
+ def __init__(
+ self,
+ plot: PlotWidget,
+ icon: Union[str, qt.QIcon],
+ text: str,
+ tooltip: Optional[str] = None,
+ triggered: Optional[Callable] = None,
+ checkable: bool = False,
+ parent: Optional[qt.QObject] = None,
+ ):
assert plot is not None
self._plotRef = weakref.ref(plot)
diff --git a/src/silx/gui/plot/actions/PlotToolAction.py b/src/silx/gui/plot/actions/PlotToolAction.py
index 8c3b3c2..479d7c2 100644
--- a/src/silx/gui/plot/actions/PlotToolAction.py
+++ b/src/silx/gui/plot/actions/PlotToolAction.py
@@ -41,16 +41,26 @@ class PlotToolAction(PlotAction):
"""Base class for QAction that maintain a tool window operating on a
PlotWidget."""
- def __init__(self, plot, icon, text, tooltip=None,
- triggered=None, checkable=False, parent=None):
- PlotAction.__init__(self,
- plot=plot,
- icon=icon,
- text=text,
- tooltip=tooltip,
- triggered=self._triggered,
- parent=parent,
- checkable=True)
+ def __init__(
+ self,
+ plot,
+ icon,
+ text,
+ tooltip=None,
+ triggered=None,
+ checkable=False,
+ parent=None,
+ ):
+ PlotAction.__init__(
+ self,
+ plot=plot,
+ icon=icon,
+ text=text,
+ tooltip=tooltip,
+ triggered=self._triggered,
+ parent=parent,
+ checkable=True,
+ )
self._previousGeometry = None
self._toolWindow = None
diff --git a/src/silx/gui/plot/actions/control.py b/src/silx/gui/plot/actions/control.py
index e75048a..c21d235 100755
--- a/src/silx/gui/plot/actions/control.py
+++ b/src/silx/gui/plot/actions/control.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -55,6 +55,7 @@ from silx.gui.plot import items
from silx.gui.plot._utils import applyZoomToPlot as _applyZoomToPlot
from silx.gui import qt
from silx.gui import icons
+from silx.utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
@@ -68,10 +69,14 @@ class ResetZoomAction(PlotAction):
def __init__(self, plot, parent=None):
super(ResetZoomAction, self).__init__(
- plot, icon='zoom-original', text='Reset Zoom',
- tooltip='Auto-scale the graph',
+ plot,
+ icon="zoom-original",
+ text="Reset Zoom",
+ tooltip="Auto-scale the graph",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self._autoscaleChanged(True)
plot.getXAxis().sigAutoScaleChanged.connect(self._autoscaleChanged)
plot.getYAxis().sigAutoScaleChanged.connect(self._autoscaleChanged)
@@ -82,13 +87,13 @@ class ResetZoomAction(PlotAction):
self.setEnabled(xAxis.isAutoScale() or yAxis.isAutoScale())
if xAxis.isAutoScale() and yAxis.isAutoScale():
- tooltip = 'Auto-scale the graph'
+ tooltip = "Auto-scale the graph"
elif xAxis.isAutoScale(): # And not Y axis
- tooltip = 'Auto-scale the x-axis of the graph only'
+ tooltip = "Auto-scale the x-axis of the graph only"
elif yAxis.isAutoScale(): # And not X axis
- tooltip = 'Auto-scale the y-axis of the graph only'
+ tooltip = "Auto-scale the y-axis of the graph only"
else: # no axis in autoscale
- tooltip = 'Auto-scale the graph'
+ tooltip = "Auto-scale the graph"
self.setToolTip(tooltip)
def _actionTriggered(self, checked=False):
@@ -104,10 +109,14 @@ class ZoomBackAction(PlotAction):
def __init__(self, plot, parent=None):
super(ZoomBackAction, self).__init__(
- plot, icon='zoom-back', text='Zoom Back',
- tooltip='Zoom back the plot',
+ plot,
+ icon="zoom-back",
+ text="Zoom Back",
+ tooltip="Zoom back the plot",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcutContext(qt.Qt.WidgetShortcut)
def _actionTriggered(self, checked=False):
@@ -123,10 +132,14 @@ class ZoomInAction(PlotAction):
def __init__(self, plot, parent=None):
super(ZoomInAction, self).__init__(
- plot, icon='zoom-in', text='Zoom In',
- tooltip='Zoom in the plot',
+ plot,
+ icon="zoom-in",
+ text="Zoom In",
+ tooltip="Zoom in the plot",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcut(qt.QKeySequence.ZoomIn)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@@ -143,15 +156,19 @@ class ZoomOutAction(PlotAction):
def __init__(self, plot, parent=None):
super(ZoomOutAction, self).__init__(
- plot, icon='zoom-out', text='Zoom Out',
- tooltip='Zoom out the plot',
+ plot,
+ icon="zoom-out",
+ text="Zoom Out",
+ tooltip="Zoom out the plot",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcut(qt.QKeySequence.ZoomOut)
self.setShortcutContext(qt.Qt.WidgetShortcut)
def _actionTriggered(self, checked=False):
- _applyZoomToPlot(self.plot, 1. / 1.1)
+ _applyZoomToPlot(self.plot, 1.0 / 1.1)
class XAxisAutoScaleAction(PlotAction):
@@ -163,11 +180,15 @@ class XAxisAutoScaleAction(PlotAction):
def __init__(self, plot, parent=None):
super(XAxisAutoScaleAction, self).__init__(
- plot, icon='plot-xauto', text='X Autoscale',
- tooltip='Enable x-axis auto-scale when checked.\n'
- 'If unchecked, x-axis does not change when reseting zoom.',
+ plot,
+ icon="plot-xauto",
+ text="X Autoscale",
+ tooltip="Enable x-axis auto-scale when checked.\n"
+ "If unchecked, x-axis does not change when reseting zoom.",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(plot.getXAxis().isAutoScale())
plot.getXAxis().sigAutoScaleChanged.connect(self.setChecked)
@@ -186,11 +207,15 @@ class YAxisAutoScaleAction(PlotAction):
def __init__(self, plot, parent=None):
super(YAxisAutoScaleAction, self).__init__(
- plot, icon='plot-yauto', text='Y Autoscale',
- tooltip='Enable y-axis auto-scale when checked.\n'
- 'If unchecked, y-axis does not change when reseting zoom.',
+ plot,
+ icon="plot-yauto",
+ text="Y Autoscale",
+ tooltip="Enable y-axis auto-scale when checked.\n"
+ "If unchecked, y-axis does not change when reseting zoom.",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(plot.getYAxis().isAutoScale())
plot.getYAxis().sigAutoScaleChanged.connect(self.setChecked)
@@ -209,10 +234,14 @@ class XAxisLogarithmicAction(PlotAction):
def __init__(self, plot, parent=None):
super(XAxisLogarithmicAction, self).__init__(
- plot, icon='plot-xlog', text='X Log. scale',
- tooltip='Logarithmic x-axis when checked',
+ plot,
+ icon="plot-xlog",
+ text="X Log. scale",
+ tooltip="Logarithmic x-axis when checked",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.axis = plot.getXAxis()
self.setChecked(self.axis.getScale() == self.axis.LOGARITHMIC)
self.axis.sigScaleChanged.connect(self._setCheckedIfLogScale)
@@ -234,10 +263,14 @@ class YAxisLogarithmicAction(PlotAction):
def __init__(self, plot, parent=None):
super(YAxisLogarithmicAction, self).__init__(
- plot, icon='plot-ylog', text='Y Log. scale',
- tooltip='Logarithmic y-axis when checked',
+ plot,
+ icon="plot-ylog",
+ text="Y Log. scale",
+ tooltip="Logarithmic y-axis when checked",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.axis = plot.getYAxis()
self.setChecked(self.axis.getScale() == self.axis.LOGARITHMIC)
self.axis.sigScaleChanged.connect(self._setCheckedIfLogScale)
@@ -259,21 +292,25 @@ class GridAction(PlotAction):
:param parent: See :class:`QAction`
"""
- def __init__(self, plot, gridMode='both', parent=None):
- assert gridMode in ('both', 'major')
+ def __init__(self, plot, gridMode="both", parent=None):
+ assert gridMode in ("both", "major")
self._gridMode = gridMode
super(GridAction, self).__init__(
- plot, icon='plot-grid', text='Grid',
- tooltip='Toggle grid (on/off)',
+ plot,
+ icon="plot-grid",
+ text="Grid",
+ tooltip="Toggle grid (on/off)",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(plot.getGraphGrid() is not None)
plot.sigSetGraphGrid.connect(self._gridChanged)
def _gridChanged(self, which):
"""Slot listening for PlotWidget grid mode change."""
- self.setChecked(which != 'None')
+ self.setChecked(which != "None")
def _actionTriggered(self, checked=False):
self.plot.setGraphGrid(self._gridMode if checked else None)
@@ -291,14 +328,17 @@ class CurveStyleAction(PlotAction):
def __init__(self, plot, parent=None):
super(CurveStyleAction, self).__init__(
- plot, icon='plot-toggle-points', text='Curve style',
- tooltip='Change curve line and markers style',
+ plot,
+ icon="plot-toggle-points",
+ text="Curve style",
+ tooltip="Change curve line and markers style",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
def _actionTriggered(self, checked=False):
- currentState = (self.plot.isDefaultPlotLines(),
- self.plot.isDefaultPlotPoints())
+ currentState = (self.plot.isDefaultPlotLines(), self.plot.isDefaultPlotPoints())
if currentState == (False, False):
newState = True, False
@@ -323,21 +363,39 @@ class ColormapAction(PlotAction):
def __init__(self, plot, parent=None):
self._dialog = None # To store an instance of ColormapDialog
super(ColormapAction, self).__init__(
- plot, icon='colormap', text='Colormap',
+ plot,
+ icon="colormap",
+ text="Colormap",
tooltip="Change colormap",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.plot.sigActiveImageChanged.connect(self._updateColormap)
self.plot.sigActiveScatterChanged.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)
+ def setColormapDialog(self, dialog):
+ """Set a specific colormap dialog instead of using the default one."""
+ assert dialog is not None
+ if self._dialog is not None:
+ self._dialog.visibleChanged.disconnect(self._dialogVisibleChanged)
+
+ self._dialog = dialog
+ self._dialog.visibleChanged.connect(
+ self._dialogVisibleChanged, qt.Qt.UniqueConnection
+ )
self.setChecked(self._dialog.isVisible())
+ @deprecated(replacement="setColormapDialog", since_version="2.0")
+ def setColorDialog(self, colorDialog):
+ self.setColormapDialog(colorDialog)
+
+ def getColormapDialog(self):
+ if self._dialog is None:
+ self._dialog = self._createDialog(self.plot)
+ self._dialog.visibleChanged.connect(self._dialogVisibleChanged)
+ return self._dialog
+
@staticmethod
def _createDialog(parent):
"""Create the dialog if not already existing
@@ -346,22 +404,20 @@ class ColormapAction(PlotAction):
:rtype: ColormapDialog
"""
from silx.gui.dialog.ColormapDialog import 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."""
- if self._dialog is None:
- self._dialog = self._createDialog(self.plot)
- self._dialog.visibleChanged.connect(self._dialogVisibleChanged)
-
+ dialog = self.getColormapDialog()
# Run the dialog listening to colormap change
if checked is True:
self._updateColormap()
- self._dialog.show()
+ dialog.show()
else:
- self._dialog.hide()
+ dialog.hide()
def _dialogVisibleChanged(self, isVisible):
self.setChecked(isVisible)
@@ -380,7 +436,7 @@ class ColormapAction(PlotAction):
else:
# No active image or active image is RGBA,
# Check for active scatter plot
- scatter = self.plot._getActiveItem(kind='scatter')
+ scatter = self.plot.getActiveScatter()
if scatter is not None:
colormap = scatter.getColormap()
self._dialog.setItem(scatter)
@@ -405,10 +461,14 @@ class ColorBarAction(PlotAction):
def __init__(self, plot, parent=None):
self._dialog = None # To store an instance of ColorBar
super(ColorBarAction, self).__init__(
- plot, icon='colorbar', text='Colorbar',
+ plot,
+ icon="colorbar",
+ text="Colorbar",
tooltip="Show/Hide the colorbar",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
colorBarWidget = self.plot.getColorBarWidget()
old = self.blockSignals(True)
self.setChecked(colorBarWidget.isVisibleTo(self.plot))
@@ -439,23 +499,24 @@ class KeepAspectRatioAction(PlotAction):
def __init__(self, plot, parent=None):
# Uses two images for checked/unchecked states
self._states = {
- False: (icons.getQIcon('shape-circle-solid'),
- "Keep data aspect ratio"),
- True: (icons.getQIcon('shape-ellipse-solid'),
- "Do no keep data aspect ratio")
+ False: (icons.getQIcon("shape-circle-solid"), "Keep data aspect ratio"),
+ True: (
+ icons.getQIcon("shape-ellipse-solid"),
+ "Do no keep data aspect ratio",
+ ),
}
icon, tooltip = self._states[plot.isKeepDataAspectRatio()]
super(KeepAspectRatioAction, self).__init__(
plot,
icon=icon,
- text='Toggle keep aspect ratio',
+ text="Toggle keep aspect ratio",
tooltip=tooltip,
triggered=self._actionTriggered,
checkable=False,
- parent=parent)
- plot.sigSetKeepDataAspectRatio.connect(
- self._keepDataAspectRatioChanged)
+ parent=parent,
+ )
+ plot.sigSetKeepDataAspectRatio.connect(self._keepDataAspectRatioChanged)
def _keepDataAspectRatioChanged(self, aspectRatio):
"""Handle Plot set keep aspect ratio signal"""
@@ -478,21 +539,20 @@ class YAxisInvertedAction(PlotAction):
def __init__(self, plot, parent=None):
# Uses two images for checked/unchecked states
self._states = {
- False: (icons.getQIcon('plot-ydown'),
- "Orient Y axis downward"),
- True: (icons.getQIcon('plot-yup'),
- "Orient Y axis upward"),
+ False: (icons.getQIcon("plot-ydown"), "Orient Y axis downward"),
+ True: (icons.getQIcon("plot-yup"), "Orient Y axis upward"),
}
icon, tooltip = self._states[plot.getYAxis().isInverted()]
super(YAxisInvertedAction, self).__init__(
plot,
icon=icon,
- text='Invert Y Axis',
+ text="Invert Y Axis",
tooltip=tooltip,
triggered=self._actionTriggered,
checkable=False,
- parent=parent)
+ parent=parent,
+ )
plot.getYAxis().sigInvertedChanged.connect(self._yAxisInvertedChanged)
def _yAxisInvertedChanged(self, inverted):
@@ -517,8 +577,7 @@ class CrosshairAction(PlotAction):
:param parent: See :class:`QAction`
"""
- def __init__(self, plot, color='black', linewidth=1, linestyle='-',
- parent=None):
+ def __init__(self, plot, color="black", linewidth=1, linestyle="-", parent=None):
self.color = color
"""Color used to draw the crosshair (str)."""
@@ -529,18 +588,24 @@ class CrosshairAction(PlotAction):
"""Style of line of the cursor (str)."""
super(CrosshairAction, self).__init__(
- plot, icon='crosshair', text='Crosshair Cursor',
- tooltip='Enable crosshair cursor when checked',
+ plot,
+ icon="crosshair",
+ text="Crosshair Cursor",
+ tooltip="Enable crosshair cursor when checked",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(plot.getGraphCursor() is not None)
plot.sigSetGraphCursor.connect(self.setChecked)
def _actionTriggered(self, checked=False):
- self.plot.setGraphCursor(checked,
- color=self.color,
- linestyle=self.linestyle,
- linewidth=self.linewidth)
+ self.plot.setGraphCursor(
+ checked,
+ color=self.color,
+ linestyle=self.linestyle,
+ linewidth=self.linewidth,
+ )
class PanWithArrowKeysAction(PlotAction):
@@ -551,12 +616,15 @@ class PanWithArrowKeysAction(PlotAction):
"""
def __init__(self, plot, parent=None):
-
super(PanWithArrowKeysAction, self).__init__(
- plot, icon='arrow-keys', text='Pan with arrow keys',
- tooltip='Enable pan with arrow keys when checked',
+ plot,
+ icon="arrow-keys",
+ text="Pan with arrow keys",
+ tooltip="Enable pan with arrow keys when checked",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(plot.isPanWithArrowKeys())
plot.sigSetPanWithArrowKeys.connect(self.setChecked)
@@ -572,15 +640,17 @@ class ShowAxisAction(PlotAction):
"""
def __init__(self, plot, parent=None):
- tooltip = 'Show plot axis when checked, otherwise hide them'
- PlotAction.__init__(self,
- plot,
- icon='axis',
- text='show axis',
- tooltip=tooltip,
- triggered=self._actionTriggered,
- checkable=True,
- parent=parent)
+ tooltip = "Show plot axis when checked, otherwise hide them"
+ PlotAction.__init__(
+ self,
+ plot,
+ icon="axis",
+ text="show axis",
+ tooltip=tooltip,
+ triggered=self._actionTriggered,
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(self.plot.isAxesDisplayed())
plot._sigAxesVisibilityChanged.connect(self.setChecked)
@@ -597,15 +667,17 @@ class ClosePolygonInteractionAction(PlotAction):
"""
def __init__(self, plot, parent=None):
- tooltip = 'Close the current polygon drawn'
- PlotAction.__init__(self,
- plot,
- icon='add-shape-polygon',
- text='Close the polygon',
- tooltip=tooltip,
- triggered=self._actionTriggered,
- checkable=True,
- parent=parent)
+ tooltip = "Close the current polygon drawn"
+ PlotAction.__init__(
+ self,
+ plot,
+ icon="add-shape-polygon",
+ text="Close the polygon",
+ tooltip=tooltip,
+ triggered=self._actionTriggered,
+ checkable=True,
+ parent=parent,
+ )
self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
self._modeChanged(None)
@@ -615,7 +687,7 @@ class ClosePolygonInteractionAction(PlotAction):
self.setEnabled(enabled)
def _actionTriggered(self, checked=False):
- self.plot._eventHandler.validate()
+ self.plot.interaction()._validate()
class OpenGLAction(PlotAction):
@@ -630,29 +702,32 @@ class OpenGLAction(PlotAction):
def __init__(self, plot, parent=None):
# Uses two images for checked/unchecked states
self._states = {
- "opengl": (icons.getQIcon('backend-opengl'),
- "OpenGL rendering (fast)\nClick to disable OpenGL"),
- "matplotlib": (icons.getQIcon('backend-opengl'),
- "Matplotlib rendering (safe)\nClick to enable OpenGL"),
- "unknown": (icons.getQIcon('backend-opengl'),
- "Custom rendering")
+ "opengl": (
+ icons.getQIcon("backend-opengl"),
+ "OpenGL rendering (fast)\nClick to disable OpenGL",
+ ),
+ "matplotlib": (
+ icons.getQIcon("backend-opengl"),
+ "Matplotlib rendering (safe)\nClick to enable OpenGL",
+ ),
+ "unknown": (icons.getQIcon("backend-opengl"), "Custom rendering"),
}
name = self._getBackendName(plot)
- self.__state = name
icon, tooltip = self._states[name]
super(OpenGLAction, self).__init__(
plot,
icon=icon,
- text='Enable/disable OpenGL rendering',
+ text="Enable/disable OpenGL rendering",
tooltip=tooltip,
triggered=self._actionTriggered,
checkable=True,
- parent=parent)
+ parent=parent,
+ )
+ plot.sigBackendChanged.connect(self._backendUpdated)
def _backendUpdated(self):
name = self._getBackendName(self.plot)
- self.__state = name
icon, tooltip = self._states[name]
self.setIcon(icon)
self.setToolTip(tooltip)
@@ -671,21 +746,15 @@ class OpenGLAction(PlotAction):
def _actionTriggered(self, checked=False):
plot = self.plot
name = self._getBackendName(self.plot)
- if self.__state != name:
- # THere is no event to know the backend was updated
- # So here we check if there is a mismatch between the displayed state
- # and the real state of the widget
- self._backendUpdated()
- return
if name != "opengl":
from silx.gui.utils import glutils
+
result = glutils.isOpenGLAvailable()
if not result:
- qt.QMessageBox.critical(plot, "OpenGL rendering not available", result.error)
- # Uncheck if needed
- self._backendUpdated()
+ qt.QMessageBox.critical(
+ plot, "OpenGL rendering is not available", result.error
+ )
return
plot.setBackend("opengl")
else:
plot.setBackend("matplotlib")
- self._backendUpdated()
diff --git a/src/silx/gui/plot/actions/fit.py b/src/silx/gui/plot/actions/fit.py
index 3489f70..ae8835a 100644
--- a/src/silx/gui/plot/actions/fit.py
+++ b/src/silx/gui/plot/actions/fit.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -42,7 +42,6 @@ import numpy
from .PlotToolAction import PlotToolAction
from .. import items
-from ....utils.deprecation import deprecated
from silx.gui import qt
from silx.gui.plot.ItemsSelectionDialog import ItemsSelectionDialog
@@ -63,10 +62,8 @@ def _getUniqueCurveOrHistogram(plot):
return curve
visibleItems = [item for item in plot.getItems() if item.isVisible()]
- histograms = [item for item in visibleItems
- if isinstance(item, items.Histogram)]
- curves = [item for item in visibleItems
- if isinstance(item, items.Curve)]
+ histograms = [item for item in visibleItems if isinstance(item, items.Histogram)]
+ curves = [item for item in visibleItems if isinstance(item, items.Curve)]
if len(histograms) == 1 and len(curves) == 0:
return histograms[0]
@@ -114,12 +111,11 @@ class _FitItemSelector(qt.QObject):
# disconnect from previous plot
previousPlotWidget = self.getPlotWidget()
if previousPlotWidget is not None:
- previousPlotWidget.sigItemAdded.disconnect(
- self.__plotWidgetUpdated)
- previousPlotWidget.sigItemRemoved.disconnect(
- self.__plotWidgetUpdated)
+ previousPlotWidget.sigItemAdded.disconnect(self.__plotWidgetUpdated)
+ previousPlotWidget.sigItemRemoved.disconnect(self.__plotWidgetUpdated)
previousPlotWidget.sigActiveCurveChanged.disconnect(
- self.__plotWidgetUpdated)
+ self.__plotWidgetUpdated
+ )
if plotWidget is None:
self.__plotWidgetRef = None
@@ -184,49 +180,15 @@ class FitAction(PlotToolAction):
self.__legend = None
super(FitAction, self).__init__(
- plot, icon='math-fit', text='Fit curve',
- tooltip='Open a fit dialog',
- parent=parent)
+ plot,
+ icon="math-fit",
+ text="Fit curve",
+ tooltip="Open a fit dialog",
+ parent=parent,
+ )
self.__fitItemSelector = _FitItemSelector()
- self.__fitItemSelector.sigCurrentItemChanged.connect(
- self._setFittedItem)
-
-
- @property
- @deprecated(replacement='getXRange()[0]', since_version='0.13.0')
- def xmin(self):
- return self.getXRange()[0]
-
- @property
- @deprecated(replacement='getXRange()[1]', since_version='0.13.0')
- def xmax(self):
- return self.getXRange()[1]
-
- @property
- @deprecated(replacement='getXData()', since_version='0.13.0')
- def x(self):
- return self.getXData()
-
- @property
- @deprecated(replacement='getYData()', since_version='0.13.0')
- def y(self):
- return self.getYData()
-
- @property
- @deprecated(since_version='0.13.0')
- def xlabel(self):
- return self.__curveParams.get('xlabel', None)
-
- @property
- @deprecated(since_version='0.13.0')
- def ylabel(self):
- return self.__curveParams.get('ylabel', None)
-
- @property
- @deprecated(since_version='0.13.0')
- def legend(self):
- return self.__legend
+ self.__fitItemSelector.sigCurrentItemChanged.connect(self._setFittedItem)
def _createToolWindow(self):
# import done here rather than at module level to avoid circular import
@@ -299,11 +261,10 @@ class FitAction(PlotToolAction):
else:
xmin, xmax = self.getXRange()
- fitWidget.setData(
- xdata, ydata, xmin=xmin, xmax=xmax)
+ fitWidget.setData(xdata, ydata, xmin=xmin, xmax=xmax)
fitWidget.setWindowTitle(
- "Fitting " + item.getName() +
- " on x range %f-%f" % (xmin, xmax))
+ "Fitting " + item.getName() + " on x range %f-%f" % (xmin, xmax)
+ )
# X Range management
@@ -397,12 +358,12 @@ class FitAction(PlotToolAction):
self.__updateFitWidget()
return
- axis = item.getYAxis() if isinstance(item, items.YAxisMixIn) else 'left'
+ axis = item.getYAxis() if isinstance(item, items.YAxisMixIn) else "left"
self.__curveParams = {
- 'yaxis': axis,
- 'xlabel': plot.getXAxis().getLabel(),
- 'ylabel': plot.getYAxis(axis).getLabel(),
- }
+ "yaxis": axis,
+ "xlabel": plot.getXAxis().getLabel(),
+ "ylabel": plot.getYAxis(axis).getLabel(),
+ }
self.__legend = item.getName()
if isinstance(item, items.Histogram):
@@ -415,7 +376,7 @@ class FitAction(PlotToolAction):
self.__x = item.getXData(copy=False)
self.__y = item.getYData(copy=False)
- self.__item = item
+ self.__item = item
self.__updateFitWidget()
def __setFittedItemAutoUpdateEnabled(self, enabled):
@@ -468,14 +429,13 @@ class FitAction(PlotToolAction):
return
y_fit = fit_widget.fitmanager.gendata()
if fit_curve is None:
- self.plot.addCurve(x_fit, y_fit,
- fit_legend,
- resetzoom=False,
- **self.__curveParams)
+ self.plot.addCurve(
+ x_fit, y_fit, fit_legend, resetzoom=False, **self.__curveParams
+ )
else:
fit_curve.setData(x_fit, y_fit)
fit_curve.setVisible(True)
- fit_curve.setYAxis(self.__curveParams.get('yaxis', 'left'))
+ fit_curve.setYAxis(self.__curveParams.get("yaxis", "left"))
if ddict["event"] in ["FitStarted", "FitFailed"]:
if fit_curve is not None:
diff --git a/src/silx/gui/plot/actions/histogram.py b/src/silx/gui/plot/actions/histogram.py
index 448dd55..39c669b 100644
--- a/src/silx/gui/plot/actions/histogram.py
+++ b/src/silx/gui/plot/actions/histogram.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -31,7 +31,7 @@ The following QAction are available:
"""
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
-__date__ = "01/12/2020"
+__date__ = "07/11/2023"
__license__ = "MIT"
from typing import Optional, Tuple
@@ -47,7 +47,6 @@ from silx.gui import qt
from silx.gui.plot import items
from silx.gui.widgets.ElidedLabel import ElidedLabel
from silx.gui.widgets.RangeSlider import RangeSlider
-from silx.utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
@@ -62,7 +61,7 @@ class _ElidedLabel(ElidedLabel):
def sizeHint(self):
hint = super().sizeHint()
nbchar = max(len(self.text()), 12)
- width = self.fontMetrics().boundingRect('#' * nbchar).width()
+ width = self.fontMetrics().boundingRect("#" * nbchar).width()
return qt.QSize(max(hint.width(), width), hint.height())
@@ -73,7 +72,7 @@ class _StatWidget(qt.QWidget):
:param name:
"""
- def __init__(self, parent=None, name: str=''):
+ def __init__(self, parent=None, name: str = ""):
super().__init__(parent)
layout = qt.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
@@ -84,7 +83,8 @@ class _StatWidget(qt.QWidget):
self.__valueWidget = _ElidedLabel(parent=self)
self.__valueWidget.setText("-")
self.__valueWidget.setTextInteractionFlags(
- qt.Qt.TextSelectableByMouse | qt.Qt.TextSelectableByKeyboard)
+ qt.Qt.TextSelectableByMouse | qt.Qt.TextSelectableByKeyboard
+ )
layout.addWidget(self.__valueWidget)
def setValue(self, value: Optional[float]):
@@ -92,8 +92,7 @@ class _StatWidget(qt.QWidget):
:param value:
"""
- self.__valueWidget.setText(
- "-" if value is None else "{:.5g}".format(value))
+ self.__valueWidget.setText("-" if value is None else "{:.5g}".format(value))
class _IntEdit(qt.QLineEdit):
@@ -124,9 +123,7 @@ class _IntEdit(qt.QLineEdit):
font = self.font()
font.setStyle(qt.QFont.StyleItalic)
fontMetrics = qt.QFontMetrics(font)
- self.setMaximumWidth(
- fontMetrics.boundingRect('0' * (nbchar + 1)).width()
- )
+ self.setMaximumWidth(fontMetrics.boundingRect("0" * (nbchar + 1)).width())
self.setMaxLength(nbchar)
def __textEdited(self, _):
@@ -191,7 +188,7 @@ class _IntEdit(qt.QLineEdit):
self.setRange(min(value, bottom), max(value, top))
return numpy.clip(value, *self.getRange())
- def setDefaultValue(self, value: int, extend_range: bool=False):
+ def setDefaultValue(self, value: int, extend_range: bool = False):
"""Set default value when QLineEdit is empty
:param int value:
@@ -210,7 +207,7 @@ class _IntEdit(qt.QLineEdit):
except ValueError:
return None
- def setCurrentValue(self, value: int, extend_range: bool=False):
+ def setCurrentValue(self, value: int, extend_range: bool = False):
"""Set the currently displayed value
:param int value:
@@ -236,7 +233,7 @@ class HistogramWidget(qt.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.setWindowTitle('Histogram')
+ self.setWindowTitle("Histogram")
self.__itemRef = None # weakref on the item to track
@@ -247,6 +244,7 @@ class HistogramWidget(qt.QWidget):
# Plot
# Lazy import to avoid circular dependencies
from silx.gui.plot.PlotWindow import Plot1D
+
self.__plot = Plot1D(self)
layout.addWidget(self.__plot)
@@ -266,16 +264,18 @@ class HistogramWidget(qt.QWidget):
controlsLayout.addWidget(qt.QLabel("N. bins:"))
self.__nbinsLineEdit = _IntEdit(self)
self.__nbinsLineEdit.setRange(2, 9999)
- self.__nbinsLineEdit.sigValueChanged.connect(
- self.__updateHistogramFromControls)
+ self.__nbinsLineEdit.sigValueChanged.connect(self.__updateHistogramFromControls)
controlsLayout.addWidget(self.__nbinsLineEdit)
self.__rangeLabel = qt.QLabel("Range:")
controlsLayout.addWidget(self.__rangeLabel)
self.__rangeSlider = RangeSlider(parent=self)
- self.__rangeSlider.sigValueChanged.connect(
- self.__updateHistogramFromControls)
+ self.__rangeSlider.sigValueChanged.connect(self.__updateHistogramFromControls)
self.__rangeSlider.sigValueChanged.connect(self.__rangeChanged)
controlsLayout.addWidget(self.__rangeSlider)
+ self.__weightCheckBox = qt.QCheckBox(self)
+ self.__weightCheckBox.setText("Use weights")
+ self.__weightCheckBox.clicked.connect(self.__weightChanged)
+ controlsLayout.addWidget(self.__weightCheckBox)
controlsLayout.addStretch(1)
# Stats display
@@ -286,7 +286,8 @@ class HistogramWidget(qt.QWidget):
self.__statsWidgets = dict(
(name, _StatWidget(parent=statsWidget, name=name))
- for name in ("min", "max", "mean", "std", "sum"))
+ for name in ("min", "max", "mean", "std", "sum")
+ )
for widget in self.__statsWidgets.values():
statsLayout.addWidget(widget)
@@ -336,8 +337,10 @@ class HistogramWidget(qt.QWidget):
hist = self.getHistogram(copy=False)
if hist is not None:
count, edges = hist
- if (len(count) == self.__nbinsLineEdit.getValue() and
- (edges[0], edges[-1]) == self.__rangeSlider.getValues()):
+ if (
+ len(count) == self.__nbinsLineEdit.getValue()
+ and (edges[0], edges[-1]) == self.__rangeSlider.getValues()
+ ):
return # Nothing has changed
self._updateFromItem()
@@ -348,6 +351,9 @@ class HistogramWidget(qt.QWidget):
self.__rangeSlider.setToolTip(tooltip)
self.__rangeLabel.setToolTip(tooltip)
+ def __weightChanged(self, value):
+ self._updateFromItem()
+
def _updateFromItem(self):
"""Update histogram and stats from the item"""
item = self.getItem()
@@ -388,31 +394,39 @@ class HistogramWidget(qt.QWidget):
if xmin == 0:
range_ = -0.01, 0.01
else:
- range_ = sorted((xmin * .99, xmin * 1.01))
+ range_ = sorted((xmin * 0.99, xmin * 1.01))
else:
range_ = xmin, xmax
self.__rangeSlider.setRange(*range_)
self.__rangeSlider.setPositions(*previousPositions)
+ data = array.ravel().astype(numpy.float32)
histogram = Histogramnd(
- array.ravel().astype(numpy.float32),
+ data,
n_bins=max(2, self.__nbinsLineEdit.getValue()),
histo_range=self.__rangeSlider.getValues(),
+ weights=data,
)
if len(histogram.edges) != 1:
_logger.error("Error while computing the histogram")
self.reset()
return
- self.setHistogram(histogram.histo, histogram.edges[0])
+ if self.__weightCheckBox.isChecked():
+ self.setHistogram(histogram.weighted_histo, histogram.edges[0])
+ self.__plot.getYAxis().setLabel("Count * Value")
+ else:
+ self.setHistogram(histogram.histo, histogram.edges[0])
+ self.__plot.getYAxis().setLabel("Count")
self.resetZoom()
self.setStatistics(
min_=xmin,
max_=xmax,
mean=numpy.nanmean(array),
std=numpy.nanstd(array),
- sum_=numpy.nansum(array))
+ sum_=numpy.nansum(array),
+ )
def setHistogram(self, histogram, edges):
"""Set displayed histogram
@@ -422,20 +436,21 @@ class HistogramWidget(qt.QWidget):
"""
# Only useful if setHistogram is called directly
# TODO
- #nbins = len(histogram)
- #if nbins != self.__nbinsLineEdit.getDefaultValue():
+ # nbins = len(histogram)
+ # if nbins != self.__nbinsLineEdit.getDefaultValue():
# self.__nbinsLineEdit.setValue(nbins, extend_range=True)
- #self.__rangeSlider.setValues(edges[0], edges[-1])
+ # self.__rangeSlider.setValues(edges[0], edges[-1])
self.getPlotWidget().addHistogram(
histogram=histogram,
edges=edges,
- legend='histogram',
+ legend="histogram",
fill=True,
- color='#66aad7',
- resetzoom=False)
+ color="#66aad7",
+ resetzoom=False,
+ )
- def getHistogram(self, copy: bool=True):
+ def getHistogram(self, copy: bool = True):
"""Returns currently displayed histogram.
:param copy: True to get a copy,
@@ -443,24 +458,25 @@ class HistogramWidget(qt.QWidget):
:return: (histogram, edges) or None
"""
for item in self.getPlotWidget().getItems():
- if item.getName() == 'histogram':
- return (item.getValueData(copy=copy),
- item.getBinEdgesData(copy=copy))
+ if item.getName() == "histogram":
+ return (item.getValueData(copy=copy), item.getBinEdgesData(copy=copy))
else:
return None
- def setStatistics(self,
- min_: Optional[float] = None,
- max_: Optional[float] = None,
- mean: Optional[float] = None,
- std: Optional[float] = None,
- sum_: Optional[float] = None):
+ def setStatistics(
+ self,
+ min_: Optional[float] = None,
+ max_: Optional[float] = None,
+ mean: Optional[float] = None,
+ std: Optional[float] = None,
+ sum_: Optional[float] = None,
+ ):
"""Set displayed statistic indicators."""
- self.__statsWidgets['min'].setValue(min_)
- self.__statsWidgets['max'].setValue(max_)
- self.__statsWidgets['mean'].setValue(mean)
- self.__statsWidgets['std'].setValue(std)
- self.__statsWidgets['sum'].setValue(sum_)
+ self.__statsWidgets["min"].setValue(min_)
+ self.__statsWidgets["max"].setValue(max_)
+ self.__statsWidgets["mean"].setValue(mean)
+ self.__statsWidgets["std"].setValue(std)
+ self.__statsWidgets["sum"].setValue(sum_)
class PixelIntensitiesHistoAction(PlotToolAction):
@@ -471,12 +487,14 @@ class PixelIntensitiesHistoAction(PlotToolAction):
"""
def __init__(self, plot, parent=None):
- PlotToolAction.__init__(self,
- plot,
- icon='pixel-intensities',
- text='pixels intensity',
- tooltip='Compute image intensity distribution',
- parent=parent)
+ PlotToolAction.__init__(
+ self,
+ plot,
+ icon="pixel-intensities",
+ text="pixels intensity",
+ tooltip="Compute image intensity distribution",
+ parent=parent,
+ )
def _connectPlot(self, window):
plot = self.plot
@@ -514,19 +532,10 @@ class PixelIntensitiesHistoAction(PlotToolAction):
if self._isWindowInUse():
self._updateSelectedItem()
- @deprecated(since_version='0.15.0')
- def computeIntensityDistribution(self):
- self.getHistogramWidget()._updateFromItem()
-
def getHistogramWidget(self):
"""Returns the widget displaying the histogram"""
return self._getToolWindow()
- @deprecated(since_version='0.15.0',
- replacement='getHistogramWidget().getPlotWidget()')
- def getHistogramPlotWidget(self):
- return self._getToolWindow().getPlotWidget()
-
def _createToolWindow(self):
return HistogramWidget(self.plot, qt.Qt.Window)
diff --git a/src/silx/gui/plot/actions/io.py b/src/silx/gui/plot/actions/io.py
index 1ed9649..1ff95f3 100644
--- a/src/silx/gui/plot/actions/io.py
+++ b/src/silx/gui/plot/actions/io.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -36,30 +36,27 @@ __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "25/09/2020"
-from . import PlotAction
-from silx.io.utils import save1D, savespec, NEXUS_HDF5_EXT
-from silx.io.nxdata import save_NXdata
+from io import BytesIO
import logging
import sys
import os.path
-from collections import OrderedDict
import traceback
import numpy
-from silx.utils.deprecation import deprecated
+from fabio.TiffIO import TiffIO
+from fabio.edfimage import EdfImage
+
from silx.gui import qt, printer
from silx.gui.dialog.GroupDialog import GroupDialog
-from silx.third_party.EdfFile import EdfFile
-from silx.third_party.TiffIO import TiffIO
+from silx.io.utils import save1D, savespec, NEXUS_HDF5_EXT
+from silx.io.nxdata import save_NXdata
+
+from . import PlotAction
from ...utils.image import convertArrayToQImage
-if sys.version_info[0] == 3:
- from io import BytesIO
-else:
- import cStringIO as _StringIO
- BytesIO = _StringIO.StringIO
+
_logger = logging.getLogger(__name__)
-_NEXUS_HDF5_EXT_STR = ' '.join(['*' + ext for ext in NEXUS_HDF5_EXT])
+_NEXUS_HDF5_EXT_STR = " ".join(["*" + ext for ext in NEXUS_HDF5_EXT])
def selectOutputGroup(h5filename):
@@ -87,111 +84,142 @@ class SaveAction(PlotAction):
:param parent: See :class:`QAction`.
"""
- SNAPSHOT_FILTER_SVG = 'Plot Snapshot as SVG (*.svg)'
- SNAPSHOT_FILTER_PNG = 'Plot Snapshot as PNG (*.png)'
+ SNAPSHOT_FILTER_SVG = "Plot Snapshot as SVG (*.svg)"
+ SNAPSHOT_FILTER_PNG = "Plot Snapshot as PNG (*.png)"
DEFAULT_ALL_FILTERS = (SNAPSHOT_FILTER_PNG, SNAPSHOT_FILTER_SVG)
# Dict of curve filters with CSV-like format
# Using ordered dict to guarantee filters order
# Note: '%.18e' is numpy.savetxt default format
- CURVE_FILTERS_TXT = OrderedDict((
- ('Curve as Raw ASCII (*.txt)',
- {'fmt': '%.18e', 'delimiter': ' ', 'header': False}),
- ('Curve as ";"-separated CSV (*.csv)',
- {'fmt': '%.18e', 'delimiter': ';', 'header': True}),
- ('Curve as ","-separated CSV (*.csv)',
- {'fmt': '%.18e', 'delimiter': ',', 'header': True}),
- ('Curve as tab-separated CSV (*.csv)',
- {'fmt': '%.18e', 'delimiter': '\t', 'header': True}),
- ('Curve as OMNIC CSV (*.csv)',
- {'fmt': '%.7E', 'delimiter': ',', 'header': False}),
- ('Curve as SpecFile (*.dat)',
- {'fmt': '%.10g', 'delimiter': '', 'header': False})
- ))
-
- CURVE_FILTER_NPY = 'Curve as NumPy binary file (*.npy)'
-
- CURVE_FILTER_NXDATA = 'Curve as NXdata (%s)' % _NEXUS_HDF5_EXT_STR
+ CURVE_FILTERS_TXT = dict(
+ (
+ (
+ "Curve as Raw ASCII (*.txt)",
+ {"fmt": "%.18e", "delimiter": " ", "header": False},
+ ),
+ (
+ 'Curve as ";"-separated CSV (*.csv)',
+ {"fmt": "%.18e", "delimiter": ";", "header": True},
+ ),
+ (
+ 'Curve as ","-separated CSV (*.csv)',
+ {"fmt": "%.18e", "delimiter": ",", "header": True},
+ ),
+ (
+ "Curve as tab-separated CSV (*.csv)",
+ {"fmt": "%.18e", "delimiter": "\t", "header": True},
+ ),
+ (
+ "Curve as OMNIC CSV (*.csv)",
+ {"fmt": "%.7E", "delimiter": ",", "header": False},
+ ),
+ (
+ "Curve as SpecFile (*.dat)",
+ {"fmt": "%.10g", "delimiter": "", "header": False},
+ ),
+ )
+ )
+
+ CURVE_FILTER_NPY = "Curve as NumPy binary file (*.npy)"
+
+ CURVE_FILTER_NXDATA = "Curve as NXdata (%s)" % _NEXUS_HDF5_EXT_STR
DEFAULT_CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [
- CURVE_FILTER_NPY, CURVE_FILTER_NXDATA]
+ CURVE_FILTER_NPY,
+ CURVE_FILTER_NXDATA,
+ ]
DEFAULT_ALL_CURVES_FILTERS = ("All curves as SpecFile (*.dat)",)
- IMAGE_FILTER_EDF = 'Image data as EDF (*.edf)'
- IMAGE_FILTER_TIFF = 'Image data as TIFF (*.tif)'
- IMAGE_FILTER_NUMPY = 'Image data as NumPy binary file (*.npy)'
- IMAGE_FILTER_ASCII = 'Image data as ASCII (*.dat)'
- IMAGE_FILTER_CSV_COMMA = 'Image data as ,-separated CSV (*.csv)'
- IMAGE_FILTER_CSV_SEMICOLON = 'Image data as ;-separated CSV (*.csv)'
- IMAGE_FILTER_CSV_TAB = 'Image data as tab-separated CSV (*.csv)'
- IMAGE_FILTER_RGB_PNG = 'Image as PNG (*.png)'
- IMAGE_FILTER_NXDATA = 'Image as NXdata (%s)' % _NEXUS_HDF5_EXT_STR
-
- DEFAULT_IMAGE_FILTERS = (IMAGE_FILTER_EDF,
- IMAGE_FILTER_TIFF,
- IMAGE_FILTER_NUMPY,
- IMAGE_FILTER_ASCII,
- IMAGE_FILTER_CSV_COMMA,
- IMAGE_FILTER_CSV_SEMICOLON,
- IMAGE_FILTER_CSV_TAB,
- IMAGE_FILTER_RGB_PNG,
- IMAGE_FILTER_NXDATA)
-
- SCATTER_FILTER_NXDATA = 'Scatter as NXdata (%s)' % _NEXUS_HDF5_EXT_STR
+ IMAGE_FILTER_EDF = "Image data as EDF (*.edf)"
+ IMAGE_FILTER_TIFF = "Image data as TIFF (*.tif)"
+ IMAGE_FILTER_NUMPY = "Image data as NumPy binary file (*.npy)"
+ IMAGE_FILTER_ASCII = "Image data as ASCII (*.dat)"
+ IMAGE_FILTER_CSV_COMMA = "Image data as ,-separated CSV (*.csv)"
+ IMAGE_FILTER_CSV_SEMICOLON = "Image data as ;-separated CSV (*.csv)"
+ IMAGE_FILTER_CSV_TAB = "Image data as tab-separated CSV (*.csv)"
+ IMAGE_FILTER_RGB_PNG = "Image as PNG (*.png)"
+ IMAGE_FILTER_NXDATA = "Image as NXdata (%s)" % _NEXUS_HDF5_EXT_STR
+
+ DEFAULT_IMAGE_FILTERS = (
+ IMAGE_FILTER_EDF,
+ IMAGE_FILTER_TIFF,
+ IMAGE_FILTER_NUMPY,
+ IMAGE_FILTER_ASCII,
+ IMAGE_FILTER_CSV_COMMA,
+ IMAGE_FILTER_CSV_SEMICOLON,
+ IMAGE_FILTER_CSV_TAB,
+ IMAGE_FILTER_RGB_PNG,
+ IMAGE_FILTER_NXDATA,
+ )
+
+ SCATTER_FILTER_NXDATA = "Scatter as NXdata (%s)" % _NEXUS_HDF5_EXT_STR
DEFAULT_SCATTER_FILTERS = (SCATTER_FILTER_NXDATA,)
# filters for which we don't want an "overwrite existing file" warning
- DEFAULT_APPEND_FILTERS = (CURVE_FILTER_NXDATA, IMAGE_FILTER_NXDATA,
- SCATTER_FILTER_NXDATA)
+ DEFAULT_APPEND_FILTERS = (
+ CURVE_FILTER_NXDATA,
+ IMAGE_FILTER_NXDATA,
+ SCATTER_FILTER_NXDATA,
+ )
def __init__(self, plot, parent=None):
self._filters = {
- 'all': OrderedDict(),
- 'curve': OrderedDict(),
- 'curves': OrderedDict(),
- 'image': OrderedDict(),
- 'scatter': OrderedDict()}
+ "all": {},
+ "curve": {},
+ "curves": {},
+ "image": {},
+ "scatter": {},
+ }
self._appendFilters = list(self.DEFAULT_APPEND_FILTERS)
# Initialize filters
for nameFilter in self.DEFAULT_ALL_FILTERS:
self.setFileFilter(
- dataKind='all', nameFilter=nameFilter, func=self._saveSnapshot)
+ dataKind="all", nameFilter=nameFilter, func=self._saveSnapshot
+ )
for nameFilter in self.DEFAULT_CURVE_FILTERS:
self.setFileFilter(
- dataKind='curve', nameFilter=nameFilter, func=self._saveCurve)
+ dataKind="curve", nameFilter=nameFilter, func=self._saveCurve
+ )
for nameFilter in self.DEFAULT_ALL_CURVES_FILTERS:
self.setFileFilter(
- dataKind='curves', nameFilter=nameFilter, func=self._saveCurves)
+ dataKind="curves", nameFilter=nameFilter, func=self._saveCurves
+ )
for nameFilter in self.DEFAULT_IMAGE_FILTERS:
self.setFileFilter(
- dataKind='image', nameFilter=nameFilter, func=self._saveImage)
+ dataKind="image", nameFilter=nameFilter, func=self._saveImage
+ )
for nameFilter in self.DEFAULT_SCATTER_FILTERS:
self.setFileFilter(
- dataKind='scatter', nameFilter=nameFilter, func=self._saveScatter)
+ dataKind="scatter", nameFilter=nameFilter, func=self._saveScatter
+ )
super(SaveAction, self).__init__(
- plot, icon='document-save', text='Save as...',
- tooltip='Save curve/image/plot snapshot dialog',
+ plot,
+ icon="document-save",
+ text="Save as...",
+ tooltip="Save curve/image/plot snapshot dialog",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcut(qt.QKeySequence.Save)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@staticmethod
- def _errorMessage(informativeText='', parent=None):
+ def _errorMessage(informativeText="", parent=None):
"""Display an error message."""
# TODO issue with QMessageBox size fixed and too small
msg = qt.QMessageBox(parent)
msg.setIcon(qt.QMessageBox.Critical)
- msg.setInformativeText(informativeText + ' ' + str(sys.exc_info()[1]))
+ msg.setInformativeText(informativeText + " " + str(sys.exc_info()[1]))
msg.setDetailedText(traceback.format_exc())
msg.exec()
@@ -204,12 +232,11 @@ class SaveAction(PlotAction):
True otherwise.
"""
if nameFilter == self.SNAPSHOT_FILTER_PNG:
- fileFormat = 'png'
+ fileFormat = "png"
elif nameFilter == self.SNAPSHOT_FILTER_SVG:
- fileFormat = 'svg'
+ fileFormat = "svg"
else: # Format not supported
- _logger.error(
- 'Saving plot snapshot failed: format not supported')
+ _logger.error("Saving plot snapshot failed: format not supported")
return False
plot.saveGraph(filename, fileFormat=fileFormat)
@@ -260,8 +287,11 @@ class SaveAction(PlotAction):
@staticmethod
def _selectWriteableOutputGroup(filename, parent):
- if os.path.exists(filename) and os.path.isfile(filename) \
- and os.access(filename, os.W_OK):
+ if (
+ os.path.exists(filename)
+ and os.path.isfile(filename)
+ and os.access(filename, os.W_OK)
+ ):
entryPath = selectOutputGroup(filename)
if entryPath is None:
_logger.info("Save operation cancelled")
@@ -271,7 +301,7 @@ class SaveAction(PlotAction):
# create new entry in new file
return "/entry"
else:
- SaveAction._errorMessage('Save failed (file access issue)\n', parent=parent)
+ SaveAction._errorMessage("Save failed (file access issue)\n", parent=parent)
return None
def _saveCurveAsNXdata(self, curve, filename):
@@ -292,7 +322,8 @@ class SaveAction(PlotAction):
axes_long_names=[xlabel],
signal_errors=curve.getYErrorData(copy=False),
axes_errors=[curve.getXErrorData(copy=True)],
- title=self.plot.getGraphTitle())
+ title=self.plot.getGraphTitle(),
+ )
def _saveCurve(self, plot, filename, nameFilter):
"""Save a curve from the plot.
@@ -318,9 +349,9 @@ class SaveAction(PlotAction):
if nameFilter in self.CURVE_FILTERS_TXT:
filter_ = self.CURVE_FILTERS_TXT[nameFilter]
- fmt = filter_['fmt']
- csvdelim = filter_['delimiter']
- autoheader = filter_['header']
+ fmt = filter_["fmt"]
+ csvdelim = filter_["delimiter"]
+ autoheader = filter_["header"]
else:
# .npy or nxdata
fmt, csvdelim, autoheader = ("", "", False)
@@ -331,13 +362,18 @@ class SaveAction(PlotAction):
xdata, data, xlabel, labels = self._get1dData(curve)
try:
- save1D(filename,
- xdata, data,
- xlabel, labels,
- fmt=fmt, csvdelim=csvdelim,
- autoheader=autoheader)
+ save1D(
+ filename,
+ xdata,
+ data,
+ xlabel,
+ labels,
+ fmt=fmt,
+ csvdelim=csvdelim,
+ autoheader=autoheader,
+ )
except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
+ self._errorMessage("Save failed\n", parent=self.plot)
return False
return True
@@ -363,28 +399,39 @@ class SaveAction(PlotAction):
try:
xdata, data, xlabel, labels = self._get1dData(curve)
- specfile = savespec(filename,
- xdata, data,
- xlabel, labels,
- fmt="%.7g", scan_number=1, mode="w",
- write_file_header=True,
- close_file=False)
+ specfile = savespec(
+ filename,
+ xdata,
+ data,
+ xlabel,
+ labels,
+ fmt="%.7g",
+ scan_number=1,
+ mode="w",
+ write_file_header=True,
+ close_file=False,
+ )
except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
+ self._errorMessage("Save failed\n", parent=self.plot)
return False
for curve in curves[1:]:
try:
scanno += 1
xdata, data, xlabel, labels = self._get1dData(curve)
- specfile = savespec(specfile,
- xdata, data,
- xlabel, labels,
- fmt="%.7g", scan_number=scanno,
- write_file_header=False,
- close_file=False)
+ specfile = savespec(
+ specfile,
+ xdata,
+ data,
+ xlabel,
+ labels,
+ fmt="%.7g",
+ scan_number=scanno,
+ write_file_header=False,
+ close_file=False,
+ )
except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
+ self._errorMessage("Save failed\n", parent=self.plot)
return False
specfile.close()
@@ -403,28 +450,26 @@ class SaveAction(PlotAction):
image = plot.getActiveImage()
if image is None:
- qt.QMessageBox.warning(
- plot, "No Data", "No image to be saved")
+ qt.QMessageBox.warning(plot, "No Data", "No image to be saved")
return False
data = image.getData(copy=False)
# TODO Use silx.io for writing files
if nameFilter == self.IMAGE_FILTER_EDF:
- edfFile = EdfFile(filename, access="w+")
- edfFile.WriteImage({}, data, Append=0)
+ EdfImage(data=data, header={}).write(filename)
return True
elif nameFilter == self.IMAGE_FILTER_TIFF:
- tiffFile = TiffIO(filename, mode='w')
- tiffFile.writeImage(data, software='silx')
+ tiffFile = TiffIO(filename, mode="w")
+ tiffFile.writeImage(data, software="silx")
return True
elif nameFilter == self.IMAGE_FILTER_NUMPY:
try:
numpy.save(filename, data)
except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
+ self._errorMessage("Save failed\n", parent=self.plot)
return False
return True
@@ -439,39 +484,47 @@ class SaveAction(PlotAction):
xlabel, ylabel = self._getAxesLabels(image)
interpretation = "image" if len(data.shape) == 2 else "rgba-image"
- return save_NXdata(filename,
- nxentry_name=entryPath,
- signal=data,
- axes=[yaxis, xaxis],
- signal_name="image",
- axes_names=["y", "x"],
- axes_long_names=[ylabel, xlabel],
- title=plot.getGraphTitle(),
- interpretation=interpretation)
-
- elif nameFilter in (self.IMAGE_FILTER_ASCII,
- self.IMAGE_FILTER_CSV_COMMA,
- self.IMAGE_FILTER_CSV_SEMICOLON,
- self.IMAGE_FILTER_CSV_TAB):
+ return save_NXdata(
+ filename,
+ nxentry_name=entryPath,
+ signal=data,
+ axes=[yaxis, xaxis],
+ signal_name="image",
+ axes_names=["y", "x"],
+ axes_long_names=[ylabel, xlabel],
+ title=plot.getGraphTitle(),
+ interpretation=interpretation,
+ )
+
+ elif nameFilter in (
+ self.IMAGE_FILTER_ASCII,
+ self.IMAGE_FILTER_CSV_COMMA,
+ self.IMAGE_FILTER_CSV_SEMICOLON,
+ self.IMAGE_FILTER_CSV_TAB,
+ ):
csvdelim, filetype = {
- self.IMAGE_FILTER_ASCII: (' ', 'txt'),
- self.IMAGE_FILTER_CSV_COMMA: (',', 'csv'),
- self.IMAGE_FILTER_CSV_SEMICOLON: (';', 'csv'),
- self.IMAGE_FILTER_CSV_TAB: ('\t', 'csv'),
- }[nameFilter]
+ self.IMAGE_FILTER_ASCII: (" ", "txt"),
+ self.IMAGE_FILTER_CSV_COMMA: (",", "csv"),
+ self.IMAGE_FILTER_CSV_SEMICOLON: (";", "csv"),
+ self.IMAGE_FILTER_CSV_TAB: ("\t", "csv"),
+ }[nameFilter]
height, width = data.shape
rows, cols = numpy.mgrid[0:height, 0:width]
try:
- save1D(filename, rows.ravel(), (cols.ravel(), data.ravel()),
- filetype=filetype,
- xlabel='row',
- ylabels=['column', 'value'],
- csvdelim=csvdelim,
- autoheader=True)
+ save1D(
+ filename,
+ rows.ravel(),
+ (cols.ravel(), data.ravel()),
+ filetype=filetype,
+ xlabel="row",
+ ylabels=["column", "value"],
+ csvdelim=csvdelim,
+ autoheader=True,
+ )
except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
+ self._errorMessage("Save failed\n", parent=self.plot)
return False
return True
@@ -481,14 +534,13 @@ class SaveAction(PlotAction):
# Convert RGB QImage
qimage = convertArrayToQImage(rgbaImage[:, :, :3])
- if qimage.save(filename, 'PNG'):
+ if qimage.save(filename, "PNG"):
return True
else:
- _logger.error('Failed to save image as %s', filename)
+ _logger.error("Failed to save image as %s", filename)
qt.QMessageBox.critical(
- self.parent(),
- 'Save image as',
- 'Failed to save image')
+ self.parent(), "Save image as", "Failed to save image"
+ )
return False
@@ -533,7 +585,8 @@ class SaveAction(PlotAction):
axes_names=["x", "y"],
axes_long_names=[xlabel, ylabel],
axes_errors=[xerror, yerror],
- title=plot.getGraphTitle())
+ title=plot.getGraphTitle(),
+ )
def setFileFilter(self, dataKind, nameFilter, func, index=None, appendToFile=False):
"""Set a name filter to add/replace a file format support
@@ -550,7 +603,7 @@ class SaveAction(PlotAction):
file.
:param integer index: Index of the filter in the final list (or None)
"""
- assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter')
+ assert dataKind in ("all", "curve", "curves", "image", "scatter")
if appendToFile:
self._appendFilters.append(nameFilter)
@@ -572,7 +625,7 @@ class SaveAction(PlotAction):
if index >= len(keyList):
# nothing to be done, already at the end
- txt = 'Requested index %d impossible, already at the end' % index
+ txt = "Requested index %d impossible, already at the end" % index
_logger.info(txt)
return
@@ -582,7 +635,7 @@ class SaveAction(PlotAction):
keyList.insert(index, nameFilter)
# build the new filters
- newFilters = OrderedDict()
+ newFilters = {}
for key in keyList:
newFilters[key] = self._filters[dataKind][key]
@@ -597,34 +650,33 @@ class SaveAction(PlotAction):
The kind of data for which the provided filter is valid.
On of: 'all', 'curve', 'curves', 'image', 'scatter'
:return: {nameFilter: function} associations.
- :rtype: collections.OrderedDict
+ :rtype: dict
"""
- assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter')
+ assert dataKind in ("all", "curve", "curves", "image", "scatter")
return self._filters[dataKind].copy()
def _actionTriggered(self, checked=False):
"""Handle save action."""
# Set-up filters
- filters = OrderedDict()
+ filters = {}
# Add image filters if there is an active image
if self.plot.getActiveImage() is not None:
- filters.update(self._filters['image'].items())
+ filters.update(self._filters["image"].items())
# Add curve filters if there is a curve to save
- if (self.plot.getActiveCurve() is not None or
- len(self.plot.getAllCurves()) == 1):
- filters.update(self._filters['curve'].items())
+ if self.plot.getActiveCurve() is not None or len(self.plot.getAllCurves()) == 1:
+ filters.update(self._filters["curve"].items())
if len(self.plot.getAllCurves()) >= 1:
- filters.update(self._filters['curves'].items())
+ filters.update(self._filters["curves"].items())
# Add scatter filters if there is a scatter
# todo: CSV
if self.plot.getScatter() is not None:
- filters.update(self._filters['scatter'].items())
+ filters.update(self._filters["scatter"].items())
- filters.update(self._filters['all'].items())
+ filters.update(self._filters["all"].items())
# Create and run File dialog
dialog = qt.QFileDialog(self.plot)
@@ -653,14 +705,18 @@ class SaveAction(PlotAction):
filename = dialog.selectedFiles()[0]
dialog.close()
- if '(' in nameFilter and ')' == nameFilter.strip()[-1]:
+ if "(" in nameFilter and ")" == nameFilter.strip()[-1]:
# Check for correct file extension
# Extract file extensions as .something
- extensions = [ext[ext.find('.'):] for ext in
- nameFilter[nameFilter.find('(') + 1:-1].split()]
+ extensions = [
+ ext[ext.find(".") :]
+ for ext in nameFilter[nameFilter.find("(") + 1 : -1].split()
+ ]
for ext in extensions:
- if (len(filename) > len(ext) and
- filename[-len(ext):].lower() == ext.lower()):
+ if (
+ len(filename) > len(ext)
+ and filename[-len(ext) :].lower() == ext.lower()
+ ):
break
else: # filename has no extension supported in nameFilter, add one
if len(extensions) >= 1:
@@ -671,7 +727,7 @@ class SaveAction(PlotAction):
if func is not None:
return func(self.plot, filename, nameFilter)
else:
- _logger.error('Unsupported file filter: %s', nameFilter)
+ _logger.error("Unsupported file filter: %s", nameFilter)
return False
@@ -681,7 +737,7 @@ def _plotAsPNG(plot):
:param plot: The :class:`Plot` to save
"""
pngFile = BytesIO()
- plot.saveGraph(pngFile, fileFormat='png')
+ plot.saveGraph(pngFile, fileFormat="png")
pngFile.flush()
pngFile.seek(0)
data = pngFile.read()
@@ -703,10 +759,14 @@ class PrintAction(PlotAction):
def __init__(self, plot, parent=None):
super(PrintAction, self).__init__(
- plot, icon='document-print', text='Print...',
- tooltip='Open print dialog',
+ plot,
+ icon="document-print",
+ text="Print...",
+ tooltip="Open print dialog",
triggered=self.printPlot,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcut(qt.QKeySequence.Print)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@@ -717,11 +777,6 @@ class PrintAction(PlotAction):
"""
return printer.getDefaultPrinter()
- @property
- @deprecated(replacement="getPrinter()", since_version="0.8.0")
- def printer(self):
- return self.getPrinter()
-
def printPlotAsWidget(self):
"""Open the print dialog and print the plot.
@@ -730,7 +785,7 @@ class PrintAction(PlotAction):
:return: True if successful
"""
dialog = qt.QPrintDialog(self.getPrinter(), self.plot)
- dialog.setWindowTitle('Print Plot')
+ dialog.setWindowTitle("Print Plot")
if not dialog.exec():
return False
@@ -746,9 +801,9 @@ class PrintAction(PlotAction):
yScale = pageRect.height() / widget.height()
scale = min(xScale, yScale)
- painter.translate(pageRect.width() / 2., 0.)
+ painter.translate(pageRect.width() / 2.0, 0.0)
painter.scale(scale, scale)
- painter.translate(-widget.width() / 2., 0.)
+ painter.translate(-widget.width() / 2.0, 0.0)
widget.render(painter)
painter.end()
@@ -763,7 +818,7 @@ class PrintAction(PlotAction):
"""
# Init printer and start printer dialog
dialog = qt.QPrintDialog(self.getPrinter(), self.plot)
- dialog.setWindowTitle('Print Plot')
+ dialog.setWindowTitle("Print Plot")
if not dialog.exec():
return False
@@ -771,7 +826,7 @@ class PrintAction(PlotAction):
pngData = _plotAsPNG(self.plot)
pixmap = qt.QPixmap()
- pixmap.loadFromData(pngData, 'png')
+ pixmap.loadFromData(pngData, "png")
pageRect = self.getPrinter().pageRect(qt.QPrinter.DevicePixel)
xScale = pageRect.width() / pixmap.width()
@@ -783,10 +838,9 @@ class PrintAction(PlotAction):
if not painter.begin(self.getPrinter()):
return False
- painter.drawPixmap(0, 0,
- pixmap.width() * scale,
- pixmap.height() * scale,
- pixmap)
+ painter.drawPixmap(
+ 0, 0, pixmap.width() * scale, pixmap.height() * scale, pixmap
+ )
painter.end()
return True
@@ -801,10 +855,14 @@ class CopyAction(PlotAction):
def __init__(self, plot, parent=None):
super(CopyAction, self).__init__(
- plot, icon='edit-copy', text='Copy plot',
- tooltip='Copy a snapshot of the plot into the clipboard',
+ plot,
+ icon="edit-copy",
+ text="Copy plot",
+ tooltip="Copy a snapshot of the plot into the clipboard",
triggered=self.copyPlot,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcut(qt.QKeySequence.Copy)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@@ -812,5 +870,5 @@ class CopyAction(PlotAction):
"""Copy plot content to the clipboard as a bitmap."""
# Save Plot as PNG and make a QImage from it with default dpi
pngData = _plotAsPNG(self.plot)
- image = qt.QImage.fromData(pngData, 'png')
+ image = qt.QImage.fromData(pngData, "png")
qt.QApplication.clipboard().setImage(image)
diff --git a/src/silx/gui/plot/actions/medfilt.py b/src/silx/gui/plot/actions/medfilt.py
index 25fcdb2..a335499 100644
--- a/src/silx/gui/plot/actions/medfilt.py
+++ b/src/silx/gui/plot/actions/medfilt.py
@@ -54,12 +54,14 @@ class MedianFilterAction(PlotToolAction):
"""
def __init__(self, plot, parent=None):
- PlotToolAction.__init__(self,
- plot,
- icon='median-filter',
- text='median filter',
- tooltip='Apply a median filter on the image',
- parent=parent)
+ PlotToolAction.__init__(
+ self,
+ plot,
+ icon="median-filter",
+ text="median filter",
+ tooltip="Apply a median filter on the image",
+ parent=parent,
+ )
self._originalImage = None
self._legend = None
self._filteredImage = None
@@ -85,7 +87,9 @@ class MedianFilterAction(PlotToolAction):
self._originalImage = None
self._legend = None
else:
- self._originalImage = self.plot.getImage(self._activeImageLegend).getData(copy=False)
+ self._originalImage = self.plot.getImage(self._activeImageLegend).getData(
+ copy=False
+ )
self._legend = self.plot.getImage(self._activeImageLegend).getName()
def _updateFilter(self, kernelWidth, conditional=False):
@@ -94,13 +98,11 @@ class MedianFilterAction(PlotToolAction):
self.plot.sigActiveImageChanged.disconnect(self._updateActiveImage)
filteredImage = self._computeFilteredImage(kernelWidth, conditional)
- self.plot.addImage(data=filteredImage,
- legend=self._legend,
- replace=True)
+ self.plot.addImage(data=filteredImage, legend=self._legend, replace=True)
self.plot.sigActiveImageChanged.connect(self._updateActiveImage)
def _computeFilteredImage(self, kernelWidth, conditional):
- raise NotImplementedError('MedianFilterAction is a an abstract class')
+ raise NotImplementedError("MedianFilterAction is a an abstract class")
def getFilteredImage(self):
"""
@@ -114,16 +116,13 @@ class MedianFilter1DAction(MedianFilterAction):
:param plot: :class:`.PlotWidget` instance on which to operate
:param parent: See :class:`QAction`
"""
+
def __init__(self, plot, parent=None):
- MedianFilterAction.__init__(self,
- plot,
- parent=parent)
+ MedianFilterAction.__init__(self, plot, parent=parent)
def _computeFilteredImage(self, kernelWidth, conditional):
- assert(self.plot is not None)
- return medfilt2d(self._originalImage,
- (kernelWidth, 1),
- conditional)
+ assert self.plot is not None
+ return medfilt2d(self._originalImage, (kernelWidth, 1), conditional)
class MedianFilter2DAction(MedianFilterAction):
@@ -132,13 +131,10 @@ class MedianFilter2DAction(MedianFilterAction):
:param plot: :class:`.PlotWidget` instance on which to operate
:param parent: See :class:`QAction`
"""
+
def __init__(self, plot, parent=None):
- MedianFilterAction.__init__(self,
- plot,
- parent=parent)
+ MedianFilterAction.__init__(self, plot, parent=parent)
def _computeFilteredImage(self, kernelWidth, conditional):
- assert(self.plot is not None)
- return medfilt2d(self._originalImage,
- (kernelWidth, kernelWidth),
- conditional)
+ assert self.plot is not None
+ return medfilt2d(self._originalImage, (kernelWidth, kernelWidth), conditional)
diff --git a/src/silx/gui/plot/actions/mode.py b/src/silx/gui/plot/actions/mode.py
index 7edc8bb..511a8df 100644
--- a/src/silx/gui/plot/actions/mode.py
+++ b/src/silx/gui/plot/actions/mode.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -35,10 +35,11 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "16/08/2017"
-from . import PlotAction
-import logging
-_logger = logging.getLogger(__name__)
+from silx.gui import qt
+
+from ..tools.menus import ZoomEnabledAxesMenu
+from . import PlotAction
class ZoomModeAction(PlotAction):
@@ -50,25 +51,58 @@ class ZoomModeAction(PlotAction):
def __init__(self, plot, parent=None):
super(ZoomModeAction, self).__init__(
- plot, icon='zoom', text='Zoom mode',
- tooltip='Zoom in or out',
+ plot,
+ icon="zoom",
+ text="Zoom mode",
+ tooltip="Zoom-in on mouse selection",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
- # Listen to mode change
- self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
+ checkable=True,
+ parent=parent,
+ )
+
+ self.__menu = ZoomEnabledAxesMenu(self.plot, self.plot)
+
+ # Listen to interaction configuration change
+ self.plot.interaction().sigChanged.connect(self._interactionChanged)
# Init the state
- self._modeChanged(None)
+ self._interactionChanged()
+
+ def isAxesMenuEnabled(self) -> bool:
+ """Returns whether the axes selection menu is enabled or not (default: False)"""
+ return self.menu() is self.__menu
+
+ def setAxesMenuEnabled(self, enabled: bool):
+ """Toggle the availability of the axes selection menu (default: False)"""
+ if enabled == self.isAxesMenuEnabled():
+ return
+
+ self.setMenu(self.__menu if enabled else None)
+
+ # Update associated QToolButton's popupMode if any, this is not done at least with Qt5
+ parent = self.parent()
+ if not isinstance(parent, qt.QToolBar):
+ return
+ widget = parent.widgetForAction(self)
+ if not isinstance(widget, qt.QToolButton):
+ return
+ widget.setPopupMode(
+ qt.QToolButton.MenuButtonPopup if enabled else qt.QToolButton.DelayedPopup
+ )
+ widget.update()
+
+ def _interactionChanged(self):
+ plot = self.plot
+ if plot is None:
+ return
- def _modeChanged(self, source):
- modeDict = self.plot.getInteractiveMode()
- old = self.blockSignals(True)
- self.setChecked(modeDict["mode"] == "zoom")
- self.blockSignals(old)
+ self.setChecked(plot.getInteractiveMode()["mode"] == "zoom")
def _actionTriggered(self, checked=False):
plot = self.plot
- if plot is not None:
- plot.setInteractiveMode('zoom', source=self)
+ if plot is None:
+ return
+
+ plot.setInteractiveMode("zoom", source=self)
class PanModeAction(PlotAction):
@@ -80,10 +114,14 @@ class PanModeAction(PlotAction):
def __init__(self, plot, parent=None):
super(PanModeAction, self).__init__(
- plot, icon='pan', text='Pan mode',
- tooltip='Pan the view',
+ plot,
+ icon="pan",
+ text="Pan mode",
+ tooltip="Pan the view",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
# Listen to mode change
self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
# Init the state
@@ -98,4 +136,4 @@ class PanModeAction(PlotAction):
def _actionTriggered(self, checked=False):
plot = self.plot
if plot is not None:
- plot.setInteractiveMode('pan', source=self)
+ plot.setInteractiveMode("pan", source=self)
diff --git a/src/silx/gui/plot/backends/BackendBase.py b/src/silx/gui/plot/backends/BackendBase.py
index d7653f3..8d70286 100755
--- a/src/silx/gui/plot/backends/BackendBase.py
+++ b/src/silx/gui/plot/backends/BackendBase.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -28,20 +28,26 @@ It documents the Plot backend API.
This API is a simplified version of PyMca PlotBackend API.
"""
+from __future__ import annotations
+
+
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "21/12/2018"
+from collections.abc import Callable
import weakref
+from silx.gui.colors import RGBAColorType
+
from ... import qt
# Names for setCursor
-CURSOR_DEFAULT = 'default'
-CURSOR_POINTING = 'pointing'
-CURSOR_SIZE_HOR = 'size horizontal'
-CURSOR_SIZE_VER = 'size vertical'
-CURSOR_SIZE_ALL = 'size all'
+CURSOR_DEFAULT = "default"
+CURSOR_POINTING = "pointing"
+CURSOR_SIZE_HOR = "size horizontal"
+CURSOR_SIZE_VER = "size vertical"
+CURSOR_SIZE_ALL = "size all"
class BackendBase(object):
@@ -53,8 +59,8 @@ class BackendBase(object):
:param Plot plot: The Plot this backend is attached to
:param parent: The parent widget of the plot widget.
"""
- self.__xLimits = 1., 100.
- self.__yLimits = {'left': (1., 100.), 'right': (1., 100.)}
+ self.__xLimits = 1.0, 100.0
+ self.__yLimits = {"left": (1.0, 100.0), "right": (1.0, 100.0)}
self.__yAxisInverted = False
self.__keepDataAspectRatio = False
self.__xAxisTimeSeries = False
@@ -66,11 +72,11 @@ class BackendBase(object):
def _plot(self):
"""The plot this backend is attached to."""
if self._plotRef is None:
- raise RuntimeError('This backend is not attached to a Plot')
+ raise RuntimeError("This backend is not attached to a Plot")
plot = self._plotRef()
if plot is None:
- raise RuntimeError('This backend is no more attached to a Plot')
+ raise RuntimeError("This backend is no more attached to a Plot")
return plot
def _setPlot(self, plot):
@@ -82,11 +88,23 @@ class BackendBase(object):
# Add methods
- def addCurve(self, x, y,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror,
- fill, alpha, symbolsize, baseline):
+ def addCurve(
+ self,
+ x,
+ y,
+ color,
+ gapcolor,
+ symbol,
+ linewidth,
+ linestyle,
+ yaxis,
+ xerror,
+ yerror,
+ fill,
+ alpha,
+ symbolsize,
+ baseline,
+ ):
"""Add a 1D curve given by x an y to the graph.
:param numpy.ndarray x: The data corresponding to the x axis
@@ -94,6 +112,8 @@ class BackendBase(object):
:param color: color(s) to be used
:type color: string ("#RRGGBB") or (npoints, 4) unsigned byte array or
one of the predefined color names defined in colors.py
+ :param Union[str, None] gapcolor:
+ color used to fill dashed line gaps.
:param str symbol: Symbol to be drawn at each (x, y) position::
- ' ' or '' no symbol
@@ -106,13 +126,14 @@ class BackendBase(object):
- 's' square
:param float linewidth: The width of the curve in pixels
- :param str linestyle: Type of line::
+ :param linestyle: Type of line::
- ' ' or '' no line
- '-' solid line
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
+ - (offset, (dash pattern))
:param str yaxis: The Y axis this curve belongs to in: 'left', 'right'
:param xerror: Values with the uncertainties on the x values
@@ -127,9 +148,7 @@ class BackendBase(object):
"""
return object()
- def addImage(self, data,
- origin, scale,
- colormap, alpha):
+ def addImage(self, data, origin, scale, colormap, alpha):
"""Add an image to the plot.
:param numpy.ndarray data: (nrows, ncolumns) data or
@@ -147,8 +166,7 @@ class BackendBase(object):
"""
return object()
- def addTriangles(self, x, y, triangles,
- color, alpha):
+ def addTriangles(self, x, y, triangles, color, alpha):
"""Add a set of triangles.
:param numpy.ndarray x: The data corresponding to the x axis
@@ -161,8 +179,9 @@ class BackendBase(object):
"""
return object()
- def addShape(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
+ def addShape(
+ self, x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
+ ):
"""Add an item (i.e. a shape) to the plot.
:param numpy.ndarray x: The X coords of the points of the shape
@@ -172,7 +191,7 @@ class BackendBase(object):
:param str color: Color of the item
:param bool fill: True to fill the shape
:param bool overlay: True if item is an overlay, False otherwise
- :param str linestyle: Style of the line.
+ :param linestyle: Style of the line.
Only relevant for line markers where X or Y is None.
Value in:
@@ -181,25 +200,39 @@ class BackendBase(object):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
+ - (offset, (dash pattern))
:param float linewidth: Width of the line.
Only relevant for line markers where X or Y is None.
- :param str linebgcolor: Background color of the line, e.g., 'blue', 'b',
+ :param str gapcolor: Background color of the line, e.g., 'blue', 'b',
'#FF0000'. It is used to draw dotted line using a second color.
:returns: The handle used by the backend to univocally access the item
"""
return object()
- def addMarker(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
+ def addMarker(
+ self,
+ x: float | None,
+ y: float | None,
+ text: str | None,
+ color: str,
+ symbol: str | None,
+ linestyle: str | tuple[float, tuple[float, ...] | None],
+ linewidth: float,
+ constraint: Callable[[float, float], tuple[float, float]] | None,
+ yaxis: str,
+ font: qt.QFont,
+ bgcolor: RGBAColorType | None,
+ ) -> object:
"""Add a point, vertical line or horizontal line marker to the plot.
- :param float x: Horizontal position of the marker in graph coordinates.
- If None, the marker is a horizontal line.
- :param float y: Vertical position of the marker in graph coordinates.
- If None, the marker is a vertical line.
- :param str text: Text associated to the marker (or None for no text)
- :param str color: Color to be used for instance 'blue', 'b', '#FF0000'
- :param str symbol: Symbol representing the marker.
+ :param x: Horizontal position of the marker in graph coordinates.
+ If None, the marker is a horizontal line.
+ :param y: Vertical position of the marker in graph coordinates.
+ If None, the marker is a vertical line.
+ :param text: Text associated to the marker (or None for no text)
+ :param color: Color to be used for instance 'blue', 'b', '#FF0000'
+ :param bgcolor: Text background color to be used for instance 'blue', 'b', '#FF0000'
+ :param symbol: Symbol representing the marker.
Only relevant for point markers where X and Y are not None.
Value in:
@@ -210,7 +243,7 @@ class BackendBase(object):
- 'x' x-cross
- 'd' diamond
- 's' square
- :param str linestyle: Style of the line.
+ :param linestyle: Style of the line.
Only relevant for line markers where X or Y is None.
Value in:
@@ -219,16 +252,16 @@ class BackendBase(object):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
- :param float linewidth: Width of the line.
+ - (offset, (dash pattern))
+ :param linewidth: Width of the line.
Only relevant for line markers where X or Y is None.
:param constraint: A function filtering marker displacement by
- dragging operations or None for no filter.
- This function is called each time a marker is
- moved.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- and that returns the filtered coordinates.
- :param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
+ dragging operations or None for no filter.
+ This function is called each time a marker is moved.
+ It takes the coordinates of the current cursor position in the plot
+ as input and that returns the filtered coordinates.
+ :param yaxis: The Y axis this marker belongs to in: 'left', 'right'
+ :param font: QFont to use to render text
:return: Handle used by the backend to univocally access the marker
"""
return object()
@@ -270,8 +303,9 @@ class BackendBase(object):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
+ - (offset, (dash pattern))
- :type linestyle: None or one of the predefined styles.
+ :type linestyle: None, one of the predefined styles or (offset, (dash pattern)).
"""
pass
@@ -295,8 +329,8 @@ class BackendBase(object):
content = [item for item in content if condition(item)]
return sorted(
- content,
- key=lambda i: ((1 if i.isOverlay() else 0), i.getZValue()))
+ content, key=lambda i: ((1 if i.isOverlay() else 0), i.getZValue())
+ )
def pickItem(self, x, y, item):
"""Return picked indices if any, or None.
@@ -384,9 +418,9 @@ class BackendBase(object):
:param float y2max: maximum right axis value
"""
self.__xLimits = xmin, xmax
- self.__yLimits['left'] = ymin, ymax
+ self.__yLimits["left"] = ymin, ymax
if y2min is not None and y2max is not None:
- self.__yLimits['right'] = y2min, y2max
+ self.__yLimits["right"] = y2min, y2max
def getGraphXLimits(self):
"""Get the graph X (bottom) limits.
@@ -422,7 +456,6 @@ class BackendBase(object):
# Graph axes
-
def getXAxisTimeZone(self):
"""Returns tzinfo that is used if the X-Axis plots date-times.
@@ -480,6 +513,10 @@ class BackendBase(object):
"""Return True if left Y axis is inverted, False otherwise."""
return self.__yAxisInverted
+ def isYRightAxisVisible(self) -> bool:
+ """Return True if the Y axis on the right side of the plot is visible"""
+ return False
+
def isKeepDataAspectRatio(self):
"""Returns whether the plot is keeping data aspect ratio or not."""
return self.__keepDataAspectRatio
@@ -553,7 +590,7 @@ class BackendBase(object):
def setForegroundColors(self, foregroundColor, gridColor):
"""Set foreground and grid colors used to display this widget.
-
+
:param List[float] foregroundColor: RGBA foreground color of the widget
:param List[float] gridColor: RGBA grid color of the data view
"""
diff --git a/src/silx/gui/plot/backends/BackendMatplotlib.py b/src/silx/gui/plot/backends/BackendMatplotlib.py
index 1b31582..facb63c 100755
--- a/src/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/src/silx/gui/plot/backends/BackendMatplotlib.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2004-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -23,6 +23,8 @@
# ###########################################################################*/
"""Matplotlib Plot backend."""
+from __future__ import annotations
+
__authors__ = ["V.A. Sole", "T. Vincent, H. Payno"]
__license__ = "MIT"
__date__ = "21/12/2018"
@@ -33,7 +35,7 @@ import datetime as dt
from typing import Tuple, Union
import numpy
-from pkg_resources import parse_version as _parse_version
+from packaging.version import Version
_logger = logging.getLogger(__name__)
@@ -42,7 +44,11 @@ _logger = logging.getLogger(__name__)
from ... import qt
# First of all init matplotlib and set its backend
-from ...utils.matplotlib import FigureCanvasQTAgg
+from ...utils.matplotlib import (
+ DefaultTickFormatter,
+ FigureCanvasQTAgg,
+ qFontToFontProperties,
+)
import matplotlib
from matplotlib.container import Container
from matplotlib.figure import Figure
@@ -52,7 +58,7 @@ from matplotlib.backend_bases import MouseEvent
from matplotlib.lines import Line2D
from matplotlib.text import Text
from matplotlib.collections import PathCollection, LineCollection
-from matplotlib.ticker import Formatter, ScalarFormatter, Locator
+from matplotlib.ticker import Formatter, Locator
from matplotlib.tri import Triangulation
from matplotlib.collections import TriMesh
from matplotlib import path as mpath
@@ -60,15 +66,21 @@ from matplotlib import path as mpath
from . import BackendBase
from .. import items
from .._utils import FLOAT32_MINPOS
-from .._utils.dtime_ticklayout import calcTicks, bestFormatString, timestamp
+from .._utils.dtime_ticklayout import (
+ calcTicks,
+ formatDatetimes,
+ timestamp,
+)
from ...qt import inspect as qt_inspect
+from .... import config
+from silx.gui.colors import RGBAColorType
_PATCH_LINESTYLE = {
- "-": 'solid',
- "--": 'dashed',
- '-.': 'dashdot',
- ':': 'dotted',
- '': "solid",
+ "-": "solid",
+ "--": "dashed",
+ "-.": "dashdot",
+ ":": "dotted",
+ "": "solid",
None: "solid",
}
"""Patches do not uses the same matplotlib syntax"""
@@ -77,14 +89,14 @@ _MARKER_PATHS = {}
"""Store cached extra marker paths"""
_SPECIAL_MARKERS = {
- 'tickleft': 0,
- 'tickright': 1,
- 'tickup': 2,
- 'tickdown': 3,
- 'caretleft': 4,
- 'caretright': 5,
- 'caretup': 6,
- 'caretdown': 7,
+ "tickleft": 0,
+ "tickright": 1,
+ "tickup": 2,
+ "tickdown": 3,
+ "caretleft": 4,
+ "caretright": 5,
+ "caretup": 6,
+ "caretdown": 7,
}
@@ -92,6 +104,7 @@ def normalize_linestyle(linestyle):
"""Normalize known old-style linestyle, else return the provided value."""
return _PATCH_LINESTYLE.get(linestyle, linestyle)
+
def get_path_from_symbol(symbol):
"""Get the path representation of a symbol, else None if
it is not provided.
@@ -99,21 +112,40 @@ def get_path_from_symbol(symbol):
:param str symbol: Symbol description used by silx
:rtype: Union[None,matplotlib.path.Path]
"""
- if symbol == u'\u2665':
+ if symbol == "\u2665":
path = _MARKER_PATHS.get(symbol, None)
if path is not None:
return path
- vertices = numpy.array([
- [0,-99],
- [31,-73], [47,-55], [55,-46],
- [63,-37], [94,-2], [94,33],
- [94,69], [71,89], [47,89],
- [24,89], [8,74], [0,58],
- [-8,74], [-24,89], [-47,89],
- [-71,89], [-94,69], [-94,33],
- [-94,-2], [-63,-37], [-55,-46],
- [-47,-55], [-31,-73], [0,-99],
- [0,-99]])
+ vertices = numpy.array(
+ [
+ [0, -99],
+ [31, -73],
+ [47, -55],
+ [55, -46],
+ [63, -37],
+ [94, -2],
+ [94, 33],
+ [94, 69],
+ [71, 89],
+ [47, 89],
+ [24, 89],
+ [8, 74],
+ [0, 58],
+ [-8, 74],
+ [-24, 89],
+ [-47, 89],
+ [-71, 89],
+ [-94, 69],
+ [-94, 33],
+ [-94, -2],
+ [-63, -37],
+ [-55, -46],
+ [-47, -55],
+ [-31, -73],
+ [0, -99],
+ [0, -99],
+ ]
+ )
codes = [mpath.Path.CURVE4] * len(vertices)
codes[0] = mpath.Path.MOVETO
codes[-1] = mpath.Path.CLOSEPOLY
@@ -122,6 +154,7 @@ def get_path_from_symbol(symbol):
return path
return None
+
class NiceDateLocator(Locator):
"""
Matplotlib Locator that uses Nice Numbers algorithm (adapted to dates)
@@ -130,6 +163,7 @@ class NiceDateLocator(Locator):
Expects the data to be posix timestampes (i.e. seconds since 1970)
"""
+
def __init__(self, numTicks=5, tz=None):
"""
:param numTicks: target number of ticks
@@ -144,12 +178,12 @@ class NiceDateLocator(Locator):
@property
def spacing(self):
- """ The current spacing. Will be updated when new tick value are made"""
+ """The current spacing. Will be updated when new tick value are made"""
return self._spacing
@property
def unit(self):
- """ The current DtUnit. Will be updated when new tick value are made"""
+ """The current DtUnit. Will be updated when new tick value are made"""
return self._unit
def __call__(self):
@@ -158,8 +192,7 @@ class NiceDateLocator(Locator):
return self.tick_values(vmin, vmax)
def tick_values(self, vmin, vmax):
- """ Calculates tick values
- """
+ """Calculates tick values"""
if vmax < vmin:
vmin, vmax = vmax, vmin
@@ -171,8 +204,7 @@ class NiceDateLocator(Locator):
_logger.warning("Data range cannot be displayed with time axis")
return []
- dtTicks, self._spacing, self._unit = \
- calcTicks(dtMin, dtMax, self.numTicks)
+ dtTicks, self._spacing, self._unit = calcTicks(dtMin, dtMax, self.numTicks)
# Convert datetime back to time stamps.
ticks = [timestamp(dtTick) for dtTick in dtTicks]
@@ -194,21 +226,25 @@ class NiceAutoDateFormatter(Formatter):
self.locator = locator
self.tz = tz
- @property
- def formatString(self):
- if self.locator.spacing is None or self.locator.unit is None:
- # Locator has no spacing or units yet. Return elaborate fmtString
- return "Y-%m-%d %H:%M:%S"
- else:
- return bestFormatString(self.locator.spacing, self.locator.unit)
-
def __call__(self, x, pos=None):
"""Return the format for tick val *x* at position *pos*
- Expects x to be a POSIX timestamp (seconds since 1 Jan 1970)
+ Expects x to be a POSIX timestamp (seconds since 1 Jan 1970)
"""
- dateTime = dt.datetime.fromtimestamp(x, tz=self.tz)
- tickStr = dateTime.strftime(self.formatString)
- return tickStr
+ datetime = dt.datetime.fromtimestamp(x, tz=self.tz)
+ return formatDatetimes(
+ [datetime],
+ self.locator.spacing,
+ self.locator.unit,
+ )[datetime]
+
+ def format_ticks(self, values):
+ return tuple(
+ formatDatetimes(
+ [dt.datetime.fromtimestamp(value, tz=self.tz) for value in values],
+ self.locator.spacing,
+ self.locator.unit,
+ ).values()
+ )
class _PickableContainer(Container):
@@ -222,7 +258,7 @@ class _PickableContainer(Container):
def axes(self):
"""Mimin Artist.axes"""
for child in self.get_children():
- if hasattr(child, 'axes'):
+ if hasattr(child, "axes"):
return child.axes
return None
@@ -354,18 +390,19 @@ class _MarkerContainer(_PickableContainer):
:param yinverted: True if the y axis is inverted
"""
if self.text is not None:
- visible = ((self.x is None or xmin <= self.x <= xmax) and
- (self.y is None or ymin <= self.y <= ymax))
+ visible = (self.x is None or xmin <= self.x <= xmax) and (
+ self.y is None or ymin <= self.y <= ymax
+ )
self.text.set_visible(visible)
if self.x is not None and self.y is not None:
if self.symbol is None:
- valign = 'baseline'
+ valign = "baseline"
else:
if yinverted:
- valign = 'bottom'
+ valign = "bottom"
else:
- valign = 'top'
+ valign = "top"
self.text.set_verticalalignment(valign)
elif self.y is None: # vertical line
@@ -393,42 +430,47 @@ class _MarkerContainer(_PickableContainer):
return self.line.contains(mouseevent)
-class _DoubleColoredLinePatch(matplotlib.patches.Patch):
- """Matplotlib patch to display any patch using double color."""
+class SecondEdgeColorPatchMixIn:
+ """Mix-in class to add a second color for patches with dashed lines"""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._second_edgecolor = None
- def __init__(self, patch):
- super(_DoubleColoredLinePatch, self).__init__()
- self.__patch = patch
- self.linebgcolor = None
+ def set_second_edgecolor(self, color):
+ """Set the second color used to fill dashed edges"""
+ self._second_edgecolor = color
- def __getattr__(self, name):
- return getattr(self.__patch, name)
+ def get_second_edgecolor(self):
+ """Returns the second color used to fill dashed edges"""
+ return self._second_edgecolor
def draw(self, renderer):
- oldLineStype = self.__patch.get_linestyle()
- if self.linebgcolor is not None and oldLineStype != "solid":
- oldLineColor = self.__patch.get_edgecolor()
- oldHatch = self.__patch.get_hatch()
- self.__patch.set_linestyle("solid")
- self.__patch.set_edgecolor(self.linebgcolor)
- self.__patch.set_hatch(None)
- self.__patch.draw(renderer)
- self.__patch.set_linestyle(oldLineStype)
- self.__patch.set_edgecolor(oldLineColor)
- self.__patch.set_hatch(oldHatch)
- self.__patch.draw(renderer)
+ linestyle = self.get_linestyle()
+ if linestyle == "solid" or self.get_second_edgecolor() is None:
+ super().draw(renderer)
+ return
- def set_transform(self, transform):
- self.__patch.set_transform(transform)
+ edgecolor = self.get_edgecolor()
+ hatch = self.get_hatch()
- def get_path(self):
- return self.__patch.get_path()
+ self.set_linestyle("solid")
+ self.set_edgecolor(self.get_second_edgecolor())
+ self.set_hatch(None)
+ super().draw(renderer)
- def contains(self, mouseevent, radius=None):
- return self.__patch.contains(mouseevent, radius)
+ self.set_linestyle(linestyle)
+ self.set_edgecolor(edgecolor)
+ self.set_hatch(hatch)
+ super().draw(renderer)
- def contains_point(self, point, radius=None):
- return self.__patch.contains_point(point, radius)
+
+class Rectangle2EdgeColor(SecondEdgeColorPatchMixIn, Rectangle):
+ """Rectangle patch with a second edge color for dashed line"""
+
+
+class Polygon2EdgeColor(SecondEdgeColorPatchMixIn, Polygon):
+ """Polygon patch with a second edge color for dashed line"""
class Image(AxesImage):
@@ -438,10 +480,7 @@ class Image(AxesImage):
:param List[float] silx_scale: (sx, sy) Scale of the image.
"""
- def __init__(self, *args,
- silx_origin=(0., 0.),
- silx_scale=(1., 1.),
- **kwargs):
+ def __init__(self, *args, silx_origin=(0.0, 0.0), silx_scale=(1.0, 1.0), **kwargs):
super().__init__(*args, **kwargs)
self.__silx_origin = silx_origin
self.__silx_scale = silx_scale
@@ -456,7 +495,7 @@ class Image(AxesImage):
height, width = self.get_size()
column = numpy.clip(int((x - ox) / sx), 0, width - 1)
row = numpy.clip(int((y - oy) / sy), 0, height - 1)
- info['ind'] = (row,), (column,)
+ info["ind"] = (row,), (column,)
return inside, info
def set_data(self, A):
@@ -489,12 +528,17 @@ class BackendMatplotlib(BackendBase.BackendBase):
# when getting the limits at the expense of a replot
self._dirtyLimits = True
self._axesDisplayed = True
- self._matplotlibVersion = _parse_version(matplotlib.__version__)
+ self._matplotlibVersion = Version(matplotlib.__version__)
- self.fig = Figure()
+ self.fig = Figure(
+ tight_layout=config._MPL_TIGHT_LAYOUT,
+ )
self.fig.set_facecolor("w")
- self.ax = self.fig.add_axes([.15, .15, .75, .75], label="left")
+ if config._MPL_TIGHT_LAYOUT:
+ self.ax = self.fig.add_subplot(label="left")
+ else:
+ self.ax = self.fig.add_axes([0.15, 0.15, 0.75, 0.75], label="left")
self.ax2 = self.ax.twinx()
self.ax2.set_label("right")
# Make sure background of Axes is displayed
@@ -504,28 +548,17 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Set axis zorder=0.5 so grid is displayed at 0.5
self.ax.set_axisbelow(True)
- # disable the use of offsets
- try:
- axes = [
- self.ax.get_yaxis().get_major_formatter(),
- self.ax.get_xaxis().get_major_formatter(),
- self.ax2.get_yaxis().get_major_formatter(),
- self.ax2.get_xaxis().get_major_formatter(),
- ]
- for axis in axes:
- axis.set_useOffset(False)
- axis.set_scientific(False)
- except:
- _logger.warning('Cannot disabled axes offsets in %s '
- % matplotlib.__version__)
+ # Configure axes tick label formatter
+ for axis in (self.ax.yaxis, self.ax.xaxis, self.ax2.yaxis, self.ax2.xaxis):
+ axis.set_major_formatter(DefaultTickFormatter())
self.ax2.set_autoscaley_on(True)
# this works but the figure color is left
- if self._matplotlibVersion < _parse_version('2'):
- self.ax.set_axis_bgcolor('none')
+ if self._matplotlibVersion < Version("2"):
+ self.ax.set_axis_bgcolor("none")
else:
- self.ax.set_facecolor('none')
+ self.ax.set_facecolor("none")
self.fig.sca(self.ax)
self._background = None
@@ -534,30 +567,33 @@ class BackendMatplotlib(BackendBase.BackendBase):
self._graphCursor = tuple()
- self._enableAxis('right', False)
+ self._enableAxis("right", False)
self._isXAxisTimeSeries = False
def getItemsFromBackToFront(self, condition=None):
"""Order as BackendBase + take into account matplotlib Axes structure"""
+
def axesOrder(item):
if item.isOverlay():
return 2
- elif isinstance(item, items.YAxisMixIn) and item.getYAxis() == 'right':
+ elif isinstance(item, items.YAxisMixIn) and item.getYAxis() == "right":
return 1
else:
return 0
return sorted(
- BackendBase.BackendBase.getItemsFromBackToFront(
- self, condition=condition),
- key=axesOrder)
+ BackendBase.BackendBase.getItemsFromBackToFront(self, condition=condition),
+ key=axesOrder,
+ )
def _overlayItems(self):
"""Generator of backend renderer for overlay items"""
for item in self._plot.getItems():
- if (item.isOverlay() and
- item.isVisible() and
- item._backendRenderer is not None):
+ if (
+ item.isOverlay()
+ and item.isVisible()
+ and item._backendRenderer is not None
+ ):
yield item._backendRenderer
def _hasOverlays(self):
@@ -591,19 +627,40 @@ class BackendMatplotlib(BackendBase.BackendBase):
# This symbol must be supported by matplotlib
return symbol
- def addCurve(self, x, y,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror,
- fill, alpha, symbolsize, baseline):
- for parameter in (x, y, color, symbol, linewidth, linestyle,
- yaxis, fill, alpha, symbolsize):
+ def addCurve(
+ self,
+ x,
+ y,
+ color,
+ gapcolor,
+ symbol,
+ linewidth,
+ linestyle,
+ yaxis,
+ xerror,
+ yerror,
+ fill,
+ alpha,
+ symbolsize,
+ baseline,
+ ):
+ for parameter in (
+ x,
+ y,
+ color,
+ symbol,
+ linewidth,
+ linestyle,
+ yaxis,
+ fill,
+ alpha,
+ symbolsize,
+ ):
assert parameter is not None
- assert yaxis in ('left', 'right')
+ assert yaxis in ("left", "right")
- if (len(color) == 4 and
- type(color[3]) in [type(1), numpy.uint8, numpy.int8]):
- color = numpy.array(color, dtype=numpy.float64) / 255.
+ if len(color) == 4 and type(color[3]) in [type(1), numpy.uint8, numpy.int8]:
+ color = numpy.array(color, dtype=numpy.float64) / 255.0
if yaxis == "right":
axes = self.ax2
@@ -617,50 +674,62 @@ class BackendMatplotlib(BackendBase.BackendBase):
# First add errorbars if any so they are behind the curve
if xerror is not None or yerror is not None:
- if hasattr(color, 'dtype') and len(color) == len(x):
- errorbarColor = 'k'
+ if hasattr(color, "dtype") and len(color) == len(x):
+ errorbarColor = "k"
else:
errorbarColor = color
# Nx1 error array deprecated in matplotlib >=3.1 (removed in 3.3)
- if (isinstance(xerror, numpy.ndarray) and xerror.ndim == 2 and
- xerror.shape[1] == 1):
+ if (
+ isinstance(xerror, numpy.ndarray)
+ and xerror.ndim == 2
+ and xerror.shape[1] == 1
+ ):
xerror = numpy.ravel(xerror)
- if (isinstance(yerror, numpy.ndarray) and yerror.ndim == 2 and
- yerror.shape[1] == 1):
+ if (
+ isinstance(yerror, numpy.ndarray)
+ and yerror.ndim == 2
+ and yerror.shape[1] == 1
+ ):
yerror = numpy.ravel(yerror)
- errorbars = axes.errorbar(x, y,
- xerr=xerror, yerr=yerror,
- linestyle=' ', color=errorbarColor)
+ errorbars = axes.errorbar(
+ x, y, xerr=xerror, yerr=yerror, linestyle=" ", color=errorbarColor
+ )
artists += list(errorbars.get_children())
- if hasattr(color, 'dtype') and len(color) == len(x):
+ if hasattr(color, "dtype") and len(color) == len(x):
# scatter plot
if color.dtype not in [numpy.float32, numpy.float64]:
- actualColor = color / 255.
+ actualColor = color / 255.0
else:
actualColor = color
if linestyle not in ["", " ", None]:
# scatter plot with an actual line ...
# we need to assign a color ...
- curveList = axes.plot(x, y,
- linestyle=linestyle,
- color=actualColor[0],
- linewidth=linewidth,
- picker=True,
- pickradius=pickradius,
- marker=None)
+ curveList = axes.plot(
+ x,
+ y,
+ linestyle=linestyle,
+ color=actualColor[0],
+ linewidth=linewidth,
+ picker=True,
+ pickradius=pickradius,
+ marker=None,
+ )
artists += list(curveList)
marker = self._getMarkerFromSymbol(symbol)
- scatter = axes.scatter(x, y,
- color=actualColor,
- marker=marker,
- picker=True,
- pickradius=pickradius,
- s=symbolsize**2)
+ scatter = axes.scatter(
+ x,
+ y,
+ color=actualColor,
+ marker=marker,
+ picker=True,
+ pickradius=pickradius,
+ s=symbolsize**2,
+ )
artists.append(scatter)
if fill:
@@ -668,18 +737,28 @@ class BackendMatplotlib(BackendBase.BackendBase):
_baseline = FLOAT32_MINPOS
else:
_baseline = baseline
- artists.append(axes.fill_between(
- x, _baseline, y, facecolor=actualColor[0], linestyle=''))
+ artists.append(
+ axes.fill_between(
+ x, _baseline, y, facecolor=actualColor[0], linestyle=""
+ )
+ )
else: # Curve
- curveList = axes.plot(x, y,
- linestyle=linestyle,
- color=color,
- linewidth=linewidth,
- marker=symbol,
- picker=True,
- pickradius=pickradius,
- markersize=symbolsize)
+ curveList = axes.plot(
+ x,
+ y,
+ linestyle=linestyle,
+ color=color,
+ linewidth=linewidth,
+ marker=symbol,
+ picker=True,
+ pickradius=pickradius,
+ markersize=symbolsize,
+ )
+
+ if gapcolor is not None and self._matplotlibVersion >= Version("3.6.0"):
+ for line2d in curveList:
+ line2d.set_gapcolor(gapcolor)
artists += list(curveList)
if fill:
@@ -687,8 +766,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
_baseline = FLOAT32_MINPOS
else:
_baseline = baseline
- artists.append(
- axes.fill_between(x, _baseline, y, facecolor=color))
+ artists.append(axes.fill_between(x, _baseline, y, facecolor=color))
for artist in artists:
if alpha < 1:
@@ -709,12 +787,14 @@ class BackendMatplotlib(BackendBase.BackendBase):
height, width = data.shape[0:2]
# All image are shown as RGBA image
- image = Image(self.ax,
- interpolation='nearest',
- picker=True,
- origin='lower',
- silx_origin=origin,
- silx_scale=scale)
+ image = Image(
+ self.ax,
+ interpolation="nearest",
+ picker=True,
+ origin="lower",
+ silx_origin=origin,
+ silx_scale=scale,
+ )
if alpha < 1:
image.set_alpha(alpha)
@@ -722,21 +802,21 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Set image extent
xmin = origin[0]
xmax = xmin + scale[0] * width
- if scale[0] < 0.:
+ if scale[0] < 0.0:
xmin, xmax = xmax, xmin
ymin = origin[1]
ymax = ymin + scale[1] * height
- if scale[1] < 0.:
+ if scale[1] < 0.0:
ymin, ymax = ymax, ymin
image.set_extent((xmin, xmax, ymin, ymax))
# Set image data
- if scale[0] < 0. or scale[1] < 0.:
+ if scale[0] < 0.0 or scale[1] < 0.0:
# For negative scale, step by -1
- xstep = 1 if scale[0] >= 0. else -1
- ystep = 1 if scale[1] >= 0. else -1
+ xstep = 1 if scale[0] >= 0.0 else -1
+ ystep = 1 if scale[1] >= 0.0 else -1
data = data[::ystep, ::xstep]
if data.ndim == 2: # Data image, convert to RGBA image
@@ -745,7 +825,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Normalize uint16 data to have a similar behavior as opengl backend
data = data.astype(numpy.float32)
data /= 65535
-
+
image.set_data(data)
self.ax.add_artist(image)
return image
@@ -758,87 +838,92 @@ class BackendMatplotlib(BackendBase.BackendBase):
assert color.ndim == 2 and len(color) == len(x)
if color.dtype not in [numpy.float32, numpy.float64]:
- color = color.astype(numpy.float32) / 255.
+ color = color.astype(numpy.float32) / 255.0
collection = TriMesh(
- Triangulation(x, y, triangles),
- alpha=alpha,
- pickradius=0) # 0 enables picking on filled triangle
+ Triangulation(x, y, triangles), alpha=alpha, pickradius=0
+ ) # 0 enables picking on filled triangle
collection.set_color(color)
self.ax.add_collection(collection)
return collection
- def addShape(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
- if (linebgcolor is not None and
- shape not in ('rectangle', 'polygon', 'polylines')):
+ def addShape(
+ self, x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
+ ):
+ if gapcolor is not None and shape not in (
+ "rectangle",
+ "polygon",
+ "polylines",
+ ):
_logger.warning(
- 'linebgcolor not implemented for %s with matplotlib backend',
- shape)
+ "gapcolor not implemented for %s with matplotlib backend", shape
+ )
xView = numpy.array(x, copy=False)
yView = numpy.array(y, copy=False)
linestyle = normalize_linestyle(linestyle)
if shape == "line":
- item = self.ax.plot(x, y, color=color,
- linestyle=linestyle, linewidth=linewidth,
- marker=None)[0]
+ item = self.ax.plot(
+ x, y, color=color, linestyle=linestyle, linewidth=linewidth, marker=None
+ )[0]
elif shape == "hline":
if hasattr(y, "__len__"):
y = y[-1]
- item = self.ax.axhline(y, color=color,
- linestyle=linestyle, linewidth=linewidth)
+ item = self.ax.axhline(
+ y, color=color, linestyle=linestyle, linewidth=linewidth
+ )
elif shape == "vline":
if hasattr(x, "__len__"):
x = x[-1]
- item = self.ax.axvline(x, color=color,
- linestyle=linestyle, linewidth=linewidth)
+ item = self.ax.axvline(
+ x, color=color, linestyle=linestyle, linewidth=linewidth
+ )
- elif shape == 'rectangle':
+ elif shape == "rectangle":
xMin = numpy.nanmin(xView)
xMax = numpy.nanmax(xView)
yMin = numpy.nanmin(yView)
yMax = numpy.nanmax(yView)
w = xMax - xMin
h = yMax - yMin
- item = Rectangle(xy=(xMin, yMin),
- width=w,
- height=h,
- fill=False,
- color=color,
- linestyle=linestyle,
- linewidth=linewidth)
- if fill:
- item.set_hatch('.')
+ item = Rectangle2EdgeColor(
+ xy=(xMin, yMin),
+ width=w,
+ height=h,
+ fill=False,
+ color=color,
+ linestyle=linestyle,
+ linewidth=linewidth,
+ )
+ item.set_second_edgecolor(gapcolor)
- if linestyle != "solid" and linebgcolor is not None:
- item = _DoubleColoredLinePatch(item)
- item.linebgcolor = linebgcolor
+ if fill:
+ item.set_hatch(".")
self.ax.add_patch(item)
- elif shape in ('polygon', 'polylines'):
+ elif shape in ("polygon", "polylines"):
points = numpy.array((xView, yView)).T
- if shape == 'polygon':
+ if shape == "polygon":
closed = True
else: # shape == 'polylines'
closed = numpy.all(numpy.equal(points[0], points[-1]))
- item = Polygon(points,
- closed=closed,
- fill=False,
- color=color,
- linestyle=linestyle,
- linewidth=linewidth)
- if fill and shape == 'polygon':
- item.set_hatch('/')
-
- if linestyle != "solid" and linebgcolor is not None:
- item = _DoubleColoredLinePatch(item)
- item.linebgcolor = linebgcolor
+ item = Polygon2EdgeColor(
+ points,
+ closed=closed,
+ fill=False,
+ color=color,
+ linestyle=linestyle,
+ linewidth=linewidth,
+ )
+ item.set_second_edgecolor(gapcolor)
+
+ if fill and shape == "polygon":
+ item.set_hatch("/")
self.ax.add_patch(item)
@@ -850,61 +935,87 @@ class BackendMatplotlib(BackendBase.BackendBase):
return item
- def addMarker(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
+ def addMarker(
+ self,
+ x,
+ y,
+ text,
+ color,
+ symbol,
+ linestyle,
+ linewidth,
+ constraint,
+ yaxis,
+ font,
+ bgcolor: RGBAColorType | None,
+ ):
textArtist = None
+ fontProperties = None if font is None else qFontToFontProperties(font)
xmin, xmax = self.getGraphXLimits()
ymin, ymax = self.getGraphYLimits(axis=yaxis)
- if yaxis == 'left':
+ if yaxis == "left":
ax = self.ax
- elif yaxis == 'right':
+ elif yaxis == "right":
ax = self.ax2
else:
- assert(False)
+ assert False
+
+ if bgcolor is None:
+ bgcolor = "none"
marker = self._getMarkerFromSymbol(symbol)
if x is not None and y is not None:
- line = ax.plot(x, y,
- linestyle=" ",
- color=color,
- marker=marker,
- markersize=10.)[-1]
+ line = ax.plot(
+ x, y, linestyle=" ", color=color, marker=marker, markersize=10.0
+ )[-1]
if text is not None:
- textArtist = _TextWithOffset(x, y, text,
- color=color,
- horizontalalignment='left')
+ textArtist = _TextWithOffset(
+ x,
+ y,
+ text,
+ color=color,
+ backgroundcolor=bgcolor,
+ horizontalalignment="left",
+ fontproperties=fontProperties,
+ )
if symbol is not None:
textArtist.pixel_offset = 10, 3
elif x is not None:
- line = ax.axvline(x,
- color=color,
- linewidth=linewidth,
- linestyle=linestyle)
+ line = ax.axvline(x, color=color, linewidth=linewidth, linestyle=linestyle)
if text is not None:
# Y position will be updated in updateMarkerText call
- textArtist = _TextWithOffset(x, 1., text,
- color=color,
- horizontalalignment='left',
- verticalalignment='top')
+ textArtist = _TextWithOffset(
+ x,
+ 1.0,
+ text,
+ color=color,
+ backgroundcolor=bgcolor,
+ horizontalalignment="left",
+ verticalalignment="top",
+ fontproperties=fontProperties,
+ )
textArtist.pixel_offset = 5, 3
elif y is not None:
- line = ax.axhline(y,
- color=color,
- linewidth=linewidth,
- linestyle=linestyle)
+ line = ax.axhline(y, color=color, linewidth=linewidth, linestyle=linestyle)
if text is not None:
# X position will be updated in updateMarkerText call
- textArtist = _TextWithOffset(1., y, text,
- color=color,
- horizontalalignment='right',
- verticalalignment='top')
+ textArtist = _TextWithOffset(
+ 1.0,
+ y,
+ text,
+ color=color,
+ backgroundcolor=bgcolor,
+ horizontalalignment="right",
+ verticalalignment="top",
+ fontproperties=fontProperties,
+ )
textArtist.pixel_offset = 5, 3
else:
- raise RuntimeError('A marker must at least have one coordinate')
+ raise RuntimeError("A marker must at least have one coordinate")
line.set_picker(True)
line.set_pickradius(5)
@@ -928,7 +1039,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
yinverted = self.isYAxisInverted()
for item in self._overlayItems():
if isinstance(item, _MarkerContainer):
- if item.yAxis == 'left':
+ if item.yAxis == "left":
item.updateMarkerText(xmin, xmax, ymin1, ymax1, yinverted)
else:
item.updateMarkerText(xmin, xmax, ymin2, ymax2, yinverted)
@@ -946,13 +1057,21 @@ class BackendMatplotlib(BackendBase.BackendBase):
def setGraphCursor(self, flag, color, linewidth, linestyle):
if flag:
lineh = self.ax.axhline(
- self.ax.get_ybound()[0], visible=False, color=color,
- linewidth=linewidth, linestyle=linestyle)
+ self.ax.get_ybound()[0],
+ visible=False,
+ color=color,
+ linewidth=linewidth,
+ linestyle=linestyle,
+ )
lineh.set_animated(True)
linev = self.ax.axvline(
- self.ax.get_xbound()[0], visible=False, color=color,
- linewidth=linewidth, linestyle=linestyle)
+ self.ax.get_xbound()[0],
+ visible=False,
+ color=color,
+ linewidth=linewidth,
+ linestyle=linestyle,
+ )
linev.set_animated(True)
self._graphCursor = lineh, linev
@@ -974,8 +1093,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
artist.set_facecolors(color)
artist.set_edgecolors(color)
else:
- _logger.warning(
- 'setActiveCurve ignoring artist %s', str(artist))
+ _logger.warning("setActiveCurve ignoring artist %s", str(artist))
# Misc.
@@ -988,8 +1106,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
:param str axis: Axis name: 'left' or 'right'
:param bool flag: Default, True
"""
- assert axis in ('right', 'left')
- axes = self.ax2 if axis == 'right' else self.ax
+ assert axis in ("right", "left")
+ axes = self.ax2 if axis == "right" else self.ax
axes.get_yaxis().set_visible(flag)
def replot(self):
@@ -1007,18 +1125,20 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Hide right Y axis if no line is present
self._dirtyLimits = False
if not self.ax2.lines:
- self._enableAxis('right', False)
+ self._enableAxis("right", False)
def _drawOverlays(self):
"""Draw overlays if any."""
+
def condition(item):
- return (item.isVisible() and
- item._backendRenderer is not None and
- item.isOverlay())
+ return (
+ item.isVisible()
+ and item._backendRenderer is not None
+ and item.isOverlay()
+ )
for item in self.getItemsFromBackToFront(condition=condition):
- if (isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right'):
+ if isinstance(item, items.YAxisMixIn) and item.getYAxis() == "right":
axes = self.ax2
else:
axes = self.ax
@@ -1030,14 +1150,15 @@ class BackendMatplotlib(BackendBase.BackendBase):
def updateZOrder(self):
"""Reorder all items with z order from 0 to 1"""
items = self.getItemsFromBackToFront(
- lambda item: item.isVisible() and item._backendRenderer is not None)
+ lambda item: item.isVisible() and item._backendRenderer is not None
+ )
count = len(items)
for index, item in enumerate(items):
if item.getZValue() < 0.5:
# Make sure matplotlib z order is below the grid (with z=0.5)
zorder = 0.5 * index / count
else: # Make sure matplotlib z order is above the grid (> 0.5)
- zorder = 1. + index / count
+ zorder = 1.0 + index / count
if zorder != item._backendRenderer.get_zorder():
item._backendRenderer.set_zorder(zorder)
@@ -1060,7 +1181,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax.set_xlabel(label)
def setGraphYLabel(self, label, axis):
- axes = self.ax if axis == 'left' else self.ax2
+ axes = self.ax if axis == "left" else self.ax2
axes.set_ylabel(label)
# Graph limits
@@ -1096,8 +1217,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
self._updateMarkers()
def getGraphYLimits(self, axis):
- assert axis in ('left', 'right')
- ax = self.ax2 if axis == 'right' else self.ax
+ assert axis in ("left", "right")
+ ax = self.ax2 if axis == "right" else self.ax
if not ax.get_visible():
return None
@@ -1110,7 +1231,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
return ax.get_ybound()
def setGraphYLimits(self, ymin, ymax, axis):
- ax = self.ax2 if axis == 'right' else self.ax
+ ax = self.ax2 if axis == "right" else self.ax
if ymax < ymin:
ymin, ymax = ymax, ymin
self._dirtyLimits = True
@@ -1137,6 +1258,23 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Graph axes
+ def __initXAxisFormatterAndLocator(self):
+ if self.ax.xaxis.get_scale() != "linear":
+ return # Do not override formatter and locator
+
+ if not self.isXAxisTimeSeries():
+ self.ax.xaxis.set_major_formatter(DefaultTickFormatter())
+ return
+
+ # We can't use a matplotlib.dates.DateFormatter because it expects
+ # the data to be in datetimes. Silx works internally with
+ # timestamps (floats).
+ locator = NiceDateLocator(tz=self.getXAxisTimeZone())
+ self.ax.xaxis.set_major_locator(locator)
+ self.ax.xaxis.set_major_formatter(
+ NiceAutoDateFormatter(locator, tz=self.getXAxisTimeZone())
+ )
+
def setXAxisTimeZone(self, tz):
super(BackendMatplotlib, self).setXAxisTimeZone(tz)
@@ -1148,40 +1286,27 @@ class BackendMatplotlib(BackendBase.BackendBase):
def setXAxisTimeSeries(self, isTimeSeries):
self._isXAxisTimeSeries = isTimeSeries
- if self._isXAxisTimeSeries:
- # We can't use a matplotlib.dates.DateFormatter because it expects
- # the data to be in datetimes. Silx works internally with
- # timestamps (floats).
- locator = NiceDateLocator(tz=self.getXAxisTimeZone())
- self.ax.xaxis.set_major_locator(locator)
- self.ax.xaxis.set_major_formatter(
- NiceAutoDateFormatter(locator, tz=self.getXAxisTimeZone()))
- else:
- try:
- scalarFormatter = ScalarFormatter(useOffset=False)
- except:
- _logger.warning('Cannot disabled axes offsets in %s ' %
- matplotlib.__version__)
- scalarFormatter = ScalarFormatter()
- self.ax.xaxis.set_major_formatter(scalarFormatter)
+ self.__initXAxisFormatterAndLocator()
def setXAxisLogarithmic(self, flag):
# Workaround for matplotlib 2.1.0 when one tries to set an axis
# to log scale with both limits <= 0
# In this case a draw with positive limits is needed first
- if flag and self._matplotlibVersion >= _parse_version('2.1.0'):
+ if flag and self._matplotlibVersion >= Version("2.1.0"):
xlim = self.ax.get_xlim()
if xlim[0] <= 0 and xlim[1] <= 0:
self.ax.set_xlim(1, 10)
self.draw()
- self.ax2.set_xscale('log' if flag else 'linear')
- self.ax.set_xscale('log' if flag else 'linear')
+ xscale = "log" if flag else "linear"
+ self.ax2.set_xscale(xscale)
+ self.ax.set_xscale(xscale)
+ self.__initXAxisFormatterAndLocator()
def setYAxisLogarithmic(self, flag):
# Workaround for matplotlib 2.0 issue with negative bounds
# before switching to log scale
- if flag and self._matplotlibVersion >= _parse_version('2.0.0'):
+ if flag and self._matplotlibVersion >= Version("2.0.0"):
redraw = False
for axis, dataRangeIndex in ((self.ax, 1), (self.ax2, 2)):
ylim = axis.get_ylim()
@@ -1194,8 +1319,15 @@ class BackendMatplotlib(BackendBase.BackendBase):
if redraw:
self.draw()
- self.ax2.set_yscale('log' if flag else 'linear')
- self.ax.set_yscale('log' if flag else 'linear')
+ if flag:
+ self.ax2.set_yscale("log")
+ self.ax.set_yscale("log")
+ return
+
+ self.ax2.set_yscale("linear")
+ self.ax2.yaxis.set_major_formatter(DefaultTickFormatter())
+ self.ax.set_yscale("linear")
+ self.ax.yaxis.set_major_formatter(DefaultTickFormatter())
def setYAxisInverted(self, flag):
if self.ax.yaxis_inverted() != bool(flag):
@@ -1205,15 +1337,18 @@ class BackendMatplotlib(BackendBase.BackendBase):
def isYAxisInverted(self):
return self.ax.yaxis_inverted()
+ def isYRightAxisVisible(self):
+ return self.ax2.yaxis.get_visible()
+
def isKeepDataAspectRatio(self):
- return self.ax.get_aspect() in (1.0, 'equal')
+ return self.ax.get_aspect() in (1.0, "equal")
def setKeepDataAspectRatio(self, flag):
- self.ax.set_aspect(1.0 if flag else 'auto')
- self.ax2.set_aspect(1.0 if flag else 'auto')
+ self.ax.set_aspect(1.0 if flag else "auto")
+ self.ax2.set_aspect(1.0 if flag else "auto")
def setGraphGrid(self, which):
- self.ax.grid(False, which='both') # Disable all grid first
+ self.ax.grid(False, which="both") # Disable all grid first
if which is not None:
self.ax.grid(True, which=which)
@@ -1221,23 +1356,19 @@ class BackendMatplotlib(BackendBase.BackendBase):
def _getDevicePixelRatio(self) -> float:
"""Compatibility wrapper for devicePixelRatioF"""
- return 1.
+ return 1.0
def _mplToQtPosition(
- self,
- x: Union[float,numpy.ndarray],
- y: Union[float,numpy.ndarray]
- ) -> Tuple[Union[float,numpy.ndarray], Union[float,numpy.ndarray]]:
- """Convert matplotlib "display" space coord to Qt widget logical pixel
- """
+ self, x: Union[float, numpy.ndarray], y: Union[float, numpy.ndarray]
+ ) -> Tuple[Union[float, numpy.ndarray], Union[float, numpy.ndarray]]:
+ """Convert matplotlib "display" space coord to Qt widget logical pixel"""
ratio = self._getDevicePixelRatio()
# Convert from matplotlib origin (bottom) to Qt origin (top)
# and apply device pixel ratio
return x / ratio, (self.fig.get_window_extent().height - y) / ratio
def _qtToMplPosition(self, x: float, y: float) -> Tuple[float, float]:
- """Convert Qt widget logical pixel to matplotlib "display" space coord
- """
+ """Convert Qt widget logical pixel to matplotlib "display" space coord"""
ratio = self._getDevicePixelRatio()
# Apply device pixel ration and
# convert from Qt origin (top) to matplotlib origin (bottom)
@@ -1258,18 +1389,33 @@ class BackendMatplotlib(BackendBase.BackendBase):
bbox = self.ax.get_window_extent()
# Warning this is not returning int...
ratio = self._getDevicePixelRatio()
- return tuple(int(value / ratio) for value in (
- bbox.xmin,
- self.fig.get_window_extent().height - bbox.ymax,
- bbox.width,
- bbox.height))
+ return tuple(
+ int(value / ratio)
+ for value in (
+ bbox.xmin,
+ self.fig.get_window_extent().height - bbox.ymax,
+ bbox.width,
+ bbox.height,
+ )
+ )
def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
- width, height = 1. - left - right, 1. - top - bottom
+ width, height = 1.0 - left - right, 1.0 - top - bottom
position = left, bottom, width, height
+ istight = config._MPL_TIGHT_LAYOUT and (left, top, right, bottom) != (
+ 0,
+ 0,
+ 0,
+ 0,
+ )
+ if self._matplotlibVersion >= Version("3.6"):
+ self.fig.set_layout_engine("tight" if istight else None)
+ else:
+ self.fig.set_tight_layout(True if istight else None)
+
# Toggle display of axes and viewbox rect
- isFrameOn = position != (0., 0., 1., 1.)
+ isFrameOn = position != (0.0, 0.0, 1.0, 1.0)
self.ax.set_frame_on(isFrameOn)
self.ax2.set_frame_on(isFrameOn)
@@ -1291,7 +1437,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
if self.ax.get_frame_on():
self.fig.patch.set_facecolor(backgroundColor)
- if self._matplotlibVersion < _parse_version('2'):
+ if self._matplotlibVersion < Version("2"):
self.ax.set_axis_bgcolor(dataBackgroundColor)
else:
self.ax.set_facecolor(dataBackgroundColor)
@@ -1309,12 +1455,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
for axes in (self.ax, self.ax2):
if axes.get_frame_on():
- axes.spines['bottom'].set_color(foregroundColor)
- axes.spines['top'].set_color(foregroundColor)
- axes.spines['right'].set_color(foregroundColor)
- axes.spines['left'].set_color(foregroundColor)
- axes.tick_params(axis='x', colors=foregroundColor)
- axes.tick_params(axis='y', colors=foregroundColor)
+ axes.spines["bottom"].set_color(foregroundColor)
+ axes.spines["top"].set_color(foregroundColor)
+ axes.spines["right"].set_color(foregroundColor)
+ axes.spines["left"].set_color(foregroundColor)
+ axes.tick_params(axis="x", colors=foregroundColor)
+ axes.tick_params(axis="y", colors=foregroundColor)
axes.yaxis.label.set_color(foregroundColor)
axes.xaxis.label.set_color(foregroundColor)
axes.title.set_color(foregroundColor)
@@ -1350,19 +1496,19 @@ class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
self._limitsBeforeResize = None
FigureCanvasQTAgg.setSizePolicy(
- self, qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ self, qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding
+ )
FigureCanvasQTAgg.updateGeometry(self)
# Make postRedisplay asynchronous using Qt signal
- self._sigPostRedisplay.connect(
- self.__deferredReplot, qt.Qt.QueuedConnection)
+ self._sigPostRedisplay.connect(self.__deferredReplot, qt.Qt.QueuedConnection)
self._picked = None
- self.mpl_connect('button_press_event', self._onMousePress)
- self.mpl_connect('button_release_event', self._onMouseRelease)
- self.mpl_connect('motion_notify_event', self._onMouseMove)
- self.mpl_connect('scroll_event', self._onMouseWheel)
+ self.mpl_connect("button_press_event", self._onMousePress)
+ self.mpl_connect("button_release_event", self._onMouseRelease)
+ self.mpl_connect("motion_notify_event", self._onMouseMove)
+ self.mpl_connect("scroll_event", self._onMouseWheel)
def postRedisplay(self):
self._sigPostRedisplay.emit()
@@ -1370,23 +1516,21 @@ class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
def __deferredReplot(self):
# Since this is deferred, makes sure it is still needed
plot = self._plotRef()
- if (plot is not None and
- plot._getDirtyPlot() and
- plot.getBackend() is self):
+ if plot is not None and plot._getDirtyPlot() and plot.getBackend() is self:
self.replot()
def _getDevicePixelRatio(self) -> float:
"""Compatibility wrapper for devicePixelRatioF"""
- if hasattr(self, 'devicePixelRatioF'):
+ if hasattr(self, "devicePixelRatioF"):
ratio = self.devicePixelRatioF()
else: # Qt < 5.6 compatibility
ratio = float(self.devicePixelRatio())
# Safety net: avoid returning 0
- return ratio if ratio != 0. else 1.
+ return ratio if ratio != 0.0 else 1.0
# Mouse event forwarding
- _MPL_TO_PLOT_BUTTONS = {1: 'left', 2: 'middle', 3: 'right'}
+ _MPL_TO_PLOT_BUTTONS = {1: "left", 2: "middle", 3: "right"}
def _onMousePress(self, event):
button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None)
@@ -1397,8 +1541,7 @@ class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
def _onMouseMove(self, event):
x, y = self._mplToQtPosition(event.x, event.y)
if self._graphCursor:
- position = self._plot.pixelToData(
- x, y, axis='left', check=True)
+ position = self._plot.pixelToData(x, y, axis="left", check=True)
lineh, linev = self._graphCursor
if position is not None:
linev.set_visible(True)
@@ -1407,9 +1550,9 @@ class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
lineh.set_ydata((position[1], position[1]))
self._plot._setDirtyPlot(overlayOnly=True)
elif lineh.get_visible():
- lineh.set_visible(False)
- linev.set_visible(False)
- self._plot._setDirtyPlot(overlayOnly=True)
+ lineh.set_visible(False)
+ linev.set_visible(False)
+ self._plot._setDirtyPlot(overlayOnly=True)
# onMouseMove must trigger replot if dirty flag is raised
self._plot.onMouseMove(int(x), int(y))
@@ -1438,11 +1581,13 @@ class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
def pickItem(self, x, y, item):
xDisplay, yDisplay = self._qtToMplPosition(x, y)
mouseEvent = MouseEvent(
- 'button_press_event', self, int(xDisplay), int(yDisplay))
+ "button_press_event", self, int(xDisplay), int(yDisplay)
+ )
# Override axes and data position with the axes
mouseEvent.inaxes = item.axes
mouseEvent.xdata, mouseEvent.ydata = self.pixelToData(
- x, y, axis='left' if item.axes is self.ax else 'right')
+ x, y, axis="left" if item.axes is self.ax else "right"
+ )
picked, info = item.contains(mouseEvent)
if not picked:
@@ -1451,26 +1596,30 @@ class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
elif isinstance(item, TriMesh):
# Convert selected triangle to data point indices
triangulation = item._triangulation
- indices = triangulation.get_masked_triangles()[info['ind'][0]]
+ indices = triangulation.get_masked_triangles()[info["ind"][0]]
# Sort picked triangle points by distance to mouse
# from furthest to closest to put closest point last
# This is to be somewhat consistent with last scatter point
# being the top one.
- xdata, ydata = self.pixelToData(x, y, axis='left')
- dists = ((triangulation.x[indices] - xdata) ** 2 +
- (triangulation.y[indices] - ydata) ** 2)
+ xdata, ydata = self.pixelToData(x, y, axis="left")
+ dists = (triangulation.x[indices] - xdata) ** 2 + (
+ triangulation.y[indices] - ydata
+ ) ** 2
return indices[numpy.flip(numpy.argsort(dists), axis=0)]
else: # Returns indices if any
- return info.get('ind', ())
+ return info.get("ind", ())
# replot control
def resizeEvent(self, event):
# Store current limits
self._limitsBeforeResize = (
- self.ax.get_xbound(), self.ax.get_ybound(), self.ax2.get_ybound())
+ self.ax.get_xbound(),
+ self.ax.get_ybound(),
+ self.ax2.get_ybound(),
+ )
FigureCanvasQTAgg.resizeEvent(self, event)
if self.isKeepDataAspectRatio() or self._hasOverlays():
@@ -1490,15 +1639,20 @@ class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
self.updateZOrder()
+ if not qt_inspect.isValid(self):
+ _logger.info("draw requested but widget no longer exists")
+ return
+
# Starting with mpl 2.1.0, toggling autoscale raises a ValueError
# in some situations. See #1081, #1136, #1163,
- if self._matplotlibVersion >= _parse_version("2.0.0"):
+ if self._matplotlibVersion >= Version("2.0.0"):
try:
FigureCanvasQTAgg.draw(self)
except ValueError as err:
_logger.debug(
- "ValueError caught while calling FigureCanvasQTAgg.draw: "
- "'%s'", err)
+ "ValueError caught while calling FigureCanvasQTAgg.draw: " "'%s'",
+ err,
+ )
else:
FigureCanvasQTAgg.draw(self)
@@ -1513,16 +1667,15 @@ class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
xLimits, yLimits, yRightLimits = self._limitsBeforeResize
self._limitsBeforeResize = None
- if (xLimits != self.ax.get_xbound() or
- yLimits != self.ax.get_ybound()):
+ if xLimits != self.ax.get_xbound() or yLimits != self.ax.get_ybound():
self._updateMarkers()
if xLimits != self.ax.get_xbound():
self._plot.getXAxis()._emitLimitsChanged()
if yLimits != self.ax.get_ybound():
- self._plot.getYAxis(axis='left')._emitLimitsChanged()
+ self._plot.getYAxis(axis="left")._emitLimitsChanged()
if yRightLimits != self.ax2.get_ybound():
- self._plot.getYAxis(axis='right')._emitLimitsChanged()
+ self._plot.getYAxis(axis="right")._emitLimitsChanged()
self._drawOverlays()
@@ -1536,7 +1689,7 @@ class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
dirtyFlag = self._plot._getDirtyPlot()
- if dirtyFlag == 'overlay':
+ if dirtyFlag == "overlay":
# Only redraw overlays using fast rendering path
if self._background is None:
self._background = self.copy_from_bbox(self.fig.bbox)
@@ -1548,8 +1701,9 @@ class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
self.draw()
# Workaround issue of rendering overlays with some matplotlib versions
- if (_parse_version('1.5') <= self._matplotlibVersion < _parse_version('2.1') and
- not hasattr(self, '_firstReplot')):
+ if Version("1.5") <= self._matplotlibVersion < Version(
+ "2.1"
+ ) and not hasattr(self, "_firstReplot"):
self._firstReplot = False
if self._hasOverlays():
qt.QTimer.singleShot(0, self.draw) # Request async draw
diff --git a/src/silx/gui/plot/backends/BackendOpenGL.py b/src/silx/gui/plot/backends/BackendOpenGL.py
index d7e8346..370f14b 100755
--- a/src/silx/gui/plot/backends/BackendOpenGL.py
+++ b/src/silx/gui/plot/backends/BackendOpenGL.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2014-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 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
@@ -23,6 +23,8 @@
# ############################################################################*/
"""OpenGL Plot backend."""
+from __future__ import annotations
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "21/12/2018"
@@ -42,6 +44,7 @@ from ..._glutils import gl
from ... import _glutils as glu
from . import glutils
from .glutils.PlotImageFile import saveImageToFile
+from silx.gui.colors import RGBAColorType
_logger = logging.getLogger(__name__)
@@ -52,64 +55,95 @@ _logger = logging.getLogger(__name__)
# Content #####################################################################
+
class _ShapeItem(dict):
- def __init__(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
+ def __init__(
+ self,
+ x,
+ y,
+ shape,
+ color,
+ fill,
+ overlay,
+ linewidth,
+ dashoffset,
+ dashpattern,
+ gapcolor,
+ ):
super(_ShapeItem, self).__init__()
- if shape not in ('polygon', 'rectangle', 'line',
- 'vline', 'hline', 'polylines'):
+ if shape not in ("polygon", "rectangle", "line", "vline", "hline", "polylines"):
raise NotImplementedError("Unsupported shape {0}".format(shape))
x = numpy.array(x, copy=False)
y = numpy.array(y, copy=False)
- if shape == 'rectangle':
+ if shape == "rectangle":
xMin, xMax = x
x = numpy.array((xMin, xMin, xMax, xMax))
yMin, yMax = y
y = numpy.array((yMin, yMax, yMax, yMin))
# Ignore fill for polylines to mimic matplotlib
- fill = fill if shape != 'polylines' else False
-
- self.update({
- 'shape': shape,
- 'color': colors.rgba(color),
- 'fill': 'hatch' if fill else None,
- 'x': x,
- 'y': y,
- 'linestyle': linestyle,
- 'linewidth': linewidth,
- 'linebgcolor': linebgcolor,
- })
+ fill = fill if shape != "polylines" else False
+
+ self.update(
+ {
+ "shape": shape,
+ "color": colors.rgba(color),
+ "fill": "hatch" if fill else None,
+ "x": x,
+ "y": y,
+ "linewidth": linewidth,
+ "dashoffset": dashoffset,
+ "dashpattern": dashpattern,
+ "gapcolor": gapcolor,
+ }
+ )
class _MarkerItem(dict):
- def __init__(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
+ def __init__(
+ self,
+ x,
+ y,
+ text,
+ color,
+ symbol,
+ linewidth,
+ dashoffset,
+ dashpattern,
+ constraint,
+ yaxis,
+ font,
+ bgcolor,
+ ):
super(_MarkerItem, self).__init__()
if symbol is None:
- symbol = '+'
+ symbol = "+"
# Apply constraint to provided position
- isConstraint = (constraint is not None and
- x is not None and y is not None)
+ isConstraint = constraint is not None and x is not None and y is not None
if isConstraint:
x, y = constraint(x, y)
- self.update({
- 'x': x,
- 'y': y,
- 'text': text,
- 'color': colors.rgba(color),
- 'constraint': constraint if isConstraint else None,
- 'symbol': symbol,
- 'linestyle': linestyle,
- 'linewidth': linewidth,
- 'yaxis': yaxis,
- })
+ self.update(
+ {
+ "x": x,
+ "y": y,
+ "text": text,
+ "color": colors.rgba(color),
+ "constraint": constraint if isConstraint else None,
+ "symbol": symbol,
+ "linewidth": linewidth,
+ "dashoffset": dashoffset,
+ "dashpattern": dashpattern,
+ "yaxis": yaxis,
+ "font": font,
+ "bgcolor": bgcolor,
+ }
+ )
# shaders #####################################################################
@@ -193,26 +227,30 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
So, the caller should not modify these arrays afterwards.
"""
+ _TEXT_MARKER_PADDING = 4
+
def __init__(self, plot, parent=None, f=qt.Qt.Widget):
- glu.OpenGLWidget.__init__(self, parent,
- alphaBufferSize=8,
- depthBufferSize=0,
- stencilBufferSize=0,
- version=(2, 1),
- f=f)
+ glu.OpenGLWidget.__init__(
+ self,
+ parent,
+ alphaBufferSize=8,
+ depthBufferSize=0,
+ stencilBufferSize=0,
+ version=(2, 1),
+ f=f,
+ )
BackendBase.BackendBase.__init__(self, plot, parent)
+ self._defaultFont: qt.QFont = None
self.__isOpenGLValid = False
- self._backgroundColor = 1., 1., 1., 1.
- self._dataBackgroundColor = 1., 1., 1., 1.
+ self._backgroundColor = 1.0, 1.0, 1.0, 1.0
+ self._dataBackgroundColor = 1.0, 1.0, 1.0, 1.0
self.matScreenProj = glutils.mat4Identity()
- self._progBase = glu.Program(
- _baseVertShd, _baseFragShd, attrib0='position')
- self._progTex = glu.Program(
- _texVertShd, _texFragShd, attrib0='position')
+ self._progBase = glu.Program(_baseVertShd, _baseFragShd, attrib0="position")
+ self._progTex = glu.Program(_texVertShd, _texFragShd, attrib0="position")
self._plotFBOs = weakref.WeakKeyDictionary()
self._keepDataAspectRatio = False
@@ -223,12 +261,15 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._glGarbageCollector = []
self._plotFrame = glutils.GLPlotFrame2D(
- foregroundColor=(0., 0., 0., 1.),
- gridColor=(.7, .7, .7, 1.),
- marginRatios=(.15, .1, .1, .15))
+ foregroundColor=(0.0, 0.0, 0.0, 1.0),
+ gridColor=(0.7, 0.7, 0.7, 1.0),
+ marginRatios=(0.15, 0.1, 0.1, 0.15),
+ font=self.getDefaultFont(),
+ )
self._plotFrame.size = ( # Init size with size int
int(self.getDevicePixelRatio() * 640),
- int(self.getDevicePixelRatio() * 480))
+ int(self.getDevicePixelRatio() * 480),
+ )
self.setAutoFillBackground(False)
self.setMouseTracking(True)
@@ -236,9 +277,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# QWidget
_MOUSE_BTNS = {
- qt.Qt.LeftButton: 'left',
- qt.Qt.RightButton: 'right',
- qt.Qt.MiddleButton: 'middle',
+ qt.Qt.LeftButton: "left",
+ qt.Qt.RightButton: "right",
+ qt.Qt.MiddleButton: "middle",
}
def sizeHint(self):
@@ -262,8 +303,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
else:
self._mousePosInPixels = None # Mouse outside plot area
- if (self._crosshairCursor is not None and
- previousMousePosInPixels != self._mousePosInPixels):
+ if (
+ self._crosshairCursor is not None
+ and previousMousePosInPixels != self._mousePosInPixels
+ ):
# Avoid replot when cursor remains outside plot area
self._plot._setDirtyPlot(overlayOnly=True)
@@ -279,7 +322,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def wheelEvent(self, event):
delta = event.angleDelta().y()
- angleInDegrees = delta / 8.
+ angleInDegrees = delta / 8.0
x, y = qt.getMouseEventPosition(event)
self._plot.onMouseWheel(x, y, angleInDegrees)
event.accept()
@@ -298,10 +341,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
gl.glEnable(gl.GL_BLEND)
# gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
- gl.glBlendFuncSeparate(gl.GL_SRC_ALPHA,
- gl.GL_ONE_MINUS_SRC_ALPHA,
- gl.GL_ONE,
- gl.GL_ONE)
+ gl.glBlendFuncSeparate(
+ gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA, gl.GL_ONE, gl.GL_ONE
+ )
# For lines
gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
@@ -319,28 +361,33 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def _paintFBOGL(self):
context = glu.Context.getCurrent()
plotFBOTex = self._plotFBOs.get(context)
- if (self._plot._getDirtyPlot() or self._plotFrame.isDirty or
- plotFBOTex is None):
+ if self._plot._getDirtyPlot() or self._plotFrame.isDirty or plotFBOTex is None:
self._plotVertices = (
# Vertex coordinates
- numpy.array(((-1., -1.), (1., -1.), (-1., 1.), (1., 1.)),
- dtype=numpy.float32),
- # Texture coordinates
- numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
- dtype=numpy.float32))
- if plotFBOTex is None or \
- plotFBOTex.shape[1] != self._plotFrame.size[0] or \
- plotFBOTex.shape[0] != self._plotFrame.size[1]:
+ numpy.array(
+ ((-1.0, -1.0), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)),
+ dtype=numpy.float32,
+ ),
+ # Texture coordinates
+ numpy.array(
+ ((0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)),
+ dtype=numpy.float32,
+ ),
+ )
+ if (
+ plotFBOTex is None
+ or plotFBOTex.shape[1] != self._plotFrame.size[0]
+ or plotFBOTex.shape[0] != self._plotFrame.size[1]
+ ):
if plotFBOTex is not None:
plotFBOTex.discard()
plotFBOTex = glu.FramebufferTexture(
gl.GL_RGBA,
- shape=(self._plotFrame.size[1],
- self._plotFrame.size[0]),
+ shape=(self._plotFrame.size[1], self._plotFrame.size[0]),
minFilter=gl.GL_NEAREST,
magFilter=gl.GL_NEAREST,
- wrap=(gl.GL_CLAMP_TO_EDGE,
- gl.GL_CLAMP_TO_EDGE))
+ wrap=(gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE),
+ )
self._plotFBOs[context] = plotFBOTex
with plotFBOTex:
@@ -355,25 +402,33 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._progTex.use()
texUnit = 0
- gl.glUniform1i(self._progTex.uniforms['tex'], texUnit)
- gl.glUniformMatrix4fv(self._progTex.uniforms['matrix'], 1, gl.GL_TRUE,
- glutils.mat4Identity().astype(numpy.float32))
-
- gl.glEnableVertexAttribArray(self._progTex.attributes['position'])
- gl.glVertexAttribPointer(self._progTex.attributes['position'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._plotVertices[0])
-
- gl.glEnableVertexAttribArray(self._progTex.attributes['texCoords'])
- gl.glVertexAttribPointer(self._progTex.attributes['texCoords'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._plotVertices[1])
+ gl.glUniform1i(self._progTex.uniforms["tex"], texUnit)
+ gl.glUniformMatrix4fv(
+ self._progTex.uniforms["matrix"],
+ 1,
+ gl.GL_TRUE,
+ glutils.mat4Identity().astype(numpy.float32),
+ )
+
+ gl.glEnableVertexAttribArray(self._progTex.attributes["position"])
+ gl.glVertexAttribPointer(
+ self._progTex.attributes["position"],
+ 2,
+ gl.GL_FLOAT,
+ gl.GL_FALSE,
+ 0,
+ self._plotVertices[0],
+ )
+
+ gl.glEnableVertexAttribArray(self._progTex.attributes["texCoords"])
+ gl.glVertexAttribPointer(
+ self._progTex.attributes["texCoords"],
+ 2,
+ gl.GL_FLOAT,
+ gl.GL_FALSE,
+ 0,
+ self._plotVertices[1],
+ )
with plotFBOTex.texture:
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices[0]))
@@ -404,6 +459,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Sync plot frame with window
self._plotFrame.devicePixelRatio = self.getDevicePixelRatio()
+ self._plotFrame.dotsPerInch = self.getDotsPerInch()
# self._paintDirectGL()
self._paintFBOGL()
@@ -434,18 +490,22 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
)
for plotItem in self.getItemsFromBackToFront(
- condition=lambda i: i.isVisible() and i.isOverlay() == overlay):
+ condition=lambda i: i.isVisible() and i.isOverlay() == overlay
+ ):
if plotItem._backendRenderer is None:
continue
item = plotItem._backendRenderer
if isinstance(item, glutils.GLPlotItem): # Render data items
- gl.glViewport(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
+ gl.glViewport(
+ self._plotFrame.margins.left,
+ self._plotFrame.margins.bottom,
+ plotWidth,
+ plotHeight,
+ )
# Set matrix
- if item.yaxis == 'right':
+ if item.yaxis == "right":
context.matrix = self._plotFrame.transformedDataY2ProjMat
else:
context.matrix = self._plotFrame.transformedDataProjMat
@@ -454,140 +514,187 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
elif isinstance(item, _ShapeItem): # Render shape items
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
- if ((isXLog and numpy.min(item['x']) < FLOAT32_MINPOS) or
- (isYLog and numpy.min(item['y']) < FLOAT32_MINPOS)):
+ if (isXLog and numpy.min(item["x"]) < FLOAT32_MINPOS) or (
+ isYLog and numpy.min(item["y"]) < FLOAT32_MINPOS
+ ):
# Ignore items <= 0. on log axes
continue
- if item['shape'] == 'hline':
+ if item["shape"] == "hline":
width = self._plotFrame.size[0]
_, yPixel = self._plotFrame.dataToPixel(
- 0.5 * sum(self._plotFrame.dataRanges[0]),
- item['y'],
- axis='left')
- subShapes = [numpy.array(((0., yPixel), (width, yPixel)),
- dtype=numpy.float32)]
+ 0.5 * sum(self._plotFrame.dataRanges[0]), item["y"], axis="left"
+ )
+ subShapes = [
+ numpy.array(
+ ((0.0, yPixel), (width, yPixel)), dtype=numpy.float32
+ )
+ ]
- elif item['shape'] == 'vline':
+ elif item["shape"] == "vline":
xPixel, _ = self._plotFrame.dataToPixel(
- item['x'],
- 0.5 * sum(self._plotFrame.dataRanges[1]),
- axis='left')
+ item["x"], 0.5 * sum(self._plotFrame.dataRanges[1]), axis="left"
+ )
height = self._plotFrame.size[1]
- subShapes = [numpy.array(((xPixel, 0), (xPixel, height)),
- dtype=numpy.float32)]
+ subShapes = [
+ numpy.array(
+ ((xPixel, 0), (xPixel, height)), dtype=numpy.float32
+ )
+ ]
else:
# Split sub-shapes at not finite values
- splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
- numpy.isfinite(item['x']), numpy.isfinite(item['y']))))[0]
- splits = numpy.concatenate(([-1], splits, [len(item['x'])]))
+ splits = numpy.nonzero(
+ numpy.logical_not(
+ numpy.logical_and(
+ numpy.isfinite(item["x"]), numpy.isfinite(item["y"])
+ )
+ )
+ )[0]
+ splits = numpy.concatenate(([-1], splits, [len(item["x"])]))
subShapes = []
for begin, end in zip(splits[:-1] + 1, splits[1:]):
if end > begin:
- subShapes.append(numpy.array([
- self._plotFrame.dataToPixel(x, y, axis='left')
- for (x, y) in zip(item['x'][begin:end], item['y'][begin:end])]))
+ subShapes.append(
+ numpy.array(
+ [
+ self._plotFrame.dataToPixel(x, y, axis="left")
+ for (x, y) in zip(
+ item["x"][begin:end], item["y"][begin:end]
+ )
+ ]
+ )
+ )
for points in subShapes: # Draw each sub-shape
# Draw the fill
- if (item['fill'] is not None and
- item['shape'] not in ('hline', 'vline')):
+ if item["fill"] is not None and item["shape"] not in (
+ "hline",
+ "vline",
+ ):
self._progBase.use()
gl.glUniformMatrix4fv(
- self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
- self.matScreenProj.astype(numpy.float32))
- gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
- gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
+ self._progBase.uniforms["matrix"],
+ 1,
+ gl.GL_TRUE,
+ self.matScreenProj.astype(numpy.float32),
+ )
+ gl.glUniform2i(self._progBase.uniforms["isLog"], False, False)
+ gl.glUniform1f(self._progBase.uniforms["tickLen"], 0.0)
shape2D = glutils.FilledShape2D(
- points, style=item['fill'], color=item['color'])
+ points, style=item["fill"], color=item["color"]
+ )
shape2D.render(
- posAttrib=self._progBase.attributes['position'],
- colorUnif=self._progBase.uniforms['color'],
- hatchStepUnif=self._progBase.uniforms['hatchStep'])
+ posAttrib=self._progBase.attributes["position"],
+ colorUnif=self._progBase.uniforms["color"],
+ hatchStepUnif=self._progBase.uniforms["hatchStep"],
+ )
# Draw the stroke
- if item['linestyle'] not in ('', ' ', None):
- if item['shape'] != 'polylines':
+ if item["dashpattern"] is not None:
+ if item["shape"] != "polylines":
# close the polyline
- points = numpy.append(points,
- numpy.atleast_2d(points[0]), axis=0)
+ points = numpy.append(
+ points, numpy.atleast_2d(points[0]), axis=0
+ )
lines = glutils.GLLines2D(
- points[:, 0], points[:, 1],
- style=item['linestyle'],
- color=item['color'],
- dash2ndColor=item['linebgcolor'],
- width=item['linewidth'])
+ points[:, 0],
+ points[:, 1],
+ color=item["color"],
+ gapColor=item["gapcolor"],
+ width=item["linewidth"],
+ dashOffset=item["dashoffset"],
+ dashPattern=item["dashpattern"],
+ )
context.matrix = self.matScreenProj
lines.render(context)
elif isinstance(item, _MarkerItem):
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
- xCoord, yCoord, yAxis = item['x'], item['y'], item['yaxis']
+ xCoord, yCoord, yAxis = item["x"], item["y"], item["yaxis"]
- if ((isXLog and xCoord is not None and xCoord <= 0) or
- (isYLog and yCoord is not None and yCoord <= 0)):
+ if (isXLog and xCoord is not None and xCoord <= 0) or (
+ isYLog and yCoord is not None and yCoord <= 0
+ ):
# Do not render markers with negative coords on log axis
continue
- color = item['color']
- intensity = color[0] * 0.299 + color[1] * 0.587 + color[2] * 0.114
- bgColor = (1., 1., 1., 0.75) if intensity <= 0.5 else (0., 0., 0., 0.75)
+ color = item["color"]
+ bgColor = item["bgcolor"]
if xCoord is None or yCoord is None:
if xCoord is None: # Horizontal line in data space
pixelPos = self._plotFrame.dataToPixel(
- 0.5 * sum(self._plotFrame.dataRanges[0]),
- yCoord,
- axis=yAxis)
-
- if item['text'] is not None:
- x = self._plotFrame.size[0] - \
- self._plotFrame.margins.right - pixelOffset
+ 0.5 * sum(self._plotFrame.dataRanges[0]), yCoord, axis=yAxis
+ )
+
+ if item["text"] is not None:
+ x = (
+ self._plotFrame.size[0]
+ - self._plotFrame.margins.right
+ - pixelOffset
+ )
y = pixelPos[1] - pixelOffset
label = glutils.Text2D(
- item['text'], x, y,
- color=item['color'],
+ item["text"],
+ item["font"],
+ x,
+ y,
+ color=color,
bgColor=bgColor,
align=glutils.RIGHT,
valign=glutils.BOTTOM,
- devicePixelRatio=self.getDevicePixelRatio())
+ devicePixelRatio=self.getDevicePixelRatio(),
+ padding=self._TEXT_MARKER_PADDING,
+ )
labels.append(label)
width = self._plotFrame.size[0]
lines = glutils.GLLines2D(
- (0, width), (pixelPos[1], pixelPos[1]),
- style=item['linestyle'],
- color=item['color'],
- width=item['linewidth'])
+ (0, width),
+ (pixelPos[1], pixelPos[1]),
+ color=color,
+ width=item["linewidth"],
+ dashOffset=item["dashoffset"],
+ dashPattern=item["dashpattern"],
+ )
context.matrix = self.matScreenProj
lines.render(context)
else: # yCoord is None: vertical line in data space
- yRange = self._plotFrame.dataRanges[1 if yAxis == 'left' else 2]
+ yRange = self._plotFrame.dataRanges[1 if yAxis == "left" else 2]
pixelPos = self._plotFrame.dataToPixel(
- xCoord, 0.5 * sum(yRange), axis=yAxis)
+ xCoord, 0.5 * sum(yRange), axis=yAxis
+ )
- if item['text'] is not None:
+ if item["text"] is not None:
x = pixelPos[0] + pixelOffset
y = self._plotFrame.margins.top + pixelOffset
label = glutils.Text2D(
- item['text'], x, y,
- color=item['color'],
+ item["text"],
+ item["font"],
+ x,
+ y,
+ color=color,
bgColor=bgColor,
align=glutils.LEFT,
valign=glutils.TOP,
- devicePixelRatio=self.getDevicePixelRatio())
+ devicePixelRatio=self.getDevicePixelRatio(),
+ padding=self._TEXT_MARKER_PADDING,
+ )
labels.append(label)
height = self._plotFrame.size[1]
lines = glutils.GLLines2D(
- (pixelPos[0], pixelPos[0]), (0, height),
- style=item['linestyle'],
- color=item['color'],
- width=item['linewidth'])
+ (pixelPos[0], pixelPos[0]),
+ (0, height),
+ color=color,
+ width=item["linewidth"],
+ dashOffset=item["dashoffset"],
+ dashPattern=item["dashpattern"],
+ )
context.matrix = self.matScreenProj
lines.render(context)
@@ -597,8 +704,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if not xmin < xCoord < xmax or not ymin < yCoord < ymax:
# Do not render markers outside visible plot area
continue
- pixelPos = self._plotFrame.dataToPixel(
- xCoord, yCoord, axis=yAxis)
+ pixelPos = self._plotFrame.dataToPixel(xCoord, yCoord, axis=yAxis)
if isYInverted:
valign = glutils.BOTTOM
@@ -607,16 +713,21 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
valign = glutils.TOP
vPixelOffset = pixelOffset
- if item['text'] is not None:
+ if item["text"] is not None:
x = pixelPos[0] + pixelOffset
y = pixelPos[1] + vPixelOffset
label = glutils.Text2D(
- item['text'], x, y,
- color=item['color'],
+ item["text"],
+ item["font"],
+ x,
+ y,
+ color=color,
bgColor=bgColor,
align=glutils.LEFT,
valign=valign,
- devicePixelRatio=self.getDevicePixelRatio())
+ devicePixelRatio=self.getDevicePixelRatio(),
+ padding=self._TEXT_MARKER_PADDING,
+ )
labels.append(label)
# For now simple implementation: using a curve for each marker
@@ -624,30 +735,33 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
marker = glutils.Points2D(
(pixelPos[0],),
(pixelPos[1],),
- marker=item['symbol'],
- color=item['color'],
+ marker=item["symbol"],
+ color=color,
size=11,
)
context.matrix = self.matScreenProj
marker.render(context)
else:
- _logger.error('Unsupported item: %s', str(item))
+ _logger.error("Unsupported item: %s", str(item))
continue
# Render marker labels
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
for label in labels:
- label.render(self.matScreenProj)
+ label.render(self.matScreenProj, self._plotFrame.dotsPerInch)
def _renderOverlayGL(self):
"""Render overlay layer: overlay items and crosshair."""
plotWidth, plotHeight = self._plotFrame.plotSize
# Scissor to plot area
- gl.glScissor(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
+ gl.glScissor(
+ self._plotFrame.margins.left,
+ self._plotFrame.margins.bottom,
+ plotWidth,
+ plotHeight,
+ )
gl.glEnable(gl.GL_SCISSOR_TEST)
self._renderItems(overlay=True)
@@ -655,17 +769,18 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Render crosshair cursor
if self._crosshairCursor is not None and self._mousePosInPixels is not None:
self._progBase.use()
- gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
- gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
- posAttrib = self._progBase.attributes['position']
- matrixUnif = self._progBase.uniforms['matrix']
- colorUnif = self._progBase.uniforms['color']
- hatchStepUnif = self._progBase.uniforms['hatchStep']
+ gl.glUniform2i(self._progBase.uniforms["isLog"], False, False)
+ gl.glUniform1f(self._progBase.uniforms["tickLen"], 0.0)
+ posAttrib = self._progBase.attributes["position"]
+ matrixUnif = self._progBase.uniforms["matrix"]
+ colorUnif = self._progBase.uniforms["color"]
+ hatchStepUnif = self._progBase.uniforms["hatchStep"]
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
- gl.glUniformMatrix4fv(matrixUnif, 1, gl.GL_TRUE,
- self.matScreenProj.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ matrixUnif, 1, gl.GL_TRUE, self.matScreenProj.astype(numpy.float32)
+ )
color, lineWidth = self._crosshairCursor
gl.glUniform4f(colorUnif, *color)
@@ -673,18 +788,20 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
xPixel, yPixel = self._mousePosInPixels
xPixel, yPixel = xPixel + 0.5, yPixel + 0.5
- vertices = numpy.array(((0., yPixel),
- (self._plotFrame.size[0], yPixel),
- (xPixel, 0.),
- (xPixel, self._plotFrame.size[1])),
- dtype=numpy.float32)
+ vertices = numpy.array(
+ (
+ (0.0, yPixel),
+ (self._plotFrame.size[0], yPixel),
+ (xPixel, 0.0),
+ (xPixel, self._plotFrame.size[1]),
+ ),
+ dtype=numpy.float32,
+ )
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, vertices)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, vertices
+ )
gl.glLineWidth(lineWidth)
gl.glDrawArrays(gl.GL_LINES, 0, len(vertices))
@@ -697,9 +814,12 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
"""
plotWidth, plotHeight = self._plotFrame.plotSize
- gl.glScissor(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
+ gl.glScissor(
+ self._plotFrame.margins.left,
+ self._plotFrame.margins.bottom,
+ plotWidth,
+ plotHeight,
+ )
gl.glEnable(gl.GL_SCISSOR_TEST)
if self._dataBackgroundColor != self._backgroundColor:
@@ -722,29 +842,28 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._plotFrame.size = (
int(self.getDevicePixelRatio() * width),
- int(self.getDevicePixelRatio() * height))
+ int(self.getDevicePixelRatio() * height),
+ )
self.matScreenProj = glutils.mat4Ortho(
- 0, self._plotFrame.size[0],
- self._plotFrame.size[1], 0,
- 1, -1)
+ 0, self._plotFrame.size[0], self._plotFrame.size[1], 0, 1, -1
+ )
# Store current ranges
previousXRange = self.getGraphXLimits()
- previousYRange = self.getGraphYLimits(axis='left')
- previousYRightRange = self.getGraphYLimits(axis='right')
+ previousYRange = self.getGraphYLimits(axis="left")
+ previousYRightRange = self.getGraphYLimits(axis="right")
- (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \
- self._plotFrame.dataRanges
+ (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = self._plotFrame.dataRanges
self.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
# If plot range has changed, then emit signal
if previousXRange != self.getGraphXLimits():
self._plot.getXAxis()._emitLimitsChanged()
- if previousYRange != self.getGraphYLimits(axis='left'):
- self._plot.getYAxis(axis='left')._emitLimitsChanged()
- if previousYRightRange != self.getGraphYLimits(axis='right'):
- self._plot.getYAxis(axis='right')._emitLimitsChanged()
+ if previousYRange != self.getGraphYLimits(axis="left"):
+ self._plot.getYAxis(axis="left")._emitLimitsChanged()
+ if previousYRightRange != self.getGraphYLimits(axis="right"):
+ self._plot.getYAxis(axis="right")._emitLimitsChanged()
# Add methods
@@ -761,39 +880,92 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
elif numpy.issubdtype(v.dtype, numpy.integer):
return numpy.float32 if v.itemsize <= 2 else numpy.float64
else:
- raise ValueError('Unsupported data type')
-
- def addCurve(self, x, y,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror,
- fill, alpha, symbolsize, baseline):
- for parameter in (x, y, color, symbol, linewidth, linestyle,
- yaxis, fill, symbolsize):
+ raise ValueError("Unsupported data type")
+
+ _DASH_PATTERNS = {
+ "": (0.0, None),
+ " ": (0.0, None),
+ "-": (0.0, ()),
+ "--": (0.0, (3.7, 1.6, 3.7, 1.6)),
+ "-.": (0.0, (6.4, 1.6, 1, 1.6)),
+ ":": (0.0, (1, 1.65, 1, 1.65)),
+ None: (0.0, None),
+ }
+ """Convert from linestyle to (offset, (dash pattern))
+
+ Note: dash pattern internal convention differs from matplotlib:
+ - None: no line at all
+ - (): "solid" line
+ """
+
+ def _lineStyleToDashOffsetPattern(
+ self, style
+ ) -> tuple[float, tuple[float, float, float, float] | tuple[()] | None]:
+ """Convert a linestyle to its corresponding offset and dash pattern"""
+ if style is None or isinstance(style, str):
+ return self._DASH_PATTERNS[style]
+
+ # (offset, (dash pattern)) case
+ offset, pattern = style
+ if pattern is None:
+ # Convert from matplotlib to internal representation of solid
+ pattern = ()
+ if len(pattern) == 2:
+ pattern = pattern * 2
+ return float(offset), tuple(float(v) for v in pattern)
+
+ def addCurve(
+ self,
+ x,
+ y,
+ color,
+ gapcolor,
+ symbol,
+ linewidth,
+ linestyle,
+ yaxis,
+ xerror,
+ yerror,
+ fill,
+ alpha,
+ symbolsize,
+ baseline,
+ ):
+ for parameter in (
+ x,
+ y,
+ color,
+ symbol,
+ linewidth,
+ linestyle,
+ yaxis,
+ fill,
+ symbolsize,
+ ):
assert parameter is not None
- assert yaxis in ('left', 'right')
+ assert yaxis in ("left", "right")
# Convert input data
x = numpy.array(x, copy=False)
y = numpy.array(y, copy=False)
# Check if float32 is enough
- if (self._castArrayTo(x) is numpy.float32 and
- self._castArrayTo(y) is numpy.float32):
+ if (
+ self._castArrayTo(x) is numpy.float32
+ and self._castArrayTo(y) is numpy.float32
+ ):
dtype = numpy.float32
else:
dtype = numpy.float64
- x = numpy.array(x, dtype=dtype, copy=False, order='C')
- y = numpy.array(y, dtype=dtype, copy=False, order='C')
+ x = numpy.array(x, dtype=dtype, copy=False, order="C")
+ y = numpy.array(y, dtype=dtype, copy=False, order="C")
# Convert errors to float32
if xerror is not None:
- xerror = numpy.array(
- xerror, dtype=numpy.float32, copy=False, order='C')
+ xerror = numpy.array(xerror, dtype=numpy.float32, copy=False, order="C")
if yerror is not None:
- yerror = numpy.array(
- yerror, dtype=numpy.float32, copy=False, order='C')
+ yerror = numpy.array(yerror, dtype=numpy.float32, copy=False, order="C")
# Handle axes log scale: convert data
@@ -803,21 +975,21 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if xerror is not None:
# Transform xerror so that
# log10(x) +/- xerror' = log10(x +/- xerror)
- if hasattr(xerror, 'shape') and len(xerror.shape) == 2:
+ if hasattr(xerror, "shape") and len(xerror.shape) == 2:
xErrorMinus, xErrorPlus = xerror[0], xerror[1]
else:
xErrorMinus, xErrorPlus = xerror, xerror
- with numpy.errstate(divide='ignore', invalid='ignore'):
+ with numpy.errstate(divide="ignore", invalid="ignore"):
# Ignore divide by zero, invalid value encountered in log10
xErrorMinus = logX - numpy.log10(x - xErrorMinus)
xErrorPlus = numpy.log10(x + xErrorPlus) - logX
- xerror = numpy.array((xErrorMinus, xErrorPlus),
- dtype=numpy.float32)
+ xerror = numpy.array((xErrorMinus, xErrorPlus), dtype=numpy.float32)
x = logX
- isYLog = (yaxis == 'left' and self._plotFrame.yAxis.isLog) or (
- yaxis == 'right' and self._plotFrame.y2Axis.isLog)
+ isYLog = (yaxis == "left" and self._plotFrame.yAxis.isLog) or (
+ yaxis == "right" and self._plotFrame.y2Axis.isLog
+ )
if isYLog:
logY = numpy.log10(y)
@@ -825,25 +997,23 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if yerror is not None:
# Transform yerror so that
# log10(y) +/- yerror' = log10(y +/- yerror)
- if hasattr(yerror, 'shape') and len(yerror.shape) == 2:
+ if hasattr(yerror, "shape") and len(yerror.shape) == 2:
yErrorMinus, yErrorPlus = yerror[0], yerror[1]
else:
yErrorMinus, yErrorPlus = yerror, yerror
- with numpy.errstate(divide='ignore', invalid='ignore'):
+ with numpy.errstate(divide="ignore", invalid="ignore"):
# Ignore divide by zero, invalid value encountered in log10
yErrorMinus = logY - numpy.log10(y - yErrorMinus)
yErrorPlus = numpy.log10(y + yErrorPlus) - logY
- yerror = numpy.array((yErrorMinus, yErrorPlus),
- dtype=numpy.float32)
+ yerror = numpy.array((yErrorMinus, yErrorPlus), dtype=numpy.float32)
y = logY
# TODO check if need more filtering of error (e.g., clip to positive)
# TODO check and improve this
- if (len(color) == 4 and
- type(color[3]) in [type(1), numpy.uint8, numpy.int8]):
- color = numpy.array(color, dtype=numpy.float32) / 255.
+ if len(color) == 4 and type(color[3]) in [type(1), numpy.uint8, numpy.int8]:
+ color = numpy.array(color, dtype=numpy.float32) / 255.0
if isinstance(color, numpy.ndarray) and color.ndim == 2:
colorArray = color
@@ -852,7 +1022,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
colorArray = None
color = colors.rgba(color)
- if alpha < 1.: # Apply image transparency
+ if alpha < 1.0: # Apply image transparency
if colorArray is not None and colorArray.shape[1] == 4:
# multiply alpha channel
colorArray[:, 3] = colorArray[:, 3] * alpha
@@ -862,43 +1032,49 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
fillColor = None
if fill is True:
fillColor = color
+
+ dashoffset, dashpattern = self._lineStyleToDashOffsetPattern(linestyle)
curve = glutils.GLPlotCurve2D(
- x, y, colorArray,
+ x,
+ y,
+ colorArray,
xError=xerror,
yError=yerror,
- lineStyle=linestyle,
lineColor=color,
+ lineGapColor=gapcolor,
lineWidth=linewidth,
+ lineDashOffset=dashoffset,
+ lineDashPattern=dashpattern,
marker=symbol,
markerColor=color,
markerSize=symbolsize,
fillColor=fillColor,
baseline=baseline,
- isYLog=isYLog)
- curve.yaxis = 'left' if yaxis is None else yaxis
+ isYLog=isYLog,
+ )
+ curve.yaxis = "left" if yaxis is None else yaxis
if yaxis == "right":
self._plotFrame.isY2Axis = True
return curve
- def addImage(self, data,
- origin, scale,
- colormap, alpha):
+ def addImage(self, data, origin, scale, colormap, alpha):
for parameter in (data, origin, scale):
assert parameter is not None
if data.ndim == 2:
# Ensure array is contiguous and eventually convert its type
- dtypes = [dtype for dtype in (
- numpy.float32, numpy.float16, numpy.uint8, numpy.uint16)
- if glu.isSupportedGLType(dtype)]
+ dtypes = [
+ dtype
+ for dtype in (numpy.float32, numpy.float16, numpy.uint8, numpy.uint16)
+ if glu.isSupportedGLType(dtype)
+ ]
if data.dtype in dtypes:
- data = numpy.array(data, copy=False, order='C')
+ data = numpy.array(data, copy=False, order="C")
else:
- _logger.info(
- 'addImage: Convert %s data to float32', str(data.dtype))
- data = numpy.array(data, dtype=numpy.float32, order='C')
+ _logger.info("addImage: Convert %s data to float32", str(data.dtype))
+ data = numpy.array(data, dtype=numpy.float32, order="C")
normalization = colormap.getNormalization()
if normalization in glutils.GLPlotColormap.SUPPORTED_NORMALIZATIONS:
@@ -917,7 +1093,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
gamma,
cmapRange,
alpha,
- nanColor)
+ nanColor,
+ )
else: # Fallback applying colormap on CPU
rgba = colormap.applyToData(data)
@@ -934,7 +1111,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
elif numpy.issubdtype(data.dtype, numpy.integer):
data = numpy.array(data, dtype=numpy.uint8, copy=False)
else:
- raise ValueError('Unsupported data type')
+ raise ValueError("Unsupported data type")
image = glutils.GLPlotRGBAImage(data, origin, scale, alpha)
@@ -942,17 +1119,14 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
raise RuntimeError("Unsupported data shape {0}".format(data.shape))
# TODO is this needed?
- if self._plotFrame.xAxis.isLog and image.xMin <= 0.:
- raise RuntimeError(
- 'Cannot add image with X <= 0 with X axis log scale')
- if self._plotFrame.yAxis.isLog and image.yMin <= 0.:
- raise RuntimeError(
- 'Cannot add image with Y <= 0 with Y axis log scale')
+ if self._plotFrame.xAxis.isLog and image.xMin <= 0.0:
+ raise RuntimeError("Cannot add image with X <= 0 with X axis log scale")
+ if self._plotFrame.yAxis.isLog and image.yMin <= 0.0:
+ raise RuntimeError("Cannot add image with Y <= 0 with Y axis log scale")
return image
- def addTriangles(self, x, y, triangles,
- color, alpha):
+ def addTriangles(self, x, y, triangles, color, alpha):
# Handle axes log scale: convert data
if self._plotFrame.xAxis.isLog:
x = numpy.log10(x)
@@ -963,36 +1137,90 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return triangles
- def addShape(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
+ def addShape(
+ self, x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
+ ):
x = numpy.array(x, copy=False)
y = numpy.array(y, copy=False)
# TODO is this needed?
- if self._plotFrame.xAxis.isLog and x.min() <= 0.:
- raise RuntimeError(
- 'Cannot add item with X <= 0 with X axis log scale')
- if self._plotFrame.yAxis.isLog and y.min() <= 0.:
- raise RuntimeError(
- 'Cannot add item with Y <= 0 with Y axis log scale')
-
- return _ShapeItem(x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor)
+ if self._plotFrame.xAxis.isLog and x.min() <= 0.0:
+ raise RuntimeError("Cannot add item with X <= 0 with X axis log scale")
+ if self._plotFrame.yAxis.isLog and y.min() <= 0.0:
+ raise RuntimeError("Cannot add item with Y <= 0 with Y axis log scale")
+
+ dashoffset, dashpattern = self._lineStyleToDashOffsetPattern(linestyle)
+ return _ShapeItem(
+ x,
+ y,
+ shape,
+ color,
+ fill,
+ overlay,
+ linewidth,
+ dashoffset,
+ dashpattern,
+ gapcolor,
+ )
- def addMarker(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
- return _MarkerItem(x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis)
+ def getDefaultFont(self):
+ """Returns the default font, used by raw markers and axes labels"""
+ if self._defaultFont is None:
+ from matplotlib.font_manager import findfont, FontProperties
+
+ font_filename = findfont(FontProperties(family=["sans-serif"]))
+ _logger.debug("Load font from mpl: %s", font_filename)
+ id = qt.QFontDatabase.addApplicationFont(font_filename)
+ family = qt.QFontDatabase.applicationFontFamilies(id)[0]
+ font = qt.QFont(family, 10, qt.QFont.Normal, False)
+ font.setStyleStrategy(qt.QFont.PreferAntialias)
+ self._defaultFont = font
+ return self._defaultFont
+
+ def addMarker(
+ self,
+ x,
+ y,
+ text,
+ color,
+ symbol,
+ linestyle,
+ linewidth,
+ constraint,
+ yaxis,
+ font,
+ bgcolor: RGBAColorType | None,
+ ):
+ if font is None:
+ font = self.getDefaultFont()
+
+ dashoffset, dashpattern = self._lineStyleToDashOffsetPattern(linestyle)
+ return _MarkerItem(
+ x,
+ y,
+ text,
+ color,
+ symbol,
+ linewidth,
+ dashoffset,
+ dashpattern,
+ constraint,
+ yaxis,
+ font,
+ bgcolor,
+ )
# Remove methods
def remove(self, item):
if isinstance(item, glutils.GLPlotItem):
- if item.yaxis == 'right':
+ if item.yaxis == "right":
# Check if some curves remains on the right Y axis
- y2AxisItems = (item for item in self._plot.getItems()
- if isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right')
+ y2AxisItems = (
+ item
+ for item in self._plot.getItems()
+ if isinstance(item, items.YAxisMixIn) and item.getYAxis() == "right"
+ )
self._plotFrame.isY2Axis = next(y2AxisItems, None) is not None
if item.isInitialized():
@@ -1002,7 +1230,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
pass # No-op
else:
- _logger.error('Unsupported item: %s', str(item))
+ _logger.error("Unsupported item: %s", str(item))
# Interaction methods
@@ -1022,9 +1250,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
super(BackendOpenGL, self).setCursor(qt.QCursor(cursor))
def setGraphCursor(self, flag, color, linewidth, linestyle):
- if linestyle != '-':
- _logger.warning(
- "BackendOpenGL.setGraphCursor linestyle parameter ignored")
+ if linestyle != "-":
+ _logger.warning("BackendOpenGL.setGraphCursor linestyle parameter ignored")
if flag:
color = colors.rgba(color)
@@ -1048,8 +1275,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
:rtype: List[float]
"""
left, top, width, height = self.getPlotBoundsInPixels()
- return (numpy.clip(x, left, left + width - 1), # TODO -1?
- numpy.clip(y, top, top + height - 1))
+ return (
+ numpy.clip(x, left, left + width - 1), # TODO -1?
+ numpy.clip(y, top, top + height - 1),
+ )
def __pickCurves(self, item, x, y):
"""Perform picking on a curve item.
@@ -1064,24 +1293,26 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if item.marker is not None:
# Convert markerSize from points to qt pixels
qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
- size = item.markerSize / 72. * qtDpi
- offset = max(size / 2., offset)
- if item.lineStyle is not None:
+ size = item.markerSize / 72.0 * qtDpi
+ offset = max(size / 2.0, offset)
+ if item.lineDashPattern is not None:
# Convert line width from points to qt pixels
qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
- lineWidth = item.lineWidth / 72. * qtDpi
- offset = max(lineWidth / 2., offset)
+ lineWidth = item.lineWidth / 72.0 * qtDpi
+ offset = max(lineWidth / 2.0, offset)
inAreaPos = self._mouseInPlotArea(x - offset, y - offset)
- dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=item.yaxis, check=True)
+ dataPos = self._plot.pixelToData(
+ inAreaPos[0], inAreaPos[1], axis=item.yaxis, check=True
+ )
if dataPos is None:
return None
xPick0, yPick0 = dataPos
inAreaPos = self._mouseInPlotArea(x + offset, y + offset)
- dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=item.yaxis, check=True)
+ dataPos = self._plot.pixelToData(
+ inAreaPos[0], inAreaPos[1], axis=item.yaxis, check=True
+ )
if dataPos is None:
return None
xPick1, yPick1 = dataPos
@@ -1101,17 +1332,17 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
xPickMin = numpy.log10(xPickMin)
xPickMax = numpy.log10(xPickMax)
- if (item.yaxis == 'left' and self._plotFrame.yAxis.isLog) or (
- item.yaxis == 'right' and self._plotFrame.y2Axis.isLog):
+ if (item.yaxis == "left" and self._plotFrame.yAxis.isLog) or (
+ item.yaxis == "right" and self._plotFrame.y2Axis.isLog
+ ):
yPickMin = numpy.log10(yPickMin)
yPickMax = numpy.log10(yPickMax)
- return item.pick(xPickMin, yPickMin,
- xPickMax, yPickMax)
+ return item.pick(xPickMin, yPickMin, xPickMax, yPickMax)
def pickItem(self, x, y, item):
# Picking is performed in Qt widget pixels not device pixels
- dataPos = self._plot.pixelToData(x, y, axis='left', check=True)
+ dataPos = self._plot.pixelToData(x, y, axis="left", check=True)
if dataPos is None:
return None # Outside plot area
@@ -1121,32 +1352,36 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Pick markers
if isinstance(item, _MarkerItem):
- yaxis = item['yaxis']
+ yaxis = item["yaxis"]
pixelPos = self._plot.dataToPixel(
- item['x'], item['y'], axis=yaxis, check=False)
+ item["x"], item["y"], axis=yaxis, check=False
+ )
if pixelPos is None:
return None # negative coord on a log axis
- if item['x'] is None: # Horizontal line
+ if item["x"] is None: # Horizontal line
pt1 = self._plot.pixelToData(
- x, y - self._PICK_OFFSET, axis=yaxis, check=False)
+ x, y - self._PICK_OFFSET, axis=yaxis, check=False
+ )
pt2 = self._plot.pixelToData(
- x, y + self._PICK_OFFSET, axis=yaxis, check=False)
- isPicked = (min(pt1[1], pt2[1]) <= item['y'] <=
- max(pt1[1], pt2[1]))
+ x, y + self._PICK_OFFSET, axis=yaxis, check=False
+ )
+ isPicked = min(pt1[1], pt2[1]) <= item["y"] <= max(pt1[1], pt2[1])
- elif item['y'] is None: # Vertical line
+ elif item["y"] is None: # Vertical line
pt1 = self._plot.pixelToData(
- x - self._PICK_OFFSET, y, axis=yaxis, check=False)
+ x - self._PICK_OFFSET, y, axis=yaxis, check=False
+ )
pt2 = self._plot.pixelToData(
- x + self._PICK_OFFSET, y, axis=yaxis, check=False)
- isPicked = (min(pt1[0], pt2[0]) <= item['x'] <=
- max(pt1[0], pt2[0]))
+ x + self._PICK_OFFSET, y, axis=yaxis, check=False
+ )
+ isPicked = min(pt1[0], pt2[0]) <= item["x"] <= max(pt1[0], pt2[0])
else:
isPicked = (
- numpy.fabs(x - pixelPos[0]) <= self._PICK_OFFSET and
- numpy.fabs(y - pixelPos[1]) <= self._PICK_OFFSET)
+ numpy.fabs(x - pixelPos[0]) <= self._PICK_OFFSET
+ and numpy.fabs(y - pixelPos[1]) <= self._PICK_OFFSET
+ )
return (0,) if isPicked else None
@@ -1177,11 +1412,11 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if dpi is not None:
_logger.warning("saveGraph ignores dpi parameter")
- if fileFormat not in ['png', 'ppm', 'svg', 'tiff']:
- raise NotImplementedError('Unsupported format: %s' % fileFormat)
+ if fileFormat not in ["png", "ppm", "svg", "tif", "tiff"]:
+ raise NotImplementedError("Unsupported format: %s" % fileFormat)
if not self.isValid():
- _logger.error('OpenGL 2.1 not available, cannot save OpenGL image')
+ _logger.error("OpenGL 2.1 not available, cannot save OpenGL image")
width, height = self._plotFrame.size
data = numpy.zeros((height, width, 3), dtype=numpy.uint8)
else:
@@ -1189,7 +1424,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
data = numpy.empty(
(self._plotFrame.size[1], self._plotFrame.size[0], 3),
- dtype=numpy.uint8, order='C')
+ dtype=numpy.uint8,
+ order="C",
+ )
context = self.context()
framebufferTexture = self._plotFBOs.get(context)
@@ -1205,8 +1442,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fboName)
gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
- gl.glReadPixels(0, 0, width, height,
- gl.GL_RGB, gl.GL_UNSIGNED_BYTE, data)
+ gl.glReadPixels(0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, data)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer)
# glReadPixels gives bottom to top,
@@ -1225,7 +1461,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._plotFrame.xAxis.title = label
def setGraphYLabel(self, label, axis):
- if axis == 'left':
+ if axis == "left":
self._plotFrame.yAxis.title = label
else: # right axis
self._plotFrame.y2Axis.title = label
@@ -1258,24 +1494,27 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if keepDim is None:
ranges = self._plot.getDataRange()
- if (ranges.y is not None and
- ranges.x is not None and
- (ranges.y[1] - ranges.y[0]) != 0.):
- dataRatio = (ranges.x[1] - ranges.x[0]) / float(ranges.y[1] - ranges.y[0])
+ if (
+ ranges.y is not None
+ and ranges.x is not None
+ and (ranges.y[1] - ranges.y[0]) != 0.0
+ ):
+ dataRatio = (ranges.x[1] - ranges.x[0]) / float(
+ ranges.y[1] - ranges.y[0]
+ )
plotRatio = plotWidth / float(plotHeight) # Test != 0 before
- keepDim = 'x' if dataRatio > plotRatio else 'y'
+ keepDim = "x" if dataRatio > plotRatio else "y"
else: # Limit case
- keepDim = 'x'
+ keepDim = "x"
- (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \
- self._plotFrame.dataRanges
- if keepDim == 'y':
+ (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = self._plotFrame.dataRanges
+ if keepDim == "y":
dataW = (yMax - yMin) * plotWidth / float(plotHeight)
xCenter = 0.5 * (xMin + xMax)
xMin = xCenter - 0.5 * dataW
xMax = xCenter + 0.5 * dataW
- elif keepDim == 'x':
+ elif keepDim == "x":
dataH = (xMax - xMin) * plotHeight / float(plotWidth)
yCenter = 0.5 * (yMin + yMax)
yMin = yCenter - 0.5 * dataH
@@ -1284,19 +1523,14 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
y2Min = y2Center - 0.5 * dataH
y2Max = y2Center + 0.5 * dataH
else:
- raise RuntimeError('Unsupported dimension to keep: %s' % keepDim)
+ raise RuntimeError("Unsupported dimension to keep: %s" % keepDim)
# Update plot frame bounds
- self._setDataRanges(xlim=(xMin, xMax),
- ylim=(yMin, yMax),
- y2lim=(y2Min, y2Max))
+ self._setDataRanges(xlim=(xMin, xMax), ylim=(yMin, yMax), y2lim=(y2Min, y2Max))
- def _setPlotBounds(self, xRange=None, yRange=None, y2Range=None,
- keepDim=None):
+ def _setPlotBounds(self, xRange=None, yRange=None, y2Range=None, keepDim=None):
# Update axes range with a clipped range if too wide
- self._setDataRanges(xlim=xRange,
- ylim=yRange,
- y2lim=y2Range)
+ self._setDataRanges(xlim=xRange, ylim=yRange, y2lim=y2Range)
# Keep data aspect ratio
if self.isKeepDataAspectRatio():
@@ -1318,7 +1552,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def setGraphXLimits(self, xmin, xmax):
assert xmin < xmax
- self._setPlotBounds(xRange=(xmin, xmax), keepDim='x')
+ self._setPlotBounds(xRange=(xmin, xmax), keepDim="x")
def getGraphYLimits(self, axis):
assert axis in ("left", "right")
@@ -1332,9 +1566,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
assert axis in ("left", "right")
if axis == "left":
- self._setPlotBounds(yRange=(ymin, ymax), keepDim='y')
+ self._setPlotBounds(yRange=(ymin, ymax), keepDim="y")
else:
- self._setPlotBounds(y2Range=(ymin, ymax), keepDim='y')
+ self._setPlotBounds(y2Range=(ymin, ymax), keepDim="y")
# Graph axes
@@ -1353,17 +1587,14 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def setXAxisLogarithmic(self, flag):
if flag != self._plotFrame.xAxis.isLog:
if flag and self._keepDataAspectRatio:
- _logger.warning(
- "KeepDataAspectRatio is ignored with log axes")
+ _logger.warning("KeepDataAspectRatio is ignored with log axes")
self._plotFrame.xAxis.isLog = flag
def setYAxisLogarithmic(self, flag):
- if (flag != self._plotFrame.yAxis.isLog or
- flag != self._plotFrame.y2Axis.isLog):
+ if flag != self._plotFrame.yAxis.isLog or flag != self._plotFrame.y2Axis.isLog:
if flag and self._keepDataAspectRatio:
- _logger.warning(
- "KeepDataAspectRatio is ignored with log axes")
+ _logger.warning("KeepDataAspectRatio is ignored with log axes")
self._plotFrame.yAxis.isLog = flag
self._plotFrame.y2Axis.isLog = flag
@@ -1375,6 +1606,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def isYAxisInverted(self):
return self._plotFrame.isYAxisInverted
+ def isYRightAxisVisible(self):
+ return self._plotFrame.isY2Axis
+
def isKeepDataAspectRatio(self):
if self._plotFrame.xAxis.isLog or self._plotFrame.yAxis.isLog:
return False
@@ -1382,14 +1616,13 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return self._keepDataAspectRatio
def setKeepDataAspectRatio(self, flag):
- if flag and (self._plotFrame.xAxis.isLog or
- self._plotFrame.yAxis.isLog):
+ if flag and (self._plotFrame.xAxis.isLog or self._plotFrame.yAxis.isLog):
_logger.warning("KeepDataAspectRatio is ignored with log axes")
self._keepDataAspectRatio = flag
def setGraphGrid(self, which):
- assert which in (None, 'major', 'both')
+ assert which in (None, "major", "both")
self._plotFrame.grid = which is not None # TODO True grid support
# Data <-> Pixel coordinates conversion
@@ -1400,17 +1633,20 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return None
else:
devicePixelRatio = self.getDevicePixelRatio()
- return tuple(value/devicePixelRatio for value in result)
+ return tuple(value / devicePixelRatio for value in result)
def pixelToData(self, x, y, axis):
devicePixelRatio = self.getDevicePixelRatio()
return self._plotFrame.pixelToData(
- x * devicePixelRatio, y * devicePixelRatio, axis)
+ x * devicePixelRatio, y * devicePixelRatio, axis
+ )
def getPlotBoundsInPixels(self):
devicePixelRatio = self.getDevicePixelRatio()
- return tuple(int(value / devicePixelRatio)
- for value in self._plotFrame.plotOrigin + self._plotFrame.plotSize)
+ return tuple(
+ int(value / devicePixelRatio)
+ for value in self._plotFrame.plotOrigin + self._plotFrame.plotSize
+ )
def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
self._plotFrame.marginRatios = left, top, right, bottom
diff --git a/src/silx/gui/plot/backends/glutils/GLPlotCurve.py b/src/silx/gui/plot/backends/glutils/GLPlotCurve.py
index 4825479..26442d7 100644
--- a/src/silx/gui/plot/backends/glutils/GLPlotCurve.py
+++ b/src/silx/gui/plot/backends/glutils/GLPlotCurve.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2014-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 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
@@ -46,7 +46,7 @@ from .GLPlotImage import GLPlotItem
_logger = logging.getLogger(__name__)
-_MPL_NONES = None, 'None', '', ' '
+_MPL_NONES = None, "None", "", " "
"""Possible values for None"""
@@ -75,6 +75,7 @@ def _notNaNSlices(array, length=1):
# fill ########################################################################
+
class _Fill2D(object):
"""Object rendering curve filling as polygons
@@ -107,12 +108,17 @@ class _Fill2D(object):
gl_FragColor = color;
}
""",
- attrib0='xPos')
-
- def __init__(self, xData=None, yData=None,
- baseline=0,
- color=(0., 0., 0., 1.),
- offset=(0., 0.)):
+ attrib0="xPos",
+ )
+
+ def __init__(
+ self,
+ xData=None,
+ yData=None,
+ baseline=0,
+ color=(0.0, 0.0, 0.0, 1.0),
+ offset=(0.0, 0.0),
+ ):
self.xData = xData
self.yData = yData
self._xFillVboData = None
@@ -125,9 +131,11 @@ class _Fill2D(object):
def prepare(self):
"""Rendering preparation: build indices and bounding box vertices"""
- if (self._xFillVboData is None and
- self.xData is not None and self.yData is not None):
-
+ if (
+ self._xFillVboData is None
+ and self.xData is not None
+ and self.yData is not None
+ ):
# Get slices of not NaN values longer than 1 element
isnan = numpy.logical_or(numpy.isnan(self.xData), numpy.isnan(self.yData))
notnan = numpy.logical_not(isnan)
@@ -151,20 +159,28 @@ class _Fill2D(object):
new_y_data = numpy.append(self.yData, self.baseline)
for start, end in slices:
# Duplicate first point for connecting degenerated triangle
- points[offset:offset+2] = self.xData[start], new_y_data[start]
+ points[offset : offset + 2] = self.xData[start], new_y_data[start]
# 2nd point of the polygon is last point
- points[offset+2] = self.xData[start], self.baseline[start]
-
- indices = numpy.append(numpy.arange(start, end),
- numpy.arange(len(self.xData) + end-1, len(self.xData) + start-1, -1))
+ points[offset + 2] = self.xData[start], self.baseline[start]
+
+ indices = numpy.append(
+ numpy.arange(start, end),
+ numpy.arange(
+ len(self.xData) + end - 1, len(self.xData) + start - 1, -1
+ ),
+ )
indices = indices[buildFillMaskIndices(len(indices))]
- points[offset+3:offset+3+len(indices), 0] = self.xData[indices % len(self.xData)]
- points[offset+3:offset+3+len(indices), 1] = new_y_data[indices]
+ points[offset + 3 : offset + 3 + len(indices), 0] = self.xData[
+ indices % len(self.xData)
+ ]
+ points[offset + 3 : offset + 3 + len(indices), 1] = new_y_data[indices]
# Duplicate last point for connecting degenerated triangle
- points[offset+3+len(indices)] = points[offset+3+len(indices)-1]
+ points[offset + 3 + len(indices)] = points[
+ offset + 3 + len(indices) - 1
+ ]
offset += len(indices) + 4
@@ -183,14 +199,18 @@ class _Fill2D(object):
self._PROGRAM.use()
gl.glUniformMatrix4fv(
- self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE,
- numpy.dot(context.matrix,
- mat4Translate(*self.offset)).astype(numpy.float32))
+ self._PROGRAM.uniforms["matrix"],
+ 1,
+ gl.GL_TRUE,
+ numpy.dot(context.matrix, mat4Translate(*self.offset)).astype(
+ numpy.float32
+ ),
+ )
- gl.glUniform4f(self._PROGRAM.uniforms['color'], *self.color)
+ gl.glUniform4f(self._PROGRAM.uniforms["color"], *self.color)
- xPosAttrib = self._PROGRAM.attributes['xPos']
- yPosAttrib = self._PROGRAM.attributes['yPos']
+ xPosAttrib = self._PROGRAM.attributes["xPos"]
+ yPosAttrib = self._PROGRAM.attributes["yPos"]
gl.glEnableVertexAttribArray(xPosAttrib)
self._xFillVboData.setVertexAttrib(xPosAttrib)
@@ -215,16 +235,30 @@ class _Fill2D(object):
gl.glDepthMask(gl.GL_TRUE)
# Draw directly in NDC
- gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE,
- mat4Identity().astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ self._PROGRAM.uniforms["matrix"],
+ 1,
+ gl.GL_TRUE,
+ mat4Identity().astype(numpy.float32),
+ )
# NDC vertices
gl.glVertexAttribPointer(
- xPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0,
- numpy.array((-1., -1., 1., 1.), dtype=numpy.float32))
+ xPosAttrib,
+ 1,
+ gl.GL_FLOAT,
+ gl.GL_FALSE,
+ 0,
+ numpy.array((-1.0, -1.0, 1.0, 1.0), dtype=numpy.float32),
+ )
gl.glVertexAttribPointer(
- yPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0,
- numpy.array((-1., 1., -1., 1.), dtype=numpy.float32))
+ yPosAttrib,
+ 1,
+ gl.GL_FLOAT,
+ gl.GL_FALSE,
+ 0,
+ numpy.array((-1.0, 1.0, -1.0, 1.0), dtype=numpy.float32),
+ )
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
@@ -244,8 +278,6 @@ class _Fill2D(object):
# line ########################################################################
-SOLID, DASHED, DASHDOT, DOTTED = '-', '--', '-.', ':'
-
class GLLines2D(object):
"""Object rendering curve as a polyline
@@ -254,17 +286,18 @@ class GLLines2D(object):
:param yVboData: Y coordinates VBO
:param colorVboData: VBO of colors
:param distVboData: VBO of distance along the polyline
- :param str style: Line style in: '-', '--', '-.', ':'
:param List[float] color: RGBA color as 4 float in [0, 1]
:param float width: Line width
- :param float dashPeriod: Period of dashes
+ :param List[float] dashPattern:
+ "unscaled" dash pattern as 4 lengths in points (dash1, gap1, dash2, gap2).
+ This pattern is scaled with the line width.
+ Set to () to draw solid lines (default), and to None to disable rendering.
+ :param float dashOffset: The offset in points the patterns starts at.
+ The offset is scaled with the line width.
:param drawMode: OpenGL drawing mode
:param List[float] offset: Translation of coordinates (ox, oy)
"""
- STYLES = SOLID, DASHED, DASHDOT, DOTTED
- """Supported line styles"""
-
_SOLID_PROGRAM = Program(
vertexShader="""
#version 120
@@ -290,7 +323,8 @@ class GLLines2D(object):
gl_FragColor = vColor;
}
""",
- attrib0='xPos')
+ attrib0="xPos",
+ )
# Limitation: Dash using an estimate of distance in screen coord
# to avoid computing distance when viewport is resized
@@ -321,51 +355,60 @@ class GLLines2D(object):
/* Dashes: [0, x], [y, z]
Dash period: w */
uniform vec4 dash;
- uniform vec4 dash2ndColor;
+ uniform float dashOffset;
+ uniform vec4 gapColor;
varying float vDist;
varying vec4 vColor;
void main(void) {
- float dist = mod(vDist, dash.w);
+ float dist = mod(vDist + dashOffset, dash.w);
if ((dist > dash.x && dist < dash.y) || dist > dash.z) {
- if (dash2ndColor.a == 0.) {
+ if (gapColor.a == 0.) {
discard; // Discard full transparent bg color
} else {
- gl_FragColor = dash2ndColor;
+ gl_FragColor = gapColor;
}
} else {
gl_FragColor = vColor;
}
}
""",
- attrib0='xPos')
-
- def __init__(self, xVboData=None, yVboData=None,
- colorVboData=None, distVboData=None,
- style=SOLID, color=(0., 0., 0., 1.), dash2ndColor=None,
- width=1, dashPeriod=10., drawMode=None,
- offset=(0., 0.)):
- if (xVboData is not None and
- not isinstance(xVboData, VertexBufferAttrib)):
+ attrib0="xPos",
+ )
+
+ def __init__(
+ self,
+ xVboData=None,
+ yVboData=None,
+ colorVboData=None,
+ distVboData=None,
+ color=(0.0, 0.0, 0.0, 1.0),
+ gapColor=None,
+ width=1,
+ dashOffset=0.0,
+ dashPattern=(),
+ drawMode=None,
+ offset=(0.0, 0.0),
+ ):
+ if xVboData is not None and not isinstance(xVboData, VertexBufferAttrib):
xVboData = numpy.array(xVboData, copy=False, dtype=numpy.float32)
self.xVboData = xVboData
- if (yVboData is not None and
- not isinstance(yVboData, VertexBufferAttrib)):
+ if yVboData is not None and not isinstance(yVboData, VertexBufferAttrib):
yVboData = numpy.array(yVboData, copy=False, dtype=numpy.float32)
self.yVboData = yVboData
# Compute distances if not given while providing numpy array coordinates
- if (isinstance(self.xVboData, numpy.ndarray) and
- isinstance(self.yVboData, numpy.ndarray) and
- distVboData is None):
+ if (
+ isinstance(self.xVboData, numpy.ndarray)
+ and isinstance(self.yVboData, numpy.ndarray)
+ and distVboData is None
+ ):
distVboData = distancesFromArrays(self.xVboData, self.yVboData)
- if (distVboData is not None and
- not isinstance(distVboData, VertexBufferAttrib)):
- distVboData = numpy.array(
- distVboData, copy=False, dtype=numpy.float32)
+ if distVboData is not None and not isinstance(distVboData, VertexBufferAttrib):
+ distVboData = numpy.array(distVboData, copy=False, dtype=numpy.float32)
self.distVboData = distVboData
if colorVboData is not None:
@@ -374,28 +417,14 @@ class GLLines2D(object):
self.useColorVboData = colorVboData is not None
self.color = color
- self.dash2ndColor = dash2ndColor
+ self.gapColor = gapColor
self.width = width
- self._style = None
- self.style = style
- self.dashPeriod = dashPeriod
+ self.dashPattern = dashPattern
+ self.dashOffset = dashOffset
self.offset = offset
self._drawMode = drawMode if drawMode is not None else gl.GL_LINE_STRIP
- @property
- def style(self):
- """Line style (Union[str,None])"""
- return self._style
-
- @style.setter
- def style(self, style):
- if style in _MPL_NONES:
- self._style = None
- else:
- assert style in self.STYLES
- self._style = style
-
@classmethod
def init(cls):
"""OpenGL context initialization"""
@@ -406,74 +435,57 @@ class GLLines2D(object):
:param RenderContext context:
"""
- width = self.width / 72. * context.dpi
-
- style = self.style
- if style is None:
+ if self.dashPattern is None: # Nothing to display
return
- elif style == SOLID:
+ if self.dashPattern == (): # No dash: solid line
program = self._SOLID_PROGRAM
program.use()
- else: # DASHED, DASHDOT, DOTTED
+ else: # Dashed line defined by 4 control points
program = self._DASH_PROGRAM
program.use()
- dashPeriod = self.dashPeriod * width
- if self.style == DOTTED:
- dash = (0.2 * dashPeriod,
- 0.5 * dashPeriod,
- 0.7 * dashPeriod,
- dashPeriod)
- elif self.style == DASHDOT:
- dash = (0.3 * dashPeriod,
- 0.5 * dashPeriod,
- 0.6 * dashPeriod,
- dashPeriod)
- else:
- dash = (0.5 * dashPeriod,
- dashPeriod,
- dashPeriod,
- dashPeriod)
-
- gl.glUniform4f(program.uniforms['dash'], *dash)
+ # Scale pattern by width, convert from lengths in points to offsets in pixels
+ scale = self.width / 72.0 * context.dpi
+ dashOffsets = tuple(
+ offset * scale for offset in numpy.cumsum(self.dashPattern)
+ )
+ gl.glUniform4f(program.uniforms["dash"], *dashOffsets)
+ gl.glUniform1f(program.uniforms["dashOffset"], self.dashOffset * scale)
- if self.dash2ndColor is None:
+ if self.gapColor is None:
# Use fully transparent color which gets discarded in shader
- dash2ndColor = (0., 0., 0., 0.)
+ gapColor = (0.0, 0.0, 0.0, 0.0)
else:
- dash2ndColor = self.dash2ndColor
- gl.glUniform4f(program.uniforms['dash2ndColor'], *dash2ndColor)
+ gapColor = self.gapColor
+ gl.glUniform4f(program.uniforms["gapColor"], *gapColor)
viewWidth = gl.glGetFloatv(gl.GL_VIEWPORT)[2]
xNDCPerData = (
- numpy.dot(context.matrix, [1., 0., 0., 1.])[0] -
- numpy.dot(context.matrix, [0., 0., 0., 1.])[0])
+ numpy.dot(context.matrix, [1.0, 0.0, 0.0, 1.0])[0]
+ - numpy.dot(context.matrix, [0.0, 0.0, 0.0, 1.0])[0]
+ )
xPixelPerData = 0.5 * viewWidth * xNDCPerData
- gl.glUniform1f(program.uniforms['distanceScale'], xPixelPerData)
+ gl.glUniform1f(program.uniforms["distanceScale"], xPixelPerData)
- distAttrib = program.attributes['distance']
+ distAttrib = program.attributes["distance"]
gl.glEnableVertexAttribArray(distAttrib)
if isinstance(self.distVboData, VertexBufferAttrib):
self.distVboData.setVertexAttrib(distAttrib)
else:
- gl.glVertexAttribPointer(distAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.distVboData)
-
- if width != 1:
- gl.glEnable(gl.GL_LINE_SMOOTH)
-
- matrix = numpy.dot(context.matrix,
- mat4Translate(*self.offset)).astype(numpy.float32)
- gl.glUniformMatrix4fv(program.uniforms['matrix'],
- 1, gl.GL_TRUE, matrix)
-
- colorAttrib = program.attributes['color']
+ gl.glVertexAttribPointer(
+ distAttrib, 1, gl.GL_FLOAT, False, 0, self.distVboData
+ )
+
+ gl.glEnable(gl.GL_LINE_SMOOTH)
+
+ matrix = numpy.dot(context.matrix, mat4Translate(*self.offset)).astype(
+ numpy.float32
+ )
+ gl.glUniformMatrix4fv(program.uniforms["matrix"], 1, gl.GL_TRUE, matrix)
+
+ colorAttrib = program.attributes["color"]
if self.useColorVboData and self.colorVboData is not None:
gl.glEnableVertexAttribArray(colorAttrib)
self.colorVboData.setVertexAttrib(colorAttrib)
@@ -481,37 +493,31 @@ class GLLines2D(object):
gl.glDisableVertexAttribArray(colorAttrib)
gl.glVertexAttrib4f(colorAttrib, *self.color)
- xPosAttrib = program.attributes['xPos']
+ xPosAttrib = program.attributes["xPos"]
gl.glEnableVertexAttribArray(xPosAttrib)
if isinstance(self.xVboData, VertexBufferAttrib):
self.xVboData.setVertexAttrib(xPosAttrib)
else:
- gl.glVertexAttribPointer(xPosAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.xVboData)
-
- yPosAttrib = program.attributes['yPos']
+ gl.glVertexAttribPointer(
+ xPosAttrib, 1, gl.GL_FLOAT, False, 0, self.xVboData
+ )
+
+ yPosAttrib = program.attributes["yPos"]
gl.glEnableVertexAttribArray(yPosAttrib)
if isinstance(self.yVboData, VertexBufferAttrib):
self.yVboData.setVertexAttrib(yPosAttrib)
else:
- gl.glVertexAttribPointer(yPosAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.yVboData)
-
- gl.glLineWidth(width)
+ gl.glVertexAttribPointer(
+ yPosAttrib, 1, gl.GL_FLOAT, False, 0, self.yVboData
+ )
+
+ gl.glLineWidth(self.width / 72.0 * context.dpi)
gl.glDrawArrays(self._drawMode, 0, self.xVboData.size)
gl.glDisable(gl.GL_LINE_SMOOTH)
-def distancesFromArrays(xData, yData, ratio: float=1.):
+def distancesFromArrays(xData, yData, ratio: float = 1.0):
"""Returns distances between each points
:param numpy.ndarray xData: X coordinate of points
@@ -520,8 +526,11 @@ def distancesFromArrays(xData, yData, ratio: float=1.):
:rtype: numpy.ndarray
"""
# Split array into sub-shapes at not finite points
- splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
- numpy.isfinite(xData), numpy.isfinite(yData))))[0]
+ splits = numpy.nonzero(
+ numpy.logical_not(
+ numpy.logical_and(numpy.isfinite(xData), numpy.isfinite(yData))
+ )
+ )[0]
splits = numpy.concatenate(([-1], splits, [len(xData) - 1]))
# Compute distance independently for each sub-shapes,
@@ -530,23 +539,35 @@ def distancesFromArrays(xData, yData, ratio: float=1.):
for begin, end in zip(splits[:-1] + 1, splits[1:] + 1):
if begin == end: # Empty shape
continue
- elif end - begin == 1: # Single element
+ elif end - begin == 1: # Single element
distances.append(numpy.array([0], dtype=numpy.float32))
else:
- deltas = numpy.dstack((
- numpy.ediff1d(xData[begin:end], to_begin=numpy.float32(0.)),
- numpy.ediff1d(yData[begin:end] * ratio, to_begin=numpy.float32(0.))))[0]
- distances.append(
- numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1))))
+ deltas = numpy.dstack(
+ (
+ numpy.ediff1d(xData[begin:end], to_begin=numpy.float32(0.0)),
+ numpy.ediff1d(
+ yData[begin:end] * ratio, to_begin=numpy.float32(0.0)
+ ),
+ )
+ )[0]
+ distances.append(numpy.cumsum(numpy.sqrt(numpy.sum(deltas**2, axis=1))))
return numpy.concatenate(distances)
# points ######################################################################
-DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK = \
- 'd', 'o', 's', '+', 'x', '.', ',', '*'
+DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK = (
+ "d",
+ "o",
+ "s",
+ "+",
+ "x",
+ ".",
+ ",",
+ "*",
+)
-H_LINE, V_LINE, HEART = '_', '|', u'\u2665'
+H_LINE, V_LINE, HEART = "_", "|", "\u2665"
TICK_LEFT = "tickleft"
TICK_RIGHT = "tickright"
@@ -570,9 +591,27 @@ class Points2D(object):
:param List[float] offset: Translation of coordinates (ox, oy)
"""
- MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK,
- H_LINE, V_LINE, HEART, TICK_LEFT, TICK_RIGHT, TICK_UP, TICK_DOWN,
- CARET_LEFT, CARET_RIGHT, CARET_UP, CARET_DOWN)
+ MARKERS = (
+ DIAMOND,
+ CIRCLE,
+ SQUARE,
+ PLUS,
+ X_MARKER,
+ POINT,
+ PIXEL,
+ ASTERISK,
+ H_LINE,
+ V_LINE,
+ HEART,
+ TICK_LEFT,
+ TICK_RIGHT,
+ TICK_UP,
+ TICK_DOWN,
+ CARET_LEFT,
+ CARET_RIGHT,
+ CARET_UP,
+ CARET_DOWN,
+ )
"""List of supported markers"""
_VERTEX_SHADER = """
@@ -595,47 +634,39 @@ class Points2D(object):
"""
_FRAGMENT_SHADER_SYMBOLS = {
- DIAMOND: """
+ DIAMOND: """
float alphaSymbol(vec2 coord, float size) {
vec2 centerCoord = abs(coord - vec2(0.5, 0.5));
float f = centerCoord.x + centerCoord.y;
return clamp(size * (0.5 - f), 0.0, 1.0);
}
""",
- CIRCLE: """
+ CIRCLE: """
float alphaSymbol(vec2 coord, float size) {
float radius = 0.5;
float r = distance(coord, vec2(0.5, 0.5));
return clamp(size * (radius - r), 0.0, 1.0);
}
""",
- SQUARE: """
+ SQUARE: """
float alphaSymbol(vec2 coord, float size) {
return 1.0;
}
""",
- PLUS: """
+ PLUS: """
float alphaSymbol(vec2 coord, float size) {
vec2 d = abs(size * (coord - vec2(0.5, 0.5)));
- if (min(d.x, d.y) < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
+ return local_smoothstep(1.5, 0.5, min(d.x, d.y));
}
""",
- X_MARKER: """
+ X_MARKER: """
float alphaSymbol(vec2 coord, float size) {
vec2 pos = floor(size * coord) + 0.5;
vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size));
- if (min(d_x.x, d_x.y) <= 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
+ return local_smoothstep(1.5, 0.5, min(d_x.x, d_x.y));
}
""",
- ASTERISK: """
+ ASTERISK: """
float alphaSymbol(vec2 coord, float size) {
/* Combining +, x and circle */
vec2 d_plus = abs(size * (coord - vec2(0.5, 0.5)));
@@ -651,27 +682,19 @@ class Points2D(object):
}
}
""",
- H_LINE: """
+ H_LINE: """
float alphaSymbol(vec2 coord, float size) {
- float dy = abs(size * (coord.y - 0.5));
- if (dy < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
+ float d = abs(size * (coord.y - 0.5));
+ return local_smoothstep(1.5, 0.5, d);
}
""",
- V_LINE: """
+ V_LINE: """
float alphaSymbol(vec2 coord, float size) {
- float dx = abs(size * (coord.x - 0.5));
- if (dx < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
+ float d = abs(size * (coord.x - 0.5));
+ return local_smoothstep(1.5, 0.5, d);
}
""",
- HEART: """
+ HEART: """
float alphaSymbol(vec2 coord, float size) {
coord = (coord - 0.5) * 2.;
coord *= 0.75;
@@ -682,93 +705,89 @@ class Points2D(object):
float d = (13.0*h - 22.0*h*h + 10.0*h*h*h)/(6.0-5.0*h);
float res = clamp(r-d, 0., 1.);
// antialiasing
- res = smoothstep(0.1, 0.001, res);
+ res = local_smoothstep(0.1, 0.001, res);
return res;
}
""",
- TICK_LEFT: """
+ TICK_LEFT: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float dy = abs(coord.y);
- if (dy < 0.5 && coord.x < 0.5) {
- return 1.0;
- } else {
+ if (coord.x > 0.5) {
return 0.0;
}
+ return local_smoothstep(1.5, 0.5, dy);
}
""",
- TICK_RIGHT: """
+ TICK_RIGHT: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float dy = abs(coord.y);
- if (dy < 0.5 && coord.x > -0.5) {
- return 1.0;
- } else {
+ if (coord.x < -0.5) {
return 0.0;
}
+ return local_smoothstep(1.5, 0.5, dy);
}
""",
- TICK_UP: """
+ TICK_UP: """
float alphaSymbol(vec2 coord, float size) {
- coord = size * (coord - 0.5);
+ coord = size * (coord - 0.5);
float dx = abs(coord.x);
- if (dx < 0.5 && coord.y < 0.5) {
- return 1.0;
- } else {
+ if (coord.y > 0.5) {
return 0.0;
}
+ return local_smoothstep(1.5, 0.5, dx);
}
""",
- TICK_DOWN: """
+ TICK_DOWN: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float dx = abs(coord.x);
- if (dx < 0.5 && coord.y > -0.5) {
- return 1.0;
- } else {
+ if (coord.y < -0.5) {
return 0.0;
}
+ return local_smoothstep(1.5, 0.5, dx);
}
""",
- CARET_LEFT: """
+ CARET_LEFT: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float d = abs(coord.x) - abs(coord.y);
if (d >= -0.1 && coord.x > 0.5) {
- return smoothstep(-0.1, 0.1, d);
+ return local_smoothstep(-0.1, 0.1, d);
} else {
return 0.0;
}
}
""",
- CARET_RIGHT: """
+ CARET_RIGHT: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float d = abs(coord.x) - abs(coord.y);
if (d >= -0.1 && coord.x < 0.5) {
- return smoothstep(-0.1, 0.1, d);
+ return local_smoothstep(-0.1, 0.1, d);
} else {
return 0.0;
}
}
""",
- CARET_UP: """
+ CARET_UP: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float d = abs(coord.y) - abs(coord.x);
if (d >= -0.1 && coord.y > 0.5) {
- return smoothstep(-0.1, 0.1, d);
+ return local_smoothstep(-0.1, 0.1, d);
} else {
return 0.0;
}
}
""",
- CARET_DOWN: """
+ CARET_DOWN: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float d = abs(coord.y) - abs(coord.x);
if (d >= -0.1 && coord.y < 0.5) {
- return smoothstep(-0.1, 0.1, d);
+ return local_smoothstep(-0.1, 0.1, d);
} else {
return 0.0;
}
@@ -783,6 +802,13 @@ class Points2D(object):
varying vec4 vColor;
+ /* smoothstep function implementation to support GLSL 1.20 */
+ float local_smoothstep(float edge0, float edge1, float x) {
+ float t;
+ t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
+ return t * t * (3.0 - 2.0 * t);
+ }
+
%s
void main(void) {
@@ -797,22 +823,27 @@ class Points2D(object):
_PROGRAMS = {}
- def __init__(self, xVboData=None, yVboData=None, colorVboData=None,
- marker=SQUARE, color=(0., 0., 0., 1.), size=7,
- offset=(0., 0.)):
+ def __init__(
+ self,
+ xVboData=None,
+ yVboData=None,
+ colorVboData=None,
+ marker=SQUARE,
+ color=(0.0, 0.0, 0.0, 1.0),
+ size=7,
+ offset=(0.0, 0.0),
+ ):
self.color = color
self._marker = None
self.marker = marker
self.size = size
self.offset = offset
- if (xVboData is not None and
- not isinstance(xVboData, VertexBufferAttrib)):
+ if xVboData is not None and not isinstance(xVboData, VertexBufferAttrib):
xVboData = numpy.array(xVboData, copy=False, dtype=numpy.float32)
self.xVboData = xVboData
- if (yVboData is not None and
- not isinstance(yVboData, VertexBufferAttrib)):
+ if yVboData is not None and not isinstance(yVboData, VertexBufferAttrib):
yVboData = numpy.array(yVboData, copy=False, dtype=numpy.float32)
self.yVboData = yVboData
@@ -845,9 +876,11 @@ class Points2D(object):
if marker not in cls._PROGRAMS:
cls._PROGRAMS[marker] = Program(
vertexShader=cls._VERTEX_SHADER,
- fragmentShader=(cls._FRAGMENT_SHADER_TEMPLATE %
- cls._FRAGMENT_SHADER_SYMBOLS[marker]),
- attrib0='xPos')
+ fragmentShader=(
+ cls._FRAGMENT_SHADER_TEMPLATE % cls._FRAGMENT_SHADER_SYMBOLS[marker]
+ ),
+ attrib0="xPos",
+ )
return cls._PROGRAMS[marker]
@@ -873,9 +906,10 @@ class Points2D(object):
program = self._getProgram(self.marker)
program.use()
- matrix = numpy.dot(context.matrix,
- mat4Translate(*self.offset)).astype(numpy.float32)
- gl.glUniformMatrix4fv(program.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
+ matrix = numpy.dot(context.matrix, mat4Translate(*self.offset)).astype(
+ numpy.float32
+ )
+ gl.glUniformMatrix4fv(program.uniforms["matrix"], 1, gl.GL_TRUE, matrix)
if self.marker == PIXEL:
size = 1
@@ -883,17 +917,24 @@ class Points2D(object):
size = math.ceil(0.5 * self.size) + 1 # Mimic Matplotlib point
else:
size = self.size
- size = size / 72. * context.dpi
-
- if self.marker in (PLUS, H_LINE, V_LINE,
- TICK_LEFT, TICK_RIGHT, TICK_UP, TICK_DOWN):
+ size = size / 72.0 * context.dpi
+
+ if self.marker in (
+ PLUS,
+ H_LINE,
+ V_LINE,
+ TICK_LEFT,
+ TICK_RIGHT,
+ TICK_UP,
+ TICK_DOWN,
+ ):
# Convert to nearest odd number
- size = size // 2 * 2 + 1.
+ size = size // 2 * 2 + 1.0
- gl.glUniform1f(program.uniforms['size'], size)
+ gl.glUniform1f(program.uniforms["size"], size)
# gl.glPointSize(self.size)
- cAttrib = program.attributes['color']
+ cAttrib = program.attributes["color"]
if self.useColorVboData and self.colorVboData is not None:
gl.glEnableVertexAttribArray(cAttrib)
self.colorVboData.setVertexAttrib(cAttrib)
@@ -901,36 +942,30 @@ class Points2D(object):
gl.glDisableVertexAttribArray(cAttrib)
gl.glVertexAttrib4f(cAttrib, *self.color)
- xPosAttrib = program.attributes['xPos']
+ xPosAttrib = program.attributes["xPos"]
gl.glEnableVertexAttribArray(xPosAttrib)
if isinstance(self.xVboData, VertexBufferAttrib):
self.xVboData.setVertexAttrib(xPosAttrib)
else:
- gl.glVertexAttribPointer(xPosAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.xVboData)
-
- yPosAttrib = program.attributes['yPos']
+ gl.glVertexAttribPointer(
+ xPosAttrib, 1, gl.GL_FLOAT, False, 0, self.xVboData
+ )
+
+ yPosAttrib = program.attributes["yPos"]
gl.glEnableVertexAttribArray(yPosAttrib)
if isinstance(self.yVboData, VertexBufferAttrib):
self.yVboData.setVertexAttrib(yPosAttrib)
else:
- gl.glVertexAttribPointer(yPosAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.yVboData)
-
+ gl.glVertexAttribPointer(
+ yPosAttrib, 1, gl.GL_FLOAT, False, 0, self.yVboData
+ )
gl.glDrawArrays(gl.GL_POINTS, 0, self.xVboData.size)
# error bars ##################################################################
+
class _ErrorBars(object):
"""Display errors bars.
@@ -956,49 +991,58 @@ class _ErrorBars(object):
:param List[float] offset: Translation of coordinates (ox, oy)
"""
- def __init__(self, xData, yData, xError, yError,
- xMin, yMin,
- color=(0., 0., 0., 1.),
- offset=(0., 0.)):
+ def __init__(
+ self,
+ xData,
+ yData,
+ xError,
+ yError,
+ xMin,
+ yMin,
+ color=(0.0, 0.0, 0.0, 1.0),
+ offset=(0.0, 0.0),
+ ):
self._attribs = None
self._xMin, self._yMin = xMin, yMin
self.offset = offset
if xError is not None or yError is not None:
- self._xData = numpy.array(
- xData, order='C', dtype=numpy.float32, copy=False)
- self._yData = numpy.array(
- yData, order='C', dtype=numpy.float32, copy=False)
+ self._xData = numpy.array(xData, order="C", dtype=numpy.float32, copy=False)
+ self._yData = numpy.array(yData, order="C", dtype=numpy.float32, copy=False)
# This also works if xError, yError is a float/int
self._xError = numpy.array(
- xError, order='C', dtype=numpy.float32, copy=False)
+ xError, order="C", dtype=numpy.float32, copy=False
+ )
self._yError = numpy.array(
- yError, order='C', dtype=numpy.float32, copy=False)
+ yError, order="C", dtype=numpy.float32, copy=False
+ )
else:
self._xData, self._yData = None, None
self._xError, self._yError = None, None
self._lines = GLLines2D(
- None, None, color=color, drawMode=gl.GL_LINES, offset=offset)
+ None, None, color=color, drawMode=gl.GL_LINES, offset=offset
+ )
self._xErrPoints = Points2D(
- None, None, color=color, marker=V_LINE, offset=offset)
+ None, None, color=color, marker=V_LINE, offset=offset
+ )
self._yErrPoints = Points2D(
- None, None, color=color, marker=H_LINE, offset=offset)
+ None, None, color=color, marker=H_LINE, offset=offset
+ )
def _buildVertices(self):
"""Generates error bars vertices"""
- nbLinesPerDataPts = (0 if self._xError is None else 2) + \
- (0 if self._yError is None else 2)
+ nbLinesPerDataPts = (0 if self._xError is None else 2) + (
+ 0 if self._yError is None else 2
+ )
nbDataPts = len(self._xData)
# interleave coord+error, coord-error.
# xError vertices first if any, then yError vertices if any.
- xCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2,
- dtype=numpy.float32)
- yCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2,
- dtype=numpy.float32)
+ xCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2, dtype=numpy.float32)
+ yCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2, dtype=numpy.float32)
if self._xError is not None: # errors on the X axis
if len(self._xError.shape) == 2:
@@ -1010,15 +1054,15 @@ class _ErrorBars(object):
# Interleave vertices for xError
endXError = 4 * nbDataPts
with numpy.errstate(invalid="ignore"):
- xCoords[0:endXError-3:4] = self._xData + xErrorPlus
- xCoords[1:endXError-2:4] = self._xData
- xCoords[2:endXError-1:4] = self._xData
+ xCoords[0 : endXError - 3 : 4] = self._xData + xErrorPlus
+ xCoords[1 : endXError - 2 : 4] = self._xData
+ xCoords[2 : endXError - 1 : 4] = self._xData
with numpy.errstate(invalid="ignore"):
xCoords[3:endXError:4] = self._xData - xErrorMinus
- yCoords[0:endXError-3:4] = self._yData
- yCoords[1:endXError-2:4] = self._yData
- yCoords[2:endXError-1:4] = self._yData
+ yCoords[0 : endXError - 3 : 4] = self._yData
+ yCoords[1 : endXError - 2 : 4] = self._yData
+ yCoords[2 : endXError - 1 : 4] = self._yData
yCoords[3:endXError:4] = self._yData
else:
@@ -1033,16 +1077,16 @@ class _ErrorBars(object):
# Interleave vertices for yError
xCoords[endXError::4] = self._xData
- xCoords[endXError+1::4] = self._xData
- xCoords[endXError+2::4] = self._xData
- xCoords[endXError+3::4] = self._xData
+ xCoords[endXError + 1 :: 4] = self._xData
+ xCoords[endXError + 2 :: 4] = self._xData
+ xCoords[endXError + 3 :: 4] = self._xData
with numpy.errstate(invalid="ignore"):
yCoords[endXError::4] = self._yData + yErrorPlus
- yCoords[endXError+1::4] = self._yData
- yCoords[endXError+2::4] = self._yData
+ yCoords[endXError + 1 :: 4] = self._yData
+ yCoords[endXError + 2 :: 4] = self._yData
with numpy.errstate(invalid="ignore"):
- yCoords[endXError+3::4] = self._yData - yErrorMinus
+ yCoords[endXError + 3 :: 4] = self._yData - yErrorMinus
return xCoords, yCoords
@@ -1069,12 +1113,10 @@ class _ErrorBars(object):
# Set yError points using the same VBO as lines
self._yErrPoints.xVboData = xAttrib.copy()
self._yErrPoints.xVboData.size //= 2
- self._yErrPoints.xVboData.offset += (xAttrib.itemsize *
- xAttrib.size // 2)
+ self._yErrPoints.xVboData.offset += xAttrib.itemsize * xAttrib.size // 2
self._yErrPoints.yVboData = yAttrib.copy()
self._yErrPoints.yVboData.size //= 2
- self._yErrPoints.yVboData.offset += (yAttrib.itemsize *
- yAttrib.size // 2)
+ self._yErrPoints.yVboData.offset += yAttrib.itemsize * yAttrib.size // 2
def render(self, context):
"""Perform rendering
@@ -1103,12 +1145,14 @@ class _ErrorBars(object):
# curves ######################################################################
+
def _proxyProperty(*componentsAttributes):
"""Create a property to access an attribute of attribute(s).
Useful for composition.
Supports multiple components this way:
getter returns the first found, setter sets all
"""
+
def getter(self):
for compName, attrName in componentsAttributes:
try:
@@ -1122,22 +1166,30 @@ def _proxyProperty(*componentsAttributes):
for compName, attrName in componentsAttributes:
component = getattr(self, compName)
setattr(component, attrName, value)
+
return property(getter, setter)
class GLPlotCurve2D(GLPlotItem):
- def __init__(self, xData, yData, colorData=None,
- xError=None, yError=None,
- lineStyle=SOLID,
- lineColor=(0., 0., 0., 1.),
- lineWidth=1,
- lineDashPeriod=20,
- marker=SQUARE,
- markerColor=(0., 0., 0., 1.),
- markerSize=7,
- fillColor=None,
- baseline=None,
- isYLog=False):
+ def __init__(
+ self,
+ xData,
+ yData,
+ colorData=None,
+ xError=None,
+ yError=None,
+ lineColor=(0.0, 0.0, 0.0, 1.0),
+ lineGapColor=None,
+ lineWidth=1,
+ lineDashOffset=0.0,
+ lineDashPattern=(),
+ marker=SQUARE,
+ markerColor=(0.0, 0.0, 0.0, 1.0),
+ markerSize=7,
+ fillColor=None,
+ baseline=None,
+ isYLog=False,
+ ):
super().__init__()
self._ratio = None
self.colorData = colorData
@@ -1147,7 +1199,7 @@ class GLPlotCurve2D(GLPlotItem):
self.xMin, self.xMax = min_max(xData, min_positive=False)
else:
# Takes the error into account
- if hasattr(xError, 'shape') and len(xError.shape) == 2:
+ if hasattr(xError, "shape") and len(xError.shape) == 2:
xErrorMinus, xErrorPlus = xError[0], xError[1]
else:
xErrorMinus, xErrorPlus = xError, xError
@@ -1159,7 +1211,7 @@ class GLPlotCurve2D(GLPlotItem):
self.yMin, self.yMax = min_max(yData, min_positive=False)
else:
# Takes the error into account
- if hasattr(yError, 'shape') and len(yError.shape) == 2:
+ if hasattr(yError, "shape") and len(yError.shape) == 2:
yErrorMinus, yErrorPlus = yError[0], yError[1]
else:
yErrorMinus, yErrorPlus = yError, yError
@@ -1175,44 +1227,53 @@ class GLPlotCurve2D(GLPlotItem):
self.yData = (yData - self.offset[1]).astype(numpy.float32)
else: # float32
- self.offset = 0., 0.
+ self.offset = 0.0, 0.0
self.xData = xData
self.yData = yData
if fillColor is not None:
+
def deduce_baseline(baseline):
if baseline is None:
_baseline = 0
else:
_baseline = baseline
if not isinstance(_baseline, numpy.ndarray):
- _baseline = numpy.repeat(_baseline,
- len(self.xData))
+ _baseline = numpy.repeat(_baseline, len(self.xData))
if isYLog is True:
- with numpy.errstate(divide='ignore', invalid='ignore'):
+ with numpy.errstate(divide="ignore", invalid="ignore"):
log_val = numpy.log10(_baseline)
- _baseline = numpy.where(_baseline>0.0, log_val, -38)
+ _baseline = numpy.where(_baseline > 0.0, log_val, -38)
return _baseline
_baseline = deduce_baseline(baseline)
# Use different baseline depending of Y log scale
- self.fill = _Fill2D(self.xData, self.yData,
- baseline=_baseline,
- color=fillColor,
- offset=self.offset)
+ self.fill = _Fill2D(
+ self.xData,
+ self.yData,
+ baseline=_baseline,
+ color=fillColor,
+ offset=self.offset,
+ )
else:
self.fill = None
- self._errorBars = _ErrorBars(self.xData, self.yData,
- xError, yError,
- self.xMin, self.yMin,
- offset=self.offset)
+ self._errorBars = _ErrorBars(
+ self.xData,
+ self.yData,
+ xError,
+ yError,
+ self.xMin,
+ self.yMin,
+ offset=self.offset,
+ )
self.lines = GLLines2D()
- self.lines.style = lineStyle
self.lines.color = lineColor
+ self.lines.gapColor = lineGapColor
self.lines.width = lineWidth
- self.lines.dashPeriod = lineDashPeriod
+ self.lines.dashOffset = lineDashOffset
+ self.lines.dashPattern = lineDashPattern
self.lines.offset = self.offset
self.points = Points2D()
@@ -1221,31 +1282,33 @@ class GLPlotCurve2D(GLPlotItem):
self.points.size = markerSize
self.points.offset = self.offset
- xVboData = _proxyProperty(('lines', 'xVboData'), ('points', 'xVboData'))
+ xVboData = _proxyProperty(("lines", "xVboData"), ("points", "xVboData"))
+
+ yVboData = _proxyProperty(("lines", "yVboData"), ("points", "yVboData"))
- yVboData = _proxyProperty(('lines', 'yVboData'), ('points', 'yVboData'))
+ colorVboData = _proxyProperty(("lines", "colorVboData"), ("points", "colorVboData"))
- colorVboData = _proxyProperty(('lines', 'colorVboData'),
- ('points', 'colorVboData'))
+ useColorVboData = _proxyProperty(
+ ("lines", "useColorVboData"), ("points", "useColorVboData")
+ )
- useColorVboData = _proxyProperty(('lines', 'useColorVboData'),
- ('points', 'useColorVboData'))
+ distVboData = _proxyProperty(("lines", "distVboData"))
- distVboData = _proxyProperty(('lines', 'distVboData'))
+ lineColor = _proxyProperty(("lines", "color"))
- lineStyle = _proxyProperty(('lines', 'style'))
+ lineGapColor = _proxyProperty(("lines", "gapColor"))
- lineColor = _proxyProperty(('lines', 'color'))
+ lineWidth = _proxyProperty(("lines", "width"))
- lineWidth = _proxyProperty(('lines', 'width'))
+ lineDashOffset = _proxyProperty(("lines", "dashOffset"))
- lineDashPeriod = _proxyProperty(('lines', 'dashPeriod'))
+ lineDashPattern = _proxyProperty(("lines", "dashPattern"))
- marker = _proxyProperty(('points', 'marker'))
+ marker = _proxyProperty(("points", "marker"))
- markerColor = _proxyProperty(('points', 'color'))
+ markerColor = _proxyProperty(("points", "color"))
- markerSize = _proxyProperty(('points', 'size'))
+ markerSize = _proxyProperty(("points", "size"))
@classmethod
def init(cls):
@@ -1257,25 +1320,28 @@ class GLPlotCurve2D(GLPlotItem):
"""Rendering preparation: build indices and bounding box vertices"""
if self.xVboData is None:
xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None
- if self.lineStyle in (DASHED, DASHDOT, DOTTED):
+ if self.lineDashPattern:
dists = distancesFromArrays(self.xData, self.yData, self._ratio)
if self.colorData is None:
xAttrib, yAttrib, dAttrib = vertexBuffer(
- (self.xData, self.yData, dists))
+ (self.xData, self.yData, dists)
+ )
else:
xAttrib, yAttrib, cAttrib, dAttrib = vertexBuffer(
- (self.xData, self.yData, self.colorData, dists))
+ (self.xData, self.yData, self.colorData, dists)
+ )
elif self.colorData is None:
xAttrib, yAttrib = vertexBuffer((self.xData, self.yData))
else:
xAttrib, yAttrib, cAttrib = vertexBuffer(
- (self.xData, self.yData, self.colorData))
+ (self.xData, self.yData, self.colorData)
+ )
self.xVboData = xAttrib
self.yVboData = yAttrib
self.distVboData = dAttrib
- if cAttrib is not None and self.colorData.dtype.kind == 'u':
+ if cAttrib is not None and self.colorData.dtype.kind == "u":
cAttrib.normalization = True # Normalize uint to [0, 1]
self.colorVboData = cAttrib
self.useColorVboData = cAttrib is not None
@@ -1285,13 +1351,17 @@ class GLPlotCurve2D(GLPlotItem):
:param RenderContext context: Rendering information
"""
- if self.lineStyle in (DASHED, DASHDOT, DOTTED):
+ if self.lineDashPattern:
visibleRanges = context.plotFrame.transformedDataRanges
xLimits = visibleRanges.x
- yLimits = visibleRanges.y if self.yaxis == 'left' else visibleRanges.y2
+ yLimits = visibleRanges.y if self.yaxis == "left" else visibleRanges.y2
width, height = context.plotFrame.plotSize
- ratio = (height * (xLimits[1] - xLimits[0])) / (width * (yLimits[1] - yLimits[0]))
- if self._ratio is None or abs(1. - ratio/self._ratio) > 0.05: # Tolerate 5% difference
+ ratio = (height * (xLimits[1] - xLimits[0])) / (
+ width * (yLimits[1] - yLimits[0])
+ )
+ if (
+ self._ratio is None or abs(1.0 - ratio / self._ratio) > 0.05
+ ): # Tolerate 5% difference
# Rebuild curve buffers to update distances
self._ratio = ratio
self.discard()
@@ -1318,9 +1388,11 @@ class GLPlotCurve2D(GLPlotItem):
self.fill.discard()
def isInitialized(self):
- return (self.xVboData is not None or
- self._errorBars.isInitialized() or
- (self.fill is not None and self.fill.isInitialized()))
+ return (
+ self.xVboData is not None
+ or self._errorBars.isInitialized()
+ or (self.fill is not None and self.fill.isInitialized())
+ )
def pick(self, xPickMin, yPickMin, xPickMax, yPickMax):
"""Perform picking on the curve according to its rendering.
@@ -1335,9 +1407,13 @@ class GLPlotCurve2D(GLPlotItem):
:return: The indices of the picked data
:rtype: Union[List[int],None]
"""
- if (self.marker is None and self.lineStyle is None) or \
- self.xMin > xPickMax or xPickMin > self.xMax or \
- self.yMin > yPickMax or yPickMin > self.yMax:
+ if (
+ (self.marker is None and self.lineDashPattern is None)
+ or self.xMin > xPickMax
+ or xPickMin > self.xMax
+ or self.yMin > yPickMax
+ or yPickMin > self.yMax
+ ):
return None
# offset picking bounds
@@ -1346,25 +1422,27 @@ class GLPlotCurve2D(GLPlotItem):
yPickMin = yPickMin - self.offset[1]
yPickMax = yPickMax - self.offset[1]
- if self.lineStyle is not None:
+ if self.lineDashPattern is not None:
# Using Cohen-Sutherland algorithm for line clipping
- with numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings
- codes = ((self.yData > yPickMax) << 3) | \
- ((self.yData < yPickMin) << 2) | \
- ((self.xData > xPickMax) << 1) | \
- (self.xData < xPickMin)
-
- notNaN = numpy.logical_not(numpy.logical_or(
- numpy.isnan(self.xData), numpy.isnan(self.yData)))
+ with numpy.errstate(invalid="ignore"): # Ignore NaN comparison warnings
+ codes = (
+ ((self.yData > yPickMax) << 3)
+ | ((self.yData < yPickMin) << 2)
+ | ((self.xData > xPickMax) << 1)
+ | (self.xData < xPickMin)
+ )
+
+ notNaN = numpy.logical_not(
+ numpy.logical_or(numpy.isnan(self.xData), numpy.isnan(self.yData))
+ )
# Add all points that are inside the picking area
- indices = numpy.nonzero(
- numpy.logical_and(codes == 0, notNaN))[0].tolist()
+ indices = numpy.nonzero(numpy.logical_and(codes == 0, notNaN))[0].tolist()
# Segment that might cross the area with no end point inside it
- segToTestIdx = numpy.nonzero((codes[:-1] != 0) &
- (codes[1:] != 0) &
- ((codes[:-1] & codes[1:]) == 0))[0]
+ segToTestIdx = numpy.nonzero(
+ (codes[:-1] != 0) & (codes[1:] != 0) & ((codes[:-1] & codes[1:]) == 0)
+ )[0]
TOP, BOTTOM, RIGHT, LEFT = (1 << 3), (1 << 2), (1 << 1), (1 << 0)
@@ -1405,10 +1483,12 @@ class GLPlotCurve2D(GLPlotItem):
indices.sort()
else:
- with numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings
- indices = numpy.nonzero((self.xData >= xPickMin) &
- (self.xData <= xPickMax) &
- (self.yData >= yPickMin) &
- (self.yData <= yPickMax))[0].tolist()
+ with numpy.errstate(invalid="ignore"): # Ignore NaN comparison warnings
+ indices = numpy.nonzero(
+ (self.xData >= xPickMin)
+ & (self.xData <= xPickMax)
+ & (self.yData >= yPickMin)
+ & (self.yData <= yPickMax)
+ )[0].tolist()
return tuple(indices) if len(indices) > 0 else None
diff --git a/src/silx/gui/plot/backends/glutils/GLPlotFrame.py b/src/silx/gui/plot/backends/glutils/GLPlotFrame.py
index e5fabf2..42cfa50 100644
--- a/src/silx/gui/plot/backends/glutils/GLPlotFrame.py
+++ b/src/silx/gui/plot/backends/glutils/GLPlotFrame.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2014-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 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
@@ -25,6 +25,8 @@
This modules provides the rendering of plot titles, axes and grid.
"""
+from __future__ import annotations
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "03/04/2017"
@@ -44,12 +46,19 @@ from collections import namedtuple
import numpy
+from .... import qt
from ...._glutils import gl, Program
+from ....utils.matplotlib import DefaultTickFormatter
from ..._utils import checkAxisLimits, FLOAT32_MINPOS
from .GLSupport import mat4Ortho
from .GLText import Text2D, CENTER, BOTTOM, TOP, LEFT, RIGHT, ROTATE_270
from ..._utils.ticklayout import niceNumbersAdaptative, niceNumbersForLog10
-from ..._utils.dtime_ticklayout import calcTicksAdaptive, bestFormatString
+from ..._utils.dtime_ticklayout import (
+ DtUnit,
+ bestUnit,
+ calcTicksAdaptive,
+ formatDatetimes,
+)
from ..._utils.dtime_ticklayout import timestamp
_logger = logging.getLogger(__name__)
@@ -57,36 +66,52 @@ _logger = logging.getLogger(__name__)
# PlotAxis ####################################################################
+
class PlotAxis(object):
"""Represents a 1D axis of the plot.
This class is intended to be used with :class:`GLPlotFrame`.
"""
- def __init__(self, plotFrame,
- tickLength=(0., 0.),
- foregroundColor=(0., 0., 0., 1.0),
- labelAlign=CENTER, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=CENTER,
- titleRotate=0, titleOffset=(0., 0.)):
+ def __init__(
+ self,
+ plotFrame,
+ tickLength=(0.0, 0.0),
+ foregroundColor=(0.0, 0.0, 0.0, 1.0),
+ labelAlign=CENTER,
+ labelVAlign=CENTER,
+ titleAlign=CENTER,
+ titleVAlign=CENTER,
+ orderOffsetAlign=CENTER,
+ orderOffsetVAlign=CENTER,
+ titleRotate=0,
+ titleOffset=(0.0, 0.0),
+ font: qt.QFont | None = None,
+ ):
+ self._tickFormatter = DefaultTickFormatter()
self._ticks = None
+ self._orderAndOffsetText = ""
self._plotFrameRef = weakref.ref(plotFrame)
self._isDateTime = False
self._timeZone = None
self._isLog = False
- self._dataRange = 1., 100.
- self._displayCoords = (0., 0.), (1., 0.)
- self._title = ''
+ self._dataRange = 1.0, 100.0
+ self._displayCoords = (0.0, 0.0), (1.0, 0.0)
+ self._title = ""
self._tickLength = tickLength
self._foregroundColor = foregroundColor
self._labelAlign = labelAlign
self._labelVAlign = labelVAlign
+ self._orderOffetAnchor = (1.0, 0.0)
+ self._orderOffsetAlign = orderOffsetAlign
+ self._orderOffsetVAlign = orderOffsetVAlign
self._titleAlign = titleAlign
self._titleVAlign = titleVAlign
self._titleRotate = titleRotate
self._titleOffset = titleOffset
+ self._font = font
@property
def dataRange(self):
@@ -94,6 +119,12 @@ class PlotAxis(object):
of 2 floats: (min, max)."""
return self._dataRange
+ @property
+ def font(self) -> qt.QFont:
+ if self._font is None:
+ return qt.QApplication.instance().font()
+ return self._font
+
@dataRange.setter
def dataRange(self, dataRange):
assert len(dataRange) == 2
@@ -161,7 +192,13 @@ class PlotAxis(object):
def devicePixelRatio(self):
"""Returns the ratio between qt pixels and device pixels."""
plotFrame = self._plotFrameRef()
- return plotFrame.devicePixelRatio if plotFrame is not None else 1.
+ return plotFrame.devicePixelRatio if plotFrame is not None else 1.0
+
+ @property
+ def dotsPerInch(self):
+ """Returns the screen DPI"""
+ plotFrame = self._plotFrameRef()
+ return plotFrame.dotsPerInch if plotFrame is not None else 92
@property
def title(self):
@@ -175,6 +212,17 @@ class PlotAxis(object):
self._dirtyPlotFrame()
@property
+ def orderOffetAnchor(self) -> tuple[float, float]:
+ """Anchor position for the tick order&offset text"""
+ return self._orderOffetAnchor
+
+ @orderOffetAnchor.setter
+ def orderOffetAnchor(self, position: tuple[float, float]):
+ if position != self._orderOffetAnchor:
+ self._orderOffetAnchor = position
+ self._dirtyTicks()
+
+ @property
def titleOffset(self):
"""Title offset in pixels (x: int, y: int)"""
return self._titleOffset
@@ -193,8 +241,9 @@ class PlotAxis(object):
@foregroundColor.setter
def foregroundColor(self, color):
"""Color used for frame and labels"""
- assert len(color) == 4, \
- "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
+ assert len(color) == 4, "foregroundColor must have length 4, got {}".format(
+ len(self._foregroundColor)
+ )
if self._foregroundColor != color:
self._foregroundColor = color
self._dirtyTicks()
@@ -213,7 +262,6 @@ class PlotAxis(object):
"""
vertices = list(self.displayCoords) # Add start and end points
labels = []
- tickLabelsSize = [0., 0.]
xTickLength, yTickLength = self._tickLength
xTickLength *= self.devicePixelRatio
@@ -222,27 +270,24 @@ class PlotAxis(object):
if text is None:
tickScale = 0.5
else:
- tickScale = 1.
-
- label = Text2D(text=text,
- color=self._foregroundColor,
- x=xPixel - xTickLength,
- y=yPixel - yTickLength,
- align=self._labelAlign,
- valign=self._labelVAlign,
- devicePixelRatio=self.devicePixelRatio)
-
- width, height = label.size
- if width > tickLabelsSize[0]:
- tickLabelsSize[0] = width
- if height > tickLabelsSize[1]:
- tickLabelsSize[1] = height
-
+ tickScale = 1.0
+
+ label = Text2D(
+ text=text,
+ font=self.font,
+ color=self._foregroundColor,
+ x=xPixel - xTickLength,
+ y=yPixel - yTickLength,
+ align=self._labelAlign,
+ valign=self._labelVAlign,
+ devicePixelRatio=self.devicePixelRatio,
+ )
labels.append(label)
vertices.append((xPixel, yPixel))
- vertices.append((xPixel + tickScale * xTickLength,
- yPixel + tickScale * yTickLength))
+ vertices.append(
+ (xPixel + tickScale * xTickLength, yPixel + tickScale * yTickLength)
+ )
(x0, y0), (x1, y1) = self.displayCoords
xAxisCenter = 0.5 * (x0 + x1)
@@ -257,16 +302,33 @@ class PlotAxis(object):
# yOffset = -tickLabelsSize[1] * yTickLength / tickNorm
# yOffset -= 3 * yTickLength
- axisTitle = Text2D(text=self.title,
- color=self._foregroundColor,
- x=xAxisCenter + xOffset,
- y=yAxisCenter + yOffset,
- align=self._titleAlign,
- valign=self._titleVAlign,
- rotate=self._titleRotate,
- devicePixelRatio=self.devicePixelRatio)
+ axisTitle = Text2D(
+ text=self.title,
+ font=self.font,
+ color=self._foregroundColor,
+ x=xAxisCenter + xOffset,
+ y=yAxisCenter + yOffset,
+ align=self._titleAlign,
+ valign=self._titleVAlign,
+ rotate=self._titleRotate,
+ devicePixelRatio=self.devicePixelRatio,
+ )
labels.append(axisTitle)
+ if self._orderAndOffsetText:
+ xOrderOffset, yOrderOffet = self.orderOffetAnchor
+ labels.append(
+ Text2D(
+ text=self._orderAndOffsetText,
+ font=self.font,
+ color=self._foregroundColor,
+ x=xOrderOffset,
+ y=yOrderOffet,
+ align=self._orderOffsetAlign,
+ valign=self._orderOffsetVAlign,
+ devicePixelRatio=self.devicePixelRatio,
+ )
+ )
return vertices, labels
def _dirtyPlotFrame(self):
@@ -291,19 +353,19 @@ class PlotAxis(object):
"""Generator of ticks as tuples:
((x, y) in display, dataPos, textLabel).
"""
+ self._orderAndOffsetText = ""
+
dataMin, dataMax = self.dataRange
- if self.isLog and dataMin <= 0.:
- _logger.warning(
- 'Getting ticks while isLog=True and dataRange[0]<=0.')
- dataMin = 1.
+ if self.isLog and dataMin <= 0.0:
+ _logger.warning("Getting ticks while isLog=True and dataRange[0]<=0.")
+ dataMin = 1.0
if dataMax < dataMin:
- dataMax = 1.
+ dataMax = 1.0
if dataMin != dataMax: # data range is not null
(x0, y0), (x1, y1) = self.displayCoords
if self.isLog:
-
if self.isTimeSeries:
_logger.warning("Time series not implemented for log-scale")
@@ -315,16 +377,16 @@ class PlotAxis(object):
for logPos in self._frange(tickMin, tickMax, step):
if logMin <= logPos <= logMax:
- dataPos = 10 ** logPos
+ dataPos = 10**logPos
xPixel = x0 + (logPos - logMin) * xScale
yPixel = y0 + (logPos - logMin) * yScale
- text = '1e%+03d' % logPos
+ text = "1e%+03d" % logPos
yield ((xPixel, yPixel), dataPos, text)
if step == 1:
ticks = list(self._frange(tickMin, tickMax, step))[:-1]
for logPos in ticks:
- dataOrigPos = 10 ** logPos
+ dataOrigPos = 10**logPos
for index in range(2, 10):
dataPos = dataOrigPos * index
if dataMin <= dataPos <= dataMax:
@@ -337,26 +399,34 @@ class PlotAxis(object):
xScale = (x1 - x0) / (dataMax - dataMin)
yScale = (y1 - y0) / (dataMax - dataMin)
- nbPixels = math.sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2)) / self.devicePixelRatio
+ nbPixels = (
+ math.sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2)) / self.devicePixelRatio
+ )
# Density of 1.3 label per 92 pixels
# i.e., 1.3 label per inch on a 92 dpi screen
- tickDensity = 1.3 / 92
+ tickDensity = 1.3 * self.devicePixelRatio / self.dotsPerInch
if not self.isTimeSeries:
- tickMin, tickMax, step, nbFrac = niceNumbersAdaptative(
- dataMin, dataMax, nbPixels, tickDensity)
-
- for dataPos in self._frange(tickMin, tickMax, step):
- if dataMin <= dataPos <= dataMax:
- xPixel = x0 + (dataPos - dataMin) * xScale
- yPixel = y0 + (dataPos - dataMin) * yScale
-
- if nbFrac == 0:
- text = '%g' % dataPos
- else:
- text = ('%.' + str(nbFrac) + 'f') % dataPos
- yield ((xPixel, yPixel), dataPos, text)
+ tickMin, tickMax, step, _ = niceNumbersAdaptative(
+ dataMin, dataMax, nbPixels, tickDensity
+ )
+
+ visibleTickPositions = [
+ pos
+ for pos in self._frange(tickMin, tickMax, step)
+ if dataMin <= pos <= dataMax
+ ]
+ self._tickFormatter.axis.set_view_interval(dataMin, dataMax)
+ self._tickFormatter.axis.set_data_interval(dataMin, dataMax)
+ texts = self._tickFormatter.format_ticks(visibleTickPositions)
+ self._orderAndOffsetText = self._tickFormatter.get_offset()
+
+ for dataPos, text in zip(visibleTickPositions, texts):
+ xPixel = x0 + (dataPos - dataMin) * xScale
+ yPixel = y0 + (dataPos - dataMin) * yScale
+ yield ((xPixel, yPixel), dataPos, text)
+
else:
# Time series
try:
@@ -366,24 +436,30 @@ class PlotAxis(object):
_logger.warning("Data range cannot be displayed with time axis")
return # Range is out of bound of the datetime
- tickDateTimes, spacing, unit = calcTicksAdaptive(
- dtMin, dtMax, nbPixels, tickDensity)
+ if bestUnit(
+ (dtMax - dtMin).total_seconds() == DtUnit.MICRO_SECONDS
+ ):
+ # Special case for micro seconds: Reduce tick density
+ tickDensity = 1.0 * self.devicePixelRatio / self.dotsPerInch
- for tickDateTime in tickDateTimes:
- if dtMin <= tickDateTime <= dtMax:
-
- dataPos = timestamp(tickDateTime)
- xPixel = x0 + (dataPos - dataMin) * xScale
- yPixel = y0 + (dataPos - dataMin) * yScale
-
- fmtStr = bestFormatString(spacing, unit)
- text = tickDateTime.strftime(fmtStr)
-
- yield ((xPixel, yPixel), dataPos, text)
+ tickDateTimes, spacing, unit = calcTicksAdaptive(
+ dtMin, dtMax, nbPixels, tickDensity
+ )
+ visibleDatetimes = tuple(
+ dt for dt in tickDateTimes if dtMin <= dt <= dtMax
+ )
+ ticks = formatDatetimes(visibleDatetimes, spacing, unit)
+
+ for tickDateTime, text in ticks.items():
+ dataPos = timestamp(tickDateTime)
+ xPixel = x0 + (dataPos - dataMin) * xScale
+ yPixel = y0 + (dataPos - dataMin) * yScale
+ yield ((xPixel, yPixel), dataPos, text)
# GLPlotFrame #################################################################
+
class GLPlotFrame(object):
"""Base class for rendering a 2D frame surrounded by axes."""
@@ -391,7 +467,7 @@ class GLPlotFrame(object):
_LINE_WIDTH = 1
_SHADERS = {
- 'vertex': """
+ "vertex": """
attribute vec2 position;
uniform mat4 matrix;
@@ -399,7 +475,7 @@ class GLPlotFrame(object):
gl_Position = matrix * vec4(position, 0.0, 1.0);
}
""",
- 'fragment': """
+ "fragment": """
uniform vec4 color;
uniform float tickFactor; /* = 1./tickLength or 0. for solid line */
@@ -410,15 +486,15 @@ class GLPlotFrame(object):
discard;
}
}
- """
+ """,
}
- _Margins = namedtuple('Margins', ('left', 'right', 'top', 'bottom'))
+ _Margins = namedtuple("Margins", ("left", "right", "top", "bottom"))
# Margins used when plot frame is not displayed
_NoDisplayMargins = _Margins(0, 0, 0, 0)
- def __init__(self, marginRatios, foregroundColor, gridColor):
+ def __init__(self, marginRatios, foregroundColor, gridColor, font: qt.QFont):
"""
:param List[float] marginRatios:
The ratios of margins around plot area for axis and labels.
@@ -427,6 +503,7 @@ class GLPlotFrame(object):
:type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0
:param gridColor: color used for grid lines.
:type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0
+ :param font: Font used by the axes label
"""
self._renderResources = None
@@ -439,10 +516,12 @@ class GLPlotFrame(object):
self.axes = [] # List of PlotAxis to be updated by subclasses
self._grid = False
- self._size = 0., 0.
- self._title = ''
+ self._size = 0.0, 0.0
+ self._title = ""
+ self._font: qt.QFont = font
- self._devicePixelRatio = 1.
+ self._devicePixelRatio = 1.0
+ self._dpi = 92
@property
def isDirty(self):
@@ -452,18 +531,19 @@ class GLPlotFrame(object):
GRID_NONE = 0
GRID_MAIN_TICKS = 1
GRID_SUB_TICKS = 2
- GRID_ALL_TICKS = (GRID_MAIN_TICKS + GRID_SUB_TICKS)
+ GRID_ALL_TICKS = GRID_MAIN_TICKS + GRID_SUB_TICKS
@property
def foregroundColor(self):
"""Color used for frame and labels"""
return self._foregroundColor
-
+
@foregroundColor.setter
def foregroundColor(self, color):
"""Color used for frame and labels"""
- assert len(color) == 4, \
- "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
+ assert len(color) == 4, "foregroundColor must have length 4, got {}".format(
+ len(self._foregroundColor)
+ )
if self._foregroundColor != color:
self._foregroundColor = color
for axis in self.axes:
@@ -474,20 +554,20 @@ class GLPlotFrame(object):
def gridColor(self):
"""Color used for frame and labels"""
return self._gridColor
-
+
@gridColor.setter
def gridColor(self, color):
"""Color used for frame and labels"""
- assert len(color) == 4, \
- "gridColor must have length 4, got {}".format(len(self._gridColor))
+ assert len(color) == 4, "gridColor must have length 4, got {}".format(
+ len(self._gridColor)
+ )
if self._gridColor != color:
self._gridColor = color
self._dirty()
@property
def marginRatios(self):
- """Plot margin ratios: (left, top, right, bottom) as 4 float in [0, 1].
- """
+ """Plot margin ratios: (left, top, right, bottom) as 4 float in [0, 1]."""
return self.__marginRatios
@marginRatios.setter
@@ -495,9 +575,9 @@ class GLPlotFrame(object):
ratios = tuple(float(v) for v in ratios)
assert len(ratios) == 4
for value in ratios:
- assert 0. <= value <= 1.
- assert ratios[0] + ratios[2] < 1.
- assert ratios[1] + ratios[3] < 1.
+ assert 0.0 <= value <= 1.0
+ assert ratios[0] + ratios[2] < 1.0
+ assert ratios[1] + ratios[3] < 1.0
if self.__marginRatios != ratios:
self.__marginRatios = ratios
@@ -511,10 +591,11 @@ class GLPlotFrame(object):
width, height = self.size
left, top, right, bottom = self.marginRatios
self.__marginsCache = self._Margins(
- left=int(left*width),
- right=int(right*width),
- top=int(top*height),
- bottom=int(bottom*height))
+ left=int(left * width),
+ right=int(right * width),
+ top=int(top * height),
+ bottom=int(bottom * height),
+ )
return self.__marginsCache
@property
@@ -528,6 +609,16 @@ class GLPlotFrame(object):
self._dirty()
@property
+ def dotsPerInch(self):
+ return self._dpi
+
+ @dotsPerInch.setter
+ def dotsPerInch(self, dpi):
+ if dpi != self._dpi:
+ self._dpi = dpi
+ self._dirty()
+
+ @property
def grid(self):
"""Grid display mode:
- 0: No grid.
@@ -538,8 +629,12 @@ class GLPlotFrame(object):
@grid.setter
def grid(self, grid):
- assert grid in (self.GRID_NONE, self.GRID_MAIN_TICKS,
- self.GRID_SUB_TICKS, self.GRID_ALL_TICKS)
+ assert grid in (
+ self.GRID_NONE,
+ self.GRID_MAIN_TICKS,
+ self.GRID_SUB_TICKS,
+ self.GRID_ALL_TICKS,
+ )
if grid != self._grid:
self._grid = grid
self._dirty()
@@ -595,16 +690,22 @@ class GLPlotFrame(object):
return []
elif self._grid == self.GRID_MAIN_TICKS:
+
def test(text):
return text is not None
+
elif self._grid == self.GRID_SUB_TICKS:
+
def test(text):
return text is None
+
elif self._grid == self.GRID_ALL_TICKS:
+
def test(_):
return True
+
else:
- logging.warning('Wrong grid mode: %d' % self._grid)
+ logging.warning("Wrong grid mode: %d" % self._grid)
return []
return self._buildGridVerticesWithTest(test)
@@ -626,25 +727,27 @@ class GLPlotFrame(object):
vertices = numpy.array(vertices, dtype=numpy.float32)
# Add main title
- xTitle = (self.size[0] + self.margins.left -
- self.margins.right) // 2
+ xTitle = (self.size[0] + self.margins.left - self.margins.right) // 2
yTitle = self.margins.top - self._TICK_LENGTH_IN_PIXELS
- labels.append(Text2D(text=self.title,
- color=self._foregroundColor,
- x=xTitle,
- y=yTitle,
- align=CENTER,
- valign=BOTTOM,
- devicePixelRatio=self.devicePixelRatio))
+ labels.append(
+ Text2D(
+ text=self.title,
+ font=self._font,
+ color=self._foregroundColor,
+ x=xTitle,
+ y=yTitle,
+ align=CENTER,
+ valign=BOTTOM,
+ devicePixelRatio=self.devicePixelRatio,
+ )
+ )
# grid
- gridVertices = numpy.array(self._buildGridVertices(),
- dtype=numpy.float32)
+ gridVertices = numpy.array(self._buildGridVertices(), dtype=numpy.float32)
self._renderResources = (vertices, gridVertices, labels)
- _program = Program(
- _SHADERS['vertex'], _SHADERS['fragment'], attrib0='position')
+ _program = Program(_SHADERS["vertex"], _SHADERS["fragment"], attrib0="position")
def render(self):
if self.margins == self._NoDisplayMargins:
@@ -664,22 +767,21 @@ class GLPlotFrame(object):
gl.glLineWidth(self._LINE_WIDTH)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- matProj.astype(numpy.float32))
- gl.glUniform4f(prog.uniforms['color'], *self._foregroundColor)
- gl.glUniform1f(prog.uniforms['tickFactor'], 0.)
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, matProj.astype(numpy.float32)
+ )
+ gl.glUniform4f(prog.uniforms["color"], *self._foregroundColor)
+ gl.glUniform1f(prog.uniforms["tickFactor"], 0.0)
- gl.glEnableVertexAttribArray(prog.attributes['position'])
- gl.glVertexAttribPointer(prog.attributes['position'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, vertices)
+ gl.glEnableVertexAttribArray(prog.attributes["position"])
+ gl.glVertexAttribPointer(
+ prog.attributes["position"], 2, gl.GL_FLOAT, gl.GL_FALSE, 0, vertices
+ )
gl.glDrawArrays(gl.GL_LINES, 0, len(vertices))
for label in labels:
- label.render(matProj)
+ label.render(matProj, self.dotsPerInch)
def renderGrid(self):
if self._grid == self.GRID_NONE:
@@ -698,25 +800,25 @@ class GLPlotFrame(object):
prog.use()
gl.glLineWidth(self._LINE_WIDTH)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- matProj.astype(numpy.float32))
- gl.glUniform4f(prog.uniforms['color'], *self._gridColor)
- gl.glUniform1f(prog.uniforms['tickFactor'], 0.) # 1/2.) # 1/tickLen
-
- gl.glEnableVertexAttribArray(prog.attributes['position'])
- gl.glVertexAttribPointer(prog.attributes['position'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, gridVertices)
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, matProj.astype(numpy.float32)
+ )
+ gl.glUniform4f(prog.uniforms["color"], *self._gridColor)
+ gl.glUniform1f(prog.uniforms["tickFactor"], 0.0) # 1/2.) # 1/tickLen
+
+ gl.glEnableVertexAttribArray(prog.attributes["position"])
+ gl.glVertexAttribPointer(
+ prog.attributes["position"], 2, gl.GL_FLOAT, gl.GL_FALSE, 0, gridVertices
+ )
gl.glDrawArrays(gl.GL_LINES, 0, len(gridVertices))
# GLPlotFrame2D ###############################################################
+
class GLPlotFrame2D(GLPlotFrame):
- def __init__(self, marginRatios, foregroundColor, gridColor):
+ def __init__(self, marginRatios, foregroundColor, gridColor, font: qt.QFont):
"""
:param List[float] marginRatios:
The ratios of margins around plot area for axis and labels.
@@ -725,38 +827,66 @@ class GLPlotFrame2D(GLPlotFrame):
:type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0
:param gridColor: color used for grid lines.
:type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0
-
+ :param font: Font used by the axes label
"""
- super(GLPlotFrame2D, self).__init__(marginRatios, foregroundColor, gridColor)
- self.axes.append(PlotAxis(self,
- tickLength=(0., -5.),
- foregroundColor=self._foregroundColor,
- labelAlign=CENTER, labelVAlign=TOP,
- titleAlign=CENTER, titleVAlign=TOP,
- titleRotate=0))
+ super(GLPlotFrame2D, self).__init__(
+ marginRatios, foregroundColor, gridColor, font
+ )
+ self._font = font
+
+ self.axes.append(
+ PlotAxis(
+ self,
+ tickLength=(0.0, -5.0),
+ foregroundColor=self._foregroundColor,
+ labelAlign=CENTER,
+ labelVAlign=TOP,
+ orderOffsetAlign=RIGHT,
+ orderOffsetVAlign=TOP,
+ titleAlign=CENTER,
+ titleVAlign=TOP,
+ titleRotate=0,
+ font=self._font,
+ )
+ )
self._x2AxisCoords = ()
- self.axes.append(PlotAxis(self,
- tickLength=(5., 0.),
- foregroundColor=self._foregroundColor,
- labelAlign=RIGHT, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=BOTTOM,
- titleRotate=ROTATE_270))
+ self.axes.append(
+ PlotAxis(
+ self,
+ tickLength=(5.0, 0.0),
+ foregroundColor=self._foregroundColor,
+ labelAlign=RIGHT,
+ labelVAlign=CENTER,
+ orderOffsetAlign=LEFT,
+ orderOffsetVAlign=BOTTOM,
+ titleAlign=CENTER,
+ titleVAlign=BOTTOM,
+ titleRotate=ROTATE_270,
+ font=self._font,
+ )
+ )
- self._y2Axis = PlotAxis(self,
- tickLength=(-5., 0.),
- foregroundColor=self._foregroundColor,
- labelAlign=LEFT, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=TOP,
- titleRotate=ROTATE_270)
+ self._y2Axis = PlotAxis(
+ self,
+ tickLength=(-5.0, 0.0),
+ foregroundColor=self._foregroundColor,
+ labelAlign=LEFT,
+ labelVAlign=CENTER,
+ orderOffsetAlign=RIGHT,
+ orderOffsetVAlign=BOTTOM,
+ titleAlign=CENTER,
+ titleVAlign=TOP,
+ titleRotate=ROTATE_270,
+ font=self._font,
+ )
self._isYAxisInverted = False
- self._dataRanges = {
- 'x': (1., 100.), 'y': (1., 100.), 'y2': (1., 100.)}
+ self._dataRanges = {"x": (1.0, 100.0), "y": (1.0, 100.0), "y2": (1.0, 100.0)}
- self._baseVectors = (1., 0.), (0., 1.)
+ self._baseVectors = (1.0, 0.0), (0.0, 1.0)
self._transformedDataRanges = None
self._transformedDataProjMat = None
@@ -771,10 +901,12 @@ class GLPlotFrame2D(GLPlotFrame):
@property
def isDirty(self):
"""True if it need to refresh graphic rendering, False otherwise."""
- return (super(GLPlotFrame2D, self).isDirty or
- self._transformedDataRanges is None or
- self._transformedDataProjMat is None or
- self._transformedDataY2ProjMat is None)
+ return (
+ super(GLPlotFrame2D, self).isDirty
+ or self._transformedDataRanges is None
+ or self._transformedDataProjMat is None
+ or self._transformedDataY2ProjMat is None
+ )
@property
def xAxis(self):
@@ -815,7 +947,7 @@ class GLPlotFrame2D(GLPlotFrame):
self._isYAxisInverted = value
self._dirty()
- DEFAULT_BASE_VECTORS = (1., 0.), (0., 1.)
+ DEFAULT_BASE_VECTORS = (1.0, 0.0), (0.0, 1.0)
"""Values of baseVectors for orthogonal axes."""
@property
@@ -835,10 +967,9 @@ class GLPlotFrame2D(GLPlotFrame):
(xx, xy), (yx, yy) = baseVectors
vectors = (float(xx), float(xy)), (float(yx), float(yy))
- det = (vectors[0][0] * vectors[1][1] - vectors[1][0] * vectors[0][1])
- if det == 0.:
- raise ValueError("Singular matrix for base vectors: " +
- str(vectors))
+ det = vectors[0][0] * vectors[1][1] - vectors[1][0] * vectors[0][1]
+ if det == 0.0:
+ raise ValueError("Singular matrix for base vectors: " + str(vectors))
if vectors != self._baseVectors:
self._baseVectors = vectors
@@ -870,9 +1001,9 @@ class GLPlotFrame2D(GLPlotFrame):
Type: ((xMin, xMax), (yMin, yMax), (y2Min, y2Max))
"""
- return self._DataRanges(self._dataRanges['x'],
- self._dataRanges['y'],
- self._dataRanges['y2'])
+ return self._DataRanges(
+ self._dataRanges["x"], self._dataRanges["y"], self._dataRanges["y2"]
+ )
def setDataRanges(self, x=None, y=None, y2=None):
"""Set data range over each axes.
@@ -885,22 +1016,25 @@ class GLPlotFrame2D(GLPlotFrame):
:param y2: (min, max) data range over Y2 axis
"""
if x is not None:
- self._dataRanges['x'] = checkAxisLimits(
- x[0], x[1], self.xAxis.isLog, name='x')
+ self._dataRanges["x"] = checkAxisLimits(
+ x[0], x[1], self.xAxis.isLog, name="x"
+ )
if y is not None:
- self._dataRanges['y'] = checkAxisLimits(
- y[0], y[1], self.yAxis.isLog, name='y')
+ self._dataRanges["y"] = checkAxisLimits(
+ y[0], y[1], self.yAxis.isLog, name="y"
+ )
if y2 is not None:
- self._dataRanges['y2'] = checkAxisLimits(
- y2[0], y2[1], self.y2Axis.isLog, name='y2')
+ self._dataRanges["y2"] = checkAxisLimits(
+ y2[0], y2[1], self.y2Axis.isLog, name="y2"
+ )
- self.xAxis.dataRange = self._dataRanges['x']
- self.yAxis.dataRange = self._dataRanges['y']
- self.y2Axis.dataRange = self._dataRanges['y2']
+ self.xAxis.dataRange = self._dataRanges["x"]
+ self.yAxis.dataRange = self._dataRanges["y"]
+ self.y2Axis.dataRange = self._dataRanges["y2"]
- _DataRanges = namedtuple('dataRanges', ('x', 'y', 'y2'))
+ _DataRanges = namedtuple("dataRanges", ("x", "y", "y2"))
@property
def transformedDataRanges(self):
@@ -916,39 +1050,40 @@ class GLPlotFrame2D(GLPlotFrame):
try:
xMin = math.log10(xMin)
except ValueError:
- _logger.info('xMin: warning log10(%f)', xMin)
- xMin = 0.
+ _logger.info("xMin: warning log10(%f)", xMin)
+ xMin = 0.0
try:
xMax = math.log10(xMax)
except ValueError:
- _logger.info('xMax: warning log10(%f)', xMax)
- xMax = 0.
+ _logger.info("xMax: warning log10(%f)", xMax)
+ xMax = 0.0
if self.yAxis.isLog:
try:
yMin = math.log10(yMin)
except ValueError:
- _logger.info('yMin: warning log10(%f)', yMin)
- yMin = 0.
+ _logger.info("yMin: warning log10(%f)", yMin)
+ yMin = 0.0
try:
yMax = math.log10(yMax)
except ValueError:
- _logger.info('yMax: warning log10(%f)', yMax)
- yMax = 0.
+ _logger.info("yMax: warning log10(%f)", yMax)
+ yMax = 0.0
try:
y2Min = math.log10(y2Min)
except ValueError:
- _logger.info('yMin: warning log10(%f)', y2Min)
- y2Min = 0.
+ _logger.info("yMin: warning log10(%f)", y2Min)
+ y2Min = 0.0
try:
y2Max = math.log10(y2Max)
except ValueError:
- _logger.info('yMax: warning log10(%f)', y2Max)
- y2Max = 0.
+ _logger.info("yMax: warning log10(%f)", y2Max)
+ y2Max = 0.0
self._transformedDataRanges = self._DataRanges(
- (xMin, xMax), (yMin, yMax), (y2Min, y2Max))
+ (xMin, xMax), (yMin, yMax), (y2Min, y2Max)
+ )
return self._transformedDataRanges
@@ -991,10 +1126,9 @@ class GLPlotFrame2D(GLPlotFrame):
@staticmethod
def __applyLog(
- data: Union[float, numpy.ndarray],
- isLog: bool
+ data: Union[float, numpy.ndarray], isLog: bool
) -> Optional[Union[float, numpy.ndarray]]:
- """Apply log to data filtering out """
+ """Apply log to data filtering out"""
if not isLog:
return data
@@ -1006,13 +1140,12 @@ class GLPlotFrame2D(GLPlotFrame):
data = numpy.array(data, copy=True, dtype=numpy.float64)
data[isBelowMin] = numpy.nan
- with numpy.errstate(divide='ignore'):
+ with numpy.errstate(divide="ignore"):
return numpy.log10(data)
- def dataToPixel(self, x, y, axis='left'):
- """Convert data coordinate to widget pixel coordinate.
- """
- assert axis in ('left', 'right')
+ def dataToPixel(self, x, y, axis="left"):
+ """Convert data coordinate to widget pixel coordinate."""
+ assert axis in ("left", "right")
trBounds = self.transformedDataRanges
@@ -1034,13 +1167,12 @@ class GLPlotFrame2D(GLPlotFrame):
plotWidth, plotHeight = self.plotSize
- xPixel = (self.margins.left +
- plotWidth * (xDataTr - trBounds.x[0]) /
- (trBounds.x[1] - trBounds.x[0]))
+ xPixel = self.margins.left + plotWidth * (xDataTr - trBounds.x[0]) / (
+ trBounds.x[1] - trBounds.x[0]
+ )
usedAxis = trBounds.y if axis == "left" else trBounds.y2
- yOffset = (plotHeight * (yDataTr - usedAxis[0]) /
- (usedAxis[1] - usedAxis[0]))
+ yOffset = plotHeight * (yDataTr - usedAxis[0]) / (usedAxis[1] - usedAxis[0])
if self.isYAxisInverted:
yPixel = self.margins.top + yOffset
@@ -1048,8 +1180,12 @@ class GLPlotFrame2D(GLPlotFrame):
yPixel = self.size[1] - self.margins.bottom - yOffset
return (
- int(xPixel) if isinstance(xPixel, numbers.Real) else xPixel.astype(numpy.int64),
- int(yPixel) if isinstance(yPixel, numbers.Real) else yPixel.astype(numpy.int64),
+ int(xPixel)
+ if isinstance(xPixel, numbers.Real)
+ else xPixel.astype(numpy.int64),
+ int(yPixel)
+ if isinstance(yPixel, numbers.Real)
+ else yPixel.astype(numpy.int64),
)
def pixelToData(self, x, y, axis="left"):
@@ -1105,8 +1241,7 @@ class GLPlotFrame2D(GLPlotFrame):
if axis == self.xAxis:
vertices.append((xPixel, self.margins.top))
elif axis == self.yAxis:
- vertices.append((self.size[0] - self.margins.right,
- yPixel))
+ vertices.append((self.size[0] - self.margins.right, yPixel))
else: # axis == self.y2Axis
vertices.append((self.margins.left, yPixel))
@@ -1115,28 +1250,33 @@ class GLPlotFrame2D(GLPlotFrame):
plotLeft, plotTop = self.plotOrigin
plotWidth, plotHeight = self.plotSize
- corners = [(plotLeft, plotTop),
- (plotLeft, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop)]
+ corners = [
+ (plotLeft, plotTop),
+ (plotLeft, plotTop + plotHeight),
+ (plotLeft + plotWidth, plotTop + plotHeight),
+ (plotLeft + plotWidth, plotTop),
+ ]
for axis in self.axes:
if axis == self.xAxis:
- cornersInData = numpy.array([
- self.pixelToData(x, y) for (x, y) in corners])
- borders = ((cornersInData[0], cornersInData[3]), # top
- (cornersInData[1], cornersInData[0]), # left
- (cornersInData[3], cornersInData[2])) # right
+ cornersInData = numpy.array(
+ [self.pixelToData(x, y) for (x, y) in corners]
+ )
+ borders = (
+ (cornersInData[0], cornersInData[3]), # top
+ (cornersInData[1], cornersInData[0]), # left
+ (cornersInData[3], cornersInData[2]),
+ ) # right
for (xPixel, yPixel), data, text in axis.ticks:
if test(text):
for (x0, y0), (x1, y1) in borders:
if min(x0, x1) <= data < max(x0, x1):
- yIntersect = (data - x0) * \
- (y1 - y0) / (x1 - x0) + y0
+ yIntersect = (data - x0) * (y1 - y0) / (
+ x1 - x0
+ ) + y0
- pixelPos = self.dataToPixel(
- data, yIntersect)
+ pixelPos = self.dataToPixel(data, yIntersect)
if pixelPos is not None:
vertices.append((xPixel, yPixel))
vertices.append(pixelPos)
@@ -1144,32 +1284,38 @@ class GLPlotFrame2D(GLPlotFrame):
else: # y or y2 axes
if axis == self.yAxis:
- axis_name = 'left'
- cornersInData = numpy.array([
- self.pixelToData(x, y) for (x, y) in corners])
+ axis_name = "left"
+ cornersInData = numpy.array(
+ [self.pixelToData(x, y) for (x, y) in corners]
+ )
borders = (
(cornersInData[3], cornersInData[2]), # right
(cornersInData[0], cornersInData[3]), # top
- (cornersInData[2], cornersInData[1])) # bottom
+ (cornersInData[2], cornersInData[1]),
+ ) # bottom
else: # axis == self.y2Axis
- axis_name = 'right'
- corners = numpy.array([self.pixelToData(
- x, y, axis='right') for (x, y) in corners])
+ axis_name = "right"
+ corners = numpy.array(
+ [self.pixelToData(x, y, axis="right") for (x, y) in corners]
+ )
borders = (
(cornersInData[1], cornersInData[0]), # left
(cornersInData[0], cornersInData[3]), # top
- (cornersInData[2], cornersInData[1])) # bottom
+ (cornersInData[2], cornersInData[1]),
+ ) # bottom
for (xPixel, yPixel), data, text in axis.ticks:
if test(text):
for (x0, y0), (x1, y1) in borders:
if min(y0, y1) <= data < max(y0, y1):
- xIntersect = (data - y0) * \
- (x1 - x0) / (y1 - y0) + x0
+ xIntersect = (data - y0) * (x1 - x0) / (
+ y1 - y0
+ ) + x0
pixelPos = self.dataToPixel(
- xIntersect, data, axis=axis_name)
+ xIntersect, data, axis=axis_name
+ )
if pixelPos is not None:
vertices.append((xPixel, yPixel))
vertices.append(pixelPos)
@@ -1180,26 +1326,47 @@ class GLPlotFrame2D(GLPlotFrame):
def _buildVerticesAndLabels(self):
width, height = self.size
- xCoords = (self.margins.left - 0.5,
- width - self.margins.right + 0.5)
- yCoords = (height - self.margins.bottom + 0.5,
- self.margins.top - 0.5)
+ xCoords = (self.margins.left - 0.5, width - self.margins.right + 0.5)
+ yCoords = (height - self.margins.bottom + 0.5, self.margins.top - 0.5)
- self.axes[0].displayCoords = ((xCoords[0], yCoords[0]),
- (xCoords[1], yCoords[0]))
+ self.axes[0].displayCoords = (
+ (xCoords[0], yCoords[0]),
+ (xCoords[1], yCoords[0]),
+ )
- self._x2AxisCoords = ((xCoords[0], yCoords[1]),
- (xCoords[1], yCoords[1]))
+ self._x2AxisCoords = ((xCoords[0], yCoords[1]), (xCoords[1], yCoords[1]))
+
+ # Set order&offset anchor **before** handling Y axis inversion
+ fontPixelSize = self._font.pixelSize()
+ if fontPixelSize == -1:
+ fontPixelSize = self._font.pointSizeF() / 72.0 * self.dotsPerInch
+
+ self.axes[0].orderOffetAnchor = (
+ xCoords[1],
+ yCoords[0] + fontPixelSize * 1.2,
+ )
+ self.axes[1].orderOffetAnchor = (
+ xCoords[0],
+ yCoords[1] - 4 * self.devicePixelRatio,
+ )
+ self._y2Axis.orderOffetAnchor = (
+ xCoords[1],
+ yCoords[1] - 4 * self.devicePixelRatio,
+ )
if self.isYAxisInverted:
# Y axes are inverted, axes coordinates are inverted
yCoords = yCoords[1], yCoords[0]
- self.axes[1].displayCoords = ((xCoords[0], yCoords[0]),
- (xCoords[0], yCoords[1]))
+ self.axes[1].displayCoords = (
+ (xCoords[0], yCoords[0]),
+ (xCoords[0], yCoords[1]),
+ )
- self._y2Axis.displayCoords = ((xCoords[1], yCoords[0]),
- (xCoords[1], yCoords[1]))
+ self._y2Axis.displayCoords = (
+ (xCoords[1], yCoords[0]),
+ (xCoords[1], yCoords[1]),
+ )
super(GLPlotFrame2D, self)._buildVerticesAndLabels()
@@ -1211,8 +1378,7 @@ class GLPlotFrame2D(GLPlotFrame):
if not self.isY2Axis:
extraVertices += self._y2Axis.displayCoords
- extraVertices = numpy.array(
- extraVertices, copy=False, dtype=numpy.float32)
+ extraVertices = numpy.array(extraVertices, copy=False, dtype=numpy.float32)
vertices = numpy.append(vertices, extraVertices, axis=0)
self._renderResources = (vertices, gridVertices, labels)
@@ -1225,8 +1391,9 @@ class GLPlotFrame2D(GLPlotFrame):
@foregroundColor.setter
def foregroundColor(self, color):
"""Color used for frame and labels"""
- assert len(color) == 4, \
- "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
+ assert len(color) == 4, "foregroundColor must have length 4, got {}".format(
+ len(self._foregroundColor)
+ )
if self._foregroundColor != color:
self._y2Axis.foregroundColor = color
- GLPlotFrame.foregroundColor.fset(self, color) # call parent property
+ GLPlotFrame.foregroundColor.fset(self, color) # call parent property
diff --git a/src/silx/gui/plot/backends/glutils/GLPlotImage.py b/src/silx/gui/plot/backends/glutils/GLPlotImage.py
index 8353911..0973c47 100644
--- a/src/silx/gui/plot/backends/glutils/GLPlotImage.py
+++ b/src/silx/gui/plot/backends/glutils/GLPlotImage.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2014-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 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
@@ -33,8 +33,6 @@ __date__ = "03/04/2017"
import math
import numpy
-from silx.math.combo import min_max
-
from ...._glutils import gl, Program, Texture
from ..._utils import FLOAT32_MINPOS
from .GLSupport import mat4Translate, mat4Scale
@@ -64,29 +62,28 @@ class _GLPlotData2D(GLPlotItem):
@property
def xMin(self):
ox, sx = self.origin[0], self.scale[0]
- return ox if sx >= 0. else ox + sx * self.data.shape[1]
+ return ox if sx >= 0.0 else ox + sx * self.data.shape[1]
@property
def yMin(self):
oy, sy = self.origin[1], self.scale[1]
- return oy if sy >= 0. else oy + sy * self.data.shape[0]
+ return oy if sy >= 0.0 else oy + sy * self.data.shape[0]
@property
def xMax(self):
ox, sx = self.origin[0], self.scale[0]
- return ox + sx * self.data.shape[1] if sx >= 0. else ox
+ return ox + sx * self.data.shape[1] if sx >= 0.0 else ox
@property
def yMax(self):
oy, sy = self.origin[1], self.scale[1]
- return oy + sy * self.data.shape[0] if sy >= 0. else oy
+ return oy + sy * self.data.shape[0] if sy >= 0.0 else oy
class GLPlotColormap(_GLPlotData2D):
-
_SHADERS = {
- 'linear': {
- 'vertex': """
+ "linear": {
+ "vertex": """
#version 120
uniform mat4 matrix;
@@ -100,14 +97,14 @@ class GLPlotColormap(_GLPlotData2D):
gl_Position = matrix * vec4(position, 0.0, 1.0);
}
""",
- 'fragTransform': """
+ "fragTransform": """
vec2 textureCoords(void) {
return coords;
}
- """},
-
- 'log': {
- 'vertex': """
+ """,
+ },
+ "log": {
+ "vertex": """
#version 120
attribute vec2 position;
@@ -131,7 +128,7 @@ class GLPlotColormap(_GLPlotData2D):
gl_Position = matrix * dataPos;
}
""",
- 'fragTransform': """
+ "fragTransform": """
uniform bvec2 isLog;
uniform vec2 bounds_oneOverRange;
uniform vec2 bounds_originOverRange;
@@ -147,9 +144,9 @@ class GLPlotColormap(_GLPlotData2D):
return pos * bounds_oneOverRange - bounds_originOverRange;
// TODO texture coords in range different from [0, 1]
}
- """},
-
- 'fragment': """
+ """,
+ },
+ "fragment": """
#version 120
/* isnan declaration for compatibility with GLSL 1.20 */
@@ -209,7 +206,7 @@ class GLPlotColormap(_GLPlotData2D):
}
gl_FragColor.a *= alpha;
}
- """
+ """,
}
_DATA_TEX_UNIT = 0
@@ -223,21 +220,32 @@ class GLPlotColormap(_GLPlotData2D):
numpy.dtype(numpy.uint8): gl.GL_R8,
}
- _linearProgram = Program(_SHADERS['linear']['vertex'],
- _SHADERS['fragment'] %
- _SHADERS['linear']['fragTransform'],
- attrib0='position')
-
- _logProgram = Program(_SHADERS['log']['vertex'],
- _SHADERS['fragment'] %
- _SHADERS['log']['fragTransform'],
- attrib0='position')
-
- SUPPORTED_NORMALIZATIONS = 'linear', 'log', 'sqrt', 'gamma', 'arcsinh'
-
- def __init__(self, data, origin, scale,
- colormap, normalization='linear', gamma=0., cmapRange=None,
- alpha=1.0, nancolor=(1., 1., 1., 0.)):
+ _linearProgram = Program(
+ _SHADERS["linear"]["vertex"],
+ _SHADERS["fragment"] % _SHADERS["linear"]["fragTransform"],
+ attrib0="position",
+ )
+
+ _logProgram = Program(
+ _SHADERS["log"]["vertex"],
+ _SHADERS["fragment"] % _SHADERS["log"]["fragTransform"],
+ attrib0="position",
+ )
+
+ SUPPORTED_NORMALIZATIONS = "linear", "log", "sqrt", "gamma", "arcsinh"
+
+ def __init__(
+ self,
+ data,
+ origin,
+ scale,
+ colormap,
+ normalization="linear",
+ gamma=0.0,
+ cmapRange=None,
+ alpha=1.0,
+ nancolor=(1.0, 1.0, 1.0, 0.0),
+ ):
"""Create a 2D colormap
:param data: The 2D scalar data array to display
@@ -267,10 +275,10 @@ class GLPlotColormap(_GLPlotData2D):
self.colormap = numpy.array(colormap, copy=False)
self.normalization = normalization
self.gamma = gamma
- self._cmapRange = (1., 10.) # Colormap range
+ self._cmapRange = (1.0, 10.0) # Colormap range
self.cmapRange = cmapRange # Update _cmapRange
- self._alpha = numpy.clip(alpha, 0., 1.)
- self._nancolor = numpy.clip(nancolor, 0., 1.)
+ self._alpha = numpy.clip(alpha, 0.0, 1.0)
+ self._nancolor = numpy.clip(nancolor, 0.0, 1.0)
self._cmap_texture = None
self._texture = None
@@ -287,15 +295,14 @@ class GLPlotColormap(_GLPlotData2D):
self._textureIsDirty = False
def isInitialized(self):
- return (self._cmap_texture is not None or
- self._texture is not None)
+ return self._cmap_texture is not None or self._texture is not None
@property
def cmapRange(self):
- if self.normalization == 'log':
- assert self._cmapRange[0] > 0. and self._cmapRange[1] > 0.
- elif self.normalization == 'sqrt':
- assert self._cmapRange[0] >= 0. and self._cmapRange[1] >= 0.
+ if self.normalization == "log":
+ assert self._cmapRange[0] > 0.0 and self._cmapRange[1] > 0.0
+ elif self.normalization == "sqrt":
+ assert self._cmapRange[0] >= 0.0 and self._cmapRange[1] >= 0.0
return self._cmapRange
@cmapRange.setter
@@ -314,8 +321,7 @@ class GLPlotColormap(_GLPlotData2D):
self.data = data
if self._texture is not None:
- if (self.data.shape != oldData.shape or
- self.data.dtype != oldData.dtype):
+ if self.data.shape != oldData.shape or self.data.dtype != oldData.dtype:
self.discard()
else:
self._textureIsDirty = True
@@ -324,74 +330,77 @@ class GLPlotColormap(_GLPlotData2D):
if self._cmap_texture is None:
# TODO share cmap texture accross Images
# put all cmaps in one texture
- colormap = numpy.empty((16, 256, self.colormap.shape[1]),
- dtype=self.colormap.dtype)
+ colormap = numpy.empty(
+ (16, 256, self.colormap.shape[1]), dtype=self.colormap.dtype
+ )
colormap[:] = self.colormap
format_ = gl.GL_RGBA if colormap.shape[-1] == 4 else gl.GL_RGB
- self._cmap_texture = Texture(internalFormat=format_,
- data=colormap,
- format_=format_,
- texUnit=self._CMAP_TEX_UNIT,
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=(gl.GL_CLAMP_TO_EDGE,
- gl.GL_CLAMP_TO_EDGE))
+ self._cmap_texture = Texture(
+ internalFormat=format_,
+ data=colormap,
+ format_=format_,
+ texUnit=self._CMAP_TEX_UNIT,
+ minFilter=gl.GL_NEAREST,
+ magFilter=gl.GL_NEAREST,
+ wrap=(gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE),
+ )
self._cmap_texture.prepare()
if self._texture is None:
internalFormat = self._INTERNAL_FORMATS[self.data.dtype]
- self._texture = Image(internalFormat,
- self.data,
- format_=gl.GL_RED,
- texUnit=self._DATA_TEX_UNIT)
+ self._texture = Image(
+ internalFormat,
+ self.data,
+ format_=gl.GL_RED,
+ texUnit=self._DATA_TEX_UNIT,
+ )
elif self._textureIsDirty:
self._textureIsDirty = True
self._texture.updateAll(format_=gl.GL_RED, data=self.data)
def _setCMap(self, prog):
dataMin, dataMax = self.cmapRange # If log, it is stricly positive
- param = 0.
+ param = 0.0
if self.data.dtype in (numpy.uint16, numpy.uint8):
# Using unsigned int as normalized integer in OpenGL
# So revert normalization in the shader
dataScale = float(numpy.iinfo(self.data.dtype).max)
else:
- dataScale = 1.
+ dataScale = 1.0
- if self.normalization == 'log':
+ if self.normalization == "log":
dataMin = math.log10(dataMin)
dataMax = math.log10(dataMax)
normID = 1
- elif self.normalization == 'sqrt':
+ elif self.normalization == "sqrt":
dataMin = math.sqrt(dataMin)
dataMax = math.sqrt(dataMax)
normID = 2
- elif self.normalization == 'gamma':
+ elif self.normalization == "gamma":
# Keep dataMin, dataMax as is
param = self.gamma
normID = 3
- elif self.normalization == 'arcsinh':
+ elif self.normalization == "arcsinh":
dataMin = numpy.arcsinh(dataMin)
dataMax = numpy.arcsinh(dataMax)
normID = 4
else: # Linear and fallback
normID = 0
- gl.glUniform1f(prog.uniforms['data_scale'], dataScale)
- gl.glUniform1i(prog.uniforms['cmap_texture'],
- self._cmap_texture.texUnit)
- gl.glUniform1i(prog.uniforms['cmap_normalization'], normID)
- gl.glUniform1f(prog.uniforms['cmap_parameter'], param)
- gl.glUniform1f(prog.uniforms['cmap_min'], dataMin)
+ gl.glUniform1f(prog.uniforms["data_scale"], dataScale)
+ gl.glUniform1i(prog.uniforms["cmap_texture"], self._cmap_texture.texUnit)
+ gl.glUniform1i(prog.uniforms["cmap_normalization"], normID)
+ gl.glUniform1f(prog.uniforms["cmap_parameter"], param)
+ gl.glUniform1f(prog.uniforms["cmap_min"], dataMin)
if dataMax > dataMin:
- oneOverRange = 1. / (dataMax - dataMin)
+ oneOverRange = 1.0 / (dataMax - dataMin)
else:
- oneOverRange = 0. # Fall-back
- gl.glUniform1f(prog.uniforms['cmap_oneOverRange'], oneOverRange)
+ oneOverRange = 0.0 # Fall-back
+ gl.glUniform1f(prog.uniforms["cmap_oneOverRange"], oneOverRange)
- gl.glUniform4f(prog.uniforms['nancolor'], *self._nancolor)
+ gl.glUniform4f(prog.uniforms["nancolor"], *self._nancolor)
self._cmap_texture.bind()
@@ -405,21 +414,25 @@ class GLPlotColormap(_GLPlotData2D):
prog = self._linearProgram
prog.use()
- gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
+ gl.glUniform1i(prog.uniforms["data"], self._DATA_TEX_UNIT)
- mat = numpy.dot(numpy.dot(context.matrix,
- mat4Translate(*self.origin)),
- mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
+ mat = numpy.dot(
+ numpy.dot(context.matrix, mat4Translate(*self.origin)),
+ mat4Scale(*self.scale),
+ )
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, mat.astype(numpy.float32)
+ )
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
+ gl.glUniform1f(prog.uniforms["alpha"], self.alpha)
self._setCMap(prog)
- self._texture.render(prog.attributes['position'],
- prog.attributes['texCoords'],
- self._DATA_TEX_UNIT)
+ self._texture.render(
+ prog.attributes["position"],
+ prog.attributes["texCoords"],
+ self._DATA_TEX_UNIT,
+ )
def _renderLog10(self, context):
"""Perform rendering when one axis has log scale
@@ -427,8 +440,9 @@ class GLPlotColormap(_GLPlotData2D):
:param RenderContext context: Rendering information
"""
xMin, yMin = self.xMin, self.yMin
- if ((context.isXLog and xMin < FLOAT32_MINPOS) or
- (context.isYLog and yMin < FLOAT32_MINPOS)):
+ if (context.isXLog and xMin < FLOAT32_MINPOS) or (
+ context.isYLog and yMin < FLOAT32_MINPOS
+ ):
# Do not render images that are partly or totally <= 0
return
@@ -439,27 +453,33 @@ class GLPlotColormap(_GLPlotData2D):
ox, oy = self.origin
- gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
+ gl.glUniform1i(prog.uniforms["data"], self._DATA_TEX_UNIT)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- context.matrix.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, context.matrix.astype(numpy.float32)
+ )
mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matOffset"], 1, gl.GL_TRUE, mat.astype(numpy.float32)
+ )
- gl.glUniform2i(prog.uniforms['isLog'], context.isXLog, context.isYLog)
+ gl.glUniform2i(prog.uniforms["isLog"], context.isXLog, context.isYLog)
ex = ox + self.scale[0] * self.data.shape[1]
ey = oy + self.scale[1] * self.data.shape[0]
- xOneOverRange = 1. / (ex - ox)
- yOneOverRange = 1. / (ey - oy)
- gl.glUniform2f(prog.uniforms['bounds_originOverRange'],
- ox * xOneOverRange, oy * yOneOverRange)
- gl.glUniform2f(prog.uniforms['bounds_oneOverRange'],
- xOneOverRange, yOneOverRange)
+ xOneOverRange = 1.0 / (ex - ox)
+ yOneOverRange = 1.0 / (ey - oy)
+ gl.glUniform2f(
+ prog.uniforms["bounds_originOverRange"],
+ ox * xOneOverRange,
+ oy * yOneOverRange,
+ )
+ gl.glUniform2f(
+ prog.uniforms["bounds_oneOverRange"], xOneOverRange, yOneOverRange
+ )
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
+ gl.glUniform1f(prog.uniforms["alpha"], self.alpha)
self._setCMap(prog)
@@ -469,20 +489,19 @@ class GLPlotColormap(_GLPlotData2D):
raise RuntimeError("No texture, discard has already been called")
if len(tiles) > 1:
raise NotImplementedError(
- "Image over multiple textures not supported with log scale")
+ "Image over multiple textures not supported with log scale"
+ )
texture, vertices, info = tiles[0]
texture.bind(self._DATA_TEX_UNIT)
- posAttrib = prog.attributes['position']
+ posAttrib = prog.attributes["position"]
stride = vertices.shape[-1] * vertices.itemsize
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, vertices)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, stride, vertices
+ )
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
@@ -503,11 +522,11 @@ class GLPlotColormap(_GLPlotData2D):
# image #######################################################################
-class GLPlotRGBAImage(_GLPlotData2D):
+class GLPlotRGBAImage(_GLPlotData2D):
_SHADERS = {
- 'linear': {
- 'vertex': """
+ "linear": {
+ "vertex": """
#version 120
attribute vec2 position;
@@ -521,7 +540,7 @@ class GLPlotRGBAImage(_GLPlotData2D):
coords = texCoords;
}
""",
- 'fragment': """
+ "fragment": """
#version 120
uniform sampler2D tex;
@@ -533,10 +552,10 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl_FragColor = texture2D(tex, coords);
gl_FragColor.a *= alpha;
}
- """},
-
- 'log': {
- 'vertex': """
+ """,
+ },
+ "log": {
+ "vertex": """
#version 120
attribute vec2 position;
@@ -560,7 +579,7 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl_Position = matrix * dataPos;
}
""",
- 'fragment': """
+ "fragment": """
#version 120
uniform sampler2D tex;
@@ -587,22 +606,25 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl_FragColor = texture2D(tex, textureCoords());
gl_FragColor.a *= alpha;
}
- """}
+ """,
+ },
}
_DATA_TEX_UNIT = 0
- _SUPPORTED_DTYPES = (numpy.dtype(numpy.float32),
- numpy.dtype(numpy.uint8),
- numpy.dtype(numpy.uint16))
+ _SUPPORTED_DTYPES = (
+ numpy.dtype(numpy.float32),
+ numpy.dtype(numpy.uint8),
+ numpy.dtype(numpy.uint16),
+ )
- _linearProgram = Program(_SHADERS['linear']['vertex'],
- _SHADERS['linear']['fragment'],
- attrib0='position')
+ _linearProgram = Program(
+ _SHADERS["linear"]["vertex"], _SHADERS["linear"]["fragment"], attrib0="position"
+ )
- _logProgram = Program(_SHADERS['log']['vertex'],
- _SHADERS['log']['fragment'],
- attrib0='position')
+ _logProgram = Program(
+ _SHADERS["log"]["vertex"], _SHADERS["log"]["fragment"], attrib0="position"
+ )
def __init__(self, data, origin, scale, alpha):
"""Create a 2D RGB(A) image from data
@@ -621,7 +643,7 @@ class GLPlotRGBAImage(_GLPlotData2D):
super(GLPlotRGBAImage, self).__init__(data, origin, scale)
self._texture = None
self._textureIsDirty = False
- self._alpha = numpy.clip(alpha, 0., 1.)
+ self._alpha = numpy.clip(alpha, 0.0, 1.0)
@property
def alpha(self):
@@ -649,17 +671,16 @@ class GLPlotRGBAImage(_GLPlotData2D):
def prepare(self):
if self._texture is None:
- formatName = 'GL_RGBA' if self.data.shape[2] == 4 else 'GL_RGB'
+ formatName = "GL_RGBA" if self.data.shape[2] == 4 else "GL_RGB"
format_ = getattr(gl, formatName)
if self.data.dtype == numpy.uint16:
- formatName += '16' # Use sized internal format for uint16
+ formatName += "16" # Use sized internal format for uint16
internalFormat = getattr(gl, formatName)
- self._texture = Image(internalFormat,
- self.data,
- format_=format_,
- texUnit=self._DATA_TEX_UNIT)
+ self._texture = Image(
+ internalFormat, self.data, format_=format_, texUnit=self._DATA_TEX_UNIT
+ )
elif self._textureIsDirty:
self._textureIsDirty = False
@@ -677,18 +698,23 @@ class GLPlotRGBAImage(_GLPlotData2D):
prog = self._linearProgram
prog.use()
- gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
+ gl.glUniform1i(prog.uniforms["tex"], self._DATA_TEX_UNIT)
- mat = numpy.dot(numpy.dot(context.matrix, mat4Translate(*self.origin)),
- mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
+ mat = numpy.dot(
+ numpy.dot(context.matrix, mat4Translate(*self.origin)),
+ mat4Scale(*self.scale),
+ )
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, mat.astype(numpy.float32)
+ )
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
+ gl.glUniform1f(prog.uniforms["alpha"], self.alpha)
- self._texture.render(prog.attributes['position'],
- prog.attributes['texCoords'],
- self._DATA_TEX_UNIT)
+ self._texture.render(
+ prog.attributes["position"],
+ prog.attributes["texCoords"],
+ self._DATA_TEX_UNIT,
+ )
def _renderLog(self, context):
"""Perform rendering with axes having log scale
@@ -702,27 +728,33 @@ class GLPlotRGBAImage(_GLPlotData2D):
ox, oy = self.origin
- gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
+ gl.glUniform1i(prog.uniforms["tex"], self._DATA_TEX_UNIT)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- context.matrix.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, context.matrix.astype(numpy.float32)
+ )
mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matOffset"], 1, gl.GL_TRUE, mat.astype(numpy.float32)
+ )
- gl.glUniform2i(prog.uniforms['isLog'], context.isXLog, context.isYLog)
+ gl.glUniform2i(prog.uniforms["isLog"], context.isXLog, context.isYLog)
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
+ gl.glUniform1f(prog.uniforms["alpha"], self.alpha)
ex = ox + self.scale[0] * self.data.shape[1]
ey = oy + self.scale[1] * self.data.shape[0]
- xOneOverRange = 1. / (ex - ox)
- yOneOverRange = 1. / (ey - oy)
- gl.glUniform2f(prog.uniforms['bounds_originOverRange'],
- ox * xOneOverRange, oy * yOneOverRange)
- gl.glUniform2f(prog.uniforms['bounds_oneOverRange'],
- xOneOverRange, yOneOverRange)
+ xOneOverRange = 1.0 / (ex - ox)
+ yOneOverRange = 1.0 / (ey - oy)
+ gl.glUniform2f(
+ prog.uniforms["bounds_originOverRange"],
+ ox * xOneOverRange,
+ oy * yOneOverRange,
+ )
+ gl.glUniform2f(
+ prog.uniforms["bounds_oneOverRange"], xOneOverRange, yOneOverRange
+ )
try:
tiles = self._texture.tiles
@@ -730,20 +762,19 @@ class GLPlotRGBAImage(_GLPlotData2D):
raise RuntimeError("No texture, discard has already been called")
if len(tiles) > 1:
raise NotImplementedError(
- "Image over multiple textures not supported with log scale")
+ "Image over multiple textures not supported with log scale"
+ )
texture, vertices, info = tiles[0]
texture.bind(self._DATA_TEX_UNIT)
- posAttrib = prog.attributes['position']
+ posAttrib = prog.attributes["position"]
stride = vertices.shape[-1] * vertices.itemsize
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, vertices)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, stride, vertices
+ )
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
diff --git a/src/silx/gui/plot/backends/glutils/GLPlotItem.py b/src/silx/gui/plot/backends/glutils/GLPlotItem.py
index 58f5f41..0287ad5 100644
--- a/src/silx/gui/plot/backends/glutils/GLPlotItem.py
+++ b/src/silx/gui/plot/backends/glutils/GLPlotItem.py
@@ -39,7 +39,9 @@ class RenderContext:
:param float dpi: Number of device pixels per inch
"""
- def __init__(self, matrix=None, isXLog=False, isYLog=False, dpi=96., plotFrame=None):
+ def __init__(
+ self, matrix=None, isXLog=False, isYLog=False, dpi=96.0, plotFrame=None
+ ):
self.matrix = matrix
"""Current transformation matrix"""
@@ -73,7 +75,7 @@ class GLPlotItem:
"""Base class for primitives used in the PlotWidget OpenGL backend"""
def __init__(self):
- self.yaxis = 'left'
+ self.yaxis = "left"
"YAxis this item is attached to (either 'left' or 'right')"
def pick(self, x, y):
@@ -99,6 +101,5 @@ class GLPlotItem:
pass
def isInitialized(self) -> bool:
- """Returns True if resources where initialized and requires `discard`.
- """
+ """Returns True if resources where initialized and requires `discard`."""
return True
diff --git a/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py b/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py
index a67afd9..e8a8e4a 100644
--- a/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py
+++ b/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py
@@ -70,9 +70,10 @@ class GLPlotTriangles(GLPlotItem):
gl_FragColor.a *= alpha;
}
""",
- attrib0='xPos')
+ attrib0="xPos",
+ )
- def __init__(self, x, y, color, triangles, alpha=1.):
+ def __init__(self, x, y, color, triangles, alpha=1.0):
"""
:param numpy.ndarray x: X coordinates of triangle corners
@@ -97,14 +98,14 @@ class GLPlotTriangles(GLPlotItem):
elif numpy.issubdtype(color.dtype, numpy.integer):
color = numpy.array(color, dtype=numpy.uint8, copy=False)
else:
- raise ValueError('Unsupported color type')
+ raise ValueError("Unsupported color type")
assert triangles.ndim == 2 and triangles.shape[1] == 3
self.__x_y_color = x, y, color
self.xMin, self.xMax = min_max(x, finite=True)
self.yMin, self.yMax = min_max(y, finite=True)
self.__triangles = triangles
- self.__alpha = numpy.clip(float(alpha), 0., 1.)
+ self.__alpha = numpy.clip(float(alpha), 0.0, 1.0)
self.__vbos = None
self.__indicesVbo = None
self.__picking_triangles = None
@@ -117,21 +118,22 @@ class GLPlotTriangles(GLPlotItem):
:return: List of picked data point indices
:rtype: Union[List[int],None]
"""
- if (x < self.xMin or x > self.xMax or
- y < self.yMin or y > self.yMax):
+ if x < self.xMin or x > self.xMax or y < self.yMin or y > self.yMax:
return None
xPts, yPts = self.__x_y_color[:2]
if self.__picking_triangles is None:
self.__picking_triangles = numpy.zeros(
- self.__triangles.shape + (3,), dtype=numpy.float32)
+ self.__triangles.shape + (3,), dtype=numpy.float32
+ )
self.__picking_triangles[:, :, 0] = xPts[self.__triangles]
self.__picking_triangles[:, :, 1] = yPts[self.__triangles]
segment = numpy.array(((x, y, -1), (x, y, 1)), dtype=numpy.float32)
# Picked triangle indices
indices = glutils.segmentTrianglesIntersection(
- segment, self.__picking_triangles)[0]
+ segment, self.__picking_triangles
+ )[0]
# Point indices
indices = numpy.unique(numpy.ravel(self.__triangles[indices]))
@@ -163,7 +165,8 @@ class GLPlotTriangles(GLPlotItem):
self.__indicesVbo = glutils.VertexBuffer(
numpy.ravel(self.__triangles),
usage=gl.GL_STATIC_DRAW,
- target=gl.GL_ELEMENT_ARRAY_BUFFER)
+ target=gl.GL_ELEMENT_ARRAY_BUFFER,
+ )
def render(self, context):
"""Perform rendering
@@ -177,20 +180,24 @@ class GLPlotTriangles(GLPlotItem):
self._PROGRAM.use()
- gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'],
- 1,
- gl.GL_TRUE,
- context.matrix.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ self._PROGRAM.uniforms["matrix"],
+ 1,
+ gl.GL_TRUE,
+ context.matrix.astype(numpy.float32),
+ )
- gl.glUniform1f(self._PROGRAM.uniforms['alpha'], self.__alpha)
+ gl.glUniform1f(self._PROGRAM.uniforms["alpha"], self.__alpha)
- for index, name in enumerate(('xPos', 'yPos', 'color')):
+ for index, name in enumerate(("xPos", "yPos", "color")):
attr = self._PROGRAM.attributes[name]
gl.glEnableVertexAttribArray(attr)
self.__vbos[index].setVertexAttrib(attr)
with self.__indicesVbo:
- gl.glDrawElements(gl.GL_TRIANGLES,
- self.__triangles.size,
- glutils.numpyToGLType(self.__triangles.dtype),
- ctypes.c_void_p(0))
+ gl.glDrawElements(
+ gl.GL_TRIANGLES,
+ self.__triangles.size,
+ glutils.numpyToGLType(self.__triangles.dtype),
+ ctypes.c_void_p(0),
+ )
diff --git a/src/silx/gui/plot/backends/glutils/GLSupport.py b/src/silx/gui/plot/backends/glutils/GLSupport.py
index f5357e2..c9afda0 100644
--- a/src/silx/gui/plot/backends/glutils/GLSupport.py
+++ b/src/silx/gui/plot/backends/glutils/GLSupport.py
@@ -54,8 +54,7 @@ def buildFillMaskIndices(nIndices, dtype=None):
splitIndex = lastIndex // 2 + 1
indices = numpy.empty(nIndices, dtype=dtype)
indices[::2] = numpy.arange(0, splitIndex, step=1, dtype=dtype)
- indices[1::2] = numpy.arange(lastIndex, splitIndex - 1, step=-1,
- dtype=dtype)
+ indices[1::2] = numpy.arange(lastIndex, splitIndex - 1, step=-1, dtype=dtype)
return indices
@@ -63,16 +62,17 @@ class FilledShape2D(object):
_NO_HATCH = 0
_HATCH_STEP = 20
- def __init__(self, points, style='solid', color=(0., 0., 0., 1.)):
+ def __init__(self, points, style="solid", color=(0.0, 0.0, 0.0, 1.0)):
self.vertices = numpy.array(points, dtype=numpy.float32, copy=False)
self._indices = buildFillMaskIndices(len(self.vertices))
tVertex = numpy.transpose(self.vertices)
xMin, xMax = min(tVertex[0]), max(tVertex[0])
yMin, yMax = min(tVertex[1]), max(tVertex[1])
- self.bboxVertices = numpy.array(((xMin, yMin), (xMin, yMax),
- (xMax, yMin), (xMax, yMax)),
- dtype=numpy.float32)
+ self.bboxVertices = numpy.array(
+ ((xMin, yMin), (xMin, yMax), (xMax, yMin), (xMax, yMax)),
+ dtype=numpy.float32,
+ )
self._xMin, self._xMax = xMin, xMax
self._yMin, self._yMax = yMin, yMax
@@ -80,18 +80,16 @@ class FilledShape2D(object):
self.color = color
def render(self, posAttrib, colorUnif, hatchStepUnif):
- assert self.style in ('hatch', 'solid')
+ assert self.style in ("hatch", "solid")
gl.glUniform4f(colorUnif, *self.color)
- step = self._HATCH_STEP if self.style == 'hatch' else self._NO_HATCH
+ step = self._HATCH_STEP if self.style == "hatch" else self._NO_HATCH
gl.glUniform1i(hatchStepUnif, step)
# Prepare fill mask
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, self.vertices)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, self.vertices
+ )
gl.glEnable(gl.GL_STENCIL_TEST)
gl.glStencilMask(1)
@@ -100,8 +98,12 @@ class FilledShape2D(object):
gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
gl.glDepthMask(gl.GL_FALSE)
- gl.glDrawElements(gl.GL_TRIANGLE_STRIP, len(self._indices),
- gl.GL_UNSIGNED_SHORT, self._indices)
+ gl.glDrawElements(
+ gl.GL_TRIANGLE_STRIP,
+ len(self._indices),
+ gl.GL_UNSIGNED_SHORT,
+ self._indices,
+ )
gl.glStencilFunc(gl.GL_EQUAL, 1, 1)
# Reset stencil while drawing
@@ -109,11 +111,9 @@ class FilledShape2D(object):
gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDepthMask(gl.GL_TRUE)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, self.bboxVertices)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, self.bboxVertices
+ )
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self.bboxVertices))
gl.glDisable(gl.GL_STENCIL_TEST)
@@ -121,37 +121,54 @@ class FilledShape2D(object):
# matrix ######################################################################
+
def mat4Ortho(left, right, bottom, top, near, far):
"""Orthographic projection matrix (row-major)"""
- return numpy.array((
- (2./(right - left), 0., 0., -(right+left)/float(right-left)),
- (0., 2./(top - bottom), 0., -(top+bottom)/float(top-bottom)),
- (0., 0., -2./(far-near), -(far+near)/float(far-near)),
- (0., 0., 0., 1.)), dtype=numpy.float64)
-
-
-def mat4Translate(x=0., y=0., z=0.):
+ return numpy.array(
+ (
+ (2.0 / (right - left), 0.0, 0.0, -(right + left) / float(right - left)),
+ (0.0, 2.0 / (top - bottom), 0.0, -(top + bottom) / float(top - bottom)),
+ (0.0, 0.0, -2.0 / (far - near), -(far + near) / float(far - near)),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float64,
+ )
+
+
+def mat4Translate(x=0.0, y=0.0, z=0.0):
"""Translation matrix (row-major)"""
- return numpy.array((
- (1., 0., 0., x),
- (0., 1., 0., y),
- (0., 0., 1., z),
- (0., 0., 0., 1.)), dtype=numpy.float64)
-
-
-def mat4Scale(sx=1., sy=1., sz=1.):
+ return numpy.array(
+ (
+ (1.0, 0.0, 0.0, x),
+ (0.0, 1.0, 0.0, y),
+ (0.0, 0.0, 1.0, z),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float64,
+ )
+
+
+def mat4Scale(sx=1.0, sy=1.0, sz=1.0):
"""Scale matrix (row-major)"""
- return numpy.array((
- (sx, 0., 0., 0.),
- (0., sy, 0., 0.),
- (0., 0., sz, 0.),
- (0., 0., 0., 1.)), dtype=numpy.float64)
+ return numpy.array(
+ (
+ (sx, 0.0, 0.0, 0.0),
+ (0.0, sy, 0.0, 0.0),
+ (0.0, 0.0, sz, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float64,
+ )
def mat4Identity():
"""Identity matrix"""
- return numpy.array((
- (1., 0., 0., 0.),
- (0., 1., 0., 0.),
- (0., 0., 1., 0.),
- (0., 0., 0., 1.)), dtype=numpy.float64)
+ return numpy.array(
+ (
+ (1.0, 0.0, 0.0, 0.0),
+ (0.0, 1.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float64,
+ )
diff --git a/src/silx/gui/plot/backends/glutils/GLText.py b/src/silx/gui/plot/backends/glutils/GLText.py
index 4862bff..15d7a70 100644
--- a/src/silx/gui/plot/backends/glutils/GLText.py
+++ b/src/silx/gui/plot/backends/glutils/GLText.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 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
@@ -26,6 +26,8 @@ This module provides minimalistic text support for OpenGL.
It provides Latin-1 (ISO8859-1) characters for one monospace font at one size.
"""
+from __future__ import annotations
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "03/04/2017"
@@ -36,14 +38,13 @@ import weakref
import numpy
+from .... import qt
from ...._glutils import font, gl, Context, Program, Texture
from .GLSupport import mat4Translate
+from silx.gui.colors import RGBAColorType
-# TODO: Font should be configurable by the main program: using mpl.rcParams?
-
-
-class _Cache(object):
+class _Cache:
"""LRU (Least Recent Used) cache.
:param int maxsize: Maximum number of (key, value) pairs in the cache
@@ -55,7 +56,7 @@ class _Cache(object):
def __init__(self, maxsize=128, callback=None):
self._maxsize = int(maxsize)
self._callback = callback
- self._cache = OrderedDict()
+ self._cache = OrderedDict() # Needed for popitem(last=False)
def __contains__(self, item):
return item in self._cache
@@ -84,15 +85,14 @@ class _Cache(object):
# Text2D ######################################################################
-LEFT, CENTER, RIGHT = 'left', 'center', 'right'
-TOP, BASELINE, BOTTOM = 'top', 'baseline', 'bottom'
+LEFT, CENTER, RIGHT = "left", "center", "right"
+TOP, BASELINE, BOTTOM = "top", "baseline", "bottom"
ROTATE_90, ROTATE_180, ROTATE_270 = 90, 180, 270
-class Text2D(object):
-
+class Text2D:
_SHADERS = {
- 'vertex': """
+ "vertex": """
#version 120
attribute vec2 position;
@@ -106,7 +106,7 @@ class Text2D(object):
vCoords = texCoords;
}
""",
- 'fragment': """
+ "fragment": """
#version 120
uniform sampler2D texText;
@@ -116,130 +116,134 @@ class Text2D(object):
varying vec2 vCoords;
void main(void) {
- gl_FragColor = mix(bgColor, color, texture2D(texText, vCoords).r);
+ if (vCoords.x < 0.0 || vCoords.x > 1.0 || vCoords.y < 0.0 || vCoords.y > 1.0) {
+ gl_FragColor = bgColor;
+ } else {
+ gl_FragColor = mix(bgColor, color, texture2D(texText, vCoords).r);
+ }
}
- """
+ """,
}
- _TEX_COORDS = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
- dtype=numpy.float32).ravel()
-
- _program = Program(_SHADERS['vertex'],
- _SHADERS['fragment'],
- attrib0='position')
+ _program = Program(_SHADERS["vertex"], _SHADERS["fragment"], attrib0="position")
# Discard texture objects when removed from the cache
_textures = weakref.WeakKeyDictionary()
"""Cache already created textures"""
- _sizes = _Cache()
- """Cache already computed sizes"""
-
- def __init__(self, text, x=0, y=0,
- color=(0., 0., 0., 1.),
- bgColor=None,
- align=LEFT, valign=BASELINE,
- rotate=0,
- devicePixelRatio= 1.):
+ def __init__(
+ self,
+ text: str,
+ font: qt.QFont,
+ x: float = 0.0,
+ y: float = 0.0,
+ color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0),
+ bgColor: RGBAColorType | None = None,
+ align: str = LEFT,
+ valign: str = BASELINE,
+ rotate: float = 0.0,
+ devicePixelRatio: float = 1.0,
+ padding: int = 0,
+ ):
self.devicePixelRatio = devicePixelRatio
+ self.font = font
self._vertices = None
self._text = text
+ self._padding = padding
self.x = x
self.y = y
self.color = color
self.bgColor = bgColor
if align not in (LEFT, CENTER, RIGHT):
- raise ValueError(
- "Horizontal alignment not supported: {0}".format(align))
+ raise ValueError("Horizontal alignment not supported: {0}".format(align))
self._align = align
if valign not in (TOP, CENTER, BASELINE, BOTTOM):
- raise ValueError(
- "Vertical alignment not supported: {0}".format(valign))
+ raise ValueError("Vertical alignment not supported: {0}".format(valign))
self._valign = valign
self._rotate = numpy.radians(rotate)
- def _getTexture(self, text, devicePixelRatio):
+ def _getTexture(self, dotsPerInch: float) -> tuple[Texture, int]:
# Retrieve/initialize texture cache for current context
- textureKey = text, devicePixelRatio
+ key = self.text, self.font.key(), dotsPerInch
context = Context.getCurrent()
if context not in self._textures:
self._textures[context] = _Cache(
- callback=lambda key, value: value[0].discard())
+ callback=lambda key, value: value[0].discard()
+ )
textures = self._textures[context]
- if textureKey not in textures:
- image, offset = font.rasterText(
- text,
- font.getDefaultFontFamily(),
- devicePixelRatio=self.devicePixelRatio)
- if textureKey not in self._sizes:
- self._sizes[textureKey] = image.shape[1], image.shape[0]
+ if key not in textures:
+ image, offset = font.rasterText(self.text, self.font, dotsPerInch)
texture = Texture(
gl.GL_RED,
data=image,
minFilter=gl.GL_NEAREST,
magFilter=gl.GL_NEAREST,
- wrap=(gl.GL_CLAMP_TO_EDGE,
- gl.GL_CLAMP_TO_EDGE))
+ wrap=(gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE),
+ )
texture.prepare()
- textures[textureKey] = texture, offset
+ textures[key] = texture, offset
- return textures[textureKey]
+ return textures[key]
@property
- def text(self):
+ def text(self) -> str:
return self._text
@property
- def size(self):
- textureKey = self.text, self.devicePixelRatio
- if textureKey not in self._sizes:
- image, offset = font.rasterText(
- self.text,
- font.getDefaultFontFamily(),
- devicePixelRatio=self.devicePixelRatio)
- self._sizes[textureKey] = image.shape[1], image.shape[0]
- return self._sizes[textureKey]
-
- def getVertices(self, offset, shape):
+ def padding(self) -> int:
+ return self._padding
+
+ def getVertices(self, offset: int, shape: tuple[int, int]) -> numpy.ndarray:
height, width = shape
if self._align == LEFT:
xOrig = 0
elif self._align == RIGHT:
- xOrig = - width
+ xOrig = -width
else: # CENTER
- xOrig = - width // 2
+ xOrig = -width // 2
if self._valign == BASELINE:
- yOrig = - offset
+ yOrig = -offset
elif self._valign == TOP:
yOrig = 0
elif self._valign == BOTTOM:
- yOrig = - height
+ yOrig = -height
else: # CENTER
- yOrig = - height // 2
-
- vertices = numpy.array((
- (xOrig, yOrig),
- (xOrig + width, yOrig),
- (xOrig, yOrig + height),
- (xOrig + width, yOrig + height)), dtype=numpy.float32)
+ yOrig = -height // 2
+
+ vertices = numpy.array(
+ (
+ (xOrig, yOrig),
+ (xOrig + width, yOrig),
+ (xOrig, yOrig + height),
+ (xOrig + width, yOrig + height),
+ ),
+ dtype=numpy.float32,
+ )
cos, sin = numpy.cos(self._rotate), numpy.sin(self._rotate)
- vertices = numpy.ascontiguousarray(numpy.transpose(numpy.array((
- cos * vertices[:, 0] - sin * vertices[:, 1],
- sin * vertices[:, 0] + cos * vertices[:, 1]),
- dtype=numpy.float32)))
+ vertices = numpy.ascontiguousarray(
+ numpy.transpose(
+ numpy.array(
+ (
+ cos * vertices[:, 0] - sin * vertices[:, 1],
+ sin * vertices[:, 0] + cos * vertices[:, 1],
+ ),
+ dtype=numpy.float32,
+ )
+ )
+ )
return vertices
- def render(self, matrix):
+ def render(self, matrix: numpy.ndarray, dotsPerInch: float):
if not self.text.strip():
return
@@ -247,40 +251,47 @@ class Text2D(object):
prog.use()
texUnit = 0
- texture, offset = self._getTexture(self.text, self.devicePixelRatio)
+ texture, offset = self._getTexture(dotsPerInch)
- gl.glUniform1i(prog.uniforms['texText'], texUnit)
+ gl.glUniform1i(prog.uniforms["texText"], texUnit)
mat = numpy.dot(matrix, mat4Translate(int(self.x), int(self.y)))
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, mat.astype(numpy.float32)
+ )
- gl.glUniform4f(prog.uniforms['color'], *self.color)
+ gl.glUniform4f(prog.uniforms["color"], *self.color)
if self.bgColor is not None:
bgColor = self.bgColor
else:
- bgColor = self.color[0], self.color[1], self.color[2], 0.
- gl.glUniform4f(prog.uniforms['bgColor'], *bgColor)
+ bgColor = self.color[0], self.color[1], self.color[2], 0.0
+ gl.glUniform4f(prog.uniforms["bgColor"], *bgColor)
- vertices = self.getVertices(offset, texture.shape)
+ paddingOffset = max(0, int(self.padding * self.devicePixelRatio))
+ height, width = texture.shape
+ vertices = self.getVertices(
+ offset, (height + 2 * paddingOffset, width + 2 * paddingOffset)
+ )
- posAttrib = prog.attributes['position']
+ posAttrib = prog.attributes["position"]
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- vertices)
-
- texAttrib = prog.attributes['texCoords']
+ gl.glVertexAttribPointer(posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, vertices)
+
+ xoffset = paddingOffset / width
+ yoffset = paddingOffset / height
+ texCoords = numpy.array(
+ (
+ (-xoffset, -yoffset),
+ (1.0 + xoffset, -yoffset),
+ (-xoffset, 1.0 + yoffset),
+ (1.0 + xoffset, 1.0 + yoffset),
+ ),
+ dtype=numpy.float32,
+ ).ravel()
+
+ texAttrib = prog.attributes["texCoords"]
gl.glEnableVertexAttribArray(texAttrib)
- gl.glVertexAttribPointer(texAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._TEX_COORDS)
+ gl.glVertexAttribPointer(texAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, texCoords)
with texture:
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
diff --git a/src/silx/gui/plot/backends/glutils/GLTexture.py b/src/silx/gui/plot/backends/glutils/GLTexture.py
index caca111..cbbe7ac 100644
--- a/src/silx/gui/plot/backends/glutils/GLTexture.py
+++ b/src/silx/gui/plot/backends/glutils/GLTexture.py
@@ -39,29 +39,33 @@ from ...._glutils import gl, Texture, numpyToGLType
_logger = logging.getLogger(__name__)
-def _checkTexture2D(internalFormat, shape,
- format_=None, type_=gl.GL_FLOAT, border=0):
+def _checkTexture2D(internalFormat, shape, format_=None, type_=gl.GL_FLOAT, border=0):
"""Check if texture size with provided parameters is supported
:rtype: bool
"""
height, width = shape
- gl.glTexImage2D(gl.GL_PROXY_TEXTURE_2D, 0, internalFormat,
- width, height, border,
- format_ or internalFormat,
- type_, c_void_p(0))
- width = gl.glGetTexLevelParameteriv(
- gl.GL_PROXY_TEXTURE_2D, 0, gl.GL_TEXTURE_WIDTH)
+ gl.glTexImage2D(
+ gl.GL_PROXY_TEXTURE_2D,
+ 0,
+ internalFormat,
+ width,
+ height,
+ border,
+ format_ or internalFormat,
+ type_,
+ c_void_p(0),
+ )
+ width = gl.glGetTexLevelParameteriv(gl.GL_PROXY_TEXTURE_2D, 0, gl.GL_TEXTURE_WIDTH)
return bool(width)
MIN_TEXTURE_SIZE = 64
-def _getMaxSquareTexture2DSize(internalFormat=gl.GL_RGBA,
- format_=None,
- type_=gl.GL_FLOAT,
- border=0):
+def _getMaxSquareTexture2DSize(
+ internalFormat=gl.GL_RGBA, format_=None, type_=gl.GL_FLOAT, border=0
+):
"""Returns a supported size for a corresponding square texture
:returns: GL_MAX_TEXTURE_SIZE or a smaller supported size (not optimal)
@@ -69,16 +73,15 @@ def _getMaxSquareTexture2DSize(internalFormat=gl.GL_RGBA,
"""
# Is this useful?
maxTexSize = gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE)
- while maxTexSize > MIN_TEXTURE_SIZE and \
- not _checkTexture2D(internalFormat, (maxTexSize, maxTexSize),
- format_, type_, border):
+ while maxTexSize > MIN_TEXTURE_SIZE and not _checkTexture2D(
+ internalFormat, (maxTexSize, maxTexSize), format_, type_, border
+ ):
maxTexSize //= 2
return max(MIN_TEXTURE_SIZE, maxTexSize)
class Image(object):
- """Image of any size eventually using multiple textures or larger texture
- """
+ """Image of any size eventually using multiple textures or larger texture"""
_WRAP = (gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE)
_MIN_FILTER = gl.GL_NEAREST
@@ -90,34 +93,48 @@ class Image(object):
type_ = numpyToGLType(data.dtype)
if _checkTexture2D(internalFormat, data.shape[0:2], format_, type_):
- texture = Texture(internalFormat,
- data,
- format_,
- texUnit=texUnit,
- minFilter=self._MIN_FILTER,
- magFilter=self._MAG_FILTER,
- wrap=self._WRAP)
+ texture = Texture(
+ internalFormat,
+ data,
+ format_,
+ texUnit=texUnit,
+ minFilter=self._MIN_FILTER,
+ magFilter=self._MAG_FILTER,
+ wrap=self._WRAP,
+ )
texture.prepare()
- vertices = numpy.array((
- (0., 0., 0., 0.),
- (self.width, 0., 1., 0.),
- (0., self.height, 0., 1.),
- (self.width, self.height, 1., 1.)), dtype=numpy.float32)
- self.tiles = ((texture, vertices,
- {'xOrigData': 0, 'yOrigData': 0,
- 'wData': self.width, 'hData': self.height}),)
+ vertices = numpy.array(
+ (
+ (0.0, 0.0, 0.0, 0.0),
+ (self.width, 0.0, 1.0, 0.0),
+ (0.0, self.height, 0.0, 1.0),
+ (self.width, self.height, 1.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+ self.tiles = (
+ (
+ texture,
+ vertices,
+ {
+ "xOrigData": 0,
+ "yOrigData": 0,
+ "wData": self.width,
+ "hData": self.height,
+ },
+ ),
+ )
else:
# Handle dimension too large: make tiles
- maxTexSize = _getMaxSquareTexture2DSize(internalFormat,
- format_, type_)
+ maxTexSize = _getMaxSquareTexture2DSize(internalFormat, format_, type_)
- nCols = (self.width+maxTexSize-1) // maxTexSize
+ nCols = (self.width + maxTexSize - 1) // maxTexSize
colWidths = [self.width // nCols] * nCols
colWidths[-1] += self.width % nCols
- nRows = (self.height+maxTexSize-1) // maxTexSize
- rowHeights = [self.height//nRows] * nRows
+ nRows = (self.height + maxTexSize - 1) // maxTexSize
+ rowHeights = [self.height // nRows] * nRows
rowHeights[-1] += self.height % nRows
tiles = []
@@ -125,30 +142,32 @@ class Image(object):
for hData in rowHeights:
xOrig = 0
for wData in colWidths:
- if (hData < MIN_TEXTURE_SIZE or wData < MIN_TEXTURE_SIZE) \
- and not _checkTexture2D(internalFormat,
- (hData, wData),
- format_,
- type_):
+ if (
+ hData < MIN_TEXTURE_SIZE or wData < MIN_TEXTURE_SIZE
+ ) and not _checkTexture2D(
+ internalFormat, (hData, wData), format_, type_
+ ):
# Ensure texture size is at least MIN_TEXTURE_SIZE
tH = max(hData, MIN_TEXTURE_SIZE)
tW = max(wData, MIN_TEXTURE_SIZE)
- uMax, vMax = float(wData)/tW, float(hData)/tH
+ uMax, vMax = float(wData) / tW, float(hData) / tH
# TODO issue with type_ and alignment
- texture = Texture(internalFormat,
- data=None,
- format_=format_,
- shape=(tH, tW),
- texUnit=texUnit,
- minFilter=self._MIN_FILTER,
- magFilter=self._MAG_FILTER,
- wrap=self._WRAP)
+ texture = Texture(
+ internalFormat,
+ data=None,
+ format_=format_,
+ shape=(tH, tW),
+ texUnit=texUnit,
+ minFilter=self._MIN_FILTER,
+ magFilter=self._MAG_FILTER,
+ wrap=self._WRAP,
+ )
# TODO handle unpack
- texture.update(format_,
- data[yOrig:yOrig+hData,
- xOrig:xOrig+wData])
+ texture.update(
+ format_, data[yOrig : yOrig + hData, xOrig : xOrig + wData]
+ )
# texture.update(format_, type_, data,
# width=wData, height=hData,
# unpackRowLength=width,
@@ -159,28 +178,41 @@ class Image(object):
# TODO issue with type_ and unpacking tiles
# TODO idea to handle unpack: use array strides
# As it is now, it will make a copy
- texture = Texture(internalFormat,
- data[yOrig:yOrig+hData,
- xOrig:xOrig+wData],
- format_,
- texUnit=texUnit,
- minFilter=self._MIN_FILTER,
- magFilter=self._MAG_FILTER,
- wrap=self._WRAP)
+ texture = Texture(
+ internalFormat,
+ data[yOrig : yOrig + hData, xOrig : xOrig + wData],
+ format_,
+ texUnit=texUnit,
+ minFilter=self._MIN_FILTER,
+ magFilter=self._MAG_FILTER,
+ wrap=self._WRAP,
+ )
# TODO
# unpackRowLength=width,
# unpackSkipPixels=xOrig,
# unpackSkipRows=yOrig)
- vertices = numpy.array((
- (xOrig, yOrig, 0., 0.),
- (xOrig + wData, yOrig, uMax, 0.),
- (xOrig, yOrig + hData, 0., vMax),
- (xOrig + wData, yOrig + hData, uMax, vMax)),
- dtype=numpy.float32)
+ vertices = numpy.array(
+ (
+ (xOrig, yOrig, 0.0, 0.0),
+ (xOrig + wData, yOrig, uMax, 0.0),
+ (xOrig, yOrig + hData, 0.0, vMax),
+ (xOrig + wData, yOrig + hData, uMax, vMax),
+ ),
+ dtype=numpy.float32,
+ )
texture.prepare()
- tiles.append((texture, vertices,
- {'xOrigData': xOrig, 'yOrigData': yOrig,
- 'wData': wData, 'hData': hData}))
+ tiles.append(
+ (
+ texture,
+ vertices,
+ {
+ "xOrigData": xOrig,
+ "yOrigData": yOrig,
+ "wData": wData,
+ "hData": hData,
+ },
+ )
+ )
xOrig += wData
yOrig += hData
self.tiles = tuple(tiles)
@@ -191,7 +223,7 @@ class Image(object):
del self.tiles
def updateAll(self, format_, data, texUnit=0):
- if not hasattr(self, 'tiles'):
+ if not hasattr(self, "tiles"):
raise RuntimeError("No texture, discard has already been called")
assert data.shape[:2] == (self.height, self.width)
@@ -199,11 +231,13 @@ class Image(object):
self.tiles[0][0].update(format_, data, texUnit=texUnit)
else:
for texture, _, info in self.tiles:
- yOrig, xOrig = info['yOrigData'], info['xOrigData']
- height, width = info['hData'], info['wData']
- texture.update(format_,
- data[yOrig:yOrig+height, xOrig:xOrig+width],
- texUnit=texUnit)
+ yOrig, xOrig = info["yOrigData"], info["xOrigData"]
+ height, width = info["hData"], info["wData"]
+ texture.update(
+ format_,
+ data[yOrig : yOrig + height, xOrig : xOrig + width],
+ texUnit=texUnit,
+ )
texture.prepare()
# TODO check
# width=info['wData'], height=info['hData'],
@@ -223,18 +257,13 @@ class Image(object):
stride = vertices.shape[-1] * vertices.itemsize
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, vertices)
-
- texCoordsPtr = c_void_p(vertices.ctypes.data +
- 2 * vertices.itemsize)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, stride, vertices
+ )
+
+ texCoordsPtr = c_void_p(vertices.ctypes.data + 2 * vertices.itemsize)
gl.glEnableVertexAttribArray(texAttrib)
- gl.glVertexAttribPointer(texAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, texCoordsPtr)
+ gl.glVertexAttribPointer(
+ texAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, stride, texCoordsPtr
+ )
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
diff --git a/src/silx/gui/plot/backends/glutils/PlotImageFile.py b/src/silx/gui/plot/backends/glutils/PlotImageFile.py
index 75ee50b..1622122 100644
--- a/src/silx/gui/plot/backends/glutils/PlotImageFile.py
+++ b/src/silx/gui/plot/backends/glutils/PlotImageFile.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 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
@@ -30,12 +30,14 @@ __date__ = "03/04/2017"
import base64
import struct
-import sys
import zlib
+from fabio.TiffIO import TiffIO
+
# Image writer ################################################################
+
def convertRGBDataToPNG(data):
"""Convert a RGB bitmap to PNG.
@@ -53,29 +55,42 @@ def convertRGBDataToPNG(data):
colorType = 2 # 'truecolor' = RGB
interlace = 0 # No
- IHDRdata = struct.pack(">ccccIIBBBBB", b'I', b'H', b'D', b'R',
- width, height, depth, colorType,
- 0, 0, interlace)
+ IHDRdata = struct.pack(
+ ">ccccIIBBBBB",
+ b"I",
+ b"H",
+ b"D",
+ b"R",
+ width,
+ height,
+ depth,
+ colorType,
+ 0,
+ 0,
+ interlace,
+ )
# Add filter 'None' before each scanline
- preparedData = b'\x00' + b'\x00'.join(line.tobytes() for line in data)
+ preparedData = b"\x00" + b"\x00".join(line.tobytes() for line in data)
compressedData = zlib.compress(preparedData, 8)
- IDATdata = struct.pack("cccc", b'I', b'D', b'A', b'T')
+ IDATdata = struct.pack("cccc", b"I", b"D", b"A", b"T")
IDATdata += compressedData
- return b''.join([
- b'\x89PNG\r\n\x1a\n', # PNG signature
- # IHDR chunk: Image Header
- struct.pack(">I", 13), # length
- IHDRdata,
- struct.pack(">I", zlib.crc32(IHDRdata) & 0xffffffff), # CRC
- # IDAT chunk: Payload
- struct.pack(">I", len(compressedData)),
- IDATdata,
- struct.pack(">I", zlib.crc32(IDATdata) & 0xffffffff), # CRC
- b'\x00\x00\x00\x00IEND\xaeB`\x82' # IEND chunk: footer
- ])
+ return b"".join(
+ [
+ b"\x89PNG\r\n\x1a\n", # PNG signature
+ # IHDR chunk: Image Header
+ struct.pack(">I", 13), # length
+ IHDRdata,
+ struct.pack(">I", zlib.crc32(IHDRdata) & 0xFFFFFFFF), # CRC
+ # IDAT chunk: Payload
+ struct.pack(">I", len(compressedData)),
+ IDATdata,
+ struct.pack(">I", zlib.crc32(IDATdata) & 0xFFFFFFFF), # CRC
+ b"\x00\x00\x00\x00IEND\xaeB`\x82", # IEND chunk: footer
+ ]
+ )
def saveImageToFile(data, fileNameOrObj, fileFormat):
@@ -89,64 +104,56 @@ def saveImageToFile(data, fileNameOrObj, fileFormat):
"""
assert len(data.shape) == 3
assert data.shape[2] == 3
- assert fileFormat in ('png', 'ppm', 'svg', 'tiff')
+ assert fileFormat in ("png", "ppm", "svg", "tif", "tiff")
- if not hasattr(fileNameOrObj, 'write'):
- if sys.version_info < (3, ):
+ if not hasattr(fileNameOrObj, "write"):
+ if fileFormat in ("png", "ppm", "tiff"):
+ # Open in binary mode
fileObj = open(fileNameOrObj, "wb")
else:
- if fileFormat in ('png', 'ppm', 'tiff'):
- # Open in binary mode
- fileObj = open(fileNameOrObj, 'wb')
- else:
- fileObj = open(fileNameOrObj, 'w', newline='')
+ fileObj = open(fileNameOrObj, "w", newline="")
else: # Use as a file-like object
fileObj = fileNameOrObj
- if fileFormat == 'svg':
+ if fileFormat == "svg":
height, width = data.shape[:2]
base64Data = base64.b64encode(convertRGBDataToPNG(data))
- fileObj.write(
- '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n')
+ fileObj.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n')
fileObj.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n')
- fileObj.write(
- ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
+ fileObj.write(' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
fileObj.write('<svg xmlns:xlink="http://www.w3.org/1999/xlink"\n')
fileObj.write(' xmlns="http://www.w3.org/2000/svg"\n')
fileObj.write(' version="1.1"\n')
fileObj.write(' width="%d"\n' % width)
fileObj.write(' height="%d">\n' % height)
fileObj.write(' <image xlink:href="data:image/png;base64,')
- fileObj.write(base64Data.decode('ascii'))
+ fileObj.write(base64Data.decode("ascii"))
fileObj.write('"\n')
fileObj.write(' x="0"\n')
fileObj.write(' y="0"\n')
fileObj.write(' width="%d"\n' % width)
fileObj.write(' height="%d"\n' % height)
fileObj.write(' id="image" />\n')
- fileObj.write('</svg>')
+ fileObj.write("</svg>")
- elif fileFormat == 'ppm':
+ elif fileFormat == "ppm":
height, width = data.shape[:2]
- fileObj.write(b'P6\n')
- fileObj.write(b'%d %d\n' % (width, height))
- fileObj.write(b'255\n')
+ fileObj.write(b"P6\n")
+ fileObj.write(b"%d %d\n" % (width, height))
+ fileObj.write(b"255\n")
fileObj.write(data.tobytes())
- elif fileFormat == 'png':
+ elif fileFormat == "png":
fileObj.write(convertRGBDataToPNG(data))
- elif fileFormat == 'tiff':
+ elif fileFormat in ("tif", "tiff"):
if fileObj == fileNameOrObj:
- raise NotImplementedError(
- 'Save TIFF to a file-like object not implemented')
-
- from silx.third_party.TiffIO import TiffIO
+ raise NotImplementedError("Save TIFF to a file-like object not implemented")
- tif = TiffIO(fileNameOrObj, mode='wb+')
- tif.writeImage(data, info={'Title': 'OpenGL Plot Snapshot'})
+ tif = TiffIO(fileNameOrObj, mode="wb+")
+ tif.writeImage(data, info={"Title": "OpenGL Plot Snapshot"})
if fileObj != fileNameOrObj:
fileObj.close()
diff --git a/src/silx/gui/plot/items/__init__.py b/src/silx/gui/plot/items/__init__.py
index 6e26c64..bbb4220 100644
--- a/src/silx/gui/plot/items/__init__.py
+++ b/src/silx/gui/plot/items/__init__.py
@@ -31,22 +31,50 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "22/06/2017"
-from .core import (Item, DataItem, # noqa
- LabelsMixIn, DraggableMixIn, ColormapMixIn, # noqa
- SymbolMixIn, ColorMixIn, YAxisMixIn, FillMixIn, # noqa
- AlphaMixIn, LineMixIn, ScatterVisualizationMixIn, # noqa
- ComplexMixIn, ItemChangedType, PointsBase) # noqa
+from .core import (
+ Item,
+ DataItem, # noqa
+ LabelsMixIn,
+ DraggableMixIn,
+ ColormapMixIn,
+ LineGapColorMixIn, # noqa
+ SymbolMixIn,
+ ColorMixIn,
+ YAxisMixIn,
+ FillMixIn, # noqa
+ AlphaMixIn,
+ LineMixIn,
+ ScatterVisualizationMixIn, # noqa
+ ComplexMixIn,
+ ItemChangedType,
+ PointsBase,
+) # noqa
from .complex import ImageComplexData # noqa
from .curve import Curve, CurveStyle # noqa
from .histogram import Histogram # noqa
-from .image import ImageBase, ImageData, ImageDataBase, ImageRgba, ImageStack, MaskImageData # noqa
+from .image import (
+ ImageBase,
+ ImageData,
+ ImageDataBase,
+ ImageRgba,
+ ImageStack,
+ MaskImageData,
+) # noqa
from .image_aggregated import ImageDataAggregated # noqa
from .shape import Line, Shape, BoundingRect, XAxisExtent, YAxisExtent # noqa
from .scatter import Scatter # noqa
from .marker import MarkerBase, Marker, XMarker, YMarker # noqa
from .axis import Axis, XAxis, YAxis, YRightAxis
-DATA_ITEMS = (ImageComplexData, Curve, Histogram, ImageBase, Scatter,
- BoundingRect, XAxisExtent, YAxisExtent)
+DATA_ITEMS = (
+ ImageComplexData,
+ Curve,
+ Histogram,
+ ImageBase,
+ Scatter,
+ BoundingRect,
+ XAxisExtent,
+ YAxisExtent,
+)
"""Classes of items representing data and to consider to compute data bounds.
"""
diff --git a/src/silx/gui/plot/items/_arc_roi.py b/src/silx/gui/plot/items/_arc_roi.py
index 40711b7..658573a 100644
--- a/src/silx/gui/plot/items/_arc_roi.py
+++ b/src/silx/gui/plot/items/_arc_roi.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2018-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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
@@ -30,6 +30,8 @@ __date__ = "28/06/2018"
import logging
import numpy
+import enum
+from typing import Tuple
from ... import utils
from .. import items
@@ -50,8 +52,18 @@ class _ArcGeometry:
The aim is is to switch between consistent state without dealing with
intermediate values.
"""
- def __init__(self, center, startPoint, endPoint, radius,
- weight, startAngle, endAngle, closed=False):
+
+ def __init__(
+ self,
+ center,
+ startPoint,
+ endPoint,
+ radius,
+ weight,
+ startAngle,
+ endAngle,
+ closed=False,
+ ):
"""Constructor for a consistent arc geometry.
There is also specific class method to create different kind of arc
@@ -68,46 +80,59 @@ class _ArcGeometry:
@classmethod
def createEmpty(cls):
- """Create an arc geometry from an empty shape
- """
+ """Create an arc geometry from an empty shape"""
zero = numpy.array([0, 0])
return cls(zero, zero.copy(), zero.copy(), 0, 0, 0, 0)
@classmethod
def createRect(cls, startPoint, endPoint, weight):
- """Create an arc geometry from a definition of a rectangle
- """
+ """Create an arc geometry from a definition of a rectangle"""
return cls(None, startPoint, endPoint, None, weight, None, None, False)
@classmethod
- def createCircle(cls, center, startPoint, endPoint, radius,
- weight, startAngle, endAngle):
- """Create an arc geometry from a definition of a circle
- """
- return cls(center, startPoint, endPoint, radius,
- weight, startAngle, endAngle, True)
+ def createCircle(
+ cls, center, startPoint, endPoint, radius, weight, startAngle, endAngle
+ ):
+ """Create an arc geometry from a definition of a circle"""
+ return cls(
+ center, startPoint, endPoint, radius, weight, startAngle, endAngle, True
+ )
def withWeight(self, weight):
- """Return a new geometry based on this object, with a specific weight
- """
- return _ArcGeometry(self.center, self.startPoint, self.endPoint,
- self.radius, weight,
- self.startAngle, self.endAngle, self._closed)
+ """Return a new geometry based on this object, with a specific weight"""
+ return _ArcGeometry(
+ self.center,
+ self.startPoint,
+ self.endPoint,
+ self.radius,
+ weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed,
+ )
def withRadius(self, radius):
"""Return a new geometry based on this object, with a specific radius.
The weight and the center is conserved.
"""
- startPoint = self.center + (self.startPoint - self.center) / self.radius * radius
+ startPoint = (
+ self.center + (self.startPoint - self.center) / self.radius * radius
+ )
endPoint = self.center + (self.endPoint - self.center) / self.radius * radius
- return _ArcGeometry(self.center, startPoint, endPoint,
- radius, self.weight,
- self.startAngle, self.endAngle, self._closed)
+ return _ArcGeometry(
+ self.center,
+ startPoint,
+ endPoint,
+ radius,
+ self.weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed,
+ )
def withStartAngle(self, startAngle):
- """Return a new geometry based on this object, with a specific start angle
- """
+ """Return a new geometry based on this object, with a specific start angle"""
vector = numpy.array([numpy.cos(startAngle), numpy.sin(startAngle)])
startPoint = self.center + vector * self.radius
@@ -131,8 +156,7 @@ class _ArcGeometry:
)
def withEndAngle(self, endAngle):
- """Return a new geometry based on this object, with a specific end angle
- """
+ """Return a new geometry based on this object, with a specific end angle"""
vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
endPoint = self.center + vector * self.radius
@@ -161,9 +185,16 @@ class _ArcGeometry:
center = None if self.center is None else self.center + delta
startPoint = None if self.startPoint is None else self.startPoint + delta
endPoint = None if self.endPoint is None else self.endPoint + delta
- return _ArcGeometry(center, startPoint, endPoint,
- self.radius, self.weight,
- self.startAngle, self.endAngle, self._closed)
+ return _ArcGeometry(
+ center,
+ startPoint,
+ endPoint,
+ self.radius,
+ self.weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed,
+ )
def getKind(self):
"""Returns the kind of shape defined"""
@@ -191,14 +222,18 @@ class _ArcGeometry:
return self._closed
def __str__(self):
- return str((self.center,
- self.startPoint,
- self.endPoint,
- self.radius,
- self.weight,
- self.startAngle,
- self.endAngle,
- self._closed))
+ return str(
+ (
+ self.center,
+ self.startPoint,
+ self.endPoint,
+ self.radius,
+ self.weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed,
+ )
+ )
class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
@@ -210,19 +245,37 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
- 1 anchor to translate the shape.
"""
- ICON = 'add-shape-arc'
- NAME = 'arc ROI'
+ ICON = "add-shape-arc"
+ NAME = "arc ROI"
SHORT_NAME = "arc"
"""Metadata for this kind of ROI"""
_plotShape = "line"
"""Plot shape which is used for the first interaction"""
- ThreePointMode = RoiInteractionMode("3 points", "Provides 3 points to define the main radius circle")
- PolarMode = RoiInteractionMode("Polar", "Provides anchors to edit the ROI in polar coords")
+ ThreePointMode = RoiInteractionMode(
+ "3 points", "Provides 3 points to define the main radius circle"
+ )
+ PolarMode = RoiInteractionMode(
+ "Polar", "Provides anchors to edit the ROI in polar coords"
+ )
# FIXME: MoveMode was designed cause there is too much anchors
# FIXME: It would be good replace it by a dnd on the shape
- MoveMode = RoiInteractionMode("Translation", "Provides anchors to only move the ROI")
+ MoveMode = RoiInteractionMode(
+ "Translation", "Provides anchors to only move the ROI"
+ )
+
+ class Role(enum.Enum):
+ """Identify a set of roles which can be used for now to reach some positions"""
+
+ START = 0
+ """Location of the anchor at the start of the arc"""
+ STOP = 1
+ """Location of the anchor at the stop of the arc"""
+ MIDDLE = 2
+ """Location of the anchor at the middle of the arc"""
+ CENTER = 3
+ """Location of the center of the circle"""
def __init__(self, parent=None):
HandleBasedROI.__init__(self, parent=parent)
@@ -265,22 +318,28 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
:param RoiInteractionMode modeId:
"""
if modeId is self.ThreePointMode:
+ self._handleStart.setVisible(True)
+ self._handleEnd.setVisible(True)
+ self._handleWeight.setVisible(True)
self._handleStart.setSymbol("s")
self._handleMid.setSymbol("s")
self._handleEnd.setSymbol("s")
self._handleWeight.setSymbol("d")
self._handleMove.setSymbol("+")
elif modeId is self.PolarMode:
+ self._handleStart.setVisible(True)
+ self._handleEnd.setVisible(True)
+ self._handleWeight.setVisible(True)
self._handleStart.setSymbol("o")
self._handleMid.setSymbol("o")
self._handleEnd.setSymbol("o")
self._handleWeight.setSymbol("d")
self._handleMove.setSymbol("+")
elif modeId is self.MoveMode:
- self._handleStart.setSymbol("")
+ self._handleStart.setVisible(False)
+ self._handleEnd.setVisible(False)
+ self._handleWeight.setVisible(False)
self._handleMid.setSymbol("+")
- self._handleEnd.setSymbol("")
- self._handleWeight.setSymbol("")
self._handleMove.setSymbol("+")
else:
assert False
@@ -302,7 +361,7 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
self.__shape.setLineWidth(style.getLineWidth())
def setFirstShapePoints(self, points):
- """"Initialize the ROI using the points from the first interaction.
+ """Initialize the ROI using the points from the first interaction.
This interaction is constrained by the plot API and only supports few
shapes.
@@ -367,7 +426,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
elif geometry.center is not None:
midAngle = (geometry.startAngle + geometry.endAngle) * 0.5
vector = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
- weightPos = geometry.center + (geometry.radius + geometry.weight * 0.5) * vector
+ weightPos = (
+ geometry.center + (geometry.radius + geometry.weight * 0.5) * vector
+ )
with utils.blockSignals(self._handleWeight):
self._handleWeight.setPosition(*weightPos)
@@ -393,7 +454,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
self._updateWeightHandle()
self._updateShape()
- def _updateCurvature(self, start, mid, end, updateCurveHandles, checkClosed=False, updateStart=False):
+ def _updateCurvature(
+ self, start, mid, end, updateCurveHandles, checkClosed=False, updateStart=False
+ ):
"""Update the curvature using 3 control points in the curve
:param bool updateCurveHandles: If False curve handles are already at
@@ -418,7 +481,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
self._handleEnd.setPosition(*end)
weight = self._geometry.weight
- geometry = self._createGeometryFromControlPoints(start, mid, end, weight, closed=closed)
+ geometry = self._createGeometryFromControlPoints(
+ start, mid, end, weight, closed=closed
+ )
self._geometry = geometry
self._updateWeightHandle()
@@ -433,10 +498,10 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
sign = 1 if geometry.startAngle < geometry.endAngle else -1
if updateStart:
geometry.startPoint = geometry.endPoint
- geometry.startAngle = geometry.endAngle - sign * 2*numpy.pi
+ geometry.startAngle = geometry.endAngle - sign * 2 * numpy.pi
else:
geometry.endPoint = geometry.startPoint
- geometry.endAngle = geometry.startAngle + sign * 2*numpy.pi
+ geometry.endAngle = geometry.startAngle + sign * 2 * numpy.pi
def handleDragUpdated(self, handle, origin, previous, current):
modeId = self.getInteractionMode()
@@ -445,8 +510,12 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
mid = numpy.array(self._handleMid.getPosition())
end = numpy.array(self._handleEnd.getPosition())
self._updateCurvature(
- current, mid, end, checkClosed=True, updateStart=True,
- updateCurveHandles=False
+ current,
+ mid,
+ end,
+ checkClosed=True,
+ updateStart=True,
+ updateCurveHandles=False,
)
elif modeId is self.PolarMode:
v = current - self._geometry.center
@@ -477,8 +546,12 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
start = numpy.array(self._handleStart.getPosition())
mid = numpy.array(self._handleMid.getPosition())
self._updateCurvature(
- start, mid, current, checkClosed=True, updateStart=False,
- updateCurveHandles=False
+ start,
+ mid,
+ current,
+ checkClosed=True,
+ updateStart=False,
+ updateCurveHandles=False,
)
elif modeId is self.PolarMode:
v = current - self._geometry.center
@@ -511,8 +584,7 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) < 15
def _normalizeGeometry(self):
- """Keep the same phisical geometry, but with normalized parameters.
- """
+ """Keep the same phisical geometry, but with normalized parameters."""
geometry = self._geometry
if geometry.weight * 0.5 >= geometry.radius:
radius = (geometry.weight * 0.5 + geometry.radius) * 0.5
@@ -582,8 +654,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
if endAngle > startAngle:
endAngle -= 2 * numpy.pi
- return _ArcGeometry(center, start, end,
- radius, weight, startAngle, endAngle)
+ return _ArcGeometry(
+ center, start, end, radius, weight, startAngle, endAngle
+ )
def _createShapeFromGeometry(self, geometry):
kind = geometry.getKind()
@@ -595,11 +668,14 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
distance = numpy.linalg.norm(normal)
if distance != 0:
normal /= distance
- points = numpy.array([
- geometry.startPoint + normal * geometry.weight * 0.5,
- geometry.endPoint + normal * geometry.weight * 0.5,
- geometry.endPoint - normal * geometry.weight * 0.5,
- geometry.startPoint - normal * geometry.weight * 0.5])
+ points = numpy.array(
+ [
+ geometry.startPoint + normal * geometry.weight * 0.5,
+ geometry.endPoint + normal * geometry.weight * 0.5,
+ geometry.endPoint - normal * geometry.weight * 0.5,
+ geometry.startPoint - normal * geometry.weight * 0.5,
+ ]
+ )
elif kind == "point":
# It is not an arc
# but we can display it as an intermediate shape
@@ -712,7 +788,29 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
geometry = self._geometry
if geometry.center is None:
raise ValueError("This ROI can't be represented as a section of circle")
- return geometry.center, self.getInnerRadius(), self.getOuterRadius(), geometry.startAngle, geometry.endAngle
+ return (
+ geometry.center,
+ self.getInnerRadius(),
+ self.getOuterRadius(),
+ geometry.startAngle,
+ geometry.endAngle,
+ )
+
+ def getPosition(self, role: Role = Role.CENTER) -> Tuple[float, float]:
+ """Returns a position by it's role.
+
+ By default returns the center of the circle of the arc ROI.
+ """
+ if role == self.Role.START:
+ return self._handleStart.getPosition()
+ if role == self.Role.STOP:
+ return self._handleEnd.getPosition()
+ if role == self.Role.MIDDLE:
+ return self._handleMid.getPosition()
+ if role == self.Role.CENTER:
+ p = self.getCenter()
+ return p[0], p[1]
+ raise ValueError(f"{role} is not supported")
def isClosed(self):
"""Returns true if the arc is a closed shape, like a circle or a donut.
@@ -795,9 +893,16 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
endPoint = center + vector * radius
- geometry = _ArcGeometry(center, startPoint, endPoint,
- radius, weight,
- startAngle, endAngle, closed=None)
+ geometry = _ArcGeometry(
+ center,
+ startPoint,
+ endPoint,
+ radius,
+ weight,
+ startAngle,
+ endAngle,
+ closed=None,
+ )
self._geometry = geometry
self._updateHandles()
@@ -805,7 +910,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
def contains(self, position):
# first check distance, fastest
center = self.getCenter()
- distance = numpy.sqrt((position[1] - center[1]) ** 2 + ((position[0] - center[0])) ** 2)
+ distance = numpy.sqrt(
+ (position[1] - center[1]) ** 2 + ((position[0] - center[0])) ** 2
+ )
is_in_distance = self.getInnerRadius() <= distance <= self.getOuterRadius()
if not is_in_distance:
return False
@@ -871,8 +978,15 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
def __str__(self):
try:
center, innerRadius, outerRadius, startAngle, endAngle = self.getGeometry()
- params = center[0], center[1], innerRadius, outerRadius, startAngle, endAngle
- params = 'center: %f %f; radius: %f %f; angles: %f %f' % params
+ params = (
+ center[0],
+ center[1],
+ innerRadius,
+ outerRadius,
+ startAngle,
+ endAngle,
+ )
+ params = "center: %f %f; radius: %f %f; angles: %f %f" % params
except ValueError:
params = "invalid"
return "%s(%s)" % (self.__class__.__name__, params)
diff --git a/src/silx/gui/plot/items/_band_roi.py b/src/silx/gui/plot/items/_band_roi.py
index a60a177..0d2ad4e 100644
--- a/src/silx/gui/plot/items/_band_roi.py
+++ b/src/silx/gui/plot/items/_band_roi.py
@@ -100,7 +100,7 @@ class BandGeometry(NamedTuple):
def slope(self) -> float:
"""Slope of the line (begin, end), infinity for a vertical line"""
if self.begin.x == self.end.x:
- return float('inf')
+ return float("inf")
return (self.end.y - self.begin.y) / (self.end.x - self.begin.x)
@property
@@ -309,18 +309,20 @@ class BandROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
)
@staticmethod
- def __snap(point: Tuple[float, float], fixed: Tuple[float, float]) -> Tuple[float, float]:
+ def __snap(
+ point: Tuple[float, float], fixed: Tuple[float, float]
+ ) -> Tuple[float, float]:
"""Snap point so that vector [point, fixed] snap to direction 0, 45 or 90 degrees
:return: the snapped point position.
"""
vector = point[0] - fixed[0], point[1] - fixed[1]
angle = numpy.arctan2(vector[1], vector[0])
- snapAngle = numpy.pi/4 * numpy.round(angle / (numpy.pi/4))
+ snapAngle = numpy.pi / 4 * numpy.round(angle / (numpy.pi / 4))
length = numpy.linalg.norm(vector)
return (
fixed[0] + length * numpy.cos(snapAngle),
- fixed[1] + length * numpy.sin(snapAngle)
+ fixed[1] + length * numpy.sin(snapAngle),
)
def handleDragUpdated(self, handle, origin, previous, current):
@@ -353,12 +355,16 @@ class BandROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
def __handleWidthUpConstraint(self, x: float, y: float) -> Tuple[float, float]:
geometry = self.getGeometry()
- offset = max(0, numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center))
+ offset = max(
+ 0, numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center)
+ )
return tuple(geometry.center + offset * numpy.array(geometry.normal))
def __handleWidthDownConstraint(self, x: float, y: float) -> Tuple[float, float]:
geometry = self.getGeometry()
- offset = max(0, -numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center))
+ offset = max(
+ 0, -numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center)
+ )
return tuple(geometry.center - offset * numpy.array(geometry.normal))
@docstring(_RegionOfInterestBase)
diff --git a/src/silx/gui/plot/items/_roi_base.py b/src/silx/gui/plot/items/_roi_base.py
index 765a538..43c5381 100644
--- a/src/silx/gui/plot/items/_roi_base.py
+++ b/src/silx/gui/plot/items/_roi_base.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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,14 +37,14 @@ __date__ = "28/06/2018"
import logging
import numpy
import weakref
+import functools
+from typing import Optional
from ....utils.weakref import WeakList
from ... import qt
from .. import items
from ..items import core
from ...colors import rgba
-import silx.utils.deprecation
-from ....utils.proxy import docstring
logger = logging.getLogger(__name__)
@@ -68,8 +68,10 @@ class _RegionOfInterestBase(qt.QObject):
"""
def __init__(self, parent=None):
- qt.QObject.__init__(self, parent=parent)
- self.__name = ''
+ qt.QObject.__init__(self)
+ if parent is not None:
+ self.setParent(parent)
+ self.__name = ""
def getName(self):
"""Returns the name of the ROI
@@ -120,10 +122,12 @@ class RoiInteractionMode(object):
@property
def label(self):
+ """Short name"""
return self._label
@property
def description(self):
+ """Longer description of the interaction mode"""
return self._description
@@ -188,6 +192,28 @@ class InteractionModeMixIn(object):
"""
return self.__modeId
+ def createMenuForInteractionMode(self, parent: qt.QWidget) -> qt.QMenu:
+ """Create a menu providing access to the different interaction modes"""
+ availableModes = self.availableInteractionModes()
+ currentMode = self.getInteractionMode()
+ submenu = qt.QMenu(parent)
+ modeGroup = qt.QActionGroup(parent)
+ modeGroup.setExclusive(True)
+ for mode in availableModes:
+ action = qt.QAction(parent)
+ action.setText(mode.label)
+ action.setToolTip(mode.description)
+ action.setCheckable(True)
+ if mode is currentMode:
+ action.setChecked(True)
+ else:
+ callback = functools.partial(self.setInteractionMode, mode)
+ action.triggered.connect(callback)
+ modeGroup.addAction(action)
+ submenu.addAction(action)
+ submenu.setTitle("Interaction mode")
+ return submenu
+
class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""Object describing a region of interest in a plot.
@@ -196,10 +222,10 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
The RegionOfInterestManager that created this object
"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH = 1.0
"""Default line width of the curve"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE = "-"
"""Default line style of the curve"""
_DEFAULT_HIGHLIGHT_STYLE = items.CurveStyle(linewidth=2)
@@ -225,15 +251,18 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
def __init__(self, parent=None):
# Avoid circular dependency
from ..tools import roi as roi_tools
+
assert parent is None or isinstance(parent, roi_tools.RegionOfInterestManager)
+ # Must be done before _RegionOfInterestBase.__init__
+ self._child = WeakList()
_RegionOfInterestBase.__init__(self, parent)
core.HighlightedMixIn.__init__(self)
- self._color = rgba('red')
+ self.__text = None
+ self._color = rgba("red")
self._editable = False
self._selectable = False
self._focusProxy = None
self._visible = True
- self._child = WeakList()
def _connectToPlot(self, plot):
"""Called after connection to a plot"""
@@ -263,8 +292,11 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""
# Avoid circular dependency
from ..tools import roi as roi_tools
- if (parent is not None and not isinstance(parent, roi_tools.RegionOfInterestManager)):
- raise ValueError('Unsupported parent')
+
+ if parent is not None and not isinstance(
+ parent, roi_tools.RegionOfInterestManager
+ ):
+ raise ValueError("Unsupported parent")
previousParent = self.parent()
if previousParent is not None:
@@ -292,7 +324,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""
assert item is not None
self._child.append(item)
- if item.getName() == '':
+ if item.getName() == "":
self._setItemName(item)
manager = self.parent()
if manager is not None:
@@ -352,26 +384,6 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
self._color = color
self._updated(items.ItemChangedType.COLOR)
- @silx.utils.deprecation.deprecated(reason='API modification',
- replacement='getName()',
- since_version=0.12)
- def getLabel(self):
- """Returns the label displayed for this ROI.
-
- :rtype: str
- """
- return self.getName()
-
- @silx.utils.deprecation.deprecated(reason='API modification',
- replacement='setName(name)',
- since_version=0.12)
- def setLabel(self, label):
- """Set the label displayed with this ROI.
-
- :param str label: The text label to display
- """
- self.setName(name=label)
-
def isEditable(self):
"""Returns whether the ROI is editable by the user or not.
@@ -457,6 +469,26 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
self._visible = visible
self._updated(items.ItemChangedType.VISIBLE)
+ def getText(self) -> str:
+ """Returns the currently displayed text for this ROI"""
+ return self.getName() if self.__text is None else self.__text
+
+ def setText(self, text: Optional[str] = None) -> None:
+ """Set the displayed text for this ROI.
+
+ If None (the default), the ROI name is used.
+ """
+ if self.__text != text:
+ self.__text = text
+ self._updated(items.ItemChangedType.TEXT)
+
+ def _updateText(self, text: str) -> None:
+ """Update the text displayed by this ROI
+
+ Override in subclass to custom text display
+ """
+ pass
+
@classmethod
def showFirstInteractionShape(cls):
"""Returns True if the shape created by the first interaction and
@@ -478,7 +510,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
return cls._plotShape
def setFirstShapePoints(self, points):
- """"Initialize the ROI using the points from the first interaction.
+ """Initialize the ROI using the points from the first interaction.
This interaction is constrained by the plot API and only supports few
shapes.
@@ -486,13 +518,11 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
raise NotImplementedError()
def creationStarted(self):
- """"Called when the ROI creation interaction was started.
- """
+ """Called when the ROI creation interaction was started."""
pass
def creationFinalized(self):
- """"Called when the ROI creation interaction was finalized.
- """
+ """Called when the ROI creation interaction was finalized."""
pass
def _updateItemProperty(self, event, source, destination):
@@ -544,15 +574,23 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
assert False
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.HIGHLIGHTED:
+ if event == items.ItemChangedType.TEXT:
+ self._updateText(self.getText())
+ elif event == items.ItemChangedType.HIGHLIGHTED:
+ for item in self.getItems():
+ zoffset = 1000 if self.isHighlighted() else 0
+ item.setZValue(item._DEFAULT_Z_LAYER + zoffset)
+
style = self.getCurrentStyle()
self._updatedStyle(event, style)
else:
- styleEvents = [items.ItemChangedType.COLOR,
- items.ItemChangedType.LINE_STYLE,
- items.ItemChangedType.LINE_WIDTH,
- items.ItemChangedType.SYMBOL,
- items.ItemChangedType.SYMBOL_SIZE]
+ styleEvents = [
+ items.ItemChangedType.COLOR,
+ items.ItemChangedType.LINE_STYLE,
+ items.ItemChangedType.LINE_WIDTH,
+ items.ItemChangedType.SYMBOL,
+ items.ItemChangedType.SYMBOL_SIZE,
+ ]
if self.isHighlighted():
styleEvents.append(items.ItemChangedType.HIGHLIGHTED_STYLE)
@@ -562,7 +600,11 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
super(RegionOfInterest, self)._updated(event, checkVisibility)
- def _updatedStyle(self, event, style):
+ # Displayed text has changed, send a text event
+ if event == items.ItemChangedType.NAME and self.__text is None:
+ self._updated(items.ItemChangedType.TEXT, checkVisibility)
+
+ def _updatedStyle(self, event, style: items.CurveStyle):
"""Called when the current displayed style of the ROI was changed.
:param event: The event responsible of the change of the style
@@ -570,7 +612,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""
pass
- def getCurrentStyle(self):
+ def getCurrentStyle(self) -> items.CurveStyle:
"""Returns the current curve style.
Curve style depends on curve highlighting
@@ -588,7 +630,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
baseSymbol = self.getSymbol()
baseSymbolsize = self.getSymbolSize()
else:
- baseSymbol = 'o'
+ baseSymbol = "o"
baseSymbolsize = 1
if self.isHighlighted():
@@ -604,13 +646,16 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
linestyle=baseLinestyle if linestyle is None else linestyle,
linewidth=baseLinewidth if linewidth is None else linewidth,
symbol=baseSymbol if symbol is None else symbol,
- symbolsize=baseSymbolsize if symbolsize is None else symbolsize)
+ symbolsize=baseSymbolsize if symbolsize is None else symbolsize,
+ )
else:
- return items.CurveStyle(color=baseColor,
- linestyle=baseLinestyle,
- linewidth=baseLinewidth,
- symbol=baseSymbol,
- symbolsize=baseSymbolsize)
+ return items.CurveStyle(
+ color=baseColor,
+ linestyle=baseLinestyle,
+ linewidth=baseLinewidth,
+ symbol=baseSymbol,
+ symbolsize=baseSymbolsize,
+ )
def _editingStarted(self):
assert self._editable is True
@@ -619,6 +664,10 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
def _editingFinished(self):
self.sigEditingFinished.emit()
+ def populateContextMenu(self, menu: qt.QMenu):
+ """Populate a menu used as a context menu"""
+ pass
+
class HandleBasedROI(RegionOfInterest):
"""Manage a ROI based on a set of handles"""
@@ -730,9 +779,7 @@ class HandleBasedROI(RegionOfInterest):
See :class:`~silx.gui.plot.items.Item._updated`
"""
- if event == items.ItemChangedType.NAME:
- self._updateText(self.getName())
- elif event == items.ItemChangedType.VISIBLE:
+ if event == items.ItemChangedType.VISIBLE:
for item, role in self._handles:
visible = self.isVisible()
editionVisible = visible and self.isEditable()
@@ -754,9 +801,9 @@ class HandleBasedROI(RegionOfInterest):
color = rgba(self.getColor())
handleColor = self._computeHandleColor(color)
for item, role in self._handles:
- if role == 'user':
+ if role == "user":
pass
- elif role == 'label':
+ elif role == "label":
item.setColor(color)
else:
item.setColor(handleColor)
@@ -825,10 +872,3 @@ class HandleBasedROI(RegionOfInterest):
:rtype: Union[numpy.array,Tuple,List]
"""
return color[:3] + (0.5,)
-
- def _updateText(self, text):
- """Update the text displayed by this ROI
-
- :param str text: A text
- """
- pass
diff --git a/src/silx/gui/plot/items/axis.py b/src/silx/gui/plot/items/axis.py
index fa3f6d7..1ae1ef1 100644
--- a/src/silx/gui/plot/items/axis.py
+++ b/src/silx/gui/plot/items/axis.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -24,28 +24,28 @@
"""This module provides the class for axes of the :class:`PlotWidget`.
"""
+from __future__ import annotations
+
__authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "22/11/2018"
import datetime as dt
import enum
-import logging
+from typing import Optional
import dateutil.tz
-import numpy
+from ....utils.proxy import docstring
from ... import qt
from .. import _utils
-_logger = logging.getLogger(__name__)
-
-
class TickMode(enum.Enum):
"""Determines if ticks are regular number or datetimes."""
- DEFAULT = 0 # Ticks are regular numbers
- TIME_SERIES = 1 # Ticks are datetime objects
+
+ DEFAULT = 0 # Ticks are regular numbers
+ TIME_SERIES = 1 # Ticks are datetime objects
class Axis(qt.QObject):
@@ -53,6 +53,7 @@ class Axis(qt.QObject):
Note: This is an abstract class.
"""
+
# States are half-stored on the backend of the plot, and half-stored on this
# object.
# TODO It would be good to store all the states of an axis in this object.
@@ -91,10 +92,10 @@ class Axis(qt.QObject):
self._scale = self.LINEAR
self._isAutoScale = True
# Store default labels provided to setGraph[X|Y]Label
- self._defaultLabel = ''
+ self._defaultLabel = ""
# Store currently displayed labels
# Current label can differ from input one with active curve handling
- self._currentLabel = ''
+ self._currentLabel = ""
def _getPlot(self):
"""Returns the PlotWidget this Axis belongs to.
@@ -150,7 +151,12 @@ class Axis(qt.QObject):
:rtype: 2-tuple of float
"""
return _utils.checkAxisLimits(
- vmin, vmax, isLog=self._isLogarithmic(), name=self._defaultLabel)
+ vmin, vmax, isLog=self._isLogarithmic(), name=self._defaultLabel
+ )
+
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ """Returns the range of data items over this axis as (vmin, vmax)"""
+ raise NotImplementedError()
def isInverted(self):
"""Return True if the axis is inverted (top to bottom for the y-axis),
@@ -172,6 +178,10 @@ class Axis(qt.QObject):
return
raise NotImplementedError()
+ def isVisible(self) -> bool:
+ """Returns whether the axis is displayed or not"""
+ return True
+
def getLabel(self):
"""Return the current displayed label of this axis.
@@ -199,10 +209,10 @@ class Axis(qt.QObject):
:param str label: Currently displayed label
"""
- if label is None or label == '':
+ if label is None or label == "":
label = self._defaultLabel
if label is None:
- label = ''
+ label = ""
self._currentLabel = label
self._internalSetCurrentLabel(label)
@@ -218,7 +228,7 @@ class Axis(qt.QObject):
:param str scale: Name of the scale ("log", or "linear")
"""
- assert(scale in self._SCALES)
+ assert scale in self._SCALES
if self._scale == scale:
return
@@ -227,6 +237,8 @@ class Axis(qt.QObject):
self._scale = scale
+ vmin, vmax = self.getLimits()
+
# TODO hackish way of forcing update of curves and images
plot = self._getPlot()
for item in plot.getItems():
@@ -235,13 +247,20 @@ class Axis(qt.QObject):
if scale == self.LOGARITHMIC:
self._internalSetLogarithmic(True)
+ if vmin <= 0:
+ dataRange = self._getDataRange()
+ if dataRange is None:
+ self.setLimits(1.0, 100.0)
+ else:
+ if vmax > 0 and dataRange[0] < vmax:
+ self.setLimits(dataRange[0], vmax)
+ else:
+ self.setLimits(*dataRange)
elif scale == self.LINEAR:
self._internalSetLogarithmic(False)
else:
raise ValueError("Scale %s unsupported" % scale)
- plot._forceResetZoom()
-
self.sigScaleChanged.emit(self._scale)
if emitLog:
self._sigLogarithmicChanged.emit(self._scale == self.LOGARITHMIC)
@@ -328,7 +347,7 @@ class Axis(qt.QObject):
plot = self._getPlot()
xMin, xMax = plot.getXAxis().getLimits()
yMin, yMax = plot.getYAxis().getLimits()
- y2Min, y2Max = plot.getYAxis('right').getLimits()
+ y2Min, y2Max = plot.getYAxis("right").getLimits()
plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
return updated
@@ -351,7 +370,7 @@ class Axis(qt.QObject):
plot = self._getPlot()
xMin, xMax = plot.getXAxis().getLimits()
yMin, yMax = plot.getYAxis().getLimits()
- y2Min, y2Max = plot.getYAxis('right').getLimits()
+ y2Min, y2Max = plot.getYAxis("right").getLimits()
plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
return updated
@@ -368,7 +387,7 @@ class XAxis(Axis):
def setTimeZone(self, tz):
if isinstance(tz, str) and tz.upper() == "UTC":
tz = dateutil.tz.tzutc()
- elif not(tz is None or isinstance(tz, dt.tzinfo)):
+ elif not (tz is None or isinstance(tz, dt.tzinfo)):
raise TypeError("tz must be a dt.tzinfo object, None or 'UTC'.")
self._getBackend().setXAxisTimeZone(tz)
@@ -410,6 +429,11 @@ class XAxis(Axis):
updated = constrains.update(minXRange=minRange, maxXRange=maxRange)
return updated
+ @docstring(Axis)
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ ranges = self._getPlot().getDataRange()
+ return ranges.x
+
class YAxis(Axis):
"""Axis class defining primitives for the Y axis"""
@@ -418,13 +442,13 @@ class YAxis(Axis):
# specialised implementations (prefixel by '_internal')
def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphYLabel(label, axis='left')
+ self._getBackend().setGraphYLabel(label, axis="left")
def _internalGetLimits(self):
- return self._getBackend().getGraphYLimits(axis='left')
+ return self._getBackend().getGraphYLimits(axis="left")
def _internalSetLimits(self, ymin, ymax):
- self._getBackend().setGraphYLimits(ymin, ymax, axis='left')
+ self._getBackend().setGraphYLimits(ymin, ymax, axis="left")
def _internalSetLogarithmic(self, flag):
self._getBackend().setYAxisLogarithmic(flag)
@@ -462,6 +486,11 @@ class YAxis(Axis):
updated = constrains.update(minYRange=minRange, maxYRange=maxRange)
return updated
+ @docstring(Axis)
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ ranges = self._getPlot().getDataRange()
+ return ranges.y
+
class YRightAxis(Axis):
"""Proxy axis for the secondary Y axes. It manages it own label and limit
@@ -485,13 +514,13 @@ class YRightAxis(Axis):
self.__mainAxis.sigAutoScaleChanged.connect(self.sigAutoScaleChanged.emit)
def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphYLabel(label, axis='right')
+ self._getBackend().setGraphYLabel(label, axis="right")
def _internalGetLimits(self):
- return self._getBackend().getGraphYLimits(axis='right')
+ return self._getBackend().getGraphYLimits(axis="right")
def _internalSetLimits(self, ymin, ymax):
- self._getBackend().setGraphYLimits(ymin, ymax, axis='right')
+ self._getBackend().setGraphYLimits(ymin, ymax, axis="right")
def setInverted(self, flag=True):
"""Set the Y axis orientation.
@@ -505,6 +534,10 @@ class YRightAxis(Axis):
"""Return True if Y axis goes from top to bottom, False otherwise."""
return self.__mainAxis.isInverted()
+ def isVisible(self) -> bool:
+ """Returns whether the axis is displayed or not"""
+ return self._getBackend().isYRightAxisVisible()
+
def getScale(self):
"""Return the name of the scale used by this axis.
@@ -541,3 +574,8 @@ class YRightAxis(Axis):
False to disable it.
"""
return self.__mainAxis.setAutoScale(flag)
+
+ @docstring(Axis)
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ ranges = self._getPlot().getDataRange()
+ return ranges.y2
diff --git a/src/silx/gui/plot/items/complex.py b/src/silx/gui/plot/items/complex.py
index 82d821f..d10767f 100644
--- a/src/silx/gui/plot/items/complex.py
+++ b/src/silx/gui/plot/items/complex.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -34,7 +34,6 @@ import logging
import numpy
from ....utils.proxy import docstring
-from ....utils.deprecation import deprecated
from ...colors import Colormap
from .core import ColormapMixIn, ComplexMixIn, ItemChangedType
from .image import ImageBase
@@ -45,6 +44,7 @@ _logger = logging.getLogger(__name__)
# Complex colormap functions
+
def _phase2rgb(colormap, data):
"""Creates RGBA image with colour-coded phase.
@@ -60,7 +60,7 @@ def _phase2rgb(colormap, data):
return colormap.applyToData(phase)
-def _complex2rgbalog(phaseColormap, data, amin=0., dlogs=2, smax=None):
+def _complex2rgbalog(phaseColormap, data, amin=0.0, dlogs=2, smax=None):
"""Returns RGBA colors: colour-coded phases and log10(amplitude) in alpha.
:param Colormap phaseColormap: Colormap to use for the phase
@@ -117,7 +117,8 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
ComplexMixIn.ComplexMode.IMAGINARY,
ComplexMixIn.ComplexMode.AMPLITUDE_PHASE,
ComplexMixIn.ComplexMode.LOG10_AMPLITUDE_PHASE,
- ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE)
+ ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE,
+ )
"""Overrides supported ComplexMode"""
def __init__(self):
@@ -130,10 +131,7 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
# Use default from ColormapMixIn
colormap = super(ImageComplexData, self).getColormap()
- phaseColormap = Colormap(
- name='hsv',
- vmin=-numpy.pi,
- vmax=numpy.pi)
+ phaseColormap = Colormap(name="hsv", vmin=-numpy.pi, vmax=numpy.pi)
self._colormaps = { # Default colormaps for all modes
self.ComplexMode.ABSOLUTE: colormap,
@@ -154,8 +152,10 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
return None
mode = self.getComplexMode()
- if mode in (self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
+ if mode in (
+ self.ComplexMode.AMPLITUDE_PHASE,
+ self.ComplexMode.LOG10_AMPLITUDE_PHASE,
+ ):
# For those modes, compute RGBA image here
colormap = None
data = self.getRgbaImageData(copy=False)
@@ -171,11 +171,13 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
if data.size == 0:
return None # No data to display
- return backend.addImage(data,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=colormap,
- alpha=self.getAlpha())
+ return backend.addImage(
+ data,
+ origin=self.getOrigin(),
+ scale=self.getScale(),
+ colormap=colormap,
+ alpha=self.getAlpha(),
+ )
@docstring(ComplexMixIn)
def setComplexMode(self, mode):
@@ -247,7 +249,7 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
return self._colormaps[mode]
def setData(self, data, copy=True):
- """"Set the image complex data
+ """Set the image complex data
:param numpy.ndarray data: 2D array of complex with 2 dimensions (h, w)
:param bool copy: True (Default) to get a copy,
@@ -257,7 +259,8 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
assert data.ndim == 2
if not numpy.issubdtype(data.dtype, numpy.complexfloating):
_logger.warning(
- 'Image is not complex, converting it to complex to plot it.')
+ "Image is not complex, converting it to complex to plot it."
+ )
data = numpy.array(data, dtype=numpy.complex64)
# Compute current mode data and set colormap data
@@ -274,8 +277,9 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
if event in (ItemChangedType.DATA, ItemChangedType.MASK):
# Color-mapped data is NOT the `getValueData` for some modes
if self.getComplexMode() in (
- self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
+ self.ComplexMode.AMPLITUDE_PHASE,
+ self.ComplexMode.LOG10_AMPLITUDE_PHASE,
+ ):
data = self.getData(copy=False, mode=self.ComplexMode.PHASE)
mask = self.getMaskData(copy=False)
if mask is not None:
@@ -308,16 +312,18 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
return numpy.real(data)
elif mode is self.ComplexMode.IMAGINARY:
return numpy.imag(data)
- elif mode in (self.ComplexMode.ABSOLUTE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE,
- self.ComplexMode.AMPLITUDE_PHASE):
+ elif mode in (
+ self.ComplexMode.ABSOLUTE,
+ self.ComplexMode.LOG10_AMPLITUDE_PHASE,
+ self.ComplexMode.AMPLITUDE_PHASE,
+ ):
return numpy.absolute(data)
elif mode is self.ComplexMode.SQUARE_AMPLITUDE:
return numpy.absolute(data) ** 2
else:
_logger.error(
- 'Unsupported conversion mode: %s, fallback to absolute',
- str(mode))
+ "Unsupported conversion mode: %s, fallback to absolute", str(mode)
+ )
return numpy.absolute(data)
def getData(self, copy=True, mode=None):
@@ -340,7 +346,8 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
if mode not in self._dataByModesCache:
self._dataByModesCache[mode] = self.__convertComplexData(
- self.getComplexData(copy=False), mode)
+ self.getComplexData(copy=False), mode
+ )
return numpy.array(self._dataByModesCache[mode], copy=copy)
@@ -373,11 +380,3 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
# Backward compatibility
Mode = ComplexMixIn.ComplexMode
-
- @deprecated(replacement='setComplexMode', since_version='0.11.0')
- def setVisualizationMode(self, mode):
- return self.setComplexMode(mode)
-
- @deprecated(replacement='getComplexMode', since_version='0.11.0')
- def getVisualizationMode(self):
- return self.getComplexMode()
diff --git a/src/silx/gui/plot/items/core.py b/src/silx/gui/plot/items/core.py
index 074c168..7d754a7 100644
--- a/src/silx/gui/plot/items/core.py
+++ b/src/silx/gui/plot/items/core.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -23,16 +23,14 @@
# ###########################################################################*/
"""This module provides the base class for items of the :class:`Plot`.
"""
+from __future__ import annotations
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "08/12/2020"
-import collections
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
from copy import deepcopy
import logging
import enum
@@ -41,13 +39,12 @@ import weakref
import numpy
-from ....utils.deprecation import deprecated
from ....utils.proxy import docstring
from ....utils.enum import Enum as _Enum
from ....math.combo import min_max
from ... import qt
from ... import colors
-from ...colors import Colormap
+from ...colors import Colormap, _Colormappable
from ._pick import PickingResult
from silx import config
@@ -58,98 +55,109 @@ _logger = logging.getLogger(__name__)
@enum.unique
class ItemChangedType(enum.Enum):
"""Type of modification provided by :attr:`Item.sigItemChanged` signal."""
+
# Private setters and setInfo are not emitting sigItemChanged signal.
# Signals to consider:
# COLORMAP_SET emitted when setColormap is called but not forward colormap object signal
# CURRENT_COLOR_CHANGED emitted current color changed because highlight changed,
# highlighted color changed or color changed depending on hightlight state.
- VISIBLE = 'visibleChanged'
+ VISIBLE = "visibleChanged"
"""Item's visibility changed flag."""
- ZVALUE = 'zValueChanged'
+ ZVALUE = "zValueChanged"
"""Item's Z value changed flag."""
- COLORMAP = 'colormapChanged' # Emitted when set + forward events from the colormap object
+ COLORMAP = (
+ "colormapChanged" # Emitted when set + forward events from the colormap object
+ )
"""Item's colormap changed flag.
This is emitted both when setting a new colormap and
when the current colormap object is updated.
"""
- SYMBOL = 'symbolChanged'
+ SYMBOL = "symbolChanged"
"""Item's symbol changed flag."""
- SYMBOL_SIZE = 'symbolSizeChanged'
+ SYMBOL_SIZE = "symbolSizeChanged"
"""Item's symbol size changed flag."""
- LINE_WIDTH = 'lineWidthChanged'
+ LINE_WIDTH = "lineWidthChanged"
"""Item's line width changed flag."""
- LINE_STYLE = 'lineStyleChanged'
+ LINE_STYLE = "lineStyleChanged"
"""Item's line style changed flag."""
- COLOR = 'colorChanged'
+ COLOR = "colorChanged"
"""Item's color changed flag."""
- LINE_BG_COLOR = 'lineBgColorChanged'
- """Item's line background color changed flag."""
+ LINE_BG_COLOR = "lineBgColorChanged" # Deprecated, use LINE_GAP_COLOR
+
+ LINE_GAP_COLOR = "lineGapColorChanged"
+ """Item's dashed line gap color changed flag."""
- YAXIS = 'yAxisChanged'
+ YAXIS = "yAxisChanged"
"""Item's Y axis binding changed flag."""
- FILL = 'fillChanged'
+ FILL = "fillChanged"
"""Item's fill changed flag."""
- ALPHA = 'alphaChanged'
+ ALPHA = "alphaChanged"
"""Item's transparency alpha changed flag."""
- DATA = 'dataChanged'
+ DATA = "dataChanged"
"""Item's data changed flag"""
- MASK = 'maskChanged'
+ MASK = "maskChanged"
"""Item's mask changed flag"""
- HIGHLIGHTED = 'highlightedChanged'
+ HIGHLIGHTED = "highlightedChanged"
"""Item's highlight state changed flag."""
- HIGHLIGHTED_COLOR = 'highlightedColorChanged'
+ HIGHLIGHTED_COLOR = "highlightedColorChanged"
"""Deprecated, use HIGHLIGHTED_STYLE instead."""
- HIGHLIGHTED_STYLE = 'highlightedStyleChanged'
+ HIGHLIGHTED_STYLE = "highlightedStyleChanged"
"""Item's highlighted style changed flag."""
- SCALE = 'scaleChanged'
+ SCALE = "scaleChanged"
"""Item's scale changed flag."""
- TEXT = 'textChanged'
+ TEXT = "textChanged"
"""Item's text changed flag."""
- POSITION = 'positionChanged'
+ POSITION = "positionChanged"
"""Item's position changed flag.
This is emitted when a marker position changed and
when an image origin changed.
"""
- OVERLAY = 'overlayChanged'
+ OVERLAY = "overlayChanged"
"""Item's overlay state changed flag."""
- VISUALIZATION_MODE = 'visualizationModeChanged'
+ VISUALIZATION_MODE = "visualizationModeChanged"
"""Item's visualization mode changed flag."""
- COMPLEX_MODE = 'complexModeChanged'
+ COMPLEX_MODE = "complexModeChanged"
"""Item's complex data visualization mode changed flag."""
- NAME = 'nameChanged'
+ NAME = "nameChanged"
"""Item's name changed flag."""
- EDITABLE = 'editableChanged'
+ EDITABLE = "editableChanged"
"""Item's editable state changed flags."""
- SELECTABLE = 'selectableChanged'
+ SELECTABLE = "selectableChanged"
"""Item's selectable state changed flags."""
+ FONT = "fontChanged"
+ """Item's text font changed flag."""
+
+ BACKGROUND_COLOR = "backgroundColorChanged"
+ """Item's text background color changed flag."""
+
class Item(qt.QObject):
"""Description of an item of the plot"""
@@ -184,7 +192,7 @@ class Item(qt.QObject):
self._info = None
self._xlabel = None
self._ylabel = None
- self.__name = ''
+ self.__name = ""
self.__visibleBoundsTracking = False
self.__previousVisibleBounds = None
@@ -206,7 +214,7 @@ class Item(qt.QObject):
:param Union[~silx.gui.plot.PlotWidget,None] plot: The Plot instance.
"""
if plot is not None and self._plotRef is not None:
- raise RuntimeError('Trying to add a node at two places.')
+ raise RuntimeError("Trying to add a node at two places.")
self.__disconnectFromPlotWidget()
self._plotRef = None if plot is None else weakref.ref(plot)
self.__connectToPlotWidget()
@@ -240,8 +248,7 @@ class Item(qt.QObject):
if visible != self._visible:
self._visible = visible
# When visibility has changed, always mark as dirty
- self._updated(ItemChangedType.VISIBLE,
- checkVisibility=False)
+ self._updated(ItemChangedType.VISIBLE, checkVisibility=False)
if visible:
self._visibleBoundsChanged()
@@ -268,8 +275,7 @@ class Item(qt.QObject):
name = str(name)
if self.__name != name:
if self.getPlot() is not None:
- raise RuntimeError(
- "Cannot change name while item is in a PlotWidget")
+ raise RuntimeError("Cannot change name while item is in a PlotWidget")
self.__name = name
self._updated(ItemChangedType.NAME)
@@ -277,11 +283,6 @@ class Item(qt.QObject):
def getLegend(self): # Replaced by getName for API consistency
return self.getName()
- @deprecated(replacement='setName', since_version='0.13')
- def _setLegend(self, legend):
- legend = str(legend) if legend is not None else ''
- self.setName(legend)
-
def isSelectable(self):
"""Returns true if item is selectable (bool)"""
return self._selectable
@@ -332,7 +333,8 @@ class Item(qt.QObject):
xmin, xmax = numpy.clip(bounds[:2], *plot.getXAxis().getLimits())
ymin, ymax = numpy.clip(
- bounds[2:], *plot.getYAxis(self.__getYAxis()).getLimits())
+ bounds[2:], *plot.getYAxis(self.__getYAxis()).getLimits()
+ )
if xmin == xmax or ymin == ymax: # Outside the plot area
return None
@@ -360,7 +362,7 @@ class Item(qt.QObject):
def __getYAxis(self) -> str:
"""Returns current Y axis ('left' or 'right')"""
- return self.getYAxis() if isinstance(self, YAxisMixIn) else 'left'
+ return self.getYAxis() if isinstance(self, YAxisMixIn) else "left"
def __connectToPlotWidget(self) -> None:
"""Connect to PlotWidget signals and install event filter"""
@@ -486,7 +488,7 @@ class Item(qt.QObject):
class DataItem(Item):
"""Item with a data extent in the plot"""
- def _boundsChanged(self, checkVisibility: bool=True) -> None:
+ def _boundsChanged(self, checkVisibility: bool = True) -> None:
"""Call this method in subclass when data bounds has changed.
:param bool checkVisibility:
@@ -506,6 +508,7 @@ class DataItem(Item):
self._boundsChanged(checkVisibility=False)
super().setVisible(visible)
+
# Mix-in classes ##############################################################
@@ -522,8 +525,7 @@ class ItemMixInBase(object):
:param bool checkVisibility: True to only mark as dirty if visible,
False to always mark as dirty.
"""
- raise RuntimeError(
- "Issue with Mix-In class inheritance order")
+ raise RuntimeError("Issue with Mix-In class inheritance order")
class LabelsMixIn(ItemMixInBase):
@@ -597,7 +599,7 @@ class DraggableMixIn(ItemMixInBase):
raise NotImplementedError("Must be implemented in subclass")
-class ColormapMixIn(ItemMixInBase):
+class ColormapMixIn(_Colormappable, ItemMixInBase):
"""Mix-in class for items with colormap"""
def __init__(self):
@@ -631,8 +633,9 @@ class ColormapMixIn(ItemMixInBase):
"""Handle updates of the colormap"""
self._updated(ItemChangedType.COLORMAP)
- def _setColormappedData(self, data, copy=True,
- min_=None, minPositive=None, max_=None):
+ def _setColormappedData(
+ self, data, copy=True, min_=None, minPositive=None, max_=None
+ ):
"""Set the data used to compute the colormapped display.
It also resets the cache of data ranges.
@@ -653,7 +656,10 @@ class ColormapMixIn(ItemMixInBase):
if min_ is not None and numpy.isfinite(min_):
self.__cacheColormapRange[Colormap.LINEAR, Colormap.MINMAX] = min_, max_
if minPositive is not None and numpy.isfinite(minPositive):
- self.__cacheColormapRange[Colormap.LOGARITHM, Colormap.MINMAX] = minPositive, max_
+ self.__cacheColormapRange[Colormap.LOGARITHM, Colormap.MINMAX] = (
+ minPositive,
+ max_,
+ )
colormap = self.getColormap()
if None in (colormap.getVMin(), colormap.getVMax()):
@@ -705,26 +711,29 @@ class SymbolMixIn(ItemMixInBase):
_DEFAULT_SYMBOL_SIZE = config.DEFAULT_PLOT_SYMBOL_SIZE
"""Default marker size of the item"""
- _SUPPORTED_SYMBOLS = collections.OrderedDict((
- ('o', 'Circle'),
- ('d', 'Diamond'),
- ('s', 'Square'),
- ('+', 'Plus'),
- ('x', 'Cross'),
- ('.', 'Point'),
- (',', 'Pixel'),
- ('|', 'Vertical line'),
- ('_', 'Horizontal line'),
- ('tickleft', 'Tick left'),
- ('tickright', 'Tick right'),
- ('tickup', 'Tick up'),
- ('tickdown', 'Tick down'),
- ('caretleft', 'Caret left'),
- ('caretright', 'Caret right'),
- ('caretup', 'Caret up'),
- ('caretdown', 'Caret down'),
- (u'\u2665', 'Heart'),
- ('', 'None')))
+ _SUPPORTED_SYMBOLS = dict(
+ (
+ ("o", "Circle"),
+ ("d", "Diamond"),
+ ("s", "Square"),
+ ("+", "Plus"),
+ ("x", "Cross"),
+ (".", "Point"),
+ (",", "Pixel"),
+ ("|", "Vertical line"),
+ ("_", "Horizontal line"),
+ ("tickleft", "Tick left"),
+ ("tickright", "Tick right"),
+ ("tickup", "Tick up"),
+ ("tickdown", "Tick down"),
+ ("caretleft", "Caret left"),
+ ("caretright", "Caret right"),
+ ("caretup", "Caret up"),
+ ("caretdown", "Caret down"),
+ ("\u2665", "Heart"),
+ ("", "None"),
+ )
+ )
"""Dict of supported symbols"""
def __init__(self):
@@ -799,7 +808,7 @@ class SymbolMixIn(ItemMixInBase):
symbol = symbolCode
break
else:
- raise ValueError('Unsupported symbol %s' % str(symbol))
+ raise ValueError("Unsupported symbol %s" % str(symbol))
if symbol != self._symbol:
self._symbol = symbol
@@ -826,50 +835,74 @@ class SymbolMixIn(ItemMixInBase):
self._updated(ItemChangedType.SYMBOL_SIZE)
+LineStyleType = Union[
+ str,
+ Tuple[Union[float, int], None],
+ Tuple[Union[float, int], Tuple[Union[float, int], Union[float, int]]],
+ Tuple[Union[float, int], Tuple[Union[float, int], Union[float, int], Union[float, int], Union[float, int]]],
+]
+"""Type for :class:`LineMixIn`'s line style"""
+
+
class LineMixIn(ItemMixInBase):
"""Mix-in class for item with line"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH: float = 1.0
"""Default line width"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE: LineStyleType = "-"
"""Default line style"""
- _SUPPORTED_LINESTYLE = '', ' ', '-', '--', '-.', ':', None
+ _SUPPORTED_LINESTYLE = "", " ", "-", "--", "-.", ":", None
"""Supported line styles"""
def __init__(self):
- self._linewidth = self._DEFAULT_LINEWIDTH
- self._linestyle = self._DEFAULT_LINESTYLE
+ self._linewidth: float = self._DEFAULT_LINEWIDTH
+ self._linestyle: LineStyleType = self._DEFAULT_LINESTYLE
@classmethod
- def getSupportedLineStyles(cls):
- """Returns list of supported line styles.
-
- :rtype: List[str,None]
- """
+ def getSupportedLineStyles(cls) -> tuple[str | None]:
+ """Returns list of supported constant line styles."""
return cls._SUPPORTED_LINESTYLE
- def getLineWidth(self):
- """Return the curve line width in pixels
-
- :rtype: float
- """
+ def getLineWidth(self) -> float:
+ """Return the curve line width in pixels"""
return self._linewidth
- def setLineWidth(self, width):
+ def setLineWidth(self, width: float):
"""Set the width in pixel of the curve line
See :meth:`getLineWidth`.
-
- :param float width: Width in pixels
"""
width = float(width)
if width != self._linewidth:
self._linewidth = width
self._updated(ItemChangedType.LINE_WIDTH)
- def getLineStyle(self):
+ @classmethod
+ def isValidLineStyle(cls, style: LineStyleType | None) -> bool:
+ """Returns True for valid styles"""
+ if style is None or style in cls.getSupportedLineStyles():
+ return True
+ if not isinstance(style, tuple):
+ return False
+ if (
+ len(style) == 2
+ and isinstance(style[0], (float, int))
+ and (
+ style[1] is None
+ or style[1] == ()
+ or (
+ isinstance(style[1], tuple)
+ and len(style[1]) in (2, 4)
+ and all(map(lambda item: isinstance(item, (float, int)), style[1]))
+ )
+ )
+ ):
+ return True
+ return False
+
+ def getLineStyle(self) -> LineStyleType:
"""Return the type of the line
Type of line::
@@ -879,20 +912,19 @@ class LineMixIn(ItemMixInBase):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
-
- :rtype: str
+ - (offset, (dash pattern))
"""
return self._linestyle
- def setLineStyle(self, style):
+ def setLineStyle(self, style: LineStyleType | None):
"""Set the style of the curve line.
See :meth:`getLineStyle`.
- :param str style: Line style
+ :param style: Line style
"""
- style = str(style)
- assert style in self.getSupportedLineStyles()
+ if not self.isValidLineStyle(style):
+ raise ValueError(f"No a valid line style: {style}")
if style is None:
style = self._DEFAULT_LINESTYLE
if style != self._linestyle:
@@ -903,7 +935,7 @@ class LineMixIn(ItemMixInBase):
class ColorMixIn(ItemMixInBase):
"""Mix-in class for item with color"""
- _DEFAULT_COLOR = (0., 0., 0., 1.)
+ _DEFAULT_COLOR = (0.0, 0.0, 0.0, 1.0)
"""Default color of the item"""
def __init__(self):
@@ -941,10 +973,43 @@ class ColorMixIn(ItemMixInBase):
self._updated(ItemChangedType.COLOR)
+class LineGapColorMixIn(ItemMixInBase):
+ """Mix-in class for dashed line gap color"""
+
+ _DEFAULT_LINE_GAP_COLOR = None
+ """Default dashed line gap color of the item"""
+
+ def __init__(self):
+ self.__lineGapColor = self._DEFAULT_LINE_GAP_COLOR
+
+ def getLineGapColor(self):
+ """Returns the RGBA color of dashed line gap of the item
+
+ :rtype: 4-tuple of float in [0, 1] or None
+ """
+ return self.__lineGapColor
+
+ def setLineGapColor(self, color):
+ """Set dashed line gap color
+
+ It supports:
+ - color names: e.g., 'green'
+ - color codes: '#RRGGBB' and '#RRGGBBAA'
+ - indexed color names: e.g., 'C0'
+ - RGB(A) sequence of uint8 in [0, 255] or float in [0, 1]
+ - QColor
+
+ :param color: line background color to be used
+ :type color: Union[str, List[int], List[float], QColor, None]
+ """
+ self.__lineGapColor = None if color is None else colors.rgba(color)
+ self._updated(ItemChangedType.LINE_GAP_COLOR)
+
+
class YAxisMixIn(ItemMixInBase):
"""Mix-in class for item with yaxis"""
- _DEFAULT_YAXIS = 'left'
+ _DEFAULT_YAXIS = "left"
"""Default Y axis the item belongs to"""
def __init__(self):
@@ -965,7 +1030,7 @@ class YAxisMixIn(ItemMixInBase):
:param str yaxis: 'left' or 'right'
"""
yaxis = str(yaxis)
- assert yaxis in ('left', 'right')
+ assert yaxis in ("left", "right")
if yaxis != self._yaxis:
self._yaxis = yaxis
# Handle data extent changed for DataItem
@@ -977,11 +1042,13 @@ class YAxisMixIn(ItemMixInBase):
# Switch Y axis signal connection
plot = self.getPlot()
if plot is not None:
- previousYAxis = 'left' if self.getXAxis() == 'right' else 'right'
+ previousYAxis = "left" if self.getXAxis() == "right" else "right"
plot.getYAxis(previousYAxis).sigLimitsChanged.disconnect(
- self._visibleBoundsChanged)
+ self._visibleBoundsChanged
+ )
plot.getYAxis(self.getYAxis()).sigLimitsChanged.connect(
- self._visibleBoundsChanged)
+ self._visibleBoundsChanged
+ )
self._visibleBoundsChanged()
self._updated(ItemChangedType.YAXIS)
@@ -1015,7 +1082,7 @@ class AlphaMixIn(ItemMixInBase):
"""Mix-in class for item with opacity"""
def __init__(self):
- self._alpha = 1.
+ self._alpha = 1.0
def getAlpha(self):
"""Returns the opacity of the item
@@ -1038,7 +1105,7 @@ class AlphaMixIn(ItemMixInBase):
:type alpha: float
"""
alpha = float(alpha)
- alpha = max(0., min(alpha, 1.)) # Clip alpha to [0., 1.] range
+ alpha = max(0.0, min(alpha, 1.0)) # Clip alpha to [0., 1.] range
if alpha != self._alpha:
self._alpha = alpha
self._updated(ItemChangedType.ALPHA)
@@ -1052,14 +1119,15 @@ class ComplexMixIn(ItemMixInBase):
class ComplexMode(_Enum):
"""Identify available display mode for complex"""
- NONE = 'none'
- ABSOLUTE = 'amplitude'
- PHASE = 'phase'
- REAL = 'real'
- IMAGINARY = 'imaginary'
- AMPLITUDE_PHASE = 'amplitude_phase'
- LOG10_AMPLITUDE_PHASE = 'log10_amplitude_phase'
- SQUARE_AMPLITUDE = 'square_amplitude'
+
+ NONE = "none"
+ ABSOLUTE = "amplitude"
+ PHASE = "phase"
+ REAL = "real"
+ IMAGINARY = "imaginary"
+ AMPLITUDE_PHASE = "amplitude_phase"
+ LOG10_AMPLITUDE_PHASE = "log10_amplitude_phase"
+ SQUARE_AMPLITUDE = "square_amplitude"
def __init__(self):
self.__complex_mode = self.ComplexMode.ABSOLUTE
@@ -1115,7 +1183,7 @@ class ComplexMixIn(ItemMixInBase):
elif mode is self.ComplexMode.SQUARE_AMPLITUDE:
return numpy.absolute(data) ** 2
else:
- raise ValueError('Unsupported conversion mode: %s', str(mode))
+ raise ValueError("Unsupported conversion mode: %s", str(mode))
@classmethod
def supportedComplexModes(cls):
@@ -1141,22 +1209,22 @@ class ScatterVisualizationMixIn(ItemMixInBase):
class Visualization(_Enum):
"""Different modes of scatter plot visualizations"""
- POINTS = 'points'
+ POINTS = "points"
"""Display scatter plot as a point cloud"""
- LINES = 'lines'
+ LINES = "lines"
"""Display scatter plot as a wireframe.
This is based on Delaunay triangulation
"""
- SOLID = 'solid'
+ SOLID = "solid"
"""Display scatter plot as a set of filled triangles.
This is based on Delaunay triangulation
"""
- REGULAR_GRID = 'regular_grid'
+ REGULAR_GRID = "regular_grid"
"""Display scatter plot as an image.
It expects the points to be the intersection of a regular grid,
@@ -1165,7 +1233,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
(either all lines from left to right or all from right to left).
"""
- IRREGULAR_GRID = 'irregular_grid'
+ IRREGULAR_GRID = "irregular_grid"
"""Display scatter plot as contiguous quadrilaterals.
It expects the points to be the intersection of an irregular grid,
@@ -1174,7 +1242,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
(either all lines from left to right or all from right to left).
"""
- BINNED_STATISTIC = 'binned_statistic'
+ BINNED_STATISTIC = "binned_statistic"
"""Display scatter plot as 2D binned statistic (i.e., generalized histogram).
"""
@@ -1182,13 +1250,13 @@ class ScatterVisualizationMixIn(ItemMixInBase):
class VisualizationParameter(_Enum):
"""Different parameter names for scatter plot visualizations"""
- GRID_MAJOR_ORDER = 'grid_major_order'
+ GRID_MAJOR_ORDER = "grid_major_order"
"""The major order of points in the regular grid.
Either 'row' (row-major, fast X) or 'column' (column-major, fast Y).
"""
- GRID_BOUNDS = 'grid_bounds'
+ GRID_BOUNDS = "grid_bounds"
"""The expected range in data coordinates of the regular grid.
A 2-tuple of 2-tuple: (begin (x, y), end (x, y)).
@@ -1197,24 +1265,24 @@ class ScatterVisualizationMixIn(ItemMixInBase):
As for `GRID_SHAPE`, this can be wider than the current data.
"""
- GRID_SHAPE = 'grid_shape'
+ GRID_SHAPE = "grid_shape"
"""The expected size of the regular grid (height, width).
The given shape can be wider than the number of points,
in which case the grid is not fully filled.
"""
- BINNED_STATISTIC_SHAPE = 'binned_statistic_shape'
+ BINNED_STATISTIC_SHAPE = "binned_statistic_shape"
"""The number of bins in each dimension (height, width).
"""
- BINNED_STATISTIC_FUNCTION = 'binned_statistic_function'
+ BINNED_STATISTIC_FUNCTION = "binned_statistic_function"
"""The reduction function to apply to each bin (str).
Available reduction functions are: 'mean' (default), 'count', 'sum'.
"""
- DATA_BOUNDS_HINT = 'data_bounds_hint'
+ DATA_BOUNDS_HINT = "data_bounds_hint"
"""The expected bounds of the data in data coordinates.
A 2-tuple of 2-tuple: ((ymin, ymax), (xmin, xmax)).
@@ -1225,8 +1293,8 @@ class ScatterVisualizationMixIn(ItemMixInBase):
"""
_SUPPORTED_VISUALIZATION_PARAMETER_VALUES = {
- VisualizationParameter.GRID_MAJOR_ORDER: ('row', 'column'),
- VisualizationParameter.BINNED_STATISTIC_FUNCTION: ('mean', 'count', 'sum'),
+ VisualizationParameter.GRID_MAJOR_ORDER: ("row", "column"),
+ VisualizationParameter.BINNED_STATISTIC_FUNCTION: ("mean", "count", "sum"),
}
"""Supported visualization parameter values.
@@ -1235,9 +1303,12 @@ class ScatterVisualizationMixIn(ItemMixInBase):
def __init__(self):
self.__visualization = self.Visualization.POINTS
- self.__parameters = dict(# Init parameters to None
- (parameter, None) for parameter in self.VisualizationParameter)
- self.__parameters[self.VisualizationParameter.BINNED_STATISTIC_FUNCTION] = 'mean'
+ self.__parameters = dict( # Init parameters to None
+ (parameter, None) for parameter in self.VisualizationParameter
+ )
+ self.__parameters[
+ self.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ] = "mean"
@classmethod
def supportedVisualizations(cls):
@@ -1263,8 +1334,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
:returns: tuple of supported of values or None if not defined.
"""
parameter = cls.VisualizationParameter(parameter)
- return cls._SUPPORTED_VISUALIZATION_PARAMETER_VALUES.get(
- parameter, None)
+ return cls._SUPPORTED_VISUALIZATION_PARAMETER_VALUES.get(parameter, None)
def setVisualization(self, mode):
"""Set the scatter plot visualization mode to use.
@@ -1351,6 +1421,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
"""Base class for :class:`Curve` and :class:`Scatter`"""
+
# note: _logFilterData must be overloaded if you overload
# getData to change its signature
@@ -1398,22 +1469,18 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
errorClipped[mask] = valueMinusError[mask] <= 0
if numpy.any(errorClipped): # Need filtering
-
# expand errorbars to 2xN
if error.size == 1: # Scalar
- error = numpy.full(
- (2, len(value)), error, dtype=numpy.float64)
+ error = numpy.full((2, len(value)), error, dtype=numpy.float64)
elif error.ndim == 1: # N array
- newError = numpy.empty((2, len(value)),
- dtype=numpy.float64)
- newError[0,:] = error
- newError[1,:] = error
+ newError = numpy.empty((2, len(value)), dtype=numpy.float64)
+ newError[0, :] = error
+ newError[1, :] = error
error = newError
elif error.size == 2 * len(value): # 2xN array
- error = numpy.array(
- error, copy=True, dtype=numpy.float64)
+ error = numpy.array(error, copy=True, dtype=numpy.float64)
else:
_logger.error("Unhandled error array")
@@ -1437,16 +1504,17 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
if xPositive:
x = self.getXData(copy=False)
- with numpy.errstate(invalid='ignore'): # Ignore NaN warnings
+ with numpy.errstate(invalid="ignore"): # Ignore NaN warnings
xclipped = x <= 0
if yPositive:
y = self.getYData(copy=False)
- with numpy.errstate(invalid='ignore'): # Ignore NaN warnings
+ with numpy.errstate(invalid="ignore"): # Ignore NaN warnings
yclipped = y <= 0
- self._clippedCache[(xPositive, yPositive)] = \
- numpy.logical_or(xclipped, yclipped)
+ self._clippedCache[(xPositive, yPositive)] = numpy.logical_or(
+ xclipped, yclipped
+ )
return self._clippedCache[(xPositive, yPositive)]
def _logFilterData(self, xPositive, yPositive):
@@ -1484,7 +1552,7 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
def __minMaxDataWithError(
data: numpy.ndarray,
error: Optional[Union[float, numpy.ndarray]],
- positiveOnly: bool
+ positiveOnly: bool,
) -> Tuple[float]:
if error is None:
min_, max_ = min_max(data, finite=True)
@@ -1532,9 +1600,12 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
xmin, xmax = self.__minMaxDataWithError(x, xerror, xPositive)
ymin, ymax = self.__minMaxDataWithError(y, yerror, yPositive)
- self._boundsCache[(xPositive, yPositive)] = tuple([
- (bound if bound is not None else numpy.nan)
- for bound in (xmin, xmax, ymin, ymax)])
+ self._boundsCache[(xPositive, yPositive)] = tuple(
+ [
+ (bound if bound is not None else numpy.nan)
+ for bound in (xmin, xmax, ymin, ymax)
+ ]
+ )
return self._boundsCache[(xPositive, yPositive)]
def _getCachedData(self):
@@ -1548,8 +1619,9 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
if xPositive or yPositive:
# At least one axis has log scale, filter data
if (xPositive, yPositive) not in self._filteredCache:
- self._filteredCache[(xPositive, yPositive)] = \
- self._logFilterData(xPositive, yPositive)
+ self._filteredCache[(xPositive, yPositive)] = self._logFilterData(
+ xPositive, yPositive
+ )
return self._filteredCache[(xPositive, yPositive)]
return None
@@ -1570,10 +1642,12 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
if cached_data is not None:
return cached_data
- return (self.getXData(copy),
- self.getYData(copy),
- self.getXErrorData(copy),
- self.getYErrorData(copy))
+ return (
+ self.getXData(copy),
+ self.getYData(copy),
+ self.getXErrorData(copy),
+ self.getYErrorData(copy),
+ )
def getXData(self, copy=True):
"""Returns the x coordinates of the data points
@@ -1640,12 +1714,10 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
# Convert complex data
if numpy.iscomplexobj(x):
- _logger.warning(
- 'Converting x data to absolute value to plot it.')
+ _logger.warning("Converting x data to absolute value to plot it.")
x = numpy.absolute(x)
if numpy.iscomplexobj(y):
- _logger.warning(
- 'Converting y data to absolute value to plot it.')
+ _logger.warning("Converting y data to absolute value to plot it.")
y = numpy.absolute(y)
if xerror is not None:
@@ -1653,7 +1725,8 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
xerror = numpy.array(xerror, copy=copy)
if numpy.iscomplexobj(xerror):
_logger.warning(
- 'Converting xerror data to absolute value to plot it.')
+ "Converting xerror data to absolute value to plot it."
+ )
xerror = numpy.absolute(xerror)
else:
xerror = float(xerror)
@@ -1662,7 +1735,8 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
yerror = numpy.array(yerror, copy=copy)
if numpy.iscomplexobj(yerror):
_logger.warning(
- 'Converting yerror data to absolute value to plot it.')
+ "Converting yerror data to absolute value to plot it."
+ )
yerror = numpy.absolute(yerror)
else:
yerror = float(yerror)
@@ -1691,7 +1765,7 @@ class BaselineMixIn(object):
:param baseline: baseline value(s)
:type: Union[None,float,numpy.ndarray]
"""
- if (isinstance(baseline, abc.Iterable)):
+ if isinstance(baseline, abc.Iterable):
baseline = numpy.array(baseline)
self._baseline = baseline
@@ -1713,7 +1787,6 @@ class _Style:
class HighlightedMixIn(ItemMixInBase):
-
def __init__(self):
self._highlightStyle = self._DEFAULT_HIGHLIGHT_STYLE
self._highlighted = False
diff --git a/src/silx/gui/plot/items/curve.py b/src/silx/gui/plot/items/curve.py
index 93e4719..e8d0d52 100644
--- a/src/silx/gui/plot/items/curve.py
+++ b/src/silx/gui/plot/items/curve.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -23,6 +23,7 @@
# ###########################################################################*/
"""This module provides the :class:`Curve` item of the :class:`Plot`.
"""
+from __future__ import annotations
__authors__ = ["T. Vincent"]
__license__ = "MIT"
@@ -33,11 +34,22 @@ import logging
import numpy
-from ....utils.deprecation import deprecated
+from ....utils.deprecation import deprecated_warning
from ... import colors
-from .core import (PointsBase, LabelsMixIn, ColorMixIn, YAxisMixIn,
- FillMixIn, LineMixIn, SymbolMixIn, ItemChangedType,
- BaselineMixIn, HighlightedMixIn, _Style)
+from .core import (
+ PointsBase,
+ LabelsMixIn,
+ ColorMixIn,
+ YAxisMixIn,
+ FillMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ LineStyleType,
+ SymbolMixIn,
+ BaselineMixIn,
+ HighlightedMixIn,
+ _Style,
+)
_logger = logging.getLogger(__name__)
@@ -49,14 +61,22 @@ class CurveStyle(_Style):
Set a value to None to use the default
:param color: Color
- :param Union[str,None] linestyle: Style of the line
- :param Union[float,None] linewidth: Width of the line
- :param Union[str,None] symbol: Symbol for markers
- :param Union[float,None] symbolsize: Size of the markers
+ :param linestyle: Style of the line
+ :param linewidth: Width of the line
+ :param symbol: Symbol for markers
+ :param symbolsize: Size of the markers
+ :param gapcolor: Color of gaps of dashed line
"""
- def __init__(self, color=None, linestyle=None, linewidth=None,
- symbol=None, symbolsize=None):
+ def __init__(
+ self,
+ color: colors.RGBAColorType | None = None,
+ linestyle: LineStyleType | None = None,
+ linewidth: float | None = None,
+ symbol: str | None = None,
+ symbolsize: float | None = None,
+ gapcolor: colors.RGBAColorType | None = None,
+ ):
if color is None:
self._color = None
else:
@@ -68,8 +88,8 @@ class CurveStyle(_Style):
color = colors.rgba(color)
self._color = color
- if linestyle is not None:
- assert linestyle in LineMixIn.getSupportedLineStyles()
+ if not LineMixIn.isValidLineStyle(linestyle):
+ raise ValueError(f"Not a valid line style: {linestyle}")
self._linestyle = linestyle
self._linewidth = None if linewidth is None else float(linewidth)
@@ -80,6 +100,8 @@ class CurveStyle(_Style):
self._symbolsize = None if symbolsize is None else float(symbolsize)
+ self._gapcolor = None if gapcolor is None else colors.rgba(gapcolor)
+
def getColor(self, copy=True):
"""Returns the color or None if not set.
@@ -93,7 +115,14 @@ class CurveStyle(_Style):
else:
return self._color
- def getLineStyle(self):
+ def getLineGapColor(self):
+ """Returns the color of dashed line gaps or None if not set.
+
+ :rtype: Union[List[float],None]
+ """
+ return self._gapcolor
+
+ def getLineStyle(self) -> LineStyleType | None:
"""Return the type of the line or None if not set.
Type of line::
@@ -103,8 +132,7 @@ class CurveStyle(_Style):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
-
- :rtype: Union[str,None]
+ - (offset, (dash pattern))
"""
return self._linestyle
@@ -141,17 +169,29 @@ class CurveStyle(_Style):
def __eq__(self, other):
if isinstance(other, CurveStyle):
- return (numpy.array_equal(self.getColor(), other.getColor()) and
- self.getLineStyle() == other.getLineStyle() and
- self.getLineWidth() == other.getLineWidth() and
- self.getSymbol() == other.getSymbol() and
- self.getSymbolSize() == other.getSymbolSize())
+ return (
+ numpy.array_equal(self.getColor(), other.getColor())
+ and self.getLineStyle() == other.getLineStyle()
+ and self.getLineWidth() == other.getLineWidth()
+ and self.getSymbol() == other.getSymbol()
+ and self.getSymbolSize() == other.getSymbolSize()
+ and self.getLineGapColor() == other.getLineGapColor()
+ )
else:
return False
-class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
- LineMixIn, BaselineMixIn, HighlightedMixIn):
+class Curve(
+ PointsBase,
+ ColorMixIn,
+ YAxisMixIn,
+ FillMixIn,
+ LabelsMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ BaselineMixIn,
+ HighlightedMixIn,
+):
"""Description of a curve"""
_DEFAULT_Z_LAYER = 1
@@ -160,13 +200,13 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
_DEFAULT_SELECTABLE = True
"""Default selectable state for curves"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH = 1.0
"""Default line width of the curve"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE = "-"
"""Default line style of the curve"""
- _DEFAULT_HIGHLIGHT_STYLE = CurveStyle(color='black')
+ _DEFAULT_HIGHLIGHT_STYLE = CurveStyle(color="black")
"""Default highlight style of the item"""
_DEFAULT_BASELINE = None
@@ -178,6 +218,7 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
FillMixIn.__init__(self)
LabelsMixIn.__init__(self)
LineMixIn.__init__(self)
+ LineGapColorMixIn.__init__(self)
BaselineMixIn.__init__(self)
HighlightedMixIn.__init__(self)
@@ -186,29 +227,38 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
def _addBackendRenderer(self, backend):
"""Update backend renderer"""
# Filter-out values <= 0
- xFiltered, yFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
+ xFiltered, yFiltered, xerror, yerror = self.getData(copy=False, displayed=True)
if len(xFiltered) == 0 or not numpy.any(numpy.isfinite(xFiltered)):
return None # No data to display, do not add renderer to backend
style = self.getCurrentStyle()
- return backend.addCurve(xFiltered, yFiltered,
- color=style.getColor(),
- symbol=style.getSymbol(),
- linestyle=style.getLineStyle(),
- linewidth=style.getLineWidth(),
- yaxis=self.getYAxis(),
- xerror=xerror,
- yerror=yerror,
- fill=self.isFill(),
- alpha=self.getAlpha(),
- symbolsize=style.getSymbolSize(),
- baseline=self.getBaseline(copy=False))
+ return backend.addCurve(
+ xFiltered,
+ yFiltered,
+ color=style.getColor(),
+ gapcolor=style.getLineGapColor(),
+ symbol=style.getSymbol(),
+ linestyle=style.getLineStyle(),
+ linewidth=style.getLineWidth(),
+ yaxis=self.getYAxis(),
+ xerror=xerror,
+ yerror=yerror,
+ fill=self.isFill(),
+ alpha=self.getAlpha(),
+ symbolsize=style.getSymbolSize(),
+ baseline=self.getBaseline(copy=False),
+ )
def __getitem__(self, item):
"""Compatibility with PyMca and silx <= 0.4.0"""
+ deprecated_warning(
+ "Attributes",
+ "__getitem__",
+ since_version="2.0.0",
+ replacement="Use Curve methods",
+ )
if isinstance(item, slice):
return [self[index] for index in range(*item.indices(5))]
elif item == 0:
@@ -222,44 +272,24 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
return {} if info is None else info
elif item == 4:
params = {
- 'info': self.getInfo(),
- 'color': self.getColor(),
- 'symbol': self.getSymbol(),
- 'linewidth': self.getLineWidth(),
- 'linestyle': self.getLineStyle(),
- 'xlabel': self.getXLabel(),
- 'ylabel': self.getYLabel(),
- 'yaxis': self.getYAxis(),
- 'xerror': self.getXErrorData(copy=False),
- 'yerror': self.getYErrorData(copy=False),
- 'z': self.getZValue(),
- 'selectable': self.isSelectable(),
- 'fill': self.isFill(),
+ "info": self.getInfo(),
+ "color": self.getColor(),
+ "symbol": self.getSymbol(),
+ "linewidth": self.getLineWidth(),
+ "linestyle": self.getLineStyle(),
+ "xlabel": self.getXLabel(),
+ "ylabel": self.getYLabel(),
+ "yaxis": self.getYAxis(),
+ "xerror": self.getXErrorData(copy=False),
+ "yerror": self.getYErrorData(copy=False),
+ "z": self.getZValue(),
+ "selectable": self.isSelectable(),
+ "fill": self.isFill(),
}
return params
else:
raise IndexError("Index out of range: %s", str(item))
- @deprecated(replacement='Curve.getHighlightedStyle().getColor()',
- since_version='0.9.0')
- def getHighlightedColor(self):
- """Returns the RGBA highlight color of the item
-
- :rtype: 4-tuple of float in [0, 1]
- """
- return self.getHighlightedStyle().getColor()
-
- @deprecated(replacement='Curve.setHighlightedStyle()',
- since_version='0.9.0')
- def setHighlightedColor(self, color):
- """Set the color to use when highlighted
-
- :param color: color(s) to be used for highlight
- :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in colors.py
- """
- self.setHighlightedStyle(CurveStyle(color))
-
def getCurrentStyle(self):
"""Returns the current curve style.
@@ -274,32 +304,26 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
linewidth = style.getLineWidth()
symbol = style.getSymbol()
symbolsize = style.getSymbolSize()
+ gapcolor = style.getLineGapColor()
return CurveStyle(
color=self.getColor() if color is None else color,
linestyle=self.getLineStyle() if linestyle is None else linestyle,
linewidth=self.getLineWidth() if linewidth is None else linewidth,
symbol=self.getSymbol() if symbol is None else symbol,
- symbolsize=self.getSymbolSize() if symbolsize is None else symbolsize)
+ symbolsize=self.getSymbolSize() if symbolsize is None else symbolsize,
+ gapcolor=self.getLineGapColor() if gapcolor is None else gapcolor,
+ )
else:
- return CurveStyle(color=self.getColor(),
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- symbol=self.getSymbol(),
- symbolsize=self.getSymbolSize())
-
- @deprecated(replacement='Curve.getCurrentStyle()',
- since_version='0.9.0')
- def getCurrentColor(self):
- """Returns the current color of the curve.
-
- This color is either the color of the curve or the highlighted color,
- depending on the highlight state.
-
- :rtype: 4-tuple of float in [0, 1]
- """
- return self.getCurrentStyle().getColor()
+ return CurveStyle(
+ color=self.getColor(),
+ linestyle=self.getLineStyle(),
+ linewidth=self.getLineWidth(),
+ symbol=self.getSymbol(),
+ symbolsize=self.getSymbolSize(),
+ gapcolor=self.getLineGapColor(),
+ )
def setData(self, x, y, xerror=None, yerror=None, baseline=None, copy=True):
"""Set the data of the curve.
@@ -319,6 +343,5 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
:param bool copy: True make a copy of the data (default),
False to use provided arrays.
"""
- PointsBase.setData(self, x=x, y=y, xerror=xerror, yerror=yerror,
- copy=copy)
+ PointsBase.setData(self, x=x, y=y, xerror=xerror, yerror=yerror, copy=copy)
self._setBaseline(baseline=baseline)
diff --git a/src/silx/gui/plot/items/histogram.py b/src/silx/gui/plot/items/histogram.py
index 007f0c7..1dc851b 100644
--- a/src/silx/gui/plot/items/histogram.py
+++ b/src/silx/gui/plot/items/histogram.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -32,15 +32,20 @@ import logging
import typing
import numpy
-from collections import OrderedDict, namedtuple
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
from ....utils.proxy import docstring
-from .core import (DataItem, AlphaMixIn, BaselineMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, ItemChangedType, Item)
+from .core import (
+ DataItem,
+ AlphaMixIn,
+ BaselineMixIn,
+ ColorMixIn,
+ FillMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ YAxisMixIn,
+ ItemChangedType,
+)
from ._pick import PickingResult
_logger = logging.getLogger(__name__)
@@ -62,17 +67,17 @@ def _computeEdges(x, histogramType):
"""
# for now we consider that the spaces between xs are constant
edges = x.copy()
- if histogramType == 'left':
+ if histogramType == "left":
width = 1
if len(x) > 1:
width = x[1] - x[0]
edges = numpy.append(x[0] - width, edges)
- if histogramType == 'center':
- edges = _computeEdges(edges, 'right')
+ if histogramType == "center":
+ edges = _computeEdges(edges, "right")
widths = (edges[1:] - edges[0:-1]) / 2.0
widths = numpy.append(widths, widths[-1])
edges = edges - widths
- if histogramType == 'right':
+ if histogramType == "right":
width = 1
if len(x) > 1:
width = x[-1] - x[-2]
@@ -102,8 +107,16 @@ def _getHistogramCurve(histogram, edges):
# TODO: Yerror, test log scale
-class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, BaselineMixIn):
+class Histogram(
+ DataItem,
+ AlphaMixIn,
+ ColorMixIn,
+ FillMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ YAxisMixIn,
+ BaselineMixIn,
+):
"""Description of an histogram"""
_DEFAULT_Z_LAYER = 1
@@ -112,10 +125,10 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
_DEFAULT_SELECTABLE = False
"""Default selectable state for histograms"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH = 1.0
"""Default line width of the histogram"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE = "-"
"""Default line style of the histogram"""
_DEFAULT_BASELINE = None
@@ -127,6 +140,7 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
ColorMixIn.__init__(self)
FillMixIn.__init__(self)
LineMixIn.__init__(self)
+ LineGapColorMixIn.__init__(self)
YAxisMixIn.__init__(self)
self._histogram = ()
@@ -156,26 +170,30 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
if xPositive or yPositive:
clipped = numpy.logical_or(
- (x <= 0) if xPositive else False,
- (y <= 0) if yPositive else False)
+ (x <= 0) if xPositive else False, (y <= 0) if yPositive else False
+ )
# Make a copy and replace negative points by NaN
x = numpy.array(x, dtype=numpy.float64)
y = numpy.array(y, dtype=numpy.float64)
x[clipped] = numpy.nan
y[clipped] = numpy.nan
- return backend.addCurve(x, y,
- color=self.getColor(),
- symbol='',
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- yaxis=self.getYAxis(),
- xerror=None,
- yerror=None,
- fill=self.isFill(),
- alpha=self.getAlpha(),
- baseline=baseline,
- symbolsize=1)
+ return backend.addCurve(
+ x,
+ y,
+ color=self.getColor(),
+ gapcolor=self.getLineGapColor(),
+ symbol="",
+ linestyle=self.getLineStyle(),
+ linewidth=self.getLineWidth(),
+ yaxis=self.getYAxis(),
+ xerror=None,
+ yerror=None,
+ fill=self.isFill(),
+ alpha=self.getAlpha(),
+ baseline=baseline,
+ symbolsize=1,
+ )
def _getBounds(self):
values, edges, baseline = self.getData(copy=False)
@@ -193,11 +211,10 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
if xPositive:
# Replace edges <= 0 by NaN and corresponding values by NaN
- clipped_edges = (edges <= 0)
+ clipped_edges = edges <= 0
edges = numpy.array(edges, copy=True, dtype=numpy.float64)
edges[clipped_edges] = numpy.nan
- clipped_values = numpy.logical_or(clipped_edges[:-1],
- clipped_edges[1:])
+ clipped_values = numpy.logical_or(clipped_edges[:-1], clipped_edges[1:])
else:
clipped_values = numpy.zeros_like(values, dtype=bool)
@@ -208,20 +225,26 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
values[clipped_values] = numpy.nan
if yPositive:
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- numpy.nanmin(values),
- numpy.nanmax(values))
+ return (
+ numpy.nanmin(edges),
+ numpy.nanmax(edges),
+ numpy.nanmin(values),
+ numpy.nanmax(values),
+ )
else: # No log scale on y axis, include 0 in bounds
if numpy.all(numpy.isnan(values)):
return None
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- min(0, numpy.nanmin(values)),
- max(0, numpy.nanmax(values)))
-
- def __pickFilledHistogram(self, x: float, y: float) -> typing.Optional[PickingResult]:
+ return (
+ numpy.nanmin(edges),
+ numpy.nanmax(edges),
+ min(0, numpy.nanmin(values)),
+ max(0, numpy.nanmax(values)),
+ )
+
+ def __pickFilledHistogram(
+ self, x: float, y: float
+ ) -> typing.Optional[PickingResult]:
"""Picking implementation for filled histogram
:param x: X position in pixels
@@ -241,7 +264,7 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
# Check x
edges = self.getBinEdgesData(copy=False)
- index = numpy.searchsorted(edges, (xData,), side='left')[0] - 1
+ index = numpy.searchsorted(edges, (xData,), side="left")[0] - 1
# Safe indexing in histogram values
index = numpy.clip(index, 0, len(edges) - 2)
@@ -251,8 +274,9 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
baseline = 0 # Default value
value = self.getValueData(copy=False)[index]
- if ((baseline <= value and baseline <= yData <= value) or
- (value < baseline and value <= yData <= baseline)):
+ if (baseline <= value and baseline <= yData <= value) or (
+ value < baseline and value <= yData <= baseline
+ ):
return PickingResult(self, numpy.array([index]))
else:
return None
@@ -296,12 +320,13 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
:returns: (N histogram value, N+1 bin edges)
:rtype: 2-tuple of numpy.nadarray
"""
- return (self.getValueData(copy),
- self.getBinEdgesData(copy),
- self.getBaseline(copy))
+ return (
+ self.getValueData(copy),
+ self.getBinEdgesData(copy),
+ self.getBaseline(copy),
+ )
- def setData(self, histogram, edges, align='center', baseline=None,
- copy=True):
+ def setData(self, histogram, edges, align="center", baseline=None, copy=True):
"""Set the histogram values and bin edges.
:param numpy.ndarray histogram: The values of the histogram.
@@ -324,7 +349,7 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
assert histogram.ndim == 1
assert edges.ndim == 1
assert edges.size in (histogram.size, histogram.size + 1)
- assert align in ('center', 'left', 'right')
+ assert align in ("center", "left", "right")
if histogram.size == 0: # No data
self._histogram = ()
@@ -338,12 +363,12 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
edgesDiff = edgesDiff[numpy.logical_not(numpy.isnan(edgesDiff))]
assert numpy.all(edgesDiff >= 0) or numpy.all(edgesDiff <= 0)
# manage baseline
- if (isinstance(baseline, abc.Iterable)):
+ if isinstance(baseline, abc.Iterable):
baseline = numpy.array(baseline)
if baseline.size == histogram.size:
new_baseline = numpy.empty(baseline.shape[0] * 2)
for i_value, value in enumerate(baseline):
- new_baseline[i_value*2:i_value*2+2] = value
+ new_baseline[i_value * 2 : i_value * 2 + 2] = value
baseline = new_baseline
self._histogram = histogram
self._edges = edges
@@ -376,11 +401,11 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
"""
# for now we consider that the spaces between xs are constant
edges = x.copy()
- if histogramType == 'left':
+ if histogramType == "left":
return edges[1:]
- if histogramType == 'center':
+ if histogramType == "center":
edges = (edges[1:] + edges[:-1]) / 2.0
- if histogramType == 'right':
+ if histogramType == "right":
width = 1
if len(x) > 1:
width = x[-1] + x[-2]
diff --git a/src/silx/gui/plot/items/image.py b/src/silx/gui/plot/items/image.py
index eaee05a..18310d9 100644
--- a/src/silx/gui/plot/items/image.py
+++ b/src/silx/gui/plot/items/image.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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,17 +29,21 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "08/12/2020"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
import logging
import numpy
from ....utils.proxy import docstring
-from .core import (DataItem, LabelsMixIn, DraggableMixIn, ColormapMixIn,
- AlphaMixIn, ItemChangedType)
+from ....utils.deprecation import deprecated_warning
+from .core import (
+ DataItem,
+ LabelsMixIn,
+ DraggableMixIn,
+ ColormapMixIn,
+ AlphaMixIn,
+ ItemChangedType,
+)
_logger = logging.getLogger(__name__)
@@ -62,23 +66,22 @@ def _convertImageToRgba32(image, copy=True):
assert image.shape[-1] in (3, 4)
# Convert type to uint8
- if image.dtype.name != 'uint8':
- if image.dtype.kind == 'f': # Float in [0, 1]
- image = (numpy.clip(image, 0., 1.) * 255).astype(numpy.uint8)
- elif image.dtype.kind == 'b': # boolean
+ if image.dtype.name != "uint8":
+ if image.dtype.kind == "f": # Float in [0, 1]
+ image = (numpy.clip(image, 0.0, 1.0) * 255).astype(numpy.uint8)
+ elif image.dtype.kind == "b": # boolean
image = image.astype(numpy.uint8) * 255
- elif image.dtype.kind in ('i', 'u'): # int, uint
+ elif image.dtype.kind in ("i", "u"): # int, uint
image = numpy.clip(image, 0, 255).astype(numpy.uint8)
else:
- raise ValueError('Unsupported image dtype: %s', image.dtype.name)
+ raise ValueError("Unsupported image dtype: %s", image.dtype.name)
copy = False # A copy as already been done, avoid next one
# Convert RGB to RGBA
if image.shape[-1] == 3:
- new_image = numpy.empty((image.shape[0], image.shape[1], 4),
- dtype=numpy.uint8)
- new_image[:,:,:3] = image
- new_image[:,:, 3] = 255
+ new_image = numpy.empty((image.shape[0], image.shape[1], 4), dtype=numpy.uint8)
+ new_image[:, :, :3] = image
+ new_image[:, :, 3] = 255
return new_image # This is a copy anyway
else:
return numpy.array(image, copy=copy)
@@ -100,11 +103,17 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
self._data = data
self._mask = mask
self.__valueDataCache = None # Store default data
- self._origin = (0., 0.)
- self._scale = (1., 1.)
+ self._origin = (0.0, 0.0)
+ self._scale = (1.0, 1.0)
def __getitem__(self, item):
"""Compatibility with PyMca and silx <= 0.4.0"""
+ deprecated_warning(
+ "Attributes",
+ "__getitem__",
+ since_version="2.0.0",
+ replacement="Use ImageBase methods",
+ )
if isinstance(item, slice):
return [self[index] for index in range(*item.indices(5))]
elif item == 0:
@@ -118,15 +127,15 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
return None
elif item == 4:
params = {
- 'info': self.getInfo(),
- 'origin': self.getOrigin(),
- 'scale': self.getScale(),
- 'z': self.getZValue(),
- 'selectable': self.isSelectable(),
- 'draggable': self.isDraggable(),
- 'colormap': None,
- 'xlabel': self.getXLabel(),
- 'ylabel': self.getYLabel(),
+ "info": self.getInfo(),
+ "origin": self.getOrigin(),
+ "scale": self.getScale(),
+ "z": self.getZValue(),
+ "selectable": self.isSelectable(),
+ "draggable": self.isDraggable(),
+ "colormap": None,
+ "xlabel": self.getXLabel(),
+ "ylabel": self.getYLabel(),
}
return params
else:
@@ -167,8 +176,7 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
@docstring(DraggableMixIn)
def drag(self, from_, to):
origin = self.getOrigin()
- self.setOrigin((origin[0] + to[0] - from_[0],
- origin[1] + to[1] - from_[1]))
+ self.setOrigin((origin[0] + to[0] - from_[0], origin[1] + to[1] - from_[1]))
def getData(self, copy=True):
"""Returns the image data
@@ -190,8 +198,10 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
self._boundsChanged()
self._updated(ItemChangedType.DATA)
- if (self.getMaskData(copy=False) is not None and
- previousShape != self._data.shape):
+ if (
+ self.getMaskData(copy=False) is not None
+ and previousShape != self._data.shape
+ ):
# Data shape changed, so mask shape changes.
# Send event, mask is lazily updated in getMaskData
self._updated(ItemChangedType.MASK)
@@ -211,7 +221,9 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
if self._mask.shape != shape:
# Clip/extend mask to match data
newMask = numpy.zeros(shape, dtype=self._mask.dtype)
- newMask[:self._mask.shape[0], :self._mask.shape[1]] = self._mask[:shape[0], :shape[1]]
+ newMask[: self._mask.shape[0], : self._mask.shape[1]] = self._mask[
+ : shape[0], : shape[1]
+ ]
self._mask = newMask
return numpy.array(self._mask, copy=copy)
@@ -228,7 +240,9 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
shape = self.getData(copy=False).shape[:2]
if mask.shape != shape:
- _logger.warning("Inconsistent shape between mask and data %s, %s", mask.shape, shape)
+ _logger.warning(
+ "Inconsistent shape between mask and data %s, %s", mask.shape, shape
+ )
# Clip/extent is done lazily in getMaskData
elif self._mask is None:
return # No update
@@ -278,7 +292,7 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
False to use internal representation (do not modify!)
:returns: numpy.ndarray of uint8 of shape (height, width, 4)
"""
- raise NotImplementedError('This MUST be implemented in sub-class')
+ raise NotImplementedError("This MUST be implemented in sub-class")
def getOrigin(self):
"""Returns the offset from origin at which to display the image.
@@ -336,9 +350,11 @@ class ImageDataBase(ImageBase, ColormapMixIn):
def _getColormapForRendering(self):
colormap = self.getColormap()
if colormap.isAutoscale():
+ # NOTE: Make sure getColormapRange comes from the original object
+ vrange = colormap.getColormapRange(self)
# Avoid backend to compute autoscale: use item cache
colormap = colormap.copy()
- colormap.setVRange(*colormap.getColormapRange(self))
+ colormap.setVRange(*vrange)
return colormap
def getRgbaImageData(self, copy=True):
@@ -350,7 +366,7 @@ class ImageDataBase(ImageBase, ColormapMixIn):
return self.getColormap().applyToData(self)
def setData(self, data, copy=True):
- """"Set the image data
+ """Set the image data
:param numpy.ndarray data: Data array with 2 dimensions (h, w)
:param bool copy: True (Default) to get a copy,
@@ -358,13 +374,11 @@ class ImageDataBase(ImageBase, ColormapMixIn):
"""
data = numpy.array(data, copy=copy)
assert data.ndim == 2
- if data.dtype.kind == 'b':
- _logger.warning(
- 'Converting boolean image to int8 to plot it.')
+ if data.dtype.kind == "b":
+ _logger.warning("Converting boolean image to int8 to plot it.")
data = numpy.array(data, copy=False, dtype=numpy.int8)
elif numpy.iscomplexobj(data):
- _logger.warning(
- 'Converting complex image to absolute value to plot it.')
+ _logger.warning("Converting complex image to absolute value to plot it.")
data = numpy.absolute(data)
super().setData(data)
@@ -391,8 +405,10 @@ class ImageData(ImageDataBase):
# Do not render with non linear scales
return None
- if (self.getAlternativeImageData(copy=False) is not None or
- self.getAlphaData(copy=False) is not None):
+ if (
+ self.getAlternativeImageData(copy=False) is not None
+ or self.getAlphaData(copy=False) is not None
+ ):
dataToUse = self.getRgbaImageData(copy=False)
else:
dataToUse = self.getData(copy=False)
@@ -400,20 +416,28 @@ class ImageData(ImageDataBase):
if dataToUse.size == 0:
return None # No data to display
- return backend.addImage(dataToUse,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=self._getColormapForRendering(),
- alpha=self.getAlpha())
+ return backend.addImage(
+ dataToUse,
+ origin=self.getOrigin(),
+ scale=self.getScale(),
+ colormap=self._getColormapForRendering(),
+ alpha=self.getAlpha(),
+ )
def __getitem__(self, item):
"""Compatibility with PyMca and silx <= 0.4.0"""
+ deprecated_warning(
+ "Attributes",
+ "__getitem__",
+ since_version="2.0.0",
+ replacement="Use ImageData methods",
+ )
if item == 3:
return self.getAlternativeImageData(copy=False)
params = ImageBase.__getitem__(self, item)
if item == 4:
- params['colormap'] = self.getColormap()
+ params["colormap"] = self.getColormap()
return params
@@ -431,7 +455,7 @@ class ImageData(ImageDataBase):
alphaImage = self.getAlphaData(copy=False)
if alphaImage is not None:
# Apply transparency
- image[:,:, 3] = image[:,:, 3] * alphaImage
+ image[:, :, 3] = image[:, :, 3] * alphaImage
return image
def getAlternativeImageData(self, copy=True):
@@ -459,7 +483,7 @@ class ImageData(ImageDataBase):
return numpy.array(self.__alpha, copy=copy)
def setData(self, data, alternative=None, alpha=None, copy=True):
- """"Set the image data and optionally an alternative RGB(A) representation
+ """Set the image data and optionally an alternative RGB(A) representation
:param numpy.ndarray data: Data array with 2 dimensions (h, w)
:param alternative: RGB(A) image to display instead of data,
@@ -484,10 +508,10 @@ class ImageData(ImageDataBase):
if alpha is not None:
alpha = numpy.array(alpha, copy=copy)
assert alpha.shape == data.shape
- if alpha.dtype.kind != 'f':
+ if alpha.dtype.kind != "f":
alpha = alpha.astype(numpy.float32)
- if numpy.any(numpy.logical_or(alpha < 0., alpha > 1.)):
- alpha = numpy.clip(alpha, 0., 1.)
+ if numpy.any(numpy.logical_or(alpha < 0.0, alpha > 1.0)):
+ alpha = numpy.clip(alpha, 0.0, 1.0)
self.__alpha = alpha
super().setData(data)
@@ -512,11 +536,13 @@ class ImageRgba(ImageBase):
if data.size == 0:
return None # No data to display
- return backend.addImage(data,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=None,
- alpha=self.getAlpha())
+ return backend.addImage(
+ data,
+ origin=self.getOrigin(),
+ scale=self.getScale(),
+ colormap=None,
+ alpha=self.getAlpha(),
+ )
def getRgbaImageData(self, copy=True):
"""Get the displayed RGB(A) image
@@ -533,8 +559,14 @@ class ImageRgba(ImageBase):
False to use internal representation (do not modify!)
"""
data = numpy.array(data, copy=copy)
- assert data.ndim == 3
- assert data.shape[-1] in (3, 4)
+ if data.ndim != 3:
+ raise ValueError(
+ f"RGB(A) image is expected to be a 3D dataset. Got {data.ndim} dimensions"
+ )
+ if data.shape[-1] not in (3, 4):
+ raise ValueError(
+ f"RGB(A) image is expected to have 3 or 4 elements as last dimension. Got {data.shape[-1]}"
+ )
super().setData(data)
def _getValueData(self, copy=True):
@@ -545,10 +577,10 @@ class ImageRgba(ImageBase):
:param bool copy:
"""
rgba = self.getRgbaImageData(copy=False).astype(numpy.float32)
- intensity = (rgba[:, :, 0] * 0.299 +
- rgba[:, :, 1] * 0.587 +
- rgba[:, :, 2] * 0.114)
- intensity *= rgba[:, :, 3] / 255.
+ intensity = (
+ rgba[:, :, 0] * 0.299 + rgba[:, :, 1] * 0.587 + rgba[:, :, 2] * 0.114
+ )
+ intensity *= rgba[:, :, 3] / 255.0
return intensity
@@ -558,6 +590,7 @@ class MaskImageData(ImageData):
This class is used to flag mask items. This information is used to improve
internal silx widgets.
"""
+
pass
diff --git a/src/silx/gui/plot/items/image_aggregated.py b/src/silx/gui/plot/items/image_aggregated.py
index ffd41b2..b35e00a 100644
--- a/src/silx/gui/plot/items/image_aggregated.py
+++ b/src/silx/gui/plot/items/image_aggregated.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2021 European Synchrotron Radiation Facility
+# Copyright (c) 2021-2023 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
@@ -31,6 +31,7 @@ __date__ = "07/07/2021"
import enum
import logging
from typing import Tuple, Union
+import warnings
import numpy
@@ -68,7 +69,7 @@ class ImageDataAggregated(ImageDataBase):
self.__currentLOD = 0, 0
self.__aggregationMode = self.Aggregation.NONE
- def setAggregationMode(self, mode: Union[str,Aggregation]):
+ def setAggregationMode(self, mode: Union[str, Aggregation]):
"""Set the aggregation method used to reduce the data to screen resolution.
:param Aggregation mode: The aggregation method
@@ -115,12 +116,14 @@ class ImageDataAggregated(ImageDataBase):
if (lodx, lody) not in self.__cacheLODData:
height, width = data.shape
- self.__cacheLODData[(lodx, lody)] = aggregator(
- data[: (height // lody) * lody, : (width // lodx) * lodx].reshape(
- height // lody, lody, width // lodx, lodx
- ),
- axis=(1, 3),
- )
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", category=RuntimeWarning)
+ self.__cacheLODData[(lodx, lody)] = aggregator(
+ data[
+ : (height // lody) * lody, : (width // lodx) * lodx
+ ].reshape(height // lody, lody, width // lodx, lodx),
+ axis=(1, 3),
+ )
self.__currentLOD = lodx, lody
displayedData = self.__cacheLODData[self.__currentLOD]
@@ -153,10 +156,7 @@ class ImageDataAggregated(ImageDataBase):
xaxis = plot.getXAxis()
yaxis = plot.getYAxis(axis)
- if (
- xaxis.getScale() != Axis.LINEAR
- or yaxis.getScale() != Axis.LINEAR
- ):
+ if xaxis.getScale() != Axis.LINEAR or yaxis.getScale() != Axis.LINEAR:
raise RuntimeError("Only available with linear axes")
xmin, xmax = xaxis.getLimits()
@@ -200,8 +200,10 @@ class ImageDataAggregated(ImageDataBase):
def __plotLimitsChanged(self):
"""Trigger update if level of details has changed"""
- if (self.getAggregationMode() != self.Aggregation.NONE and
- self.__currentLOD != self._getLevelOfDetails()):
+ if (
+ self.getAggregationMode() != self.Aggregation.NONE
+ and self.__currentLOD != self._getLevelOfDetails()
+ ):
self._updated()
@docstring(ImageDataBase)
diff --git a/src/silx/gui/plot/items/marker.py b/src/silx/gui/plot/items/marker.py
index 7596eb0..b3da451 100755
--- a/src/silx/gui/plot/items/marker.py
+++ b/src/silx/gui/plot/items/marker.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -23,6 +23,7 @@
# ###########################################################################*/
"""This module provides markers item of the :class:`Plot`.
"""
+from __future__ import annotations
__authors__ = ["T. Vincent"]
__license__ = "MIT"
@@ -30,11 +31,22 @@ __date__ = "06/03/2017"
import logging
+import numpy
from ....utils.proxy import docstring
-from .core import (Item, DraggableMixIn, ColorMixIn, LineMixIn, SymbolMixIn,
- ItemChangedType, YAxisMixIn)
+from .core import (
+ Item,
+ DraggableMixIn,
+ ColorMixIn,
+ LineMixIn,
+ SymbolMixIn,
+ ItemChangedType,
+ YAxisMixIn,
+)
+from silx import config
from silx.gui import qt
+from silx.gui import colors
+
_logger = logging.getLogger(__name__)
@@ -47,7 +59,7 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
sigDragFinished = qt.Signal()
"""Signal emitted when the marker is released"""
- _DEFAULT_COLOR = (0., 0., 0., 1.)
+ _DEFAULT_COLOR = (0.0, 0.0, 0.0, 1.0)
"""Default color of the markers"""
def __init__(self):
@@ -56,14 +68,21 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
ColorMixIn.__init__(self)
YAxisMixIn.__init__(self)
- self._text = ''
+ self._text = ""
+ self._font = None
+ if config.DEFAULT_PLOT_MARKER_TEXT_FONT_SIZE is not None:
+ self._font = qt.QFont(
+ qt.QApplication.instance().font().family(),
+ config.DEFAULT_PLOT_MARKER_TEXT_FONT_SIZE,
+ )
+
self._x = None
self._y = None
+ self._bgColor: colors.RGBAColorType | None = None
self._constraint = self._defaultConstraint
self.__isBeingDragged = False
- def _addRendererCall(self, backend,
- symbol=None, linestyle='-', linewidth=1):
+ def _addRendererCall(self, backend, symbol=None, linestyle="-", linewidth=1):
"""Perform the update of the backend renderer"""
return backend.addMarker(
x=self.getXPosition(),
@@ -74,7 +93,10 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
linestyle=linestyle,
linewidth=linewidth,
constraint=self.getConstraint(),
- yaxis=self.getYAxis())
+ yaxis=self.getYAxis(),
+ font=self._font, # Do not use getFont to spare creating a new QFont
+ bgcolor=self.getBackgroundColor(),
+ )
def _addBackendRenderer(self, backend):
"""Update backend renderer"""
@@ -108,6 +130,39 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
self._text = text
self._updated(ItemChangedType.TEXT)
+ def getFont(self) -> qt.QFont | None:
+ """Returns a copy of the QFont used to render text.
+
+ To modify the text font, use :meth:`setFont`.
+ """
+ return None if self._font is None else qt.QFont(self._font)
+
+ def setFont(self, font: qt.QFont | None):
+ """Set the QFont used to render text, use None for default.
+
+ A copy is stored, so further modification of the provided font are not taken into account.
+ """
+ if font != self._font:
+ self._font = None if font is None else qt.QFont(font)
+ self._updated(ItemChangedType.FONT)
+
+ def getBackgroundColor(self) -> colors.RGBAColorType | None:
+ """Returns the RGBA background color of the item"""
+ return self._bgColor
+
+ def setBackgroundColor(self, color):
+ """Set item text background color
+
+ :param color: color(s) to be used as a str ("#RRGGBB") or (npoints, 4)
+ unsigned byte array or one of the predefined color names
+ defined in colors.py
+ """
+ if color is not None:
+ color = colors.rgba(color)
+ if self._bgColor != color:
+ self._bgColor = color
+ self._updated(ItemChangedType.BACKGROUND_COLOR)
+
def getXPosition(self):
"""Returns the X position of the marker line in data coordinates
@@ -122,14 +177,14 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
"""
return self._y
- def getPosition(self):
+ def getPosition(self) -> tuple[float | None, float | None]:
"""Returns the (x, y) position of the marker in data coordinates
:rtype: 2-tuple of float or None
"""
return self._x, self._y
- def setPosition(self, x, y):
+ def setPosition(self, x: float, y: float):
"""Set marker position in data coordinates
Constraint are applied if any.
@@ -188,15 +243,15 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
class Marker(MarkerBase, SymbolMixIn):
"""Description of a marker"""
- _DEFAULT_SYMBOL = '+'
+ _DEFAULT_SYMBOL = "+"
"""Default symbol of the marker"""
def __init__(self):
MarkerBase.__init__(self)
SymbolMixIn.__init__(self)
- self._x = 0.
- self._y = 0.
+ self._x = 0.0
+ self._y = 0.0
def _addBackendRenderer(self, backend):
return self._addRendererCall(backend, symbol=self.getSymbol())
@@ -209,9 +264,9 @@ class Marker(MarkerBase, SymbolMixIn):
:param constraint: The constraint of the dragging of this marker
:type: constraint: callable or str
"""
- if constraint == 'horizontal':
+ if constraint == "horizontal":
constraint = self._horizontalConstraint
- elif constraint == 'vertical':
+ elif constraint == "vertical":
constraint = self._verticalConstraint
super(Marker, self)._setConstraint(constraint)
@@ -231,9 +286,9 @@ class _LineMarker(MarkerBase, LineMixIn):
LineMixIn.__init__(self)
def _addBackendRenderer(self, backend):
- return self._addRendererCall(backend,
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth())
+ return self._addRendererCall(
+ backend, linestyle=self.getLineStyle(), linewidth=self.getLineWidth()
+ )
class XMarker(_LineMarker):
@@ -241,7 +296,7 @@ class XMarker(_LineMarker):
def __init__(self):
_LineMarker.__init__(self)
- self._x = 0.
+ self._x = 0.0
def setPosition(self, x, y):
"""Set marker line position in data coordinates
@@ -263,7 +318,7 @@ class YMarker(_LineMarker):
def __init__(self):
_LineMarker.__init__(self)
- self._y = 0.
+ self._y = 0.0
def setPosition(self, x, y):
"""Set marker line position in data coordinates
diff --git a/src/silx/gui/plot/items/roi.py b/src/silx/gui/plot/items/roi.py
index 559e7e0..7390b88 100644
--- a/src/silx/gui/plot/items/roi.py
+++ b/src/silx/gui/plot/items/roi.py
@@ -35,6 +35,7 @@ __date__ = "28/06/2018"
import logging
import numpy
+from typing import Tuple
from ... import utils
from .. import items
@@ -60,15 +61,15 @@ logger = logging.getLogger(__name__)
class PointROI(RegionOfInterest, items.SymbolMixIn):
"""A ROI identifying a point in a 2D plot."""
- ICON = 'add-shape-point'
- NAME = 'point markers'
+ ICON = "add-shape-point"
+ NAME = "point markers"
SHORT_NAME = "point"
"""Metadata for this kind of ROI"""
_plotShape = "point"
"""Plot shape which is used for the first interaction"""
- _DEFAULT_SYMBOL = '+'
+ _DEFAULT_SYMBOL = "+"
"""Default symbol of the PointROI
It overwrite the `SymbolMixIn` class attribte.
@@ -88,30 +89,26 @@ class PointROI(RegionOfInterest, items.SymbolMixIn):
self.setPosition(points[0])
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
+ if event == items.ItemChangedType.EDITABLE:
self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
self._updateItemProperty(event, self, self._marker)
super(PointROI, self)._updated(event, checkVisibility)
+ def _updateText(self, text: str):
+ self._marker.setText(text)
+
def _updatedStyle(self, event, style):
self._marker.setColor(style.getColor())
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
+ def getPosition(self) -> Tuple[float, float]:
+ """Returns the position of this ROI"""
return self._marker.getPosition()
def setPosition(self, pos):
"""Set the position of this ROI
- :param numpy.ndarray pos: 2d-coordinate of this point
+ :param pos: 2d-coordinate of this point
"""
self._marker.setPosition(*pos)
@@ -126,16 +123,15 @@ class PointROI(RegionOfInterest, items.SymbolMixIn):
self.sigRegionChanged.emit()
def __str__(self):
- params = '%f %f' % self.getPosition()
+ params = "%f %f" % self.getPosition()
return "%s(%s)" % (self.__class__.__name__, params)
class CrossROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a point in a 2D plot and displayed as a cross
- """
+ """A ROI identifying a point in a 2D plot and displayed as a cross"""
- ICON = 'add-shape-cross'
- NAME = 'cross marker'
+ ICON = "add-shape-cross"
+ NAME = "cross marker"
SHORT_NAME = "cross"
"""Metadata for this kind of ROI"""
@@ -177,17 +173,14 @@ class CrossROI(HandleBasedROI, items.LineMixIn):
pos = points[0]
self.setPosition(pos)
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
+ def getPosition(self) -> Tuple[float, float]:
+ """Returns the position of this ROI"""
return self._handle.getPosition()
- def setPosition(self, pos):
+ def setPosition(self, pos: Tuple[float, float]):
"""Set the position of this ROI
- :param numpy.ndarray pos: 2d-coordinate of this point
+ :param pos: 2d-coordinate of this point
"""
self._handle.setPosition(*pos)
@@ -213,8 +206,8 @@ class LineROI(HandleBasedROI, items.LineMixIn):
in the center to translate the full ROI.
"""
- ICON = 'add-shape-diagonal'
- NAME = 'line ROI'
+ ICON = "add-shape-diagonal"
+ NAME = "line ROI"
SHORT_NAME = "line"
"""Metadata for this kind of ROI"""
@@ -244,11 +237,12 @@ class LineROI(HandleBasedROI, items.LineMixIn):
self._updateItemProperty(event, self, self.__shape)
super(LineROI, self)._updated(event, checkVisibility)
- def _updatedStyle(self, event, style):
+ def _updatedStyle(self, event, style: items.CurveStyle):
super(LineROI, self)._updatedStyle(event, style)
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -257,7 +251,7 @@ class LineROI(HandleBasedROI, items.LineMixIn):
def _updateText(self, text):
self._handleLabel.setText(text)
- def setEndPoints(self, startPoint, endPoint):
+ def setEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray):
"""Set this line location using the ending points
:param numpy.ndarray startPoint: Staring bounding point of the line
@@ -266,7 +260,7 @@ class LineROI(HandleBasedROI, items.LineMixIn):
if not numpy.array_equal((startPoint, endPoint), self.getEndPoints()):
self.__updateEndPoints(startPoint, endPoint)
- def __updateEndPoints(self, startPoint, endPoint):
+ def __updateEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray):
"""Update marker and shape to match given end points
:param numpy.ndarray startPoint: Staring bounding point of the line
@@ -328,28 +322,44 @@ class LineROI(HandleBasedROI, items.LineMixIn):
return False
return (
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=bottom_left, seg2_end_pt=bottom_right) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=bottom_right, seg2_end_pt=top_right) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=top_right, seg2_end_pt=top_left) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=top_left, seg2_end_pt=bottom_left)
+ segments_intersection(
+ seg1_start_pt=line_pt1,
+ seg1_end_pt=line_pt2,
+ seg2_start_pt=bottom_left,
+ seg2_end_pt=bottom_right,
+ )
+ or segments_intersection(
+ seg1_start_pt=line_pt1,
+ seg1_end_pt=line_pt2,
+ seg2_start_pt=bottom_right,
+ seg2_end_pt=top_right,
+ )
+ or segments_intersection(
+ seg1_start_pt=line_pt1,
+ seg1_end_pt=line_pt2,
+ seg2_start_pt=top_right,
+ seg2_end_pt=top_left,
+ )
+ or segments_intersection(
+ seg1_start_pt=line_pt1,
+ seg1_end_pt=line_pt2,
+ seg2_start_pt=top_left,
+ seg2_end_pt=bottom_left,
+ )
) is not None
def __str__(self):
start, end = self.getEndPoints()
params = start[0], start[1], end[0], end[1]
- params = 'start: %f %f; end: %f %f' % params
+ params = "start: %f %f; end: %f %f" % params
return "%s(%s)" % (self.__class__.__name__, params)
class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
"""A ROI identifying an horizontal line in a 2D plot."""
- ICON = 'add-shape-horizontal'
- NAME = 'horizontal line ROI'
+ ICON = "add-shape-horizontal"
+ NAME = "horizontal line ROI"
SHORT_NAME = "hline"
"""Metadata for this kind of ROI"""
@@ -366,16 +376,15 @@ class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
self.addItem(self._marker)
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
+ if event == items.ItemChangedType.EDITABLE:
self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
self._updateItemProperty(event, self, self._marker)
super(HorizontalLineROI, self)._updated(event, checkVisibility)
+ def _updateText(self, text: str):
+ self._marker.setText(text)
+
def _updatedStyle(self, event, style):
self._marker.setColor(style.getColor())
self._marker.setLineStyle(style.getLineStyle())
@@ -387,18 +396,15 @@ class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
return
self.setPosition(pos)
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
+ def getPosition(self) -> float:
+ """Returns the position of this line if the horizontal axis"""
pos = self._marker.getPosition()
return pos[1]
- def setPosition(self, pos):
+ def setPosition(self, pos: float):
"""Set the position of this ROI
- :param float pos: Horizontal position of this line
+ :param pos: Horizontal position of this line
"""
self._marker.setPosition(0, pos)
@@ -412,15 +418,15 @@ class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
self.sigRegionChanged.emit()
def __str__(self):
- params = 'y: %f' % self.getPosition()
+ params = "y: %f" % self.getPosition()
return "%s(%s)" % (self.__class__.__name__, params)
class VerticalLineROI(RegionOfInterest, items.LineMixIn):
"""A ROI identifying a vertical line in a 2D plot."""
- ICON = 'add-shape-vertical'
- NAME = 'vertical line ROI'
+ ICON = "add-shape-vertical"
+ NAME = "vertical line ROI"
SHORT_NAME = "vline"
"""Metadata for this kind of ROI"""
@@ -437,16 +443,15 @@ class VerticalLineROI(RegionOfInterest, items.LineMixIn):
self.addItem(self._marker)
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
+ if event == items.ItemChangedType.EDITABLE:
self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
self._updateItemProperty(event, self, self._marker)
super(VerticalLineROI, self)._updated(event, checkVisibility)
+ def _updateText(self, text: str):
+ self._marker.setText(text)
+
def _updatedStyle(self, event, style):
self._marker.setColor(style.getColor())
self._marker.setLineStyle(style.getLineStyle())
@@ -456,15 +461,12 @@ class VerticalLineROI(RegionOfInterest, items.LineMixIn):
pos = points[0, 0]
self.setPosition(pos)
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
+ def getPosition(self) -> float:
+ """Returns the position of this line if the horizontal axis"""
pos = self._marker.getPosition()
return pos[0]
- def setPosition(self, pos):
+ def setPosition(self, pos: float):
"""Set the position of this ROI
:param float pos: Horizontal position of this line
@@ -481,7 +483,7 @@ class VerticalLineROI(RegionOfInterest, items.LineMixIn):
self.sigRegionChanged.emit()
def __str__(self):
- params = 'x: %f' % self.getPosition()
+ params = "x: %f" % self.getPosition()
return "%s(%s)" % (self.__class__.__name__, params)
@@ -492,8 +494,8 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
center to translate the full ROI.
"""
- ICON = 'add-shape-rectangle'
- NAME = 'rectangle ROI'
+ ICON = "add-shape-rectangle"
+ NAME = "rectangle ROI"
SHORT_NAME = "rectangle"
"""Metadata for this kind of ROI"""
@@ -530,6 +532,7 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -598,11 +601,12 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
self.setGeometry(center=position, size=size)
def setGeometry(self, origin=None, size=None, center=None):
- """Set the geometry of the ROI
- """
- if ((origin is None or numpy.array_equal(origin, self.getOrigin())) and
- (center is None or numpy.array_equal(center, self.getCenter())) and
- numpy.array_equal(size, self.getSize())):
+ """Set the geometry of the ROI"""
+ if (
+ (origin is None or numpy.array_equal(origin, self.getOrigin()))
+ and (center is None or numpy.array_equal(center, self.getCenter()))
+ and numpy.array_equal(size, self.getSize())
+ ):
return # Nothing has changed
self._updateGeometry(origin, size, center)
@@ -661,17 +665,38 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
points = numpy.array([current, current2])
# Switch handles if they were crossed by interaction
- if self._handleBottomLeft.getXPosition() > self._handleBottomRight.getXPosition():
- self._handleBottomLeft, self._handleBottomRight = self._handleBottomRight, self._handleBottomLeft
+ if (
+ self._handleBottomLeft.getXPosition()
+ > self._handleBottomRight.getXPosition()
+ ):
+ self._handleBottomLeft, self._handleBottomRight = (
+ self._handleBottomRight,
+ self._handleBottomLeft,
+ )
if self._handleTopLeft.getXPosition() > self._handleTopRight.getXPosition():
- self._handleTopLeft, self._handleTopRight = self._handleTopRight, self._handleTopLeft
-
- if self._handleBottomLeft.getYPosition() > self._handleTopLeft.getYPosition():
- self._handleBottomLeft, self._handleTopLeft = self._handleTopLeft, self._handleBottomLeft
-
- if self._handleBottomRight.getYPosition() > self._handleTopRight.getYPosition():
- self._handleBottomRight, self._handleTopRight = self._handleTopRight, self._handleBottomRight
+ self._handleTopLeft, self._handleTopRight = (
+ self._handleTopRight,
+ self._handleTopLeft,
+ )
+
+ if (
+ self._handleBottomLeft.getYPosition()
+ > self._handleTopLeft.getYPosition()
+ ):
+ self._handleBottomLeft, self._handleTopLeft = (
+ self._handleTopLeft,
+ self._handleBottomLeft,
+ )
+
+ if (
+ self._handleBottomRight.getYPosition()
+ > self._handleTopRight.getYPosition()
+ ):
+ self._handleBottomRight, self._handleTopRight = (
+ self._handleTopRight,
+ self._handleBottomRight,
+ )
self._setBound(points)
@@ -679,7 +704,7 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
origin = self.getOrigin()
w, h = self.getSize()
params = origin[0], origin[1], w, h
- params = 'origin: %f %f; width: %f; height: %f' % params
+ params = "origin: %f %f; width: %f; height: %f" % params
return "%s(%s)" % (self.__class__.__name__, params)
@@ -690,8 +715,8 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
and one anchor on the perimeter to change the radius.
"""
- ICON = 'add-shape-circle'
- NAME = 'circle ROI'
+ ICON = "add-shape-circle"
+ NAME = "circle ROI"
SHORT_NAME = "circle"
"""Metadata for this kind of ROI"""
@@ -731,6 +756,7 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -779,8 +805,7 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
self._updateGeometry()
def setGeometry(self, center, radius):
- """Set the geometry of the ROI
- """
+ """Set the geometry of the ROI"""
if numpy.array_equal(center, self.getCenter()):
self.setRadius(radius)
else:
@@ -797,8 +822,9 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
nbpoints = 27
angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints
- circleShape = numpy.array((numpy.cos(angles) * self.__radius,
- numpy.sin(angles) * self.__radius)).T
+ circleShape = numpy.array(
+ (numpy.cos(angles) * self.__radius, numpy.sin(angles) * self.__radius)
+ ).T
circleShape += center
self.__shape.setPoints(circleShape)
self.sigRegionChanged.emit()
@@ -821,7 +847,7 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
center = self.getCenter()
radius = self.getRadius()
params = center[0], center[1], radius
- params = 'center: %f %f; radius: %f;' % params
+ params = "center: %f %f; radius: %f;" % params
return "%s(%s)" % (self.__class__.__name__, params)
@@ -833,8 +859,8 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
minor-radius. These two anchors also allow to change the orientation.
"""
- ICON = 'add-shape-ellipse'
- NAME = 'ellipse ROI'
+ ICON = "add-shape-ellipse"
+ NAME = "ellipse ROI"
SHORT_NAME = "ellipse"
"""Metadata for this kind of ROI"""
@@ -860,8 +886,10 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
self.__shape = shape
self.addItem(shape)
- self._radius = 0., 0.
- self._orientation = 0. # angle in radians between the X-axis and the _handleAxis0
+ self._radius = 0.0, 0.0
+ self._orientation = (
+ 0.0 # angle in radians between the X-axis and the _handleAxis0
+ )
def _updated(self, event=None, checkVisibility=True):
if event == items.ItemChangedType.VISIBLE:
@@ -873,6 +901,7 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -905,9 +934,9 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
center = points[0]
radius = numpy.linalg.norm(points[0] - points[1])
orientation = self._calculateOrientation(points[0], points[1])
- self.setGeometry(center=center,
- radius=(radius, radius),
- orientation=orientation)
+ self.setGeometry(
+ center=center, radius=(radius, radius), orientation=orientation
+ )
def _updateText(self, text):
self._handleLabel.setText(text)
@@ -1007,10 +1036,11 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
# ensure that we store the orientation in range [0, 2*pi
orientation = numpy.mod(orientation, 2 * numpy.pi)
- if (numpy.array_equal(center, self.getCenter()) or
- radius != self._radius or
- orientation != self._orientation):
-
+ if (
+ numpy.array_equal(center, self.getCenter())
+ or radius != self._radius
+ or orientation != self._orientation
+ ):
# Update parameters directly
self._radius = radius
self._orientation = orientation
@@ -1030,10 +1060,18 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
# _handleAxis1 is the major axis
orientation -= numpy.pi / 2
- point0 = numpy.array([center[0] + self._radius[0] * numpy.cos(orientation),
- center[1] + self._radius[0] * numpy.sin(orientation)])
- point1 = numpy.array([center[0] - self._radius[1] * numpy.sin(orientation),
- center[1] + self._radius[1] * numpy.cos(orientation)])
+ point0 = numpy.array(
+ [
+ center[0] + self._radius[0] * numpy.cos(orientation),
+ center[1] + self._radius[0] * numpy.sin(orientation),
+ ]
+ )
+ point1 = numpy.array(
+ [
+ center[0] - self._radius[1] * numpy.sin(orientation),
+ center[1] + self._radius[1] * numpy.cos(orientation),
+ ]
+ )
with utils.blockSignals(self._handleAxis0):
self._handleAxis0.setPosition(*point0)
with utils.blockSignals(self._handleAxis1):
@@ -1043,10 +1081,12 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
nbpoints = 27
angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints
- X = (self._radius[0] * numpy.cos(angles) * numpy.cos(orientation)
- - self._radius[1] * numpy.sin(angles) * numpy.sin(orientation))
- Y = (self._radius[0] * numpy.cos(angles) * numpy.sin(orientation)
- + self._radius[1] * numpy.sin(angles) * numpy.cos(orientation))
+ X = self._radius[0] * numpy.cos(angles) * numpy.cos(orientation) - self._radius[
+ 1
+ ] * numpy.sin(angles) * numpy.sin(orientation)
+ Y = self._radius[0] * numpy.cos(angles) * numpy.sin(orientation) + self._radius[
+ 1
+ ] * numpy.sin(angles) * numpy.cos(orientation)
ellipseShape = numpy.array((X, Y)).T
ellipseShape += center
@@ -1083,8 +1123,10 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
major, minor = self.getMajorRadius(), self.getMinorRadius()
delta = self.getOrientation()
x, y = position - self.getCenter()
- return ((x*numpy.cos(delta) + y*numpy.sin(delta))**2/major**2 +
- (x*numpy.sin(delta) - y*numpy.cos(delta))**2/minor**2) <= 1
+ return (
+ (x * numpy.cos(delta) + y * numpy.sin(delta)) ** 2 / major**2
+ + (x * numpy.sin(delta) - y * numpy.cos(delta)) ** 2 / minor**2
+ ) <= 1
def __str__(self):
center = self.getCenter()
@@ -1092,7 +1134,10 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
minor = self.getMinorRadius()
orientation = self.getOrientation()
params = center[0], center[1], major, minor, orientation
- params = 'center: %f %f; major radius: %f: minor radius: %f; orientation: %f' % params
+ params = (
+ "center: %f %f; major radius: %f: minor radius: %f; orientation: %f"
+ % params
+ )
return "%s(%s)" % (self.__class__.__name__, params)
@@ -1102,8 +1147,8 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
This ROI provides 1 anchor for each point of the polygon.
"""
- ICON = 'add-shape-polygon'
- NAME = 'polygon ROI'
+ ICON = "add-shape-polygon"
+ NAME = "polygon ROI"
SHORT_NAME = "polygon"
"""Metadata for this kind of ROI"""
@@ -1134,6 +1179,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
if self._handleClose is not None:
color = self._computeHandleColor(style.getColor())
self._handleClose.setColor(color)
@@ -1156,8 +1202,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
self.setPoints(points)
def creationStarted(self):
- """"Called when the ROI creation interaction was started.
- """
+ """Called when the ROI creation interaction was started."""
# Handle to see where to close the polygon
self._handleClose = self.addUserHandle()
self._handleClose.setSymbol("o")
@@ -1178,8 +1223,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
return self._handleClose is not None
def creationFinalized(self):
- """"Called when the ROI creation interaction was finalized.
- """
+ """Called when the ROI creation interaction was finalized."""
self.removeHandle(self._handleClose)
self._handleClose = None
self.removeItem(self.__shape)
@@ -1206,7 +1250,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
:param numpy.ndarray pos: 2d-coordinate of this point
"""
- assert(len(points.shape) == 2 and points.shape[1] == 2)
+ assert len(points.shape) == 2 and points.shape[1] == 2
if numpy.array_equal(points, self._points):
return # Nothing has changed
@@ -1277,7 +1321,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
def __str__(self):
points = self._points
- params = '; '.join('%f %f' % (pt[0], pt[1]) for pt in points)
+ params = "; ".join("%f %f" % (pt[0], pt[1]) for pt in points)
return "%s(%s)" % (self.__class__.__name__, params)
@docstring(HandleBasedROI)
@@ -1300,8 +1344,8 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
"""A ROI identifying an horizontal range in a 1D plot."""
- ICON = 'add-range-horizontal'
- NAME = 'horizontal range ROI'
+ ICON = "add-range-horizontal"
+ NAME = "horizontal range ROI"
SHORT_NAME = "hrange"
_plotShape = "line"
@@ -1333,16 +1377,13 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
self._updatePos(vmin, vmax)
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- self._updateText()
- elif event == items.ItemChangedType.EDITABLE:
+ if event == items.ItemChangedType.EDITABLE:
self._updateEditable()
- self._updateText()
+ self._updateText(self.getText())
elif event == items.ItemChangedType.LINE_STYLE:
markers = [self._markerMin, self._markerMax]
self._updateItemProperty(event, self, markers)
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
markers = [self._markerMin, self._markerMax, self._markerCen]
self._updateItemProperty(event, self, markers)
super(HorizontalRangeROI, self)._updated(event, checkVisibility)
@@ -1353,8 +1394,7 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
m.setColor(style.getColor())
m.setLineWidth(style.getLineWidth())
- def _updateText(self):
- text = self.getName()
+ def _updateText(self, text: str):
if self.isEditable():
self._markerMin.setText("")
self._markerCen.setText(text)
@@ -1409,8 +1449,10 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
err = "Can't set vmin or vmax to None"
raise ValueError(err)
if vmin > vmax:
- err = "Can't set vmin and vmax because vmin >= vmax " \
- "vmin = %s, vmax = %s" % (vmin, vmax)
+ err = (
+ "Can't set vmin and vmax because vmin >= vmax "
+ "vmin = %s, vmax = %s" % (vmin, vmax)
+ )
raise ValueError(err)
self._updatePos(vmin, vmax)
@@ -1515,5 +1557,5 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
def __str__(self):
vrange = self.getRange()
- params = 'min: %f; max: %f' % vrange
+ params = "min: %f; max: %f" % vrange
return "%s(%s)" % (self.__class__.__name__, params)
diff --git a/src/silx/gui/plot/items/scatter.py b/src/silx/gui/plot/items/scatter.py
index 96fb311..c46b60c 100644
--- a/src/silx/gui/plot/items/scatter.py
+++ b/src/silx/gui/plot/items/scatter.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -33,6 +33,7 @@ from collections import namedtuple
import logging
import threading
import numpy
+from matplotlib.tri import LinearTriInterpolator, Triangulation
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, CancelledError
@@ -41,7 +42,6 @@ from ....utils.proxy import docstring
from ....math.combo import min_max
from ....math.histogram import Histogramnd
from ....utils.weakref import WeakList
-from .._utils.delaunay import delaunay
from .core import PointsBase, ColormapMixIn, ScatterVisualizationMixIn
from .axis import Axis
from ._pick import PickingResult
@@ -51,8 +51,7 @@ _logger = logging.getLogger(__name__)
class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
- """:class:`ThreadPoolExecutor` with an extra :meth:`submit_greedy` method.
- """
+ """:class:`ThreadPoolExecutor` with an extra :meth:`submit_greedy` method."""
def __init__(self, *args, **kwargs):
super(_GreedyThreadPoolExecutor, self).__init__(*args, **kwargs)
@@ -76,8 +75,7 @@ class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
if not future.done():
future.cancel()
- future = super(_GreedyThreadPoolExecutor, self).submit(
- fn, *args, **kwargs)
+ future = super(_GreedyThreadPoolExecutor, self).submit(fn, *args, **kwargs)
self.__futures[queue].append(future)
return future
@@ -85,6 +83,7 @@ class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
# Functions to guess grid shape from coordinates
+
def _get_z_line_length(array):
"""Return length of line if array is a Z-like 2D regular grid.
@@ -97,7 +96,7 @@ def _get_z_line_length(array):
if len(sign) == 0 or sign[0] == 0: # We don't handle that
return 0
# Check this way to account for 0 sign (i.e., diff == 0)
- beginnings = numpy.where(sign == - sign[0])[0] + 1
+ beginnings = numpy.where(sign == -sign[0])[0] + 1
if len(beginnings) == 0:
return 0
length = beginnings[0]
@@ -121,11 +120,11 @@ def _guess_z_grid_shape(x, y):
"""
width = _get_z_line_length(x)
if width != 0:
- return 'row', (int(numpy.ceil(len(x) / width)), width)
+ return "row", (int(numpy.ceil(len(x) / width)), width)
else:
height = _get_z_line_length(y)
if height != 0:
- return 'column', (height, int(numpy.ceil(len(y) / height)))
+ return "column", (height, int(numpy.ceil(len(y) / height)))
return None
@@ -139,7 +138,7 @@ def is_monotonic(array):
:rtype: int
"""
diff = numpy.diff(numpy.ravel(array))
- with numpy.errstate(invalid='ignore'):
+ with numpy.errstate(invalid="ignore"):
if numpy.all(diff >= 0):
return 1
elif numpy.all(diff <= 0):
@@ -168,7 +167,7 @@ def _guess_grid(x, y):
else:
# Cannot guess a regular grid
# Let's assume it's a single line
- order = 'row' # or 'column' doesn't matter for a single line
+ order = "row" # or 'column' doesn't matter for a single line
y_monotonic = is_monotonic(y)
if is_monotonic(x) or y_monotonic: # we can guess a line
x_min, x_max = min_max(x)
@@ -211,18 +210,24 @@ def _quadrilateral_grid_coords(points):
neighbour_view = numpy.lib.stride_tricks.as_strided(
points,
shape=(dim0 - 1, dim1 - 1, 2, 2, points.shape[2]),
- strides=points.strides[:2] + points.strides[:2] + points.strides[-1:], writeable=False)
+ strides=points.strides[:2] + points.strides[:2] + points.strides[-1:],
+ writeable=False,
+ )
inner_points = numpy.mean(neighbour_view, axis=(2, 3))
grid_points[1:-1, 1:-1] = inner_points
# Compute 'vertical' sides
# Alternative: grid_points[1:-1, [0, -1]] = points[:-1, [0, -1]] + points[1:, [0, -1]] - inner_points[:, [0, -1]]
- grid_points[1:-1, [0, -1], 0] = points[:-1, [0, -1], 0] + points[1:, [0, -1], 0] - inner_points[:, [0, -1], 0]
+ grid_points[1:-1, [0, -1], 0] = (
+ points[:-1, [0, -1], 0] + points[1:, [0, -1], 0] - inner_points[:, [0, -1], 0]
+ )
grid_points[1:-1, [0, -1], 1] = inner_points[:, [0, -1], 1]
# Compute 'horizontal' sides
grid_points[[0, -1], 1:-1, 0] = inner_points[[0, -1], :, 0]
- grid_points[[0, -1], 1:-1, 1] = points[[0, -1], :-1, 1] + points[[0, -1], 1:, 1] - inner_points[[0, -1], :, 1]
+ grid_points[[0, -1], 1:-1, 1] = (
+ points[[0, -1], :-1, 1] + points[[0, -1], 1:, 1] - inner_points[[0, -1], :, 1]
+ )
# Compute corners
d0, d1 = [0, 0, -1, -1], [0, -1, -1, 0]
@@ -259,11 +264,13 @@ def _quadrilateral_grid_as_triangles(points):
_RegularGridInfo = namedtuple(
- '_RegularGridInfo', ['bounds', 'origin', 'scale', 'shape', 'order'])
+ "_RegularGridInfo", ["bounds", "origin", "scale", "shape", "order"]
+)
_HistogramInfo = namedtuple(
- '_HistogramInfo', ['mean', 'count', 'sum', 'origin', 'scale', 'shape'])
+ "_HistogramInfo", ["mean", "count", "sum", "origin", "scale", "shape"]
+)
class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
@@ -278,7 +285,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
ScatterVisualizationMixIn.Visualization.REGULAR_GRID,
ScatterVisualizationMixIn.Visualization.IRREGULAR_GRID,
ScatterVisualizationMixIn.Visualization.BINNED_STATISTIC,
- )
+ )
"""Overrides supported Visualizations"""
def __init__(self):
@@ -288,7 +295,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
self._value = ()
self.__alpha = None
# Cache Delaunay triangulation future object
- self.__delaunayFuture = None
+ self.__triangulationFuture = None
# Cache interpolator future object
self.__interpolatorFuture = None
self.__executor = None
@@ -310,7 +317,9 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
data = getattr(
histoInfo,
self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION))
+ self.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ),
+ )
else:
data = self.getValueData(copy=False)
self._setColormappedData(data, copy=False)
@@ -319,8 +328,9 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
def setVisualization(self, mode):
previous = self.getVisualization()
if super().setVisualization(mode):
- if (bool(mode is self.Visualization.BINNED_STATISTIC) ^
- bool(previous is self.Visualization.BINNED_STATISTIC)):
+ if bool(mode is self.Visualization.BINNED_STATISTIC) ^ bool(
+ previous is self.Visualization.BINNED_STATISTIC
+ ):
self._updateColormappedData()
return True
else:
@@ -331,16 +341,22 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
parameter = self.VisualizationParameter.from_value(parameter)
if super(Scatter, self).setVisualizationParameter(parameter, value):
- if parameter in (self.VisualizationParameter.GRID_BOUNDS,
- self.VisualizationParameter.GRID_MAJOR_ORDER,
- self.VisualizationParameter.GRID_SHAPE):
+ if parameter in (
+ self.VisualizationParameter.GRID_BOUNDS,
+ self.VisualizationParameter.GRID_MAJOR_ORDER,
+ self.VisualizationParameter.GRID_SHAPE,
+ ):
self.__cacheRegularGridInfo = None
- if parameter in (self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION,
- self.VisualizationParameter.DATA_BOUNDS_HINT):
- if parameter in (self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- self.VisualizationParameter.DATA_BOUNDS_HINT):
+ if parameter in (
+ self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
+ self.VisualizationParameter.BINNED_STATISTIC_FUNCTION,
+ self.VisualizationParameter.DATA_BOUNDS_HINT,
+ ):
+ if parameter in (
+ self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
+ self.VisualizationParameter.DATA_BOUNDS_HINT,
+ ):
self.__cacheHistogramInfo = None # Clean-up cache
if self.getVisualization() is self.Visualization.BINNED_STATISTIC:
self._updateColormappedData()
@@ -351,14 +367,16 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
@docstring(ScatterVisualizationMixIn)
def getCurrentVisualizationParameter(self, parameter):
value = self.getVisualizationParameter(parameter)
- if (parameter is self.VisualizationParameter.DATA_BOUNDS_HINT or
- value is not None):
+ if (
+ parameter is self.VisualizationParameter.DATA_BOUNDS_HINT
+ or value is not None
+ ):
return value # Value has been set, return it
elif parameter is self.VisualizationParameter.GRID_BOUNDS:
grid = self.__getRegularGridInfo()
return None if grid is None else grid.bounds
-
+
elif parameter is self.VisualizationParameter.GRID_MAJOR_ORDER:
grid = self.__getRegularGridInfo()
return None if grid is None else grid.order
@@ -378,15 +396,19 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
"""Get grid info"""
if self.__cacheRegularGridInfo is None:
shape = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_SHAPE)
+ self.VisualizationParameter.GRID_SHAPE
+ )
order = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_MAJOR_ORDER)
+ self.VisualizationParameter.GRID_MAJOR_ORDER
+ )
if shape is None or order is None:
- guess = _guess_grid(self.getXData(copy=False),
- self.getYData(copy=False))
+ guess = _guess_grid(
+ self.getXData(copy=False), self.getYData(copy=False)
+ )
if guess is None:
_logger.warning(
- 'Cannot guess a grid: Cannot display as regular grid image')
+ "Cannot guess a grid: Cannot display as regular grid image"
+ )
return None
if shape is None:
shape = guess[1]
@@ -397,16 +419,18 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
if nbpoints > shape[0] * shape[1]:
# More data points that provided grid shape: enlarge grid
_logger.warning(
- "More data points than provided grid shape size: extends grid")
+ "More data points than provided grid shape size: extends grid"
+ )
dim0, dim1 = shape
- if order == 'row': # keep dim1, enlarge dim0
+ if order == "row": # keep dim1, enlarge dim0
dim0 = nbpoints // dim1 + (1 if nbpoints % dim1 else 0)
else: # keep dim0, enlarge dim1
dim1 = nbpoints // dim0 + (1 if nbpoints % dim0 else 0)
shape = dim0, dim1
bounds = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_BOUNDS)
+ self.VisualizationParameter.GRID_BOUNDS
+ )
if bounds is None:
x, y = self.getXData(copy=False), self.getYData(copy=False)
min_, max_ = min_max(x)
@@ -416,10 +440,12 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
bounds = (xRange[0], yRange[0]), (xRange[1], yRange[1])
begin, end = bounds
- scale = ((end[0] - begin[0]) / max(1, shape[1] - 1),
- (end[1] - begin[1]) / max(1, shape[0] - 1))
+ scale = (
+ (end[0] - begin[0]) / max(1, shape[1] - 1),
+ (end[1] - begin[1]) / max(1, shape[0] - 1),
+ )
if scale[0] == 0 and scale[1] == 0:
- scale = 1., 1.
+ scale = 1.0, 1.0
elif scale[0] == 0:
scale = scale[1], scale[1]
elif scale[1] == 0:
@@ -428,7 +454,8 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
origin = begin[0] - 0.5 * scale[0], begin[1] - 0.5 * scale[1]
self.__cacheRegularGridInfo = _RegularGridInfo(
- bounds=bounds, origin=origin, scale=scale, shape=shape, order=order)
+ bounds=bounds, origin=origin, scale=scale, shape=shape, order=order
+ )
return self.__cacheRegularGridInfo
@@ -436,9 +463,10 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
"""Get histogram info"""
if self.__cacheHistogramInfo is None:
shape = self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_SHAPE)
+ self.VisualizationParameter.BINNED_STATISTIC_SHAPE
+ )
if shape is None:
- shape = 100, 100 # TODO compute auto shape
+ shape = 100, 100 # TODO compute auto shape
x, y, values = self.getData(copy=False)[:3]
if len(x) == 0: # No histogram
@@ -451,31 +479,40 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
if not numpy.issubdtype(values.dtype, numpy.floating):
values = values.astype(numpy.float64)
- ranges = (tuple(min_max(y, finite=True)),
- tuple(min_max(x, finite=True)))
+ ranges = (tuple(min_max(y, finite=True)), tuple(min_max(x, finite=True)))
rangesHint = self.getVisualizationParameter(
- self.VisualizationParameter.DATA_BOUNDS_HINT)
+ self.VisualizationParameter.DATA_BOUNDS_HINT
+ )
if rangesHint is not None:
- ranges = tuple((min(dataMin, hintMin), max(dataMax, hintMax))
- for (dataMin, dataMax), (hintMin, hintMax) in zip(ranges, rangesHint))
+ ranges = tuple(
+ (min(dataMin, hintMin), max(dataMax, hintMax))
+ for (dataMin, dataMax), (hintMin, hintMax) in zip(
+ ranges, rangesHint
+ )
+ )
points = numpy.transpose(numpy.array((y, x)))
counts, sums, bin_edges = Histogramnd(
- points,
- histo_range=ranges,
- n_bins=shape,
- weights=values)
+ points, histo_range=ranges, n_bins=shape, weights=values
+ )
yEdges, xEdges = bin_edges
origin = xEdges[0], yEdges[0]
- scale = ((xEdges[-1] - xEdges[0]) / (len(xEdges) - 1),
- (yEdges[-1] - yEdges[0]) / (len(yEdges) - 1))
+ scale = (
+ (xEdges[-1] - xEdges[0]) / (len(xEdges) - 1),
+ (yEdges[-1] - yEdges[0]) / (len(yEdges) - 1),
+ )
- with numpy.errstate(divide='ignore', invalid='ignore'):
+ with numpy.errstate(divide="ignore", invalid="ignore"):
histo = sums / counts
self.__cacheHistogramInfo = _HistogramInfo(
- mean=histo, count=counts, sum=sums,
- origin=origin, scale=scale, shape=shape)
+ mean=histo,
+ count=counts,
+ sum=sums,
+ origin=origin,
+ scale=scale,
+ shape=shape,
+ )
return self.__cacheHistogramInfo
@@ -495,7 +532,8 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
"""Update backend renderer"""
# Filter-out values <= 0
xFiltered, yFiltered, valueFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
+ copy=False, displayed=True
+ )
# Remove not finite numbers (this includes filtered out x, y <= 0)
mask = numpy.logical_and(numpy.isfinite(xFiltered), numpy.isfinite(yFiltered))
@@ -509,62 +547,79 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
if visualization is self.Visualization.BINNED_STATISTIC:
plot = self.getPlot()
- if (plot is None or
- plot.getXAxis().getScale() != Axis.LINEAR or
- plot.getYAxis().getScale() != Axis.LINEAR):
+ if (
+ plot is None
+ or plot.getXAxis().getScale() != Axis.LINEAR
+ or plot.getYAxis().getScale() != Axis.LINEAR
+ ):
# Those visualizations are not available with log scaled axes
return None
histoInfo = self.__getHistogramInfo()
if histoInfo is None:
return None
- data = getattr(histoInfo, self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION))
+ data = getattr(
+ histoInfo,
+ self.getVisualizationParameter(
+ self.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ),
+ )
return backend.addImage(
data=data,
origin=histoInfo.origin,
scale=histoInfo.scale,
colormap=self.getColormap(),
- alpha=self.getAlpha())
+ alpha=self.getAlpha(),
+ )
elif visualization is self.Visualization.POINTS:
rgbacolors = self.__applyColormapToData()
- return backend.addCurve(xFiltered, yFiltered,
- color=rgbacolors[mask],
- symbol=self.getSymbol(),
- linewidth=0,
- linestyle="",
- yaxis='left',
- xerror=xerror,
- yerror=yerror,
- fill=False,
- alpha=self.getAlpha(),
- symbolsize=self.getSymbolSize(),
- baseline=None)
+ return backend.addCurve(
+ xFiltered,
+ yFiltered,
+ color=rgbacolors[mask],
+ gapcolor=None,
+ symbol=self.getSymbol(),
+ linewidth=0,
+ linestyle="",
+ yaxis="left",
+ xerror=xerror,
+ yerror=yerror,
+ fill=False,
+ alpha=self.getAlpha(),
+ symbolsize=self.getSymbolSize(),
+ baseline=None,
+ )
else:
plot = self.getPlot()
- if (plot is None or
- plot.getXAxis().getScale() != Axis.LINEAR or
- plot.getYAxis().getScale() != Axis.LINEAR):
+ if (
+ plot is None
+ or plot.getXAxis().getScale() != Axis.LINEAR
+ or plot.getYAxis().getScale() != Axis.LINEAR
+ ):
# Those visualizations are not available with log scaled axes
return None
if visualization is self.Visualization.SOLID:
- triangulation = self._getDelaunay().result()
- if triangulation is None:
+ try:
+ triangulation = self._getTriangulationFuture().result()
+ except (RuntimeError, ValueError):
_logger.warning(
- 'Cannot get a triangulation: Cannot display as solid surface')
+ "Cannot get a triangulation: Cannot display as solid surface"
+ )
return None
else:
rgbacolors = self.__applyColormapToData()
- triangles = triangulation.simplices.astype(numpy.int32)
- return backend.addTriangles(xFiltered,
- yFiltered,
- triangles,
- color=rgbacolors[mask],
- alpha=self.getAlpha())
+ triangles = triangulation.triangles.astype(numpy.int32)
+ return backend.addTriangles(
+ xFiltered,
+ yFiltered,
+ triangles,
+ color=rgbacolors[mask],
+ alpha=self.getAlpha(),
+ )
elif visualization is self.Visualization.REGULAR_GRID:
gridInfo = self.__getRegularGridInfo()
@@ -572,7 +627,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
return None
dim0, dim1 = gridInfo.shape
- if gridInfo.order == 'column': # transposition needed
+ if gridInfo.order == "column": # transposition needed
dim0, dim1 = dim1, dim0
values = self.getValueData(copy=False)
@@ -580,20 +635,21 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
image = values.reshape(dim0, dim1)
else:
# The points do not fill the whole image
- if (self.__alpha is None and
- numpy.issubdtype(values.dtype, numpy.floating)):
+ if self.__alpha is None and numpy.issubdtype(
+ values.dtype, numpy.floating
+ ):
image = numpy.empty(dim0 * dim1, dtype=values.dtype)
- image[:len(values)] = values
- image[len(values):] = float('nan') # Transparent pixels
+ image[: len(values)] = values
+ image[len(values) :] = float("nan") # Transparent pixels
image.shape = dim0, dim1
else: # Per value alpha or no NaN, so convert to RGBA
rgbacolors = self.__applyColormapToData()
image = numpy.empty((dim0 * dim1, 4), dtype=numpy.uint8)
- image[:len(rgbacolors)] = rgbacolors
- image[len(rgbacolors):] = (0, 0, 0, 0) # Transparent pixels
+ image[: len(rgbacolors)] = rgbacolors
+ image[len(rgbacolors) :] = (0, 0, 0, 0) # Transparent pixels
image.shape = dim0, dim1, 4
- if gridInfo.order == 'column':
+ if gridInfo.order == "column":
if image.ndim == 2:
image = numpy.transpose(image)
else:
@@ -613,7 +669,8 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
origin=gridInfo.origin,
scale=gridInfo.scale,
colormap=colormap,
- alpha=self.getAlpha())
+ alpha=self.getAlpha(),
+ )
elif visualization is self.Visualization.IRREGULAR_GRID:
gridInfo = self.__getRegularGridInfo()
@@ -629,33 +686,37 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
nbpoints = len(xFiltered)
if nbpoints == 1:
# single point, render as a square points
- return backend.addCurve(xFiltered, yFiltered,
- color=rgbacolors[mask],
- symbol='s',
- linewidth=0,
- linestyle="",
- yaxis='left',
- xerror=None,
- yerror=None,
- fill=False,
- alpha=self.getAlpha(),
- symbolsize=7,
- baseline=None)
+ return backend.addCurve(
+ xFiltered,
+ yFiltered,
+ color=rgbacolors[mask],
+ gapcolor=None,
+ symbol="s",
+ linewidth=0,
+ linestyle="",
+ yaxis="left",
+ xerror=None,
+ yerror=None,
+ fill=False,
+ alpha=self.getAlpha(),
+ symbolsize=7,
+ baseline=None,
+ )
# Make shape include all points
gridOrder = gridInfo.order
if nbpoints != numpy.prod(shape):
- if gridOrder == 'row':
+ if gridOrder == "row":
shape = int(numpy.ceil(nbpoints / shape[1])), shape[1]
- else: # column-major order
+ else: # column-major order
shape = shape[0], int(numpy.ceil(nbpoints / shape[0]))
if shape[0] < 2 or shape[1] < 2: # Single line, at least 2 points
points = numpy.ones((2, nbpoints, 2), dtype=numpy.float64)
# Use row/column major depending on shape, not on info value
- gridOrder = 'row' if shape[0] == 1 else 'column'
+ gridOrder = "row" if shape[0] == 1 else "column"
- if gridOrder == 'row':
+ if gridOrder == "row":
points[0, :, 0] = xFiltered
points[0, :, 1] = yFiltered
else: # column-major order
@@ -663,35 +724,51 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
points[0, :, 1] = xFiltered
# Add a second line that will be clipped in the end
- points[1, :-1] = points[0, :-1] + numpy.cross(
- points[0, 1:] - points[0, :-1], (0., 0., 1.))[:, :2]
- points[1, -1] = points[0, -1] + numpy.cross(
- points[0, -1] - points[0, -2], (0., 0., 1.))[:2]
+ points[1, :-1] = (
+ points[0, :-1]
+ + numpy.cross(points[0, 1:] - points[0, :-1], (0.0, 0.0, 1.0))[
+ :, :2
+ ]
+ )
+ points[1, -1] = (
+ points[0, -1]
+ + numpy.cross(points[0, -1] - points[0, -2], (0.0, 0.0, 1.0))[
+ :2
+ ]
+ )
points.shape = 2, nbpoints, 2 # Use same shape for both orders
coords, indices = _quadrilateral_grid_as_triangles(points)
- elif gridOrder == 'row': # row-major order
+ elif gridOrder == "row": # row-major order
if nbpoints != numpy.prod(shape):
- points = numpy.empty((numpy.prod(shape), 2), dtype=numpy.float64)
+ points = numpy.empty(
+ (numpy.prod(shape), 2), dtype=numpy.float64
+ )
points[:nbpoints, 0] = xFiltered
points[:nbpoints, 1] = yFiltered
# Index of last element of last fully filled row
index = (nbpoints // shape[1]) * shape[1]
- points[nbpoints:, 0] = xFiltered[index - (numpy.prod(shape) - nbpoints):index]
+ points[nbpoints:, 0] = xFiltered[
+ index - (numpy.prod(shape) - nbpoints) : index
+ ]
points[nbpoints:, 1] = yFiltered[-1]
else:
points = numpy.transpose((xFiltered, yFiltered))
points.shape = shape[0], shape[1], 2
- else: # column-major order
+ else: # column-major order
if nbpoints != numpy.prod(shape):
- points = numpy.empty((numpy.prod(shape), 2), dtype=numpy.float64)
+ points = numpy.empty(
+ (numpy.prod(shape), 2), dtype=numpy.float64
+ )
points[:nbpoints, 0] = yFiltered
points[:nbpoints, 1] = xFiltered
# Index of last element of last fully filled column
index = (nbpoints // shape[0]) * shape[0]
- points[nbpoints:, 0] = yFiltered[index - (numpy.prod(shape) - nbpoints):index]
+ points[nbpoints:, 0] = yFiltered[
+ index - (numpy.prod(shape) - nbpoints) : index
+ ]
points[nbpoints:, 1] = xFiltered[-1]
else:
points = numpy.transpose((yFiltered, xFiltered))
@@ -700,25 +777,24 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
coords, indices = _quadrilateral_grid_as_triangles(points)
# Remove unused extra triangles
- coords = coords[:4*nbpoints]
- indices = indices[:2*nbpoints]
+ coords = coords[: 4 * nbpoints]
+ indices = indices[: 2 * nbpoints]
- if gridOrder == 'row':
+ if gridOrder == "row":
x, y = coords[:, 0], coords[:, 1]
else: # column-major order
y, x = coords[:, 0], coords[:, 1]
rgbacolors = rgbacolors[mask] # Filter-out not finite points
gridcolors = numpy.empty(
- (4 * nbpoints, rgbacolors.shape[-1]), dtype=rgbacolors.dtype)
+ (4 * nbpoints, rgbacolors.shape[-1]), dtype=rgbacolors.dtype
+ )
for first in range(4):
gridcolors[first::4] = rgbacolors[:nbpoints]
- return backend.addTriangles(x,
- y,
- indices,
- color=gridcolors,
- alpha=self.getAlpha())
+ return backend.addTriangles(
+ x, y, indices, color=gridcolors, alpha=self.getAlpha()
+ )
else:
_logger.error("Unhandled visualization %s", visualization)
@@ -747,11 +823,13 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
if gridInfo is None:
return None
- if gridInfo.order == 'row':
+ if gridInfo.order == "row":
index = row * gridInfo.shape[1] + column
else:
index = row + column * gridInfo.shape[0]
- if index >= len(self.getXData(copy=False)): # OK as long as not log scale
+ if index >= len(
+ self.getXData(copy=False)
+ ): # OK as long as not log scale
return None # Image can be larger than scatter
result = PickingResult(self, (index,))
@@ -768,9 +846,16 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
ox, oy = histoInfo.origin
xdata = self.getXData(copy=False)
ydata = self.getYData(copy=False)
- indices = numpy.nonzero(numpy.logical_and(
- numpy.logical_and(xdata >= ox + sx * col, xdata < ox + sx * (col + 1)),
- numpy.logical_and(ydata >= oy + sy * row, ydata < oy + sy * (row + 1))))[0]
+ indices = numpy.nonzero(
+ numpy.logical_and(
+ numpy.logical_and(
+ xdata >= ox + sx * col, xdata < ox + sx * (col + 1)
+ ),
+ numpy.logical_and(
+ ydata >= oy + sy * row, ydata < oy + sy * (row + 1)
+ ),
+ )
+ )[0]
result = None if len(indices) == 0 else PickingResult(self, indices)
return result
@@ -784,69 +869,43 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
self.__executor = _GreedyThreadPoolExecutor(max_workers=2)
return self.__executor
- def _getDelaunay(self):
- """Returns a :class:`Future` which result is the Delaunay object.
+ def _getTriangulationFuture(self):
+ """Returns a :class:`Future` which result is the Triangulation object.
:rtype: concurrent.futures.Future
"""
- if self.__delaunayFuture is None or self.__delaunayFuture.cancelled():
+ if self.__triangulationFuture is None or self.__triangulationFuture.cancelled():
# Need to init a new delaunay
x, y = self.getData(copy=False)[:2]
# Remove not finite points
mask = numpy.logical_and(numpy.isfinite(x), numpy.isfinite(y))
- self.__delaunayFuture = self.__getExecutor().submit_greedy(
- 'delaunay', delaunay, x[mask], y[mask])
+ self.__triangulationFuture = self.__getExecutor().submit_greedy(
+ "Triangulation", Triangulation, x[mask], y[mask]
+ )
- return self.__delaunayFuture
+ return self.__triangulationFuture
@staticmethod
- def __initInterpolator(delaunayFuture, values):
+ def __initInterpolator(triangulationFuture, values):
"""Returns an interpolator for the given data points
- :param concurrent.futures.Future delaunayFuture:
- Future object which result is a Delaunay object
+ :param concurrent.futures.Future triangulationFuture:
+ Future object which result is a Triangulation object
:param numpy.ndarray values: The data value of valid points.
:rtype: Union[callable,None]
"""
- # Wait for Delaunay to complete
+ # Wait for Triangulation to complete
try:
- triangulation = delaunayFuture.result()
+ triangulation = triangulationFuture.result()
+ except (RuntimeError, ValueError):
+ return None # triangulation failed
except CancelledError:
- triangulation = None
-
- if triangulation is None:
- interpolator = None # Error case
- else:
- # Lazy-loading of interpolator
- try:
- from scipy.interpolate import LinearNDInterpolator
- except ImportError:
- LinearNDInterpolator = None
-
- if LinearNDInterpolator is not None:
- interpolator = LinearNDInterpolator(triangulation, values)
-
- # First call takes a while, do it here
- interpolator([(0., 0.)])
-
- else:
- # Fallback using matplotlib interpolator
- import matplotlib.tri
-
- x, y = triangulation.points.T
- tri = matplotlib.tri.Triangulation(
- x, y, triangles=triangulation.simplices)
- mplInterpolator = matplotlib.tri.LinearTriInterpolator(
- tri, values)
-
- # Wrap interpolator to have same API as scipy's one
- def interpolator(points):
- return mplInterpolator(*points.T)
+ return None
- return interpolator
+ return LinearTriInterpolator(triangulation, values)
- def _getInterpolator(self):
+ def _getInterpolatorFuture(self):
"""Returns a :class:`Future` which result is the interpolator.
The interpolator is a callable taking an array Nx2 of points
@@ -856,8 +915,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
:rtype: concurrent.futures.Future
"""
- if (self.__interpolatorFuture is None or
- self.__interpolatorFuture.cancelled()):
+ if self.__interpolatorFuture is None or self.__interpolatorFuture.cancelled():
# Need to init a new interpolator
x, y, values = self.getData(copy=False)[:3]
# Remove not finite points
@@ -865,8 +923,11 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
x, y, values = x[mask], y[mask], values[mask]
self.__interpolatorFuture = self.__getExecutor().submit_greedy(
- 'interpolator',
- self.__initInterpolator, self._getDelaunay(), values)
+ "interpolator",
+ self.__initInterpolator,
+ self._getTriangulationFuture(),
+ values,
+ )
return self.__interpolatorFuture
def _logFilterData(self, xPositive, yPositive):
@@ -928,11 +989,13 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
assert len(data) == 5
return data
- return (self.getXData(copy),
- self.getYData(copy),
- self.getValueData(copy),
- self.getXErrorData(copy),
- self.getYErrorData(copy))
+ return (
+ self.getXData(copy),
+ self.getYData(copy),
+ self.getValueData(copy),
+ self.getXErrorData(copy),
+ self.getYErrorData(copy),
+ )
# reimplemented from PointsBase to handle `value`
def setData(self, x, y, value, xerror=None, yerror=None, alpha=None, copy=True):
@@ -951,7 +1014,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
:param yerror: Values with the uncertainties on the y values
:type yerror: A float, or a numpy.ndarray of float32. See xerror.
:param alpha: Values with the transparency (between 0 and 1)
- :type alpha: A float, or a numpy.ndarray of float32
+ :type alpha: A float, or a numpy.ndarray of float32
:param bool copy: True make a copy of the data (default),
False to use provided arrays.
"""
@@ -961,14 +1024,13 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
# Convert complex data
if numpy.iscomplexobj(value):
- _logger.warning(
- 'Converting value data to absolute value to plot it.')
+ _logger.warning("Converting value data to absolute value to plot it.")
value = numpy.absolute(value)
# Reset triangulation and interpolator
- if self.__delaunayFuture is not None:
- self.__delaunayFuture.cancel()
- self.__delaunayFuture = None
+ if self.__triangulationFuture is not None:
+ self.__triangulationFuture.cancel()
+ self.__triangulationFuture = None
if self.__interpolatorFuture is not None:
self.__interpolatorFuture.cancel()
self.__interpolatorFuture = None
@@ -984,10 +1046,10 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
alpha = numpy.array(alpha, copy=copy)
assert alpha.ndim == 1
assert len(x) == len(alpha)
- if alpha.dtype.kind != 'f':
+ if alpha.dtype.kind != "f":
alpha = alpha.astype(numpy.float32)
- if numpy.any(numpy.logical_or(alpha < 0., alpha > 1.)):
- alpha = numpy.clip(alpha, 0., 1.)
+ if numpy.any(numpy.logical_or(alpha < 0.0, alpha > 1.0)):
+ alpha = numpy.clip(alpha, 0.0, 1.0)
self.__alpha = alpha
# set x, y, xerror, yerror
diff --git a/src/silx/gui/plot/items/shape.py b/src/silx/gui/plot/items/shape.py
index dc35864..c911924 100644
--- a/src/silx/gui/plot/items/shape.py
+++ b/src/silx/gui/plot/items/shape.py
@@ -33,11 +33,18 @@ import logging
import numpy
-from ... import colors
-from ..utils.intersections import lines_intersection
from .core import (
- Item, DataItem,
- AlphaMixIn, ColorMixIn, FillMixIn, ItemChangedType, ItemMixInBase, LineMixIn, YAxisMixIn)
+ Item,
+ DataItem,
+ AlphaMixIn,
+ ColorMixIn,
+ FillMixIn,
+ ItemChangedType,
+ LineMixIn,
+ LineGapColorMixIn,
+ YAxisMixIn,
+)
+from ....utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
@@ -65,41 +72,20 @@ class _OverlayItem(Item):
self._updated(ItemChangedType.OVERLAY)
-class _TwoColorsLineMixIn(LineMixIn):
+class _TwoColorsLineMixIn(LineMixIn, LineGapColorMixIn):
"""Mix-in class for items with a background color for dashes"""
def __init__(self):
LineMixIn.__init__(self)
- self.__backgroundColor = None
+ LineGapColorMixIn.__init__(self)
+ @deprecated(replacement="getLineGapColor", since_version="2.0.0")
def getLineBgColor(self):
- """Returns the RGBA background color of dash line
+ return self.getLineGapColor()
- :rtype: 4-tuple of float in [0, 1] or array of colors
- """
- return self.__backgroundColor
-
- def setLineBgColor(self, color, copy: bool=True):
- """Set dash line background color
-
- :param color: color(s) to be used
- :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in colors.py
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- if color is not None:
- if isinstance(color, str):
- color = colors.rgba(color)
- else:
- color = numpy.array(color, copy=copy)
- # TODO more checks + improve color array support
- if color.ndim == 1: # Single RGBA color
- color = colors.rgba(color)
- else: # Array of colors
- assert color.ndim == 2
-
- self.__backgroundColor = color
+ @deprecated(replacement="setLineGapColor", since_version="2.0.0")
+ def setLineBgColor(self, color, copy: bool = True):
+ self.setLineGapColor(color)
self._updated(ItemChangedType.LINE_BG_COLOR)
@@ -117,7 +103,7 @@ class Shape(_OverlayItem, ColorMixIn, FillMixIn, _TwoColorsLineMixIn):
ColorMixIn.__init__(self)
FillMixIn.__init__(self)
_TwoColorsLineMixIn.__init__(self)
- assert type_ in ('hline', 'polygon', 'rectangle', 'vline', 'polylines')
+ assert type_ in ("hline", "polygon", "rectangle", "vline", "polylines")
self._type = type_
self._points = ()
self._handle = None
@@ -126,15 +112,17 @@ class Shape(_OverlayItem, ColorMixIn, FillMixIn, _TwoColorsLineMixIn):
"""Update backend renderer"""
points = self.getPoints(copy=False)
x, y = points.T[0], points.T[1]
- return backend.addShape(x,
- y,
- shape=self.getType(),
- color=self.getColor(),
- fill=self.isFill(),
- overlay=self.isOverlay(),
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- linebgcolor=self.getLineBgColor())
+ return backend.addShape(
+ x,
+ y,
+ shape=self.getType(),
+ color=self.getColor(),
+ fill=self.isFill(),
+ overlay=self.isOverlay(),
+ linestyle=self.getLineStyle(),
+ linewidth=self.getLineWidth(),
+ gapcolor=self.getLineGapColor(),
+ )
def getType(self):
"""Returns the type of shape to draw.
@@ -226,11 +214,11 @@ class _BaseExtent(DataItem):
:param str axis: Either 'x' or 'y'.
"""
- def __init__(self, axis='x'):
- assert axis in ('x', 'y')
+ def __init__(self, axis="x"):
+ assert axis in ("x", "y")
DataItem.__init__(self)
self.__axis = axis
- self.__range = 1., 100.
+ self.__range = 1.0, 100.0
def setRange(self, min_, max_):
"""Set the range of the extent of this item in data coordinates.
@@ -262,17 +250,17 @@ class _BaseExtent(DataItem):
plot = self.getPlot()
if plot is not None:
- axis = plot.getXAxis() if self.__axis == 'x' else plot.getYAxis()
+ axis = plot.getXAxis() if self.__axis == "x" else plot.getYAxis()
if axis._isLogarithmic():
if max_ <= 0:
return None
if min_ <= 0:
min_ = max_
- if self.__axis == 'x':
- return min_, max_, float('nan'), float('nan')
+ if self.__axis == "x":
+ return min_, max_, float("nan"), float("nan")
else:
- return float('nan'), float('nan'), min_, max_
+ return float("nan"), float("nan"), min_, max_
class XAxisExtent(_BaseExtent):
@@ -282,8 +270,9 @@ class XAxisExtent(_BaseExtent):
item with a horizontal extent regarding plot data bounds, i.e.,
:meth:`PlotWidget.resetZoom` will take this horizontal extent into account.
"""
+
def __init__(self):
- _BaseExtent.__init__(self, axis='x')
+ _BaseExtent.__init__(self, axis="x")
class YAxisExtent(_BaseExtent, YAxisMixIn):
@@ -295,7 +284,7 @@ class YAxisExtent(_BaseExtent, YAxisMixIn):
"""
def __init__(self):
- _BaseExtent.__init__(self, axis='y')
+ _BaseExtent.__init__(self, axis="y")
YAxisMixIn.__init__(self)
@@ -305,7 +294,7 @@ class Line(_OverlayItem, AlphaMixIn, ColorMixIn, _TwoColorsLineMixIn):
Warning: If slope is not finite, then the line is x = intercept.
"""
- def __init__(self, slope: float=0, intercept: float=0):
+ def __init__(self, slope: float = 0, intercept: float = 0):
assert numpy.isfinite(intercept)
_OverlayItem.__init__(self)
@@ -378,7 +367,7 @@ class Line(_OverlayItem, AlphaMixIn, ColorMixIn, _TwoColorsLineMixIn):
"""Set slope and intercept from 2 (x, y) points"""
x0, y0 = point0
x1, y1 = point1
- if x0 == x1: # Special case: vertical line
+ if x0 == x1: # Special case: vertical line
self.setSlope(float("inf"))
self.setIntercept(x0)
return
@@ -394,11 +383,11 @@ class Line(_OverlayItem, AlphaMixIn, ColorMixIn, _TwoColorsLineMixIn):
return backend.addShape(
*self.__coordinates,
- shape='polylines',
+ shape="polylines",
color=self.getColor(),
fill=False,
overlay=self.isOverlay(),
linestyle=self.getLineStyle(),
linewidth=self.getLineWidth(),
- linebgcolor=self.getLineBgColor(),
+ gapcolor=self.getLineGapColor(),
)
diff --git a/src/silx/gui/plot/matplotlib/Colormap.py b/src/silx/gui/plot/matplotlib/Colormap.py
deleted file mode 100644
index 1131df8..0000000
--- a/src/silx/gui/plot/matplotlib/Colormap.py
+++ /dev/null
@@ -1,248 +0,0 @@
-# /*##########################################################################
-# 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
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ############################################################################*/
-"""Matplotlib's new colormaps"""
-
-import numpy
-import logging
-from matplotlib.colors import ListedColormap
-import matplotlib.colors
-import matplotlib.cm
-import silx.resources
-from silx.utils.deprecation import deprecated, deprecated_warning
-
-
-deprecated_warning(type_='module',
- name=__file__,
- replacement='silx.gui.colors.Colormap',
- since_version='0.10.0')
-
-
-_logger = logging.getLogger(__name__)
-
-_AVAILABLE_AS_RESOURCE = ('magma', 'inferno', 'plasma', 'viridis')
-"""List available colormap name as resources"""
-
-_AVAILABLE_AS_BUILTINS = ('gray', 'reversed gray',
- 'temperature', 'red', 'green', 'blue')
-"""List of colormaps available through built-in declarations"""
-
-_CMAPS = {}
-"""Cache colormaps"""
-
-
-@property
-@deprecated(since_version='0.10.0')
-def magma():
- return getColormap('magma')
-
-
-@property
-@deprecated(since_version='0.10.0')
-def inferno():
- return getColormap('inferno')
-
-
-@property
-@deprecated(since_version='0.10.0')
-def plasma():
- return getColormap('plasma')
-
-
-@property
-@deprecated(since_version='0.10.0')
-def viridis():
- return getColormap('viridis')
-
-
-@deprecated(since_version='0.10.0')
-def getColormap(name):
- """Returns matplotlib colormap corresponding to given name
-
- :param str name: The name of the colormap
- :return: The corresponding colormap
- :rtype: matplolib.colors.Colormap
- """
- if not _CMAPS: # Lazy initialization of own colormaps
- cdict = {'red': ((0.0, 0.0, 0.0),
- (1.0, 1.0, 1.0)),
- 'green': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0))}
- _CMAPS['red'] = matplotlib.colors.LinearSegmentedColormap(
- 'red', cdict, 256)
-
- cdict = {'red': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'green': ((0.0, 0.0, 0.0),
- (1.0, 1.0, 1.0)),
- 'blue': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0))}
- _CMAPS['green'] = matplotlib.colors.LinearSegmentedColormap(
- 'green', cdict, 256)
-
- cdict = {'red': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'green': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 0.0, 0.0),
- (1.0, 1.0, 1.0))}
- _CMAPS['blue'] = matplotlib.colors.LinearSegmentedColormap(
- 'blue', cdict, 256)
-
- # Temperature as defined in spslut
- cdict = {'red': ((0.0, 0.0, 0.0),
- (0.5, 0.0, 0.0),
- (0.75, 1.0, 1.0),
- (1.0, 1.0, 1.0)),
- 'green': ((0.0, 0.0, 0.0),
- (0.25, 1.0, 1.0),
- (0.75, 1.0, 1.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 1.0, 1.0),
- (0.25, 1.0, 1.0),
- (0.5, 0.0, 0.0),
- (1.0, 0.0, 0.0))}
- # but limited to 256 colors for a faster display (of the colorbar)
- _CMAPS['temperature'] = \
- matplotlib.colors.LinearSegmentedColormap(
- 'temperature', cdict, 256)
-
- # reversed gray
- cdict = {'red': ((0.0, 1.0, 1.0),
- (1.0, 0.0, 0.0)),
- 'green': ((0.0, 1.0, 1.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 1.0, 1.0),
- (1.0, 0.0, 0.0))}
-
- _CMAPS['reversed gray'] = \
- matplotlib.colors.LinearSegmentedColormap(
- 'yerg', cdict, 256)
-
- if name in _CMAPS:
- return _CMAPS[name]
- elif name in _AVAILABLE_AS_RESOURCE:
- filename = silx.resources.resource_filename("gui/colormaps/%s.npy" % name)
- data = numpy.load(filename)
- lut = ListedColormap(data, name=name)
- _CMAPS[name] = lut
- return lut
- else:
- # matplotlib built-in
- return matplotlib.cm.get_cmap(name)
-
-
-@deprecated(since_version='0.10.0')
-def getScalarMappable(colormap, data=None):
- """Returns matplotlib ScalarMappable corresponding to colormap
-
- :param :class:`.Colormap` colormap: The colormap to convert
- :param numpy.ndarray data:
- The data on which the colormap is applied.
- If provided, it is used to compute autoscale.
- :return: matplotlib object corresponding to colormap
- :rtype: matplotlib.cm.ScalarMappable
- """
- assert colormap is not None
-
- if colormap.getName() is not None:
- cmap = getColormap(colormap.getName())
-
- else: # No name, use custom colors
- if colormap.getColormapLUT() is None:
- raise ValueError(
- 'addImage: colormap no name nor list of colors.')
- colors = colormap.getColormapLUT()
- assert len(colors.shape) == 2
- assert colors.shape[-1] in (3, 4)
- if colors.dtype == numpy.uint8:
- # Convert to float in [0., 1.]
- colors = colors.astype(numpy.float32) / 255.
- cmap = matplotlib.colors.ListedColormap(colors)
-
- vmin, vmax = colormap.getColormapRange(data)
- normalization = colormap.getNormalization()
- if normalization == colormap.LOGARITHM:
- norm = matplotlib.colors.LogNorm(vmin, vmax)
- elif normalization == colormap.LINEAR:
- norm = matplotlib.colors.Normalize(vmin, vmax)
- else:
- raise RuntimeError("Unsupported normalization: %s" % normalization)
-
- return matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap)
-
-
-@deprecated(replacement='silx.colors.Colormap.applyToData',
- since_version='0.8.0')
-def applyColormapToData(data, colormap):
- """Apply a colormap to the data and returns the RGBA image
-
- This supports data of any dimensions (not only of dimension 2).
- The returned array will have one more dimension (with 4 entries)
- than the input data to store the RGBA channels
- corresponding to each bin in the array.
-
- :param numpy.ndarray data: The data to convert.
- :param :class:`.Colormap`: The colormap to apply
- """
- # Debian 7 specific support
- # No transparent colormap with matplotlib < 1.2.0
- # Add support for transparent colormap for uint8 data with
- # colormap with 256 colors, linear norm, [0, 255] range
- if matplotlib.__version__ < '1.2.0':
- if (colormap.getName() is None and
- colormap.getColormapLUT() is not None):
- colors = colormap.getColormapLUT()
- if (colors.shape[-1] == 4 and
- not numpy.all(numpy.equal(colors[3], 255))):
- # This is a transparent colormap
- if (colors.shape == (256, 4) and
- colormap.getNormalization() == 'linear' and
- not colormap.isAutoscale() and
- colormap.getVMin() == 0 and
- colormap.getVMax() == 255 and
- data.dtype == numpy.uint8):
- # Supported case, convert data to RGBA
- return colors[data.reshape(-1)].reshape(
- data.shape + (4,))
- else:
- _logger.warning(
- 'matplotlib %s does not support transparent '
- 'colormap.', matplotlib.__version__)
-
- scalarMappable = getScalarMappable(colormap, data)
- rgbaImage = scalarMappable.to_rgba(data, bytes=True)
-
- return rgbaImage
-
-
-@deprecated(replacement='silx.colors.Colormap.getSupportedColormaps',
- since_version='0.10.0')
-def getSupportedColormaps():
- """Get the supported colormap names as a tuple of str.
- """
- colormaps = set(matplotlib.cm.datad.keys())
- colormaps.update(_AVAILABLE_AS_BUILTINS)
- colormaps.update(_AVAILABLE_AS_RESOURCE)
- return tuple(sorted(colormaps))
diff --git a/src/silx/gui/plot/stats/stats.py b/src/silx/gui/plot/stats/stats.py
index d266d5c..d575e3f 100644
--- a/src/silx/gui/plot/stats/stats.py
+++ b/src/silx/gui/plot/stats/stats.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -31,7 +31,6 @@ __license__ = "MIT"
__date__ = "06/06/2018"
-from collections import OrderedDict
from functools import lru_cache
import logging
@@ -44,12 +43,11 @@ from ..items.roi import RegionOfInterest
from ....math.combo import min_max
from silx.utils.proxy import docstring
-from ....utils.deprecation import deprecated
logger = logging.getLogger(__name__)
-class Stats(OrderedDict):
+class Stats(dict):
"""Class to define a set of statistic relative to a dataset
(image, curve...).
@@ -60,15 +58,17 @@ class Stats(OrderedDict):
:param List statslist: List of the :class:`Stat` object to be computed.
"""
+
def __init__(self, statslist=None):
- OrderedDict.__init__(self)
+ super().__init__()
_statslist = statslist if not None else []
if statslist is not None:
for stat in _statslist:
self.add(stat)
- def calculate(self, item, plot, onlimits, roi, data_changed=False,
- roi_changed=False):
+ def calculate(
+ self, item, plot, onlimits, roi, data_changed=False, roi_changed=False
+ ):
"""
Call all :class:`Stat` object registered and return the result of the
computation.
@@ -87,27 +87,26 @@ class Stats(OrderedDict):
of the calculation as value
"""
res = {}
- context = self._getContext(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ context = self._getContext(item=item, plot=plot, onlimits=onlimits, roi=roi)
for statName, stat in list(self.items()):
if context.kind not in stat.compatibleKinds:
- logger.debug('kind %s not managed by statistic %s'
- % (context.kind, stat.name))
+ logger.debug(
+ "kind %s not managed by statistic %s" % (context.kind, stat.name)
+ )
res[statName] = None
else:
if roi_changed is True:
context.clear_mask()
if data_changed is True or roi_changed is True:
# if data changed or mask changed
- context.clipData(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ context.clipData(item=item, plot=plot, onlimits=onlimits, roi=roi)
# init roi and data
res[statName] = stat.calculate(context)
return res
def __setitem__(self, key, value):
assert isinstance(value, StatBase)
- OrderedDict.__setitem__(self, key, value)
+ super().__setitem__(key, value)
def add(self, stat):
"""Add a :class:`Stat` to the set
@@ -134,14 +133,11 @@ class Stats(OrderedDict):
from ...plot3d import items as items3d # Lazy import
if isinstance(item, (items3d.Scatter2D, items3d.Scatter3D)):
- context = _plot3DScatterContext(item, plot, onlimits,
- roi=roi)
- elif isinstance(item,
- (items3d.ImageData, items3d.ScalarField3D)):
- context = _plot3DArrayContext(item, plot, onlimits,
- roi=roi)
+ context = _plot3DScatterContext(item, plot, onlimits, roi=roi)
+ elif isinstance(item, (items3d.ImageData, items3d.ScalarField3D)):
+ context = _plot3DArrayContext(item, plot, onlimits, roi=roi)
if context is None:
- raise ValueError('Item type not managed')
+ raise ValueError("Item type not managed")
return context
@@ -164,6 +160,7 @@ class _StatsContext(object):
For now, incompatible with `onlimits` calculation
:type roi: Union[None,:class:`_RegionOfInterestBase`]
"""
+
def __init__(self, item, kind, plot, onlimits, roi):
assert item
assert plot
@@ -234,13 +231,6 @@ class _StatsContext(object):
"""
raise NotImplementedError("Base class")
- @deprecated(reason="context are now stored and keep during stats life."
- "So this function will be called only once",
- replacement="clipData", since_version="0.13.0")
- def createContext(self, item, plot, onlimits, roi):
- return self.clipData(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
-
def isStructuredData(self):
"""Returns True if data as an array-like structure.
@@ -271,15 +261,18 @@ class _StatsContext(object):
def _checkContextInputs(self, item, plot, onlimits, roi):
if roi is not None and onlimits is True:
- raise ValueError('Stats context is unable to manage both a ROI'
- 'and the `onlimits` option')
+ raise ValueError(
+ "Stats context is unable to manage both a ROI"
+ "and the `onlimits` option"
+ )
class _ScatterCurveHistoMixInContext(_StatsContext):
def __init__(self, kind, item, plot, onlimits, roi):
self.clear_mask()
- _StatsContext.__init__(self, item=item, kind=kind,
- plot=plot, onlimits=onlimits, roi=roi)
+ _StatsContext.__init__(
+ self, item=item, kind=kind, plot=plot, onlimits=onlimits, roi=roi
+ )
def _set_mask_validity(self, onlimits, from_, to_):
self._onlimits = onlimits
@@ -292,8 +285,7 @@ class _ScatterCurveHistoMixInContext(_StatsContext):
self._to_ = None
def is_mask_valid(self, onlimits, from_, to_):
- return (onlimits == self.onlimits and from_ == self._from_ and
- to_ == self._to_)
+ return onlimits == self.onlimits and from_ == self._from_ and to_ == self._to_
class _CurveContext(_ScatterCurveHistoMixInContext):
@@ -308,15 +300,15 @@ class _CurveContext(_ScatterCurveHistoMixInContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
- _ScatterCurveHistoMixInContext.__init__(self, kind='curve', item=item,
- plot=plot, onlimits=onlimits,
- roi=roi)
+ _ScatterCurveHistoMixInContext.__init__(
+ self, kind="curve", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
@docstring(_StatsContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
self.roi = roi
self.onlimits = onlimits
xData, yData = item.getData(copy=True)[0:2]
@@ -353,10 +345,11 @@ class _CurveContext(_ScatterCurveHistoMixInContext):
self.axes = (xData,)
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, ROI):
- raise TypeError('curve `context` can ony manage 1D roi')
+ raise TypeError("curve `context` can ony manage 1D roi")
class _HistogramContext(_ScatterCurveHistoMixInContext):
@@ -371,15 +364,15 @@ class _HistogramContext(_ScatterCurveHistoMixInContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
- _ScatterCurveHistoMixInContext.__init__(self, kind='histogram',
- item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _ScatterCurveHistoMixInContext.__init__(
+ self, kind="histogram", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
@docstring(_StatsContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
yData, edges = item.getData(copy=True)[0:2]
xData = item._revertComputeEdges(x=edges, histogramType=item.getAlignment())
@@ -392,13 +385,16 @@ class _HistogramContext(_ScatterCurveHistoMixInContext):
mask = mask == 0
self._set_mask_validity(onlimits=onlimits, from_=minX, to_=maxX)
elif roi:
- if self.is_mask_valid(onlimits=onlimits, from_=roi._fromdata, to_=roi._todata):
+ if self.is_mask_valid(
+ onlimits=onlimits, from_=roi._fromdata, to_=roi._todata
+ ):
mask = self.mask
else:
mask = (roi._fromdata <= xData) & (xData <= roi._todata)
mask = mask == 0
- self._set_mask_validity(onlimits=onlimits, from_=roi._fromdata,
- to_=roi._todata)
+ self._set_mask_validity(
+ onlimits=onlimits, from_=roi._fromdata, to_=roi._todata
+ )
else:
mask = numpy.zeros_like(yData)
mask = mask.astype(numpy.uint32)
@@ -414,11 +410,12 @@ class _HistogramContext(_ScatterCurveHistoMixInContext):
self.axes = (self.xData,)
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, ROI):
- raise TypeError('curve `context` can ony manage 1D roi')
+ raise TypeError("curve `context` can ony manage 1D roi")
class _ScatterContext(_ScatterCurveHistoMixInContext):
@@ -434,15 +431,15 @@ class _ScatterContext(_ScatterCurveHistoMixInContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
- _ScatterCurveHistoMixInContext.__init__(self, kind='scatter',
- item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _ScatterCurveHistoMixInContext.__init__(
+ self, kind="scatter", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
@docstring(_ScatterCurveHistoMixInContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
valueData = item.getValueData(copy=True)
xData = item.getXData(copy=True)
yData = item.getYData(copy=True)
@@ -461,8 +458,9 @@ class _ScatterContext(_ScatterCurveHistoMixInContext):
yData = yData[(minY <= yData) & (yData <= maxY)]
if roi:
- if self.is_mask_valid(onlimits=onlimits, from_=roi.getFrom(),
- to_=roi.getTo()):
+ if self.is_mask_valid(
+ onlimits=onlimits, from_=roi.getFrom(), to_=roi.getTo()
+ ):
mask = self.mask
else:
mask = (xData < roi.getFrom()) | (xData > roi.getTo())
@@ -480,11 +478,12 @@ class _ScatterContext(_ScatterCurveHistoMixInContext):
self.min, self.max = None, None
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, ROI):
- raise TypeError('curve `context` can ony manage 1D roi')
+ raise TypeError("curve `context` can ony manage 1D roi")
class _ImageContext(_StatsContext):
@@ -511,13 +510,14 @@ class _ImageContext(_StatsContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
self.clear_mask()
- _StatsContext.__init__(self, kind='image', item=item,
- plot=plot, onlimits=onlimits, roi=roi)
+ _StatsContext.__init__(
+ self, kind="image", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
- def _set_mask_validity(self, xmin: float, xmax: float, ymin: float, ymax
- : float):
+ def _set_mask_validity(self, xmin: float, xmax: float, ymin: float, ymax: float):
self._mask_x_min = xmin
self._mask_x_max = xmax
self._mask_y_min = ymin
@@ -530,13 +530,16 @@ class _ImageContext(_StatsContext):
self._mask_y_max = None
def is_mask_valid(self, xmin, xmax, ymin, ymax):
- return (xmin == self._mask_x_min and xmax == self._mask_x_max and
- ymin == self._mask_y_min and ymax == self._mask_y_max)
+ return (
+ xmin == self._mask_x_min
+ and xmax == self._mask_x_max
+ and ymin == self._mask_y_min
+ and ymax == self._mask_y_max
+ )
@docstring(_StatsContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
self.origin = item.getOrigin()
self.scale = item.getScale()
@@ -560,8 +563,9 @@ class _ImageContext(_StatsContext):
if XMaxBound <= XMinBound or YMaxBound <= YMinBound:
self.data = None
else:
- self.data = self.data[YMinBound:YMaxBound + 1,
- XMinBound:XMaxBound + 1]
+ self.data = self.data[
+ YMinBound : YMaxBound + 1, XMinBound : XMaxBound + 1
+ ]
mask = numpy.zeros_like(self.data)
elif roi:
minX, maxX = 0, self.data.shape[1]
@@ -572,8 +576,9 @@ class _ImageContext(_StatsContext):
XMaxBound = min(maxX, self.data.shape[1])
YMaxBound = min(maxY, self.data.shape[0])
- if self.is_mask_valid(xmin=XMinBound, xmax=XMaxBound,
- ymin=YMinBound, ymax=YMaxBound):
+ if self.is_mask_valid(
+ xmin=XMinBound, xmax=XMaxBound, ymin=YMinBound, ymax=YMaxBound
+ ):
mask = self.mask
else:
for x in range(XMinBound, XMaxBound):
@@ -581,8 +586,9 @@ class _ImageContext(_StatsContext):
_x = (x * self.scale[0]) + self.origin[0]
_y = (y * self.scale[1]) + self.origin[1]
mask[y, x] = not roi.contains((_x, _y))
- self._set_mask_validity(xmin=XMinBound, xmax=XMaxBound,
- ymin=YMinBound, ymax=YMaxBound)
+ self._set_mask_validity(
+ xmin=XMinBound, xmax=XMaxBound, ymin=YMinBound, ymax=YMaxBound
+ )
self.values = numpy.ma.array(self.data, mask=mask)
if self.values.compressed().size > 0:
self.min, self.max = min_max(self.values.compressed())
@@ -590,15 +596,18 @@ class _ImageContext(_StatsContext):
self.min, self.max = None, None
if self.values is not None:
- self.axes = (self.origin[1] + self.scale[1] * numpy.arange(self.data.shape[0]),
- self.origin[0] + self.scale[0] * numpy.arange(self.data.shape[1]))
+ self.axes = (
+ self.origin[1] + self.scale[1] * numpy.arange(self.data.shape[0]),
+ self.origin[0] + self.scale[0] * numpy.arange(self.data.shape[1]),
+ )
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, RegionOfInterest):
- raise TypeError('curve `context` can ony manage 2D roi')
+ raise TypeError("curve `context` can ony manage 2D roi")
class _plot3DScatterContext(_StatsContext):
@@ -615,14 +624,15 @@ class _plot3DScatterContext(_StatsContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
- _StatsContext.__init__(self, kind='scatter', item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext.__init__(
+ self, kind="scatter", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
@docstring(_StatsContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
if onlimits:
raise RuntimeError("Unsupported plot %s" % str(plot))
values = item.getValueData(copy=False)
@@ -646,11 +656,12 @@ class _plot3DScatterContext(_StatsContext):
self.min, self.max = None, None
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, RegionOfInterest):
- raise TypeError('curve `context` can ony manage 2D roi')
+ raise TypeError("curve `context` can ony manage 2D roi")
class _plot3DArrayContext(_StatsContext):
@@ -667,14 +678,15 @@ class _plot3DArrayContext(_StatsContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
- _StatsContext.__init__(self, kind='image', item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext.__init__(
+ self, kind="image", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
@docstring(_StatsContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
if onlimits:
raise RuntimeError("Unsupported plot %s" % str(plot))
@@ -696,14 +708,15 @@ class _plot3DArrayContext(_StatsContext):
self.min, self.max = None, None
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, RegionOfInterest):
- raise TypeError('curve `context` can ony manage 2D roi')
+ raise TypeError("curve `context` can ony manage 2D roi")
-BASIC_COMPATIBLE_KINDS = 'curve', 'image', 'scatter', 'histogram'
+BASIC_COMPATIBLE_KINDS = "curve", "image", "scatter", "histogram"
class StatBase(object):
@@ -714,6 +727,7 @@ class StatBase(object):
:param List[str] compatibleKinds:
The kind of items (curve, scatter...) for which the statistic apply.
"""
+
def __init__(self, name, compatibleKinds=BASIC_COMPATIBLE_KINDS, description=None):
self.name = name
self.compatibleKinds = compatibleKinds
@@ -726,7 +740,7 @@ class StatBase(object):
:param _StatsContext context:
:return dict: key is stat name, statistic computed is the dict value
"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def getToolTip(self, kind):
"""
@@ -749,6 +763,7 @@ class Stat(StatBase):
:param tuple kinds: the compatible item kinds of the function (curve,
image...)
"""
+
def __init__(self, name, fct, kinds=BASIC_COMPATIBLE_KINDS):
StatBase.__init__(self, name, kinds)
self._fct = fct
@@ -759,16 +774,18 @@ class Stat(StatBase):
if context.kind in self.compatibleKinds:
return self._fct(context.values)
else:
- raise ValueError('Kind %s not managed by %s'
- '' % (context.kind, self.name))
+ raise ValueError(
+ "Kind %s not managed by %s" "" % (context.kind, self.name)
+ )
else:
return None
class StatMin(StatBase):
"""Compute the minimal value on data"""
+
def __init__(self):
- StatBase.__init__(self, name='min')
+ StatBase.__init__(self, name="min")
@docstring(StatBase)
def calculate(self, context):
@@ -777,8 +794,9 @@ class StatMin(StatBase):
class StatMax(StatBase):
"""Compute the maximal value on data"""
+
def __init__(self):
- StatBase.__init__(self, name='max')
+ StatBase.__init__(self, name="max")
@docstring(StatBase)
def calculate(self, context):
@@ -787,8 +805,9 @@ class StatMax(StatBase):
class StatDelta(StatBase):
"""Compute the delta between minimal and maximal on data"""
+
def __init__(self):
- StatBase.__init__(self, name='delta')
+ StatBase.__init__(self, name="delta")
@docstring(StatBase)
def calculate(self, context):
@@ -822,8 +841,9 @@ class _StatCoord(StatBase):
class StatCoordMin(_StatCoord):
"""Compute the coordinates of the first minimum value of the data"""
+
def __init__(self):
- _StatCoord.__init__(self, name='coords min')
+ _StatCoord.__init__(self, name="coords min")
@docstring(StatBase)
def calculate(self, context):
@@ -840,8 +860,9 @@ class StatCoordMin(_StatCoord):
class StatCoordMax(_StatCoord):
"""Compute the coordinates of the first maximum value of the data"""
+
def __init__(self):
- _StatCoord.__init__(self, name='coords max')
+ _StatCoord.__init__(self, name="coords max")
@docstring(StatBase)
def calculate(self, context):
@@ -860,8 +881,9 @@ class StatCoordMax(_StatCoord):
class StatCOM(StatBase):
"""Compute data center of mass"""
+
def __init__(self):
- StatBase.__init__(self, name='COM', description='Center of mass')
+ StatBase.__init__(self, name="COM", description="Center of mass")
@docstring(StatBase)
def calculate(self, context):
@@ -870,7 +892,7 @@ class StatCOM(StatBase):
values = numpy.ma.array(context.values, mask=context.mask, dtype=numpy.float64)
sum_ = numpy.sum(values)
- if sum_ == 0. or numpy.ma.is_masked(sum_):
+ if sum_ == 0.0 or numpy.ma.is_masked(sum_):
return (numpy.nan,) * len(context.axes)
if context.isStructuredData():
@@ -878,11 +900,11 @@ class StatCOM(StatBase):
for index, axis in enumerate(context.axes):
axes = tuple([i for i in range(len(context.axes)) if i != index])
centerofmass.append(
- numpy.sum(axis * numpy.sum(values, axis=axes)) / sum_)
+ numpy.sum(axis * numpy.sum(values, axis=axes)) / sum_
+ )
return tuple(reversed(centerofmass))
else:
- return tuple(
- numpy.sum(axis * values) / sum_ for axis in context.axes)
+ return tuple(numpy.sum(axis * values) / sum_ for axis in context.axes)
@docstring(StatBase)
def getToolTip(self, kind):
diff --git a/src/silx/gui/plot/stats/statshandler.py b/src/silx/gui/plot/stats/statshandler.py
index 1531ba2..8e7e08b 100644
--- a/src/silx/gui/plot/stats/statshandler.py
+++ b/src/silx/gui/plot/stats/statshandler.py
@@ -48,8 +48,8 @@ class _FloatItem(qt.QTableWidgetItem):
qt.QTableWidgetItem.__init__(self, type=type)
def __lt__(self, other):
- self_values = self.text().lstrip('(').rstrip(')').split(',')
- other_values = other.text().lstrip('(').rstrip(')').split(',')
+ self_values = self.text().lstrip("(").rstrip(")").split(",")
+ other_values = other.text().lstrip("(").rstrip(")").split(",")
for self_value, other_value in zip(self_values, other_values):
f_self_value = float(self_value)
f_other_value = float(other_value)
@@ -67,7 +67,8 @@ class StatFormatter(object):
which will be used to display the result of the
statistic computation.
"""
- DEFAULT_FORMATTER = '{0:.3f}'
+
+ DEFAULT_FORMATTER = "{0:.3f}"
def __init__(self, formatter=DEFAULT_FORMATTER, qItemClass=_FloatItem):
self.formatter = formatter
@@ -121,9 +122,11 @@ class StatsHandler(object):
if isinstance(arg[0], statsmdl.StatBase):
stat = arg[0]
if len(arg) > 2:
- raise ValueError('To many argument with %s. At most one '
- 'argument can be associated with the '
- 'BaseStat (the `StatFormatter`')
+ raise ValueError(
+ "To many argument with %s. At most one "
+ "argument can be associated with the "
+ "BaseStat (the `StatFormatter`"
+ )
if len(arg) == 2:
assert arg[1] is None or isinstance(arg[1], (StatFormatter, str))
formatter = arg[1]
@@ -134,15 +137,20 @@ class StatsHandler(object):
arg = arg[0]
if type(arg[0]) is not str:
- raise ValueError('first element of the tuple should be a string'
- ' or a StatBase instance')
+ raise ValueError(
+ "first element of the tuple should be a string"
+ " or a StatBase instance"
+ )
if len(arg) == 1:
- raise ValueError('A function should be associated with the'
- 'stat name')
+ raise ValueError(
+ "A function should be associated with the" "stat name"
+ )
if len(arg) > 3:
- raise ValueError('Two much argument given for defining statistic.'
- 'Take at most three arguments (name, function, '
- 'kinds)')
+ raise ValueError(
+ "Two much argument given for defining statistic."
+ "Take at most three arguments (name, function, "
+ "kinds)"
+ )
if len(arg) == 2:
stat = statsmdl.Stat(name=arg[0], fct=arg[1])
else:
@@ -180,12 +188,13 @@ class StatsHandler(object):
if isinstance(val, (tuple, list)):
res = []
[res.append(self.formatters[name].format(_val)) for _val in val]
- return ', '.join(res)
+ return ", ".join(res)
else:
return self.formatters[name].format(val)
- def calculate(self, item, plot, onlimits, roi=None, data_changed=False,
- roi_changed=False):
+ def calculate(
+ self, item, plot, onlimits, roi=None, data_changed=False, roi_changed=False
+ ):
"""
compute all statistic registered and return the list of formatted
statistics result.
@@ -200,8 +209,14 @@ class StatsHandler(object):
:return: list of formatted statistics (as str)
:rtype: dict
"""
- res = self.stats.calculate(item, plot, onlimits, roi,
- data_changed=data_changed, roi_changed=roi_changed)
+ res = self.stats.calculate(
+ item,
+ plot,
+ onlimits,
+ roi,
+ data_changed=data_changed,
+ roi_changed=roi_changed,
+ )
for resName, resValue in list(res.items()):
res[resName] = self.format(resName, res[resName])
return res
diff --git a/src/silx/gui/plot/PlotTools.py b/src/silx/gui/plot/test/conftest.py
index 35d0f48..78475fb 100644
--- a/src/silx/gui/plot/PlotTools.py
+++ b/src/silx/gui/plot/test/conftest.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2023 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
@@ -21,20 +21,23 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""Set of widgets to associate with a :class:'PlotWidget'.
-"""
+"""Test PlotWidget active item"""
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "01/03/2018"
+__date__ = "13/12/2023"
-from ...utils.deprecation import deprecated_warning
+import pytest
+from silx.gui.plot import PlotWidget
-deprecated_warning(type_='module',
- name=__file__,
- reason='Plot tools refactoring',
- replacement='silx.gui.plot.tools',
- since_version='0.8')
-from .tools import PositionInfo, LimitsToolBar # noqa
+@pytest.fixture
+def plotWidget(qWidgetFactory, request):
+ try:
+ backend = request.param
+ except AttributeError:
+ backend = "mpl" # Backend was not defined
+ if backend == "gl":
+ request.getfixturevalue("use_opengl") # Skip test if OpenGL test disabled
+ yield qWidgetFactory(PlotWidget, backend=backend)
diff --git a/src/silx/gui/plot/test/testAlphaSlider.py b/src/silx/gui/plot/test/testAlphaSlider.py
index 8641da7..e9ccb45 100644
--- a/src/silx/gui/plot/test/testAlphaSlider.py
+++ b/src/silx/gui/plot/test/testAlphaSlider.py
@@ -29,7 +29,6 @@ __license__ = "MIT"
__date__ = "28/03/2017"
import numpy
-import unittest
from silx.gui import qt
from silx.gui.utils.testutils import TestCaseQt
@@ -76,19 +75,16 @@ class TestActiveImageAlphaSlider(TestCaseQt):
def testGetImage(self):
self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]))
- self.assertEqual(self.plot.getActiveImage(),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getActiveImage(), self.aslider.getItem())
self.plot.addImage(numpy.array([[0, 1, 3], [2, 4, 6]]), legend="2")
self.plot.setActiveImage("2")
- self.assertEqual(self.plot.getImage("2"),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getImage("2"), self.aslider.getItem())
def testGetAlpha(self):
self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1")
self.aslider.setValue(137)
- self.assertAlmostEqual(self.aslider.getAlpha(),
- 137. / 255)
+ self.assertAlmostEqual(self.aslider.getAlpha(), 137.0 / 255)
class TestNamedImageAlphaSlider(TestCaseQt):
@@ -130,19 +126,16 @@ class TestNamedImageAlphaSlider(TestCaseQt):
self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1")
self.plot.addImage(numpy.array([[0, 1, 3], [2, 4, 6]]), legend="2")
self.aslider.setLegend("1")
- self.assertEqual(self.plot.getImage("1"),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getImage("1"), self.aslider.getItem())
self.aslider.setLegend("2")
- self.assertEqual(self.plot.getImage("2"),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getImage("2"), self.aslider.getItem())
def testGetAlpha(self):
self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1")
self.aslider.setLegend("1")
self.aslider.setValue(128)
- self.assertAlmostEqual(self.aslider.getAlpha(),
- 128. / 255)
+ self.assertAlmostEqual(self.aslider.getAlpha(), 128.0 / 255)
class TestNamedScatterAlphaSlider(TestCaseQt):
@@ -175,29 +168,22 @@ class TestNamedScatterAlphaSlider(TestCaseQt):
# no Scatter set initially, slider must be deactivate
self.assertFalse(self.aslider.isEnabled())
- self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7],
- legend="1")
+ self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7], legend="1")
self.aslider.setLegend("1")
# now we have an image set
self.assertTrue(self.aslider.isEnabled())
def testGetScatter(self):
- self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7],
- legend="1")
- self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70],
- legend="2")
+ self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7], legend="1")
+ self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70], legend="2")
self.aslider.setLegend("1")
- self.assertEqual(self.plot.getScatter("1"),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getScatter("1"), self.aslider.getItem())
self.aslider.setLegend("2")
- self.assertEqual(self.plot.getScatter("2"),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getScatter("2"), self.aslider.getItem())
def testGetAlpha(self):
- self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70],
- legend="1")
+ self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70], legend="1")
self.aslider.setLegend("1")
self.aslider.setValue(128)
- self.assertAlmostEqual(self.aslider.getAlpha(),
- 128. / 255)
+ self.assertAlmostEqual(self.aslider.getAlpha(), 128.0 / 255)
diff --git a/src/silx/gui/plot/test/testAxis.py b/src/silx/gui/plot/test/testAxis.py
new file mode 100644
index 0000000..dcf2f06
--- /dev/null
+++ b/src/silx/gui/plot/test/testAxis.py
@@ -0,0 +1,147 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Tests of PlotWidget Axis items"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "15/06/2023"
+
+
+from silx.gui.plot import PlotWidget
+
+
+def testAxisIsVisible(qapp, qWidgetFactory):
+ """Test Axis.isVisible method"""
+ plotWidget = qWidgetFactory(PlotWidget)
+
+ assert plotWidget.getXAxis().isVisible()
+ assert plotWidget.getYAxis().isVisible()
+ assert not plotWidget.getYAxis("right").isVisible()
+
+ # Add curve on right axis
+ plotWidget.addCurve((0, 1, 2), (1, 2, 3), yaxis="right")
+ qapp.processEvents()
+
+ assert plotWidget.getYAxis("right").isVisible()
+
+ # hide curve on right axis
+ curve = plotWidget.getItems()[0]
+ curve.setVisible(False)
+ qapp.processEvents()
+
+ assert not plotWidget.getYAxis("right").isVisible()
+
+ # show curve on right axis
+ curve.setVisible(True)
+ qapp.processEvents()
+
+ assert plotWidget.getYAxis("right").isVisible()
+
+ # Move curve to left axis
+ curve.setYAxis("left")
+ qapp.processEvents()
+
+ assert not plotWidget.getYAxis("right").isVisible()
+
+
+def testAxisSetScaleLogNoData(qapp, qWidgetFactory):
+ """Test Axis.setScale('log') method with an empty plot
+
+ Limits are reset only when negative
+ """
+ plotWidget = qWidgetFactory(PlotWidget)
+ xaxis = plotWidget.getXAxis()
+ yaxis = plotWidget.getYAxis()
+ y2axis = plotWidget.getYAxis("right")
+
+ xaxis.setLimits(-1.0, 1.0)
+ yaxis.setLimits(2.0, 3.0)
+ y2axis.setLimits(-2.0, -1.0)
+
+ xaxis.setScale("log")
+ qapp.processEvents()
+
+ assert xaxis.getLimits() == (1.0, 100.0)
+ assert yaxis.getLimits() == (2.0, 3.0)
+ assert y2axis.getLimits() == (-2.0, -1.0)
+
+ xaxis.setLimits(10.0, 20.0)
+
+ yaxis.setScale("log")
+ qapp.processEvents()
+
+ assert xaxis.getLimits() == (10.0, 20.0)
+ assert yaxis.getLimits() == (2.0, 3.0) # Positive range is preserved
+ assert y2axis.getLimits() == (1.0, 100.0) # Negative min is reset
+
+
+def testAxisSetScaleLogWithData(qapp, qWidgetFactory):
+ """Test Axis.setScale('log') method with data
+
+ Limits are reset only when negative and takes the data range into account
+ """
+ plotWidget = qWidgetFactory(PlotWidget)
+ xaxis = plotWidget.getXAxis()
+ yaxis = plotWidget.getYAxis()
+ plotWidget.addCurve((-1, 1, 2, 3), (-1, 1, 2, 3))
+
+ xaxis.setLimits(-1.0, 0.5) # Limits contains no positive data
+ yaxis.setLimits(-1.0, 2.0) # Limits contains positive data
+
+ xaxis.setScale("log")
+ yaxis.setScale("log")
+ qapp.processEvents()
+
+ assert xaxis.getLimits() == (1.0, 3.0) # Reset to positive data range
+ assert yaxis.getLimits() == (1.0, 2.0) # Keep max limit
+
+
+def testAxisSetScaleLinear(qapp, qWidgetFactory):
+ """Test Axis.setScale('linear') method: Limits are not changed"""
+ plotWidget = qWidgetFactory(PlotWidget)
+ xaxis = plotWidget.getXAxis()
+ yaxis = plotWidget.getYAxis()
+ y2axis = plotWidget.getYAxis("right")
+ xaxis.setScale("log")
+ yaxis.setScale("log")
+ plotWidget.resetZoom()
+ qapp.processEvents()
+
+ xaxis.setLimits(10.0, 1000.0)
+ yaxis.setLimits(20.0, 2000.0)
+ y2axis.setLimits(30.0, 3000.0)
+
+ xaxis.setScale("linear")
+ qapp.processEvents()
+
+ assert xaxis.getLimits() == (10.0, 1000.0)
+ assert yaxis.getLimits() == (20.0, 2000.0)
+ assert y2axis.getLimits() == (30.0, 3000.0)
+
+ yaxis.setScale("linear")
+ qapp.processEvents()
+
+ assert xaxis.getLimits() == (10.0, 1000.0)
+ assert yaxis.getLimits() == (20.0, 2000.0)
+ assert y2axis.getLimits() == (30.0, 3000.0)
diff --git a/src/silx/gui/plot/test/testColorBar.py b/src/silx/gui/plot/test/testColorBar.py
index 199726b..7202bc2 100644
--- a/src/silx/gui/plot/test/testColorBar.py
+++ b/src/silx/gui/plot/test/testColorBar.py
@@ -27,7 +27,6 @@ __authors__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "24/04/2018"
-import unittest
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.plot.ColorBar import _ColorScale
from silx.gui.plot.ColorBar import ColorBarWidget
@@ -40,6 +39,7 @@ import numpy
class TestColorScale(TestCaseQt):
"""Test that interaction with the colorScale is correct"""
+
def setUp(self):
super(TestColorScale, self).setUp()
self.colorScaleWidget = _ColorScale(colormap=None, parent=None)
@@ -59,37 +59,32 @@ class TestColorScale(TestCaseQt):
self.assertIsNone(colormap)
def testRelativePositionLinear(self):
- self.colorMapLin1 = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=0.0,
- vmax=1.0)
+ self.colorMapLin1 = Colormap(
+ name="gray", normalization=Colormap.LINEAR, vmin=0.0, vmax=1.0
+ )
self.colorScaleWidget.setColormap(self.colorMapLin1)
self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.25) == 0.25)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.5) == 0.5)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(1.0) == 1.0)
-
- self.colorMapLin2 = Colormap(name='viridis',
- normalization=Colormap.LINEAR,
- vmin=-10,
- vmax=0)
+ self.colorScaleWidget.getValueFromRelativePosition(0.25) == 0.25
+ )
+ self.assertTrue(self.colorScaleWidget.getValueFromRelativePosition(0.5) == 0.5)
+ self.assertTrue(self.colorScaleWidget.getValueFromRelativePosition(1.0) == 1.0)
+
+ self.colorMapLin2 = Colormap(
+ name="viridis", normalization=Colormap.LINEAR, vmin=-10, vmax=0
+ )
self.colorScaleWidget.setColormap(self.colorMapLin2)
self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.25) == -7.5)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.5) == -5.0)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(1.0) == 0.0)
+ self.colorScaleWidget.getValueFromRelativePosition(0.25) == -7.5
+ )
+ self.assertTrue(self.colorScaleWidget.getValueFromRelativePosition(0.5) == -5.0)
+ self.assertTrue(self.colorScaleWidget.getValueFromRelativePosition(1.0) == 0.0)
def testRelativePositionLog(self):
- self.colorMapLog1 = Colormap(name='temperature',
- normalization=Colormap.LOGARITHM,
- vmin=1.0,
- vmax=100.0)
+ self.colorMapLog1 = Colormap(
+ name="temperature", normalization=Colormap.LOGARITHM, vmin=1.0, vmax=100.0
+ )
self.colorScaleWidget.setColormap(self.colorMapLog1)
@@ -130,14 +125,13 @@ class TestNoAutoscale(TestCaseQt):
super(TestNoAutoscale, self).tearDown()
def testLogNormNoAutoscale(self):
- colormapLog = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=1.0,
- vmax=100.0)
+ colormapLog = Colormap(
+ name="gray", normalization=Colormap.LOGARITHM, vmin=1.0, vmax=100.0
+ )
data = numpy.linspace(10, 1e10, 9).reshape(3, 3)
- self.plot.addImage(data=data, colormap=colormapLog, legend='toto')
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=data, colormap=colormapLog, legend="toto")
+ self.plot.setActiveImage("toto")
# test Ticks
self.tickBar.setTicksNumber(10)
@@ -155,14 +149,13 @@ class TestNoAutoscale(TestCaseQt):
self.assertTrue(val == 1.0)
def testLinearNormNoAutoscale(self):
- colormapLog = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=-4,
- vmax=5)
+ colormapLog = Colormap(
+ name="gray", normalization=Colormap.LINEAR, vmin=-4, vmax=5
+ )
data = numpy.linspace(1, 9, 9).reshape(3, 3)
- self.plot.addImage(data=data, colormap=colormapLog, legend='toto')
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=data, colormap=colormapLog, legend="toto")
+ self.plot.setActiveImage("toto")
# test Ticks
self.tickBar.setTicksNumber(10)
@@ -209,15 +202,14 @@ class TestColorBarWidget(TestCaseQt):
Note : colorbar is modified by the Plot directly not ColorBarWidget
"""
- colormapLog = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=None)
+ colormapLog = Colormap(
+ name="gray", normalization=Colormap.LOGARITHM, vmin=None, vmax=None
+ )
data = numpy.array([-5, -4, 0, 2, 3, 5, 10, 20, 30])
data = data.reshape(3, 3)
- self.plot.addImage(data=data, colormap=colormapLog, legend='toto')
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=data, colormap=colormapLog, legend="toto")
+ self.plot.setActiveImage("toto")
# default behavior when with log and negative values: should set vmin
# to 1 and vmax to 10
@@ -226,52 +218,43 @@ class TestColorBarWidget(TestCaseQt):
# if data is positive
data[data < 1] = data.max()
- self.plot.addImage(data=data,
- colormap=colormapLog,
- legend='toto',
- replace=True)
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=data, colormap=colormapLog, legend="toto", replace=True)
+ self.plot.setActiveImage("toto")
self.assertTrue(self.colorBar.getColorScaleBar().minVal == data.min())
self.assertTrue(self.colorBar.getColorScaleBar().maxVal == data.max())
def testPlotAssocation(self):
"""Make sure the ColorBarWidget is properly connected with the plot"""
- colormap = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None)
+ colormap = Colormap(
+ name="gray", normalization=Colormap.LINEAR, vmin=None, vmax=None
+ )
# make sure that default settings are the same (but a copy of the
self.colorBar.setPlot(self.plot)
- self.assertTrue(
- self.colorBar.getColormap() is self.plot.getDefaultColormap())
+ self.assertTrue(self.colorBar.getColormap() is self.plot.getDefaultColormap())
data = numpy.linspace(0, 10, 100).reshape(10, 10)
- self.plot.addImage(data=data, colormap=colormap, legend='toto')
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=data, colormap=colormap, legend="toto")
+ self.plot.setActiveImage("toto")
# make sure the modification of the colormap has been done
- self.assertFalse(
- self.colorBar.getColormap() is self.plot.getDefaultColormap())
- self.assertTrue(
- self.colorBar.getColormap() is colormap)
+ self.assertFalse(self.colorBar.getColormap() is self.plot.getDefaultColormap())
+ self.assertTrue(self.colorBar.getColormap() is colormap)
# test that colorbar is updated when default plot colormap changes
self.plot.clear()
- plotColormap = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=None)
+ plotColormap = Colormap(
+ name="gray", normalization=Colormap.LOGARITHM, vmin=None, vmax=None
+ )
self.plot.setDefaultColormap(plotColormap)
self.assertTrue(self.colorBar.getColormap() is plotColormap)
def testColormapWithoutRange(self):
"""Test with a colormap with vmin==vmax"""
- colormap = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=1.0,
- vmax=1.0)
+ colormap = Colormap(
+ name="gray", normalization=Colormap.LINEAR, vmin=1.0, vmax=1.0
+ )
self.colorBar.setColormap(colormap)
@@ -300,40 +283,35 @@ class TestColorBarUpdate(TestCaseQt):
super(TestColorBarUpdate, self).tearDown()
def testUpdateColorMap(self):
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=0,
- vmax=1)
+ colormap = Colormap(name="gray", normalization="linear", vmin=0, vmax=1)
# check inital state
- self.plot.addImage(data=self.data, colormap=colormap, legend='toto')
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=self.data, colormap=colormap, legend="toto")
+ self.plot.setActiveImage("toto")
self.assertTrue(self.colorBar.getColorScaleBar().minVal == 0)
self.assertTrue(self.colorBar.getColorScaleBar().maxVal == 1)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmin == 0)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmax == 1)
+ self.assertTrue(self.colorBar.getColorScaleBar().getTickBar()._vmin == 0)
+ self.assertTrue(self.colorBar.getColorScaleBar().getTickBar()._vmax == 1)
self.assertIsInstance(
self.colorBar.getColorScaleBar().getTickBar()._normalizer,
- LinearNormalization)
+ LinearNormalization,
+ )
# update colormap
colormap.setVMin(0.5)
self.assertTrue(self.colorBar.getColorScaleBar().minVal == 0.5)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmin == 0.5)
+ self.assertTrue(self.colorBar.getColorScaleBar().getTickBar()._vmin == 0.5)
colormap.setVMax(0.8)
self.assertTrue(self.colorBar.getColorScaleBar().maxVal == 0.8)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmax == 0.8)
+ self.assertTrue(self.colorBar.getColorScaleBar().getTickBar()._vmax == 0.8)
- colormap.setNormalization('log')
+ colormap.setNormalization("log")
self.assertIsInstance(
self.colorBar.getColorScaleBar().getTickBar()._normalizer,
- LogarithmicNormalization)
+ LogarithmicNormalization,
+ )
# TODO : should also check that if the colormap is changing then values (especially in log scale)
# should be coherent if in autoscale
diff --git a/src/silx/gui/plot/test/testCompareImages.py b/src/silx/gui/plot/test/testCompareImages.py
index 9b5065d..4bc52b4 100644
--- a/src/silx/gui/plot/test/testCompareImages.py
+++ b/src/silx/gui/plot/test/testCompareImages.py
@@ -27,79 +27,210 @@ __authors__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "23/07/2018"
-import unittest
+import pytest
import numpy
import weakref
-from silx.gui.utils.testutils import TestCaseQt
+from silx.gui import qt
from silx.gui.plot.CompareImages import CompareImages
-class TestCompareImages(TestCaseQt):
- """Test that CompareImages widget is working in some cases"""
-
- def setUp(self):
- super(TestCompareImages, self).setUp()
- self.widget = CompareImages()
-
- def tearDown(self):
- ref = weakref.ref(self.widget)
- self.widget = None
- self.qWaitForDestroy(ref)
- super(TestCompareImages, self).tearDown()
-
- def testIntensityImage(self):
- image1 = numpy.random.rand(10, 10)
- image2 = numpy.random.rand(10, 10)
- self.widget.setData(image1, image2)
-
- def testRgbImage(self):
- image1 = numpy.random.randint(0, 255, size=(10, 10, 3))
- image2 = numpy.random.randint(0, 255, size=(10, 10, 3))
- self.widget.setData(image1, image2)
-
- def testRgbaImage(self):
- image1 = numpy.random.randint(0, 255, size=(10, 10, 4))
- image2 = numpy.random.randint(0, 255, size=(10, 10, 4))
- self.widget.setData(image1, image2)
-
- def testVizualisations(self):
- image1 = numpy.random.rand(10, 10)
- image2 = numpy.random.rand(10, 10)
- self.widget.setData(image1, image2)
- for mode in CompareImages.VisualizationMode:
- self.widget.setVisualizationMode(mode)
-
- def testAlignemnt(self):
- image1 = numpy.random.rand(10, 10)
- image2 = numpy.random.rand(5, 5)
- self.widget.setData(image1, image2)
- for mode in CompareImages.AlignmentMode:
- self.widget.setAlignmentMode(mode)
-
- def testGetPixel(self):
- image1 = numpy.random.rand(11, 11)
- image2 = numpy.random.rand(5, 5)
- image1[5, 5] = 111.111
- image2[2, 2] = 222.222
- self.widget.setData(image1, image2)
- expectedValue = {}
- expectedValue[CompareImages.AlignmentMode.CENTER] = 222.222
- expectedValue[CompareImages.AlignmentMode.STRETCH] = 222.222
- expectedValue[CompareImages.AlignmentMode.ORIGIN] = None
- for mode in expectedValue.keys():
- self.widget.setAlignmentMode(mode)
- data = self.widget.getRawPixelData(11 / 2.0, 11 / 2.0)
- data1, data2 = data
- self.assertEqual(data1, 111.111)
- self.assertEqual(data2, expectedValue[mode])
-
- def testImageEmpty(self):
- self.widget.setData(image1=None, image2=None)
- self.assertTrue(self.widget.getRawPixelData(11 / 2.0, 11 / 2.0) == (None, None))
-
- def testSetImageSeparately(self):
- self.widget.setImage1(numpy.random.rand(10, 10))
- self.widget.setImage2(numpy.random.rand(10, 10))
- for mode in CompareImages.VisualizationMode:
- self.widget.setVisualizationMode(mode)
+@pytest.fixture
+def compareImages(qapp, qapp_utils):
+ widget = CompareImages()
+ widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ yield widget
+ widget.close()
+ ref = weakref.ref(widget)
+ widget = None
+ qapp_utils.qWaitForDestroy(ref)
+
+
+def testIntensityImage(compareImages):
+ image1 = numpy.random.rand(10, 10)
+ image2 = numpy.random.rand(10, 10)
+ compareImages.setData(image1, image2)
+
+
+def testRgbImage(compareImages):
+ image1 = numpy.random.randint(0, 255, size=(10, 10, 3))
+ image2 = numpy.random.randint(0, 255, size=(10, 10, 3))
+ compareImages.setData(image1, image2)
+
+
+def testRgbaImage(compareImages):
+ image1 = numpy.random.randint(0, 255, size=(10, 10, 4))
+ image2 = numpy.random.randint(0, 255, size=(10, 10, 4))
+ compareImages.setData(image1, image2)
+
+
+def testAlignemnt(compareImages):
+ image1 = numpy.random.rand(10, 10)
+ image2 = numpy.random.rand(5, 5)
+ compareImages.setData(image1, image2)
+ for mode in CompareImages.AlignmentMode:
+ compareImages.setAlignmentMode(mode)
+
+
+def testGetPixel(compareImages):
+ image1 = numpy.random.rand(11, 11)
+ image2 = numpy.random.rand(5, 5)
+ image1[5, 5] = 111.111
+ image2[2, 2] = 222.222
+ compareImages.setData(image1, image2)
+ expectedValue = {}
+ expectedValue[CompareImages.AlignmentMode.CENTER] = 222.222
+ expectedValue[CompareImages.AlignmentMode.STRETCH] = 222.222
+ expectedValue[CompareImages.AlignmentMode.ORIGIN] = None
+ for mode in expectedValue.keys():
+ compareImages.setAlignmentMode(mode)
+ data = compareImages.getRawPixelData(11 / 2.0, 11 / 2.0)
+ data1, data2 = data
+ assert data1 == 111.111
+ assert data2 == expectedValue[mode]
+
+
+def testImageEmpty(compareImages):
+ compareImages.setData(image1=None, image2=None)
+
+
+def testSetImageSeparately(compareImages):
+ compareImages.setImage1(numpy.random.rand(10, 10))
+ compareImages.setImage2(numpy.random.rand(10, 10))
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.VisualizationMode.COMPOSITE_A_MINUS_B,),
+ (CompareImages.VisualizationMode.COMPOSITE_RED_BLUE_GRAY,),
+ (CompareImages.VisualizationMode.HORIZONTAL_LINE,),
+ (CompareImages.VisualizationMode.VERTICAL_LINE,),
+ (CompareImages.VisualizationMode.ONLY_A,),
+ (CompareImages.VisualizationMode.ONLY_B,),
+ ],
+)
+def testVisualizationMode(compareImages, data):
+ (visualizationMode,) = data
+ compareImages.setImage1(numpy.random.rand(10, 10))
+ compareImages.setImage2(numpy.random.rand(10, 10))
+ compareImages.setVisualizationMode(visualizationMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.VisualizationMode.COMPOSITE_A_MINUS_B,),
+ (CompareImages.VisualizationMode.COMPOSITE_RED_BLUE_GRAY,),
+ (CompareImages.VisualizationMode.HORIZONTAL_LINE,),
+ (CompareImages.VisualizationMode.VERTICAL_LINE,),
+ (CompareImages.VisualizationMode.ONLY_A,),
+ (CompareImages.VisualizationMode.ONLY_B,),
+ ],
+)
+def testVisualizationModeWithoutImage(compareImages, data):
+ (visualizationMode,) = data
+ compareImages.setImage1(None)
+ compareImages.setImage2(None)
+ compareImages.setVisualizationMode(visualizationMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.VisualizationMode.COMPOSITE_A_MINUS_B,),
+ (CompareImages.VisualizationMode.COMPOSITE_RED_BLUE_GRAY,),
+ (CompareImages.VisualizationMode.HORIZONTAL_LINE,),
+ (CompareImages.VisualizationMode.VERTICAL_LINE,),
+ (CompareImages.VisualizationMode.ONLY_A,),
+ (CompareImages.VisualizationMode.ONLY_B,),
+ ],
+)
+def testVisualizationModeWithOnlyImage1(compareImages, data):
+ (visualizationMode,) = data
+ compareImages.setImage1(numpy.random.rand(10, 10))
+ compareImages.setImage2(None)
+ compareImages.setVisualizationMode(visualizationMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.VisualizationMode.COMPOSITE_A_MINUS_B,),
+ (CompareImages.VisualizationMode.COMPOSITE_RED_BLUE_GRAY,),
+ (CompareImages.VisualizationMode.HORIZONTAL_LINE,),
+ (CompareImages.VisualizationMode.VERTICAL_LINE,),
+ (CompareImages.VisualizationMode.ONLY_A,),
+ (CompareImages.VisualizationMode.ONLY_B,),
+ ],
+)
+def testVisualizationModeWithOnlyImage2(compareImages, data):
+ (visualizationMode,) = data
+ compareImages.setImage1(None)
+ compareImages.setImage2(numpy.random.rand(10, 10))
+ compareImages.setVisualizationMode(visualizationMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.VisualizationMode.COMPOSITE_A_MINUS_B,),
+ (CompareImages.VisualizationMode.COMPOSITE_RED_BLUE_GRAY,),
+ (CompareImages.VisualizationMode.HORIZONTAL_LINE,),
+ (CompareImages.VisualizationMode.VERTICAL_LINE,),
+ (CompareImages.VisualizationMode.ONLY_A,),
+ (CompareImages.VisualizationMode.ONLY_B,),
+ ],
+)
+def testVisualizationModeWithRGBImage(compareImages, data):
+ (visualizationMode,) = data
+ image1 = numpy.random.rand(10, 10)
+ image2 = numpy.random.randint(0, 255, size=(10, 10, 3))
+ compareImages.setData(image1, image2)
+ compareImages.setVisualizationMode(visualizationMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.AlignmentMode.STRETCH,),
+ (CompareImages.AlignmentMode.AUTO,),
+ (CompareImages.AlignmentMode.CENTER,),
+ (CompareImages.AlignmentMode.ORIGIN,),
+ ],
+)
+def testAlignemntModeWithoutImages(compareImages, data):
+ (alignmentMode,) = data
+ compareImages.setAlignmentMode(alignmentMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.AlignmentMode.STRETCH,),
+ (CompareImages.AlignmentMode.AUTO,),
+ (CompareImages.AlignmentMode.CENTER,),
+ (CompareImages.AlignmentMode.ORIGIN,),
+ ],
+)
+def testAlignemntModeWithSingleImage(compareImages, data):
+ (alignmentMode,) = data
+ compareImages.setImage1(numpy.arange(9).reshape(3, 3))
+ compareImages.setAlignmentMode(alignmentMode)
+
+
+def testTooltip(compareImages):
+ compareImages.setImage1(numpy.arange(9).reshape(3, 3))
+ compareImages.setImage2(numpy.arange(9).reshape(3, 3))
+ compareImages.getRawPixelData(1.5, 1.5)
+
+
+def testTooltipWithoutImage(compareImages):
+ compareImages.setImage1(numpy.arange(9).reshape(3, 3))
+ compareImages.setImage2(numpy.arange(9).reshape(3, 3))
+ compareImages.getRawPixelData(1.5, 1.5)
+
+
+def testTooltipWithSingleImage(compareImages):
+ compareImages.setImage1(numpy.arange(9).reshape(3, 3))
+ compareImages.getRawPixelData(1.5, 1.5)
diff --git a/src/silx/gui/plot/test/testComplexImageView.py b/src/silx/gui/plot/test/testComplexImageView.py
index c26df25..f8b331b 100644
--- a/src/silx/gui/plot/test/testComplexImageView.py
+++ b/src/silx/gui/plot/test/testComplexImageView.py
@@ -28,7 +28,6 @@ __license__ = "MIT"
__date__ = "17/01/2018"
-import unittest
import logging
import numpy
@@ -57,7 +56,7 @@ class TestComplexImageView(PlotWidgetTestCase, ParametricTestCase):
# Test colormap API
colormap = self.plot.getColormap().copy()
- colormap.setName('magma')
+ colormap.setName("magma")
self.plot.setColormap(colormap)
self.qWait(100)
diff --git a/src/silx/gui/plot/test/testCurvesROIWidget.py b/src/silx/gui/plot/test/testCurvesROIWidget.py
index 32ac057..05acd36 100644
--- a/src/silx/gui/plot/test/testCurvesROIWidget.py
+++ b/src/silx/gui/plot/test/testCurvesROIWidget.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -30,8 +30,6 @@ __date__ = "16/11/2017"
import logging
import os.path
-import pytest
-from collections import OrderedDict
import numpy
from silx.gui import qt
@@ -40,9 +38,7 @@ from silx.gui.plot import Plot1D
from silx.test.utils import temp_dir
from silx.gui.utils.testutils import TestCaseQt, SignalListener
from silx.gui.plot import PlotWindow, CurvesROIWidget
-from silx.gui.plot.CurvesROIWidget import ROITable
from silx.gui.utils.testutils import getQToolButtonFromAction
-from silx.gui.plot.PlotInteraction import ItemsInteraction
_logger = logging.getLogger(__name__)
@@ -74,10 +70,12 @@ class TestCurvesROIWidget(TestCaseQt):
def testDummyAPI(self):
"""Simple test of the getRois and setRois API"""
- roi_neg = CurvesROIWidget.ROI(name='negative', fromdata=-20,
- todata=-10, type_='X')
- roi_pos = CurvesROIWidget.ROI(name='positive', fromdata=10,
- todata=20, type_='X')
+ roi_neg = CurvesROIWidget.ROI(
+ name="negative", fromdata=-20, todata=-10, type_="X"
+ )
+ roi_pos = CurvesROIWidget.ROI(
+ name="positive", fromdata=10, todata=20, type_="X"
+ )
self.widget.roiWidget.setRois((roi_pos, roi_neg))
@@ -87,9 +85,11 @@ class TestCurvesROIWidget(TestCaseQt):
def testWithCurves(self):
"""Plot with curves: test all ROI widget buttons"""
for offset in range(2):
- self.plot.addCurve(numpy.arange(1000),
- offset + numpy.random.random(1000),
- legend=str(offset))
+ self.plot.addCurve(
+ numpy.arange(1000),
+ offset + numpy.random.random(1000),
+ legend=str(offset),
+ )
# Add two ROI
self.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton)
@@ -105,7 +105,7 @@ class TestCurvesROIWidget(TestCaseQt):
self.qWait(200)
with temp_dir() as tmpDir:
- self.tmpFile = os.path.join(tmpDir, 'test.ini')
+ self.tmpFile = os.path.join(tmpDir, "test.ini")
# Save ROIs
self.widget.roiWidget.save(self.tmpFile)
@@ -113,13 +113,12 @@ class TestCurvesROIWidget(TestCaseQt):
self.assertEqual(len(self.widget.getRois()), 2)
# Reset ROIs
- self.mouseClick(self.widget.roiWidget.resetButton,
- qt.Qt.LeftButton)
+ self.mouseClick(self.widget.roiWidget.resetButton, qt.Qt.LeftButton)
self.qWait(200)
rois = self.widget.getRois()
self.assertEqual(len(rois), 1)
roiID = list(rois.keys())[0]
- self.assertEqual(rois[roiID].getName(), 'ICR')
+ self.assertEqual(rois[roiID].getName(), "ICR")
# Load ROIs
self.widget.roiWidget.load(self.tmpFile)
@@ -135,18 +134,20 @@ class TestCurvesROIWidget(TestCaseQt):
self.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton)
for roiID in self.widget.roiWidget.roiTable._markersHandler._roiMarkerHandlers:
- handler = self.widget.roiWidget.roiTable._markersHandler._roiMarkerHandlers[roiID]
- assert handler.getMarker('min')
- xleftMarker = handler.getMarker('min').getXPosition()
- xMiddleMarker = handler.getMarker('middle').getXPosition()
- xRightMarker = handler.getMarker('max').getXPosition()
- thValue = xleftMarker + (xRightMarker - xleftMarker) / 2.
+ handler = self.widget.roiWidget.roiTable._markersHandler._roiMarkerHandlers[
+ roiID
+ ]
+ assert handler.getMarker("min")
+ xleftMarker = handler.getMarker("min").getXPosition()
+ xMiddleMarker = handler.getMarker("middle").getXPosition()
+ xRightMarker = handler.getMarker("max").getXPosition()
+ thValue = xleftMarker + (xRightMarker - xleftMarker) / 2.0
self.assertAlmostEqual(xMiddleMarker, thValue)
def testAreaCalculation(self):
"""Test result of area calculation"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
+ x = numpy.arange(100.0)
+ y = numpy.arange(100.0)
# Add two curves
self.plot.addCurve(x, y, legend="positive")
@@ -156,30 +157,30 @@ class TestCurvesROIWidget(TestCaseQt):
self.plot.setActiveCurve("positive")
# Add two ROIs
- roi_neg = CurvesROIWidget.ROI(name='negative', fromdata=-20,
- todata=-10, type_='X')
- roi_pos = CurvesROIWidget.ROI(name='positive', fromdata=10,
- todata=20, type_='X')
+ roi_neg = CurvesROIWidget.ROI(
+ name="negative", fromdata=-20, todata=-10, type_="X"
+ )
+ roi_pos = CurvesROIWidget.ROI(
+ name="positive", fromdata=10, todata=20, type_="X"
+ )
self.widget.roiWidget.setRois((roi_pos, roi_neg))
- posCurve = self.plot.getCurve('positive')
- negCurve = self.plot.getCurve('negative')
+ posCurve = self.plot.getCurve("positive")
+ negCurve = self.plot.getCurve("negative")
- self.assertEqual(roi_pos.computeRawAndNetArea(posCurve),
- (numpy.trapz(y=[10, 20], x=[10, 20]),
- 0.0))
- self.assertEqual(roi_pos.computeRawAndNetArea(negCurve),
- (0.0, 0.0))
- self.assertEqual(roi_neg.computeRawAndNetArea(posCurve),
- ((0.0), 0.0))
- self.assertEqual(roi_neg.computeRawAndNetArea(negCurve),
- ((-150.0), 0.0))
+ self.assertEqual(
+ roi_pos.computeRawAndNetArea(posCurve),
+ (numpy.trapz(y=[10, 20], x=[10, 20]), 0.0),
+ )
+ self.assertEqual(roi_pos.computeRawAndNetArea(negCurve), (0.0, 0.0))
+ self.assertEqual(roi_neg.computeRawAndNetArea(posCurve), ((0.0), 0.0))
+ self.assertEqual(roi_neg.computeRawAndNetArea(negCurve), ((-150.0), 0.0))
def testCountsCalculation(self):
"""Test result of count calculation"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
+ x = numpy.arange(100.0)
+ y = numpy.arange(100.0)
# Add two curves
self.plot.addCurve(x, y, legend="positive")
@@ -189,36 +190,38 @@ class TestCurvesROIWidget(TestCaseQt):
self.plot.setActiveCurve("positive")
# Add two ROIs
- roi_neg = CurvesROIWidget.ROI(name='negative', fromdata=-20,
- todata=-10, type_='X')
- roi_pos = CurvesROIWidget.ROI(name='positive', fromdata=10,
- todata=20, type_='X')
+ roi_neg = CurvesROIWidget.ROI(
+ name="negative", fromdata=-20, todata=-10, type_="X"
+ )
+ roi_pos = CurvesROIWidget.ROI(
+ name="positive", fromdata=10, todata=20, type_="X"
+ )
self.widget.roiWidget.setRois((roi_pos, roi_neg))
- posCurve = self.plot.getCurve('positive')
- negCurve = self.plot.getCurve('negative')
+ posCurve = self.plot.getCurve("positive")
+ negCurve = self.plot.getCurve("negative")
- self.assertEqual(roi_pos.computeRawAndNetCounts(posCurve),
- (y[10:21].sum(), 0.0))
- self.assertEqual(roi_pos.computeRawAndNetCounts(negCurve),
- (0.0, 0.0))
- self.assertEqual(roi_neg.computeRawAndNetCounts(posCurve),
- ((0.0), 0.0))
- self.assertEqual(roi_neg.computeRawAndNetCounts(negCurve),
- (y[10:21].sum(), 0.0))
+ self.assertEqual(
+ roi_pos.computeRawAndNetCounts(posCurve), (y[10:21].sum(), 0.0)
+ )
+ self.assertEqual(roi_pos.computeRawAndNetCounts(negCurve), (0.0, 0.0))
+ self.assertEqual(roi_neg.computeRawAndNetCounts(posCurve), ((0.0), 0.0))
+ self.assertEqual(
+ roi_neg.computeRawAndNetCounts(negCurve), (y[10:21].sum(), 0.0)
+ )
def testDeferedInit(self):
"""Test behavior of the deferedInit"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
+ x = numpy.arange(100.0)
+ y = numpy.arange(100.0)
self.plot.addCurve(x=x, y=y, legend="name", replace="True")
- roisDefs = OrderedDict([
- ["range1",
- OrderedDict([["from", 20], ["to", 200], ["type", "energy"]])],
- ["range2",
- OrderedDict([["from", 300], ["to", 500], ["type", "energy"]])]
- ])
+ roisDefs = dict(
+ [
+ ["range1", dict([["from", 20], ["to", 200], ["type", "energy"]])],
+ ["range2", dict([["from", 300], ["to", 500], ["type", "energy"]])],
+ ]
+ )
roiWidget = self.plot.getCurvesRoiDockWidget().roiWidget
self.plot.getCurvesRoiDockWidget().setRois(roisDefs)
@@ -228,34 +231,41 @@ class TestCurvesROIWidget(TestCaseQt):
def testDictCompatibility(self):
"""Test that ROI api is valid with dict and not information is lost"""
- roiDict = {'from': 20, 'to': 200, 'type': 'energy', 'comment': 'no',
- 'name': 'myROI', 'calibration': [1, 2, 3]}
+ roiDict = {
+ "from": 20,
+ "to": 200,
+ "type": "energy",
+ "comment": "no",
+ "name": "myROI",
+ "calibration": [1, 2, 3],
+ }
roi = CurvesROIWidget.ROI._fromDict(roiDict)
self.assertEqual(roi.toDict(), roiDict)
def testShowAllROI(self):
"""Test the show allROI action"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
+ x = numpy.arange(100.0)
+ y = numpy.arange(100.0)
self.plot.addCurve(x=x, y=y, legend="name", replace="True")
roisDefsDict = {
- "range1": {"from": 20, "to": 200,"type": "energy"},
- "range2": {"from": 300, "to": 500, "type": "energy"}
+ "range1": {"from": 20, "to": 200, "type": "energy"},
+ "range2": {"from": 300, "to": 500, "type": "energy"},
}
roisDefsObj = (
- CurvesROIWidget.ROI(name='range3', fromdata=20, todata=200,
- type_='energy'),
- CurvesROIWidget.ROI(name='range4', fromdata=300, todata=500,
- type_='energy')
+ CurvesROIWidget.ROI(name="range3", fromdata=20, todata=200, type_="energy"),
+ CurvesROIWidget.ROI(
+ name="range4", fromdata=300, todata=500, type_="energy"
+ ),
)
self.widget.roiWidget.showAllMarkers(True)
roiWidget = self.plot.getCurvesRoiDockWidget().roiWidget
roiWidget.setRois(roisDefsDict)
- markers = [item for item in self.plot.getItems()
- if isinstance(item, items.MarkerBase)]
- self.assertEqual(len(markers), 2*3)
+ markers = [
+ item for item in self.plot.getItems() if isinstance(item, items.MarkerBase)
+ ]
+ self.assertEqual(len(markers), 2 * 3)
markersHandler = self.widget.roiWidget.roiTable._markersHandler
roiWidget.showAllMarkers(True)
@@ -268,9 +278,10 @@ class TestCurvesROIWidget(TestCaseQt):
roiWidget.setRois(roisDefsObj)
self.qapp.processEvents()
- markers = [item for item in self.plot.getItems()
- if isinstance(item, items.MarkerBase)]
- self.assertEqual(len(markers), 2*3)
+ markers = [
+ item for item in self.plot.getItems() if isinstance(item, items.MarkerBase)
+ ]
+ self.assertEqual(len(markers), 2 * 3)
markersHandler = self.widget.roiWidget.roiTable._markersHandler
roiWidget.showAllMarkers(True)
@@ -282,52 +293,51 @@ class TestCurvesROIWidget(TestCaseQt):
self.assertEqual(len(ICRROI), 1)
def testRoiEdition(self):
- """Make sure if the ROI object is edited the ROITable will be updated
- """
- roi = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
- self.widget.roiWidget.setRois((roi, ))
+ """Make sure if the ROI object is edited the ROITable will be updated"""
+ roi = CurvesROIWidget.ROI(name="linear", fromdata=0, todata=5)
+ self.widget.roiWidget.setRois((roi,))
x = (0, 1, 1, 2, 2, 3)
y = (1, 1, 2, 2, 1, 1)
- self.plot.addCurve(x=x, y=y, legend='linearCurve')
- self.plot.setActiveCurve(legend='linearCurve')
+ self.plot.addCurve(x=x, y=y, legend="linearCurve")
+ self.plot.setActiveCurve(legend="linearCurve")
self.widget.calculateROIs()
roiTable = self.widget.roiWidget.roiTable
indexesColumns = CurvesROIWidget.ROITable.COLUMNS_INDEX
- itemRawCounts = roiTable.item(0, indexesColumns['Raw Counts'])
- itemNetCounts = roiTable.item(0, indexesColumns['Net Counts'])
+ itemRawCounts = roiTable.item(0, indexesColumns["Raw Counts"])
+ itemNetCounts = roiTable.item(0, indexesColumns["Net Counts"])
- self.assertTrue(itemRawCounts.text() == '8.0')
- self.assertTrue(itemNetCounts.text() == '2.0')
+ self.assertTrue(itemRawCounts.text() == "8.0")
+ self.assertTrue(itemNetCounts.text() == "2.0")
- itemRawArea = roiTable.item(0, indexesColumns['Raw Area'])
- itemNetArea = roiTable.item(0, indexesColumns['Net Area'])
+ itemRawArea = roiTable.item(0, indexesColumns["Raw Area"])
+ itemNetArea = roiTable.item(0, indexesColumns["Net Area"])
- self.assertTrue(itemRawArea.text() == '4.0')
- self.assertTrue(itemNetArea.text() == '1.0')
+ self.assertTrue(itemRawArea.text() == "4.0")
+ self.assertTrue(itemNetArea.text() == "1.0")
roi.setTo(2)
- itemRawArea = roiTable.item(0, indexesColumns['Raw Area'])
- self.assertTrue(itemRawArea.text() == '3.0')
+ itemRawArea = roiTable.item(0, indexesColumns["Raw Area"])
+ self.assertTrue(itemRawArea.text() == "3.0")
roi.setFrom(1)
- itemRawArea = roiTable.item(0, indexesColumns['Raw Area'])
- self.assertTrue(itemRawArea.text() == '2.0')
+ itemRawArea = roiTable.item(0, indexesColumns["Raw Area"])
+ self.assertTrue(itemRawArea.text() == "2.0")
def testRemoveActiveROI(self):
"""Test widget behavior when removing the active ROI"""
- roi = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
+ roi = CurvesROIWidget.ROI(name="linear", fromdata=0, todata=5)
self.widget.roiWidget.setRois((roi,))
self.widget.roiWidget.roiTable.setActiveRoi(None)
self.assertEqual(len(self.widget.roiWidget.roiTable.selectedItems()), 0)
self.widget.roiWidget.setRois((roi,))
- self.plot.setActiveCurve(legend='linearCurve')
+ self.plot.setActiveCurve(legend="linearCurve")
self.widget.calculateROIs()
def testEmitCurrentROI(self):
"""Test behavior of the CurvesROIWidget.sigROISignal"""
- roi = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
+ roi = CurvesROIWidget.ROI(name="linear", fromdata=0, todata=5)
self.widget.roiWidget.setRois((roi,))
signalListener = SignalListener()
self.widget.roiWidget.sigROISignal.connect(signalListener.partial())
@@ -352,7 +362,7 @@ class TestRoiWidgetSignals(TestCaseQt):
self.plot = Plot1D()
x = range(20)
y = range(20)
- self.plot.addCurve(x, y, legend='curve0')
+ self.plot.addCurve(x, y, legend="curve0")
self.listener = SignalListener()
self.curves_roi_widget = self.plot.getCurvesRoiWidget()
self.curves_roi_widget.sigROISignal.connect(self.listener)
@@ -383,33 +393,33 @@ class TestRoiWidgetSignals(TestCaseQt):
"""Test SigROISignal when adding and removing ROIS"""
self.listener.clear()
- roi1 = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
+ roi1 = CurvesROIWidget.ROI(name="linear", fromdata=0, todata=5)
self.curves_roi_widget.roiTable.addRoi(roi1)
self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear')
+ self.assertTrue(self.listener.arguments()[0][0]["current"] == "linear")
self.listener.clear()
- roi2 = CurvesROIWidget.ROI(name='linear2', fromdata=0, todata=5)
+ roi2 = CurvesROIWidget.ROI(name="linear2", fromdata=0, todata=5)
self.curves_roi_widget.roiTable.addRoi(roi2)
self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear2')
+ self.assertTrue(self.listener.arguments()[0][0]["current"] == "linear2")
self.listener.clear()
self.curves_roi_widget.roiTable.removeROI(roi2)
self.assertEqual(self.listener.callCount(), 1)
self.assertTrue(self.curves_roi_widget.roiTable.activeRoi == roi1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear')
+ self.assertTrue(self.listener.arguments()[0][0]["current"] == "linear")
self.listener.clear()
self.curves_roi_widget.roiTable.deleteActiveRoi()
self.assertEqual(self.listener.callCount(), 1)
self.assertTrue(self.curves_roi_widget.roiTable.activeRoi is None)
- self.assertTrue(self.listener.arguments()[0][0]['current'] is None)
+ self.assertTrue(self.listener.arguments()[0][0]["current"] is None)
self.listener.clear()
self.curves_roi_widget.roiTable.addRoi(roi1)
self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear')
+ self.assertTrue(self.listener.arguments()[0][0]["current"] == "linear")
self.assertTrue(self.curves_roi_widget.roiTable.activeRoi == roi1)
self.listener.clear()
self.qapp.processEvents()
@@ -417,13 +427,13 @@ class TestRoiWidgetSignals(TestCaseQt):
self.curves_roi_widget.roiTable.removeROI(roi1)
self.qapp.processEvents()
self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'ICR')
+ self.assertTrue(self.listener.arguments()[0][0]["current"] == "ICR")
self.listener.clear()
def testSigROISignalModifyROI(self):
"""Test SigROISignal when modifying it"""
self.curves_roi_widget.roiTable.setMiddleROIMarkerFlag(True)
- roi1 = CurvesROIWidget.ROI(name='linear', fromdata=2, todata=5)
+ roi1 = CurvesROIWidget.ROI(name="linear", fromdata=2, todata=5)
self.curves_roi_widget.roiTable.addRoi(roi1)
self.curves_roi_widget.roiTable.setActiveRoi(roi1)
@@ -435,10 +445,10 @@ class TestRoiWidgetSignals(TestCaseQt):
roi1.setTo(2.56)
self.assertEqual(self.listener.callCount(), 1)
self.listener.clear()
- roi1.setName('linear2')
+ roi1.setName("linear2")
self.assertEqual(self.listener.callCount(), 1)
self.listener.clear()
- roi1.setType('new type')
+ roi1.setType("new type")
self.assertEqual(self.listener.callCount(), 1)
widget = self.plot.getWidgetHandle()
@@ -447,18 +457,24 @@ class TestRoiWidgetSignals(TestCaseQt):
self.qapp.processEvents()
# modify roi limits (from the gui)
- roi_marker_handler = self.curves_roi_widget.roiTable._markersHandler.getMarkerHandler(roi1.getID())
- for marker_type in ('min', 'max', 'middle'):
+ roi_marker_handler = (
+ self.curves_roi_widget.roiTable._markersHandler.getMarkerHandler(
+ roi1.getID()
+ )
+ )
+ for marker_type in ("min", "max", "middle"):
with self.subTest(marker_type=marker_type):
self.listener.clear()
marker = roi_marker_handler.getMarker(marker_type)
- x_pix, y_pix = self.plot.dataToPixel(marker.getXPosition(), marker.getYPosition())
+ x_pix, y_pix = self.plot.dataToPixel(
+ marker.getXPosition(), marker.getYPosition()
+ )
self.mouseMove(widget, pos=(x_pix, y_pix))
self.qWait(100)
self.mousePress(widget, qt.Qt.LeftButton, pos=(x_pix, y_pix))
- self.mouseMove(widget, pos=(x_pix+20, y_pix))
+ self.mouseMove(widget, pos=(x_pix + 20, y_pix))
self.qWait(100)
- self.mouseRelease(widget, qt.Qt.LeftButton, pos=(x_pix+20, y_pix))
+ self.mouseRelease(widget, qt.Qt.LeftButton, pos=(x_pix + 20, y_pix))
self.qWait(100)
self.mouseMove(widget, pos=(x_pix, y_pix))
self.qapp.processEvents()
@@ -466,8 +482,8 @@ class TestRoiWidgetSignals(TestCaseQt):
def testSetActiveCurve(self):
"""Test sigRoiSignal when set an active curve"""
- roi1 = CurvesROIWidget.ROI(name='linear', fromdata=2, todata=5)
+ roi1 = CurvesROIWidget.ROI(name="linear", fromdata=2, todata=5)
self.curves_roi_widget.roiTable.setActiveRoi(roi1)
self.listener.clear()
- self.plot.setActiveCurve('curve0')
+ self.plot.setActiveCurve("curve0")
self.assertEqual(self.listener.callCount(), 0)
diff --git a/src/silx/gui/plot/test/testImageStack.py b/src/silx/gui/plot/test/testImageStack.py
index 702f0fe..482cdfd 100644
--- a/src/silx/gui/plot/test/testImageStack.py
+++ b/src/silx/gui/plot/test/testImageStack.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2020 European Synchrotron Radiation Facility
+# Copyright (c) 2020-2023 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
@@ -28,7 +28,6 @@ __license__ = "MIT"
__date__ = "15/01/2020"
-import unittest
import tempfile
import numpy
import h5py
@@ -38,7 +37,6 @@ from silx.gui.utils.testutils import TestCaseQt
from silx.io.url import DataUrl
from silx.gui.plot.ImageStack import ImageStack
from silx.gui.utils.testutils import SignalListener
-from collections import OrderedDict
import os
import time
import shutil
@@ -49,21 +47,21 @@ class TestImageStack(TestCaseQt):
def setUp(self):
TestCaseQt.setUp(self)
- self.urls = OrderedDict()
+ self.urls = {}
self._raw_data = {}
self._folder = tempfile.mkdtemp()
self._n_urls = 10
- file_name = os.path.join(self._folder, 'test_inage_stack_file.h5')
- with h5py.File(file_name, 'w') as h5f:
+ file_name = os.path.join(self._folder, "test_inage_stack_file.h5")
+ with h5py.File(file_name, "w") as h5f:
for i in range(self._n_urls):
width = numpy.random.randint(10, 40)
height = numpy.random.randint(10, 40)
raw_data = numpy.random.random((width, height))
self._raw_data[i] = raw_data
h5f[str(i)] = raw_data
- self.urls[i] = DataUrl(file_path=file_name,
- data_path=str(i),
- scheme='silx')
+ self.urls[i] = DataUrl(
+ file_path=file_name, data_path=str(i), scheme="silx"
+ )
self.widget = ImageStack()
self.urlLoadedListener = SignalListener()
@@ -79,8 +77,7 @@ class TestImageStack(TestCaseQt):
TestCaseQt.setUp(self)
def testControls(self):
- """Test that selection using the url table and the slider are working
- """
+ """Test that selection using the url table and the slider are working"""
self.widget.show()
self.assertEqual(self.widget.getCurrentUrl(), None)
self.assertEqual(self.widget.getCurrentUrlIndex(), None)
@@ -95,13 +92,15 @@ class TestImageStack(TestCaseQt):
self.assertEqual(self.urlLoadedListener.callCount(), self._n_urls)
numpy.testing.assert_array_equal(
self.widget.getPlotWidget().getActiveImage(just_legend=False).getData(),
- self._raw_data[0])
+ self._raw_data[0],
+ )
self.assertEqual(self.widget._slider.value(), 0)
self.widget._urlsTable.setUrl(self.urls[4])
numpy.testing.assert_array_equal(
self.widget.getPlotWidget().getActiveImage(just_legend=False).getData(),
- self._raw_data[4])
+ self._raw_data[4],
+ )
self.assertEqual(self.widget._slider.value(), 4)
self.assertEqual(self.widget.getCurrentUrl(), self.urls[4])
self.assertEqual(self.widget.getCurrentUrlIndex(), 4)
@@ -109,9 +108,11 @@ class TestImageStack(TestCaseQt):
self.widget._slider.setUrlIndex(6)
numpy.testing.assert_array_equal(
self.widget.getPlotWidget().getActiveImage(just_legend=False).getData(),
- self._raw_data[6])
- self.assertEqual(self.widget._urlsTable.currentItem().text(),
- self.urls[6].path())
+ self._raw_data[6],
+ )
+ self.assertEqual(
+ self.widget._urlsTable.currentItem().text(), self.urls[6].path()
+ )
def testCurrentUrlSignals(self):
"""Test emission of 'currentUrlChangedListener'"""
@@ -151,26 +152,72 @@ class TestImageStack(TestCaseQt):
self.assertEqual(urls_values[0], self.urls[0])
self.assertEqual(urls_values[7], self.urls[7])
- self.assertEqual(self.widget._getNextUrl(urls_values[2]).path(),
- urls_values[3].path())
+ self.assertEqual(
+ self.widget._getNextUrl(urls_values[2]).path(), urls_values[3].path()
+ )
self.assertEqual(self.widget._getPreviousUrl(urls_values[0]), None)
- self.assertEqual(self.widget._getPreviousUrl(urls_values[6]).path(),
- urls_values[5].path())
-
- self.assertEqual(self.widget._getNNextUrls(2, urls_values[0]),
- urls_values[1:3])
- self.assertEqual(self.widget._getNNextUrls(5, urls_values[7]),
- urls_values[8:])
- self.assertEqual(self.widget._getNPreviousUrls(3, urls_values[2]),
- urls_values[:2])
- self.assertEqual(self.widget._getNPreviousUrls(5, urls_values[8]),
- urls_values[3:8])
+ self.assertEqual(
+ self.widget._getPreviousUrl(urls_values[6]).path(), urls_values[5].path()
+ )
+
+ self.assertEqual(self.widget._getNNextUrls(2, urls_values[0]), urls_values[1:3])
+ self.assertEqual(self.widget._getNNextUrls(5, urls_values[7]), urls_values[8:])
+ self.assertEqual(
+ self.widget._getNPreviousUrls(3, urls_values[2]), urls_values[:2]
+ )
+ self.assertEqual(
+ self.widget._getNPreviousUrls(5, urls_values[8]), urls_values[3:8]
+ )
+
+ def testRemoveUrlFromList(self):
+ """
+ Test behavior when some item (url) are removed from the list
+ """
+ self.widget.setUrlsEditable(True)
+ self.widget.show()
+ self.widget.setUrls(list(self.urls.values()))
+ self.assertEqual(len(self.widget.getUrls()), len(self.urls))
+
+ # wait for image to be loaded
+ self._waitUntilUrlLoaded()
+ ll_slider = self.widget._slider._slider
+ assert ll_slider.maximum() - ll_slider.minimum() + 1 == len(self.urls)
+
+ # remove some urls from the list (~ simulating behavior with a right click)
+ urlsTable = self.widget._urlsTable._urlsTable
+ urlsTable.clearSelection()
+ urlsTable.item(1).setSelected(True)
+ urlsTable.item(2).setSelected(True)
+ urlsTable._removeSelectedItems()
+ self.qapp.processEvents()
+
+ # make sure slider has been updated
+ assert ll_slider.maximum() - ll_slider.minimum() + 1 == len(self.urls) - 2
+ # as the ImageStack widget
+ assert len(self.widget._urls) == len(self.urls) - 2
+ removed_urls = list(self.urls.values())[1:3]
+
+ existing_urls_as_str = [url.path() for url in self.widget._urls.values()]
+ for removed_url in removed_urls:
+ assert type(removed_url) == type(tuple(self.widget._urls.values())[0])
+ assert removed_url.path() not in existing_urls_as_str
+ # make sure we have some data plot
+ self.widget.getPlotWidget().getActiveImage() is not None
+
+ # test removing remaining urls
+ urlsTable.selectAll()
+ urlsTable._removeSelectedItems()
+ self.qapp.processEvents()
+ assert len(self.widget._urls) == 0
+ assert ll_slider.maximum() - ll_slider.minimum() == 0
+ # make sure if all urls are removed nothing is plot anymore
+ self.widget.getPlotWidget().getActiveImage() is None
def _waitUntilUrlLoaded(self, timeout=2.0):
"""Wait until all image urls are loaded"""
loop_duration = 0.2
remaining_duration = timeout
- while(len(self.widget._loadingThreads) > 0 and remaining_duration > 0):
+ while len(self.widget._loadingThreads) > 0 and remaining_duration > 0:
remaining_duration -= loop_duration
time.sleep(loop_duration)
self.qapp.processEvents()
@@ -179,7 +226,9 @@ class TestImageStack(TestCaseQt):
remaining_urls = []
for thread_ in self.widget._loadingThreads:
remaining_urls.append(thread_.url.path())
- mess = 'All images are not loaded after the time out. ' \
- 'Remaining urls are: ' + str(remaining_urls)
+ mess = (
+ "All images are not loaded after the time out. "
+ "Remaining urls are: " + str(remaining_urls)
+ )
raise TimeoutError(mess)
return True
diff --git a/src/silx/gui/plot/test/testImageView.py b/src/silx/gui/plot/test/testImageView.py
index 9fb6a5d..df19ab7 100644
--- a/src/silx/gui/plot/test/testImageView.py
+++ b/src/silx/gui/plot/test/testImageView.py
@@ -92,31 +92,33 @@ class TestImageView(TestCaseQt):
self.plot.setImage(image)
# Colormap as dict
- self.plot.setColormap({'name': 'viridis',
- 'normalization': 'log',
- 'autoscale': False,
- 'vmin': 0,
- 'vmax': 1})
+ self.plot.setColormap(
+ {
+ "name": "viridis",
+ "normalization": "log",
+ "autoscale": False,
+ "vmin": 0,
+ "vmax": 1,
+ }
+ )
colormap = self.plot.getColormap()
- self.assertEqual(colormap.getName(), 'viridis')
- self.assertEqual(colormap.getNormalization(), 'log')
+ self.assertEqual(colormap.getName(), "viridis")
+ self.assertEqual(colormap.getNormalization(), "log")
self.assertEqual(colormap.getVMin(), 0)
self.assertEqual(colormap.getVMax(), 1)
# Colormap as keyword arguments
- self.plot.setColormap(colormap='magma',
- normalization='linear',
- autoscale=True,
- vmin=1,
- vmax=2)
- self.assertEqual(colormap.getName(), 'magma')
- self.assertEqual(colormap.getNormalization(), 'linear')
+ self.plot.setColormap(
+ colormap="magma", normalization="linear", autoscale=True, vmin=1, vmax=2
+ )
+ self.assertEqual(colormap.getName(), "magma")
+ self.assertEqual(colormap.getNormalization(), "linear")
self.assertEqual(colormap.getVMin(), None)
self.assertEqual(colormap.getVMax(), None)
# Update colormap with keyword argument
- self.plot.setColormap(normalization='log')
- self.assertEqual(colormap.getNormalization(), 'log')
+ self.plot.setColormap(normalization="log")
+ self.assertEqual(colormap.getNormalization(), "log")
# Colormap as Colormap object
cmap = Colormap()
@@ -130,7 +132,7 @@ class TestImageView(TestCaseQt):
ImageView.ProfileWindowBehavior.POPUP,
)
- self.plot.setProfileWindowBehavior('embedded')
+ self.plot.setProfileWindowBehavior("embedded")
self.assertIs(
self.plot.getProfileWindowBehavior(),
ImageView.ProfileWindowBehavior.EMBEDDED,
@@ -139,9 +141,7 @@ class TestImageView(TestCaseQt):
image = numpy.arange(100).reshape(10, 10)
self.plot.setImage(image)
- self.plot.setProfileWindowBehavior(
- ImageView.ProfileWindowBehavior.POPUP
- )
+ self.plot.setProfileWindowBehavior(ImageView.ProfileWindowBehavior.POPUP)
self.assertIs(
self.plot.getProfileWindowBehavior(),
ImageView.ProfileWindowBehavior.POPUP,
@@ -170,7 +170,9 @@ class TestImageView(TestCaseQt):
image = numpy.arange(100).reshape(10, 10)
self.plot.setImage(image, reset=True)
self.qWait(100)
- self.plot.getAggregationModeAction().setAggregationMode(items.ImageDataAggregated.Aggregation.MAX)
+ self.plot.getAggregationModeAction().setAggregationMode(
+ items.ImageDataAggregated.Aggregation.MAX
+ )
self.qWait(100)
def testImageAggregationModeBackToNormalMode(self):
@@ -178,9 +180,13 @@ class TestImageView(TestCaseQt):
image = numpy.arange(100).reshape(10, 10)
self.plot.setImage(image, reset=True)
self.qWait(100)
- self.plot.getAggregationModeAction().setAggregationMode(items.ImageDataAggregated.Aggregation.MAX)
+ self.plot.getAggregationModeAction().setAggregationMode(
+ items.ImageDataAggregated.Aggregation.MAX
+ )
self.qWait(100)
- self.plot.getAggregationModeAction().setAggregationMode(items.ImageDataAggregated.Aggregation.NONE)
+ self.plot.getAggregationModeAction().setAggregationMode(
+ items.ImageDataAggregated.Aggregation.NONE
+ )
self.qWait(100)
def testRGBAInAggregationMode(self):
@@ -189,5 +195,7 @@ class TestImageView(TestCaseQt):
self.plot.setImage(image, reset=True)
self.qWait(100)
- self.plot.getAggregationModeAction().setAggregationMode(items.ImageDataAggregated.Aggregation.MAX)
+ self.plot.getAggregationModeAction().setAggregationMode(
+ items.ImageDataAggregated.Aggregation.MAX
+ )
self.qWait(100)
diff --git a/src/silx/gui/plot/test/testInteraction.py b/src/silx/gui/plot/test/testInteraction.py
index 459b132..b031454 100644
--- a/src/silx/gui/plot/test/testInteraction.py
+++ b/src/silx/gui/plot/test/testInteraction.py
@@ -40,38 +40,40 @@ class TestInteraction(unittest.TestCase):
class TestClickOrDrag(Interaction.ClickOrDrag):
def click(self, x, y, btn):
- events.append(('click', x, y, btn))
+ events.append(("click", x, y, btn))
def beginDrag(self, x, y, btn):
- events.append(('beginDrag', x, y, btn))
+ events.append(("beginDrag", x, y, btn))
def drag(self, x, y, btn):
- events.append(('drag', x, y, btn))
+ events.append(("drag", x, y, btn))
def endDrag(self, start, end, btn):
- events.append(('endDrag', start, end, btn))
+ events.append(("endDrag", start, end, btn))
clickOrDrag = TestClickOrDrag()
# click
- clickOrDrag.handleEvent('press', 10, 10, Interaction.LEFT_BTN)
+ clickOrDrag.handleEvent("press", 10, 10, Interaction.LEFT_BTN)
self.assertEqual(len(events), 0)
- clickOrDrag.handleEvent('release', 10, 10, Interaction.LEFT_BTN)
+ clickOrDrag.handleEvent("release", 10, 10, Interaction.LEFT_BTN)
self.assertEqual(len(events), 1)
- self.assertEqual(events[0], ('click', 10, 10, Interaction.LEFT_BTN))
+ self.assertEqual(events[0], ("click", 10, 10, Interaction.LEFT_BTN))
# drag
events = []
- clickOrDrag.handleEvent('press', 10, 10, Interaction.LEFT_BTN)
+ clickOrDrag.handleEvent("press", 10, 10, Interaction.LEFT_BTN)
self.assertEqual(len(events), 0)
- clickOrDrag.handleEvent('move', 15, 10)
+ clickOrDrag.handleEvent("move", 15, 10)
self.assertEqual(len(events), 2) # Received beginDrag and drag
- self.assertEqual(events[0], ('beginDrag', 10, 10, Interaction.LEFT_BTN))
- self.assertEqual(events[1], ('drag', 15, 10, Interaction.LEFT_BTN))
- clickOrDrag.handleEvent('move', 20, 10)
+ self.assertEqual(events[0], ("beginDrag", 10, 10, Interaction.LEFT_BTN))
+ self.assertEqual(events[1], ("drag", 15, 10, Interaction.LEFT_BTN))
+ clickOrDrag.handleEvent("move", 20, 10)
self.assertEqual(len(events), 3)
- self.assertEqual(events[-1], ('drag', 20, 10, Interaction.LEFT_BTN))
- clickOrDrag.handleEvent('release', 20, 10, Interaction.LEFT_BTN)
+ self.assertEqual(events[-1], ("drag", 20, 10, Interaction.LEFT_BTN))
+ clickOrDrag.handleEvent("release", 20, 10, Interaction.LEFT_BTN)
self.assertEqual(len(events), 4)
- self.assertEqual(events[-1], ('endDrag', (10, 10), (20, 10), Interaction.LEFT_BTN))
+ self.assertEqual(
+ events[-1], ("endDrag", (10, 10), (20, 10), Interaction.LEFT_BTN)
+ )
diff --git a/src/silx/gui/plot/test/testItem.py b/src/silx/gui/plot/test/testItem.py
index 7b4f636..8a6db40 100644
--- a/src/silx/gui/plot/test/testItem.py
+++ b/src/silx/gui/plot/test/testItem.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -28,11 +28,11 @@ __license__ = "MIT"
__date__ = "01/09/2017"
-import unittest
-
import numpy
+import pytest
from silx.gui.utils.testutils import SignalListener
+from silx.gui.plot.items.roi import RegionOfInterest
from silx.gui.plot.items import ItemChangedType
from silx.gui.plot import items
from .utils import PlotWidgetTestCase
@@ -43,8 +43,8 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
def testCurveChanged(self):
"""Test sigItemChanged for curve"""
- self.plot.addCurve(numpy.arange(10), numpy.arange(10), legend='test')
- curve = self.plot.getCurve('test')
+ self.plot.addCurve(numpy.arange(10), numpy.arange(10), legend="test")
+ curve = self.plot.getCurve("test")
listener = SignalListener()
curve.sigItemChanged.connect(listener)
@@ -58,8 +58,8 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
curve.setData(numpy.arange(100), numpy.arange(100))
# SymbolMixIn
- curve.setSymbol('Circle')
- curve.setSymbol('d')
+ curve.setSymbol("Circle")
+ curve.setSymbol("d")
curve.setSymbolSize(20)
# AlphaMixIn
@@ -67,49 +67,51 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# Test for signals in Curve class
# ColorMixIn
- curve.setColor('yellow')
+ curve.setColor("yellow")
# YAxisMixIn
- curve.setYAxis('right')
+ curve.setYAxis("right")
# FillMixIn
curve.setFill(True)
# LineMixIn
- curve.setLineStyle(':')
- curve.setLineStyle(':') # Not sending event
+ curve.setLineStyle(":")
+ curve.setLineStyle(":") # Not sending event
curve.setLineWidth(2)
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.VISIBLE,
- ItemChangedType.VISIBLE,
- ItemChangedType.ZVALUE,
- ItemChangedType.DATA,
- ItemChangedType.SYMBOL,
- ItemChangedType.SYMBOL,
- ItemChangedType.SYMBOL_SIZE,
- ItemChangedType.ALPHA,
- ItemChangedType.COLOR,
- ItemChangedType.YAXIS,
- ItemChangedType.FILL,
- ItemChangedType.LINE_STYLE,
- ItemChangedType.LINE_WIDTH])
+ self.assertEqual(
+ listener.arguments(argumentIndex=0),
+ [
+ ItemChangedType.VISIBLE,
+ ItemChangedType.VISIBLE,
+ ItemChangedType.ZVALUE,
+ ItemChangedType.DATA,
+ ItemChangedType.SYMBOL,
+ ItemChangedType.SYMBOL,
+ ItemChangedType.SYMBOL_SIZE,
+ ItemChangedType.ALPHA,
+ ItemChangedType.COLOR,
+ ItemChangedType.YAXIS,
+ ItemChangedType.FILL,
+ ItemChangedType.LINE_STYLE,
+ ItemChangedType.LINE_WIDTH,
+ ],
+ )
def testHistogramChanged(self):
"""Test sigItemChanged for Histogram"""
- self.plot.addHistogram(
- numpy.arange(10), edges=numpy.arange(11), legend='test')
- histogram = self.plot.getHistogram('test')
+ self.plot.addHistogram(numpy.arange(10), edges=numpy.arange(11), legend="test")
+ histogram = self.plot.getHistogram("test")
listener = SignalListener()
histogram.sigItemChanged.connect(listener)
# Test signals in Histogram class
histogram.setData(numpy.zeros(10), numpy.arange(11))
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.DATA])
+ self.assertEqual(listener.arguments(argumentIndex=0), [ItemChangedType.DATA])
def testImageDataChanged(self):
"""Test sigItemChanged for ImageData"""
- self.plot.addImage(numpy.arange(100).reshape(10, 10), legend='test')
- image = self.plot.getImage('test')
+ self.plot.addImage(numpy.arange(100).reshape(10, 10), legend="test")
+ image = self.plot.getImage("test")
listener = SignalListener()
image.sigItemChanged.connect(listener)
@@ -117,7 +119,7 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# ColormapMixIn
colormap = self.plot.getDefaultColormap().copy()
image.setColormap(colormap)
- image.getColormap().setName('viridis')
+ image.getColormap().setName("viridis")
# Test of signals in ImageBase class
image.setOrigin(10)
@@ -126,18 +128,22 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# Test of signals in ImageData class
image.setData(numpy.ones((10, 10)))
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.COLORMAP,
- ItemChangedType.COLORMAP,
- ItemChangedType.POSITION,
- ItemChangedType.SCALE,
- ItemChangedType.COLORMAP,
- ItemChangedType.DATA])
+ self.assertEqual(
+ listener.arguments(argumentIndex=0),
+ [
+ ItemChangedType.COLORMAP,
+ ItemChangedType.COLORMAP,
+ ItemChangedType.POSITION,
+ ItemChangedType.SCALE,
+ ItemChangedType.COLORMAP,
+ ItemChangedType.DATA,
+ ],
+ )
def testImageRgbaChanged(self):
"""Test sigItemChanged for ImageRgba"""
- self.plot.addImage(numpy.ones((10, 10, 3)), legend='rgb')
- image = self.plot.getImage('rgb')
+ self.plot.addImage(numpy.ones((10, 10, 3)), legend="rgb")
+ image = self.plot.getImage("rgb")
listener = SignalListener()
image.sigItemChanged.connect(listener)
@@ -145,13 +151,12 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# Test of signals in ImageRgba class
image.setData(numpy.zeros((10, 10, 3)))
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.DATA])
+ self.assertEqual(listener.arguments(argumentIndex=0), [ItemChangedType.DATA])
def testMarkerChanged(self):
"""Test sigItemChanged for markers"""
- self.plot.addMarker(10, 20, legend='test')
- marker = self.plot._getMarker('test')
+ self.plot.addMarker(10, 20, legend="test")
+ marker = self.plot._getMarker("test")
listener = SignalListener()
marker.sigItemChanged.connect(listener)
@@ -159,42 +164,45 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# Test signals in _BaseMarker
marker.setPosition(10, 10)
marker.setPosition(10, 10) # Not sending event
- marker.setText('toto')
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.POSITION,
- ItemChangedType.TEXT])
+ marker.setText("toto")
+ self.assertEqual(
+ listener.arguments(argumentIndex=0),
+ [ItemChangedType.POSITION, ItemChangedType.TEXT],
+ )
# XMarker
- self.plot.addXMarker(10, legend='x')
- marker = self.plot._getMarker('x')
+ self.plot.addXMarker(10, legend="x")
+ marker = self.plot._getMarker("x")
listener = SignalListener()
marker.sigItemChanged.connect(listener)
marker.setPosition(20, 20)
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.POSITION])
+ self.assertEqual(
+ listener.arguments(argumentIndex=0), [ItemChangedType.POSITION]
+ )
# YMarker
- self.plot.addYMarker(10, legend='x')
- marker = self.plot._getMarker('x')
+ self.plot.addYMarker(10, legend="x")
+ marker = self.plot._getMarker("x")
listener = SignalListener()
marker.sigItemChanged.connect(listener)
marker.setPosition(20, 20)
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.POSITION])
+ self.assertEqual(
+ listener.arguments(argumentIndex=0), [ItemChangedType.POSITION]
+ )
def testScatterChanged(self):
"""Test sigItemChanged for scatter"""
data = numpy.arange(10)
- self.plot.addScatter(data, data, data, legend='test')
- scatter = self.plot.getScatter('test')
+ self.plot.addScatter(data, data, data, legend="test")
+ scatter = self.plot.getScatter("test")
listener = SignalListener()
scatter.sigItemChanged.connect(listener)
# ColormapMixIn
- scatter.getColormap().setName('viridis')
+ scatter.getColormap().setName("viridis")
# Test of signals in Scatter class
scatter.setData((0, 1, 2), (1, 0, 2), (0, 1, 2))
@@ -202,44 +210,48 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# Visualization mode changed
scatter.setVisualization(scatter.Visualization.SOLID)
- self.assertEqual(listener.arguments(),
- [(ItemChangedType.COLORMAP,),
- (ItemChangedType.DATA,),
- (ItemChangedType.COLORMAP,),
- (ItemChangedType.VISUALIZATION_MODE,)])
+ self.assertEqual(
+ listener.arguments(),
+ [
+ (ItemChangedType.COLORMAP,),
+ (ItemChangedType.DATA,),
+ (ItemChangedType.COLORMAP,),
+ (ItemChangedType.VISUALIZATION_MODE,),
+ ],
+ )
def testShapeChanged(self):
"""Test sigItemChanged for shape"""
- data = numpy.array((1., 10.))
- self.plot.addShape(data, data, legend='test', shape='rectangle')
- shape = self.plot._getItem(kind='item', legend='test')
+ data = numpy.array((1.0, 10.0))
+ self.plot.addShape(data, data, legend="test", shape="rectangle")
+ shape = self.plot._getItem(kind="item", legend="test")
listener = SignalListener()
shape.sigItemChanged.connect(listener)
shape.setOverlay(True)
- shape.setPoints(((2., 2.), (3., 3.)))
+ shape.setPoints(((2.0, 2.0), (3.0, 3.0)))
- self.assertEqual(listener.arguments(),
- [(ItemChangedType.OVERLAY,),
- (ItemChangedType.DATA,)])
+ self.assertEqual(
+ listener.arguments(), [(ItemChangedType.OVERLAY,), (ItemChangedType.DATA,)]
+ )
class TestSymbol(PlotWidgetTestCase):
- """Test item's symbol """
+ """Test item's symbol"""
def test(self):
"""Test sigItemChanged for curve"""
- self.plot.addCurve(numpy.arange(10), numpy.arange(10), legend='test')
- curve = self.plot.getCurve('test')
+ self.plot.addCurve(numpy.arange(10), numpy.arange(10), legend="test")
+ curve = self.plot.getCurve("test")
# SymbolMixIn
- curve.setSymbol('o')
+ curve.setSymbol("o")
name = curve.getSymbolName()
- self.assertEqual('Circle', name)
+ self.assertEqual("Circle", name)
- name = curve.getSymbolName('d')
- self.assertEqual('Diamond', name)
+ name = curve.getSymbolName("d")
+ self.assertEqual("Diamond", name)
class TestVisibleExtent(PlotWidgetTestCase):
@@ -253,7 +265,7 @@ class TestVisibleExtent(PlotWidgetTestCase):
curve.setData((1, 2, 3), (0, 1, 2))
histogram = items.Histogram()
- histogram.setData((0, 1, 2), (1, 5/3, 7/3, 3))
+ histogram.setData((0, 1, 2), (1, 5 / 3, 7 / 3, 3))
image = items.ImageData()
image.setOrigin((1, 0))
@@ -271,10 +283,10 @@ class TestVisibleExtent(PlotWidgetTestCase):
xaxis.setLimits(0, 100)
yaxis.setLimits(0, 100)
self.plot.addItem(item)
- self.assertEqual(item.getVisibleBounds(), (1., 3., 0., 2.))
+ self.assertEqual(item.getVisibleBounds(), (1.0, 3.0, 0.0, 2.0))
xaxis.setLimits(0.5, 2.5)
- self.assertEqual(item.getVisibleBounds(), (1, 2.5, 0., 2.))
+ self.assertEqual(item.getVisibleBounds(), (1, 2.5, 0.0, 2.0))
yaxis.setLimits(0.5, 1.5)
self.assertEqual(item.getVisibleBounds(), (1, 2.5, 0.5, 1.5))
@@ -349,11 +361,205 @@ class TestImageDataAggregated(PlotWidgetTestCase):
# Zoom-out
for i in range(4):
xmin, xmax = self.plot.getXAxis().getLimits()
- ymin, ymax = self.plot.getYAxis().getLimits()
+ ymin, ymax = self.plot.getYAxis().getLimits()
self.plot.setLimits(
- xmin - (xmax - xmin)/2,
- xmax + (xmax - xmin)/2,
- ymin - (ymax - ymin)/2,
- ymax + (ymax - ymin)/2,
+ xmin - (xmax - xmin) / 2,
+ xmax + (xmax - xmin) / 2,
+ ymin - (ymax - ymin) / 2,
+ ymax + (ymax - ymin) / 2,
)
self.qapp.processEvents()
+
+
+def testRegionOfInterestText():
+ roi = RegionOfInterest()
+
+ listener = SignalListener()
+ roi.sigItemChanged.connect(listener)
+
+ assert roi.getName() == roi.getText()
+
+ roi.setText("some text")
+ assert listener.arguments(argumentIndex=0) == [ItemChangedType.TEXT]
+ listener.clear()
+ assert roi.getText() == "some text"
+
+ roi.setName("new_name")
+ assert listener.arguments(argumentIndex=0) == [ItemChangedType.NAME]
+ listener.clear()
+ assert roi.getText() == "some text"
+
+ roi.setText(None)
+ assert listener.arguments(argumentIndex=0) == [ItemChangedType.TEXT]
+ listener.clear()
+ assert roi.getText() == "new_name"
+
+ roi.setName("even_newer_name")
+ assert listener.arguments(argumentIndex=0) == [
+ ItemChangedType.NAME,
+ ItemChangedType.TEXT,
+ ]
+ assert roi.getText() == "even_newer_name"
+
+
+def testPlotAddItemsWithoutLegend(plotWidget):
+ curve1 = items.Curve()
+ curve1.setData([0, 10], [0, 20])
+ plotWidget.addItem(curve1)
+
+ curve2 = items.Curve()
+ curve2.setData([0, -10], [0, -20])
+ plotWidget.addItem(curve2)
+
+ assert plotWidget.getItems() == (curve1, curve2)
+
+ datarange = plotWidget.getDataRange()
+ assert datarange.x == (-10, 10)
+ assert datarange.y == (-20, 20)
+
+ plotWidget.resetZoom()
+ assert plotWidget.getXAxis().getLimits() == (-10, 10)
+ assert plotWidget.getYAxis().getLimits() == (-20, 20)
+
+
+def testPlotWidgetAddCurve(plotWidget):
+ curve = plotWidget.addCurve(x=(0, 1), y=(1, 0), legend="test", symbol="s")
+ assert isinstance(curve, items.Curve)
+ assert numpy.array_equal(curve.getXData(copy=False), (0, 1))
+ assert numpy.array_equal(curve.getYData(copy=False), (1, 0))
+ assert curve.getName() == "test"
+ assert curve.getSymbol() == "s"
+
+ curveUpdated = plotWidget.addCurve(
+ x=(0, 1, 2), y=(1, 0, 1), legend="test", symbol="o"
+ )
+ assert curveUpdated is curve
+ assert numpy.array_equal(curveUpdated.getXData(copy=False), (0, 1, 2))
+ assert numpy.array_equal(curveUpdated.getYData(copy=False), (1, 0, 1))
+ assert curveUpdated.getName() == "test"
+ assert curveUpdated.getSymbol() == "o"
+
+
+def testPlotWidgetAddImage(plotWidget):
+ image = plotWidget.addImage(((0, 1), (2, 3)), legend="test")
+ assert isinstance(image, items.ImageData)
+ assert numpy.array_equal(image.getData(copy=False), ((0, 1), (2, 3)))
+ assert image.getName() == "test"
+
+ imageUpdated = plotWidget.addImage([(0, 1)], legend="test")
+ assert imageUpdated is image
+ assert numpy.array_equal(image.getData(copy=False), [(0, 1)])
+ assert image.getName() == "test"
+
+ # Update with a 1pixel RGB image
+ imageRgb = plotWidget.addImage([[(0.0, 0.0, 1.0)]], legend="test")
+ assert isinstance(imageRgb, items.ImageRgba)
+ assert numpy.array_equal(imageRgb.getData(copy=False), [[(0.0, 0.0, 1.0)]])
+ assert imageRgb.getName() == "test"
+
+ # Update with a 1pixel RGB image
+ imageRgbUpdated = plotWidget.addImage([[(1.0, 0.0, 0.0)]], legend="test")
+ assert imageRgbUpdated is imageRgb
+ assert numpy.array_equal(imageRgbUpdated.getData(copy=False), [[(1.0, 0.0, 0.0)]])
+ assert imageRgbUpdated.getName() == "test"
+
+
+def testPlotWidgetAddScatter(plotWidget):
+ scatter = plotWidget.addScatter(
+ x=(0, 1), y=(0, 1), value=(0, 1), legend="test", symbol="s"
+ )
+ assert isinstance(scatter, items.Scatter)
+ assert numpy.array_equal(scatter.getXData(copy=False), (0, 1))
+ assert numpy.array_equal(scatter.getYData(copy=False), (0, 1))
+ assert numpy.array_equal(scatter.getValueData(copy=False), (0, 1))
+ assert scatter.getName() == "test"
+ assert scatter.getSymbol() == "s"
+
+
+def testPlotWidgetAddHistogram(plotWidget):
+ histogram = plotWidget.addHistogram(
+ histogram=[1], edges=(0, 1), legend="test", fill=True
+ )
+ assert isinstance(histogram, items.Histogram)
+ assert numpy.array_equal(histogram.getBinEdgesData(copy=False), (0, 1))
+ assert numpy.array_equal(histogram.getValueData(copy=False), [1])
+ assert histogram.getName() == "test"
+ assert histogram.isFill()
+
+
+def testPlotWidgetAddMarker(plotWidget):
+ marker = plotWidget.addMarker(x=0, y=1, legend="test")
+ assert isinstance(marker, items.Marker)
+ assert marker.getPosition() == (0, 1)
+ assert marker.getName() == "test"
+ assert plotWidget.getItems() == (marker,)
+
+ xmarker = plotWidget.addXMarker(1, legend="test")
+ assert isinstance(xmarker, items.XMarker)
+ assert xmarker.getPosition() == (1, None)
+ assert xmarker.getName() == "test"
+ assert plotWidget.getItems() == (xmarker,)
+
+ ymarker = plotWidget.addYMarker(2, legend="test")
+ assert isinstance(ymarker, items.YMarker)
+ assert ymarker.getPosition() == (None, 2)
+ assert ymarker.getName() == "test"
+ assert plotWidget.getItems() == (ymarker,)
+
+
+def testPlotWidgetAddShape(plotWidget):
+ shape = plotWidget.addShape(
+ xdata=(0, 1), ydata=(0, 1), legend="test", shape="polygon"
+ )
+ assert isinstance(shape, items.Shape)
+ assert numpy.array_equal(shape.getPoints(copy=False), ((0, 0), (1, 1)))
+ assert shape.getName() == "test"
+ assert shape.getType() == "polygon"
+
+
+@pytest.mark.parametrize(
+ "linestyle",
+ (
+ "",
+ "-",
+ "--",
+ "-.",
+ ":",
+ (0.0, None),
+ (0.5, ()),
+ (0.0, (5.0, 5.0)),
+ (4.0, (8.0, 4.0, 4.0, 4.0)),
+ ),
+)
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testLineStyle(qapp_utils, plotWidget, linestyle):
+ """Test different line styles for LineMixIn items"""
+ plotWidget.setGraphTitle(f"Line style: {linestyle}")
+
+ curve = plotWidget.addCurve((0, 1), (0, 1), linestyle=linestyle)
+ assert curve.getLineStyle() == linestyle
+
+ histogram = plotWidget.addHistogram((0.25, 0.75, 0.25), (0.0, 0.33, 0.66, 1.0))
+ histogram.setLineStyle(linestyle)
+ assert histogram.getLineStyle() == linestyle
+
+ polylines = plotWidget.addShape(
+ (0, 1), (1, 0), shape="polylines", linestyle=linestyle
+ )
+ assert polylines.getLineStyle() == linestyle
+
+ rectangle = plotWidget.addShape(
+ (0.4, 0.6), (0.4, 0.6), shape="rectangle", linestyle=linestyle
+ )
+ assert rectangle.getLineStyle() == linestyle
+
+ xmarker = plotWidget.addXMarker(0.5)
+ xmarker.setLineStyle(linestyle)
+ assert xmarker.getLineStyle() == linestyle
+
+ ymarker = plotWidget.addYMarker(0.5)
+ ymarker.setLineStyle(linestyle)
+ assert ymarker.getLineStyle() == linestyle
+
+ plotWidget.replot()
+ qapp_utils.qWait(100)
diff --git a/src/silx/gui/plot/test/testLegendSelector.py b/src/silx/gui/plot/test/testLegendSelector.py
index 3a596ac..a1f000a 100644
--- a/src/silx/gui/plot/test/testLegendSelector.py
+++ b/src/silx/gui/plot/test/testLegendSelector.py
@@ -29,7 +29,6 @@ __date__ = "15/05/2017"
import logging
-import unittest
from silx.gui import qt
from silx.gui.utils.testutils import TestCaseQt
@@ -44,6 +43,7 @@ class TestLegendSelector(TestCaseQt):
def testLegendSelector(self):
"""Test copied from __main__ of LegendSelector in PyMca"""
+
class Notifier(qt.QObject):
def __init__(self):
qt.QObject.__init__(self)
@@ -51,22 +51,31 @@ class TestLegendSelector(TestCaseQt):
def signalReceived(self, **kw):
obj = self.sender()
- _logger.info('NOTIFIER -- signal received\n\tsender: %s',
- str(obj))
+ _logger.info("NOTIFIER -- signal received\n\tsender: %s", str(obj))
notifier = Notifier()
- legends = ['Legend0',
- 'Legend1',
- 'Long Legend 2',
- 'Foo Legend 3',
- 'Even Longer Legend 4',
- 'Short Leg 5',
- 'Dot symbol 6',
- 'Comma symbol 7']
- colors = [qt.Qt.darkRed, qt.Qt.green, qt.Qt.yellow, qt.Qt.darkCyan,
- qt.Qt.blue, qt.Qt.darkBlue, qt.Qt.red, qt.Qt.darkYellow]
- symbols = ['o', 't', '+', 'x', 's', 'd', '.', ',']
+ legends = [
+ "Legend0",
+ "Legend1",
+ "Long Legend 2",
+ "Foo Legend 3",
+ "Even Longer Legend 4",
+ "Short Leg 5",
+ "Dot symbol 6",
+ "Comma symbol 7",
+ ]
+ colors = [
+ qt.Qt.darkRed,
+ qt.Qt.green,
+ qt.Qt.yellow,
+ qt.Qt.darkCyan,
+ qt.Qt.blue,
+ qt.Qt.darkBlue,
+ qt.Qt.red,
+ qt.Qt.darkYellow,
+ ]
+ symbols = ["o", "t", "+", "x", "s", "d", ".", ","]
win = LegendSelector.LegendListView()
# win = LegendListContextMenu()
@@ -77,9 +86,9 @@ class TestLegendSelector(TestCaseQt):
for _idx, (l, c, s) in enumerate(zip(legends, colors, symbols)):
ddict = {
- 'color': qt.QColor(c),
- 'linewidth': 4,
- 'symbol': s,
+ "color": qt.QColor(c),
+ "linewidth": 4,
+ "symbol": s,
}
legend = l
llist.append((legend, ddict))
@@ -116,14 +125,15 @@ class TestRenameCurveDialog(TestCaseQt):
def testDialog(self):
"""Create dialog, change name and press OK"""
self.dialog = LegendSelector.RenameCurveDialog(
- None, 'curve1', ['curve1', 'curve2', 'curve3'])
+ None, "curve1", ["curve1", "curve2", "curve3"]
+ )
self.dialog.open()
self.qWaitForWindowExposed(self.dialog)
- self.keyClicks(self.dialog.lineEdit, 'changed')
+ self.keyClicks(self.dialog.lineEdit, "changed")
self.mouseClick(self.dialog.okButton, qt.Qt.LeftButton)
self.qapp.processEvents()
ret = self.dialog.result()
self.assertEqual(ret, qt.QDialog.Accepted)
newName = self.dialog.getText()
- self.assertEqual(newName, 'curve1changed')
+ self.assertEqual(newName, "curve1changed")
del self.dialog
diff --git a/src/silx/gui/plot/test/testMaskToolsWidget.py b/src/silx/gui/plot/test/testMaskToolsWidget.py
index 5f36ec2..1428687 100644
--- a/src/silx/gui/plot/test/testMaskToolsWidget.py
+++ b/src/silx/gui/plot/test/testMaskToolsWidget.py
@@ -30,7 +30,6 @@ __date__ = "17/01/2018"
import logging
import os.path
-import unittest
import numpy
@@ -41,8 +40,6 @@ from silx.gui.utils.testutils import getQToolButtonFromAction
from silx.gui.plot import PlotWindow, MaskToolsWidget
from .utils import PlotWidgetTestCase
-import fabio
-
_logger = logging.getLogger(__name__)
@@ -55,7 +52,7 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def setUp(self):
super(TestMaskToolsWidget, self).setUp()
- self.widget = MaskToolsWidget.MaskToolsDockWidget(plot=self.plot, name='TEST')
+ self.widget = MaskToolsWidget.MaskToolsDockWidget(plot=self.plot, name="TEST")
self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, self.widget)
self.maskWidget = self.widget.widget()
@@ -66,10 +63,10 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def testEmptyPlot(self):
"""Empty plot, display MaskToolsDockWidget, toggle multiple masks"""
- self.maskWidget.setMultipleMasks('single')
+ self.maskWidget.setMultipleMasks("single")
self.qapp.processEvents()
- self.maskWidget.setMultipleMasks('exclusive')
+ self.maskWidget.setMultipleMasks("exclusive")
self.qapp.processEvents()
def _drag(self):
@@ -99,12 +96,14 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
x, y = plot.width() // 2, plot.height() // 2
offset = min(plot.width(), plot.height()) // 10
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset),
- (x, y + offset)] # Close polygon
+ star = [
+ (x, y + offset),
+ (x - offset, y - offset),
+ (x + offset, y),
+ (x - offset, y),
+ (x + offset, y - offset),
+ (x, y + offset),
+ ] # Close polygon
self.mouseMove(plot, pos=(0, 0))
for pos in star:
@@ -121,28 +120,33 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
x, y = plot.width() // 2, plot.height() // 2
offset = min(plot.width(), plot.height()) // 10
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset)]
+ star = [
+ (x, y + offset),
+ (x - offset, y - offset),
+ (x + offset, y),
+ (x - offset, y),
+ (x + offset, y - offset),
+ ]
self.mouseMove(plot, pos=(0, 0))
for start, end in zip(star[:-1], star[1:]):
- self.mouseMove(plot, pos=start)
- self.mousePress(plot, qt.Qt.LeftButton, pos=start)
- self.qapp.processEvents()
- self.mouseMove(plot, pos=end)
- self.qapp.processEvents()
- self.mouseRelease(plot, qt.Qt.LeftButton, pos=end)
- self.qapp.processEvents()
+ self.mouseMove(plot, pos=start)
+ self.mousePress(plot, qt.Qt.LeftButton, pos=start)
+ self.qapp.processEvents()
+ self.mouseMove(plot, pos=end)
+ self.qapp.processEvents()
+ self.mouseRelease(plot, qt.Qt.LeftButton, pos=end)
+ self.qapp.processEvents()
def _isMaskItemSync(self):
"""Check if masks from item and tools are sync or not"""
if self.maskWidget.isItemMaskUpdated():
- return numpy.all(numpy.equal(
- self.maskWidget.getSelectionMask(),
- self.plot.getActiveImage().getMaskData(copy=False)))
+ return numpy.all(
+ numpy.equal(
+ self.maskWidget.getSelectionMask(),
+ self.plot.getActiveImage().getMaskData(copy=False),
+ )
+ )
else:
return True
@@ -150,30 +154,36 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
"""Plot with an image: test MaskToolsWidget interactions"""
# Add and remove a image (this should enable/disable GUI + change mask)
- self.plot.addImage(numpy.random.random(1024**2).reshape(1024, 1024),
- legend='test')
+ self.plot.addImage(
+ numpy.random.random(1024**2).reshape(1024, 1024), legend="test"
+ )
self.qapp.processEvents()
- self.plot.remove('test', kind='image')
+ self.plot.remove("test", kind="image")
self.qapp.processEvents()
- tests = [((0, 0), (1, 1)),
- ((1000, 1000), (1, 1)),
- ((0, 0), (-1, -1)),
- ((1000, 1000), (-1, -1))]
+ tests = [
+ ((0, 0), (1, 1)),
+ ((1000, 1000), (1, 1)),
+ ((0, 0), (-1, -1)),
+ ((1000, 1000), (-1, -1)),
+ ]
for itemMaskUpdated in (False, True):
for origin, scale in tests:
with self.subTest(origin=origin, scale=scale):
self.maskWidget.setItemMaskUpdated(itemMaskUpdated)
- self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024),
- legend='test',
- origin=origin,
- scale=scale)
+ self.plot.addImage(
+ numpy.arange(1024**2).reshape(1024, 1024),
+ legend="test",
+ origin=origin,
+ scale=scale,
+ )
self.qapp.processEvents()
self.assertEqual(
- self.maskWidget.isItemMaskUpdated(), itemMaskUpdated)
+ self.maskWidget.isItemMaskUpdated(), itemMaskUpdated
+ )
# Test draw rectangle #
toolButton = getQToolButtonFromAction(self.maskWidget.rectAction)
@@ -185,7 +195,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drag()
self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# unmask same region
@@ -193,7 +204,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drag()
self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# Test draw polygon #
@@ -206,7 +218,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drawPolygon()
self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# unmask same region
@@ -214,7 +227,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drawPolygon()
self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# Test draw pencil #
@@ -230,7 +244,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drawPencil()
self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# unmask same region
@@ -238,7 +253,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drawPencil()
self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# Test no draw tool #
@@ -250,8 +266,7 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def __loadSave(self, file_format):
"""Plot with an image: test MaskToolsWidget operations"""
- self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024),
- legend='test')
+ self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024), legend="test")
self.qapp.processEvents()
# Draw a polygon mask
@@ -264,16 +279,18 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.assertFalse(numpy.all(numpy.equal(ref_mask, 0)))
with temp_dir() as tmp:
- mask_filename = os.path.join(tmp, 'mask.' + file_format)
+ mask_filename = os.path.join(tmp, "mask." + file_format)
self.maskWidget.save(mask_filename, file_format)
self.maskWidget.resetSelectionMask()
self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.maskWidget.load(mask_filename)
- self.assertTrue(numpy.all(numpy.equal(
- self.maskWidget.getSelectionMask(), ref_mask)))
+ self.assertTrue(
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), ref_mask))
+ )
def testLoadSaveNpy(self):
self.__loadSave("npy")
@@ -282,8 +299,7 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.__loadSave("msk")
def testSigMaskChangedEmitted(self):
- self.plot.addImage(numpy.arange(512**2).reshape(512, 512),
- legend='test')
+ self.plot.addImage(numpy.arange(512**2).reshape(512, 512), legend="test")
self.plot.resetZoom()
self.qapp.processEvents()
diff --git a/src/silx/gui/plot/test/testPixelIntensityHistoAction.py b/src/silx/gui/plot/test/testPixelIntensityHistoAction.py
index 43d7588..7fd87e8 100644
--- a/src/silx/gui/plot/test/testPixelIntensityHistoAction.py
+++ b/src/silx/gui/plot/test/testPixelIntensityHistoAction.py
@@ -29,7 +29,6 @@ __date__ = "02/03/2018"
import numpy
-import unittest
from silx.utils.testutils import ParametricTestCase
from silx.gui.utils.testutils import TestCaseQt, getQToolButtonFromAction
@@ -53,7 +52,7 @@ class TestPixelIntensitiesHisto(TestCaseQt, ParametricTestCase):
def testShowAndHide(self):
"""Simple test that the plot is showing and hiding when activating the
action"""
- self.plotImage.addImage(self.image, origin=(0, 0), legend='sino')
+ self.plotImage.addImage(self.image, origin=(0, 0), legend="sino")
self.plotImage.show()
histoAction = self.plotImage.getIntensityHistogramAction()
@@ -67,7 +66,7 @@ class TestPixelIntensitiesHisto(TestCaseQt, ParametricTestCase):
self.assertTrue(histoAction.getHistogramWidget().isVisible())
# test the pixel intensity diagram is hiding
- self.qapp.setActiveWindow(self.plotImage)
+ self.plotImage.activateWindow()
self.qapp.processEvents()
self.mouseMove(button)
self.mouseClick(button, qt.Qt.LeftButton)
@@ -76,19 +75,25 @@ class TestPixelIntensitiesHisto(TestCaseQt, ParametricTestCase):
def testImageFormatInput(self):
"""Test multiple type as image input"""
- typesToTest = [numpy.uint8, numpy.int8, numpy.int16, numpy.int32,
- numpy.float32, numpy.float64]
- self.plotImage.addImage(self.image, origin=(0, 0), legend='sino')
+ typesToTest = [
+ numpy.uint8,
+ numpy.int8,
+ numpy.int16,
+ numpy.int32,
+ numpy.float32,
+ numpy.float64,
+ ]
+ self.plotImage.addImage(self.image, origin=(0, 0), legend="sino")
self.plotImage.show()
- button = getQToolButtonFromAction(
- self.plotImage.getIntensityHistogramAction())
+ button = getQToolButtonFromAction(self.plotImage.getIntensityHistogramAction())
self.mouseMove(button)
self.mouseClick(button, qt.Qt.LeftButton)
self.qapp.processEvents()
for typeToTest in typesToTest:
with self.subTest(typeToTest=typeToTest):
- self.plotImage.addImage(self.image.astype(typeToTest),
- origin=(0, 0), legend='sino')
+ self.plotImage.addImage(
+ self.image.astype(typeToTest), origin=(0, 0), legend="sino"
+ )
def testScatter(self):
"""Test that an histogram from a scatter is displayed"""
@@ -136,7 +141,7 @@ class TestPixelIntensitiesHisto(TestCaseQt, ParametricTestCase):
data1 = items[0].getValueData(copy=False)
# Set another item to the plot
- self.plotImage.addImage(self.image, origin=(0, 0), legend='sino')
+ self.plotImage.addImage(self.image, origin=(0, 0), legend="sino")
self.qapp.processEvents()
data2 = items[0].getValueData(copy=False)
diff --git a/src/silx/gui/plot/test/testPlotActions.py b/src/silx/gui/plot/test/testPlotActions.py
index 4006ab9..9f56aad 100644
--- a/src/silx/gui/plot/test/testPlotActions.py
+++ b/src/silx/gui/plot/test/testPlotActions.py
@@ -40,17 +40,13 @@ import numpy
@pytest.fixture
def colormap1():
- colormap = Colormap(name='gray',
- vmin=10.0, vmax=20.0,
- normalization='linear')
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
yield colormap
@pytest.fixture
def colormap2():
- colormap = Colormap(name='red',
- vmin=10.0, vmax=20.0,
- normalization='linear')
+ colormap = Colormap(name="red", vmin=10.0, vmax=20.0, normalization="linear")
yield colormap
@@ -70,25 +66,25 @@ def test_action_active_colormap(qapp_utils, plot, colormap1, colormap2):
defaultColormap = plot.getDefaultColormap()
assert colormapDialog.getColormap() is defaultColormap
- plot.addImage(data=numpy.random.rand(10, 10), legend='img1',
- origin=(0, 0),
- colormap=colormap1)
- plot.setActiveImage('img1')
+ plot.addImage(
+ data=numpy.random.rand(10, 10), legend="img1", origin=(0, 0), colormap=colormap1
+ )
+ plot.setActiveImage("img1")
assert colormapDialog.getColormap() is colormap1
- plot.addImage(data=numpy.random.rand(10, 10), legend='img2',
- origin=(0, 0), colormap=colormap2)
- plot.addImage(data=numpy.random.rand(10, 10), legend='img3',
- origin=(0, 0))
+ plot.addImage(
+ data=numpy.random.rand(10, 10), legend="img2", origin=(0, 0), colormap=colormap2
+ )
+ plot.addImage(data=numpy.random.rand(10, 10), legend="img3", origin=(0, 0))
- plot.setActiveImage('img3')
+ plot.setActiveImage("img3")
assert colormapDialog.getColormap() is defaultColormap
plot.getActiveImage().setColormap(colormap2)
assert colormapDialog.getColormap() is colormap2
- plot.remove('img2')
- plot.remove('img3')
- plot.remove('img1')
+ plot.remove("img2")
+ plot.remove("img3")
+ plot.remove("img1")
assert colormapDialog.getColormap() is defaultColormap
@@ -100,10 +96,11 @@ def test_action_show_hide_colormap_dialog(qapp_utils, plot, colormap1):
assert not plot.getColormapAction().isChecked()
plot.getColormapAction()._actionTriggered(checked=True)
assert plot.getColormapAction().isChecked()
- plot.addImage(data=numpy.random.rand(10, 10), legend='img1',
- origin=(0, 0), colormap=colormap1)
- colormap1.setName('red')
+ plot.addImage(
+ data=numpy.random.rand(10, 10), legend="img1", origin=(0, 0), colormap=colormap1
+ )
+ colormap1.setName("red")
plot.getColormapAction()._actionTriggered()
- colormap1.setName('blue')
+ colormap1.setName("blue")
colormapDialog.close()
assert not plot.getColormapAction().isChecked()
diff --git a/src/silx/gui/plot/test/testPlotInteraction.py b/src/silx/gui/plot/test/testPlotInteraction.py
index 17aad97..a97a694 100644
--- a/src/silx/gui/plot/test/testPlotInteraction.py
+++ b/src/silx/gui/plot/test/testPlotInteraction.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2016=2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -27,9 +27,10 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "01/09/2017"
+import pytest
-import unittest
from silx.gui import qt
+from silx.gui.plot import PlotWidget
from .utils import PlotWidgetTestCase
@@ -78,82 +79,154 @@ class TestSelectPolygon(PlotWidgetTestCase):
def test(self):
"""Test draw polygons + events"""
- self.plot.sigInteractiveModeChanged.connect(
- self._interactionModeChanged)
+ self.plot.sigInteractiveModeChanged.connect(self._interactionModeChanged)
- self.plot.setInteractiveMode(
- 'draw', shape='polygon', label='test', source=self)
+ self.plot.setInteractiveMode("draw", shape="polygon", label="test", source=self)
interaction = self.plot.getInteractiveMode()
- self.assertEqual(interaction['mode'], 'draw')
- self.assertEqual(interaction['shape'], 'polygon')
+ self.assertEqual(interaction["mode"], "draw")
+ self.assertEqual(interaction["shape"], "polygon")
- self.plot.sigInteractiveModeChanged.disconnect(
- self._interactionModeChanged)
+ self.plot.sigInteractiveModeChanged.disconnect(self._interactionModeChanged)
plot = self.plot.getWidgetHandle()
xCenter, yCenter = plot.width() // 2, plot.height() // 2
offset = min(plot.width(), plot.height()) // 10
# Star polygon
- star = [(xCenter, yCenter + offset),
- (xCenter - offset, yCenter - offset),
- (xCenter + offset, yCenter),
- (xCenter - offset, yCenter),
- (xCenter + offset, yCenter - offset),
- (xCenter, yCenter + offset)] # Close polygon
+ star = [
+ (xCenter, yCenter + offset),
+ (xCenter - offset, yCenter - offset),
+ (xCenter + offset, yCenter),
+ (xCenter - offset, yCenter),
+ (xCenter + offset, yCenter - offset),
+ (xCenter, yCenter + offset),
+ ] # Close polygon
# Draw while dumping signals
events = self._draw(star)
# Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 6)
+ drawEvents = [event for event in events if event["event"].startswith("drawing")]
+ self.assertEqual(drawEvents[-1]["event"], "drawingFinished")
+ self.assertEqual(len(drawEvents[-1]["points"]), 6)
# Large square
- largeSquare = [(xCenter - offset, yCenter - offset),
- (xCenter + offset, yCenter - offset),
- (xCenter + offset, yCenter + offset),
- (xCenter - offset, yCenter + offset),
- (xCenter - offset, yCenter - offset)] # Close polygon
+ largeSquare = [
+ (xCenter - offset, yCenter - offset),
+ (xCenter + offset, yCenter - offset),
+ (xCenter + offset, yCenter + offset),
+ (xCenter - offset, yCenter + offset),
+ (xCenter - offset, yCenter - offset),
+ ] # Close polygon
# Draw while dumping signals
events = self._draw(largeSquare)
# Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 5)
+ drawEvents = [event for event in events if event["event"].startswith("drawing")]
+ self.assertEqual(drawEvents[-1]["event"], "drawingFinished")
+ self.assertEqual(len(drawEvents[-1]["points"]), 5)
# Rectangle too thin along X: Some points are ignored
- thinRectX = [(xCenter, yCenter - offset),
- (xCenter, yCenter + offset),
- (xCenter + 1, yCenter + offset),
- (xCenter + 1, yCenter - offset)] # Close polygon
+ thinRectX = [
+ (xCenter, yCenter - offset),
+ (xCenter, yCenter + offset),
+ (xCenter + 1, yCenter + offset),
+ (xCenter + 1, yCenter - offset),
+ ] # Close polygon
# Draw while dumping signals
events = self._draw(thinRectX)
# Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 3)
+ drawEvents = [event for event in events if event["event"].startswith("drawing")]
+ self.assertEqual(drawEvents[-1]["event"], "drawingFinished")
+ self.assertEqual(len(drawEvents[-1]["points"]), 3)
# Rectangle too thin along Y: Some points are ignored
- thinRectY = [(xCenter - offset, yCenter),
- (xCenter + offset, yCenter),
- (xCenter + offset, yCenter + 1),
- (xCenter - offset, yCenter + 1)] # Close polygon
+ thinRectY = [
+ (xCenter - offset, yCenter),
+ (xCenter + offset, yCenter),
+ (xCenter + offset, yCenter + 1),
+ (xCenter - offset, yCenter + 1),
+ ] # Close polygon
# Draw while dumping signals
events = self._draw(thinRectY)
# Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 3)
+ drawEvents = [event for event in events if event["event"].startswith("drawing")]
+ self.assertEqual(drawEvents[-1]["event"], "drawingFinished")
+ self.assertEqual(len(drawEvents[-1]["points"]), 3)
+
+
+@pytest.mark.parametrize("scale", ["linear", "log"])
+@pytest.mark.parametrize("xaxis", [True, False])
+@pytest.mark.parametrize("yaxis", [True, False])
+@pytest.mark.parametrize("y2axis", [True, False])
+def testZoomEnabledAxes(qapp, qWidgetFactory, scale, xaxis, yaxis, y2axis):
+ """Test PlotInteraction.setZoomEnabledAxes effect on zoom interaction"""
+ plotWidget = qWidgetFactory(PlotWidget)
+ plotWidget.getXAxis().setScale(scale)
+ plotWidget.getYAxis("left").setScale(scale)
+ plotWidget.getYAxis("right").setScale(scale)
+ qapp.processEvents()
+
+ xLimits = plotWidget.getXAxis().getLimits()
+ yLimits = plotWidget.getYAxis("left").getLimits()
+ y2Limits = plotWidget.getYAxis("right").getLimits()
+
+ interaction = plotWidget.interaction()
+
+ assert interaction.getZoomEnabledAxes() == (True, True, True)
+
+ enabledAxes = xaxis, yaxis, y2axis
+ interaction.setZoomEnabledAxes(*enabledAxes)
+ assert interaction.getZoomEnabledAxes() == enabledAxes
+
+ cx, cy = plotWidget.width() // 2, plotWidget.height() // 2
+ plotWidget.onMouseWheel(cx, cy, 10)
+ qapp.processEvents()
+
+ xZoomed = plotWidget.getXAxis().getLimits() != xLimits
+ yZoomed = plotWidget.getYAxis("left").getLimits() != yLimits
+ y2Zoomed = plotWidget.getYAxis("right").getLimits() != y2Limits
+
+ assert xZoomed == enabledAxes[0]
+ assert yZoomed == enabledAxes[1]
+ assert y2Zoomed == enabledAxes[2]
+
+
+@pytest.mark.parametrize("scale", ["linear", "log"])
+@pytest.mark.parametrize("zoomOnWheel", [True, False])
+def testZoomOnWheelEnabled(qapp, qWidgetFactory, zoomOnWheel, scale):
+ """Test PlotInteraction.setZoomOnWheelEnabled"""
+ plotWidget = qWidgetFactory(PlotWidget)
+ plotWidget.getXAxis().setScale(scale)
+ plotWidget.getYAxis("left").setScale(scale)
+ plotWidget.getYAxis("right").setScale(scale)
+ qapp.processEvents()
+
+ xLimits = plotWidget.getXAxis().getLimits()
+ yLimits = plotWidget.getYAxis("left").getLimits()
+ y2Limits = plotWidget.getYAxis("right").getLimits()
+
+ interaction = plotWidget.interaction()
+
+ assert interaction.isZoomOnWheelEnabled()
+
+ interaction.setZoomOnWheelEnabled(zoomOnWheel)
+ assert interaction.isZoomOnWheelEnabled() == zoomOnWheel
+
+ cx, cy = plotWidget.width() // 2, plotWidget.height() // 2
+ plotWidget.onMouseWheel(cx, cy, 10)
+ qapp.processEvents()
+
+ xZoomed = plotWidget.getXAxis().getLimits() != xLimits
+ yZoomed = plotWidget.getYAxis("left").getLimits() != yLimits
+ y2Zoomed = plotWidget.getYAxis("right").getLimits() != y2Limits
+
+ assert xZoomed == zoomOnWheel
+ assert yZoomed == zoomOnWheel
+ assert y2Zoomed == zoomOnWheel
diff --git a/src/silx/gui/plot/test/testPlotWidget.py b/src/silx/gui/plot/test/testPlotWidget.py
index 19a34a9..842e880 100755
--- a/src/silx/gui/plot/test/testPlotWidget.py
+++ b/src/silx/gui/plot/test/testPlotWidget.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2016-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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,7 +29,6 @@ __date__ = "03/01/2019"
import unittest
-import logging
import numpy
import pytest
@@ -39,7 +38,6 @@ from silx.gui.utils.testutils import TestCaseQt
from silx.gui import qt
from silx.gui.plot import PlotWidget
-from silx.gui.plot.items.curve import CurveStyle
from silx.gui.plot.items import BoundingRect, XAxisExtent, YAxisExtent, Axis
from silx.gui.colors import Colormap
@@ -49,16 +47,12 @@ from .utils import PlotWidgetTestCase
SIZE = 1024
"""Size of the test image"""
-DATA_2D = numpy.arange(SIZE ** 2).reshape(SIZE, SIZE)
+DATA_2D = numpy.arange(SIZE**2).reshape(SIZE, SIZE)
"""Image data set"""
-logger = logging.getLogger(__name__)
-
-
class TestSpecialBackend(PlotWidgetTestCase, ParametricTestCase):
-
- def __init__(self, methodName='runTest', backend=None):
+ def __init__(self, methodName="runTest", backend=None):
TestCaseQt.__init__(self, methodName=methodName)
self.__backend = backend
@@ -79,7 +73,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
def testSetTitleLabels(self):
"""Set title and axes labels"""
- title, xlabel, ylabel = 'the title', 'x label', 'y label'
+ title, xlabel, ylabel = "the title", "x label", "y label"
self.plot.setGraphTitle(title)
self.plot.getXAxis().setLabel(xlabel)
self.plot.getYAxis().setLabel(ylabel)
@@ -89,10 +83,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
self.assertEqual(self.plot.getXAxis().getLabel(), xlabel)
self.assertEqual(self.plot.getYAxis().getLabel(), ylabel)
- def _checkLimits(self,
- expectedXLim=None,
- expectedYLim=None,
- expectedRatio=None):
+ def _checkLimits(self, expectedXLim=None, expectedYLim=None, expectedRatio=None):
"""Assert that limits are as expected"""
xlim = self.plot.getXAxis().getLimits()
ylim = self.plot.getYAxis().getLimits()
@@ -105,8 +96,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
self.assertEqual(expectedYLim, ylim)
if expectedRatio is not None:
- self.assertTrue(
- numpy.allclose(expectedRatio, ratio, atol=0.01))
+ self.assertTrue(numpy.allclose(expectedRatio, ratio, atol=0.01))
def testChangeLimitsWithAspectRatio(self):
self.plot.setKeepDataAspectRatio()
@@ -115,15 +105,15 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
ylim = self.plot.getYAxis().getLimits()
defaultRatio = abs(xlim[1] - xlim[0]) / abs(ylim[1] - ylim[0])
- self.plot.getXAxis().setLimits(1., 10.)
- self._checkLimits(expectedXLim=(1., 10.), expectedRatio=defaultRatio)
+ self.plot.getXAxis().setLimits(1.0, 10.0)
+ self._checkLimits(expectedXLim=(1.0, 10.0), expectedRatio=defaultRatio)
self.qapp.processEvents()
- self._checkLimits(expectedXLim=(1., 10.), expectedRatio=defaultRatio)
+ self._checkLimits(expectedXLim=(1.0, 10.0), expectedRatio=defaultRatio)
- self.plot.getYAxis().setLimits(1., 10.)
- self._checkLimits(expectedYLim=(1., 10.), expectedRatio=defaultRatio)
+ self.plot.getYAxis().setLimits(1.0, 10.0)
+ self._checkLimits(expectedYLim=(1.0, 10.0), expectedRatio=defaultRatio)
self.qapp.processEvents()
- self._checkLimits(expectedYLim=(1., 10.), expectedRatio=defaultRatio)
+ self._checkLimits(expectedYLim=(1.0, 10.0), expectedRatio=defaultRatio)
def testResizeWidget(self):
"""Test resizing the widget and receiving limitsChanged events"""
@@ -135,8 +125,8 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
ylim = self.plot.getYAxis().getLimits()
listener = SignalListener()
- self.plot.getXAxis().sigLimitsChanged.connect(listener.partial('x'))
- self.plot.getYAxis().sigLimitsChanged.connect(listener.partial('y'))
+ self.plot.getXAxis().sigLimitsChanged.connect(listener.partial("x"))
+ self.plot.getYAxis().sigLimitsChanged.connect(listener.partial("y"))
# Resize without aspect ratio
self.plot.resize(200, 300)
@@ -159,17 +149,17 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
def testAddRemoveItemSignals(self):
"""Test sigItemAdded and sigItemAboutToBeRemoved"""
listener = SignalListener()
- self.plot.sigItemAdded.connect(listener.partial('add'))
- self.plot.sigItemAboutToBeRemoved.connect(listener.partial('remove'))
+ self.plot.sigItemAdded.connect(listener.partial("add"))
+ self.plot.sigItemAboutToBeRemoved.connect(listener.partial("remove"))
- self.plot.addCurve((1, 2, 3), (3, 2, 1), legend='curve')
+ self.plot.addCurve((1, 2, 3), (3, 2, 1), legend="curve")
self.assertEqual(listener.callCount(), 1)
- curve = self.plot.getCurve('curve')
- self.plot.remove('curve')
+ curve = self.plot.getCurve("curve")
+ self.plot.remove("curve")
self.assertEqual(listener.callCount(), 2)
- self.assertEqual(listener.arguments(callIndex=0), ('add', curve))
- self.assertEqual(listener.arguments(callIndex=1), ('remove', curve))
+ self.assertEqual(listener.arguments(callIndex=0), ("add", curve))
+ self.assertEqual(listener.arguments(callIndex=1), ("remove", curve))
def testGetItems(self):
"""Test getItems method"""
@@ -183,7 +173,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
self.plot.addMarker(*marker_pos)
marker_x = 6
self.plot.addXMarker(marker_x)
- self.plot.addShape((0, 5), (2, 10), shape='rectangle')
+ self.plot.addShape((0, 5), (2, 10), shape="rectangle")
items = self.plot.getItems()
self.assertEqual(len(items), 6)
@@ -192,7 +182,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
self.assertTrue(numpy.all(numpy.equal(items[2].getXData(), scatter_x)))
self.assertTrue(numpy.all(numpy.equal(items[3].getPosition(), marker_pos)))
self.assertTrue(numpy.all(numpy.equal(items[4].getPosition()[0], marker_x)))
- self.assertEqual(items[5].getType(), 'rectangle')
+ self.assertEqual(items[5].getType(), "rectangle")
def testRemoveDiscardItem(self):
"""Test removeItem and discardItem"""
@@ -232,7 +222,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
# Back to default
- self.plot.setBackgroundColor('white')
+ self.plot.setBackgroundColor("white")
self.plot.setDataBackgroundColor(None)
color = self.plot.getBackgroundColor()
self.assertTrue(color.isValid())
@@ -248,116 +238,132 @@ class TestPlotImage(PlotWidgetTestCase, ParametricTestCase):
def setUp(self):
super(TestPlotImage, self).setUp()
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
+ self.plot.getYAxis().setLabel("Rows")
+ self.plot.getXAxis().setLabel("Columns")
def testPlotColormapTemperature(self):
- self.plot.setGraphTitle('Temp. Linear')
+ self.plot.setGraphTitle("Temp. Linear")
- colormap = Colormap(name='temperature',
- normalization='linear',
- vmin=None,
- vmax=None)
+ colormap = Colormap(
+ name="temperature", normalization="linear", vmin=None, vmax=None
+ )
self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
def testPlotColormapGray(self):
self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('Gray Linear')
+ self.plot.setGraphTitle("Gray Linear")
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
+ colormap = Colormap(name="gray", normalization="linear", vmin=None, vmax=None)
self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
def testPlotColormapTemperatureLog(self):
- self.plot.setGraphTitle('Temp. Log')
+ self.plot.setGraphTitle("Temp. Log")
- colormap = Colormap(name='temperature',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=None)
+ colormap = Colormap(
+ name="temperature", normalization=Colormap.LOGARITHM, vmin=None, vmax=None
+ )
self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
def testPlotRgbRgba(self):
self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('RGB + RGBA')
+ self.plot.setGraphTitle("RGB + RGBA")
rgb = numpy.array(
- (((0, 0, 0), (128, 0, 0), (255, 0, 0)),
- ((0, 128, 0), (0, 128, 128), (0, 128, 255))),
- dtype=numpy.uint8)
+ (
+ ((0, 0, 0), (128, 0, 0), (255, 0, 0)),
+ ((0, 128, 0), (0, 128, 128), (0, 128, 255)),
+ ),
+ dtype=numpy.uint8,
+ )
- self.plot.addImage(rgb, legend="rgb_uint8",
- origin=(0, 0), scale=(1, 1),
- resetzoom=False)
+ self.plot.addImage(
+ rgb, legend="rgb_uint8", origin=(0, 0), scale=(1, 1), resetzoom=False
+ )
rgb = numpy.array(
- (((0, 0, 0), (32768, 0, 0), (65535, 0, 0)),
- ((0, 32768, 0), (0, 32768, 32768), (0, 32768, 65535))),
- dtype=numpy.uint16)
+ (
+ ((0, 0, 0), (32768, 0, 0), (65535, 0, 0)),
+ ((0, 32768, 0), (0, 32768, 32768), (0, 32768, 65535)),
+ ),
+ dtype=numpy.uint16,
+ )
- self.plot.addImage(rgb, legend="rgb_uint16",
- origin=(3, 2), scale=(2, 2),
- resetzoom=False)
+ self.plot.addImage(
+ rgb, legend="rgb_uint16", origin=(3, 2), scale=(2, 2), resetzoom=False
+ )
rgba = numpy.array(
- (((0, 0, 0, .5), (.5, 0, 0, 1), (1, 0, 0, .5)),
- ((0, .5, 0, 1), (0, .5, .5, 1), (0, 1, 1, .5))),
- dtype=numpy.float32)
+ (
+ ((0, 0, 0, 0.5), (0.5, 0, 0, 1), (1, 0, 0, 0.5)),
+ ((0, 0.5, 0, 1), (0, 0.5, 0.5, 1), (0, 1, 1, 0.5)),
+ ),
+ dtype=numpy.float32,
+ )
- self.plot.addImage(rgba, legend="rgba_float32",
- origin=(9, 6), scale=(1, 1),
- resetzoom=False)
+ self.plot.addImage(
+ rgba, legend="rgba_float32", origin=(9, 6), scale=(1, 1), resetzoom=False
+ )
self.plot.resetZoom()
def testPlotColormapCustom(self):
self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('Custom colormap')
-
- colormap = Colormap(name=None,
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None,
- colors=((0., 0., 0.), (1., 0., 0.),
- (0., 1., 0.), (0., 0., 1.)))
- self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap,
- resetzoom=False)
-
- colormap = Colormap(name=None,
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None,
- colors=numpy.array(
- ((0, 0, 0, 0), (0, 0, 0, 128),
- (128, 128, 128, 128), (255, 255, 255, 255)),
- dtype=numpy.uint8))
- self.plot.addImage(DATA_2D, legend="image 2", colormap=colormap,
- origin=(DATA_2D.shape[0], 0),
- resetzoom=False)
+ self.plot.setGraphTitle("Custom colormap")
+
+ colormap = Colormap(
+ name=None,
+ normalization=Colormap.LINEAR,
+ vmin=None,
+ vmax=None,
+ colors=((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
+ )
+ self.plot.addImage(
+ DATA_2D, legend="image 1", colormap=colormap, resetzoom=False
+ )
+
+ colormap = Colormap(
+ name=None,
+ normalization=Colormap.LINEAR,
+ vmin=None,
+ vmax=None,
+ colors=numpy.array(
+ (
+ (0, 0, 0, 0),
+ (0, 0, 0, 128),
+ (128, 128, 128, 128),
+ (255, 255, 255, 255),
+ ),
+ dtype=numpy.uint8,
+ ),
+ )
+ self.plot.addImage(
+ DATA_2D,
+ legend="image 2",
+ colormap=colormap,
+ origin=(DATA_2D.shape[0], 0),
+ resetzoom=False,
+ )
self.plot.resetZoom()
def testPlotColormapNaNColor(self):
self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('Colormap with NaN color')
+ self.plot.setGraphTitle("Colormap with NaN color")
colormap = Colormap()
- colormap.setNaNColor('red')
+ colormap.setNaNColor("red")
self.assertEqual(colormap.getNaNColor(), qt.QColor(255, 0, 0))
data = DATA_2D.astype(numpy.float32)
- data[len(data)//2:] = numpy.nan
- self.plot.addImage(data, legend="image 1", colormap=colormap,
- resetzoom=False)
+ data[len(data) // 2 :] = numpy.nan
+ self.plot.addImage(data, legend="image 1", colormap=colormap, resetzoom=False)
self.plot.resetZoom()
- colormap.setNaNColor((0., 1., 0., 1.))
+ colormap.setNaNColor((0.0, 1.0, 0.0, 1.0))
self.assertEqual(colormap.getNaNColor(), qt.QColor(0, 255, 0))
self.qapp.processEvents()
def testImageOriginScale(self):
"""Test of image with different origin and scale"""
- self.plot.setGraphTitle('origin and scale')
+ self.plot.setGraphTitle("origin and scale")
tests = [ # (origin, scale)
((10, 20), (1, 1)),
@@ -367,7 +373,7 @@ class TestPlotImage(PlotWidgetTestCase, ParametricTestCase):
(100, 2),
(-100, (1, 1)),
((10, 20), 2),
- ]
+ ]
for origin, scale in tests:
with self.subTest(origin=origin, scale=scale):
@@ -408,31 +414,30 @@ class TestPlotImage(PlotWidgetTestCase, ParametricTestCase):
def testPlotColormapDictAPI(self):
"""Test that the addImage API using a colormap dictionary is still
working"""
- self.plot.setGraphTitle('Temp. Log')
+ self.plot.setGraphTitle("Temp. Log")
colormap = {
- 'name': 'temperature',
- 'normalization': 'log',
- 'vmin': None,
- 'vmax': None
+ "name": "temperature",
+ "normalization": "log",
+ "vmin": None,
+ "vmax": None,
}
self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
def testPlotComplexImage(self):
"""Test that a complex image is displayed as its absolute value."""
data = numpy.linspace(1, 1j, 100).reshape(10, 10)
- self.plot.addImage(data, legend='complex')
+ self.plot.addImage(data, legend="complex")
image = self.plot.getActiveImage()
retrievedData = image.getData(copy=False)
- self.assertTrue(
- numpy.all(numpy.equal(retrievedData, numpy.absolute(data))))
+ self.assertTrue(numpy.all(numpy.equal(retrievedData, numpy.absolute(data))))
def testPlotBooleanImage(self):
"""Test that a boolean image is displayed and converted to int8."""
data = numpy.zeros((10, 10), dtype=bool)
data[::2, ::2] = True
- self.plot.addImage(data, legend='boolean')
+ self.plot.addImage(data, legend="boolean")
image = self.plot.getActiveImage()
retrievedData = image.getData(copy=False)
@@ -443,7 +448,7 @@ class TestPlotImage(PlotWidgetTestCase, ParametricTestCase):
"""Test with an alpha image layer"""
data = numpy.random.random((10, 10))
alpha = numpy.linspace(0, 1, 100).reshape(10, 10)
- self.plot.addImage(data, legend='image')
+ self.plot.addImage(data, legend="image")
image = self.plot.getActiveImage()
image.setData(data, alpha=alpha)
self.qapp.processEvents()
@@ -461,19 +466,19 @@ class TestPlotCurve(PlotWidgetTestCase):
def setUp(self):
super(TestPlotCurve, self).setUp()
- self.plot.setGraphTitle('Curve')
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
+ self.plot.setGraphTitle("Curve")
+ self.plot.getYAxis().setLabel("Rows")
+ self.plot.getXAxis().setLabel("Columns")
self.plot.setActiveCurveHandling(False)
def testPlotCurveInfinite(self):
"""Test plot curves with not finite data"""
tests = {
- 'y all not finite': ([0, 1, 2], [numpy.inf, numpy.nan, -numpy.inf]),
- 'x all not finite': ([numpy.inf, numpy.nan, -numpy.inf], [0, 1, 2]),
- 'x some inf': ([0, numpy.inf, 2], [0, 1, 2]),
- 'y some inf': ([0, 1, 2], [0, numpy.inf, 2])
+ "y all not finite": ([0, 1, 2], [numpy.inf, numpy.nan, -numpy.inf]),
+ "x all not finite": ([numpy.inf, numpy.nan, -numpy.inf], [0, 1, 2]),
+ "x some inf": ([0, numpy.inf, 2], [0, 1, 2]),
+ "y some inf": ([0, 1, 2], [0, numpy.inf, 2]),
}
for name, args in tests.items():
with self.subTest(name):
@@ -483,65 +488,111 @@ class TestPlotCurve(PlotWidgetTestCase):
self.plot.clear()
def testPlotCurveColorFloat(self):
- color = numpy.array(numpy.random.random(3 * 1000),
- dtype=numpy.float32).reshape(1000, 3)
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 1",
- replace=False, resetzoom=False,
- color=color,
- linestyle="", symbol="s")
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ color = numpy.array(numpy.random.random(3 * 1000), dtype=numpy.float32).reshape(
+ 1000, 3
+ )
+
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 1",
+ replace=False,
+ resetzoom=False,
+ color=color,
+ linestyle="",
+ symbol="s",
+ )
+ self.plot.addCurve(
+ self.xData2,
+ self.yData2,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
self.plot.resetZoom()
def testPlotCurveColorByte(self):
- color = numpy.array(255 * numpy.random.random(3 * 1000),
- dtype=numpy.uint8).reshape(1000, 3)
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 1",
- replace=False, resetzoom=False,
- color=color,
- linestyle="", symbol="s")
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ color = numpy.array(
+ 255 * numpy.random.random(3 * 1000), dtype=numpy.uint8
+ ).reshape(1000, 3)
+
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 1",
+ replace=False,
+ resetzoom=False,
+ color=color,
+ linestyle="",
+ symbol="s",
+ )
+ self.plot.addCurve(
+ self.xData2,
+ self.yData2,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
self.plot.resetZoom()
def testPlotCurveColors(self):
- color = numpy.array(numpy.random.random(3 * 1000),
- dtype=numpy.float32).reshape(1000, 3)
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color=color, linestyle="-", symbol='o')
+ color = numpy.array(numpy.random.random(3 * 1000), dtype=numpy.float32).reshape(
+ 1000, 3
+ )
+
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color=color,
+ linestyle="-",
+ symbol="o",
+ )
self.plot.resetZoom()
# Test updating color array
# From array to array
newColors = numpy.ones((len(self.xData), 3), dtype=numpy.float32)
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color=newColors, symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color=newColors,
+ symbol="o",
+ )
# Array to single color
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color='green', symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ symbol="o",
+ )
# single color to array
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color=color, symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color=color,
+ symbol="o",
+ )
def testPlotBaselineNumpyArray(self):
"""simple test of the API with baseline as a numpy array"""
@@ -550,8 +601,9 @@ class TestPlotCurve(PlotWidgetTestCase):
y = numpy.arange(-4, 6, step=0.1) + my_sin
baseline = y - 1.0
- self.plot.addCurve(x=x, y=y, color='grey', legend='curve1', fill=True,
- baseline=baseline)
+ self.plot.addCurve(
+ x=x, y=y, color="grey", legend="curve1", fill=True, baseline=baseline
+ )
def testPlotBaselineScalar(self):
"""simple test of the API with baseline as an int"""
@@ -559,8 +611,9 @@ class TestPlotCurve(PlotWidgetTestCase):
my_sin = numpy.sin(x)
y = numpy.arange(-4, 6, step=0.1) + my_sin
- self.plot.addCurve(x=x, y=y, color='grey', legend='curve1', fill=True,
- baseline=0)
+ self.plot.addCurve(
+ x=x, y=y, color="grey", legend="curve1", fill=True, baseline=0
+ )
def testPlotBaselineList(self):
"""simple test of the API with baseline as an int"""
@@ -568,35 +621,70 @@ class TestPlotCurve(PlotWidgetTestCase):
my_sin = numpy.sin(x)
y = numpy.arange(-4, 6, step=0.1) + my_sin
- self.plot.addCurve(x=x, y=y, color='grey', legend='curve1', fill=True,
- baseline=list(range(0, 100, 1)))
+ self.plot.addCurve(
+ x=x,
+ y=y,
+ color="grey",
+ legend="curve1",
+ fill=True,
+ baseline=list(range(0, 100, 1)),
+ )
def testPlotCurveComplexData(self):
"""Test curve with complex data"""
- data = numpy.arange(100.) + 1j
+ data = numpy.arange(100.0) + 1j
self.plot.addCurve(x=data, y=data, xerror=data, yerror=data)
+ def testPlotCurveGapColor(self):
+ """Test dashed curve with gap color"""
+ data = numpy.arange(100)
+ self.plot.addCurve(
+ x=data, y=data, legend="curve1", linestyle="--", color="blue"
+ )
+ curve = self.plot.getCurve("curve1")
+ assert curve.getLineGapColor() is None
+ curve.setLineGapColor("red")
+ assert curve.getLineGapColor() == (1.0, 0.0, 0.0, 1.0)
+
class TestPlotHistogram(PlotWidgetTestCase):
"""Basic tests for add Histogram"""
+
def setUp(self):
super(TestPlotHistogram, self).setUp()
self.edges = numpy.arange(0, 10, step=1)
self.histogram = numpy.random.random(len(self.edges))
def testPlot(self):
- self.plot.addHistogram(histogram=self.histogram,
- edges=self.edges,
- legend='histogram1')
+ self.plot.addHistogram(
+ histogram=self.histogram, edges=self.edges, legend="histogram1"
+ )
def testPlotBaseline(self):
- self.plot.addHistogram(histogram=self.histogram,
- edges=self.edges,
- legend='histogram1',
- color='blue',
- baseline=-2,
- z=2,
- fill=True)
+ self.plot.addHistogram(
+ histogram=self.histogram,
+ edges=self.edges,
+ legend="histogram1",
+ color="blue",
+ baseline=-2,
+ z=2,
+ fill=True,
+ )
+
+ def testPlotGapColor(self):
+ """Test dashed histogram with gap color"""
+ data = numpy.arange(100)
+ self.plot.addHistogram(
+ histogram=self.histogram,
+ edges=self.edges,
+ legend="histogram1",
+ color="blue",
+ )
+ histogram = self.plot.getItems()[0]
+ assert histogram.getLineGapColor() is None
+ histogram.setLineGapColor("red")
+ assert histogram.getLineGapColor() == (1.0, 0.0, 0.0, 1.0)
+ histogram.setLineStyle(":")
class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
@@ -611,9 +699,8 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
def testScatterComplexData(self):
"""Test scatter item with complex data"""
- data = numpy.arange(100.) + 1j
- self.plot.addScatter(
- x=data, y=data, value=data, xerror=data, yerror=data)
+ data = numpy.arange(100.0) + 1j
+ self.plot.addScatter(x=data, y=data, value=data, xerror=data, yerror=data)
self.plot.resetZoom()
def testScatterVisualization(self):
@@ -623,16 +710,18 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
scatter = self.plot.getItems()[0]
- for visualization in ('solid',
- 'points',
- 'regular_grid',
- 'irregular_grid',
- 'binned_statistic',
- scatter.Visualization.SOLID,
- scatter.Visualization.POINTS,
- scatter.Visualization.REGULAR_GRID,
- scatter.Visualization.IRREGULAR_GRID,
- scatter.Visualization.BINNED_STATISTIC):
+ for visualization in (
+ "solid",
+ "points",
+ "regular_grid",
+ "irregular_grid",
+ "binned_statistic",
+ scatter.Visualization.SOLID,
+ scatter.Visualization.POINTS,
+ scatter.Visualization.REGULAR_GRID,
+ scatter.Visualization.IRREGULAR_GRID,
+ scatter.Visualization.BINNED_STATISTIC,
+ ):
with self.subTest(visualization=visualization):
scatter.setVisualization(visualization)
self.qapp.processEvents()
@@ -640,28 +729,30 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
def testGridVisualization(self):
"""Test regular and irregular grid mode with different points"""
points = { # name: (x, y, order)
- 'single point': ((1.,), (1.,), 'row'),
- 'horizontal line': ((0, 1, 2), (0, 0, 0), 'row'),
- 'horizontal line backward': ((2, 1, 0), (0, 0, 0), 'row'),
- 'vertical line': ((0, 0, 0), (0, 1, 2), 'row'),
- 'vertical line backward': ((0, 0, 0), (2, 1, 0), 'row'),
- 'grid fast x, +x +y': ((0, 1, 2, 0, 1, 2), (0, 0, 0, 1, 1, 1), 'row'),
- 'grid fast x, +x -y': ((0, 1, 2, 0, 1, 2), (1, 1, 1, 0, 0, 0), 'row'),
- 'grid fast x, -x -y': ((2, 1, 0, 2, 1, 0), (1, 1, 1, 0, 0, 0), 'row'),
- 'grid fast x, -x +y': ((2, 1, 0, 2, 1, 0), (0, 0, 0, 1, 1, 1), 'row'),
- 'grid fast y, +x +y': ((0, 0, 0, 1, 1, 1), (0, 1, 2, 0, 1, 2), 'column'),
- 'grid fast y, +x -y': ((0, 0, 0, 1, 1, 1), (2, 1, 0, 2, 1, 0), 'column'),
- 'grid fast y, -x -y': ((1, 1, 1, 0, 0, 0), (2, 1, 0, 2, 1, 0), 'column'),
- 'grid fast y, -x +y': ((1, 1, 1, 0, 0, 0), (0, 1, 2, 0, 1, 2), 'column'),
- }
+ "single point": ((1.0,), (1.0,), "row"),
+ "horizontal line": ((0, 1, 2), (0, 0, 0), "row"),
+ "horizontal line backward": ((2, 1, 0), (0, 0, 0), "row"),
+ "vertical line": ((0, 0, 0), (0, 1, 2), "row"),
+ "vertical line backward": ((0, 0, 0), (2, 1, 0), "row"),
+ "grid fast x, +x +y": ((0, 1, 2, 0, 1, 2), (0, 0, 0, 1, 1, 1), "row"),
+ "grid fast x, +x -y": ((0, 1, 2, 0, 1, 2), (1, 1, 1, 0, 0, 0), "row"),
+ "grid fast x, -x -y": ((2, 1, 0, 2, 1, 0), (1, 1, 1, 0, 0, 0), "row"),
+ "grid fast x, -x +y": ((2, 1, 0, 2, 1, 0), (0, 0, 0, 1, 1, 1), "row"),
+ "grid fast y, +x +y": ((0, 0, 0, 1, 1, 1), (0, 1, 2, 0, 1, 2), "column"),
+ "grid fast y, +x -y": ((0, 0, 0, 1, 1, 1), (2, 1, 0, 2, 1, 0), "column"),
+ "grid fast y, -x -y": ((1, 1, 1, 0, 0, 0), (2, 1, 0, 2, 1, 0), "column"),
+ "grid fast y, -x +y": ((1, 1, 1, 0, 0, 0), (0, 1, 2, 0, 1, 2), "column"),
+ }
self.plot.addScatter((), (), ())
scatter = self.plot.getItems()[0]
self.qapp.processEvents()
- for visualization in (scatter.Visualization.REGULAR_GRID,
- scatter.Visualization.IRREGULAR_GRID):
+ for visualization in (
+ scatter.Visualization.REGULAR_GRID,
+ scatter.Visualization.IRREGULAR_GRID,
+ ):
scatter.setVisualization(visualization)
self.assertIs(scatter.getVisualization(), visualization)
@@ -673,16 +764,19 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
order = scatter.getCurrentVisualizationParameter(
- scatter.VisualizationParameter.GRID_MAJOR_ORDER)
+ scatter.VisualizationParameter.GRID_MAJOR_ORDER
+ )
self.assertEqual(ref_order, order)
ref_bounds = (x[0], y[0]), (x[-1], y[-1])
bounds = scatter.getCurrentVisualizationParameter(
- scatter.VisualizationParameter.GRID_BOUNDS)
+ scatter.VisualizationParameter.GRID_BOUNDS
+ )
self.assertEqual(ref_bounds, bounds)
shape = scatter.getCurrentVisualizationParameter(
- scatter.VisualizationParameter.GRID_SHAPE)
+ scatter.VisualizationParameter.GRID_SHAPE
+ )
self.plot.getXAxis().setLimits(numpy.min(x) - 1, numpy.max(x) + 1)
self.plot.getYAxis().setLimits(numpy.min(y) - 1, numpy.max(y) + 1)
@@ -700,12 +794,15 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
self.plot.addScatter((), (), ())
scatter = self.plot.getItems()[0]
scatter.setVisualization(scatter.Visualization.BINNED_STATISTIC)
- self.assertIs(scatter.getVisualization(),
- scatter.Visualization.BINNED_STATISTIC)
+ self.assertIs(
+ scatter.getVisualization(), scatter.Visualization.BINNED_STATISTIC
+ )
self.assertEqual(
scatter.getVisualizationParameter(
- scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION),
- 'mean')
+ scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ),
+ "mean",
+ )
self.qapp.processEvents()
@@ -716,15 +813,17 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
scatter.setData(*numpy.random.random(3000).reshape(3, -1))
self.qapp.processEvents()
- for reduction in ('count', 'sum', 'mean'):
+ for reduction in ("count", "sum", "mean"):
with self.subTest(reduction=reduction):
scatter.setVisualizationParameter(
- scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION,
- reduction)
+ scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION, reduction
+ )
self.assertEqual(
scatter.getVisualizationParameter(
- scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION),
- reduction)
+ scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ),
+ reduction,
+ )
self.qapp.processEvents()
@@ -734,23 +833,23 @@ class TestPlotMarker(PlotWidgetTestCase):
def setUp(self):
super(TestPlotMarker, self).setUp()
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
+ self.plot.getYAxis().setLabel("Rows")
+ self.plot.getXAxis().setLabel("Columns")
self.plot.getXAxis().setAutoScale(False)
self.plot.getYAxis().setAutoScale(False)
self.plot.setKeepDataAspectRatio(False)
- self.plot.setLimits(0., 100., -100., 100.)
+ self.plot.setLimits(0.0, 100.0, -100.0, 100.0)
def testPlotMarkerX(self):
- self.plot.setGraphTitle('Markers X')
+ self.plot.setGraphTitle("Markers X")
markers = [
- (10., 'blue', False, False),
- (20., 'red', False, False),
- (40., 'green', True, False),
- (60., 'gray', True, True),
- (80., 'black', False, True),
+ (10.0, "blue", False, False),
+ (20.0, "red", False, False),
+ (40.0, "green", True, False),
+ (60.0, "gray", True, True),
+ (80.0, "black", False, True),
]
for x, color, select, drag in markers:
@@ -763,14 +862,14 @@ class TestPlotMarker(PlotWidgetTestCase):
self.plot.resetZoom()
def testPlotMarkerY(self):
- self.plot.setGraphTitle('Markers Y')
+ self.plot.setGraphTitle("Markers Y")
markers = [
- (-50., 'blue', False, False),
- (-30., 'red', False, False),
- (0., 'green', True, False),
- (10., 'gray', True, True),
- (80., 'black', False, True),
+ (-50.0, "blue", False, False),
+ (-30.0, "red", False, False),
+ (0.0, "green", True, False),
+ (10.0, "gray", True, True),
+ (80.0, "black", False, True),
]
for y, color, select, drag in markers:
@@ -783,14 +882,14 @@ class TestPlotMarker(PlotWidgetTestCase):
self.plot.resetZoom()
def testPlotMarkerPt(self):
- self.plot.setGraphTitle('Markers Pt')
+ self.plot.setGraphTitle("Markers Pt")
markers = [
- (10., -50., 'blue', False, False),
- (40., -30., 'red', False, False),
- (50., 0., 'green', True, False),
- (50., 20., 'gray', True, True),
- (70., 50., 'black', False, True),
+ (10.0, -50.0, "blue", False, False),
+ (40.0, -30.0, "red", False, False),
+ (50.0, 0.0, "green", True, False),
+ (50.0, 20.0, "gray", True, True),
+ (70.0, 50.0, "black", False, True),
]
for x, y, color, select, drag in markers:
name = "{0},{1}".format(x, y)
@@ -803,52 +902,45 @@ class TestPlotMarker(PlotWidgetTestCase):
self.plot.resetZoom()
def testPlotMarkerWithoutLegend(self):
- self.plot.setGraphTitle('Markers without legend')
+ self.plot.setGraphTitle("Markers without legend")
self.plot.getYAxis().setInverted(True)
# Markers without legend
self.plot.addMarker(10, 10)
self.plot.addMarker(10, 20)
- self.plot.addMarker(40, 50, text='test', symbol=None)
- self.plot.addMarker(40, 50, text='test', symbol='+')
+ self.plot.addMarker(40, 50, text="test", symbol=None)
+ self.plot.addMarker(40, 50, text="test", symbol="+")
self.plot.addXMarker(25)
self.plot.addXMarker(35)
- self.plot.addXMarker(45, text='test')
+ self.plot.addXMarker(45, text="test")
self.plot.addYMarker(55)
self.plot.addYMarker(65)
- self.plot.addYMarker(75, text='test')
+ self.plot.addYMarker(75, text="test")
self.plot.resetZoom()
def testPlotMarkerYAxis(self):
# Check only the API
- legend = self.plot.addMarker(10, 10)
- item = self.plot._getMarker(legend)
+ item = self.plot.addMarker(10, 10)
self.assertEqual(item.getYAxis(), "left")
- legend = self.plot.addMarker(10, 10, yaxis="right")
- item = self.plot._getMarker(legend)
+ item = self.plot.addMarker(10, 10, yaxis="right")
self.assertEqual(item.getYAxis(), "right")
- legend = self.plot.addMarker(10, 10, yaxis="left")
- item = self.plot._getMarker(legend)
+ item = self.plot.addMarker(10, 10, yaxis="left")
self.assertEqual(item.getYAxis(), "left")
- legend = self.plot.addXMarker(10, yaxis="right")
- item = self.plot._getMarker(legend)
+ item = self.plot.addXMarker(10, yaxis="right")
self.assertEqual(item.getYAxis(), "right")
- legend = self.plot.addXMarker(10, yaxis="left")
- item = self.plot._getMarker(legend)
+ item = self.plot.addXMarker(10, yaxis="left")
self.assertEqual(item.getYAxis(), "left")
- legend = self.plot.addYMarker(10, yaxis="right")
- item = self.plot._getMarker(legend)
+ item = self.plot.addYMarker(10, yaxis="right")
self.assertEqual(item.getYAxis(), "right")
- legend = self.plot.addYMarker(10, yaxis="left")
- item = self.plot._getMarker(legend)
+ item = self.plot.addYMarker(10, yaxis="left")
self.assertEqual(item.getYAxis(), "left")
self.plot.resetZoom()
@@ -856,39 +948,72 @@ class TestPlotMarker(PlotWidgetTestCase):
# TestPlotItem ################################################################
+
class TestPlotItem(PlotWidgetTestCase):
"""Basic tests for addItem."""
# Polygon coordinates and color
POLYGONS = [ # legend, x coords, y coords, color
- ('triangle', numpy.array((10, 30, 50)),
- numpy.array((55, 70, 55)), 'red'),
- ('square', numpy.array((10, 10, 50, 50)),
- numpy.array((10, 50, 50, 10)), 'green'),
- ('star', numpy.array((60, 70, 80, 60, 80)),
- numpy.array((25, 50, 25, 40, 40)), 'blue'),
- ('2 triangles-simple',
- numpy.array((90., 95., 100., numpy.nan, 90., 95., 100.)),
- numpy.array((25., 5., 25., numpy.nan, 30., 50., 30.)),
- 'pink'),
- ('2 triangles-extra NaN',
- numpy.array((numpy.nan, 90., 95., 100., numpy.nan, 0., 90., 95., 100., numpy.nan)),
- numpy.array((0., 55., 70., 55., numpy.nan, numpy.nan, 75., 90., 75., numpy.nan)),
- 'black'),
+ ("triangle", numpy.array((10, 30, 50)), numpy.array((55, 70, 55)), "red"),
+ (
+ "square",
+ numpy.array((10, 10, 50, 50)),
+ numpy.array((10, 50, 50, 10)),
+ "green",
+ ),
+ (
+ "star",
+ numpy.array((60, 70, 80, 60, 80)),
+ numpy.array((25, 50, 25, 40, 40)),
+ "blue",
+ ),
+ (
+ "2 triangles-simple",
+ numpy.array((90.0, 95.0, 100.0, numpy.nan, 90.0, 95.0, 100.0)),
+ numpy.array((25.0, 5.0, 25.0, numpy.nan, 30.0, 50.0, 30.0)),
+ "pink",
+ ),
+ (
+ "2 triangles-extra NaN",
+ numpy.array(
+ (
+ numpy.nan,
+ 90.0,
+ 95.0,
+ 100.0,
+ numpy.nan,
+ 0.0,
+ 90.0,
+ 95.0,
+ 100.0,
+ numpy.nan,
+ )
+ ),
+ numpy.array(
+ (
+ 0.0,
+ 55.0,
+ 70.0,
+ 55.0,
+ numpy.nan,
+ numpy.nan,
+ 75.0,
+ 90.0,
+ 75.0,
+ numpy.nan,
+ )
+ ),
+ "black",
+ ),
]
# Rectangle coordinantes and color
RECTANGLES = [ # legend, x coords, y coords, color
- ('square 1', numpy.array((1., 10.)),
- numpy.array((1., 10.)), 'red'),
- ('square 2', numpy.array((10., 20.)),
- numpy.array((10., 20.)), 'green'),
- ('square 3', numpy.array((20., 30.)),
- numpy.array((20., 30.)), 'blue'),
- ('rect 1', numpy.array((1., 30.)),
- numpy.array((35., 40.)), 'black'),
- ('line h', numpy.array((1., 30.)),
- numpy.array((45., 45.)), 'darkRed'),
+ ("square 1", numpy.array((1.0, 10.0)), numpy.array((1.0, 10.0)), "red"),
+ ("square 2", numpy.array((10.0, 20.0)), numpy.array((10.0, 20.0)), "green"),
+ ("square 3", numpy.array((20.0, 30.0)), numpy.array((20.0, 30.0)), "blue"),
+ ("rect 1", numpy.array((1.0, 30.0)), numpy.array((35.0, 40.0)), "black"),
+ ("line h", numpy.array((1.0, 30.0)), numpy.array((45.0, 45.0)), "darkRed"),
]
SCALES = Axis.LINEAR, Axis.LOGARITHMIC
@@ -896,12 +1021,12 @@ class TestPlotItem(PlotWidgetTestCase):
def setUp(self):
super(TestPlotItem, self).setUp()
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
+ self.plot.getYAxis().setLabel("Rows")
+ self.plot.getXAxis().setLabel("Columns")
self.plot.getXAxis().setAutoScale(False)
self.plot.getYAxis().setAutoScale(False)
self.plot.setKeepDataAspectRatio(False)
- self.plot.setLimits(0., 100., -100., 100.)
+ self.plot.setLimits(0.0, 100.0, -100.0, 100.0)
def testPlotItemPolygonFill(self):
for scale in self.SCALES:
@@ -909,12 +1034,19 @@ class TestPlotItem(PlotWidgetTestCase):
self.plot.clear()
self.plot.getXAxis().setScale(scale)
self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Item Fill %s' % scale)
+ self.plot.setGraphTitle("Item Fill %s" % scale)
for legend, xList, yList, color in self.POLYGONS:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False, linestyle='--',
- shape="polygon", fill=True, color=color)
+ self.plot.addShape(
+ xList,
+ yList,
+ legend=legend,
+ replace=False,
+ linestyle="--",
+ shape="polygon",
+ fill=True,
+ color=color,
+ )
self.plot.resetZoom()
def testPlotItemPolygonNoFill(self):
@@ -923,12 +1055,19 @@ class TestPlotItem(PlotWidgetTestCase):
self.plot.clear()
self.plot.getXAxis().setScale(scale)
self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Item No Fill %s' % scale)
+ self.plot.setGraphTitle("Item No Fill %s" % scale)
for legend, xList, yList, color in self.POLYGONS:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False, linestyle='--',
- shape="polygon", fill=False, color=color)
+ self.plot.addShape(
+ xList,
+ yList,
+ legend=legend,
+ replace=False,
+ linestyle="--",
+ shape="polygon",
+ fill=False,
+ color=color,
+ )
self.plot.resetZoom()
def testPlotItemRectangleFill(self):
@@ -937,12 +1076,18 @@ class TestPlotItem(PlotWidgetTestCase):
self.plot.clear()
self.plot.getXAxis().setScale(scale)
self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Rectangle Fill %s' % scale)
+ self.plot.setGraphTitle("Rectangle Fill %s" % scale)
for legend, xList, yList, color in self.RECTANGLES:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False,
- shape="rectangle", fill=True, color=color)
+ self.plot.addShape(
+ xList,
+ yList,
+ legend=legend,
+ replace=False,
+ shape="rectangle",
+ fill=True,
+ color=color,
+ )
self.plot.resetZoom()
def testPlotItemRectangleNoFill(self):
@@ -951,230 +1096,44 @@ class TestPlotItem(PlotWidgetTestCase):
self.plot.clear()
self.plot.getXAxis().setScale(scale)
self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Rectangle No Fill %s' % scale)
+ self.plot.setGraphTitle("Rectangle No Fill %s" % scale)
for legend, xList, yList, color in self.RECTANGLES:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False,
- shape="rectangle", fill=False, color=color)
+ self.plot.addShape(
+ xList,
+ yList,
+ legend=legend,
+ replace=False,
+ shape="rectangle",
+ fill=False,
+ color=color,
+ )
self.plot.resetZoom()
-class TestPlotActiveCurveImage(PlotWidgetTestCase):
- """Basic tests for active curve and image handling"""
- xData = numpy.arange(1000)
- yData = -500 + 100 * numpy.sin(xData)
- xData2 = xData + 1000
- yData2 = xData - 1000 + 200 * numpy.random.random(1000)
-
- def tearDown(self):
- self.plot.setActiveCurveHandling(False)
- super(TestPlotActiveCurveImage, self).tearDown()
-
- def testActiveCurveAndLabels(self):
- # Active curve handling off, no label change
- self.plot.setActiveCurveHandling(False)
- self.plot.getXAxis().setLabel('XLabel')
- self.plot.getYAxis().setLabel('YLabel')
- self.plot.addCurve((1, 2), (1, 2))
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.addCurve((1, 2), (2, 3), xlabel='x1', ylabel='y1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.clear()
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- # Active curve handling on, label changes
- self.plot.setActiveCurveHandling(True)
- self.plot.getXAxis().setLabel('XLabel')
- self.plot.getYAxis().setLabel('YLabel')
-
- # labels changed as active curve
- self.plot.addCurve((1, 2), (1, 2), legend='1',
- xlabel='x1', ylabel='y1')
- self.plot.setActiveCurve('1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels not changed as not active curve
- self.plot.addCurve((1, 2), (2, 3), legend='2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels changed
- self.plot.setActiveCurve('2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.setActiveCurve('1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- self.plot.clear()
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- def testPlotActiveCurveSelectionMode(self):
- self.plot.clear()
- self.plot.setActiveCurveHandling(True)
- legend = "curve 1"
- self.plot.addCurve(self.xData, self.yData,
- legend=legend,
- color="green")
-
- # active curve should be None
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
-
- # active curve should be None when None is set as active curve
- self.plot.setActiveCurve(legend)
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, legend)
- self.plot.setActiveCurve(None)
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, None)
-
- # testing it automatically toggles if there is only one
- self.plot.setActiveCurveSelectionMode("legacy")
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, legend)
-
- # active curve should not change when None set as active curve
- self.assertEqual(self.plot.getActiveCurveSelectionMode(), "legacy")
- self.plot.setActiveCurve(None)
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, legend)
-
- # situation where no curve is active
- self.plot.clear()
- self.plot.setActiveCurveHandling(True)
- self.assertEqual(self.plot.getActiveCurveSelectionMode(), "atmostone")
- self.plot.addCurve(self.xData, self.yData,
- legend=legend,
- color="green")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- color="red")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
- self.plot.setActiveCurveSelectionMode("legacy")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
-
- # the first curve added should be active
- self.plot.clear()
- self.plot.addCurve(self.xData, self.yData,
- legend=legend,
- color="green")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), legend)
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- color="red")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), legend)
-
- def testActiveCurveStyle(self):
- """Test change of active curve style"""
- self.plot.setActiveCurveHandling(True)
- self.plot.setActiveCurveStyle(color='black')
- style = self.plot.getActiveCurveStyle()
- self.assertEqual(style.getColor(), (0., 0., 0., 1.))
- self.assertIsNone(style.getLineStyle())
- self.assertIsNone(style.getLineWidth())
- self.assertIsNone(style.getSymbol())
- self.assertIsNone(style.getSymbolSize())
-
- self.plot.addCurve(x=self.xData, y=self.yData, legend="curve1")
- curve = self.plot.getCurve("curve1")
- curve.setColor('blue')
- curve.setLineStyle('-')
- curve.setLineWidth(1)
- curve.setSymbol('o')
- curve.setSymbolSize(5)
-
- # Check default current style
- defaultStyle = curve.getCurrentStyle()
- self.assertEqual(defaultStyle, CurveStyle(color='blue',
- linestyle='-',
- linewidth=1,
- symbol='o',
- symbolsize=5))
-
- # Activate curve with highlight color=black
- self.plot.setActiveCurve("curve1")
- style = curve.getCurrentStyle()
- self.assertEqual(style.getColor(), (0., 0., 0., 1.))
- self.assertEqual(style.getLineStyle(), '-')
- self.assertEqual(style.getLineWidth(), 1)
- self.assertEqual(style.getSymbol(), 'o')
- self.assertEqual(style.getSymbolSize(), 5)
-
- # Change highlight to linewidth=2
- self.plot.setActiveCurveStyle(linewidth=2)
- style = curve.getCurrentStyle()
- self.assertEqual(style.getColor(), (0., 0., 1., 1.))
- self.assertEqual(style.getLineStyle(), '-')
- self.assertEqual(style.getLineWidth(), 2)
- self.assertEqual(style.getSymbol(), 'o')
- self.assertEqual(style.getSymbolSize(), 5)
-
- self.plot.setActiveCurve(None)
- self.assertEqual(curve.getCurrentStyle(), defaultStyle)
-
- def testActiveImageAndLabels(self):
- # Active image handling always on, no API for toggling it
- self.plot.getXAxis().setLabel('XLabel')
- self.plot.getYAxis().setLabel('YLabel')
-
- # labels changed as active curve
- self.plot.addImage(numpy.arange(100).reshape(10, 10),
- legend='1', xlabel='x1', ylabel='y1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels not changed as not active curve
- self.plot.addImage(numpy.arange(100).reshape(10, 10),
- legend='2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels changed
- self.plot.setActiveImage('2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.setActiveImage('1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- self.plot.clear()
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
-
##############################################################################
# Log
##############################################################################
+
class TestPlotEmptyLog(PlotWidgetTestCase):
"""Basic tests for log plot"""
+
def testEmptyPlotTitleLabelsLog(self):
- self.plot.setGraphTitle('Empty Log Log')
- self.plot.getXAxis().setLabel('X')
- self.plot.getYAxis().setLabel('Y')
+ self.plot.setGraphTitle("Empty Log Log")
+ self.plot.getXAxis().setLabel("X")
+ self.plot.getYAxis().setLabel("Y")
self.plot.getXAxis()._setLogarithmic(True)
self.plot.getYAxis()._setLogarithmic(True)
self.plot.resetZoom()
class TestPlotAxes(TestCaseQt, ParametricTestCase):
-
# Test data
xData = numpy.arange(1, 10)
- yData = xData ** 2
+ yData = xData**2
- def __init__(self, methodName='runTest', backend=None):
+ def __init__(self, methodName="runTest", backend=None):
unittest.TestCase.__init__(self, methodName)
self.__backend = backend
@@ -1234,7 +1193,7 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
with self.subTest():
if setter is not None:
if not isinstance(value, tuple):
- value = (value, )
+ value = (value,)
setter(*value)
if getter is not None:
self.assertEqual(getter(), expected)
@@ -1324,22 +1283,34 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.assertEqual(self.plot.isYAxisInverted(), False)
def testLogXWithData(self):
- self.plot.setGraphTitle('Curve X: Log Y: Linear')
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.setGraphTitle("Curve X: Log Y: Linear")
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
axis = self.plot.getXAxis()
axis.setScale(axis.LOGARITHMIC)
self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
def testLogYWithData(self):
- self.plot.setGraphTitle('Curve X: Linear Y: Log')
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.setGraphTitle("Curve X: Linear Y: Log")
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
axis = self.plot.getYAxis()
axis.setScale(axis.LOGARITHMIC)
@@ -1348,11 +1319,17 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
def testLogYRightWithData(self):
- self.plot.setGraphTitle('Curve X: Linear Y: Log')
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.setGraphTitle("Curve X: Linear Y: Log")
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
axis = self.plot.getYAxis(axis="right")
axis.setScale(axis.LOGARITHMIC)
@@ -1361,36 +1338,58 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
def testLimitsChanged_setLimits(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
listener = SignalListener()
self.plot.getXAxis().sigLimitsChanged.connect(listener.partial(axis="x"))
self.plot.getYAxis().sigLimitsChanged.connect(listener.partial(axis="y"))
- self.plot.getYAxis(axis="right").sigLimitsChanged.connect(listener.partial(axis="y2"))
+ self.plot.getYAxis(axis="right").sigLimitsChanged.connect(
+ listener.partial(axis="y2")
+ )
self.plot.setLimits(0, 1, 0, 1, 0, 1)
# at least one event per axis
self.assertEqual(len(set(listener.karguments(argumentName="axis"))), 3)
def testLimitsChanged_resetZoom(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
listener = SignalListener()
self.plot.getXAxis().sigLimitsChanged.connect(listener.partial(axis="x"))
self.plot.getYAxis().sigLimitsChanged.connect(listener.partial(axis="y"))
- self.plot.getYAxis(axis="right").sigLimitsChanged.connect(listener.partial(axis="y2"))
+ self.plot.getYAxis(axis="right").sigLimitsChanged.connect(
+ listener.partial(axis="y2")
+ )
self.plot.resetZoom()
# at least one event per axis
self.assertEqual(len(set(listener.karguments(argumentName="axis"))), 3)
def testLimitsChanged_setXLimit(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
listener = SignalListener()
axis = self.plot.getXAxis()
axis.sigLimitsChanged.connect(listener)
@@ -1400,10 +1399,16 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.assertEqual(axis.getLimits(), (20.0, 30.0))
def testLimitsChanged_setYLimit(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
listener = SignalListener()
axis = self.plot.getYAxis()
axis.sigLimitsChanged.connect(listener)
@@ -1413,10 +1418,16 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.assertEqual(axis.getLimits(), (20.0, 30.0))
def testLimitsChanged_setYRightLimit(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
listener = SignalListener()
axis = self.plot.getYAxis(axis="right")
axis.sigLimitsChanged.connect(listener)
@@ -1481,9 +1492,9 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.qWaitForWindowExposed(self.plot)
margins = self.plot.getAxesMargins()
- self.assertEqual(margins, (.15, .1, .1, .15))
+ self.assertEqual(margins, (0.15, 0.1, 0.1, 0.15))
- for margins in ((0., 0., 0., 0.), (.15, .1, .1, .15)):
+ for margins in ((0.0, 0.0, 0.0, 0.0), (0.15, 0.1, 0.1, 0.15)):
with self.subTest(margins=margins):
self.plot.setAxesMargins(*margins)
self.qapp.processEvents()
@@ -1538,18 +1549,21 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
def testAxisExtent(self):
"""Test XAxisExtent and yAxisExtent"""
- for cls, axis in ((XAxisExtent, self.plot.getXAxis()),
- (YAxisExtent, self.plot.getYAxis())):
- for range_, logRange in (((2, 3), (2, 3)),
- ((-2, -1), (1, 100)),
- ((-1, 3), (3. * 0.9, 3. * 1.1))):
+ for cls, axis in (
+ (XAxisExtent, self.plot.getXAxis()),
+ (YAxisExtent, self.plot.getYAxis()),
+ ):
+ for range_, logRange in (
+ ((2, 3), (2, 3)),
+ ((-2, -1), (1, 100)),
+ ((-1, 3), (3.0 * 0.9, 3.0 * 1.1)),
+ ):
extent = cls()
extent.setRange(*range_)
self.plot.addItem(extent)
for isLog, plotRange in ((False, range_), (True, logRange)):
- with self.subTest(
- cls=cls.__name__, range=range_, isLog=isLog):
+ with self.subTest(cls=cls.__name__, range=range_, isLog=isLog):
axis._setLogarithmic(isLog)
self.plot.resetZoom()
self.qapp.processEvents()
@@ -1564,9 +1578,7 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
for scale in ("linear", "log"):
xaxis.setScale(scale)
yaxis.setScale(scale)
- for limits in ((1e300, 1e308),
- (-1e308, 1e308),
- (1e-300, 2e-300)):
+ for limits in ((1e300, 1e308), (-1e308, 1e308), (1e-300, 2e-300)):
with self.subTest(scale=scale, limits=limits):
xaxis.setLimits(*limits)
self.qapp.processEvents()
@@ -1581,44 +1593,62 @@ class TestPlotCurveLog(PlotWidgetTestCase, ParametricTestCase):
# Test data
xData = numpy.arange(1000) + 1
- yData = xData ** 2
+ yData = xData**2
def _setLabels(self):
- self.plot.getXAxis().setLabel('X')
- self.plot.getYAxis().setLabel('X * X')
+ self.plot.getXAxis().setLabel("X")
+ self.plot.getYAxis().setLabel("X * X")
def testPlotCurveLogX(self):
self._setLabels()
self.plot.getXAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('Curve X: Log Y: Linear')
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.setGraphTitle("Curve X: Log Y: Linear")
+
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
def testPlotCurveLogY(self):
self._setLabels()
self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('Curve X: Linear Y: Log')
+ self.plot.setGraphTitle("Curve X: Linear Y: Log")
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
def testPlotCurveLogXY(self):
self._setLabels()
self.plot.getXAxis()._setLogarithmic(True)
self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('Curve X: Log Y: Log')
+ self.plot.setGraphTitle("Curve X: Log Y: Log")
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
def testPlotCurveErrorLogXY(self):
self.plot.getXAxis()._setLogarithmic(True)
@@ -1629,24 +1659,31 @@ class TestPlotCurveLog(PlotWidgetTestCase, ParametricTestCase):
errors[::2] = self.xData[::2] + 1
tests = [ # name, xerror, yerror
- ('xerror=3', 3, None),
- ('xerror=N array', errors, None),
- ('xerror=Nx1 array', errors.reshape(len(errors), 1), None),
- ('xerror=2xN array', numpy.array((errors, errors)), None),
- ('yerror=6', None, 6),
- ('yerror=N array', None, errors ** 2),
- ('yerror=Nx1 array', None, (errors ** 2).reshape(len(errors), 1)),
- ('yerror=2xN array', None, numpy.array((errors, errors)) ** 2),
+ ("xerror=3", 3, None),
+ ("xerror=N array", errors, None),
+ ("xerror=Nx1 array", errors.reshape(len(errors), 1), None),
+ ("xerror=2xN array", numpy.array((errors, errors)), None),
+ ("yerror=6", None, 6),
+ ("yerror=N array", None, errors**2),
+ ("yerror=Nx1 array", None, (errors**2).reshape(len(errors), 1)),
+ ("yerror=2xN array", None, numpy.array((errors, errors)) ** 2),
]
for name, xError, yError in tests:
with self.subTest(name):
self.plot.setGraphTitle(name)
- self.plot.addCurve(self.xData, self.yData,
- legend=name,
- xerror=xError, yerror=yError,
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend=name,
+ xerror=xError,
+ yerror=yError,
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
self.qapp.processEvents()
@@ -1678,12 +1715,12 @@ class TestPlotCurveLog(PlotWidgetTestCase, ParametricTestCase):
"""Add a curve with negative data and toggle log axis"""
arange = numpy.arange(1000) + 1
tests = [ # name, xData, yData
- ('x>0, some negative y', arange, arange - 500),
- ('x>0, y<0', arange, -arange),
- ('some negative x, y>0', arange - 500, arange),
- ('x<0, y>0', -arange, arange),
- ('some negative x and y', arange - 500, arange - 500),
- ('x<0, y<0', -arange, -arange),
+ ("x>0, some negative y", arange, arange - 500),
+ ("x>0, y<0", arange, -arange),
+ ("some negative x, y>0", arange - 500, arange),
+ ("x<0, y>0", -arange, arange),
+ ("some negative x and y", arange - 500, arange - 500),
+ ("x<0, y<0", -arange, -arange),
]
for name, xData, yData in tests:
@@ -1705,54 +1742,65 @@ class TestPlotCurveLog(PlotWidgetTestCase, ParametricTestCase):
yLim = self.plot.getYAxis().getLimits()
positives = xData > 0
if numpy.any(positives):
- self.assertTrue(numpy.allclose(
- xLim, (min(xData[positives]), max(xData[positives]))))
- self.assertEqual(
- yLim, (min(yData[positives]), max(yData[positives])))
+ self.assertTrue(
+ numpy.allclose(
+ xLim, (min(xData[positives]), max(xData[positives]))
+ )
+ )
else: # No positive x in the curve
- self.assertEqual(xLim, (1., 100.))
- self.assertEqual(yLim, (1., 100.))
+ self.assertEqual(xLim, (1.0, 100.0))
+ self.assertEqual(yLim, (min(yData), max(yData)))
# x axis and y axis log
+ previousXLim = self.plot.getXAxis().getLimits()
+ previousYLim = self.plot.getYAxis().getLimits()
self.plot.getYAxis()._setLogarithmic(True)
self.qapp.processEvents()
xLim = self.plot.getXAxis().getLimits()
yLim = self.plot.getYAxis().getLimits()
+
+ self.assertEqual(xLim, previousXLim)
positives = numpy.logical_and(xData > 0, yData > 0)
- if numpy.any(positives):
- self.assertTrue(numpy.allclose(
- xLim, (min(xData[positives]), max(xData[positives]))))
- self.assertTrue(numpy.allclose(
- yLim, (min(yData[positives]), max(yData[positives]))))
+ if previousYLim[0] > 0:
+ self.assertEqual(yLim, previousYLim)
+ elif numpy.any(positives):
+ expectedLimits = min(yData[positives]), max(yData[positives])
+ self.assertTrue(
+ numpy.allclose(yLim, expectedLimits),
+ f"{yLim} != {expectedLimits}",
+ )
else: # No positive x and y in the curve
- self.assertEqual(xLim, (1., 100.))
- self.assertEqual(yLim, (1., 100.))
+ self.assertEqual(yLim, (1.0, 100.0))
# y axis log
+ previousXLim = self.plot.getXAxis().getLimits()
self.plot.getXAxis()._setLogarithmic(False)
self.qapp.processEvents()
xLim = self.plot.getXAxis().getLimits()
yLim = self.plot.getYAxis().getLimits()
+ self.assertEqual(xLim, previousXLim)
positives = yData > 0
if numpy.any(positives):
- self.assertEqual(
- xLim, (min(xData[positives]), max(xData[positives])))
- self.assertTrue(numpy.allclose(
- yLim, (min(yData[positives]), max(yData[positives]))))
+ self.assertTrue(
+ numpy.allclose(
+ yLim, (min(yData[positives]), max(yData[positives]))
+ )
+ )
else: # No positive y in the curve
- self.assertEqual(xLim, (1., 100.))
- self.assertEqual(yLim, (1., 100.))
+ self.assertEqual(yLim, (1.0, 100.0))
# no log axis
+ previousXLim = self.plot.getXAxis().getLimits()
+ previousYLim = self.plot.getYAxis().getLimits()
self.plot.getYAxis()._setLogarithmic(False)
self.qapp.processEvents()
xLim = self.plot.getXAxis().getLimits()
- self.assertEqual(xLim, (min(xData), max(xData)))
+ self.assertEqual(xLim, previousXLim)
yLim = self.plot.getYAxis().getLimits()
- self.assertEqual(yLim, (min(yData), max(yData)))
+ self.assertEqual(yLim, previousYLim)
self.plot.clear()
self.plot.resetZoom()
@@ -1765,71 +1813,83 @@ class TestPlotImageLog(PlotWidgetTestCase):
def setUp(self):
super(TestPlotImageLog, self).setUp()
- self.plot.getXAxis().setLabel('Columns')
- self.plot.getYAxis().setLabel('Rows')
+ self.plot.getXAxis().setLabel("Columns")
+ self.plot.getYAxis().setLabel("Rows")
def testPlotColormapGrayLogX(self):
self.plot.getXAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('CMap X: Log Y: Linear')
-
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1",
- origin=(1., 1.), scale=(1., 1.),
- resetzoom=False, colormap=colormap)
+ self.plot.setGraphTitle("CMap X: Log Y: Linear")
+
+ colormap = Colormap(name="gray", normalization="linear", vmin=None, vmax=None)
+ self.plot.addImage(
+ DATA_2D,
+ legend="image 1",
+ origin=(1.0, 1.0),
+ scale=(1.0, 1.0),
+ resetzoom=False,
+ colormap=colormap,
+ )
self.plot.resetZoom()
def testPlotColormapGrayLogY(self):
self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('CMap X: Linear Y: Log')
-
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1",
- origin=(1., 1.), scale=(1., 1.),
- resetzoom=False, colormap=colormap)
+ self.plot.setGraphTitle("CMap X: Linear Y: Log")
+
+ colormap = Colormap(name="gray", normalization="linear", vmin=None, vmax=None)
+ self.plot.addImage(
+ DATA_2D,
+ legend="image 1",
+ origin=(1.0, 1.0),
+ scale=(1.0, 1.0),
+ resetzoom=False,
+ colormap=colormap,
+ )
self.plot.resetZoom()
def testPlotColormapGrayLogXY(self):
self.plot.getXAxis()._setLogarithmic(True)
self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('CMap X: Log Y: Log')
-
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1",
- origin=(1., 1.), scale=(1., 1.),
- resetzoom=False, colormap=colormap)
+ self.plot.setGraphTitle("CMap X: Log Y: Log")
+
+ colormap = Colormap(name="gray", normalization="linear", vmin=None, vmax=None)
+ self.plot.addImage(
+ DATA_2D,
+ legend="image 1",
+ origin=(1.0, 1.0),
+ scale=(1.0, 1.0),
+ resetzoom=False,
+ colormap=colormap,
+ )
self.plot.resetZoom()
def testPlotRgbRgbaLogXY(self):
self.plot.getXAxis()._setLogarithmic(True)
self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('RGB + RGBA X: Log Y: Log')
+ self.plot.setGraphTitle("RGB + RGBA X: Log Y: Log")
rgb = numpy.array(
- (((0, 0, 0), (128, 0, 0), (255, 0, 0)),
- ((0, 128, 0), (0, 128, 128), (0, 128, 256))),
- dtype=numpy.uint8)
+ (
+ ((0, 0, 0), (128, 0, 0), (255, 0, 0)),
+ ((0, 128, 0), (0, 128, 128), (0, 128, 255)),
+ ),
+ dtype=numpy.uint8,
+ )
- self.plot.addImage(rgb, legend="rgb",
- origin=(1, 1), scale=(10, 10),
- resetzoom=False)
+ self.plot.addImage(
+ rgb, legend="rgb", origin=(1, 1), scale=(10, 10), resetzoom=False
+ )
rgba = numpy.array(
- (((0, 0, 0, .5), (.5, 0, 0, 1), (1, 0, 0, .5)),
- ((0, .5, 0, 1), (0, .5, .5, 1), (0, 1, 1, .5))),
- dtype=numpy.float32)
-
- self.plot.addImage(rgba, legend="rgba",
- origin=(5., 5.), scale=(10., 10.),
- resetzoom=False)
+ (
+ ((0, 0, 0, 0.5), (0.5, 0, 0, 1), (1, 0, 0, 0.5)),
+ ((0, 0.5, 0, 1), (0, 0.5, 0.5, 1), (0, 1, 1, 0.5)),
+ ),
+ dtype=numpy.float32,
+ )
+
+ self.plot.addImage(
+ rgba, legend="rgba", origin=(5.0, 5.0), scale=(10.0, 10.0), resetzoom=False
+ )
self.plot.resetZoom()
@@ -1838,27 +1898,27 @@ class TestPlotMarkerLog(PlotWidgetTestCase):
# Test marker parameters
markers = [ # x, y, color, selectable, draggable
- (10., 10., 'blue', False, False),
- (20., 20., 'red', False, False),
- (40., 100., 'green', True, False),
- (40., 500., 'gray', True, True),
- (60., 800., 'black', False, True),
+ (10.0, 10.0, "blue", False, False),
+ (20.0, 20.0, "red", False, False),
+ (40.0, 100.0, "green", True, False),
+ (40.0, 500.0, "gray", True, True),
+ (60.0, 800.0, "black", False, True),
]
def setUp(self):
super(TestPlotMarkerLog, self).setUp()
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
+ self.plot.getYAxis().setLabel("Rows")
+ self.plot.getXAxis().setLabel("Columns")
self.plot.getXAxis().setAutoScale(False)
self.plot.getYAxis().setAutoScale(False)
self.plot.setKeepDataAspectRatio(False)
- self.plot.setLimits(1., 100., 1., 1000.)
+ self.plot.setLimits(1.0, 100.0, 1.0, 1000.0)
self.plot.getXAxis()._setLogarithmic(True)
self.plot.getYAxis()._setLogarithmic(True)
def testPlotMarkerXLog(self):
- self.plot.setGraphTitle('Markers X, Log axes')
+ self.plot.setGraphTitle("Markers X, Log axes")
for x, _, color, select, drag in self.markers:
name = str(x)
@@ -1870,7 +1930,7 @@ class TestPlotMarkerLog(PlotWidgetTestCase):
self.plot.resetZoom()
def testPlotMarkerYLog(self):
- self.plot.setGraphTitle('Markers Y, Log axes')
+ self.plot.setGraphTitle("Markers Y, Log axes")
for _, y, color, select, drag in self.markers:
name = str(y)
@@ -1882,7 +1942,7 @@ class TestPlotMarkerLog(PlotWidgetTestCase):
self.plot.resetZoom()
def testPlotMarkerPtLog(self):
- self.plot.setGraphTitle('Markers Pt, Log axes')
+ self.plot.setGraphTitle("Markers Pt, Log axes")
for x, y, color, select, drag in self.markers:
name = "{0},{1}".format(x, y)
@@ -1901,9 +1961,9 @@ class TestPlotWidgetSwitchBackend(PlotWidgetTestCase):
@pytest.mark.usefixtures("test_options")
def testSwitchBackend(self):
"""Test switching a plot with a few items"""
- backends = {'none': 'BackendBase', 'mpl': 'BackendMatplotlibQt'}
+ backends = {"none": "BackendBase", "mpl": "BackendMatplotlibQt"}
if self.test_options.WITH_GL_TEST:
- backends['gl'] = 'BackendOpenGL'
+ backends["gl"] = "BackendOpenGL"
self.plot.addImage(numpy.arange(100).reshape(10, 10))
self.plot.addCurve((-3, -2, -1), (1, 2, 3))
@@ -1925,208 +1985,65 @@ class TestPlotWidgetSwitchBackend(PlotWidgetTestCase):
self.assertEqual(self.plot.getItems(), items)
-class TestPlotWidgetSelection(PlotWidgetTestCase):
- """Test PlotWidget.selection and active items handling"""
-
- def _checkSelection(self, selection, current=None, selected=()):
- """Check current item and selected items."""
- self.assertIs(selection.getCurrentItem(), current)
- self.assertEqual(selection.getSelectedItems(), selected)
-
- def testSyncWithActiveItems(self):
- """Test update of PlotWidgetSelection according to active items"""
- listener = SignalListener()
-
- selection = self.plot.selection()
- selection.sigCurrentItemChanged.connect(listener)
- self._checkSelection(selection)
-
- # Active item is current
- self.plot.addImage(((0, 1), (2, 3)), legend='image')
- image = self.plot.getActiveImage()
- self.assertEqual(listener.callCount(), 1)
- self._checkSelection(selection, image, (image,))
-
- # No active = no current
- self.plot.setActiveImage(None)
- self.assertEqual(listener.callCount(), 2)
- self._checkSelection(selection)
-
- # Active item is current
- self.plot.setActiveImage('image')
- self.assertEqual(listener.callCount(), 3)
- self._checkSelection(selection, image, (image,))
-
- # Mosted recently "actived" item is current
- self.plot.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend='scatter')
- scatter = self.plot.getActiveScatter()
- self.assertEqual(listener.callCount(), 4)
- self._checkSelection(selection, scatter, (scatter, image))
-
- # Previously mosted recently "actived" item is current
- self.plot.setActiveScatter(None)
- self.assertEqual(listener.callCount(), 5)
- self._checkSelection(selection, image, (image,))
-
- # Mosted recently "actived" item is current
- self.plot.setActiveScatter('scatter')
- self.assertEqual(listener.callCount(), 6)
- self._checkSelection(selection, scatter, (scatter, image))
-
- # No active = no current
- self.plot.setActiveImage(None)
- self.plot.setActiveScatter(None)
- self.assertEqual(listener.callCount(), 7)
- self._checkSelection(selection)
-
- # Mosted recently "actived" item is current
- self.plot.setActiveScatter('scatter')
- self.assertEqual(listener.callCount(), 8)
- self.plot.setActiveImage('image')
- self.assertEqual(listener.callCount(), 9)
- self._checkSelection(selection, image, (image, scatter))
-
- # Add a curve which is not active by default
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve')
- curve = self.plot.getCurve('curve')
- self.assertEqual(listener.callCount(), 9)
- self._checkSelection(selection, image, (image, scatter))
-
- # Mosted recently "actived" item is current
- self.plot.setActiveCurve('curve')
- self.assertEqual(listener.callCount(), 10)
- self._checkSelection(selection, curve, (curve, image, scatter))
-
- # Add a curve which is not active by default
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve2')
- curve2 = self.plot.getCurve('curve2')
- self.assertEqual(listener.callCount(), 10)
- self._checkSelection(selection, curve, (curve, image, scatter))
-
- # Mosted recently "actived" item is current, previous curve is removed
- self.plot.setActiveCurve('curve2')
- self.assertEqual(listener.callCount(), 11)
- self._checkSelection(selection, curve2, (curve2, image, scatter))
-
- # No items = no current
- self.plot.clear()
- self.assertEqual(listener.callCount(), 12)
- self._checkSelection(selection)
-
- def testPlotWidgetWithItems(self):
- """Test init of selection on a plot with items"""
- self.plot.addImage(((0, 1), (2, 3)), legend='image')
- self.plot.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend='scatter')
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve')
- self.plot.setActiveCurve('curve')
-
- selection = self.plot.selection()
- self.assertIsNotNone(selection.getCurrentItem())
- selected = selection.getSelectedItems()
- self.assertEqual(len(selected), 3)
- self.assertIn(self.plot.getActiveCurve(), selected)
- self.assertIn(self.plot.getActiveImage(), selected)
- self.assertIn(self.plot.getActiveScatter(), selected)
-
- def testSetCurrentItem(self):
- """Test setCurrentItem"""
- # Add items to the plot
- self.plot.addImage(((0, 1), (2, 3)), legend='image')
- image = self.plot.getActiveImage()
- self.plot.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend='scatter')
- scatter = self.plot.getActiveScatter()
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve')
- self.plot.setActiveCurve('curve')
- curve = self.plot.getActiveCurve()
-
- selection = self.plot.selection()
- self.assertIsNotNone(selection.getCurrentItem())
- self.assertEqual(len(selection.getSelectedItems()), 3)
-
- # Set current to None reset all active items
- selection.setCurrentItem(None)
- self._checkSelection(selection)
- self.assertIsNone(self.plot.getActiveCurve())
- self.assertIsNone(self.plot.getActiveImage())
- self.assertIsNone(self.plot.getActiveScatter())
-
- # Set current to an item makes it active
- selection.setCurrentItem(image)
- self._checkSelection(selection, image, (image,))
- self.assertIsNone(self.plot.getActiveCurve())
- self.assertIs(self.plot.getActiveImage(), image)
- self.assertIsNone(self.plot.getActiveScatter())
-
- # Set current to an item makes it active and keeps other active
- selection.setCurrentItem(curve)
- self._checkSelection(selection, curve, (curve, image))
- self.assertIs(self.plot.getActiveCurve(), curve)
- self.assertIs(self.plot.getActiveImage(), image)
- self.assertIsNone(self.plot.getActiveScatter())
-
- # Set current to an item makes it active and keeps other active
- selection.setCurrentItem(scatter)
- self._checkSelection(selection, scatter, (scatter, curve, image))
- self.assertIs(self.plot.getActiveCurve(), curve)
- self.assertIs(self.plot.getActiveImage(), image)
- self.assertIs(self.plot.getActiveScatter(), scatter)
-
-
@pytest.mark.usefixtures("use_opengl")
class TestPlotWidget_Gl(TestPlotWidget):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotImage_Gl(TestPlotImage):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotCurve_Gl(TestPlotCurve):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotHistogram_Gl(TestPlotHistogram):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotScatter_Gl(TestPlotScatter):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotMarker_Gl(TestPlotMarker):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotItem_Gl(TestPlotItem):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotAxes_Gl(TestPlotAxes):
- backend="gl"
+ backend = "gl"
-@pytest.mark.usefixtures("use_opengl")
-class TestPlotActiveCurveImage_Gl(TestPlotActiveCurveImage):
- backend="gl"
@pytest.mark.usefixtures("use_opengl")
class TestPlotEmptyLog_Gl(TestPlotEmptyLog):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotCurveLog_Gl(TestPlotCurveLog):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotImageLog_Gl(TestPlotImageLog):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotMarkerLog_Gl(TestPlotMarkerLog):
- backend="gl"
+ backend = "gl"
-@pytest.mark.usefixtures("use_opengl")
-class TestPlotWidgetSelection_Gl(TestPlotWidgetSelection):
- backend="gl"
class TestSpecial_ExplicitMplBackend(TestSpecialBackend):
- backend="mpl"
+ backend = "mpl"
diff --git a/src/silx/gui/plot/test/testPlotWidgetActiveItem.py b/src/silx/gui/plot/test/testPlotWidgetActiveItem.py
new file mode 100755
index 0000000..99285a8
--- /dev/null
+++ b/src/silx/gui/plot/test/testPlotWidgetActiveItem.py
@@ -0,0 +1,416 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Test PlotWidget active item"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "11/12/2023"
+
+
+import numpy
+import pytest
+
+from silx.gui.utils.testutils import SignalListener
+from silx.gui.plot.items.curve import CurveStyle
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testActiveCurveAndLabels(plotWidget):
+ # Active curve handling off, no label change
+ plotWidget.setActiveCurveHandling(False)
+ plotWidget.getXAxis().setLabel("XLabel")
+ plotWidget.getYAxis().setLabel("YLabel")
+ plotWidget.addCurve((1, 2), (1, 2))
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.addCurve((1, 2), (2, 3), xlabel="x1", ylabel="y1")
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.clear()
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ # Active curve handling on, label changes
+ plotWidget.setActiveCurveHandling(True)
+ plotWidget.getXAxis().setLabel("XLabel")
+ plotWidget.getYAxis().setLabel("YLabel")
+
+ # labels changed as active curve
+ plotWidget.addCurve((1, 2), (1, 2), legend="1", xlabel="x1", ylabel="y1")
+ plotWidget.setActiveCurve("1")
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ # labels not changed as not active curve
+ plotWidget.addCurve((1, 2), (2, 3), legend="2")
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ # labels changed
+ plotWidget.setActiveCurve("2")
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.setActiveCurve("1")
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ plotWidget.clear()
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.setActiveCurveHandling(False)
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testPlotActiveCurveSelectionMode(plotWidget):
+ xData = numpy.arange(1000)
+ yData = -500 + 100 * numpy.sin(xData)
+ xData2 = xData + 1000
+ yData2 = xData - 1000 + 200 * numpy.random.random(1000)
+
+ plotWidget.clear()
+ plotWidget.setActiveCurveHandling(True)
+ legend = "curve 1"
+ plotWidget.addCurve(xData, yData, legend=legend, color="green")
+
+ # active curve should be None
+ assert plotWidget.getActiveCurve(just_legend=True) is None
+
+ # active curve should be None when None is set as active curve
+ plotWidget.setActiveCurve(legend)
+ current = plotWidget.getActiveCurve(just_legend=True)
+ assert current == legend
+ plotWidget.setActiveCurve(None)
+ current = plotWidget.getActiveCurve(just_legend=True)
+ assert current is None
+
+ # testing it automatically toggles if there is only one
+ plotWidget.setActiveCurveSelectionMode("legacy")
+ current = plotWidget.getActiveCurve(just_legend=True)
+ assert current == legend
+
+ # active curve should not change when None set as active curve
+ assert plotWidget.getActiveCurveSelectionMode() == "legacy"
+ plotWidget.setActiveCurve(None)
+ current = plotWidget.getActiveCurve(just_legend=True)
+ assert current == legend
+
+ # situation where no curve is active
+ plotWidget.clear()
+ plotWidget.setActiveCurveHandling(True)
+ assert plotWidget.getActiveCurveSelectionMode() == "atmostone"
+ plotWidget.addCurve(xData, yData, legend=legend, color="green")
+ assert plotWidget.getActiveCurve(just_legend=True) is None
+ plotWidget.addCurve(xData2, yData2, legend="curve 2", color="red")
+ assert plotWidget.getActiveCurve(just_legend=True) is None
+ plotWidget.setActiveCurveSelectionMode("legacy")
+ assert plotWidget.getActiveCurve(just_legend=True) is None
+
+ # the first curve added should be active
+ plotWidget.clear()
+ plotWidget.addCurve(xData, yData, legend=legend, color="green")
+ assert plotWidget.getActiveCurve(just_legend=True) == legend
+ plotWidget.addCurve(xData2, yData2, legend="curve 2", color="red")
+ assert plotWidget.getActiveCurve(just_legend=True) == legend
+
+ plotWidget.setActiveCurveHandling(False)
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testActiveCurveStyle(plotWidget):
+ """Test change of active curve style"""
+ plotWidget.setActiveCurveHandling(True)
+ plotWidget.setActiveCurveStyle(color="black")
+ style = plotWidget.getActiveCurveStyle()
+ assert style.getColor() == (0.0, 0.0, 0.0, 1.0)
+ assert style.getLineStyle() is None
+ assert style.getLineWidth() is None
+ assert style.getSymbol() is None
+ assert style.getSymbolSize() is None
+
+ xData = numpy.arange(1000)
+ yData = -500 + 100 * numpy.sin(xData)
+ plotWidget.addCurve(x=xData, y=yData, legend="curve1")
+ curve = plotWidget.getCurve("curve1")
+ curve.setColor("blue")
+ curve.setLineStyle("-")
+ curve.setLineWidth(1)
+ curve.setSymbol("o")
+ curve.setSymbolSize(5)
+
+ # Check default current style
+ defaultStyle = curve.getCurrentStyle()
+ assert defaultStyle == CurveStyle(
+ color="blue", linestyle="-", linewidth=1, symbol="o", symbolsize=5
+ )
+
+ # Activate curve with highlight color=black
+ plotWidget.setActiveCurve("curve1")
+ style = curve.getCurrentStyle()
+ assert style.getColor() == (0.0, 0.0, 0.0, 1.0)
+ assert style.getLineStyle() == "-"
+ assert style.getLineWidth() == 1
+ assert style.getSymbol() == "o"
+ assert style.getSymbolSize() == 5
+
+ # Change highlight to linewidth=2
+ plotWidget.setActiveCurveStyle(linewidth=2)
+ style = curve.getCurrentStyle()
+ assert style.getColor() == (0.0, 0.0, 1.0, 1.0)
+ assert style.getLineStyle() == "-"
+ assert style.getLineWidth() == 2
+ assert style.getSymbol() == "o"
+ assert style.getSymbolSize() == 5
+
+ plotWidget.setActiveCurve(None)
+ assert curve.getCurrentStyle() == defaultStyle
+
+ plotWidget.setActiveCurveHandling(False)
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testActiveImageAndLabels(plotWidget):
+ # Active image handling always on, no API for toggling it
+ plotWidget.getXAxis().setLabel("XLabel")
+ plotWidget.getYAxis().setLabel("YLabel")
+
+ # labels changed as active curve
+ plotWidget.addImage(
+ numpy.arange(100).reshape(10, 10), legend="1", xlabel="x1", ylabel="y1"
+ )
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ # labels not changed as not active curve
+ plotWidget.addImage(numpy.arange(100).reshape(10, 10), legend="2")
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ # labels changed
+ plotWidget.setActiveImage("2")
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.setActiveImage("1")
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ plotWidget.clear()
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.setActiveCurveHandling(False)
+
+
+def _checkSelection(selection, current=None, selected=()):
+ """Check current item and selected items."""
+ assert selection.getCurrentItem() is current
+ assert selection.getSelectedItems() == selected
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testSelectionSyncWithActiveItems(plotWidget):
+ """Test update of PlotWidgetSelection according to active items"""
+ listener = SignalListener()
+
+ selection = plotWidget.selection()
+ selection.sigCurrentItemChanged.connect(listener)
+ _checkSelection(selection)
+
+ # Active item is current
+ plotWidget.addImage(((0, 1), (2, 3)), legend="image")
+ image = plotWidget.getActiveImage()
+ assert listener.callCount() == 1
+ _checkSelection(selection, image, (image,))
+
+ # No active = no current
+ plotWidget.setActiveImage(None)
+ assert listener.callCount() == 2
+ _checkSelection(selection)
+
+ # Active item is current
+ plotWidget.setActiveImage("image")
+ assert listener.callCount() == 3
+ _checkSelection(selection, image, (image,))
+
+ # Mosted recently "actived" item is current
+ plotWidget.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend="scatter")
+ scatter = plotWidget.getActiveScatter()
+ assert listener.callCount() == 4
+ _checkSelection(selection, scatter, (scatter, image))
+
+ # Previously mosted recently "actived" item is current
+ plotWidget.setActiveScatter(None)
+ assert listener.callCount() == 5
+ _checkSelection(selection, image, (image,))
+
+ # Mosted recently "actived" item is current
+ plotWidget.setActiveScatter("scatter")
+ assert listener.callCount() == 6
+ _checkSelection(selection, scatter, (scatter, image))
+
+ # No active = no current
+ plotWidget.setActiveImage(None)
+ plotWidget.setActiveScatter(None)
+ assert listener.callCount() == 7
+ _checkSelection(selection)
+
+ # Mosted recently "actived" item is current
+ plotWidget.setActiveScatter("scatter")
+ assert listener.callCount() == 8
+ plotWidget.setActiveImage("image")
+ assert listener.callCount() == 9
+ _checkSelection(selection, image, (image, scatter))
+
+ # Add a curve which is not active by default
+ plotWidget.addCurve((0, 1, 2), (0, 1, 2), legend="curve")
+ curve = plotWidget.getCurve("curve")
+ assert listener.callCount() == 9
+ _checkSelection(selection, image, (image, scatter))
+
+ # Mosted recently "actived" item is current
+ plotWidget.setActiveCurve("curve")
+ assert listener.callCount() == 10
+ _checkSelection(selection, curve, (curve, image, scatter))
+
+ # Add a curve which is not active by default
+ plotWidget.addCurve((0, 1, 2), (0, 1, 2), legend="curve2")
+ curve2 = plotWidget.getCurve("curve2")
+ assert listener.callCount() == 10
+ _checkSelection(selection, curve, (curve, image, scatter))
+
+ # Mosted recently "actived" item is current, previous curve is removed
+ plotWidget.setActiveCurve("curve2")
+ assert listener.callCount() == 11
+ _checkSelection(selection, curve2, (curve2, image, scatter))
+
+ # No items = no current
+ plotWidget.clear()
+ assert listener.callCount() == 12
+ _checkSelection(selection)
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testSelectionWithItems(plotWidget):
+ """Test init of selection on a plot with items"""
+ plotWidget.addImage(((0, 1), (2, 3)), legend="image")
+ plotWidget.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend="scatter")
+ plotWidget.addCurve((0, 1, 2), (0, 1, 2), legend="curve")
+ plotWidget.setActiveCurve("curve")
+
+ selection = plotWidget.selection()
+ assert selection.getCurrentItem() is not None
+ selected = selection.getSelectedItems()
+ assert len(selected) == 3
+ assert plotWidget.getActiveCurve() in selected
+ assert plotWidget.getActiveImage() in selected
+ assert plotWidget.getActiveScatter() in selected
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testSelectionSetCurrentItem(plotWidget):
+ """Test setCurrentItem"""
+ # Add items to the plot
+ plotWidget.addImage(((0, 1), (2, 3)), legend="image")
+ image = plotWidget.getActiveImage()
+ plotWidget.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend="scatter")
+ scatter = plotWidget.getActiveScatter()
+ plotWidget.addCurve((0, 1, 2), (0, 1, 2), legend="curve")
+ plotWidget.setActiveCurve("curve")
+ curve = plotWidget.getActiveCurve()
+
+ selection = plotWidget.selection()
+ assert selection.getCurrentItem() is not None
+ assert len(selection.getSelectedItems()) == 3
+
+ # Set current to None reset all active items
+ selection.setCurrentItem(None)
+ _checkSelection(selection)
+ assert plotWidget.getActiveCurve() is None
+ assert plotWidget.getActiveImage() is None
+ assert plotWidget.getActiveScatter() is None
+
+ # Set current to an item makes it active
+ selection.setCurrentItem(image)
+ _checkSelection(selection, image, (image,))
+ assert plotWidget.getActiveCurve() is None
+ assert plotWidget.getActiveImage() is image
+ assert plotWidget.getActiveScatter() is None
+
+ # Set current to an item makes it active and keeps other active
+ selection.setCurrentItem(curve)
+ _checkSelection(selection, curve, (curve, image))
+ assert plotWidget.getActiveCurve() is curve
+ assert plotWidget.getActiveImage() is image
+ assert plotWidget.getActiveScatter() is None
+
+ # Set current to an item makes it active and keeps other active
+ selection.setCurrentItem(scatter)
+ _checkSelection(selection, scatter, (scatter, curve, image))
+ assert plotWidget.getActiveCurve() is curve
+ assert plotWidget.getActiveImage() is image
+ assert plotWidget.getActiveScatter() is scatter
+
+
+def testSetActiveCurveWithInstance(plotWidget):
+ """Test setting the active curve with a curve item instance"""
+ plotWidget.addCurve((0, 1), (0, 1), legend="curve0")
+ plotWidget.addCurve((0, 1), (1, 0), legend="curve1")
+ curve0, curve1 = plotWidget.getItems()
+
+ plotWidget.setActiveCurve(curve0)
+ assert plotWidget.getActiveCurve() is curve0
+
+ plotWidget.setActiveCurve(curve1)
+ assert plotWidget.getActiveCurve() is curve1
+
+ plotWidget.setActiveCurve(None)
+ assert plotWidget.getActiveCurve() is None
+
+
+def testSetActiveImageWithInstance(plotWidget):
+ """Test setting the active image with an image item instance"""
+ plotWidget.addImage(((0, 1), (2, 3)), legend="image")
+ image = plotWidget.getItems()[0]
+
+ plotWidget.setActiveImage(None)
+ assert plotWidget.getActiveImage() is None
+
+ plotWidget.setActiveImage(image)
+ assert plotWidget.getActiveImage() is image
+
+
+def testSetActiveScatterWithInstance(plotWidget):
+ """Test setting the active scatter with a scatter item instance"""
+ plotWidget.addScatter((0, 1), (0, 1), (0, 1), legend="scatter")
+ scatter = plotWidget.getItems()[0]
+
+ plotWidget.setActiveScatter(None)
+ assert plotWidget.getActiveScatter() is None
+
+ plotWidget.setActiveScatter(scatter)
+ assert plotWidget.getActiveScatter() is scatter
diff --git a/src/silx/gui/plot/test/testPlotWidgetDataMargins.py b/src/silx/gui/plot/test/testPlotWidgetDataMargins.py
new file mode 100644
index 0000000..4eb5134
--- /dev/null
+++ b/src/silx/gui/plot/test/testPlotWidgetDataMargins.py
@@ -0,0 +1,135 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Test PlotWidget features related to data margins"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "11/05/2023"
+
+import numpy
+import pytest
+
+
+def testDefaultDataMargins(plotWidget):
+ """Test default PlotWidget data margins: No margins"""
+ assert plotWidget.getDataMargins() == (0, 0, 0, 0)
+
+
+def testResetZoomDataMarginsLinearAxes(qapp, plotWidget):
+ """Test PlotWidget.setDataMargins effect on resetZoom with linear axis scales"""
+
+ margins = 0.1, 0.2, 0.3, 0.4
+ plotWidget.setDataMargins(*margins)
+
+ plotWidget.resetZoom()
+ qapp.processEvents()
+
+ retrievedMargins = plotWidget.getDataMargins()
+ assert retrievedMargins == margins
+
+ dataRange = 100 - 1
+ expectedXLimits = 1 - 0.1 * dataRange, 100 + 0.2 * dataRange
+ expectedYLimits = 1 - 0.3 * dataRange, 100 + 0.4 * dataRange
+
+ assert plotWidget.getXAxis().getLimits() == expectedXLimits
+ assert plotWidget.getYAxis().getLimits() == expectedYLimits
+ assert plotWidget.getYAxis(axis="right").getLimits() == expectedYLimits
+
+
+def testResetZoomDataMarginsLogAxes(qapp, plotWidget):
+ """Test PlotWidget.setDataMargins effect on resetZoom with log axis scales"""
+ plotWidget.getXAxis().setScale("log")
+ plotWidget.getYAxis().setScale("log")
+
+ dataMargins = 0.1, 0.2, 0.3, 0.4
+ plotWidget.setDataMargins(*dataMargins)
+
+ plotWidget.resetZoom()
+ qapp.processEvents()
+
+ retrievedMargins = plotWidget.getDataMargins()
+ assert retrievedMargins == dataMargins
+
+ logMin, logMax = numpy.log10(1), numpy.log10(100)
+ logRange = logMax - logMin
+ expectedXLimits = pow(10.0, logMin - 0.1 * logRange), pow(
+ 10.0, logMax + 0.2 * logRange
+ )
+ expectedYLimits = pow(10.0, logMin - 0.3 * logRange), pow(
+ 10.0, logMax + 0.4 * logRange
+ )
+
+ assert plotWidget.getXAxis().getLimits() == expectedXLimits
+ assert plotWidget.getYAxis().getLimits() == expectedYLimits
+ assert plotWidget.getYAxis(axis="right").getLimits() == expectedYLimits
+
+
+@pytest.mark.parametrize("margins", [False, True, (0, 0, 0, 0)])
+def testSetLimitsNoDataMargins(plotWidget, margins):
+ """Test PlotWidget.setLimits without data margins"""
+ xlimits = 1, 2
+ ylimits = 3, 4
+ y2limits = 5, 6
+ plotWidget.setLimits(*xlimits, *ylimits, *y2limits, margins=margins)
+
+ assert plotWidget.getXAxis().getLimits() == xlimits
+ assert plotWidget.getYAxis().getLimits() == ylimits
+ assert plotWidget.getYAxis(axis="right").getLimits() == y2limits
+
+
+@pytest.mark.parametrize(
+ "margins,expectedLimits",
+ [
+ # margins=False: use limits as is
+ (
+ False,
+ (1, 2, 3, 4, 5, 6),
+ ),
+ # margins=True: apply data margins
+ (
+ True,
+ (1 - 0.1, 2 + 0.2, 3 - 0.3, 4 + 0.4, 5 - 0.3, 6 + 0.4),
+ ),
+ # margins=tuple: apply provided margins
+ (
+ (0.4, 0.3, 0.2, 0.1),
+ (1 - 0.4, 2 + 0.3, 3 - 0.2, 4 + 0.1, 5 - 0.2, 6 + 0.1),
+ ),
+ ],
+)
+def testSetLimitsWithDataMargins(qapp, plotWidget, margins, expectedLimits):
+ """Test PlotWidget.setLimits with data margins"""
+ dataMargins = 0.1, 0.2, 0.3, 0.4
+ limits = 1, 2, 3, 4, 5, 6
+
+ plotWidget.setDataMargins(*dataMargins)
+ plotWidget.setLimits(*limits, margins=margins)
+ qapp.processEvents()
+
+ retrievedLimits = (
+ *plotWidget.getXAxis().getLimits(),
+ *plotWidget.getYAxis().getLimits(),
+ *plotWidget.getYAxis(axis="right").getLimits(),
+ )
+ assert retrievedLimits == expectedLimits
diff --git a/src/silx/gui/plot/test/testPlotWidgetNoBackend.py b/src/silx/gui/plot/test/testPlotWidgetNoBackend.py
index 787d5a8..d9d5706 100644
--- a/src/silx/gui/plot/test/testPlotWidgetNoBackend.py
+++ b/src/silx/gui/plot/test/testPlotWidgetNoBackend.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -34,6 +34,8 @@ from silx.utils.testutils import ParametricTestCase
import numpy
+import silx
+from silx.gui.colors import rgba
from silx.gui.plot.PlotWidget import PlotWidget
from silx.gui.plot.items.histogram import _getHistogramCurve, _computeEdges
@@ -44,9 +46,9 @@ class TestPlot(unittest.TestCase):
def testPlotTitleLabels(self):
"""Create a Plot and set the labels"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
- title, xlabel, ylabel = 'the title', 'x label', 'y label'
+ title, xlabel, ylabel = "the title", "x label", "y label"
plot.setGraphTitle(title)
plot.getXAxis().setLabel(xlabel)
plot.getYAxis().setLabel(ylabel)
@@ -58,26 +60,29 @@ class TestPlot(unittest.TestCase):
def testAddNoRemove(self):
"""add objects to the Plot"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
plot.addCurve(x=(1, 2, 3), y=(3, 2, 1))
- plot.addImage(numpy.arange(100.).reshape(10, -1))
- plot.addShape(numpy.array((1., 10.)),
- numpy.array((10., 10.)),
- shape="rectangle")
- plot.addXMarker(10.)
+ plot.addImage(numpy.arange(100.0).reshape(10, -1))
+ plot.addShape(
+ numpy.array((1.0, 10.0)), numpy.array((10.0, 10.0)), shape="rectangle"
+ )
+ plot.addXMarker(10.0)
class TestPlotRanges(ParametricTestCase):
"""Basic tests of Plot data ranges without backend"""
- _getValidValues = {True: lambda ar: ar > 0,
- False: lambda ar: numpy.ones(shape=ar.shape,
- dtype=bool)}
+ _getValidValues = {
+ True: lambda ar: ar > 0,
+ False: lambda ar: numpy.ones(shape=ar.shape, dtype=bool),
+ }
@staticmethod
def _getRanges(arrays, are_logs):
- gen = (TestPlotRanges._getValidValues[is_log](ar)
- for (ar, is_log) in zip(arrays, are_logs))
+ gen = (
+ TestPlotRanges._getValidValues[is_log](ar)
+ for (ar, is_log) in zip(arrays, are_logs)
+ )
indices = numpy.where(reduce(numpy.logical_and, gen))[0]
if len(indices) > 0:
ranges = [(ar[indices[0]], ar[indices[-1]]) for ar in arrays]
@@ -96,13 +101,15 @@ class TestPlotRanges(ParametricTestCase):
def testDataRangeNoPlot(self):
"""empty plot data range"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
@@ -114,27 +121,25 @@ class TestPlotRanges(ParametricTestCase):
def testDataRangeLeft(self):
"""left axis range"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
xData = numpy.arange(10) - 4.9 # range : -4.9 , 4.1
yData = numpy.arange(10) - 6.9 # range : -6.9 , 2.1
- plot.addCurve(x=xData,
- y=yData,
- legend='plot_0',
- yaxis='left')
+ plot.addCurve(x=xData, y=yData, legend="plot_0", yaxis="left")
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
- xRange, yRange = self._getRanges([xData, yData],
- [logX, logY])
+ xRange, yRange = self._getRanges([xData, yData], [logX, logY])
self.assertSequenceEqual(dataRange.x, xRange)
self.assertSequenceEqual(dataRange.y, yRange)
self.assertIsNone(dataRange.yright)
@@ -142,25 +147,23 @@ class TestPlotRanges(ParametricTestCase):
def testDataRangeRight(self):
"""right axis range"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
xData = numpy.arange(10) - 4.9 # range : -4.9 , 4.1
yData = numpy.arange(10) - 6.9 # range : -6.9 , 2.1
- plot.addCurve(x=xData,
- y=yData,
- legend='plot_0',
- yaxis='right')
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ plot.addCurve(x=xData, y=yData, legend="plot_0", yaxis="right")
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
- xRange, yRange = self._getRanges([xData, yData],
- [logX, logY])
+ xRange, yRange = self._getRanges([xData, yData], [logX, logY])
self.assertSequenceEqual(dataRange.x, xRange)
self.assertIsNone(dataRange.y)
self.assertSequenceEqual(dataRange.yright, yRange)
@@ -169,69 +172,70 @@ class TestPlotRanges(ParametricTestCase):
"""image data range"""
origin = (-10, 25)
- scale = (3., 8.)
- image = numpy.arange(100.).reshape(20, 5)
-
- plot = PlotWidget(backend='none')
- plot.addImage(image,
- origin=origin, scale=scale)
-
- xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
- yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
-
- ranges = {(False, False): (xRange, yRange),
- (True, False): (None, None),
- (True, True): (None, None),
- (False, True): (None, None)}
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ scale = (3.0, 8.0)
+ image = numpy.arange(100.0).reshape(20, 5)
+
+ plot = PlotWidget(backend="none")
+ plot.addImage(image, origin=origin, scale=scale)
+
+ xRange = numpy.array([0.0, image.shape[1] * scale[0]]) + origin[0]
+ yRange = numpy.array([0.0, image.shape[0] * scale[1]]) + origin[1]
+
+ ranges = {
+ (False, False): (xRange, yRange),
+ (True, False): (None, None),
+ (True, True): (None, None),
+ (False, True): (None, None),
+ }
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
xRange, yRange = ranges[logX, logY]
- self.assertTrue(numpy.array_equal(dataRange.x, xRange),
- msg='{0} != {1}'.format(dataRange.x, xRange))
- self.assertTrue(numpy.array_equal(dataRange.y, yRange),
- msg='{0} != {1}'.format(dataRange.y, yRange))
+ self.assertTrue(
+ numpy.array_equal(dataRange.x, xRange),
+ msg="{0} != {1}".format(dataRange.x, xRange),
+ )
+ self.assertTrue(
+ numpy.array_equal(dataRange.y, yRange),
+ msg="{0} != {1}".format(dataRange.y, yRange),
+ )
self.assertIsNone(dataRange.yright)
def testDataRangeLeftRight(self):
"""right+left axis range"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
xData_l = numpy.arange(10) - 0.9 # range : -0.9 , 8.1
yData_l = numpy.arange(10) - 1.9 # range : -1.9 , 7.1
- plot.addCurve(x=xData_l,
- y=yData_l,
- legend='plot_l',
- yaxis='left')
+ plot.addCurve(x=xData_l, y=yData_l, legend="plot_l", yaxis="left")
xData_r = numpy.arange(10) - 4.9 # range : -4.9 , 4.1
yData_r = numpy.arange(10) - 6.9 # range : -6.9 , 2.1
- plot.addCurve(x=xData_r,
- y=yData_r,
- legend='plot_r',
- yaxis='right')
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ plot.addCurve(x=xData_r, y=yData_r, legend="plot_r", yaxis="right")
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
- xRangeL, yRangeL = self._getRanges([xData_l, yData_l],
- [logX, logY])
- xRangeR, yRangeR = self._getRanges([xData_r, yData_r],
- [logX, logY])
+ xRangeL, yRangeL = self._getRanges([xData_l, yData_l], [logX, logY])
+ xRangeR, yRangeR = self._getRanges([xData_r, yData_r], [logX, logY])
xRangeLR = self._getRangesMinmax([xRangeL, xRangeR])
self.assertSequenceEqual(dataRange.x, xRangeLR)
self.assertSequenceEqual(dataRange.y, yRangeL)
@@ -244,51 +248,42 @@ class TestPlotRanges(ParametricTestCase):
# image sets x min and y max
# plot_left sets y min
# plot_right sets x max (and yright)
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
origin = (-10, 5)
- scale = (3., 8.)
- image = numpy.arange(100.).reshape(20, 5)
+ scale = (3.0, 8.0)
+ image = numpy.arange(100.0).reshape(20, 5)
- plot.addImage(image,
- origin=origin, scale=scale, legend='image')
+ plot.addImage(image, origin=origin, scale=scale, legend="image")
xData_l = numpy.arange(10) - 0.9 # range : -0.9 , 8.1
yData_l = numpy.arange(10) - 1.9 # range : -1.9 , 7.1
- plot.addCurve(x=xData_l,
- y=yData_l,
- legend='plot_l',
- yaxis='left')
+ plot.addCurve(x=xData_l, y=yData_l, legend="plot_l", yaxis="left")
xData_r = numpy.arange(10) + 4.1 # range : 4.1 , 13.1
yData_r = numpy.arange(10) - 0.9 # range : -0.9 , 8.1
- plot.addCurve(x=xData_r,
- y=yData_r,
- legend='plot_r',
- yaxis='right')
-
- imgXRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
- imgYRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ plot.addCurve(x=xData_r, y=yData_r, legend="plot_r", yaxis="right")
+
+ imgXRange = numpy.array([0.0, image.shape[1] * scale[0]]) + origin[0]
+ imgYRange = numpy.array([0.0, image.shape[0] * scale[1]]) + origin[1]
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
- xRangeL, yRangeL = self._getRanges([xData_l, yData_l],
- [logX, logY])
- xRangeR, yRangeR = self._getRanges([xData_r, yData_r],
- [logX, logY])
+ xRangeL, yRangeL = self._getRanges([xData_l, yData_l], [logX, logY])
+ xRangeR, yRangeR = self._getRanges([xData_r, yData_r], [logX, logY])
if logX or logY:
xRangeLR = self._getRangesMinmax([xRangeL, xRangeR])
else:
- xRangeLR = self._getRangesMinmax([xRangeL,
- xRangeR,
- imgXRange])
+ xRangeLR = self._getRangesMinmax([xRangeL, xRangeR, imgXRange])
yRangeL = self._getRangesMinmax([yRangeL, imgYRange])
self.assertSequenceEqual(dataRange.x, xRangeLR)
self.assertSequenceEqual(dataRange.y, yRangeL)
@@ -298,83 +293,97 @@ class TestPlotRanges(ParametricTestCase):
"""image data range, negative scale"""
origin = (-10, 25)
- scale = (-3., 8.)
- image = numpy.arange(100.).reshape(20, 5)
+ scale = (-3.0, 8.0)
+ image = numpy.arange(100.0).reshape(20, 5)
- plot = PlotWidget(backend='none')
- plot.addImage(image,
- origin=origin, scale=scale)
+ plot = PlotWidget(backend="none")
+ plot.addImage(image, origin=origin, scale=scale)
- xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
+ xRange = numpy.array([0.0, image.shape[1] * scale[0]]) + origin[0]
xRange.sort() # negative scale!
- yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
-
- ranges = {(False, False): (xRange, yRange),
- (True, False): (None, None),
- (True, True): (None, None),
- (False, True): (None, None)}
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ yRange = numpy.array([0.0, image.shape[0] * scale[1]]) + origin[1]
+
+ ranges = {
+ (False, False): (xRange, yRange),
+ (True, False): (None, None),
+ (True, True): (None, None),
+ (False, True): (None, None),
+ }
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
xRange, yRange = ranges[logX, logY]
- self.assertTrue(numpy.array_equal(dataRange.x, xRange),
- msg='{0} != {1}'.format(dataRange.x, xRange))
- self.assertTrue(numpy.array_equal(dataRange.y, yRange),
- msg='{0} != {1}'.format(dataRange.y, yRange))
+ self.assertTrue(
+ numpy.array_equal(dataRange.x, xRange),
+ msg="{0} != {1}".format(dataRange.x, xRange),
+ )
+ self.assertTrue(
+ numpy.array_equal(dataRange.y, yRange),
+ msg="{0} != {1}".format(dataRange.y, yRange),
+ )
self.assertIsNone(dataRange.yright)
def testDataRangeImageNegativeScaleY(self):
"""image data range, negative scale"""
origin = (-10, 25)
- scale = (3., -8.)
- image = numpy.arange(100.).reshape(20, 5)
+ scale = (3.0, -8.0)
+ image = numpy.arange(100.0).reshape(20, 5)
- plot = PlotWidget(backend='none')
- plot.addImage(image,
- origin=origin, scale=scale)
+ plot = PlotWidget(backend="none")
+ plot.addImage(image, origin=origin, scale=scale)
- xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
- yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
+ xRange = numpy.array([0.0, image.shape[1] * scale[0]]) + origin[0]
+ yRange = numpy.array([0.0, image.shape[0] * scale[1]]) + origin[1]
yRange.sort() # negative scale!
- ranges = {(False, False): (xRange, yRange),
- (True, False): (None, None),
- (True, True): (None, None),
- (False, True): (None, None)}
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ ranges = {
+ (False, False): (xRange, yRange),
+ (True, False): (None, None),
+ (True, True): (None, None),
+ (False, True): (None, None),
+ }
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
xRange, yRange = ranges[logX, logY]
- self.assertTrue(numpy.array_equal(dataRange.x, xRange),
- msg='{0} != {1}'.format(dataRange.x, xRange))
- self.assertTrue(numpy.array_equal(dataRange.y, yRange),
- msg='{0} != {1}'.format(dataRange.y, yRange))
+ self.assertTrue(
+ numpy.array_equal(dataRange.x, xRange),
+ msg="{0} != {1}".format(dataRange.x, xRange),
+ )
+ self.assertTrue(
+ numpy.array_equal(dataRange.y, yRange),
+ msg="{0} != {1}".format(dataRange.y, yRange),
+ )
self.assertIsNone(dataRange.yright)
def testDataRangeHiddenCurve(self):
"""curves with a hidden curve"""
- plot = PlotWidget(backend='none')
- plot.addCurve((0, 1), (0, 1), legend='shown')
- plot.addCurve((0, 1, 2), (5, 5, 5), legend='hidden')
+ plot = PlotWidget(backend="none")
+ plot.addCurve((0, 1), (0, 1), legend="shown")
+ plot.addCurve((0, 1, 2), (5, 5, 5), legend="hidden")
range1 = plot.getDataRange()
self.assertEqual(range1.x, (0, 2))
self.assertEqual(range1.y, (0, 5))
- plot.hideCurve('hidden')
+ plot.hideCurve("hidden")
range2 = plot.getDataRange()
self.assertEqual(range2.x, (0, 1))
self.assertEqual(range2.y, (0, 1))
@@ -386,108 +395,108 @@ class TestPlotGetCurveImage(unittest.TestCase):
def testGetCurve(self):
"""PlotWidget.getCurve and Plot.getActiveCurve tests"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No curve
curve = plot.getCurve()
self.assertIsNone(curve) # No curve
plot.setActiveCurveHandling(True)
- plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 0')
- plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 1')
- plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 2')
- plot.setActiveCurve('curve 0')
+ plot.addCurve(x=(0, 1), y=(0, 1), legend="curve 0")
+ plot.addCurve(x=(0, 1), y=(0, 1), legend="curve 1")
+ plot.addCurve(x=(0, 1), y=(0, 1), legend="curve 2")
+ plot.setActiveCurve("curve 0")
# Active curve
active = plot.getActiveCurve()
- self.assertEqual(active.getName(), 'curve 0')
+ self.assertEqual(active.getName(), "curve 0")
curve = plot.getCurve()
- self.assertEqual(curve.getName(), 'curve 0')
+ self.assertEqual(curve.getName(), "curve 0")
# No active curve and curves
plot.setActiveCurveHandling(False)
active = plot.getActiveCurve()
self.assertIsNone(active) # No active curve
curve = plot.getCurve()
- self.assertEqual(curve.getName(), 'curve 2') # Last added curve
+ self.assertEqual(curve.getName(), "curve 2") # Last added curve
# Last curve hidden
- plot.hideCurve('curve 2', True)
+ plot.hideCurve("curve 2", True)
curve = plot.getCurve()
- self.assertEqual(curve.getName(), 'curve 1') # Last added curve
+ self.assertEqual(curve.getName(), "curve 1") # Last added curve
# All curves hidden
- plot.hideCurve('curve 1', True)
- plot.hideCurve('curve 0', True)
+ plot.hideCurve("curve 1", True)
+ plot.hideCurve("curve 0", True)
curve = plot.getCurve()
self.assertIsNone(curve)
def testGetCurveOldApi(self):
"""old API PlotWidget.getCurve and Plot.getActiveCurve tests"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No curve
curve = plot.getCurve()
self.assertIsNone(curve) # No curve
plot.setActiveCurveHandling(True)
- x = numpy.arange(10.).astype(numpy.float32)
+ x = numpy.arange(10.0).astype(numpy.float32)
y = x * x
- plot.addCurve(x=x, y=y, legend='curve 0', info=["whatever"])
- plot.addCurve(x=x, y=2*x, legend='curve 1', info="anything")
- plot.setActiveCurve('curve 0')
+ plot.addCurve(x=x, y=y, legend="curve 0", info=["whatever"])
+ plot.addCurve(x=x, y=2 * x, legend="curve 1", info="anything")
+ plot.setActiveCurve("curve 0")
# Active curve (4 elements)
xOut, yOut, legend, info = plot.getActiveCurve()[:4]
- self.assertEqual(legend, 'curve 0')
- self.assertTrue(numpy.allclose(xOut, x), 'curve 0 wrong x data')
- self.assertTrue(numpy.allclose(yOut, y), 'curve 0 wrong y data')
+ self.assertEqual(legend, "curve 0")
+ self.assertTrue(numpy.allclose(xOut, x), "curve 0 wrong x data")
+ self.assertTrue(numpy.allclose(yOut, y), "curve 0 wrong y data")
# Active curve (5 elements)
xOut, yOut, legend, info, params = plot.getCurve("curve 1")
- self.assertEqual(legend, 'curve 1')
- self.assertEqual(info, 'anything')
- self.assertTrue(numpy.allclose(xOut, x), 'curve 1 wrong x data')
- self.assertTrue(numpy.allclose(yOut, 2 * x), 'curve 1 wrong y data')
+ self.assertEqual(legend, "curve 1")
+ self.assertEqual(info, "anything")
+ self.assertTrue(numpy.allclose(xOut, x), "curve 1 wrong x data")
+ self.assertTrue(numpy.allclose(yOut, 2 * x), "curve 1 wrong y data")
def testGetImage(self):
"""PlotWidget.getImage and PlotWidget.getActiveImage tests"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No image
image = plot.getImage()
self.assertIsNone(image)
- plot.addImage(((0, 1), (2, 3)), legend='image 0')
- plot.addImage(((0, 1), (2, 3)), legend='image 1')
+ plot.addImage(((0, 1), (2, 3)), legend="image 0")
+ plot.addImage(((0, 1), (2, 3)), legend="image 1")
# Active image
active = plot.getActiveImage()
- self.assertEqual(active.getName(), 'image 0')
+ self.assertEqual(active.getName(), "image 0")
image = plot.getImage()
- self.assertEqual(image.getName(), 'image 0')
+ self.assertEqual(image.getName(), "image 0")
# No active image
- plot.addImage(((0, 1), (2, 3)), legend='image 2')
+ plot.addImage(((0, 1), (2, 3)), legend="image 2")
plot.setActiveImage(None)
active = plot.getActiveImage()
self.assertIsNone(active)
image = plot.getImage()
- self.assertEqual(image.getName(), 'image 2')
+ self.assertEqual(image.getName(), "image 2")
# Active image
- plot.setActiveImage('image 1')
+ plot.setActiveImage("image 1")
active = plot.getActiveImage()
- self.assertEqual(active.getName(), 'image 1')
+ self.assertEqual(active.getName(), "image 1")
image = plot.getImage()
- self.assertEqual(image.getName(), 'image 1')
+ self.assertEqual(image.getName(), "image 1")
def testGetImageOldApi(self):
"""PlotWidget.getImage and PlotWidget.getActiveImage old API tests"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No image
image = plot.getImage()
@@ -496,18 +505,18 @@ class TestPlotGetCurveImage(unittest.TestCase):
image = numpy.arange(10).astype(numpy.float32)
image.shape = 5, 2
- plot.addImage(image, legend='image 0', info=["Hi!"])
+ plot.addImage(image, legend="image 0", info=["Hi!"])
# Active image
data, legend, info, something, params = plot.getActiveImage()
- self.assertEqual(legend, 'image 0')
+ self.assertEqual(legend, "image 0")
self.assertEqual(info, ["Hi!"])
self.assertTrue(numpy.allclose(data, image), "image 0 data not correct")
def testGetAllImages(self):
"""PlotWidget.getAllImages test"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No image
images = plot.getAllImages()
@@ -515,35 +524,34 @@ class TestPlotGetCurveImage(unittest.TestCase):
# 2 images
data = numpy.arange(100).reshape(10, 10)
- plot.addImage(data, legend='1')
- plot.addImage(data, origin=(10, 10), legend='2')
+ plot.addImage(data, legend="1")
+ plot.addImage(data, origin=(10, 10), legend="2")
images = plot.getAllImages(just_legend=True)
- self.assertEqual(list(images), ['1', '2'])
+ self.assertEqual(list(images), ["1", "2"])
images = plot.getAllImages(just_legend=False)
self.assertEqual(len(images), 2)
- self.assertEqual(images[0].getName(), '1')
- self.assertEqual(images[1].getName(), '2')
+ self.assertEqual(images[0].getName(), "1")
+ self.assertEqual(images[1].getName(), "2")
class TestPlotAddScatter(unittest.TestCase):
"""Test of plot addScatter"""
def testAddGetScatter(self):
-
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No curve
scatter = plot._getItem(kind="scatter")
self.assertIsNone(scatter) # No curve
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 0')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 1')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 2')
- plot._setActiveItem('scatter', 'scatter 0')
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 0")
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 1")
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 2")
+ plot.setActiveScatter("scatter 0")
# Active scatter
- active = plot._getActiveItem(kind='scatter')
- self.assertEqual(active.getName(), 'scatter 0')
+ active = plot.getActiveScatter()
+ self.assertEqual(active.getName(), "scatter 0")
# check default values
self.assertAlmostEqual(active.getSymbolSize(), active._DEFAULT_SYMBOL_SIZE)
@@ -561,26 +569,26 @@ class TestPlotAddScatter(unittest.TestCase):
self.assertEqual(s0.getSymbol(), "d")
self.assertAlmostEqual(s0.getAlpha(), 0.777)
- scatter1 = plot._getItem(kind='scatter', legend='scatter 1')
- self.assertEqual(scatter1.getName(), 'scatter 1')
+ scatter1 = plot._getItem(kind="scatter", legend="scatter 1")
+ self.assertEqual(scatter1.getName(), "scatter 1")
def testGetAllScatters(self):
"""PlotWidget.getAllImages test"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
items = plot.getItems()
self.assertEqual(len(items), 0)
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 0')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 1')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 2')
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 0")
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 1")
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 2")
items = plot.getItems()
self.assertEqual(len(items), 3)
- self.assertEqual(items[0].getName(), 'scatter 0')
- self.assertEqual(items[1].getName(), 'scatter 1')
- self.assertEqual(items[2].getName(), 'scatter 2')
+ self.assertEqual(items[0].getName(), "scatter 0")
+ self.assertEqual(items[1].getName(), "scatter 1")
+ self.assertEqual(items[2].getName(), "scatter 2")
class TestPlotHistogram(unittest.TestCase):
@@ -593,13 +601,13 @@ class TestPlotHistogram(unittest.TestCase):
edgesCenter = numpy.array([-0.5, 0.5, 1.5, 2.5])
# testing x values for right
- edges = _computeEdges(x, 'right')
+ edges = _computeEdges(x, "right")
numpy.testing.assert_array_equal(edges, edgesRight)
- edges = _computeEdges(x, 'center')
+ edges = _computeEdges(x, "center")
numpy.testing.assert_array_equal(edges, edgesCenter)
- edges = _computeEdges(x, 'left')
+ edges = _computeEdges(x, "left")
numpy.testing.assert_array_equal(edges, edgesLeft)
def testHistogramCurve(self):
@@ -607,11 +615,71 @@ class TestPlotHistogram(unittest.TestCase):
edges = numpy.array([0, 1, 2, 3])
xHisto, yHisto = _getHistogramCurve(y, edges)
- numpy.testing.assert_array_equal(
- yHisto, numpy.array([3, 3, 2, 2, 5, 5]))
+ numpy.testing.assert_array_equal(yHisto, numpy.array([3, 3, 2, 2, 5, 5]))
y = numpy.array([-3, 2, 5, 0])
edges = numpy.array([-2, -1, 0, 1, 2])
xHisto, yHisto = _getHistogramCurve(y, edges)
numpy.testing.assert_array_equal(
- yHisto, numpy.array([-3, -3, 2, 2, 5, 5, 0, 0]))
+ yHisto, numpy.array([-3, -3, 2, 2, 5, 5, 0, 0])
+ )
+
+
+def testSetDefaultColors(qWidgetFactory):
+ """Basic test of PlotWidget.get|setDefaultColors"""
+ plot = qWidgetFactory(PlotWidget)
+
+ # By default using config
+ assert numpy.array_equal(
+ plot.getDefaultColors(), silx.config.DEFAULT_PLOT_CURVE_COLORS
+ )
+
+ # Use own colors
+ colors = "red", "green", "blue"
+ plot.setDefaultColors(colors)
+ assert plot.getDefaultColors() == colors
+
+ # Reset to default
+ plot.setDefaultColors(None)
+ assert numpy.array_equal(
+ plot.getDefaultColors(), silx.config.DEFAULT_PLOT_CURVE_COLORS
+ )
+
+
+def testSetDefaultColorsAddCurve(qWidgetFactory):
+ """Test that PlotWidget.setDefaultColors reset color index"""
+ plot = qWidgetFactory(PlotWidget)
+
+ plot.addCurve((0, 1), (0, 0), legend="curve0")
+ plot.addCurve((0, 1), (1, 1), legend="curve1")
+ plot.addCurve((0, 1), (2, 2), legend="curve2")
+
+ colors = "#123456", "#abcdef"
+ plot.setDefaultColors(colors)
+ assert plot.getDefaultColors() == colors
+
+ # Check that the color index is reset
+ curve = plot.addCurve((1, 2), (0, 1), legend="newcurve")
+ assert curve.getColor() == rgba(colors[0])
+
+
+def testDefaultColorsUpdateConfig(qWidgetFactory):
+ """Test that color index is reset if needed when default colors config is updated"""
+ plot = qWidgetFactory(PlotWidget)
+
+ plot.addCurve((0, 1), (0, 0), legend="curve0")
+ plot.addCurve((0, 1), (1, 1), legend="curve1")
+ plot.addCurve((0, 1), (2, 2), legend="curve2")
+
+ previous_colors = silx.config.DEFAULT_PLOT_CURVE_COLORS
+ try:
+ colors = "#123456", "#abcdef"
+ silx.config.DEFAULT_PLOT_CURVE_COLORS = colors
+ assert plot.getDefaultColors() == colors
+
+ # Check that the color index is reset
+ curve = plot.addCurve((1, 2), (0, 1), legend="newcurve")
+ assert curve.getColor() == rgba(colors[0])
+
+ finally:
+ silx.config.DEFAULT_PLOT_CURVE_COLORS = previous_colors
diff --git a/src/silx/gui/plot/test/testPlotWindow.py b/src/silx/gui/plot/test/testPlotWindow.py
index 8e3f1df..8f17bf1 100644
--- a/src/silx/gui/plot/test/testPlotWindow.py
+++ b/src/silx/gui/plot/test/testPlotWindow.py
@@ -28,7 +28,6 @@ __license__ = "MIT"
__date__ = "27/06/2017"
-import unittest
import numpy
import pytest
@@ -72,12 +71,14 @@ class TestPlotWindow(TestCaseQt):
toolButton = getQToolButtonFromAction(action)
self.assertIsNot(toolButton, None)
self.mouseClick(toolButton, qt.Qt.LeftButton)
- self.assertNotEqual(getter(), initialState,
- msg='"%s" state not changed' % action.text())
+ self.assertNotEqual(
+ getter(), initialState, msg='"%s" state not changed' % action.text()
+ )
self.mouseClick(toolButton, qt.Qt.LeftButton)
- self.assertEqual(getter(), initialState,
- msg='"%s" state not changed' % action.text())
+ self.assertEqual(
+ getter(), initialState, msg='"%s" state not changed' % action.text()
+ )
# Trigger a zoom reset
self.mouseMove(self.plot)
@@ -88,8 +89,8 @@ class TestPlotWindow(TestCaseQt):
def testDockWidgets(self):
"""Test add/remove dock widgets"""
- dock1 = qt.QDockWidget('Test 1')
- dock1.setWidget(qt.QLabel('Test 1'))
+ dock1 = qt.QDockWidget("Test 1")
+ dock1.setWidget(qt.QLabel("Test 1"))
self.plot.addTabbedDockWidget(dock1)
self.qapp.processEvents()
@@ -97,17 +98,17 @@ class TestPlotWindow(TestCaseQt):
self.plot.removeDockWidget(dock1)
self.qapp.processEvents()
- dock2 = qt.QDockWidget('Test 2')
- dock2.setWidget(qt.QLabel('Test 2'))
+ dock2 = qt.QDockWidget("Test 2")
+ dock2.setWidget(qt.QLabel("Test 2"))
self.plot.addTabbedDockWidget(dock2)
self.qapp.processEvents()
- if qt.BINDING != 'PySide2':
- # Weird bug with PySide2 later upon gc.collect() when getting the layout
- self.assertNotEqual(self.plot.layout().indexOf(dock2),
- -1,
- "dock2 not properly displayed")
+ self.assertNotEqual(
+ self.plot.layout().indexOf(dock2),
+ -1,
+ "dock2 not properly displayed",
+ )
def testToolAspectRatio(self):
self.plot.toolBar()
@@ -128,12 +129,14 @@ class TestPlotWindow(TestCaseQt):
old = Colormap._computeAutoscaleRange
self._count = 0
+
def _computeAutoscaleRange(colormap, data):
self._count = self._count + 1
return 10, 20
+
Colormap._computeAutoscaleRange = _computeAutoscaleRange
try:
- colormap = Colormap(name='red')
+ colormap = Colormap(name="red")
self.plot.setVisible(True)
# Add an image
@@ -163,11 +166,10 @@ class TestPlotWindow(TestCaseQt):
ylimits = self.plot.getYAxis().getLimits()
isKeepAspectRatio = self.plot.isKeepDataAspectRatio()
- for backend in ('gl', 'mpl'):
+ for backend in ("gl", "mpl"):
with self.subTest():
self.plot.setBackend(backend)
self.plot.replot()
self.assertEqual(self.plot.getXAxis().getLimits(), xlimits)
self.assertEqual(self.plot.getYAxis().getLimits(), ylimits)
- self.assertEqual(
- self.plot.isKeepDataAspectRatio(), isKeepAspectRatio)
+ self.assertEqual(self.plot.isKeepDataAspectRatio(), isKeepAspectRatio)
diff --git a/src/silx/gui/plot/test/testRoiStatsWidget.py b/src/silx/gui/plot/test/testRoiStatsWidget.py
index 2c1c6b3..759ebe2 100644
--- a/src/silx/gui/plot/test/testRoiStatsWidget.py
+++ b/src/silx/gui/plot/test/testRoiStatsWidget.py
@@ -32,47 +32,49 @@ from silx.gui.plot.ROIStatsWidget import ROIStatsWidget
from silx.gui.plot.CurvesROIWidget import ROI
from silx.gui.plot.items.roi import RectangleROI, PolygonROI
from silx.gui.plot.StatsWidget import UpdateMode
-import unittest
import numpy
-
class _TestRoiStatsBase(TestCaseQt):
"""Base class for several unittest relative to ROIStatsWidget"""
+
def setUp(self):
TestCaseQt.setUp(self)
# define plot
self.plot = PlotWindow()
- self.plot.addImage(numpy.arange(10000).reshape(100, 100),
- legend='img1')
- self.img_item = self.plot.getImage('img1')
- self.plot.addCurve(x=numpy.linspace(0, 10, 56), y=numpy.arange(56),
- legend='curve1')
- self.curve_item = self.plot.getCurve('curve1')
- self.plot.addHistogram(edges=numpy.linspace(0, 10, 56),
- histogram=numpy.arange(56), legend='histo1')
- self.histogram_item = self.plot.getHistogram(legend='histo1')
- self.plot.addScatter(x=numpy.linspace(0, 10, 56),
- y=numpy.linspace(0, 10, 56),
- value=numpy.arange(56),
- legend='scatter1')
- self.scatter_item = self.plot.getScatter(legend='scatter1')
+ self.plot.addImage(numpy.arange(10000).reshape(100, 100), legend="img1")
+ self.img_item = self.plot.getImage("img1")
+ self.plot.addCurve(
+ x=numpy.linspace(0, 10, 56), y=numpy.arange(56), legend="curve1"
+ )
+ self.curve_item = self.plot.getCurve("curve1")
+ self.plot.addHistogram(
+ edges=numpy.linspace(0, 10, 56), histogram=numpy.arange(56), legend="histo1"
+ )
+ self.histogram_item = self.plot.getHistogram(legend="histo1")
+ self.plot.addScatter(
+ x=numpy.linspace(0, 10, 56),
+ y=numpy.linspace(0, 10, 56),
+ value=numpy.arange(56),
+ legend="scatter1",
+ )
+ self.scatter_item = self.plot.getScatter(legend="scatter1")
# stats widget
self.statsWidget = ROIStatsWidget(plot=self.plot)
# define stats
stats = [
- ('sum', numpy.sum),
- ('mean', numpy.mean),
+ ("sum", numpy.sum),
+ ("mean", numpy.mean),
]
self.statsWidget.setStats(stats=stats)
# define rois
- self.roi1D = ROI(name='range1', fromdata=0, todata=4, type_='energy')
+ self.roi1D = ROI(name="range1", fromdata=0, todata=4, type_="energy")
self.rectangle_roi = RectangleROI()
self.rectangle_roi.setGeometry(origin=(0, 0), size=(20, 20))
- self.rectangle_roi.setName('Initial ROI')
+ self.rectangle_roi.setName("Initial ROI")
self.polygon_roi = PolygonROI()
points = numpy.array([[0, 5], [5, 0], [10, 5], [5, 10]])
self.polygon_roi.setPoints(points)
@@ -95,182 +97,164 @@ class TestRoiStatsCouple(_TestRoiStatsBase):
"""
Test different possible couple (roi, plotItem).
Check that:
-
+
* computation is correct if couple is valid
* raise an error if couple is invalid
"""
+
def testROICurve(self):
"""
- Test that the couple (ROI, curveItem) can be used for stats
+ Test that the couple (ROI, curveItem) can be used for stats
"""
- item = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.curve_item)
+ item = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.curve_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '253')
- self.assertEqual(tableItems['mean'].text(), '11.0')
+ self.assertEqual(tableItems["sum"].text(), "253")
+ self.assertEqual(tableItems["mean"].text(), "11.0")
def testRectangleImage(self):
"""
- Test that the couple (RectangleROI, imageItem) can be used for stats
+ Test that the couple (RectangleROI, imageItem) can be used for stats
"""
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.img_item)
assert item is not None
- self.plot.addImage(numpy.ones(10000).reshape(100, 100),
- legend='img1')
+ self.plot.addImage(numpy.ones(10000).reshape(100, 100), legend="img1")
self.qapp.processEvents()
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), str(float(21*21)))
- self.assertEqual(tableItems['mean'].text(), '1.0')
+ self.assertEqual(tableItems["sum"].text(), str(float(21 * 21)))
+ self.assertEqual(tableItems["mean"].text(), "1.0")
def testPolygonImage(self):
"""
- Test that the couple (PolygonROI, imageItem) can be used for stats
+ Test that the couple (PolygonROI, imageItem) can be used for stats
"""
- item = self.statsWidget.addItem(roi=self.polygon_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.polygon_roi, plotItem=self.img_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '22750')
- self.assertEqual(tableItems['mean'].text(), '455.0')
+ self.assertEqual(tableItems["sum"].text(), "22750")
+ self.assertEqual(tableItems["mean"].text(), "455.0")
def testROIImage(self):
"""
- Test that the couple (ROI, imageItem) is raising an error
+ Test that the couple (ROI, imageItem) is raising an error
"""
with self.assertRaises(TypeError):
- self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.img_item)
+ self.statsWidget.addItem(roi=self.roi1D, plotItem=self.img_item)
def testRectangleCurve(self):
"""
- Test that the couple (rectangleROI, curveItem) is raising an error
+ Test that the couple (rectangleROI, curveItem) is raising an error
"""
with self.assertRaises(TypeError):
- self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.curve_item)
+ self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.curve_item)
def testROIHistogram(self):
"""
- Test that the couple (PolygonROI, imageItem) can be used for stats
+ Test that the couple (PolygonROI, imageItem) can be used for stats
"""
- item = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.histogram_item)
+ item = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.histogram_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '253')
- self.assertEqual(tableItems['mean'].text(), '11.0')
+ self.assertEqual(tableItems["sum"].text(), "253")
+ self.assertEqual(tableItems["mean"].text(), "11.0")
def testROIScatter(self):
"""
- Test that the couple (PolygonROI, imageItem) can be used for stats
+ Test that the couple (PolygonROI, imageItem) can be used for stats
"""
- item = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.scatter_item)
+ item = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.scatter_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '253')
- self.assertEqual(tableItems['mean'].text(), '11.0')
+ self.assertEqual(tableItems["sum"].text(), "253")
+ self.assertEqual(tableItems["mean"].text(), "11.0")
class TestRoiStatsAddRemoveItem(_TestRoiStatsBase):
"""Test adding and removing (roi, plotItem) items"""
+
def testAddRemoveItems(self):
- item1 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.scatter_item)
+ item1 = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.scatter_item)
self.assertTrue(item1 is not None)
self.assertEqual(self.statsTable().rowCount(), 1)
- item2 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.histogram_item)
+ item2 = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.histogram_item)
self.assertTrue(item2 is not None)
self.assertEqual(self.statsTable().rowCount(), 2)
# try to add twice the same item
- item3 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.histogram_item)
+ item3 = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.histogram_item)
self.assertTrue(item3 is None)
self.assertEqual(self.statsTable().rowCount(), 2)
- item4 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.curve_item)
+ item4 = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.curve_item)
self.assertTrue(item4 is not None)
self.assertEqual(self.statsTable().rowCount(), 3)
- self.statsWidget.removeItem(plotItem=item4._plot_item,
- roi=item4._roi)
+ self.statsWidget.removeItem(plotItem=item4._plot_item, roi=item4._roi)
self.assertEqual(self.statsTable().rowCount(), 2)
# try to remove twice the same item
- self.statsWidget.removeItem(plotItem=item4._plot_item,
- roi=item4._roi)
+ self.statsWidget.removeItem(plotItem=item4._plot_item, roi=item4._roi)
self.assertEqual(self.statsTable().rowCount(), 2)
- self.statsWidget.removeItem(plotItem=item2._plot_item,
- roi=item2._roi)
- self.statsWidget.removeItem(plotItem=item1._plot_item,
- roi=item1._roi)
+ self.statsWidget.removeItem(plotItem=item2._plot_item, roi=item2._roi)
+ self.statsWidget.removeItem(plotItem=item1._plot_item, roi=item1._roi)
self.assertEqual(self.statsTable().rowCount(), 0)
class TestRoiStatsRoiUpdate(_TestRoiStatsBase):
"""Test that the stats will be updated if the roi is updated"""
+
def testChangeRoi(self):
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.img_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '445410')
- self.assertEqual(tableItems['mean'].text(), '1010.0')
+ self.assertEqual(tableItems["sum"].text(), "445410")
+ self.assertEqual(tableItems["mean"].text(), "1010.0")
# update roi
self.rectangle_roi.setOrigin(position=(10, 10))
- self.assertNotEqual(tableItems['sum'].text(), '445410')
- self.assertNotEqual(tableItems['mean'].text(), '1010.0')
+ self.assertNotEqual(tableItems["sum"].text(), "445410")
+ self.assertNotEqual(tableItems["mean"].text(), "1010.0")
def testUpdateModeScenario(self):
"""Test update according to a simple scenario"""
self.statsWidget._setUpdateMode(UpdateMode.AUTO)
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.img_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '445410')
- self.assertEqual(tableItems['mean'].text(), '1010.0')
+ self.assertEqual(tableItems["sum"].text(), "445410")
+ self.assertEqual(tableItems["mean"].text(), "1010.0")
self.statsWidget._setUpdateMode(UpdateMode.MANUAL)
self.rectangle_roi.setOrigin(position=(10, 10))
self.qapp.processEvents()
- self.assertNotEqual(tableItems['sum'].text(), '445410')
- self.assertNotEqual(tableItems['mean'].text(), '1010.0')
+ self.assertNotEqual(tableItems["sum"].text(), "445410")
+ self.assertNotEqual(tableItems["mean"].text(), "1010.0")
self.statsWidget._updateAllStats(is_request=True)
- self.assertNotEqual(tableItems['sum'].text(), '445410')
- self.assertNotEqual(tableItems['mean'].text(), '1010.0')
+ self.assertNotEqual(tableItems["sum"].text(), "445410")
+ self.assertNotEqual(tableItems["mean"].text(), "1010.0")
class TestRoiStatsPlotItemUpdate(_TestRoiStatsBase):
"""Test that the stats will be updated if the plot item is updated"""
+
def testChangeImage(self):
self.statsWidget._setUpdateMode(UpdateMode.AUTO)
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.img_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['mean'].text(), '1010.0')
+ self.assertEqual(tableItems["mean"].text(), "1010.0")
# update plot
- self.plot.addImage(numpy.arange(100, 10100).reshape(100, 100),
- legend='img1')
- self.assertNotEqual(tableItems['mean'].text(), '1059.5')
+ self.plot.addImage(numpy.arange(100, 10100).reshape(100, 100), legend="img1")
+ self.assertNotEqual(tableItems["mean"].text(), "1059.5")
def testUpdateModeScenario(self):
"""Test update according to a simple scenario"""
self.statsWidget._setUpdateMode(UpdateMode.MANUAL)
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.img_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['mean'].text(), '1010.0')
- self.plot.addImage(numpy.arange(100, 10100).reshape(100, 100),
- legend='img1')
- self.assertEqual(tableItems['mean'].text(), '1010.0')
+ self.assertEqual(tableItems["mean"].text(), "1010.0")
+ self.plot.addImage(numpy.arange(100, 10100).reshape(100, 100), legend="img1")
+ self.assertEqual(tableItems["mean"].text(), "1010.0")
self.statsWidget._updateAllStats(is_request=True)
- self.assertEqual(tableItems['mean'].text(), '1110.0')
+ self.assertEqual(tableItems["mean"].text(), "1110.0")
diff --git a/src/silx/gui/plot/test/testSaveAction.py b/src/silx/gui/plot/test/testSaveAction.py
index d5a06c6..f8ac7ee 100644
--- a/src/silx/gui/plot/test/testSaveAction.py
+++ b/src/silx/gui/plot/test/testSaveAction.py
@@ -39,9 +39,8 @@ from silx.gui.plot.actions.io import SaveAction
class TestSaveActionSaveCurvesAsSpec(unittest.TestCase):
-
def setUp(self):
- self.plot = PlotWidget(backend='none')
+ self.plot = PlotWidget(backend="none")
self.saveAction = SaveAction(plot=self.plot)
self.tempdir = tempfile.mkdtemp()
@@ -56,17 +55,16 @@ class TestSaveActionSaveCurvesAsSpec(unittest.TestCase):
self.plot.setGraphXLabel("graph x label")
self.plot.setGraphYLabel("graph y label")
- self.plot.addCurve([0, 1], [1, 2], "curve with labels",
- xlabel="curve0 X", ylabel="curve0 Y")
- self.plot.addCurve([-1, 3], [-6, 2], "curve with X label",
- xlabel="curve1 X")
- self.plot.addCurve([-2, 0], [8, 12], "curve with Y label",
- ylabel="curve2 Y")
+ self.plot.addCurve(
+ [0, 1], [1, 2], "curve with labels", xlabel="curve0 X", ylabel="curve0 Y"
+ )
+ self.plot.addCurve([-1, 3], [-6, 2], "curve with X label", xlabel="curve1 X")
+ self.plot.addCurve([-2, 0], [8, 12], "curve with Y label", ylabel="curve2 Y")
self.plot.addCurve([3, 1], [7, 6], "curve with no labels")
- self.saveAction._saveCurves(self.plot,
- self.out_fname,
- SaveAction.DEFAULT_ALL_CURVES_FILTERS[0]) # "All curves as SpecFile (*.dat)"
+ self.saveAction._saveCurves(
+ self.plot, self.out_fname, SaveAction.DEFAULT_ALL_CURVES_FILTERS[0]
+ ) # "All curves as SpecFile (*.dat)"
with open(self.out_fname, "rb") as f:
file_content = f.read()
@@ -99,33 +97,35 @@ class TestSaveActionExtension(PlotWidgetTestCase):
saveAction = SaveAction(plot=self.plot, parent=self.plot)
# Add a new file filter
- nameFilter = 'Dummy file (*.dummy)'
- saveAction.setFileFilter('all', nameFilter, self._dummySaveFunction)
- self.assertTrue(nameFilter in saveAction.getFileFilters('all'))
- self.assertEqual(saveAction.getFileFilters('all')[nameFilter],
- self._dummySaveFunction)
+ nameFilter = "Dummy file (*.dummy)"
+ saveAction.setFileFilter("all", nameFilter, self._dummySaveFunction)
+ self.assertTrue(nameFilter in saveAction.getFileFilters("all"))
+ self.assertEqual(
+ saveAction.getFileFilters("all")[nameFilter], self._dummySaveFunction
+ )
# Add a new file filter at a particular position
- nameFilter = 'Dummy file2 (*.dummy)'
- saveAction.setFileFilter('all', nameFilter,
- self._dummySaveFunction, index=3)
- self.assertTrue(nameFilter in saveAction.getFileFilters('all'))
- filters = saveAction.getFileFilters('all')
+ nameFilter = "Dummy file2 (*.dummy)"
+ saveAction.setFileFilter("all", nameFilter, self._dummySaveFunction, index=3)
+ self.assertTrue(nameFilter in saveAction.getFileFilters("all"))
+ filters = saveAction.getFileFilters("all")
self.assertEqual(filters[nameFilter], self._dummySaveFunction)
- self.assertEqual(list(filters.keys()).index(nameFilter),3)
+ self.assertEqual(list(filters.keys()).index(nameFilter), 3)
# Update an existing file filter
nameFilter = SaveAction.IMAGE_FILTER_EDF
- saveAction.setFileFilter('image', nameFilter, self._dummySaveFunction)
- self.assertEqual(saveAction.getFileFilters('image')[nameFilter],
- self._dummySaveFunction)
+ saveAction.setFileFilter("image", nameFilter, self._dummySaveFunction)
+ self.assertEqual(
+ saveAction.getFileFilters("image")[nameFilter], self._dummySaveFunction
+ )
# Change the position of an existing file filter
- nameFilter = 'Dummy file2 (*.dummy)'
- oldIndex = list(saveAction.getFileFilters('all')).index(nameFilter)
+ nameFilter = "Dummy file2 (*.dummy)"
+ oldIndex = list(saveAction.getFileFilters("all")).index(nameFilter)
newIndex = oldIndex - 1
- saveAction.setFileFilter('all', nameFilter,
- self._dummySaveFunction, index=newIndex)
- filters = saveAction.getFileFilters('all')
+ saveAction.setFileFilter(
+ "all", nameFilter, self._dummySaveFunction, index=newIndex
+ )
+ filters = saveAction.getFileFilters("all")
self.assertEqual(filters[nameFilter], self._dummySaveFunction)
self.assertEqual(list(filters.keys()).index(nameFilter), newIndex)
diff --git a/src/silx/gui/plot/test/testScatterMaskToolsWidget.py b/src/silx/gui/plot/test/testScatterMaskToolsWidget.py
index 68375b0..5dc14e1 100644
--- a/src/silx/gui/plot/test/testScatterMaskToolsWidget.py
+++ b/src/silx/gui/plot/test/testScatterMaskToolsWidget.py
@@ -30,7 +30,6 @@ __date__ = "17/01/2018"
import logging
import os.path
-import unittest
import numpy
@@ -41,8 +40,6 @@ from silx.gui.utils.testutils import getQToolButtonFromAction
from silx.gui.plot import PlotWindow, ScatterMaskToolsWidget
from .utils import PlotWidgetTestCase
-import fabio
-
_logger = logging.getLogger(__name__)
@@ -56,7 +53,8 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def setUp(self):
super(TestScatterMaskToolsWidget, self).setUp()
self.widget = ScatterMaskToolsWidget.ScatterMaskToolsDockWidget(
- plot=self.plot, name='TEST')
+ plot=self.plot, name="TEST"
+ )
self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, self.widget)
self.maskWidget = self.widget.widget()
@@ -68,10 +66,10 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def testEmptyPlot(self):
"""Empty plot, display MaskToolsDockWidget, toggle multiple masks"""
- self.maskWidget.setMultipleMasks('single')
+ self.maskWidget.setMultipleMasks("single")
self.qapp.processEvents()
- self.maskWidget.setMultipleMasks('exclusive')
+ self.maskWidget.setMultipleMasks("exclusive")
self.qapp.processEvents()
def _drag(self):
@@ -102,12 +100,14 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
x, y = plot.width() // 2, plot.height() // 2
offset = min(plot.width(), plot.height()) // 10
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset),
- (x, y + offset)] # Close polygon
+ star = [
+ (x, y + offset),
+ (x - offset, y - offset),
+ (x + offset, y),
+ (x - offset, y),
+ (x + offset, y - offset),
+ (x, y + offset),
+ ] # Close polygon
self.mouseMove(plot, pos=[0, 0])
for pos in star:
@@ -124,41 +124,44 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
x, y = plot.width() // 2, plot.height() // 2
offset = min(plot.width(), plot.height()) // 10
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset)]
+ star = [
+ (x, y + offset),
+ (x - offset, y - offset),
+ (x + offset, y),
+ (x - offset, y),
+ (x + offset, y - offset),
+ ]
self.mouseMove(plot, pos=[0, 0])
self.mouseMove(plot, pos=star[0])
self.mousePress(plot, qt.Qt.LeftButton, pos=star[0])
for pos in star[1:]:
self.mouseMove(plot, pos=pos)
- self.mouseRelease(
- plot, qt.Qt.LeftButton, pos=star[-1])
+ self.mouseRelease(plot, qt.Qt.LeftButton, pos=star[-1])
def testWithAScatter(self):
"""Plot with a Scatter: test MaskToolsWidget interactions"""
# Add and remove a scatter (this should enable/disable GUI + change mask)
self.plot.addScatter(
- x=numpy.arange(256),
- y=numpy.arange(256),
- value=numpy.random.random(256),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
+ x=numpy.arange(256),
+ y=numpy.arange(256),
+ value=numpy.random.random(256),
+ legend="test",
+ )
+ self.plot.setActiveScatter("test")
self.qapp.processEvents()
- self.plot.remove('test', kind='scatter')
+ self.plot.remove("test", kind="scatter")
self.qapp.processEvents()
self.plot.addScatter(
- x=numpy.arange(1000),
- y=1000 * (numpy.arange(1000) % 20),
- value=numpy.random.random(1000),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
+ x=numpy.arange(1000),
+ y=1000 * (numpy.arange(1000) % 20),
+ value=numpy.random.random(1000),
+ legend="test",
+ )
+ self.plot.setActiveScatter("test")
self.plot.resetZoom()
self.qapp.processEvents()
@@ -172,15 +175,13 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drag()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertFalse(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# unmask same region
self.maskWidget.maskStateGroup.button(0).click()
self.qapp.processEvents()
self._drag()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertTrue(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# Test draw polygon #
toolButton = getQToolButtonFromAction(self.maskWidget.polygonAction)
@@ -191,15 +192,13 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.maskWidget.maskStateGroup.button(1).click()
self.qapp.processEvents()
self._drawPolygon()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertFalse(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# unmask same region
self.maskWidget.maskStateGroup.button(0).click()
self.qapp.processEvents()
self._drawPolygon()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertTrue(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# Test draw pencil #
toolButton = getQToolButtonFromAction(self.maskWidget.pencilAction)
@@ -213,15 +212,13 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.maskWidget.maskStateGroup.button(1).click()
self.qapp.processEvents()
self._drawPencil()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertFalse(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# unmask same region
self.maskWidget.maskStateGroup.button(0).click()
self.qapp.processEvents()
self._drawPencil()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertTrue(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# Test no draw tool #
toolButton = getQToolButtonFromAction(self.maskWidget.browseAction)
@@ -232,11 +229,12 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def __loadSave(self, file_format):
self.plot.addScatter(
- x=numpy.arange(256),
- y=25 * (numpy.arange(256) % 10),
- value=numpy.random.random(256),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
+ x=numpy.arange(256),
+ y=25 * (numpy.arange(256) % 10),
+ value=numpy.random.random(256),
+ legend="test",
+ )
+ self.plot.setActiveScatter("test")
self.plot.resetZoom()
self.qapp.processEvents()
@@ -250,16 +248,18 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.assertFalse(numpy.all(numpy.equal(ref_mask, 0)))
with temp_dir() as tmp:
- mask_filename = os.path.join(tmp, 'mask.' + file_format)
+ mask_filename = os.path.join(tmp, "mask." + file_format)
self.maskWidget.save(mask_filename, file_format)
self.maskWidget.resetSelectionMask()
self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.maskWidget.load(mask_filename)
- self.assertTrue(numpy.all(numpy.equal(
- self.maskWidget.getSelectionMask(), ref_mask)))
+ self.assertTrue(
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), ref_mask))
+ )
def testLoadSaveNpy(self):
self.__loadSave("npy")
@@ -270,22 +270,24 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def testSigMaskChangedEmitted(self):
self.qapp.processEvents()
self.plot.addScatter(
- x=numpy.arange(1000),
- y=1000 * (numpy.arange(1000) % 20),
- value=numpy.ones((1000,)),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
+ x=numpy.arange(1000),
+ y=1000 * (numpy.arange(1000) % 20),
+ value=numpy.ones((1000,)),
+ legend="test",
+ )
+ self.plot.setActiveScatter("test")
self.plot.resetZoom()
self.qapp.processEvents()
- self.plot.remove('test', kind='scatter')
+ self.plot.remove("test", kind="scatter")
self.qapp.processEvents()
self.plot.addScatter(
- x=numpy.arange(1000),
- y=1000 * (numpy.arange(1000) % 20),
- value=numpy.random.random(1000),
- legend='test')
+ x=numpy.arange(1000),
+ y=1000 * (numpy.arange(1000) % 20),
+ value=numpy.random.random(1000),
+ legend="test",
+ )
l = []
diff --git a/src/silx/gui/plot/test/testScatterView.py b/src/silx/gui/plot/test/testScatterView.py
index 692612d..d6853b1 100644
--- a/src/silx/gui/plot/test/testScatterView.py
+++ b/src/silx/gui/plot/test/testScatterView.py
@@ -28,8 +28,6 @@ __license__ = "MIT"
__date__ = "06/03/2018"
-import unittest
-
import numpy
from silx.gui.plot.items import Axis, Scatter
@@ -83,7 +81,7 @@ class TestScatterView(PlotWidgetTestCase):
scale = self.plot.getYAxis().getScale()
self.assertEqual(scale, Axis.LINEAR)
- title = 'Test ScatterView'
+ title = "Test ScatterView"
self.plot.setGraphTitle(title)
self.assertEqual(self.plot.getGraphTitle(), title)
@@ -107,13 +105,15 @@ class TestScatterView(PlotWidgetTestCase):
_pts = 100
_levels = 100
_fwhm = 50
- x = numpy.random.rand(_pts)*_levels
- y = numpy.random.rand(_pts)*_levels
- value = numpy.random.rand(_pts)*_levels
- x0 = x[int(_pts/2)]
- y0 = x[int(_pts/2)]
- #2D Gaussian kernel
- alpha = numpy.exp(-4*numpy.log(2) * ((x-x0)**2 + (y-y0)**2) / _fwhm**2)
+ x = numpy.random.rand(_pts) * _levels
+ y = numpy.random.rand(_pts) * _levels
+ value = numpy.random.rand(_pts) * _levels
+ x0 = x[int(_pts / 2)]
+ y0 = x[int(_pts / 2)]
+ # 2D Gaussian kernel
+ alpha = numpy.exp(
+ -4 * numpy.log(2) * ((x - x0) ** 2 + (y - y0) ** 2) / _fwhm**2
+ )
self.plot.setData(x, y, value, alpha=alpha)
self.qapp.processEvents()
diff --git a/src/silx/gui/plot/test/testStackView.py b/src/silx/gui/plot/test/testStackView.py
index aba8678..5e0ead5 100644
--- a/src/silx/gui/plot/test/testStackView.py
+++ b/src/silx/gui/plot/test/testStackView.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -28,7 +28,6 @@ __license__ = "MIT"
__date__ = "20/03/2017"
-import unittest
import numpy
from silx.gui.utils.testutils import TestCaseQt, SignalListener
@@ -49,8 +48,10 @@ class TestStackView(TestCaseQt):
self.stackview.show()
self.qWaitForWindowExposed(self.stackview)
self.mystack = numpy.fromfunction(
- lambda i, j, k: numpy.sin(i/15.) + numpy.cos(j/4.) + 2 * numpy.sin(k/6.),
- (10, 20, 30)
+ lambda i, j, k: numpy.sin(i / 15.0)
+ + numpy.cos(j / 4.0)
+ + 2 * numpy.sin(k / 6.0),
+ (10, 20, 30),
)
def tearDown(self):
@@ -74,13 +75,11 @@ class TestStackView(TestCaseQt):
def testSetStack(self):
self.stackview.setStack(self.mystack)
- self.stackview.setColormap("viridis", autoscale=True)
+ self.stackview.setColormap("viridis")
my_trans_stack, params = self.stackview.getStack()
self.assertEqual(my_trans_stack.shape, self.mystack.shape)
- self.assertTrue(numpy.array_equal(self.mystack,
- my_trans_stack))
- self.assertEqual(params["colormap"]["name"],
- "viridis")
+ self.assertTrue(numpy.array_equal(self.mystack, my_trans_stack))
+ self.assertEqual(params["colormap"]["name"], "viridis")
def testSetStackPerspective(self):
self.stackview.setStack(self.mystack, perspective=1)
@@ -88,10 +87,15 @@ class TestStackView(TestCaseQt):
my_trans_stack, params = self.stackview.getCurrentView()
# get stack returns the transposed data, depending on the perspective
- self.assertEqual(my_trans_stack.shape,
- (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2]))
- self.assertTrue(numpy.array_equal(numpy.transpose(self.mystack, axes=(1, 0, 2)),
- my_trans_stack))
+ self.assertEqual(
+ my_trans_stack.shape,
+ (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2]),
+ )
+ self.assertTrue(
+ numpy.array_equal(
+ numpy.transpose(self.mystack, axes=(1, 0, 2)), my_trans_stack
+ )
+ )
def testSetStackListOfImages(self):
loi = [self.mystack[i] for i in range(self.mystack.shape[0])]
@@ -100,10 +104,8 @@ class TestStackView(TestCaseQt):
my_orig_stack, params = self.stackview.getStack(returnNumpyArray=True)
my_trans_stack, params = self.stackview.getStack(returnNumpyArray=True)
self.assertEqual(my_trans_stack.shape, self.mystack.shape)
- self.assertTrue(numpy.array_equal(self.mystack,
- my_trans_stack))
- self.assertTrue(numpy.array_equal(self.mystack,
- my_orig_stack))
+ self.assertTrue(numpy.array_equal(self.mystack, my_trans_stack))
+ self.assertTrue(numpy.array_equal(self.mystack, my_orig_stack))
self.assertIsInstance(my_trans_stack, numpy.ndarray)
self.stackview.setStack(loi, perspective=2)
@@ -113,88 +115,100 @@ class TestStackView(TestCaseQt):
self.assertIs(my_orig_stack, loi)
# getCurrentView(copy=False) returns a ListOfImages whose .images
# attr is the original data
- self.assertEqual(my_trans_stack.shape,
- (self.mystack.shape[2], self.mystack.shape[0], self.mystack.shape[1]))
- self.assertTrue(numpy.array_equal(numpy.array(my_trans_stack),
- numpy.transpose(self.mystack, axes=(2, 0, 1))))
- self.assertIsInstance(my_trans_stack,
- ListOfImages) # returnNumpyArray=False by default in getStack
+ self.assertEqual(
+ my_trans_stack.shape,
+ (self.mystack.shape[2], self.mystack.shape[0], self.mystack.shape[1]),
+ )
+ self.assertTrue(
+ numpy.array_equal(
+ numpy.array(my_trans_stack),
+ numpy.transpose(self.mystack, axes=(2, 0, 1)),
+ )
+ )
+ self.assertIsInstance(
+ my_trans_stack, ListOfImages
+ ) # returnNumpyArray=False by default in getStack
self.assertIs(my_trans_stack.images, loi)
def testPerspective(self):
self.stackview.setStack(numpy.arange(24).reshape((2, 3, 4)))
- self.assertEqual(self.stackview._perspective, 0,
- "Default perspective is not 0 (dim1-dim2).")
+ self.assertEqual(
+ self.stackview._perspective, 0, "Default perspective is not 0 (dim1-dim2)."
+ )
self.stackview._StackView__planeSelection.setPerspective(1)
- self.assertEqual(self.stackview._perspective, 1,
- "Plane selection combobox not updating perspective")
+ self.assertEqual(
+ self.stackview._perspective,
+ 1,
+ "Plane selection combobox not updating perspective",
+ )
self.stackview.setStack(numpy.arange(6).reshape((1, 2, 3)))
- self.assertEqual(self.stackview._perspective, 1,
- "Perspective not preserved when calling setStack "
- "without specifying the perspective parameter.")
+ self.assertEqual(
+ self.stackview._perspective,
+ 1,
+ "Perspective not preserved when calling setStack "
+ "without specifying the perspective parameter.",
+ )
self.stackview.setStack(numpy.arange(24).reshape((2, 3, 4)), perspective=2)
- self.assertEqual(self.stackview._perspective, 2,
- "Perspective not set in setStack(..., perspective=2).")
+ self.assertEqual(
+ self.stackview._perspective,
+ 2,
+ "Perspective not set in setStack(..., perspective=2).",
+ )
def testDefaultTitle(self):
"""Test that the plot title contains the proper Z information"""
- self.stackview.setStack(numpy.arange(24).reshape((4, 3, 2)),
- calibrations=[(0, 1), (-10, 10), (3.14, 3.14)])
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=0")
+ self.stackview.setStack(
+ numpy.arange(24).reshape((4, 3, 2)),
+ calibrations=[(0, 1), (-10, 10), (3.14, 3.14)],
+ )
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=0")
self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=2")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=2")
self.stackview._StackView__planeSelection.setPerspective(1)
self.stackview.setFrameNumber(0)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=-10")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=-10")
self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=10")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=10")
self.stackview._StackView__planeSelection.setPerspective(2)
self.stackview.setFrameNumber(0)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=3.14")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=3.14")
self.stackview.setFrameNumber(1)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=6.28")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=6.28")
def testCustomTitle(self):
"""Test setting the plot title with a user defined callback"""
- self.stackview.setStack(numpy.arange(24).reshape((4, 3, 2)),
- calibrations=[(0, 1), (-10, 10), (3.14, 3.14)])
+ self.stackview.setStack(
+ numpy.arange(24).reshape((4, 3, 2)),
+ calibrations=[(0, 1), (-10, 10), (3.14, 3.14)],
+ )
def title_callback(frame_idx):
return "Cubed index title %d" % (frame_idx**3)
self.stackview.setTitleCallback(title_callback)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 0")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Cubed index title 0")
self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 8")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Cubed index title 8")
# perspective should not matter, only frame index
self.stackview._StackView__planeSelection.setPerspective(1)
self.stackview.setFrameNumber(0)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 0")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Cubed index title 0")
self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 8")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Cubed index title 8")
with self.assertRaises(TypeError):
# setTitleCallback should not accept non-callable objects like strings
self.stackview.setTitleCallback(
- "Là, vous faites sirop de vingt-et-un et vous dites : "
- "beau sirop, mi-sirop, siroté, gagne-sirop, sirop-grelot,"
- " passe-montagne, sirop au bon goût.")
+ "Là, vous faites sirop de vingt-et-un et vous dites : "
+ "beau sirop, mi-sirop, siroté, gagne-sirop, sirop-grelot,"
+ " passe-montagne, sirop au bon goût."
+ )
def testStackFrameNumber(self):
self.stackview.setStack(self.mystack)
@@ -217,8 +231,10 @@ class TestStackViewMainWindow(TestCaseQt):
self.stackview.show()
self.qWaitForWindowExposed(self.stackview)
self.mystack = numpy.fromfunction(
- lambda i, j, k: numpy.sin(i/15.) + numpy.cos(j/4.) + 2 * numpy.sin(k/6.),
- (10, 20, 30)
+ lambda i, j, k: numpy.sin(i / 15.0)
+ + numpy.cos(j / 4.0)
+ + 2 * numpy.sin(k / 6.0),
+ (10, 20, 30),
)
def tearDown(self):
@@ -229,19 +245,22 @@ class TestStackViewMainWindow(TestCaseQt):
def testSetStack(self):
self.stackview.setStack(self.mystack)
- self.stackview.setColormap("viridis", autoscale=True)
+ self.stackview.setColormap("viridis")
my_trans_stack, params = self.stackview.getStack()
self.assertEqual(my_trans_stack.shape, self.mystack.shape)
- self.assertTrue(numpy.array_equal(self.mystack,
- my_trans_stack))
- self.assertEqual(params["colormap"]["name"],
- "viridis")
+ self.assertTrue(numpy.array_equal(self.mystack, my_trans_stack))
+ self.assertEqual(params["colormap"]["name"], "viridis")
def testSetStackPerspective(self):
self.stackview.setStack(self.mystack, perspective=1)
my_trans_stack, params = self.stackview.getCurrentView()
# get stack returns the transposed data, depending on the perspective
- self.assertEqual(my_trans_stack.shape,
- (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2]))
- self.assertTrue(numpy.array_equal(numpy.transpose(self.mystack, axes=(1, 0, 2)),
- my_trans_stack))
+ self.assertEqual(
+ my_trans_stack.shape,
+ (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2]),
+ )
+ self.assertTrue(
+ numpy.array_equal(
+ numpy.transpose(self.mystack, axes=(1, 0, 2)), my_trans_stack
+ )
+ )
diff --git a/src/silx/gui/plot/test/testStats.py b/src/silx/gui/plot/test/testStats.py
index c5d5181..2a2793e 100644
--- a/src/silx/gui/plot/test/testStats.py
+++ b/src/silx/gui/plot/test/testStats.py
@@ -34,13 +34,11 @@ from silx.gui.plot import StatsWidget
from silx.gui.plot.stats import statshandler
from silx.gui.utils.testutils import TestCaseQt, SignalListener
from silx.gui.plot import Plot1D, Plot2D
-from silx.gui.plot3d.SceneWidget import SceneWidget
from silx.gui.plot.items.roi import RectangleROI, PolygonROI
-from silx.gui.plot.tools.roi import RegionOfInterestManager
+from silx.gui.plot.tools.roi import RegionOfInterestManager
from silx.gui.plot.stats.stats import Stats
from silx.gui.plot.CurvesROIWidget import ROI
from silx.utils.testutils import ParametricTestCase
-import unittest
import logging
import numpy
@@ -49,6 +47,7 @@ _logger = logging.getLogger(__name__)
class TestStatsBase(object):
"""Base class for stats TestCase"""
+
def setUp(self):
self.createCurveContext()
self.createImageContext()
@@ -69,51 +68,52 @@ class TestStatsBase(object):
self.plot1d = Plot1D()
x = range(20)
y = range(20)
- self.plot1d.addCurve(x, y, legend='curve0')
+ self.plot1d.addCurve(x, y, legend="curve0")
self.curveContext = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
+ item=self.plot1d.getCurve("curve0"),
plot=self.plot1d,
onlimits=False,
- roi=None)
+ roi=None,
+ )
def createScatterContext(self):
self.scatterPlot = Plot2D()
- lgd = 'scatter plot'
+ lgd = "scatter plot"
self.xScatterData = numpy.array([0, 2, 3, 20, 50, 60, 36])
self.yScatterData = numpy.array([2, 3, 4, 26, 69, 6, 18])
self.valuesScatterData = numpy.array([5, 6, 7, 10, 90, 20, 5])
- self.scatterPlot.addScatter(self.xScatterData, self.yScatterData,
- self.valuesScatterData, legend=lgd)
+ self.scatterPlot.addScatter(
+ self.xScatterData, self.yScatterData, self.valuesScatterData, legend=lgd
+ )
self.scatterContext = stats._ScatterContext(
item=self.scatterPlot.getScatter(lgd),
plot=self.scatterPlot,
onlimits=False,
- roi=None
+ roi=None,
)
def createImageContext(self):
self.plot2d = Plot2D()
- self._imgLgd = 'test image'
- self.imageData = numpy.arange(32*128).reshape(32, 128)
- self.plot2d.addImage(data=self.imageData,
- legend=self._imgLgd, replace=False)
+ self._imgLgd = "test image"
+ self.imageData = numpy.arange(32 * 128).reshape(32, 128)
+ self.plot2d.addImage(data=self.imageData, legend=self._imgLgd, replace=False)
self.imageContext = stats._ImageContext(
item=self.plot2d.getImage(self._imgLgd),
plot=self.plot2d,
onlimits=False,
- roi=None
+ roi=None,
)
def getBasicStats(self):
return {
- 'min': stats.StatMin(),
- 'minCoords': stats.StatCoordMin(),
- 'max': stats.StatMax(),
- 'maxCoords': stats.StatCoordMax(),
- 'std': stats.Stat(name='std', fct=numpy.std),
- 'mean': stats.Stat(name='mean', fct=numpy.mean),
- 'com': stats.StatCOM()
+ "min": stats.StatMin(),
+ "minCoords": stats.StatCoordMin(),
+ "max": stats.StatMax(),
+ "maxCoords": stats.StatCoordMax(),
+ "std": stats.Stat(name="std", fct=numpy.std),
+ "mean": stats.Stat(name="mean", fct=numpy.mean),
+ "com": stats.StatCOM(),
}
@@ -121,6 +121,7 @@ class TestStats(TestStatsBase, TestCaseQt):
"""
Test :class:`BaseClass` class and inheriting classes
"""
+
def setUp(self):
TestCaseQt.setUp(self)
TestStatsBase.setUp(self)
@@ -133,41 +134,50 @@ class TestStats(TestStatsBase, TestCaseQt):
"""Test result for simple stats on a curve"""
_stats = self.getBasicStats()
xData = yData = numpy.array(range(20))
- self.assertEqual(_stats['min'].calculate(self.curveContext), 0)
- self.assertEqual(_stats['max'].calculate(self.curveContext), 19)
- self.assertEqual(_stats['minCoords'].calculate(self.curveContext), (0,))
- self.assertEqual(_stats['maxCoords'].calculate(self.curveContext), (19,))
- self.assertEqual(_stats['std'].calculate(self.curveContext), numpy.std(yData))
- self.assertEqual(_stats['mean'].calculate(self.curveContext), numpy.mean(yData))
+ self.assertEqual(_stats["min"].calculate(self.curveContext), 0)
+ self.assertEqual(_stats["max"].calculate(self.curveContext), 19)
+ self.assertEqual(_stats["minCoords"].calculate(self.curveContext), (0,))
+ self.assertEqual(_stats["maxCoords"].calculate(self.curveContext), (19,))
+ self.assertEqual(_stats["std"].calculate(self.curveContext), numpy.std(yData))
+ self.assertEqual(_stats["mean"].calculate(self.curveContext), numpy.mean(yData))
com = numpy.sum(xData * yData) / numpy.sum(yData)
- self.assertEqual(_stats['com'].calculate(self.curveContext), com)
+ self.assertEqual(_stats["com"].calculate(self.curveContext), com)
def testBasicStatsImage(self):
"""Test result for simple stats on an image"""
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.imageContext), 0)
- self.assertEqual(_stats['max'].calculate(self.imageContext), 128 * 32 - 1)
- self.assertEqual(_stats['minCoords'].calculate(self.imageContext), (0, 0))
- self.assertEqual(_stats['maxCoords'].calculate(self.imageContext), (127, 31))
- self.assertEqual(_stats['std'].calculate(self.imageContext), numpy.std(self.imageData))
- self.assertEqual(_stats['mean'].calculate(self.imageContext), numpy.mean(self.imageData))
+ self.assertEqual(_stats["min"].calculate(self.imageContext), 0)
+ self.assertEqual(_stats["max"].calculate(self.imageContext), 128 * 32 - 1)
+ self.assertEqual(_stats["minCoords"].calculate(self.imageContext), (0, 0))
+ self.assertEqual(_stats["maxCoords"].calculate(self.imageContext), (127, 31))
+ self.assertEqual(
+ _stats["std"].calculate(self.imageContext), numpy.std(self.imageData)
+ )
+ self.assertEqual(
+ _stats["mean"].calculate(self.imageContext), numpy.mean(self.imageData)
+ )
yData = numpy.sum(self.imageData.astype(numpy.float64), axis=1)
xData = numpy.sum(self.imageData.astype(numpy.float64), axis=0)
dataXRange = range(self.imageData.shape[1])
dataYRange = range(self.imageData.shape[0])
- ycom = numpy.sum(yData*dataYRange) / numpy.sum(yData)
- xcom = numpy.sum(xData*dataXRange) / numpy.sum(xData)
+ ycom = numpy.sum(yData * dataYRange) / numpy.sum(yData)
+ xcom = numpy.sum(xData * dataXRange) / numpy.sum(xData)
- self.assertEqual(_stats['com'].calculate(self.imageContext), (xcom, ycom))
+ self.assertEqual(_stats["com"].calculate(self.imageContext), (xcom, ycom))
def testStatsImageAdv(self):
"""Test that scale and origin are taking into account for images"""
image2Data = numpy.arange(32 * 128).reshape(32, 128)
- self.plot2d.addImage(data=image2Data, legend=self._imgLgd,
- replace=True, origin=(100, 10), scale=(2, 0.5))
+ self.plot2d.addImage(
+ data=image2Data,
+ legend=self._imgLgd,
+ replace=True,
+ origin=(100, 10),
+ scale=(2, 0.5),
+ )
image2Context = stats._ImageContext(
item=self.plot2d.getImage(self._imgLgd),
plot=self.plot2d,
@@ -175,18 +185,19 @@ class TestStats(TestStatsBase, TestCaseQt):
roi=None,
)
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(image2Context), 0)
+ self.assertEqual(_stats["min"].calculate(image2Context), 0)
+ self.assertEqual(_stats["max"].calculate(image2Context), 128 * 32 - 1)
+ self.assertEqual(_stats["minCoords"].calculate(image2Context), (100, 10))
self.assertEqual(
- _stats['max'].calculate(image2Context), 128 * 32 - 1)
+ _stats["maxCoords"].calculate(image2Context),
+ (127 * 2.0 + 100, 31 * 0.5 + 10),
+ )
self.assertEqual(
- _stats['minCoords'].calculate(image2Context), (100, 10))
+ _stats["std"].calculate(image2Context), numpy.std(self.imageData)
+ )
self.assertEqual(
- _stats['maxCoords'].calculate(image2Context), (127*2. + 100,
- 31 * 0.5 + 10))
- self.assertEqual(_stats['std'].calculate(image2Context),
- numpy.std(self.imageData))
- self.assertEqual(_stats['mean'].calculate(image2Context),
- numpy.mean(self.imageData))
+ _stats["mean"].calculate(image2Context), numpy.mean(self.imageData)
+ )
yData = numpy.sum(self.imageData, axis=1)
xData = numpy.sum(self.imageData, axis=0)
@@ -196,30 +207,36 @@ class TestStats(TestStatsBase, TestCaseQt):
ycom = numpy.sum(yData * dataYRange) / numpy.sum(yData)
ycom = (ycom * 0.5) + 10
xcom = numpy.sum(xData * dataXRange) / numpy.sum(xData)
- xcom = (xcom * 2.) + 100
- self.assertTrue(numpy.allclose(
- _stats['com'].calculate(image2Context), (xcom, ycom)))
+ xcom = (xcom * 2.0) + 100
+ self.assertTrue(
+ numpy.allclose(_stats["com"].calculate(image2Context), (xcom, ycom))
+ )
def testBasicStatsScatter(self):
"""Test result for simple stats on a scatter"""
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.scatterContext), 5)
- self.assertEqual(_stats['max'].calculate(self.scatterContext), 90)
- self.assertEqual(_stats['minCoords'].calculate(self.scatterContext), (0, 2))
- self.assertEqual(_stats['maxCoords'].calculate(self.scatterContext), (50, 69))
- self.assertEqual(_stats['std'].calculate(self.scatterContext), numpy.std(self.valuesScatterData))
- self.assertEqual(_stats['mean'].calculate(self.scatterContext), numpy.mean(self.valuesScatterData))
+ self.assertEqual(_stats["min"].calculate(self.scatterContext), 5)
+ self.assertEqual(_stats["max"].calculate(self.scatterContext), 90)
+ self.assertEqual(_stats["minCoords"].calculate(self.scatterContext), (0, 2))
+ self.assertEqual(_stats["maxCoords"].calculate(self.scatterContext), (50, 69))
+ self.assertEqual(
+ _stats["std"].calculate(self.scatterContext),
+ numpy.std(self.valuesScatterData),
+ )
+ self.assertEqual(
+ _stats["mean"].calculate(self.scatterContext),
+ numpy.mean(self.valuesScatterData),
+ )
data = self.valuesScatterData.astype(numpy.float64)
comx = numpy.sum(self.xScatterData * data) / numpy.sum(data)
comy = numpy.sum(self.yScatterData * data) / numpy.sum(data)
- self.assertEqual(_stats['com'].calculate(self.scatterContext),
- (comx, comy))
+ self.assertEqual(_stats["com"].calculate(self.scatterContext), (comx, comy))
def testKindNotManagedByStat(self):
"""Make sure an exception is raised if we try to execute calculate
of the base class"""
- b = stats.StatBase(name='toto', compatibleKinds='curve')
+ b = stats.StatBase(name="toto", compatibleKinds="curve")
with self.assertRaises(NotImplementedError):
b.calculate(self.imageContext)
@@ -228,7 +245,7 @@ class TestStats(TestStatsBase, TestCaseQt):
Make sure an error is raised if we try to calculate a statistic with
a context not managed
"""
- myStat = stats.Stat(name='toto', fct=numpy.std, kinds=('curve'))
+ myStat = stats.Stat(name="toto", fct=numpy.std, kinds=("curve"))
myStat.calculate(self.curveContext)
with self.assertRaises(ValueError):
myStat.calculate(self.scatterContext)
@@ -240,43 +257,48 @@ class TestStats(TestStatsBase, TestCaseQt):
self.plot1d.getXAxis().setLimitsConstraints(minPos=2, maxPos=5)
curveContextOnLimits = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
+ item=self.plot1d.getCurve("curve0"),
plot=self.plot1d,
onlimits=True,
- roi=None)
+ roi=None,
+ )
self.assertEqual(stat.calculate(curveContextOnLimits), 2)
self.plot2d.getXAxis().setLimitsConstraints(minPos=32)
imageContextOnLimits = stats._ImageContext(
- item=self.plot2d.getImage('test image'),
+ item=self.plot2d.getImage("test image"),
plot=self.plot2d,
onlimits=True,
- roi=None)
+ roi=None,
+ )
self.assertEqual(stat.calculate(imageContextOnLimits), 32)
self.scatterPlot.getXAxis().setLimitsConstraints(minPos=40)
scatterContextOnLimits = stats._ScatterContext(
- item=self.scatterPlot.getScatter('scatter plot'),
+ item=self.scatterPlot.getScatter("scatter plot"),
plot=self.scatterPlot,
onlimits=True,
- roi=None)
+ roi=None,
+ )
self.assertEqual(stat.calculate(scatterContextOnLimits), 20)
class TestStatsFormatter(TestCaseQt):
"""Simple test to check usage of the :class:`StatsFormatter`"""
+
def setUp(self):
TestCaseQt.setUp(self)
self.plot1d = Plot1D()
x = range(20)
y = range(20)
- self.plot1d.addCurve(x, y, legend='curve0')
+ self.plot1d.addCurve(x, y, legend="curve0")
self.curveContext = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
+ item=self.plot1d.getCurve("curve0"),
plot=self.plot1d,
onlimits=False,
- roi=None)
+ roi=None,
+ )
self.stat = stats.StatMin()
@@ -291,27 +313,30 @@ class TestStatsFormatter(TestCaseQt):
simple cast to str"""
emptyFormatter = statshandler.StatFormatter()
self.assertEqual(
- emptyFormatter.format(self.stat.calculate(self.curveContext)), '0.000')
+ emptyFormatter.format(self.stat.calculate(self.curveContext)), "0.000"
+ )
def testSettedFormatter(self):
"""Make sure a formatter with no formatter definition will return a
simple cast to str"""
- formatter= statshandler.StatFormatter(formatter='{0:.3f}')
+ formatter = statshandler.StatFormatter(formatter="{0:.3f}")
self.assertEqual(
- formatter.format(self.stat.calculate(self.curveContext)), '0.000')
+ formatter.format(self.stat.calculate(self.curveContext)), "0.000"
+ )
class TestStatsHandler(TestCaseQt):
- """Make sure the StatHandler is correctly making the link between
+ """Make sure the StatHandler is correctly making the link between
:class:`StatBase` and :class:`StatFormatter` and checking the API is valid
"""
+
def setUp(self):
TestCaseQt.setUp(self)
self.plot1d = Plot1D()
x = range(20)
y = range(20)
- self.plot1d.addCurve(x, y, legend='curve0')
- self.curveItem = self.plot1d.getCurve('curve0')
+ self.plot1d.addCurve(x, y, legend="curve0")
+ self.curveItem = self.plot1d.getCurve("curve0")
self.stat = stats.StatMin()
@@ -324,91 +349,94 @@ class TestStatsHandler(TestCaseQt):
def testConstructor(self):
"""Make sure the constructor can deal will all possible arguments:
-
+
* tuple of :class:`StatBase` derivated classes
* tuple of tuples (:class:`StatBase`, :class:`StatFormatter`)
* tuple of tuples (str, pointer to function, kind)
"""
- handler0 = statshandler.StatsHandler(
- (stats.StatMin(), stats.StatMax())
- )
+ handler0 = statshandler.StatsHandler((stats.StatMin(), stats.StatMax()))
- res = handler0.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('min' in res)
- self.assertEqual(res['min'], '0')
- self.assertTrue('max' in res)
- self.assertEqual(res['max'], '19')
+ res = handler0.calculate(item=self.curveItem, plot=self.plot1d, onlimits=False)
+ self.assertTrue("min" in res)
+ self.assertEqual(res["min"], "0")
+ self.assertTrue("max" in res)
+ self.assertEqual(res["max"], "19")
handler1 = statshandler.StatsHandler(
(
(stats.StatMin(), statshandler.StatFormatter(formatter=None)),
- (stats.StatMax(), statshandler.StatFormatter())
+ (stats.StatMax(), statshandler.StatFormatter()),
)
)
- res = handler1.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('min' in res)
- self.assertEqual(res['min'], '0')
- self.assertTrue('max' in res)
- self.assertEqual(res['max'], '19.000')
+ res = handler1.calculate(item=self.curveItem, plot=self.plot1d, onlimits=False)
+ self.assertTrue("min" in res)
+ self.assertEqual(res["min"], "0")
+ self.assertTrue("max" in res)
+ self.assertEqual(res["max"], "19.000")
handler2 = statshandler.StatsHandler(
+ ((stats.StatMin(), None), (stats.StatMax(), statshandler.StatFormatter()))
+ )
+
+ res = handler2.calculate(item=self.curveItem, plot=self.plot1d, onlimits=False)
+ self.assertTrue("min" in res)
+ self.assertEqual(res["min"], "0")
+ self.assertTrue("max" in res)
+ self.assertEqual(res["max"], "19.000")
+
+ handler3 = statshandler.StatsHandler(
(
- (stats.StatMin(), None),
- (stats.StatMax(), statshandler.StatFormatter())
- ))
-
- res = handler2.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('min' in res)
- self.assertEqual(res['min'], '0')
- self.assertTrue('max' in res)
- self.assertEqual(res['max'], '19.000')
-
- handler3 = statshandler.StatsHandler((
- (('amin', numpy.argmin), statshandler.StatFormatter()),
- ('amax', numpy.argmax)
- ))
-
- res = handler3.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('amin' in res)
- self.assertEqual(res['amin'], '0.000')
- self.assertTrue('amax' in res)
- self.assertEqual(res['amax'], '19')
+ (("amin", numpy.argmin), statshandler.StatFormatter()),
+ ("amax", numpy.argmax),
+ )
+ )
+
+ res = handler3.calculate(item=self.curveItem, plot=self.plot1d, onlimits=False)
+ self.assertTrue("amin" in res)
+ self.assertEqual(res["amin"], "0.000")
+ self.assertTrue("amax" in res)
+ self.assertEqual(res["amax"], "19")
with self.assertRaises(ValueError):
- statshandler.StatsHandler(('name'))
+ statshandler.StatsHandler(("name"))
class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
"""Basic test for StatsWidget with curves"""
+
def setUp(self):
TestCaseQt.setUp(self)
self.plot = Plot1D()
self.plot.show()
x = range(20)
y = range(20)
- self.plot.addCurve(x, y, legend='curve0')
+ self.plot.addCurve(x, y, legend="curve0")
y = range(12, 32)
- self.plot.addCurve(x, y, legend='curve1')
+ self.plot.addCurve(x, y, legend="curve1")
y = range(-2, 18)
- self.plot.addCurve(x, y, legend='curve2')
+ self.plot.addCurve(x, y, legend="curve2")
self.widget = StatsWidget.StatsWidget(plot=self.plot)
self.statsTable = self.widget._statsTable
- mystats = statshandler.StatsHandler((
- stats.StatMin(),
- (stats.StatCoordMin(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatMax(),
- (stats.StatCoordMax(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatDelta(),
- ('std', numpy.std),
- ('mean', numpy.mean),
- stats.StatCOM()
- ))
+ mystats = statshandler.StatsHandler(
+ (
+ stats.StatMin(),
+ (
+ stats.StatCoordMin(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ stats.StatMax(),
+ (
+ stats.StatCoordMax(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ stats.StatDelta(),
+ ("std", numpy.std),
+ ("mean", numpy.mean),
+ stats.StatCOM(),
+ )
+ )
self.statsTable.setStats(mystats)
@@ -456,42 +484,44 @@ class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
def testRemoveCurve(self):
"""Make sure the Curves stats take into account the curve removal from
plot"""
- self.plot.removeCurve('curve2')
+ self.plot.removeCurve("curve2")
self.assertEqual(self.statsTable.rowCount(), 2)
for iRow in range(2):
- self.assertTrue(self.statsTable.item(iRow, 0).text() in ('curve0', 'curve1'))
+ self.assertTrue(
+ self.statsTable.item(iRow, 0).text() in ("curve0", "curve1")
+ )
- self.plot.removeCurve('curve0')
+ self.plot.removeCurve("curve0")
self.assertEqual(self.statsTable.rowCount(), 1)
- self.plot.removeCurve('curve1')
+ self.plot.removeCurve("curve1")
self.assertEqual(self.statsTable.rowCount(), 0)
def testAddCurve(self):
"""Make sure the Curves stats take into account the add curve action"""
- self.plot.addCurve(legend='curve3', x=range(10), y=range(10))
+ self.plot.addCurve(legend="curve3", x=range(10), y=range(10))
self.assertEqual(self.statsTable.rowCount(), 4)
def testUpdateCurveFromAddCurve(self):
"""Make sure the stats of the cuve will be removed after updating a
curve"""
- self.plot.addCurve(legend='curve0', x=range(10), y=range(10))
+ self.plot.addCurve(legend="curve0", x=range(10), y=range(10))
self.qapp.processEvents()
self.assertEqual(self.statsTable.rowCount(), 3)
- curve = self.plot._getItem(kind='curve', legend='curve0')
+ curve = self.plot._getItem(kind="curve", legend="curve0")
tableItems = self.statsTable._itemToTableItems(curve)
- self.assertEqual(tableItems['max'].text(), '9')
+ self.assertEqual(tableItems["max"].text(), "9")
def testUpdateCurveFromCurveObj(self):
- self.plot.getCurve('curve0').setData(x=range(4), y=range(4))
+ self.plot.getCurve("curve0").setData(x=range(4), y=range(4))
self.qapp.processEvents()
self.assertEqual(self.statsTable.rowCount(), 3)
- curve = self.plot._getItem(kind='curve', legend='curve0')
+ curve = self.plot._getItem(kind="curve", legend="curve0")
tableItems = self.statsTable._itemToTableItems(curve)
- self.assertEqual(tableItems['max'].text(), '3')
+ self.assertEqual(tableItems["max"].text(), "3")
def testSetAnotherPlot(self):
plot2 = Plot1D()
- plot2.addCurve(x=range(26), y=range(26), legend='new curve')
+ plot2.addCurve(x=range(26), y=range(26), legend="new curve")
self.statsTable.setPlot(plot2)
self.assertEqual(self.statsTable.rowCount(), 1)
self.qapp.processEvents()
@@ -501,50 +531,62 @@ class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
def testUpdateMode(self):
"""Make sure the update modes are well take into account"""
- self.plot.setActiveCurve('curve0')
+ self.plot.setActiveCurve("curve0")
for display_only_active in (True, False):
with self.subTest(display_only_active=display_only_active):
self.widget.setDisplayOnlyActiveItem(display_only_active)
- self.plot.getCurve('curve0').setData(x=range(4), y=range(4))
+ self.plot.getCurve("curve0").setData(x=range(4), y=range(4))
self.widget.setUpdateMode(StatsWidget.UpdateMode.AUTO)
update_stats_action = self.widget._options.getUpdateStatsAction()
# test from api
- self.assertEqual(self.widget.getUpdateMode(), StatsWidget.UpdateMode.AUTO)
+ self.assertEqual(
+ self.widget.getUpdateMode(), StatsWidget.UpdateMode.AUTO
+ )
self.widget.show()
# check stats change in auto mode
- self.plot.getCurve('curve0').setData(x=range(4), y=range(-1, 3))
+ self.plot.getCurve("curve0").setData(x=range(4), y=range(-1, 3))
self.qapp.processEvents()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == -1.)
+ tableItems = self.statsTable._itemToTableItems(
+ self.plot.getCurve("curve0")
+ )
+ curve0_min = tableItems["min"].text()
+ self.assertTrue(float(curve0_min) == -1.0)
- self.plot.getCurve('curve0').setData(x=range(4), y=range(1, 5))
+ self.plot.getCurve("curve0").setData(x=range(4), y=range(1, 5))
self.qapp.processEvents()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == 1.)
+ tableItems = self.statsTable._itemToTableItems(
+ self.plot.getCurve("curve0")
+ )
+ curve0_min = tableItems["min"].text()
+ self.assertTrue(float(curve0_min) == 1.0)
# check stats change in manual mode only if requested
self.widget.setUpdateMode(StatsWidget.UpdateMode.MANUAL)
- self.assertEqual(self.widget.getUpdateMode(), StatsWidget.UpdateMode.MANUAL)
+ self.assertEqual(
+ self.widget.getUpdateMode(), StatsWidget.UpdateMode.MANUAL
+ )
- self.plot.getCurve('curve0').setData(x=range(4), y=range(2, 6))
+ self.plot.getCurve("curve0").setData(x=range(4), y=range(2, 6))
self.qapp.processEvents()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == 1.)
+ tableItems = self.statsTable._itemToTableItems(
+ self.plot.getCurve("curve0")
+ )
+ curve0_min = tableItems["min"].text()
+ self.assertTrue(float(curve0_min) == 1.0)
update_stats_action.trigger()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == 2.)
+ tableItems = self.statsTable._itemToTableItems(
+ self.plot.getCurve("curve0")
+ )
+ curve0_min = tableItems["min"].text()
+ self.assertTrue(float(curve0_min) == 2.0)
def testItemHidden(self):
"""Test if an item is hide, then the associated stats item is also
hide"""
- curve0 = self.plot.getCurve('curve0')
- curve1 = self.plot.getCurve('curve1')
- curve2 = self.plot.getCurve('curve2')
+ curve0 = self.plot.getCurve("curve0")
+ curve1 = self.plot.getCurve("curve1")
+ curve2 = self.plot.getCurve("curve2")
self.plot.show()
self.widget.show()
@@ -563,8 +605,8 @@ class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
self.qapp.processEvents()
self.assertTrue(self.statsTable.isRowHidden(1))
tableItems = self.statsTable._itemToTableItems(curve2)
- curve2_min = tableItems['min'].text()
- self.assertTrue(float(curve2_min) == -2.)
+ curve2_min = tableItems["min"].text()
+ self.assertTrue(float(curve2_min) == -2.0)
curve0.setVisible(False)
curve1.setVisible(False)
@@ -578,27 +620,38 @@ class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
class TestStatsWidgetWithImages(TestCaseQt):
"""Basic test for StatsWidget with images"""
- IMAGE_LEGEND = 'test image'
+ IMAGE_LEGEND = "test image"
def setUp(self):
TestCaseQt.setUp(self)
self.plot = Plot2D()
- self.plot.addImage(data=numpy.arange(128*128).reshape(128, 128),
- legend=self.IMAGE_LEGEND, replace=False)
+ self.plot.addImage(
+ data=numpy.arange(128 * 128).reshape(128, 128),
+ legend=self.IMAGE_LEGEND,
+ replace=False,
+ )
self.widget = StatsWidget.StatsTable(plot=self.plot)
- mystats = statshandler.StatsHandler((
- (stats.StatMin(), statshandler.StatFormatter()),
- (stats.StatCoordMin(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- (stats.StatMax(), statshandler.StatFormatter()),
- (stats.StatCoordMax(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- (stats.StatDelta(), statshandler.StatFormatter()),
- ('std', numpy.std),
- ('mean', numpy.mean),
- (stats.StatCOM(), statshandler.StatFormatter(None))
- ))
+ mystats = statshandler.StatsHandler(
+ (
+ (stats.StatMin(), statshandler.StatFormatter()),
+ (
+ stats.StatCoordMin(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ (stats.StatMax(), statshandler.StatFormatter()),
+ (
+ stats.StatCoordMax(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ (stats.StatDelta(), statshandler.StatFormatter()),
+ ("std", numpy.std),
+ ("mean", numpy.mean),
+ (stats.StatCOM(), statshandler.StatFormatter(None)),
+ )
+ )
self.widget.setStats(mystats)
@@ -613,17 +666,16 @@ class TestStatsWidgetWithImages(TestCaseQt):
TestCaseQt.tearDown(self)
def test(self):
- image = self.plot._getItem(
- kind='image', legend=self.IMAGE_LEGEND)
+ image = self.plot._getItem(kind="image", legend=self.IMAGE_LEGEND)
tableItems = self.widget._itemToTableItems(image)
- maxText = '{0:.3f}'.format((128 * 128) - 1)
- self.assertEqual(tableItems['legend'].text(), self.IMAGE_LEGEND)
- self.assertEqual(tableItems['min'].text(), '0.000')
- self.assertEqual(tableItems['max'].text(), maxText)
- self.assertEqual(tableItems['delta'].text(), maxText)
- self.assertEqual(tableItems['coords min'].text(), '0.0, 0.0')
- self.assertEqual(tableItems['coords max'].text(), '127.0, 127.0')
+ maxText = "{0:.3f}".format((128 * 128) - 1)
+ self.assertEqual(tableItems["legend"].text(), self.IMAGE_LEGEND)
+ self.assertEqual(tableItems["min"].text(), "0.000")
+ self.assertEqual(tableItems["max"].text(), maxText)
+ self.assertEqual(tableItems["delta"].text(), maxText)
+ self.assertEqual(tableItems["coords min"].text(), "0.0, 0.0")
+ self.assertEqual(tableItems["coords max"].text(), "127.0, 127.0")
def testItemHidden(self):
"""Test if an item is hide, then the associated stats item is also
@@ -638,28 +690,37 @@ class TestStatsWidgetWithImages(TestCaseQt):
class TestStatsWidgetWithScatters(TestCaseQt):
-
- SCATTER_LEGEND = 'scatter plot'
+ SCATTER_LEGEND = "scatter plot"
def setUp(self):
TestCaseQt.setUp(self)
self.scatterPlot = Plot2D()
- self.scatterPlot.addScatter([0, 1, 2, 20, 50, 60],
- [2, 3, 4, 26, 69, 6],
- [5, 6, 7, 10, 90, 20],
- legend=self.SCATTER_LEGEND)
+ self.scatterPlot.addScatter(
+ [0, 1, 2, 20, 50, 60],
+ [2, 3, 4, 26, 69, 6],
+ [5, 6, 7, 10, 90, 20],
+ legend=self.SCATTER_LEGEND,
+ )
self.widget = StatsWidget.StatsTable(plot=self.scatterPlot)
- mystats = statshandler.StatsHandler((
- stats.StatMin(),
- (stats.StatCoordMin(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatMax(),
- (stats.StatCoordMax(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatDelta(),
- ('std', numpy.std),
- ('mean', numpy.mean),
- stats.StatCOM()
- ))
+ mystats = statshandler.StatsHandler(
+ (
+ stats.StatMin(),
+ (
+ stats.StatCoordMin(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ stats.StatMax(),
+ (
+ stats.StatCoordMax(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ stats.StatDelta(),
+ ("std", numpy.std),
+ ("mean", numpy.mean),
+ stats.StatCOM(),
+ )
+ )
self.widget.setStats(mystats)
@@ -674,15 +735,14 @@ class TestStatsWidgetWithScatters(TestCaseQt):
TestCaseQt.tearDown(self)
def testStats(self):
- scatter = self.scatterPlot._getItem(
- kind='scatter', legend=self.SCATTER_LEGEND)
+ scatter = self.scatterPlot._getItem(kind="scatter", legend=self.SCATTER_LEGEND)
tableItems = self.widget._itemToTableItems(scatter)
- self.assertEqual(tableItems['legend'].text(), self.SCATTER_LEGEND)
- self.assertEqual(tableItems['min'].text(), '5')
- self.assertEqual(tableItems['coords min'].text(), '0, 2')
- self.assertEqual(tableItems['max'].text(), '90')
- self.assertEqual(tableItems['coords max'].text(), '50, 69')
- self.assertEqual(tableItems['delta'].text(), '85')
+ self.assertEqual(tableItems["legend"].text(), self.SCATTER_LEGEND)
+ self.assertEqual(tableItems["min"].text(), "5")
+ self.assertEqual(tableItems["coords min"].text(), "0, 2")
+ self.assertEqual(tableItems["max"].text(), "90")
+ self.assertEqual(tableItems["coords max"].text(), "50, 69")
+ self.assertEqual(tableItems["delta"].text(), "85")
class TestEmptyStatsWidget(TestCaseQt):
@@ -694,25 +754,26 @@ class TestEmptyStatsWidget(TestCaseQt):
class TestLineWidget(TestCaseQt):
"""Some test for the StatsLineWidget."""
+
def setUp(self):
TestCaseQt.setUp(self)
- mystats = statshandler.StatsHandler((
- (stats.StatMin(), statshandler.StatFormatter()),
- ))
+ mystats = statshandler.StatsHandler(
+ ((stats.StatMin(), statshandler.StatFormatter()),)
+ )
self.plot = Plot1D()
self.plot.show()
self.x = range(20)
self.y0 = range(20)
- self.curve0 = self.plot.addCurve(self.x, self.y0, legend='curve0')
+ self.plot.addCurve(self.x, self.y0, legend="curve0")
self.y1 = range(12, 32)
- self.plot.addCurve(self.x, self.y1, legend='curve1')
+ self.plot.addCurve(self.x, self.y1, legend="curve1")
self.y2 = range(-2, 18)
- self.plot.addCurve(self.x, self.y2, legend='curve2')
- self.widget = StatsWidget.BasicGridStatsWidget(plot=self.plot,
- kind='curve',
- stats=mystats)
+ self.plot.addCurve(self.x, self.y2, legend="curve2")
+ self.widget = StatsWidget.BasicGridStatsWidget(
+ plot=self.plot, kind="curve", stats=mystats
+ )
def tearDown(self):
Stats._getContext.cache_clear()
@@ -730,27 +791,37 @@ class TestLineWidget(TestCaseQt):
def testProcessing(self):
self.widget._lineStatsWidget.setStatsOnVisibleData(False)
self.qapp.processEvents()
- self.plot.setActiveCurve(legend='curve0')
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '0.000')
- self.plot.setActiveCurve(legend='curve1')
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '12.000')
+ self.plot.setActiveCurve(legend="curve0")
+ self.assertTrue(
+ self.widget._lineStatsWidget._statQlineEdit["min"].text() == "0.000"
+ )
+ self.plot.setActiveCurve(legend="curve1")
+ self.assertTrue(
+ self.widget._lineStatsWidget._statQlineEdit["min"].text() == "12.000"
+ )
self.plot.getXAxis().setLimitsConstraints(minPos=2, maxPos=5)
self.widget.setStatsOnVisibleData(True)
self.qapp.processEvents()
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '14.000')
+ self.assertTrue(
+ self.widget._lineStatsWidget._statQlineEdit["min"].text() == "14.000"
+ )
self.plot.setActiveCurve(None)
self.assertIsNone(self.plot.getActiveCurve())
self.widget.setStatsOnVisibleData(False)
self.qapp.processEvents()
- self.assertFalse(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '14.000')
- self.widget.setKind('image')
- self.plot.addImage(numpy.arange(100*100).reshape(100, 100) + 0.312)
+ self.assertFalse(
+ self.widget._lineStatsWidget._statQlineEdit["min"].text() == "14.000"
+ )
+ self.widget.setKind("image")
+ self.plot.addImage(numpy.arange(100 * 100).reshape(100, 100) + 0.312)
self.qapp.processEvents()
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '0.312')
+ self.assertTrue(
+ self.widget._lineStatsWidget._statQlineEdit["min"].text() == "0.312"
+ )
def testUpdateMode(self):
"""Make sure the update modes are well take into account"""
- self.plot.setActiveCurve(self.curve0)
+ self.plot.setActiveCurve("curve0")
_autoRB = self.widget._options._autoRB
_manualRB = self.widget._options._manualRB
# test from api
@@ -759,10 +830,10 @@ class TestLineWidget(TestCaseQt):
self.assertFalse(_manualRB.isChecked())
# check stats change in auto mode
- curve0_min = self.widget._lineStatsWidget._statQlineEdit['min'].text()
+ curve0_min = self.widget._lineStatsWidget._statQlineEdit["min"].text()
new_y = numpy.array(self.y0) - 2.56
- self.plot.addCurve(x=self.x, y=new_y, legend=self.curve0)
- curve0_min2 = self.widget._lineStatsWidget._statQlineEdit['min'].text()
+ self.plot.addCurve(x=self.x, y=new_y, legend="curve0")
+ curve0_min2 = self.widget._lineStatsWidget._statQlineEdit["min"].text()
self.assertTrue(curve0_min != curve0_min2)
# check stats change in manual mode only if requested
@@ -771,11 +842,11 @@ class TestLineWidget(TestCaseQt):
self.assertTrue(_manualRB.isChecked())
new_y = numpy.array(self.y0) - 1.2
- self.plot.addCurve(x=self.x, y=new_y, legend=self.curve0)
- curve0_min3 = self.widget._lineStatsWidget._statQlineEdit['min'].text()
+ self.plot.addCurve(x=self.x, y=new_y, legend="curve0")
+ curve0_min3 = self.widget._lineStatsWidget._statQlineEdit["min"].text()
self.assertTrue(curve0_min3 == curve0_min2)
self.widget._options._updateRequested()
- curve0_min3 = self.widget._lineStatsWidget._statQlineEdit['min'].text()
+ curve0_min3 = self.widget._lineStatsWidget._statQlineEdit["min"].text()
self.assertTrue(curve0_min3 != curve0_min2)
# test from gui
@@ -791,6 +862,7 @@ class TestLineWidget(TestCaseQt):
class TestUpdateModeWidget(TestCaseQt):
"""Test UpdateModeWidget"""
+
def setUp(self):
TestCaseQt.setUp(self)
self.widget = StatsWidget.UpdateModeWidget(parent=None)
@@ -832,6 +904,7 @@ class TestStatsROI(TestStatsBase, TestCaseQt):
"""
Test stats based on ROI
"""
+
def setUp(self):
TestCaseQt.setUp(self)
self.createRois()
@@ -855,7 +928,7 @@ class TestStatsROI(TestStatsBase, TestCaseQt):
TestCaseQt.tearDown(self)
def createRois(self):
- self._1Droi = ROI(name='my1DRoi', fromdata=2.0, todata=5.0)
+ self._1Droi = ROI(name="my1DRoi", fromdata=2.0, todata=5.0)
self._2Droi_rect = RectangleROI()
self._2Droi_rect.setGeometry(size=(10, 10), origin=(10, 0))
self._2Droi_poly = PolygonROI()
@@ -865,30 +938,32 @@ class TestStatsROI(TestStatsBase, TestCaseQt):
def createCurveContext(self):
TestStatsBase.createCurveContext(self)
self.curveContext = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
+ item=self.plot1d.getCurve("curve0"),
plot=self.plot1d,
onlimits=False,
- roi=self._1Droi)
+ roi=self._1Droi,
+ )
def createHistogramContext(self):
self.plotHisto = Plot1D()
x = range(20)
y = range(20)
- self.plotHisto.addHistogram(x, y, legend='histo0')
+ self.plotHisto.addHistogram(x, y, legend="histo0")
self.histoContext = stats._HistogramContext(
- item=self.plotHisto.getHistogram('histo0'),
+ item=self.plotHisto.getHistogram("histo0"),
plot=self.plotHisto,
onlimits=False,
- roi=self._1Droi)
+ roi=self._1Droi,
+ )
def createScatterContext(self):
TestStatsBase.createScatterContext(self)
self.scatterContext = stats._ScatterContext(
- item=self.scatterPlot.getScatter('scatter plot'),
+ item=self.scatterPlot.getScatter("scatter plot"),
plot=self.scatterPlot,
onlimits=False,
- roi=self._1Droi
+ roi=self._1Droi,
)
def createImageContext(self):
@@ -898,56 +973,68 @@ class TestStatsROI(TestStatsBase, TestCaseQt):
item=self.plot2d.getImage(self._imgLgd),
plot=self.plot2d,
onlimits=False,
- roi=self._2Droi_rect
+ roi=self._2Droi_rect,
)
self.imageContext_2 = stats._ImageContext(
item=self.plot2d.getImage(self._imgLgd),
plot=self.plot2d,
onlimits=False,
- roi=self._2Droi_poly
+ roi=self._2Droi_poly,
)
def testErrors(self):
# test if onlimits is True and give also a roi
with self.assertRaises(ValueError):
- stats._CurveContext(item=self.plot1d.getCurve('curve0'),
- plot=self.plot1d,
- onlimits=True,
- roi=self._1Droi)
+ stats._CurveContext(
+ item=self.plot1d.getCurve("curve0"),
+ plot=self.plot1d,
+ onlimits=True,
+ roi=self._1Droi,
+ )
# test if is a curve context and give an invalid 2D roi
with self.assertRaises(TypeError):
- stats._CurveContext(item=self.plot1d.getCurve('curve0'),
- plot=self.plot1d,
- onlimits=False,
- roi=self._2Droi_rect)
+ stats._CurveContext(
+ item=self.plot1d.getCurve("curve0"),
+ plot=self.plot1d,
+ onlimits=False,
+ roi=self._2Droi_rect,
+ )
def testBasicStatsCurve(self):
"""Test result for simple stats on a curve"""
_stats = self.getBasicStats()
xData = yData = numpy.array(range(0, 10))
- self.assertEqual(_stats['min'].calculate(self.curveContext), 2)
- self.assertEqual(_stats['max'].calculate(self.curveContext), 5)
- self.assertEqual(_stats['minCoords'].calculate(self.curveContext), (2,))
- self.assertEqual(_stats['maxCoords'].calculate(self.curveContext), (5,))
- self.assertEqual(_stats['std'].calculate(self.curveContext), numpy.std(yData[2:6]))
- self.assertEqual(_stats['mean'].calculate(self.curveContext), numpy.mean(yData[2:6]))
+ self.assertEqual(_stats["min"].calculate(self.curveContext), 2)
+ self.assertEqual(_stats["max"].calculate(self.curveContext), 5)
+ self.assertEqual(_stats["minCoords"].calculate(self.curveContext), (2,))
+ self.assertEqual(_stats["maxCoords"].calculate(self.curveContext), (5,))
+ self.assertEqual(
+ _stats["std"].calculate(self.curveContext), numpy.std(yData[2:6])
+ )
+ self.assertEqual(
+ _stats["mean"].calculate(self.curveContext), numpy.mean(yData[2:6])
+ )
com = numpy.sum(xData[2:6] * yData[2:6]) / numpy.sum(yData[2:6])
- self.assertEqual(_stats['com'].calculate(self.curveContext), com)
+ self.assertEqual(_stats["com"].calculate(self.curveContext), com)
def testBasicStatsImageRectRoi(self):
"""Test result for simple stats on an image"""
self.assertEqual(self.imageContext.values.compressed().size, 121)
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.imageContext), 10)
- self.assertEqual(_stats['max'].calculate(self.imageContext), 1300)
- self.assertEqual(_stats['minCoords'].calculate(self.imageContext), (10, 0))
- self.assertEqual(_stats['maxCoords'].calculate(self.imageContext), (20.0, 10.0))
- self.assertAlmostEqual(_stats['std'].calculate(self.imageContext),
- numpy.std(self.imageData[0:11, 10:21]))
- self.assertAlmostEqual(_stats['mean'].calculate(self.imageContext),
- numpy.mean(self.imageData[0:11, 10:21]))
+ self.assertEqual(_stats["min"].calculate(self.imageContext), 10)
+ self.assertEqual(_stats["max"].calculate(self.imageContext), 1300)
+ self.assertEqual(_stats["minCoords"].calculate(self.imageContext), (10, 0))
+ self.assertEqual(_stats["maxCoords"].calculate(self.imageContext), (20.0, 10.0))
+ self.assertAlmostEqual(
+ _stats["std"].calculate(self.imageContext),
+ numpy.std(self.imageData[0:11, 10:21]),
+ )
+ self.assertAlmostEqual(
+ _stats["mean"].calculate(self.imageContext),
+ numpy.mean(self.imageData[0:11, 10:21]),
+ )
compressed_values = self.imageContext.values.compressed()
compressed_values = compressed_values.reshape(11, 11)
@@ -957,41 +1044,47 @@ class TestStatsROI(TestStatsBase, TestCaseQt):
dataYRange = range(11)
dataXRange = range(10, 21)
- ycom = numpy.sum(yData*dataYRange) / numpy.sum(yData)
- xcom = numpy.sum(xData*dataXRange) / numpy.sum(xData)
- self.assertEqual(_stats['com'].calculate(self.imageContext), (xcom, ycom))
+ ycom = numpy.sum(yData * dataYRange) / numpy.sum(yData)
+ xcom = numpy.sum(xData * dataXRange) / numpy.sum(xData)
+ self.assertEqual(_stats["com"].calculate(self.imageContext), (xcom, ycom))
def testBasicStatsImagePolyRoi(self):
"""Test a simple rectangle ROI"""
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.imageContext_2), 0)
- self.assertEqual(_stats['max'].calculate(self.imageContext_2), 2432)
- self.assertEqual(_stats['minCoords'].calculate(self.imageContext_2), (0.0, 0.0))
+ self.assertEqual(_stats["min"].calculate(self.imageContext_2), 0)
+ self.assertEqual(_stats["max"].calculate(self.imageContext_2), 2432)
+ self.assertEqual(_stats["minCoords"].calculate(self.imageContext_2), (0.0, 0.0))
# not 0.0, 19.0 because not fully in. Should all pixel have a weight,
# on to manage them in stats. For now 0 if the center is not in, else 1
- self.assertEqual(_stats['maxCoords'].calculate(self.imageContext_2), (0.0, 19.0))
+ self.assertEqual(
+ _stats["maxCoords"].calculate(self.imageContext_2), (0.0, 19.0)
+ )
def testBasicStatsScatter(self):
self.assertEqual(self.scatterContext.values.compressed().size, 2)
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.scatterContext), 6)
- self.assertEqual(_stats['max'].calculate(self.scatterContext), 7)
- self.assertEqual(_stats['minCoords'].calculate(self.scatterContext), (2, 3))
- self.assertEqual(_stats['maxCoords'].calculate(self.scatterContext), (3, 4))
- self.assertEqual(_stats['std'].calculate(self.scatterContext), numpy.std([6, 7]))
- self.assertEqual(_stats['mean'].calculate(self.scatterContext), numpy.mean([6, 7]))
+ self.assertEqual(_stats["min"].calculate(self.scatterContext), 6)
+ self.assertEqual(_stats["max"].calculate(self.scatterContext), 7)
+ self.assertEqual(_stats["minCoords"].calculate(self.scatterContext), (2, 3))
+ self.assertEqual(_stats["maxCoords"].calculate(self.scatterContext), (3, 4))
+ self.assertEqual(
+ _stats["std"].calculate(self.scatterContext), numpy.std([6, 7])
+ )
+ self.assertEqual(
+ _stats["mean"].calculate(self.scatterContext), numpy.mean([6, 7])
+ )
def testBasicHistogram(self):
_stats = self.getBasicStats()
xData = yData = numpy.array(range(2, 6))
- self.assertEqual(_stats['min'].calculate(self.histoContext), 2)
- self.assertEqual(_stats['max'].calculate(self.histoContext), 5)
- self.assertEqual(_stats['minCoords'].calculate(self.histoContext), (2,))
- self.assertEqual(_stats['maxCoords'].calculate(self.histoContext), (5,))
- self.assertEqual(_stats['std'].calculate(self.histoContext), numpy.std(yData))
- self.assertEqual(_stats['mean'].calculate(self.histoContext), numpy.mean(yData))
+ self.assertEqual(_stats["min"].calculate(self.histoContext), 2)
+ self.assertEqual(_stats["max"].calculate(self.histoContext), 5)
+ self.assertEqual(_stats["minCoords"].calculate(self.histoContext), (2,))
+ self.assertEqual(_stats["maxCoords"].calculate(self.histoContext), (5,))
+ self.assertEqual(_stats["std"].calculate(self.histoContext), numpy.std(yData))
+ self.assertEqual(_stats["mean"].calculate(self.histoContext), numpy.mean(yData))
com = numpy.sum(xData * yData) / numpy.sum(yData)
- self.assertEqual(_stats['com'].calculate(self.histoContext), com)
+ self.assertEqual(_stats["com"].calculate(self.histoContext), com)
class TestAdvancedROIImageContext(TestCaseQt):
@@ -1016,31 +1109,35 @@ class TestAdvancedROIImageContext(TestCaseQt):
roi_origins = [(0, 0), (2, 10), (14, 20)]
img_origins = [(0, 0), (14, 20), (2, 10)]
img_scales = [1.0, 0.5, 2.0]
- _stats = {'sum': stats.Stat(name='sum', fct=numpy.sum), }
+ _stats = {
+ "sum": stats.Stat(name="sum", fct=numpy.sum),
+ }
for roi_origin in roi_origins:
for img_origin in img_origins:
for img_scale in img_scales:
- with self.subTest(roi_origin=roi_origin,
- img_origin=img_origin,
- img_scale=img_scale):
- self.plot.addImage(self.data, legend='img',
- origin=img_origin,
- scale=img_scale)
+ with self.subTest(
+ roi_origin=roi_origin,
+ img_origin=img_origin,
+ img_scale=img_scale,
+ ):
+ self.plot.addImage(
+ self.data, legend="img", origin=img_origin, scale=img_scale
+ )
roi = RectangleROI()
roi.setGeometry(origin=roi_origin, size=(20, 20))
context = stats._ImageContext(
- item=self.plot.getImage('img'),
+ item=self.plot.getImage("img"),
plot=self.plot,
onlimits=False,
- roi=roi)
+ roi=roi,
+ )
x_start = int((roi_origin[0] - img_origin[0]) / img_scale)
x_end = int(x_start + (20 / img_scale)) + 1
- y_start = int((roi_origin[1] - img_origin[1])/ img_scale)
+ y_start = int((roi_origin[1] - img_origin[1]) / img_scale)
y_end = int(y_start + (20 / img_scale)) + 1
x_start = max(x_start, 0)
x_end = min(max(x_end, 0), self.data_dims[1])
y_start = max(y_start, 0)
y_end = min(max(y_end, 0), self.data_dims[0])
th_sum = numpy.sum(self.data[y_start:y_end, x_start:x_end])
- self.assertAlmostEqual(_stats['sum'].calculate(context),
- th_sum)
+ self.assertAlmostEqual(_stats["sum"].calculate(context), th_sum)
diff --git a/src/silx/gui/plot/test/testUtilsAxis.py b/src/silx/gui/plot/test/testUtilsAxis.py
index 879ec73..d749845 100644
--- a/src/silx/gui/plot/test/testUtilsAxis.py
+++ b/src/silx/gui/plot/test/testUtilsAxis.py
@@ -28,7 +28,6 @@ __license__ = "MIT"
__date__ = "20/11/2018"
-import unittest
from silx.gui.plot import PlotWidget
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.plot.utils.axis import SyncAxes
@@ -51,7 +50,9 @@ class TestAxisSync(TestCaseQt):
def testMoveFirstAxis(self):
"""Test synchronization after construction"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
self.plot1.getXAxis().setLimits(10, 500)
self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
@@ -60,7 +61,9 @@ class TestAxisSync(TestCaseQt):
def testMoveSecondAxis(self):
"""Test synchronization after construction"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
self.plot2.getXAxis().setLimits(10, 500)
self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
@@ -69,7 +72,9 @@ class TestAxisSync(TestCaseQt):
def testMoveTwoAxes(self):
"""Test synchronization after construction"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
self.plot1.getXAxis().setLimits(1, 50)
self.plot2.getXAxis().setLimits(10, 500)
@@ -79,7 +84,9 @@ class TestAxisSync(TestCaseQt):
def testDestruction(self):
"""Test synchronization when sync object is destroyed"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
del sync
self.plot1.getXAxis().setLimits(10, 500)
@@ -89,10 +96,13 @@ class TestAxisSync(TestCaseQt):
def testAxisDestruction(self):
"""Test synchronization when an axis disappear"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
# Destroy the plot is possible
import weakref
+
plot = weakref.ref(self.plot2)
self.plot2 = None
result = self.qWaitForDestroy(plot)
@@ -105,7 +115,9 @@ class TestAxisSync(TestCaseQt):
def testStop(self):
"""Test synchronization after calling stop"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
sync.stop()
self.plot1.getXAxis().setLimits(10, 500)
@@ -115,7 +127,9 @@ class TestAxisSync(TestCaseQt):
def testStopMovingStart(self):
"""Test synchronization after calling stop, moving an axis, then start again"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
sync.stop()
self.plot1.getXAxis().setLimits(10, 500)
self.plot2.getXAxis().setLimits(1, 50)
@@ -129,26 +143,40 @@ class TestAxisSync(TestCaseQt):
def testDoubleStop(self):
"""Test double stop"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
sync.stop()
self.assertRaises(RuntimeError, sync.stop)
def testDoubleStart(self):
"""Test double stop"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
self.assertRaises(RuntimeError, sync.start)
def testScale(self):
"""Test scale change"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
self.plot1.getXAxis().setScale(self.plot1.getXAxis().LOGARITHMIC)
- self.assertEqual(self.plot1.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC)
- self.assertEqual(self.plot2.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC)
- self.assertEqual(self.plot3.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC)
+ self.assertEqual(
+ self.plot1.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC
+ )
+ self.assertEqual(
+ self.plot2.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC
+ )
+ self.assertEqual(
+ self.plot3.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC
+ )
def testDirection(self):
"""Test direction change"""
- _sync = SyncAxes([self.plot1.getYAxis(), self.plot2.getYAxis(), self.plot3.getYAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getYAxis(), self.plot2.getYAxis(), self.plot3.getYAxis()]
+ )
self.plot1.getYAxis().setInverted(True)
self.assertEqual(self.plot1.getYAxis().isInverted(), True)
self.assertEqual(self.plot2.getYAxis().isInverted(), True)
@@ -160,8 +188,11 @@ class TestAxisSync(TestCaseQt):
self.plot1.getXAxis().setLimits(0, 200)
self.plot2.getXAxis().setLimits(0, 20)
self.plot3.getXAxis().setLimits(0, 2)
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()],
- syncLimits=False, syncCenter=True)
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()],
+ syncLimits=False,
+ syncCenter=True,
+ )
self.assertEqual(self.plot1.getXAxis().getLimits(), (0, 200))
self.assertEqual(self.plot2.getXAxis().getLimits(), (100 - 10, 100 + 10))
@@ -173,8 +204,12 @@ class TestAxisSync(TestCaseQt):
self.plot1.getXAxis().setLimits(0, 200)
self.plot2.getXAxis().setLimits(0, 20)
self.plot3.getXAxis().setLimits(0, 2)
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()],
- syncLimits=False, syncCenter=True, syncZoom=True)
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()],
+ syncLimits=False,
+ syncCenter=True,
+ syncZoom=True,
+ )
# Supposing all the plots use the same size
self.assertEqual(self.plot1.getXAxis().getLimits(), (0, 200))
@@ -193,7 +228,9 @@ class TestAxisSync(TestCaseQt):
def testRemoveAxis(self):
"""Test synchronization after construction"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
sync.removeAxis(self.plot3.getXAxis())
self.plot1.getXAxis().setLimits(10, 500)
diff --git a/src/silx/gui/plot/test/utils.py b/src/silx/gui/plot/test/utils.py
index faa40bb..d48a467 100644
--- a/src/silx/gui/plot/test/utils.py
+++ b/src/silx/gui/plot/test/utils.py
@@ -30,7 +30,6 @@ __date__ = "26/01/2018"
import logging
import pytest
-import unittest
from silx.gui.utils.testutils import TestCaseQt
@@ -47,6 +46,7 @@ class PlotWidgetTestCase(TestCaseQt):
plot attribute is the PlotWidget created for the test.
"""
+
__screenshot_already_taken = False
backend = None
diff --git a/src/silx/gui/plot/tools/CurveLegendsWidget.py b/src/silx/gui/plot/tools/CurveLegendsWidget.py
index c9b0101..0ebea0d 100644
--- a/src/silx/gui/plot/tools/CurveLegendsWidget.py
+++ b/src/silx/gui/plot/tools/CurveLegendsWidget.py
@@ -74,11 +74,10 @@ class _LegendWidget(qt.QWidget):
return icon.getCurve()
def _update(self):
- """Update widget according to current curve state.
- """
+ """Update widget according to current curve state."""
curve = self.getCurve()
if curve is None:
- _logger.error('Curve no more exists')
+ _logger.error("Curve no more exists")
self.setVisible(False)
return
@@ -95,9 +94,11 @@ class _LegendWidget(qt.QWidget):
:param event: Kind of change
"""
- if event in (items.ItemChangedType.VISIBLE,
- items.ItemChangedType.HIGHLIGHTED,
- items.ItemChangedType.HIGHLIGHTED_STYLE):
+ if event in (
+ items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.HIGHLIGHTED,
+ items.ItemChangedType.HIGHLIGHTED_STYLE,
+ ):
self._update()
@@ -142,7 +143,7 @@ class CurveLegendsWidget(qt.QWidget):
"""
previousPlot = self.getPlotWidget()
if previousPlot is not None:
- previousPlot.sigItemAdded.disconnect( self._itemAdded)
+ previousPlot.sigItemAdded.disconnect(self._itemAdded)
previousPlot.sigItemAboutToBeRemoved.disconnect(self._itemRemoved)
for legend in list(self._legends.keys()):
self._removeLegend(legend)
@@ -168,7 +169,7 @@ class CurveLegendsWidget(qt.QWidget):
elif len(args) == 2:
point = qt.QPoint(*args)
else:
- raise ValueError('Unsupported arguments')
+ raise ValueError("Unsupported arguments")
assert isinstance(point, qt.QPoint)
widget = self.childAt(point)
@@ -202,7 +203,7 @@ class CurveLegendsWidget(qt.QWidget):
curve = plot.getCurve(legend)
if curve is None:
- _logger.error('Curve not found: %s' % legend)
+ _logger.error("Curve not found: %s" % legend)
return
widget = _LegendWidget(parent=self, curve=curve)
@@ -216,7 +217,7 @@ class CurveLegendsWidget(qt.QWidget):
"""
widget = self._legends.pop(legend, None)
if widget is None:
- _logger.warning('Unknown legend: %s' % legend)
+ _logger.warning("Unknown legend: %s" % legend)
else:
self.layout().removeWidget(widget)
widget.setParent(None)
diff --git a/src/silx/gui/plot/tools/LimitsToolBar.py b/src/silx/gui/plot/tools/LimitsToolBar.py
index d7f4bf5..5ed09f7 100644
--- a/src/silx/gui/plot/tools/LimitsToolBar.py
+++ b/src/silx/gui/plot/tools/LimitsToolBar.py
@@ -56,7 +56,7 @@ class LimitsToolBar(qt.QToolBar):
:param str title: See :class:`QToolBar`.
"""
- def __init__(self, parent=None, plot=None, title='Limits'):
+ def __init__(self, parent=None, plot=None, title="Limits"):
super(LimitsToolBar, self).__init__(title, parent)
assert plot is not None
self._plot = plot
@@ -74,32 +74,28 @@ class LimitsToolBar(qt.QToolBar):
xMin, xMax = self.plot.getXAxis().getLimits()
yMin, yMax = self.plot.getYAxis().getLimits()
- self.addWidget(qt.QLabel('Limits: '))
- self.addWidget(qt.QLabel(' X: '))
+ self.addWidget(qt.QLabel("Limits: "))
+ self.addWidget(qt.QLabel(" X: "))
self._xMinFloatEdit = FloatEdit(self, xMin)
- self._xMinFloatEdit.editingFinished[()].connect(
- self._xFloatEditChanged)
+ self._xMinFloatEdit.editingFinished[()].connect(self._xFloatEditChanged)
self.addWidget(self._xMinFloatEdit)
self._xMaxFloatEdit = FloatEdit(self, xMax)
- self._xMaxFloatEdit.editingFinished[()].connect(
- self._xFloatEditChanged)
+ self._xMaxFloatEdit.editingFinished[()].connect(self._xFloatEditChanged)
self.addWidget(self._xMaxFloatEdit)
- self.addWidget(qt.QLabel(' Y: '))
+ self.addWidget(qt.QLabel(" Y: "))
self._yMinFloatEdit = FloatEdit(self, yMin)
- self._yMinFloatEdit.editingFinished[()].connect(
- self._yFloatEditChanged)
+ self._yMinFloatEdit.editingFinished[()].connect(self._yFloatEditChanged)
self.addWidget(self._yMinFloatEdit)
self._yMaxFloatEdit = FloatEdit(self, yMax)
- self._yMaxFloatEdit.editingFinished[()].connect(
- self._yFloatEditChanged)
+ self._yMaxFloatEdit.editingFinished[()].connect(self._yFloatEditChanged)
self.addWidget(self._yMaxFloatEdit)
def _plotWidgetSlot(self, event):
"""Listen to :class:`PlotWidget` events."""
- if event['event'] not in ('limitsChanged',):
+ if event["event"] not in ("limitsChanged",):
return
xMin, xMax = self.plot.getXAxis().getLimits()
diff --git a/src/silx/gui/plot/tools/PlotToolButton.py b/src/silx/gui/plot/tools/PlotToolButton.py
new file mode 100644
index 0000000..3a14f77
--- /dev/null
+++ b/src/silx/gui/plot/tools/PlotToolButton.py
@@ -0,0 +1,92 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides an abstract PlotToolButton that can be use to create
+plot tools for a toolbar.
+"""
+
+from __future__ import annotations
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "20/12/2023"
+
+
+import logging
+import weakref
+
+from silx.gui import qt
+
+
+_logger = logging.getLogger(__name__)
+
+
+class PlotToolButton(qt.QToolButton):
+ """A QToolButton connected to a :class:`~silx.gui.plot.PlotWidget`."""
+
+ def __init__(self, parent: qt.QWidget | None = None, plot=None):
+ super(PlotToolButton, self).__init__(parent)
+ self._plotRef = None
+ if plot is not None:
+ self.setPlot(plot)
+
+ def plot(self):
+ """
+ Returns the plot connected to the widget.
+ """
+ return None if self._plotRef is None else self._plotRef()
+
+ def setPlot(self, plot):
+ """
+ Set the plot connected to the widget
+
+ :param plot: :class:`.PlotWidget` instance on which to operate.
+ """
+ previousPlot = self.plot()
+
+ if previousPlot is plot:
+ return
+ if previousPlot is not None:
+ self._disconnectPlot(previousPlot)
+
+ if plot is None:
+ self._plotRef = None
+ else:
+ self._plotRef = weakref.ref(plot)
+ self._connectPlot(plot)
+
+ def _connectPlot(self, plot):
+ """
+ Called when the plot is connected to the widget
+
+ :param plot: :class:`.PlotWidget` instance
+ """
+ pass
+
+ def _disconnectPlot(self, plot):
+ """
+ Called when the plot is disconnected from the widget
+
+ :param plot: :class:`.PlotWidget` instance
+ """
+ pass
diff --git a/src/silx/gui/plot/tools/PositionInfo.py b/src/silx/gui/plot/tools/PositionInfo.py
index cb16b80..e3b8425 100644
--- a/src/silx/gui/plot/tools/PositionInfo.py
+++ b/src/silx/gui/plot/tools/PositionInfo.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -38,7 +38,6 @@ import weakref
import numpy
-from ....utils.deprecation import deprecated
from ... import qt
from .. import items
from ...widgets.ElidedLabel import ElidedLabel
@@ -56,12 +55,13 @@ class _PositionInfoLabel(ElidedLabel):
def sizeHint(self):
hint = super().sizeHint()
- width = self.fontMetrics().boundingRect('##############').width()
+ width = self.fontMetrics().boundingRect("##############").width()
return qt.QSize(max(hint.width(), width), hint.height())
# PositionInfo ################################################################
+
class PositionInfo(qt.QWidget):
"""QWidget displaying coords converted from data coords of the mouse.
@@ -115,7 +115,7 @@ class PositionInfo(qt.QWidget):
super(PositionInfo, self).__init__(parent)
if converters is None:
- converters = (('X', lambda x, y: x), ('Y', lambda x, y: y))
+ converters = (("X", lambda x, y: x), ("Y", lambda x, y: y))
self._fields = [] # To store (QLineEdit, name, function (x, y)->v)
@@ -126,10 +126,10 @@ class PositionInfo(qt.QWidget):
# Create all QLabel and store them with the corresponding converter
for name, func in converters:
- layout.addWidget(qt.QLabel('<b>' + name + ':</b>'))
+ layout.addWidget(qt.QLabel("<b>" + name + ":</b>"))
contentWidget = _PositionInfoLabel(self)
- contentWidget.setText('------')
+ contentWidget.setText("------")
layout.addWidget(contentWidget)
self._fields.append((contentWidget, name, func))
@@ -146,11 +146,6 @@ class PositionInfo(qt.QWidget):
"""
return self._plotRef()
- @property
- @deprecated(replacement='getPlotWidget', since_version='0.8.0')
- def plot(self):
- return self.getPlotWidget()
-
def getConverters(self):
"""Return the list of converters as 2-tuple (name, function)."""
return [(name, func) for _label, name, func in self._fields]
@@ -160,17 +155,18 @@ class PositionInfo(qt.QWidget):
:param dict event: Plot event
"""
- if event['event'] == 'mouseMoved':
- x, y = event['x'], event['y']
- xPixel, yPixel = event['xpixel'], event['ypixel']
+ if event["event"] == "mouseMoved":
+ x, y = event["x"], event["y"]
+ xPixel, yPixel = event["xpixel"], event["ypixel"]
self._updateStatusBar(x, y, xPixel, yPixel)
def updateInfo(self):
"""Update displayed information"""
plot = self.getPlotWidget()
if plot is None:
- _logger.error("Trying to update PositionInfo "
- "while PlotWidget no longer exists")
+ _logger.error(
+ "Trying to update PositionInfo " "while PlotWidget no longer exists"
+ )
return
widget = plot.getWidgetHandle()
@@ -193,15 +189,15 @@ class PositionInfo(qt.QWidget):
if plot is None:
return
- styleSheet = "color: rgb(0, 0, 0);" # Default style
+ styleSheet = "" # Default style
xData, yData = x, y
snappingMode = self.getSnappingMode()
# Snapping when crosshair either not requested or active
- if (snappingMode & (self.SNAPPING_CURVE | self.SNAPPING_SCATTER) and
- (not (snappingMode & self.SNAPPING_CROSSHAIR) or
- plot.getGraphCursor())):
+ if snappingMode & (self.SNAPPING_CURVE | self.SNAPPING_SCATTER) and (
+ not (snappingMode & self.SNAPPING_CROSSHAIR) or plot.getGraphCursor()
+ ):
styleSheet = "color: rgb(255, 0, 0);" # Style far from item
if snappingMode & self.SNAPPING_ACTIVE_ONLY:
@@ -213,7 +209,7 @@ class PositionInfo(qt.QWidget):
selectedItems.append(activeCurve)
if snappingMode & self.SNAPPING_SCATTER:
- activeScatter = plot._getActiveItem(kind='scatter')
+ activeScatter = plot.getActiveScatter()
if activeScatter:
selectedItems.append(activeScatter)
@@ -224,8 +220,11 @@ class PositionInfo(qt.QWidget):
kinds.append(items.Histogram)
if snappingMode & self.SNAPPING_SCATTER:
kinds.append(items.Scatter)
- selectedItems = [item for item in plot.getItems()
- if isinstance(item, tuple(kinds)) and item.isVisible()]
+ selectedItems = [
+ item
+ for item in plot.getItems()
+ if isinstance(item, tuple(kinds)) and item.isVisible()
+ ]
# Compute distance threshold
window = plot.window()
@@ -236,12 +235,12 @@ class PositionInfo(qt.QWidget):
ratio = qt.QGuiApplication.primaryScreen().devicePixelRatio()
# Baseline squared distance threshold
- sqDistInPixels = (self.SNAP_THRESHOLD_DIST * ratio)**2
+ sqDistInPixels = (self.SNAP_THRESHOLD_DIST * ratio) ** 2
for item in selectedItems:
- if (snappingMode & self.SNAPPING_SYMBOLS_ONLY and (
- not isinstance(item, items.SymbolMixIn) or
- not item.getSymbol())):
+ if snappingMode & self.SNAPPING_SYMBOLS_ONLY and (
+ not isinstance(item, items.SymbolMixIn) or not item.getSymbol()
+ ):
# Only handled if item symbols are visible
continue
@@ -256,7 +255,7 @@ class PositionInfo(qt.QWidget):
yData = item.getValueData(copy=False)[index]
# Update label style sheet
- styleSheet = "color: rgb(0, 0, 0);"
+ styleSheet = ""
break
else: # Curve, Scatter
@@ -270,14 +269,16 @@ class PositionInfo(qt.QWidget):
if isinstance(item, items.YAxisMixIn):
axis = item.getYAxis()
else:
- axis = 'left'
+ axis = "left"
xArray = item.getXData(copy=False)[indices]
yArray = item.getYData(copy=False)[indices]
pixelPositions = plot.dataToPixel(xArray, yArray, axis=axis)
if pixelPositions is None:
continue
- sqDistances = (pixelPositions[0] - xPixel)**2 + (pixelPositions[1] - yPixel)**2
+ sqDistances = (pixelPositions[0] - xPixel) ** 2 + (
+ pixelPositions[1] - yPixel
+ ) ** 2
if not numpy.any(numpy.isfinite(sqDistances)):
continue
closestIndex = numpy.nanargmin(sqDistances)
@@ -285,7 +286,7 @@ class PositionInfo(qt.QWidget):
if closestSqDistInPixels <= sqDistInPixels:
# Update label style sheet
- styleSheet = "color: rgb(0, 0, 0);"
+ styleSheet = ""
# if close enough, snap to data point coord
xData, yData = xArray[closestIndex], yArray[closestIndex]
@@ -299,10 +300,11 @@ class PositionInfo(qt.QWidget):
text = self.valueToString(value)
label.setText(text)
except:
- label.setText('Error')
+ label.setText("Error")
_logger.error(
"Error while converting coordinates (%f, %f)"
- "with converter '%s'" % (xPixel, yPixel, name))
+ "with converter '%s'" % (xPixel, yPixel, name)
+ )
_logger.error(traceback.format_exc())
def valueToString(self, value):
@@ -311,7 +313,7 @@ class PositionInfo(qt.QWidget):
return ", ".join(value)
elif isinstance(value, numbers.Real):
# Use this for floats and int
- return '%.7g' % value
+ return "%.7g" % value
else:
# Fallback for other types
return str(value)
@@ -353,21 +355,3 @@ class PositionInfo(qt.QWidget):
:rtype: int
"""
return self._snappingMode
-
- _SNAPPING_LEGACY = (SNAPPING_CROSSHAIR |
- SNAPPING_ACTIVE_ONLY |
- SNAPPING_SYMBOLS_ONLY |
- SNAPPING_CURVE |
- SNAPPING_SCATTER)
- """Legacy snapping mode"""
-
- @property
- @deprecated(replacement="getSnappingMode", since_version="0.8")
- def autoSnapToActiveCurve(self):
- return self.getSnappingMode() == self._SNAPPING_LEGACY
-
- @autoSnapToActiveCurve.setter
- @deprecated(replacement="setSnappingMode", since_version="0.8")
- def autoSnapToActiveCurve(self, flag):
- self.setSnappingMode(
- self._SNAPPING_LEGACY if flag else self.SNAPPING_DISABLED)
diff --git a/src/silx/gui/plot/tools/RadarView.py b/src/silx/gui/plot/tools/RadarView.py
index 886f37e..8ddb98b 100644
--- a/src/silx/gui/plot/tools/RadarView.py
+++ b/src/silx/gui/plot/tools/RadarView.py
@@ -41,9 +41,9 @@ _logger = logging.getLogger(__name__)
class _DraggableRectItem(qt.QGraphicsRectItem):
"""RectItem which signals its change through visibleRectDragged."""
+
def __init__(self, *args, **kwargs):
- super(_DraggableRectItem, self).__init__(
- *args, **kwargs)
+ super(_DraggableRectItem, self).__init__(*args, **kwargs)
self._previousCursor = None
self.setFlag(qt.QGraphicsItem.ItemIsMovable)
@@ -81,8 +81,7 @@ class _DraggableRectItem(qt.QGraphicsRectItem):
def itemChange(self, change, value):
"""Callback called before applying changes to the item."""
- if (change == qt.QGraphicsItem.ItemPositionChange and
- not self._ignoreChange):
+ if change == qt.QGraphicsItem.ItemPositionChange and not self._ignoreChange:
# Makes sure that the visible area is in the data
# or that data is in the visible area if area is too wide
x, y = value.x(), value.y()
@@ -118,12 +117,12 @@ class _DraggableRectItem(qt.QGraphicsRectItem):
value.x() + self.rect().left(),
value.y() + self.rect().top(),
self.rect().width(),
- self.rect().height())
+ self.rect().height(),
+ )
return value
- return super(_DraggableRectItem, self).itemChange(
- change, value)
+ return super(_DraggableRectItem, self).itemChange(change, value)
def hoverEnterEvent(self, event):
"""Called when the mouse enters the rectangle area"""
@@ -160,37 +159,37 @@ class RadarView(qt.QGraphicsView):
It provides: left, top, width, height in data coordinates.
"""
- _DATA_PEN = qt.QPen(qt.QColor('white'))
- _DATA_BRUSH = qt.QBrush(qt.QColor('light gray'))
- _ACTIVEDATA_PEN = qt.QPen(qt.QColor('black'))
- _ACTIVEDATA_BRUSH = qt.QBrush(qt.QColor('transparent'))
+ _DATA_PEN = qt.QPen(qt.QColor("white"))
+ _DATA_BRUSH = qt.QBrush(qt.QColor("light gray"))
+ _ACTIVEDATA_PEN = qt.QPen(qt.QColor("black"))
+ _ACTIVEDATA_BRUSH = qt.QBrush(qt.QColor("transparent"))
_ACTIVEDATA_PEN.setWidth(2)
_ACTIVEDATA_PEN.setCosmetic(True)
- _VISIBLE_PEN = qt.QPen(qt.QColor('blue'))
+ _VISIBLE_PEN = qt.QPen(qt.QColor("blue"))
_VISIBLE_PEN.setWidth(2)
_VISIBLE_PEN.setCosmetic(True)
_VISIBLE_BRUSH = qt.QBrush(qt.QColor(0, 0, 0, 0))
- _TOOLTIP = 'Radar View:\nRed contour: Visible area\nGray area: The image'
+ _TOOLTIP = "Radar View:\nRed contour: Visible area\nGray area: The image"
_PIXMAP_SIZE = 256
def __init__(self, parent=None):
self.__plotRef = None
self._scene = qt.QGraphicsScene()
- self._dataRect = self._scene.addRect(0, 0, 1, 1,
- self._DATA_PEN,
- self._DATA_BRUSH)
- self._imageRect = self._scene.addRect(0, 0, 1, 1,
- self._ACTIVEDATA_PEN,
- self._ACTIVEDATA_BRUSH)
+ self._dataRect = self._scene.addRect(
+ 0, 0, 1, 1, self._DATA_PEN, self._DATA_BRUSH
+ )
+ self._imageRect = self._scene.addRect(
+ 0, 0, 1, 1, self._ACTIVEDATA_PEN, self._ACTIVEDATA_BRUSH
+ )
self._imageRect.setVisible(False)
- self._scatterRect = self._scene.addRect(0, 0, 1, 1,
- self._ACTIVEDATA_PEN,
- self._ACTIVEDATA_BRUSH)
+ self._scatterRect = self._scene.addRect(
+ 0, 0, 1, 1, self._ACTIVEDATA_PEN, self._ACTIVEDATA_BRUSH
+ )
self._scatterRect.setVisible(False)
- self._curveRect = self._scene.addRect(0, 0, 1, 1,
- self._ACTIVEDATA_PEN,
- self._ACTIVEDATA_BRUSH)
+ self._curveRect = self._scene.addRect(
+ 0, 0, 1, 1, self._ACTIVEDATA_PEN, self._ACTIVEDATA_BRUSH
+ )
self._curveRect.setVisible(False)
self._visibleRect = _DraggableRectItem(0, 0, 1, 1)
@@ -202,7 +201,7 @@ class RadarView(qt.QGraphicsView):
self.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff)
self.setFocusPolicy(qt.Qt.NoFocus)
- self.setStyleSheet('border: 0px')
+ self.setStyleSheet("border: 0px")
self.setToolTip(self._TOOLTIP)
self.__reentrant = LockReentrant()
@@ -311,7 +310,7 @@ class RadarView(qt.QGraphicsView):
# As opposed to Plot. So invert RadarView when Plot is NOT inverted.
self.resetTransform()
if not inverted:
- self.scale(1., -1.)
+ self.scale(1.0, -1.0)
self.update()
def _viewRectDragged(self, left, top, width, height):
diff --git a/src/silx/gui/plot/tools/RulerToolButton.py b/src/silx/gui/plot/tools/RulerToolButton.py
new file mode 100644
index 0000000..55cc02f
--- /dev/null
+++ b/src/silx/gui/plot/tools/RulerToolButton.py
@@ -0,0 +1,183 @@
+# /*##########################################################################
+#
+# Copyright (c) 20023 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""
+PlotToolButton to measure a distance in a plot
+"""
+
+__authors__ = ["H. Payno"]
+__license__ = "MIT"
+__date__ = "30/10/2023"
+
+
+import logging
+import numpy
+import weakref
+import typing
+
+from silx.gui import icons
+
+from .PlotToolButton import PlotToolButton
+
+from silx.gui.plot.tools.roi import RegionOfInterestManager
+from silx.gui.plot.items.roi import LineROI
+from silx.gui.plot import items
+
+
+_logger = logging.getLogger(__name__)
+
+
+class _RulerROI(LineROI):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._formatFunction: typing.Optional[
+ typing.Callable[
+ [numpy.ndarray, numpy.ndarray], str
+ ]
+ ] = None
+ self.setColor("#001122") # Only there to trig updateStyle
+
+ def registerFormatFunction(
+ self,
+ fct: typing.Callable[
+ [numpy.ndarray, numpy.ndarray], str
+ ],
+ ):
+ """Register a function for the formatting of the label"""
+ self._formatFunction = fct
+
+ def _updatedStyle(self, event, style: items.CurveStyle):
+ style = items.CurveStyle(
+ color="red",
+ gapcolor="white",
+ linestyle=(0, (5, 5)),
+ linewidth=style.getLineWidth())
+ LineROI._updatedStyle(self, event, style)
+ self._handleLabel.setColor("black")
+ self._handleLabel.setBackgroundColor("#FFFFFF60")
+ self._handleLabel.setZValue(1000)
+
+ def setEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray):
+ super().setEndPoints(startPoint=startPoint, endPoint=endPoint)
+ if self._formatFunction is not None:
+ ruler_text = self._formatFunction(
+ startPoint=startPoint, endPoint=endPoint
+ )
+ self._updateText(ruler_text)
+
+
+class RulerToolButton(PlotToolButton):
+ """
+ Button to active measurement between two point of the plot
+
+ An instance of `RulerToolButton` can be added to a plot toolbar like:
+ .. code-block:: python
+
+ plot = Plot2D()
+
+ rulerButton = RulerToolButton(parent=plot, plot=plot)
+ plot.toolBar().addWidget(rulerButton)
+ """
+
+ def __init__(
+ self,
+ parent=None,
+ plot=None,
+ ):
+ super().__init__(parent=parent, plot=plot)
+ self.setCheckable(True)
+ self._roiManager = None
+ self.__lastRoiCreated = None
+ self.setIcon(icons.getQIcon("ruler"))
+ self.toggled.connect(self._callback)
+ self._connectPlot(plot)
+
+ def setPlot(self, plot):
+ return super().setPlot(plot)
+
+ @property
+ def _lastRoiCreated(self):
+ if self.__lastRoiCreated is None:
+ return None
+ return self.__lastRoiCreated()
+
+ def _callback(self, *args, **kwargs):
+ if not self._roiManager:
+ return
+ if self._lastRoiCreated is not None:
+ self._lastRoiCreated.setVisible(self.isChecked())
+ if self.isChecked():
+ self._roiManager.start(_RulerROI, self)
+ self.__interactiveModeStarted(self._roiManager)
+ else:
+ source = self._roiManager.getInteractionSource()
+ if source is self:
+ self._roiManager.stop()
+
+ def __interactiveModeStarted(self, roiManager):
+ roiManager.sigInteractiveModeFinished.connect(self.__interactiveModeFinished)
+
+ def __interactiveModeFinished(self):
+ roiManager = self._roiManager
+ if roiManager is not None:
+ roiManager.sigInteractiveModeFinished.disconnect(
+ self.__interactiveModeFinished
+ )
+ self.setChecked(False)
+
+ def _connectPlot(self, plot):
+ """
+ Called when the plot is connected to the widget
+
+ :param plot: :class:`.PlotWidget` instance
+ """
+ if plot is None:
+ return
+ self._roiManager = RegionOfInterestManager(plot)
+ self._roiManager.sigRoiAdded.connect(self._registerCurrentROI)
+
+ def _disconnectPlot(self, plot):
+ if plot and self._lastRoiCreated is not None:
+ self._roiManager.removeRoi(self._lastRoiCreated)
+ self.__lastRoiCreated = None
+ return super()._disconnectPlot(plot)
+
+ def _registerCurrentROI(self, currentRoi):
+ if self._lastRoiCreated is None:
+ self.__lastRoiCreated = weakref.ref(currentRoi)
+ self._lastRoiCreated.registerFormatFunction(self.buildDistanceText)
+ elif currentRoi is not self._lastRoiCreated and self._roiManager is not None:
+ self._roiManager.removeRoi(self._lastRoiCreated)
+ currentRoi.registerFormatFunction(self.buildDistanceText)
+ self.__lastRoiCreated = weakref.ref(currentRoi)
+
+ def buildDistanceText(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray) -> str:
+ """
+ Define the text to be displayed by the ruler.
+
+ It can be redefine to modify precision or handle other parameters
+ (handling pixel size to display metric distance, display distance
+ on each distance - for non-square pixels...)
+ """
+ distance = numpy.linalg.norm(endPoint - startPoint)
+ return f"{distance: .1f}px"
diff --git a/src/silx/gui/plot/matplotlib/__init__.py b/src/silx/gui/plot/tools/compare/__init__.py
index 155ffd4..7f23852 100644
--- a/src/silx/gui/plot/matplotlib/__init__.py
+++ b/src/silx/gui/plot/tools/compare/__init__.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2020 European Synchrotron Radiation Facility
+# Copyright (c) 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
@@ -21,16 +21,9 @@
# THE SOFTWARE.
#
# ###########################################################################*/
+"""This module provides tools related to the compare image plot.
+"""
-__authors__ = ["T. Vincent"]
+__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "15/07/2020"
-
-from silx.utils.deprecation import deprecated_warning
-
-deprecated_warning(type_='module',
- name=__file__,
- replacement='silx.gui.utils.matplotlib',
- since_version='0.14.0')
-
-from silx.gui.utils.matplotlib import FigureCanvasQTAgg # noqa
+__date__ = "09/06/2023"
diff --git a/src/silx/gui/plot/tools/compare/core.py b/src/silx/gui/plot/tools/compare/core.py
new file mode 100644
index 0000000..90dbb79
--- /dev/null
+++ b/src/silx/gui/plot/tools/compare/core.py
@@ -0,0 +1,198 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides main objects shared by the compare image plot.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "09/06/2023"
+
+
+import numpy
+import enum
+import contextlib
+from typing import NamedTuple
+
+from silx.gui.plot.items.image import ImageBase
+from silx.gui.plot.items.core import ItemChangedType, ColormapMixIn
+
+from silx.opencl import ocl
+
+if ocl is not None:
+ try:
+ from silx.opencl import sift
+ except ImportError:
+ # sift module is not available (e.g., in official Debian packages)
+ sift = None
+else: # No OpenCL device or no pyopencl
+ sift = None
+
+
+@enum.unique
+class VisualizationMode(enum.Enum):
+ """Enum for each visualization mode available."""
+
+ ONLY_A = "a"
+ ONLY_B = "b"
+ VERTICAL_LINE = "vline"
+ HORIZONTAL_LINE = "hline"
+ COMPOSITE_RED_BLUE_GRAY = "rbgchannel"
+ COMPOSITE_RED_BLUE_GRAY_NEG = "rbgnegchannel"
+ COMPOSITE_A_MINUS_B = "aminusb"
+
+
+@enum.unique
+class AlignmentMode(enum.Enum):
+ """Enum for each alignment mode available."""
+
+ ORIGIN = "origin"
+ CENTER = "center"
+ STRETCH = "stretch"
+ AUTO = "auto"
+
+
+class AffineTransformation(NamedTuple):
+ """Description of a 2D affine transformation: translation, scale and
+ rotation.
+ """
+
+ tx: float
+ ty: float
+ sx: float
+ sy: float
+ rot: float
+
+
+class _CompareImageItem(ImageBase, ColormapMixIn):
+ """Description of a virtual item of images to compare, in order to share
+ the data through the silx components.
+ """
+
+ def __init__(self):
+ ImageBase.__init__(self)
+ ColormapMixIn.__init__(self)
+ self.__image1 = None
+ self.__image2 = None
+ self.__vizualisationMode = VisualizationMode.ONLY_A
+
+ def getImageData1(self):
+ return self.__image1
+
+ def getImageData2(self):
+ return self.__image2
+
+ def setImageData1(self, image1):
+ if self.__image1 is image1:
+ return
+ self.__image1 = image1
+ self._updated(ItemChangedType.DATA)
+
+ def setImageData2(self, image2):
+ if self.__image2 is image2:
+ return
+ self.__image2 = image2
+ self._updated(ItemChangedType.DATA)
+
+ def getVizualisationMode(self) -> VisualizationMode:
+ return self.__vizualisationMode
+
+ @contextlib.contextmanager
+ def _updateColormapRange(self, previousMode, mode):
+ """COMPOSITE_A_MINUS_B don't have the same data range than others.
+
+ If the colormap is using a fixed range, it is updated in order to set
+ a similar range with the new data.
+ """
+ normalize_colormap = (
+ previousMode == VisualizationMode.COMPOSITE_A_MINUS_B
+ or mode == VisualizationMode.COMPOSITE_A_MINUS_B
+ )
+ if normalize_colormap:
+ data = self._getConcatenatedData(copy=False)
+ if data is None or data.size == 0:
+ normalize_colormap = False
+ else:
+ std1 = numpy.nanstd(data)
+ mean1 = numpy.nanmean(data)
+ yield
+
+ def transfer(v, std1, mean1, std2, mean2):
+ """Transfer a value from a data range to another using statistics"""
+ if v is None:
+ return None
+ rv = (v - mean1) / std1
+ return rv * std2 + mean2
+
+ if normalize_colormap:
+ data = self._getConcatenatedData(copy=False)
+ if data is not None and data.size != 0:
+ std2 = numpy.nanstd(data)
+ mean2 = numpy.nanmean(data)
+ c = self.getColormap()
+ if c is not None:
+ vmin, vmax = c.getVRange()
+ vmin = transfer(vmin, std1, mean1, std2, mean2)
+ vmax = transfer(vmax, std1, mean1, std2, mean2)
+ c.setVRange(vmin, vmax)
+
+ def setVizualisationMode(self, mode: VisualizationMode):
+ if self.__vizualisationMode == mode:
+ return None
+ with self._updateColormapRange(self.__vizualisationMode, mode):
+ self.__vizualisationMode = mode
+ self._updated(ItemChangedType.DATA)
+
+ def _getConcatenatedData(self, copy=True):
+ if self.__image1 is None and self.__image2 is None:
+ return None
+ if self.__image1 is None:
+ return numpy.array(self.__image2, copy=copy)
+ if self.__image2 is None:
+ return numpy.array(self.__image1, copy=copy)
+
+ if self.__vizualisationMode == VisualizationMode.COMPOSITE_A_MINUS_B:
+ # In this case the histogram have to be special
+ if self.__image1.shape == self.__image2.shape:
+ return self.__image1.astype(numpy.float32) - self.__image2.astype(
+ numpy.float32
+ )
+ else:
+ d1 = self.__image1[numpy.isfinite(self.__image1)]
+ d2 = self.__image2[numpy.isfinite(self.__image2)]
+ return numpy.concatenate((d1, d2))
+
+ def _updated(self, event=None, checkVisibility=True):
+ # Synchronizes colormapped data if changed
+ if event in (ItemChangedType.DATA, ItemChangedType.MASK):
+ data = self._getConcatenatedData(copy=False)
+ return self._setColormappedData(data, copy=False)
+ super()._updated(event=event, checkVisibility=checkVisibility)
+
+ def getColormappedData(self, copy=True):
+ """
+ Reimplementation of the `ColormapMixIn.getColormappedData` method.
+
+ This is used to provide a consistent auto scale on the compared images.
+ """
+ return self._getConcatenatedData(copy=copy)
diff --git a/src/silx/gui/plot/tools/compare/profile.py b/src/silx/gui/plot/tools/compare/profile.py
new file mode 100644
index 0000000..afe0eba
--- /dev/null
+++ b/src/silx/gui/plot/tools/compare/profile.py
@@ -0,0 +1,173 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This provides profile ROIs.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "09/06/2023"
+
+
+import numpy
+
+from silx.gui.plot.tools.profile import rois
+from silx.gui.plot.tools.profile import core
+from .core import _CompareImageItem
+
+
+COLOR_A = "C0"
+COLOR_B = "C8"
+
+
+class ProfileImageLineROI(rois.ProfileImageLineROI):
+ """ROI for a compare image profile between 2 points.
+
+ The X profile of this ROI is the projection into one of the x/y axes,
+ using its scale and its orientation.
+ """
+
+ def computeProfile(self, item):
+ if not isinstance(item, _CompareImageItem):
+ raise TypeError("Unexpected class %s" % type(item))
+
+ origin = item.getOrigin()
+ scale = item.getScale()
+ method = self.getProfileMethod()
+ lineWidth = self.getProfileLineWidth()
+ roiInfo = self._getRoiInfo()
+
+ def createProfile2(currentData):
+ coords, profile, _area, profileName, xLabel = core.createProfile(
+ roiInfo=roiInfo,
+ currentData=currentData,
+ origin=origin,
+ scale=scale,
+ lineWidth=lineWidth,
+ method=method,
+ )
+ return coords, profile, profileName, xLabel
+
+ currentData1 = item.getImageData1()
+ currentData2 = item.getImageData2()
+
+ yLabel = "%s" % str(method).capitalize()
+ coords, profile1, title, xLabel = createProfile2(currentData1)
+ title = title + "; width = %d" % lineWidth
+ _coords, profile2, _title, _xLabel = createProfile2(currentData2)
+
+ profile1.shape = -1
+ profile2.shape = -1
+
+ title = title.format(xlabel="width", ylabel="height")
+ xLabel = xLabel.format(xlabel="width", ylabel="height")
+ yLabel = yLabel.format(xlabel="width", ylabel="height")
+
+ data = core.CurvesProfileData(
+ coords=coords,
+ profiles=[
+ core.CurveProfileDesc(profile1, color=COLOR_A, name="profileA"),
+ core.CurveProfileDesc(profile2, color=COLOR_B, name="profileB"),
+ ],
+ title=title,
+ xLabel=xLabel,
+ yLabel=yLabel,
+ )
+ return data
+
+
+class ProfileImageDirectedLineROI(rois.ProfileImageDirectedLineROI):
+ """ROI for a compare image profile between 2 points.
+
+ The X profile of the line is displayed projected into the line itself,
+ using its scale and its orientation. It's the distance from the origin.
+ """
+
+ def computeProfile(self, item):
+ if not isinstance(item, _CompareImageItem):
+ raise TypeError("Unexpected class %s" % type(item))
+
+ from silx.image.bilinear import BilinearImage
+
+ origin = item.getOrigin()
+ scale = item.getScale()
+ method = self.getProfileMethod()
+ lineWidth = self.getProfileLineWidth()
+
+ roiInfo = self._getRoiInfo()
+ roiStart, roiEnd, _lineProjectionMode = roiInfo
+
+ startPt = (
+ (roiStart[1] - origin[1]) / scale[1],
+ (roiStart[0] - origin[0]) / scale[0],
+ )
+ endPt = ((roiEnd[1] - origin[1]) / scale[1], (roiEnd[0] - origin[0]) / scale[0])
+
+ if numpy.array_equal(startPt, endPt):
+ return None
+
+ def computeProfile(data):
+ bilinear = BilinearImage(data)
+ profile = bilinear.profile_line(
+ (startPt[0] - 0.5, startPt[1] - 0.5),
+ (endPt[0] - 0.5, endPt[1] - 0.5),
+ lineWidth,
+ method=method,
+ )
+ return profile
+
+ currentData1 = item.getImageData1()
+ currentData2 = item.getImageData2()
+ profile1 = computeProfile(currentData1)
+ profile2 = computeProfile(currentData2)
+
+ # Compute the line size
+ lineSize = numpy.sqrt(
+ (roiEnd[1] - roiStart[1]) ** 2 + (roiEnd[0] - roiStart[0]) ** 2
+ )
+ coords = numpy.linspace(
+ 0, lineSize, len(profile1), endpoint=True, dtype=numpy.float32
+ )
+
+ title = rois._lineProfileTitle(*roiStart, *roiEnd)
+ title = title + "; width = %d" % lineWidth
+ xLabel = "√({xlabel}²+{ylabel}²)"
+ yLabel = str(method).capitalize()
+
+ # Use the axis names from the original plot
+ profileManager = self.getProfileManager()
+ plot = profileManager.getPlotWidget()
+ xLabel = rois._relabelAxes(plot, xLabel)
+ title = rois._relabelAxes(plot, title)
+
+ data = core.CurvesProfileData(
+ coords=coords,
+ profiles=[
+ core.CurveProfileDesc(profile1, color=COLOR_A, name="profileA"),
+ core.CurveProfileDesc(profile2, color=COLOR_B, name="profileB"),
+ ],
+ title=title,
+ xLabel=xLabel,
+ yLabel=yLabel,
+ )
+ return data
diff --git a/src/silx/gui/plot/tools/compare/statusbar.py b/src/silx/gui/plot/tools/compare/statusbar.py
new file mode 100644
index 0000000..5e43a37
--- /dev/null
+++ b/src/silx/gui/plot/tools/compare/statusbar.py
@@ -0,0 +1,218 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides tool bar helper.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "09/06/2023"
+
+
+import logging
+import weakref
+import numpy
+
+from silx.gui import qt
+
+
+_logger = logging.getLogger(__name__)
+
+
+class CompareImagesStatusBar(qt.QStatusBar):
+ """StatusBar containing specific information contained in a
+ :class:`CompareImages` widget
+
+ Use :meth:`setCompareWidget` to connect this toolbar to a specific
+ :class:`CompareImages` widget.
+
+ :param Union[qt.QWidget,None] parent: Parent of this widget.
+ """
+
+ def __init__(self, parent=None):
+ qt.QStatusBar.__init__(self, parent)
+ self.setSizeGripEnabled(False)
+ self.layout().setSpacing(0)
+ self.__compareWidget = None
+ self._label1 = qt.QLabel(self)
+ self._label1.setFrameShape(qt.QFrame.WinPanel)
+ self._label1.setFrameShadow(qt.QFrame.Sunken)
+ self._label2 = qt.QLabel(self)
+ self._label2.setFrameShape(qt.QFrame.WinPanel)
+ self._label2.setFrameShadow(qt.QFrame.Sunken)
+ self._transform = qt.QLabel(self)
+ self._transform.setFrameShape(qt.QFrame.WinPanel)
+ self._transform.setFrameShadow(qt.QFrame.Sunken)
+ self.addWidget(self._label1)
+ self.addWidget(self._label2)
+ self.addWidget(self._transform)
+ self._pos = None
+ self._updateStatusBar()
+
+ def setCompareWidget(self, widget):
+ """
+ Connect this tool bar to a specific :class:`CompareImages` widget.
+
+ :param Union[None,CompareImages] widget: The widget to connect with.
+ """
+ compareWidget = self.getCompareWidget()
+ if compareWidget is not None:
+ compareWidget.getPlot().sigPlotSignal.disconnect(self.__plotSignalReceived)
+ compareWidget.sigConfigurationChanged.disconnect(self.__dataChanged)
+ compareWidget = widget
+ if compareWidget is None:
+ self.__compareWidget = None
+ else:
+ self.__compareWidget = weakref.ref(compareWidget)
+ if compareWidget is not None:
+ compareWidget.getPlot().sigPlotSignal.connect(self.__plotSignalReceived)
+ compareWidget.sigConfigurationChanged.connect(self.__dataChanged)
+
+ def getCompareWidget(self):
+ """Returns the connected widget.
+
+ :rtype: CompareImages
+ """
+ if self.__compareWidget is None:
+ return None
+ else:
+ return self.__compareWidget()
+
+ def __plotSignalReceived(self, event):
+ """Called when old style signals at emmited from the plot."""
+ if event["event"] == "mouseMoved":
+ x, y = event["x"], event["y"]
+ self.__mouseMoved(x, y)
+
+ def __mouseMoved(self, x, y):
+ """Called when mouse move over the plot."""
+ self._pos = x, y
+ self._updateStatusBar()
+
+ def __dataChanged(self):
+ """Called when internal data from the connected widget changes."""
+ self._updateStatusBar()
+
+ def _formatData(self, data):
+ """Format pixel of an image.
+
+ It supports intensity, RGB, and RGBA.
+
+ :param Union[int,float,numpy.ndarray,str]: Value of a pixel
+ :rtype: str
+ """
+ if data is None:
+ return "No data"
+ if isinstance(data, (int, numpy.integer)):
+ return "%d" % data
+ if isinstance(data, (float, numpy.floating)):
+ return "%f" % data
+ if isinstance(data, numpy.ndarray):
+ # RGBA value
+ if data.shape == (3,):
+ return "R:%d G:%d B:%d" % (data[0], data[1], data[2])
+ elif data.shape == (4,):
+ return "R:%d G:%d B:%d A:%d" % (data[0], data[1], data[2], data[3])
+ _logger.debug("Unsupported data format %s. Cast it to string.", type(data))
+ return str(data)
+
+ def _updateStatusBar(self):
+ """Update the content of the status bar"""
+ widget = self.getCompareWidget()
+ if widget is None:
+ self._label1.setText("ImageA: NA")
+ self._label2.setText("ImageB: NA")
+ self._transform.setVisible(False)
+ else:
+ transform = widget.getTransformation()
+ self._transform.setVisible(transform is not None)
+ if transform is not None:
+ has_notable_translation = not numpy.isclose(
+ transform.tx, 0.0, atol=0.01
+ ) or not numpy.isclose(transform.ty, 0.0, atol=0.01)
+ has_notable_scale = not numpy.isclose(
+ transform.sx, 1.0, atol=0.01
+ ) or not numpy.isclose(transform.sy, 1.0, atol=0.01)
+ has_notable_rotation = not numpy.isclose(transform.rot, 0.0, atol=0.01)
+
+ strings = []
+ if has_notable_translation:
+ strings.append("Translation")
+ if has_notable_scale:
+ strings.append("Scale")
+ if has_notable_rotation:
+ strings.append("Rotation")
+ if strings == []:
+ has_translation = not numpy.isclose(
+ transform.tx, 0.0
+ ) or not numpy.isclose(transform.ty, 0.0)
+ has_scale = not numpy.isclose(
+ transform.sx, 1.0
+ ) or not numpy.isclose(transform.sy, 1.0)
+ has_rotation = not numpy.isclose(transform.rot, 0.0)
+ if has_translation or has_scale or has_rotation:
+ text = "No big changes"
+ else:
+ text = "No changes"
+ else:
+ text = "+".join(strings)
+ self._transform.setText("Align: " + text)
+
+ strings = []
+ if not numpy.isclose(transform.ty, 0.0):
+ strings.append("Translation x: %0.3fpx" % transform.tx)
+ if not numpy.isclose(transform.ty, 0.0):
+ strings.append("Translation y: %0.3fpx" % transform.ty)
+ if not numpy.isclose(transform.sx, 1.0):
+ strings.append("Scale x: %0.3f" % transform.sx)
+ if not numpy.isclose(transform.sy, 1.0):
+ strings.append("Scale y: %0.3f" % transform.sy)
+ if not numpy.isclose(transform.rot, 0.0):
+ strings.append(
+ "Rotation: %0.3fdeg" % (transform.rot * 180 / numpy.pi)
+ )
+ if strings == []:
+ text = "No transformation"
+ else:
+ text = "\n".join(strings)
+ self._transform.setToolTip(text)
+
+ if self._pos is None:
+ self._label1.setText("ImageA: NA")
+ self._label2.setText("ImageB: NA")
+ else:
+ data1, data2 = widget.getRawPixelData(self._pos[0], self._pos[1])
+ if isinstance(data1, str):
+ self._label1.setToolTip(data1)
+ text1 = "NA"
+ else:
+ self._label1.setToolTip("")
+ text1 = self._formatData(data1)
+ if isinstance(data2, str):
+ self._label2.setToolTip(data2)
+ text2 = "NA"
+ else:
+ self._label2.setToolTip("")
+ text2 = self._formatData(data2)
+ self._label1.setText("ImageA: %s" % text1)
+ self._label2.setText("ImageB: %s" % text2)
diff --git a/src/silx/gui/plot/tools/compare/toolbar.py b/src/silx/gui/plot/tools/compare/toolbar.py
new file mode 100644
index 0000000..a7f56ec
--- /dev/null
+++ b/src/silx/gui/plot/tools/compare/toolbar.py
@@ -0,0 +1,390 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides tool bar helper.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "28/06/2018"
+
+
+import logging
+import weakref
+from typing import List, Optional
+
+from silx.gui import qt
+from silx.gui import icons
+from .core import AlignmentMode
+from .core import VisualizationMode
+from .core import sift
+
+
+_logger = logging.getLogger(__name__)
+
+
+class AlignmentModeToolButton(qt.QToolButton):
+ """ToolButton to select a AlignmentMode"""
+
+ sigSelected = qt.Signal(AlignmentMode)
+
+ def __init__(self, parent=None):
+ super(AlignmentModeToolButton, self).__init__(parent=parent)
+
+ menu = qt.QMenu(self)
+ self.setMenu(menu)
+
+ self.__group = qt.QActionGroup(self)
+ self.__group.setExclusive(True)
+ self.__group.triggered.connect(self.__selectionChanged)
+
+ icon = icons.getQIcon("compare-align-origin")
+ action = qt.QAction(icon, "Align images on their upper-left pixel", self)
+ action.setProperty("enum", AlignmentMode.ORIGIN)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ self.__originAlignAction = action
+ menu.addAction(action)
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-align-center")
+ action = qt.QAction(icon, "Center images", self)
+ action.setProperty("enum", AlignmentMode.CENTER)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ self.__centerAlignAction = action
+ menu.addAction(action)
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-align-stretch")
+ action = qt.QAction(icon, "Stretch the second image on the first one", self)
+ action.setProperty("enum", AlignmentMode.STRETCH)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ self.__stretchAlignAction = action
+ menu.addAction(action)
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-align-auto")
+ action = qt.QAction(icon, "Auto-alignment of the second image", self)
+ action.setProperty("enum", AlignmentMode.AUTO)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ self.__autoAlignAction = action
+ menu.addAction(action)
+ if sift is None:
+ action.setEnabled(False)
+ action.setToolTip("Sift module is not available")
+ self.__group.addAction(action)
+
+ def getActionFromMode(self, mode: AlignmentMode) -> Optional[qt.QAction]:
+ """Returns an action from it's mode"""
+ for action in self.__group.actions():
+ actionMode = action.property("enum")
+ if mode == actionMode:
+ return action
+ return None
+
+ def setVisibleModes(self, modes: List[AlignmentMode]):
+ """Make visible only a set of modes.
+
+ The order does not matter.
+ """
+ modes = set(modes)
+ for action in self.__group.actions():
+ mode = action.property("enum")
+ action.setVisible(mode in modes)
+
+ def __selectionChanged(self, selectedAction: qt.QAction):
+ """Called when user requesting changes of the alignment mode."""
+ self.__updateMenu()
+ mode = self.getSelected()
+ self.sigSelected.emit(mode)
+
+ def __updateMenu(self):
+ """Update the state of the action containing alignment menu."""
+ selectedAction = self.__group.checkedAction()
+ if selectedAction is not None:
+ self.setText(selectedAction.text())
+ self.setIcon(selectedAction.icon())
+ self.setToolTip(selectedAction.toolTip())
+ else:
+ self.setText("")
+ self.setIcon(qt.QIcon())
+ self.setToolTip("")
+
+ def getSelected(self) -> AlignmentMode:
+ action = self.__group.checkedAction()
+ if action is None:
+ return None
+ return action.property("enum")
+
+ def setSelected(self, mode: AlignmentMode):
+ action = self.getActionFromMode(mode)
+ old = self.__group.blockSignals(True)
+ if action is not None:
+ # Check this action
+ action.setChecked(True)
+ else:
+ action = self.__group.checkedAction()
+ if action is not None:
+ # Uncheck this action
+ action.setChecked(False)
+ self.__updateMenu()
+ self.__group.blockSignals(old)
+
+
+class VisualizationModeToolButton(qt.QToolButton):
+ """ToolButton to select a VisualisationMode"""
+
+ sigSelected = qt.Signal(VisualizationMode)
+
+ def __init__(self, parent=None):
+ super(VisualizationModeToolButton, self).__init__(parent=parent)
+
+ menu = qt.QMenu(self)
+ self.setMenu(menu)
+
+ self.__group = qt.QActionGroup(self)
+ self.__group.setExclusive(True)
+ self.__group.triggered.connect(self.__selectionChanged)
+
+ icon = icons.getQIcon("compare-mode-a")
+ action = qt.QAction(icon, "Display the first image only", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_A))
+ action.setProperty("enum", VisualizationMode.ONLY_A)
+ menu.addAction(action)
+ self.__aModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-b")
+ action = qt.QAction(icon, "Display the second image only", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_B))
+ action.setProperty("enum", VisualizationMode.ONLY_B)
+ menu.addAction(action)
+ self.__bModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-vline")
+ action = qt.QAction(icon, "Vertical compare mode", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_V))
+ action.setProperty("enum", VisualizationMode.VERTICAL_LINE)
+ menu.addAction(action)
+ self.__vlineModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-hline")
+ action = qt.QAction(icon, "Horizontal compare mode", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_H))
+ action.setProperty("enum", VisualizationMode.HORIZONTAL_LINE)
+ menu.addAction(action)
+ self.__hlineModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-rb-channel")
+ action = qt.QAction(icon, "Blue/red compare mode (additive mode)", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_C))
+ action.setProperty("enum", VisualizationMode.COMPOSITE_RED_BLUE_GRAY)
+ menu.addAction(action)
+ self.__brChannelModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-rbneg-channel")
+ action = qt.QAction(icon, "Yellow/cyan compare mode (subtractive mode)", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_Y))
+ action.setProperty("enum", VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG)
+ menu.addAction(action)
+ self.__ycChannelModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-a-minus-b")
+ action = qt.QAction(icon, "Raw A minus B compare mode", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_W))
+ action.setProperty("enum", VisualizationMode.COMPOSITE_A_MINUS_B)
+ menu.addAction(action)
+ self.__ycChannelModeAction = action
+ self.__group.addAction(action)
+
+ def getActionFromMode(self, mode: VisualizationMode) -> Optional[qt.QAction]:
+ """Returns an action from it's mode"""
+ for action in self.__group.actions():
+ actionMode = action.property("enum")
+ if mode == actionMode:
+ return action
+ return None
+
+ def setVisibleModes(self, modes: List[VisualizationMode]):
+ """Make visible only a set of modes.
+
+ The order does not matter.
+ """
+ modes = set(modes)
+ for action in self.__group.actions():
+ mode = action.property("enum")
+ action.setVisible(mode in modes)
+
+ def __selectionChanged(self, selectedAction: qt.QAction):
+ """Called when user requesting changes of the visualization mode."""
+ self.__updateMenu()
+ mode = self.getSelected()
+ self.sigSelected.emit(mode)
+
+ def __updateMenu(self):
+ """Update the state of the action containing visualization menu."""
+ selectedAction = self.__group.checkedAction()
+ if selectedAction is not None:
+ self.setText(selectedAction.text())
+ self.setIcon(selectedAction.icon())
+ self.setToolTip(selectedAction.toolTip())
+ else:
+ self.setText("")
+ self.setIcon(qt.QIcon())
+ self.setToolTip("")
+
+ def getSelected(self) -> VisualizationMode:
+ action = self.__group.checkedAction()
+ if action is None:
+ return None
+ return action.property("enum")
+
+ def setSelected(self, mode: VisualizationMode):
+ action = self.getActionFromMode(mode)
+ old = self.__group.blockSignals(True)
+ if action is not None:
+ # Check this action
+ action.setChecked(True)
+ else:
+ action = self.__group.checkedAction()
+ if action is not None:
+ # Uncheck this action
+ action.setChecked(False)
+ self.__updateMenu()
+ self.__group.blockSignals(old)
+
+
+class CompareImagesToolBar(qt.QToolBar):
+ """ToolBar containing specific tools to custom the configuration of a
+ :class:`CompareImages` widget
+
+ Use :meth:`setCompareWidget` to connect this toolbar to a specific
+ :class:`CompareImages` widget.
+
+ :param Union[qt.QWidget,None] parent: Parent of this widget.
+ """
+
+ def __init__(self, parent=None):
+ qt.QToolBar.__init__(self, parent)
+ self.setWindowTitle("Compare images")
+
+ self.__compareWidget = None
+
+ self.__visualizationToolButton = VisualizationModeToolButton(self)
+ self.__visualizationToolButton.setPopupMode(qt.QToolButton.InstantPopup)
+ self.__visualizationToolButton.sigSelected.connect(self.__visualizationChanged)
+ self.addWidget(self.__visualizationToolButton)
+
+ self.__alignmentToolButton = AlignmentModeToolButton(self)
+ self.__alignmentToolButton.setPopupMode(qt.QToolButton.InstantPopup)
+ self.__alignmentToolButton.sigSelected.connect(self.__alignmentChanged)
+ self.addWidget(self.__alignmentToolButton)
+
+ icon = icons.getQIcon("compare-keypoints")
+ action = qt.QAction(icon, "Display/hide alignment keypoints", self)
+ action.setCheckable(True)
+ action.triggered.connect(self.__keypointVisibilityChanged)
+ self.addAction(action)
+ self.__displayKeypoints = action
+
+ def __visualizationChanged(self, mode: VisualizationMode):
+ widget = self.getCompareWidget()
+ if widget is not None:
+ widget.setVisualizationMode(mode)
+
+ def __alignmentChanged(self, mode: AlignmentMode):
+ widget = self.getCompareWidget()
+ if widget is not None:
+ widget.setAlignmentMode(mode)
+
+ def setCompareWidget(self, widget):
+ """
+ Connect this tool bar to a specific :class:`CompareImages` widget.
+
+ :param Union[None,CompareImages] widget: The widget to connect with.
+ """
+ compareWidget = self.getCompareWidget()
+ if compareWidget is not None:
+ compareWidget.sigConfigurationChanged.disconnect(
+ self.__updateSelectedActions
+ )
+ compareWidget = widget
+ self.setEnabled(compareWidget is not None)
+ if compareWidget is None:
+ self.__compareWidget = None
+ else:
+ self.__compareWidget = weakref.ref(compareWidget)
+ if compareWidget is not None:
+ widget.sigConfigurationChanged.connect(self.__updateSelectedActions)
+ self.__updateSelectedActions()
+
+ def getCompareWidget(self):
+ """Returns the connected widget.
+
+ :rtype: CompareImages
+ """
+ if self.__compareWidget is None:
+ return None
+ else:
+ return self.__compareWidget()
+
+ def __updateSelectedActions(self):
+ """
+ Update the state of this tool bar according to the state of the
+ connected :class:`CompareImages` widget.
+ """
+ widget = self.getCompareWidget()
+ if widget is None:
+ return
+ self.__visualizationToolButton.setSelected(widget.getVisualizationMode())
+ self.__alignmentToolButton.setSelected(widget.getAlignmentMode())
+ self.__displayKeypoints.setChecked(widget.getKeypointsVisible())
+
+ def __keypointVisibilityChanged(self):
+ """Called when action managing keypoints visibility changes"""
+ widget = self.getCompareWidget()
+ if widget is not None:
+ keypointsVisible = self.__displayKeypoints.isChecked()
+ widget.setKeypointsVisible(keypointsVisible)
diff --git a/src/silx/gui/plot/tools/menus.py b/src/silx/gui/plot/tools/menus.py
new file mode 100644
index 0000000..c748b6e
--- /dev/null
+++ b/src/silx/gui/plot/tools/menus.py
@@ -0,0 +1,93 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""
+This module provides :class:`PlotWidget`-related QMenu.
+
+The following QMenu is available:
+
+- :class:`ZoomEnabledAxesMenu`
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "12/06/2023"
+
+
+import weakref
+from typing import Optional
+
+from silx.gui import qt
+
+from ..PlotWidget import PlotWidget
+
+
+class ZoomEnabledAxesMenu(qt.QMenu):
+ """Menu to toggle axes for zoom interaction"""
+
+ def __init__(self, plot: PlotWidget, parent: Optional[qt.QWidget] = None):
+ super().__init__(parent)
+ self.setTitle("Zoom axes")
+
+ assert isinstance(plot, PlotWidget)
+ self.__plotRef = weakref.ref(plot)
+
+ self.addSection("Enabled axes")
+ self.__xAxisAction = qt.QAction("X axis", parent=self)
+ self.__yAxisAction = qt.QAction("Y left axis", parent=self)
+ self.__y2AxisAction = qt.QAction("Y right axis", parent=self)
+
+ for action in (self.__xAxisAction, self.__yAxisAction, self.__y2AxisAction):
+ action.setCheckable(True)
+ action.setChecked(True)
+ action.triggered.connect(self._axesActionTriggered)
+ self.addAction(action)
+
+ # Listen to interaction configuration change
+ plot.interaction().sigChanged.connect(self._interactionChanged)
+ # Init the state
+ self._interactionChanged()
+
+ def getPlotWidget(self) -> Optional[PlotWidget]:
+ return self.__plotRef()
+
+ def _axesActionTriggered(self, checked=False):
+ plot = self.getPlotWidget()
+ if plot is None:
+ return
+
+ plot.interaction().setZoomEnabledAxes(
+ self.__xAxisAction.isChecked(),
+ self.__yAxisAction.isChecked(),
+ self.__y2AxisAction.isChecked(),
+ )
+
+ def _interactionChanged(self):
+ plot = self.getPlotWidget()
+ if plot is None:
+ return
+
+ enabledAxes = plot.interaction().getZoomEnabledAxes()
+ self.__xAxisAction.setChecked(enabledAxes.xaxis)
+ self.__yAxisAction.setChecked(enabledAxes.yaxis)
+ self.__y2AxisAction.setChecked(enabledAxes.y2axis)
diff --git a/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py b/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
index 09f90b7..271adb8 100644
--- a/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
+++ b/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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,7 +29,6 @@ __license__ = "MIT"
__date__ = "28/06/2018"
-from silx.utils import deprecation
from . import toolbar
@@ -38,16 +37,8 @@ class ScatterProfileToolBar(toolbar.ProfileToolBar):
:param parent: See :class:`QToolBar`.
:param plot: :class:`~silx.gui.plot.PlotWidget` on which to operate.
- :param str title: See :class:`QToolBar`.
"""
- def __init__(self, parent=None, plot=None, title=None):
+ def __init__(self, parent=None, plot=None):
super(ScatterProfileToolBar, self).__init__(parent, plot)
- if title is not None:
- deprecation.deprecated_warning("Attribute",
- name="title",
- reason="removed",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
self.setScheme("scatter")
diff --git a/src/silx/gui/plot/tools/profile/core.py b/src/silx/gui/plot/tools/profile/core.py
index 5d4a674..194f459 100644
--- a/src/silx/gui/plot/tools/profile/core.py
+++ b/src/silx/gui/plot/tools/profile/core.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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
@@ -24,49 +24,63 @@
"""This module define core objects for profile tools.
"""
+from __future__ import annotations
+
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel", "H. Payno", "V. Valls"]
__license__ = "MIT"
__date__ = "17/04/2020"
-import collections
+import typing
import numpy
import weakref
from silx.image.bilinear import BilinearImage
from silx.gui import qt
+from silx.gui import colors
+import silx.gui.plot.items
+
+
+class CurveProfileData(typing.NamedTuple):
+ coords: numpy.ndarray
+ profile: numpy.ndarray
+ title: str
+ xLabel: str
+ yLabel: str
+
+
+class RgbaProfileData(typing.NamedTuple):
+ coords: numpy.ndarray
+ profile: numpy.ndarray
+ profile_r: numpy.ndarray
+ profile_g: numpy.ndarray
+ profile_b: numpy.ndarray
+ profile_a: numpy.ndarray
+ title: str
+ xLabel: str
+ yLabel: str
+
+
+class ImageProfileData(typing.NamedTuple):
+ coords: numpy.ndarray
+ profile: numpy.ndarray
+ title: str
+ xLabel: str
+ yLabel: str
+ colormap: colors.Colormap
-CurveProfileData = collections.namedtuple(
- 'CurveProfileData', [
- "coords",
- "profile",
- "title",
- "xLabel",
- "yLabel",
- ])
-
-RgbaProfileData = collections.namedtuple(
- 'RgbaProfileData', [
- "coords",
- "profile",
- "profile_r",
- "profile_g",
- "profile_b",
- "profile_a",
- "title",
- "xLabel",
- "yLabel",
- ])
-
-ImageProfileData = collections.namedtuple(
- 'ImageProfileData', [
- 'coords',
- 'profile',
- 'title',
- 'xLabel',
- 'yLabel',
- 'colormap',
- ])
+class CurveProfileDesc(typing.NamedTuple):
+ profile: numpy.ndarray
+ name: typing.Optional[str] = None
+ color: typing.Optional[str] = None
+
+
+class CurvesProfileData(typing.NamedTuple):
+ coords: numpy.ndarray
+ profiles: typing.List[CurveProfileDesc]
+ title: str
+ xLabel: str
+ yLabel: str
class ProfileRoiMixIn:
@@ -107,7 +121,7 @@ class ProfileRoiMixIn:
def _setPlotItem(self, plotItem):
"""Specify the plot item to use with this profile
- :param `~silx.gui.plot.items.item.Item` plotItem: A plot item
+ :param `~silx.gui.plot.items.Item` plotItem: A plot item
"""
previousPlotItem = self.getPlotItem()
if previousPlotItem is plotItem:
@@ -118,7 +132,7 @@ class ProfileRoiMixIn:
def getPlotItem(self):
"""Returns the plot item used by this profile
- :rtype: `~silx.gui.plot.items.item.Item`
+ :rtype: `~silx.gui.plot.items.Item`
"""
if self.__plotItem is None:
return None
@@ -171,15 +185,18 @@ class ProfileRoiMixIn:
except ValueError:
pass
- def computeProfile(self, item):
+ def computeProfile(
+ self, item: silx.gui.plot.items.Item
+ ) -> typing.Union[
+ CurveProfileData, ImageProfileData, RgbaProfileData, CurvesProfileData
+ ]:
"""
Compute the profile which will be displayed.
This method is not called from the main Qt thread, but from a thread
pool.
- :param ~silx.gui.plot.items.Item item: A plot item
- :rtype: Union[CurveProfileData,ImageProfileData]
+ :param item: A plot item
"""
raise NotImplementedError()
@@ -201,7 +218,7 @@ def _alignedFullProfile(data, origin, scale, position, roiWidth, axis, method):
"""
assert axis in (0, 1)
assert len(data.shape) == 3
- assert method in ('mean', 'sum', 'none')
+ assert method in ("mean", "sum", "none")
# Convert from plot to image coords
imgPos = int((position - origin[1 - axis]) / scale[1 - axis])
@@ -215,31 +232,35 @@ def _alignedFullProfile(data, origin, scale, position, roiWidth, axis, method):
roiWidth = min(height, roiWidth) # Clip roi width to image size
# Get [start, end[ coords of the roi in the data
- start = int(int(imgPos) + 0.5 - roiWidth / 2.)
+ start = int(int(imgPos) + 0.5 - roiWidth / 2.0)
start = min(max(0, start), height - roiWidth)
end = start + roiWidth
- if method == 'none':
+ if method == "none":
profile = None
else:
if start < height and end > 0:
- if method == 'mean':
+ if method == "mean":
fct = numpy.mean
- elif method == 'sum':
+ elif method == "sum":
fct = numpy.sum
else:
- raise ValueError('method not managed')
- profile = fct(data[:, max(0, start):min(end, height), :], axis=1).astype(numpy.float32)
+ raise ValueError("method not managed")
+ profile = fct(data[:, max(0, start) : min(end, height), :], axis=1).astype(
+ numpy.float32
+ )
else:
profile = numpy.zeros((nimages, width), dtype=numpy.float32)
# Compute effective ROI in plot coords
- profileBounds = numpy.array(
- (0, width, width, 0),
- dtype=numpy.float32) * scale[axis] + origin[axis]
- roiBounds = numpy.array(
- (start, start, end, end),
- dtype=numpy.float32) * scale[1 - axis] + origin[1 - axis]
+ profileBounds = (
+ numpy.array((0, width, width, 0), dtype=numpy.float32) * scale[axis]
+ + origin[axis]
+ )
+ roiBounds = (
+ numpy.array((start, start, end, end), dtype=numpy.float32) * scale[1 - axis]
+ + origin[1 - axis]
+ )
if axis == 0: # Horizontal profile
area = profileBounds, roiBounds
@@ -272,7 +293,7 @@ def _alignedPartialProfile(data, rowRange, colRange, axis, method):
assert len(data.shape) == 3
assert rowRange[0] < rowRange[1]
assert colRange[0] < colRange[1]
- assert method in ('mean', 'sum')
+ assert method in ("mean", "sum")
nimages, height, width = data.shape
@@ -287,22 +308,23 @@ def _alignedPartialProfile(data, rowRange, colRange, axis, method):
colStart = min(max(0, colRange[0]), width)
colEnd = min(max(0, colRange[1]), width)
- if method == 'mean':
+ if method == "mean":
_fct = numpy.mean
- elif method == 'sum':
+ elif method == "sum":
_fct = numpy.sum
else:
- raise ValueError('method not managed')
+ raise ValueError("method not managed")
- imgProfile = _fct(data[:, rowStart:rowEnd, colStart:colEnd], axis=axis + 1,
- dtype=numpy.float32)
+ imgProfile = _fct(
+ data[:, rowStart:rowEnd, colStart:colEnd], axis=axis + 1, dtype=numpy.float32
+ )
# Profile including out of bound area
profile = numpy.zeros((nimages, profileLength), dtype=numpy.float32)
# Place imgProfile in full profile
- offset = - min(0, profileRange[0])
- profile[:, offset:offset + imgProfile.shape[1]] = imgProfile
+ offset = -min(0, profileRange[0])
+ profile[:, offset : offset + imgProfile.shape[1]] = imgProfile
return profile
@@ -346,14 +368,12 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
roiWidth = max(1, lineWidth)
roiStart, roiEnd, lineProjectionMode = roiInfo
- if lineProjectionMode == 'X': # Horizontal profile on the whole image
- profile, area = _alignedFullProfile(currentData3D,
- origin, scale,
- roiStart[1], roiWidth,
- axis=0,
- method=method)
+ if lineProjectionMode == "X": # Horizontal profile on the whole image
+ profile, area = _alignedFullProfile(
+ currentData3D, origin, scale, roiStart[1], roiWidth, axis=0, method=method
+ )
- if method == 'none':
+ if method == "none":
coords = None
else:
coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
@@ -361,19 +381,17 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
yMin, yMax = min(area[1]), max(area[1]) - 1
if roiWidth <= 1:
- profileName = '{ylabel} = %g' % yMin
+ profileName = "{ylabel} = %g" % yMin
else:
- profileName = '{ylabel} = [%g, %g]' % (yMin, yMax)
- xLabel = '{xlabel}'
+ profileName = "{ylabel} = [%g, %g]" % (yMin, yMax)
+ xLabel = "{xlabel}"
- elif lineProjectionMode == 'Y': # Vertical profile on the whole image
- profile, area = _alignedFullProfile(currentData3D,
- origin, scale,
- roiStart[0], roiWidth,
- axis=1,
- method=method)
+ elif lineProjectionMode == "Y": # Vertical profile on the whole image
+ profile, area = _alignedFullProfile(
+ currentData3D, origin, scale, roiStart[0], roiWidth, axis=1, method=method
+ )
- if method == 'none':
+ if method == "none":
coords = None
else:
coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
@@ -381,21 +399,20 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
xMin, xMax = min(area[0]), max(area[0]) - 1
if roiWidth <= 1:
- profileName = '{xlabel} = %g' % xMin
+ profileName = "{xlabel} = %g" % xMin
else:
- profileName = '{xlabel} = [%g, %g]' % (xMin, xMax)
- xLabel = '{ylabel}'
+ profileName = "{xlabel} = [%g, %g]" % (xMin, xMax)
+ xLabel = "{ylabel}"
else: # Free line profile
-
# Convert start and end points in image coords as (row, col)
- startPt = ((roiStart[1] - origin[1]) / scale[1],
- (roiStart[0] - origin[0]) / scale[0])
- endPt = ((roiEnd[1] - origin[1]) / scale[1],
- (roiEnd[0] - origin[0]) / scale[0])
+ startPt = (
+ (roiStart[1] - origin[1]) / scale[1],
+ (roiStart[0] - origin[0]) / scale[0],
+ )
+ endPt = ((roiEnd[1] - origin[1]) / scale[1], (roiEnd[0] - origin[0]) / scale[0])
- if (int(startPt[0]) == int(endPt[0]) or
- int(startPt[1]) == int(endPt[1])):
+ if int(startPt[0]) == int(endPt[0]) or int(startPt[1]) == int(endPt[1]):
# Profile is aligned with one of the axes
# Convert to int
@@ -407,62 +424,75 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
startPt, endPt = endPt, startPt
if startPt[0] == endPt[0]: # Row aligned
- rowRange = (int(startPt[0] + 0.5 - 0.5 * roiWidth),
- int(startPt[0] + 0.5 + 0.5 * roiWidth))
+ rowRange = (
+ int(startPt[0] + 0.5 - 0.5 * roiWidth),
+ int(startPt[0] + 0.5 + 0.5 * roiWidth),
+ )
colRange = startPt[1], endPt[1] + 1
- if method == 'none':
+ if method == "none":
profile = None
else:
- profile = _alignedPartialProfile(currentData3D,
- rowRange, colRange,
- axis=0,
- method=method)
+ profile = _alignedPartialProfile(
+ currentData3D, rowRange, colRange, axis=0, method=method
+ )
else: # Column aligned
rowRange = startPt[0], endPt[0] + 1
- colRange = (int(startPt[1] + 0.5 - 0.5 * roiWidth),
- int(startPt[1] + 0.5 + 0.5 * roiWidth))
- if method == 'none':
+ colRange = (
+ int(startPt[1] + 0.5 - 0.5 * roiWidth),
+ int(startPt[1] + 0.5 + 0.5 * roiWidth),
+ )
+ if method == "none":
profile = None
else:
- profile = _alignedPartialProfile(currentData3D,
- rowRange, colRange,
- axis=1,
- method=method)
+ profile = _alignedPartialProfile(
+ currentData3D, rowRange, colRange, axis=1, method=method
+ )
# Convert ranges to plot coords to draw ROI area
area = (
numpy.array(
(colRange[0], colRange[1], colRange[1], colRange[0]),
- dtype=numpy.float32) * scale[0] + origin[0],
+ dtype=numpy.float32,
+ )
+ * scale[0]
+ + origin[0],
numpy.array(
(rowRange[0], rowRange[0], rowRange[1], rowRange[1]),
- dtype=numpy.float32) * scale[1] + origin[1])
+ dtype=numpy.float32,
+ )
+ * scale[1]
+ + origin[1],
+ )
else: # General case: use bilinear interpolation
-
# Ensure startPt <= endPt
- if (startPt[1] > endPt[1] or (
- startPt[1] == endPt[1] and startPt[0] > endPt[0])):
+ if startPt[1] > endPt[1] or (
+ startPt[1] == endPt[1] and startPt[0] > endPt[0]
+ ):
startPt, endPt = endPt, startPt
- if method == 'none':
+ if method == "none":
profile = None
else:
profile = []
for slice_idx in range(currentData3D.shape[0]):
bilinear = BilinearImage(currentData3D[slice_idx, :, :])
- profile.append(bilinear.profile_line(
- (startPt[0] - 0.5, startPt[1] - 0.5),
- (endPt[0] - 0.5, endPt[1] - 0.5),
- roiWidth,
- method=method))
+ profile.append(
+ bilinear.profile_line(
+ (startPt[0] - 0.5, startPt[1] - 0.5),
+ (endPt[0] - 0.5, endPt[1] - 0.5),
+ roiWidth,
+ method=method,
+ )
+ )
profile = numpy.array(profile)
# Extend ROI with half a pixel on each end, and
# Convert back to plot coords (x, y)
- length = numpy.sqrt((endPt[0] - startPt[0]) ** 2 +
- (endPt[1] - startPt[1]) ** 2)
+ length = numpy.sqrt(
+ (endPt[0] - startPt[0]) ** 2 + (endPt[1] - startPt[1]) ** 2
+ )
dRow = (endPt[0] - startPt[0]) / length
dCol = (endPt[1] - startPt[1]) / length
@@ -474,16 +504,29 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
dRow, dCol = dCol, -dRow
area = (
- numpy.array((roiStartPt[1] - 0.5 * roiWidth * dCol,
- roiStartPt[1] + 0.5 * roiWidth * dCol,
- roiEndPt[1] + 0.5 * roiWidth * dCol,
- roiEndPt[1] - 0.5 * roiWidth * dCol),
- dtype=numpy.float32) * scale[0] + origin[0],
- numpy.array((roiStartPt[0] - 0.5 * roiWidth * dRow,
- roiStartPt[0] + 0.5 * roiWidth * dRow,
- roiEndPt[0] + 0.5 * roiWidth * dRow,
- roiEndPt[0] - 0.5 * roiWidth * dRow),
- dtype=numpy.float32) * scale[1] + origin[1])
+ numpy.array(
+ (
+ roiStartPt[1] - 0.5 * roiWidth * dCol,
+ roiStartPt[1] + 0.5 * roiWidth * dCol,
+ roiEndPt[1] + 0.5 * roiWidth * dCol,
+ roiEndPt[1] - 0.5 * roiWidth * dCol,
+ ),
+ dtype=numpy.float32,
+ )
+ * scale[0]
+ + origin[0],
+ numpy.array(
+ (
+ roiStartPt[0] - 0.5 * roiWidth * dRow,
+ roiStartPt[0] + 0.5 * roiWidth * dRow,
+ roiEndPt[0] + 0.5 * roiWidth * dRow,
+ roiEndPt[0] - 0.5 * roiWidth * dRow,
+ ),
+ dtype=numpy.float32,
+ )
+ * scale[1]
+ + origin[1],
+ )
# Convert start and end points back to plot coords
y0 = startPt[0] * scale[1] + origin[1]
@@ -492,33 +535,33 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
x1 = endPt[1] * scale[0] + origin[0]
if startPt[1] == endPt[1]:
- profileName = '{xlabel} = %g; {ylabel} = [%g, %g]' % (x0, y0, y1)
- if method == 'none':
+ profileName = "{xlabel} = %g; {ylabel} = [%g, %g]" % (x0, y0, y1)
+ if method == "none":
coords = None
else:
coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
coords = coords * scale[1] + y0
- xLabel = '{ylabel}'
+ xLabel = "{ylabel}"
elif startPt[0] == endPt[0]:
- profileName = '{ylabel} = %g; {xlabel} = [%g, %g]' % (y0, x0, x1)
- if method == 'none':
+ profileName = "{ylabel} = %g; {xlabel} = [%g, %g]" % (y0, x0, x1)
+ if method == "none":
coords = None
else:
coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
coords = coords * scale[0] + x0
- xLabel = '{xlabel}'
+ xLabel = "{xlabel}"
else:
m = (y1 - y0) / (x1 - x0)
b = y0 - m * x0
- profileName = '{ylabel} = %g * {xlabel} %+g' % (m, b)
- if method == 'none':
+ profileName = "{ylabel} = %g * {xlabel} %+g" % (m, b)
+ if method == "none":
coords = None
else:
- coords = numpy.linspace(x0, x1, len(profile[0]),
- endpoint=True,
- dtype=numpy.float32)
- xLabel = '{xlabel}'
+ coords = numpy.linspace(
+ x0, x1, len(profile[0]), endpoint=True, dtype=numpy.float32
+ )
+ xLabel = "{xlabel}"
return coords, profile, area, profileName, xLabel
diff --git a/src/silx/gui/plot/tools/profile/editors.py b/src/silx/gui/plot/tools/profile/editors.py
index 1d6f198..d53f775 100644
--- a/src/silx/gui/plot/tools/profile/editors.py
+++ b/src/silx/gui/plot/tools/profile/editors.py
@@ -43,7 +43,6 @@ _logger = logging.getLogger(__name__)
class _NoProfileRoiEditor(qt.QWidget):
-
sigDataCommited = qt.Signal()
def setEditorData(self, roi):
@@ -54,7 +53,6 @@ class _NoProfileRoiEditor(qt.QWidget):
class _DefaultImageProfileRoiEditor(qt.QWidget):
-
sigDataCommited = qt.Signal()
def __init__(self, parent=None):
@@ -72,7 +70,7 @@ class _DefaultImageProfileRoiEditor(qt.QWidget):
self._methodsButton = ProfileOptionToolButton(parent=self, plot=None)
self._methodsButton.sigMethodChanged.connect(self._widgetChanged)
- label = qt.QLabel('W:')
+ label = qt.QLabel("W:")
label.setToolTip("Line width in pixels")
layout.addWidget(label)
layout.addWidget(self._lineWidth)
@@ -99,7 +97,6 @@ class _DefaultImageProfileRoiEditor(qt.QWidget):
class _DefaultImageStackProfileRoiEditor(_DefaultImageProfileRoiEditor):
-
def _initLayout(self, layout):
super(_DefaultImageStackProfileRoiEditor, self)._initLayout(layout)
self._profileDim = ProfileToolButton(parent=self, plot=None)
@@ -121,7 +118,6 @@ class _DefaultImageStackProfileRoiEditor(_DefaultImageProfileRoiEditor):
class _DefaultScatterProfileRoiEditor(qt.QWidget):
-
sigDataCommited = qt.Signal()
def __init__(self, parent=None):
@@ -134,7 +130,7 @@ class _DefaultScatterProfileRoiEditor(qt.QWidget):
layout = qt.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
- label = qt.QLabel('Samples:')
+ label = qt.QLabel("Samples:")
label.setToolTip("Number of sample points of the profile")
layout.addWidget(label)
layout.addWidget(self._nPoints)
@@ -160,6 +156,7 @@ class ProfileRoiEditorAction(qt.QWidgetAction):
:param qt.QWidget parent: Parent widget
"""
+
def __init__(self, parent=None):
super(ProfileRoiEditorAction, self).__init__(parent)
self.__roiManager = None
@@ -237,8 +234,7 @@ class ProfileRoiEditorAction(qt.QWidgetAction):
return self.__roi
def __roiPropertyChanged(self):
- """Handle changes on the property defining the ROI.
- """
+ """Handle changes on the property defining the ROI."""
self._updateWidgetValues()
def __setEditor(self, widget, editor):
@@ -265,16 +261,20 @@ class ProfileRoiEditorAction(qt.QWidgetAction):
"""Returns the editor class to use according to the ROI."""
if roi is None:
editorClass = _NoProfileRoiEditor
- elif isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn,
- rois.ProfileImageStackCrossROI)):
+ elif isinstance(
+ roi,
+ (rois._DefaultImageStackProfileRoiMixIn, rois.ProfileImageStackCrossROI),
+ ):
# Must be done before the default image ROI
# Cause ImageStack ROIs inherit from Image ROIs
editorClass = _DefaultImageStackProfileRoiEditor
- elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn,
- rois.ProfileImageCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultImageProfileRoiMixIn, rois.ProfileImageCrossROI)
+ ):
editorClass = _DefaultImageProfileRoiEditor
- elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn,
- rois.ProfileScatterCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultScatterProfileRoiMixIn, rois.ProfileScatterCrossROI)
+ ):
editorClass = _DefaultScatterProfileRoiEditor
else:
# Unsupported
diff --git a/src/silx/gui/plot/tools/profile/manager.py b/src/silx/gui/plot/tools/profile/manager.py
index 58c1c86..6f4ba35 100644
--- a/src/silx/gui/plot/tools/profile/manager.py
+++ b/src/silx/gui/plot/tools/profile/manager.py
@@ -64,12 +64,12 @@ class _RunnableComputeProfile(qt.QRunnable):
class _Signals(qt.QObject):
"""Signal holder"""
+
resultReady = qt.Signal(object, object)
runnerFinished = qt.Signal(object)
def __init__(self, threadPool, item, roi):
- """Constructor
- """
+ """Constructor"""
super(_RunnableComputeProfile, self).__init__()
self._signals = self._Signals()
self._signals.moveToThread(threadPool.thread())
@@ -114,8 +114,7 @@ class _RunnableComputeProfile(qt.QRunnable):
return self._signals.runnerFinished
def run(self):
- """Process the profile computation.
- """
+ """Process the profile computation."""
if not self._cancelled:
try:
profileData = self._roi.computeProfile(self._item)
@@ -141,7 +140,7 @@ class ProfileWindow(qt.QMainWindow):
def __init__(self, parent=None, backend=None):
qt.QMainWindow.__init__(self, parent=parent, flags=qt.Qt.Dialog)
- self.setWindowTitle('Profile window')
+ self.setWindowTitle("Profile window")
self._plot1D = None
self._plot2D = None
self._backend = backend
@@ -175,10 +174,11 @@ class ProfileWindow(qt.QMainWindow):
"""
# import here to avoid circular import
from ...PlotWindow import Plot1D
+
plot = Plot1D(parent=parent, backend=backend)
plot.setDataMargins(yMinMargin=0.1, yMaxMargin=0.1)
- plot.setGraphYLabel('Profile')
- plot.setGraphXLabel('')
+ plot.setGraphYLabel("Profile")
+ plot.setGraphXLabel("")
positionInfo = plot.getPositionInfoWidget()
positionInfo.setSnappingMode(positionInfo.SNAPPING_CURVE)
return plot
@@ -194,6 +194,7 @@ class ProfileWindow(qt.QMainWindow):
"""
# import here to avoid circular import
from ...PlotWindow import Plot2D
+
return Plot2D(parent=parent, backend=backend)
def getPlot1D(self, init=True):
@@ -241,12 +242,12 @@ class ProfileWindow(qt.QMainWindow):
return
self.__color = colors.rgba(roi.getColor())
- def _setImageProfile(self, data):
+ def _setImageProfile(self, data: core.ImageProfileData):
"""
Setup the window to display a new profile data which is represented
by an image.
- :param core.ImageProfileData data: Computed data profile
+ :param data: Computed data profile
"""
plot = self.getPlot2D()
@@ -254,25 +255,26 @@ class ProfileWindow(qt.QMainWindow):
plot.setGraphTitle(data.title)
plot.getXAxis().setLabel(data.xLabel)
-
coords = data.coords
colormap = data.colormap
profileScale = (coords[-1] - coords[0]) / data.profile.shape[1], 1
- plot.addImage(data.profile,
- legend="profile",
- colormap=colormap,
- origin=(coords[0], 0),
- scale=profileScale)
+ plot.addImage(
+ data.profile,
+ legend="profile",
+ colormap=colormap,
+ origin=(coords[0], 0),
+ scale=profileScale,
+ )
plot.getYAxis().setLabel("Frame index (depth)")
self._showPlot2D()
- def _setCurveProfile(self, data):
+ def _setCurveProfile(self, data: core.CurveProfileData):
"""
Setup the window to display a new profile data which is represented
by a curve.
- :param core.CurveProfileData data: Computed data profile
+ :param data: Computed data profile
"""
plot = self.getPlot1D()
@@ -281,19 +283,16 @@ class ProfileWindow(qt.QMainWindow):
plot.getXAxis().setLabel(data.xLabel)
plot.getYAxis().setLabel(data.yLabel)
- plot.addCurve(data.coords,
- data.profile,
- legend="level",
- color=self.__color)
+ plot.addCurve(data.coords, data.profile, legend="level", color=self.__color)
self._showPlot1D()
- def _setRgbaProfile(self, data):
+ def _setRgbaProfile(self, data: core.RgbaProfileData):
"""
Setup the window to display a new profile data which is represented
by a curve.
- :param core.RgbaProfileData data: Computed data profile
+ :param data: Computed data profile
"""
plot = self.getPlot1D()
@@ -304,17 +303,33 @@ class ProfileWindow(qt.QMainWindow):
self._showPlot1D()
- plot.addCurve(data.coords, data.profile,
- legend="level", color="black")
- plot.addCurve(data.coords, data.profile_r,
- legend="red", color="red")
- plot.addCurve(data.coords, data.profile_g,
- legend="green", color="green")
- plot.addCurve(data.coords, data.profile_b,
- legend="blue", color="blue")
+ plot.addCurve(data.coords, data.profile, legend="level", color="black")
+ plot.addCurve(data.coords, data.profile_r, legend="red", color="red")
+ plot.addCurve(data.coords, data.profile_g, legend="green", color="green")
+ plot.addCurve(data.coords, data.profile_b, legend="blue", color="blue")
if data.profile_a is not None:
plot.addCurve(data.coords, data.profile_a, legend="alpha", color="gray")
+ def _setCurvesProfile(self, data: core.CurvesProfileData):
+ """
+ Setup the window to display a new profile data which is represented
+ by multiple curves.
+
+ :param data: Computed data profile
+ """
+ plot = self.getPlot1D()
+
+ plot.clear()
+ plot.setGraphTitle(data.title)
+ plot.getXAxis().setLabel(data.xLabel)
+ plot.getYAxis().setLabel(data.yLabel)
+
+ self._showPlot1D()
+
+ for i, desc in enumerate(data.profiles):
+ name = desc.name if desc.name is not None else f"profile{i}"
+ plot.addCurve(data.coords, desc.profile, legend=name, color=desc.color)
+
def clear(self):
"""Clear the window profile"""
plot = self.getPlot1D(init=False)
@@ -346,6 +361,8 @@ class ProfileWindow(qt.QMainWindow):
self._setRgbaProfile(data)
elif isinstance(data, core.CurveProfileData):
self._setCurveProfile(data)
+ elif isinstance(data, core.CurvesProfileData):
+ self._setCurvesProfile(data)
else:
raise TypeError("Unsupported type %s" % type(data))
@@ -359,10 +376,10 @@ class _ClearAction(qt.QAction):
def __init__(self, parent, profileManager):
super(_ClearAction, self).__init__(parent)
self.__profileManager = weakref.ref(profileManager)
- icon = icons.getQIcon('profile-clear')
+ icon = icons.getQIcon("profile-clear")
self.setIcon(icon)
- self.setText('Clear profile')
- self.setToolTip('Clear the profiles')
+ self.setText("Clear profile")
+ self.setToolTip("Clear the profiles")
self.setCheckable(False)
self.setEnabled(False)
self.triggered.connect(profileManager.clearProfile)
@@ -420,37 +437,47 @@ class _StoreLastParamBehavior(qt.QObject):
if previousRoi is roi:
return
if previousRoi is not None:
- previousRoi.sigProfilePropertyChanged.disconnect(self._profilePropertyChanged)
+ previousRoi.sigProfilePropertyChanged.disconnect(
+ self._profilePropertyChanged
+ )
self.__profileRoi = None if roi is None else weakref.ref(roi)
if roi is not None:
roi.sigProfilePropertyChanged.connect(self._profilePropertyChanged)
def _profilePropertyChanged(self):
- """Handle changes on the properties defining the profile ROI.
- """
+ """Handle changes on the properties defining the profile ROI."""
if self.__filter.locked():
return
roi = self.sender()
self.storeProperties(roi)
def storeProperties(self, roi):
- if isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn,
- rois.ProfileImageStackCrossROI)):
+ if isinstance(
+ roi,
+ (rois._DefaultImageStackProfileRoiMixIn, rois.ProfileImageStackCrossROI),
+ ):
self.__properties["method"] = roi.getProfileMethod()
self.__properties["line-width"] = roi.getProfileLineWidth()
self.__properties["type"] = roi.getProfileType()
- elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn,
- rois.ProfileImageCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultImageProfileRoiMixIn, rois.ProfileImageCrossROI)
+ ):
self.__properties["method"] = roi.getProfileMethod()
self.__properties["line-width"] = roi.getProfileLineWidth()
- elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn,
- rois.ProfileScatterCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultScatterProfileRoiMixIn, rois.ProfileScatterCrossROI)
+ ):
self.__properties["npoints"] = roi.getNPoints()
def restoreProperties(self, roi):
with self.__filter:
- if isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn,
- rois.ProfileImageStackCrossROI)):
+ if isinstance(
+ roi,
+ (
+ rois._DefaultImageStackProfileRoiMixIn,
+ rois.ProfileImageStackCrossROI,
+ ),
+ ):
value = self.__properties.get("method", None)
if value is not None:
roi.setProfileMethod(value)
@@ -460,16 +487,18 @@ class _StoreLastParamBehavior(qt.QObject):
value = self.__properties.get("type", None)
if value is not None:
roi.setProfileType(value)
- elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn,
- rois.ProfileImageCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultImageProfileRoiMixIn, rois.ProfileImageCrossROI)
+ ):
value = self.__properties.get("method", None)
if value is not None:
roi.setProfileMethod(value)
value = self.__properties.get("line-width", None)
if value is not None:
roi.setProfileLineWidth(value)
- elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn,
- rois.ProfileScatterCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultScatterProfileRoiMixIn, rois.ProfileScatterCrossROI)
+ ):
value = self.__properties.get("npoints", None)
if value is not None:
roi.setNPoints(value)
@@ -482,12 +511,12 @@ class ProfileManager(qt.QObject):
:param plot: :class:`~silx.gui.plot.tools.roi.RegionOfInterestManager`
on which to operate.
"""
+
def __init__(self, parent=None, plot=None, roiManager=None):
super(ProfileManager, self).__init__(parent)
assert isinstance(plot, PlotWidget)
- self._plotRef = weakref.ref(
- plot, WeakMethodProxy(self.__plotDestroyed))
+ self._plotRef = weakref.ref(plot, WeakMethodProxy(self.__plotDestroyed))
# Set-up interaction manager
if roiManager is None:
@@ -590,14 +619,16 @@ class ProfileManager(qt.QObject):
if hasattr(profileRoiClass, "ICON"):
action.setIcon(icons.getQIcon(profileRoiClass.ICON))
if hasattr(profileRoiClass, "NAME"):
+
def articulify(word):
"""Add an an/a article in the front of the word"""
- first = word[1] if word[0] == 'h' else word[0]
+ first = word[1] if word[0] == "h" else word[0]
if first in "aeiou":
return "an " + word
return "a " + word
- action.setText('Define %s' % articulify(profileRoiClass.NAME))
- action.setToolTip('Enables %s selection mode' % profileRoiClass.NAME)
+
+ action.setText("Define %s" % articulify(profileRoiClass.NAME))
+ action.setToolTip("Enables %s selection mode" % profileRoiClass.NAME)
action.setSingleShot(True)
return action
@@ -623,7 +654,7 @@ class ProfileManager(qt.QObject):
rois.ProfileImageLineROI,
rois.ProfileImageDirectedLineROI,
rois.ProfileImageCrossROI,
- ]
+ ]
return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
def createScatterActions(self, parent):
@@ -638,7 +669,7 @@ class ProfileManager(qt.QObject):
rois.ProfileScatterVerticalLineROI,
rois.ProfileScatterLineROI,
rois.ProfileScatterCrossROI,
- ]
+ ]
return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
def createScatterSliceActions(self, parent):
@@ -655,7 +686,7 @@ class ProfileManager(qt.QObject):
rois.ProfileScatterHorizontalSliceROI,
rois.ProfileScatterVerticalSliceROI,
rois.ProfileScatterCrossSliceROI,
- ]
+ ]
return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
def createImageStackActions(self, parent):
@@ -673,7 +704,7 @@ class ProfileManager(qt.QObject):
rois.ProfileImageStackVerticalLineROI,
rois.ProfileImageStackLineROI,
rois.ProfileImageStackCrossROI,
- ]
+ ]
return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
def createEditorAction(self, parent):
@@ -705,8 +736,7 @@ class ProfileManager(qt.QObject):
self.setPlotItem(item)
def setProfileWindowClass(self, profileWindowClass):
- """Set the class which will be instantiated to display profile result.
- """
+ """Set the class which will be instantiated to display profile result."""
self._profileWindowClass = profileWindowClass
def setActiveItemTracking(self, tracking):
@@ -798,7 +828,7 @@ class ProfileManager(qt.QObject):
roiManager.removeRoi(roi)
if not roiManager.isDrawing():
- # Clean the selected mode
+ # Clean the selected mode
roiManager.stop()
def hasPendingOperations(self):
@@ -809,8 +839,7 @@ class ProfileManager(qt.QObject):
return len(self.__reentrantResults) > 0 or len(self._pendingRunners) > 0
def requestUpdateAllProfile(self):
- """Request to update the profile of all the managed ROIs.
- """
+ """Request to update the profile of all the managed ROIs."""
for roi in self._rois:
self.requestUpdateProfile(roi)
@@ -868,7 +897,7 @@ class ProfileManager(qt.QObject):
if roi in self.__reentrantResults:
# Store the data to process it in the main loop
# And not a sub loop created by initProfileWindow
- # This also remove the duplicated requested
+ # This also remove the duplicated requested
self.__reentrantResults[roi] = profileData
return
@@ -918,7 +947,7 @@ class ProfileManager(qt.QObject):
:param ~silx.gui.plot.items.item.Item item: AN item
:rtype: qt.QColor
"""
- color = 'pink'
+ color = "pink"
if isinstance(item, items.ColormapMixIn):
colormap = item.getColormap()
name = colormap.getName()
@@ -948,12 +977,13 @@ class ProfileManager(qt.QObject):
roi.setColor(color)
def __itemChanged(self, changeType):
- """Handle item changes.
- """
- if changeType in (items.ItemChangedType.DATA,
- items.ItemChangedType.MASK,
- items.ItemChangedType.POSITION,
- items.ItemChangedType.SCALE):
+ """Handle item changes."""
+ if changeType in (
+ items.ItemChangedType.DATA,
+ items.ItemChangedType.MASK,
+ items.ItemChangedType.POSITION,
+ items.ItemChangedType.SCALE,
+ ):
self.requestUpdateAllProfile()
elif changeType == (items.ItemChangedType.COLORMAP):
self._updateRoiColors()
@@ -1040,7 +1070,7 @@ class ProfileManager(qt.QObject):
window = self.getPlotWidget().window()
winGeom = window.frameGeometry()
- if qt.BINDING in ("PySide2", "PyQt5"):
+ if qt.BINDING == "PyQt5":
qapp = qt.QApplication.instance()
desktop = qapp.desktop()
screenGeom = desktop.availableGeometry(window)
@@ -1070,7 +1100,6 @@ class ProfileManager(qt.QObject):
left = screenGeom.width() - profileGeom.width()
profileWindow.move(left, top)
-
def clearProfileWindow(self, profileWindow):
"""Called when a profile window is not anymore needed.
diff --git a/src/silx/gui/plot/tools/profile/rois.py b/src/silx/gui/plot/tools/profile/rois.py
index 042aff1..23f086a 100644
--- a/src/silx/gui/plot/tools/profile/rois.py
+++ b/src/silx/gui/plot/tools/profile/rois.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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
@@ -75,13 +75,13 @@ def _lineProfileTitle(x0, y0, x1, y1):
:rtype: str
"""
if x0 == x1:
- title = '{xlabel} = %g; {ylabel} = [%g, %g]' % (x0, y0, y1)
+ title = "{xlabel} = %g; {ylabel} = [%g, %g]" % (x0, y0, y1)
elif y0 == y1:
- title = '{ylabel} = %g; {xlabel} = [%g, %g]' % (y0, x0, x1)
+ title = "{ylabel} = %g; {xlabel} = [%g, %g]" % (y0, x0, x1)
else:
m = (y1 - y0) / (x1 - x0)
b = y0 - m * x0
- title = '{ylabel} = %g * {xlabel} %+g' % (m, b)
+ title = "{ylabel} = %g * {xlabel} %+g" % (m, b)
return title
@@ -147,7 +147,8 @@ class _ImageProfileArea(items.Shape):
origin=origin,
scale=scale,
lineWidth=roi.getProfileLineWidth(),
- method="none")
+ method="none",
+ )
return area
@@ -214,8 +215,7 @@ class _SliceProfileArea(items.Shape):
class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn):
- """Provide common behavior for silx default image profile ROI.
- """
+ """Provide common behavior for silx default image profile ROI."""
ITEM_KIND = items.ImageBase
@@ -265,21 +265,21 @@ class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn):
def _getRoiInfo(self):
"""Wrapper to allow to reuse the previous Profile code.
-
+
It would be good to remove it at one point.
"""
if isinstance(self, roi_items.HorizontalLineROI):
- lineProjectionMode = 'X'
+ lineProjectionMode = "X"
y = self.getPosition()
roiStart = (0, y)
roiEnd = (1, y)
elif isinstance(self, roi_items.VerticalLineROI):
- lineProjectionMode = 'Y'
+ lineProjectionMode = "Y"
x = self.getPosition()
roiStart = (x, 0)
roiEnd = (x, 1)
elif isinstance(self, roi_items.LineROI):
- lineProjectionMode = 'D'
+ lineProjectionMode = "D"
roiStart, roiEnd = self.getEndPoints()
else:
assert False
@@ -294,15 +294,17 @@ class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn):
scale = item.getScale()
method = self.getProfileMethod()
lineWidth = self.getProfileLineWidth()
+ roiInfo = self._getRoiInfo()
def createProfile2(currentData):
coords, profile, _area, profileName, xLabel = core.createProfile(
- roiInfo=self._getRoiInfo(),
+ roiInfo=roiInfo,
currentData=currentData,
origin=origin,
scale=scale,
lineWidth=lineWidth,
- method=method)
+ method=method,
+ )
return coords, profile, profileName, xLabel
currentData = item.getValueData(copy=False)
@@ -348,61 +350,61 @@ class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn):
return data
-class ProfileImageHorizontalLineROI(roi_items.HorizontalLineROI,
- _DefaultImageProfileRoiMixIn):
+class ProfileImageHorizontalLineROI(
+ roi_items.HorizontalLineROI, _DefaultImageProfileRoiMixIn
+):
"""ROI for an horizontal profile at a location of an image"""
- ICON = 'shape-horizontal'
- NAME = 'horizontal line profile'
+ ICON = "shape-horizontal"
+ NAME = "horizontal line profile"
def __init__(self, parent=None):
roi_items.HorizontalLineROI.__init__(self, parent=parent)
_DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileImageVerticalLineROI(roi_items.VerticalLineROI,
- _DefaultImageProfileRoiMixIn):
+class ProfileImageVerticalLineROI(
+ roi_items.VerticalLineROI, _DefaultImageProfileRoiMixIn
+):
"""ROI for a vertical profile at a location of an image"""
- ICON = 'shape-vertical'
- NAME = 'vertical line profile'
+ ICON = "shape-vertical"
+ NAME = "vertical line profile"
def __init__(self, parent=None):
roi_items.VerticalLineROI.__init__(self, parent=parent)
_DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileImageLineROI(roi_items.LineROI,
- _DefaultImageProfileRoiMixIn):
+class ProfileImageLineROI(roi_items.LineROI, _DefaultImageProfileRoiMixIn):
"""ROI for an image profile between 2 points.
The X profile of this ROI is the projecting into one of the x/y axes,
using its scale and its orientation.
"""
- ICON = 'shape-diagonal'
- NAME = 'line profile'
+ ICON = "shape-diagonal"
+ NAME = "line profile"
def __init__(self, parent=None):
roi_items.LineROI.__init__(self, parent=parent)
_DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileImageDirectedLineROI(roi_items.LineROI,
- _DefaultImageProfileRoiMixIn):
+class ProfileImageDirectedLineROI(roi_items.LineROI, _DefaultImageProfileRoiMixIn):
"""ROI for an image profile between 2 points.
The X profile of the line is displayed projected into the line itself,
using its scale and its orientation. It's the distance from the origin.
"""
- ICON = 'shape-diagonal-directed'
- NAME = 'directed line profile'
+ ICON = "shape-diagonal-directed"
+ NAME = "directed line profile"
def __init__(self, parent=None):
roi_items.LineROI.__init__(self, parent=parent)
_DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
- self._handleStart.setSymbol('o')
+ self._handleStart.setSymbol("o")
def computeProfile(self, item):
if not isinstance(item, items.ImageBase):
@@ -419,10 +421,11 @@ class ProfileImageDirectedLineROI(roi_items.LineROI,
roiInfo = self._getRoiInfo()
roiStart, roiEnd, _lineProjectionMode = roiInfo
- startPt = ((roiStart[1] - origin[1]) / scale[1],
- (roiStart[0] - origin[0]) / scale[0])
- endPt = ((roiEnd[1] - origin[1]) / scale[1],
- (roiEnd[0] - origin[0]) / scale[0])
+ startPt = (
+ (roiStart[1] - origin[1]) / scale[1],
+ (roiStart[0] - origin[0]) / scale[0],
+ )
+ endPt = ((roiEnd[1] - origin[1]) / scale[1], (roiEnd[0] - origin[0]) / scale[0])
if numpy.array_equal(startPt, endPt):
return None
@@ -432,14 +435,16 @@ class ProfileImageDirectedLineROI(roi_items.LineROI,
(startPt[0] - 0.5, startPt[1] - 0.5),
(endPt[0] - 0.5, endPt[1] - 0.5),
lineWidth,
- method=method)
+ method=method,
+ )
# Compute the line size
- lineSize = numpy.sqrt((roiEnd[1] - roiStart[1]) ** 2 +
- (roiEnd[0] - roiStart[0]) ** 2)
- coords = numpy.linspace(0, lineSize, len(profile),
- endpoint=True,
- dtype=numpy.float32)
+ lineSize = numpy.sqrt(
+ (roiEnd[1] - roiStart[1]) ** 2 + (roiEnd[0] - roiStart[0]) ** 2
+ )
+ coords = numpy.linspace(
+ 0, lineSize, len(profile), endpoint=True, dtype=numpy.float32
+ )
title = _lineProfileTitle(*roiStart, *roiEnd)
title = title + "; width = %d" % lineWidth
@@ -530,8 +535,7 @@ class _ProfileCrossROI(roi_items.HandleBasedROI, core.ProfileRoiMixIn):
def __updateLineProperty(self, event=None, checkVisibility=True):
if event == items.ItemChangedType.NAME:
self.__handleLabel.setText(self.getName())
- elif event in [items.ItemChangedType.COLOR,
- items.ItemChangedType.VISIBLE]:
+ elif event in [items.ItemChangedType.COLOR, items.ItemChangedType.VISIBLE]:
lines = []
if self.__vline:
lines.append(self.__vline)
@@ -658,8 +662,8 @@ class ProfileImageCrossROI(_ProfileCrossROI):
It is managed using 2 sub ROIs for vertical and horizontal.
"""
- ICON = 'shape-cross'
- NAME = 'cross profile'
+ ICON = "shape-cross"
+ NAME = "cross profile"
ITEM_KIND = items.ImageBase
def _createLines(self, parent):
@@ -692,8 +696,7 @@ class ProfileImageCrossROI(_ProfileCrossROI):
class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
- """Provide common behavior for silx default scatter profile ROI.
- """
+ """Provide common behavior for silx default scatter profile ROI."""
ITEM_KIND = items.Scatter
@@ -736,7 +739,7 @@ class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
:param float y1: Profile end point Y coord
:return: (points, values) profile data or None
"""
- future = scatter._getInterpolator()
+ future = scatter._getInterpolatorFuture()
try:
interpolator = future.result()
except CancelledError:
@@ -745,15 +748,14 @@ class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
return None # Cannot init an interpolator
nPoints = self.getNPoints()
- points = numpy.transpose((
- numpy.linspace(x0, x1, nPoints, endpoint=True),
- numpy.linspace(y0, y1, nPoints, endpoint=True)))
-
- values = interpolator(points)
+ x = numpy.linspace(x0, x1, nPoints, endpoint=True)
+ y = numpy.linspace(y0, y1, nPoints, endpoint=True)
+ values = interpolator(x, y)
if not numpy.any(numpy.isfinite(values)):
return None # Profile outside convex hull
+ points = numpy.transpose((x, y))
return points, values
def computeProfile(self, item):
@@ -778,7 +780,7 @@ class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
x0 = x1 = self.getPosition()
y0, y1 = plot.getYAxis().getLimits()
else:
- raise RuntimeError('Unsupported ROI for profile: {}'.format(self.__class__))
+ raise RuntimeError("Unsupported ROI for profile: {}".format(self.__class__))
if x1 < x0 or (x1 == x0 and y1 < y0):
# Invert points
@@ -792,13 +794,14 @@ class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
points = profile[0]
values = profile[1]
- if (numpy.abs(points[-1, 0] - points[0, 0]) >
- numpy.abs(points[-1, 1] - points[0, 1])):
+ if numpy.abs(points[-1, 0] - points[0, 0]) > numpy.abs(
+ points[-1, 1] - points[0, 1]
+ ):
xProfile = points[:, 0]
- xLabel = '{xlabel}'
+ xLabel = "{xlabel}"
else:
xProfile = points[:, 1]
- xLabel = '{ylabel}'
+ xLabel = "{ylabel}"
# Use the axis names from the original
profileManager = self.getProfileManager()
@@ -811,41 +814,42 @@ class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
profile=values,
title=title,
xLabel=xLabel,
- yLabel='Profile',
+ yLabel="Profile",
)
return data
-class ProfileScatterHorizontalLineROI(roi_items.HorizontalLineROI,
- _DefaultScatterProfileRoiMixIn):
+class ProfileScatterHorizontalLineROI(
+ roi_items.HorizontalLineROI, _DefaultScatterProfileRoiMixIn
+):
"""ROI for an horizontal profile at a location of a scatter"""
- ICON = 'shape-horizontal'
- NAME = 'horizontal line profile'
+ ICON = "shape-horizontal"
+ NAME = "horizontal line profile"
def __init__(self, parent=None):
roi_items.HorizontalLineROI.__init__(self, parent=parent)
_DefaultScatterProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileScatterVerticalLineROI(roi_items.VerticalLineROI,
- _DefaultScatterProfileRoiMixIn):
+class ProfileScatterVerticalLineROI(
+ roi_items.VerticalLineROI, _DefaultScatterProfileRoiMixIn
+):
"""ROI for an horizontal profile at a location of a scatter"""
- ICON = 'shape-vertical'
- NAME = 'vertical line profile'
+ ICON = "shape-vertical"
+ NAME = "vertical line profile"
def __init__(self, parent=None):
roi_items.VerticalLineROI.__init__(self, parent=parent)
_DefaultScatterProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileScatterLineROI(roi_items.LineROI,
- _DefaultScatterProfileRoiMixIn):
+class ProfileScatterLineROI(roi_items.LineROI, _DefaultScatterProfileRoiMixIn):
"""ROI for an horizontal profile at a location of a scatter"""
- ICON = 'shape-diagonal'
- NAME = 'line profile'
+ ICON = "shape-diagonal"
+ NAME = "line profile"
def __init__(self, parent=None):
roi_items.LineROI.__init__(self, parent=parent)
@@ -853,11 +857,10 @@ class ProfileScatterLineROI(roi_items.LineROI,
class ProfileScatterCrossROI(_ProfileCrossROI):
- """ROI to manage a cross of profiles for scatters.
- """
+ """ROI to manage a cross of profiles for scatters."""
- ICON = 'shape-cross'
- NAME = 'cross profile'
+ ICON = "shape-cross"
+ NAME = "cross profile"
ITEM_KIND = items.Scatter
def _createLines(self, parent):
@@ -909,7 +912,9 @@ class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn):
def _getSlice(self, item):
position = self.getPosition()
- bounds = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_BOUNDS)
+ bounds = item.getCurrentVisualizationParameter(
+ items.Scatter.VisualizationParameter.GRID_BOUNDS
+ )
if isinstance(self, roi_items.HorizontalLineROI):
axis = 1
elif isinstance(self, roi_items.VerticalLineROI):
@@ -920,21 +925,25 @@ class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn):
# ROI outside of the scatter bound
return None
- major_order = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_MAJOR_ORDER)
- assert major_order == 'row'
- max_grid_yy, max_grid_xx = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_SHAPE)
+ major_order = item.getCurrentVisualizationParameter(
+ items.Scatter.VisualizationParameter.GRID_MAJOR_ORDER
+ )
+ assert major_order == "row"
+ max_grid_yy, max_grid_xx = item.getCurrentVisualizationParameter(
+ items.Scatter.VisualizationParameter.GRID_SHAPE
+ )
xx, yy, _values, _xx_error, _yy_error = item.getData(copy=False)
if isinstance(self, roi_items.HorizontalLineROI):
axis = yy
max_grid_first = max_grid_yy
max_grid_second = max_grid_xx
- major_axis = major_order == 'column'
+ major_axis = major_order == "column"
elif isinstance(self, roi_items.VerticalLineROI):
axis = xx
max_grid_first = max_grid_xx
max_grid_second = max_grid_yy
- major_axis = major_order == 'row'
+ major_axis = major_order == "row"
else:
assert False
@@ -946,7 +955,7 @@ class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn):
# slice in the middle of the scatter
actual_size_grid_second = len(axis) // max_grid_first
start = actual_size_grid_second // 2 * max_grid_first
- vslice = axis[start:start + max_grid_first]
+ vslice = axis[start : start + max_grid_first]
if len(vslice) == 0:
return None
index = argnearest(vslice, position)
@@ -954,7 +963,7 @@ class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn):
else:
# slice in the middle of the scatter
actual_size_grid_second = len(axis) // max_grid_first
- vslice = axis[actual_size_grid_second // 2::max_grid_second]
+ vslice = axis[actual_size_grid_second // 2 :: max_grid_second]
if len(vslice) == 0:
return None
index = argnearest(vslice, position)
@@ -999,28 +1008,30 @@ class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn):
return data
-class ProfileScatterHorizontalSliceROI(roi_items.HorizontalLineROI,
- _DefaultScatterProfileSliceRoiMixIn):
+class ProfileScatterHorizontalSliceROI(
+ roi_items.HorizontalLineROI, _DefaultScatterProfileSliceRoiMixIn
+):
"""ROI for an horizontal profile at a location of a scatter
using data slicing.
"""
- ICON = 'slice-horizontal'
- NAME = 'horizontal data slice profile'
+ ICON = "slice-horizontal"
+ NAME = "horizontal data slice profile"
def __init__(self, parent=None):
roi_items.HorizontalLineROI.__init__(self, parent=parent)
_DefaultScatterProfileSliceRoiMixIn.__init__(self, parent=parent)
-class ProfileScatterVerticalSliceROI(roi_items.VerticalLineROI,
- _DefaultScatterProfileSliceRoiMixIn):
+class ProfileScatterVerticalSliceROI(
+ roi_items.VerticalLineROI, _DefaultScatterProfileSliceRoiMixIn
+):
"""ROI for a vertical profile at a location of a scatter
using data slicing.
"""
- ICON = 'slice-vertical'
- NAME = 'vertical data slice profile'
+ ICON = "slice-vertical"
+ NAME = "vertical data slice profile"
def __init__(self, parent=None):
roi_items.VerticalLineROI.__init__(self, parent=parent)
@@ -1028,11 +1039,10 @@ class ProfileScatterVerticalSliceROI(roi_items.VerticalLineROI,
class ProfileScatterCrossSliceROI(_ProfileCrossROI):
- """ROI to manage a cross of slicing profiles on scatters.
- """
+ """ROI to manage a cross of slicing profiles on scatters."""
- ICON = 'slice-cross'
- NAME = 'cross data slice profile'
+ ICON = "slice-cross"
+ NAME = "cross data slice profile"
ITEM_KIND = items.Scatter
def _createLines(self, parent):
@@ -1042,7 +1052,6 @@ class ProfileScatterCrossSliceROI(_ProfileCrossROI):
class _DefaultImageStackProfileRoiMixIn(_DefaultImageProfileRoiMixIn):
-
ITEM_KIND = items.ImageStack
def __init__(self, parent=None):
@@ -1073,22 +1082,24 @@ class _DefaultImageStackProfileRoiMixIn(_DefaultImageProfileRoiMixIn):
assert kind == "2D"
+ currentData = numpy.array(item.getStackData(copy=False))
+ origin = item.getOrigin()
+ scale = item.getScale()
+ colormap = item.getColormap()
+ method = self.getProfileMethod()
+ roiInfo = self._getRoiInfo()
+
def createProfile2(currentData):
coords, profile, _area, profileName, xLabel = core.createProfile(
- roiInfo=self._getRoiInfo(),
+ roiInfo=roiInfo,
currentData=currentData,
origin=origin,
scale=scale,
lineWidth=self.getProfileLineWidth(),
- method=method)
+ method=method,
+ )
return coords, profile, profileName, xLabel
- currentData = numpy.array(item.getStackData(copy=False))
- origin = item.getOrigin()
- scale = item.getScale()
- colormap = item.getColormap()
- method = self.getProfileMethod()
-
coords, profile, profileName, xLabel = createProfile2(currentData)
profileManager = self.getProfileManager()
@@ -1105,36 +1116,37 @@ class _DefaultImageStackProfileRoiMixIn(_DefaultImageProfileRoiMixIn):
return data
-class ProfileImageStackHorizontalLineROI(roi_items.HorizontalLineROI,
- _DefaultImageStackProfileRoiMixIn):
+class ProfileImageStackHorizontalLineROI(
+ roi_items.HorizontalLineROI, _DefaultImageStackProfileRoiMixIn
+):
"""ROI for an horizontal profile at a location of a stack of images"""
- ICON = 'shape-horizontal'
- NAME = 'horizontal line profile'
+ ICON = "shape-horizontal"
+ NAME = "horizontal line profile"
def __init__(self, parent=None):
roi_items.HorizontalLineROI.__init__(self, parent=parent)
_DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileImageStackVerticalLineROI(roi_items.VerticalLineROI,
- _DefaultImageStackProfileRoiMixIn):
+class ProfileImageStackVerticalLineROI(
+ roi_items.VerticalLineROI, _DefaultImageStackProfileRoiMixIn
+):
"""ROI for an vertical profile at a location of a stack of images"""
- ICON = 'shape-vertical'
- NAME = 'vertical line profile'
+ ICON = "shape-vertical"
+ NAME = "vertical line profile"
def __init__(self, parent=None):
roi_items.VerticalLineROI.__init__(self, parent=parent)
_DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileImageStackLineROI(roi_items.LineROI,
- _DefaultImageStackProfileRoiMixIn):
+class ProfileImageStackLineROI(roi_items.LineROI, _DefaultImageStackProfileRoiMixIn):
"""ROI for an vertical profile at a location of a stack of images"""
- ICON = 'shape-diagonal'
- NAME = 'line profile'
+ ICON = "shape-diagonal"
+ NAME = "line profile"
def __init__(self, parent=None):
roi_items.LineROI.__init__(self, parent=parent)
@@ -1144,8 +1156,8 @@ class ProfileImageStackLineROI(roi_items.LineROI,
class ProfileImageStackCrossROI(ProfileImageCrossROI):
"""ROI for an vertical profile at a location of a stack of images"""
- ICON = 'shape-cross'
- NAME = 'cross profile'
+ ICON = "shape-cross"
+ NAME = "cross profile"
ITEM_KIND = items.ImageStack
def _createLines(self, parent):
diff --git a/src/silx/gui/plot/tools/profile/toolbar.py b/src/silx/gui/plot/tools/profile/toolbar.py
index 12a734a..d073717 100644
--- a/src/silx/gui/plot/tools/profile/toolbar.py
+++ b/src/silx/gui/plot/tools/profile/toolbar.py
@@ -44,10 +44,11 @@ _logger = logging.getLogger(__name__)
class ProfileToolBar(qt.QToolBar):
"""Tool bar to provide profile for a plot.
-
+
It is an helper class. For a dedicated application it would be better to
use an own tool bar in order in order have more flexibility.
"""
+
def __init__(self, parent=None, plot=None):
super(ProfileToolBar, self).__init__(parent=parent)
self.__scheme = None
diff --git a/src/silx/gui/plot/tools/roi.py b/src/silx/gui/plot/tools/roi.py
index 1da692c..21b9409 100644
--- a/src/silx/gui/plot/tools/roi.py
+++ b/src/silx/gui/plot/tools/roi.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2018-2022 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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
@@ -34,6 +34,7 @@ import logging
import time
import weakref
import functools
+from typing import Optional
import numpy
@@ -42,6 +43,8 @@ from ...utils import blockSignals
from ...utils import LockReentrant
from .. import PlotWidget
from ..items import roi as roi_items
+from ..items import ItemChangedType
+from ..items.roi import RegionOfInterest
from ...colors import rgba
@@ -87,7 +90,7 @@ class CreateRoiModeAction(qt.QAction):
iconName = "add-shape-unknown"
if name is None:
name = roiClass.__name__
- text = 'Add %s' % name
+ text = "Add %s" % name
self.setIcon(icons.getQIcon(iconName))
self.setText(text)
self.setCheckable(True)
@@ -144,7 +147,9 @@ class CreateRoiModeAction(qt.QAction):
if roiManager is not None:
roiManager.sigInteractiveRoiCreated.disconnect(self.initRoi)
roiManager.sigInteractiveRoiFinalized.disconnect(self.__finalizeRoi)
- roiManager.sigInteractiveModeFinished.disconnect(self.__interactiveModeFinished)
+ roiManager.sigInteractiveModeFinished.disconnect(
+ self.__interactiveModeFinished
+ )
self.setChecked(False)
def initRoi(self, roi):
@@ -391,7 +396,8 @@ class RegionOfInterestManager(qt.QObject):
self._roiClass = None
self._source = None
- self._color = rgba('red')
+ self._lastHoveredMarkerLabel = None
+ self._color = rgba("red")
self._label = "__RegionOfInterestManager__%d" % id(self)
@@ -404,8 +410,7 @@ class RegionOfInterestManager(qt.QObject):
parent.sigPlotSignal.connect(self._plotSignals)
- parent.sigInteractiveModeChanged.connect(
- self._plotInteractiveModeChanged)
+ parent.sigInteractiveModeChanged.connect(self._plotInteractiveModeChanged)
parent.sigItemRemoved.connect(self._itemRemoved)
@@ -432,7 +437,7 @@ class RegionOfInterestManager(qt.QObject):
:raise ValueError: If kind is not supported
"""
if not issubclass(roiClass, roi_items.RegionOfInterest):
- raise ValueError('Unsupported ROI class %s' % roiClass)
+ raise ValueError("Unsupported ROI class %s" % roiClass)
action = self._modeActions.get(roiClass, None)
if action is None: # Lazy-loading
@@ -476,19 +481,21 @@ class RegionOfInterestManager(qt.QObject):
return # Should not happen
kind = roiClass.getFirstInteractionShape()
- if kind == 'point':
- if event['event'] == 'mouseClicked' and event['button'] == 'left':
- points = numpy.array([(event['x'], event['y'])],
- dtype=numpy.float64)
+ if kind == "point":
+ if event["event"] == "mouseClicked" and event["button"] == "left":
+ points = numpy.array([(event["x"], event["y"])], dtype=numpy.float64)
# Not an interactive creation
roi = self._createInteractiveRoi(roiClass, points=points)
roi.creationFinalized()
self.sigInteractiveRoiFinalized.emit(roi)
else: # other shapes
- if (event['event'] in ('drawingProgress', 'drawingFinished') and
- event['parameters']['label'] == self._label):
- points = numpy.array((event['xdata'], event['ydata']),
- dtype=numpy.float64).T
+ if (
+ event["event"] in ("drawingProgress", "drawingFinished")
+ and event["parameters"]["label"] == self._label
+ ):
+ points = numpy.array(
+ (event["xdata"], event["ydata"]), dtype=numpy.float64
+ ).T
if self._drawnROI is None: # Create new ROI
# NOTE: Set something before createRoi, so isDrawing is True
@@ -497,8 +504,8 @@ class RegionOfInterestManager(qt.QObject):
else:
self._drawnROI.setFirstShapePoints(points)
- if event['event'] == 'drawingFinished':
- if kind == 'polygon' and len(points) > 1:
+ if event["event"] == "drawingFinished":
+ if kind == "polygon" and len(points) > 1:
self._drawnROI.setFirstShapePoints(points[:-1])
roi = self._drawnROI
self._drawnROI = None # Stop drawing
@@ -521,7 +528,7 @@ class RegionOfInterestManager(qt.QObject):
return roi
return None
- def setCurrentRoi(self, roi):
+ def setCurrentRoi(self, roi: Optional[RegionOfInterest]):
"""Set the currently selected ROI, and emit a signal.
:param Union[RegionOfInterest,None] roi: The ROI to select
@@ -545,11 +552,8 @@ class RegionOfInterestManager(qt.QObject):
self._currentRoi.setHighlighted(True)
self.sigCurrentRoiChanged.emit(roi)
- def getCurrentRoi(self):
- """Returns the currently selected ROI, else None.
-
- :rtype: Union[RegionOfInterest,None]
- """
+ def getCurrentRoi(self) -> Optional[RegionOfInterest]:
+ """Returns the currently selected ROI, else None."""
return self._currentRoi
def _plotSignals(self, event):
@@ -568,6 +572,8 @@ class RegionOfInterestManager(qt.QObject):
plot = self.parent()
marker = plot._getMarkerAt(event["xpixel"], event["ypixel"])
roi = self.__getRoiFromMarker(marker)
+ elif event["event"] == "hover":
+ self._lastHoveredMarkerLabel = event["label"]
else:
return
@@ -585,7 +591,7 @@ class RegionOfInterestManager(qt.QObject):
else:
self.setCurrentRoi(None)
- def __updateMode(self, roi):
+ def __updateMode(self, roi: RegionOfInterest):
if isinstance(roi, roi_items.InteractionModeMixIn):
available = roi.availableInteractionModes()
mode = roi.getInteractionMode()
@@ -593,46 +599,50 @@ class RegionOfInterestManager(qt.QObject):
mode = available[(imode + 1) % len(available)]
roi.setInteractionMode(mode)
- def _feedContextMenu(self, menu):
+ def _feedContextMenu(self, menu: qt.QMenu):
"""Called when the default plot context menu is about to be displayed"""
roi = self.getCurrentRoi()
if roi is not None:
if roi.isEditable():
- # Filter by data position
- # FIXME: It would be better to use GUI coords for it
- plot = self.parent()
- pos = plot.getWidgetHandle().mapFromGlobal(qt.QCursor.pos())
- data = plot.pixelToData(pos.x(), pos.y())
- if roi.contains(data):
- if isinstance(roi, roi_items.InteractionModeMixIn):
- self._contextMenuForInteractionMode(menu, roi)
-
- removeAction = qt.QAction(menu)
- removeAction.setText("Remove %s" % roi.getName())
- callback = functools.partial(self.removeRoi, roi)
- removeAction.triggered.connect(callback)
- menu.addAction(removeAction)
-
- def _contextMenuForInteractionMode(self, menu, roi):
- availableModes = roi.availableInteractionModes()
- currentMode = roi.getInteractionMode()
- submenu = qt.QMenu(menu)
- modeGroup = qt.QActionGroup(menu)
- modeGroup.setExclusive(True)
- for mode in availableModes:
- action = qt.QAction(menu)
- action.setText(mode.label)
- action.setToolTip(mode.description)
- action.setCheckable(True)
- if mode is currentMode:
- action.setChecked(True)
- else:
- callback = functools.partial(roi.setInteractionMode, mode)
- action.triggered.connect(callback)
- modeGroup.addAction(action)
- submenu.addAction(action)
- submenu.setTitle("%s interaction mode" % roi.getName())
- menu.addMenu(submenu)
+ if self._isMouseHoverRoi(roi):
+ roiMenu = self._createMenuForRoi(menu, roi)
+ menu.addMenu(roiMenu)
+
+ def _isMouseHoverRoi(self, roi: RegionOfInterest) -> bool:
+ """Check that the mouse hovers this roi"""
+ plot = self.parent()
+
+ if self._lastHoveredMarkerLabel is not None:
+ marker = plot._getMarker(self._lastHoveredMarkerLabel)
+ if marker is not None:
+ r = self.__getRoiFromMarker(marker)
+ if roi is r:
+ return True
+
+ # Filter by data position
+ # FIXME: It would be better to use GUI coords for it
+ pos = plot.getWidgetHandle().mapFromGlobal(qt.QCursor.pos())
+ data = plot.pixelToData(pos.x(), pos.y())
+ return roi.contains(data)
+
+ def _createMenuForRoi(self, parent: qt.QWidget, roi: RegionOfInterest) -> qt.QMenu:
+ """Create a QMenu for the given RegionOfInterest"""
+ roiMenu = qt.QMenu(parent)
+ roiMenu.setTitle(roi.getName())
+
+ if isinstance(roi, roi_items.InteractionModeMixIn):
+ interactionMenu = roi.createMenuForInteractionMode(roiMenu)
+ roiMenu.addMenu(interactionMenu)
+
+ removeAction = qt.QAction(roiMenu)
+ removeAction.setText("Remove")
+ callback = functools.partial(self.removeRoi, roi)
+ removeAction.triggered.connect(callback)
+ roiMenu.addAction(removeAction)
+
+ roi.populateContextMenu(roiMenu)
+
+ return roiMenu
# RegionOfInterest API
@@ -654,8 +664,7 @@ class RegionOfInterestManager(qt.QObject):
"""
if self.getRois(): # Something to reset
for roi in self._rois:
- roi.sigRegionChanged.disconnect(
- self._regionOfInterestChanged)
+ roi.sigRegionChanged.disconnect(self._regionOfInterestChanged)
roi.setParent(None)
self._rois = []
self._roisUpdated()
@@ -715,8 +724,7 @@ class RegionOfInterestManager(qt.QObject):
"""
plot = self.parent()
if plot is None:
- raise RuntimeError(
- 'Cannot add ROI: PlotWidget no more available')
+ raise RuntimeError("Cannot add ROI: PlotWidget no more available")
roi.setParent(self)
@@ -739,11 +747,12 @@ class RegionOfInterestManager(qt.QObject):
:param roi_items.RegionOfInterest roi: The ROI to remove
:raise ValueError: When ROI does not belong to this object
"""
- if not (isinstance(roi, roi_items.RegionOfInterest) and
- roi.parent() is self and
- roi in self._rois):
- raise ValueError(
- 'RegionOfInterest does not belong to this instance')
+ if not (
+ isinstance(roi, roi_items.RegionOfInterest)
+ and roi.parent() is self
+ and roi in self._rois
+ ):
+ raise ValueError("RegionOfInterest does not belong to this instance")
roi.sigAboutToBeRemoved.emit()
self.sigRoiAboutToBeRemoved.emit(roi)
@@ -834,7 +843,7 @@ class RegionOfInterestManager(qt.QObject):
self.stop()
if not issubclass(roiClass, roi_items.RegionOfInterest):
- raise ValueError('Unsupported ROI class %s' % roiClass)
+ raise ValueError("Unsupported ROI class %s" % roiClass)
plot = self.parent()
if plot is None:
@@ -859,18 +868,20 @@ class RegionOfInterestManager(qt.QObject):
plot = self.parent()
firstInteractionShapeKind = roiClass.getFirstInteractionShape()
- if firstInteractionShapeKind == 'point':
- plot.setInteractiveMode(mode='select', source=self)
+ if firstInteractionShapeKind == "point":
+ plot.setInteractiveMode(mode="select", source=self)
else:
if roiClass.showFirstInteractionShape():
color = rgba(self.getColor())
else:
color = None
- plot.setInteractiveMode(mode='select-draw',
- source=self,
- shape=firstInteractionShapeKind,
- color=color,
- label=self._label)
+ plot.setInteractiveMode(
+ mode="draw",
+ source=self,
+ shape=firstInteractionShapeKind,
+ color=color,
+ label=self._label,
+ )
def __roiInteractiveModeEnded(self):
"""Handle end of ROI draw interactive mode"""
@@ -964,7 +975,7 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
super(InteractiveRegionOfInterestManager, self).__init__(parent)
self._maxROI = None
self.__timeoutEndTime = None
- self.__message = ''
+ self.__message = ""
self.__validationMode = self.ValidationMode.ENTER
self.__execClass = None
@@ -991,11 +1002,10 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
if max_ is not None:
max_ = int(max_)
if max_ <= 0:
- raise ValueError('Max limit must be strictly positive')
+ raise ValueError("Max limit must be strictly positive")
if len(self.getRois()) > max_:
- raise ValueError(
- 'Cannot set max limit: Already too many ROIs')
+ raise ValueError("Cannot set max limit: Already too many ROIs")
self._maxROI = max_
@@ -1013,19 +1023,19 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
class ValidationMode(enum.Enum):
"""Mode of validation to leave blocking :meth:`exec`"""
- AUTO = 'auto'
+ AUTO = "auto"
"""Automatically ends the interactive mode once
the user terminates the last ROI shape."""
- ENTER = 'enter'
+ ENTER = "enter"
"""Ends the interactive mode when the *Enter* key is pressed."""
- AUTO_ENTER = 'auto_enter'
+ AUTO_ENTER = "auto_enter"
"""Ends the interactive mode when reaching max ROIs or
when the *Enter* key is pressed.
"""
- NONE = 'none'
+ NONE = "none"
"""Do not provide the user a way to end the interactive mode.
The end of :meth:`exec` is done through :meth:`quit` or timeout.
@@ -1051,9 +1061,10 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
self.__validationMode = mode
if self.isExec():
- if (self.isMaxRois() and self.getValidationMode() in
- (self.ValidationMode.AUTO,
- self.ValidationMode.AUTO_ENTER)):
+ if self.isMaxRois() and self.getValidationMode() in (
+ self.ValidationMode.AUTO,
+ self.ValidationMode.AUTO_ENTER,
+ ):
self.quit()
self.__updateMessage()
@@ -1064,17 +1075,20 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
if event.type() == qt.QEvent.KeyPress:
key = event.key()
- if (key in (qt.Qt.Key_Return, qt.Qt.Key_Enter) and
- self.getValidationMode() in (
- self.ValidationMode.ENTER,
- self.ValidationMode.AUTO_ENTER)):
+ if key in (
+ qt.Qt.Key_Return,
+ qt.Qt.Key_Enter,
+ ) and self.getValidationMode() in (
+ self.ValidationMode.ENTER,
+ self.ValidationMode.AUTO_ENTER,
+ ):
# Stop on return key pressed
self.quit()
return True # Stop further handling of this keys
- if (key in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or (
- key == qt.Qt.Key_Z and
- event.modifiers() & qt.Qt.ControlModifier)):
+ if key in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or (
+ key == qt.Qt.Key_Z and event.modifiers() & qt.Qt.ControlModifier
+ ):
rois = self.getRois()
if rois: # Something to undo
self.removeRoi(rois[-1])
@@ -1096,8 +1110,7 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
return self.__message
else:
remaining = self.__timeoutEndTime - time.time()
- return self.__message + (' - %d seconds remaining' %
- max(1, int(remaining)))
+ return self.__message + (" - %d seconds remaining" % max(1, int(remaining)))
# Listen to ROI updates
@@ -1110,9 +1123,10 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
self.removeRoi(self.getRois()[-2])
self.__updateMessage()
- if (self.isMaxRois() and
- self.getValidationMode() in (self.ValidationMode.AUTO,
- self.ValidationMode.AUTO_ENTER)):
+ if self.isMaxRois() and self.getValidationMode() in (
+ self.ValidationMode.AUTO,
+ self.ValidationMode.AUTO_ENTER,
+ ):
self.quit()
def __aboutToBeRemoved(self, *args, **kwargs):
@@ -1131,10 +1145,10 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
def __updateMessage(self, nbrois=None):
"""Update message"""
if not self.isExec():
- message = 'Done'
+ message = "Done"
elif not self.isStarted():
- message = 'Use %s ROI edition mode' % self.__execClass
+ message = "Use %s ROI edition mode" % self.__execClass
else:
if nbrois is None:
@@ -1144,16 +1158,18 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
max_ = self.getMaxRois()
if max_ is None:
- message = 'Select %ss (%d selected)' % (name, nbrois)
+ message = "Select %ss (%d selected)" % (name, nbrois)
elif max_ <= 1:
- message = 'Select a %s' % name
+ message = "Select a %s" % name
else:
- message = 'Select %d/%d %ss' % (nbrois, max_, name)
+ message = "Select %d/%d %ss" % (nbrois, max_, name)
- if (self.getValidationMode() == self.ValidationMode.ENTER and
- self.isMaxRois()):
- message += ' - Press Enter to confirm'
+ if (
+ self.getValidationMode() == self.ValidationMode.ENTER
+ and self.isMaxRois()
+ ):
+ message += " - Press Enter to confirm"
if message != self.__message:
self.__message = message
@@ -1164,9 +1180,11 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
def __timeoutUpdate(self):
"""Handle update of timeout"""
- if (self.__timeoutEndTime is not None and
- (self.__timeoutEndTime - time.time()) > 0):
- self.sigMessageChanged.emit(self.getMessage())
+ if (
+ self.__timeoutEndTime is not None
+ and (self.__timeoutEndTime - time.time()) > 0
+ ):
+ self.sigMessageChanged.emit(self.getMessage())
else: # Stop interactive mode and message timer
timer = self.sender()
if timer is not None:
@@ -1234,7 +1252,7 @@ class _DeleteRegionOfInterestToolButton(qt.QToolButton):
def __init__(self, parent, roi):
super(_DeleteRegionOfInterestToolButton, self).__init__(parent)
- self.setIcon(icons.getQIcon('remove'))
+ self.setIcon(icons.getQIcon("remove"))
self.setToolTip("Remove this ROI")
self.__roiRef = roi if roi is None else weakref.ref(roi)
self.clicked.connect(self.__clicked)
@@ -1252,11 +1270,20 @@ class _DeleteRegionOfInterestToolButton(qt.QToolButton):
class RegionOfInterestTableWidget(qt.QTableWidget):
"""Widget displaying the ROIs of a :class:`RegionOfInterestManager`"""
+ # Columns indices of the different displayed information
+ (
+ _LABEL_VISIBLE_COL,
+ _EDITABLE_COL,
+ _KIND_COL,
+ _COORDINATES_COL,
+ _DELETE_COL,
+ ) = range(5)
+
def __init__(self, parent=None):
super(RegionOfInterestTableWidget, self).__init__(parent)
self._roiManagerRef = None
- headers = ['Label', 'Edit', 'Kind', 'Coordinates', '']
+ headers = ["Label", "Edit", "Kind", "Coordinates", ""]
self.setColumnCount(len(headers))
self.setHorizontalHeaderLabels(headers)
@@ -1278,21 +1305,17 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
self.itemChanged.connect(self.__itemChanged)
def __itemChanged(self, item):
- """Handle item updates"""
+ """Handle QTableWidget item updates"""
column = item.column()
- index = item.data(qt.Qt.UserRole)
-
- if index is not None:
- manager = self.getRegionOfInterestManager()
- roi = manager.getRois()[index]
- else:
+ roi = item.data(qt.Qt.UserRole)
+ if roi is None:
return
if column == 0:
# First collect information from item, then update ROI
- # Otherwise, this causes issues issues
+ # Otherwise, this causes issues
checked = item.checkState() == qt.Qt.Checked
- text= item.text()
+ text = item.text()
roi.setVisible(checked)
roi.setName(text)
elif column == 1:
@@ -1300,7 +1323,7 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
elif column in (2, 3, 4):
pass # TODO
else:
- logger.error('Unhandled column %d', column)
+ logger.error("Unhandled column %d", column)
def setRegionOfInterestManager(self, manager):
"""Set the :class:`RegionOfInterestManager` object to sync with
@@ -1312,7 +1335,13 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
previousManager = self.getRegionOfInterestManager()
if previousManager is not None:
- previousManager.sigRoiChanged.disconnect(self._sync)
+ previousManager.sigRoiAdded.disconnect(self.__roiAdded)
+ previousManager.sigRoiAboutToBeRemoved.disconnect(
+ self.__roiAboutToBeRemoved
+ )
+ for roi in previousManager.getRois():
+ self.__disconnectRoi(roi)
+
self.setRowCount(0)
self._roiManagerRef = weakref.ref(manager)
@@ -1320,7 +1349,10 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
self._sync()
if manager is not None:
- manager.sigRoiChanged.connect(self._sync)
+ for roi in manager.getRois():
+ self.__connectRoi(roi)
+ manager.sigRoiAdded.connect(self.__roiAdded)
+ manager.sigRoiAboutToBeRemoved.connect(self.__roiAboutToBeRemoved)
def _getReadableRoiDescription(self, roi):
"""Returns modelisation of a ROI as a readable sequence of values.
@@ -1345,6 +1377,75 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
logger.debug("Backtrace", exc_info=True)
return text
+ def __connectRoi(self, roi: RegionOfInterest):
+ """Start listening ROI signals"""
+ roi.sigItemChanged.connect(self.__roiItemChanged)
+ roi.sigRegionChanged.connect(self.__roiRegionChanged)
+
+ def __disconnectRoi(self, roi: RegionOfInterest):
+ """Stop listening ROI signals"""
+ roi.sigItemChanged.disconnect(self.__roiItemChanged)
+ roi.sigRegionChanged.disconnect(self.__roiRegionChanged)
+
+ def __getRoiRow(self, roi: RegionOfInterest) -> int:
+ """Returns row index of given region of interest
+
+ :raises ValueError: If region of interest is not in the list
+ """
+ manager = self.getRegionOfInterestManager()
+ if manager is None:
+ return
+ return manager.getRois().index(roi)
+
+ def __roiAdded(self, roi: RegionOfInterest):
+ """Handle new ROI added to the manager"""
+ self.__connectRoi(roi)
+ self._sync()
+
+ def __roiAboutToBeRemoved(self, roi: RegionOfInterest):
+ """Handle removing a ROI from the manager"""
+ self.__disconnectRoi(roi)
+ self.removeRow(self.__getRoiRow(roi))
+
+ def __roiItemChanged(self, event: ItemChangedType):
+ """Handle ROI sigItemChanged events"""
+ roi = self.sender()
+ if roi is None:
+ return
+
+ try:
+ row = self.__getRoiRow(roi)
+ except ValueError:
+ return
+
+ if event == ItemChangedType.VISIBLE:
+ item = self.item(row, self._LABEL_VISIBLE_COL)
+ item.setCheckState(qt.Qt.Checked if roi.isVisible() else qt.Qt.Unchecked)
+ return
+
+ if event == ItemChangedType.NAME:
+ item = self.item(row, self._LABEL_VISIBLE_COL)
+ item.setText(roi.getName())
+ return
+
+ if event == ItemChangedType.EDITABLE:
+ item = self.item(row, self._EDITABLE_COL)
+ item.setCheckState(qt.Qt.Checked if roi.isEditable() else qt.Qt.Unchecked)
+ return
+
+ def __roiRegionChanged(self):
+ """Handle change of ROI coordinates"""
+ roi = self.sender()
+ if roi is None:
+ return
+
+ item = self.item(self.__getRoiRow(roi), self._COORDINATES_COL)
+ if item is None:
+ return
+
+ text = self._getReadableRoiDescription(roi)
+ item.setText(text)
+
def _sync(self):
"""Update widget content according to ROI manger"""
manager = self.getRegionOfInterestManager()
@@ -1360,21 +1461,19 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
baseFlags = qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled
# Label and visible
- label = roi.getName()
- item = qt.QTableWidgetItem(label)
+ item = qt.QTableWidgetItem()
item.setFlags(baseFlags | qt.Qt.ItemIsEditable | qt.Qt.ItemIsUserCheckable)
- item.setData(qt.Qt.UserRole, index)
- item.setCheckState(
- qt.Qt.Checked if roi.isVisible() else qt.Qt.Unchecked)
- self.setItem(index, 0, item)
+ item.setData(qt.Qt.UserRole, roi)
+ item.setText(roi.getName())
+ item.setCheckState(qt.Qt.Checked if roi.isVisible() else qt.Qt.Unchecked)
+ self.setItem(index, self._LABEL_VISIBLE_COL, item)
# Editable
item = qt.QTableWidgetItem()
item.setFlags(baseFlags | qt.Qt.ItemIsUserCheckable)
- item.setData(qt.Qt.UserRole, index)
- item.setCheckState(
- qt.Qt.Checked if roi.isEditable() else qt.Qt.Unchecked)
- self.setItem(index, 1, item)
+ item.setData(qt.Qt.UserRole, roi)
+ item.setCheckState(qt.Qt.Checked if roi.isEditable() else qt.Qt.Unchecked)
+ self.setItem(index, self._EDITABLE_COL, item)
item.setTextAlignment(qt.Qt.AlignCenter)
item.setText(None)
@@ -1385,19 +1484,18 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
label = roi.__class__.__name__
item = qt.QTableWidgetItem(label.capitalize())
item.setFlags(baseFlags)
- self.setItem(index, 2, item)
+ self.setItem(index, self._KIND_COL, item)
+ # Coordinates
item = qt.QTableWidgetItem()
item.setFlags(baseFlags)
-
- # Coordinates
text = self._getReadableRoiDescription(roi)
item.setText(text)
- self.setItem(index, 3, item)
+ self.setItem(index, self._COORDINATES_COL, item)
# Delete
- delBtn = _DeleteRegionOfInterestToolButton(None, roi)
widget = qt.QWidget(self)
+ delBtn = _DeleteRegionOfInterestToolButton(widget, roi)
layout = qt.QHBoxLayout()
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(0)
@@ -1405,7 +1503,7 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
layout.addStretch(1)
layout.addWidget(delBtn)
layout.addStretch(1)
- self.setCellWidget(index, 4, widget)
+ self.setCellWidget(index, self._DELETE_COL, widget)
def getRegionOfInterestManager(self):
"""Returns the :class:`RegionOfInterestManager` this widget supervise.
diff --git a/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py b/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py
index 657d328..9f1a184 100644
--- a/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py
+++ b/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py
@@ -26,8 +26,6 @@ __license__ = "MIT"
__date__ = "02/08/2018"
-import unittest
-
from silx.gui import qt
from silx.utils.testutils import ParametricTestCase
from silx.gui.utils.testutils import TestCaseQt
@@ -46,7 +44,7 @@ class TestCurveLegendsWidget(TestCaseQt, ParametricTestCase):
self.legends.setPlotWidget(self.plot)
dock = qt.QDockWidget()
- dock.setWindowTitle('Curve Legends')
+ dock.setWindowTitle("Curve Legends")
dock.setWidget(self.legends)
self.plot.addTabbedDockWidget(dock)
@@ -68,9 +66,9 @@ class TestCurveLegendsWidget(TestCaseQt, ParametricTestCase):
def testAddRemoveCurves(self):
"""Test CurveLegendsWidget while adding/removing curves"""
- self.plot.addCurve((0, 1), (1, 2), legend='a')
+ self.plot.addCurve((0, 1), (1, 2), legend="a")
self._assertNbLegends(1)
- self.plot.addCurve((0, 1), (2, 3), legend='b')
+ self.plot.addCurve((0, 1), (2, 3), legend="b")
self._assertNbLegends(2)
# Detached/attach
@@ -84,28 +82,35 @@ class TestCurveLegendsWidget(TestCaseQt, ParametricTestCase):
self._assertNbLegends(0)
def testUpdateCurves(self):
- """Test CurveLegendsWidget while updating curves """
- self.plot.addCurve((0, 1), (1, 2), legend='a')
+ """Test CurveLegendsWidget while updating curves"""
+ self.plot.addCurve((0, 1), (1, 2), legend="a")
self._assertNbLegends(1)
- self.plot.addCurve((0, 1), (2, 3), legend='b')
+ self.plot.addCurve((0, 1), (2, 3), legend="b")
self._assertNbLegends(2)
# Activate curve
- self.plot.setActiveCurve('a')
+ self.plot.setActiveCurve("a")
self.qapp.processEvents()
- self.plot.setActiveCurve('b')
+ self.plot.setActiveCurve("b")
self.qapp.processEvents()
# Change curve style
- curve = self.plot.getCurve('a')
+ curve = self.plot.getCurve("a")
curve.setLineWidth(2)
- for linestyle in (':', '', '--', '-'):
+ for linestyle in (
+ ":",
+ "",
+ "--",
+ "-",
+ (0.0, (5.0, 5.0)),
+ (5.0, (10.0, 2.0, 2.0, 5.0)),
+ ):
with self.subTest(linestyle=linestyle):
curve.setLineStyle(linestyle)
self.qapp.processEvents()
self.qWait(1000)
- for symbol in ('o', 'd', '', 's'):
+ for symbol in ("o", "d", "", "s"):
with self.subTest(symbol=symbol):
curve.setSymbol(symbol)
self.qapp.processEvents()
diff --git a/src/silx/gui/plot/tools/test/testProfile.py b/src/silx/gui/plot/tools/test/testProfile.py
index ad40e67..61b95a6 100644
--- a/src/silx/gui/plot/tools/test/testProfile.py
+++ b/src/silx/gui/plot/tools/test/testProfile.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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
@@ -26,14 +26,11 @@ __license__ = "MIT"
__date__ = "28/06/2018"
-import unittest
import contextlib
import numpy
import logging
from silx.gui import qt
-from silx.utils import deprecation
-from silx.utils import testutils
from silx.gui.utils.testutils import TestCaseQt
from silx.utils.testutils import ParametricTestCase
@@ -49,7 +46,6 @@ _logger = logging.getLogger(__name__)
class TestRois(TestCaseQt):
-
def test_init(self):
"""Check that the constructor is not called twice"""
roi = rois.ProfileImageVerticalLineROI()
@@ -59,7 +55,6 @@ class TestRois(TestCaseQt):
class TestInteractions(TestCaseQt):
-
@contextlib.contextmanager
def defaultPlot(self):
try:
@@ -168,7 +163,7 @@ class TestInteractions(TestCaseQt):
self.assertEqual(len(profileRois), 3)
else:
self.assertEqual(len(profileRois), 1)
- # The first one should be the expected one
+ # The first one should be the expected one
roi = profileRois[0]
# Test that something was displayed
@@ -227,14 +222,14 @@ class TestInteractions(TestCaseQt):
if isinstance(editor, editors._NoProfileRoiEditor):
pass
elif isinstance(editor, editors._DefaultImageStackProfileRoiEditor):
- # GUI to ROI
+ # GUI to ROI
editor._lineWidth.setValue(2)
self.assertEqual(roi.getProfileLineWidth(), 2)
editor._methodsButton.setMethod("sum")
self.assertEqual(roi.getProfileMethod(), "sum")
editor._profileDim.setDimension(1)
self.assertEqual(roi.getProfileType(), "1D")
- # ROI to GUI
+ # ROI to GUI
roi.setProfileLineWidth(3)
self.assertEqual(editor._lineWidth.value(), 3)
roi.setProfileMethod("mean")
@@ -242,21 +237,21 @@ class TestInteractions(TestCaseQt):
roi.setProfileType("2D")
self.assertEqual(editor._profileDim.getDimension(), 2)
elif isinstance(editor, editors._DefaultImageProfileRoiEditor):
- # GUI to ROI
+ # GUI to ROI
editor._lineWidth.setValue(2)
self.assertEqual(roi.getProfileLineWidth(), 2)
editor._methodsButton.setMethod("sum")
self.assertEqual(roi.getProfileMethod(), "sum")
- # ROI to GUI
+ # ROI to GUI
roi.setProfileLineWidth(3)
self.assertEqual(editor._lineWidth.value(), 3)
roi.setProfileMethod("mean")
self.assertEqual(editor._methodsButton.getMethod(), "mean")
elif isinstance(editor, editors._DefaultScatterProfileRoiEditor):
- # GUI to ROI
+ # GUI to ROI
editor._nPoints.setValue(100)
self.assertEqual(roi.getNPoints(), 100)
- # ROI to GUI
+ # ROI to GUI
roi.setNPoints(200)
self.assertEqual(editor._nPoints.value(), 200)
else:
@@ -268,17 +263,32 @@ class TestInteractions(TestCaseQt):
(rois.ProfileImageVerticalLineROI, editors._DefaultImageProfileRoiEditor),
(rois.ProfileImageLineROI, editors._DefaultImageProfileRoiEditor),
(rois.ProfileImageCrossROI, editors._DefaultImageProfileRoiEditor),
- (rois.ProfileScatterHorizontalLineROI, editors._DefaultScatterProfileRoiEditor),
- (rois.ProfileScatterVerticalLineROI, editors._DefaultScatterProfileRoiEditor),
+ (
+ rois.ProfileScatterHorizontalLineROI,
+ editors._DefaultScatterProfileRoiEditor,
+ ),
+ (
+ rois.ProfileScatterVerticalLineROI,
+ editors._DefaultScatterProfileRoiEditor,
+ ),
(rois.ProfileScatterLineROI, editors._DefaultScatterProfileRoiEditor),
(rois.ProfileScatterCrossROI, editors._DefaultScatterProfileRoiEditor),
(rois.ProfileScatterHorizontalSliceROI, editors._NoProfileRoiEditor),
(rois.ProfileScatterVerticalSliceROI, editors._NoProfileRoiEditor),
(rois.ProfileScatterCrossSliceROI, editors._NoProfileRoiEditor),
- (rois.ProfileImageStackHorizontalLineROI, editors._DefaultImageStackProfileRoiEditor),
- (rois.ProfileImageStackVerticalLineROI, editors._DefaultImageStackProfileRoiEditor),
+ (
+ rois.ProfileImageStackHorizontalLineROI,
+ editors._DefaultImageStackProfileRoiEditor,
+ ),
+ (
+ rois.ProfileImageStackVerticalLineROI,
+ editors._DefaultImageStackProfileRoiEditor,
+ ),
(rois.ProfileImageStackLineROI, editors._DefaultImageStackProfileRoiEditor),
- (rois.ProfileImageStackCrossROI, editors._DefaultImageStackProfileRoiEditor),
+ (
+ rois.ProfileImageStackCrossROI,
+ editors._DefaultImageStackProfileRoiEditor,
+ ),
]
with self.defaultPlot() as plot:
profileManager = manager.ProfileManager(plot, plot)
@@ -288,7 +298,7 @@ class TestInteractions(TestCaseQt):
roi = roiClass()
roi._setProfileManager(profileManager)
try:
- # Force widget creation
+ # Force widget creation
menu = qt.QMenu(plot)
menu.addAction(editorAction)
widgets = editorAction.createdWidgets()
@@ -319,10 +329,8 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
self.mouseMove(self.plot) # Move to center
self.qapp.processEvents()
- deprecation.FORCE = True
def tearDown(self):
- deprecation.FORCE = False
self.qapp.processEvents()
profileManager = self.toolBar.getProfileManager()
profileManager.clearProfile()
@@ -338,7 +346,7 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
"""Test horizontal and vertical profile, without and with image"""
# Use Plot backend widget to submit mouse events
widget = self.plot.getWidgetHandle()
- for method in ('sum', 'mean'):
+ for method in ("sum", "mean"):
with self.subTest(method=method):
# 2 positions to use for mouse events
pos1 = widget.width() * 0.4, widget.height() * 0.4
@@ -353,8 +361,7 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
self.mouseClick(widget, qt.Qt.LeftButton, pos=pos1)
# with image
- self.plot.addImage(
- numpy.arange(100 * 100).reshape(100, -1))
+ self.plot.addImage(numpy.arange(100 * 100).reshape(100, -1))
self.mousePress(widget, qt.Qt.LeftButton, pos=pos1)
self.mouseMove(widget, pos=pos2)
self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2)
@@ -368,16 +375,14 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
if not manager.hasPendingOperations():
break
- @testutils.validate_logging(deprecation.depreclog.name, warning=4)
def testDiagonalProfile(self):
"""Test diagonal profile, without and with image"""
# Use Plot backend widget to submit mouse events
widget = self.plot.getWidgetHandle()
- self.plot.addImage(
- numpy.arange(100 * 100).reshape(100, -1))
+ self.plot.addImage(numpy.arange(100 * 100).reshape(100, -1))
- for method in ('sum', 'mean'):
+ for method in ("sum", "mean"):
with self.subTest(method=method):
# 2 positions to use for mouse events
pos1 = widget.width() * 0.4, widget.height() * 0.4
@@ -414,10 +419,12 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
if not manager.hasPendingOperations():
break
- curveItem = self.toolBar.getProfilePlot().getAllCurves()[0]
- if method == 'sum':
+ curveItem = (
+ roi.getProfileWindow().getCurrentPlotWidget().getAllCurves()[0]
+ )
+ if method == "sum":
self.assertTrue(curveItem.getData()[1].max() > 10000)
- elif method == 'mean':
+ elif method == "mean":
self.assertTrue(curveItem.getData()[1].max() < 10000)
# Remove the ROI so the profile window is also removed
@@ -426,77 +433,26 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
self.qWait(100)
-class TestDeprecatedProfileToolBar(TestCaseQt):
- """Tests old features of the ProfileToolBar widget."""
-
- def setUp(self):
- self.plot = None
- super(TestDeprecatedProfileToolBar, self).setUp()
-
- def tearDown(self):
- if self.plot is not None:
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- self.plot = None
- self.qWait()
-
- super(TestDeprecatedProfileToolBar, self).tearDown()
-
- @testutils.validate_logging(deprecation.depreclog.name, warning=2)
- def testCustomProfileWindow(self):
- from silx.gui.plot import ProfileMainWindow
-
- self.plot = PlotWindow()
- profileWindow = ProfileMainWindow.ProfileMainWindow(self.plot)
- toolBar = Profile.ProfileToolBar(parent=self.plot,
- plot=self.plot,
- profileWindow=profileWindow)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
- profileWindow.show()
- self.qWaitForWindowExposed(profileWindow)
- self.qapp.processEvents()
-
- self.plot.addImage(numpy.arange(10 * 10).reshape(10, -1))
- profile = rois.ProfileImageHorizontalLineROI()
- profile.setPosition(5)
- toolBar.getProfileManager().getRoiManager().addRoi(profile)
- toolBar.getProfileManager().getRoiManager().setCurrentRoi(profile)
-
- for _ in range(20):
- self.qWait(200)
- if not toolBar.getProfileManager().hasPendingOperations():
- break
-
- # There is a displayed profile
- self.assertIsNotNone(profileWindow.getProfile())
- self.assertIs(toolBar.getProfileMainWindow(), profileWindow)
-
- # There is nothing anymore but the window is still there
- toolBar.getProfileManager().clearProfile()
- self.qapp.processEvents()
- self.assertIsNone(profileWindow.getProfile())
-
-
class TestProfile3DToolBar(TestCaseQt):
- """Tests for Profile3DToolBar widget.
- """
+ """Tests for Profile3DToolBar widget."""
+
def setUp(self):
super(TestProfile3DToolBar, self).setUp()
self.plot = StackView()
self.plot.show()
self.qWaitForWindowExposed(self.plot)
- self.plot.setStack(numpy.array([
- [[0, 1, 2], [3, 4, 5]],
- [[6, 7, 8], [9, 10, 11]],
- [[12, 13, 14], [15, 16, 17]]
- ]))
- deprecation.FORCE = True
+ self.plot.setStack(
+ numpy.array(
+ [
+ [[0, 1, 2], [3, 4, 5]],
+ [[6, 7, 8], [9, 10, 11]],
+ [[12, 13, 14], [15, 16, 17]],
+ ]
+ )
+ )
def tearDown(self):
- deprecation.FORCE = False
profileManager = self.plot.getProfileToolbar().getProfileManager()
profileManager.clearProfile()
profileManager = None
@@ -506,7 +462,6 @@ class TestProfile3DToolBar(TestCaseQt):
super(TestProfile3DToolBar, self).tearDown()
- @testutils.validate_logging(deprecation.depreclog.name, warning=2)
def testMethodProfile2D(self):
"""Test that the profile can have a different method if we want to
compute then in 1D or in 2D"""
@@ -530,15 +485,13 @@ class TestProfile3DToolBar(TestCaseQt):
break
# check 2D 'mean' profile
- profilePlot = toolBar.getProfilePlot()
+ profilePlot = roi.getProfileWindow().getCurrentPlotWidget()
data = profilePlot.getAllImages()[0].getData()
expected = numpy.array([[1, 4], [7, 10], [13, 16]])
numpy.testing.assert_almost_equal(data, expected)
- @testutils.validate_logging(deprecation.depreclog.name, warning=2)
def testMethodSumLine(self):
- """Simple interaction test to make sure the sum is correctly computed
- """
+ """Simple interaction test to make sure the sum is correctly computed"""
toolBar = self.plot.getProfileToolbar()
toolBar.lineAction.trigger()
@@ -563,14 +516,13 @@ class TestProfile3DToolBar(TestCaseQt):
break
# check 2D 'sum' profile
- profilePlot = toolBar.getProfilePlot()
+ profilePlot = roi.getProfileWindow().getCurrentPlotWidget()
data = profilePlot.getAllImages()[0].getData()
expected = numpy.array([[3, 12], [21, 30], [39, 48]])
numpy.testing.assert_almost_equal(data, expected)
class TestGetProfilePlot(TestCaseQt):
-
def setUp(self):
self.plot = None
super(TestGetProfilePlot, self).setUp()
@@ -618,8 +570,7 @@ class TestGetProfilePlot(TestCaseQt):
self.plot.show()
self.qWaitForWindowExposed(self.plot)
- self.plot.setStack(numpy.array([[[0, 1], [2, 3]],
- [[4, 5], [6, 7]]]))
+ self.plot.setStack(numpy.array([[[0, 1], [2, 3]], [[4, 5], [6, 7]]]))
toolBar = self.plot.getProfileToolbar()
diff --git a/src/silx/gui/plot/tools/test/testROI.py b/src/silx/gui/plot/tools/test/testRoiCore.py
index 6ce1553..e7f6d8a 100644
--- a/src/silx/gui/plot/tools/test/testROI.py
+++ b/src/silx/gui/plot/tools/test/testRoiCore.py
@@ -26,7 +26,6 @@ __license__ = "MIT"
__date__ = "28/06/2018"
-import unittest
import numpy.testing
from silx.gui import qt
@@ -37,243 +36,6 @@ import silx.gui.plot.items.roi as roi_items
from silx.gui.plot.tools import roi
-class TestRoiItems(TestCaseQt):
-
- def testLine_geometry(self):
- item = roi_items.LineROI()
- startPoint = numpy.array([1, 2])
- endPoint = numpy.array([3, 4])
- item.setEndPoints(startPoint, endPoint)
- numpy.testing.assert_allclose(item.getEndPoints()[0], startPoint)
- numpy.testing.assert_allclose(item.getEndPoints()[1], endPoint)
-
- def testHLine_geometry(self):
- item = roi_items.HorizontalLineROI()
- item.setPosition(15)
- self.assertEqual(item.getPosition(), 15)
-
- def testVLine_geometry(self):
- item = roi_items.VerticalLineROI()
- item.setPosition(15)
- self.assertEqual(item.getPosition(), 15)
-
- def testPoint_geometry(self):
- point = numpy.array([1, 2])
- item = roi_items.PointROI()
- item.setPosition(point)
- numpy.testing.assert_allclose(item.getPosition(), point)
-
- def testRectangle_originGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- center = numpy.array([5, 10])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- numpy.testing.assert_allclose(item.getOrigin(), origin)
- numpy.testing.assert_allclose(item.getSize(), size)
- numpy.testing.assert_allclose(item.getCenter(), center)
-
- def testRectangle_centerGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- center = numpy.array([5, 10])
- item = roi_items.RectangleROI()
- item.setGeometry(center=center, size=size)
- numpy.testing.assert_allclose(item.getOrigin(), origin)
- numpy.testing.assert_allclose(item.getSize(), size)
- numpy.testing.assert_allclose(item.getCenter(), center)
-
- def testRectangle_setCenterGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- newCenter = numpy.array([0, 0])
- item.setCenter(newCenter)
- expectedOrigin = numpy.array([-5, -10])
- numpy.testing.assert_allclose(item.getOrigin(), expectedOrigin)
- numpy.testing.assert_allclose(item.getCenter(), newCenter)
- numpy.testing.assert_allclose(item.getSize(), size)
-
- def testRectangle_setOriginGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- newOrigin = numpy.array([10, 10])
- item.setOrigin(newOrigin)
- expectedCenter = numpy.array([15, 20])
- numpy.testing.assert_allclose(item.getOrigin(), newOrigin)
- numpy.testing.assert_allclose(item.getCenter(), expectedCenter)
- numpy.testing.assert_allclose(item.getSize(), size)
-
- def testCircle_geometry(self):
- center = numpy.array([0, 0])
- radius = 10.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- numpy.testing.assert_allclose(item.getCenter(), center)
- numpy.testing.assert_allclose(item.getRadius(), radius)
-
- def testCircle_setCenter(self):
- center = numpy.array([0, 0])
- radius = 10.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- newCenter = numpy.array([-10, 0])
- item.setCenter(newCenter)
- numpy.testing.assert_allclose(item.getCenter(), newCenter)
- numpy.testing.assert_allclose(item.getRadius(), radius)
-
- def testCircle_setRadius(self):
- center = numpy.array([0, 0])
- radius = 10.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- newRadius = 5.1
- item.setRadius(newRadius)
- numpy.testing.assert_allclose(item.getCenter(), center)
- numpy.testing.assert_allclose(item.getRadius(), newRadius)
-
- def testCircle_contains(self):
- center = numpy.array([2, -1])
- radius = 1.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- self.assertTrue(item.contains([1, -1]))
- self.assertFalse(item.contains([0, 0]))
- self.assertTrue(item.contains([2, 0]))
- self.assertFalse(item.contains([3.01, -1]))
-
- def testEllipse_contains(self):
- center = numpy.array([-2, 0])
- item = roi_items.EllipseROI()
- item.setCenter(center)
- item.setOrientation(numpy.pi / 4.0)
- item.setMajorRadius(2)
- item.setMinorRadius(1)
- print(item.getMinorRadius(), item.getMajorRadius())
- self.assertFalse(item.contains([0, 0]))
- self.assertTrue(item.contains([-1, 1]))
- self.assertTrue(item.contains([-3, 0]))
- self.assertTrue(item.contains([-2, 0]))
- self.assertTrue(item.contains([-2, 1]))
- self.assertFalse(item.contains([-4, 1]))
-
- def testRectangle_isIn(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- self.assertTrue(item.contains(position=(0, 0)))
- self.assertTrue(item.contains(position=(2, 14)))
- self.assertFalse(item.contains(position=(14, 12)))
-
- def testPolygon_emptyGeometry(self):
- points = numpy.empty((0, 2))
- item = roi_items.PolygonROI()
- item.setPoints(points)
- numpy.testing.assert_allclose(item.getPoints(), points)
-
- def testPolygon_geometry(self):
- points = numpy.array([[10, 10], [12, 10], [50, 1]])
- item = roi_items.PolygonROI()
- item.setPoints(points)
- numpy.testing.assert_allclose(item.getPoints(), points)
-
- def testPolygon_isIn(self):
- points = numpy.array([[0, 0], [0, 10], [5, 10]])
- item = roi_items.PolygonROI()
- item.setPoints(points)
- self.assertTrue(item.contains((0, 0)))
- self.assertFalse(item.contains((6, 2)))
- self.assertFalse(item.contains((-2, 5)))
- self.assertFalse(item.contains((2, -1)))
- self.assertFalse(item.contains((8, 1)))
- self.assertTrue(item.contains((1, 8)))
-
- def testArc_getToSetGeometry(self):
- """Test that we can use getGeometry as input to setGeometry"""
- item = roi_items.ArcROI()
- item.setFirstShapePoints(numpy.array([[5, 10], [50, 100]]))
- item.setGeometry(*item.getGeometry())
-
- def testArc_degenerated_point(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 0, 0, 0
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
-
- def testArc_degenerated_line(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
-
- def testArc_special_circle(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, 3 * numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), item.getEndAngle() - numpy.pi * 2.0)
- self.assertTrue(item.isClosed())
-
- def testArc_special_donut(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi, 3 * numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), item.getEndAngle() - numpy.pi * 2.0)
- self.assertTrue(item.isClosed())
-
- def testArc_clockwiseGeometry(self):
- """Test that we can use getGeometry as input to setGeometry"""
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), startAngle)
- self.assertAlmostEqual(item.getEndAngle(), endAngle)
- self.assertAlmostEqual(item.isClosed(), False)
-
- def testArc_anticlockwiseGeometry(self):
- """Test that we can use getGeometry as input to setGeometry"""
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, -numpy.pi * 0.5
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), startAngle)
- self.assertAlmostEqual(item.getEndAngle(), endAngle)
- self.assertAlmostEqual(item.isClosed(), False)
-
- def testHRange_geometry(self):
- item = roi_items.HorizontalRangeROI()
- vmin = 1
- vmax = 3
- item.setRange(vmin, vmax)
- self.assertAlmostEqual(item.getMin(), vmin)
- self.assertAlmostEqual(item.getMax(), vmax)
- self.assertAlmostEqual(item.getCenter(), 2)
-
- def testBand_getToSetGeometry(self):
- """Test that we can use getGeometry as input to setGeometry"""
- item = roi_items.BandROI()
- item.setFirstShapePoints(numpy.array([[5, 10], [50, 100]]))
- item.setGeometry(*item.getGeometry())
-
-
class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
"""Tests for RegionOfInterestManager class"""
@@ -300,25 +62,44 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
def test(self):
"""Test ROI of different shapes"""
tests = ( # shape, points=[list of (x, y), list of (x, y)]
- (roi_items.PointROI, numpy.array(([(10., 15.)], [(20., 25.)]))),
- (roi_items.RectangleROI,
- numpy.array((((1., 10.), (11., 20.)),
- ((2., 3.), (12., 13.))))),
- (roi_items.PolygonROI,
- numpy.array((((0., 1.), (0., 10.), (10., 0.)),
- ((5., 6.), (5., 16.), (15., 6.))))),
- (roi_items.LineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
- (roi_items.HorizontalLineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
- (roi_items.VerticalLineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
- (roi_items.HorizontalLineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
+ (roi_items.PointROI, numpy.array(([(10.0, 15.0)], [(20.0, 25.0)]))),
+ (
+ roi_items.RectangleROI,
+ numpy.array((((1.0, 10.0), (11.0, 20.0)), ((2.0, 3.0), (12.0, 13.0)))),
+ ),
+ (
+ roi_items.PolygonROI,
+ numpy.array(
+ (
+ ((0.0, 1.0), (0.0, 10.0), (10.0, 0.0)),
+ ((5.0, 6.0), (5.0, 16.0), (15.0, 6.0)),
+ )
+ ),
+ ),
+ (
+ roi_items.LineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
+ (
+ roi_items.HorizontalLineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
+ (
+ roi_items.VerticalLineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
+ (
+ roi_items.HorizontalLineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
)
for roiClass, points in tests:
@@ -453,7 +234,12 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
# Arc
item = roi_items.ArcROI()
center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi
+ innerRadius, outerRadius, startAngle, endAngle = (
+ 1,
+ 100,
+ numpy.pi * 0.5,
+ numpy.pi,
+ )
item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
rois.append(item)
# Horizontal Range
@@ -493,12 +279,20 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
manager.removeRoi(item1)
self.assertIs(manager.getCurrentRoi(), None)
+ def testInitROIWithParent(self):
+ manager = roi.RegionOfInterestManager(self.plot)
+ item = roi_items.PointROI(manager)
+ manager.addRoi(item)
+ self.qapp.processEvents()
+ manager.removeRoi(item)
+ self.qapp.processEvents()
+
def testMaxROI(self):
"""Test Max ROI"""
- origin1 = numpy.array([1., 10.])
- size1 = numpy.array([10., 10.])
- origin2 = numpy.array([2., 3.])
- size2 = numpy.array([10., 10.])
+ origin1 = numpy.array([1.0, 10.0])
+ size1 = numpy.array([10.0, 10.0])
+ origin2 = numpy.array([2.0, 3.0])
+ size2 = numpy.array([10.0, 10.0])
manager = roi.InteractiveRegionOfInterestManager(self.plot)
self.roiTableWidget.setRegionOfInterestManager(manager)
@@ -587,16 +381,17 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
mx, my = self.plot.dataToPixel(*center)
self.mouseMove(widget, pos=(mx, my))
self.mousePress(widget, qt.Qt.LeftButton, pos=(mx, my))
- self.mouseMove(widget, pos=(mx, my+25))
- self.mouseMove(widget, pos=(mx, my+50))
- self.mouseRelease(widget, qt.Qt.LeftButton, pos=(mx, my+50))
+ self.mouseMove(widget, pos=(mx, my + 25))
+ self.mouseMove(widget, pos=(mx, my + 50))
+ self.mouseRelease(widget, qt.Qt.LeftButton, pos=(mx, my + 50))
result = numpy.array(item.getEndPoints())
# x location is still the same
numpy.testing.assert_allclose(points[:, 0], result[:, 0], atol=0.5)
# size is still the same
- numpy.testing.assert_allclose(points[1] - points[0],
- result[1] - result[0], atol=0.5)
+ numpy.testing.assert_allclose(
+ points[1] - points[0], result[1] - result[0], atol=0.5
+ )
# But Y is not the same
self.assertNotEqual(points[0, 1], result[0, 1])
self.assertNotEqual(points[1, 1], result[1, 1])
@@ -667,7 +462,7 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
self.assertIs(item.getInteractionMode(), roi_items.ArcROI.ThreePointMode)
self.qWait(500)
- # Click on the center
+ # Click on the center
widget = self.plot.getWidgetHandle()
mx, my = self.plot.dataToPixel(*center)
@@ -710,7 +505,7 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
assert item.getInteractionMode() is roi_items.BandROI.BoundedMode
self.qWait(500)
- # Click on the center
+ # Click on the center
widget = self.plot.getWidgetHandle()
mx, my = self.plot.dataToPixel(xcenter, ycenter)
diff --git a/src/silx/gui/plot/tools/test/testRoiItems.py b/src/silx/gui/plot/tools/test/testRoiItems.py
new file mode 100644
index 0000000..9bd9690
--- /dev/null
+++ b/src/silx/gui/plot/tools/test/testRoiItems.py
@@ -0,0 +1,313 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "28/06/2018"
+
+
+import pytest
+import numpy.testing
+
+import silx.gui.plot.items.roi as roi_items
+
+
+def testLine_geometry(qapp):
+ item = roi_items.LineROI()
+ startPoint = numpy.array([1, 2])
+ endPoint = numpy.array([3, 4])
+ item.setEndPoints(startPoint, endPoint)
+ numpy.testing.assert_allclose(item.getEndPoints()[0], startPoint)
+ numpy.testing.assert_allclose(item.getEndPoints()[1], endPoint)
+
+
+def testHLine_geometry(qapp):
+ item = roi_items.HorizontalLineROI()
+ item.setPosition(15)
+ assert item.getPosition() == 15
+
+
+def testVLine_geometry(qapp):
+ item = roi_items.VerticalLineROI()
+ item.setPosition(15)
+ assert item.getPosition() == 15
+
+
+def testPoint_geometry(qapp):
+ point = numpy.array([1, 2])
+ item = roi_items.PointROI()
+ item.setPosition(point)
+ numpy.testing.assert_allclose(item.getPosition(), point)
+
+
+def testRectangle_originGeometry(qapp):
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ center = numpy.array([5, 10])
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin, size=size)
+ numpy.testing.assert_allclose(item.getOrigin(), origin)
+ numpy.testing.assert_allclose(item.getSize(), size)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+
+
+def testRectangle_centerGeometry(qapp):
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ center = numpy.array([5, 10])
+ item = roi_items.RectangleROI()
+ item.setGeometry(center=center, size=size)
+ numpy.testing.assert_allclose(item.getOrigin(), origin)
+ numpy.testing.assert_allclose(item.getSize(), size)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+
+
+def testRectangle_setCenterGeometry(qapp):
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin, size=size)
+ newCenter = numpy.array([0, 0])
+ item.setCenter(newCenter)
+ expectedOrigin = numpy.array([-5, -10])
+ numpy.testing.assert_allclose(item.getOrigin(), expectedOrigin)
+ numpy.testing.assert_allclose(item.getCenter(), newCenter)
+ numpy.testing.assert_allclose(item.getSize(), size)
+
+
+def testRectangle_setOriginGeometry(qapp):
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin, size=size)
+ newOrigin = numpy.array([10, 10])
+ item.setOrigin(newOrigin)
+ expectedCenter = numpy.array([15, 20])
+ numpy.testing.assert_allclose(item.getOrigin(), newOrigin)
+ numpy.testing.assert_allclose(item.getCenter(), expectedCenter)
+ numpy.testing.assert_allclose(item.getSize(), size)
+
+
+def testCircle_geometry(qapp):
+ center = numpy.array([0, 0])
+ radius = 10.0
+ item = roi_items.CircleROI()
+ item.setGeometry(center=center, radius=radius)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ numpy.testing.assert_allclose(item.getRadius(), radius)
+
+
+def testCircle_setCenter(qapp):
+ center = numpy.array([0, 0])
+ radius = 10.0
+ item = roi_items.CircleROI()
+ item.setGeometry(center=center, radius=radius)
+ newCenter = numpy.array([-10, 0])
+ item.setCenter(newCenter)
+ numpy.testing.assert_allclose(item.getCenter(), newCenter)
+ numpy.testing.assert_allclose(item.getRadius(), radius)
+
+
+def testCircle_setRadius(qapp):
+ center = numpy.array([0, 0])
+ radius = 10.0
+ item = roi_items.CircleROI()
+ item.setGeometry(center=center, radius=radius)
+ newRadius = 5.1
+ item.setRadius(newRadius)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ numpy.testing.assert_allclose(item.getRadius(), newRadius)
+
+
+def testCircle_contains(qapp):
+ center = numpy.array([2, -1])
+ radius = 1.0
+ item = roi_items.CircleROI()
+ item.setGeometry(center=center, radius=radius)
+ assert item.contains([1, -1])
+ assert not item.contains([0, 0])
+ assert item.contains([2, 0])
+ assert not item.contains([3.01, -1])
+
+
+def testEllipse_contains(qapp):
+ center = numpy.array([-2, 0])
+ item = roi_items.EllipseROI()
+ item.setCenter(center)
+ item.setOrientation(numpy.pi / 4.0)
+ item.setMajorRadius(2)
+ item.setMinorRadius(1)
+ print(item.getMinorRadius(), item.getMajorRadius())
+ assert not item.contains([0, 0])
+ assert item.contains([-1, 1])
+ assert item.contains([-3, 0])
+ assert item.contains([-2, 0])
+ assert item.contains([-2, 1])
+ assert not item.contains([-4, 1])
+
+
+def testRectangle_isIn(qapp):
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin, size=size)
+ assert item.contains(position=(0, 0))
+ assert item.contains(position=(2, 14))
+ assert not item.contains(position=(14, 12))
+
+
+def testPolygon_emptyGeometry(qapp):
+ points = numpy.empty((0, 2))
+ item = roi_items.PolygonROI()
+ item.setPoints(points)
+ numpy.testing.assert_allclose(item.getPoints(), points)
+
+
+def testPolygon_geometry(qapp):
+ points = numpy.array([[10, 10], [12, 10], [50, 1]])
+ item = roi_items.PolygonROI()
+ item.setPoints(points)
+ numpy.testing.assert_allclose(item.getPoints(), points)
+
+
+def testPolygon_isIn(qapp):
+ points = numpy.array([[0, 0], [0, 10], [5, 10]])
+ item = roi_items.PolygonROI()
+ item.setPoints(points)
+ assert item.contains((0, 0))
+ assert not item.contains((6, 2))
+ assert not item.contains((-2, 5))
+ assert not item.contains((2, -1))
+ assert not item.contains((8, 1))
+ assert item.contains((1, 8))
+
+
+def testArc_getToSetGeometry(qapp):
+ """Test that we can use getGeometry as input to setGeometry"""
+ item = roi_items.ArcROI()
+ item.setFirstShapePoints(numpy.array([[5, 10], [50, 100]]))
+ item.setGeometry(*item.getGeometry())
+
+
+def testArc_degenerated_point(qapp):
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 0, 0, 0, 0
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+
+
+def testArc_degenerated_line(qapp):
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+
+
+def testArc_special_circle(qapp):
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, 3 * numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ assert item.getInnerRadius() == pytest.approx(innerRadius)
+ assert item.getOuterRadius() == pytest.approx(outerRadius)
+ assert item.getStartAngle() == pytest.approx(item.getEndAngle() - numpy.pi * 2.0)
+ assert item.isClosed()
+
+
+def testArc_special_donut(qapp):
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi, 3 * numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ assert item.getInnerRadius() == pytest.approx(innerRadius)
+ assert item.getOuterRadius() == pytest.approx(outerRadius)
+ assert item.getStartAngle() == pytest.approx(item.getEndAngle() - numpy.pi * 2.0)
+ assert item.isClosed()
+
+
+def testArc_clockwiseGeometry(qapp):
+ """Test that we can use getGeometry as input to setGeometry"""
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ assert item.getInnerRadius() == pytest.approx(innerRadius)
+ assert item.getOuterRadius() == pytest.approx(outerRadius)
+ assert item.getStartAngle() == pytest.approx(startAngle)
+ assert item.getEndAngle() == pytest.approx(endAngle)
+ assert not item.isClosed()
+
+
+def testArc_anticlockwiseGeometry(qapp):
+ """Test that we can use getGeometry as input to setGeometry"""
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = (
+ 1,
+ 100,
+ numpy.pi * 0.5,
+ -numpy.pi * 0.5,
+ )
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ assert item.getInnerRadius() == pytest.approx(innerRadius)
+ assert item.getOuterRadius() == pytest.approx(outerRadius)
+ assert item.getStartAngle() == pytest.approx(startAngle)
+ assert item.getEndAngle() == pytest.approx(endAngle)
+ assert not item.isClosed()
+
+
+def testArc_position(qapp):
+ """Test validity of getPosition"""
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ assert item.getPosition(roi_items.ArcROI.Role.START) == pytest.approx((10.0, 70.5))
+ assert item.getPosition(roi_items.ArcROI.Role.STOP) == pytest.approx((-40.5, 20.0))
+ assert item.getPosition(roi_items.ArcROI.Role.MIDDLE) == pytest.approx(
+ (-25.71, 55.71), abs=0.1
+ )
+ assert item.getPosition(roi_items.ArcROI.Role.CENTER) == pytest.approx(
+ (10.0, 20), abs=0.1
+ )
+
+
+def testHRange_geometry(qapp):
+ item = roi_items.HorizontalRangeROI()
+ vmin = 1
+ vmax = 3
+ item.setRange(vmin, vmax)
+ assert item.getMin() == pytest.approx(vmin)
+ assert item.getMax() == pytest.approx(vmax)
+ assert item.getCenter() == pytest.approx(2)
+
+
+def testBand_getToSetGeometry(qapp):
+ """Test that we can use getGeometry as input to setGeometry"""
+ item = roi_items.BandROI()
+ item.setFirstShapePoints(numpy.array([[5, 10], [50, 100]]))
+ item.setGeometry(*item.getGeometry())
diff --git a/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py b/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py
index 9b9caa1..29c9ad0 100644
--- a/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py
+++ b/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py
@@ -26,7 +26,6 @@ __license__ = "MIT"
__date__ = "28/06/2018"
-import unittest
import numpy
from silx.gui import qt
@@ -67,8 +66,9 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase):
# Add a scatter plot
self.plot.addScatter(
- x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.))
- self.plot.resetZoom(dataMargins=(.1, .1, .1, .1))
+ x=(0.0, 1.0, 1.0, 0.0), y=(0.0, 0.0, 1.0, 1.0), value=(0.0, 1.0, 2.0, 3.0)
+ )
+ self.plot.resetZoom(dataMargins=(0.1, 0.1, 0.1, 0.1))
self.qapp.processEvents()
# Set a ROI profile
@@ -107,8 +107,9 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase):
# Add a scatter plot
self.plot.addScatter(
- x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.))
- self.plot.resetZoom(dataMargins=(.1, .1, .1, .1))
+ x=(0.0, 1.0, 1.0, 0.0), y=(0.0, 0.0, 1.0, 1.0), value=(0.0, 1.0, 2.0, 3.0)
+ )
+ self.plot.resetZoom(dataMargins=(0.1, 0.1, 0.1, 0.1))
self.qapp.processEvents()
# Set a ROI profile
@@ -160,13 +161,14 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase):
# Add a scatter plot
self.plot.addScatter(
- x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.))
- self.plot.resetZoom(dataMargins=(.1, .1, .1, .1))
+ x=(0.0, 1.0, 1.0, 0.0), y=(0.0, 0.0, 1.0, 1.0), value=(0.0, 1.0, 2.0, 3.0)
+ )
+ self.plot.resetZoom(dataMargins=(0.1, 0.1, 0.1, 0.1))
self.qapp.processEvents()
# Set a ROI profile
roi = rois.ProfileScatterLineROI()
- roi.setEndPoints(numpy.array([0., 0.]), numpy.array([1., 1.]))
+ roi.setEndPoints(numpy.array([0.0, 0.0]), numpy.array([1.0, 1.0]))
roi.setNPoints(8)
roiManager.addRoi(roi)
diff --git a/src/silx/gui/plot/tools/test/testTools.py b/src/silx/gui/plot/tools/test/testTools.py
index 507b922..1212ead 100644
--- a/src/silx/gui/plot/tools/test/testTools.py
+++ b/src/silx/gui/plot/tools/test/testTools.py
@@ -29,11 +29,9 @@ __date__ = "02/03/2018"
import functools
-import unittest
import numpy
from silx.utils.testutils import LoggingValidator
-from silx.gui.utils.testutils import qWaitForWindowExposedAndActivate
from silx.gui import qt
from silx.gui.plot import PlotWindow
from silx.gui.plot import tools
@@ -82,28 +80,28 @@ class TestPositionInfo(PlotWidgetTestCase):
def testDefaultConverters(self):
"""Test PositionInfo with default converters"""
positionWidget = tools.PositionInfo(plot=self.plot)
- self._test(positionWidget, ('X', 'Y'))
+ self._test(positionWidget, ("X", "Y"))
def testCustomConverters(self):
"""Test PositionInfo with custom converters"""
converters = [
- ('Coords', lambda x, y: (int(x), int(y))),
- ('Radius', lambda x, y: numpy.sqrt(x * x + y * y)),
- ('Angle', lambda x, y: numpy.degrees(numpy.arctan2(y, x)))
+ ("Coords", lambda x, y: (int(x), int(y))),
+ ("Radius", lambda x, y: numpy.sqrt(x * x + y * y)),
+ ("Angle", lambda x, y: numpy.degrees(numpy.arctan2(y, x))),
]
- positionWidget = tools.PositionInfo(plot=self.plot,
- converters=converters)
- self._test(positionWidget, ('Coords', 'Radius', 'Angle'))
+ positionWidget = tools.PositionInfo(plot=self.plot, converters=converters)
+ self._test(positionWidget, ("Coords", "Radius", "Angle"))
def testFailingConverters(self):
"""Test PositionInfo with failing custom converters"""
+
def raiseException(x, y):
raise RuntimeError()
positionWidget = tools.PositionInfo(
- plot=self.plot,
- converters=[('Exception', raiseException)])
- self._test(positionWidget, ['Exception'], error=2)
+ plot=self.plot, converters=[("Exception", raiseException)]
+ )
+ self._test(positionWidget, ["Exception"], error=2)
def testUpdate(self):
"""Test :meth:`PositionInfo.updateInfo`"""
@@ -115,7 +113,8 @@ class TestPositionInfo(PlotWidgetTestCase):
positionWidget = tools.PositionInfo(
plot=self.plot,
- converters=[('Call count', functools.partial(update, calls))])
+ converters=[("Call count", functools.partial(update, calls))],
+ )
positionWidget.updateInfo()
self.assertEqual(len(calls), 1)
@@ -125,10 +124,12 @@ class TestPlotToolsToolbars(PlotWidgetTestCase):
"""Tests toolbars from silx.gui.plot.tools"""
def test(self):
- """"Add all toolbars"""
- for tbClass in (tools.InteractiveModeToolBar,
- tools.ImageToolBar,
- tools.CurveToolBar,
- tools.OutputToolBar):
+ """ "Add all toolbars"""
+ for tbClass in (
+ tools.InteractiveModeToolBar,
+ tools.ImageToolBar,
+ tools.CurveToolBar,
+ tools.OutputToolBar,
+ ):
tb = tbClass(parent=self.plot, plot=self.plot)
self.plot.addToolBar(tb)
diff --git a/src/silx/gui/plot/tools/toolbars.py b/src/silx/gui/plot/tools/toolbars.py
index bb89942..7f38f1c 100644
--- a/src/silx/gui/plot/tools/toolbars.py
+++ b/src/silx/gui/plot/tools/toolbars.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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
@@ -33,7 +33,6 @@ from ... import qt
from .. import actions
from ..PlotWidget import PlotWidget
from .. import PlotToolButtons
-from ....utils.deprecation import deprecated
class InteractiveModeToolBar(qt.QToolBar):
@@ -44,17 +43,15 @@ class InteractiveModeToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, plot=None, title='Plot Interaction'):
+ def __init__(self, parent=None, plot=None, title="Plot Interaction"):
super(InteractiveModeToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
- self._zoomModeAction = actions.mode.ZoomModeAction(
- parent=self, plot=plot)
+ self._zoomModeAction = actions.mode.ZoomModeAction(parent=self, plot=plot)
self.addAction(self._zoomModeAction)
- self._panModeAction = actions.mode.PanModeAction(
- parent=self, plot=plot)
+ self._panModeAction = actions.mode.PanModeAction(parent=self, plot=plot)
self.addAction(self._panModeAction)
def getZoomModeAction(self):
@@ -80,7 +77,7 @@ class OutputToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, plot=None, title='Plot Output'):
+ def __init__(self, parent=None, plot=None, title="Plot Output"):
super(OutputToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
@@ -124,25 +121,25 @@ class ImageToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, plot=None, title='Image'):
+ def __init__(self, parent=None, plot=None, title="Image"):
super(ImageToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
- self._resetZoomAction = actions.control.ResetZoomAction(
- parent=self, plot=plot)
+ self._resetZoomAction = actions.control.ResetZoomAction(parent=self, plot=plot)
self.addAction(self._resetZoomAction)
- self._colormapAction = actions.control.ColormapAction(
- parent=self, plot=plot)
+ self._colormapAction = actions.control.ColormapAction(parent=self, plot=plot)
self.addAction(self._colormapAction)
self._keepDataAspectRatioButton = PlotToolButtons.AspectToolButton(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addWidget(self._keepDataAspectRatioButton)
self._yAxisInvertedButton = PlotToolButtons.YAxisOriginToolButton(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addWidget(self._yAxisInvertedButton)
def getResetZoomAction(self):
@@ -182,37 +179,40 @@ class CurveToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, plot=None, title='Image'):
+ def __init__(self, parent=None, plot=None, title="Image"):
super(CurveToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
- self._resetZoomAction = actions.control.ResetZoomAction(
- parent=self, plot=plot)
+ self._resetZoomAction = actions.control.ResetZoomAction(parent=self, plot=plot)
self.addAction(self._resetZoomAction)
self._xAxisAutoScaleAction = actions.control.XAxisAutoScaleAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._xAxisAutoScaleAction)
self._yAxisAutoScaleAction = actions.control.YAxisAutoScaleAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._yAxisAutoScaleAction)
self._xAxisLogarithmicAction = actions.control.XAxisLogarithmicAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._xAxisLogarithmicAction)
self._yAxisLogarithmicAction = actions.control.YAxisLogarithmicAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._yAxisLogarithmicAction)
- self._gridAction = actions.control.GridAction(
- parent=self, plot=plot)
+ self._gridAction = actions.control.GridAction(parent=self, plot=plot)
self.addAction(self._gridAction)
self._curveStyleAction = actions.control.CurveStyleAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._curveStyleAction)
def getResetZoomAction(self):
@@ -273,37 +273,38 @@ class ScatterToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, plot=None, title='Scatter Tools'):
+ def __init__(self, parent=None, plot=None, title="Scatter Tools"):
super(ScatterToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
- self._resetZoomAction = actions.control.ResetZoomAction(
- parent=self, plot=plot)
+ self._resetZoomAction = actions.control.ResetZoomAction(parent=self, plot=plot)
self.addAction(self._resetZoomAction)
self._xAxisLogarithmicAction = actions.control.XAxisLogarithmicAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._xAxisLogarithmicAction)
self._yAxisLogarithmicAction = actions.control.YAxisLogarithmicAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._yAxisLogarithmicAction)
self._keepDataAspectRatioButton = PlotToolButtons.AspectToolButton(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addWidget(self._keepDataAspectRatioButton)
- self._gridAction = actions.control.GridAction(
- parent=self, plot=plot)
+ self._gridAction = actions.control.GridAction(parent=self, plot=plot)
self.addAction(self._gridAction)
- self._colormapAction = actions.control.ColormapAction(
- parent=self, plot=plot)
+ self._colormapAction = actions.control.ColormapAction(parent=self, plot=plot)
self.addAction(self._colormapAction)
- self._visualizationToolButton = \
- PlotToolButtons.ScatterVisualizationToolButton(parent=self, plot=plot)
+ self._visualizationToolButton = PlotToolButtons.ScatterVisualizationToolButton(
+ parent=self, plot=plot
+ )
self.addWidget(self._visualizationToolButton)
def getResetZoomAction(self):
@@ -354,8 +355,3 @@ class ScatterToolBar(qt.QToolBar):
:rtype: ScatterVisualizationToolButton
"""
return self._visualizationToolButton
-
- @deprecated(replacement='getScatterVisualizationToolButton',
- since_version='0.11.0')
- def getSymbolToolButton(self):
- return self.getScatterVisualizationToolButton()
diff --git a/src/silx/gui/plot/utils/axis.py b/src/silx/gui/plot/utils/axis.py
index 419a71c..4c6bcef 100644
--- a/src/silx/gui/plot/utils/axis.py
+++ b/src/silx/gui/plot/utils/axis.py
@@ -56,14 +56,16 @@ class SyncAxes(object):
.. versionadded:: 0.6
"""
- def __init__(self, axes,
- syncLimits=True,
- syncScale=True,
- syncDirection=True,
- syncCenter=False,
- syncZoom=False,
- filterHiddenPlots=False
- ):
+ def __init__(
+ self,
+ axes,
+ syncLimits=True,
+ syncScale=True,
+ syncDirection=True,
+ syncCenter=False,
+ syncZoom=False,
+ filterHiddenPlots=False,
+ ):
"""
Constructor
@@ -79,12 +81,13 @@ class SyncAxes(object):
"""
object.__init__(self)
- def implies(x, y): return bool(y ** x)
+ def implies(x, y):
+ return bool(y**x)
- assert(implies(syncZoom, not syncLimits))
- assert(implies(syncCenter, not syncLimits))
- assert(implies(syncLimits, not syncCenter))
- assert(implies(syncLimits, not syncZoom))
+ assert implies(syncZoom, not syncLimits)
+ assert implies(syncCenter, not syncLimits)
+ assert implies(syncLimits, not syncCenter)
+ assert implies(syncLimits, not syncZoom)
self.__filterHiddenPlots = filterHiddenPlots
self.__locked = False
@@ -313,7 +316,7 @@ class SyncAxes(object):
elif isinstance(axis, YAxis):
return bounds[3]
else:
- assert(False)
+ assert False
def __getLimitsFromCenter(self, axis, pos, pixelSize=None):
"""Returns the limits to apply to this axis to move the `pos` into the
diff --git a/src/silx/gui/plot/utils/intersections.py b/src/silx/gui/plot/utils/intersections.py
index 4f6ed23..faf6641 100644
--- a/src/silx/gui/plot/utils/intersections.py
+++ b/src/silx/gui/plot/utils/intersections.py
@@ -24,7 +24,9 @@
"""This module contains utils class for axes management.
"""
-__authors__ = ["H. Payno", ]
+__authors__ = [
+ "H. Payno",
+]
__license__ = "MIT"
__date__ = "18/05/2020"
@@ -59,11 +61,11 @@ def lines_intersection(line1_pt1, line1_pt2, line2_pt1, line2_pt2):
return None
return (
(num / denom.astype(float)) * dir_line2[0] + line2_pt1[0],
- (num / denom.astype(float)) * dir_line2[1] + line2_pt1[1])
+ (num / denom.astype(float)) * dir_line2[1] + line2_pt1[1],
+ )
-def segments_intersection(seg1_start_pt, seg1_end_pt, seg2_start_pt,
- seg2_end_pt):
+def segments_intersection(seg1_start_pt, seg1_end_pt, seg2_start_pt, seg2_end_pt):
"""
Compute intersection between two segments
@@ -74,10 +76,12 @@ def segments_intersection(seg1_start_pt, seg1_end_pt, seg2_start_pt,
:return: numpy.array if an intersection exists, else None
:rtype: Union[None,numpy.array]
"""
- intersection = lines_intersection(line1_pt1=seg1_start_pt,
- line1_pt2=seg1_end_pt,
- line2_pt1=seg2_start_pt,
- line2_pt2=seg2_end_pt)
+ intersection = lines_intersection(
+ line1_pt1=seg1_start_pt,
+ line1_pt2=seg1_end_pt,
+ line2_pt1=seg2_start_pt,
+ line2_pt2=seg2_end_pt,
+ )
if intersection is not None:
max_x_seg1 = max(seg1_start_pt[0], seg1_end_pt[0])
max_x_seg2 = max(seg2_start_pt[0], seg2_end_pt[0])
@@ -93,8 +97,10 @@ def segments_intersection(seg1_start_pt, seg1_end_pt, seg2_start_pt,
max_tmp_x = min(max_x_seg1, max_x_seg2)
min_tmp_y = max(min_y_seg1, min_y_seg2)
max_tmp_y = min(max_y_seg1, max_y_seg2)
- if (min_tmp_x <= intersection[0] <= max_tmp_x and
- min_tmp_y <= intersection[1] <= max_tmp_y):
+ if (
+ min_tmp_x <= intersection[0] <= max_tmp_x
+ and min_tmp_y <= intersection[1] <= max_tmp_y
+ ):
return intersection
else:
return None