From 2abdcea1ea2b17c3780de8490fd7e078472d08c3 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 15 Jun 2017 08:45:22 -0700 Subject: [PATCH 01/13] Qt5Cairo backend. --- lib/matplotlib/backends/backend_agg.py | 10 +-- lib/matplotlib/backends/backend_qt5.py | 76 +++++++++++++++-- lib/matplotlib/backends/backend_qt5agg.py | 84 ++----------------- lib/matplotlib/backends/backend_qt5cairo.py | 29 +++++++ lib/matplotlib/rcsetup.py | 2 +- .../tests/test_backends_interactive.py | 1 + 6 files changed, 109 insertions(+), 93 deletions(-) create mode 100644 lib/matplotlib/backends/backend_qt5cairo.py diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 1741e140ae0e..5ac4c29639e1 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -422,18 +422,18 @@ def draw(self): Draw the figure using the renderer """ self.renderer = self.get_renderer(cleared=True) - # acquire a lock on the shared font cache - RendererAgg.lock.acquire() - toolbar = self.toolbar try: if toolbar: toolbar.set_cursor(cursors.WAIT) - self.figure.draw(self.renderer) + with RendererAgg.lock: + self.figure.draw(self.renderer) + # A GUI class may be need to update a window using this draw, so + # don't forget to call the superclass. + super(FigureCanvasAgg, self).draw() finally: if toolbar: toolbar.set_cursor(toolbar._lastCursor) - RendererAgg.lock.release() def get_renderer(self, cleared=False): l, b, w, h = self.figure.bbox.bounds diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 6b5e8bbfb54f..093a4e5e476d 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -7,7 +7,7 @@ import re import signal import sys -from six import unichr +import traceback import matplotlib @@ -226,19 +226,16 @@ class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase): # QtCore.Qt.XButton2: None, } - def _update_figure_dpi(self): - dpi = self._dpi_ratio * self.figure._original_dpi - self.figure._set_dpi(dpi, forward=False) - @_allow_super_init def __init__(self, figure): _create_qApp() super(FigureCanvasQT, self).__init__(figure=figure) - figure._original_dpi = figure.dpi self.figure = figure + # We don't want to scale up the figure DPI more than once. + # Note, we don't handle a signal for changing DPI yet. + figure._original_dpi = figure.dpi self._update_figure_dpi() - self.resize(*self.get_width_height()) # In cases with mixed resolution displays, we need to be careful if the # dpi_ratio changes - in this case we need to resize the canvas # accordingly. We could watch for screenChanged events from Qt, but @@ -248,13 +245,23 @@ def __init__(self, figure): # needed. self._dpi_ratio_prev = None + self._draw_pending = False + self._is_drawing = False + self._draw_rect_callback = lambda painter: None + + self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) self.setMouseTracking(True) + self.resize(*self.get_width_height()) # Key auto-repeat enabled by default self._keyautorepeat = True palette = QtGui.QPalette(QtCore.Qt.white) self.setPalette(palette) + def _update_figure_dpi(self): + dpi = self._dpi_ratio * self.figure._original_dpi + self.figure._set_dpi(dpi, forward=False) + @property def _dpi_ratio(self): # Not available on Qt4 or some older Qt5. @@ -408,7 +415,7 @@ def _get_key(self, event): if event_key > MAX_UNICODE: return None - key = unichr(event_key) + key = six.unichr(event_key) # qt delivers capitalized letters. fix capitalization # note that capslock is ignored if 'shift' in mods: @@ -453,6 +460,59 @@ def stop_event_loop(self, event=None): if hasattr(self, "_event_loop"): self._event_loop.quit() + def draw(self): + """Render the figure, and queue a request for a Qt draw. + """ + # The Agg draw is done here; delaying causes problems with code that + # uses the result of the draw() to update plot elements. + if self._is_drawing: + return + self._is_drawing = True + try: + super(FigureCanvasQT, self).draw() + finally: + self._is_drawing = False + self.update() + + def draw_idle(self): + """Queue redraw of the Agg buffer and request Qt paintEvent. + """ + # The Agg draw needs to be handled by the same thread matplotlib + # modifies the scene graph from. Post Agg draw request to the + # current event loop in order to ensure thread affinity and to + # accumulate multiple draw requests from event handling. + # TODO: queued signal connection might be safer than singleShot + if not (self._draw_pending or self._is_drawing): + self._draw_pending = True + QtCore.QTimer.singleShot(0, self._draw_idle) + + def _draw_idle(self): + if self.height() < 0 or self.width() < 0: + self._draw_pending = False + return + try: + self.draw() + except Exception: + # Uncaught exceptions are fatal for PyQt5, so catch them instead. + traceback.print_exc() + finally: + self._draw_pending = False + + def drawRectangle(self, rect): + # Draw the zoom rectangle to the QPainter. _draw_rect_callback needs + # to be called at the end of paintEvent. + if rect is not None: + def _draw_rect_callback(painter): + pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio, + QtCore.Qt.DotLine) + painter.setPen(pen) + painter.drawRect(*(pt / self._dpi_ratio for pt in rect)) + else: + def _draw_rect_callback(painter): + return + self._draw_rect_callback = _draw_rect_callback + self.update() + class MainWindow(QtWidgets.QMainWindow): closing = QtCore.Signal() diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 777167132ad6..6944dff621c0 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -7,7 +7,6 @@ import six import ctypes -import traceback from matplotlib import cbook from matplotlib.transforms import Bbox @@ -19,32 +18,11 @@ from .qt_compat import QT_API -class FigureCanvasQTAggBase(FigureCanvasAgg): - """ - The canvas the figure renders into. Calls the draw and print fig - methods, creates the renderers, etc... - - Attributes - ---------- - figure : `matplotlib.figure.Figure` - A high-level Figure instance - - """ +class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT): def __init__(self, figure): - super(FigureCanvasQTAggBase, self).__init__(figure=figure) - self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) - self._agg_draw_pending = False - self._agg_is_drawing = False + super(FigureCanvasQTAgg, self).__init__(figure=figure) self._bbox_queue = [] - self._drawRect = None - - def drawRectangle(self, rect): - if rect is not None: - self._drawRect = [pt / self._dpi_ratio for pt in rect] - else: - self._drawRect = None - self.update() @property @cbook.deprecated("2.1") @@ -110,13 +88,7 @@ def paintEvent(self, e): if QT_API == 'PySide' and six.PY3: ctypes.c_long.from_address(id(buf)).value = 1 - # draw the zoom rectangle to the QPainter - if self._drawRect is not None: - pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio, - QtCore.Qt.DotLine) - painter.setPen(pen) - x, y, w, h = self._drawRect - painter.drawRect(x, y, w, h) + self._draw_rect_callback(painter) painter.end() @@ -130,42 +102,11 @@ def draw(self): self._agg_is_drawing = True try: - super(FigureCanvasQTAggBase, self).draw() + super(FigureCanvasQTAgg, self).draw() finally: self._agg_is_drawing = False self.update() - def draw_idle(self): - """Queue redraw of the Agg buffer and request Qt paintEvent. - """ - # The Agg draw needs to be handled by the same thread matplotlib - # modifies the scene graph from. Post Agg draw request to the - # current event loop in order to ensure thread affinity and to - # accumulate multiple draw requests from event handling. - # TODO: queued signal connection might be safer than singleShot - if not (self._agg_draw_pending or self._agg_is_drawing): - self._agg_draw_pending = True - QtCore.QTimer.singleShot(0, self.__draw_idle_agg) - - def __draw_idle_agg(self, *args): - # if nothing to do, bail - if not self._agg_draw_pending: - return - # we have now tried this function at least once, do not run - # again until re-armed. Doing this here rather than after - # protects against recursive calls triggered through self.draw - # The recursive call is via `repaintEvent` - self._agg_draw_pending = False - # if negative size, bail - if self.height() < 0 or self.width() < 0: - return - try: - # actually do the drawing - self.draw() - except Exception: - # Uncaught exceptions are fatal for PyQt5, so catch them instead. - traceback.print_exc() - def blit(self, bbox=None): """Blit the region in bbox. """ @@ -182,25 +123,10 @@ def blit(self, bbox=None): self.repaint(l, self.renderer.height / self._dpi_ratio - t, w, h) def print_figure(self, *args, **kwargs): - super(FigureCanvasQTAggBase, self).print_figure(*args, **kwargs) + super(FigureCanvasQTAgg, self).print_figure(*args, **kwargs) self.draw() -class FigureCanvasQTAgg(FigureCanvasQTAggBase, FigureCanvasQT): - """ - The canvas the figure renders into. Calls the draw and print fig - methods, creates the renderers, etc. - - Modified to import from Qt5 backend for new-style mouse events. - - Attributes - ---------- - figure : `matplotlib.figure.Figure` - A high-level Figure instance - - """ - - @_BackendQT5.export class _BackendQT5Agg(_BackendQT5): FigureCanvas = FigureCanvasQTAgg diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py new file mode 100644 index 000000000000..66bd645ae6e1 --- /dev/null +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -0,0 +1,29 @@ +from . import backend_cairo # Keep the RendererCairo class swappable. +from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT + + +class FigureCanvasQTCairo(FigureCanvasQT): + def __init__(self, figure): + super(FigureCanvasQTCairo, self).__init__(figure=figure) + self._renderer = backend_cairo.RendererCairo(self.figure.dpi) + + def paintEvent(self, event): + width = self.width() + height = self.height() + surface = backend_cairo.cairo.ImageSurface( + backend_cairo.cairo.FORMAT_ARGB32, width, height) + self._renderer.set_ctx_from_surface(surface) + # This should be really done by set_ctx_from_surface... + self._renderer.set_width_height(width, height) + self.figure.draw(self._renderer) + qimage = QtGui.QImage(surface.get_data(), width, height, + QtGui.QImage.Format_ARGB32_Premultiplied) + painter = QtGui.QPainter(self) + painter.drawImage(0, 0, qimage) + self._draw_rect_callback(painter) + painter.end() + + +@_BackendQT5.export +class _BackendQT5Cairo(_BackendQT5): + FigureCanvas = FigureCanvasQTCairo diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index eafc8d4eecf7..2533b3291302 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -39,7 +39,7 @@ # change for later versions. interactive_bk = ['GTK', 'GTKAgg', 'GTKCairo', 'MacOSX', - 'Qt4Agg', 'Qt5Agg', 'TkAgg', 'WX', 'WXAgg', + 'Qt4Agg', 'Qt5Agg', 'Qt5Cairo', 'TkAgg', 'WX', 'WXAgg', 'GTK3Cairo', 'GTK3Agg', 'WebAgg', 'nbAgg'] diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 07d5759bbe0c..ca174878ca13 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -21,6 +21,7 @@ def _get_testable_interactive_backends(): for deps, backend in [(["cairocffi", "pgi"], "gtk3agg"), (["cairocffi", "pgi"], "gtk3cairo"), (["PyQt5"], "qt5agg"), + (["cairocffi", "PyQt5"], "qt5cairo"), (["tkinter"], "tkagg"), (["wx"], "wxagg")]: reason = None From d4957ec0fcc8dcca4926cb3a04de2bcc0279feb2 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 18 Jun 2017 00:26:12 -0700 Subject: [PATCH 02/13] Qt4Cairo backend. --- lib/matplotlib/backends/backend_qt4cairo.py | 6 ++++++ lib/matplotlib/rcsetup.py | 17 +++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 lib/matplotlib/backends/backend_qt4cairo.py diff --git a/lib/matplotlib/backends/backend_qt4cairo.py b/lib/matplotlib/backends/backend_qt4cairo.py new file mode 100644 index 000000000000..f94851da382d --- /dev/null +++ b/lib/matplotlib/backends/backend_qt4cairo.py @@ -0,0 +1,6 @@ +from .backend_qt5cairo import _BackendQT5Cairo + + +@_BackendQT5Cairo.export +class _BackendQT4Cairo(_BackendQT5Cairo): + pass diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 2533b3291302..424b415e4aa3 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -29,20 +29,17 @@ from matplotlib.fontconfig_pattern import parse_fontconfig_pattern from matplotlib.colors import is_color_like - # Don't let the original cycler collide with our validating cycler from cycler import Cycler, cycler as ccycler -# interactive_bk = ['gtk', 'gtkagg', 'gtkcairo', 'qt4agg', -# 'tkagg', 'wx', 'wxagg', 'webagg'] -# The capitalized forms are needed for ipython at present; this may -# change for later versions. - -interactive_bk = ['GTK', 'GTKAgg', 'GTKCairo', 'MacOSX', - 'Qt4Agg', 'Qt5Agg', 'Qt5Cairo', 'TkAgg', 'WX', 'WXAgg', - 'GTK3Cairo', 'GTK3Agg', 'WebAgg', 'nbAgg'] - +interactive_bk = ['GTK', 'GTKAgg', 'GTKCairo', 'GTK3Agg', 'GTK3Cairo', + 'MacOSX', + 'nbAgg', + 'Qt4Agg', 'Qt4Cairo', 'Qt5Agg', 'Qt5Cairo', + 'TkAgg', + 'WebAgg', + 'WX', 'WXAgg'] non_interactive_bk = ['agg', 'cairo', 'gdk', 'pdf', 'pgf', 'ps', 'svg', 'template'] all_backends = interactive_bk + non_interactive_bk From d2f2bc0c61b18a2ad79d1060c5e5cf51d74bc2aa Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 18 Jun 2017 00:10:18 -0700 Subject: [PATCH 03/13] Update backends docs. --- doc/users/whats_new/qtcairo.rst | 5 ++ tutorials/introductory/usage.py | 129 ++++++++++++++++---------------- 2 files changed, 68 insertions(+), 66 deletions(-) create mode 100644 doc/users/whats_new/qtcairo.rst diff --git a/doc/users/whats_new/qtcairo.rst b/doc/users/whats_new/qtcairo.rst new file mode 100644 index 000000000000..8f6f62f630a3 --- /dev/null +++ b/doc/users/whats_new/qtcairo.rst @@ -0,0 +1,5 @@ +Cairo rendering for Qt canvases +------------------------------- + +The new ``Qt4Cairo`` and ``Qt5Cairo`` backends allow Qt canvases to use Cairo +rendering instead of Agg. diff --git a/tutorials/introductory/usage.py b/tutorials/introductory/usage.py index a6cade14fe2d..eddddde2cbb4 100644 --- a/tutorials/introductory/usage.py +++ b/tutorials/introductory/usage.py @@ -322,13 +322,18 @@ def my_plotter(ax, data1, data2, param_dict): # backend : WXAgg # use wxpython with antigrain (agg) rendering # # #. Setting the :envvar:`MPLBACKEND` environment -# variable, either for your current shell or for a single script:: +# variable, either for your current shell or for a single script. On Unix:: # -# > export MPLBACKEND="module://my_backend" +# > export MPLBACKEND=module://my_backend # > python simple_plot.py # # > MPLBACKEND="module://my_backend" python simple_plot.py # +# On Windows, only the former is possible:: +# +# > set MPLBACKEND=module://my_backend +# > python simple_plot.py +# # Setting this environment variable will override the ``backend`` parameter # in *any* ``matplotlibrc``, even if there is a ``matplotlibrc`` in your # current working directory. Therefore setting :envvar:`MPLBACKEND` @@ -360,19 +365,18 @@ def my_plotter(ax, data1, data2, param_dict): # methods given above. # # If, however, you want to write graphical user interfaces, or a web -# application server (:ref:`howto-webapp`), or need a better -# understanding of what is going on, read on. To make things a little -# more customizable for graphical user interfaces, matplotlib separates -# the concept of the renderer (the thing that actually does the drawing) -# from the canvas (the place where the drawing goes). The canonical -# renderer for user interfaces is ``Agg`` which uses the `Anti-Grain -# Geometry`_ C++ library to make a raster (pixel) image of the figure. -# All of the user interfaces except ``macosx`` can be used with -# agg rendering, e.g., -# ``WXAgg``, ``GTKAgg``, ``QT4Agg``, ``QT5Agg``, ``TkAgg``. In -# addition, some of the user interfaces support other rendering engines. -# For example, with GTK, you can also select GDK rendering (backend -# ``GTK`` deprecated in 2.0) or Cairo rendering (backend ``GTKCairo``). +# application server (:ref:`howto-webapp`), or need a better understanding of +# what is going on, read on. To make things a little more customizable for +# graphical user interfaces, matplotlib separates the concept of the renderer +# (the thing that actually does the drawing) from the canvas (the place where +# the drawing goes). The canonical renderer for user interfaces is ``Agg`` +# which uses the `Anti-Grain Geometry`_ C++ library to make a raster (pixel) +# image of the figure. All of the user interfaces except ``macosx`` can +# be used with Agg rendering, e.g., ``GTKAgg``, ``GTK3Agg``, ``Qt4Agg``, +# ``Qt5Agg``, ``TkAgg``, ``WxAgg``. In addition, some of the user interfaces +# support other rendering engines. For example, with GTK, GTK3, and QT5, +# you can also select Cairo rendering (backends ``GTKCairo``, ``GTK3Cairo``, +# ``Qt5Cairo``). # # For the rendering engines, one can also distinguish between `vector # `_ or `raster @@ -397,11 +401,10 @@ def my_plotter(ax, data1, data2, param_dict): # `Portable Document Format`_ # SVG :term:`svg` :term:`vector graphics` -- # `Scalable Vector Graphics`_ -# :term:`Cairo` :term:`png` :term:`vector graphics` -- -# :term:`ps` `Cairo graphics`_ -# :term:`pdf` +# :term:`Cairo` :term:`png` :term:`raster graphics` and +# :term:`ps` :term:`vector graphics` -- using the +# :term:`pdf` `Cairo graphics`_ library # :term:`svg` -# ... # ============= ============ ================================================ # # And here are the user interfaces and renderer combinations supported; @@ -409,53 +412,50 @@ def my_plotter(ax, data1, data2, param_dict): # and of using appropriate renderers from the table above to write to # a file: # -# ============ ================================================================ -# Backend Description -# ============ ================================================================ -# Qt5Agg Agg rendering in a :term:`Qt5` canvas (requires PyQt5_). This -# backend can be activated in IPython with ``%matplotlib qt5``. -# ipympl Agg rendering embedded in a Jupyter widget. (requires ipympl) -# This can be enabled in a Jupyter notebook with -# ``%matplotlib ipympl`` -# GTK3Agg Agg rendering to a :term:`GTK` 3.x canvas (requires PyGObject_ -# and pycairo_ or cairocffi_) -# This backend can be activated in IPython with -# ``%matplotlib gtk3``. -# macosx Agg rendering into a Cocoa canvas in OSX. -# This backend can be activated in IPython with -# ``%matplotlib osx``. -# TkAgg Agg rendering to a :term:`Tk` canvas (requires TkInter_). -# This backend can be activated in IPython with -# ``%matplotlib tk``. -# nbAgg Embed an interactive figure in a Jupyter classic notebook. This -# backend can be enabled in Jupyter notebooks via -# ``%matplotlib notebook``. -# WebAgg On ``show()`` will start a tornado server with an interactive -# figure. -# GTK3Cairo Cairo rendering to a :term:`GTK` 3.x canvas (requires PyGObject_ -# and pycairo_ or cairocffi_) -# Qt4Agg Agg rendering to a :term:`Qt4` canvas (requires PyQt4_ -# or ``pyside``). -# This backend can be activated in IPython with -# ``%matplotlib qt4``. -# GTKAgg Agg rendering to a :term:`GTK` 2.x canvas (requires PyGTK_ and -# pycairo_ or cairocffi_; Python2 only) -# This backend can be activated in IPython with -# ``%matplotlib gtk``. -# GTKCairo Cairo rendering to a :term:`GTK` 2.x canvas (requires PyGTK_ -# and pycairo_ or cairocffi_; Python2 only) -# WXAgg Agg rendering to a :term:`wxWidgets` canvas -# (requires wxPython_. v4.0 (in beta) is -# required for python3). -# This backend can be activated in IPython with -# ``%matplotlib wx``. -# ============ ================================================================ +# ========= ================================================================ +# Backend Description +# ========= ================================================================ +# Qt5Agg Agg rendering in a :term:`Qt5` canvas (requires PyQt5_). This +# backend can be activated in IPython with ``%matplotlib qt5``. +# ipympl Agg rendering embedded in a Jupyter widget. (requires ipympl). +# This backend can be enabled in a Jupyter notebook with +# ``%matplotlib ipympl``. +# GTK3Agg Agg rendering to a :term:`GTK` 3.x canvas (requires PyGObject_, +# and pycairo_ or cairocffi_). This backend can be activated in +# IPython with ``%matplotlib gtk3``. +# macosx Agg rendering into a Cocoa canvas in OSX. This backend can be +# activated in IPython with ``%matplotlib osx``. +# TkAgg Agg rendering to a :term:`Tk` canvas (requires TkInter_). This +# backend can be activated in IPython with ``%matplotlib tk``. +# nbAgg Embed an interactive figure in a Jupyter classic notebook. This +# backend can be enabled in Jupyter notebooks via +# ``%matplotlib notebook``. +# WebAgg On ``show()`` will start a tornado server with an interactive +# figure. +# Qt5Cairo Cairo rendering in a :term:`Qt5` canvas (requires PyQt5_ and +# cairocffi_). +# GTK3Cairo Cairo rendering to a :term:`GTK` 3.x canvas (requires PyGObject_, +# and pycairo_ or cairocffi_). +# Qt4Agg Agg rendering to a :term:`Qt4` canvas (requires PyQt4_ or +# ``pyside``). This backend can be activated in IPython with +# ``%matplotlib qt4``. +# Qt4Cairo Cairo rendering in a :term:`Qt5` canvas (requires PyQt5_ and +# cairocffi_). +# GTKAgg Agg rendering to a :term:`GTK` 2.x canvas (requires PyGTK_, and +# pycairo_ or cairocffi_; Python2 only). This backend can be +# activated in IPython with ``%matplotlib gtk``. +# GTKCairo Cairo rendering to a :term:`GTK` 2.x canvas (requires PyGTK_, +# and pycairo_ or cairocffi_; Python2 only). +# WXAgg Agg rendering to a :term:`wxWidgets` canvas (requires wxPython_; +# v4.0 (in beta) is required for Python3). This backend can be +# activated in IPython with ``%matplotlib wx``. +# ========= ================================================================ # # .. _`Anti-Grain Geometry`: http://antigrain.com/ # .. _Postscript: https://en.wikipedia.org/wiki/PostScript # .. _`Portable Document Format`: https://en.wikipedia.org/wiki/Portable_Document_Format # .. _`Scalable Vector Graphics`: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics -# .. _`Cairo graphics`: https://en.wikipedia.org/wiki/Cairo_(graphics) +# .. _`Cairo graphics`: https://wwW.cairographics.org # .. _`Gimp Drawing Kit`: https://en.wikipedia.org/wiki/GDK # .. _PyGTK: http://www.pygtk.org # .. _PyGObject: https://wiki.gnome.org/action/show/Projects/PyGObject @@ -489,11 +489,8 @@ def my_plotter(ax, data1, data2, param_dict): # GTK and Cairo # ------------- # -# Both `GTK2` and `GTK3` have implicit dependencies on PyCairo regardless of the -# specific Matplotlib backend used. Unfortunatly the latest release of PyCairo -# for Python3 does not implement the Python wrappers needed for the `GTK3Agg` -# backend. `Cairocffi` can be used as a replacement which implements the correct -# wrapper. +# Both `GTK2` and `GTK3` depend on a Cairo wrapper (PyCairo or cairocffi) even +# if the Agg renderer is used. On Python3, only cairocffi is supported. # # How do I select PyQt4 or PySide? # -------------------------------- From f884ffa47c51198cc038bcd5e8be1c7c5cfbe7da Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 18 Jun 2017 14:17:02 -0700 Subject: [PATCH 04/13] Update backends API docs. --- doc-requirements.txt | 1 + doc/api/backend_agg_api.rst | 8 ++++++++ doc/api/backend_cairo_api.rst | 8 ++++++++ doc/api/backend_gtk3agg_api.rst | 11 +++++++++++ doc/api/backend_gtk3cairo_api.rst | 11 +++++++++++ doc/api/backend_gtkcairo_api.rst | 11 +++++++++++ doc/api/backend_managers_api.rst | 2 +- doc/api/backend_mixed_api.rst | 1 + doc/api/backend_nbagg_api.rst | 8 ++++++++ doc/api/backend_pdf_api.rst | 1 + doc/api/backend_pgf_api.rst | 8 ++++++++ doc/api/backend_ps_api.rst | 8 ++++++++ doc/api/backend_qt4agg_api.rst | 1 - doc/api/backend_qt4cairo_api.rst | 8 ++++++++ doc/api/backend_qt5agg_api.rst | 1 - doc/api/backend_qt5cairo_api.rst | 8 ++++++++ doc/api/backend_svg_api.rst | 1 + doc/api/backend_tkagg_api.rst | 8 ++++++++ doc/api/backend_tools_api.rst | 2 +- doc/api/backend_webagg_api.rst | 12 ++++++++++++ doc/api/index_backend_api.rst | 19 ++++++++++++++----- lib/matplotlib/backends/backend_cairo.py | 21 +++++---------------- 22 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 doc/api/backend_agg_api.rst create mode 100644 doc/api/backend_cairo_api.rst create mode 100644 doc/api/backend_gtk3agg_api.rst create mode 100644 doc/api/backend_gtk3cairo_api.rst create mode 100644 doc/api/backend_gtkcairo_api.rst create mode 100644 doc/api/backend_nbagg_api.rst create mode 100644 doc/api/backend_pgf_api.rst create mode 100644 doc/api/backend_ps_api.rst create mode 100644 doc/api/backend_qt4cairo_api.rst create mode 100644 doc/api/backend_qt5cairo_api.rst create mode 100644 doc/api/backend_tkagg_api.rst create mode 100644 doc/api/backend_webagg_api.rst diff --git a/doc-requirements.txt b/doc-requirements.txt index 6bae185bf850..8f5c6ef41845 100644 --- a/doc-requirements.txt +++ b/doc-requirements.txt @@ -9,6 +9,7 @@ sphinx>=1.3,!=1.5.0,!=1.6.4 colorspacious ipython +ipywidgets mock numpydoc>=0.4 pillow diff --git a/doc/api/backend_agg_api.rst b/doc/api/backend_agg_api.rst new file mode 100644 index 000000000000..40c8cd4bce6a --- /dev/null +++ b/doc/api/backend_agg_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_agg` +====================================== + +.. automodule:: matplotlib.backends.backend_agg + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_cairo_api.rst b/doc/api/backend_cairo_api.rst new file mode 100644 index 000000000000..2623270c6781 --- /dev/null +++ b/doc/api/backend_cairo_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_cairo` +======================================== + +.. automodule:: matplotlib.backends.backend_cairo + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_gtk3agg_api.rst b/doc/api/backend_gtk3agg_api.rst new file mode 100644 index 000000000000..b05498dee7d7 --- /dev/null +++ b/doc/api/backend_gtk3agg_api.rst @@ -0,0 +1,11 @@ + +:mod:`matplotlib.backends.backend_gtk3agg` +========================================== + +**TODO** We'll add this later, importing the gtk3 backends requires an active +X-session, which is not compatible with cron jobs. + +.. .. automodule:: matplotlib.backends.backend_gtk3agg +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/backend_gtk3cairo_api.rst b/doc/api/backend_gtk3cairo_api.rst new file mode 100644 index 000000000000..b805af75e757 --- /dev/null +++ b/doc/api/backend_gtk3cairo_api.rst @@ -0,0 +1,11 @@ + +:mod:`matplotlib.backends.backend_gtk3cairo` +============================================ + +**TODO** We'll add this later, importing the gtk3 backends requires an active +X-session, which is not compatible with cron jobs. + +.. .. automodule:: matplotlib.backends.backend_gtk3cairo +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/backend_gtkcairo_api.rst b/doc/api/backend_gtkcairo_api.rst new file mode 100644 index 000000000000..562f8ea6e7ce --- /dev/null +++ b/doc/api/backend_gtkcairo_api.rst @@ -0,0 +1,11 @@ + +:mod:`matplotlib.backends.backend_gtkcairo` +=========================================== + +**TODO** We'll add this later, importing the gtk backends requires an active +X-session, which is not compatible with cron jobs. + +.. .. automodule:: matplotlib.backends.backend_gtkcairo +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/backend_managers_api.rst b/doc/api/backend_managers_api.rst index 86d1c383b966..faf4eda18de3 100644 --- a/doc/api/backend_managers_api.rst +++ b/doc/api/backend_managers_api.rst @@ -1,6 +1,6 @@ :mod:`matplotlib.backend_managers` -=================================== +================================== .. automodule:: matplotlib.backend_managers :members: diff --git a/doc/api/backend_mixed_api.rst b/doc/api/backend_mixed_api.rst index 9c55e4abaa7f..7457f6684f94 100644 --- a/doc/api/backend_mixed_api.rst +++ b/doc/api/backend_mixed_api.rst @@ -4,4 +4,5 @@ .. automodule:: matplotlib.backends.backend_mixed :members: + :undoc-members: :show-inheritance: diff --git a/doc/api/backend_nbagg_api.rst b/doc/api/backend_nbagg_api.rst new file mode 100644 index 000000000000..977eabce8db0 --- /dev/null +++ b/doc/api/backend_nbagg_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_nbagg` +======================================== + +.. automodule:: matplotlib.backends.backend_nbagg + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_pdf_api.rst b/doc/api/backend_pdf_api.rst index 115863d61875..ded143ddcf8d 100644 --- a/doc/api/backend_pdf_api.rst +++ b/doc/api/backend_pdf_api.rst @@ -4,4 +4,5 @@ .. automodule:: matplotlib.backends.backend_pdf :members: + :undoc-members: :show-inheritance: diff --git a/doc/api/backend_pgf_api.rst b/doc/api/backend_pgf_api.rst new file mode 100644 index 000000000000..ec7440080eb0 --- /dev/null +++ b/doc/api/backend_pgf_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_pgf` +====================================== + +.. automodule:: matplotlib.backends.backend_pgf + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_ps_api.rst b/doc/api/backend_ps_api.rst new file mode 100644 index 000000000000..9d585be7a0ad --- /dev/null +++ b/doc/api/backend_ps_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_ps` +===================================== + +.. automodule:: matplotlib.backends.backend_ps + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_qt4agg_api.rst b/doc/api/backend_qt4agg_api.rst index 2e2e852612c7..8bf490aa8cb9 100644 --- a/doc/api/backend_qt4agg_api.rst +++ b/doc/api/backend_qt4agg_api.rst @@ -6,4 +6,3 @@ :members: :undoc-members: :show-inheritance: - diff --git a/doc/api/backend_qt4cairo_api.rst b/doc/api/backend_qt4cairo_api.rst new file mode 100644 index 000000000000..590465d7fbc0 --- /dev/null +++ b/doc/api/backend_qt4cairo_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_qt4cairo` +=========================================== + +.. automodule:: matplotlib.backends.backend_qt4cairo + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_qt5agg_api.rst b/doc/api/backend_qt5agg_api.rst index 58e5353a32a9..8d1ad2aba0f0 100644 --- a/doc/api/backend_qt5agg_api.rst +++ b/doc/api/backend_qt5agg_api.rst @@ -6,4 +6,3 @@ :members: :undoc-members: :show-inheritance: - diff --git a/doc/api/backend_qt5cairo_api.rst b/doc/api/backend_qt5cairo_api.rst new file mode 100644 index 000000000000..73df7ac128a1 --- /dev/null +++ b/doc/api/backend_qt5cairo_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_qt5cairo` +=========================================== + +.. automodule:: matplotlib.backends.backend_qt5cairo + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_svg_api.rst b/doc/api/backend_svg_api.rst index 399042482ea8..0b26d11e8818 100644 --- a/doc/api/backend_svg_api.rst +++ b/doc/api/backend_svg_api.rst @@ -4,4 +4,5 @@ .. automodule:: matplotlib.backends.backend_svg :members: + :undoc-members: :show-inheritance: diff --git a/doc/api/backend_tkagg_api.rst b/doc/api/backend_tkagg_api.rst new file mode 100644 index 000000000000..2a55bfe5c693 --- /dev/null +++ b/doc/api/backend_tkagg_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backends.backend_tkagg` +======================================== + +.. automodule:: matplotlib.backends.backend_tkagg + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_tools_api.rst b/doc/api/backend_tools_api.rst index 32babd5844b0..7e3d5619cc35 100644 --- a/doc/api/backend_tools_api.rst +++ b/doc/api/backend_tools_api.rst @@ -1,6 +1,6 @@ :mod:`matplotlib.backend_tools` -================================ +=============================== .. automodule:: matplotlib.backend_tools :members: diff --git a/doc/api/backend_webagg_api.rst b/doc/api/backend_webagg_api.rst new file mode 100644 index 000000000000..50070c3fcb68 --- /dev/null +++ b/doc/api/backend_webagg_api.rst @@ -0,0 +1,12 @@ + +:mod:`matplotlib.backends.backend_webagg` +========================================= + +.. note:: + The WebAgg backend is not documented here, in order to avoid adding Tornado + to the doc build requirements. + +.. .. automodule:: matplotlib.backends.backend_webagg +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/index_backend_api.rst b/doc/api/index_backend_api.rst index 8588628c7c01..813c3770214e 100644 --- a/doc/api/index_backend_api.rst +++ b/doc/api/index_backend_api.rst @@ -8,12 +8,21 @@ backends backend_managers_api.rst backend_mixed_api.rst backend_tools_api.rst + backend_agg_api.rst + backend_cairo_api.rst backend_gtkagg_api.rst + backend_gtkcairo_api.rst + backend_gtk3agg_api.rst + backend_gtk3cairo_api.rst + backend_nbagg_api.rst + backend_pdf_api.rst + backend_pgf_api.rst + backend_ps_api.rst backend_qt4agg_api.rst + backend_qt4cairo_api.rst backend_qt5agg_api.rst - backend_wxagg_api.rst - backend_pdf_api.rst + backend_qt5cairo_api.rst backend_svg_api.rst -.. backend_webagg.rst - dviread.rst - type1font.rst + backend_tkagg_api.rst + backend_webagg_api.rst + backend_wxagg_api.rst diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 0c4c89ae2c4f..39311a439330 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -1,21 +1,10 @@ """ A Cairo backend for matplotlib -Author: Steve Chaplin - -Cairo is a vector graphics library with cross-device output support. -Features of Cairo: - * anti-aliasing - * alpha channel - * saves image files as PNG, PostScript, PDF - -http://cairographics.org -Requires (in order, all available from Cairo website): - cairo, pycairo - -Naming Conventions - * classes MixedUpperCase - * varables lowerUpper - * functions underscore_separated +============================== +:Author: Steve Chaplin and others + +This backend depends on `cairo `_, and either on +cairocffi, or (Python 2 only) on pycairo. """ from __future__ import (absolute_import, division, print_function, From bcb08e93a7a82441d58bc2ed3035a1f4a3db6558 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 19 Jun 2017 00:47:52 -0700 Subject: [PATCH 05/13] Let cairo track surface size; fix Pyside refcnt bug. --- lib/matplotlib/backends/backend_cairo.py | 11 +---------- lib/matplotlib/backends/backend_qt5agg.py | 8 ++++---- lib/matplotlib/backends/backend_qt5cairo.py | 10 +++++++--- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 39311a439330..b10d4e513d6d 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -102,14 +102,11 @@ def __init__(self, dpi): def set_ctx_from_surface(self, surface): self.gc.ctx = cairo.Context(surface) + self.set_width_height(surface.get_width(), surface.get_height()) def set_width_height(self, width, height): self.width = width self.height = height - self.matrix_flipy = cairo.Matrix(yy=-1, y0=self.height) - # use matrix_flipy for ALL rendering? - # - problem with text? - will need to switch matrix_flipy off, or do a - # font transform? def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides): if fill_c is not None: @@ -303,11 +300,6 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx.restore() - def flipy(self): - return True - #return False # tried - all draw objects ok except text (and images?) - # which comes out mirrored! - def get_canvas_width_height(self): return self.width, self.height @@ -503,7 +495,6 @@ def _save(self, fo, fmt, **kwargs): # surface.set_dpi() can be used renderer = RendererCairo(self.figure.dpi) - renderer.set_width_height(width_in_points, height_in_points) renderer.set_ctx_from_surface(surface) ctx = renderer.gc.ctx diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 6944dff621c0..afa1f13c7f67 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -78,15 +78,15 @@ def paintEvent(self, e): reg = self.copy_from_bbox(bbox) buf = reg.to_string_argb() qimage = QtGui.QImage(buf, w, h, QtGui.QImage.Format_ARGB32) + # Adjust the buf reference count to work around a memory leak bug + # in QImage under PySide on Python 3. + if QT_API == 'PySide' and six.PY3: + ctypes.c_long.from_address(id(buf)).value = 1 if hasattr(qimage, 'setDevicePixelRatio'): # Not available on Qt4 or some older Qt5. qimage.setDevicePixelRatio(self._dpi_ratio) origin = QtCore.QPoint(l, self.renderer.height - t) painter.drawImage(origin / self._dpi_ratio, qimage) - # Adjust the buf reference count to work around a memory - # leak bug in QImage under PySide on Python 3. - if QT_API == 'PySide' and six.PY3: - ctypes.c_long.from_address(id(buf)).value = 1 self._draw_rect_callback(painter) diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py index 66bd645ae6e1..746172bca5bf 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -1,5 +1,6 @@ from . import backend_cairo # Keep the RendererCairo class swappable. from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT +from .qt_compat import QT_API class FigureCanvasQTCairo(FigureCanvasQT): @@ -13,11 +14,14 @@ def paintEvent(self, event): surface = backend_cairo.cairo.ImageSurface( backend_cairo.cairo.FORMAT_ARGB32, width, height) self._renderer.set_ctx_from_surface(surface) - # This should be really done by set_ctx_from_surface... - self._renderer.set_width_height(width, height) self.figure.draw(self._renderer) - qimage = QtGui.QImage(surface.get_data(), width, height, + buf = surface.get_data() + qimage = QtGui.QImage(buf, width, height, QtGui.QImage.Format_ARGB32_Premultiplied) + # Adjust the buf reference count to work around a memory leak bug in + # QImage under PySide on Python 3. + if QT_API == 'PySide' and six.PY3: + ctypes.c_long.from_address(id(buf)).value = 1 painter = QtGui.QPainter(self) painter.drawImage(0, 0, qimage) self._draw_rect_callback(painter) From 2eac3fad6c3eaff209e46fc368b5c988e2aa21fa Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 19 Jun 2017 20:05:29 -0700 Subject: [PATCH 06/13] Fix mandelbrot example. (Apparently the version parsing can sometimes fail on Travis.) --- examples/showcase/mandelbrot.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/showcase/mandelbrot.py b/examples/showcase/mandelbrot.py index ed43a087b18f..e8a4366afacd 100644 --- a/examples/showcase/mandelbrot.py +++ b/examples/showcase/mandelbrot.py @@ -67,10 +67,9 @@ def mandelbrot_set(xmin, xmax, ymin, ymax, xn, yn, maxiter, horizon=2.0): # Some advertisement for matplotlib year = time.strftime("%Y") - major, minor, micro = matplotlib.__version__.split('.', 2) text = ("The Mandelbrot fractal set\n" - "Rendered with matplotlib %s.%s, %s - http://matplotlib.org" - % (major, minor, year)) + "Rendered with matplotlib %s, %s - http://matplotlib.org" + % (matplotlib.__version__, year)) ax.text(xmin+.025, ymin+.025, text, color="white", fontsize=12, alpha=0.5) plt.show() From 138ba5850d184a0cd080bb6c8bc2cf1a2f9d0516 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 20 Jun 2017 04:41:47 -0700 Subject: [PATCH 07/13] Make sure only one cairo binding is ever used; fix docs. --- lib/matplotlib/backends/backend_cairo.py | 38 +++++++++++++----------- tutorials/introductory/usage.py | 10 +++---- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index b10d4e513d6d..dc1a0e059541 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -18,26 +18,30 @@ import numpy as np -try: - import cairocffi as cairo -except ImportError: +# In order to make it possible to pick the binding, use whichever has already +# been imported, if any. (The intermediate call to iter is just to placate +# Python2.) +cairo = next( + (mod for mod in ( + sys.modules.get(name) for name in ["cairocffi", "cairo"]) if mod), + None) +if cairo is None: try: - import cairo + import cairocffi as cairo except ImportError: - raise ImportError("Cairo backend requires that cairocffi or pycairo " - "is installed.") - else: - HAS_CAIRO_CFFI = False -else: - HAS_CAIRO_CFFI = True - -_version_required = (1, 2, 0) -if cairo.version_info < _version_required: - raise ImportError("Pycairo %d.%d.%d is installed\n" - "Pycairo %d.%d.%d or later is required" - % (cairo.version_info + _version_required)) + try: + import cairo + except ImportError: + raise ImportError( + "The cairo backend requires cairocffi or pycairo") +# cairocffi can install itself as cairo (`install_as_pycairo`) -- don't get +# fooled! +HAS_CAIRO_CFFI = cairo.__name__ == "cairocffi" + +if cairo.version_info < (1, 2, 0): + raise ImportError("cairo {} is installed; " + "cairo>=1.2.0 is required".format(cairo.version)) backend_version = cairo.version -del _version_required from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, diff --git a/tutorials/introductory/usage.py b/tutorials/introductory/usage.py index eddddde2cbb4..4ec9c6a2b4d6 100644 --- a/tutorials/introductory/usage.py +++ b/tutorials/introductory/usage.py @@ -432,15 +432,15 @@ def my_plotter(ax, data1, data2, param_dict): # ``%matplotlib notebook``. # WebAgg On ``show()`` will start a tornado server with an interactive # figure. -# Qt5Cairo Cairo rendering in a :term:`Qt5` canvas (requires PyQt5_ and -# cairocffi_). +# Qt5Cairo Cairo rendering in a :term:`Qt5` canvas (requires PyQt5_, and +# pycairo_ or cairocffi_). # GTK3Cairo Cairo rendering to a :term:`GTK` 3.x canvas (requires PyGObject_, # and pycairo_ or cairocffi_). # Qt4Agg Agg rendering to a :term:`Qt4` canvas (requires PyQt4_ or # ``pyside``). This backend can be activated in IPython with # ``%matplotlib qt4``. -# Qt4Cairo Cairo rendering in a :term:`Qt5` canvas (requires PyQt5_ and -# cairocffi_). +# Qt4Cairo Cairo rendering in a :term:`Qt4` canvas (requires PyQt4_, and +# pycairo_ or cairocffi_). # GTKAgg Agg rendering to a :term:`GTK` 2.x canvas (requires PyGTK_, and # pycairo_ or cairocffi_; Python2 only). This backend can be # activated in IPython with ``%matplotlib gtk``. @@ -448,7 +448,7 @@ def my_plotter(ax, data1, data2, param_dict): # and pycairo_ or cairocffi_; Python2 only). # WXAgg Agg rendering to a :term:`wxWidgets` canvas (requires wxPython_; # v4.0 (in beta) is required for Python3). This backend can be -# activated in IPython with ``%matplotlib wx``. +# activated in IPython with ``%matplotlib wx``.# # ========= ================================================================ # # .. _`Anti-Grain Geometry`: http://antigrain.com/ From d96d1048cdf24357a2a599f23b635e317e5e47a5 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 22 Jul 2017 23:25:04 -0700 Subject: [PATCH 08/13] No need to process_updates in gtk3. gdk_window_process_updates is deprecated since GDK 3.22. Agg animations run fine without it, and removing it makes mpl_cairo animations faster. Being drawable is synonym to being mapped and visible. --- lib/matplotlib/backends/backend_gtk3.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 070056c090e1..f7df37f4dbf9 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -275,11 +275,8 @@ def on_draw_event(self, widget, ctx): pass def draw(self): - if self.get_visible() and self.get_mapped(): + if self.is_drawable(): self.queue_draw() - # do a synchronous draw (its less efficient than an async draw, - # but is required if/when animation is used) - self.get_property("window").process_updates (False) def draw_idle(self): if self._idle_draw_id != 0: From 5087c055fea96dead4ab1c6c8e647def16828fbb Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 1 Aug 2017 16:53:23 -0700 Subject: [PATCH 09/13] Qt dpi handling should be renderer-agnostic. --- lib/matplotlib/backends/backend_qt5.py | 21 ++++++++++++++++ lib/matplotlib/backends/backend_qt5agg.py | 27 ++++----------------- lib/matplotlib/backends/backend_qt5cairo.py | 1 + 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 093a4e5e476d..186357d8f83e 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -270,6 +270,26 @@ def _dpi_ratio(self): except AttributeError: return 1 + def _update_dpi(self): + # As described in __init__ above, we need to be careful in cases with + # mixed resolution displays if dpi_ratio is changing between painting + # events. + # Return whether we triggered a resizeEvent (and thus a paintEvent) + # from within this function. + if self._dpi_ratio != self._dpi_ratio_prev: + # We need to update the figure DPI. + self._update_figure_dpi() + self._dpi_ratio_prev = self._dpi_ratio + # The easiest way to resize the canvas is to emit a resizeEvent + # since we implement all the logic for resizing the canvas for + # that event. + event = QtGui.QResizeEvent(self.size(), self.size()) + self.resizeEvent(event) + # resizeEvent triggers a paintEvent itself, so we exit this one + # (after making sure that the event is immediately handled). + return True + return False + def get_width_height(self): w, h = FigureCanvasBase.get_width_height(self) return int(w / self._dpi_ratio), int(h / self._dpi_ratio) @@ -489,6 +509,7 @@ def draw_idle(self): def _draw_idle(self): if self.height() < 0 or self.width() < 0: self._draw_pending = False + if not self._draw_pending: return try: self.draw() diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index afa1f13c7f67..61e8c8c3c7b9 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -35,30 +35,13 @@ def paintEvent(self, e): In Qt, all drawing should be done inside of here when a widget is shown onscreen. """ - # if there is a pending draw, run it now as we need the updated render - # to paint the widget - if self._agg_draw_pending: - self.__draw_idle_agg() - # As described in __init__ above, we need to be careful in cases with - # mixed resolution displays if dpi_ratio is changing between painting - # events. - if self._dpi_ratio != self._dpi_ratio_prev: - # We need to update the figure DPI - self._update_figure_dpi() - self._dpi_ratio_prev = self._dpi_ratio - # The easiest way to resize the canvas is to emit a resizeEvent - # since we implement all the logic for resizing the canvas for - # that event. - event = QtGui.QResizeEvent(self.size(), self.size()) - # We use self.resizeEvent here instead of QApplication.postEvent - # since the latter doesn't guarantee that the event will be emitted - # straight away, and this causes visual delays in the changes. - self.resizeEvent(event) - # resizeEvent triggers a paintEvent itself, so we exit this one. + if self._update_dpi(): + # The dpi update triggered its own paintEvent. return + self._draw_idle() # Only does something if a draw is pending. - # if the canvas does not have a renderer, then give up and wait for - # FigureCanvasAgg.draw(self) to be called + # If the canvas does not have a renderer, then give up and wait for + # FigureCanvasAgg.draw(self) to be called. if not hasattr(self, 'renderer'): return diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py index 746172bca5bf..7bbcdd7b3678 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -9,6 +9,7 @@ def __init__(self, figure): self._renderer = backend_cairo.RendererCairo(self.figure.dpi) def paintEvent(self, event): + self._update_dpi() width = self.width() height = self.height() surface = backend_cairo.cairo.ImageSurface( From 89a87d0be2064a42d8809f45dae131e5cca3663b Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 3 Aug 2017 11:26:13 -0700 Subject: [PATCH 10/13] Fix vector output from cairo. Vector surfaces do not report their extents, so we need to pass them in manually. --- lib/matplotlib/backends/backend_cairo.py | 8 ++++++-- lib/matplotlib/backends/backend_qt5cairo.py | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index dc1a0e059541..1c6506d3c89b 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -106,7 +106,10 @@ def __init__(self, dpi): def set_ctx_from_surface(self, surface): self.gc.ctx = cairo.Context(surface) - self.set_width_height(surface.get_width(), surface.get_height()) + # Although it may appear natural to automatically call + # `self.set_width_height(surface.get_width(), surface.get_height())` + # here (instead of having the caller do so separately), this would fail + # for PDF/PS/SVG surfaces, which have no way to report their extents. def set_width_height(self, width, height): self.width = width @@ -441,9 +444,9 @@ def print_png(self, fobj, *args, **kwargs): width, height = self.get_width_height() renderer = RendererCairo(self.figure.dpi) - renderer.set_width_height(width, height) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) renderer.set_ctx_from_surface(surface) + renderer.set_width_height(width, height) self.figure.draw(renderer) surface.write_to_png(fobj) @@ -500,6 +503,7 @@ def _save(self, fo, fmt, **kwargs): # surface.set_dpi() can be used renderer = RendererCairo(self.figure.dpi) renderer.set_ctx_from_surface(surface) + renderer.set_width_height(width_in_points, height_in_points) ctx = renderer.gc.ctx if orientation == 'landscape': diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py index 7bbcdd7b3678..a0bf298e2f12 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -15,6 +15,7 @@ def paintEvent(self, event): surface = backend_cairo.cairo.ImageSurface( backend_cairo.cairo.FORMAT_ARGB32, width, height) self._renderer.set_ctx_from_surface(surface) + self._renderer.set_width_height(width, height) self.figure.draw(self._renderer) buf = surface.get_data() qimage = QtGui.QImage(buf, width, height, From 56854ae6ce6fb45d5f2e58b7f920ecd095e28481 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 3 Aug 2017 11:34:27 -0700 Subject: [PATCH 11/13] In QtCairo, also save figures using cairo. --- lib/matplotlib/backends/backend_qt5cairo.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py index a0bf298e2f12..ba3d1603507b 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -1,19 +1,18 @@ -from . import backend_cairo # Keep the RendererCairo class swappable. +from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT from .qt_compat import QT_API -class FigureCanvasQTCairo(FigureCanvasQT): +class FigureCanvasQTCairo(FigureCanvasQT, FigureCanvasCairo): def __init__(self, figure): super(FigureCanvasQTCairo, self).__init__(figure=figure) - self._renderer = backend_cairo.RendererCairo(self.figure.dpi) + self._renderer = RendererCairo(self.figure.dpi) def paintEvent(self, event): self._update_dpi() width = self.width() height = self.height() - surface = backend_cairo.cairo.ImageSurface( - backend_cairo.cairo.FORMAT_ARGB32, width, height) + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) self._renderer.set_ctx_from_surface(surface) self._renderer.set_width_height(width, height) self.figure.draw(self._renderer) From ab61783390f2d8ffad98c38f86e3d406d67e28ca Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 25 Aug 2017 20:52:31 -0700 Subject: [PATCH 12/13] Fix doc builds. --- doc/conf.py | 40 +++++++++++++----------- lib/matplotlib/backends/backend_cairo.py | 3 +- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index c89c9d2c773a..e3755275a0ed 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -335,17 +335,8 @@ def _check_deps(): ] -class MyWX(MagicMock): - class Panel(object): - pass - - class ToolBar(object): - pass - - class Frame(object): - pass - - VERSION_STRING = '2.9' +class MyCairo(MagicMock): + version_info = (1, 2, 0) class MyPyQt4(MagicMock): @@ -450,14 +441,25 @@ def getapi(*args): return 1 -mockwxversion = MagicMock() -mockwx = MyWX() -mocksip = MySip() -mockpyqt4 = MyPyQt4() -sys.modules['wxversion'] = mockwxversion -sys.modules['wx'] = mockwx -sys.modules['sip'] = mocksip -sys.modules['PyQt4'] = mockpyqt4 +class MyWX(MagicMock): + class Panel(object): + pass + + class ToolBar(object): + pass + + class Frame(object): + pass + + VERSION_STRING = '2.9' + + +sys.modules['cairo'] = MyCairo() +sys.modules['cairo'].__name__ = 'cairocffi' +sys.modules['PyQt4'] = MyPyQt4() +sys.modules['sip'] = MySip() +sys.modules['wx'] = MyWX() +sys.modules['wxversion'] = MagicMock() # numpydoc config diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 1c6506d3c89b..27a88c828b32 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -19,8 +19,7 @@ import numpy as np # In order to make it possible to pick the binding, use whichever has already -# been imported, if any. (The intermediate call to iter is just to placate -# Python2.) +# been imported, if any. cairo = next( (mod for mod in ( sys.modules.get(name) for name in ["cairocffi", "cairo"]) if mod), From 8946c385940e83b44bc80738a2dd1a66b4afc555 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 29 Dec 2017 14:17:13 -0800 Subject: [PATCH 13/13] Fix hidpi in qt5cairo. --- lib/matplotlib/backends/backend_qt5cairo.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py index ba3d1603507b..6778716f5d9e 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -10,8 +10,9 @@ def __init__(self, figure): def paintEvent(self, event): self._update_dpi() - width = self.width() - height = self.height() + dpi_ratio = self._dpi_ratio + width = dpi_ratio * self.width() + height = dpi_ratio * self.height() surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) self._renderer.set_ctx_from_surface(surface) self._renderer.set_width_height(width, height) @@ -23,6 +24,9 @@ def paintEvent(self, event): # QImage under PySide on Python 3. if QT_API == 'PySide' and six.PY3: ctypes.c_long.from_address(id(buf)).value = 1 + if hasattr(qimage, 'setDevicePixelRatio'): + # Not available on Qt4 or some older Qt5. + qimage.setDevicePixelRatio(dpi_ratio) painter = QtGui.QPainter(self) painter.drawImage(0, 0, qimage) self._draw_rect_callback(painter)