Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions ultraplot/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,12 @@ def _canvas_preprocess(self, *args, **kwargs):

skip_autolayout = getattr(fig, "_skip_autolayout", False)
layout_dirty = getattr(fig, "_layout_dirty", False)
saving_frame_count = getattr(fig, "_saving_frame_count", 0)
lock_tight_during_save = (
getattr(self, "_is_saving", False)
and saving_frame_count > 0
and getattr(fig, "_tight_active", False)
)
if (
skip_autolayout
and getattr(fig, "_layout_initialized", False)
Expand All @@ -514,14 +520,20 @@ def _canvas_preprocess(self, *args, **kwargs):
with ctx1, ctx2, ctx3:
needs_post_layout = False
if not fig._layout_initialized or layout_dirty:
fig.auto_layout()
fig.auto_layout(tight=False if lock_tight_during_save else None)
fig._layout_initialized = True
fig._layout_dirty = False
needs_post_layout = _needs_post_tight_layout(fig)
needs_post_layout = (
not lock_tight_during_save and _needs_post_tight_layout(fig)
)
result = func(self, *args, **kwargs)
if needs_post_layout:
fig.auto_layout()
result = func(self, *args, **kwargs)
if method == "print_figure" and getattr(self, "_is_saving", False):
fig._saving_frame_count = saving_frame_count + 1
elif not getattr(self, "_is_saving", False):
fig._saving_frame_count = 0
return result

# Add preprocessor
Expand Down
49 changes: 49 additions & 0 deletions ultraplot/tests/test_animation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from unittest.mock import MagicMock

import matplotlib
import numpy as np
import pytest
from matplotlib.animation import FuncAnimation
Expand Down Expand Up @@ -58,3 +59,51 @@ def update(frame):
ani = FuncAnimation(fig, update, frames=10)
# The test passes if no exception is raised
fig.canvas.draw()


def test_animation_save_only_tightens_first_frame(tmp_path):
"""
Saving an animation should not rerun tight layout on every frame after the
first saved frame, or frame geometry can shift between outputs.
"""
matplotlib.use("Agg")
state = np.random.RandomState(51423)

fig, axs = uplt.subplots(nrows=1, ncols=2, width="14cm")
mappables = []
for ax in axs:
m = ax.heatmap(state.rand(10, 10), cmap="dusk")
ax.colorbar(m, loc="t", tickdir="out", label="Axes Colorbars")
mappables.append(m)

axs.format(
abc="(a)",
abcloc="ul",
xlabel="xlabel",
ylabel="ylabel",
toplabels=("Left Axes", "Right Axes"),
urtitle="1",
suptitle="Test Animation",
)

auto_layout_calls = []
original_auto_layout = fig.auto_layout

def wrapped_auto_layout(*args, **kwargs):
auto_layout_calls.append(kwargs.get("tight", None))
return original_auto_layout(*args, **kwargs)

fig.auto_layout = wrapped_auto_layout

def update(frame):
for m in mappables:
m.set_array(state.rand(10, 10))
axs.format(urtitle=f"{frame + 1}")
return mappables

ani = FuncAnimation(fig, update, frames=3, interval=150)
ani.save(tmp_path / "test_animation.gif", writer="pillow")

assert auto_layout_calls
assert auto_layout_calls[0] is not False
assert auto_layout_calls[1:] == [False] * (len(auto_layout_calls) - 1)