#!/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 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