Skip to content

Commit 20f4603

Browse files
authored
Merge branch 'main' into fix/ci-nodeid-json-safe
2 parents dbe8317 + a782cbb commit 20f4603

4 files changed

Lines changed: 73 additions & 0 deletions

File tree

ultraplot/figure.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,7 @@ def __init__(
869869

870870
@override
871871
def draw(self, renderer):
872+
self._snap_axes_to_pixel_grid(renderer)
872873
# implement the tick sharing here
873874
# should be shareable --> either all cartesian or all geographic
874875
# but no mixing (panels can be mixed)
@@ -880,6 +881,53 @@ def draw(self, renderer):
880881
self._apply_share_label_groups()
881882
super().draw(renderer)
882883

884+
def _snap_axes_to_pixel_grid(self, renderer) -> None:
885+
"""
886+
Snap visible axes bounds to the renderer pixel grid.
887+
"""
888+
if not rc.find("subplots.pixelsnap", context=True):
889+
return
890+
891+
width = getattr(renderer, "width", None)
892+
height = getattr(renderer, "height", None)
893+
if not width or not height:
894+
return
895+
896+
width = float(width)
897+
height = float(height)
898+
if width <= 0 or height <= 0:
899+
return
900+
901+
invw = 1.0 / width
902+
invh = 1.0 / height
903+
minw = invw
904+
minh = invh
905+
906+
for ax in self._iter_axes(hidden=False, children=False, panels=True):
907+
bbox = ax.get_position(original=False)
908+
old = np.array([bbox.x0, bbox.y0, bbox.x1, bbox.y1], dtype=float)
909+
new = np.array(
910+
[
911+
round(old[0] * width) * invw,
912+
round(old[1] * height) * invh,
913+
round(old[2] * width) * invw,
914+
round(old[3] * height) * invh,
915+
],
916+
dtype=float,
917+
)
918+
919+
if new[2] <= new[0]:
920+
new[2] = new[0] + minw
921+
if new[3] <= new[1]:
922+
new[3] = new[1] + minh
923+
924+
if np.allclose(new, old, rtol=0.0, atol=1e-12):
925+
continue
926+
ax.set_position(
927+
[new[0], new[1], new[2] - new[0], new[3] - new[1]],
928+
which="both",
929+
)
930+
883931
def _share_ticklabels(self, *, axis: str) -> None:
884932
"""
885933
Tick label sharing is determined at the figure level. While

ultraplot/internals/rcsetup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2099,6 +2099,11 @@ def copy(self):
20992099
_validate_bool,
21002100
"Whether to auto-adjust the subplot spaces and figure margins.",
21012101
),
2102+
"subplots.pixelsnap": (
2103+
False,
2104+
_validate_bool,
2105+
"Whether to snap subplot bounds to the renderer pixel grid during draw.",
2106+
),
21022107
# Super title settings
21032108
"suptitle.color": (BLACK, _validate_color, "Figure title color."),
21042109
"suptitle.pad": (

ultraplot/tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ def close_figures_after_test(request):
3737
# Start from a clean rc state.
3838
uplt.rc._context.clear()
3939
uplt.rc.reset(local=False, user=False, default=True)
40+
if request.node.get_closest_marker("mpl_image_compare"):
41+
uplt.rc["subplots.pixelsnap"] = True
4042

4143
yield
4244
uplt.close("all")

ultraplot/tests/test_figure.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,21 @@ def test_suptitle_kw_position_reverted(ha, expectation):
297297
assert np.isclose(x, expectation, atol=0.1), f"Expected x={expectation}, got {x=}"
298298

299299
uplt.close("all")
300+
301+
302+
def test_subplots_pixelsnap_aligns_axes_bounds():
303+
with uplt.rc.context({"subplots.pixelsnap": True}):
304+
fig, axs = uplt.subplots(ncols=2, nrows=2)
305+
axs.plot([0, 1], [0, 1])
306+
fig.canvas.draw()
307+
308+
renderer = fig._get_renderer()
309+
width = float(renderer.width)
310+
height = float(renderer.height)
311+
312+
for ax in axs:
313+
bbox = ax.get_position(original=False)
314+
coords = np.array(
315+
[bbox.x0 * width, bbox.y0 * height, bbox.x1 * width, bbox.y1 * height]
316+
)
317+
assert np.allclose(coords, np.round(coords), atol=1e-8)

0 commit comments

Comments
 (0)