Skip to content

Commit 0b480ee

Browse files
cvanelterengepcelbeckermr
authored
Feature: Add container to encapsulate external axes (#422)
* Fix references in documentation for clarity Fix two unidenfined references in why.rst. 1. ug_apply_norm is a typo I think. 2. ug_mplrc. I'm not sure what it should be. Only by guess. * keep apply_norm * Add container class to wrap external axes objects * remove v1 * fix test_geographic_multiple_projections * fixes * add container tests * fix merge issue * correct rebase * fix mpl39 issue * fix double draw in repl * Improve external axes container layout for native appearance - Increase default shrink factor from 0.75 to 0.95 to make external axes (e.g., ternary plots) larger and more prominent - Change positioning from centered to top-aligned with left offset for better alignment with adjacent Cartesian subplots - Top alignment ensures abc labels and titles align properly across different projection types - Add 5% left offset to better utilize available horizontal space - Update both _shrink_external_for_labels and _ensure_external_fits_within_container methods for consistency This makes ternary and other external axes integrate seamlessly with standard matplotlib subplots, appearing native rather than artificially constrained. * Handle non-numeric padding conversion * adjust test * Add coverage for external axes container * Expand external container test coverage * this works * Adjust mpltern default shrink * Add mpltern container shrink tests * Document external axes containers * adding more tests * tests * Fix merge --------- Co-authored-by: Gepcel <gepcelway@gmail.com> Co-authored-by: Matthew R. Becker <beckermr@users.noreply.github.com>
1 parent d108101 commit 0b480ee

File tree

12 files changed

+4433
-26
lines changed

12 files changed

+4433
-26
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ docs/_build
2222
docs/_static/ultraplotrc
2323
docs/_static/rctable.rst
2424
docs/_static/*
25+
*.html
2526
docs/gallery/
2627
docs/sg_execution_times.rst
2728
docs/whats_new.rst
@@ -36,7 +37,8 @@ sources
3637
*.pyc
3738
.*.pyc
3839
__pycache__
39-
test.py
40+
*.ipynb
41+
4042

4143
# OS files
4244
.DS_Store

docs/usage.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,48 @@ plotting packages.
158158
Since these features are optional,
159159
UltraPlot can be used without installing any of these packages.
160160

161+
External axes containers (mpltern, others)
162+
------------------------------------------
163+
164+
UltraPlot can wrap third-party Matplotlib projections (e.g., ``mpltern``'s
165+
``"ternary"`` projection) in a lightweight container. The container keeps
166+
UltraPlot's figure/labeling behaviors while delegating plotting calls to the
167+
external axes.
168+
169+
Basic usage mirrors standard subplots:
170+
171+
.. code-block:: python
172+
173+
import mpltern
174+
import ultraplot as uplt
175+
176+
fig, axs = uplt.subplots(ncols=2, projection="ternary")
177+
axs.format(title="Ternary example", abc=True, abcloc="left")
178+
axs[0].plot([0.1, 0.7, 0.2], [0.2, 0.2, 0.6], [0.7, 0.1, 0.2])
179+
axs[1].scatter([0.2, 0.3], [0.5, 0.4], [0.3, 0.3])
180+
181+
Controlling the external content size
182+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
183+
184+
Use ``external_shrink_factor`` (or the rc setting ``external.shrink``) to
185+
shrink the *external* axes inside the container, creating margin space for
186+
titles and annotations without resizing the subplot itself:
187+
188+
.. code-block:: python
189+
190+
uplt.rc["external.shrink"] = 0.8
191+
fig, axs = uplt.subplots(projection="ternary")
192+
axs.format(external_shrink_factor=0.7)
193+
194+
Notes and performance
195+
~~~~~~~~~~~~~~~~~~~~~
196+
197+
* Titles and a-b-c labels are rendered by the container, not the external axes,
198+
so they behave like normal UltraPlot subplots.
199+
* For mpltern with ``external_shrink_factor < 1``, UltraPlot skips the costly
200+
tight-bbox fitting pass and relies on the shrink factor for layout. This
201+
keeps rendering fast and stable.
202+
161203
.. _usage_features:
162204

163205
Additional features

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ include-package-data = true
5151
write_to = "ultraplot/_version.py"
5252
write_to_template = "__version__ = '{version}'\n"
5353

54+
55+
[tool.ruff]
56+
ignore = ["I001", "I002", "I003", "I004"]
57+
5458
[tool.basedpyright]
55-
exclude = [
56-
"**/*.ipynb"
57-
]
59+
exclude = ["**/*.ipynb"]

