1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804 |
- # TODO:
- # * Documentation -- this will need a new section of the User's Guide.
- # Both for Animations and just timers.
- # - Also need to update
- # https://scipy-cookbook.readthedocs.io/items/Matplotlib_Animations.html
- # * Blit
- # * Currently broken with Qt4 for widgets that don't start on screen
- # * Still a few edge cases that aren't working correctly
- # * Can this integrate better with existing matplotlib animation artist flag?
- # - If animated removes from default draw(), perhaps we could use this to
- # simplify initial draw.
- # * Example
- # * Frameless animation - pure procedural with no loop
- # * Need example that uses something like inotify or subprocess
- # * Complex syncing examples
- # * Movies
- # * Can blit be enabled for movies?
- # * Need to consider event sources to allow clicking through multiple figures
- import abc
- import base64
- import contextlib
- from io import BytesIO, TextIOWrapper
- import itertools
- import logging
- from pathlib import Path
- import shutil
- import subprocess
- import sys
- from tempfile import TemporaryDirectory
- import uuid
- import warnings
- import numpy as np
- from PIL import Image
- import matplotlib as mpl
- from matplotlib._animation_data import (
- DISPLAY_TEMPLATE, INCLUDED_FRAMES, JS_INCLUDE, STYLE_INCLUDE)
- from matplotlib import _api, cbook
- import matplotlib.colors as mcolors
- _log = logging.getLogger(__name__)
- # Process creation flag for subprocess to prevent it raising a terminal
- # window. See for example https://stackoverflow.com/q/24130623/
- subprocess_creation_flags = (
- subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0)
- # Other potential writing methods:
- # * http://pymedia.org/
- # * libming (produces swf) python wrappers: https://github.com/libming/libming
- # * Wrap x264 API:
- # (https://stackoverflow.com/q/2940671/)
- def adjusted_figsize(w, h, dpi, n):
- """
- Compute figure size so that pixels are a multiple of n.
- Parameters
- ----------
- w, h : float
- Size in inches.
- dpi : float
- The dpi.
- n : int
- The target multiple.
- Returns
- -------
- wnew, hnew : float
- The new figure size in inches.
- """
- # this maybe simplified if / when we adopt consistent rounding for
- # pixel size across the whole library
- def correct_roundoff(x, dpi, n):
- if int(x*dpi) % n != 0:
- if int(np.nextafter(x, np.inf)*dpi) % n == 0:
- x = np.nextafter(x, np.inf)
- elif int(np.nextafter(x, -np.inf)*dpi) % n == 0:
- x = np.nextafter(x, -np.inf)
- return x
- wnew = int(w * dpi / n) * n / dpi
- hnew = int(h * dpi / n) * n / dpi
- return correct_roundoff(wnew, dpi, n), correct_roundoff(hnew, dpi, n)
- class MovieWriterRegistry:
- """Registry of available writer classes by human readable name."""
- def __init__(self):
- self._registered = dict()
- def register(self, name):
- """
- Decorator for registering a class under a name.
- Example use::
- @registry.register(name)
- class Foo:
- pass
- """
- def wrapper(writer_cls):
- self._registered[name] = writer_cls
- return writer_cls
- return wrapper
- def is_available(self, name):
- """
- Check if given writer is available by name.
- Parameters
- ----------
- name : str
- Returns
- -------
- bool
- """
- try:
- cls = self._registered[name]
- except KeyError:
- return False
- return cls.isAvailable()
- def __iter__(self):
- """Iterate over names of available writer class."""
- for name in self._registered:
- if self.is_available(name):
- yield name
- def list(self):
- """Get a list of available MovieWriters."""
- return [*self]
- def __getitem__(self, name):
- """Get an available writer class from its name."""
- if self.is_available(name):
- return self._registered[name]
- raise RuntimeError(f"Requested MovieWriter ({name}) not available")
- writers = MovieWriterRegistry()
- class AbstractMovieWriter(abc.ABC):
- """
- Abstract base class for writing movies, providing a way to grab frames by
- calling `~AbstractMovieWriter.grab_frame`.
- `setup` is called to start the process and `finish` is called afterwards.
- `saving` is provided as a context manager to facilitate this process as ::
- with moviewriter.saving(fig, outfile='myfile.mp4', dpi=100):
- # Iterate over frames
- moviewriter.grab_frame(**savefig_kwargs)
- The use of the context manager ensures that `setup` and `finish` are
- performed as necessary.
- An instance of a concrete subclass of this class can be given as the
- ``writer`` argument of `Animation.save()`.
- """
- def __init__(self, fps=5, metadata=None, codec=None, bitrate=None):
- self.fps = fps
- self.metadata = metadata if metadata is not None else {}
- self.codec = mpl._val_or_rc(codec, 'animation.codec')
- self.bitrate = mpl._val_or_rc(bitrate, 'animation.bitrate')
- @abc.abstractmethod
- def setup(self, fig, outfile, dpi=None):
- """
- Setup for writing the movie file.
- Parameters
- ----------
- fig : `~matplotlib.figure.Figure`
- The figure object that contains the information for frames.
- outfile : str
- The filename of the resulting movie file.
- dpi : float, default: ``fig.dpi``
- The DPI (or resolution) for the file. This controls the size
- in pixels of the resulting movie file.
- """
- # Check that path is valid
- Path(outfile).parent.resolve(strict=True)
- self.outfile = outfile
- self.fig = fig
- if dpi is None:
- dpi = self.fig.dpi
- self.dpi = dpi
- @property
- def frame_size(self):
- """A tuple ``(width, height)`` in pixels of a movie frame."""
- w, h = self.fig.get_size_inches()
- return int(w * self.dpi), int(h * self.dpi)
- @abc.abstractmethod
- 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
- `~.Figure.savefig` call that saves the figure. However, several
- keyword arguments that are supported by `~.Figure.savefig` may not be
- passed as they are controlled by the MovieWriter:
- - *dpi*, *bbox_inches*: These may not be passed because each frame of the
- animation much be exactly the same size in pixels.
- - *format*: This is controlled by the MovieWriter.
- """
- @abc.abstractmethod
- def finish(self):
- """Finish any processing for writing the movie."""
- @contextlib.contextmanager
- def saving(self, fig, outfile, dpi, *args, **kwargs):
- """
- Context manager to facilitate writing the movie file.
- ``*args, **kw`` are any parameters that should be passed to `setup`.
- """
- if mpl.rcParams['savefig.bbox'] == 'tight':
- _log.info("Disabling savefig.bbox = 'tight', as it may cause "
- "frame size to vary, which is inappropriate for "
- "animation.")
- # This particular sequence is what contextlib.contextmanager wants
- self.setup(fig, outfile, dpi, *args, **kwargs)
- with mpl.rc_context({'savefig.bbox': None}):
- try:
- yield self
- finally:
- self.finish()
- class MovieWriter(AbstractMovieWriter):
- """
- Base class for writing movies.
- This is a base class for MovieWriter subclasses that write a movie frame
- data to a pipe. You cannot instantiate this class directly.
- See examples for how to use its subclasses.
- Attributes
- ----------
- frame_format : str
- The format used in writing frame data, defaults to 'rgba'.
- fig : `~matplotlib.figure.Figure`
- The figure to capture data from.
- This must be provided by the subclasses.
- """
- # Builtin writer subclasses additionally define the _exec_key and _args_key
- # attributes, which indicate the rcParams entries where the path to the
- # executable and additional command-line arguments to the executable are
- # stored. Third-party writers cannot meaningfully set these as they cannot
- # extend rcParams with new keys.
- # Pipe-based writers only support RGBA, but file-based ones support more
- # formats.
- supported_formats = ["rgba"]
- def __init__(self, fps=5, codec=None, bitrate=None, extra_args=None,
- metadata=None):
- """
- Parameters
- ----------
- fps : int, default: 5
- Movie frame rate (per second).
- codec : str or None, default: :rc:`animation.codec`
- The codec to use.
- bitrate : int, default: :rc:`animation.bitrate`
- The bitrate of the movie, in kilobits per second. Higher values
- means higher quality movies, but increase the file size. A value
- of -1 lets the underlying movie encoder select the bitrate.
- extra_args : list of str or None, optional
- Extra command-line arguments passed to the underlying movie encoder. These
- arguments are passed last to the encoder, just before the filename. The
- default, None, means to use :rc:`animation.[name-of-encoder]_args` for the
- builtin writers.
- metadata : dict[str, str], default: {}
- 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.
- """
- if type(self) is MovieWriter:
- # TODO MovieWriter is still an abstract class and needs to be
- # extended with a mixin. This should be clearer in naming
- # and description. For now, just give a reasonable error
- # message to users.
- raise TypeError(
- 'MovieWriter cannot be instantiated directly. Please use one '
- 'of its subclasses.')
- super().__init__(fps=fps, metadata=metadata, codec=codec,
- bitrate=bitrate)
- self.frame_format = self.supported_formats[0]
- self.extra_args = extra_args
- def _adjust_frame_size(self):
- if self.codec == 'h264':
- wo, ho = self.fig.get_size_inches()
- w, h = adjusted_figsize(wo, ho, self.dpi, 2)
- if (wo, ho) != (w, h):
- self.fig.set_size_inches(w, h, forward=True)
- _log.info('figure size in inches has been adjusted '
- 'from %s x %s to %s x %s', wo, ho, w, h)
- else:
- w, h = self.fig.get_size_inches()
- _log.debug('frame size in pixels is %s x %s', *self.frame_size)
- return w, h
- def setup(self, fig, outfile, dpi=None):
- # docstring inherited
- super().setup(fig, outfile, dpi=dpi)
- self._w, self._h = self._adjust_frame_size()
- # Run here so that grab_frame() can write the data to a pipe. This
- # eliminates the need for temp files.
- self._run()
- def _run(self):
- # Uses subprocess to call the program for assembling frames into a
- # movie file. *args* returns the sequence of command line arguments
- # from a few configuration options.
- command = self._args()
- _log.info('MovieWriter._run: running command: %s',
- cbook._pformat_subprocess(command))
- PIPE = subprocess.PIPE
- self._proc = subprocess.Popen(
- command, stdin=PIPE, stdout=PIPE, stderr=PIPE,
- creationflags=subprocess_creation_flags)
- def finish(self):
- """Finish any processing for writing the movie."""
- out, err = self._proc.communicate()
- # Use the encoding/errors that universal_newlines would use.
- out = TextIOWrapper(BytesIO(out)).read()
- err = TextIOWrapper(BytesIO(err)).read()
- if out:
- _log.log(
- logging.WARNING if self._proc.returncode else logging.DEBUG,
- "MovieWriter stdout:\n%s", out)
- if err:
- _log.log(
- logging.WARNING if self._proc.returncode else logging.DEBUG,
- "MovieWriter stderr:\n%s", err)
- if self._proc.returncode:
- raise subprocess.CalledProcessError(
- self._proc.returncode, self._proc.args, out, err)
- def grab_frame(self, **savefig_kwargs):
- # docstring inherited
- _validate_grabframe_kwargs(savefig_kwargs)
- _log.debug('MovieWriter.grab_frame: Grabbing frame.')
- # Readjust the figure size in case it has been changed by the user.
- # All frames must have the same size to save the movie correctly.
- self.fig.set_size_inches(self._w, self._h)
- # Save the figure data to the sink, using the frame format and dpi.
- self.fig.savefig(self._proc.stdin, format=self.frame_format,
- dpi=self.dpi, **savefig_kwargs)
- def _args(self):
- """Assemble list of encoder-specific command-line arguments."""
- return NotImplementedError("args needs to be implemented by subclass.")
- @classmethod
- def bin_path(cls):
- """
- Return the binary path to the commandline tool used by a specific
- subclass. This is a class method so that the tool can be looked for
- before making a particular MovieWriter subclass available.
- """
- return str(mpl.rcParams[cls._exec_key])
- @classmethod
- def isAvailable(cls):
- """Return whether a MovieWriter subclass is actually available."""
- return shutil.which(cls.bin_path()) is not None
- class FileMovieWriter(MovieWriter):
- """
- `MovieWriter` for writing to individual files and stitching at the end.
- This must be sub-classed to be useful.
- """
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.frame_format = mpl.rcParams['animation.frame_format']
- def setup(self, fig, outfile, dpi=None, frame_prefix=None):
- """
- 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 : float, default: ``fig.dpi``
- 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. If *None* (the
- default), files are written to a temporary directory which is
- deleted by `finish`; if not *None*, no temporary files are
- deleted.
- """
- # Check that path is valid
- Path(outfile).parent.resolve(strict=True)
- self.fig = fig
- self.outfile = outfile
- if dpi is None:
- dpi = self.fig.dpi
- self.dpi = dpi
- self._adjust_frame_size()
- if frame_prefix is None:
- self._tmpdir = TemporaryDirectory()
- self.temp_prefix = str(Path(self._tmpdir.name, 'tmp'))
- else:
- self._tmpdir = None
- self.temp_prefix = frame_prefix
- self._frame_counter = 0 # used for generating sequential file names
- self._temp_paths = list()
- self.fname_format_str = '%s%%07d.%s'
- def __del__(self):
- if hasattr(self, '_tmpdir') and self._tmpdir:
- self._tmpdir.cleanup()
- @property
- def frame_format(self):
- """
- Format (png, jpeg, etc.) to use for saving the frames, which can be
- decided by the individual subclasses.
- """
- return self._frame_format
- @frame_format.setter
- def frame_format(self, frame_format):
- if frame_format in self.supported_formats:
- self._frame_format = frame_format
- else:
- _api.warn_external(
- f"Ignoring file format {frame_format!r} which is not "
- f"supported by {type(self).__name__}; using "
- f"{self.supported_formats[0]} instead.")
- self._frame_format = self.supported_formats[0]
- def _base_temp_name(self):
- # Generates a template name (without number) given the frame format
- # for extension and the prefix.
- return self.fname_format_str % (self.temp_prefix, self.frame_format)
- def grab_frame(self, **savefig_kwargs):
- # docstring inherited
- # Creates a filename for saving using basename and counter.
- _validate_grabframe_kwargs(savefig_kwargs)
- path = Path(self._base_temp_name() % self._frame_counter)
- self._temp_paths.append(path) # Record the filename for later use.
- self._frame_counter += 1 # Ensures each created name is unique.
- _log.debug('FileMovieWriter.grab_frame: Grabbing frame %d to path=%s',
- self._frame_counter, path)
- with open(path, 'wb') as sink: # Save figure to the sink.
- self.fig.savefig(sink, format=self.frame_format, dpi=self.dpi,
- **savefig_kwargs)
- def finish(self):
- # Call run here now that all frame grabbing is done. All temp files
- # are available to be assembled.
- try:
- self._run()
- super().finish()
- finally:
- if self._tmpdir:
- _log.debug(
- 'MovieWriter: clearing temporary path=%s', self._tmpdir
- )
- self._tmpdir.cleanup()
- @writers.register('pillow')
- class PillowWriter(AbstractMovieWriter):
- @classmethod
- def isAvailable(cls):
- return True
- def setup(self, fig, outfile, dpi=None):
- super().setup(fig, outfile, dpi=dpi)
- self._frames = []
- def grab_frame(self, **savefig_kwargs):
- _validate_grabframe_kwargs(savefig_kwargs)
- buf = BytesIO()
- self.fig.savefig(
- buf, **{**savefig_kwargs, "format": "rgba", "dpi": self.dpi})
- self._frames.append(Image.frombuffer(
- "RGBA", self.frame_size, buf.getbuffer(), "raw", "RGBA", 0, 1))
- def finish(self):
- self._frames[0].save(
- self.outfile, save_all=True, append_images=self._frames[1:],
- duration=int(1000 / self.fps), loop=0)
- # Base class of ffmpeg information. Has the config keys and the common set
- # of arguments that controls the *output* side of things.
- class FFMpegBase:
- """
- Mixin class for FFMpeg output.
- This is a base class for the concrete `FFMpegWriter` and `FFMpegFileWriter`
- classes.
- """
- _exec_key = 'animation.ffmpeg_path'
- _args_key = 'animation.ffmpeg_args'
- @property
- def output_args(self):
- args = []
- if Path(self.outfile).suffix == '.gif':
- self.codec = 'gif'
- else:
- args.extend(['-vcodec', self.codec])
- extra_args = (self.extra_args if self.extra_args is not None
- else mpl.rcParams[self._args_key])
- # For h264, the default format is yuv444p, which is not compatible
- # with quicktime (and others). Specifying yuv420p fixes playback on
- # iOS, as well as HTML5 video in firefox and safari (on both Win and
- # OSX). Also fixes internet explorer. This is as of 2015/10/29.
- if self.codec == 'h264' and '-pix_fmt' not in extra_args:
- args.extend(['-pix_fmt', 'yuv420p'])
- # For GIF, we're telling FFMPEG to split the video stream, to generate
- # a palette, and then use it for encoding.
- elif self.codec == 'gif' and '-filter_complex' not in extra_args:
- args.extend(['-filter_complex',
- 'split [a][b];[a] palettegen [p];[b][p] paletteuse'])
- if self.bitrate > 0:
- args.extend(['-b', '%dk' % self.bitrate]) # %dk: bitrate in kbps.
- for k, v in self.metadata.items():
- args.extend(['-metadata', f'{k}={v}'])
- args.extend(extra_args)
- return args + ['-y', self.outfile]
- # Combine FFMpeg options with pipe-based writing
- @writers.register('ffmpeg')
- class FFMpegWriter(FFMpegBase, MovieWriter):
- """
- Pipe-based ffmpeg writer.
- Frames are streamed directly to ffmpeg via a pipe and written in a single pass.
- This effectively works as a slideshow input to ffmpeg with the fps passed as
- ``-framerate``, so see also `their notes on frame rates`_ for further details.
- .. _their notes on frame rates: https://trac.ffmpeg.org/wiki/Slideshow#Framerates
- """
- def _args(self):
- # Returns the command line parameters for subprocess to use
- # ffmpeg to create a movie using a pipe.
- args = [self.bin_path(), '-f', 'rawvideo', '-vcodec', 'rawvideo',
- '-s', '%dx%d' % self.frame_size, '-pix_fmt', self.frame_format,
- '-framerate', str(self.fps)]
- # Logging is quieted because subprocess.PIPE has limited buffer size.
- # If you have a lot of frames in your animation and set logging to
- # DEBUG, you will have a buffer overrun.
- if _log.getEffectiveLevel() > logging.DEBUG:
- args += ['-loglevel', 'error']
- args += ['-i', 'pipe:'] + self.output_args
- return args
- # Combine FFMpeg options with temp file-based writing
- @writers.register('ffmpeg_file')
- class FFMpegFileWriter(FFMpegBase, FileMovieWriter):
- """
- File-based ffmpeg writer.
- Frames are written to temporary files on disk and then stitched together at the end.
- This effectively works as a slideshow input to ffmpeg with the fps passed as
- ``-framerate``, so see also `their notes on frame rates`_ for further details.
- .. _their notes on frame rates: https://trac.ffmpeg.org/wiki/Slideshow#Framerates
- """
- supported_formats = ['png', 'jpeg', 'tiff', 'raw', 'rgba']
- def _args(self):
- # Returns the command line parameters for subprocess to use
- # ffmpeg to create a movie using a collection of temp images
- args = []
- # For raw frames, we need to explicitly tell ffmpeg the metadata.
- if self.frame_format in {'raw', 'rgba'}:
- args += [
- '-f', 'image2', '-vcodec', 'rawvideo',
- '-video_size', '%dx%d' % self.frame_size,
- '-pixel_format', 'rgba',
- ]
- args += ['-framerate', str(self.fps), '-i', self._base_temp_name()]
- if not self._tmpdir:
- args += ['-frames:v', str(self._frame_counter)]
- # Logging is quieted because subprocess.PIPE has limited buffer size.
- # If you have a lot of frames in your animation and set logging to
- # DEBUG, you will have a buffer overrun.
- if _log.getEffectiveLevel() > logging.DEBUG:
- args += ['-loglevel', 'error']
- return [self.bin_path(), *args, *self.output_args]
- # Base class for animated GIFs with ImageMagick
- class ImageMagickBase:
- """
- Mixin class for ImageMagick output.
- This is a base class for the concrete `ImageMagickWriter` and
- `ImageMagickFileWriter` classes, which define an ``input_names`` attribute
- (or property) specifying the input names passed to ImageMagick.
- """
- _exec_key = 'animation.convert_path'
- _args_key = 'animation.convert_args'
- def _args(self):
- # ImageMagick does not recognize "raw".
- fmt = "rgba" if self.frame_format == "raw" else self.frame_format
- extra_args = (self.extra_args if self.extra_args is not None
- else mpl.rcParams[self._args_key])
- return [
- self.bin_path(),
- "-size", "%ix%i" % self.frame_size,
- "-depth", "8",
- "-delay", str(100 / self.fps),
- "-loop", "0",
- f"{fmt}:{self.input_names}",
- *extra_args,
- self.outfile,
- ]
- @classmethod
- def bin_path(cls):
- binpath = super().bin_path()
- if binpath == 'convert':
- binpath = mpl._get_executable_info('magick').executable
- return binpath
- @classmethod
- def isAvailable(cls):
- try:
- return super().isAvailable()
- except mpl.ExecutableNotFoundError as _enf:
- # May be raised by get_executable_info.
- _log.debug('ImageMagick unavailable due to: %s', _enf)
- return False
- # Combine ImageMagick options with pipe-based writing
- @writers.register('imagemagick')
- class ImageMagickWriter(ImageMagickBase, MovieWriter):
- """
- Pipe-based animated gif writer.
- Frames are streamed directly to ImageMagick via a pipe and written
- in a single pass.
- """
- input_names = "-" # stdin
- # Combine ImageMagick options with temp file-based writing
- @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', 'tiff', 'raw', 'rgba']
- input_names = property(
- lambda self: f'{self.temp_prefix}*.{self.frame_format}')
- # Taken directly from jakevdp's JSAnimation package at
- # http://github.com/jakevdp/JSAnimation
- def _included_frames(frame_count, frame_format, frame_dir):
- return INCLUDED_FRAMES.format(Nframes=frame_count,
- frame_dir=frame_dir,
- frame_format=frame_format)
- def _embedded_frames(frame_list, frame_format):
- """frame_list should be a list of base64-encoded png files"""
- if frame_format == 'svg':
- # Fix MIME type for svg
- frame_format = 'svg+xml'
- template = ' frames[{0}] = "data:image/{1};base64,{2}"\n'
- return "\n" + "".join(
- template.format(i, frame_format, frame_data.replace('\n', '\\\n'))
- for i, frame_data in enumerate(frame_list))
- @writers.register('html')
- class HTMLWriter(FileMovieWriter):
- """Writer for JavaScript-based HTML movies."""
- supported_formats = ['png', 'jpeg', 'tiff', 'svg']
- @classmethod
- def isAvailable(cls):
- return True
- def __init__(self, fps=30, codec=None, bitrate=None, extra_args=None,
- metadata=None, embed_frames=False, default_mode='loop',
- embed_limit=None):
- if extra_args:
- _log.warning("HTMLWriter ignores 'extra_args'")
- extra_args = () # Don't lookup nonexistent rcParam[args_key].
- self.embed_frames = embed_frames
- self.default_mode = default_mode.lower()
- _api.check_in_list(['loop', 'once', 'reflect'],
- default_mode=self.default_mode)
- # Save embed limit, which is given in MB
- self._bytes_limit = mpl._val_or_rc(embed_limit, 'animation.embed_limit')
- # Convert from MB to bytes
- self._bytes_limit *= 1024 * 1024
- super().__init__(fps, codec, bitrate, extra_args, metadata)
- def setup(self, fig, outfile, dpi=None, frame_dir=None):
- outfile = Path(outfile)
- _api.check_in_list(['.html', '.htm'], outfile_extension=outfile.suffix)
- self._saved_frames = []
- self._total_bytes = 0
- self._hit_limit = False
- if not self.embed_frames:
- if frame_dir is None:
- frame_dir = outfile.with_name(outfile.stem + '_frames')
- frame_dir.mkdir(parents=True, exist_ok=True)
- frame_prefix = frame_dir / 'frame'
- else:
- frame_prefix = None
- super().setup(fig, outfile, dpi, frame_prefix)
- self._clear_temp = False
- def grab_frame(self, **savefig_kwargs):
- _validate_grabframe_kwargs(savefig_kwargs)
- if self.embed_frames:
- # Just stop processing if we hit the limit
- if self._hit_limit:
- return
- f = BytesIO()
- self.fig.savefig(f, format=self.frame_format,
- dpi=self.dpi, **savefig_kwargs)
- imgdata64 = base64.encodebytes(f.getvalue()).decode('ascii')
- self._total_bytes += len(imgdata64)
- if self._total_bytes >= self._bytes_limit:
- _log.warning(
- "Animation size has reached %s bytes, exceeding the limit "
- "of %s. If you're sure you want a larger animation "
- "embedded, set the animation.embed_limit rc parameter to "
- "a larger value (in MB). This and further frames will be "
- "dropped.", self._total_bytes, self._bytes_limit)
- self._hit_limit = True
- else:
- self._saved_frames.append(imgdata64)
- else:
- return super().grab_frame(**savefig_kwargs)
- def finish(self):
- # save the frames to an html file
- if self.embed_frames:
- fill_frames = _embedded_frames(self._saved_frames,
- self.frame_format)
- frame_count = len(self._saved_frames)
- else:
- # temp names is filled by FileMovieWriter
- frame_count = len(self._temp_paths)
- fill_frames = _included_frames(
- frame_count, self.frame_format,
- self._temp_paths[0].parent.relative_to(self.outfile.parent))
- mode_dict = dict(once_checked='',
- loop_checked='',
- reflect_checked='')
- mode_dict[self.default_mode + '_checked'] = 'checked'
- interval = 1000 // self.fps
- with open(self.outfile, 'w') as of:
- of.write(JS_INCLUDE + STYLE_INCLUDE)
- of.write(DISPLAY_TEMPLATE.format(id=uuid.uuid4().hex,
- Nframes=frame_count,
- fill_frames=fill_frames,
- interval=interval,
- **mode_dict))
- # Duplicate the temporary file clean up logic from
- # FileMovieWriter.finish. We cannot call the inherited version of
- # finish because it assumes that there is a subprocess that we either
- # need to call to merge many frames together or that there is a
- # subprocess call that we need to clean up.
- if self._tmpdir:
- _log.debug('MovieWriter: clearing temporary path=%s', self._tmpdir)
- self._tmpdir.cleanup()
- class Animation:
- """
- A base class for Animations.
- This class is not usable as is, and should be subclassed to provide needed
- behavior.
- .. note::
- You must store the created Animation in a variable that lives as long
- as the animation should run. Otherwise, the Animation object will be
- garbage-collected and the animation stops.
- Parameters
- ----------
- fig : `~matplotlib.figure.Figure`
- The figure object used to get needed events, such as draw or resize.
- 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 `TimedAnimation`) and file
- system notifications.
- blit : bool, default: False
- Whether blitting is used to optimize drawing. If the backend does not
- support blitting, then this parameter has no effect.
- See Also
- --------
- FuncAnimation, ArtistAnimation
- """
- def __init__(self, fig, event_source=None, blit=False):
- self._draw_was_started = False
- self._fig = fig
- # Disables blitting for backends that don't support it. This
- # allows users to request it if available, but still have a
- # fallback that works if it is not.
- self._blit = blit and fig.canvas.supports_blit
- # These are the basics of the animation. The frame sequence represents
- # information for each frame of the animation and depends on how the
- # drawing is handled by the subclasses. The event source fires events
- # that cause the frame sequence to be iterated.
- self.frame_seq = self.new_frame_seq()
- self.event_source = event_source
- # Instead of starting the event source now, we connect to the figure's
- # draw_event, so that we only start once the figure has been drawn.
- self._first_draw_id = fig.canvas.mpl_connect('draw_event', self._start)
- # Connect to the figure's close_event so that we don't continue to
- # fire events and try to draw to a deleted figure.
- self._close_id = self._fig.canvas.mpl_connect('close_event',
- self._stop)
- if self._blit:
- self._setup_blit()
- def __del__(self):
- if not getattr(self, '_draw_was_started', True):
- warnings.warn(
- 'Animation was deleted without rendering anything. This is '
- 'most likely not intended. To prevent deletion, assign the '
- 'Animation to a variable, e.g. `anim`, that exists until you '
- 'output the Animation using `plt.show()` or '
- '`anim.save()`.'
- )
- def _start(self, *args):
- """
- Starts interactive animation. Adds the draw frame command to the GUI
- handler, calls show to start the event loop.
- """
- # Do not start the event source if saving() it.
- if self._fig.canvas.is_saving():
- return
- # First disconnect our draw event handler
- self._fig.canvas.mpl_disconnect(self._first_draw_id)
- # Now do any initial draw
- self._init_draw()
- # Add our callback for stepping the animation and
- # actually start the event_source.
- self.event_source.add_callback(self._step)
- self.event_source.start()
- def _stop(self, *args):
- # On stop we disconnect all of our events.
- if self._blit:
- self._fig.canvas.mpl_disconnect(self._resize_id)
- self._fig.canvas.mpl_disconnect(self._close_id)
- self.event_source.remove_callback(self._step)
- self.event_source = None
- 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, *, progress_callback=None):
- """
- Save the animation as a movie file by drawing every frame.
- Parameters
- ----------
- filename : str
- The output filename, e.g., :file:`mymovie.mp4`.
- writer : `MovieWriter` or str, default: :rc:`animation.writer`
- A `MovieWriter` instance to use or a key that identifies a
- class to use, such as 'ffmpeg'.
- fps : int, optional
- Movie frame rate (per second). If not set, the frame rate from the
- animation's frame interval.
- dpi : float, default: :rc:`savefig.dpi`
- Controls the dots per inch for the movie frames. Together with
- the figure's size in inches, this controls the size of the movie.
- codec : str, default: :rc:`animation.codec`.
- The video codec to use. Not all codecs are supported by a given
- `MovieWriter`.
- bitrate : int, default: :rc:`animation.bitrate`
- The bitrate of the movie, in kilobits per second. Higher values
- means higher quality movies, but increase the file size. A value
- of -1 lets the underlying movie encoder select the bitrate.
- extra_args : list of str or None, optional
- Extra command-line arguments passed to the underlying movie encoder. These
- arguments are passed last to the encoder, just before the output filename.
- The default, None, means to use :rc:`animation.[name-of-encoder]_args` for
- the builtin writers.
- metadata : dict[str, str], default: {}
- 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, default: []
- Additional `Animation` objects that should be included
- in the saved movie file. These need to be from the same
- `.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, default: {}
- Keyword arguments passed to each `~.Figure.savefig` call used to
- save the individual frames.
- progress_callback : function, optional
- A callback function that will be called for every frame to notify
- the saving progress. It must have the signature ::
- def func(current_frame: int, total_frames: int) -> Any
- where *current_frame* is the current frame number and *total_frames* is the
- total number of frames to be saved. *total_frames* is set to None, if the
- total number of frames cannot be determined. Return values may exist but are
- ignored.
- Example code to write the progress to stdout::
- progress_callback = lambda i, n: print(f'Saving frame {i}/{n}')
- Notes
- -----
- *fps*, *codec*, *bitrate*, *extra_args* and *metadata* are used to
- construct a `.MovieWriter` instance and can only be passed if
- *writer* is a string. If they are passed as non-*None* and *writer*
- is a `.MovieWriter`, a `RuntimeError` will be raised.
- """
- all_anim = [self]
- if extra_anim is not None:
- all_anim.extend(anim for anim in extra_anim
- if anim._fig is self._fig)
- # Disable "Animation was deleted without rendering" warning.
- for anim in all_anim:
- anim._draw_was_started = True
- if writer is None:
- writer = mpl.rcParams['animation.writer']
- elif (not isinstance(writer, str) and
- any(arg is not None
- for arg in (fps, codec, bitrate, extra_args, metadata))):
- raise RuntimeError('Passing in values for arguments '
- 'fps, codec, bitrate, extra_args, or metadata '
- 'is not supported when writer is an existing '
- 'MovieWriter instance. These should instead be '
- 'passed as arguments when creating the '
- 'MovieWriter instance.')
- if savefig_kwargs is None:
- savefig_kwargs = {}
- else:
- # we are going to mutate this below
- savefig_kwargs = dict(savefig_kwargs)
- if fps is None and hasattr(self, '_interval'):
- # Convert interval in ms to frames per second
- fps = 1000. / self._interval
- # Re-use the savefig DPI for ours if none is given
- dpi = mpl._val_or_rc(dpi, 'savefig.dpi')
- if dpi == 'figure':
- dpi = self._fig.dpi
- writer_kwargs = {}
- if codec is not None:
- writer_kwargs['codec'] = codec
- if bitrate is not None:
- writer_kwargs['bitrate'] = bitrate
- if extra_args is not None:
- writer_kwargs['extra_args'] = extra_args
- if metadata is not None:
- writer_kwargs['metadata'] = metadata
- # If we have the name of a writer, instantiate an instance of the
- # registered class.
- if isinstance(writer, str):
- try:
- writer_cls = writers[writer]
- except RuntimeError: # Raised if not available.
- writer_cls = PillowWriter # Always available.
- _log.warning("MovieWriter %s unavailable; using Pillow "
- "instead.", writer)
- writer = writer_cls(fps, **writer_kwargs)
- _log.info('Animation.save using %s', type(writer))
- if 'bbox_inches' in savefig_kwargs:
- _log.warning("Warning: discarding the 'bbox_inches' argument in "
- "'savefig_kwargs' as it may cause frame size "
- "to vary, which is inappropriate for animation.")
- savefig_kwargs.pop('bbox_inches')
- # Create a new sequence of frames for saved data. This is different
- # from new_frame_seq() to give the ability to save 'live' generated
- # frame information to be saved later.
- # TODO: Right now, after closing the figure, saving a movie won't work
- # since GUI widgets are gone. Either need to remove extra code to
- # allow for this non-existent use case or find a way to make it work.
- facecolor = savefig_kwargs.get('facecolor',
- mpl.rcParams['savefig.facecolor'])
- if facecolor == 'auto':
- facecolor = self._fig.get_facecolor()
- def _pre_composite_to_white(color):
- r, g, b, a = mcolors.to_rgba(color)
- return a * np.array([r, g, b]) + 1 - a
- savefig_kwargs['facecolor'] = _pre_composite_to_white(facecolor)
- savefig_kwargs['transparent'] = False # just to be safe!
- # canvas._is_saving = True makes the draw_event animation-starting
- # callback a no-op; canvas.manager = None prevents resizing the GUI
- # widget (both are likewise done in savefig()).
- with writer.saving(self._fig, filename, dpi), \
- cbook._setattr_cm(self._fig.canvas, _is_saving=True, manager=None):
- for anim in all_anim:
- anim._init_draw() # Clear the initial frame
- frame_number = 0
- # TODO: Currently only FuncAnimation has a save_count
- # attribute. Can we generalize this to all Animations?
- save_count_list = [getattr(a, '_save_count', None)
- for a in all_anim]
- if None in save_count_list:
- total_frames = None
- else:
- total_frames = sum(save_count_list)
- for data in zip(*[a.new_saved_frame_seq() for a in all_anim]):
- for anim, d in zip(all_anim, data):
- # TODO: See if turning off blit is really necessary
- anim._draw_next_frame(d, blit=False)
- if progress_callback is not None:
- progress_callback(frame_number, total_frames)
- frame_number += 1
- writer.grab_frame(**savefig_kwargs)
- def _step(self, *args):
- """
- Handler for getting events. By default, gets the next frame in the
- sequence and hands the data off to be drawn.
- """
- # Returns True to indicate that the event source should continue to
- # call _step, until the frame sequence reaches the end of iteration,
- # at which point False will be returned.
- try:
- framedata = next(self.frame_seq)
- self._draw_next_frame(framedata, self._blit)
- return True
- except StopIteration:
- return False
- def new_frame_seq(self):
- """Return a new sequence of frame information."""
- # Default implementation is just an iterator over self._framedata
- return iter(self._framedata)
- def new_saved_frame_seq(self):
- """Return a new sequence of saved/cached frame information."""
- # Default is the same as the regular frame sequence
- return self.new_frame_seq()
- def _draw_next_frame(self, framedata, blit):
- # Breaks down the drawing of the next frame into steps of pre- and
- # post- draw, as well as the drawing of the frame itself.
- self._pre_draw(framedata, blit)
- self._draw_frame(framedata)
- self._post_draw(framedata, blit)
- def _init_draw(self):
- # Initial draw to clear the frame. Also used by the blitting code
- # when a clean base is required.
- self._draw_was_started = True
- def _pre_draw(self, framedata, blit):
- # Perform any cleaning or whatnot before the drawing of the frame.
- # This default implementation allows blit to clear the frame.
- if blit:
- self._blit_clear(self._drawn_artists)
- def _draw_frame(self, framedata):
- # Performs actual drawing of the frame.
- raise NotImplementedError('Needs to be implemented by subclasses to'
- ' actually make an animation.')
- def _post_draw(self, framedata, blit):
- # After the frame is rendered, this handles the actual flushing of
- # the draw, which can be a direct draw_idle() or make use of the
- # blitting.
- if blit and self._drawn_artists:
- self._blit_draw(self._drawn_artists)
- else:
- self._fig.canvas.draw_idle()
- # The rest of the code in this class is to facilitate easy blitting
- def _blit_draw(self, artists):
- # Handles blitted drawing, which renders only the artists given instead
- # of the entire figure.
- updated_ax = {a.axes for a in artists}
- # Enumerate artists to cache Axes backgrounds. We do not draw
- # artists yet to not cache foreground from plots with shared axes
- for ax in updated_ax:
- # If we haven't cached the background for the current view of this
- # Axes object, do so now. This might not always be reliable, but
- # it's an attempt to automate the process.
- cur_view = ax._get_view()
- view, bg = self._blit_cache.get(ax, (object(), None))
- if cur_view != view:
- self._blit_cache[ax] = (
- cur_view, ax.figure.canvas.copy_from_bbox(ax.bbox))
- # Make a separate pass to draw foreground.
- for a in artists:
- a.axes.draw_artist(a)
- # After rendering all the needed artists, blit each Axes individually.
- for ax in updated_ax:
- ax.figure.canvas.blit(ax.bbox)
- def _blit_clear(self, artists):
- # Get a list of the Axes that need clearing from the artists that
- # have been drawn. Grab the appropriate saved background from the
- # cache and restore.
- axes = {a.axes for a in artists}
- for ax in axes:
- try:
- view, bg = self._blit_cache[ax]
- except KeyError:
- continue
- if ax._get_view() == view:
- ax.figure.canvas.restore_region(bg)
- else:
- self._blit_cache.pop(ax)
- def _setup_blit(self):
- # Setting up the blit requires: a cache of the background for the Axes
- self._blit_cache = dict()
- self._drawn_artists = []
- # _post_draw needs to be called first to initialize the renderer
- self._post_draw(None, self._blit)
- # Then we need to clear the Frame for the initial draw
- # This is typically handled in _on_resize because QT and Tk
- # emit a resize event on launch, but the macosx backend does not,
- # thus we force it here for everyone for consistency
- self._init_draw()
- # Connect to future resize events
- self._resize_id = self._fig.canvas.mpl_connect('resize_event',
- self._on_resize)
- def _on_resize(self, event):
- # On resize, we need to disable the resize event handling so we don't
- # get too many events. Also stop the animation events, so that
- # we're paused. Reset the cache and re-init. Set up an event handler
- # to catch once the draw has actually taken place.
- self._fig.canvas.mpl_disconnect(self._resize_id)
- self.event_source.stop()
- self._blit_cache.clear()
- self._init_draw()
- self._resize_id = self._fig.canvas.mpl_connect('draw_event',
- self._end_redraw)
- def _end_redraw(self, event):
- # Now that the redraw has happened, do the post draw flushing and
- # blit handling. Then re-enable all of the original events.
- self._post_draw(None, False)
- self.event_source.start()
- self._fig.canvas.mpl_disconnect(self._resize_id)
- self._resize_id = self._fig.canvas.mpl_connect('resize_event',
- self._on_resize)
- def to_html5_video(self, embed_limit=None):
- """
- Convert the animation to an HTML5 ``<video>`` tag.
- This saves the animation as an h264 video, encoded in base64
- directly into the HTML5 video tag. This respects :rc:`animation.writer`
- and :rc:`animation.bitrate`. This also makes use of the
- *interval* to control the speed, and uses the *repeat*
- parameter to decide whether to loop.
- Parameters
- ----------
- embed_limit : float, optional
- Limit, in MB, of the returned animation. No animation is created
- if the limit is exceeded.
- Defaults to :rc:`animation.embed_limit` = 20.0.
- Returns
- -------
- str
- An HTML5 video tag with the animation embedded as base64 encoded
- h264 video.
- If the *embed_limit* is exceeded, this returns the string
- "Video too large to embed."
- """
- VIDEO_TAG = r'''<video {size} {options}>
- <source type="video/mp4" src="data:video/mp4;base64,{video}">
- Your browser does not support the video tag.
- </video>'''
- # Cache the rendering of the video as HTML
- if not hasattr(self, '_base64_video'):
- # Save embed limit, which is given in MB
- embed_limit = mpl._val_or_rc(embed_limit, 'animation.embed_limit')
- # Convert from MB to bytes
- embed_limit *= 1024 * 1024
- # Can't open a NamedTemporaryFile twice on Windows, so use a
- # TemporaryDirectory instead.
- with TemporaryDirectory() as tmpdir:
- path = Path(tmpdir, "temp.m4v")
- # We create a writer manually so that we can get the
- # appropriate size for the tag
- Writer = writers[mpl.rcParams['animation.writer']]
- writer = Writer(codec='h264',
- bitrate=mpl.rcParams['animation.bitrate'],
- fps=1000. / self._interval)
- self.save(str(path), writer=writer)
- # Now open and base64 encode.
- vid64 = base64.encodebytes(path.read_bytes())
- vid_len = len(vid64)
- if vid_len >= embed_limit:
- _log.warning(
- "Animation movie is %s bytes, exceeding the limit of %s. "
- "If you're sure you want a large animation embedded, set "
- "the animation.embed_limit rc parameter to a larger value "
- "(in MB).", vid_len, embed_limit)
- else:
- self._base64_video = vid64.decode('ascii')
- self._video_size = 'width="{}" height="{}"'.format(
- *writer.frame_size)
- # If we exceeded the size, this attribute won't exist
- if hasattr(self, '_base64_video'):
- # Default HTML5 options are to autoplay and display video controls
- options = ['controls', 'autoplay']
- # If we're set to repeat, make it loop
- if getattr(self, '_repeat', False):
- options.append('loop')
- return VIDEO_TAG.format(video=self._base64_video,
- size=self._video_size,
- options=' '.join(options))
- else:
- return 'Video too large to embed.'
- def to_jshtml(self, fps=None, embed_frames=True, default_mode=None):
- """
- Generate HTML representation of the animation.
- Parameters
- ----------
- fps : int, optional
- Movie frame rate (per second). If not set, the frame rate from
- the animation's frame interval.
- embed_frames : bool, optional
- default_mode : str, optional
- What to do when the animation ends. Must be one of ``{'loop',
- 'once', 'reflect'}``. Defaults to ``'loop'`` if the *repeat*
- parameter is True, otherwise ``'once'``.
- """
- if fps is None and hasattr(self, '_interval'):
- # Convert interval in ms to frames per second
- fps = 1000 / self._interval
- # If we're not given a default mode, choose one base on the value of
- # the _repeat attribute
- if default_mode is None:
- default_mode = 'loop' if getattr(self, '_repeat',
- False) else 'once'
- if not hasattr(self, "_html_representation"):
- # Can't open a NamedTemporaryFile twice on Windows, so use a
- # TemporaryDirectory instead.
- with TemporaryDirectory() as tmpdir:
- path = Path(tmpdir, "temp.html")
- writer = HTMLWriter(fps=fps,
- embed_frames=embed_frames,
- default_mode=default_mode)
- self.save(str(path), writer=writer)
- self._html_representation = path.read_text()
- return self._html_representation
- def _repr_html_(self):
- """IPython display hook for rendering."""
- fmt = mpl.rcParams['animation.html']
- if fmt == 'html5':
- return self.to_html5_video()
- elif fmt == 'jshtml':
- return self.to_jshtml()
- def pause(self):
- """Pause the animation."""
- self.event_source.stop()
- if self._blit:
- for artist in self._drawn_artists:
- artist.set_animated(False)
- def resume(self):
- """Resume the animation."""
- self.event_source.start()
- if self._blit:
- for artist in self._drawn_artists:
- artist.set_animated(True)
- class TimedAnimation(Animation):
- """
- `Animation` subclass for time-based animation.
- A new frame is drawn every *interval* milliseconds.
- .. note::
- You must store the created Animation in a variable that lives as long
- as the animation should run. Otherwise, the Animation object will be
- garbage-collected and the animation stops.
- Parameters
- ----------
- fig : `~matplotlib.figure.Figure`
- The figure object used to get needed events, such as draw or resize.
- interval : int, default: 200
- Delay between frames in milliseconds.
- repeat_delay : int, default: 0
- The delay in milliseconds between consecutive animation runs, if
- *repeat* is True.
- repeat : bool, default: True
- Whether the animation repeats when the sequence of frames is completed.
- blit : bool, default: False
- Whether blitting is used to optimize drawing.
- """
- def __init__(self, fig, interval=200, repeat_delay=0, repeat=True,
- event_source=None, *args, **kwargs):
- self._interval = interval
- # Undocumented support for repeat_delay = None as backcompat.
- self._repeat_delay = repeat_delay if repeat_delay is not None else 0
- self._repeat = repeat
- # If we're not given an event source, create a new timer. This permits
- # sharing timers between animation objects for syncing animations.
- if event_source is None:
- event_source = fig.canvas.new_timer(interval=self._interval)
- super().__init__(fig, event_source=event_source, *args, **kwargs)
- def _step(self, *args):
- """Handler for getting events."""
- # Extends the _step() method for the Animation class. If
- # Animation._step signals that it reached the end and we want to
- # repeat, we refresh the frame sequence and return True. If
- # _repeat_delay is set, change the event_source's interval to our loop
- # delay and set the callback to one which will then set the interval
- # back.
- still_going = super()._step(*args)
- if not still_going:
- if self._repeat:
- # Restart the draw loop
- self._init_draw()
- self.frame_seq = self.new_frame_seq()
- self.event_source.interval = self._repeat_delay
- return True
- else:
- # We are done with the animation. Call pause to remove
- # animated flags from artists that were using blitting
- self.pause()
- if self._blit:
- # Remove the resize callback if we were blitting
- self._fig.canvas.mpl_disconnect(self._resize_id)
- self._fig.canvas.mpl_disconnect(self._close_id)
- self.event_source = None
- return False
- self.event_source.interval = self._interval
- return True
- repeat = _api.deprecate_privatize_attribute("3.7")
- class ArtistAnimation(TimedAnimation):
- """
- `TimedAnimation` subclass that creates an animation by using a fixed
- set of `.Artist` objects.
- Before creating an instance, all plotting should have taken place
- and the relevant artists saved.
- .. note::
- You must store the created Animation in a variable that lives as long
- as the animation should run. Otherwise, the Animation object will be
- garbage-collected and the animation stops.
- Parameters
- ----------
- fig : `~matplotlib.figure.Figure`
- The figure object used to get needed events, such as draw or resize.
- artists : list
- Each list entry is a collection of `.Artist` objects that are made
- visible on the corresponding frame. Other artists are made invisible.
- interval : int, default: 200
- Delay between frames in milliseconds.
- repeat_delay : int, default: 0
- The delay in milliseconds between consecutive animation runs, if
- *repeat* is True.
- repeat : bool, default: True
- Whether the animation repeats when the sequence of frames is completed.
- blit : bool, default: False
- Whether blitting is used to optimize drawing.
- """
- def __init__(self, fig, artists, *args, **kwargs):
- # Internal list of artists drawn in the most recent frame.
- self._drawn_artists = []
- # Use the list of artists as the framedata, which will be iterated
- # over by the machinery.
- self._framedata = artists
- super().__init__(fig, *args, **kwargs)
- def _init_draw(self):
- super()._init_draw()
- # Make all the artists involved in *any* frame invisible
- figs = set()
- for f in self.new_frame_seq():
- for artist in f:
- artist.set_visible(False)
- artist.set_animated(self._blit)
- # Assemble a list of unique figures that need flushing
- if artist.get_figure() not in figs:
- figs.add(artist.get_figure())
- # Flush the needed figures
- for fig in figs:
- fig.canvas.draw_idle()
- def _pre_draw(self, framedata, blit):
- """Clears artists from the last frame."""
- if blit:
- # Let blit handle clearing
- self._blit_clear(self._drawn_artists)
- else:
- # Otherwise, make all the artists from the previous frame invisible
- for artist in self._drawn_artists:
- artist.set_visible(False)
- def _draw_frame(self, artists):
- # Save the artists that were passed in as framedata for the other
- # steps (esp. blitting) to use.
- self._drawn_artists = artists
- # Make all the artists from the current frame visible
- for artist in artists:
- artist.set_visible(True)
- class FuncAnimation(TimedAnimation):
- """
- `TimedAnimation` subclass that makes an animation by repeatedly calling
- a function *func*.
- .. note::
- You must store the created Animation in a variable that lives as long
- as the animation should run. Otherwise, the Animation object will be
- garbage-collected and the animation stops.
- Parameters
- ----------
- fig : `~matplotlib.figure.Figure`
- The figure object used to get needed events, such as draw or resize.
- 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 using `functools.partial` or via the *fargs*
- parameter.
- The required signature is::
- def func(frame, *fargs) -> iterable_of_artists
- It is often more convenient to provide the arguments using
- `functools.partial`. In this way it is also possible to pass keyword
- arguments. To pass a function with both positional and keyword
- arguments, set all arguments as keyword arguments, just leaving the
- *frame* argument unset::
- def func(frame, art, *, y=None):
- ...
- ani = FuncAnimation(fig, partial(func, art=ln, y='foo'))
- If ``blit == True``, *func* must return an iterable of all artists
- that were modified or created. This information is used by the blitting
- algorithm to determine which parts of the figure have to be updated.
- The return value is unused if ``blit == False`` and may be omitted in
- that case.
- 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, then equivalent to passing ``range(frames)``
- - If a generator function, then must have the signature::
- def gen_function() -> obj
- - If *None*, then equivalent to passing ``itertools.count``.
- 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.
- 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.
- The required signature is::
- def init_func() -> iterable_of_artists
- If ``blit == True``, *init_func* must return an iterable of artists
- to be re-drawn. This information is used by the blitting algorithm to
- determine which parts of the figure have to be updated. The return
- value is unused if ``blit == False`` and may be omitted in that case.
- fargs : tuple or None, optional
- Additional arguments to pass to each call to *func*. Note: the use of
- `functools.partial` is preferred over *fargs*. See *func* for details.
- save_count : int, optional
- Fallback for the number of values from *frames* to cache. This is
- only used if the number of frames cannot be inferred from *frames*,
- i.e. when it's an iterator without length or a generator.
- interval : int, default: 200
- Delay between frames in milliseconds.
- repeat_delay : int, default: 0
- The delay in milliseconds between consecutive animation runs, if
- *repeat* is True.
- repeat : bool, default: True
- Whether the animation repeats when the sequence of frames is completed.
- blit : bool, default: False
- Whether blitting is used to optimize drawing. Note: when using
- blitting, any animated artists will be drawn according to their zorder;
- however, they will be drawn on top of any previous artists, regardless
- of their zorder.
- cache_frame_data : bool, default: True
- Whether frame data is cached. Disabling cache might be helpful when
- frames contain large objects.
- """
- def __init__(self, fig, func, frames=None, init_func=None, fargs=None,
- save_count=None, *, cache_frame_data=True, **kwargs):
- if fargs:
- self._args = fargs
- else:
- self._args = ()
- self._func = func
- self._init_func = init_func
- # Amount of framedata to keep around for saving movies. This is only
- # used if we don't know how many frames there will be: in the case
- # of no generator or in the case of a callable.
- self._save_count = save_count
- # Set up a function that creates a new iterable when needed. If nothing
- # is passed in for frames, just use itertools.count, which will just
- # keep counting from 0. A callable passed in for frames is assumed to
- # be a generator. An iterable will be used as is, and anything else
- # will be treated as a number of frames.
- if frames is None:
- self._iter_gen = itertools.count
- elif callable(frames):
- self._iter_gen = frames
- elif np.iterable(frames):
- if kwargs.get('repeat', True):
- self._tee_from = frames
- def iter_frames(frames=frames):
- this, self._tee_from = itertools.tee(self._tee_from, 2)
- yield from this
- self._iter_gen = iter_frames
- else:
- self._iter_gen = lambda: iter(frames)
- if hasattr(frames, '__len__'):
- self._save_count = len(frames)
- if save_count is not None:
- _api.warn_external(
- f"You passed in an explicit {save_count=} "
- "which is being ignored in favor of "
- f"{len(frames)=}."
- )
- else:
- self._iter_gen = lambda: iter(range(frames))
- self._save_count = frames
- if save_count is not None:
- _api.warn_external(
- f"You passed in an explicit {save_count=} which is being "
- f"ignored in favor of {frames=}."
- )
- if self._save_count is None and cache_frame_data:
- _api.warn_external(
- f"{frames=!r} which we can infer the length of, "
- "did not pass an explicit *save_count* "
- f"and passed {cache_frame_data=}. To avoid a possibly "
- "unbounded cache, frame data caching has been disabled. "
- "To suppress this warning either pass "
- "`cache_frame_data=False` or `save_count=MAX_FRAMES`."
- )
- cache_frame_data = False
- self._cache_frame_data = cache_frame_data
- # Needs to be initialized so the draw functions work without checking
- self._save_seq = []
- super().__init__(fig, **kwargs)
- # Need to reset the saved seq, since right now it will contain data
- # for a single frame from init, which is not what we want.
- self._save_seq = []
- def new_frame_seq(self):
- # Use the generating function to generate a new frame sequence
- return self._iter_gen()
- def new_saved_frame_seq(self):
- # Generate an iterator for the sequence of saved data. If there are
- # no saved frames, generate a new frame sequence and take the first
- # save_count entries in it.
- if self._save_seq:
- # While iterating we are going to update _save_seq
- # so make a copy to safely iterate over
- self._old_saved_seq = list(self._save_seq)
- return iter(self._old_saved_seq)
- else:
- if self._save_count is None:
- frame_seq = self.new_frame_seq()
- def gen():
- try:
- while True:
- yield next(frame_seq)
- except StopIteration:
- pass
- return gen()
- else:
- return itertools.islice(self.new_frame_seq(), self._save_count)
- def _init_draw(self):
- super()._init_draw()
- # Initialize the drawing either using the given init_func or by
- # calling the draw function with the first item of the frame sequence.
- # For blitting, the init_func should return a sequence of modified
- # artists.
- if self._init_func is None:
- try:
- frame_data = next(self.new_frame_seq())
- except StopIteration:
- # we can't start the iteration, it may have already been
- # exhausted by a previous save or just be 0 length.
- # warn and bail.
- warnings.warn(
- "Can not start iterating the frames for the initial draw. "
- "This can be caused by passing in a 0 length sequence "
- "for *frames*.\n\n"
- "If you passed *frames* as a generator "
- "it may be exhausted due to a previous display or save."
- )
- return
- self._draw_frame(frame_data)
- else:
- self._drawn_artists = self._init_func()
- if self._blit:
- if self._drawn_artists is None:
- raise RuntimeError('The init_func must return a '
- 'sequence of Artist objects.')
- for a in self._drawn_artists:
- a.set_animated(self._blit)
- self._save_seq = []
- def _draw_frame(self, framedata):
- if self._cache_frame_data:
- # Save the data for potential saving of movies.
- self._save_seq.append(framedata)
- self._save_seq = self._save_seq[-self._save_count:]
- # Call the func with framedata and args. If blitting is desired,
- # func needs to return a sequence of any artists that were modified.
- self._drawn_artists = self._func(framedata, *self._args)
- if self._blit:
- err = RuntimeError('The animation function must return a sequence '
- 'of Artist objects.')
- try:
- # check if a sequence
- iter(self._drawn_artists)
- except TypeError:
- raise err from None
- # check each item if it's artist
- for i in self._drawn_artists:
- if not isinstance(i, mpl.artist.Artist):
- raise err
- self._drawn_artists = sorted(self._drawn_artists,
- key=lambda x: x.get_zorder())
- for a in self._drawn_artists:
- a.set_animated(self._blit)
- save_count = _api.deprecate_privatize_attribute("3.7")
- def _validate_grabframe_kwargs(savefig_kwargs):
- if mpl.rcParams['savefig.bbox'] == 'tight':
- raise ValueError(
- f"{mpl.rcParams['savefig.bbox']=} must not be 'tight' as it "
- "may cause frame size to vary, which is inappropriate for animation."
- )
- for k in ('dpi', 'bbox_inches', 'format'):
- if k in savefig_kwargs:
- raise TypeError(
- f"grab_frame got an unexpected keyword argument {k!r}"
- )
|