For context: I am making lots of matplotlib plots that are controlled by ipywidgets sliders using mpl-interactions. So that all the plot updating machinery is hidden away and the easiest way to update the plot is change the value of the sliders. On occasion I succeed in making a plot that provides insight into my data - in these case I want to save an animation over all the values of the slider.
I’ve tried to accomplish this using FuncAnimation but even with repeat=False the animation always plays in a loop exactly once after anim.save finishes. Is there any way to prevent this?
If I extract the contents of matplotlib.animation.Animation.save and modify them a bit to make a save function then I can get the behavior I want. However, this is a real bummer because:
Makes use of private cbook methods
I can’t return an animation object
I’ll probably mess something up by accident
But fwiw just this save funciton works great (until some of the private methods change ofc):
import matplotlib as mpl
from matplotlib import cbook
from matplotlib.animation import writers
def save(
filename,
fig,
update_func,
frames=None,
writer=None,
interval=20,
fps=None,
codec=None,
bitrate=None,
extra_args=None,
metadata=None,
savefig_kwargs=None,
dpi=None,
):
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 = {}
if fps is None:
fps = 1000.0 / interval
# Re-use the savefig DPI for ours if none is given
if dpi is None:
dpi = mpl.rcParams["savefig.dpi"]
if dpi == "figure":
dpi = 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.
writer = writer_cls(fps, **writer_kwargs)
if "bbox_inches" in savefig_kwargs:
savefig_kwargs.pop("bbox_inches")
with mpl.rc_context({"savefig.bbox": None}), writer.saving(
fig, filename, dpi
), cbook._setattr_cm(fig.canvas, _is_saving=True, manager=None):
frame_number = 0
total_frames = frames
for i in range(frames):
update_func(i)
writer.grab_frame(**savefig_kwargs)
Ok I dug around a bit and I think it is due to this line in the Animationinit:
If I comment out that line then I get the behavior I expect. I suppose that animation is assuming that it is created before the figure has had it’s first draw. Whereas in my case the first draw had already happened, then the animation was re-triggered once it finished saving.
So I can work around this by forcing a draw and then immediately stopping the event loop.
but it would maybe be a nice improvement if Animation checked once if the figure had ever been drawn and only attached the callback to the draw event if no draw had happened? A related thought is that it would be nice to have a more general way to create an animation without starting it.