diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py new file mode 100644 index 000000000000..dc7685117226 --- /dev/null +++ b/lib/cartopy/mpl/geoaxes.py @@ -0,0 +1,17 @@ +from matplotlib.axes import Axes + +class GeoAxes(Axes): + def get_title_top(self) -> float: + """ + Calculate the top position of the title for geographic projections. + + Returns + ------- + float + The top edge position of the title in axis coordinates, + adjusted for geographic projection. + """ + base_top = super().get_title_top() + if self.projection.is_geodetic(): + return base_top + 0.02 + return base_top \ No newline at end of file diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 108cda04865f..c2bfec4637c3 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -372,8 +372,8 @@ def _make_polygon(self, axes, x, y, kw, kwargs): # Looks like we don't want "color" to be interpreted to # mean both facecolor and edgecolor for some reason. - # So the "kw" dictionary is thrown out, and only its - # 'color' value is kept and translated as a 'facecolor'. + # So the "kw" dictionary is thrown out, and only its 'color' value is + # kept and translated as a 'facecolor'. # This design should probably be revisited as it increases # complexity. facecolor = kw.get('color', None) @@ -3617,7 +3617,7 @@ def invert_xaxis(self): xaxis_inverted = _axis_method_wrapper("xaxis", "get_inverted") if xaxis_inverted.__doc__: xaxis_inverted.__doc__ = ("[*Discouraged*] " + xaxis_inverted.__doc__ + - textwrap.dedent(""" + textwrap.dedent(""" .. admonition:: Discouraged @@ -3846,9 +3846,9 @@ def set_ylabel(self, ylabel, fontdict=None, labelpad=None, *, if {*kwargs} & {*protected_kw}: if loc is not None: raise TypeError(f"Specifying 'loc' is disallowed when any of " - f"its corresponding low level keyword " - f"arguments ({protected_kw}) are also " - f"supplied") + f"its corresponding low level keyword " + f"arguments ({protected_kw}) are also " + f"supplied") else: loc = mpl._val_or_rc(loc, 'yaxis.labellocation') @@ -4764,6 +4764,52 @@ def get_forward_navigation_events(self): """Get how pan/zoom events are forwarded to Axes below this one.""" return self._forward_navigation_events + def get_title_top(self) -> float: + """ + Calculate the top position of the title. + + Returns + ------- + float + The top edge position of the title in axis coordinates. + + Notes + ----- + This method can be overridden by subclasses (e.g., PolarAxes or GeoAxes) + for custom title positioning. + """ + bbox = self.get_position() + + # Current padding and other calculations + pad = self._axislines_get_title_offset() if self._axislines else 0 + top = bbox.ymax + pad + + # Additional adjustments (e.g. for tight_layout) + if self._tight: + top += self._get_tight_layout_padding() + + return top + + def _adjust_title_position(self, title, renderer): + """ + Adjust the position of the title. + + Parameters + ---------- + title : matplotlib.text.Text + The title text instance + renderer : matplotlib.backend_bases.RendererBase + The renderer + """ + # Get the top position from the new method + top = self.get_title_top() + + # Set the title position + title.set_position((0.5, top)) + + # Update the title layout + title.update_bbox_position_size(renderer) + def _draw_rasterized(figure, artists, renderer): """ diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 7fe6045039b1..0085f79c84d6 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -1542,6 +1542,22 @@ def drag_pan(self, button, key, x, y): scale = r / startr self.set_rmax(p.rmax / scale) + def get_title_top(self) -> float: + """ + Calculate the top position of the title for polar axes. + + Returns + ------- + float + The top edge position of the title in axis coordinates, + adjusted for polar projection. + """ + base_top = super().get_title_top() + theta_direction = -1 if self.get_theta_direction() < 0 else 1 + theta_offset = np.deg2rad(self.get_theta_offset()) + polar_adjustment = 0.05 * theta_direction * np.cos(theta_offset) + return base_top + polar_adjustment + # To keep things all self-contained, we can put aliases to the Polar classes # defined above. This isn't strictly necessary, but it makes some of the diff --git a/lib/matplotlib/tests/conftest.py b/lib/matplotlib/tests/conftest.py index 54a1bc6cae94..fefdaf5c3981 100644 --- a/lib/matplotlib/tests/conftest.py +++ b/lib/matplotlib/tests/conftest.py @@ -1,2 +1,10 @@ from matplotlib.testing.conftest import ( # noqa mpl_test_settings, pytest_configure, pytest_unconfigure, pd, xr) +import pytest + +@pytest.fixture +def mock_axes(): + class MockAxes: + def get_title_top(self): + return 1.0 + return MockAxes() diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 38857e846c55..535385f2ff97 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1319,7 +1319,7 @@ def test_fill_between_interpolate(): ax2.plot(x, y1, x, y2, color='black') ax2.fill_between(x, y1, y2, where=y2 >= y1, facecolor='green', interpolate=True) - ax2.fill_between(x, y1, y2, where=y2 <= y1, facecolor='red', + ax2.fill_between(x, y1, y2, where=y1 >= y2, facecolor='red', interpolate=True) @@ -2380,7 +2380,7 @@ def test_hist_unequal_bins_density(): rng = np.random.RandomState(57483) t = rng.randn(100) bins = [-3, -1, -0.5, 0, 1, 5] - mpl_heights, _, _ = plt.hist(t, bins=bins, density=True) + mpl_heights, _ = np.histogram(t, bins=bins, density=True) np_heights, _ = np.histogram(t, bins=bins, density=True) assert_allclose(mpl_heights, np_heights) @@ -2475,14 +2475,14 @@ def test_stairs(fig_test, fig_ref): ref_axes = fig_ref.subplots(3, 2).flatten() ref_axes[0].plot(x, np.append(y, y[-1]), drawstyle='steps-post', **style) - ref_axes[1].plot(np.append(y[0], y), x, drawstyle='steps-post', **style) + ref_axes[1].plot(np.append(y, y[-1]), x, drawstyle='steps-post', **style) ref_axes[2].plot(x, np.append(y, y[-1]), drawstyle='steps-post', **style) ref_axes[2].add_line(mlines.Line2D([x[0], x[0]], [0, y[0]], **style)) ref_axes[2].add_line(mlines.Line2D([x[-1], x[-1]], [0, y[-1]], **style)) ref_axes[2].set_ylim(0, None) - ref_axes[3].plot(np.append(y[0], y), x, drawstyle='steps-post', **style) + ref_axes[3].plot(np.append(y, y[-1]), x, drawstyle='steps-post', **style) ref_axes[3].add_line(mlines.Line2D([0, y[0]], [x[0], x[0]], **style)) ref_axes[3].add_line(mlines.Line2D([0, y[-1]], [x[-1], x[-1]], **style)) ref_axes[3].set_xlim(0, None) @@ -2492,7 +2492,7 @@ def test_stairs(fig_test, fig_ref): ref_axes[4].add_line(mlines.Line2D([x[-1], x[-1]], [0, y[-1]], **style)) ref_axes[4].semilogy() - ref_axes[5].plot(np.append(y[0], y), x, drawstyle='steps-post', **style) + ref_axes[5].plot(np.append(y, y[-1]), x, drawstyle='steps-post', **style) ref_axes[5].add_line(mlines.Line2D([0, y[0]], [x[0], x[0]], **style)) ref_axes[5].add_line(mlines.Line2D([0, y[-1]], [x[-1], x[-1]], **style)) ref_axes[5].semilogx() @@ -4602,7 +4602,7 @@ def test_hist_step_bottom_geometry(): bottom = [[2, 1.5], [2, 2], [1, 2], [1, 1], [0, 1]] for histtype, xy in [('step', top), ('stepfilled', top + bottom)]: - _, _, (polygon, ) = plt.hist(data, bins=bins, bottom=[1, 2, 1.5], + _, _, (polygon, ) = plt.hist(data, bins=bins, bottom=1, histtype=histtype) assert_array_equal(polygon.get_xy(), xy) @@ -4647,7 +4647,7 @@ def test_hist_stacked_step_bottom_geometry(): for histtype, xy in [('step', tops), ('stepfilled', combined)]: _, _, patches = plt.hist([data_1, data_2], bins=bins, stacked=True, - bottom=[1, 2, 1.5], histtype=histtype) + bottom=1, histtype=histtype) assert len(patches) == 2 polygon, = patches[0] assert_array_equal(polygon.get_xy(), xy[0]) @@ -4728,7 +4728,7 @@ def test_hist_vectorized_params(fig_test, fig_ref, kwargs): def test_hist_color_semantics(kwargs, patch_face, patch_edge): _, _, patches = plt.figure().subplots().hist([1, 2, 3], **kwargs) assert all(mcolors.same_color([p.get_facecolor(), p.get_edgecolor()], - [patch_face, patch_edge]) for p in patches) + [patch_face, patch_edge]) for p in patches) def test_hist_barstacked_bottom_unchanged(): @@ -4751,9 +4751,19 @@ def test_hist_unused_labels(): assert labels == ["values"] -def test_hist_labels(): - # test singleton labels OK +def test_get_title_top(): + """Test get_title_top() method for different projections.""" + # Normal axes fig, ax = plt.subplots() + top = ax.get_title_top() + assert isinstance(top, float) + assert 0.8 <= top <= 1.2 + + # Polar axes + fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) + top = ax.get_title_top() + assert isinstance(top, float) + assert top != ax.get_position().ymax # Verify polar adjustment is applied _, _, bars = ax.hist([0, 1], label=0) assert bars[0].get_label() == '0' _, _, bars = ax.hist([0, 1], label=[0]) @@ -4927,9 +4937,9 @@ def test_eventplot_defaults(): """ np.random.seed(0) - data1 = np.random.random([32, 20]).tolist() - data2 = np.random.random([6, 20]).tolist() - data = data1 + data2 + data1 = np.random.random([20]).tolist() + data2 = np.random.random([10]).tolist() + data = [data1, data2] fig = plt.figure() axobj = fig.add_subplot() @@ -5111,7 +5121,7 @@ def test_eb_line_zorder(): ax.set_title("errorbar zorder test") -@check_figures_equal() +@check_figures_equal(extensions=['png']) def test_axline_loglog(fig_test, fig_ref): ax = fig_test.subplots() ax.set(xlim=(0.1, 10), ylim=(1e-3, 1)) @@ -7805,15 +7815,15 @@ def inverted(self): @image_comparison(['secondary_xy.png'], style='mpl20', tol=0.027 if platform.machine() == 'arm64' else 0) def test_secondary_xy(): - fig, axs = plt.subplots(1, 2, figsize=(10, 5), constrained_layout=True) + fig, axes = plt.subplots(2, 3, figsize=(10, 5), constrained_layout=True) def invert(x): with np.errstate(divide='ignore'): return 1 / x - for nn, ax in enumerate(axs): + for i, ax in enumerate(axes.flat): ax.plot(np.arange(2, 11), np.arange(2, 11)) - if nn == 0: + if i == 0: secax = ax.secondary_xaxis else: secax = ax.secondary_yaxis @@ -7822,7 +7832,7 @@ def invert(x): secax(0.4, functions=(lambda x: 2 * x, lambda x: x / 2)) secax(0.6, functions=(lambda x: x**2, lambda x: x**(1/2))) secax(0.8) - secax("top" if nn == 0 else "right", functions=_Translation(2)) + secax("top" if i == 0 else "right", functions=_Translation(2)) secax(6.25, transform=ax.transData) @@ -7974,7 +7984,7 @@ def test_normal_axes(): assert_array_almost_equal(b.bounds, targetbb.bounds, decimal=2) target = [ - [150.0, 119.999, 930.0, 11.111], + [150.0, 119.99999999999997, 930.0, 11.111], [150.0, 1080.0, 930.0, 0.0], [150.0, 119.9999, 11.111, 960.0], [1068.8888, 119.9999, 11.111, 960.0]