diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index ab156217b859..c6e0e7dc05b8 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -99,44 +99,42 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad, """ - ''' Steps: - - 1. get a list of unique gridspecs in this figure. Each gridspec will be - constrained separately. - 2. Check for gaps in the gridspecs. i.e. if not every axes slot in the - gridspec has been filled. If empty, add a ghost axis that is made so - that it cannot be seen (though visible=True). This is needed to make - a blank spot in the layout. - 3. Compare the tight_bbox of each axes to its `position`, and assume that - the difference is the space needed by the elements around the edge of - the axes (decorations) like the title, ticklabels, x-labels, etc. This - can include legends who overspill the axes boundaries. - 4. Constrain gridspec elements to line up: - a) if colnum0 != colnumC, the two subplotspecs are stacked next to - each other, with the appropriate order. - b) if colnum0 == colnumC, line up the left or right side of the - _poslayoutbox (depending if it is the min or max num that is equal). - c) do the same for rows... - 5. The above doesn't constrain relative sizes of the _poslayoutboxes at - all, and indeed zero-size is a solution that the solver often finds more - convenient than expanding the sizes. Right now the solution is to compare - subplotspec sizes (i.e. drowsC and drows0) and constrain the larger - _poslayoutbox to be larger than the ratio of the sizes. i.e. if drows0 > - drowsC, then ax._poslayoutbox > axc._poslayoutbox * drowsC / drows0. This - works fine *if* the decorations are similar between the axes. If the - larger subplotspec has much larger axes decorations, then the constraint - above is incorrect. - - We need the greater than in the above, in general, rather than an equals - sign. Consider the case of the left column having 2 rows, and the right - column having 1 row. We want the top and bottom of the _poslayoutboxes to - line up. So that means if there are decorations on the left column axes - they will be smaller than half as large as the right hand axis. - - This can break down if the decoration size for the right hand axis (the - margins) is very large. There must be a math way to check for this case. - - ''' + # Steps: + # + # 1. get a list of unique gridspecs in this figure. Each gridspec will be + # constrained separately. + # 2. Check for gaps in the gridspecs. i.e. if not every axes slot in the + # gridspec has been filled. If empty, add a ghost axis that is made so + # that it cannot be seen (though visible=True). This is needed to make + # a blank spot in the layout. + # 3. Compare the tight_bbox of each axes to its `position`, and assume that + # the difference is the space needed by the elements around the edge of + # the axes (decorations) like the title, ticklabels, x-labels, etc. This + # can include legends who overspill the axes boundaries. + # 4. Constrain gridspec elements to line up: + # a) if colnum0 != colnumC, the two subplotspecs are stacked next to + # each other, with the appropriate order. + # b) if colnum0 == colnumC, line up the left or right side of the + # _poslayoutbox (depending if it is the min or max num that is equal). + # c) do the same for rows... + # 5. The above doesn't constrain relative sizes of the _poslayoutboxes + # at all, and indeed zero-size is a solution that the solver often finds + # more convenient than expanding the sizes. Right now the solution is to + # compare subplotspec sizes (i.e. drowsC and drows0) and constrain the + # larger _poslayoutbox to be larger than the ratio of the sizes. i.e. if + # drows0 > drowsC, then ax._poslayoutbox > axc._poslayoutbox*drowsC/drows0. + # This works fine *if* the decorations are similar between the axes. + # If the larger subplotspec has much larger axes decorations, then the + # constraint above is incorrect. + # + # We need the greater than in the above, in general, rather than an equals + # sign. Consider the case of the left column having 2 rows, and the right + # column having 1 row. We want the top and bottom of the _poslayoutboxes + # to line up. So that means if there are decorations on the left column + # axes they will be smaller than half as large as the right hand axis. + # + # This can break down if the decoration size for the right hand axis (the + # margins) is very large. There must be a math way to check for this case. invTransFig = fig.transFigure.inverted().transform_bbox diff --git a/lib/matplotlib/_layoutbox.py b/lib/matplotlib/_layoutbox.py index 4f31f7bdb95e..bca14d74e947 100644 --- a/lib/matplotlib/_layoutbox.py +++ b/lib/matplotlib/_layoutbox.py @@ -623,29 +623,21 @@ def match_margins(boxes, levels=1): def seq_id(): - ''' - Generate a short sequential id for layoutbox objects... - ''' - - global _layoutboxobjnum - - return ('%06d' % (next(_layoutboxobjnum))) + """Generate a short sequential id for layoutbox objects.""" + return '%06d' % next(_layoutboxobjnum) def print_children(lb): - ''' - Print the children of the layoutbox - ''' + """Print the children of the layoutbox.""" print(lb) for child in lb.children: print_children(child) def nonetree(lb): - ''' - Make all elements in this tree none... This signals not to do any more - layout. - ''' + """ + Make all elements in this tree none, signalling not to do any more layout. + """ if lb is not None: if lb.parent is None: # Clear the solver. Hopefully this garbage collects. diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 9a670b8231d9..d51b47acc588 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -132,18 +132,14 @@ def clabel(self, levels=None, *, A list of `.Text` instances for the labels. """ - """ - NOTES on how this all works: - - clabel basically takes the input arguments and uses them to - add a list of "label specific" attributes to the ContourSet - object. These attributes are all of the form label* and names - should be fairly self explanatory. - - Once these attributes are set, clabel passes control to the - labels method (case of automatic label placement) or - `BlockingContourLabeler` (case of manual label placement). - """ + # clabel basically takes the input arguments and uses them to + # add a list of "label specific" attributes to the ContourSet + # object. These attributes are all of the form label* and names + # should be fairly self explanatory. + # + # Once these attributes are set, clabel passes control to the + # labels method (case of automatic label placement) or + # `BlockingContourLabeler` (case of manual label placement). self.labelFmt = fmt self._use_clabeltext = use_clabeltext @@ -1496,12 +1492,8 @@ def _contour_args(self, args, kwargs): def _check_xyz(self, args, kwargs): """ - For functions like contour, check that the dimensions - of the input arrays match; if x and y are 1D, convert - them to 2D using meshgrid. - - Possible change: I think we should make and use an ArgumentError - Exception class (here and elsewhere). + Check that the shapes of the input arrays match; if x and y are 1D, + convert them to 2D using meshgrid. """ x, y = args[:2] kwargs = self.ax._process_unit_info(xdata=x, ydata=y, kwargs=kwargs) @@ -1513,39 +1505,34 @@ def _check_xyz(self, args, kwargs): z = ma.asarray(args[2], dtype=np.float64) if z.ndim != 2: - raise TypeError("Input z must be a 2D array.") - elif z.shape[0] < 2 or z.shape[1] < 2: - raise TypeError("Input z must be at least a 2x2 array.") - else: - Ny, Nx = z.shape + raise TypeError(f"Input z must be 2D, not {z.ndim}D") + if z.shape[0] < 2 or z.shape[1] < 2: + raise TypeError(f"Input z must be at least a (2, 2) array, but " + f"has shape {z.shape}") + Ny, Nx = z.shape if x.ndim != y.ndim: - raise TypeError("Number of dimensions of x and y should match.") - + raise TypeError(f"Dimensionalities of x ({x.ndim}) and y " + f"({y.ndim}) do not match") if x.ndim == 1: - nx, = x.shape ny, = y.shape - if nx != Nx: - raise TypeError("Length of x must be number of columns in z.") - + raise TypeError(f"Length of x ({nx}) must match number of " + f"columns in z ({Nx})") if ny != Ny: - raise TypeError("Length of y must be number of rows in z.") - + raise TypeError(f"Length of y ({ny}) must match number of " + f"rows in z ({Ny})") x, y = np.meshgrid(x, y) - elif x.ndim == 2: - if x.shape != z.shape: - raise TypeError("Shape of x does not match that of z: found " - "{0} instead of {1}.".format(x.shape, z.shape)) - + raise TypeError( + f"Shapes of x {x.shape} and z {z.shape} do not match") if y.shape != z.shape: - raise TypeError("Shape of y does not match that of z: found " - "{0} instead of {1}.".format(y.shape, z.shape)) + raise TypeError( + f"Shapes of y {y.shape} and z {z.shape} do not match") else: - raise TypeError("Inputs x and y must be 1D or 2D.") + raise TypeError(f"Inputs x and y must be 1D or 2D, not {x.ndim}D") return x, y, z @@ -1563,9 +1550,10 @@ def _initialize_x_y(self, z): will give the minimum and maximum values of x and y. """ if z.ndim != 2: - raise TypeError("Input must be a 2D array.") + raise TypeError(f"Input z must be 2D, not {z.ndim}D") elif z.shape[0] < 2 or z.shape[1] < 2: - raise TypeError("Input z must be at least a 2x2 array.") + raise TypeError(f"Input z must be at least a (2, 2) array, but " + f"has shape {z.shape}") else: Ny, Nx = z.shape if self.origin is None: # Not for image-matching. diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index ee754bd37379..231b6594e2e3 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -74,25 +74,18 @@ def artist_picker(self, legend, evt): return self.legend.contains(evt) def finalize_offset(self): - loc_in_canvas = self.get_loc_in_canvas() - - if self._update == "loc": - self._update_loc(loc_in_canvas) - elif self._update == "bbox": - self._update_bbox_to_anchor(loc_in_canvas) - else: - raise RuntimeError("update parameter '%s' is not supported." % - self.update) + update_method = cbook._check_getitem( + {"loc": self._update_loc, "bbox": self._bbox_to_anchor}, + update=self._update) + update_method(self.get_loc_in_canvas()) def _update_loc(self, loc_in_canvas): bbox = self.legend.get_bbox_to_anchor() - # if bbox has zero width or height, the transformation is - # ill-defined. Fall back to the defaul bbox_to_anchor. + # ill-defined. Fall back to the default bbox_to_anchor. if bbox.width == 0 or bbox.height == 0: self.legend.set_bbox_to_anchor(None) bbox = self.legend.get_bbox_to_anchor() - _bbox_transform = BboxTransformFrom(bbox) self.legend._loc = tuple(_bbox_transform.transform(loc_in_canvas)) diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index f80d08e61d21..bb0ba2103219 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -5,7 +5,6 @@ import weakref import numpy as np -from pathlib import Path import pytest import matplotlib as mpl diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index ee95561986dc..84368cba8a8d 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -505,7 +505,7 @@ def test_colorbar_scale_reset(): assert cbar.ax.yaxis.get_scale() == 'linear' -def test_colorbar_get_ticks(): +def test_colorbar_get_ticks_2(): with rc_context({'_internal.classic_mode': False}): fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index 0bf4827e64d5..9c0979c33332 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -1,4 +1,5 @@ import datetime +import re import numpy as np from matplotlib.testing.decorators import image_comparison @@ -29,93 +30,32 @@ def test_contour_shape_2d_valid(): ax.contour(xg, yg, z) -def test_contour_shape_mismatch_1(): - - x = np.arange(9) - y = np.arange(9) - z = np.random.random((9, 10)) - - fig, ax = plt.subplots() - - with pytest.raises(TypeError) as excinfo: - ax.contour(x, y, z) - excinfo.match(r'Length of x must be number of columns in z.') - - -def test_contour_shape_mismatch_2(): - - x = np.arange(10) - y = np.arange(10) - z = np.random.random((9, 10)) - +@pytest.mark.parametrize("args, message", [ + ((np.arange(9), np.arange(9), np.empty((9, 10))), + 'Length of x (9) must match number of columns in z (10)'), + ((np.arange(10), np.arange(10), np.empty((9, 10))), + 'Length of y (10) must match number of rows in z (9)'), + ((np.empty((10, 10)), np.arange(10), np.empty((9, 10))), + 'Dimensionalities of x (2) and y (1) do not match'), + ((np.arange(10), np.empty((10, 10)), np.empty((9, 10))), + 'Dimensionalities of x (1) and y (2) do not match'), + ((np.empty((9, 9)), np.empty((9, 10)), np.empty((9, 10))), + 'Shapes of x (9, 9) and z (9, 10) do not match'), + ((np.empty((9, 10)), np.empty((9, 9)), np.empty((9, 10))), + 'Shapes of y (9, 9) and z (9, 10) do not match'), + ((np.empty((3, 3, 3)), np.empty((3, 3, 3)), np.empty((9, 10))), + 'Inputs x and y must be 1D or 2D, not 3D'), + ((np.empty((3, 3, 3)), np.empty((3, 3, 3)), np.empty((3, 3, 3))), + 'Input z must be 2D, not 3D'), + (([[0]],), # github issue 8197 + 'Input z must be at least a (2, 2) array, but has shape (1, 1)'), + (([0], [0], [[0]]), + 'Input z must be at least a (2, 2) array, but has shape (1, 1)'), +]) +def test_contour_shape_error(args, message): fig, ax = plt.subplots() - - with pytest.raises(TypeError) as excinfo: - ax.contour(x, y, z) - excinfo.match(r'Length of y must be number of rows in z.') - - -def test_contour_shape_mismatch_3(): - - x = np.arange(10) - y = np.arange(10) - xg, yg = np.meshgrid(x, y) - z = np.random.random((9, 10)) - - fig, ax = plt.subplots() - - with pytest.raises(TypeError) as excinfo: - ax.contour(xg, y, z) - excinfo.match(r'Number of dimensions of x and y should match.') - - with pytest.raises(TypeError) as excinfo: - ax.contour(x, yg, z) - excinfo.match(r'Number of dimensions of x and y should match.') - - -def test_contour_shape_mismatch_4(): - - g = np.random.random((9, 10)) - b = np.random.random((9, 9)) - z = np.random.random((9, 10)) - - fig, ax = plt.subplots() - - with pytest.raises(TypeError) as excinfo: - ax.contour(b, g, z) - excinfo.match(r'Shape of x does not match that of z: found \(9L?, 9L?\) ' + - r'instead of \(9L?, 10L?\)') - - with pytest.raises(TypeError) as excinfo: - ax.contour(g, b, z) - excinfo.match(r'Shape of y does not match that of z: found \(9L?, 9L?\) ' + - r'instead of \(9L?, 10L?\)') - - -def test_contour_shape_invalid_1(): - - x = np.random.random((3, 3, 3)) - y = np.random.random((3, 3, 3)) - z = np.random.random((9, 10)) - - fig, ax = plt.subplots() - - with pytest.raises(TypeError) as excinfo: - ax.contour(x, y, z) - excinfo.match(r'Inputs x and y must be 1D or 2D.') - - -def test_contour_shape_invalid_2(): - - x = np.random.random((3, 3, 3)) - y = np.random.random((3, 3, 3)) - z = np.random.random((3, 3, 3)) - - fig, ax = plt.subplots() - - with pytest.raises(TypeError) as excinfo: - ax.contour(x, y, z) - excinfo.match(r'Input z must be a 2D array.') + with pytest.raises(TypeError, match=re.escape(message)): + ax.contour(*args) def test_contour_empty_levels(): @@ -311,47 +251,31 @@ def test_contourf_symmetric_locator(): assert_array_almost_equal(cs.levels, np.linspace(-12, 12, 5)) -def test_contour_1x1_array(): - # github issue 8197 - with pytest.raises(TypeError) as excinfo: - plt.contour([[0]]) - excinfo.match(r'Input z must be at least a 2x2 array.') - - with pytest.raises(TypeError) as excinfo: - plt.contour([0], [0], [[0]]) - excinfo.match(r'Input z must be at least a 2x2 array.') - - -def test_internal_cpp_api(): - # Following github issue 8197. +@pytest.mark.parametrize("args, cls, message", [ + ((), TypeError, + 'function takes exactly 6 arguments (0 given)'), + ((1, 2, 3, 4, 5, 6), ValueError, + 'Expected 2-dimensional array, got 0'), + (([[0]], [[0]], [[]], None, True, 0), ValueError, + 'x, y and z must all be 2D arrays with the same dimensions'), + (([[0]], [[0]], [[0]], None, True, 0), ValueError, + 'x, y and z must all be at least 2x2 arrays'), + ((*[np.arange(4).reshape((2, 2))] * 3, [[0]], True, 0), ValueError, + 'If mask is set it must be a 2D array with the same dimensions as x.'), +]) +def test_internal_cpp_api(args, cls, message): # Github issue 8197. import matplotlib._contour as _contour + with pytest.raises(cls, match=re.escape(message)): + _contour.QuadContourGenerator(*args) - with pytest.raises(TypeError) as excinfo: - qcg = _contour.QuadContourGenerator() - excinfo.match(r'function takes exactly 6 arguments \(0 given\)') - - with pytest.raises(ValueError) as excinfo: - qcg = _contour.QuadContourGenerator(1, 2, 3, 4, 5, 6) - excinfo.match(r'Expected 2-dimensional array, got 0') - - with pytest.raises(ValueError) as excinfo: - qcg = _contour.QuadContourGenerator([[0]], [[0]], [[]], None, True, 0) - excinfo.match(r'x, y and z must all be 2D arrays with the same dimensions') - - with pytest.raises(ValueError) as excinfo: - qcg = _contour.QuadContourGenerator([[0]], [[0]], [[0]], None, True, 0) - excinfo.match(r'x, y and z must all be at least 2x2 arrays') +def test_internal_cpp_api_2(): + import matplotlib._contour as _contour arr = [[0, 1], [2, 3]] - with pytest.raises(ValueError) as excinfo: - qcg = _contour.QuadContourGenerator(arr, arr, arr, [[0]], True, 0) - excinfo.match(r'If mask is set it must be a 2D array with the same ' + - r'dimensions as x.') - qcg = _contour.QuadContourGenerator(arr, arr, arr, None, True, 0) - with pytest.raises(ValueError) as excinfo: + with pytest.raises( + ValueError, match=r'filled contour levels must be increasing'): qcg.create_filled_contour(1, 0) - excinfo.match(r'filled contour levels must be increasing') def test_circular_contour_warning(): diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 690cf86b22bd..7f370c9051d1 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -2,7 +2,6 @@ from copy import copy import io import os -import sys from pathlib import Path import platform import sys diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 644f662c361f..9051ff5bc8dd 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -769,7 +769,7 @@ def _rendercursor(self): widthtext = self.text[:self.cursor_index] no_text = False - if(widthtext == "" or widthtext == " " or widthtext == " "): + if widthtext in ["", " ", " "]: no_text = widthtext == "" widthtext = "," @@ -1028,11 +1028,9 @@ def __init__(self, ax, labels, active=0, activecolor='blue'): axcolor = ax.get_facecolor() # scale the radius of the circle with the spacing between each one - circle_radius = (dy / 2) - 0.01 - - # defaul to hard-coded value if the radius becomes too large - if(circle_radius > 0.05): - circle_radius = 0.05 + circle_radius = dy / 2 - 0.01 + # default to hard-coded value if the radius becomes too large + circle_radius = min(circle_radius, 0.05) self.labels = [] self.circles = [] diff --git a/lib/mpl_toolkits/tests/test_axes_grid1.py b/lib/mpl_toolkits/tests/test_axes_grid1.py index 6537ae087e1c..f01eef6e6f7d 100644 --- a/lib/mpl_toolkits/tests/test_axes_grid1.py +++ b/lib/mpl_toolkits/tests/test_axes_grid1.py @@ -1,30 +1,24 @@ +from itertools import product +import platform + import matplotlib import matplotlib.pyplot as plt +from matplotlib import cbook from matplotlib.cbook import MatplotlibDeprecationWarning +from matplotlib.backend_bases import MouseEvent +from matplotlib.colors import LogNorm +from matplotlib.transforms import Bbox, TransformedBbox from matplotlib.testing.decorators import ( image_comparison, remove_ticks_and_titles) -from mpl_toolkits.axes_grid1 import host_subplot -from mpl_toolkits.axes_grid1 import make_axes_locatable -from mpl_toolkits.axes_grid1 import AxesGrid -from mpl_toolkits.axes_grid1 import ImageGrid -from mpl_toolkits.axes_grid1.inset_locator import ( - zoomed_inset_axes, - mark_inset, - inset_axes, - BboxConnectorPatch -) +from mpl_toolkits.axes_grid1 import ( + host_subplot, make_axes_locatable, AxesGrid, ImageGrid) from mpl_toolkits.axes_grid1.anchored_artists import ( - AnchoredSizeBar, - AnchoredDirectionArrows) - -from matplotlib.backend_bases import MouseEvent -from matplotlib.colors import LogNorm -from matplotlib.transforms import Bbox, TransformedBbox -from itertools import product + AnchoredSizeBar, AnchoredDirectionArrows) +from mpl_toolkits.axes_grid1.inset_locator import ( + zoomed_inset_axes, mark_inset, inset_axes, BboxConnectorPatch) import pytest -import platform import numpy as np from numpy.testing import assert_array_equal, assert_array_almost_equal @@ -125,18 +119,12 @@ def test_axesgrid_colorbar_log_smoketest(legacy_colorbar): @image_comparison(['inset_locator.png'], style='default', remove_text=True) def test_inset_locator(): - def get_demo_image(): - from matplotlib.cbook import get_sample_data - import numpy as np - f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False) - z = np.load(f) - # z is a numpy array of 15x15 - return z, (-3, 4, -4, 3) - fig, ax = plt.subplots(figsize=[5, 4]) # prepare the demo image - Z, extent = get_demo_image() + # Z is a 15x15 array + Z = np.load(cbook.get_sample_data("axes_grid/bivariate_normal.npy")) + extent = (-3, 4, -4, 3) Z2 = np.zeros([150, 150], dtype="d") ny, nx = Z.shape Z2[30:30 + ny, 30:30 + nx] = Z @@ -173,18 +161,12 @@ def get_demo_image(): @image_comparison(['inset_axes.png'], style='default', remove_text=True) def test_inset_axes(): - def get_demo_image(): - from matplotlib.cbook import get_sample_data - import numpy as np - f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False) - z = np.load(f) - # z is a numpy array of 15x15 - return z, (-3, 4, -4, 3) - fig, ax = plt.subplots(figsize=[5, 4]) # prepare the demo image - Z, extent = get_demo_image() + # Z is a 15x15 array + Z = np.load(cbook.get_sample_data("axes_grid/bivariate_normal.npy")) + extent = (-3, 4, -4, 3) Z2 = np.zeros([150, 150], dtype="d") ny, nx = Z.shape Z2[30:30 + ny, 30:30 + nx] = Z