diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index d43dc351abae..e0c8cfe79712 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -1,12 +1,333 @@ -********* -animation -********* +====================== + ``animation`` module +====================== +.. automodule:: matplotlib.animation -:mod:`matplotlib.animation` -=========================== +.. contents:: Table of Contents + :depth: 1 + :local: + :backlinks: entry -.. automodule:: matplotlib.animation - :members: - :undoc-members: - :show-inheritance: + +Animation +========= + +The easiest way to make a live animation in matplotlib is to use one of the +`Animation` classes. + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + FuncAnimation + ArtistAnimation + +In both cases it is critical to keep a reference to the instance +object. The animation is advanced by a timer (typically from the host +GUI framework) which the `Animation` object holds the only reference +to. If you do not hold a reference to the `Animation` object, it (and +hence the timers), will be garbage collected which will stop the +animation. + +To save an animation to disk use + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + Animation.save + Animation.to_html5_video + +See :ref:`ani_writer_classes` below for details about what movie formats are supported. + + +``FuncAnimation`` +----------------- + +The inner workings of `FuncAnimation` is more-or-less:: + + for d in frames: + artists = func(d, *fargs) + fig.canvas.draw_idle() + plt.pause(interval) + + +with details to handle 'blitting' (to dramatically improve the live +performance), to be non-blocking, handle repeats, multiple animated +axes, and easily save the animation to a movie file. + +'Blitting' is a `old technique +`__ in computer graphics. The +general gist is to take an existing bit map (in our case a mostly +rasterized figure) and then 'blit' one more artist on top. Thus, by +managing a saved 'clean' bitmap, we can only re-draw the few artists +that are changing at each frame and possibly save significant amounts of +time. When using blitting (by passing ``blit=True``) the core loop of +`FuncAnimation` gets a bit more complicated :: + + ax = fig.gca() + + def update_blit(artists): + fig.canvas.restore_region(bg_cache) + for a in artists: + a.axes.draw_artist(a) + + ax.figure.canvas.blit(ax.bbox) + + artists = init_func() + + for a in artists: + a.set_animated(True) + + fig.canvas.draw() + bg_cache = fig.canvas.copy_from_bbox(ax.bbox) + + for f in frames: + artists = func(f, *fargs) + update_blit(artists) + plt.pause(interval) + +This is of course leaving out many details (such as updating the +background when the figure is resized or fully re-drawn). However, +this hopefully minimalist example gives a sense of how ``init_func`` +and ``func`` are used inside of `FuncAnimation` and the theory of how +'blitting' works. + +The expected signature on ``func`` and ``init_func`` is very simple to +keep `FuncAnimation` out of your book keeping and plotting logic, but +this means that the callable objects you pass in must know what +artists they should be working on. There are several approaches to +handling this, of varying complexity and encapsulation. The simplest +approach, which works quite well in the case of a script, is to define the +artist at a global scope and let Python sort things out. For example :: + + import numpy as np + import matplotlib.pyplot as plt + from matplotlib.animation import FuncAnimation + + fig, ax = plt.subplots() + xdata, ydata = [], [] + ln, = plt.plot([], [], 'ro', animated=True) + + def init(): + ax.set_xlim(0, 2*np.pi) + ax.set_ylim(-1, 1) + return ln, + + def update(frame): + xdata.append(frame) + ydata.append(np.sin(frame)) + ln.set_data(xdata, ydata) + return ln, + + ani = FuncAnimation(fig, update, frames=np.linspace(0, 2*np.pi, 128), + init_func=init, blit=True) + plt.show() + + +The second method is to us `functools.partial` to 'bind' artists to +function. A third method is to use closures to build up the required +artists and functions. A fourth method is to create a class. + + + + +Examples +~~~~~~~~ + +.. toctree:: + :maxdepth: 1 + + ../examples/animation/animate_decay + ../examples/animation/bayes_update + ../examples/animation/double_pendulum_animated + ../examples/animation/dynamic_image + ../examples/animation/histogram + ../examples/animation/rain + ../examples/animation/random_data + ../examples/animation/simple_3danim + ../examples/animation/simple_anim + ../examples/animation/strip_chart_demo + ../examples/animation/unchained + +``ArtistAnimation`` +------------------- + + +Examples +~~~~~~~~ + +.. toctree:: + :maxdepth: 1 + + ../examples/animation/basic_example + ../examples/animation/basic_example_writer + ../examples/animation/dynamic_image2 + + + + +Writer Classes +============== + + + +The provided writers fall into two broad categories: pipe-based and +file-based. The pipe-based writers stream the captured frames over a +pipe to an external process. The pipe-based variants tend to be more +performant, but may not work on all systems. + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + + FFMpegWriter + ImageMagickFileWriter + AVConvWriter + +Alternatively the file-based writers save temporary files for each +frame which are stitched into a single file at the end. Although +slower, these writers can be easier to debug. + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + FFMpegFileWriter + ImageMagickWriter + AVConvFileWriter + + +Fundamentally, a `MovieWriter` provides a way to grab sequential frames +from the same underlying `~matplotlib.figure.Figure` object. The base +class `MovieWriter` implements 3 methods and a context manager. The +only difference between the pipe-based and file-based writers is in the +arguments to their respective ``setup`` methods. + + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + MovieWriter.setup + FileMovieWriter.setup + MovieWriter.grab_frame + MovieWriter.finish + MovieWriter.saving + + +The ``setup()`` method is used to prepare the writer (possibly opening +a pipe), successive calls to ``grab_frame()`` capture a single frame +at a time and ``finish()`` finalizes the movie and writes the output +file to disk. For example :: + + moviewriter = MovieWriter(...) + moviewriter.setup(fig=fig, 'my_movie.ext', dpi=100) + for j in range(n): + update_figure(n) + moviewriter.grab_frame() + moviewriter.finish() + + +If using the writer classes directly (not through `Animation.save`), it is strongly encouraged +to use the `~MovieWriter.saving` context manager :: + + with moviewriter.saving(fig, 'myfile.mp4', dpi=100): + for j in range(n): + update_figure(n) + moviewriter.grab_frame() + + +to ensures that setup and cleanup are performed as necessary. + + +:ref:`animation-moviewriter` + + +.. _ani_writer_classes: + +Helper Classes +============== + + +Animation Base Classes +---------------------- + + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + Animation + TimedAnimation + + +Custom Animation classes +------------------------ + +:ref:`animation-subplots` + +Writer Registry +--------------- + +A module-level registry is provided to map between the name of the +writer and the class to allow a string to be passed to +`Animation.save` instead of a writer instance. + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + MovieWriterRegistry + +Writer Base Classes +------------------- + +To reduce code duplication base classes + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + AbstractMovieWriter + MovieWriter + FileMovieWriter + +and mixins are provided + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + AVConvBase + FFMpegBase + ImageMagickBase + +See the source code for how to easily implement new `MovieWriter` +classes. + + +Inheritance Diagrams +==================== + +.. inheritance-diagram:: matplotlib.animation.FuncAnimation matplotlib.animation.ArtistAnimation + :private-bases: + +.. inheritance-diagram:: matplotlib.animation.AVConvFileWriter matplotlib.animation.AVConvWriter matplotlib.animation.FFMpegFileWriter matplotlib.animation.FFMpegWriter matplotlib.animation.ImageMagickFileWriter matplotlib.animation.ImageMagickWriter + :private-bases: + + + +Deprecated +========== + + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + MencoderBase + MencoderFileWriter + MencoderWriter diff --git a/doc/api/api_changes.rst b/doc/api/api_changes.rst index fdc4c20547b1..7ec22368c975 100644 --- a/doc/api/api_changes.rst +++ b/doc/api/api_changes.rst @@ -141,8 +141,8 @@ the kwarg is None which internally sets it to the 'auto' string, triggering a new algorithm for adjusting the maximum according to the axis length relative to the ticklabel font size. -`matplotlib.ticker.LogFormatter` gains minor_thresholds kwarg -------------------------------------------------------------- +`matplotlib.ticker.LogFormatter`: two new kwargs +------------------------------------------------ Previously, minor ticks on log-scaled axes were not labeled by default. An algorithm has been added to the @@ -151,6 +151,9 @@ ticks between integer powers of the base. The algorithm uses two parameters supplied in a kwarg tuple named 'minor_thresholds'. See the docstring for further explanation. +To improve support for axes using `~matplotlib.ticker.SymmetricLogLocator`, +a 'linthresh' kwarg was added. + New defaults for 3D quiver function in mpl_toolkits.mplot3d.axes3d.py --------------------------------------------------------------------- diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 312acb4b3a84..fee64a1e1f79 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -69,6 +69,7 @@ def adjusted_figsize(w, h, dpi, n): # A registry for available MovieWriter classes class MovieWriterRegistry(object): + '''Registry of available writer classes by human readable name.''' def __init__(self): self.avail = dict() self._registered = dict() @@ -110,6 +111,16 @@ def list(self): return list(self.avail.keys()) def is_available(self, name): + '''Check if given writer is available by name. + + Parameters + ---------- + name : str + + Returns + ------- + available : bool + ''' self.ensure_not_dirty() return name in self.avail @@ -174,37 +185,41 @@ def saving(self, fig, outfile, dpi, *args, **kwargs): ''' Context manager to facilitate writing the movie file. - All arguments are passed on to `setup`. + ``*args, **kw`` are any parameters that should be passed to `setup`. ''' + # This particular sequence is what contextlib.contextmanager wants self.setup(fig, outfile, dpi, *args, **kwargs) - yield - self.finish() + try: + yield self + finally: + self.finish() class MovieWriter(AbstractMovieWriter): - ''' - Base class for writing movies. Fundamentally, what a MovieWriter does - is provide is a way to grab frames by calling grab_frame(). setup() - is called to start the process and finish() is called afterwards. - This class is set up to provide for writing movie frame data to a pipe. - saving() is provided as a context manager to facilitate this process as:: + '''Base class for writing movies. - with moviewriter.saving(fig, outfile='myfile.mp4', dpi=100): - # Iterate over frames - moviewriter.grab_frame(**savefig_kwargs) + This class is set up to provide for writing movie frame data to a + pipe. See examples for how to use these classes. - The use of the context manager ensures that setup and cleanup are - performed as necessary. - frame_format: string + Attributes + ---------- + + frame_format : str The format used in writing frame data, defaults to 'rgba' + + fig : `~matplotlib.figure.Figure` + The figure to capture data from. + This must be provided by the sub-classes. + ''' def __init__(self, fps=5, codec=None, bitrate=None, extra_args=None, metadata=None): - ''' - Construct a new MovieWriter object. + '''MovieWriter + Parameters + ---------- fps: int Framerate for movie. codec: string or None, optional @@ -270,11 +285,14 @@ def setup(self, fig, outfile, dpi): ''' Perform setup for writing the movie file. - fig: `matplotlib.Figure` instance + Parameters + ---------- + + fig : `matplotlib.Figure` instance The figure object that contains the information for frames - outfile: string + outfile : string The filename of the resulting movie file - dpi: int + dpi : int The DPI (or resolution) for the file. This controls the size in pixels of the resulting movie file. ''' @@ -310,6 +328,7 @@ def finish(self): def grab_frame(self, **savefig_kwargs): ''' Grab the image information from the figure and save as a movie frame. + All keyword arguments in savefig_kwargs are passed on to the 'savefig' command that saves the figure. ''' @@ -377,29 +396,34 @@ def isAvailable(cls): class FileMovieWriter(MovieWriter): - '`MovieWriter` subclass that handles writing to a file.' + '''`MovieWriter` for writing to individual files and stitching at the end. + This must be sub-classed to be useful. + ''' def __init__(self, *args, **kwargs): MovieWriter.__init__(self, *args, **kwargs) self.frame_format = rcParams['animation.frame_format'] def setup(self, fig, outfile, dpi, frame_prefix='_tmp', clear_temp=True): - ''' - Perform setup for writing the movie file. + '''Perform setup for writing the movie file. + + Parameters + ---------- + fig : matplotlib.figure.Figure + The figure to grab the rendered frames from. + outfile : str + The filename of the resulting movie file. + dpi : number + The dpi of the output file. This, with the figure size, + controls the size in pixels of the resulting movie file. + frame_prefix : str, optional + The filename prefix to use for temporary files. Defaults to + '_tmp'. + clear_temp : bool, optional + If the temporary files should be deleted after stitching + the final result. Setting this to `False` can be useful for + debugging. Defaults to `True`. - fig: `matplotlib.Figure` instance - The figure object that contains the information for frames - outfile: string - The filename of the resulting movie file - dpi: int - The DPI (or resolution) for the file. This controls the size - in pixels of the resulting movie file. - frame_prefix: string, optional - The filename prefix to use for the temporary files. Defaults - to '_tmp' - clear_temp: bool - Specifies whether the temporary files should be deleted after - the movie is written. (Useful for debugging.) Defaults to True. ''' self.fig = fig self.outfile = outfile @@ -511,6 +535,12 @@ def cleanup(self): # Base class of ffmpeg information. Has the config keys and the common set # of arguments that controls the *output* side of things. class FFMpegBase(object): + '''Mixin class for FFMpeg output. + + To be useful this must be multiply-inherited from with a + `MovieWriterBase` sub-class. + ''' + exec_key = 'animation.ffmpeg_path' args_key = 'animation.ffmpeg_args' @@ -538,6 +568,11 @@ def output_args(self): # Combine FFMpeg options with pipe-based writing @writers.register('ffmpeg') class FFMpegWriter(MovieWriter, FFMpegBase): + '''Pipe-based ffmpeg writer. + + Frames are streamed directly to ffmpeg via a pipe and written in a single + pass. + ''' def _args(self): # Returns the command line parameters for subprocess to use # ffmpeg to create a movie using a pipe. @@ -554,6 +589,12 @@ def _args(self): # Combine FFMpeg options with temp file-based writing @writers.register('ffmpeg_file') class FFMpegFileWriter(FileMovieWriter, FFMpegBase): + '''File-based ffmpeg writer. + + Frames are written to temporary files on disk and then stitched + together at the end. + + ''' supported_formats = ['png', 'jpeg', 'ppm', 'tiff', 'sgi', 'bmp', 'pbm', 'raw', 'rgba'] @@ -568,6 +609,12 @@ def _args(self): # Base class of avconv information. AVConv has identical arguments to # FFMpeg class AVConvBase(FFMpegBase): + '''Mixin class for avconv output. + + To be useful this must be multiply-inherited from with a + `MovieWriterBase` sub-class. + ''' + exec_key = 'animation.avconv_path' args_key = 'animation.avconv_args' @@ -575,13 +622,22 @@ class AVConvBase(FFMpegBase): # Combine AVConv options with pipe-based writing @writers.register('avconv') class AVConvWriter(AVConvBase, FFMpegWriter): - pass + '''Pipe-based avconv writer. + + Frames are streamed directly to avconv via a pipe and written in a single + pass. + ''' # Combine AVConv options with file-based writing @writers.register('avconv_file') class AVConvFileWriter(AVConvBase, FFMpegFileWriter): - pass + '''File-based avconv writer. + + Frames are written to temporary files on disk and then stitched + together at the end. + + ''' # Base class of mencoder information. Contains configuration key information @@ -664,6 +720,12 @@ def _args(self): # Base class for animated GIFs with convert utility class ImageMagickBase(object): + '''Mixin class for ImageMagick output. + + To be useful this must be multiply-inherited from with a + `MovieWriterBase` sub-class. + ''' + exec_key = 'animation.convert_path' args_key = 'animation.convert_args' @@ -696,7 +758,7 @@ def _init_from_registry(cls): @classmethod def isAvailable(cls): ''' - Check to see if a ImageMagickWriter is actually available + Check to see if a ImageMagickWriter is actually available. Done by first checking the windows registry (if applicable) and then running the commandline tool. @@ -715,6 +777,12 @@ def isAvailable(cls): # former. @writers.register('imagemagick') class ImageMagickWriter(ImageMagickBase, MovieWriter): + '''Pipe-based animated gif. + + Frames are streamed directly to ImageMagick via a pipe and written + in a single pass. + + ''' def _args(self): return ([self.bin_path(), '-size', '%ix%i' % self.frame_size, '-depth', '8', @@ -729,6 +797,13 @@ def _args(self): # former. @writers.register('imagemagick_file') class ImageMagickFileWriter(ImageMagickBase, FileMovieWriter): + '''File-based animated gif writer. + + Frames are written to temporary files on disk and then stitched + together at the end. + + ''' + supported_formats = ['png', 'jpeg', 'ppm', 'tiff', 'sgi', 'bmp', 'pbm', 'raw', 'rgba'] @@ -739,19 +814,34 @@ def _args(self): class Animation(object): - ''' - This class wraps the creation of an animation using matplotlib. It is - only a base class which should be subclassed to provide needed behavior. + '''This class wraps the creation of an animation using matplotlib. + + It is only a base class which should be subclassed to provide + needed behavior. + + This class is not typically used directly. + + Parameters + ---------- + fig : matplotlib.figure.Figure + The figure object that is used to get draw, resize, and any + other needed events. + + event_source : object, optional + A class that can run a callback when desired events + are generated, as well as be stopped and started. + + Examples include timers (see :class:`TimedAnimation`) and file + system notifications. - *fig* is the figure object that is used to get draw, resize, and any - other needed events. + blit : bool, optional + controls whether blitting is used to optimize drawing. Defaults + to `False`. - *event_source* is a class that can run a callback when desired events - are generated, as well as be stopped and started. Examples include timers - (see :class:`TimedAnimation`) and file system notifications. + See Also + -------- + FuncAnimation, ArtistAnimation - *blit* is a boolean that controls whether blitting is used to optimize - drawing. ''' def __init__(self, fig, event_source=None, blit=False): self._fig = fig @@ -806,55 +896,72 @@ def _stop(self, *args): def save(self, filename, writer=None, fps=None, dpi=None, codec=None, bitrate=None, extra_args=None, metadata=None, extra_anim=None, savefig_kwargs=None): - ''' - Saves a movie file by drawing every frame. - - *filename* is the output filename, e.g., :file:`mymovie.mp4` - - *writer* is either an instance of :class:`AbstractMovieWriter` or - a string key that identifies a class to use, such as 'ffmpeg' or - 'mencoder'. If nothing is passed, the value of the rcparam - `animation.writer` is used. - - *dpi* controls the dots per inch for the movie frames. This combined - with the figure's size in inches controls the size of the movie. - - *savefig_kwargs* is a dictionary containing keyword arguments to be - passed on to the 'savefig' command which is called repeatedly to save - the individual frames. This can be used to set tight bounding boxes, - for example. - - *extra_anim* is a list of additional `Animation` objects that should - be included in the saved movie file. These need to be from the same - `matplotlib.Figure` instance. Also, animation frames will just be - simply combined, so there should be a 1:1 correspondence between - the frames from the different animations. - - These remaining arguments are used to construct a :class:`MovieWriter` - instance when necessary and are only considered valid if *writer* is - not a :class:`MovieWriter` instance. - - *fps* is the frames per second in the movie. Defaults to None, - which will use the animation's specified interval to set the frames - per second. - - *codec* is the video codec to be used. Not all codecs are supported - by a given :class:`MovieWriter`. If none is given, this defaults to the - value specified by the rcparam `animation.codec`. - - *bitrate* specifies the amount of bits used per second in the - compressed movie, in kilobits per second. A higher number means a - higher quality movie, but at the cost of increased file size. If no - value is given, this defaults to the value given by the rcparam - `animation.bitrate`. - - *extra_args* is a list of extra string arguments to be passed to the - underlying movie utility. The default is None, which passes the - additional arguments in the 'animation.extra_args' rcParam. - - *metadata* is a dictionary of keys and values for metadata to include - in the output file. Some keys that may be of use include: - title, artist, genre, subject, copyright, srcform, comment. + '''Saves a movie file by drawing every frame. + + Parameters + ---------- + + filename : str + The output filename, e.g., :file:`mymovie.mp4` + + writer : :class:`MovieWriter` or str, optional + A `MovieWriter` instance to use or a key that identifies a + class to use, such as 'ffmpeg' or 'mencoder'. If `None`, + defaults to ``rcParams['animation.writer']`` + + fps : number, optional + frames per second in the movie. Defaults to None, + which will use the animation's specified interval to set + the frames per second. + + dpi : number, optional + Controls the dots per inch for the movie frames. This + combined with the figure's size in inches controls the size of + the movie. If None, defaults to ``rcparam['savefig.dpi']`` + + codec : str, optional + The video codec to be used. Not all codecs are supported by + a given :class:`MovieWriter`. If `None`, + default to ``rcParams['animation.codec']`` + + bitrate : number, optional + Specifies the number of bits used per second in the + compressed movie, in kilobits per second. A higher number + means a higher quality movie, but at the cost of increased + file size. If `None`, defaults to + ``rcParam['animation.bitrate']`` + + extra_args : list, optional + List of extra string arguments to be passed to the + underlying movie utility. If `None`, defaults to + ``rcParams['animation.extra_args']`` + + metadata : dict, optional + Dictionary of keys and values for metadata to include in + the output file. Some keys that may be of use include: + title, artist, genre, subject, copyright, srcform, comment. + + extra_anim : list, optional + Additional `Animation` objects that should be included in + the saved movie file. These need to be from the same + `matplotlib.Figure` instance. Also, animation frames will + just be simply combined, so there should be a 1:1 + correspondence between the frames from the different + animations. + + savefig_kwargs : dict, optional + Is a dictionary containing keyword arguments to be passed + on to the 'savefig' command which is called repeatedly to + save the individual frames. + + Notes + ----- + fps, codec, bitrate, extra_args, metadata are used to + construct a :class:`MovieWriter` instance and can only be + passed if `writer` is a string. If they are passed as + non-`None` and ``writer`` is a :class:`MovieWriter`, a + `RuntimeError` will be raised. + ''' # If the writer is None, use the rc param to find the name of the one # to use @@ -1129,15 +1236,31 @@ def _repr_html_(self): class TimedAnimation(Animation): - ''' - :class:`Animation` subclass that supports time-based animation, drawing - a new frame every *interval* milliseconds. + ''':class:`Animation` subclass for time-based animation. + + A new frame is drawn every *interval* milliseconds. + + Parameters + ---------- + fig : matplotlib.figure.Figure + The figure object that is used to get draw, resize, and any + other needed events. + + interval : number, optional + Delay between frames in milliseconds. Defaults to 200. - *repeat* controls whether the animation should repeat when the sequence - of frames is completed. + repeat_delay : number, optional + If the animation in repeated, adds a delay in milliseconds + before repeating the animation. Defaults to `None`. + + repeat : bool, optional + Controls whether the animation should repeat when the sequence + of frames is completed. Defaults to `True`. + + blit : bool, optional + Controls whether blitting is used to optimize drawing. Defaults + to `False`. - *repeat_delay* optionally adds a delay in milliseconds before repeating - the animation. ''' def __init__(self, fig, interval=200, repeat_delay=None, repeat=True, event_source=None, *args, **kwargs): @@ -1195,13 +1318,38 @@ def _loop_delay(self, *args): class ArtistAnimation(TimedAnimation): - ''' - Before calling this function, all plotting should have taken place + '''Animation using a fixed set of `Artist` objects. + + Before creating an instance, all plotting should have taken place and the relevant artists saved. - *artists* is a list, with each list entry a collection of artists that - represent what needs to be enabled on each frame. These will be disabled - for other frames. + + Parameters + ---------- + fig : matplotlib.figure.Figure + The figure object that is used to get draw, resize, and any + other needed events. + + artists : list + Each list entry a collection of artists that represent what + needs to be enabled on each frame. These will be disabled for + other frames. + + interval : number, optional + Delay between frames in milliseconds. Defaults to 200. + + repeat_delay : number, optional + If the animation in repeated, adds a delay in milliseconds + before repeating the animation. Defaults to `None`. + + repeat : bool, optional + Controls whether the animation should repeat when the sequence + of frames is completed. Defaults to `True`. + + blit : bool, optional + Controls whether blitting is used to optimize drawing. Defaults + to `False`. + ''' def __init__(self, fig, artists, *args, **kwargs): # Internal list of artists drawn in the most recent frame. @@ -1251,24 +1399,76 @@ def _draw_frame(self, artists): class FuncAnimation(TimedAnimation): ''' - Makes an animation by repeatedly calling a function *func*, passing in - (optional) arguments in *fargs*. + Makes an animation by repeatedly calling a function ``func``. + + + Parameters + ---------- + fig : matplotlib.figure.Figure + The figure object that is used to get draw, resize, and any + other needed events. + + func : callable + The function to call at each frame. The first argument will + be the next value in ``frames``. Any additional positional + arguments can be supplied via the ``fargs`` parameter. + + The required signature is :: + + def func(fr: object, *fargs) -> iterable_of_artists: + + frames : iterable, int, generator function, or None, optional + Source of data to pass ``func`` and each frame of the animation + + If an iterable, then simply use the values provided. If the + iterable has a length, it will override the ``save_count`` kwarg. + + If an integer, equivalent to passing ``range(frames)`` + + If a generator function, then must have the signature :: + + def gen_function() -> obj: + + In all of these cases, the values in `frames` is simply + passed through to the user-supplied `func` and thus can be + of any type. + + If `None`, then equivalent to passing ``itertools.count``. + + init_func : callable, optional + A function used to draw a clear frame. If not given, the + results of drawing from the first item in the frames sequence + will be used. This function will be called once before the + first frame. + + If blit=True, ``init_func`` must return an iterable of artists + to be re-drawn. + + The required signature is :: + + def init_func() -> iterable_of_artists: + + fargs : tuple or None, optional + Additional arguments to pass to each call to ``func`` + + save_count : int, optional + The number of values from `frames` to cache. + + interval : number, optional + Delay between frames in milliseconds. Defaults to 200. - *frames* can be a generator, an iterable, or a number of frames. + repeat_delay : number, optional + If the animation in repeated, adds a delay in milliseconds + before repeating the animation. Defaults to `None`. - *init_func* is a function used to draw a clear frame. If not given, the - results of drawing from the first item in the frames sequence will be - used. This function will be called once before the first frame. + repeat : bool, optional + Controls whether the animation should repeat when the sequence + of frames is completed. Defaults to `True`. - If blit=True, *func* and *init_func* must return an iterable of - artists to be re-drawn. + blit : bool, optional + Controls whether blitting is used to optimize drawing. Defaults + to `False`. - *kwargs* include *repeat*, *repeat_delay*, and *interval*: - *interval* draws a new frame every *interval* milliseconds. - *repeat* controls whether the animation should repeat when the sequence - of frames is completed. - *repeat_delay* optionally adds a delay in milliseconds before repeating - the animation. ''' def __init__(self, fig, func, frames=None, init_func=None, fargs=None, save_count=None, **kwargs): diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 223f230aed96..db31e4d5960a 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -122,11 +122,13 @@ MSFolders = \ r'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders' -MSFontDirectories = [ + +MSFontDirectories = [ r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts', r'SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts'] -X11FontDirectories = [ + +X11FontDirectories = [ # an old standard installation point "/usr/X11R6/lib/X11/fonts/TTF/", "/usr/X11/lib/X11/fonts", @@ -156,6 +158,7 @@ path = os.path.join(home, '.fonts') X11FontDirectories.append(path) + def get_fontext_synonyms(fontext): """ Return a list of file extensions extensions that are synonyms for @@ -165,6 +168,7 @@ def get_fontext_synonyms(fontext): 'otf': ('ttf', 'otf'), 'afm': ('afm',)}[fontext] + def list_fonts(directory, extensions): """ Return a list of all fonts matching any of the extensions, @@ -174,6 +178,7 @@ def list_fonts(directory, extensions): for ext in extensions]) return cbook.listFiles(directory, pattern) + def win32FontDirectory(): """ Return the user-specified font directory for Win32. This is @@ -186,7 +191,7 @@ def win32FontDirectory(): try: from six.moves import winreg except ImportError: - pass # Fall through to default + pass # Fall through to default else: try: user = winreg.OpenKey(winreg.HKEY_CURRENT_USER, MSFolders) @@ -194,13 +199,14 @@ def win32FontDirectory(): try: return winreg.QueryValueEx(user, 'Fonts')[0] except OSError: - pass # Fall through to default + pass # Fall through to default finally: winreg.CloseKey(user) except OSError: - pass # Fall through to default + pass # Fall through to default return os.path.join(os.environ['WINDIR'], 'Fonts') + def win32InstalledFonts(directory=None, fontext='ttf'): """ Search for fonts in the specified font directory, or use the @@ -246,6 +252,7 @@ def win32InstalledFonts(directory=None, fontext='ttf'): winreg.CloseKey(local) return None + def OSXInstalledFonts(directories=None, fontext='ttf'): """ Get list of font files on OS X - ignores font suffix by default. @@ -338,6 +345,7 @@ def findSystemFonts(fontpaths=None, fontext='ttf'): return [fname for fname in fontfiles if os.path.exists(fname)] + def weight_as_number(weight): """ Return the weight property as a numeric value. String values @@ -419,7 +427,6 @@ def ttfFontProperty(font): else: style = 'normal' - # Variants are: small-caps and normal (default) # !!!! Untested @@ -451,8 +458,8 @@ def ttfFontProperty(font): # Relative stretches are: wider, narrower # Child value is: inherit - if sfnt4.find('narrow') >= 0 or sfnt4.find('condensed') >= 0 or \ - sfnt4.find('cond') >= 0: + if (sfnt4.find('narrow') >= 0 or sfnt4.find('condensed') >= 0 or + sfnt4.find('cond') >= 0): stretch = 'condensed' elif sfnt4.find('demi cond') >= 0: stretch = 'semi-condensed' @@ -672,12 +679,12 @@ def __init__(self, fname = None, # if this is set, it's a hardcoded filename to use _init = None # used only by copy() ): - self._family = None - self._slant = None - self._variant = None - self._weight = None - self._stretch = None - self._size = None + self._family = _normalize_font_family(rcParams['font.family']) + self._slant = rcParams['font.style'] + self._variant = rcParams['font.variant'] + self._weight = rcParams['font.weight'] + self._stretch = rcParams['font.stretch'] + self._size = rcParams['font.size'] self._file = None # This is used only by copy() @@ -731,11 +738,6 @@ def get_family(self): """ Return a list of font names that comprise the font family. """ - if self._family is None: - family = rcParams['font.family'] - if is_string_like(family): - return [family] - return family return self._family def get_name(self): @@ -750,8 +752,6 @@ def get_style(self): Return the font style. Values are: 'normal', 'italic' or 'oblique'. """ - if self._slant is None: - return rcParams['font.style'] return self._slant get_slant = get_style @@ -760,8 +760,6 @@ def get_variant(self): Return the font variant. Values are: 'normal' or 'small-caps'. """ - if self._variant is None: - return rcParams['font.variant'] return self._variant def get_weight(self): @@ -771,8 +769,6 @@ def get_weight(self): 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black' """ - if self._weight is None: - return rcParams['font.weight'] return self._weight def get_stretch(self): @@ -781,26 +777,16 @@ def get_stretch(self): 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'. """ - if self._stretch is None: - return rcParams['font.stretch'] return self._stretch def get_size(self): """ Return the font size. """ - if self._size is None: - return rcParams['font.size'] return self._size def get_size_in_points(self): - if self._size is not None: - try: - return float(self._size) - except ValueError: - pass - default_size = FontManager.get_default_size() - return default_size * font_scalings.get(self._size) + return self._size def get_file(self): """ @@ -832,11 +818,7 @@ def set_family(self, family): """ if family is None: family = rcParams['font.family'] - if is_string_like(family): - family = [six.text_type(family)] - elif not is_string_like(family) and isinstance(family, Iterable): - family = [six.text_type(f) for f in family] - self._family = family + self._family = _normalize_font_family(family) set_name = set_family def set_style(self, style): @@ -846,7 +828,7 @@ def set_style(self, style): """ if style is None: style = rcParams['font.style'] - if style not in ('normal', 'italic', 'oblique', None): + if style not in ('normal', 'italic', 'oblique'): raise ValueError("style must be normal, italic or oblique") self._slant = style set_slant = set_style @@ -857,7 +839,7 @@ def set_variant(self, variant): """ if variant is None: variant = rcParams['font.variant'] - if variant not in ('normal', 'small-caps', None): + if variant not in ('normal', 'small-caps'): raise ValueError("variant must be normal or small-caps") self._variant = variant @@ -909,10 +891,15 @@ def set_size(self, size): try: size = float(size) except ValueError: - if size is not None and size not in font_scalings: + try: + scale = font_scalings[size] + except KeyError: raise ValueError( "Size is invalid. Valid font size are " + ", ".join( str(i) for i in font_scalings.keys())) + else: + size = scale * FontManager.get_default_size() + self._size = size def set_file(self, file): @@ -941,7 +928,8 @@ def set_fontconfig_pattern(self, pattern): def copy(self): """Return a deep copy of self""" - return FontProperties(_init = self) + return FontProperties(_init=self) + def ttfdict_to_fnames(d): """ @@ -957,6 +945,7 @@ def ttfdict_to_fnames(d): fnames.append(fname) return fnames + class JSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, FontManager): @@ -966,6 +955,7 @@ def default(self, o): else: return super(JSONEncoder, self).default(o) + def _json_decode(o): cls = o.pop('_class', None) if cls is None: @@ -981,6 +971,7 @@ def _json_decode(o): else: raise ValueError("don't know how to deserialize _class=%s" % cls) + def json_dump(data, filename): """Dumps a data structure as JSON in the named file. Handles FontManager and its fields.""" @@ -988,6 +979,7 @@ def json_dump(data, filename): with open(filename, 'w') as fh: json.dump(data, fh, cls=JSONEncoder, indent=2) + def json_load(filename): """Loads a data structure as JSON from the named file. Handles FontManager and its fields.""" @@ -995,6 +987,15 @@ def json_load(filename): with open(filename, 'r') as fh: return json.load(fh, object_hook=_json_decode) + +def _normalize_font_family(family): + if is_string_like(family): + family = [six.text_type(family)] + elif isinstance(family, Iterable): + family = [six.text_type(f) for f in family] + return family + + class TempCache(object): """ A class to store temporary caches that are (a) not saved to disk diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index bad1661e39fe..ca0292e96377 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -264,6 +264,10 @@ def limit_range_for_scale(self, vmin, vmax, minpos): """ Limit the domain to positive values. """ + if not np.isfinite(minpos): + minpos = 1e-300 # This value should rarely if ever + # end up with a visible effect. + return (minpos if vmin <= 0 else vmin, minpos if vmax <= 0 else vmax) @@ -499,7 +503,10 @@ def limit_range_for_scale(self, vmin, vmax, minpos): """ Limit the domain to values between 0 and 1 (excluded). """ - return (minpos if vmin <= 0 else minpos, + if not np.isfinite(minpos): + minpos = 1e-7 # This value should rarely if ever + # end up with a visible effect. + return (minpos if vmin <= 0 else vmin, 1 - minpos if vmax >= 1 else vmax) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index f73aa2ddeacc..f070b320d4b4 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -174,6 +174,25 @@ def test_autoscale_tight(): assert_allclose(ax.get_xlim(), (-0.15, 3.15)) assert_allclose(ax.get_ylim(), (1.0, 4.0)) + +@cleanup(style='default') +def test_autoscale_log_shared(): + # related to github #7587 + # array starts at zero to trigger _minpos handling + x = np.arange(100, dtype=float) + fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True) + ax1.loglog(x, x) + ax2.semilogx(x, x) + ax1.autoscale(tight=True) + ax2.autoscale(tight=True) + plt.draw() + lims = (x[1], x[-1]) + assert_allclose(ax1.get_xlim(), lims) + assert_allclose(ax1.get_ylim(), lims) + assert_allclose(ax2.get_xlim(), lims) + assert_allclose(ax2.get_ylim(), (x[0], x[-1])) + + @cleanup(style='default') def test_use_sticky_edges(): fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 88bbc4ad0349..44d2abb38aee 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -399,3 +399,15 @@ def test_agg_text_clip(): ax1.text(x, y, "foo", clip_on=True) ax2.text(x, y, "foo") plt.show() + + +@cleanup +def test_text_size_binding(): + from matplotlib.font_manager import FontProperties + + matplotlib.rcParams['font.size'] = 10 + fp = FontProperties(size='large') + sz1 = fp.get_size_in_points() + matplotlib.rcParams['font.size'] = 100 + + assert sz1 == fp.get_size_in_points() diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 5224a037dab7..b9a00dc259bb 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -355,7 +355,7 @@ def _get_layout(self, renderer): baseline = 0 for i, line in enumerate(lines): - clean_line, ismath = self.is_math_text(line) + clean_line, ismath = self.is_math_text(line, self.get_usetex()) if clean_line: w, h, d = renderer.get_text_width_height_descent(clean_line, self._fontproperties, @@ -781,7 +781,8 @@ def draw(self, renderer): y = y + posy if renderer.flipy(): y = canvash - y - clean_line, ismath = textobj.is_math_text(line) + clean_line, ismath = textobj.is_math_text(line, + self.get_usetex()) if textobj.get_path_effects(): from matplotlib.patheffects import PathEffectRenderer @@ -1211,7 +1212,7 @@ def set_text(self, s): self.stale = True @staticmethod - def is_math_text(s): + def is_math_text(s, usetex=None): """ Returns a cleaned string and a boolean flag. The flag indicates if the given string *s* contains any mathtext, @@ -1221,7 +1222,9 @@ def is_math_text(s): """ # Did we find an even number of non-escaped dollar signs? # If so, treat is as math text. - if rcParams['text.usetex']: + if usetex is None: + usetex = rcParams['text.usetex'] + if usetex: if s == ' ': s = r'\ ' return s, 'TeX' @@ -1255,7 +1258,7 @@ def set_usetex(self, usetex): `rcParams['text.usetex']` """ if usetex is None: - self._usetex = None + self._usetex = rcParams['text.usetex'] else: self._usetex = bool(usetex) self.stale = True diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 58ed2cb5295d..afea9f748e05 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -931,10 +931,6 @@ def set_locs(self, locs=None): self._sublabels = None return - b = self._base - - vmin, vmax = self.axis.get_view_interval() - # Handle symlog case: linthresh = self._linthresh if linthresh is None: @@ -943,6 +939,18 @@ def set_locs(self, locs=None): except AttributeError: pass + vmin, vmax = self.axis.get_view_interval() + if vmin > vmax: + vmin, vmax = vmax, vmin + + if linthresh is None and vmin <= 0: + # It's probably a colorbar with + # a format kwarg setting a LogFormatter in the manner + # that worked with 1.5.x, but that doesn't work now. + self._sublabels = set((1,)) # label powers of base + return + + b = self._base if linthresh is not None: # symlog # Only compute the number of decades in the logarithmic part of the # axis @@ -972,37 +980,38 @@ def set_locs(self, locs=None): # Label all integer multiples of base**n. self._sublabels = set(np.arange(1, b + 1)) + def _num_to_string(self, x, vmin, vmax): + if x > 10000: + s = '%1.0e' % x + elif x < 1: + s = '%1.0e' % x + else: + s = self.pprint_val(x, vmax - vmin) + def __call__(self, x, pos=None): """ Return the format for tick val `x`. """ - vmin, vmax = self.axis.get_view_interval() - vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) - d = abs(vmax - vmin) - b = self._base - if x == 0.0: + if x == 0.0: # Symlog return '0' + sign = np.sign(x) x = abs(x) + b = self._base # only label the decades fx = math.log(x) / math.log(b) is_x_decade = is_close_to_int(fx) exponent = np.round(fx) if is_x_decade else np.floor(fx) coeff = np.round(x / b ** exponent) + if self.labelOnlyBase and not is_x_decade: return '' if self._sublabels is not None and coeff not in self._sublabels: return '' - if x > 10000: - s = '%1.0e' % x - elif x < 1: - s = '%1.0e' % x - else: - s = self.pprint_val(x, d) - if sign == -1: - s = '-%s' % s - + vmin, vmax = self.axis.get_view_interval() + vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) + s = self._num_to_string(x, vmin, vmax) return self.fix_minus(s) def format_data(self, value): @@ -1055,41 +1064,16 @@ class LogFormatterExponent(LogFormatter): """ Format values for log axis using ``exponent = log_base(value)``. """ - def __call__(self, x, pos=None): - """ - Return the format for tick value `x`. - """ - vmin, vmax = self.axis.get_view_interval() - vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) - d = abs(vmax - vmin) - b = self._base - if x == 0: - return '0' - sign = np.sign(x) - x = abs(x) - # only label the decades - fx = math.log(x) / math.log(b) - - is_x_decade = is_close_to_int(fx) - exponent = np.round(fx) if is_x_decade else np.floor(fx) - coeff = np.round(x / b ** exponent) - - if self.labelOnlyBase and not is_x_decade: - return '' - if self._sublabels is not None and coeff not in self._sublabels: - return '' - + def _num_to_string(self, x, vmin, vmax): + fx = math.log(x) / math.log(self._base) if abs(fx) > 10000: s = '%1.0g' % fx elif abs(fx) < 1: s = '%1.0g' % fx else: - fd = math.log(abs(d)) / math.log(b) + fd = math.log(vmax - vmin) / math.log(self._base) s = self.pprint_val(fx, fd) - if sign == -1: - s = '-%s' % s - - return self.fix_minus(s) + return s class LogFormatterMathtext(LogFormatter): @@ -1111,11 +1095,8 @@ def __call__(self, x, pos=None): The position `pos` is ignored. """ - b = self._base usetex = rcParams['text.usetex'] - - # only label the decades - if x == 0: + if x == 0: # Symlog if usetex: return '$0$' else: @@ -1123,23 +1104,25 @@ def __call__(self, x, pos=None): sign_string = '-' if x < 0 else '' x = abs(x) + b = self._base + # only label the decades fx = math.log(x) / math.log(b) is_x_decade = is_close_to_int(fx) exponent = np.round(fx) if is_x_decade else np.floor(fx) coeff = np.round(x / b ** exponent) + if self.labelOnlyBase and not is_x_decade: + return '' + if self._sublabels is not None and coeff not in self._sublabels: + return '' + # use string formatting of the base if it is not an integer if b % 1 == 0.0: base = '%d' % b else: base = '%s' % b - if self.labelOnlyBase and not is_x_decade: - return '' - if self._sublabels is not None and coeff not in self._sublabels: - return '' - if not is_x_decade: return self._non_decade_format(sign_string, base, fx, usetex) else: @@ -2142,23 +2125,11 @@ def view_limits(self, vmin, vmax): 'Try to choose the view limits intelligently' b = self._base - if vmax < vmin: - vmin, vmax = vmax, vmin + vmin, vmax = self.nonsingular(vmin, vmax) if self.axis.axes.name == 'polar': vmax = math.ceil(math.log(vmax) / math.log(b)) vmin = b ** (vmax - self.numdecs) - return vmin, vmax - - minpos = self.axis.get_minpos() - - if minpos <= 0 or not np.isfinite(minpos): - raise ValueError( - "Data has no positive values, and therefore can not be " - "log-scaled.") - - if vmin <= 0: - vmin = minpos if rcParams['axes.autolimit_mode'] == 'round_numbers': if not is_decade(vmin, self._base): @@ -2166,12 +2137,29 @@ def view_limits(self, vmin, vmax): if not is_decade(vmax, self._base): vmax = decade_up(vmax, self._base) - if vmin == vmax: - vmin = decade_down(vmin, self._base) - vmax = decade_up(vmax, self._base) + return vmin, vmax - result = mtransforms.nonsingular(vmin, vmax) - return result + def nonsingular(self, vmin, vmax): + if not np.isfinite(vmin) or not np.isfinite(vmax): + return 1, 10 # initial range, no data plotted yet + + if vmin > vmax: + vmin, vmax = vmax, vmin + if vmax <= 0: + warnings.warn( + "Data has no positive values, and therefore cannot be " + "log-scaled.") + return 1, 10 + + minpos = self.axis.get_minpos() + if not np.isfinite(minpos): + minpos = 1e-300 # This should never take effect. + if vmin <= 0: + vmin = minpos + if vmin == vmax: + vmin = decade_down(vmin, self._base) + vmax = decade_up(vmax, self._base) + return vmin, vmax class SymmetricalLogLocator(Locator): @@ -2370,32 +2358,7 @@ def tick_values(self, vmin, vmax): if hasattr(self.axis, 'axes') and self.axis.axes.name == 'polar': raise NotImplementedError('Polar axis cannot be logit scaled yet') - # what to do if a window beyond ]0, 1[ is chosen - if vmin <= 0.0: - if self.axis is not None: - vmin = self.axis.get_minpos() - - if (vmin <= 0.0) or (not np.isfinite(vmin)): - raise ValueError( - "Data has no values in ]0, 1[ and therefore can not be " - "logit-scaled.") - - # NOTE: for vmax, we should query a property similar to get_minpos, but - # related to the maximal, less-than-one data point. Unfortunately, - # get_minpos is defined very deep in the BBox and updated with data, - # so for now we use the trick below. - if vmax >= 1.0: - if self.axis is not None: - vmax = 1 - self.axis.get_minpos() - - if (vmax >= 1.0) or (not np.isfinite(vmax)): - raise ValueError( - "Data has no values in ]0, 1[ and therefore can not be " - "logit-scaled.") - - if vmax < vmin: - vmin, vmax = vmax, vmin - + vmin, vmax = self.nonsingular(vmin, vmax) vmin = np.log10(vmin / (1 - vmin)) vmax = np.log10(vmax / (1 - vmax)) @@ -2430,6 +2393,36 @@ def tick_values(self, vmin, vmax): return self.raise_if_exceeds(np.array(ticklocs)) + def nonsingular(self, vmin, vmax): + initial_range = (1e-7, 1 - 1e-7) + if not np.isfinite(vmin) or not np.isfinite(vmax): + return initial_range # no data plotted yet + + if vmin > vmax: + vmin, vmax = vmax, vmin + + # what to do if a window beyond ]0, 1[ is chosen + if self.axis is not None: + minpos = self.axis.get_minpos() + if not np.isfinite(minpos): + return initial_range # again, no data plotted + else: + minpos = 1e-7 # should not occur in normal use + + # NOTE: for vmax, we should query a property similar to get_minpos, but + # related to the maximal, less-than-one data point. Unfortunately, + # Bbox._minpos is defined very deep in the BBox and updated with data, + # so for now we use 1 - minpos as a substitute. + + if vmin <= 0: + vmin = minpos + if vmax >= 1: + vmax = 1 - minpos + if vmin == vmax: + return 0.1 * vmin, 1 - 0.1 * vmin + + return vmin, vmax + class AutoLocator(MaxNLocator): def __init__(self): diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index f1296313c3b3..e2cc782e6a70 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -790,7 +790,7 @@ def __init__(self, points, **kwargs): raise ValueError('Bbox points must be of the form ' '"[[x0, y0], [x1, y1]]".') self._points = points - self._minpos = np.array([0.0000001, 0.0000001]) + self._minpos = np.array([np.inf, np.inf]) self._ignore = True # it is helpful in some contexts to know if the bbox is a # default or has been mutated; we store the orig points to