ultraplot/axes/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
from ..internals import context
88
from .base import Axes # noqa: F401
99
from .cartesian import CartesianAxes
10-
from .geo import GeoAxes # noqa: F401
11-
from .geo import _BasemapAxes, _CartopyAxes
10+
from .container import ExternalAxesContainer # noqa: F401
11+
from .geo import (
12+
GeoAxes, # noqa: F401
13+
_BasemapAxes,
14+
_CartopyAxes,
15+
)
1216
from .plot import PlotAxes # noqa: F401
1317
from .polar import PolarAxes
1418
from .shared import _SharedAxes # noqa: F401
@@ -22,6 +26,7 @@
2226
"PolarAxes",
2327
"GeoAxes",
2428
"ThreeAxes",
29+
"ExternalAxesContainer",
2530
]
2631

2732
# Register projections with package prefix to avoid conflicts

ultraplot/axes/base.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,18 @@ def _add_inset_axes(
967967
zoom = ax._inset_zoom = _not_none(zoom, zoom_default)
968968
if zoom:
969969
zoom_kw = zoom_kw or {}
970-
ax.indicate_inset_zoom(**zoom_kw)
970+
# Check if the inset axes is an Ultraplot axes class.
971+
# Ultraplot axes have a custom indicate_inset_zoom that can be
972+
# called on the inset itself (uses self._inset_parent internally).
973+
# Non-Ultraplot axes (e.g., raw matplotlib/cartopy) require calling
974+
# matplotlib's indicate_inset_zoom on the parent with the inset as first argument.
975+
if isinstance(ax, Axes):
976+
# Ultraplot axes: call on inset (uses self._inset_parent internally)
977+
ax.indicate_inset_zoom(**zoom_kw)
978+
else:
979+
# Non-Ultraplot axes: call matplotlib's parent class method
980+
# with inset as first argument (matplotlib API)
981+
maxes.Axes.indicate_inset_zoom(self, ax, **zoom_kw)
971982
return ax
972983

973984
def _add_queued_guides(self):
@@ -2588,7 +2599,20 @@ def _range_subplotspec(self, s):
25882599
if not isinstance(self, maxes.SubplotBase):
25892600
raise RuntimeError("Axes must be a subplot.")
25902601
ss = self.get_subplotspec().get_topmost_subplotspec()
2591-
row1, row2, col1, col2 = ss._get_rows_columns()
2602+
2603+
# Check if this is an ultraplot SubplotSpec with _get_rows_columns method
2604+
if not hasattr(ss, "_get_rows_columns"):
2605+
# Fall back to standard matplotlib SubplotSpec attributes
2606+
# This can happen when axes are created directly without ultraplot's gridspec
2607+
if hasattr(ss, "rowspan") and hasattr(ss, "colspan"):
2608+
row1, row2 = ss.rowspan.start, ss.rowspan.stop - 1
2609+
col1, col2 = ss.colspan.start, ss.colspan.stop - 1
2610+
else:
2611+
# Unable to determine range, return default
2612+
row1, row2, col1, col2 = 0, 0, 0, 0
2613+
else:
2614+
row1, row2, col1, col2 = ss._get_rows_columns()
2615+
25922616
if s == "x":
25932617
return (col1, col2)
25942618
else:

0 commit comments

Comments
 (0)