Skip to content
Open
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
85 changes: 85 additions & 0 deletions ultraplot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import annotations

import sys
from functools import wraps
from pathlib import Path
from typing import Optional

Expand Down Expand Up @@ -86,6 +87,7 @@ def _setup():
from .utils import check_for_update

check_for_update("ultraplot")
_patch_funcanimation_draw_idle()
success = True
finally:
if success:
Expand All @@ -106,6 +108,89 @@ def setup(eager: Optional[bool] = None) -> None:
_LOADER.load_all(globals())


def _patch_funcanimation_draw_idle():
try:
import matplotlib.animation as mpl_animation
except Exception:
return

try:
from .config import rc
except Exception:
return
if not rc.get("animation.force_draw_idle", True):
return

if getattr(mpl_animation.FuncAnimation, "_ultra_draw_idle_patched", False):
return

orig_init = mpl_animation.FuncAnimation.__init__
orig_stop = getattr(mpl_animation.FuncAnimation, "_stop", None)

def _install_draw_idle(self, fig):
if fig is None or not hasattr(fig, "_layout_dirty"):
return
canvas = getattr(fig, "canvas", None)
if canvas is None or not hasattr(canvas, "draw_idle"):
return
if getattr(canvas, "manager", None) is None:
return

count = getattr(canvas, "_ultra_draw_idle_count", 0)
if count == 0:
canvas._ultra_draw_idle_orig = canvas.draw_idle

# TODO: Replace this monkeypatch with a backend-level fix once
# draw_idle artifacts are resolved upstream.
def draw_idle(*args, **kwargs):
return canvas.draw(*args, **kwargs)

canvas.draw_idle = draw_idle
canvas._ultra_draw_idle_count = count + 1

import weakref

canvas_ref = weakref.ref(canvas)

def restore():
canvas = canvas_ref()
if canvas is None:
return
count = getattr(canvas, "_ultra_draw_idle_count", 0)
if count <= 1:
orig = getattr(canvas, "_ultra_draw_idle_orig", None)
if orig is not None:
canvas.draw_idle = orig
delattr(canvas, "_ultra_draw_idle_orig")
canvas._ultra_draw_idle_count = 0
else:
canvas._ultra_draw_idle_count = count - 1

self._ultra_restore_draw_idle = restore
self._ultra_draw_idle_finalizer = weakref.finalize(self, restore)

@wraps(orig_init)
def __init__(self, fig, *args, **kwargs):
orig_init(self, fig, *args, **kwargs)
_install_draw_idle(self, fig)

mpl_animation.FuncAnimation.__init__ = __init__

if orig_stop is not None:

@wraps(orig_stop)
def _stop(self, *args, **kwargs):
restore = getattr(self, "_ultra_restore_draw_idle", None)
if restore is not None:
restore()
self._ultra_restore_draw_idle = None
return orig_stop(self, *args, **kwargs)

mpl_animation.FuncAnimation._stop = _stop

mpl_animation.FuncAnimation._ultra_draw_idle_patched = True


def _build_registry_map():
global _REGISTRY_ATTRS
if _REGISTRY_ATTRS is not None:
Expand Down
58 changes: 49 additions & 9 deletions ultraplot/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,8 @@ def _canvas_preprocess(self, *args, **kwargs):
ctx2 = fig._context_authorized() # skip backend set_constrained_layout()
ctx3 = rc.context(fig._render_context) # draw with figure-specific setting
with ctx1, ctx2, ctx3:
fig.auto_layout()
if fig._layout_dirty:
fig.auto_layout()
return func(self, *args, **kwargs)

