summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends/BackendMatplotlib.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/backends/BackendMatplotlib.py')
-rwxr-xr-xsilx/gui/plot/backends/BackendMatplotlib.py252
1 files changed, 181 insertions, 71 deletions
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
index 2336494..036e630 100755
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/silx/gui/plot/backends/BackendMatplotlib.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2004-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
@@ -52,6 +52,7 @@ from matplotlib.patches import Rectangle, Polygon
from matplotlib.image import AxesImage
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.tri import Triangulation
@@ -252,6 +253,60 @@ class _PickableContainer(Container):
return False, {}
+class _TextWithOffset(Text):
+ """Text object which can be displayed at a specific position
+ of the plot, but with a pixel offset"""
+
+ def __init__(self, *args, **kwargs):
+ Text.__init__(self, *args, **kwargs)
+ self.pixel_offset = (0, 0)
+ self.__cache = None
+
+ def draw(self, renderer):
+ self.__cache = None
+ return Text.draw(self, renderer)
+
+ def __get_xy(self):
+ if self.__cache is not None:
+ return self.__cache
+
+ align = self.get_horizontalalignment()
+ if align == "left":
+ xoffset = self.pixel_offset[0]
+ elif align == "right":
+ xoffset = -self.pixel_offset[0]
+ else:
+ xoffset = 0
+
+ align = self.get_verticalalignment()
+ if align == "top":
+ yoffset = -self.pixel_offset[1]
+ elif align == "bottom":
+ yoffset = self.pixel_offset[1]
+ else:
+ yoffset = 0
+
+ trans = self.get_transform()
+ invtrans = self.get_transform().inverted()
+
+ x = super(_TextWithOffset, self).convert_xunits(self._x)
+ y = super(_TextWithOffset, self).convert_xunits(self._y)
+ pos = x, y
+ proj = trans.transform_point(pos)
+ proj = proj + numpy.array((xoffset, yoffset))
+ pos = invtrans.transform_point(proj)
+ self.__cache = pos
+ return pos
+
+ def convert_xunits(self, x):
+ """Return the pixel position of the annotated point."""
+ return self.__get_xy()[0]
+
+ def convert_yunits(self, y):
+ """Return the pixel position of the annotated point."""
+ return self.__get_xy()[1]
+
+
class _MarkerContainer(_PickableContainer):
"""Marker artists container supporting draw/remove and text position update
@@ -263,9 +318,10 @@ class _MarkerContainer(_PickableContainer):
:param y: Y coordinate of the marker (None for vertical lines)
"""
- def __init__(self, artists, x, y, yAxis):
+ def __init__(self, artists, symbol, x, y, yAxis):
self.line = artists[0]
self.text = artists[1] if len(artists) > 1 else None
+ self.symbol = symbol
self.x = x
self.y = y
self.yAxis = yAxis
@@ -278,27 +334,39 @@ class _MarkerContainer(_PickableContainer):
if self.text is not None:
self.text.draw(*args, **kwargs)
- def updateMarkerText(self, xmin, xmax, ymin, ymax):
+ def updateMarkerText(self, xmin, xmax, ymin, ymax, yinverted):
"""Update marker text position and visibility according to plot limits
:param xmin: X axis lower limit
:param xmax: X axis upper limit
:param ymin: Y axis lower limit
- :param ymax: Y axis upprt limit
+ :param ymax: Y axis upper limit
+ :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))
self.text.set_visible(visible)
- if self.x is not None and self.y is None: # vertical line
- delta = abs(ymax - ymin)
- if ymin > ymax:
- ymax = ymin
- ymax -= 0.005 * delta
- self.text.set_y(ymax)
-
- if self.x is None and self.y is not None: # Horizontal line
+ if self.x is not None and self.y is not None:
+ if self.symbol is None:
+ valign = 'baseline'
+ else:
+ if yinverted:
+ valign = 'bottom'
+ else:
+ valign = 'top'
+ self.text.set_verticalalignment(valign)
+
+ elif self.y is None: # vertical line
+ # Always display it on top
+ center = (ymax + ymin) * 0.5
+ pos = (ymax - ymin) * 0.5 * 0.99
+ if yinverted:
+ pos = -pos
+ self.text.set_y(center + pos)
+
+ elif self.x is None: # Horizontal line
delta = abs(xmax - xmin)
if xmin > xmax:
xmax = xmin
@@ -354,9 +422,35 @@ class _DoubleColoredLinePatch(matplotlib.patches.Patch):
class Image(AxesImage):
- """An AxesImage with a fast path for uint8 RGBA images"""
+ """An AxesImage with a fast path for uint8 RGBA images.
+
+ :param List[float] silx_origin: (ox, oy) Offset of the image.
+ :param List[float] silx_scale: (sx, sy) Scale of the image.
+ """
+
+ def __init__(self, *args,
+ silx_origin=(0., 0.),
+ silx_scale=(1., 1.),
+ **kwargs):
+ super().__init__(*args, **kwargs)
+ self.__silx_origin = silx_origin
+ self.__silx_scale = silx_scale
+
+ def contains(self, mouseevent):
+ """Overridden to fill 'ind' with row and column"""
+ inside, info = super().contains(mouseevent)
+ if inside:
+ x, y = mouseevent.xdata, mouseevent.ydata
+ ox, oy = self.__silx_origin
+ sx, sy = self.__silx_scale
+ 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,)
+ return inside, info
def set_data(self, A):
+ """Overridden to add a fast path for RGBA unit8 images"""
A = numpy.array(A, copy=False)
if A.ndim != 3 or A.shape[2] != 4 or A.dtype != numpy.uint8:
super(Image, self).set_data(A)
@@ -402,10 +496,15 @@ class BackendMatplotlib(BackendBase.BackendBase):
# disable the use of offsets
try:
- self.ax.get_yaxis().get_major_formatter().set_useOffset(False)
- self.ax.get_xaxis().get_major_formatter().set_useOffset(False)
- self.ax2.get_yaxis().get_major_formatter().set_useOffset(False)
- self.ax2.get_xaxis().get_major_formatter().set_useOffset(False)
+ 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__)
@@ -485,10 +584,10 @@ class BackendMatplotlib(BackendBase.BackendBase):
def addCurve(self, x, y,
color, symbol, linewidth, linestyle,
yaxis,
- xerror, yerror, z,
+ xerror, yerror,
fill, alpha, symbolsize, baseline):
for parameter in (x, y, color, symbol, linewidth, linestyle,
- yaxis, z, fill, alpha, symbolsize):
+ yaxis, fill, alpha, symbolsize):
assert parameter is not None
assert yaxis in ('left', 'right')
@@ -584,12 +683,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
return _PickableContainer(artists)
- def addImage(self, data, origin, scale, z, colormap, alpha):
+ def addImage(self, data, origin, scale, colormap, alpha):
# Non-uniform image
# http://wiki.scipy.org/Cookbook/Histograms
# Non-linear axes
# http://stackoverflow.com/questions/11488800/non-linear-axes-for-imshow-in-matplotlib
- for parameter in (data, origin, scale, z):
+ for parameter in (data, origin, scale):
assert parameter is not None
origin = float(origin[0]), float(origin[1])
@@ -600,7 +699,9 @@ class BackendMatplotlib(BackendBase.BackendBase):
image = Image(self.ax,
interpolation='nearest',
picker=True,
- origin='lower')
+ origin='lower',
+ silx_origin=origin,
+ silx_scale=scale)
if alpha < 1:
image.set_alpha(alpha)
@@ -627,13 +728,17 @@ class BackendMatplotlib(BackendBase.BackendBase):
if data.ndim == 2: # Data image, convert to RGBA image
data = colormap.applyToData(data)
-
+ elif data.dtype == numpy.uint16:
+ # 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
- def addTriangles(self, x, y, triangles, color, z, alpha):
- for parameter in (x, y, triangles, color, z, alpha):
+ def addTriangles(self, x, y, triangles, color, alpha):
+ for parameter in (x, y, triangles, color, alpha):
assert parameter is not None
color = numpy.array(color, copy=False)
@@ -651,8 +756,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
return collection
- def addItem(self, x, y, shape, color, fill, overlay, z,
- linestyle, linewidth, linebgcolor):
+ def addShape(self, x, y, shape, color, fill, overlay,
+ linestyle, linewidth, linebgcolor):
if (linebgcolor is not None and
shape not in ('rectangle', 'polygon', 'polylines')):
_logger.warning(
@@ -755,17 +860,11 @@ class BackendMatplotlib(BackendBase.BackendBase):
markersize=10.)[-1]
if text is not None:
- if symbol is None:
- valign = 'baseline'
- else:
- valign = 'top'
- text = " " + text
-
- textArtist = ax.text(x, y, text,
- color=color,
- horizontalalignment='left',
- verticalalignment=valign)
-
+ textArtist = _TextWithOffset(x, y, text,
+ color=color,
+ horizontalalignment='left')
+ if symbol is not None:
+ textArtist.pixel_offset = 10, 3
elif x is not None:
line = ax.axvline(x,
color=color,
@@ -773,11 +872,11 @@ class BackendMatplotlib(BackendBase.BackendBase):
linestyle=linestyle)
if text is not None:
# Y position will be updated in updateMarkerText call
- textArtist = ax.text(x, 1., " " + text,
- color=color,
- horizontalalignment='left',
- verticalalignment='top')
-
+ textArtist = _TextWithOffset(x, 1., text,
+ color=color,
+ horizontalalignment='left',
+ verticalalignment='top')
+ textArtist.pixel_offset = 5, 3
elif y is not None:
line = ax.axhline(y,
color=color,
@@ -786,11 +885,11 @@ class BackendMatplotlib(BackendBase.BackendBase):
if text is not None:
# X position will be updated in updateMarkerText call
- textArtist = ax.text(1., y, " " + text,
- color=color,
- horizontalalignment='right',
- verticalalignment='top')
-
+ textArtist = _TextWithOffset(1., y, text,
+ color=color,
+ horizontalalignment='right',
+ verticalalignment='top')
+ textArtist.pixel_offset = 5, 3
else:
raise RuntimeError('A marker must at least have one coordinate')
@@ -799,11 +898,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
# All markers are overlays
line.set_animated(True)
if textArtist is not None:
+ ax.add_artist(textArtist)
textArtist.set_animated(True)
artists = [line] if textArtist is None else [line, textArtist]
- container = _MarkerContainer(artists, x, y, yaxis)
- container.updateMarkerText(xmin, xmax, ymin, ymax)
+ container = _MarkerContainer(artists, symbol, x, y, yaxis)
+ container.updateMarkerText(xmin, xmax, ymin, ymax, self.isYAxisInverted())
return container
@@ -811,12 +911,13 @@ class BackendMatplotlib(BackendBase.BackendBase):
xmin, xmax = self.ax.get_xbound()
ymin1, ymax1 = self.ax.get_ybound()
ymin2, ymax2 = self.ax2.get_ybound()
+ yinverted = self.isYAxisInverted()
for item in self._overlayItems():
if isinstance(item, _MarkerContainer):
if item.yAxis == 'left':
- item.updateMarkerText(xmin, xmax, ymin1, ymax1)
+ item.updateMarkerText(xmin, xmax, ymin1, ymax1, yinverted)
else:
- item.updateMarkerText(xmin, xmax, ymin2, ymax2)
+ item.updateMarkerText(xmin, xmax, ymin2, ymax2, yinverted)
# Remove methods
@@ -1076,6 +1177,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
def setYAxisInverted(self, flag):
if self.ax.yaxis_inverted() != bool(flag):
self.ax.invert_yaxis()
+ self._updateMarkers()
def isYAxisInverted(self):
return self.ax.yaxis_inverted()
@@ -1143,15 +1245,15 @@ class BackendMatplotlib(BackendBase.BackendBase):
BackendBase.BackendBase.setAxesDisplayed(self, displayed)
if displayed:
# show axes and viewbox rect
- self.ax.set_axis_on()
- self.ax2.set_axis_on()
+ self.ax.set_frame_on(True)
+ self.ax2.set_frame_on(True)
# set the default margins
self.ax.set_position([.15, .15, .75, .75])
self.ax2.set_position([.15, .15, .75, .75])
else:
# hide axes and viewbox rect
- self.ax.set_axis_off()
- self.ax2.set_axis_off()
+ self.ax.set_frame_on(False)
+ self.ax2.set_frame_on(False)
# remove external margins
self.ax.set_position([0, 0, 1, 1])
self.ax2.set_position([0, 0, 1, 1])
@@ -1168,7 +1270,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
dataBackgroundColor = backgroundColor
- if self.ax.axison:
+ if self.ax.get_frame_on():
self.fig.patch.set_facecolor(backgroundColor)
if self._matplotlibVersion < _parse_version('2'):
self.ax.set_axis_bgcolor(dataBackgroundColor)
@@ -1187,7 +1289,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
gridColor = foregroundColor
for axes in (self.ax, self.ax2):
- if axes.axison:
+ if axes.get_frame_on():
axes.spines['bottom'].set_color(foregroundColor)
axes.spines['top'].set_color(foregroundColor)
axes.spines['right'].set_color(foregroundColor)
@@ -1244,11 +1346,6 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
self.mpl_connect('motion_notify_event', self._onMouseMove)
self.mpl_connect('scroll_event', self._onMouseWheel)
- def contextMenuEvent(self, event):
- """Override QWidget.contextMenuEvent to implement the context menu"""
- # Makes sure it is overridden (issue with PySide)
- BackendBase.BackendBase.contextMenuEvent(self, event)
-
def postRedisplay(self):
self._sigPostRedisplay.emit()
@@ -1265,17 +1362,22 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
def _onMouseMove(self, event):
if self._graphCursor:
+ position = self._plot.pixelToData(
+ event.x,
+ self._mplQtYAxisCoordConversion(event.y),
+ axis='left',
+ check=True)
lineh, linev = self._graphCursor
- if event.inaxes not in (self.ax, self.ax2) and lineh.get_visible():
- lineh.set_visible(False)
- linev.set_visible(False)
- self._plot._setDirtyPlot(overlayOnly=True)
- else:
+ if position is not None:
linev.set_visible(True)
- linev.set_xdata((event.xdata, event.xdata))
+ linev.set_xdata((position[0], position[0]))
lineh.set_visible(True)
- lineh.set_ydata((event.ydata, event.ydata))
+ 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)
# onMouseMove must trigger replot if dirty flag is raised
self._plot.onMouseMove(
@@ -1294,14 +1396,22 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
def leaveEvent(self, event):
"""QWidget event handler"""
- self._plot.onMouseLeaveWidget()
+ try:
+ plot = self._plot
+ except RuntimeError:
+ pass
+ else:
+ plot.onMouseLeaveWidget()
# picking
def pickItem(self, x, y, item):
mouseEvent = MouseEvent(
'button_press_event', self, x, self._mplQtYAxisCoordConversion(y))
+ # 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')
picked, info = item.contains(mouseEvent)
if not picked: