Source code for ultraplot.axes.container

#!/usr/bin/env python3
"""
Container class for external axes (e.g., mpltern, cartopy custom axes).

This module provides the ExternalAxesContainer class which acts as a wrapper
around external axes classes, allowing them to be used within ultraplot's
figure system while maintaining their native functionality.
"""

import matplotlib.axes as maxes
import matplotlib.transforms as mtransforms
from matplotlib import cbook, container

from ..config import rc
from ..internals import _pop_rc, warnings
from .cartesian import CartesianAxes

__all__ = ["ExternalAxesContainer"]

_ABOVE_AXES_TITLE_LOCS = {"left", "center", "right"}


[docs] class ExternalAxesContainer(CartesianAxes): """ Container axes that wraps an external axes instance. This class inherits from ultraplot's CartesianAxes and creates/manages an external axes as a child. It provides ultraplot's interface while delegating drawing and interaction to the wrapped external axes. Parameters ---------- *args Positional arguments passed to Axes.__init__ external_axes_class : type The external axes class to instantiate (e.g., mpltern.TernaryAxes) external_axes_kwargs : dict, optional Keyword arguments to pass to the external axes constructor external_shrink_factor : float, optional, default: :rc:`external.shrink` The factor by which to shrink the external axes within the container to leave room for labels. For ternary plots, labels extend significantly beyond the plot area, so a value of 0.90 (10% padding) helps prevent overlap with adjacent subplots while keeping the axes large. external_padding : float, optional, default: 5.0 Padding in points to add around the external axes tight bbox. This creates space between the external axes and adjacent subplots, preventing overlap with tick labels or other elements. Set to 0 to disable padding. **kwargs Keyword arguments passed to Axes.__init__ Notes ----- When using external axes containers with multiple subplots, the external axes (e.g., ternary plots) are automatically shrunk to prevent label overlap with adjacent subplots. If you still experience overlap, you can: 1. Increase spacing with ``wspace`` or ``hspace`` in subplots() 2. Decrease ``external_shrink_factor`` (more aggressive shrinking) 3. Use tight_layout or constrained_layout for automatic spacing Example: ``uplt.subplots(ncols=2, projection=('ternary', None), wspace=5)`` To reduce padding between external axes and adjacent subplots, use: ``external_padding=2`` or ``external_padding=0`` to disable padding entirely. """ _EXTERNAL_DELEGATE_BLOCKLIST = { # Keep UltraPlot formatting/guide behaviors on the container. "format", "colorbar", "legend", "set_title", } def __init__( self, *args, external_axes_class=None, external_axes_kwargs=None, **kwargs ): """Initialize the container and create the external axes child.""" # Initialize instance variables self._syncing_position = False self._external_axes = None self._last_external_position = None self._position_synced = False self._external_stale = True # Track if external axes needs redrawing # Store external axes class and kwargs self._external_axes_class = external_axes_class self._external_axes_kwargs = external_axes_kwargs or {} # Store shrink factor for external axes (to fit labels) # Can be customized per-axes or set globally shrink = kwargs.pop("external_shrink_factor", None) if shrink is None and external_axes_class is not None: if external_axes_class.__module__.startswith("mpltern"): shrink = 0.68 if shrink is None: shrink = rc["external.shrink"] self._external_shrink_factor = shrink # Store padding for tight bbox (prevents overlap with adjacent subplot elements) # Default 5 points (~7 pixels at 96 dpi) self._external_padding = kwargs.pop("external_padding", 5.0) # Pop the projection kwarg if it exists (matplotlib will add it) # We don't want to pass it to parent since we're using cartesian for container kwargs.pop("projection", None) # Pop format kwargs before passing to parent rc_kw, rc_mode = _pop_rc(kwargs) format_kwargs = {} # Extract common format parameters # Include both general format params and GeoAxes-specific params # to handle cases where GeoAxes might be incorrectly wrapped format_params = [ "title", "ltitle", "ctitle", "rtitle", "ultitle", "uctitle", "urtitle", "lltitle", "lctitle", "lrtitle", "abc", "abcloc", "abcstyle", "abcformat", "xlabel", "ylabel", "xlim", "ylim", "aspect", "grid", "gridminor", # GeoAxes-specific parameters "extent", "map_projection", "lonlim", "latlim", "land", "ocean", "coast", "rivers", "borders", "innerborders", "lakes", "labels", "latlines", "lonlines", "latlabels", "lonlabels", "lonlocator", "latlocator", "lonformatter", "latformatter", "lonticklen", "latticklen", "gridminor", "round", "boundinglat", ] for param in format_params: if param in kwargs: format_kwargs[param] = kwargs.pop(param) # Initialize parent ultraplot Axes # Don't set projection here - the class itself is already the right projection # and matplotlib has already resolved it before instantiation # Note: _subplot_spec is handled by parent Axes.__init__, no need to pop/restore it # Disable autoshare for external axes containers since they manage # external axes that don't participate in ultraplot's sharing system kwargs.setdefault("autoshare", False) super().__init__(*args, **kwargs) # Make the container axes invisible (it's just a holder) # But keep it functional for layout purposes self.patch.set_visible(False) self.patch.set_facecolor("none") # Hide spines for spine in self.spines.values(): spine.set_visible(False) # Hide axes self.xaxis.set_visible(False) self.yaxis.set_visible(False) # Hide axis labels explicitly self.set_xlabel("") self.set_ylabel("") self.xaxis.label.set_visible(False) self.yaxis.label.set_visible(False) # Hide tick labels self.tick_params( axis="both", which="both", labelbottom=False, labeltop=False, labelleft=False, labelright=False, bottom=False, top=False, left=False, right=False, ) # Ensure container participates in layout self.set_frame_on(False) # Create the external axes as a child if external_axes_class is not None: self._create_external_axes() # Debug: verify external axes was created if self._external_axes is None: warnings._warn_ultraplot( f"Failed to create external axes of type {external_axes_class.__name__}" ) # Apply any format kwargs if format_kwargs: self.format(**format_kwargs) def _create_external_axes(self): """Create the external axes instance as a child of this container.""" if self._external_axes_class is None: return # Get the figure fig = self.get_figure() if fig is None: warnings._warn_ultraplot("Cannot create external axes without a figure") return # Prepare kwargs for external axes external_kwargs = self._external_axes_kwargs.copy() # Get projection name projection_name = external_kwargs.pop("projection", None) # Get the subplot spec from the container subplotspec = self.get_subplotspec() # Direct instantiation of the external axes class try: # Most external axes expect (fig, *args, projection=name, **kwargs) # or use SubplotBase initialization with subplotspec if subplotspec is not None: # Try with subplotspec (standard matplotlib way) try: # Don't pass projection= since the class is already the right projection self._external_axes = self._external_axes_class( fig, subplotspec, **external_kwargs ) except TypeError as e: # Some axes might not accept subplotspec this way # Try with rect instead rect = self.get_position() # Don't pass projection= since the class is already the right projection self._external_axes = self._external_axes_class( fig, [rect.x0, rect.y0, rect.width, rect.height], **external_kwargs, ) else: # No subplotspec, use position rect rect = self.get_position() # Don't pass projection= since the class is already the right projection self._external_axes = self._external_axes_class( fig, [rect.x0, rect.y0, rect.width, rect.height], **external_kwargs, ) # Note: Most axes classes automatically register themselves with the figure # during __init__. We need to REMOVE them from fig.axes so that ultraplot # doesn't try to call ultraplot-specific methods on them. # The container will handle all the rendering. if self._external_axes in fig.axes: fig.axes.remove(self._external_axes) # Ensure external axes is visible and has higher zorder than container if hasattr(self._external_axes, "set_visible"): self._external_axes.set_visible(True) if hasattr(self._external_axes, "set_zorder"): # Set higher zorder so external axes draws on top of container container_zorder = self.get_zorder() self._external_axes.set_zorder(container_zorder + 1) if hasattr(self._external_axes.patch, "set_visible"): self._external_axes.patch.set_visible(True) # Ensure the external axes patch has white background by default if hasattr(self._external_axes.patch, "set_facecolor"): self._external_axes.patch.set_facecolor("white") # Ensure all spines are visible if hasattr(self._external_axes, "spines"): for spine in self._external_axes.spines.values(): if hasattr(spine, "set_visible"): spine.set_visible(True) # Ensure axes frame is on if hasattr(self._external_axes, "set_frame_on"): self._external_axes.set_frame_on(True) # Set subplotspec on the external axes if it has the method if subplotspec is not None and hasattr( self._external_axes, "set_subplotspec" ): self._external_axes.set_subplotspec(subplotspec) # Set up position synchronization self._sync_position_to_external() # Mark external axes as stale (needs drawing) self._external_stale = True # Note: Do NOT add external axes as a child artist to the container. # The container's draw() method explicitly handles drawing the external axes # (line ~514), and adding it as a child would cause matplotlib to draw it # twice - once via our explicit call and once via the parent's child iteration. # This double-draw is especially visible in REPL environments where figures # are displayed multiple times. # After creation, ensure external axes fits within container by measuring # This is done lazily on first draw to ensure renderer is available except Exception as e: warnings._warn_ultraplot( f"Failed to create external axes {self._external_axes_class.__name__}: {e}" ) self._external_axes = None def _shrink_external_for_labels(self, base_pos=None): """ Shrink the external axes to leave room for labels that extend beyond the plot area. This is particularly important for ternary plots where axis labels can extend significantly beyond the triangular plot region. """ if self._external_axes is None: return # Get the base position to shrink from pos = base_pos if base_pos is not None else self._external_axes.get_position() # Shrink to leave room for labels that extend beyond the plot area # For ternary axes, labels typically need about 10% padding (0.90 shrink factor) # This prevents label overlap with adjacent subplots # Use the configured shrink factor shrink_factor = getattr(self, "_external_shrink_factor", rc["external.shrink"]) # Center the external axes within the container to add uniform margins. new_width = pos.width * shrink_factor new_height = pos.height * shrink_factor new_x0 = pos.x0 + (pos.width - new_width) / 2 new_y0 = pos.y0 + (pos.height - new_height) / 2 # Set the new position from matplotlib.transforms import Bbox new_pos = Bbox.from_bounds(new_x0, new_y0, new_width, new_height) if hasattr(self._external_axes, "set_position"): self._external_axes.set_position(new_pos) # Also adjust aspect if the external axes has aspect control # This helps ternary axes maintain their triangular shape if hasattr(self._external_axes, "set_aspect"): try: self._external_axes.set_aspect("equal", adjustable="box") except Exception: pass # Some axes types don't support aspect adjustment def _ensure_external_fits_within_container(self, renderer): """ Iteratively shrink external axes until it fits completely within container bounds. This ensures that external axes labels don't extend beyond the container's allocated space and overlap with adjacent subplots. """ if self._external_axes is None: return if ( self._external_axes.__class__.__module__.startswith("mpltern") and self._external_shrink_factor < 1 ): return if not hasattr(self._external_axes, "get_tightbbox"): return # Get container bounds in display coordinates container_pos = self.get_position() container_bbox = container_pos.transformed(self.figure.transFigure) # Reserve vertical space for titles/abc labels. title_pad_px = 0.0 for loc, obj in self._title_dict.items(): if not self._title_reserves_external_space(loc): continue if not obj.get_visible(): continue if not obj.get_text(): continue try: bbox = obj.get_window_extent(renderer) except Exception: continue if bbox.height > title_pad_px: title_pad_px = bbox.height if title_pad_px > 0 and title_pad_px < container_bbox.height: from matplotlib.transforms import Bbox container_bbox = Bbox.from_bounds( container_bbox.x0, container_bbox.y0, container_bbox.width, container_bbox.height - title_pad_px, ) padding = getattr(self, "_external_padding", 0.0) or 0.0 ptp = getattr(renderer, "points_to_pixels", None) if padding > 0 and callable(ptp): try: pad_px = ptp(padding) if not isinstance(pad_px, (int, float)): raise TypeError("points_to_pixels returned non-numeric value") if ( pad_px * 2 < container_bbox.width and pad_px * 2 < container_bbox.height ): from matplotlib.transforms import Bbox container_bbox = Bbox.from_bounds( container_bbox.x0 + pad_px, container_bbox.y0 + pad_px, container_bbox.width - 2 * pad_px, container_bbox.height - 2 * pad_px, ) except Exception: # If renderer can't convert points to pixels, skip padding. pass # Try up to 10 iterations to fit the external axes within container max_iterations = 10 tolerance = 1.0 # 1 pixel tolerance for iteration in range(max_iterations): # Get external axes tight bbox (includes labels) ext_tight = self._external_axes.get_tightbbox(renderer) if ext_tight is None: break # Check if external axes extends beyond container extends_left = ext_tight.x0 < container_bbox.x0 - tolerance extends_right = ext_tight.x1 > container_bbox.x1 + tolerance extends_bottom = ext_tight.y0 < container_bbox.y0 - tolerance extends_top = ext_tight.y1 > container_bbox.y1 + tolerance if not (extends_left or extends_right or extends_bottom or extends_top): # Fits within container, we're done break # Calculate how much we need to shrink current_pos = self._external_axes.get_position() # Calculate shrink factors needed in each direction shrink_x = 1.0 shrink_y = 1.0 if extends_left or extends_right: # Need to shrink horizontally available_width = container_bbox.width needed_width = ext_tight.width if needed_width > 0: shrink_x = min(0.95, available_width / needed_width * 0.95) if extends_bottom or extends_top: # Need to shrink vertically available_height = container_bbox.height needed_height = ext_tight.height if needed_height > 0: shrink_y = min(0.95, available_height / needed_height * 0.95) # Use the more aggressive shrink factor shrink_factor = min(shrink_x, shrink_y) # Apply shrinking with top-aligned, left-offset positioning center_x = current_pos.x0 + current_pos.width / 2 new_width = current_pos.width * shrink_factor new_height = current_pos.height * shrink_factor # Move 5% to the left from center new_x0 = center_x - new_width / 2 - current_pos.width * 0.05 left_bound = current_pos.x0 right_bound = current_pos.x0 + current_pos.width - new_width if right_bound >= left_bound: new_x0 = min(max(new_x0, left_bound), right_bound) new_y0 = current_pos.y0 + current_pos.height - new_height from matplotlib.transforms import Bbox new_pos = Bbox.from_bounds(new_x0, new_y0, new_width, new_height) self._external_axes.set_position(new_pos) # Mark as stale to ensure it redraws with new position if hasattr(self._external_axes, "stale"): self._external_axes.stale = True def _sync_position_to_external(self): """Synchronize the container position to the external axes.""" if self._external_axes is None: return # Copy position from container to external axes and apply shrink pos = self.get_position() if hasattr(self._external_axes, "set_position"): self._external_axes.set_position(pos) self._shrink_external_for_labels(base_pos=pos)
[docs] def set_position(self, pos, which="both"): """Override to sync position changes to external axes.""" super().set_position(pos, which=which) if not getattr(self, "_syncing_position", False): self._sync_position_to_external() self._last_external_position = None self._position_synced = False self._external_stale = True
def _reposition_subplot(self): super()._reposition_subplot() if not getattr(self, "_syncing_position", False): self._sync_position_to_external() self._last_external_position = None self._position_synced = False self._external_stale = True def _update_title_position(self, renderer): super()._update_title_position(renderer) if self._external_axes is None: return if not self._external_axes.__class__.__module__.startswith("mpltern"): return fig = self.figure if fig is None: return container_bbox = self.get_position().transformed(fig.transFigure) if container_bbox.height <= 0: return for loc, obj in self._title_dict.items(): if not self._title_reserves_external_space(loc): continue bbox = obj.get_window_extent(renderer) overflow = bbox.y1 - container_bbox.y1 if overflow > 0: x, y = obj.get_position() y -= overflow / container_bbox.height obj.set_position((x, y)) def _title_reserves_external_space(self, loc): """ Return whether a title-like artist needs room above an external axes. """ if loc == "abc": loc = self._abc_loc return loc in _ABOVE_AXES_TITLE_LOCS def _iter_axes(self, hidden=True, children=True, panels=True): """ Override to only yield the container itself, not the external axes. The external axes is a rendering child, not a logical ultraplot child, so we don't want ultraplot's iteration to find it and call ultraplot methods on it. """ # Only yield self (the container), never the external axes yield self # Plotting method delegation # Override common plotting methods to delegate to external axes
[docs] def plot(self, *args, **kwargs): """Delegate plot to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.plot(*args, **kwargs) return super().plot(*args, **kwargs)
[docs] def scatter(self, *args, **kwargs): """Delegate scatter to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.scatter(*args, **kwargs) return super().scatter(*args, **kwargs)
[docs] def fill(self, *args, **kwargs): """Delegate fill to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.fill(*args, **kwargs) return super().fill(*args, **kwargs)
[docs] def contour(self, *args, **kwargs): """Delegate contour to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.contour(*args, **kwargs) return super().contour(*args, **kwargs)
[docs] def contourf(self, *args, **kwargs): """Delegate contourf to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.contourf(*args, **kwargs) return super().contourf(*args, **kwargs)
[docs] def pcolormesh(self, *args, **kwargs): """Delegate pcolormesh to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.pcolormesh(*args, **kwargs) return super().pcolormesh(*args, **kwargs)
[docs] def tripcolor(self, *args, **kwargs): """Delegate tripcolor to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.tripcolor(*args, **kwargs) return super().tripcolor(*args, **kwargs)
[docs] def tricontour(self, *args, **kwargs): """Delegate tricontour to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.tricontour(*args, **kwargs) return super().tricontour(*args, **kwargs)
[docs] def tricontourf(self, *args, **kwargs): """Delegate tricontourf to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.tricontourf(*args, **kwargs) return super().tricontourf(*args, **kwargs)
[docs] def triplot(self, *args, **kwargs): """Delegate triplot to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.triplot(*args, **kwargs) return super().triplot(*args, **kwargs)
[docs] def imshow(self, *args, **kwargs): """Delegate imshow to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.imshow(*args, **kwargs) return super().imshow(*args, **kwargs)
[docs] def hexbin(self, *args, **kwargs): """Delegate hexbin to external axes.""" if self._external_axes is not None: self._external_stale = True # Mark for redraw return self._external_axes.hexbin(*args, **kwargs) return super().hexbin(*args, **kwargs)
[docs] def get_external_axes(self): """ Get the wrapped external axes instance. Returns ------- axes The external axes instance, or None if not created """ return self._external_axes
[docs] def has_external_child(self): """ Check if this container has an external axes child. Returns ------- bool True if an external axes instance exists, False otherwise """ return self._external_axes is not None
[docs] def get_external_child(self): """ Get the external axes child (alias for get_external_axes). Returns ------- axes The external axes instance, or None if not created """ return self.get_external_axes()
[docs] def clear(self): """Clear the container and mark external axes as stale.""" # Mark external axes as stale before clearing self._external_stale = True # Clear the container super().clear() # If we have external axes, clear it too if self._external_axes is not None: self._external_axes.clear()
[docs] def format(self, **kwargs): """ Format the container and delegate to external axes where appropriate. This method handles ultraplot-specific formatting on the container and attempts to delegate common parameters to the external axes. Parameters ---------- **kwargs Formatting parameters. Common matplotlib parameters (title, xlabel, ylabel, xlim, ylim) are delegated to the external axes if supported. """ # Separate kwargs into container and external external_kwargs = {} container_kwargs = {} shrink = kwargs.pop("external_shrink_factor", None) if shrink is not None: self._external_shrink_factor = shrink self._sync_position_to_external() # Parameters that can be delegated to external axes delegatable = ["title", "xlabel", "ylabel", "xlim", "ylim"] is_mpltern = ( self._external_axes is not None and self._external_axes.__class__.__module__.startswith("mpltern") ) for key, value in kwargs.items(): if key in delegatable and self._external_axes is not None: if key == "title" and is_mpltern: container_kwargs[key] = value continue # Check if external axes has the method method_name = f"set_{key}" if hasattr(self._external_axes, method_name): external_kwargs[key] = value else: container_kwargs[key] = value else: container_kwargs[key] = value # Apply container formatting (for ultraplot-specific features) if container_kwargs: super().format(**container_kwargs) # Apply external axes formatting if external_kwargs and self._external_axes is not None: self._external_axes.set(**external_kwargs)
[docs] def draw(self, renderer): """Override draw to render container (with abc/titles) and external axes.""" # Draw external axes first - it may adjust its own position for labels if self._external_axes is not None: # Check if external axes is stale (needs redrawing) # This avoids redundant draws on external axes that haven't changed external_stale = getattr(self._external_axes, "stale", True) is_mpltern = self._external_axes.__class__.__module__.startswith("mpltern") # Only draw if external axes is stale or we haven't synced positions yet if external_stale or not self._position_synced or self._external_stale: # First, ensure external axes fits within container bounds # This prevents labels from overlapping with adjacent subplots self._ensure_external_fits_within_container(renderer) self._external_axes.draw(renderer) self._external_stale = False # Sync container position to external axes if needed # This ensures abc labels and titles are positioned correctly ext_pos = self._external_axes.get_position() # Quick check if position changed since last draw position_changed = False if self._last_external_position is None: position_changed = True else: last_pos = self._last_external_position # Use a slightly larger tolerance to avoid excessive sync calls if ( abs(ext_pos.x0 - last_pos.x0) > 0.001 or abs(ext_pos.y0 - last_pos.y0) > 0.001 or abs(ext_pos.width - last_pos.width) > 0.001 or abs(ext_pos.height - last_pos.height) > 0.001 ): position_changed = True # Only update if position actually changed if position_changed: if is_mpltern: # Keep container position for mpltern to avoid shifting titles/abc. self._last_external_position = ext_pos self._position_synced = True else: container_pos = self.get_position() # Check if container needs updating if ( abs(container_pos.x0 - ext_pos.x0) > 0.001 or abs(container_pos.y0 - ext_pos.y0) > 0.001 or abs(container_pos.width - ext_pos.width) > 0.001 or abs(container_pos.height - ext_pos.height) > 0.001 ): # Temporarily disable position sync to avoid recursion self._syncing_position = True self.set_position(ext_pos) self._syncing_position = False # Cache the current external position self._last_external_position = ext_pos self._position_synced = True # Draw the container (with abc labels, titles, etc.) super().draw(renderer)
[docs] def stale_callback(self, *args, **kwargs): """Mark external axes as stale when container is marked stale.""" # When container is marked stale, mark external axes as stale too if self._external_axes is not None: self._external_stale = True # Call parent stale callback if it exists if hasattr(super(), "stale_callback"): super().stale_callback(*args, **kwargs)
[docs] def get_tightbbox(self, renderer, *args, **kwargs): """ Override to return the container bbox for consistent layout positioning. By returning the container's bbox, we ensure the layout engine positions the container properly within the subplot grid, and we rely on our iterative shrinking to ensure the external axes fits within the container. """ # Simply return the container's position bbox # This gives the layout engine a symmetric, predictable bbox to work with container_pos = self.get_position() container_bbox = container_pos.transformed(self.figure.transFigure) return container_bbox
def __getattr__(self, name): """ Delegate missing attributes to the external axes unless blocked. """ if name in self._EXTERNAL_DELEGATE_BLOCKLIST: raise AttributeError( f"'{type(self).__name__}' object has no attribute '{name}'" ) if self._external_axes is not None: return getattr(self._external_axes, name) raise AttributeError( f"'{type(self).__name__}' object has no attribute '{name}'" ) def __dir__(self): """Include external axes attributes in dir() output.""" attrs = set(super().__dir__()) if self._external_axes is not None: attrs.update(dir(self._external_axes)) return sorted(attrs)
def create_external_axes_container(external_axes_class, projection_name=None): """ Factory function to create a container class for a specific external axes type. Parameters ---------- external_axes_class : type The external axes class to wrap projection_name : str, optional The projection name to register with matplotlib Returns ------- type A subclass of ExternalAxesContainer configured for the external axes class """ class SpecificContainer(ExternalAxesContainer): """Container for {external_axes_class.__name__}""" def __init__(self, *args, **kwargs): # Pop external_axes_class and external_axes_kwargs if passed in kwargs # (they're passed from Figure._add_subplot) ext_class = kwargs.pop("external_axes_class", None) ext_kwargs = kwargs.pop("external_axes_kwargs", None) # Pop projection - it's already been handled and shouldn't be passed to parent kwargs.pop("projection", None) # Use the provided class or fall back to the factory default if ext_class is None: ext_class = external_axes_class if ext_kwargs is None: ext_kwargs = {} # Inject the external axes class kwargs["external_axes_class"] = ext_class kwargs["external_axes_kwargs"] = ext_kwargs super().__init__(*args, **kwargs) # Set proper name and module SpecificContainer.__name__ = f"{external_axes_class.__name__}Container" SpecificContainer.__qualname__ = f"{external_axes_class.__name__}Container" if projection_name: SpecificContainer.name = projection_name return SpecificContainer