1""" 2Abstract base classes define the primitives that renderers and 3graphics contexts must implement to serve as a matplotlib backend 4 5:class:`RendererBase` 6 An abstract base class to handle drawing/rendering operations. 7 8:class:`FigureCanvasBase` 9 The abstraction layer that separates the 10 :class:`matplotlib.figure.Figure` from the backend specific 11 details like a user interface drawing area 12 13:class:`GraphicsContextBase` 14 An abstract base class that provides color, line styles, etc... 15 16:class:`Event` 17 The base class for all of the matplotlib event 18 handling. Derived classes such as :class:`KeyEvent` and 19 :class:`MouseEvent` store the meta data like keys and buttons 20 pressed, x and y locations in pixel and 21 :class:`~matplotlib.axes.Axes` coordinates. 22 23:class:`ShowBase` 24 The base class for the Show class of each interactive backend; 25 the 'show' callable is then set to Show.__call__, inherited from 26 ShowBase. 27 28:class:`ToolContainerBase` 29 The base class for the Toolbar class of each interactive backend. 30 31:class:`StatusbarBase` 32 The base class for the messaging area. 33""" 34 35from __future__ import (absolute_import, division, print_function, 36 unicode_literals) 37 38import six 39from six.moves import xrange 40 41from contextlib import contextmanager 42from functools import partial 43import importlib 44import io 45import os 46import sys 47import time 48import warnings 49from weakref import WeakKeyDictionary 50 51import numpy as np 52 53from matplotlib import ( 54 backend_tools as tools, cbook, colors, textpath, tight_bbox, transforms, 55 widgets, get_backend, is_interactive, rcParams) 56from matplotlib._pylab_helpers import Gcf 57from matplotlib.transforms import Bbox, TransformedBbox, Affine2D 58from matplotlib.path import Path 59 60try: 61 from PIL import Image 62 _has_pil = True 63 del Image 64except ImportError: 65 _has_pil = False 66 67 68_default_filetypes = { 69 'ps': 'Postscript', 70 'eps': 'Encapsulated Postscript', 71 'pdf': 'Portable Document Format', 72 'pgf': 'PGF code for LaTeX', 73 'png': 'Portable Network Graphics', 74 'raw': 'Raw RGBA bitmap', 75 'rgba': 'Raw RGBA bitmap', 76 'svg': 'Scalable Vector Graphics', 77 'svgz': 'Scalable Vector Graphics' 78} 79 80 81_default_backends = { 82 'ps': 'matplotlib.backends.backend_ps', 83 'eps': 'matplotlib.backends.backend_ps', 84 'pdf': 'matplotlib.backends.backend_pdf', 85 'pgf': 'matplotlib.backends.backend_pgf', 86 'png': 'matplotlib.backends.backend_agg', 87 'raw': 'matplotlib.backends.backend_agg', 88 'rgba': 'matplotlib.backends.backend_agg', 89 'svg': 'matplotlib.backends.backend_svg', 90 'svgz': 'matplotlib.backends.backend_svg', 91} 92 93 94def register_backend(format, backend, description=None): 95 """ 96 Register a backend for saving to a given file format. 97 98 Parameters 99 ---------- 100 format : str 101 File extension 102 103 backend : module string or canvas class 104 Backend for handling file output 105 106 description : str, optional 107 Description of the file type. Defaults to an empty string 108 """ 109 if description is None: 110 description = '' 111 _default_backends[format] = backend 112 _default_filetypes[format] = description 113 114 115def get_registered_canvas_class(format): 116 """ 117 Return the registered default canvas for given file format. 118 Handles deferred import of required backend. 119 """ 120 if format not in _default_backends: 121 return None 122 backend_class = _default_backends[format] 123 if isinstance(backend_class, six.string_types): 124 backend_class = importlib.import_module(backend_class).FigureCanvas 125 _default_backends[format] = backend_class 126 return backend_class 127 128 129class _Backend(object): 130 # A backend can be defined by using the following pattern: 131 # 132 # @_Backend.export 133 # class FooBackend(_Backend): 134 # # override the attributes and methods documented below. 135 136 # The following attributes and methods must be overridden by subclasses. 137 138 # The `FigureCanvas` and `FigureManager` classes must be defined. 139 FigureCanvas = None 140 FigureManager = None 141 142 # The following methods must be left as None for non-interactive backends. 143 # For interactive backends, `trigger_manager_draw` should be a function 144 # taking a manager as argument and triggering a canvas draw, and `mainloop` 145 # should be a function taking no argument and starting the backend main 146 # loop. 147 trigger_manager_draw = None 148 mainloop = None 149 150 # The following methods will be automatically defined and exported, but 151 # can be overridden. 152 153 @classmethod 154 def new_figure_manager(cls, num, *args, **kwargs): 155 """Create a new figure manager instance. 156 """ 157 # This import needs to happen here due to circular imports. 158 from matplotlib.figure import Figure 159 fig_cls = kwargs.pop('FigureClass', Figure) 160 fig = fig_cls(*args, **kwargs) 161 return cls.new_figure_manager_given_figure(num, fig) 162 163 @classmethod 164 def new_figure_manager_given_figure(cls, num, figure): 165 """Create a new figure manager instance for the given figure. 166 """ 167 canvas = cls.FigureCanvas(figure) 168 manager = cls.FigureManager(canvas, num) 169 return manager 170 171 @classmethod 172 def draw_if_interactive(cls): 173 if cls.trigger_manager_draw is not None and is_interactive(): 174 manager = Gcf.get_active() 175 if manager: 176 cls.trigger_manager_draw(manager) 177 178 @classmethod 179 def show(cls, block=None): 180 """Show all figures. 181 182 `show` blocks by calling `mainloop` if *block* is ``True``, or if it 183 is ``None`` and we are neither in IPython's ``%pylab`` mode, nor in 184 `interactive` mode. 185 """ 186 if cls.mainloop is None: 187 return 188 managers = Gcf.get_all_fig_managers() 189 if not managers: 190 return 191 for manager in managers: 192 manager.show() 193 if block is None: 194 # Hack: Are we in IPython's pylab mode? 195 from matplotlib import pyplot 196 try: 197 # IPython versions >= 0.10 tack the _needmain attribute onto 198 # pyplot.show, and always set it to False, when in %pylab mode. 199 ipython_pylab = not pyplot.show._needmain 200 except AttributeError: 201 ipython_pylab = False 202 block = not ipython_pylab and not is_interactive() 203 # TODO: The above is a hack to get the WebAgg backend working with 204 # ipython's `%pylab` mode until proper integration is implemented. 205 if get_backend() == "WebAgg": 206 block = True 207 if block: 208 cls.mainloop() 209 210 # This method is the one actually exporting the required methods. 211 212 @staticmethod 213 def export(cls): 214 for name in ["FigureCanvas", 215 "FigureManager", 216 "new_figure_manager", 217 "new_figure_manager_given_figure", 218 "draw_if_interactive", 219 "show"]: 220 setattr(sys.modules[cls.__module__], name, getattr(cls, name)) 221 222 # For back-compatibility, generate a shim `Show` class. 223 224 class Show(ShowBase): 225 def mainloop(self): 226 return cls.mainloop() 227 228 setattr(sys.modules[cls.__module__], "Show", Show) 229 return cls 230 231 232class ShowBase(_Backend): 233 """ 234 Simple base class to generate a show() callable in backends. 235 236 Subclass must override mainloop() method. 237 """ 238 239 def __call__(self, block=None): 240 return self.show(block=block) 241 242 243class RendererBase(object): 244 """An abstract base class to handle drawing/rendering operations. 245 246 The following methods must be implemented in the backend for full 247 functionality (though just implementing :meth:`draw_path` alone would 248 give a highly capable backend): 249 250 * :meth:`draw_path` 251 * :meth:`draw_image` 252 * :meth:`draw_gouraud_triangle` 253 254 The following methods *should* be implemented in the backend for 255 optimization reasons: 256 257 * :meth:`draw_text` 258 * :meth:`draw_markers` 259 * :meth:`draw_path_collection` 260 * :meth:`draw_quad_mesh` 261 262 """ 263 def __init__(self): 264 self._texmanager = None 265 self._text2path = textpath.TextToPath() 266 267 def open_group(self, s, gid=None): 268 """ 269 Open a grouping element with label *s*. If *gid* is given, use 270 *gid* as the id of the group. Is only currently used by 271 :mod:`~matplotlib.backends.backend_svg`. 272 """ 273 274 def close_group(self, s): 275 """ 276 Close a grouping element with label *s* 277 Is only currently used by :mod:`~matplotlib.backends.backend_svg` 278 """ 279 280 def draw_path(self, gc, path, transform, rgbFace=None): 281 """ 282 Draws a :class:`~matplotlib.path.Path` instance using the 283 given affine transform. 284 """ 285 raise NotImplementedError 286 287 def draw_markers(self, gc, marker_path, marker_trans, path, 288 trans, rgbFace=None): 289 """ 290 Draws a marker at each of the vertices in path. This includes 291 all vertices, including control points on curves. To avoid 292 that behavior, those vertices should be removed before calling 293 this function. 294 295 This provides a fallback implementation of draw_markers that 296 makes multiple calls to :meth:`draw_path`. Some backends may 297 want to override this method in order to draw the marker only 298 once and reuse it multiple times. 299 300 Parameters 301 ---------- 302 gc : `GraphicsContextBase` 303 The graphics context 304 305 marker_trans : `matplotlib.transforms.Transform` 306 An affine transform applied to the marker. 307 308 trans : `matplotlib.transforms.Transform` 309 An affine transform applied to the path. 310 311 """ 312 for vertices, codes in path.iter_segments(trans, simplify=False): 313 if len(vertices): 314 x, y = vertices[-2:] 315 self.draw_path(gc, marker_path, 316 marker_trans + 317 transforms.Affine2D().translate(x, y), 318 rgbFace) 319 320 def draw_path_collection(self, gc, master_transform, paths, all_transforms, 321 offsets, offsetTrans, facecolors, edgecolors, 322 linewidths, linestyles, antialiaseds, urls, 323 offset_position): 324 """ 325 Draws a collection of paths selecting drawing properties from 326 the lists *facecolors*, *edgecolors*, *linewidths*, 327 *linestyles* and *antialiaseds*. *offsets* is a list of 328 offsets to apply to each of the paths. The offsets in 329 *offsets* are first transformed by *offsetTrans* before being 330 applied. *offset_position* may be either "screen" or "data" 331 depending on the space that the offsets are in. 332 333 This provides a fallback implementation of 334 :meth:`draw_path_collection` that makes multiple calls to 335 :meth:`draw_path`. Some backends may want to override this in 336 order to render each set of path data only once, and then 337 reference that path multiple times with the different offsets, 338 colors, styles etc. The generator methods 339 :meth:`_iter_collection_raw_paths` and 340 :meth:`_iter_collection` are provided to help with (and 341 standardize) the implementation across backends. It is highly 342 recommended to use those generators, so that changes to the 343 behavior of :meth:`draw_path_collection` can be made globally. 344 """ 345 path_ids = [] 346 for path, transform in self._iter_collection_raw_paths( 347 master_transform, paths, all_transforms): 348 path_ids.append((path, transforms.Affine2D(transform))) 349 350 for xo, yo, path_id, gc0, rgbFace in self._iter_collection( 351 gc, master_transform, all_transforms, path_ids, offsets, 352 offsetTrans, facecolors, edgecolors, linewidths, linestyles, 353 antialiaseds, urls, offset_position): 354 path, transform = path_id 355 transform = transforms.Affine2D( 356 transform.get_matrix()).translate(xo, yo) 357 self.draw_path(gc0, path, transform, rgbFace) 358 359 def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, 360 coordinates, offsets, offsetTrans, facecolors, 361 antialiased, edgecolors): 362 """ 363 This provides a fallback implementation of 364 :meth:`draw_quad_mesh` that generates paths and then calls 365 :meth:`draw_path_collection`. 366 """ 367 368 from matplotlib.collections import QuadMesh 369 paths = QuadMesh.convert_mesh_to_paths( 370 meshWidth, meshHeight, coordinates) 371 372 if edgecolors is None: 373 edgecolors = facecolors 374 linewidths = np.array([gc.get_linewidth()], float) 375 376 return self.draw_path_collection( 377 gc, master_transform, paths, [], offsets, offsetTrans, facecolors, 378 edgecolors, linewidths, [], [antialiased], [None], 'screen') 379 380 def draw_gouraud_triangle(self, gc, points, colors, transform): 381 """ 382 Draw a Gouraud-shaded triangle. 383 384 Parameters 385 ---------- 386 points : array_like, shape=(3, 2) 387 Array of (x, y) points for the triangle. 388 389 colors : array_like, shape=(3, 4) 390 RGBA colors for each point of the triangle. 391 392 transform : `matplotlib.transforms.Transform` 393 An affine transform to apply to the points. 394 395 """ 396 raise NotImplementedError 397 398 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, 399 transform): 400 """ 401 Draws a series of Gouraud triangles. 402 403 Parameters 404 ---------- 405 points : array_like, shape=(N, 3, 2) 406 Array of *N* (x, y) points for the triangles. 407 408 colors : array_like, shape=(N, 3, 4) 409 Array of *N* RGBA colors for each point of the triangles. 410 411 transform : `matplotlib.transforms.Transform` 412 An affine transform to apply to the points. 413 """ 414 transform = transform.frozen() 415 for tri, col in zip(triangles_array, colors_array): 416 self.draw_gouraud_triangle(gc, tri, col, transform) 417 418 def _iter_collection_raw_paths(self, master_transform, paths, 419 all_transforms): 420 """ 421 This is a helper method (along with :meth:`_iter_collection`) to make 422 it easier to write a space-efficient :meth:`draw_path_collection` 423 implementation in a backend. 424 425 This method yields all of the base path/transform 426 combinations, given a master transform, a list of paths and 427 list of transforms. 428 429 The arguments should be exactly what is passed in to 430 :meth:`draw_path_collection`. 431 432 The backend should take each yielded path and transform and 433 create an object that can be referenced (reused) later. 434 """ 435 Npaths = len(paths) 436 Ntransforms = len(all_transforms) 437 N = max(Npaths, Ntransforms) 438 439 if Npaths == 0: 440 return 441 442 transform = transforms.IdentityTransform() 443 for i in xrange(N): 444 path = paths[i % Npaths] 445 if Ntransforms: 446 transform = Affine2D(all_transforms[i % Ntransforms]) 447 yield path, transform + master_transform 448 449 def _iter_collection_uses_per_path(self, paths, all_transforms, 450 offsets, facecolors, edgecolors): 451 """ 452 Compute how many times each raw path object returned by 453 _iter_collection_raw_paths would be used when calling 454 _iter_collection. This is intended for the backend to decide 455 on the tradeoff between using the paths in-line and storing 456 them once and reusing. Rounds up in case the number of uses 457 is not the same for every path. 458 """ 459 Npaths = len(paths) 460 if Npaths == 0 or (len(facecolors) == 0 and len(edgecolors) == 0): 461 return 0 462 Npath_ids = max(Npaths, len(all_transforms)) 463 N = max(Npath_ids, len(offsets)) 464 return (N + Npath_ids - 1) // Npath_ids 465 466 def _iter_collection(self, gc, master_transform, all_transforms, 467 path_ids, offsets, offsetTrans, facecolors, 468 edgecolors, linewidths, linestyles, 469 antialiaseds, urls, offset_position): 470 """ 471 This is a helper method (along with 472 :meth:`_iter_collection_raw_paths`) to make it easier to write 473 a space-efficient :meth:`draw_path_collection` implementation in a 474 backend. 475 476 This method yields all of the path, offset and graphics 477 context combinations to draw the path collection. The caller 478 should already have looped over the results of 479 :meth:`_iter_collection_raw_paths` to draw this collection. 480 481 The arguments should be the same as that passed into 482 :meth:`draw_path_collection`, with the exception of 483 *path_ids*, which is a list of arbitrary objects that the 484 backend will use to reference one of the paths created in the 485 :meth:`_iter_collection_raw_paths` stage. 486 487 Each yielded result is of the form:: 488 489 xo, yo, path_id, gc, rgbFace 490 491 where *xo*, *yo* is an offset; *path_id* is one of the elements of 492 *path_ids*; *gc* is a graphics context and *rgbFace* is a color to 493 use for filling the path. 494 """ 495 Ntransforms = len(all_transforms) 496 Npaths = len(path_ids) 497 Noffsets = len(offsets) 498 N = max(Npaths, Noffsets) 499 Nfacecolors = len(facecolors) 500 Nedgecolors = len(edgecolors) 501 Nlinewidths = len(linewidths) 502 Nlinestyles = len(linestyles) 503 Naa = len(antialiaseds) 504 Nurls = len(urls) 505 506 if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0: 507 return 508 if Noffsets: 509 toffsets = offsetTrans.transform(offsets) 510 511 gc0 = self.new_gc() 512 gc0.copy_properties(gc) 513 514 if Nfacecolors == 0: 515 rgbFace = None 516 517 if Nedgecolors == 0: 518 gc0.set_linewidth(0.0) 519 520 xo, yo = 0, 0 521 for i in xrange(N): 522 path_id = path_ids[i % Npaths] 523 if Noffsets: 524 xo, yo = toffsets[i % Noffsets] 525 if offset_position == 'data': 526 if Ntransforms: 527 transform = ( 528 Affine2D(all_transforms[i % Ntransforms]) + 529 master_transform) 530 else: 531 transform = master_transform 532 xo, yo = transform.transform_point((xo, yo)) 533 xp, yp = transform.transform_point((0, 0)) 534 xo = -(xp - xo) 535 yo = -(yp - yo) 536 if not (np.isfinite(xo) and np.isfinite(yo)): 537 continue 538 if Nfacecolors: 539 rgbFace = facecolors[i % Nfacecolors] 540 if Nedgecolors: 541 if Nlinewidths: 542 gc0.set_linewidth(linewidths[i % Nlinewidths]) 543 if Nlinestyles: 544 gc0.set_dashes(*linestyles[i % Nlinestyles]) 545 fg = edgecolors[i % Nedgecolors] 546 if len(fg) == 4: 547 if fg[3] == 0.0: 548 gc0.set_linewidth(0) 549 else: 550 gc0.set_foreground(fg) 551 else: 552 gc0.set_foreground(fg) 553 if rgbFace is not None and len(rgbFace) == 4: 554 if rgbFace[3] == 0: 555 rgbFace = None 556 gc0.set_antialiased(antialiaseds[i % Naa]) 557 if Nurls: 558 gc0.set_url(urls[i % Nurls]) 559 560 yield xo, yo, path_id, gc0, rgbFace 561 gc0.restore() 562 563 def get_image_magnification(self): 564 """ 565 Get the factor by which to magnify images passed to :meth:`draw_image`. 566 Allows a backend to have images at a different resolution to other 567 artists. 568 """ 569 return 1.0 570 571 def draw_image(self, gc, x, y, im, transform=None): 572 """ 573 Draw an RGBA image. 574 575 Parameters 576 ---------- 577 gc : `GraphicsContextBase` 578 a graphics context with clipping information. 579 580 x : scalar 581 the distance in physical units (i.e., dots or pixels) from the left 582 hand side of the canvas. 583 584 y : scalar 585 the distance in physical units (i.e., dots or pixels) from the 586 bottom side of the canvas. 587 588 im : array_like, shape=(N, M, 4), dtype=np.uint8 589 An array of RGBA pixels. 590 591 transform : `matplotlib.transforms.Affine2DBase` 592 If and only if the concrete backend is written such that 593 :meth:`option_scale_image` returns ``True``, an affine 594 transformation *may* be passed to :meth:`draw_image`. It takes the 595 form of a :class:`~matplotlib.transforms.Affine2DBase` instance. 596 The translation vector of the transformation is given in physical 597 units (i.e., dots or pixels). Note that the transformation does not 598 override `x` and `y`, and has to be applied *before* translating 599 the result by `x` and `y` (this can be accomplished by adding `x` 600 and `y` to the translation vector defined by `transform`). 601 """ 602 raise NotImplementedError 603 604 def option_image_nocomposite(self): 605 """ 606 override this method for renderers that do not necessarily always 607 want to rescale and composite raster images. (like SVG, PDF, or PS) 608 """ 609 return False 610 611 def option_scale_image(self): 612 """ 613 override this method for renderers that support arbitrary affine 614 transformations in :meth:`draw_image` (most vector backends). 615 """ 616 return False 617 618 def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): 619 """ 620 """ 621 self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") 622 623 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): 624 """ 625 Draw the text instance 626 627 Parameters 628 ---------- 629 gc : `GraphicsContextBase` 630 the graphics context 631 632 x : scalar 633 the x location of the text in display coords 634 635 y : scalar 636 the y location of the text baseline in display coords 637 638 s : str 639 the text string 640 641 prop : `matplotlib.font_manager.FontProperties` 642 font properties 643 644 angle : scalar 645 the rotation angle in degrees 646 647 mtext : `matplotlib.text.Text` 648 the original text object to be rendered 649 650 Notes 651 ----- 652 **backend implementers note** 653 654 When you are trying to determine if you have gotten your bounding box 655 right (which is what enables the text layout/alignment to work 656 properly), it helps to change the line in text.py:: 657 658 if 0: bbox_artist(self, renderer) 659 660 to if 1, and then the actual bounding box will be plotted along with 661 your text. 662 """ 663 664 self._draw_text_as_path(gc, x, y, s, prop, angle, ismath) 665 666 def _get_text_path_transform(self, x, y, s, prop, angle, ismath): 667 """ 668 return the text path and transform 669 670 Parameters 671 ---------- 672 prop : `matplotlib.font_manager.FontProperties` 673 font property 674 675 s : str 676 text to be converted 677 678 usetex : bool 679 If True, use matplotlib usetex mode. 680 681 ismath : bool 682 If True, use mathtext parser. If "TeX", use *usetex* mode. 683 """ 684 685 text2path = self._text2path 686 fontsize = self.points_to_pixels(prop.get_size_in_points()) 687 688 if ismath == "TeX": 689 verts, codes = text2path.get_text_path(prop, s, ismath=False, 690 usetex=True) 691 else: 692 verts, codes = text2path.get_text_path(prop, s, ismath=ismath, 693 usetex=False) 694 695 path = Path(verts, codes) 696 angle = np.deg2rad(angle) 697 if self.flipy(): 698 transform = Affine2D().scale(fontsize / text2path.FONT_SCALE, 699 fontsize / text2path.FONT_SCALE) 700 transform = transform.rotate(angle).translate(x, self.height - y) 701 else: 702 transform = Affine2D().scale(fontsize / text2path.FONT_SCALE, 703 fontsize / text2path.FONT_SCALE) 704 transform = transform.rotate(angle).translate(x, y) 705 706 return path, transform 707 708 def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): 709 """ 710 draw the text by converting them to paths using textpath module. 711 712 Parameters 713 ---------- 714 prop : `matplotlib.font_manager.FontProperties` 715 font property 716 717 s : str 718 text to be converted 719 720 usetex : bool 721 If True, use matplotlib usetex mode. 722 723 ismath : bool 724 If True, use mathtext parser. If "TeX", use *usetex* mode. 725 """ 726 path, transform = self._get_text_path_transform( 727 x, y, s, prop, angle, ismath) 728 color = gc.get_rgb() 729 730 gc.set_linewidth(0.0) 731 self.draw_path(gc, path, transform, rgbFace=color) 732 733 def get_text_width_height_descent(self, s, prop, ismath): 734 """ 735 Get the width, height, and descent (offset from the bottom 736 to the baseline), in display coords, of the string *s* with 737 :class:`~matplotlib.font_manager.FontProperties` *prop* 738 """ 739 if ismath == 'TeX': 740 # todo: handle props 741 size = prop.get_size_in_points() 742 texmanager = self._text2path.get_texmanager() 743 fontsize = prop.get_size_in_points() 744 w, h, d = texmanager.get_text_width_height_descent( 745 s, fontsize, renderer=self) 746 return w, h, d 747 748 dpi = self.points_to_pixels(72) 749 if ismath: 750 dims = self._text2path.mathtext_parser.parse(s, dpi, prop) 751 return dims[0:3] # return width, height, descent 752 753 flags = self._text2path._get_hinting_flag() 754 font = self._text2path._get_font(prop) 755 size = prop.get_size_in_points() 756 font.set_size(size, dpi) 757 # the width and height of unrotated string 758 font.set_text(s, 0.0, flags=flags) 759 w, h = font.get_width_height() 760 d = font.get_descent() 761 w /= 64.0 # convert from subpixels 762 h /= 64.0 763 d /= 64.0 764 return w, h, d 765 766 def flipy(self): 767 """ 768 Return true if y small numbers are top for renderer Is used 769 for drawing text (:mod:`matplotlib.text`) and images 770 (:mod:`matplotlib.image`) only 771 """ 772 return True 773 774 def get_canvas_width_height(self): 775 'return the canvas width and height in display coords' 776 return 1, 1 777 778 def get_texmanager(self): 779 """ 780 return the :class:`matplotlib.texmanager.TexManager` instance 781 """ 782 if self._texmanager is None: 783 from matplotlib.texmanager import TexManager 784 self._texmanager = TexManager() 785 return self._texmanager 786 787 def new_gc(self): 788 """ 789 Return an instance of a :class:`GraphicsContextBase` 790 """ 791 return GraphicsContextBase() 792 793 def points_to_pixels(self, points): 794 """ 795 Convert points to display units 796 797 You need to override this function (unless your backend 798 doesn't have a dpi, e.g., postscript or svg). Some imaging 799 systems assume some value for pixels per inch:: 800 801 points to pixels = points * pixels_per_inch/72.0 * dpi/72.0 802 803 Parameters 804 ---------- 805 points : scalar or array_like 806 a float or a numpy array of float 807 808 Returns 809 ------- 810 Points converted to pixels 811 """ 812 return points 813 814 def strip_math(self, s): 815 return cbook.strip_math(s) 816 817 def start_rasterizing(self): 818 """ 819 Used in MixedModeRenderer. Switch to the raster renderer. 820 """ 821 822 def stop_rasterizing(self): 823 """ 824 Used in MixedModeRenderer. Switch back to the vector renderer 825 and draw the contents of the raster renderer as an image on 826 the vector renderer. 827 """ 828 829 def start_filter(self): 830 """ 831 Used in AggRenderer. Switch to a temporary renderer for image 832 filtering effects. 833 """ 834 835 def stop_filter(self, filter_func): 836 """ 837 Used in AggRenderer. Switch back to the original renderer. 838 The contents of the temporary renderer is processed with the 839 *filter_func* and is drawn on the original renderer as an 840 image. 841 """ 842 843 844class GraphicsContextBase(object): 845 """ 846 An abstract base class that provides color, line styles, etc... 847 """ 848 849 def __init__(self): 850 self._alpha = 1.0 851 self._forced_alpha = False # if True, _alpha overrides A from RGBA 852 self._antialiased = 1 # use 0,1 not True, False for extension code 853 self._capstyle = 'butt' 854 self._cliprect = None 855 self._clippath = None 856 self._dashes = None, None 857 self._joinstyle = 'round' 858 self._linestyle = 'solid' 859 self._linewidth = 1 860 self._rgb = (0.0, 0.0, 0.0, 1.0) 861 self._hatch = None 862 self._hatch_color = colors.to_rgba(rcParams['hatch.color']) 863 self._hatch_linewidth = rcParams['hatch.linewidth'] 864 self._url = None 865 self._gid = None 866 self._snap = None 867 self._sketch = None 868 869 def copy_properties(self, gc): 870 'Copy properties from gc to self' 871 self._alpha = gc._alpha 872 self._forced_alpha = gc._forced_alpha 873 self._antialiased = gc._antialiased 874 self._capstyle = gc._capstyle 875 self._cliprect = gc._cliprect 876 self._clippath = gc._clippath 877 self._dashes = gc._dashes 878 self._joinstyle = gc._joinstyle 879 self._linestyle = gc._linestyle 880 self._linewidth = gc._linewidth 881 self._rgb = gc._rgb 882 self._hatch = gc._hatch 883 self._hatch_color = gc._hatch_color 884 self._hatch_linewidth = gc._hatch_linewidth 885 self._url = gc._url 886 self._gid = gc._gid 887 self._snap = gc._snap 888 self._sketch = gc._sketch 889 890 def restore(self): 891 """ 892 Restore the graphics context from the stack - needed only 893 for backends that save graphics contexts on a stack 894 """ 895 896 def get_alpha(self): 897 """ 898 Return the alpha value used for blending - not supported on 899 all backends 900 """ 901 return self._alpha 902 903 def get_antialiased(self): 904 "Return true if the object should try to do antialiased rendering" 905 return self._antialiased 906 907 def get_capstyle(self): 908 """ 909 Return the capstyle as a string in ('butt', 'round', 'projecting') 910 """ 911 return self._capstyle 912 913 def get_clip_rectangle(self): 914 """ 915 Return the clip rectangle as a :class:`~matplotlib.transforms.Bbox` 916 instance 917 """ 918 return self._cliprect 919 920 def get_clip_path(self): 921 """ 922 Return the clip path in the form (path, transform), where path 923 is a :class:`~matplotlib.path.Path` instance, and transform is 924 an affine transform to apply to the path before clipping. 925 """ 926 if self._clippath is not None: 927 return self._clippath.get_transformed_path_and_affine() 928 return None, None 929 930 def get_dashes(self): 931 """ 932 Return the dash information as an offset dashlist tuple. 933 934 The dash list is a even size list that gives the ink on, ink 935 off in pixels. 936 937 See p107 of to PostScript `BLUEBOOK 938 <https://www-cdf.fnal.gov/offline/PostScript/BLUEBOOK.PDF>`_ 939 for more info. 940 941 Default value is None 942 """ 943 return self._dashes 944 945 def get_forced_alpha(self): 946 """ 947 Return whether the value given by get_alpha() should be used to 948 override any other alpha-channel values. 949 """ 950 return self._forced_alpha 951 952 def get_joinstyle(self): 953 """ 954 Return the line join style as one of ('miter', 'round', 'bevel') 955 """ 956 return self._joinstyle 957 958 @cbook.deprecated("2.1") 959 def get_linestyle(self): 960 """ 961 Return the linestyle: one of ('solid', 'dashed', 'dashdot', 962 'dotted'). 963 """ 964 return self._linestyle 965 966 def get_linewidth(self): 967 """ 968 Return the line width in points as a scalar 969 """ 970 return self._linewidth 971 972 def get_rgb(self): 973 """ 974 returns a tuple of three or four floats from 0-1. 975 """ 976 return self._rgb 977 978 def get_url(self): 979 """ 980 returns a url if one is set, None otherwise 981 """ 982 return self._url 983 984 def get_gid(self): 985 """ 986 Return the object identifier if one is set, None otherwise. 987 """ 988 return self._gid 989 990 def get_snap(self): 991 """ 992 returns the snap setting which may be: 993 994 * True: snap vertices to the nearest pixel center 995 996 * False: leave vertices as-is 997 998 * None: (auto) If the path contains only rectilinear line 999 segments, round to the nearest pixel center 1000 """ 1001 return self._snap 1002 1003 def set_alpha(self, alpha): 1004 """ 1005 Set the alpha value used for blending - not supported on all backends. 1006 If ``alpha=None`` (the default), the alpha components of the 1007 foreground and fill colors will be used to set their respective 1008 transparencies (where applicable); otherwise, ``alpha`` will override 1009 them. 1010 """ 1011 if alpha is not None: 1012 self._alpha = alpha 1013 self._forced_alpha = True 1014 else: 1015 self._alpha = 1.0 1016 self._forced_alpha = False 1017 self.set_foreground(self._rgb, isRGBA=True) 1018 1019 def set_antialiased(self, b): 1020 """ 1021 True if object should be drawn with antialiased rendering 1022 """ 1023 1024 # use 0, 1 to make life easier on extension code trying to read the gc 1025 if b: 1026 self._antialiased = 1 1027 else: 1028 self._antialiased = 0 1029 1030 def set_capstyle(self, cs): 1031 """ 1032 Set the capstyle as a string in ('butt', 'round', 'projecting') 1033 """ 1034 if cs in ('butt', 'round', 'projecting'): 1035 self._capstyle = cs 1036 else: 1037 raise ValueError('Unrecognized cap style. Found %s' % cs) 1038 1039 def set_clip_rectangle(self, rectangle): 1040 """ 1041 Set the clip rectangle with sequence (left, bottom, width, height) 1042 """ 1043 self._cliprect = rectangle 1044 1045 def set_clip_path(self, path): 1046 """ 1047 Set the clip path and transformation. Path should be a 1048 :class:`~matplotlib.transforms.TransformedPath` instance. 1049 """ 1050 if (path is not None 1051 and not isinstance(path, transforms.TransformedPath)): 1052 raise ValueError("Path should be a " 1053 "matplotlib.transforms.TransformedPath instance") 1054 self._clippath = path 1055 1056 def set_dashes(self, dash_offset, dash_list): 1057 """ 1058 Set the dash style for the gc. 1059 1060 Parameters 1061 ---------- 1062 dash_offset : float 1063 is the offset (usually 0). 1064 1065 dash_list : array_like 1066 specifies the on-off sequence as points. 1067 ``(None, None)`` specifies a solid line 1068 1069 """ 1070 if dash_list is not None: 1071 dl = np.asarray(dash_list) 1072 if np.any(dl < 0.0): 1073 raise ValueError( 1074 "All values in the dash list must be positive") 1075 self._dashes = dash_offset, dash_list 1076 1077 def set_foreground(self, fg, isRGBA=False): 1078 """ 1079 Set the foreground color. fg can be a MATLAB format string, a 1080 html hex color string, an rgb or rgba unit tuple, or a float between 0 1081 and 1. In the latter case, grayscale is used. 1082 1083 If you know fg is rgba, set ``isRGBA=True`` for efficiency. 1084 """ 1085 if self._forced_alpha and isRGBA: 1086 self._rgb = fg[:3] + (self._alpha,) 1087 elif self._forced_alpha: 1088 self._rgb = colors.to_rgba(fg, self._alpha) 1089 elif isRGBA: 1090 self._rgb = fg 1091 else: 1092 self._rgb = colors.to_rgba(fg) 1093 1094 def set_joinstyle(self, js): 1095 """ 1096 Set the join style to be one of ('miter', 'round', 'bevel') 1097 """ 1098 if js in ('miter', 'round', 'bevel'): 1099 self._joinstyle = js 1100 else: 1101 raise ValueError('Unrecognized join style. Found %s' % js) 1102 1103 def set_linewidth(self, w): 1104 """ 1105 Set the linewidth in points 1106 """ 1107 self._linewidth = float(w) 1108 1109 @cbook.deprecated("2.1") 1110 def set_linestyle(self, style): 1111 """ 1112 Set the linestyle to be one of ('solid', 'dashed', 'dashdot', 1113 'dotted'). These are defined in the rcParams 1114 `lines.dashed_pattern`, `lines.dashdot_pattern` and 1115 `lines.dotted_pattern`. One may also specify customized dash 1116 styles by providing a tuple of (offset, dash pairs). 1117 """ 1118 self._linestyle = style 1119 1120 def set_url(self, url): 1121 """ 1122 Sets the url for links in compatible backends 1123 """ 1124 self._url = url 1125 1126 def set_gid(self, id): 1127 """ 1128 Sets the id. 1129 """ 1130 self._gid = id 1131 1132 def set_snap(self, snap): 1133 """ 1134 Sets the snap setting which may be: 1135 1136 * True: snap vertices to the nearest pixel center 1137 1138 * False: leave vertices as-is 1139 1140 * None: (auto) If the path contains only rectilinear line 1141 segments, round to the nearest pixel center 1142 """ 1143 self._snap = snap 1144 1145 def set_hatch(self, hatch): 1146 """ 1147 Sets the hatch style for filling 1148 """ 1149 self._hatch = hatch 1150 1151 def get_hatch(self): 1152 """ 1153 Gets the current hatch style 1154 """ 1155 return self._hatch 1156 1157 def get_hatch_path(self, density=6.0): 1158 """ 1159 Returns a Path for the current hatch. 1160 """ 1161 hatch = self.get_hatch() 1162 if hatch is None: 1163 return None 1164 return Path.hatch(hatch, density) 1165 1166 def get_hatch_color(self): 1167 """ 1168 Gets the color to use for hatching. 1169 """ 1170 return self._hatch_color 1171 1172 def set_hatch_color(self, hatch_color): 1173 """ 1174 sets the color to use for hatching. 1175 """ 1176 self._hatch_color = hatch_color 1177 1178 def get_hatch_linewidth(self): 1179 """ 1180 Gets the linewidth to use for hatching. 1181 """ 1182 return self._hatch_linewidth 1183 1184 def get_sketch_params(self): 1185 """ 1186 Returns the sketch parameters for the artist. 1187 1188 Returns 1189 ------- 1190 sketch_params : tuple or `None` 1191 1192 A 3-tuple with the following elements: 1193 1194 * `scale`: The amplitude of the wiggle perpendicular to the 1195 source line. 1196 1197 * `length`: The length of the wiggle along the line. 1198 1199 * `randomness`: The scale factor by which the length is 1200 shrunken or expanded. 1201 1202 May return `None` if no sketch parameters were set. 1203 """ 1204 return self._sketch 1205 1206 def set_sketch_params(self, scale=None, length=None, randomness=None): 1207 """ 1208 Sets the sketch parameters. 1209 1210 Parameters 1211 ---------- 1212 1213 scale : float, optional 1214 The amplitude of the wiggle perpendicular to the source 1215 line, in pixels. If scale is `None`, or not provided, no 1216 sketch filter will be provided. 1217 1218 length : float, optional 1219 The length of the wiggle along the line, in pixels 1220 (default 128) 1221 1222 randomness : float, optional 1223 The scale factor by which the length is shrunken or 1224 expanded (default 16) 1225 """ 1226 self._sketch = ( 1227 None if scale is None 1228 else (scale, length or 128., randomness or 16.)) 1229 1230 1231class TimerBase(object): 1232 ''' 1233 A base class for providing timer events, useful for things animations. 1234 Backends need to implement a few specific methods in order to use their 1235 own timing mechanisms so that the timer events are integrated into their 1236 event loops. 1237 1238 Mandatory functions that must be implemented: 1239 1240 * `_timer_start`: Contains backend-specific code for starting 1241 the timer 1242 1243 * `_timer_stop`: Contains backend-specific code for stopping 1244 the timer 1245 1246 Optional overrides: 1247 1248 * `_timer_set_single_shot`: Code for setting the timer to 1249 single shot operating mode, if supported by the timer 1250 object. If not, the `Timer` class itself will store the flag 1251 and the `_on_timer` method should be overridden to support 1252 such behavior. 1253 1254 * `_timer_set_interval`: Code for setting the interval on the 1255 timer, if there is a method for doing so on the timer 1256 object. 1257 1258 * `_on_timer`: This is the internal function that any timer 1259 object should call, which will handle the task of running 1260 all callbacks that have been set. 1261 1262 Attributes 1263 ---------- 1264 interval : scalar 1265 The time between timer events in milliseconds. Default is 1000 ms. 1266 1267 single_shot : bool 1268 Boolean flag indicating whether this timer should operate as single 1269 shot (run once and then stop). Defaults to `False`. 1270 1271 callbacks : List[Tuple[callable, Tuple, Dict]] 1272 Stores list of (func, args, kwargs) tuples that will be called upon 1273 timer events. This list can be manipulated directly, or the 1274 functions `add_callback` and `remove_callback` can be used. 1275 1276 ''' 1277 def __init__(self, interval=None, callbacks=None): 1278 #Initialize empty callbacks list and setup default settings if necssary 1279 if callbacks is None: 1280 self.callbacks = [] 1281 else: 1282 self.callbacks = callbacks[:] # Create a copy 1283 1284 if interval is None: 1285 self._interval = 1000 1286 else: 1287 self._interval = interval 1288 1289 self._single = False 1290 1291 # Default attribute for holding the GUI-specific timer object 1292 self._timer = None 1293 1294 def __del__(self): 1295 'Need to stop timer and possibly disconnect timer.' 1296 self._timer_stop() 1297 1298 def start(self, interval=None): 1299 ''' 1300 Start the timer object. `interval` is optional and will be used 1301 to reset the timer interval first if provided. 1302 ''' 1303 if interval is not None: 1304 self._set_interval(interval) 1305 self._timer_start() 1306 1307 def stop(self): 1308 ''' 1309 Stop the timer. 1310 ''' 1311 self._timer_stop() 1312 1313 def _timer_start(self): 1314 pass 1315 1316 def _timer_stop(self): 1317 pass 1318 1319 def _get_interval(self): 1320 return self._interval 1321 1322 def _set_interval(self, interval): 1323 # Force to int since none of the backends actually support fractional 1324 # milliseconds, and some error or give warnings. 1325 interval = int(interval) 1326 self._interval = interval 1327 self._timer_set_interval() 1328 1329 interval = property(_get_interval, _set_interval) 1330 1331 def _get_single_shot(self): 1332 return self._single 1333 1334 def _set_single_shot(self, ss=True): 1335 self._single = ss 1336 self._timer_set_single_shot() 1337 1338 single_shot = property(_get_single_shot, _set_single_shot) 1339 1340 def add_callback(self, func, *args, **kwargs): 1341 ''' 1342 Register `func` to be called by timer when the event fires. Any 1343 additional arguments provided will be passed to `func`. 1344 ''' 1345 self.callbacks.append((func, args, kwargs)) 1346 1347 def remove_callback(self, func, *args, **kwargs): 1348 ''' 1349 Remove `func` from list of callbacks. `args` and `kwargs` are optional 1350 and used to distinguish between copies of the same function registered 1351 to be called with different arguments. 1352 ''' 1353 if args or kwargs: 1354 self.callbacks.remove((func, args, kwargs)) 1355 else: 1356 funcs = [c[0] for c in self.callbacks] 1357 if func in funcs: 1358 self.callbacks.pop(funcs.index(func)) 1359 1360 def _timer_set_interval(self): 1361 """Used to set interval on underlying timer object.""" 1362 1363 def _timer_set_single_shot(self): 1364 """Used to set single shot on underlying timer object.""" 1365 1366 def _on_timer(self): 1367 ''' 1368 Runs all function that have been registered as callbacks. Functions 1369 can return False (or 0) if they should not be called any more. If there 1370 are no callbacks, the timer is automatically stopped. 1371 ''' 1372 for func, args, kwargs in self.callbacks: 1373 ret = func(*args, **kwargs) 1374 # docstring above explains why we use `if ret == 0` here, 1375 # instead of `if not ret`. 1376 # This will also catch `ret == False` as `False == 0` 1377 # but does not annoy the linters 1378 # https://docs.python.org/3/library/stdtypes.html#boolean-values 1379 if ret == 0: 1380 self.callbacks.remove((func, args, kwargs)) 1381 1382 if len(self.callbacks) == 0: 1383 self.stop() 1384 1385 1386class Event(object): 1387 """ 1388 A matplotlib event. Attach additional attributes as defined in 1389 :meth:`FigureCanvasBase.mpl_connect`. The following attributes 1390 are defined and shown with their default values 1391 1392 Attributes 1393 ---------- 1394 name : str 1395 the event name 1396 1397 canvas : `FigureCanvasBase` 1398 the backend-specific canvas instance generating the event 1399 1400 guiEvent 1401 the GUI event that triggered the matplotlib event 1402 1403 """ 1404 def __init__(self, name, canvas, guiEvent=None): 1405 self.name = name 1406 self.canvas = canvas 1407 self.guiEvent = guiEvent 1408 1409 1410@cbook.deprecated("2.1") 1411class IdleEvent(Event): 1412 """ 1413 An event triggered by the GUI backend when it is idle -- useful 1414 for passive animation 1415 """ 1416 1417 1418class DrawEvent(Event): 1419 """ 1420 An event triggered by a draw operation on the canvas 1421 1422 In most backends callbacks subscribed to this callback will be 1423 fired after the rendering is complete but before the screen is 1424 updated. Any extra artists drawn to the canvas's renderer will 1425 be reflected without an explicit call to ``blit``. 1426 1427 .. warning :: 1428 1429 Calling ``canvas.draw`` and ``canvas.blit`` in these callbacks may 1430 not be safe with all backends and may cause infinite recursion. 1431 1432 In addition to the :class:`Event` attributes, the following event 1433 attributes are defined: 1434 1435 Attributes 1436 ---------- 1437 renderer : `RendererBase` 1438 the renderer for the draw event 1439 1440 """ 1441 def __init__(self, name, canvas, renderer): 1442 Event.__init__(self, name, canvas) 1443 self.renderer = renderer 1444 1445 1446class ResizeEvent(Event): 1447 """ 1448 An event triggered by a canvas resize 1449 1450 In addition to the :class:`Event` attributes, the following event 1451 attributes are defined: 1452 1453 Attributes 1454 ---------- 1455 width : scalar 1456 width of the canvas in pixels 1457 1458 height : scalar 1459 height of the canvas in pixels 1460 1461 """ 1462 def __init__(self, name, canvas): 1463 Event.__init__(self, name, canvas) 1464 self.width, self.height = canvas.get_width_height() 1465 1466 1467class CloseEvent(Event): 1468 """ 1469 An event triggered by a figure being closed 1470 1471 """ 1472 def __init__(self, name, canvas, guiEvent=None): 1473 Event.__init__(self, name, canvas, guiEvent) 1474 1475 1476class LocationEvent(Event): 1477 """ 1478 An event that has a screen location 1479 1480 The following additional attributes are defined and shown with 1481 their default values. 1482 1483 In addition to the :class:`Event` attributes, the following 1484 event attributes are defined: 1485 1486 Attributes 1487 ---------- 1488 x : scalar 1489 x position - pixels from left of canvas 1490 1491 y : scalar 1492 y position - pixels from bottom of canvas 1493 1494 inaxes : bool 1495 the :class:`~matplotlib.axes.Axes` instance if mouse is over axes 1496 1497 xdata : scalar 1498 x coord of mouse in data coords 1499 1500 ydata : scalar 1501 y coord of mouse in data coords 1502 1503 """ 1504 x = None # x position - pixels from left of canvas 1505 y = None # y position - pixels from right of canvas 1506 inaxes = None # the Axes instance if mouse us over axes 1507 xdata = None # x coord of mouse in data coords 1508 ydata = None # y coord of mouse in data coords 1509 1510 # the last event that was triggered before this one 1511 lastevent = None 1512 1513 def __init__(self, name, canvas, x, y, guiEvent=None): 1514 """ 1515 *x*, *y* in figure coords, 0,0 = bottom, left 1516 """ 1517 Event.__init__(self, name, canvas, guiEvent=guiEvent) 1518 self.x = x 1519 self.y = y 1520 1521 if x is None or y is None: 1522 # cannot check if event was in axes if no x,y info 1523 self.inaxes = None 1524 self._update_enter_leave() 1525 return 1526 1527 # Find all axes containing the mouse 1528 if self.canvas.mouse_grabber is None: 1529 axes_list = [a for a in self.canvas.figure.get_axes() 1530 if a.in_axes(self)] 1531 else: 1532 axes_list = [self.canvas.mouse_grabber] 1533 1534 if axes_list: 1535 self.inaxes = cbook._topmost_artist(axes_list) 1536 try: 1537 trans = self.inaxes.transData.inverted() 1538 xdata, ydata = trans.transform_point((x, y)) 1539 except ValueError: 1540 self.xdata = None 1541 self.ydata = None 1542 else: 1543 self.xdata = xdata 1544 self.ydata = ydata 1545 else: 1546 self.inaxes = None 1547 1548 self._update_enter_leave() 1549 1550 def _update_enter_leave(self): 1551 'process the figure/axes enter leave events' 1552 if LocationEvent.lastevent is not None: 1553 last = LocationEvent.lastevent 1554 if last.inaxes != self.inaxes: 1555 # process axes enter/leave events 1556 try: 1557 if last.inaxes is not None: 1558 last.canvas.callbacks.process('axes_leave_event', last) 1559 except: 1560 pass 1561 # See ticket 2901582. 1562 # I think this is a valid exception to the rule 1563 # against catching all exceptions; if anything goes 1564 # wrong, we simply want to move on and process the 1565 # current event. 1566 if self.inaxes is not None: 1567 self.canvas.callbacks.process('axes_enter_event', self) 1568 1569 else: 1570 # process a figure enter event 1571 if self.inaxes is not None: 1572 self.canvas.callbacks.process('axes_enter_event', self) 1573 1574 LocationEvent.lastevent = self 1575 1576 1577class MouseEvent(LocationEvent): 1578 """ 1579 A mouse event ('button_press_event', 1580 'button_release_event', 1581 'scroll_event', 1582 'motion_notify_event'). 1583 1584 In addition to the :class:`Event` and :class:`LocationEvent` 1585 attributes, the following attributes are defined: 1586 1587 Attributes 1588 ---------- 1589 button : None, scalar, or str 1590 button pressed None, 1, 2, 3, 'up', 'down' (up and down are used 1591 for scroll events). Note that in the nbagg backend, both the 1592 middle and right clicks return 3 since right clicking will bring 1593 up the context menu in some browsers. 1594 1595 key : None, or str 1596 the key depressed when the mouse event triggered (see 1597 :class:`KeyEvent`) 1598 1599 step : scalar 1600 number of scroll steps (positive for 'up', negative for 'down') 1601 1602 Examples 1603 -------- 1604 Usage:: 1605 1606 def on_press(event): 1607 print('you pressed', event.button, event.xdata, event.ydata) 1608 1609 cid = fig.canvas.mpl_connect('button_press_event', on_press) 1610 1611 """ 1612 x = None # x position - pixels from left of canvas 1613 y = None # y position - pixels from right of canvas 1614 button = None # button pressed None, 1, 2, 3 1615 dblclick = None # whether or not the event is the result of a double click 1616 inaxes = None # the Axes instance if mouse us over axes 1617 xdata = None # x coord of mouse in data coords 1618 ydata = None # y coord of mouse in data coords 1619 step = None # scroll steps for scroll events 1620 1621 def __init__(self, name, canvas, x, y, button=None, key=None, 1622 step=0, dblclick=False, guiEvent=None): 1623 """ 1624 x, y in figure coords, 0,0 = bottom, left 1625 button pressed None, 1, 2, 3, 'up', 'down' 1626 """ 1627 LocationEvent.__init__(self, name, canvas, x, y, guiEvent=guiEvent) 1628 self.button = button 1629 self.key = key 1630 self.step = step 1631 self.dblclick = dblclick 1632 1633 def __str__(self): 1634 return ("MPL MouseEvent: xy=(%d,%d) xydata=(%s,%s) button=%s " + 1635 "dblclick=%s inaxes=%s") % (self.x, self.y, self.xdata, 1636 self.ydata, self.button, 1637 self.dblclick, self.inaxes) 1638 1639 1640class PickEvent(Event): 1641 """ 1642 a pick event, fired when the user picks a location on the canvas 1643 sufficiently close to an artist. 1644 1645 Attrs: all the :class:`Event` attributes plus 1646 1647 Attributes 1648 ---------- 1649 mouseevent : `MouseEvent` 1650 the mouse event that generated the pick 1651 1652 artist : `matplotlib.artist.Artist` 1653 the picked artist 1654 1655 other 1656 extra class dependent attrs -- e.g., a 1657 :class:`~matplotlib.lines.Line2D` pick may define different 1658 extra attributes than a 1659 :class:`~matplotlib.collections.PatchCollection` pick event 1660 1661 Examples 1662 -------- 1663 Usage:: 1664 1665 ax.plot(np.rand(100), 'o', picker=5) # 5 points tolerance 1666 1667 def on_pick(event): 1668 line = event.artist 1669 xdata, ydata = line.get_data() 1670 ind = event.ind 1671 print('on pick line:', np.array([xdata[ind], ydata[ind]]).T) 1672 1673 cid = fig.canvas.mpl_connect('pick_event', on_pick) 1674 1675 """ 1676 def __init__(self, name, canvas, mouseevent, artist, 1677 guiEvent=None, **kwargs): 1678 Event.__init__(self, name, canvas, guiEvent) 1679 self.mouseevent = mouseevent 1680 self.artist = artist 1681 self.__dict__.update(kwargs) 1682 1683 1684class KeyEvent(LocationEvent): 1685 """ 1686 A key event (key press, key release). 1687 1688 Attach additional attributes as defined in 1689 :meth:`FigureCanvasBase.mpl_connect`. 1690 1691 In addition to the :class:`Event` and :class:`LocationEvent` 1692 attributes, the following attributes are defined: 1693 1694 Attributes 1695 ---------- 1696 key : None or str 1697 the key(s) pressed. Could be **None**, a single case sensitive ascii 1698 character ("g", "G", "#", etc.), a special key 1699 ("control", "shift", "f1", "up", etc.) or a 1700 combination of the above (e.g., "ctrl+alt+g", "ctrl+alt+G"). 1701 1702 Notes 1703 ----- 1704 Modifier keys will be prefixed to the pressed key and will be in the order 1705 "ctrl", "alt", "super". The exception to this rule is when the pressed key 1706 is itself a modifier key, therefore "ctrl+alt" and "alt+control" can both 1707 be valid key values. 1708 1709 Examples 1710 -------- 1711 Usage:: 1712 1713 def on_key(event): 1714 print('you pressed', event.key, event.xdata, event.ydata) 1715 1716 cid = fig.canvas.mpl_connect('key_press_event', on_key) 1717 1718 """ 1719 def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): 1720 LocationEvent.__init__(self, name, canvas, x, y, guiEvent=guiEvent) 1721 self.key = key 1722 1723 1724class FigureCanvasBase(object): 1725 """ 1726 The canvas the figure renders into. 1727 1728 Public attributes 1729 1730 Attributes 1731 ---------- 1732 figure : `matplotlib.figure.Figure` 1733 A high-level figure instance 1734 1735 """ 1736 events = [ 1737 'resize_event', 1738 'draw_event', 1739 'key_press_event', 1740 'key_release_event', 1741 'button_press_event', 1742 'button_release_event', 1743 'scroll_event', 1744 'motion_notify_event', 1745 'pick_event', 1746 'idle_event', 1747 'figure_enter_event', 1748 'figure_leave_event', 1749 'axes_enter_event', 1750 'axes_leave_event', 1751 'close_event' 1752 ] 1753 1754 supports_blit = True 1755 fixed_dpi = None 1756 1757 filetypes = _default_filetypes 1758 if _has_pil: 1759 # JPEG support 1760 register_backend('jpg', 'matplotlib.backends.backend_agg', 1761 'Joint Photographic Experts Group') 1762 register_backend('jpeg', 'matplotlib.backends.backend_agg', 1763 'Joint Photographic Experts Group') 1764 # TIFF support 1765 register_backend('tif', 'matplotlib.backends.backend_agg', 1766 'Tagged Image File Format') 1767 register_backend('tiff', 'matplotlib.backends.backend_agg', 1768 'Tagged Image File Format') 1769 1770 def __init__(self, figure): 1771 self._is_idle_drawing = True 1772 self._is_saving = False 1773 figure.set_canvas(self) 1774 self.figure = figure 1775 # a dictionary from event name to a dictionary that maps cid->func 1776 self.callbacks = cbook.CallbackRegistry() 1777 self.widgetlock = widgets.LockDraw() 1778 self._button = None # the button pressed 1779 self._key = None # the key pressed 1780 self._lastx, self._lasty = None, None 1781 self.button_pick_id = self.mpl_connect('button_press_event', self.pick) 1782 self.scroll_pick_id = self.mpl_connect('scroll_event', self.pick) 1783 self.mouse_grabber = None # the axes currently grabbing mouse 1784 self.toolbar = None # NavigationToolbar2 will set me 1785 self._is_idle_drawing = False 1786 1787 @contextmanager 1788 def _idle_draw_cntx(self): 1789 self._is_idle_drawing = True 1790 yield 1791 self._is_idle_drawing = False 1792 1793 def is_saving(self): 1794 """ 1795 Returns whether the renderer is in the process of saving 1796 to a file, rather than rendering for an on-screen buffer. 1797 """ 1798 return self._is_saving 1799 1800 @cbook.deprecated("2.2") 1801 def onRemove(self, ev): 1802 """ 1803 Mouse event processor which removes the top artist 1804 under the cursor. Connect this to the 'mouse_press_event' 1805 using:: 1806 1807 canvas.mpl_connect('mouse_press_event',canvas.onRemove) 1808 """ 1809 # Find the top artist under the cursor 1810 under = cbook._topmost_artist(self.figure.hitlist(ev)) 1811 h = None 1812 if under: 1813 h = under[-1] 1814 1815 # Try deleting that artist, or its parent if you 1816 # can't delete the artist 1817 while h: 1818 if h.remove(): 1819 self.draw_idle() 1820 break 1821 parent = None 1822 for p in under: 1823 if h in p.get_children(): 1824 parent = p 1825 break 1826 h = parent 1827 1828 def pick(self, mouseevent): 1829 if not self.widgetlock.locked(): 1830 self.figure.pick(mouseevent) 1831 1832 def blit(self, bbox=None): 1833 """Blit the canvas in bbox (default entire canvas).""" 1834 1835 def resize(self, w, h): 1836 """Set the canvas size in pixels.""" 1837 1838 def draw_event(self, renderer): 1839 """Pass a `DrawEvent` to all functions connected to ``draw_event``.""" 1840 s = 'draw_event' 1841 event = DrawEvent(s, self, renderer) 1842 self.callbacks.process(s, event) 1843 1844 def resize_event(self): 1845 """Pass a `ResizeEvent` to all functions connected to ``resize_event``. 1846 """ 1847 s = 'resize_event' 1848 event = ResizeEvent(s, self) 1849 self.callbacks.process(s, event) 1850 self.draw_idle() 1851 1852 def close_event(self, guiEvent=None): 1853 """Pass a `CloseEvent` to all functions connected to ``close_event``. 1854 """ 1855 s = 'close_event' 1856 try: 1857 event = CloseEvent(s, self, guiEvent=guiEvent) 1858 self.callbacks.process(s, event) 1859 except (TypeError, AttributeError): 1860 pass 1861 # Suppress the TypeError when the python session is being killed. 1862 # It may be that a better solution would be a mechanism to 1863 # disconnect all callbacks upon shutdown. 1864 # AttributeError occurs on OSX with qt4agg upon exiting 1865 # with an open window; 'callbacks' attribute no longer exists. 1866 1867 def key_press_event(self, key, guiEvent=None): 1868 """Pass a `KeyEvent` to all functions connected to ``key_press_event``. 1869 """ 1870 self._key = key 1871 s = 'key_press_event' 1872 event = KeyEvent( 1873 s, self, key, self._lastx, self._lasty, guiEvent=guiEvent) 1874 self.callbacks.process(s, event) 1875 1876 def key_release_event(self, key, guiEvent=None): 1877 """ 1878 Pass a `KeyEvent` to all functions connected to ``key_release_event``. 1879 """ 1880 s = 'key_release_event' 1881 event = KeyEvent( 1882 s, self, key, self._lastx, self._lasty, guiEvent=guiEvent) 1883 self.callbacks.process(s, event) 1884 self._key = None 1885 1886 def pick_event(self, mouseevent, artist, **kwargs): 1887 """ 1888 This method will be called by artists who are picked and will 1889 fire off :class:`PickEvent` callbacks registered listeners 1890 """ 1891 s = 'pick_event' 1892 event = PickEvent(s, self, mouseevent, artist, 1893 guiEvent=mouseevent.guiEvent, 1894 **kwargs) 1895 self.callbacks.process(s, event) 1896 1897 def scroll_event(self, x, y, step, guiEvent=None): 1898 """ 1899 Backend derived classes should call this function on any 1900 scroll wheel event. x,y are the canvas coords: 0,0 is lower, 1901 left. button and key are as defined in MouseEvent. 1902 1903 This method will be call all functions connected to the 1904 'scroll_event' with a :class:`MouseEvent` instance. 1905 """ 1906 if step >= 0: 1907 self._button = 'up' 1908 else: 1909 self._button = 'down' 1910 s = 'scroll_event' 1911 mouseevent = MouseEvent(s, self, x, y, self._button, self._key, 1912 step=step, guiEvent=guiEvent) 1913 self.callbacks.process(s, mouseevent) 1914 1915 def button_press_event(self, x, y, button, dblclick=False, guiEvent=None): 1916 """ 1917 Backend derived classes should call this function on any mouse 1918 button press. x,y are the canvas coords: 0,0 is lower, left. 1919 button and key are as defined in :class:`MouseEvent`. 1920 1921 This method will be call all functions connected to the 1922 'button_press_event' with a :class:`MouseEvent` instance. 1923 """ 1924 self._button = button 1925 s = 'button_press_event' 1926 mouseevent = MouseEvent(s, self, x, y, button, self._key, 1927 dblclick=dblclick, guiEvent=guiEvent) 1928 self.callbacks.process(s, mouseevent) 1929 1930 def button_release_event(self, x, y, button, guiEvent=None): 1931 """ 1932 Backend derived classes should call this function on any mouse 1933 button release. 1934 1935 This method will call all functions connected to the 1936 'button_release_event' with a :class:`MouseEvent` instance. 1937 1938 Parameters 1939 ---------- 1940 x : scalar 1941 the canvas coordinates where 0=left 1942 1943 y : scalar 1944 the canvas coordinates where 0=bottom 1945 1946 guiEvent 1947 the native UI event that generated the mpl event 1948 1949 """ 1950 s = 'button_release_event' 1951 event = MouseEvent(s, self, x, y, button, self._key, guiEvent=guiEvent) 1952 self.callbacks.process(s, event) 1953 self._button = None 1954 1955 def motion_notify_event(self, x, y, guiEvent=None): 1956 """ 1957 Backend derived classes should call this function on any 1958 motion-notify-event. 1959 1960 This method will call all functions connected to the 1961 'motion_notify_event' with a :class:`MouseEvent` instance. 1962 1963 Parameters 1964 ---------- 1965 x : scalar 1966 the canvas coordinates where 0=left 1967 1968 y : scalar 1969 the canvas coordinates where 0=bottom 1970 1971 guiEvent 1972 the native UI event that generated the mpl event 1973 1974 """ 1975 self._lastx, self._lasty = x, y 1976 s = 'motion_notify_event' 1977 event = MouseEvent(s, self, x, y, self._button, self._key, 1978 guiEvent=guiEvent) 1979 self.callbacks.process(s, event) 1980 1981 def leave_notify_event(self, guiEvent=None): 1982 """ 1983 Backend derived classes should call this function when leaving 1984 canvas 1985 1986 Parameters 1987 ---------- 1988 guiEvent 1989 the native UI event that generated the mpl event 1990 1991 """ 1992 1993 self.callbacks.process('figure_leave_event', LocationEvent.lastevent) 1994 LocationEvent.lastevent = None 1995 self._lastx, self._lasty = None, None 1996 1997 def enter_notify_event(self, guiEvent=None, xy=None): 1998 """ 1999 Backend derived classes should call this function when entering 2000 canvas 2001 2002 Parameters 2003 ---------- 2004 guiEvent 2005 the native UI event that generated the mpl event 2006 xy : tuple of 2 scalars 2007 the coordinate location of the pointer when the canvas is 2008 entered 2009 2010 """ 2011 if xy is not None: 2012 x, y = xy 2013 self._lastx, self._lasty = x, y 2014 2015 event = Event('figure_enter_event', self, guiEvent) 2016 self.callbacks.process('figure_enter_event', event) 2017 2018 @cbook.deprecated("2.1") 2019 def idle_event(self, guiEvent=None): 2020 """Called when GUI is idle.""" 2021 s = 'idle_event' 2022 event = IdleEvent(s, self, guiEvent=guiEvent) 2023 self.callbacks.process(s, event) 2024 2025 def grab_mouse(self, ax): 2026 """ 2027 Set the child axes which are currently grabbing the mouse events. 2028 Usually called by the widgets themselves. 2029 It is an error to call this if the mouse is already grabbed by 2030 another axes. 2031 """ 2032 if self.mouse_grabber not in (None, ax): 2033 raise RuntimeError("Another Axes already grabs mouse input") 2034 self.mouse_grabber = ax 2035 2036 def release_mouse(self, ax): 2037 """ 2038 Release the mouse grab held by the axes, ax. 2039 Usually called by the widgets. 2040 It is ok to call this even if you ax doesn't have the mouse 2041 grab currently. 2042 """ 2043 if self.mouse_grabber is ax: 2044 self.mouse_grabber = None 2045 2046 def draw(self, *args, **kwargs): 2047 """Render the :class:`~matplotlib.figure.Figure`.""" 2048 2049 def draw_idle(self, *args, **kwargs): 2050 """ 2051 :meth:`draw` only if idle; defaults to draw but backends can override 2052 """ 2053 if not self._is_idle_drawing: 2054 with self._idle_draw_cntx(): 2055 self.draw(*args, **kwargs) 2056 2057 def draw_cursor(self, event): 2058 """ 2059 Draw a cursor in the event.axes if inaxes is not None. Use 2060 native GUI drawing for efficiency if possible 2061 """ 2062 2063 def get_width_height(self): 2064 """ 2065 Return the figure width and height in points or pixels 2066 (depending on the backend), truncated to integers 2067 """ 2068 return int(self.figure.bbox.width), int(self.figure.bbox.height) 2069 2070 @classmethod 2071 def get_supported_filetypes(cls): 2072 """Return dict of savefig file formats supported by this backend""" 2073 return cls.filetypes 2074 2075 @classmethod 2076 def get_supported_filetypes_grouped(cls): 2077 """Return a dict of savefig file formats supported by this backend, 2078 where the keys are a file type name, such as 'Joint Photographic 2079 Experts Group', and the values are a list of filename extensions used 2080 for that filetype, such as ['jpg', 'jpeg'].""" 2081 groupings = {} 2082 for ext, name in six.iteritems(cls.filetypes): 2083 groupings.setdefault(name, []).append(ext) 2084 groupings[name].sort() 2085 return groupings 2086 2087 def _get_output_canvas(self, fmt): 2088 """ 2089 Return a canvas suitable for saving figures to a specified file format. 2090 2091 If necessary, this function will switch to a registered backend that 2092 supports the format. 2093 """ 2094 method_name = 'print_%s' % fmt 2095 # Return the current canvas if it supports the requested format. 2096 if hasattr(self, method_name): 2097 return self 2098 # Return a default canvas for the requested format, if it exists. 2099 canvas_class = get_registered_canvas_class(fmt) 2100 if canvas_class: 2101 return self.switch_backends(canvas_class) 2102 # Else report error for unsupported format. 2103 raise ValueError( 2104 "Format {!r} is not supported (supported formats: {})" 2105 .format(fmt, ", ".join(sorted(self.get_supported_filetypes())))) 2106 2107 def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None, 2108 orientation='portrait', format=None, **kwargs): 2109 """ 2110 Render the figure to hardcopy. Set the figure patch face and edge 2111 colors. This is useful because some of the GUIs have a gray figure 2112 face color background and you'll probably want to override this on 2113 hardcopy. 2114 2115 Parameters 2116 ---------- 2117 filename 2118 can also be a file object on image backends 2119 2120 orientation : {'landscape', 'portrait'}, optional 2121 only currently applies to PostScript printing. 2122 2123 dpi : scalar, optional 2124 the dots per inch to save the figure in; if None, use savefig.dpi 2125 2126 facecolor : color spec or None, optional 2127 the facecolor of the figure; if None, defaults to savefig.facecolor 2128 2129 edgecolor : color spec or None, optional 2130 the edgecolor of the figure; if None, defaults to savefig.edgecolor 2131 2132 format : str, optional 2133 when set, forcibly set the file format to save to 2134 2135 bbox_inches : str or `~matplotlib.transforms.Bbox`, optional 2136 Bbox in inches. Only the given portion of the figure is 2137 saved. If 'tight', try to figure out the tight bbox of 2138 the figure. If None, use savefig.bbox 2139 2140 pad_inches : scalar, optional 2141 Amount of padding around the figure when bbox_inches is 2142 'tight'. If None, use savefig.pad_inches 2143 2144 bbox_extra_artists : list of `~matplotlib.artist.Artist`, optional 2145 A list of extra artists that will be considered when the 2146 tight bbox is calculated. 2147 2148 """ 2149 self._is_saving = True 2150 # Remove the figure manager, if any, to avoid resizing the GUI widget. 2151 # Having *no* manager and a *None* manager are currently different (see 2152 # Figure.show); should probably be normalized to None at some point. 2153 _no_manager = object() 2154 if hasattr(self, 'manager'): 2155 manager = self.manager 2156 del self.manager 2157 else: 2158 manager = _no_manager 2159 2160 if format is None: 2161 # get format from filename, or from backend's default filetype 2162 if isinstance(filename, getattr(os, "PathLike", ())): 2163 filename = os.fspath(filename) 2164 if isinstance(filename, six.string_types): 2165 format = os.path.splitext(filename)[1][1:] 2166 if format is None or format == '': 2167 format = self.get_default_filetype() 2168 if isinstance(filename, six.string_types): 2169 filename = filename.rstrip('.') + '.' + format 2170 format = format.lower() 2171 2172 # get canvas object and print method for format 2173 canvas = self._get_output_canvas(format) 2174 print_method = getattr(canvas, 'print_%s' % format) 2175 2176 if dpi is None: 2177 dpi = rcParams['savefig.dpi'] 2178 2179 if dpi == 'figure': 2180 dpi = getattr(self.figure, '_original_dpi', self.figure.dpi) 2181 2182 if facecolor is None: 2183 facecolor = rcParams['savefig.facecolor'] 2184 if edgecolor is None: 2185 edgecolor = rcParams['savefig.edgecolor'] 2186 2187 origDPI = self.figure.dpi 2188 origfacecolor = self.figure.get_facecolor() 2189 origedgecolor = self.figure.get_edgecolor() 2190 2191 self.figure.dpi = dpi 2192 self.figure.set_facecolor(facecolor) 2193 self.figure.set_edgecolor(edgecolor) 2194 2195 bbox_inches = kwargs.pop("bbox_inches", None) 2196 if bbox_inches is None: 2197 bbox_inches = rcParams['savefig.bbox'] 2198 2199 if bbox_inches: 2200 # call adjust_bbox to save only the given area 2201 if bbox_inches == "tight": 2202 # When bbox_inches == "tight", it saves the figure twice. The 2203 # first save command (to a BytesIO) is just to estimate the 2204 # bounding box of the figure. 2205 result = print_method( 2206 io.BytesIO(), 2207 dpi=dpi, 2208 facecolor=facecolor, 2209 edgecolor=edgecolor, 2210 orientation=orientation, 2211 dryrun=True, 2212 **kwargs) 2213 renderer = self.figure._cachedRenderer 2214 bbox_inches = self.figure.get_tightbbox(renderer) 2215 2216 bbox_artists = kwargs.pop("bbox_extra_artists", None) 2217 if bbox_artists is None: 2218 bbox_artists = self.figure.get_default_bbox_extra_artists() 2219 2220 bbox_filtered = [] 2221 for a in bbox_artists: 2222 bbox = a.get_window_extent(renderer) 2223 if a.get_clip_on(): 2224 clip_box = a.get_clip_box() 2225 if clip_box is not None: 2226 bbox = Bbox.intersection(bbox, clip_box) 2227 clip_path = a.get_clip_path() 2228 if clip_path is not None and bbox is not None: 2229 clip_path = clip_path.get_fully_transformed_path() 2230 bbox = Bbox.intersection(bbox, 2231 clip_path.get_extents()) 2232 if bbox is not None and (bbox.width != 0 or 2233 bbox.height != 0): 2234 bbox_filtered.append(bbox) 2235 2236 if bbox_filtered: 2237 _bbox = Bbox.union(bbox_filtered) 2238 trans = Affine2D().scale(1.0 / self.figure.dpi) 2239 bbox_extra = TransformedBbox(_bbox, trans) 2240 bbox_inches = Bbox.union([bbox_inches, bbox_extra]) 2241 2242 pad = kwargs.pop("pad_inches", None) 2243 if pad is None: 2244 pad = rcParams['savefig.pad_inches'] 2245 2246 bbox_inches = bbox_inches.padded(pad) 2247 2248 restore_bbox = tight_bbox.adjust_bbox(self.figure, bbox_inches, 2249 canvas.fixed_dpi) 2250 2251 _bbox_inches_restore = (bbox_inches, restore_bbox) 2252 else: 2253 _bbox_inches_restore = None 2254 2255 try: 2256 result = print_method( 2257 filename, 2258 dpi=dpi, 2259 facecolor=facecolor, 2260 edgecolor=edgecolor, 2261 orientation=orientation, 2262 bbox_inches_restore=_bbox_inches_restore, 2263 **kwargs) 2264 finally: 2265 if bbox_inches and restore_bbox: 2266 restore_bbox() 2267 2268 self.figure.dpi = origDPI 2269 self.figure.set_facecolor(origfacecolor) 2270 self.figure.set_edgecolor(origedgecolor) 2271 self.figure.set_canvas(self) 2272 if manager is not _no_manager: 2273 self.manager = manager 2274 self._is_saving = False 2275 return result 2276 2277 @classmethod 2278 def get_default_filetype(cls): 2279 """ 2280 Get the default savefig file format as specified in rcParam 2281 ``savefig.format``. Returned string excludes period. Overridden 2282 in backends that only support a single file type. 2283 """ 2284 return rcParams['savefig.format'] 2285 2286 def get_window_title(self): 2287 """ 2288 Get the title text of the window containing the figure. 2289 Return None if there is no window (e.g., a PS backend). 2290 """ 2291 if hasattr(self, "manager"): 2292 return self.manager.get_window_title() 2293 2294 def set_window_title(self, title): 2295 """ 2296 Set the title text of the window containing the figure. Note that 2297 this has no effect if there is no window (e.g., a PS backend). 2298 """ 2299 if hasattr(self, "manager"): 2300 self.manager.set_window_title(title) 2301 2302 def get_default_filename(self): 2303 """ 2304 Return a string, which includes extension, suitable for use as 2305 a default filename. 2306 """ 2307 default_basename = self.get_window_title() or 'image' 2308 default_basename = default_basename.replace(' ', '_') 2309 default_filetype = self.get_default_filetype() 2310 default_filename = default_basename + '.' + default_filetype 2311 2312 save_dir = os.path.expanduser(rcParams['savefig.directory']) 2313 2314 # ensure non-existing filename in save dir 2315 i = 1 2316 while os.path.isfile(os.path.join(save_dir, default_filename)): 2317 # attach numerical count to basename 2318 default_filename = ( 2319 '{}-{}.{}'.format(default_basename, i, default_filetype)) 2320 i += 1 2321 2322 return default_filename 2323 2324 def switch_backends(self, FigureCanvasClass): 2325 """ 2326 Instantiate an instance of FigureCanvasClass 2327 2328 This is used for backend switching, e.g., to instantiate a 2329 FigureCanvasPS from a FigureCanvasGTK. Note, deep copying is 2330 not done, so any changes to one of the instances (e.g., setting 2331 figure size or line props), will be reflected in the other 2332 """ 2333 newCanvas = FigureCanvasClass(self.figure) 2334 newCanvas._is_saving = self._is_saving 2335 return newCanvas 2336 2337 def mpl_connect(self, s, func): 2338 """ 2339 Connect event with string *s* to *func*. The signature of *func* is:: 2340 2341 def func(event) 2342 2343 where event is a :class:`matplotlib.backend_bases.Event`. The 2344 following events are recognized 2345 2346 - 'button_press_event' 2347 - 'button_release_event' 2348 - 'draw_event' 2349 - 'key_press_event' 2350 - 'key_release_event' 2351 - 'motion_notify_event' 2352 - 'pick_event' 2353 - 'resize_event' 2354 - 'scroll_event' 2355 - 'figure_enter_event', 2356 - 'figure_leave_event', 2357 - 'axes_enter_event', 2358 - 'axes_leave_event' 2359 - 'close_event' 2360 2361 For the location events (button and key press/release), if the 2362 mouse is over the axes, the variable ``event.inaxes`` will be 2363 set to the :class:`~matplotlib.axes.Axes` the event occurs is 2364 over, and additionally, the variables ``event.xdata`` and 2365 ``event.ydata`` will be defined. This is the mouse location 2366 in data coords. See 2367 :class:`~matplotlib.backend_bases.KeyEvent` and 2368 :class:`~matplotlib.backend_bases.MouseEvent` for more info. 2369 2370 Return value is a connection id that can be used with 2371 :meth:`~matplotlib.backend_bases.Event.mpl_disconnect`. 2372 2373 Examples 2374 -------- 2375 Usage:: 2376 2377 def on_press(event): 2378 print('you pressed', event.button, event.xdata, event.ydata) 2379 2380 cid = canvas.mpl_connect('button_press_event', on_press) 2381 2382 """ 2383 if s == 'idle_event': 2384 cbook.warn_deprecated(1.5, 2385 "idle_event is only implemented for the wx backend, and will " 2386 "be removed in matplotlib 2.1. Use the animations module " 2387 "instead.") 2388 2389 return self.callbacks.connect(s, func) 2390 2391 def mpl_disconnect(self, cid): 2392 """ 2393 Disconnect callback id cid 2394 2395 Examples 2396 -------- 2397 Usage:: 2398 2399 cid = canvas.mpl_connect('button_press_event', on_press) 2400 #...later 2401 canvas.mpl_disconnect(cid) 2402 """ 2403 return self.callbacks.disconnect(cid) 2404 2405 def new_timer(self, *args, **kwargs): 2406 """ 2407 Creates a new backend-specific subclass of 2408 :class:`backend_bases.Timer`. This is useful for getting periodic 2409 events through the backend's native event loop. Implemented only for 2410 backends with GUIs. 2411 2412 Other Parameters 2413 ---------------- 2414 interval : scalar 2415 Timer interval in milliseconds 2416 2417 callbacks : List[Tuple[callable, Tuple, Dict]] 2418 Sequence of (func, args, kwargs) where ``func(*args, **kwargs)`` 2419 will be executed by the timer every *interval*. 2420 2421 callbacks which return ``False`` or ``0`` will be removed from the 2422 timer. 2423 2424 Examples 2425 -------- 2426 2427 >>> timer = fig.canvas.new_timer(callbacks=[(f1, (1, ), {'a': 3}),]) 2428 2429 """ 2430 return TimerBase(*args, **kwargs) 2431 2432 def flush_events(self): 2433 """Flush the GUI events for the figure. 2434 2435 Interactive backends need to reimplement this method. 2436 """ 2437 2438 def start_event_loop(self, timeout=0): 2439 """Start a blocking event loop. 2440 2441 Such an event loop is used by interactive functions, such as `ginput` 2442 and `waitforbuttonpress`, to wait for events. 2443 2444 The event loop blocks until a callback function triggers 2445 `stop_event_loop`, or *timeout* is reached. 2446 2447 If *timeout* is negative, never timeout. 2448 2449 Only interactive backends need to reimplement this method and it relies 2450 on `flush_events` being properly implemented. 2451 2452 Interactive backends should implement this in a more native way. 2453 """ 2454 if timeout <= 0: 2455 timeout = np.inf 2456 timestep = 0.01 2457 counter = 0 2458 self._looping = True 2459 while self._looping and counter * timestep < timeout: 2460 self.flush_events() 2461 time.sleep(timestep) 2462 counter += 1 2463 2464 def stop_event_loop(self): 2465 """Stop the current blocking event loop. 2466 2467 Interactive backends need to reimplement this to match 2468 `start_event_loop` 2469 """ 2470 self._looping = False 2471 2472 start_event_loop_default = cbook.deprecated( 2473 "2.1", name="start_event_loop_default")(start_event_loop) 2474 stop_event_loop_default = cbook.deprecated( 2475 "2.1", name="stop_event_loop_default")(stop_event_loop) 2476 2477 2478def key_press_handler(event, canvas, toolbar=None): 2479 """ 2480 Implement the default mpl key bindings for the canvas and toolbar 2481 described at :ref:`key-event-handling` 2482 2483 Parameters 2484 ---------- 2485 event : :class:`KeyEvent` 2486 a key press/release event 2487 canvas : :class:`FigureCanvasBase` 2488 the backend-specific canvas instance 2489 toolbar : :class:`NavigationToolbar2` 2490 the navigation cursor toolbar 2491 2492 """ 2493 # these bindings happen whether you are over an axes or not 2494 2495 if event.key is None: 2496 return 2497 2498 # Load key-mappings from your matplotlibrc file. 2499 fullscreen_keys = rcParams['keymap.fullscreen'] 2500 home_keys = rcParams['keymap.home'] 2501 back_keys = rcParams['keymap.back'] 2502 forward_keys = rcParams['keymap.forward'] 2503 pan_keys = rcParams['keymap.pan'] 2504 zoom_keys = rcParams['keymap.zoom'] 2505 save_keys = rcParams['keymap.save'] 2506 quit_keys = rcParams['keymap.quit'] 2507 grid_keys = rcParams['keymap.grid'] 2508 grid_minor_keys = rcParams['keymap.grid_minor'] 2509 toggle_yscale_keys = rcParams['keymap.yscale'] 2510 toggle_xscale_keys = rcParams['keymap.xscale'] 2511 all_keys = rcParams['keymap.all_axes'] 2512 2513 # toggle fullscreen mode ('f', 'ctrl + f') 2514 if event.key in fullscreen_keys: 2515 try: 2516 canvas.manager.full_screen_toggle() 2517 except AttributeError: 2518 pass 2519 2520 # quit the figure (default key 'ctrl+w') 2521 if event.key in quit_keys: 2522 Gcf.destroy_fig(canvas.figure) 2523 2524 if toolbar is not None: 2525 # home or reset mnemonic (default key 'h', 'home' and 'r') 2526 if event.key in home_keys: 2527 toolbar.home() 2528 # forward / backward keys to enable left handed quick navigation 2529 # (default key for backward: 'left', 'backspace' and 'c') 2530 elif event.key in back_keys: 2531 toolbar.back() 2532 # (default key for forward: 'right' and 'v') 2533 elif event.key in forward_keys: 2534 toolbar.forward() 2535 # pan mnemonic (default key 'p') 2536 elif event.key in pan_keys: 2537 toolbar.pan() 2538 toolbar._set_cursor(event) 2539 # zoom mnemonic (default key 'o') 2540 elif event.key in zoom_keys: 2541 toolbar.zoom() 2542 toolbar._set_cursor(event) 2543 # saving current figure (default key 's') 2544 elif event.key in save_keys: 2545 toolbar.save_figure() 2546 2547 if event.inaxes is None: 2548 return 2549 2550 # these bindings require the mouse to be over an axes to trigger 2551 def _get_uniform_gridstate(ticks): 2552 # Return True/False if all grid lines are on or off, None if they are 2553 # not all in the same state. 2554 if all(tick.gridOn for tick in ticks): 2555 return True 2556 elif not any(tick.gridOn for tick in ticks): 2557 return False 2558 else: 2559 return None 2560 2561 ax = event.inaxes 2562 # toggle major grids in current axes (default key 'g') 2563 # Both here and below (for 'G'), we do nothing if *any* grid (major or 2564 # minor, x or y) is not in a uniform state, to avoid messing up user 2565 # customization. 2566 if (event.key in grid_keys 2567 # Exclude minor grids not in a uniform state. 2568 and None not in [_get_uniform_gridstate(ax.xaxis.minorTicks), 2569 _get_uniform_gridstate(ax.yaxis.minorTicks)]): 2570 x_state = _get_uniform_gridstate(ax.xaxis.majorTicks) 2571 y_state = _get_uniform_gridstate(ax.yaxis.majorTicks) 2572 cycle = [(False, False), (True, False), (True, True), (False, True)] 2573 try: 2574 x_state, y_state = ( 2575 cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)]) 2576 except ValueError: 2577 # Exclude major grids not in a uniform state. 2578 pass 2579 else: 2580 # If turning major grids off, also turn minor grids off. 2581 ax.grid(x_state, which="major" if x_state else "both", axis="x") 2582 ax.grid(y_state, which="major" if y_state else "both", axis="y") 2583 canvas.draw_idle() 2584 # toggle major and minor grids in current axes (default key 'G') 2585 if (event.key in grid_minor_keys 2586 # Exclude major grids not in a uniform state. 2587 and None not in [_get_uniform_gridstate(ax.xaxis.majorTicks), 2588 _get_uniform_gridstate(ax.yaxis.majorTicks)]): 2589 x_state = _get_uniform_gridstate(ax.xaxis.minorTicks) 2590 y_state = _get_uniform_gridstate(ax.yaxis.minorTicks) 2591 cycle = [(False, False), (True, False), (True, True), (False, True)] 2592 try: 2593 x_state, y_state = ( 2594 cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)]) 2595 except ValueError: 2596 # Exclude minor grids not in a uniform state. 2597 pass 2598 else: 2599 ax.grid(x_state, which="both", axis="x") 2600 ax.grid(y_state, which="both", axis="y") 2601 canvas.draw_idle() 2602 # toggle scaling of y-axes between 'log and 'linear' (default key 'l') 2603 elif event.key in toggle_yscale_keys: 2604 scale = ax.get_yscale() 2605 if scale == 'log': 2606 ax.set_yscale('linear') 2607 ax.figure.canvas.draw_idle() 2608 elif scale == 'linear': 2609 try: 2610 ax.set_yscale('log') 2611 except ValueError as exc: 2612 warnings.warn(str(exc)) 2613 ax.set_yscale('linear') 2614 ax.figure.canvas.draw_idle() 2615 # toggle scaling of x-axes between 'log and 'linear' (default key 'k') 2616 elif event.key in toggle_xscale_keys: 2617 scalex = ax.get_xscale() 2618 if scalex == 'log': 2619 ax.set_xscale('linear') 2620 ax.figure.canvas.draw_idle() 2621 elif scalex == 'linear': 2622 try: 2623 ax.set_xscale('log') 2624 except ValueError as exc: 2625 warnings.warn(str(exc)) 2626 ax.set_xscale('linear') 2627 ax.figure.canvas.draw_idle() 2628 2629 elif (event.key.isdigit() and event.key != '0') or event.key in all_keys: 2630 # keys in list 'all' enables all axes (default key 'a'), 2631 # otherwise if key is a number only enable this particular axes 2632 # if it was the axes, where the event was raised 2633 if not (event.key in all_keys): 2634 n = int(event.key) - 1 2635 for i, a in enumerate(canvas.figure.get_axes()): 2636 # consider axes, in which the event was raised 2637 # FIXME: Why only this axes? 2638 if event.x is not None and event.y is not None \ 2639 and a.in_axes(event): 2640 if event.key in all_keys: 2641 a.set_navigate(True) 2642 else: 2643 a.set_navigate(i == n) 2644 2645 2646class NonGuiException(Exception): 2647 pass 2648 2649 2650class FigureManagerBase(object): 2651 """ 2652 Helper class for pyplot mode, wraps everything up into a neat bundle 2653 2654 Attributes 2655 ---------- 2656 canvas : :class:`FigureCanvasBase` 2657 The backend-specific canvas instance 2658 2659 num : int or str 2660 The figure number 2661 2662 key_press_handler_id : int 2663 The default key handler cid, when using the toolmanager. Can be used 2664 to disable default key press handling :: 2665 2666 figure.canvas.mpl_disconnect( 2667 figure.canvas.manager.key_press_handler_id) 2668 """ 2669 def __init__(self, canvas, num): 2670 self.canvas = canvas 2671 canvas.manager = self # store a pointer to parent 2672 self.num = num 2673 2674 self.key_press_handler_id = None 2675 if rcParams['toolbar'] != 'toolmanager': 2676 self.key_press_handler_id = self.canvas.mpl_connect( 2677 'key_press_event', 2678 self.key_press) 2679 2680 def show(self): 2681 """ 2682 For GUI backends, show the figure window and redraw. 2683 For non-GUI backends, raise an exception to be caught 2684 by :meth:`~matplotlib.figure.Figure.show`, for an 2685 optional warning. 2686 """ 2687 raise NonGuiException() 2688 2689 def destroy(self): 2690 pass 2691 2692 def full_screen_toggle(self): 2693 pass 2694 2695 def resize(self, w, h): 2696 """"For GUI backends, resize the window (in pixels).""" 2697 2698 def key_press(self, event): 2699 """ 2700 Implement the default mpl key bindings defined at 2701 :ref:`key-event-handling` 2702 """ 2703 if rcParams['toolbar'] != 'toolmanager': 2704 key_press_handler(event, self.canvas, self.canvas.toolbar) 2705 2706 @cbook.deprecated("2.2") 2707 def show_popup(self, msg): 2708 """Display message in a popup -- GUI only.""" 2709 2710 def get_window_title(self): 2711 """Get the title text of the window containing the figure. 2712 2713 Return None for non-GUI (e.g., PS) backends. 2714 """ 2715 return 'image' 2716 2717 def set_window_title(self, title): 2718 """Set the title text of the window containing the figure. 2719 2720 This has no effect for non-GUI (e.g., PS) backends. 2721 """ 2722 2723 2724cursors = tools.cursors 2725 2726 2727class NavigationToolbar2(object): 2728 """ 2729 Base class for the navigation cursor, version 2 2730 2731 backends must implement a canvas that handles connections for 2732 'button_press_event' and 'button_release_event'. See 2733 :meth:`FigureCanvasBase.mpl_connect` for more information 2734 2735 2736 They must also define 2737 2738 :meth:`save_figure` 2739 save the current figure 2740 2741 :meth:`set_cursor` 2742 if you want the pointer icon to change 2743 2744 :meth:`_init_toolbar` 2745 create your toolbar widget 2746 2747 :meth:`draw_rubberband` (optional) 2748 draw the zoom to rect "rubberband" rectangle 2749 2750 :meth:`press` (optional) 2751 whenever a mouse button is pressed, you'll be notified with 2752 the event 2753 2754 :meth:`release` (optional) 2755 whenever a mouse button is released, you'll be notified with 2756 the event 2757 2758 :meth:`set_message` (optional) 2759 display message 2760 2761 :meth:`set_history_buttons` (optional) 2762 you can change the history back / forward buttons to 2763 indicate disabled / enabled state. 2764 2765 That's it, we'll do the rest! 2766 """ 2767 2768 # list of toolitems to add to the toolbar, format is: 2769 # ( 2770 # text, # the text of the button (often not visible to users) 2771 # tooltip_text, # the tooltip shown on hover (where possible) 2772 # image_file, # name of the image for the button (without the extension) 2773 # name_of_method, # name of the method in NavigationToolbar2 to call 2774 # ) 2775 toolitems = ( 2776 ('Home', 'Reset original view', 'home', 'home'), 2777 ('Back', 'Back to previous view', 'back', 'back'), 2778 ('Forward', 'Forward to next view', 'forward', 'forward'), 2779 (None, None, None, None), 2780 ('Pan', 'Pan axes with left mouse, zoom with right', 'move', 'pan'), 2781 ('Zoom', 'Zoom to rectangle', 'zoom_to_rect', 'zoom'), 2782 ('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'), 2783 (None, None, None, None), 2784 ('Save', 'Save the figure', 'filesave', 'save_figure'), 2785 ) 2786 2787 def __init__(self, canvas): 2788 self.canvas = canvas 2789 canvas.toolbar = self 2790 self._nav_stack = cbook.Stack() 2791 self._xypress = None # the location and axis info at the time 2792 # of the press 2793 self._idPress = None 2794 self._idRelease = None 2795 self._active = None 2796 # This cursor will be set after the initial draw. 2797 self._lastCursor = cursors.POINTER 2798 self._init_toolbar() 2799 self._idDrag = self.canvas.mpl_connect( 2800 'motion_notify_event', self.mouse_move) 2801 2802 self._ids_zoom = [] 2803 self._zoom_mode = None 2804 2805 self._button_pressed = None # determined by the button pressed 2806 # at start 2807 2808 self.mode = '' # a mode string for the status bar 2809 self.set_history_buttons() 2810 2811 def set_message(self, s): 2812 """Display a message on toolbar or in status bar.""" 2813 2814 def back(self, *args): 2815 """move back up the view lim stack""" 2816 self._nav_stack.back() 2817 self.set_history_buttons() 2818 self._update_view() 2819 2820 @cbook.deprecated("2.1", alternative="canvas.draw_idle") 2821 def dynamic_update(self): 2822 self.canvas.draw_idle() 2823 2824 def draw_rubberband(self, event, x0, y0, x1, y1): 2825 """Draw a rectangle rubberband to indicate zoom limits. 2826 2827 Note that it is not guaranteed that ``x0 <= x1`` and ``y0 <= y1``. 2828 """ 2829 2830 def remove_rubberband(self): 2831 """Remove the rubberband.""" 2832 2833 def forward(self, *args): 2834 """Move forward in the view lim stack.""" 2835 self._nav_stack.forward() 2836 self.set_history_buttons() 2837 self._update_view() 2838 2839 def home(self, *args): 2840 """Restore the original view.""" 2841 self._nav_stack.home() 2842 self.set_history_buttons() 2843 self._update_view() 2844 2845 def _init_toolbar(self): 2846 """ 2847 This is where you actually build the GUI widgets (called by 2848 __init__). The icons ``home.xpm``, ``back.xpm``, ``forward.xpm``, 2849 ``hand.xpm``, ``zoom_to_rect.xpm`` and ``filesave.xpm`` are standard 2850 across backends (there are ppm versions in CVS also). 2851 2852 You just need to set the callbacks 2853 2854 home : self.home 2855 back : self.back 2856 forward : self.forward 2857 hand : self.pan 2858 zoom_to_rect : self.zoom 2859 filesave : self.save_figure 2860 2861 You only need to define the last one - the others are in the base 2862 class implementation. 2863 2864 """ 2865 raise NotImplementedError 2866 2867 def _set_cursor(self, event): 2868 if not event.inaxes or not self._active: 2869 if self._lastCursor != cursors.POINTER: 2870 self.set_cursor(cursors.POINTER) 2871 self._lastCursor = cursors.POINTER 2872 else: 2873 if (self._active == 'ZOOM' 2874 and self._lastCursor != cursors.SELECT_REGION): 2875 self.set_cursor(cursors.SELECT_REGION) 2876 self._lastCursor = cursors.SELECT_REGION 2877 elif (self._active == 'PAN' and 2878 self._lastCursor != cursors.MOVE): 2879 self.set_cursor(cursors.MOVE) 2880 self._lastCursor = cursors.MOVE 2881 2882 def mouse_move(self, event): 2883 self._set_cursor(event) 2884 2885 if event.inaxes and event.inaxes.get_navigate(): 2886 2887 try: 2888 s = event.inaxes.format_coord(event.xdata, event.ydata) 2889 except (ValueError, OverflowError): 2890 pass 2891 else: 2892 artists = [a for a in event.inaxes.mouseover_set 2893 if a.contains(event) and a.get_visible()] 2894 2895 if artists: 2896 a = cbook._topmost_artist(artists) 2897 if a is not event.inaxes.patch: 2898 data = a.get_cursor_data(event) 2899 if data is not None: 2900 s += ' [%s]' % a.format_cursor_data(data) 2901 2902 if len(self.mode): 2903 self.set_message('%s, %s' % (self.mode, s)) 2904 else: 2905 self.set_message(s) 2906 else: 2907 self.set_message(self.mode) 2908 2909 def pan(self, *args): 2910 """Activate the pan/zoom tool. pan with left button, zoom with right""" 2911 # set the pointer icon and button press funcs to the 2912 # appropriate callbacks 2913 2914 if self._active == 'PAN': 2915 self._active = None 2916 else: 2917 self._active = 'PAN' 2918 if self._idPress is not None: 2919 self._idPress = self.canvas.mpl_disconnect(self._idPress) 2920 self.mode = '' 2921 2922 if self._idRelease is not None: 2923 self._idRelease = self.canvas.mpl_disconnect(self._idRelease) 2924 self.mode = '' 2925 2926 if self._active: 2927 self._idPress = self.canvas.mpl_connect( 2928 'button_press_event', self.press_pan) 2929 self._idRelease = self.canvas.mpl_connect( 2930 'button_release_event', self.release_pan) 2931 self.mode = 'pan/zoom' 2932 self.canvas.widgetlock(self) 2933 else: 2934 self.canvas.widgetlock.release(self) 2935 2936 for a in self.canvas.figure.get_axes(): 2937 a.set_navigate_mode(self._active) 2938 2939 self.set_message(self.mode) 2940 2941 def press(self, event): 2942 """Called whenever a mouse button is pressed.""" 2943 2944 def press_pan(self, event): 2945 """Callback for mouse button press in pan/zoom mode.""" 2946 2947 if event.button == 1: 2948 self._button_pressed = 1 2949 elif event.button == 3: 2950 self._button_pressed = 3 2951 else: 2952 self._button_pressed = None 2953 return 2954 2955 if self._nav_stack() is None: 2956 # set the home button to this view 2957 self.push_current() 2958 2959 x, y = event.x, event.y 2960 self._xypress = [] 2961 for i, a in enumerate(self.canvas.figure.get_axes()): 2962 if (x is not None and y is not None and a.in_axes(event) and 2963 a.get_navigate() and a.can_pan()): 2964 a.start_pan(x, y, event.button) 2965 self._xypress.append((a, i)) 2966 self.canvas.mpl_disconnect(self._idDrag) 2967 self._idDrag = self.canvas.mpl_connect('motion_notify_event', 2968 self.drag_pan) 2969 2970 self.press(event) 2971 2972 def press_zoom(self, event): 2973 """Callback for mouse button press in zoom to rect mode.""" 2974 # If we're already in the middle of a zoom, pressing another 2975 # button works to "cancel" 2976 if self._ids_zoom != []: 2977 for zoom_id in self._ids_zoom: 2978 self.canvas.mpl_disconnect(zoom_id) 2979 self.release(event) 2980 self.draw() 2981 self._xypress = None 2982 self._button_pressed = None 2983 self._ids_zoom = [] 2984 return 2985 2986 if event.button == 1: 2987 self._button_pressed = 1 2988 elif event.button == 3: 2989 self._button_pressed = 3 2990 else: 2991 self._button_pressed = None 2992 return 2993 2994 if self._nav_stack() is None: 2995 # set the home button to this view 2996 self.push_current() 2997 2998 x, y = event.x, event.y 2999 self._xypress = [] 3000 for i, a in enumerate(self.canvas.figure.get_axes()): 3001 if (x is not None and y is not None and a.in_axes(event) and 3002 a.get_navigate() and a.can_zoom()): 3003 self._xypress.append((x, y, a, i, a._get_view())) 3004 3005 id1 = self.canvas.mpl_connect('motion_notify_event', self.drag_zoom) 3006 id2 = self.canvas.mpl_connect('key_press_event', 3007 self._switch_on_zoom_mode) 3008 id3 = self.canvas.mpl_connect('key_release_event', 3009 self._switch_off_zoom_mode) 3010 3011 self._ids_zoom = id1, id2, id3 3012 self._zoom_mode = event.key 3013 3014 self.press(event) 3015 3016 def _switch_on_zoom_mode(self, event): 3017 self._zoom_mode = event.key 3018 self.mouse_move(event) 3019 3020 def _switch_off_zoom_mode(self, event): 3021 self._zoom_mode = None 3022 self.mouse_move(event) 3023 3024 def push_current(self): 3025 """Push the current view limits and position onto the stack.""" 3026 self._nav_stack.push( 3027 WeakKeyDictionary( 3028 {ax: (ax._get_view(), 3029 # Store both the original and modified positions. 3030 (ax.get_position(True).frozen(), 3031 ax.get_position().frozen())) 3032 for ax in self.canvas.figure.axes})) 3033 self.set_history_buttons() 3034 3035 def release(self, event): 3036 """Callback for mouse button release.""" 3037 3038 def release_pan(self, event): 3039 """Callback for mouse button release in pan/zoom mode.""" 3040 3041 if self._button_pressed is None: 3042 return 3043 self.canvas.mpl_disconnect(self._idDrag) 3044 self._idDrag = self.canvas.mpl_connect( 3045 'motion_notify_event', self.mouse_move) 3046 for a, ind in self._xypress: 3047 a.end_pan() 3048 if not self._xypress: 3049 return 3050 self._xypress = [] 3051 self._button_pressed = None 3052 self.push_current() 3053 self.release(event) 3054 self.draw() 3055 3056 def drag_pan(self, event): 3057 """Callback for dragging in pan/zoom mode.""" 3058 for a, ind in self._xypress: 3059 #safer to use the recorded button at the press than current button: 3060 #multiple button can get pressed during motion... 3061 a.drag_pan(self._button_pressed, event.key, event.x, event.y) 3062 self.canvas.draw_idle() 3063 3064 def drag_zoom(self, event): 3065 """Callback for dragging in zoom mode.""" 3066 if self._xypress: 3067 x, y = event.x, event.y 3068 lastx, lasty, a, ind, view = self._xypress[0] 3069 (x1, y1), (x2, y2) = np.clip( 3070 [[lastx, lasty], [x, y]], a.bbox.min, a.bbox.max) 3071 if self._zoom_mode == "x": 3072 y1, y2 = a.bbox.intervaly 3073 elif self._zoom_mode == "y": 3074 x1, x2 = a.bbox.intervalx 3075 self.draw_rubberband(event, x1, y1, x2, y2) 3076 3077 def release_zoom(self, event): 3078 """Callback for mouse button release in zoom to rect mode.""" 3079 for zoom_id in self._ids_zoom: 3080 self.canvas.mpl_disconnect(zoom_id) 3081 self._ids_zoom = [] 3082 3083 self.remove_rubberband() 3084 3085 if not self._xypress: 3086 return 3087 3088 last_a = [] 3089 3090 for cur_xypress in self._xypress: 3091 x, y = event.x, event.y 3092 lastx, lasty, a, ind, view = cur_xypress 3093 # ignore singular clicks - 5 pixels is a threshold 3094 # allows the user to "cancel" a zoom action 3095 # by zooming by less than 5 pixels 3096 if ((abs(x - lastx) < 5 and self._zoom_mode!="y") or 3097 (abs(y - lasty) < 5 and self._zoom_mode!="x")): 3098 self._xypress = None 3099 self.release(event) 3100 self.draw() 3101 return 3102 3103 # detect twinx,y axes and avoid double zooming 3104 twinx, twiny = False, False 3105 if last_a: 3106 for la in last_a: 3107 if a.get_shared_x_axes().joined(a, la): 3108 twinx = True 3109 if a.get_shared_y_axes().joined(a, la): 3110 twiny = True 3111 last_a.append(a) 3112 3113 if self._button_pressed == 1: 3114 direction = 'in' 3115 elif self._button_pressed == 3: 3116 direction = 'out' 3117 else: 3118 continue 3119 3120 a._set_view_from_bbox((lastx, lasty, x, y), direction, 3121 self._zoom_mode, twinx, twiny) 3122 3123 self.draw() 3124 self._xypress = None 3125 self._button_pressed = None 3126 3127 self._zoom_mode = None 3128 3129 self.push_current() 3130 self.release(event) 3131 3132 def draw(self): 3133 """Redraw the canvases, update the locators.""" 3134 for a in self.canvas.figure.get_axes(): 3135 xaxis = getattr(a, 'xaxis', None) 3136 yaxis = getattr(a, 'yaxis', None) 3137 locators = [] 3138 if xaxis is not None: 3139 locators.append(xaxis.get_major_locator()) 3140 locators.append(xaxis.get_minor_locator()) 3141 if yaxis is not None: 3142 locators.append(yaxis.get_major_locator()) 3143 locators.append(yaxis.get_minor_locator()) 3144 3145 for loc in locators: 3146 loc.refresh() 3147 self.canvas.draw_idle() 3148 3149 def _update_view(self): 3150 """Update the viewlim and position from the view and 3151 position stack for each axes. 3152 """ 3153 nav_info = self._nav_stack() 3154 if nav_info is None: 3155 return 3156 # Retrieve all items at once to avoid any risk of GC deleting an Axes 3157 # while in the middle of the loop below. 3158 items = list(nav_info.items()) 3159 for ax, (view, (pos_orig, pos_active)) in items: 3160 ax._set_view(view) 3161 # Restore both the original and modified positions 3162 ax._set_position(pos_orig, 'original') 3163 ax._set_position(pos_active, 'active') 3164 self.canvas.draw_idle() 3165 3166 def save_figure(self, *args): 3167 """Save the current figure.""" 3168 raise NotImplementedError 3169 3170 def set_cursor(self, cursor): 3171 """Set the current cursor to one of the :class:`Cursors` enums values. 3172 3173 If required by the backend, this method should trigger an update in 3174 the backend event loop after the cursor is set, as this method may be 3175 called e.g. before a long-running task during which the GUI is not 3176 updated. 3177 """ 3178 3179 def update(self): 3180 """Reset the axes stack.""" 3181 self._nav_stack.clear() 3182 self.set_history_buttons() 3183 3184 def zoom(self, *args): 3185 """Activate zoom to rect mode.""" 3186 if self._active == 'ZOOM': 3187 self._active = None 3188 else: 3189 self._active = 'ZOOM' 3190 3191 if self._idPress is not None: 3192 self._idPress = self.canvas.mpl_disconnect(self._idPress) 3193 self.mode = '' 3194 3195 if self._idRelease is not None: 3196 self._idRelease = self.canvas.mpl_disconnect(self._idRelease) 3197 self.mode = '' 3198 3199 if self._active: 3200 self._idPress = self.canvas.mpl_connect('button_press_event', 3201 self.press_zoom) 3202 self._idRelease = self.canvas.mpl_connect('button_release_event', 3203 self.release_zoom) 3204 self.mode = 'zoom rect' 3205 self.canvas.widgetlock(self) 3206 else: 3207 self.canvas.widgetlock.release(self) 3208 3209 for a in self.canvas.figure.get_axes(): 3210 a.set_navigate_mode(self._active) 3211 3212 self.set_message(self.mode) 3213 3214 def set_history_buttons(self): 3215 """Enable or disable the back/forward button.""" 3216 3217 3218class ToolContainerBase(object): 3219 """ 3220 Base class for all tool containers, e.g. toolbars. 3221 3222 Attributes 3223 ---------- 3224 toolmanager : `ToolManager` 3225 The tools with which this `ToolContainer` wants to communicate. 3226 """ 3227 3228 _icon_extension = '.png' 3229 """ 3230 Toolcontainer button icon image format extension 3231 3232 **String**: Image extension 3233 """ 3234 3235 def __init__(self, toolmanager): 3236 self.toolmanager = toolmanager 3237 self.toolmanager.toolmanager_connect('tool_removed_event', 3238 self._remove_tool_cbk) 3239 3240 def _tool_toggled_cbk(self, event): 3241 """ 3242 Captures the 'tool_trigger_[name]' 3243 3244 This only gets used for toggled tools 3245 """ 3246 self.toggle_toolitem(event.tool.name, event.tool.toggled) 3247 3248 def add_tool(self, tool, group, position=-1): 3249 """ 3250 Adds a tool to this container 3251 3252 Parameters 3253 ---------- 3254 tool : tool_like 3255 The tool to add, see `ToolManager.get_tool`. 3256 group : str 3257 The name of the group to add this tool to. 3258 position : int (optional) 3259 The position within the group to place this tool. Defaults to end. 3260 """ 3261 tool = self.toolmanager.get_tool(tool) 3262 image = self._get_image_filename(tool.image) 3263 toggle = getattr(tool, 'toggled', None) is not None 3264 self.add_toolitem(tool.name, group, position, 3265 image, tool.description, toggle) 3266 if toggle: 3267 self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name, 3268 self._tool_toggled_cbk) 3269 # If initially toggled 3270 if tool.toggled: 3271 self.toggle_toolitem(tool.name, True) 3272 3273 def _remove_tool_cbk(self, event): 3274 """Captures the 'tool_removed_event' signal and removes the tool.""" 3275 self.remove_toolitem(event.tool.name) 3276 3277 def _get_image_filename(self, image): 3278 """Find the image based on its name.""" 3279 if not image: 3280 return None 3281 3282 basedir = os.path.join(rcParams['datapath'], 'images') 3283 possible_images = ( 3284 image, 3285 image + self._icon_extension, 3286 os.path.join(basedir, image), 3287 os.path.join(basedir, image) + self._icon_extension) 3288 3289 for fname in possible_images: 3290 if os.path.isfile(fname): 3291 return fname 3292 3293 def trigger_tool(self, name): 3294 """ 3295 Trigger the tool 3296 3297 Parameters 3298 ---------- 3299 name : String 3300 Name (id) of the tool triggered from within the container 3301 """ 3302 self.toolmanager.trigger_tool(name, sender=self) 3303 3304 def add_toolitem(self, name, group, position, image, description, toggle): 3305 """ 3306 Add a toolitem to the container 3307 3308 This method must get implemented per backend 3309 3310 The callback associated with the button click event, 3311 must be **EXACTLY** `self.trigger_tool(name)` 3312 3313 Parameters 3314 ---------- 3315 name : string 3316 Name of the tool to add, this gets used as the tool's ID and as the 3317 default label of the buttons 3318 group : String 3319 Name of the group that this tool belongs to 3320 position : Int 3321 Position of the tool within its group, if -1 it goes at the End 3322 image_file : String 3323 Filename of the image for the button or `None` 3324 description : String 3325 Description of the tool, used for the tooltips 3326 toggle : Bool 3327 * `True` : The button is a toggle (change the pressed/unpressed 3328 state between consecutive clicks) 3329 * `False` : The button is a normal button (returns to unpressed 3330 state after release) 3331 """ 3332 raise NotImplementedError 3333 3334 def toggle_toolitem(self, name, toggled): 3335 """ 3336 Toggle the toolitem without firing event 3337 3338 Parameters 3339 ---------- 3340 name : String 3341 Id of the tool to toggle 3342 toggled : bool 3343 Whether to set this tool as toggled or not. 3344 """ 3345 raise NotImplementedError 3346 3347 def remove_toolitem(self, name): 3348 """ 3349 Remove a toolitem from the `ToolContainer` 3350 3351 This method must get implemented per backend 3352 3353 Called when `ToolManager` emits a `tool_removed_event` 3354 3355 Parameters 3356 ---------- 3357 name : string 3358 Name of the tool to remove 3359 """ 3360 raise NotImplementedError 3361 3362 3363class StatusbarBase(object): 3364 """Base class for the statusbar""" 3365 def __init__(self, toolmanager): 3366 self.toolmanager = toolmanager 3367 self.toolmanager.toolmanager_connect('tool_message_event', 3368 self._message_cbk) 3369 3370 def _message_cbk(self, event): 3371 """Captures the 'tool_message_event' and set the message""" 3372 self.set_message(event.message) 3373 3374 def set_message(self, s): 3375 """ 3376 Display a message on toolbar or in status bar 3377 3378 Parameters 3379 ---------- 3380 s : str 3381 Message text 3382 """ 3383 pass 3384