diff --git a/doc/api/next_api_changes/2018-11-04-JMK.rst b/doc/api/next_api_changes/2018-11-04-JMK.rst new file mode 100644 index 000000000000..878273d7fda9 --- /dev/null +++ b/doc/api/next_api_changes/2018-11-04-JMK.rst @@ -0,0 +1,18 @@ +get_window_extents changes: +--------------------------- + +`.matplotlib.axes.Axes.get_window_extent` used to return a bounding box +that was slightly larger than the axes, presumably to take into account +the ticks that may be on a spine. However, it was not scaling the tick sizes +according to the dpi of the canvas, and it did not check if the ticks were +visible, or on the spine. + +Now `.matplotlib.axes.Axes.get_window_extent` just returns the axes extent +with no padding for ticks. + +This affects `.matplotlib.axes.Axes.get_tightbbox` in cases where there are +outward ticks with no tick labels, and it also removes the (small) pad around +axes in that case. + +`.spines.get_window_extent` now takes into account ticks that are on the +spine. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 441fc822ce09..48b7a499aed6 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -576,18 +576,21 @@ def __setstate__(self, state): def get_window_extent(self, *args, **kwargs): """ - get the axes bounding box in display space; *args* and - *kwargs* are empty - """ - bbox = self.bbox - x_pad = 0 - if self.axison and self.xaxis.get_visible(): - x_pad = self.xaxis.get_tick_padding() - y_pad = 0 - if self.axison and self.yaxis.get_visible(): - y_pad = self.yaxis.get_tick_padding() - return mtransforms.Bbox([[bbox.x0 - x_pad, bbox.y0 - y_pad], - [bbox.x1 + x_pad, bbox.y1 + y_pad]]) + Return the axes bounding box in display space; *args* and *kwargs* + are empty. + + This bounding box does not include the spines, ticks, ticklables, + or other labels. For a bounding box including these elements use + `~matplotlib.axes.Axes.get_tightbbox`. + + See Also + -------- + matplotlib.axes.Axes.get_tightbbox + matplotlib.axis.Axis.get_tightbbox + matplotlib.spines.get_window_extent + + """ + return self.bbox def _init_axis(self): "move this out of __init__ because non-separable axes don't use it" @@ -4286,6 +4289,13 @@ def get_tightbbox(self, renderer, call_axes_locator=True, ------- bbox : `.BboxBase` bounding box in figure pixel coordinates. + + See Also + -------- + matplotlib.axis.Axes.get_window_extent + matplotlib.axis.Axis.get_tightbbox + matplotlib.spines.get_window_extent + """ bb = [] @@ -4300,13 +4310,14 @@ def get_tightbbox(self, renderer, call_axes_locator=True, else: self.apply_aspect() - bb_xaxis = self.xaxis.get_tightbbox(renderer) - if bb_xaxis: - bb.append(bb_xaxis) + if self.axison: + bb_xaxis = self.xaxis.get_tightbbox(renderer) + if bb_xaxis: + bb.append(bb_xaxis) - bb_yaxis = self.yaxis.get_tightbbox(renderer) - if bb_yaxis: - bb.append(bb_yaxis) + bb_yaxis = self.yaxis.get_tightbbox(renderer) + if bb_yaxis: + bb.append(bb_yaxis) self._update_title_position(renderer) bb.append(self.get_window_extent(renderer)) diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index d5d3880b88d5..042bf84d4bfc 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -145,10 +145,60 @@ def get_patch_transform(self): return super().get_patch_transform() def get_window_extent(self, renderer=None): + """ + Return the window extent of the spines in display space, including + padding for ticks (but not their labels) + + See Also + -------- + matplotlib.axes.Axes.get_tightbbox + matplotlib.axes.Axes.get_window_extent + + """ # make sure the location is updated so that transforms etc are # correct: self._adjust_location() - return super().get_window_extent(renderer=renderer) + bb = super().get_window_extent(renderer=renderer) + bboxes = [bb] + tickstocheck = [self.axis.majorTicks[0]] + if len(self.axis.minorTicks) > 1: + # only pad for minor ticks if there are more than one + # of them. There is always one... + tickstocheck.append(self.axis.minorTicks[1]) + for tick in tickstocheck: + bb0 = bb.frozen() + tickl = tick._size + tickdir = tick._tickdir + if tickdir == 'out': + padout = 1 + padin = 0 + elif tickdir == 'in': + padout = 0 + padin = 1 + else: + padout = 0.5 + padin = 0.5 + padout = padout * tickl / 72 * self.figure.dpi + padin = padin * tickl / 72 * self.figure.dpi + + if tick.tick1line.get_visible(): + if self.spine_type in ['left']: + bb0.x0 = bb0.x0 - padout + bb0.x1 = bb0.x1 + padin + elif self.spine_type in ['bottom']: + bb0.y0 = bb0.y0 - padout + bb0.y1 = bb0.y1 + padin + + if tick.tick2line.get_visible(): + if self.spine_type in ['right']: + bb0.x1 = bb0.x1 + padout + bb0.x0 = bb0.x0 - padin + elif self.spine_type in ['top']: + bb0.y1 = bb0.y1 + padout + bb0.y0 = bb0.y0 - padout + bboxes.append(bb0) + + return mtransforms.Bbox.union(bboxes) def get_path(self): return self._path diff --git a/lib/matplotlib/tests/baseline_images/test_axes/secondary_xy.png b/lib/matplotlib/tests/baseline_images/test_axes/secondary_xy.png index 2e91f63326ee..392038e08dd5 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/secondary_xy.png and b/lib/matplotlib/tests/baseline_images/test_axes/secondary_xy.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_bbox_inches.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_bbox_inches.pdf index 1223b97a27fb..24e16a2873c6 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_bbox_inches.pdf and b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_bbox_inches.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png index 6d2351922688..c5361afd402e 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_table/table_auto_column.png b/lib/matplotlib/tests/baseline_images/test_table/table_auto_column.png index 9e0472b3c011..6dc11a67464e 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_table/table_auto_column.png and b/lib/matplotlib/tests/baseline_images/test_table/table_auto_column.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_table/table_cell_manipulation.png b/lib/matplotlib/tests/baseline_images/test_table/table_cell_manipulation.png index bd805406edd3..778e57802982 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_table/table_cell_manipulation.png and b/lib/matplotlib/tests/baseline_images/test_table/table_cell_manipulation.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_table/table_labels.png b/lib/matplotlib/tests/baseline_images/test_table/table_labels.png index 419abd0ca050..f59acb1a4a33 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_table/table_labels.png and b/lib/matplotlib/tests/baseline_images/test_table/table_labels.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_table/table_zorder.png b/lib/matplotlib/tests/baseline_images/test_table/table_zorder.png index 58f70066e7ab..025ea5e0c4d2 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_table/table_zorder.png and b/lib/matplotlib/tests/baseline_images/test_table/table_zorder.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_text/large_subscript_title.png b/lib/matplotlib/tests/baseline_images/test_text/large_subscript_title.png index 978f1291455b..36c0eeb5f88e 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_text/large_subscript_title.png and b/lib/matplotlib/tests/baseline_images/test_text/large_subscript_title.png differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 25dc08dc5420..e0378419acb0 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -23,8 +23,10 @@ import matplotlib.markers as mmarkers import matplotlib.patches as mpatches import matplotlib.colors as mcolors +import matplotlib.transforms as mtransforms from numpy.testing import ( assert_allclose, assert_array_equal, assert_array_almost_equal) +from matplotlib import rc_context from matplotlib.cbook import ( IgnoredKeywordWarning, MatplotlibDeprecationWarning) @@ -5891,9 +5893,9 @@ def test_tick_padding_tightbbox(): plt.rcParams["xtick.direction"] = "out" plt.rcParams["ytick.direction"] = "out" fig, ax = plt.subplots() - bb = ax.get_window_extent(fig.canvas.get_renderer()) + bb = ax.get_tightbbox(fig.canvas.get_renderer()) ax.axis('off') - bb2 = ax.get_window_extent(fig.canvas.get_renderer()) + bb2 = ax.get_tightbbox(fig.canvas.get_renderer()) assert bb.x0 < bb2.x0 assert bb.y0 < bb2.y0 @@ -6063,3 +6065,198 @@ def invert(x): fig.canvas.draw() fig.set_size_inches((7, 4)) assert_allclose(ax.get_position().extents, [0.125, 0.1, 0.9, 0.9]) + + +def color_boxes(fig, axs): + """ + Helper for the tests below that test the extents of various axes elements + """ + fig.canvas.draw() + + renderer = fig.canvas.get_renderer() + bbaxis = [] + for nn, axx in enumerate([axs.xaxis, axs.yaxis]): + bb = axx.get_tightbbox(renderer) + if bb: + axisr = plt.Rectangle((bb.x0, bb.y0), width=bb.width, + height=bb.height, linewidth=0.7, edgecolor='y', + facecolor="none", transform=None, zorder=3) + fig.add_artist(axisr) + bbaxis += [bb] + + bbspines = [] + for nn, a in enumerate(['bottom', 'top', 'left', 'right']): + bb = axs.spines[a].get_window_extent(renderer) + spiner = plt.Rectangle((bb.x0, bb.y0), width=bb.width, + height=bb.height, linewidth=0.7, + edgecolor="green", facecolor="none", + transform=None, zorder=3) + fig.add_artist(spiner) + bbspines += [bb] + + bb = axs.get_window_extent() + rect2 = plt.Rectangle((bb.x0, bb.y0), width=bb.width, height=bb.height, + linewidth=1.5, edgecolor="magenta", + facecolor="none", transform=None, zorder=2) + fig.add_artist(rect2) + bbax = bb + + bb2 = axs.get_tightbbox(renderer) + rect2 = plt.Rectangle((bb2.x0, bb2.y0), width=bb2.width, + height=bb2.height, linewidth=3, edgecolor="red", + facecolor="none", transform=None, zorder=1) + fig.add_artist(rect2) + bbtb = bb2 + return bbaxis, bbspines, bbax, bbtb + + +def test_normal_axes(): + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + fig.canvas.draw() + plt.close(fig) + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + + # test the axis bboxes + target = [ + [123.375, 75.88888888888886, 983.25, 33.0], + [85.51388888888889, 99.99999999999997, 53.375, 993.0] + ] + for nn, b in enumerate(bbaxis): + targetbb = mtransforms.Bbox.from_bounds(*target[nn]) + assert_array_almost_equal(b.bounds, targetbb.bounds, decimal=2) + + target = [ + [150.0, 119.999, 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] + ] + for nn, b in enumerate(bbspines): + targetbb = mtransforms.Bbox.from_bounds(*target[nn]) + assert_array_almost_equal(b.bounds, targetbb.bounds, decimal=2) + + target = [150.0, 119.99999999999997, 930.0, 960.0] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_array_almost_equal(bbax.bounds, targetbb.bounds, decimal=2) + + target = [85.5138, 75.88888, 1021.11, 1017.11] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_array_almost_equal(bbtb.bounds, targetbb.bounds, decimal=2) + + # test that get_position roundtrips to get_window_extent + axbb = ax.get_position().transformed(fig.transFigure).bounds + assert_array_almost_equal(axbb, ax.get_window_extent().bounds, decimal=2) + + +def test_nodecorator(): + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + fig.canvas.draw() + ax.set(xticklabels=[], yticklabels=[]) + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + + # test the axis bboxes + target = [ + None, + None + ] + for nn, b in enumerate(bbaxis): + assert b is None + + target = [ + [150.0, 119.999, 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] + ] + for nn, b in enumerate(bbspines): + targetbb = mtransforms.Bbox.from_bounds(*target[nn]) + assert_allclose(b.bounds, targetbb.bounds, atol=1e-2) + + target = [150.0, 119.99999999999997, 930.0, 960.0] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_allclose(bbax.bounds, targetbb.bounds, atol=1e-2) + + target = [150., 120., 930., 960.] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_allclose(bbtb.bounds, targetbb.bounds, atol=1e-2) + + +def test_displaced_spine(): + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + ax.set(xticklabels=[], yticklabels=[]) + ax.spines['bottom'].set_position(('axes', -0.1)) + fig.canvas.draw() + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + + target = [ + [150., 24., 930., 11.111111], + [150.0, 1080.0, 930.0, 0.0], + [150.0, 119.9999, 11.111, 960.0], + [1068.8888, 119.9999, 11.111, 960.0] + ] + for nn, b in enumerate(bbspines): + targetbb = mtransforms.Bbox.from_bounds(*target[nn]) + + target = [150.0, 119.99999999999997, 930.0, 960.0] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_allclose(bbax.bounds, targetbb.bounds, atol=1e-2) + + target = [150., 24., 930., 1056.] + targetbb = mtransforms.Bbox.from_bounds(*target) + assert_allclose(bbtb.bounds, targetbb.bounds, atol=1e-2) + + +def test_tickdirs(): + """ + Switch the tickdirs and make sure the bboxes switch with them + """ + targets = [[[150.0, 120.0, 930.0, 11.1111], + [150.0, 120.0, 11.111, 960.0]], + [[150.0, 108.8889, 930.0, 11.111111111111114], + [138.889, 120, 11.111, 960.0]], + [[150.0, 114.44444444444441, 930.0, 11.111111111111114], + [144.44444444444446, 119.999, 11.111, 960.0]]] + for dnum, dirs in enumerate(['in', 'out', 'inout']): + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + ax.tick_params(direction=dirs) + fig.canvas.draw() + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + for nn, num in enumerate([0, 2]): + targetbb = mtransforms.Bbox.from_bounds(*targets[dnum][nn]) + assert_allclose(bbspines[num].bounds, targetbb.bounds, + atol=1e-2) + + +def test_minor_accountedfor(): + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + fig.canvas.draw() + ax.tick_params(which='both', direction='out') + + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + targets = [[150.0, 108.88888888888886, 930.0, 11.111111111111114], + [138.8889, 119.9999, 11.1111, 960.0]] + for n in range(2): + targetbb = mtransforms.Bbox.from_bounds(*targets[n]) + assert_allclose(bbspines[n * 2].bounds, targetbb.bounds, + atol=1e-2) + + fig, ax = plt.subplots(dpi=200, figsize=(6, 6)) + fig.canvas.draw() + ax.tick_params(which='both', direction='out') + ax.minorticks_on() + ax.tick_params(axis='both', which='minor', length=30) + fig.canvas.draw() + bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) + targets = [[150.0, 36.66666666666663, 930.0, 83.33333333333334], + [66.6667, 120.0, 83.3333, 960.0]] + + for n in range(2): + targetbb = mtransforms.Bbox.from_bounds(*targets[n]) + assert_allclose(bbspines[n * 2].bounds, targetbb.bounds, + atol=1e-2) diff --git a/lib/matplotlib/tests/test_tightlayout.py b/lib/matplotlib/tests/test_tightlayout.py index bae8951f8cd3..0f9f8737a305 100644 --- a/lib/matplotlib/tests/test_tightlayout.py +++ b/lib/matplotlib/tests/test_tightlayout.py @@ -184,14 +184,15 @@ def test_outward_ticks(): ax.xaxis.set_tick_params(tickdir='out', length=32, width=3) ax.yaxis.set_tick_params(tickdir='out', length=32, width=3) plt.tight_layout() - assert_array_equal( - np.round([ax.get_position().get_points() for ax in fig.axes], 3), - # These values were obtained after visual checking that they correspond - # to a tight layouting that did take the ticks into account. - [[[0.091, 0.590], [0.437, 0.903]], - [[0.581, 0.590], [0.927, 0.903]], - [[0.091, 0.140], [0.437, 0.454]], - [[0.581, 0.140], [0.927, 0.454]]]) + # These values were obtained after visual checking that they correspond + # to a tight layouting that did take the ticks into account. + ans = [[[0.091, 0.607], [0.433, 0.933]], + [[0.579, 0.607], [0.922, 0.933]], + [[0.091, 0.140], [0.433, 0.466]], + [[0.579, 0.140], [0.922, 0.466]]] + for nn, ax in enumerate(fig.axes): + assert_array_equal(np.round(ax.get_position().get_points(), 3), + ans[nn]) def add_offsetboxes(ax, size=10, margin=.1, color='black'): diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/image_grid.png b/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/image_grid.png index c4a2aae5607e..a696787a0248 100644 Binary files a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/image_grid.png and b/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/image_grid.png differ