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-x[-rw-r--r--]silx/gui/plot/backends/BackendMatplotlib.py519
1 files changed, 329 insertions, 190 deletions
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
index 7739329..2336494 100644..100755
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/silx/gui/plot/backends/BackendMatplotlib.py
@@ -56,12 +56,13 @@ from matplotlib.collections import PathCollection, LineCollection
from matplotlib.ticker import Formatter, ScalarFormatter, Locator
from matplotlib.tri import Triangulation
from matplotlib.collections import TriMesh
+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
-
_PATCH_LINESTYLE = {
"-": 'solid',
"--": 'dashed',
@@ -72,11 +73,54 @@ _PATCH_LINESTYLE = {
}
"""Patches do not uses the same matplotlib syntax"""
+_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,
+}
+
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.
+
+ :param str symbol: Symbol description used by silx
+ :rtype: Union[None,matplotlib.path.Path]
+ """
+ if symbol == u'\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]])
+ codes = [mpath.Path.CURVE4] * len(vertices)
+ codes[0] = mpath.Path.MOVETO
+ codes[-1] = mpath.Path.CLOSEPOLY
+ path = mpath.Path(vertices, codes)
+ _MARKER_PATHS[symbol] = path
+ return path
+ return None
class NiceDateLocator(Locator):
"""
@@ -162,7 +206,53 @@ class NiceAutoDateFormatter(Formatter):
return tickStr
-class _MarkerContainer(Container):
+class _PickableContainer(Container):
+ """Artists container with a :meth:`contains` method"""
+
+ def __init__(self, *args, **kwargs):
+ Container.__init__(self, *args, **kwargs)
+ self.__zorder = None
+
+ @property
+ def axes(self):
+ """Mimin Artist.axes"""
+ for child in self.get_children():
+ if hasattr(child, 'axes'):
+ return child.axes
+ return None
+
+ def draw(self, *args, **kwargs):
+ """artist-like draw to broadcast draw to children"""
+ for child in self.get_children():
+ child.draw(*args, **kwargs)
+
+ def get_zorder(self):
+ """Mimic Artist.get_zorder"""
+ return self.__zorder
+
+ def set_zorder(self, z):
+ """Mimic Artist.set_zorder to broadcast to children"""
+ if z != self.__zorder:
+ self.__zorder = z
+ for child in self.get_children():
+ child.set_zorder(z)
+
+ def contains(self, mouseevent):
+ """Mimic Artist.contains, and call it on all children.
+
+ :param mouseevent:
+ :return: Picking status and associated information as a dict
+ :rtype: (bool,dict)
+ """
+ # Goes through children from front to back and return first picked one.
+ for child in reversed(self.get_children()):
+ picked, info = child.contains(mouseevent)
+ if picked:
+ return picked, info
+ return False, {}
+
+
+class _MarkerContainer(_PickableContainer):
"""Marker artists container supporting draw/remove and text position update
:param artists:
@@ -173,13 +263,14 @@ class _MarkerContainer(Container):
:param y: Y coordinate of the marker (None for vertical lines)
"""
- def __init__(self, artists, x, y):
+ def __init__(self, artists, x, y, yAxis):
self.line = artists[0]
self.text = artists[1] if len(artists) > 1 else None
self.x = x
self.y = y
+ self.yAxis = yAxis
- Container.__init__(self, artists)
+ _PickableContainer.__init__(self, artists)
def draw(self, *args, **kwargs):
"""artist-like draw to broadcast draw to line and text"""
@@ -214,6 +305,15 @@ class _MarkerContainer(Container):
xmax -= 0.005 * delta
self.text.set_x(xmax)
+ def contains(self, mouseevent):
+ """Mimic Artist.contains, and call it on the line Artist.
+
+ :param mouseevent:
+ :return: Picking status and associated information as a dict
+ :rtype: (bool,dict)
+ """
+ return self.line.contains(mouseevent)
+
class _DoubleColoredLinePatch(matplotlib.patches.Patch):
"""Matplotlib patch to display any patch using double color."""
@@ -294,7 +394,11 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax2 = self.ax.twinx()
self.ax2.set_label("right")
# Make sure background of Axes is displayed
- self.ax2.patch.set_visible(True)
+ self.ax2.patch.set_visible(False)
+ self.ax.patch.set_visible(True)
+
+ # Set axis zorder=0.5 so grid is displayed at 0.5
+ self.ax.set_axisbelow(True)
# disable the use of offsets
try:
@@ -306,10 +410,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
_logger.warning('Cannot disabled axes offsets in %s '
% matplotlib.__version__)
- # critical for picking!!!!
- self.ax2.set_zorder(0)
self.ax2.set_autoscaley_on(True)
- self.ax.set_zorder(1)
+
# this works but the figure color is left
if self._matplotlibVersion < _parse_version('2'):
self.ax.set_axis_bgcolor('none')
@@ -317,7 +419,6 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax.set_facecolor('none')
self.fig.sca(self.ax)
- self._overlays = set()
self._background = None
self._colormaps = {}
@@ -327,15 +428,67 @@ class BackendMatplotlib(BackendBase.BackendBase):
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':
+ return 1
+ else:
+ return 0
+
+ return sorted(
+ 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):
+ yield item._backendRenderer
+
+ def _hasOverlays(self):
+ """Returns whether there is an overlay layer or not.
+
+ The overlay layers contains overlay items and the crosshair.
+
+ :rtype: bool
+ """
+ if self._graphCursor:
+ return True # There is the crosshair
+
+ for item in self._overlayItems():
+ return True # There is at least one overlay item
+ return False
+
# Add methods
- def addCurve(self, x, y, legend,
+ def _getMarkerFromSymbol(self, symbol):
+ """Returns a marker that can be displayed by matplotlib.
+
+ :param str symbol: A symbol description used by silx
+ :rtype: Union[str,int,matplotlib.path.Path]
+ """
+ path = get_path_from_symbol(symbol)
+ if path is not None:
+ return path
+ num = _SPECIAL_MARKERS.get(symbol, None)
+ if num is not None:
+ return num
+ # This symbol must be supported by matplotlib
+ return symbol
+
+ def addCurve(self, x, y,
color, symbol, linewidth, linestyle,
yaxis,
- xerror, yerror, z, selectable,
- fill, alpha, symbolsize):
- for parameter in (x, y, legend, color, symbol, linewidth, linestyle,
- yaxis, z, selectable, fill, alpha, symbolsize):
+ xerror, yerror, z,
+ fill, alpha, symbolsize, baseline):
+ for parameter in (x, y, color, symbol, linewidth, linestyle,
+ yaxis, z, fill, alpha, symbolsize):
assert parameter is not None
assert yaxis in ('left', 'right')
@@ -349,7 +502,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
axes = self.ax
- picker = 3 if selectable else None
+ picker = 3
artists = [] # All the artists composing the curve
@@ -368,7 +521,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
yerror.shape[1] == 1):
yerror = numpy.ravel(yerror)
- errorbars = axes.errorbar(x, y, label=legend,
+ errorbars = axes.errorbar(x, y,
xerr=xerror, yerr=yerror,
linestyle=' ', color=errorbarColor)
artists += list(errorbars.get_children())
@@ -383,7 +536,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
if linestyle not in ["", " ", None]:
# scatter plot with an actual line ...
# we need to assign a color ...
- curveList = axes.plot(x, y, label=legend,
+ curveList = axes.plot(x, y,
linestyle=linestyle,
color=actualColor[0],
linewidth=linewidth,
@@ -391,21 +544,24 @@ class BackendMatplotlib(BackendBase.BackendBase):
marker=None)
artists += list(curveList)
+ marker = self._getMarkerFromSymbol(symbol)
scatter = axes.scatter(x, y,
- label=legend,
color=actualColor,
- marker=symbol,
+ marker=marker,
picker=picker,
s=symbolsize**2)
artists.append(scatter)
if fill:
+ if baseline is None:
+ _baseline = FLOAT32_MINPOS
+ else:
+ _baseline = baseline
artists.append(axes.fill_between(
- x, FLOAT32_MINPOS, y, facecolor=actualColor[0], linestyle=''))
+ x, _baseline, y, facecolor=actualColor[0], linestyle=''))
else: # Curve
curveList = axes.plot(x, y,
- label=legend,
linestyle=linestyle,
color=color,
linewidth=linewidth,
@@ -415,40 +571,35 @@ class BackendMatplotlib(BackendBase.BackendBase):
artists += list(curveList)
if fill:
+ if baseline is None:
+ _baseline = FLOAT32_MINPOS
+ else:
+ _baseline = baseline
artists.append(
- axes.fill_between(x, FLOAT32_MINPOS, y, facecolor=color))
+ axes.fill_between(x, _baseline, y, facecolor=color))
for artist in artists:
- artist.set_zorder(z)
if alpha < 1:
artist.set_alpha(alpha)
- return Container(artists)
+ return _PickableContainer(artists)
- def addImage(self, data, legend,
- origin, scale, z,
- selectable, draggable,
- colormap, alpha):
+ def addImage(self, data, origin, scale, z, 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, legend, origin, scale, z,
- selectable, draggable):
+ for parameter in (data, origin, scale, z):
assert parameter is not None
origin = float(origin[0]), float(origin[1])
scale = float(scale[0]), float(scale[1])
height, width = data.shape[0:2]
- picker = (selectable or draggable)
-
# All image are shown as RGBA image
image = Image(self.ax,
- label="__IMAGE__" + legend,
interpolation='nearest',
- picker=picker,
- zorder=z,
+ picker=True,
origin='lower')
if alpha < 1:
@@ -481,15 +632,10 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax.add_artist(image)
return image
- def addTriangles(self, x, y, triangles, legend,
- color, z, selectable, alpha):
- for parameter in (x, y, triangles, legend, color,
- z, selectable, alpha):
+ def addTriangles(self, x, y, triangles, color, z, alpha):
+ for parameter in (x, y, triangles, color, z, alpha):
assert parameter is not None
- # 0 enables picking on filled triangle
- picker = 0 if selectable else None
-
color = numpy.array(color, copy=False)
assert color.ndim == 2 and len(color) == len(x)
@@ -498,16 +644,14 @@ class BackendMatplotlib(BackendBase.BackendBase):
collection = TriMesh(
Triangulation(x, y, triangles),
- label=legend,
alpha=alpha,
- picker=picker,
- zorder=z)
+ picker=0) # 0 enables picking on filled triangle
collection.set_color(color)
self.ax.add_collection(collection)
return collection
- def addItem(self, x, y, legend, shape, color, fill, overlay, z,
+ def addItem(self, x, y, shape, color, fill, overlay, z,
linestyle, linewidth, linebgcolor):
if (linebgcolor is not None and
shape not in ('rectangle', 'polygon', 'polylines')):
@@ -520,20 +664,20 @@ class BackendMatplotlib(BackendBase.BackendBase):
linestyle = normalize_linestyle(linestyle)
if shape == "line":
- item = self.ax.plot(x, y, label=legend, color=color,
+ 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, label=legend, color=color,
+ 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, label=legend, color=color,
+ item = self.ax.axvline(x, color=color,
linestyle=linestyle, linewidth=linewidth)
elif shape == 'rectangle':
@@ -568,7 +712,6 @@ class BackendMatplotlib(BackendBase.BackendBase):
item = Polygon(points,
closed=closed,
fill=False,
- label=legend,
color=color,
linestyle=linestyle,
linewidth=linewidth)
@@ -584,30 +727,32 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
raise NotImplementedError("Unsupported item shape %s" % shape)
- item.set_zorder(z)
-
if overlay:
item.set_animated(True)
- self._overlays.add(item)
return item
- def addMarker(self, x, y, legend, text, color,
- selectable, draggable,
- symbol, linestyle, linewidth, constraint):
- legend = "__MARKER__" + legend
-
+ def addMarker(self, x, y, text, color,
+ symbol, linestyle, linewidth, constraint, yaxis):
textArtist = None
xmin, xmax = self.getGraphXLimits()
- ymin, ymax = self.getGraphYLimits(axis='left')
+ ymin, ymax = self.getGraphYLimits(axis=yaxis)
+
+ if yaxis == 'left':
+ ax = self.ax
+ elif yaxis == 'right':
+ ax = self.ax2
+ else:
+ assert(False)
+ marker = self._getMarkerFromSymbol(symbol)
if x is not None and y is not None:
- line = self.ax.plot(x, y, label=legend,
- linestyle=" ",
- color=color,
- marker=symbol,
- markersize=10.)[-1]
+ line = ax.plot(x, y,
+ linestyle=" ",
+ color=color,
+ marker=marker,
+ markersize=10.)[-1]
if text is not None:
if symbol is None:
@@ -616,43 +761,40 @@ class BackendMatplotlib(BackendBase.BackendBase):
valign = 'top'
text = " " + text
- textArtist = self.ax.text(x, y, text,
- color=color,
- horizontalalignment='left',
- verticalalignment=valign)
+ textArtist = ax.text(x, y, text,
+ color=color,
+ horizontalalignment='left',
+ verticalalignment=valign)
elif x is not None:
- line = self.ax.axvline(x,
- label=legend,
- 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 = self.ax.text(x, 1., " " + text,
- color=color,
- horizontalalignment='left',
- verticalalignment='top')
+ textArtist = ax.text(x, 1., " " + text,
+ color=color,
+ horizontalalignment='left',
+ verticalalignment='top')
elif y is not None:
- line = self.ax.axhline(y,
- label=legend,
- 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 = self.ax.text(1., y, " " + text,
- color=color,
- horizontalalignment='right',
- verticalalignment='top')
+ textArtist = ax.text(1., y, " " + text,
+ color=color,
+ horizontalalignment='right',
+ verticalalignment='top')
else:
raise RuntimeError('A marker must at least have one coordinate')
- if selectable or draggable:
- line.set_picker(5)
+ line.set_picker(5)
# All markers are overlays
line.set_animated(True)
@@ -660,24 +802,25 @@ class BackendMatplotlib(BackendBase.BackendBase):
textArtist.set_animated(True)
artists = [line] if textArtist is None else [line, textArtist]
- container = _MarkerContainer(artists, x, y)
+ container = _MarkerContainer(artists, x, y, yaxis)
container.updateMarkerText(xmin, xmax, ymin, ymax)
- self._overlays.add(container)
return container
def _updateMarkers(self):
xmin, xmax = self.ax.get_xbound()
- ymin, ymax = self.ax.get_ybound()
- for item in self._overlays:
+ ymin1, ymax1 = self.ax.get_ybound()
+ ymin2, ymax2 = self.ax2.get_ybound()
+ for item in self._overlayItems():
if isinstance(item, _MarkerContainer):
- item.updateMarkerText(xmin, xmax, ymin, ymax)
+ if item.yAxis == 'left':
+ item.updateMarkerText(xmin, xmax, ymin1, ymax1)
+ else:
+ item.updateMarkerText(xmin, xmax, ymin2, ymax2)
# Remove methods
def remove(self, item):
- # Warning: It also needs to remove extra stuff if added as for markers
- self._overlays.discard(item)
try:
item.remove()
except ValueError:
@@ -699,7 +842,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
self._graphCursor = lineh, linev
else:
- if self._graphCursor is not None:
+ if self._graphCursor:
lineh, linev = self._graphCursor
lineh.remove()
linev.remove()
@@ -746,7 +889,37 @@ class BackendMatplotlib(BackendBase.BackendBase):
if not self.ax2.lines:
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())
+
+ for item in self.getItemsFromBackToFront(condition=condition):
+ if (isinstance(item, items.YAxisMixIn) and
+ item.getYAxis() == 'right'):
+ axes = self.ax2
+ else:
+ axes = self.ax
+ axes.draw_artist(item._backendRenderer)
+
+ for item in self._graphCursor:
+ self.ax.draw_artist(item)
+
+ 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)
+ count = len(items)
+ for index, item in enumerate(items):
+ zorder = 1. + index / count
+ if zorder != item._backendRenderer.get_zorder():
+ item._backendRenderer.set_zorder(zorder)
+
def saveGraph(self, fileName, fileFormat, dpi):
+ self.updateZOrder()
+
# fileName can be also a StringIO or file instance
if dpi is not None:
self.fig.savefig(fileName, format=fileFormat, dpi=dpi)
@@ -788,7 +961,9 @@ class BackendMatplotlib(BackendBase.BackendBase):
def getGraphXLimits(self):
if self._dirtyLimits and self.isKeepDataAspectRatio():
- self.replot() # makes sure we get the right limits
+ self.ax.apply_aspect()
+ self.ax2.apply_aspect()
+ self._dirtyLimits = False
return self.ax.get_xbound()
def setGraphXLimits(self, xmin, xmax):
@@ -804,7 +979,9 @@ class BackendMatplotlib(BackendBase.BackendBase):
return None
if self._dirtyLimits and self.isKeepDataAspectRatio():
- self.replot() # makes sure we get the right limits
+ self.ax.apply_aspect()
+ self.ax2.apply_aspect()
+ self._dirtyLimits = False
return ax.get_ybound()
@@ -917,13 +1094,16 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Data <-> Pixel coordinates conversion
- def _mplQtYAxisCoordConversion(self, y):
+ def _mplQtYAxisCoordConversion(self, y, asint=True):
"""Qt origin (top) to/from matplotlib origin (bottom) conversion.
+ :param y:
+ :param bool asint: True to cast to int, False to keep as float
+
:rtype: float
"""
- height = self.fig.get_window_extent().height
- return height - y
+ value = self.fig.get_window_extent().height - y
+ return int(value) if asint else value
def dataToPixel(self, x, y, axis):
ax = self.ax2 if axis == "right" else self.ax
@@ -932,35 +1112,27 @@ class BackendMatplotlib(BackendBase.BackendBase):
xPixel, yPixel = pixels.T
# Convert from matplotlib origin (bottom) to Qt origin (top)
- yPixel = self._mplQtYAxisCoordConversion(yPixel)
+ yPixel = self._mplQtYAxisCoordConversion(yPixel, asint=False)
return xPixel, yPixel
- def pixelToData(self, x, y, axis, check):
+ def pixelToData(self, x, y, axis):
ax = self.ax2 if axis == "right" else self.ax
# Convert from Qt origin (top) to matplotlib origin (bottom)
- y = self._mplQtYAxisCoordConversion(y)
+ y = self._mplQtYAxisCoordConversion(y, asint=False)
inv = ax.transData.inverted()
x, y = inv.transform_point((x, y))
-
- if check:
- xmin, xmax = self.getGraphXLimits()
- ymin, ymax = self.getGraphYLimits(axis=axis)
-
- if x > xmax or x < xmin or y > ymax or y < ymin:
- return None # (x, y) is out of plot area
-
return x, y
def getPlotBoundsInPixels(self):
bbox = self.ax.get_window_extent()
# Warning this is not returning int...
- return (bbox.xmin,
- self._mplQtYAxisCoordConversion(bbox.ymax),
- bbox.width,
- bbox.height)
+ return (int(bbox.xmin),
+ self._mplQtYAxisCoordConversion(bbox.ymax, asint=True),
+ int(bbox.width),
+ int(bbox.height))
def setAxesDisplayed(self, displayed):
"""Display or not the axes.
@@ -996,12 +1168,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
dataBackgroundColor = backgroundColor
- if self.ax2.axison:
+ if self.ax.axison:
self.fig.patch.set_facecolor(backgroundColor)
if self._matplotlibVersion < _parse_version('2'):
- self.ax2.set_axis_bgcolor(dataBackgroundColor)
+ self.ax.set_axis_bgcolor(dataBackgroundColor)
else:
- self.ax2.set_facecolor(dataBackgroundColor)
+ self.ax.set_facecolor(dataBackgroundColor)
else:
self.fig.patch.set_facecolor(dataBackgroundColor)
@@ -1033,6 +1205,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
line.set_color(gridColor)
# axes.grid().set_markeredgecolor(gridColor)
+ def setBackgroundColors(self, backgroundColor, dataBackgroundColor):
+ self._synchronizeBackgroundColors()
+
+ def setForegroundColors(self, foregroundColor, gridColor):
+ self._synchronizeForegroundColors()
+
class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
"""QWidget matplotlib backend using a QtAgg canvas.
@@ -1079,14 +1257,16 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
_MPL_TO_PLOT_BUTTONS = {1: 'left', 2: 'middle', 3: 'right'}
def _onMousePress(self, event):
- self._plot.onMousePress(
- event.x, self._mplQtYAxisCoordConversion(event.y),
- self._MPL_TO_PLOT_BUTTONS[event.button])
+ button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None)
+ if button is not None:
+ self._plot.onMousePress(
+ event.x, self._mplQtYAxisCoordConversion(event.y),
+ button)
def _onMouseMove(self, event):
if self._graphCursor:
lineh, linev = self._graphCursor
- if event.inaxes != self.ax and lineh.get_visible():
+ 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)
@@ -1102,9 +1282,11 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
event.x, self._mplQtYAxisCoordConversion(event.y))
def _onMouseRelease(self, event):
- self._plot.onMouseRelease(
- event.x, self._mplQtYAxisCoordConversion(event.y),
- self._MPL_TO_PLOT_BUTTONS[event.button])
+ button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None)
+ if button is not None:
+ self._plot.onMouseRelease(
+ event.x, self._mplQtYAxisCoordConversion(event.y),
+ button)
def _onMouseWheel(self, event):
self._plot.onMouseWheel(
@@ -1116,58 +1298,31 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
# picking
- def _onPick(self, event):
- # TODO not very nice and fragile, find a better way?
- # Make a selection according to kind
- if self._picked is None:
- _logger.error('Internal picking error')
- return
+ def pickItem(self, x, y, item):
+ mouseEvent = MouseEvent(
+ 'button_press_event', self, x, self._mplQtYAxisCoordConversion(y))
+ mouseEvent.inaxes = item.axes
+ picked, info = item.contains(mouseEvent)
- label = event.artist.get_label()
- if label.startswith('__MARKER__'):
- self._picked.append({'kind': 'marker', 'legend': label[10:]})
-
- elif label.startswith('__IMAGE__'):
- self._picked.append({'kind': 'image', 'legend': label[9:]})
+ if not picked:
+ return None
- elif isinstance(event.artist, TriMesh):
+ elif isinstance(item, TriMesh):
# Convert selected triangle to data point indices
- triangulation = event.artist._triangulation
- indices = triangulation.get_masked_triangles()[event.ind[0]]
+ triangulation = item._triangulation
+ 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.
- dists = ((triangulation.x[indices] - event.mouseevent.xdata) ** 2 +
- (triangulation.y[indices] - event.mouseevent.ydata) ** 2)
- indices = indices[numpy.flip(numpy.argsort(dists))]
+ 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)]
- self._picked.append({'kind': 'curve', 'legend': label,
- 'indices': indices})
-
- else: # it's a curve, item have no picker for now
- if not isinstance(event.artist, (PathCollection, Line2D)):
- _logger.info('Unsupported artist, ignored')
- return
-
- self._picked.append({'kind': 'curve', 'legend': label,
- 'indices': event.ind})
-
- def pickItems(self, x, y, kinds):
- self._picked = []
-
- # Weird way to do an explicit picking: Simulate a button press event
- mouseEvent = MouseEvent('button_press_event',
- self, x, self._mplQtYAxisCoordConversion(y))
- cid = self.mpl_connect('pick_event', self._onPick)
- self.fig.pick(mouseEvent)
- self.mpl_disconnect(cid)
-
- picked = [p for p in self._picked if p['kind'] in kinds]
- self._picked = None
-
- return picked
+ else: # Returns indices if any
+ return info.get('ind', ())
# replot control
@@ -1177,22 +1332,10 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
self.ax.get_xbound(), self.ax.get_ybound(), self.ax2.get_ybound())
FigureCanvasQTAgg.resizeEvent(self, event)
- if self.isKeepDataAspectRatio() or self._overlays or self._graphCursor:
+ if self.isKeepDataAspectRatio() or self._hasOverlays():
# 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):
"""Overload draw
@@ -1201,6 +1344,8 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
This is directly called by matplotlib for widget resize.
"""
+ self.updateZOrder()
+
# 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"):
@@ -1213,7 +1358,7 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
else:
FigureCanvasQTAgg.draw(self)
- if self._overlays or self._graphCursor:
+ if self._hasOverlays():
# Save background
self._background = self.copy_from_bbox(self.fig.bbox)
else:
@@ -1257,7 +1402,7 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
if (_parse_version('1.5') <= self._matplotlibVersion < _parse_version('2.1') and
not hasattr(self, '_firstReplot')):
self._firstReplot = False
- if self._overlays or self._graphCursor:
+ if self._hasOverlays():
qt.QTimer.singleShot(0, self.draw) # Request async draw
# cursor
@@ -1276,9 +1421,3 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
else:
cursor = self._QT_CURSORS[cursor]
FigureCanvasQTAgg.setCursor(self, qt.QCursor(cursor))
-
- def setBackgroundColors(self, backgroundColor, dataBackgroundColor):
- self._synchronizeBackgroundColors()
-
- def setForegroundColors(self, foregroundColor, gridColor):
- self._synchronizeForegroundColors()