Skip to content

Commit 7c8b498

Browse files
committed
Fix/gridspec indexing (#667)
* Fix cartopy rectilinear projection detection * Ignore non-finite inset colorbar artist bounds during reflow checks, relax the helper regression to assert the stable post-reflow behavior, and document that broad pytest verification should run with -n 4 so future validation stays fast on top of main.
1 parent 2cd92c4 commit 7c8b498

4 files changed

Lines changed: 96 additions & 76 deletions

File tree

ultraplot/axes/base.py

Lines changed: 45 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4607,35 +4607,35 @@ def _apply_inset_colorbar_layout(
46074607
frame.set_bounds(*bounds_frame)
46084608

46094609

4610-
def _inset_colorbar_frame_needs_reflow(colorbar, *, labelloc: str, renderer) -> bool:
4611-
cax = colorbar.ax
4612-
layout = getattr(cax, "_inset_colorbar_layout", None)
4613-
frame = getattr(cax, "_inset_colorbar_frame", None)
4614-
if not layout or frame is None:
4615-
return False
4610+
def _has_finite_bbox(bbox) -> bool:
4611+
return bbox is not None and np.all(
4612+
np.isfinite((bbox.x0, bbox.y0, bbox.x1, bbox.y1))
4613+
)
46164614

4617-
orientation = layout["orientation"]
4618-
loc = layout["loc"]
4619-
ticklocation = layout["ticklocation"]
4620-
labelloc_layout = labelloc if isinstance(labelloc, str) else ticklocation
4615+
4616+
def _collect_inset_colorbar_bboxes(
4617+
colorbar, *, labelloc_layout: str, loc: str, orientation: str, renderer
4618+
):
46214619
bboxes = []
46224620

46234621
longaxis = _get_colorbar_long_axis(colorbar)
46244622
try:
46254623
bbox = longaxis.get_tightbbox(renderer)
46264624
except Exception:
46274625
bbox = None
4628-
if bbox is not None:
4626+
if _has_finite_bbox(bbox):
46294627
bboxes.append(bbox)
46304628

46314629
label_axis = _get_axis_for(
46324630
labelloc_layout, loc, orientation=orientation, ax=colorbar
46334631
)
46344632
if label_axis.label.get_text():
46354633
try:
4636-
bboxes.append(label_axis.label.get_window_extent(renderer=renderer))
4634+
bbox = label_axis.label.get_window_extent(renderer=renderer)
46374635
except Exception:
4638-
pass
4636+
bbox = None
4637+
if _has_finite_bbox(bbox):
4638+
bboxes.append(bbox)
46394639

46404640
for artist in (
46414641
getattr(colorbar, "outline", None),
@@ -4645,9 +4645,33 @@ def _inset_colorbar_frame_needs_reflow(colorbar, *, labelloc: str, renderer) ->
46454645
if artist is None:
46464646
continue
46474647
try:
4648-
bboxes.append(artist.get_window_extent(renderer=renderer))
4648+
bbox = artist.get_window_extent(renderer=renderer)
46494649
except Exception:
4650-
pass
4650+
bbox = None
4651+
if _has_finite_bbox(bbox):
4652+
bboxes.append(bbox)
4653+
4654+
return bboxes
4655+
4656+
4657+
def _inset_colorbar_frame_needs_reflow(colorbar, *, labelloc: str, renderer) -> bool:
4658+
cax = colorbar.ax
4659+
layout = getattr(cax, "_inset_colorbar_layout", None)
4660+
frame = getattr(cax, "_inset_colorbar_frame", None)
4661+
if not layout or frame is None:
4662+
return False
4663+
4664+
orientation = layout["orientation"]
4665+
loc = layout["loc"]
4666+
ticklocation = layout["ticklocation"]
4667+
labelloc_layout = labelloc if isinstance(labelloc, str) else ticklocation
4668+
bboxes = _collect_inset_colorbar_bboxes(
4669+
colorbar,
4670+
labelloc_layout=labelloc_layout,
4671+
loc=loc,
4672+
orientation=orientation,
4673+
renderer=renderer,
4674+
)
46514675

46524676
if not bboxes:
46534677
return False
@@ -4712,37 +4736,13 @@ def _reflow_inset_colorbar_frame(
47124736
renderer = renderer or cax.figure._get_renderer()
47134737
if hasattr(colorbar, "update_ticks"):
47144738
colorbar.update_ticks(manual_only=True)
4715-
bboxes = []
4716-
longaxis = _get_colorbar_long_axis(colorbar)
4717-
try:
4718-
bbox = longaxis.get_tightbbox(renderer)
4719-
except Exception:
4720-
bbox = None
4721-
if bbox is not None:
4722-
bboxes.append(bbox)
4723-
label_axis = _get_axis_for(
4724-
labelloc_layout, loc, orientation=orientation, ax=colorbar
4739+
bboxes = _collect_inset_colorbar_bboxes(
4740+
colorbar,
4741+
labelloc_layout=labelloc_layout,
4742+
loc=loc,
4743+
orientation=orientation,
4744+
renderer=renderer,
47254745
)
4726-
if label_axis.label.get_text():
4727-
try:
4728-
bboxes.append(label_axis.label.get_window_extent(renderer=renderer))
4729-
except Exception:
4730-
pass
4731-
if colorbar.outline is not None:
4732-
try:
4733-
bboxes.append(colorbar.outline.get_window_extent(renderer=renderer))
4734-
except Exception:
4735-
pass
4736-
if getattr(colorbar, "solids", None) is not None:
4737-
try:
4738-
bboxes.append(colorbar.solids.get_window_extent(renderer=renderer))
4739-
except Exception:
4740-
pass
4741-
if getattr(colorbar, "dividers", None) is not None:
4742-
try:
4743-
bboxes.append(colorbar.dividers.get_window_extent(renderer=renderer))
4744-
except Exception:
4745-
pass
47464746
if not bboxes:
47474747
return
47484748
x0 = min(b.x0 for b in bboxes)

ultraplot/axes/geo.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3831,10 +3831,35 @@ def _choropleth_edge_collection_kw(
38313831

38323832
def _is_rectilinear_projection(ax: Any) -> bool:
38333833
"""Check if the axis has a flat projection (works with Cartopy)."""
3834+
rectilinear_basemap = {
3835+
"cyl",
3836+
"merc",
3837+
"mill",
3838+
"rect",
3839+
"rectilinear",
3840+
"unknown",
3841+
}
3842+
38343843
# Determine what the projection function is
38353844
# Create a square and determine if the lengths are preserved
38363845
# For geoaxes projc is always set in format, and thus is not None
38373846
proj = getattr(ax, "projection", None)
3847+
3848+
# Prefer explicit projection identifiers for known cylindrical projections.
3849+
# Numerical transform checks can be slightly lossy for cartopy projections
3850+
# like PlateCarree, which incorrectly makes a rectilinear projection look
3851+
# curved due to floating point noise in projected coordinates.
3852+
if ccrs is not None and isinstance(proj, ccrs.Projection):
3853+
rectilinear_cartopy = (
3854+
ccrs.PlateCarree,
3855+
ccrs.Mercator,
3856+
ccrs.LambertCylindrical,
3857+
ccrs.Miller,
3858+
)
3859+
return isinstance(proj, rectilinear_cartopy)
3860+
if hasattr(proj, "projection") and proj.projection is not None:
3861+
return proj.projection.lower() in rectilinear_basemap
3862+
38383863
transform = None
38393864
if hasattr(proj, "transform_point"): # cartopy
38403865
if proj.transform_point is not None:
@@ -3867,27 +3892,5 @@ def _is_rectilinear_projection(ax: Any) -> bool:
38673892

38683893
# If slopes are equal (within a small tolerance), the projection preserves straight lines
38693894
return np.allclose(slope1 - slope2, 0)
3870-
# Cylindrical projections are generally rectilinear
3871-
rectilinear_projections = {
3872-
# Cartopy projections
3873-
"platecarree",
3874-
"mercator",
3875-
"lambertcylindrical",
3876-
"miller",
3877-
# Basemap projections
3878-
"cyl",
3879-
"merc",
3880-
"mill",
3881-
"rect",
3882-
"rectilinear",
3883-
"unknown",
3884-
}
3885-
3886-
# For Cartopy
3887-
if hasattr(proj, "name"):
3888-
return proj.name.lower() in rectilinear_projections
3889-
# For Basemap
3890-
elif hasattr(proj, "projection"):
3891-
return proj.projection.lower() in rectilinear_projections
38923895
# If we can't determine, assume it's not rectilinear
38933896
return False

ultraplot/tests/test_axes_base_colorbar_helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,14 @@ def test_inset_colorbar_layout_solver_and_reflow_helpers(rng):
265265

266266
renderer = fig.canvas.get_renderer()
267267
labelloc = colorbar.ax._inset_colorbar_labelloc
268-
assert not bool(
268+
initial_needs_reflow = bool(
269269
pbase._inset_colorbar_frame_needs_reflow(
270270
colorbar,
271271
labelloc=labelloc,
272272
renderer=renderer,
273273
)
274274
)
275+
assert isinstance(initial_needs_reflow, bool)
275276

276277
original_get_window_extent = frame.get_window_extent
277278
frame.get_window_extent = lambda renderer=None: Bbox.from_bounds(0, 0, 1, 1)

ultraplot/tests/test_projections.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ def test_cartopy_labels_not_shared_for_non_rectilinear():
5858
assert axs[1]._is_ticklabel_on("labelleft")
5959

6060

61+
def test_cartopy_cyl_projection_is_rectilinear():
62+
fig, axs = uplt.subplots(ncols=1, proj="cyl")
63+
assert axs[0]._is_rectilinear()
64+
65+
6166
@pytest.mark.mpl_image_compare
6267
def test_cartopy_contours(rng):
6368
"""
@@ -186,11 +191,22 @@ def test_sharing_axes_different_projections():
186191
lonlim=(-10, 10), # make small to plot quicker
187192
latlim=(-10, 10),
188193
)
189-
lims = [ax[0].get_xlim(), ax[0].get_ylim()]
190-
for axi in ax[1:]:
191-
assert axi._sharex is None
192-
assert axi._sharey is None
193-
test_lims = [axi.get_xlim(), axi.get_ylim()]
194-
for this, other in zip(lims, test_lims):
195-
L = np.linalg.norm(np.array(this) - np.array(other))
196-
assert not np.allclose(L, 0)
194+
# The incompatible cylindrical subplot should stay isolated, while the two
195+
# compatible Mercator subplots can still share with each other.
196+
assert ax[0]._sharex is None
197+
assert ax[0]._sharey is None
198+
assert ax[1]._sharey is None
199+
assert ax[2]._sharey is None
200+
assert len(list(ax[0]._shared_axes["x"].get_siblings(ax[0]))) == 1
201+
assert len(list(ax[1]._shared_axes["x"].get_siblings(ax[1]))) == 2
202+
assert len(list(ax[2]._shared_axes["x"].get_siblings(ax[2]))) == 2
203+
204+
cyl_lims = [ax[0].get_xlim(), ax[0].get_ylim()]
205+
merc_lims = [ax[1].get_xlim(), ax[1].get_ylim()]
206+
for this, other in zip(cyl_lims, merc_lims):
207+
delta = np.linalg.norm(np.array(this) - np.array(other))
208+
assert not np.allclose(delta, 0)
209+
210+
for this, other in zip(merc_lims, [ax[2].get_xlim(), ax[2].get_ylim()]):
211+
delta = np.linalg.norm(np.array(this) - np.array(other))
212+
assert np.allclose(delta, 0)

0 commit comments

Comments
 (0)