diff options
author | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2017-08-18 14:48:52 +0200 |
---|---|---|
committer | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2017-08-18 14:48:52 +0200 |
commit | f7bdc2acff3c13a6d632c28c4569690ab106eed7 (patch) | |
tree | 9d67cdb7152ee4e711379e03fe0546c7c3b97303 /silx/gui/plot/backends/BackendMatplotlib.py |
Import Upstream version 0.5.0+dfsg
Diffstat (limited to 'silx/gui/plot/backends/BackendMatplotlib.py')
-rw-r--r-- | silx/gui/plot/backends/BackendMatplotlib.py | 821 |
1 files changed, 821 insertions, 0 deletions
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py new file mode 100644 index 0000000..f9e60d5 --- /dev/null +++ b/silx/gui/plot/backends/BackendMatplotlib.py @@ -0,0 +1,821 @@ +# coding: utf-8 +# /*########################################################################## +# +# 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. +# +# ###########################################################################*/ +"""Matplotlib Plot backend.""" + +from __future__ import division + +__authors__ = ["V.A. Sole", "T. Vincent, H. Payno"] +__license__ = "MIT" +__date__ = "18/01/2017" + + +import logging + +import numpy + + +_logger = logging.getLogger(__name__) + + +from ... import qt + +from ._matplotlib import FigureCanvasQTAgg +import matplotlib +from matplotlib.container import Container +from matplotlib.figure import Figure +from matplotlib.patches import Rectangle, Polygon +from matplotlib.image import AxesImage +from matplotlib.backend_bases import MouseEvent +from matplotlib.lines import Line2D +from matplotlib.collections import PathCollection, LineCollection + +from .ModestImage import ModestImage +from . import BackendBase +from .. import Colors +from .._utils import FLOAT32_MINPOS + + +class BackendMatplotlib(BackendBase.BackendBase): + """Base class for Matplotlib backend without a FigureCanvas. + + For interactive on screen plot, see :class:`BackendMatplotlibQt`. + + See :class:`BackendBase.BackendBase` for public API documentation. + """ + + def __init__(self, plot, parent=None): + super(BackendMatplotlib, self).__init__(plot, parent) + + # matplotlib is handling keep aspect ratio at draw time + # When keep aspect ratio is on, and one changes the limits and + # ask them *before* next draw has been performed he will get the + # limits without applying keep aspect ratio. + # This attribute is used to ensure consistent values returned + # when getting the limits at the expense of a replot + self._dirtyLimits = True + + self.fig = Figure() + self.fig.set_facecolor("w") + + self.ax = self.fig.add_axes([.15, .15, .75, .75], label="left") + self.ax2 = self.ax.twinx() + self.ax2.set_label("right") + + # 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 matplotlib.__version__[0] < '2': + self.ax.set_axis_bgcolor('none') + else: + self.ax.set_facecolor('none') + self.fig.sca(self.ax) + + self._overlays = set() + self._background = None + + self._colormaps = {} + + self._graphCursor = tuple() + self.matplotlibVersion = matplotlib.__version__ + + self.setGraphXLimits(0., 100.) + self.setGraphYLimits(0., 100., axis='right') + self.setGraphYLimits(0., 100., axis='left') + + self._enableAxis('right', False) + + # Add methods + + def addCurve(self, x, y, legend, + 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): + assert parameter is not None + 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.float) / 255. + + if yaxis == "right": + axes = self.ax2 + self._enableAxis("right", True) + else: + axes = self.ax + + picker = 3 if selectable else None + + artists = [] # All the artists composing the curve + + # 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' + else: + errorbarColor = color + + # On Debian 7 at least, Nx1 array yerr does not seems supported + if (yerror is not None and yerror.ndim == 2 and + yerror.shape[1] == 1 and len(x) != 1): + yerror = numpy.ravel(yerror) + + errorbars = axes.errorbar(x, y, label=legend, + xerr=xerror, yerr=yerror, + linestyle=' ', color=errorbarColor) + artists += list(errorbars.get_children()) + + if hasattr(color, 'dtype') and len(color) == len(x): + # scatter plot + if color.dtype not in [numpy.float32, numpy.float]: + actualColor = color / 255. + 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, label=legend, + linestyle=linestyle, + color=actualColor[0], + linewidth=linewidth, + picker=picker, + marker=None) + artists += list(curveList) + + scatter = axes.scatter(x, y, + label=legend, + color=actualColor, + marker=symbol, + picker=picker, + s=symbolsize) + artists.append(scatter) + + if fill: + artists.append(axes.fill_between( + x, FLOAT32_MINPOS, y, facecolor=actualColor[0], linestyle='')) + + else: # Curve + curveList = axes.plot(x, y, + label=legend, + linestyle=linestyle, + color=color, + linewidth=linewidth, + marker=symbol, + picker=picker, + markersize=symbolsize) + artists += list(curveList) + + if fill: + artists.append( + axes.fill_between(x, FLOAT32_MINPOS, y, facecolor=color)) + + for artist in artists: + artist.set_zorder(z) + if alpha < 1: + artist.set_alpha(alpha) + + return Container(artists) + + def addImage(self, data, legend, + origin, scale, z, + selectable, draggable, + 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): + 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) + + # 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 (len(data.shape) == 2 and colormap['name'] is None and + 'colors' in colormap): + colors = numpy.array(colormap['colors'], copy=False) + 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['normalization'] == 'linear' and + not colormap['autoscale'] and + colormap['vmin'] == 0 and + colormap['vmax'] == 255 and + data.dtype == numpy.uint8): + # Supported case, convert data to RGBA + data = colors[data.reshape(-1)].reshape( + data.shape + (4,)) + else: + _logger.warning( + 'matplotlib %s does not support transparent ' + 'colormap.', matplotlib.__version__) + + if ((height * width) > 5.0e5 and + origin == (0., 0.) and scale == (1., 1.)): + imageClass = ModestImage + else: + imageClass = AxesImage + + # the normalization can be a source of time waste + # Two possibilities, we receive data or a ready to show image + if len(data.shape) == 3: # RGBA image + image = imageClass(self.ax, + label="__IMAGE__" + legend, + interpolation='nearest', + picker=picker, + zorder=z, + origin='lower') + + else: + # Convert colormap argument to matplotlib colormap + scalarMappable = Colors.getMPLScalarMappable(colormap, data) + + # try as data + image = imageClass(self.ax, + label="__IMAGE__" + legend, + interpolation='nearest', + cmap=scalarMappable.cmap, + picker=picker, + zorder=z, + norm=scalarMappable.norm, + origin='lower') + if alpha < 1: + image.set_alpha(alpha) + + # Set image extent + xmin = origin[0] + xmax = xmin + scale[0] * width + if scale[0] < 0.: + xmin, xmax = xmax, xmin + + ymin = origin[1] + ymax = ymin + scale[1] * height + if scale[1] < 0.: + ymin, ymax = ymax, ymin + + image.set_extent((xmin, xmax, ymin, ymax)) + + # Set image data + if scale[0] < 0. or scale[1] < 0.: + # For negative scale, step by -1 + xstep = 1 if scale[0] >= 0. else -1 + ystep = 1 if scale[1] >= 0. else -1 + data = data[::ystep, ::xstep] + + image.set_data(data) + + self.ax.add_artist(image) + + return image + + def addItem(self, x, y, legend, shape, color, fill, overlay, z): + xView = numpy.array(x, copy=False) + yView = numpy.array(y, copy=False) + + if shape == "line": + item = self.ax.plot(x, y, label=legend, color=color, + linestyle='-', marker=None)[0] + + elif shape == "hline": + if hasattr(y, "__len__"): + y = y[-1] + item = self.ax.axhline(y, label=legend, color=color) + + elif shape == "vline": + if hasattr(x, "__len__"): + x = x[-1] + item = self.ax.axvline(x, label=legend, color=color) + + 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) + if fill: + item.set_hatch('.') + + self.ax.add_patch(item) + + elif shape in ('polygon', 'polylines'): + xView = xView.reshape(1, -1) + yView = yView.reshape(1, -1) + item = Polygon(numpy.vstack((xView, yView)).T, + closed=(shape == 'polygon'), + fill=False, + label=legend, + color=color) + if fill and shape == 'polygon': + item.set_hatch('/') + + self.ax.add_patch(item) + + 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, constraint, overlay): + legend = "__MARKER__" + legend + + 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] + + if text is not None: + xtmp, ytmp = self.ax.transData.transform_point((x, y)) + inv = self.ax.transData.inverted() + xtmp, ytmp = inv.transform_point((xtmp, ytmp)) + + if symbol is None: + valign = 'baseline' + else: + valign = 'top' + text = " " + text + + line._infoText = self.ax.text(x, ytmp, text, + color=color, + horizontalalignment='left', + verticalalignment=valign) + + elif x is not None: + line = self.ax.axvline(x, label=legend, color=color) + if text is not None: + text = " " + text + ymin, ymax = self.getGraphYLimits(axis='left') + delta = abs(ymax - ymin) + if ymin > ymax: + ymax = ymin + ymax -= 0.005 * delta + line._infoText = self.ax.text(x, ymax, text, + color=color, + horizontalalignment='left', + verticalalignment='top') + + elif y is not None: + line = self.ax.axhline(y, label=legend, color=color) + + if text is not None: + text = " " + text + xmin, xmax = self.getGraphXLimits() + delta = abs(xmax - xmin) + if xmin > xmax: + xmax = xmin + xmax -= 0.005 * delta + line._infoText = self.ax.text(xmax, 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) + + if overlay: + line.set_animated(True) + self._overlays.add(line) + + return line + + # Remove methods + + def remove(self, item): + # Warning: It also needs to remove extra stuff if added as for markers + if hasattr(item, "_infoText"): # For markers text + item._infoText.remove() + item._infoText = None + self._overlays.discard(item) + item.remove() + + # Interaction methods + + 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) + lineh.set_animated(True) + + linev = self.ax.axvline( + self.ax.get_xbound()[0], visible=False, color=color, + linewidth=linewidth, linestyle=linestyle) + linev.set_animated(True) + + self._graphCursor = lineh, linev + else: + if self._graphCursor is not None: + lineh, linev = self._graphCursor + lineh.remove() + linev.remove() + self._graphCursor = tuple() + + # Active curve + + def setCurveColor(self, curve, color): + # Store Line2D and PathCollection + for artist in curve.get_children(): + if isinstance(artist, (Line2D, LineCollection)): + artist.set_color(color) + elif isinstance(artist, PathCollection): + artist.set_facecolors(color) + artist.set_edgecolors(color) + else: + _logger.warning( + 'setActiveCurve ignoring artist %s', str(artist)) + + # Misc. + + def getWidgetHandle(self): + return self.fig.canvas + + def _enableAxis(self, axis, flag=True): + """Show/hide Y axis + + :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 + axes.get_yaxis().set_visible(flag) + + def replot(self): + """Do not perform rendering. + + Override in subclass to actually draw something. + """ + # TODO images, markers? scatter plot? move in remove? + # Right Y axis only support curve for now + # Hide right Y axis if no line is present + self._dirtyLimits = False + if not self.ax2.lines: + self._enableAxis('right', False) + + def saveGraph(self, fileName, fileFormat, dpi): + # fileName can be also a StringIO or file instance + if dpi is not None: + self.fig.savefig(fileName, format=fileFormat, dpi=dpi) + else: + self.fig.savefig(fileName, format=fileFormat) + self._plot._setDirtyPlot() + + # Graph labels + + def setGraphTitle(self, title): + self.ax.set_title(title) + + def setGraphXLabel(self, label): + self.ax.set_xlabel(label) + + def setGraphYLabel(self, label, axis): + axes = self.ax if axis == 'left' else self.ax2 + axes.set_ylabel(label) + + # Graph limits + + def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None): + # Let matplotlib taking care of keep aspect ratio if any + self._dirtyLimits = True + self.ax.set_xlim(min(xmin, xmax), max(xmin, xmax)) + + if y2min is not None and y2max is not None: + if not self.isYAxisInverted(): + self.ax2.set_ylim(min(y2min, y2max), max(y2min, y2max)) + else: + self.ax2.set_ylim(max(y2min, y2max), min(y2min, y2max)) + + if not self.isYAxisInverted(): + self.ax.set_ylim(min(ymin, ymax), max(ymin, ymax)) + else: + self.ax.set_ylim(max(ymin, ymax), min(ymin, ymax)) + + def getGraphXLimits(self): + if self._dirtyLimits and self.isKeepDataAspectRatio(): + self.replot() # makes sure we get the right limits + return self.ax.get_xbound() + + def setGraphXLimits(self, xmin, xmax): + self._dirtyLimits = True + self.ax.set_xlim(min(xmin, xmax), max(xmin, xmax)) + + def getGraphYLimits(self, axis): + assert axis in ('left', 'right') + ax = self.ax2 if axis == 'right' else self.ax + + if not ax.get_visible(): + return None + + if self._dirtyLimits and self.isKeepDataAspectRatio(): + self.replot() # makes sure we get the right limits + + return ax.get_ybound() + + def setGraphYLimits(self, ymin, ymax, axis): + ax = self.ax2 if axis == 'right' else self.ax + if ymax < ymin: + ymin, ymax = ymax, ymin + self._dirtyLimits = True + + if self.isKeepDataAspectRatio(): + # matplotlib keeps limits of shared axis when keeping aspect ratio + # So x limits are kept when changing y limits.... + # Change x limits first by taking into account aspect ratio + # and then change y limits.. so matplotlib does not need + # to make change (to y) to keep aspect ratio + xmin, xmax = ax.get_xbound() + curYMin, curYMax = ax.get_ybound() + + newXRange = (xmax - xmin) * (ymax - ymin) / (curYMax - curYMin) + xcenter = 0.5 * (xmin + xmax) + ax.set_xlim(xcenter - 0.5 * newXRange, xcenter + 0.5 * newXRange) + + if not self.isYAxisInverted(): + ax.set_ylim(ymin, ymax) + else: + ax.set_ylim(ymax, ymin) + + # Graph axes + + def setXAxisLogarithmic(self, flag): + self.ax2.set_xscale('log' if flag else 'linear') + self.ax.set_xscale('log' if flag else 'linear') + + def setYAxisLogarithmic(self, flag): + self.ax2.set_yscale('log' if flag else 'linear') + self.ax.set_yscale('log' if flag else 'linear') + + def setYAxisInverted(self, flag): + if self.ax.yaxis_inverted() != bool(flag): + self.ax.invert_yaxis() + + def isYAxisInverted(self): + return self.ax.yaxis_inverted() + + def isKeepDataAspectRatio(self): + 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') + + def setGraphGrid(self, which): + self.ax.grid(False, which='both') # Disable all grid first + if which is not None: + self.ax.grid(True, which=which) + + # Data <-> Pixel coordinates conversion + + def dataToPixel(self, x, y, axis): + ax = self.ax2 if axis == "right" else self.ax + + pixels = ax.transData.transform_point((x, y)) + xPixel, yPixel = pixels.T + return xPixel, yPixel + + def pixelToData(self, x, y, axis, check): + ax = self.ax2 if axis == "right" else self.ax + + 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().transformed( + self.fig.dpi_scale_trans.inverted()) + dpi = self.fig.dpi + # Warning this is not returning int... + return (bbox.bounds[0] * dpi, bbox.bounds[1] * dpi, + bbox.bounds[2] * dpi, bbox.bounds[3] * dpi) + + +class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): + """QWidget matplotlib backend using a QtAgg canvas. + + It adds fast overlay drawing and mouse event management. + """ + + _sigPostRedisplay = qt.Signal() + """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) + + FigureCanvasQTAgg.setSizePolicy( + self, qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + FigureCanvasQTAgg.updateGeometry(self) + + # Make postRedisplay asynchronous using Qt signal + self._sigPostRedisplay.connect( + super(BackendMatplotlibQt, self).postRedisplay, + 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) + + def postRedisplay(self): + self._sigPostRedisplay.emit() + + # Mouse event forwarding + + _MPL_TO_PLOT_BUTTONS = {1: 'left', 2: 'middle', 3: 'right'} + + def _onMousePress(self, event): + self._plot.onMousePress( + event.x, event.y, self._MPL_TO_PLOT_BUTTONS[event.button]) + + def _onMouseMove(self, event): + if self._graphCursor: + lineh, linev = self._graphCursor + if event.inaxes != self.ax and lineh.get_visible(): + lineh.set_visible(False) + linev.set_visible(False) + self._plot._setDirtyPlot(overlayOnly=True) + else: + linev.set_visible(True) + linev.set_xdata((event.xdata, event.xdata)) + lineh.set_visible(True) + lineh.set_ydata((event.ydata, event.ydata)) + self._plot._setDirtyPlot(overlayOnly=True) + # onMouseMove must trigger replot if dirty flag is raised + + self._plot.onMouseMove(event.x, event.y) + + def _onMouseRelease(self, event): + self._plot.onMouseRelease( + event.x, event.y, self._MPL_TO_PLOT_BUTTONS[event.button]) + + def _onMouseWheel(self, event): + self._plot.onMouseWheel(event.x, event.y, event.step) + + def leaveEvent(self, event): + """QWidget event handler""" + self._plot.onMouseLeaveWidget() + + # 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 + + 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:]}) + + else: # it's a curve, item have no picker for now + if isinstance(event.artist, PathCollection): + data = event.artist.get_offsets()[event.ind, :] + xdata, ydata = data[:, 0], data[:, 1] + elif isinstance(event.artist, Line2D): + xdata = event.artist.get_xdata()[event.ind] + ydata = event.artist.get_ydata()[event.ind] + else: + _logger.info('Unsupported artist, ignored') + return + + self._picked.append({'kind': 'curve', 'legend': label, + 'xdata': xdata, 'ydata': ydata}) + + def pickItems(self, x, y): + self._picked = [] + + # Weird way to do an explicit picking: Simulate a button press event + mouseEvent = MouseEvent('button_press_event', self, x, y) + cid = self.mpl_connect('pick_event', self._onPick) + self.fig.pick(mouseEvent) + self.mpl_disconnect(cid) + picked = self._picked + self._picked = None + + return picked + + # replot control + + def resizeEvent(self, event): + self._insideResizeEventMethod = True + # Need to dirty the whole plot on resize. + self._plot._setDirtyPlot() + FigureCanvasQTAgg.resizeEvent(self, event) + self._insideResizeEventMethod = False + + def draw(self): + """Override canvas draw method to support faster draw of overlays.""" + if self._plot._getDirtyPlot(): # Need a full redraw + FigureCanvasQTAgg.draw(self) + self._background = None # Any saved background is dirty + + if (self._overlays or self._graphCursor or + self._plot._getDirtyPlot() == 'overlay'): + # There are overlays or crosshair, or they is just no more overlays + + # 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.restore_region(self._background) + + # 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) + + self.blit(self.fig.bbox) + + def replot(self): + BackendMatplotlib.replot(self) + self.draw() + + # cursor + + _QT_CURSORS = { + None: qt.Qt.ArrowCursor, + BackendBase.CURSOR_DEFAULT: qt.Qt.ArrowCursor, + BackendBase.CURSOR_POINTING: qt.Qt.PointingHandCursor, + BackendBase.CURSOR_SIZE_HOR: qt.Qt.SizeHorCursor, + BackendBase.CURSOR_SIZE_VER: qt.Qt.SizeVerCursor, + BackendBase.CURSOR_SIZE_ALL: qt.Qt.SizeAllCursor, + } + + def setGraphCursorShape(self, cursor): + cursor = self._QT_CURSORS[cursor] + + FigureCanvasQTAgg.setCursor(self, qt.QCursor(cursor)) |