Skip to content

Commit 63cf012

Browse files
Add external context mode for axes (#406)
* add seaborn context processing * rm debug * add unittest * resolve iterable * relax legend filter * add seaborn import * add more unittests * add ctx texts * implement mark external and context managing * fix test * refactor classes for clarity * update tests * more fixes * more tests * minor fix * minor fix * fix for mpl 3.9 * remove stack frame * adjust and remove unecessary tests * more fixes * add external to pass test * restore test * rm dup * finalize docstring * remove fallback * Apply suggestion from @beckermr * Apply suggestion from @beckermr * fix bar and test --------- Co-authored-by: Matthew R. Becker <beckermr@users.noreply.github.com>
1 parent 55d3d67 commit 63cf012

File tree

7 files changed

+596
-75
lines changed

7 files changed

+596
-75
lines changed

ultraplot/axes/base.py

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
import copy
77
import inspect
88
import re
9+
import sys
910
import types
10-
from numbers import Integral, Number
11-
from typing import Union, Iterable, MutableMapping, Optional, Tuple
1211
from collections.abc import Iterable as IterableType
12+
from numbers import Integral, Number
13+
from typing import Iterable, MutableMapping, Optional, Tuple, Union
1314

1415
try:
1516
# From python 3.12
@@ -34,12 +35,11 @@
3435
from matplotlib import cbook
3536
from packaging import version
3637

37-
from .. import legend as plegend
3838
from .. import colors as pcolors
3939
from .. import constructor
40+
from .. import legend as plegend
4041
from .. import ticker as pticker
4142
from ..config import rc
42-
from ..internals import ic # noqa: F401
4343
from ..internals import (
4444
_kwargs_to_args,
4545
_not_none,
@@ -51,6 +51,7 @@
5151
_version_mpl,
5252
docstring,
5353
guides,
54+
ic, # noqa: F401
5455
labels,
5556
rcsetup,
5657
warnings,
@@ -700,7 +701,52 @@ def __call__(self, ax, renderer): # noqa: U100
700701
return bbox
701702

702703

703-
class Axes(maxes.Axes):
704+
class _ExternalModeMixin:
705+
"""
706+
Mixin providing explicit external-mode control and a context manager.
707+
"""
708+
709+
def set_external(self, value=True):
710+
"""
711+
Set explicit external-mode override for this axes.
712+
713+
value:
714+
- True: force external behavior (defer on-the-fly guides, etc.)
715+
- False: force UltraPlot behavior
716+
"""
717+
if value not in (True, False):
718+
raise ValueError("set_external expects True or False")
719+
setattr(self, "_integration_external", value)
720+
return self
721+
722+
class _ExternalContext:
723+
def __init__(self, ax, value=True):
724+
self._ax = ax
725+
self._value = True if value is None else value
726+
self._prev = getattr(ax, "_integration_external", None)
727+
728+
def __enter__(self):
729+
self._ax._integration_external = self._value
730+
return self._ax
731+
732+
def __exit__(self, exc_type, exc, tb):
733+
self._ax._integration_external = self._prev
734+
735+
def external(self, value=True):
736+
"""
737+
Context manager toggling external mode during the block.
738+
"""
739+
return _ExternalModeMixin._ExternalContext(self, value)
740+
741+
def _in_external_context(self):
742+
"""
743+
Return True if UltraPlot helper behaviors should be suppressed.
744+
"""
745+
mode = getattr(self, "_integration_external", None)
746+
return mode is True
747+
748+
749+
class Axes(_ExternalModeMixin, maxes.Axes):
704750
"""
705751
The lowest-level `~matplotlib.axes.Axes` subclass used by ultraplot.
706752
Implements basic universal features.
@@ -822,6 +868,7 @@ def __init__(self, *args, **kwargs):
822868
self._panel_sharey_group = False # see _apply_auto_share
823869
self._panel_side = None
824870
self._tight_bbox = None # bounding boxes are saved
871+
self._integration_external = None # explicit external-mode override (None=auto)
825872
self.xaxis.isDefault_minloc = True # ensure enabled at start (needed for dual)
826873
self.yaxis.isDefault_minloc = True
827874

@@ -1739,14 +1786,20 @@ def _get_legend_handles(self, handler_map=None):
17391786
handler_map_full = plegend.Legend.get_default_handler_map()
17401787
handler_map_full = handler_map_full.copy()
17411788
handler_map_full.update(handler_map or {})
1789+
# Prefer synthetic tagging to exclude helper artists; see _ultraplot_synthetic flag on artists.
17421790
for ax in axs:
17431791
for attr in ("lines", "patches", "collections", "containers"):
17441792
for handle in getattr(ax, attr, []): # guard against API changes
17451793
label = handle.get_label()
17461794
handler = plegend.Legend.get_legend_handler(
17471795
handler_map_full, handle
17481796
) # noqa: E501
1749-
if handler and label and label[0] != "_":
1797+
if (
1798+
handler
1799+
and label
1800+
and label[0] != "_"
1801+
and not getattr(handle, "_ultraplot_synthetic", False)
1802+
):
17501803
handles.append(handle)
17511804
return handles
17521805

@@ -1897,11 +1950,17 @@ def _update_guide(
18971950
if legend:
18981951
align = legend_kw.pop("align", None)
18991952
queue = legend_kw.pop("queue", queue_legend)
1900-
self.legend(objs, loc=legend, align=align, queue=queue, **legend_kw)
1953+
# Avoid immediate legend creation in external context
1954+
if not self._in_external_context():
1955+
self.legend(objs, loc=legend, align=align, queue=queue, **legend_kw)
19011956
if colorbar:
19021957
align = colorbar_kw.pop("align", None)
19031958
queue = colorbar_kw.pop("queue", queue_colorbar)
1904-
self.colorbar(objs, loc=colorbar, align=align, queue=queue, **colorbar_kw)
1959+
# Avoid immediate colorbar creation in external context
1960+
if not self._in_external_context():
1961+
self.colorbar(
1962+
objs, loc=colorbar, align=align, queue=queue, **colorbar_kw
1963+
)
19051964

19061965
@staticmethod
19071966
def _parse_frame(guide, fancybox=None, shadow=None, **kwargs):
@@ -2423,6 +2482,8 @@ def _legend_label(*objs): # noqa: E301
24232482
labs = []
24242483
for obj in objs:
24252484
if hasattr(obj, "get_label"): # e.g. silent list
2485+
if getattr(obj, "_ultraplot_synthetic", False):
2486+
continue
24262487
lab = obj.get_label()
24272488
if lab is not None and not str(lab).startswith("_"):
24282489
labs.append(lab)
@@ -2453,10 +2514,15 @@ def _legend_tuple(*objs): # noqa: E306
24532514
if hs:
24542515
handles.extend(hs)
24552516
elif obj: # fallback to first element
2456-
handles.append(obj[0])
2517+
# Skip synthetic helpers and fill_between collections
2518+
if not getattr(obj[0], "_ultraplot_synthetic", False):
2519+
handles.append(obj[0])
24572520
else:
24582521
handles.append(obj)
24592522
elif hasattr(obj, "get_label"):
2523+
# Skip synthetic helpers and fill_between collections
2524+
if getattr(obj, "_ultraplot_synthetic", False):
2525+
continue
24602526
handles.append(obj)
24612527
else:
24622528
warnings._warn_ultraplot(f"Ignoring invalid legend handle {obj!r}.")
@@ -3332,6 +3398,7 @@ def _label_key(self, side: str) -> str:
33323398
labelright/labelleft respectively.
33333399
"""
33343400
from packaging import version
3401+
33353402
from ..internals import _version_mpl
33363403

33373404
# TODO: internal deprecation warning when we drop 3.9, we need to remove this

0 commit comments

Comments
 (0)