From 71e71ffe84e2cec6121ad809753ae0a8154b033c Mon Sep 17 00:00:00 2001 From: Mann Batra Date: Fri, 17 Jul 2026 19:43:04 +0530 Subject: [PATCH 1/4] Make Axis.get_tick_space account for tick label rotation --- lib/matplotlib/axis.py | 31 ++++++++++++++++++++++++++----- lib/matplotlib/tests/test_axes.py | 20 ++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 349e728ba8ff..c50d8cf186fb 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1831,6 +1831,19 @@ def _get_tick_label_size(self, axis_name): mpl.rcParams[f'{axis_name}tick.labelsize']) return mtext.FontProperties(size=size).get_size_in_points() + def _get_tick_label_rotation(self): + """ + Return the rotation (in degrees) applied to tick labels on this + Axis, as set via `.Axis.set_tick_params(rotation=...)`. + """ + rotation = self._major_tick_kw.get('labelrotation', 0) + if isinstance(rotation, (tuple, list)): + rotation = rotation[1] + try: + return float(rotation) + except (TypeError, ValueError): + return 0.0 + def _copy_tick_props(self, src, dest): """Copy the properties from *src* tick to *dest* tick.""" if src is None or dest is None: @@ -2828,9 +2841,13 @@ def get_tick_space(self): ends = mtransforms.Bbox.unit().transformed( self.axes.transAxes - self.get_figure(root=False).dpi_scale_trans) length = ends.width * 72 - # There is a heuristic here that the aspect ratio of tick text - # is no more than 3:1 - size = self._get_tick_label_size('x') * 3 + # There is a heuristic here that the aspect ratio of horizontal + # tick text is no more than 3:1 (width:height); as the label is + # rotated towards vertical, its footprint along the x-axis shrinks + # towards a single line-height (aspect ratio ~1:1). + rotation = np.deg2rad(self._get_tick_label_rotation()) + aspect = abs(3 * np.cos(rotation)) + abs(np.sin(rotation)) + size = self._get_tick_label_size('x') * aspect if size > 0: return int(np.floor(length / size)) else: @@ -3058,8 +3075,12 @@ def get_tick_space(self): ends = mtransforms.Bbox.unit().transformed( self.axes.transAxes - self.get_figure(root=False).dpi_scale_trans) length = ends.height * 72 - # Having a spacing of at least 2 just looks good. - size = self._get_tick_label_size('y') * 2 + # Having a spacing of at least 2 just looks good, for horizontal + # labels; as the label rotates towards vertical, its footprint + # along the y-axis grows towards a horizontal-text-like 3:1 ratio. + rotation = np.deg2rad(self._get_tick_label_rotation()) + aspect = abs(2 * np.cos(rotation)) + abs(3 * np.sin(rotation)) + size = self._get_tick_label_size('y') * aspect if size > 0: return int(np.floor(length / size)) else: diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 3189076c7655..295b96559c92 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4501,6 +4501,26 @@ def test_tick_space_size_0(): plt.savefig(b, dpi=80, format='raw') +def test_tick_space_rotation(): + # Rotating x tick labels towards vertical should let more x ticks fit + # (their horizontal footprint shrinks), while rotating y tick labels + # towards vertical should let fewer y ticks fit (their vertical + # footprint grows). See issue #32005. + fig, ax = plt.subplots(figsize=(1.8, 1.8), layout="constrained") + fig.canvas.draw() + xspace_horizontal = ax.xaxis.get_tick_space() + yspace_horizontal = ax.yaxis.get_tick_space() + + fig2, ax2 = plt.subplots(figsize=(1.8, 1.8), layout="constrained") + ax2.tick_params(rotation=90) + fig2.canvas.draw() + xspace_vertical = ax2.xaxis.get_tick_space() + yspace_vertical = ax2.yaxis.get_tick_space() + + assert xspace_vertical > xspace_horizontal + assert yspace_vertical < yspace_horizontal + + @image_comparison(['errorbar_basic.png', 'errorbar_mixed.png', 'errorbar_basic.png'], style='mpl20') def test_errorbar(): From d31474f5b337125b055dc0a2e65314230cf535d3 Mon Sep 17 00:00:00 2001 From: Mann Batra Date: Tue, 21 Jul 2026 10:05:10 +0530 Subject: [PATCH 2/4] minimising the computing cost --- lib/matplotlib/tests/test_axes.py | 21 --------------------- lib/matplotlib/tests/test_axis.py | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 295b96559c92..c3d5d9176fa3 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4500,27 +4500,6 @@ def test_tick_space_size_0(): b = io.BytesIO() plt.savefig(b, dpi=80, format='raw') - -def test_tick_space_rotation(): - # Rotating x tick labels towards vertical should let more x ticks fit - # (their horizontal footprint shrinks), while rotating y tick labels - # towards vertical should let fewer y ticks fit (their vertical - # footprint grows). See issue #32005. - fig, ax = plt.subplots(figsize=(1.8, 1.8), layout="constrained") - fig.canvas.draw() - xspace_horizontal = ax.xaxis.get_tick_space() - yspace_horizontal = ax.yaxis.get_tick_space() - - fig2, ax2 = plt.subplots(figsize=(1.8, 1.8), layout="constrained") - ax2.tick_params(rotation=90) - fig2.canvas.draw() - xspace_vertical = ax2.xaxis.get_tick_space() - yspace_vertical = ax2.yaxis.get_tick_space() - - assert xspace_vertical > xspace_horizontal - assert yspace_vertical < yspace_horizontal - - @image_comparison(['errorbar_basic.png', 'errorbar_mixed.png', 'errorbar_basic.png'], style='mpl20') def test_errorbar(): diff --git a/lib/matplotlib/tests/test_axis.py b/lib/matplotlib/tests/test_axis.py index 3776b6f054b9..c99872f26782 100644 --- a/lib/matplotlib/tests/test_axis.py +++ b/lib/matplotlib/tests/test_axis.py @@ -128,3 +128,23 @@ def test_set_ticks_emits_lim_changed(): ax2.callbacks.connect("ylim_changed", called_polar.append) ax2.set_rticks([1, 2, 3]) assert called_polar + + + def test_tick_space_rotation(): + # Rotating x tick labels towards vertical should let more x ticks fit + # (their horizontal footprint shrinks), while rotating y tick labels + # towards vertical should let fewer y ticks fit (their vertical + # footprint grows). See issue #32005. + fig, ax = plt.subplots(figsize=(1.8, 1.8), layout="constrained") + fig.canvas.draw() + xspace_horizontal = ax.xaxis.get_tick_space() + yspace_horizontal = ax.yaxis.get_tick_space() + + fig2, ax2 = plt.subplots(figsize=(1.8, 1.8), layout="constrained") + ax2.tick_params(rotation=90) + fig2.canvas.draw() + xspace_vertical = ax2.xaxis.get_tick_space() + yspace_vertical = ax2.yaxis.get_tick_space() + + assert xspace_vertical > xspace_horizontal + assert yspace_vertical < yspace_horizontal From c4557ef80903785e442d37f232a82006e569a363 Mon Sep 17 00:00:00 2001 From: Mann Batra Date: Tue, 21 Jul 2026 10:25:04 +0530 Subject: [PATCH 3/4] Simplify _get_tick_label_rotation per review feedback --- lib/matplotlib/axis.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index c50d8cf186fb..66e42da16cc0 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1836,13 +1836,7 @@ def _get_tick_label_rotation(self): Return the rotation (in degrees) applied to tick labels on this Axis, as set via `.Axis.set_tick_params(rotation=...)`. """ - rotation = self._major_tick_kw.get('labelrotation', 0) - if isinstance(rotation, (tuple, list)): - rotation = rotation[1] - try: - return float(rotation) - except (TypeError, ValueError): - return 0.0 + return float(self._major_tick_kw.get('labelrotation', 0)) def _copy_tick_props(self, src, dest): """Copy the properties from *src* tick to *dest* tick.""" From 25ed72baa9a7cb698c452a975f1aff96171b62e2 Mon Sep 17 00:00:00 2001 From: Mann Batra Date: Tue, 21 Jul 2026 10:27:52 +0530 Subject: [PATCH 4/4] ruff error fixed --- lib/matplotlib/tests/test_axes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index c3d5d9176fa3..3189076c7655 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4500,6 +4500,7 @@ def test_tick_space_size_0(): b = io.BytesIO() plt.savefig(b, dpi=80, format='raw') + @image_comparison(['errorbar_basic.png', 'errorbar_mixed.png', 'errorbar_basic.png'], style='mpl20') def test_errorbar():