summaryrefslogtreecommitdiff
path: root/silx/gui
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui')
-rw-r--r--silx/gui/data/NXdataWidgets.py8
-rw-r--r--silx/gui/plot/ImageView.py17
-rw-r--r--silx/gui/plot/LegendSelector.py7
-rw-r--r--silx/gui/plot/PlotTools.py11
-rw-r--r--silx/gui/plot/PlotWidget.py130
-rw-r--r--silx/gui/plot/Profile.py9
-rw-r--r--silx/gui/plot/backends/BackendMatplotlib.py134
-rw-r--r--silx/gui/plot/items/axis.py2
-rw-r--r--silx/gui/plot/items/core.py7
-rw-r--r--silx/gui/plot/items/curve.py3
-rw-r--r--silx/gui/plot/test/__init__.py4
-rw-r--r--silx/gui/plot/test/testImageView.py136
-rw-r--r--silx/gui/plot/test/testPlotWidget.py21
-rw-r--r--silx/gui/plot3d/Plot3DWindow.py2
-rw-r--r--silx/gui/plot3d/SFViewParamTree.py28
-rw-r--r--silx/gui/plot3d/actions/__init__.py7
-rw-r--r--silx/gui/plot3d/actions/viewpoint.py98
17 files changed, 495 insertions, 129 deletions
diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py
index b820380..7aaf3ad 100644
--- a/silx/gui/data/NXdataWidgets.py
+++ b/silx/gui/data/NXdataWidgets.py
@@ -116,7 +116,7 @@ class ArrayCurvePlot(qt.QWidget):
:param title: Graph title
"""
self.__signal = y
- self.__signal_name = ylabel
+ self.__signal_name = ylabel or "Y"
self.__signal_errors = yerror
self.__axis = x
self.__axis_name = xlabel
@@ -136,7 +136,7 @@ class ArrayCurvePlot(qt.QWidget):
self._plot.setGraphTitle(title or "")
self._plot.getXAxis().setLabel(self.__axis_name or "X")
- self._plot.getYAxis().setLabel(self.__signal_name or "Y")
+ self._plot.getYAxis().setLabel(self.__signal_name)
self._updateCurve()
if not self.__selector_is_connected:
@@ -175,8 +175,8 @@ class ArrayCurvePlot(qt.QWidget):
xerror=self.__axis_errors,
yerror=y_errors)
- # x monotonically increasing: curve
- elif numpy.all(numpy.diff(x) > 0):
+ # x monotonically increasing or decreasiing: curve
+ elif numpy.all(numpy.diff(x) > 0) or numpy.all(numpy.diff(x) < 0):
self._plot.addCurve(x, y, legend=legend,
xerror=self.__axis_errors,
yerror=y_errors)
diff --git a/silx/gui/plot/ImageView.py b/silx/gui/plot/ImageView.py
index 803a2fc..46e56e6 100644
--- a/silx/gui/plot/ImageView.py
+++ b/silx/gui/plot/ImageView.py
@@ -311,16 +311,13 @@ class ImageView(PlotWindow):
def _initWidgets(self, backend):
"""Set-up layout and plots."""
- # Monkey-patch for histogram size
- # alternative: create a layout that does not use widget size hints
- def sizeHint():
- return qt.QSize(self.HISTOGRAMS_HEIGHT, self.HISTOGRAMS_HEIGHT)
-
self._histoHPlot = PlotWidget(backend=backend)
+ self._histoHPlot.getWidgetHandle().setMinimumHeight(
+ self.HISTOGRAMS_HEIGHT)
+ self._histoHPlot.getWidgetHandle().setMaximumHeight(
+ self.HISTOGRAMS_HEIGHT)
self._histoHPlot.setInteractiveMode('zoom')
self._histoHPlot.sigPlotSignal.connect(self._histoHPlotCB)
- self._histoHPlot.getWidgetHandle().sizeHint = sizeHint
- self._histoHPlot.getWidgetHandle().minimumSizeHint = sizeHint
self.setPanWithArrowKeys(True)
@@ -330,10 +327,12 @@ class ImageView(PlotWindow):
self.sigActiveImageChanged.connect(self._activeImageChangedSlot)
self._histoVPlot = PlotWidget(backend=backend)
+ self._histoVPlot.getWidgetHandle().setMinimumWidth(
+ self.HISTOGRAMS_HEIGHT)
+ self._histoVPlot.getWidgetHandle().setMaximumWidth(
+ self.HISTOGRAMS_HEIGHT)
self._histoVPlot.setInteractiveMode('zoom')
self._histoVPlot.sigPlotSignal.connect(self._histoVPlotCB)
- self._histoVPlot.getWidgetHandle().sizeHint = sizeHint
- self._histoVPlot.getWidgetHandle().minimumSizeHint = sizeHint
self._radarView = RadarView()
self._radarView.visibleRectDragged.connect(self._radarViewCB)
diff --git a/silx/gui/plot/LegendSelector.py b/silx/gui/plot/LegendSelector.py
index 31bc3db..e9cfd1d 100644
--- a/silx/gui/plot/LegendSelector.py
+++ b/silx/gui/plot/LegendSelector.py
@@ -29,7 +29,7 @@ This widget is meant to work with :class:`PlotWindow`.
__authors__ = ["V.A. Sole", "T. Rueter", "T. Vincent"]
__license__ = "MIT"
-__data__ = "08/08/2016"
+__data__ = "16/10/2017"
import logging
@@ -971,9 +971,10 @@ class LegendsDockWidget(qt.QDockWidget):
def renameCurve(self, oldLegend, newLegend):
"""Change the name of a curve using remove and addCurve
- :param str oldLegend: The legend of the curve to be change
+ :param str oldLegend: The legend of the curve to be changed
:param str newLegend: The new legend of the curve
"""
+ 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),
@@ -992,6 +993,8 @@ class LegendsDockWidget(qt.QDockWidget):
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"""
diff --git a/silx/gui/plot/PlotTools.py b/silx/gui/plot/PlotTools.py
index 85dcc31..ed62d48 100644
--- a/silx/gui/plot/PlotTools.py
+++ b/silx/gui/plot/PlotTools.py
@@ -29,7 +29,7 @@ from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "02/10/2017"
+__date__ = "16/10/2017"
import logging
@@ -151,13 +151,16 @@ class PositionInfo(qt.QWidget):
"""
if event['event'] == 'mouseMoved':
x, y = event['x'], event['y']
- self._updateStatusBar(x, y)
+ xPixel, yPixel = event['xpixel'], event['ypixel']
+ self._updateStatusBar(x, y, xPixel, yPixel)
- def _updateStatusBar(self, x, y):
+ def _updateStatusBar(self, x, y, xPixel, yPixel):
"""Update information from the status bar using the definitions.
:param float x: Position-x in data
:param float y: Position-y in data
+ :param float xPixel: Position-x in pixels
+ :param float yPixel: Position-y in pixels
"""
styleSheet = "color: rgb(0, 0, 0);" # Default style
@@ -180,8 +183,6 @@ class PositionInfo(qt.QWidget):
closestInPixels = self.plot.dataToPixel(
xClosest, yClosest, axis=activeCurve.getYAxis())
if closestInPixels is not None:
- xPixel, yPixel = event['xpixel'], event['ypixel']
-
if (abs(closestInPixels[0] - xPixel) < 5 and
abs(closestInPixels[1] - yPixel) < 5):
# Update label style sheet
diff --git a/silx/gui/plot/PlotWidget.py b/silx/gui/plot/PlotWidget.py
index 8fd5a5e..5bf2b59 100644
--- a/silx/gui/plot/PlotWidget.py
+++ b/silx/gui/plot/PlotWidget.py
@@ -175,7 +175,7 @@ from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "30/08/2017"
+__date__ = "18/10/2017"
from collections import OrderedDict, namedtuple
@@ -612,8 +612,7 @@ class PlotWidget(qt.QMainWindow):
if (kind == 'curve' and not self.getAllCurves(just_legend=True,
withhidden=True)):
- self._colorIndex = 0
- self._styleIndex = 0
+ self._resetColorAndStyle()
self.notify('contentChanged', action='remove',
kind=kind, legend=legend)
@@ -780,6 +779,9 @@ class PlotWidget(qt.QMainWindow):
# Check if curve was previously active
wasActive = self.getActiveCurve(just_legend=True) == legend
+ if replace:
+ self._resetColorAndStyle()
+
# Create/Update curve object
curve = self.getCurve(legend)
mustBeAdded = curve is None
@@ -2319,7 +2321,7 @@ class PlotWidget(qt.QMainWindow):
flag = bool(flag)
self._backend.setKeepDataAspectRatio(flag=flag)
self._setDirtyPlot()
- self.resetZoom()
+ self._forceResetZoom()
self.notify('setKeepDataAspectRatio', state=flag)
def getGraphGrid(self):
@@ -2431,6 +2433,10 @@ class PlotWidget(qt.QMainWindow):
"""
return Colormap.getSupportedColormaps()
+ def _resetColorAndStyle(self):
+ self._colorIndex = 0
+ self._styleIndex = 0
+
def _getColorAndStyle(self):
color = self.colorList[self._colorIndex]
style = self._styleList[self._styleIndex]
@@ -2614,6 +2620,66 @@ class PlotWidget(qt.QMainWindow):
self._backend.replot()
self._dirty = False # reset dirty flag
+ def _forceResetZoom(self, dataMargins=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.
+
+ Extra margins can be added around the data inside the plot area
+ (see :meth:`setDataMargins`).
+ Margins are given as one ratio of the data range per limit of the
+ 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).
+ """
+ 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
+ if ranges.yright is None:
+ ymin2, ymax2 = None, None
+ else:
+ ymin2, ymax2 = 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:]
+ plotRatio = plotHeight / plotWidth
+
+ if plotRatio > 0.:
+ 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)
+
def resetZoom(self, dataMargins=None):
"""Reset the plot limits to the bounds of the data and redraw the plot.
@@ -2631,9 +2697,6 @@ class PlotWidget(qt.QMainWindow):
the plot area for each side (default: no margins).
:type dataMargins: A 4-tuple of float as (xMin, xMax, yMin, yMax).
"""
- if dataMargins is None:
- dataMargins = self._defaultDataMargins
-
xLimits = self._xAxis.getLimits()
yLimits = self._yAxis.getLimits()
y2Limits = self._yRightAxis.getLimits()
@@ -2641,52 +2704,19 @@ class PlotWidget(qt.QMainWindow):
xAuto = self._xAxis.isAutoScale()
yAuto = self._yAxis.isAutoScale()
+ # With log axes, autoscale if limits are <= 0
+ # 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):
+ yAuto = True
+
if not xAuto and not yAuto:
_logger.debug("Nothing to autoscale")
else: # Some axes to autoscale
+ self._forceResetZoom(dataMargins=dataMargins)
- # 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
- if ranges.yright is None:
- ymin2, ymax2 = None, None
- else:
- ymin2, ymax2 = 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:]
- plotRatio = plotHeight / plotWidth
-
- if plotRatio > 0.:
- 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)
-
+ # Restore limits for axis not in autoscale
if not xAuto and yAuto:
self.setGraphXLimits(*xLimits)
elif xAuto and not yAuto:
@@ -2696,8 +2726,6 @@ class PlotWidget(qt.QMainWindow):
if yLimits is not None:
self.setGraphYLimits(yLimits[0], yLimits[1], axis='left')
- self._setDirtyPlot()
-
if (xLimits != self._xAxis.getLimits() or
yLimits != self._yAxis.getLimits() or
y2Limits != self._yRightAxis.getLimits()):
diff --git a/silx/gui/plot/Profile.py b/silx/gui/plot/Profile.py
index ff85695..4a74fa7 100644
--- a/silx/gui/plot/Profile.py
+++ b/silx/gui/plot/Profile.py
@@ -31,6 +31,8 @@ __license__ = "MIT"
__date__ = "17/08/2017"
+import weakref
+
import numpy
from silx.image.bilinear import BilinearImage
@@ -348,7 +350,7 @@ class ProfileToolBar(qt.QToolBar):
title='Profile Selection'):
super(ProfileToolBar, self).__init__(title, parent)
assert plot is not None
- self.plot = plot
+ self._plotRef = weakref.ref(plot)
self._overlayColor = None
self._defaultOverlayColor = 'red' # update when active image change
@@ -443,6 +445,11 @@ class ProfileToolBar(qt.QToolBar):
self.getProfileMainWindow().sigClose.connect(self.clearProfile)
@property
+ def plot(self):
+ """The :class:`.PlotWidget` associated to the toolbar."""
+ return self._plotRef()
+
+ @property
@deprecated(since_version="0.6.0")
def browseAction(self):
return self._browseAction
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
index 59e753e..b41f20e 100644
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/silx/gui/plot/backends/BackendMatplotlib.py
@@ -28,7 +28,7 @@ from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent, H. Payno"]
__license__ = "MIT"
-__date__ = "16/08/2017"
+__date__ = "18/10/2017"
import logging
@@ -306,6 +306,14 @@ class BackendMatplotlib(BackendBase.BackendBase):
ystep = 1 if scale[1] >= 0. else -1
data = data[::ystep, ::xstep]
+ if matplotlib.__version__ < "2.1":
+ # matplotlib 1.4.2 do not support float128
+ dtype = data.dtype
+ if dtype.kind == "f" and dtype.itemsize >= 16:
+ _logger.warning("Your matplotlib version do not support "
+ "float128. Data converted to floa64.")
+ data = data.astype(numpy.float64)
+
image.set_data(data)
self.ax.add_artist(image)
@@ -602,16 +610,32 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Graph axes
def setXAxisLogarithmic(self, flag):
- if matplotlib.__version__ >= "2.0.0":
- self.ax.cla()
- self.ax2.cla()
+ # 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 matplotlib.__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')
def setYAxisLogarithmic(self, flag):
- if matplotlib.__version__ >= "2.0.0":
- self.ax.cla()
- self.ax2.cla()
+ # 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 matplotlib.__version__ >= '2.1.0':
+ redraw = False
+ for axis in (self.ax, self.ax2):
+ ylim = axis.get_ylim()
+ if ylim[0] <= 0 and ylim[1] <= 0:
+ axis.set_ylim(1, 10)
+ redraw = True
+ if redraw:
+ self.draw()
+
self.ax2.set_yscale('log' if flag else 'linear')
self.ax.set_yscale('log' if flag else 'linear')
@@ -700,12 +724,12 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
"""Signal handling automatic asynchronous replot"""
def __init__(self, plot, parent=None):
- self._insideResizeEventMethod = False
-
BackendMatplotlib.__init__(self, plot, parent)
FigureCanvasQTAgg.__init__(self, self.fig)
self.setParent(parent)
+ self._limitsBeforeResize = None
+
FigureCanvasQTAgg.setSizePolicy(
self, qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
FigureCanvasQTAgg.updateGeometry(self)
@@ -806,55 +830,83 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
# replot control
def resizeEvent(self, event):
- self._insideResizeEventMethod = True
- # Need to dirty the whole plot on resize.
- self._plot._setDirtyPlot()
+ # Store current limits
+ self._limitsBeforeResize = (
+ self.ax.get_xbound(), self.ax.get_ybound(), self.ax2.get_ybound())
+
FigureCanvasQTAgg.resizeEvent(self, event)
- self._insideResizeEventMethod = False
+ if self.isKeepDataAspectRatio() or self._overlays or self._graphCursor:
+ # This is needed with matplotlib 1.5.x and 2.0.x
+ self._plot._setDirtyPlot()
+
+ def _drawOverlays(self):
+ """Draw overlays if any."""
+ if self._overlays or self._graphCursor:
+ # There is some overlays or crosshair
+
+ # This assume that items are only on left/bottom Axes
+ for item in self._overlays:
+ self.ax.draw_artist(item)
+
+ for item in self._graphCursor:
+ self.ax.draw_artist(item)
def draw(self):
- """Override canvas draw method to support faster draw of overlays."""
- if self._plot._getDirtyPlot(): # Need a full redraw
- # Store previous limits
- xLimits = self.ax.get_xbound()
- yLimits = self.ax.get_ybound()
- yRightLimits = self.ax2.get_ybound()
+ """Overload draw
+ It performs a full redraw (including overlays) of the plot.
+ It also resets background and emit limits changed signal.
+
+ This is directly called by matplotlib for widget resize.
+ """
+ # Starting with mpl 2.1.0, toggling autoscale raises a ValueError
+ # in some situations. See #1081, #1136, #1163,
+ if matplotlib.__version__ >= "2.0.0":
+ try:
+ FigureCanvasQTAgg.draw(self)
+ except ValueError as err:
+ _logger.debug(
+ "ValueError caught while calling FigureCanvasQTAgg.draw: "
+ "'%s'", err)
+ else:
FigureCanvasQTAgg.draw(self)
- self._background = None # Any saved background is dirty
- # Check if limits changed due to a resize of the widget
+ if self._overlays or self._graphCursor:
+ # Save background
+ self._background = self.copy_from_bbox(self.fig.bbox)
+ else:
+ self._background = None # Reset background
+
+ # Check if limits changed due to a resize of the widget
+ if self._limitsBeforeResize is not None:
+ xLimits, yLimits, yRightLimits = self._limitsBeforeResize
+ self._limitsBeforeResize = None
+
if xLimits != self.ax.get_xbound():
self._plot.getXAxis()._emitLimitsChanged()
if yLimits != self.ax.get_ybound():
self._plot.getYAxis(axis='left')._emitLimitsChanged()
if yRightLimits != self.ax2.get_ybound():
- self._plot.getYAxis(axis='left')._emitLimitsChanged()
-
- if (self._overlays or self._graphCursor or
- self._plot._getDirtyPlot() == 'overlay'):
- # There are overlays or crosshair, or they is just no more overlays
+ self._plot.getYAxis(axis='right')._emitLimitsChanged()
- # Specific case: called from resizeEvent:
- # avoid store/restore background, just draw the overlay
- if not self._insideResizeEventMethod:
- if self._background is None: # First store the background
- self._background = self.copy_from_bbox(self.fig.bbox)
+ self._drawOverlays()
- self.restore_region(self._background)
-
- # This assume that items are only on left/bottom Axes
- for item in self._overlays:
- self.ax.draw_artist(item)
+ def replot(self):
+ BackendMatplotlib.replot(self)
- for item in self._graphCursor:
- self.ax.draw_artist(item)
+ dirtyFlag = self._plot._getDirtyPlot()
+ if dirtyFlag == 'overlay':
+ # Only redraw overlays using fast rendering path
+ if self._background is None:
+ self._background = self.copy_from_bbox(self.fig.bbox)
+ self.restore_region(self._background)
+ self._drawOverlays()
self.blit(self.fig.bbox)
- def replot(self):
- BackendMatplotlib.replot(self)
- self.draw()
+ elif dirtyFlag: # Need full redraw
+ self.draw()
+
# cursor
diff --git a/silx/gui/plot/items/axis.py b/silx/gui/plot/items/axis.py
index 56fd762..ff36512 100644
--- a/silx/gui/plot/items/axis.py
+++ b/silx/gui/plot/items/axis.py
@@ -220,7 +220,7 @@ class Axis(qt.QObject):
for item in self._plot._getItems(withhidden=True):
item._updated()
self._plot._invalidateDataRange()
- self._plot.resetZoom()
+ self._plot._forceResetZoom()
self.sigScaleChanged.emit(self._scale)
if emitLog:
diff --git a/silx/gui/plot/items/core.py b/silx/gui/plot/items/core.py
index 0f4ffb9..34ac700 100644
--- a/silx/gui/plot/items/core.py
+++ b/silx/gui/plot/items/core.py
@@ -543,7 +543,7 @@ class ColorMixIn(object):
def getColor(self):
"""Returns the RGBA color of the item
- :rtype: 4-tuple of float in [0, 1]
+ :rtype: 4-tuple of float in [0, 1] or array of colors
"""
return self._color
@@ -566,9 +566,8 @@ class ColorMixIn(object):
else: # Array of colors
assert color.ndim == 2
- if self._color != color:
- self._color = color
- self._updated(ItemChangedType.COLOR)
+ self._color = color
+ self._updated(ItemChangedType.COLOR)
class YAxisMixIn(object):
diff --git a/silx/gui/plot/items/curve.py b/silx/gui/plot/items/curve.py
index ce7f03e..0ba475d 100644
--- a/silx/gui/plot/items/curve.py
+++ b/silx/gui/plot/items/curve.py
@@ -31,6 +31,7 @@ __date__ = "06/03/2017"
import logging
+import numpy
from .. import Colors
from .core import (Points, LabelsMixIn, ColorMixIn, YAxisMixIn,
@@ -75,7 +76,7 @@ class Curve(Points, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn, LineMixIn):
xFiltered, yFiltered, xerror, yerror = self.getData(
copy=False, displayed=True)
- if len(xFiltered) == 0:
+ if len(xFiltered) == 0 or not numpy.any(numpy.isfinite(xFiltered)):
return None # No data to display, do not add renderer to backend
return backend.addCurve(xFiltered, yFiltered, self.getLegend(),
diff --git a/silx/gui/plot/test/__init__.py b/silx/gui/plot/test/__init__.py
index 2c2943e..07338b6 100644
--- a/silx/gui/plot/test/__init__.py
+++ b/silx/gui/plot/test/__init__.py
@@ -51,6 +51,7 @@ from . import testItem
from . import testUtilsAxis
from . import testLimitConstraints
from . import testComplexImageView
+from . import testImageView
def suite():
@@ -77,5 +78,6 @@ def suite():
testItem.suite(),
testUtilsAxis.suite(),
testLimitConstraints.suite(),
- testComplexImageView.suite()])
+ testComplexImageView.suite(),
+ testImageView.suite()])
return test_suite
diff --git a/silx/gui/plot/test/testImageView.py b/silx/gui/plot/test/testImageView.py
new file mode 100644
index 0000000..641d438
--- /dev/null
+++ b/silx/gui/plot/test/testImageView.py
@@ -0,0 +1,136 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 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.
+#
+# ###########################################################################*/
+"""Basic tests for PlotWindow"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "22/09/2017"
+
+
+import unittest
+import numpy
+
+from silx.gui import qt
+from silx.gui.test.utils import TestCaseQt
+
+from silx.gui.plot import ImageView
+from silx.gui.plot.Colormap import Colormap
+
+
+class TestImageView(TestCaseQt):
+ """Tests of ImageView widget."""
+
+ def setUp(self):
+ super(TestImageView, self).setUp()
+ self.plot = ImageView()
+ self.plot.show()
+ self.qWaitForWindowExposed(self.plot)
+
+ def tearDown(self):
+ self.qapp.processEvents()
+ self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.plot.close()
+ del self.plot
+ self.qapp.processEvents()
+ super(TestImageView, self).tearDown()
+
+ def testSetImage(self):
+ """Test setImage"""
+ image = numpy.arange(100).reshape(10, 10)
+
+ self.plot.setImage(image, reset=True)
+ self.qWait(100)
+ self.assertEqual(self.plot.getXAxis().getLimits(), (0, 10))
+ self.assertEqual(self.plot.getYAxis().getLimits(), (0, 10))
+
+ # With reset=False
+ self.plot.setImage(image[::2, ::2], reset=False)
+ self.qWait(100)
+ self.assertEqual(self.plot.getXAxis().getLimits(), (0, 10))
+ self.assertEqual(self.plot.getYAxis().getLimits(), (0, 10))
+
+ self.plot.setImage(image, origin=(10, 20), scale=(2, 4), reset=False)
+ self.qWait(100)
+ self.assertEqual(self.plot.getXAxis().getLimits(), (0, 10))
+ self.assertEqual(self.plot.getYAxis().getLimits(), (0, 10))
+
+ # With reset=True
+ self.plot.setImage(image, origin=(1, 2), scale=(1, 0.5), reset=True)
+ self.qWait(100)
+ self.assertEqual(self.plot.getXAxis().getLimits(), (1, 11))
+ self.assertEqual(self.plot.getYAxis().getLimits(), (2, 7))
+
+ self.plot.setImage(image[::2, ::2], reset=True)
+ self.qWait(100)
+ self.assertEqual(self.plot.getXAxis().getLimits(), (0, 5))
+ self.assertEqual(self.plot.getYAxis().getLimits(), (0, 5))
+
+ def testColormap(self):
+ """Test get|setColormap"""
+ image = numpy.arange(100).reshape(10, 10)
+ self.plot.setImage(image)
+
+ # Colormap as dict
+ 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.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.assertEqual(colormap.getVMin(), None)
+ self.assertEqual(colormap.getVMax(), None)
+
+ # Update colormap with keyword argument
+ self.plot.setColormap(normalization='log')
+ self.assertEqual(colormap.getNormalization(), 'log')
+
+ # Colormap as Colormap object
+ cmap = Colormap()
+ self.plot.setColormap(cmap)
+ self.assertIs(self.plot.getColormap(), cmap)
+
+
+def suite():
+ test_suite = unittest.TestSuite()
+ test_suite.addTest(
+ unittest.defaultTestLoader.loadTestsFromTestCase(TestImageView))
+ return test_suite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testPlotWidget.py b/silx/gui/plot/test/testPlotWidget.py
index deeb198..ccee428 100644
--- a/silx/gui/plot/test/testPlotWidget.py
+++ b/silx/gui/plot/test/testPlotWidget.py
@@ -371,6 +371,27 @@ class TestPlotCurve(PlotWidgetTestCase):
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')
+
+ # Array to single color
+ 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')
+
class TestPlotMarker(PlotWidgetTestCase):
"""Basic tests for add*Marker"""
diff --git a/silx/gui/plot3d/Plot3DWindow.py b/silx/gui/plot3d/Plot3DWindow.py
index 1bc2738..d8c393e 100644
--- a/silx/gui/plot3d/Plot3DWindow.py
+++ b/silx/gui/plot3d/Plot3DWindow.py
@@ -35,6 +35,7 @@ __date__ = "26/01/2017"
from silx.gui import qt
from .Plot3DWidget import Plot3DWidget
+from .actions.viewpoint import RotateViewport
from .tools import OutputToolBar, InteractiveModeToolBar
from .tools import ViewpointToolButton
@@ -58,6 +59,7 @@ class Plot3DWindow(qt.QMainWindow):
toolbar = qt.QToolBar(self)
toolbar.addWidget(ViewpointToolButton(plot3D=self._plot3D))
+ toolbar.addAction(RotateViewport(parent=toolbar, plot3d=self._plot3D))
self.addToolBar(toolbar)
toolbar = OutputToolBar(parent=self)
diff --git a/silx/gui/plot3d/SFViewParamTree.py b/silx/gui/plot3d/SFViewParamTree.py
index 8b144df..e67c17e 100644
--- a/silx/gui/plot3d/SFViewParamTree.py
+++ b/silx/gui/plot3d/SFViewParamTree.py
@@ -304,7 +304,10 @@ class ColorItem(SubjectItem):
def getEditor(self, parent, option, index):
editor = QColorEditor(parent)
editor.color = self.getColor()
- editor.sigColorChanged.connect(self._editorSlot)
+
+ # Wrapping call in lambda is a workaround for PySide with Python 3
+ editor.sigColorChanged.connect(
+ lambda color: self._editorSlot(color))
return editor
def _editorSlot(self, color):
@@ -645,7 +648,9 @@ class IsoSurfaceColorItem(SubjectItem):
color = self.subject.getColor()
color.setAlpha(255)
editor.color = color
- editor.sigColorChanged.connect(self.__editorChanged)
+ # Wrapping call in lambda is a workaround for PySide with Python 3
+ editor.sigColorChanged.connect(
+ lambda color: self.__editorChanged(color))
return editor
def __editorChanged(self, color):
@@ -740,7 +745,9 @@ class IsoSurfaceAlphaItem(SubjectItem):
color = self.subject.getColor()
editor.setValue(color.alpha())
- editor.valueChanged.connect(self.__editorChanged)
+ # Wrapping call in lambda is a workaround for PySide with Python 3
+ editor.valueChanged.connect(
+ lambda value: self.__editorChanged(value))
return editor
@@ -1007,7 +1014,10 @@ class PlaneOrientationItem(SubjectItem):
editor = qt.QComboBox(parent)
for iconName, text, tooltip, normal in self._PLANE_ACTIONS:
editor.addItem(getQIcon(iconName), text)
- editor.currentIndexChanged[int].connect(self.__editorChanged)
+
+ # Wrapping call in lambda is a workaround for PySide with Python 3
+ editor.currentIndexChanged[int].connect(
+ lambda index: self.__editorChanged(index))
return editor
def __editorChanged(self, index):
@@ -1074,7 +1084,10 @@ class PlaneColormapItem(ColormapBase):
def getEditor(self, parent, option, index):
editor = qt.QComboBox(parent)
editor.addItems(self.listValues)
- editor.currentIndexChanged[int].connect(self.__editorChanged)
+
+ # Wrapping call in lambda is a workaround for PySide with Python 3
+ editor.currentIndexChanged[int].connect(
+ lambda index: self.__editorChanged(index))
return editor
@@ -1154,7 +1167,10 @@ class NormalizationNode(ColormapBase):
def getEditor(self, parent, option, index):
editor = qt.QComboBox(parent)
editor.addItems(self.listValues)
- editor.currentIndexChanged[int].connect(self.__editorChanged)
+
+ # Wrapping call in lambda is a workaround for PySide with Python 3
+ editor.currentIndexChanged[int].connect(
+ lambda index: self.__editorChanged(index))
return editor
diff --git a/silx/gui/plot3d/actions/__init__.py b/silx/gui/plot3d/actions/__init__.py
index ebc57d2..26243cf 100644
--- a/silx/gui/plot3d/actions/__init__.py
+++ b/silx/gui/plot3d/actions/__init__.py
@@ -28,6 +28,7 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "06/09/2017"
-from .Plot3DAction import Plot3DAction
-from . import io
-from . import mode
+from .Plot3DAction import Plot3DAction # noqa
+from . import viewpoint # noqa
+from . import io # noqa
+from . import mode # noqa
diff --git a/silx/gui/plot3d/actions/viewpoint.py b/silx/gui/plot3d/actions/viewpoint.py
new file mode 100644
index 0000000..6aa7400
--- /dev/null
+++ b/silx/gui/plot3d/actions/viewpoint.py
@@ -0,0 +1,98 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 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.
+#
+# ###########################################################################*/
+"""This module provides Plot3DAction controlling the viewpoint.
+
+It provides QAction to rotate or pan a Plot3DWidget.
+"""
+
+from __future__ import absolute_import, division
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "03/10/2017"
+
+
+import time
+import logging
+
+from silx.gui import qt
+from silx.gui.icons import getQIcon
+from .Plot3DAction import Plot3DAction
+
+
+_logger = logging.getLogger(__name__)
+
+
+class RotateViewport(Plot3DAction):
+ """QAction to rotate the scene of a Plot3DWidget
+
+ :param parent: See :class:`QAction`
+ :param Plot3DWidget plot3d: Plot3DWidget the action is associated with
+ """
+
+ _TIMEOUT_MS = 50
+ """Time interval between to frames (in milliseconds)"""
+
+ _DEGREE_PER_SECONDS = 360. / 5.
+ """Rotation speed of the animation"""
+
+ def __init__(self, parent, plot3d=None):
+ super(RotateViewport, self).__init__(parent, plot3d)
+
+ self._previousTime = None
+
+ self._timer = qt.QTimer(self)
+ self._timer.setInterval(self._TIMEOUT_MS) # 20fps
+ self._timer.timeout.connect(self._rotate)
+
+ self.setIcon(getQIcon('cube-rotate'))
+ self.setText('Rotate scene')
+ self.setToolTip('Rotate the 3D scene around the vertical axis')
+ self.setCheckable(True)
+ self.triggered[bool].connect(self._triggered)
+
+
+ def _triggered(self, checked=False):
+ plot3d = self.getPlot3DWidget()
+ if plot3d is None:
+ _logger.error(
+ 'Cannot start/stop rotation, no associated Plot3DWidget')
+ elif checked:
+ self._previousTime = time.time()
+ self._timer.start()
+ else:
+ self._timer.stop()
+ self._previousTime = None
+
+ def _rotate(self):
+ """Perform a step of the rotation"""
+ if self._previousTime is None:
+ _logger.error('Previous time not set!')
+ angleStep = 0.
+ else:
+ angleStep = self._DEGREE_PER_SECONDS * (time.time() - self._previousTime)
+
+ self.getPlot3DWidget().viewport.orbitCamera('left', angleStep)
+ self._previousTime = time.time()