|
6 | 6 | import copy |
7 | 7 | import inspect |
8 | 8 | import re |
| 9 | +import sys |
9 | 10 | import types |
10 | | -from numbers import Integral, Number |
11 | | -from typing import Union, Iterable, MutableMapping, Optional, Tuple |
12 | 11 | from collections.abc import Iterable as IterableType |
| 12 | +from numbers import Integral, Number |
| 13 | +from typing import Iterable, MutableMapping, Optional, Tuple, Union |
13 | 14 |
|
14 | 15 | try: |
15 | 16 | # From python 3.12 |
|
34 | 35 | from matplotlib import cbook |
35 | 36 | from packaging import version |
36 | 37 |
|
37 | | -from .. import legend as plegend |
38 | 38 | from .. import colors as pcolors |
39 | 39 | from .. import constructor |
| 40 | +from .. import legend as plegend |
40 | 41 | from .. import ticker as pticker |
41 | 42 | from ..config import rc |
42 | | -from ..internals import ic # noqa: F401 |
43 | 43 | from ..internals import ( |
44 | 44 | _kwargs_to_args, |
45 | 45 | _not_none, |
|
51 | 51 | _version_mpl, |
52 | 52 | docstring, |
53 | 53 | guides, |
| 54 | + ic, # noqa: F401 |
54 | 55 | labels, |
55 | 56 | rcsetup, |
56 | 57 | warnings, |
@@ -700,7 +701,52 @@ def __call__(self, ax, renderer): # noqa: U100 |
700 | 701 | return bbox |
701 | 702 |
|
702 | 703 |
|
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): |
704 | 750 | """ |
705 | 751 | The lowest-level `~matplotlib.axes.Axes` subclass used by ultraplot. |
706 | 752 | Implements basic universal features. |
@@ -822,6 +868,7 @@ def __init__(self, *args, **kwargs): |
822 | 868 | self._panel_sharey_group = False # see _apply_auto_share |
823 | 869 | self._panel_side = None |
824 | 870 | self._tight_bbox = None # bounding boxes are saved |
| 871 | + self._integration_external = None # explicit external-mode override (None=auto) |
825 | 872 | self.xaxis.isDefault_minloc = True # ensure enabled at start (needed for dual) |
826 | 873 | self.yaxis.isDefault_minloc = True |
827 | 874 |
|
@@ -1739,14 +1786,20 @@ def _get_legend_handles(self, handler_map=None): |
1739 | 1786 | handler_map_full = plegend.Legend.get_default_handler_map() |
1740 | 1787 | handler_map_full = handler_map_full.copy() |
1741 | 1788 | handler_map_full.update(handler_map or {}) |
| 1789 | + # Prefer synthetic tagging to exclude helper artists; see _ultraplot_synthetic flag on artists. |
1742 | 1790 | for ax in axs: |
1743 | 1791 | for attr in ("lines", "patches", "collections", "containers"): |
1744 | 1792 | for handle in getattr(ax, attr, []): # guard against API changes |
1745 | 1793 | label = handle.get_label() |
1746 | 1794 | handler = plegend.Legend.get_legend_handler( |
1747 | 1795 | handler_map_full, handle |
1748 | 1796 | ) # 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 | + ): |
1750 | 1803 | handles.append(handle) |
1751 | 1804 | return handles |
1752 | 1805 |
|
@@ -1897,11 +1950,17 @@ def _update_guide( |
1897 | 1950 | if legend: |
1898 | 1951 | align = legend_kw.pop("align", None) |
1899 | 1952 | 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) |
1901 | 1956 | if colorbar: |
1902 | 1957 | align = colorbar_kw.pop("align", None) |
1903 | 1958 | 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 | + ) |
1905 | 1964 |
|
1906 | 1965 | @staticmethod |
1907 | 1966 | def _parse_frame(guide, fancybox=None, shadow=None, **kwargs): |
@@ -2423,6 +2482,8 @@ def _legend_label(*objs): # noqa: E301 |
2423 | 2482 | labs = [] |
2424 | 2483 | for obj in objs: |
2425 | 2484 | if hasattr(obj, "get_label"): # e.g. silent list |
| 2485 | + if getattr(obj, "_ultraplot_synthetic", False): |
| 2486 | + continue |
2426 | 2487 | lab = obj.get_label() |
2427 | 2488 | if lab is not None and not str(lab).startswith("_"): |
2428 | 2489 | labs.append(lab) |
@@ -2453,10 +2514,15 @@ def _legend_tuple(*objs): # noqa: E306 |
2453 | 2514 | if hs: |
2454 | 2515 | handles.extend(hs) |
2455 | 2516 | 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]) |
2457 | 2520 | else: |
2458 | 2521 | handles.append(obj) |
2459 | 2522 | elif hasattr(obj, "get_label"): |
| 2523 | + # Skip synthetic helpers and fill_between collections |
| 2524 | + if getattr(obj, "_ultraplot_synthetic", False): |
| 2525 | + continue |
2460 | 2526 | handles.append(obj) |
2461 | 2527 | else: |
2462 | 2528 | warnings._warn_ultraplot(f"Ignoring invalid legend handle {obj!r}.") |
@@ -3332,6 +3398,7 @@ def _label_key(self, side: str) -> str: |
3332 | 3398 | labelright/labelleft respectively. |
3333 | 3399 | """ |
3334 | 3400 | from packaging import version |
| 3401 | + |
3335 | 3402 | from ..internals import _version_mpl |
3336 | 3403 |
|
3337 | 3404 | # TODO: internal deprecation warning when we drop 3.9, we need to remove this |
|
0 commit comments