# Add preprocessor
Expand Down Expand Up @@ -799,6 +800,9 @@ def __init__(
self._is_authorized = False
self._includepanels = None
self._render_context = {}
self._layout_dirty = True
self._ultra_layout_scheduled = False
self._ultra_layout_in_progress = False
rc_kw, rc_mode = _pop_rc(kwargs)
kw_format = _pop_params(kwargs, self._format_signature)
if figwidth is not None and figheight is not None:
Expand Down Expand Up @@ -1497,6 +1501,7 @@ def _add_axes_panel(
pax.yaxis.set_tick_params(**{pax._label_key("labelright"): on})
ax.yaxis.set_tick_params(**{ax._label_key("labelright"): False})

self._layout_dirty = True
return pax

@_clear_border_cache
Expand Down Expand Up @@ -1539,6 +1544,7 @@ def _add_figure_panel(
pax._panel_side = side
pax._panel_share = False
pax._panel_parent = None
self._layout_dirty = True
return pax

@_clear_border_cache
Expand Down Expand Up @@ -2308,6 +2314,7 @@ def add_axes(self, rect, **kwargs):
%(figure.axes)s
"""
kwargs = self._parse_proj(**kwargs)
self._layout_dirty = True
return super().add_axes(rect, **kwargs)

@docstring._concatenate_inherited
Expand All @@ -2316,7 +2323,9 @@ def add_subplot(self, *args, **kwargs):
"""
%(figure.subplot)s
"""
return self._add_subplot(*args, **kwargs)
ax = self._add_subplot(*args, **kwargs)
self._layout_dirty = True
return ax

@docstring._snippet_manager
def subplot(self, *args, **kwargs): # shorthand
Expand All @@ -2330,7 +2339,9 @@ def add_subplots(self, *args, **kwargs):
"""
%(figure.subplots)s
"""
return self._add_subplots(*args, **kwargs)
axs = self._add_subplots(*args, **kwargs)
self._layout_dirty = True
return axs

@docstring._snippet_manager
def subplots(self, *args, **kwargs):
Expand Down Expand Up @@ -2363,14 +2374,13 @@ def auto_layout(self, renderer=None, aspect=None, tight=None, resize=None):
to `~Figure.subplots`, `~Figure.set_size_inches` was called manually,
or the figure was resized manually with an interactive backend.
"""

# *Impossible* to get notebook backend to work with auto resizing so we
# just do the tight layout adjustments and skip resizing.
gs = self.gridspec
renderer = self._get_renderer()
if aspect is None:
aspect = True
if tight is None:
tight = self._tight_active
layout_aspect = True if aspect is None else aspect
layout_tight = self._tight_active if tight is None else tight
if resize is False: # fix the size
self._figwidth, self._figheight = self.get_size_inches()
self._refwidth = self._refheight = None # critical!
Expand All @@ -2390,19 +2400,47 @@ def _align_content(): # noqa: E306
self._align_super_labels(side, renderer)
self._align_super_title(renderer)

before = self._layout_signature()

# Update the layout
# WARNING: Tried to avoid two figure resizes but made
# subsequent tight layout really weird. Have to resize twice.
_draw_content()
if not gs:
self._layout_dirty = False
return
if aspect:
if layout_aspect:
gs._auto_layout_aspect()
_align_content()
if tight:
if layout_tight:
gs._auto_layout_tight(renderer)
_align_content()

after = self._layout_signature()
self._layout_dirty = before != after
if self._layout_dirty and getattr(self, "canvas", None):
# Only schedule when an interactive manager exists to avoid recursion.
if getattr(self.canvas, "manager", None) is not None:
if not getattr(self.canvas, "_is_idle_drawing", False):
self.canvas.draw_idle()

def _layout_signature(self):
"""
Snapshot layout-defining state to detect convergence across draws.
"""
gs = self.gridspec
if not gs:
return None
return (
tuple(self.get_size_inches()),
gs._left_default,
gs._right_default,
gs._bottom_default,
gs._top_default,
tuple(gs._hspace_total_default),
tuple(gs._wspace_total_default),
)

@warnings._rename_kwargs(
"0.10.0", mathtext_fallback="uplt.rc.mathtext_fallback = {}"
)
Expand Down Expand Up @@ -2780,6 +2818,7 @@ def colorbar(
pad=pad,
)
cb = ax.colorbar(mappable, values, loc="fill", **kwargs)
self._layout_dirty = True
return cb

@docstring._concatenate_inherited
Expand Down Expand Up @@ -2995,6 +3034,7 @@ def legend(
pad=pad,
)
leg = ax.legend(handles, labels, loc="fill", **kwargs)
self._layout_dirty = True
return leg

@docstring._snippet_manager
Expand Down
1 change: 1 addition & 0 deletions ultraplot/gridspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,7 @@ def _update_params(
wratios=None,
width_ratios=None,
height_ratios=None,
layout_array=None,
):
"""
Update the user-specified properties.
Expand Down
6 changes: 6 additions & 0 deletions ultraplot/internals/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,12 @@ def copy(self):
"name. If ``None``, a custom ultraplot style is used. "
"If ``'default'``, the default matplotlib style is used.",
),
"animation.force_draw_idle": (
True,
_validate_bool,
"Whether to force `draw_idle` to call `draw` during animations for "
"ultraplot figures to avoid backend redraw artifacts.",
),
# A-b-c labels
"abc": (
False,
Expand Down
Loading