From 80d8dd858338cee11ceb0adef3724d3d1b06a9c6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 7 Dec 2016 17:14:49 -0500 Subject: [PATCH 01/24] DOC: partially numpydoc-ify animation module --- lib/matplotlib/animation.py | 443 +++++++++++++++++++++++++++--------- 1 file changed, 331 insertions(+), 112 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index b4f1b77bb967..1f1ad0591c41 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -68,6 +68,8 @@ def adjusted_figsize(w, h, dpi, n): # A registry for available MovieWriter classes class MovieWriterRegistry(object): + '''Registry of of available writer classes by human readable name + ''' def __init__(self): self.avail = dict() self._registered = dict() @@ -109,6 +111,15 @@ def list(self): return list(self.avail.keys()) def is_available(self, name): + '''If given writer is available + + Parameters + ---------- + name : str + + Returns + available : bool + ''' self.ensure_not_dirty() return name in self.avail @@ -122,11 +133,36 @@ def __getitem__(self, name): class MovieWriter(object): - ''' - 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. + '''Base class for writing movies. + + This class is set up to provide for writing movie frame data to a + pipe. See examples for how to use these classes. + + Attributes + ---------- + + frame_format : string + 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 + + Examples + -------- + + 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 :: + + moviewriter = MovieWriter(...) + moveiewriter.setup() + for j in range(n): + update_figure(n) + moviewriter.grab_frame() + moviewriter.finish() + + saving() is provided as a context manager to facilitate this process as:: with moviewriter.saving('myfile.mp4'): @@ -136,15 +172,13 @@ class MovieWriter(object): The use of the context manager ensures that setup and cleanup are performed as necessary. - frame_format: string - The format used in writing frame data, defaults to 'rgba' ''' def __init__(self, fps=5, codec=None, bitrate=None, extra_args=None, metadata=None): ''' - Construct a new MovieWriter object. - + Parameters + ---------- fps: int Framerate for movie. codec: string or None, optional @@ -210,11 +244,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. ''' @@ -329,29 +366,37 @@ 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. - - 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. - frame_prefix: string, optional + frame_prefix : string, optional The filename prefix to use for the temporary files. Defaults to '_tmp' - clear_temp: bool + clear_temp : bool Specifies whether the temporary files should be deleted after the movie is written. (Useful for debugging.) Defaults to True. + + ''' + + 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. + ''' self.fig = fig self.outfile = outfile @@ -463,6 +508,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 + `MoveWriterBase` sub-class. + ''' + exec_key = 'animation.ffmpeg_path' args_key = 'animation.ffmpeg_args' @@ -490,6 +541,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. @@ -506,6 +562,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'] @@ -520,6 +582,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 + `MoveWriterBase` sub-class. + ''' + exec_key = 'animation.avconv_path' args_key = 'animation.avconv_args' @@ -527,13 +595,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 @@ -616,6 +693,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 + `MoveWriterBase` sub-class. + ''' + exec_key = 'animation.convert_path' args_key = 'animation.convert_args' @@ -667,6 +750,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', @@ -681,6 +770,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'] @@ -691,19 +787,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 @@ -758,55 +869,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:`MovieWriter` 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 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 `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 are used to + construct a :class:`MovieWriter` instance and can only be + passed if `writer` is a string. If they are pass 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 @@ -1081,15 +1209,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. 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* controls whether the animation should repeat when the sequence - of frames is completed. + repeat: bool, optional + controls whether the animation should repeat when the sequence + of frames is completed. + + 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): @@ -1147,13 +1291,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 + 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. + + interval : number, optional + Delay between frames. 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. + + 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. @@ -1202,25 +1371,75 @@ 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(): + + 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. 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. - 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): From 24c7aad75a21840bedcc6ee6a68ece2a35a2c758 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 7 Dec 2016 22:51:35 -0500 Subject: [PATCH 02/24] DOC: use auto summary in animation_api.rst --- doc/api/animation_api.rst | 119 +++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 9 deletions(-) diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index d43dc351abae..57c248765143 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -1,12 +1,113 @@ -********* -animation -********* +====================== + ``animation`` module +====================== +.. currentmodule:: matplotlib.animation -:mod:`matplotlib.animation` -=========================== -.. automodule:: matplotlib.animation - :members: - :undoc-members: - :show-inheritance: +Animation +========= + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + FuncAnimation + ArtistAnimation + Animation.save + + +Writer Classes +============== + + + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + AVConvFileWriter + AVConvWriter + FFMpegFileWriter + FFMpegWriter + ImageMagickFileWriter + ImageMagickWriter + + +Helper Classes +============== + + +Animation Base Classes +---------------------- + + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + Animation + TimedAnimation + + +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: + + 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 From e465fb71bd86c81ae2f91c3bebc80b3c84fee113 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 8 Dec 2016 00:35:23 -0500 Subject: [PATCH 03/24] DOC: start to add animation prose --- doc/api/animation_api.rst | 162 +++++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 1 deletion(-) diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index 57c248765143..5d9a7c3497b5 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -2,19 +2,170 @@ ``animation`` module ====================== -.. currentmodule:: matplotlib.animation +.. automodule:: matplotlib.animation + +.. contents:: Table of Contents + :depth: 1 + :local: + :backlinks: entry Animation ========= +The easiest way to make a live animation in mpl 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 tho 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 use 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: + arts = 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 as 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(arts): + fig.canvas.restore_region(bg_cache) + for a in arts: + a.axes.draw_artist(a) + + ax.figure.canvas.blit(ax.bbox) + + arts = init_func() + + for a in arts: + a.set_animated(True) + + fig.canvas.draw() + bg_cache = fig.canvas.copy_from_bbox(ax.bbox) + + for f in frames: + arts = func(f, *fargs) + update_blit(arts) + 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(i): + xdata.append(i) + ydata.append(np.sin(i)) + 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 forth 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 @@ -33,6 +184,10 @@ Writer Classes ImageMagickFileWriter ImageMagickWriter +:ref:`animation-moviewriter` + + +.. _ani_writer_classes: Helper Classes ============== @@ -50,6 +205,11 @@ Animation Base Classes TimedAnimation +Custom Animation classes +------------------------ + +:ref:`animation-subplots` + Writer Registry --------------- From ff1e71e31495e0fc87858c44228cf8bce8d5cb79 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 10 Dec 2016 21:31:22 -0500 Subject: [PATCH 04/24] DOC: address comments --- doc/api/animation_api.rst | 10 ++-- lib/matplotlib/animation.py | 110 ++++++++++++++---------------------- 2 files changed, 46 insertions(+), 74 deletions(-) diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index 5d9a7c3497b5..5836ff94463f 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -23,7 +23,7 @@ The easiest way to make a live animation in mpl is to use one of the FuncAnimation ArtistAnimation -In both cases it is critical to keep a reference to tho instance +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 @@ -115,9 +115,9 @@ artist at a global scope and let Python sort things out. For example :: ax.set_ylim(-1, 1) return ln, - def update(i): - xdata.append(i) - ydata.append(np.sin(i)) + def update(frame): + xdata.append(frame) + ydata.append(np.sin(frame)) ln.set_data(xdata, ydata) return ln, @@ -128,7 +128,7 @@ artist at a global scope and let Python sort things out. For example :: 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 forth method is to create a class. +artists and functions. A fourth method is to create a class. diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 1f1ad0591c41..dc6e7a1b7e3a 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -68,7 +68,7 @@ def adjusted_figsize(w, h, dpi, n): # A registry for available MovieWriter classes class MovieWriterRegistry(object): - '''Registry of of available writer classes by human readable name + '''Registry of available writer classes by human readable name ''' def __init__(self): self.avail = dict() @@ -111,13 +111,14 @@ def list(self): return list(self.avail.keys()) def is_available(self, name): - '''If given writer is available + '''Check if given writer is available by name Parameters ---------- name : str Returns + ------- available : bool ''' self.ensure_not_dirty() @@ -141,42 +142,19 @@ class MovieWriter(object): Attributes ---------- - frame_format : string + 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 - - Examples - -------- - - 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 :: - - moviewriter = MovieWriter(...) - moveiewriter.setup() - for j in range(n): - update_figure(n) - moviewriter.grab_frame() - moviewriter.finish() - - - saving() is provided as a context manager to facilitate this process as:: - - with moviewriter.saving('myfile.mp4'): - # Iterate over frames - moviewriter.grab_frame() - - The use of the context manager ensures that setup and cleanup are - performed as necessary. + This must be provided by the sub-classes. ''' def __init__(self, fps=5, codec=None, bitrate=None, extra_args=None, metadata=None): - ''' + '''MovieWriter + Parameters ---------- fps: int @@ -371,31 +349,28 @@ class FileMovieWriter(MovieWriter): This must be sub-classed to be useful. ''' def __init__(self, *args, **kwargs): - ''' - Parameters - ---------- - 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. - - ''' - 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`. ''' self.fig = fig @@ -541,7 +516,7 @@ def output_args(self): # Combine FFMpeg options with pipe-based writing @writers.register('ffmpeg') class FFMpegWriter(MovieWriter, FFMpegBase): - '''Pipe based ffmpeg writer. + '''Pipe-based ffmpeg writer. Frames are streamed directly to ffmpeg via a pipe and written in a single pass. @@ -562,7 +537,7 @@ def _args(self): # Combine FFMpeg options with temp file-based writing @writers.register('ffmpeg_file') class FFMpegFileWriter(FileMovieWriter, FFMpegBase): - '''File based ffmpeg writer + '''File-based ffmpeg writer Frames are written to temporary files on disk and then stitched together at the end. @@ -595,7 +570,7 @@ class AVConvBase(FFMpegBase): # Combine AVConv options with pipe-based writing @writers.register('avconv') class AVConvWriter(AVConvBase, FFMpegWriter): - '''Pipe based avconv writer. + '''Pipe-based avconv writer. Frames are streamed directly to avconv via a pipe and written in a single pass. @@ -605,7 +580,7 @@ class AVConvWriter(AVConvBase, FFMpegWriter): # Combine AVConv options with file-based writing @writers.register('avconv_file') class AVConvFileWriter(AVConvBase, FFMpegFileWriter): - '''File based avconv writer + '''File-based avconv writer Frames are written to temporary files on disk and then stitched together at the end. @@ -750,7 +725,7 @@ def isAvailable(cls): # former. @writers.register('imagemagick') class ImageMagickWriter(ImageMagickBase, MovieWriter): - '''Pipe based animated gif + '''Pipe-based animated gif Frames are streamed directly to ImageMagick via a pipe and written in a single pass. @@ -770,7 +745,7 @@ def _args(self): # former. @writers.register('imagemagick_file') class ImageMagickFileWriter(ImageMagickBase, FileMovieWriter): - '''File based animated gif writer + '''File-based animated gif writer Frames are written to temporary files on disk and then stitched together at the end. @@ -895,7 +870,7 @@ class to use, such as 'ffmpeg' or 'mencoder'. If `None`, 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']`` + default to ``rcParams['animation.codec']`` bitrate : number, optional specifies the amount of bits used per second in the @@ -931,7 +906,7 @@ class to use, such as 'ffmpeg' or 'mencoder'. If `None`, ----- fps, codec, bitrate, extra_args, metadata are used are used to construct a :class:`MovieWriter` instance and can only be - passed if `writer` is a string. If they are pass as + passed if `writer` is a string. If they are passed as non-`None` and ``writer`` is a :class:`MovieWriter`, a `RuntimeError` will be raised. @@ -1220,7 +1195,7 @@ class TimedAnimation(Animation): other needed events. interval : number, optional - Delay between frames. Defaults to 200 + Delay between frames in milliseconds. Defaults to 200. repeat_delay : number, optional If the animation in repeated, adds a delay in milliseconds @@ -1228,7 +1203,7 @@ class TimedAnimation(Animation): repeat: bool, optional controls whether the animation should repeat when the sequence - of frames is completed. + of frames is completed. Defaults to `True`. blit : bool, optional controls whether blitting is used to optimize drawing. Defaults @@ -1309,15 +1284,15 @@ class ArtistAnimation(TimedAnimation): be disabled for other frames. interval : number, optional - Delay between frames. Defaults to 200 + Delay between frames in miliseconds. Defaults to 200. repeat_delay : number, optional If the animation in repeated, adds a delay in milliseconds - before repeating the animation. Defaults to None + before repeating the animation. Defaults to `None`. repeat: bool, optional controls whether the animation should repeat when the sequence - of frames is completed. + of frames is completed. Defaults to `True`. blit : bool, optional controls whether blitting is used to optimize drawing. Defaults @@ -1381,7 +1356,6 @@ class FuncAnimation(TimedAnimation): 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. @@ -1390,7 +1364,6 @@ class FuncAnimation(TimedAnimation): 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 @@ -1406,7 +1379,6 @@ def gen_function(): 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 @@ -1426,15 +1398,15 @@ def init_func() -> iterable_of_artists: The number of values from `frames` to cache. interval : number, optional - Delay between frames. Defaults to 200 + 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 + before repeating the animation. Defaults to `None`. repeat: bool, optional controls whether the animation should repeat when the sequence - of frames is completed. + of frames is completed. Defaults to `True` blit : bool, optional controls whether blitting is used to optimize drawing. Defaults From 55017cd1aca60ac18cff4ce699909f4d32fe6756 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 10 Dec 2016 21:51:33 -0500 Subject: [PATCH 05/24] DOC: re-work MovieWriter section a bit --- doc/api/animation_api.rst | 66 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index 5836ff94463f..3b433fcf7d75 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -173,16 +173,76 @@ 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: - AVConvFileWriter - AVConvWriter - FFMpegFileWriter + 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 does is provide is 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 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(...) + moveiewriter.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` From 437386b35bcecc362abc37f97ebac5c0b2dceebf Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 10 Dec 2016 21:55:15 -0500 Subject: [PATCH 06/24] MNT: tweak MovieWriter.saving to be more cautious - always try to call `finish` even if an exception is raised. - return `self` so that with writer.saving(...) as mv: mv.grab_frame() works as expected. --- lib/matplotlib/animation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index b4f1b77bb967..48d596fbc0ec 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -236,8 +236,10 @@ def saving(self, *args, **kw): ''' # This particular sequence is what contextlib.contextmanager wants self.setup(*args, **kw) - yield - self.finish() + try: + yield self + finally: + self.finish() def _run(self): # Uses subprocess to call the program for assembling frames into a From 1a6c61c8fa6215b74453e5ebebf0ec516796a609 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 10 Dec 2016 22:05:45 -0500 Subject: [PATCH 07/24] DOC: address the rest of the comments --- doc/api/animation_api.rst | 24 +++++++-------- lib/matplotlib/animation.py | 58 ++++++++++++++++++++----------------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index 3b433fcf7d75..8a95b2314da6 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -13,7 +13,7 @@ Animation ========= -The easiest way to make a live animation in mpl is to use one of the +The easiest way to make a live animation in matplotlib is to use one of the `Animation` classes. .. autosummary:: @@ -30,7 +30,7 @@ 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 use to disk use +To save an animation to disk use .. autosummary:: :toctree: _as_gen @@ -48,7 +48,7 @@ See :ref:`ani_writer_classes` below for details about what movie formats are sup The inner workings of `FuncAnimation` is more-or-less:: for d in frames: - arts = func(d, *fargs) + artists = func(d, *fargs) fig.canvas.draw_idle() plt.pause(interval) @@ -59,7 +59,7 @@ axes, and easily save the animation to a movie file. 'Blitting' is a `old technique `__ in computer graphics. The -general gist is to take as existing bit map (in our case a mostly +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 @@ -68,24 +68,24 @@ time. When using blitting (by passing ``blit=True``) the core loop of ax = fig.gca() - def update_blit(arts): + def update_blit(artists): fig.canvas.restore_region(bg_cache) - for a in arts: + for a in artists: a.axes.draw_artist(a) ax.figure.canvas.blit(ax.bbox) - arts = init_func() + artists = init_func() - for a in arts: + for a in artists: a.set_animated(True) fig.canvas.draw() bg_cache = fig.canvas.copy_from_bbox(ax.bbox) for f in frames: - arts = func(f, *fargs) - update_blit(arts) + artists = func(f, *fargs) + update_blit(artists) plt.pause(interval) This is of course leaving out many details (such as updating the @@ -98,7 +98,7 @@ 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 +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 :: @@ -200,7 +200,7 @@ slower, these writers can be easier to debug. AVConvFileWriter -Fundamentally, a MovieWriter does is provide is a way to grab +Fundamentally, a `MovieWriter` does is provide is 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 diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index dc6e7a1b7e3a..83cdbb0430f2 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -486,7 +486,7 @@ class FFMpegBase(object): '''Mixin class for FFMpeg output To be useful this must be multiply-inherited from with a - `MoveWriterBase` sub-class. + `MovieWriterBase` sub-class. ''' exec_key = 'animation.ffmpeg_path' @@ -560,7 +560,7 @@ class AVConvBase(FFMpegBase): '''Mixin class for avconv output To be useful this must be multiply-inherited from with a - `MoveWriterBase` sub-class. + `MovieWriterBase` sub-class. ''' exec_key = 'animation.avconv_path' @@ -671,7 +671,7 @@ class ImageMagickBase(object): '''Mixin class for ImageMagick output To be useful this must be multiply-inherited from with a - `MoveWriterBase` sub-class. + `MovieWriterBase` sub-class. ''' exec_key = 'animation.convert_path' @@ -850,7 +850,7 @@ def save(self, filename, writer=None, fps=None, dpi=None, codec=None, ---------- filename : str - the output filename, e.g., :file:`mymovie.mp4` + 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 @@ -863,34 +863,34 @@ class to use, such as 'ffmpeg' or 'mencoder'. If `None`, the frames per second. dpi : number, optional - controls the dots per inch for the movie frames. This + 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 + 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 amount of bits used per second in the + 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 `None`, defaults to ``rcParam['animation.bitrate']`` extra_args : list, optional - list of extra string arguments to be passed to the + 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 + 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 + 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 @@ -898,13 +898,13 @@ class to use, such as 'ffmpeg' or 'mencoder'. If `None`, animations. savefig_kwargs : dict, optional - is a dictionary containing keyword arguments to be passed + 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 are used to + 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 @@ -1201,13 +1201,13 @@ class TimedAnimation(Animation): 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 + 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`. + Controls whether blitting is used to optimize drawing. Defaults + to `False`. ''' def __init__(self, fig, interval=200, repeat_delay=None, repeat=True, @@ -1279,7 +1279,7 @@ class ArtistAnimation(TimedAnimation): other needed events. artists : list - with each list entry a collection of artists that + 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. @@ -1290,13 +1290,13 @@ class ArtistAnimation(TimedAnimation): 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 + 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`. + Controls whether blitting is used to optimize drawing. Defaults + to `False`. ''' def __init__(self, fig, artists, *args, **kwargs): @@ -1374,12 +1374,16 @@ def func(fr: object, *fargs) -> iterable_of_artists: If a generator function, then must have the signature :: - def gen_function(): + 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`` + If `None`, then equivalent to passing ``itertools.count``. init_func : callable, optional - a function used to draw a clear frame. If not given, the + 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. @@ -1404,12 +1408,12 @@ def init_func() -> iterable_of_artists: 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 + 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 + Controls whether blitting is used to optimize drawing. Defaults to `False`. ''' From 8e8291c46372fc01371e3900e91587a71a020092 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 10 Dec 2016 23:15:24 -0500 Subject: [PATCH 08/24] DOC: Fix more typos and minor errors --- doc/api/animation_api.rst | 13 ++++++------- lib/matplotlib/animation.py | 37 +++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index 8a95b2314da6..3519bca48c23 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -200,12 +200,11 @@ slower, these writers can be easier to debug. AVConvFileWriter -Fundamentally, a `MovieWriter` does is provide is 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 in the arguments to their respective ``setup`` -methods. +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:: @@ -225,7 +224,7 @@ at a time and ``finish()`` finalizes the movie and writes the output file to disk. For example :: moviewriter = MovieWriter(...) - moveiewriter.setup(fig=fig, 'my_movie.ext', dpi=100) + moviewriter.setup(fig=fig, 'my_movie.ext', dpi=100) for j in range(n): update_figure(n) moviewriter.grab_frame() diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 83cdbb0430f2..f18118b366e5 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -68,8 +68,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 - ''' + '''Registry of available writer classes by human readable name.''' def __init__(self): self.avail = dict() self._registered = dict() @@ -111,7 +110,7 @@ def list(self): return list(self.avail.keys()) def is_available(self, name): - '''Check if given writer is available by name + '''Check if given writer is available by name. Parameters ---------- @@ -277,6 +276,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. ''' @@ -344,7 +344,7 @@ def isAvailable(cls): class FileMovieWriter(MovieWriter): - '''`MovieWriter` for writing to individual files and stitching at the end + '''`MovieWriter` for writing to individual files and stitching at the end. This must be sub-classed to be useful. ''' @@ -483,7 +483,7 @@ 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 + '''Mixin class for FFMpeg output. To be useful this must be multiply-inherited from with a `MovieWriterBase` sub-class. @@ -537,7 +537,7 @@ def _args(self): # Combine FFMpeg options with temp file-based writing @writers.register('ffmpeg_file') class FFMpegFileWriter(FileMovieWriter, FFMpegBase): - '''File-based ffmpeg writer + '''File-based ffmpeg writer. Frames are written to temporary files on disk and then stitched together at the end. @@ -557,7 +557,7 @@ def _args(self): # Base class of avconv information. AVConv has identical arguments to # FFMpeg class AVConvBase(FFMpegBase): - '''Mixin class for avconv output + '''Mixin class for avconv output. To be useful this must be multiply-inherited from with a `MovieWriterBase` sub-class. @@ -580,7 +580,7 @@ class AVConvWriter(AVConvBase, FFMpegWriter): # Combine AVConv options with file-based writing @writers.register('avconv_file') class AVConvFileWriter(AVConvBase, FFMpegFileWriter): - '''File-based avconv writer + '''File-based avconv writer. Frames are written to temporary files on disk and then stitched together at the end. @@ -668,7 +668,7 @@ def _args(self): # Base class for animated GIFs with convert utility class ImageMagickBase(object): - '''Mixin class for ImageMagick output + '''Mixin class for ImageMagick output. To be useful this must be multiply-inherited from with a `MovieWriterBase` sub-class. @@ -706,7 +706,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. @@ -725,7 +725,7 @@ def isAvailable(cls): # former. @writers.register('imagemagick') class ImageMagickWriter(ImageMagickBase, MovieWriter): - '''Pipe-based animated gif + '''Pipe-based animated gif. Frames are streamed directly to ImageMagick via a pipe and written in a single pass. @@ -745,7 +745,7 @@ def _args(self): # former. @writers.register('imagemagick_file') class ImageMagickFileWriter(ImageMagickBase, FileMovieWriter): - '''File-based animated gif writer + '''File-based animated gif writer. Frames are written to temporary files on disk and then stitched together at the end. @@ -873,7 +873,7 @@ class to use, such as 'ffmpeg' or 'mencoder'. If `None`, default to ``rcParams['animation.codec']`` bitrate : number, optional - Specifies the amount of bits used per second in the + 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 @@ -1184,7 +1184,7 @@ def _repr_html_(self): class TimedAnimation(Animation): - ''':class:`Animation` subclass for time-based animation + ''':class:`Animation` subclass for time-based animation. A new frame is drawn every *interval* milliseconds. @@ -1199,7 +1199,7 @@ class TimedAnimation(Animation): repeat_delay : number, optional If the animation in repeated, adds a delay in milliseconds - before repeating the animation. Defaults to None + before repeating the animation. Defaults to `None` repeat : bool, optional Controls whether the animation should repeat when the sequence @@ -1284,7 +1284,7 @@ class ArtistAnimation(TimedAnimation): be disabled for other frames. interval : number, optional - Delay between frames in miliseconds. Defaults to 200. + Delay between frames in milliseconds. Defaults to 200. repeat_delay : number, optional If the animation in repeated, adds a delay in milliseconds @@ -1346,7 +1346,8 @@ def _draw_frame(self, artists): class FuncAnimation(TimedAnimation): - '''Makes an animation by repeatedly calling a function ``func`` + ''' + Makes an animation by repeatedly calling a function ``func``. Parameters @@ -1410,7 +1411,7 @@ def init_func() -> iterable_of_artists: repeat : bool, optional Controls whether the animation should repeat when the sequence - of frames is completed. Defaults to `True` + of frames is completed. Defaults to `True`. blit : bool, optional Controls whether blitting is used to optimize drawing. Defaults From e3de3bea24a147f8dee0bd0bed753a5aa87098f8 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Thu, 8 Dec 2016 22:04:41 -1000 Subject: [PATCH 09/24] BUG: fix minpos handling and other log ticker problems Closes #7595, #7493, #7587. Bbox._minpos is now initialized to [np.inf, np.inf] instead of [1e-7, 1e-7]. Old code with a colorbar in which the formatter is set to a LogFormatter and a SymlogNorm is used now works again (although the results are better in 2.0 if the colorbar is left to handle the formatter automatically). LogLocator now has its own nonsingular() method which provides a reasonable starting point for a log axis when no data have been plotted. --- lib/matplotlib/scale.py | 9 ++- lib/matplotlib/ticker.py | 110 +++++++++++++++++++++-------------- lib/matplotlib/transforms.py | 2 +- 3 files changed, 74 insertions(+), 47 deletions(-) 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/ticker.py b/lib/matplotlib/ticker.py index cd40422e2f97..7b23c30f9695 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -914,6 +914,16 @@ def set_locs(self, locs=None): except AttributeError: pass + 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 + if linthresh is not None: # symlog # Only compute the number of decades in the logarithmic part of the # axis @@ -951,7 +961,7 @@ def __call__(self, x, pos=None): 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) @@ -2032,23 +2042,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): @@ -2056,12 +2054,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): @@ -2260,32 +2275,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)) @@ -2320,6 +2310,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 a2ef105b7b21..16889e7048d7 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -792,7 +792,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 From 8bdabcdb3e92434a50511307af71041b7af88933 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sat, 10 Dec 2016 10:37:01 -1000 Subject: [PATCH 10/24] refactor LogFormatter classes for consistency, readability --- lib/matplotlib/ticker.py | 83 ++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 55 deletions(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 7b23c30f9695..2b7f19c20ab0 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -902,10 +902,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: @@ -914,6 +910,7 @@ def set_locs(self, locs=None): except AttributeError: pass + vmin, vmax = self.axis.get_view_interval() if vmin > vmax: vmin, vmax = vmax, vmin @@ -924,6 +921,7 @@ def set_locs(self, locs=None): 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 @@ -953,37 +951,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: # 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): @@ -1036,41 +1035,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): @@ -1092,11 +1066,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: @@ -1104,23 +1075,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: From a8e757546b53b1f40b440d40a90e7eecf7443b6a Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sat, 10 Dec 2016 10:46:19 -1000 Subject: [PATCH 11/24] api_changes: add note about LogFormatter linthresh kwarg. --- doc/api/api_changes.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 --------------------------------------------------------------------- From aa36ec308c66b1307eb4493090c27d109e6525f7 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Sat, 10 Dec 2016 11:16:31 -1000 Subject: [PATCH 12/24] test for shared log axes --- lib/matplotlib/tests/test_axes.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 07bb89adfc46..8203c0ad3a03 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -176,6 +176,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() From 93d09be711290e333a21663ebed71bd311f84a72 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sun, 11 Dec 2016 19:40:55 -0500 Subject: [PATCH 13/24] Merge pull request #7596 from anntzer/delay-fc-list-warning Delay fc-list warning by 5s. --- doc/api/api_changes/2016-12-09-AL_afm.rst | 7 ++ lib/matplotlib/font_manager.py | 92 +++++++++++------------ 2 files changed, 52 insertions(+), 47 deletions(-) create mode 100644 doc/api/api_changes/2016-12-09-AL_afm.rst diff --git a/doc/api/api_changes/2016-12-09-AL_afm.rst b/doc/api/api_changes/2016-12-09-AL_afm.rst new file mode 100644 index 000000000000..0df780a78544 --- /dev/null +++ b/doc/api/api_changes/2016-12-09-AL_afm.rst @@ -0,0 +1,7 @@ +`afm.get_fontconfig_fonts` returns a list of paths and does not check for existence +``````````````````````````````````````````````````````````````````````````````````` + +`afm.get_fontconfig_fonts` used to return a set of paths encoded as a +``{key: 1, ...}`` dict, and checked for the existence of the paths. It now +returns a list and dropped the existence check, as the same check is performed +by the caller (`afm.findSystemFonts`) as well. diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 45e9f3973f43..51920abd2781 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -47,21 +47,19 @@ see license/LICENSE_TTFQUERY. """ -import os, sys, warnings -try: - set -except NameError: - from sets import Set as set from collections import Iterable +import json +import os +import sys +from threading import Timer +import warnings + import matplotlib -from matplotlib import afm -from matplotlib import ft2font -from matplotlib import rcParams, get_cachedir +from matplotlib import afm, cbook, ft2font, rcParams, get_cachedir from matplotlib.cbook import is_string_like -import matplotlib.cbook as cbook from matplotlib.compat import subprocess -from matplotlib.fontconfig_pattern import \ - parse_fontconfig_pattern, generate_fontconfig_pattern +from matplotlib.fontconfig_pattern import ( + parse_fontconfig_pattern, generate_fontconfig_pattern) try: from functools import lru_cache @@ -265,39 +263,39 @@ def OSXInstalledFonts(directories=None, fontext='ttf'): files.extend(list_fonts(path, fontext)) return files -def get_fontconfig_fonts(fontext='ttf'): + +@lru_cache() +def _call_fc_list(): + """Cache and list the font filenames known to `fc-list`. """ - Grab a list of all the fonts that are being tracked by fontconfig - by making a system call to ``fc-list``. This is an easy way to - grab all of the fonts the user wants to be made available to - applications, without needing knowing where all of them reside. + # Delay the warning by 5s. + timer = Timer(5, lambda: warnings.warn( + 'Matplotlib is building the font cache using fc-list. ' + 'This may take a moment.')) + timer.start() + try: + out = subprocess.check_output(['fc-list', '--format=%{file}']) + except (OSError, subprocess.CalledProcessError): + return [] + finally: + timer.cancel() + fnames = [] + for fname in out.split(b'\n'): + try: + fname = six.text_type(fname, sys.getfilesystemencoding()) + except UnicodeDecodeError: + continue + fnames.append(fname) + return fnames + + +def get_fontconfig_fonts(fontext='ttf'): + """List the font filenames known to `fc-list` having the given extension. """ fontext = get_fontext_synonyms(fontext) + return [fname for fname in _call_fc_list() + if os.path.splitext(fname)[1][1:] in fontext] - fontfiles = {} - try: - warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.') - pipe = subprocess.Popen(['fc-list', '--format=%{file}\\n'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - output = pipe.communicate()[0] - except (OSError, IOError): - # Calling fc-list did not work, so we'll just return nothing - return fontfiles - - if pipe.returncode == 0: - # The line breaks between results are in ascii, but each entry - # is in in sys.filesystemencoding(). - for fname in output.split(b'\n'): - try: - fname = six.text_type(fname, sys.getfilesystemencoding()) - except UnicodeDecodeError: - continue - if (os.path.splitext(fname)[1][1:] in fontext and - os.path.exists(fname)): - fontfiles[fname] = 1 - - return fontfiles def findSystemFonts(fontpaths=None, fontext='ttf'): """ @@ -307,7 +305,7 @@ def findSystemFonts(fontpaths=None, fontext='ttf'): available. A list of TrueType fonts are returned by default with AFM fonts as an option. """ - fontfiles = {} + fontfiles = set() fontexts = get_fontext_synonyms(fontext) if fontpaths is None: @@ -319,16 +317,16 @@ def findSystemFonts(fontpaths=None, fontext='ttf'): for f in win32InstalledFonts(fontdir): base, ext = os.path.splitext(f) if len(ext)>1 and ext[1:].lower() in fontexts: - fontfiles[f] = 1 + fontfiles.add(f) else: fontpaths = X11FontDirectories # check for OS X & load its fonts if present if sys.platform == 'darwin': for f in OSXInstalledFonts(fontext=fontext): - fontfiles[f] = 1 + fontfiles.add(f) for f in get_fontconfig_fonts(fontext): - fontfiles[f] = 1 + fontfiles.add(f) elif isinstance(fontpaths, six.string_types): fontpaths = [fontpaths] @@ -336,9 +334,9 @@ def findSystemFonts(fontpaths=None, fontext='ttf'): for path in fontpaths: files = list_fonts(path, fontexts) for fname in files: - fontfiles[os.path.abspath(fname)] = 1 + fontfiles.add(os.path.abspath(fname)) - return [fname for fname in six.iterkeys(fontfiles) if os.path.exists(fname)] + return [fname for fname in fontfiles if os.path.exists(fname)] def weight_as_number(weight): """ @@ -837,7 +835,7 @@ def set_family(self, family): family = rcParams['font.family'] if is_string_like(family): family = [six.text_type(family)] - elif (not is_string_like(family) and isinstance(family, Iterable)): + elif not is_string_like(family) and isinstance(family, Iterable): family = [six.text_type(f) for f in family] self._family = family set_name = set_family From 1a21bea8e50650d8e0d2f9d022a7181d858964a1 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 12 Dec 2016 10:02:38 -0500 Subject: [PATCH 14/24] Merge pull request #7618 from samuelstjean/patch-1 DOC: fixed typo in mlab.py --- lib/matplotlib/mlab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index b431de349cf2..3d19fd2bcd8e 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -10,7 +10,7 @@ Coherence (normalized cross spectral density) :func:`csd` - Cross spectral density uing Welch's average periodogram + Cross spectral density using Welch's average periodogram :func:`detrend` Remove the mean or best fit line from an array @@ -30,7 +30,7 @@ Principal Component Analysis :func:`psd` - Power spectral density uing Welch's average periodogram + Power spectral density using Welch's average periodogram :func:`rk4` A 4th order runge kutta integrator for 1D or ND systems From 4c4779b3e6812b7e277debe7c8f95fbe709da5d1 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 12 Dec 2016 14:41:51 -0500 Subject: [PATCH 15/24] DOC: minor tweaks --- lib/matplotlib/animation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index f18118b366e5..7588dcd41ca5 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1199,7 +1199,7 @@ class TimedAnimation(Animation): repeat_delay : number, optional If the animation in repeated, adds a delay in milliseconds - before repeating the animation. Defaults to `None` + before repeating the animation. Defaults to `None`. repeat : bool, optional Controls whether the animation should repeat when the sequence @@ -1279,9 +1279,9 @@ class ArtistAnimation(TimedAnimation): other needed events. artists : 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. + 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. From 06dce269968b13af2b3c6370974f1ba331aa588f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 29 Aug 2016 23:35:59 -0400 Subject: [PATCH 16/24] API: convert string fontsize to points immediately This bakes into the text artist (via the FontProperties) the default fontsize at the time of creation. --- lib/matplotlib/font_manager.py | 8 ++++++-- lib/matplotlib/tests/test_text.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 51920abd2781..39b1068f57f8 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -910,7 +910,10 @@ 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] + size = scale * FontManager.get_default_size() + except KeyError: raise ValueError( "Size is invalid. Valid font size are " + ", ".join( str(i) for i in font_scalings.keys())) @@ -942,7 +945,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): """ 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() From 2ddcc68c7de0cfef089e5b09bcad0c4a3d04afab Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 3 Dec 2016 16:57:12 -0500 Subject: [PATCH 17/24] API: cache usetex value at text creation time --- lib/matplotlib/text.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 029cbb65f098..8f853133ce0d 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1222,7 +1222,7 @@ 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 self.get_usetex(): if s == ' ': s = r'\ ' return s, 'TeX' @@ -1256,7 +1256,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 From 98e9fb20623645ae414e875bc8ce4c88ce16eb57 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 3 Dec 2016 17:28:01 -0500 Subject: [PATCH 18/24] STY: some whitespace cleanups --- lib/matplotlib/font_manager.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 39b1068f57f8..61926a6c095b 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. @@ -297,6 +304,7 @@ def get_fontconfig_fonts(fontext='ttf'): if os.path.splitext(fname)[1][1:] in fontext] + def findSystemFonts(fontpaths=None, fontext='ttf'): """ Search for fonts in the specified font paths. If no paths are @@ -338,6 +346,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 +428,6 @@ def ttfFontProperty(font): else: style = 'normal' - # Variants are: small-caps and normal (default) # !!!! Untested @@ -451,8 +459,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' From bb37ab6fd1e2cbb9049feb2e66225701c36ba68e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 3 Dec 2016 17:28:12 -0500 Subject: [PATCH 19/24] MNT: ensure that defaults are cached init This appears to already be the case for all of these properties, but remove the rcparams lookup from the get_* methods just to be sure. --- lib/matplotlib/font_manager.py | 33 ++++++--------------------------- lib/matplotlib/text.py | 10 ++++++---- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 61926a6c095b..505f46094828 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -682,11 +682,11 @@ def __init__(self, _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._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() @@ -740,11 +740,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): @@ -759,8 +754,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 @@ -769,8 +762,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): @@ -780,8 +771,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): @@ -790,26 +779,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): """ diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 8f853133ce0d..a37e9e575df8 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, @@ -782,7 +782,7 @@ 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 @@ -1212,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, @@ -1222,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 self.get_usetex(): + if usetex is None: + usetex = rcParams['text.usetex'] + if usetex: if s == ' ': s = r'\ ' return s, 'TeX' From a5786285092e880e6a44bf8bcf142f9e4aed6b75 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 4 Dec 2016 16:32:49 -0500 Subject: [PATCH 20/24] STY: wrap long line --- lib/matplotlib/text.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index a37e9e575df8..2fad1ee19caf 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -782,7 +782,8 @@ def draw(self, renderer): y = y + posy if renderer.flipy(): y = canvash - y - clean_line, ismath = textobj.is_math_text(line, self.get_usetex()) + clean_line, ismath = textobj.is_math_text(line, + self.get_usetex()) if textobj.get_path_effects(): from matplotlib.patheffects import PathEffectRenderer From b079583f32723951c122066fe38ebaa2d8335ed1 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 4 Dec 2016 16:40:05 -0500 Subject: [PATCH 21/24] MNT: remove None from value checks `None` used to be a valid value for the style/slant and variant, however in cc617006f7f0a18396cecf4a9f1e222f1ee5204e the behavior was changed to look up the defaults as set time (not get time) so there is no reason to allow invalid values to be set to the internal state. --- lib/matplotlib/font_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 505f46094828..e9c43530ea89 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -834,7 +834,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 @@ -845,7 +845,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 From 55c266c97924b04cfa1843bac2ea1ce1ae663ed2 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 4 Dec 2016 20:43:46 -0500 Subject: [PATCH 22/24] MNT: set the family to rcParam value in init --- lib/matplotlib/font_manager.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index e9c43530ea89..e642e251c8cf 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -681,7 +681,7 @@ 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._family = _normalize_font_family(rcParams['font.family']) self._slant = rcParams['font.style'] self._variant = rcParams['font.variant'] self._weight = rcParams['font.weight'] @@ -820,10 +820,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] + family = _normalize_font_family(family) self._family = family set_name = set_family @@ -967,6 +964,14 @@ def pickle_load(filename): return data +def _normalize_font_family(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] + return family + + class TempCache(object): """ A class to store temporary caches that are (a) not saved to disk From b6278e3e9402852c2a6d8a8fb053482406ade8ee Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 4 Dec 2016 20:47:17 -0500 Subject: [PATCH 23/24] MNT: use else block to localize exceptions --- lib/matplotlib/font_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index e642e251c8cf..f30c8697dcc2 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -896,11 +896,13 @@ def set_size(self, size): except ValueError: try: scale = font_scalings[size] - size = scale * FontManager.get_default_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): From 5f4eeadbd3a9c9b2d395e33728092ceda87578ca Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 12 Dec 2016 15:16:59 -0500 Subject: [PATCH 24/24] MNT: minor simplification --- lib/matplotlib/font_manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index f30c8697dcc2..dc41207b0a18 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -820,8 +820,7 @@ def set_family(self, family): """ if family is None: family = rcParams['font.family'] - family = _normalize_font_family(family) - self._family = family + self._family = _normalize_font_family(family) set_name = set_family def set_style(self, style): @@ -948,6 +947,7 @@ def ttfdict_to_fnames(d): fnames.append(fname) return fnames + def pickle_dump(data, filename): """ Equivalent to pickle.dump(data, open(filename, 'w')) @@ -956,6 +956,7 @@ def pickle_dump(data, filename): with open(filename, 'wb') as fh: pickle.dump(data, fh) + def pickle_load(filename): """ Equivalent to pickle.load(open(filename, 'r')) @@ -969,7 +970,7 @@ def pickle_load(filename): def _normalize_font_family(family): if is_string_like(family): family = [six.text_type(family)] - elif (not is_string_like(family) and isinstance(family, Iterable)): + elif isinstance(family, Iterable): family = [six.text_type(f) for f in family] return family