diff --git a/.flake8 b/.flake8 index 450b743e87b1..87ece8254ddc 100644 --- a/.flake8 +++ b/.flake8 @@ -94,4 +94,4 @@ per-file-ignores = galleries/examples/user_interfaces/pylab_with_gtk3_sgskip.py: E402 galleries/examples/user_interfaces/pylab_with_gtk4_sgskip.py: E402 galleries/examples/userdemo/pgf_preamble_sgskip.py: E402 -force-check = True +force-check = True \ No newline at end of file diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index b776979b67ed..82da1890660f 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -61,6 +61,14 @@ jobs: with: fetch-depth: 0 + # Something changed somewhere that prevents the downloaded-at-build-time + # licenses from being included in built wheels, so pre-download them so + # that they exist before the build and are included. + - name: Pre-download bundled licenses + run: > + curl -Lo LICENSE/LICENSE_QHULL + https://github.com/qhull/qhull/raw/2020.2/COPYING.txt + - name: Build wheels for CPython 3.11 uses: pypa/cibuildwheel@v2.12.0 env: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000000..b73e517f698a --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,25 @@ +name: 'Label inactive PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + if: github.repository == 'matplotlib/matplotlib' + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v7 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + operations-per-run: 50 + stale-pr-message: 'Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it.' + stale-pr-label: 'inactive' + days-before-pr-stale: 60 + days-before-pr-close: -1 + stale-issue-message: 'This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!' + stale-issue-label: 'inactive' + days-before-issue-stale: 365 + days-before-issue-close: 30 + ascending: true + exempt-issue-labels: "keep" + exempt-pr-labels: "keep,status: orphaned PR" diff --git a/doc/api/next_api_changes/deprecations/25352-GL.rst b/doc/api/next_api_changes/deprecations/25352-GL.rst new file mode 100644 index 000000000000..e7edd57a6453 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/25352-GL.rst @@ -0,0 +1,4 @@ +``Grouper.clean()`` +~~~~~~~~~~~~~~~~~~~ + +with no replacement. The Grouper class now cleans itself up automatically. diff --git a/doc/users/explain/interactive.rst b/doc/users/explain/interactive.rst index 83d1ee2835a6..942b682fb04c 100644 --- a/doc/users/explain/interactive.rst +++ b/doc/users/explain/interactive.rst @@ -269,7 +269,7 @@ Jupyter Notebooks / JupyterLab cells. To get interactive figures in the 'classic' notebook or Jupyter lab, -use the `ipympl `__ backend +use the `ipympl `__ backend (must be installed separately) which uses the **ipywidget** framework. If ``ipympl`` is installed use the magic: diff --git a/galleries/examples/specialty_plots/ishikawa.py b/galleries/examples/specialty_plots/ishikawa.py new file mode 100644 index 000000000000..97ce113590d5 --- /dev/null +++ b/galleries/examples/specialty_plots/ishikawa.py @@ -0,0 +1,223 @@ +""" +================= +Ishikawa Diagrams +================= + +Ishikawa Diagrams, fishbone diagrams, herringbone diagrams, or cause-and-effect +diagrams are useful for visualizing the effect to many cause relationships +Source: https://en.wikipedia.org/wiki/Ishikawa_diagram + +""" +from typing import List + +import matplotlib.pyplot as plt +import numpy as np + +import matplotlib + + +def drawspines(data: dict, ax: plt.Axes, parentspine: None, + primary_spine_angle: float = 60.0, + secondary_spine_angle: float = 0, + primary_spines_rel_xpos: List[float] = [np.nan], + spine_color: str = "black", reclevel: int = 1, + max_recursion_level: int = 4): + """ + Draw an Ishikawa spine with recursion + + Parameters + ---------- + data : dict + dictionary structure containing problem, causes... + ax : plt.Axes + Matplotlib current axes + parentspine : None + Parent spine object + primary_spine_angle : float, optional + First spine angle using during recursion. The default is 60.0. + secondary_spine_angle : float, optional + Secondary spine angle using during recursion. The default is 0. + primary_spines_rel_xpos : List[float], optional + Relative X-position of primary spines, a list of float is accepted. + The default is [np.nan]. + spine_color : str, optional + Spine color. The default is "black". + reclevel : int, optional + Current recursion level. The default is 1. + max_recursion_level : int, optional + Maximum recursion level set. The default is 4. + + Raises + ------ + AttributeError + Maximum recursion level reached or passed a bad parentspine object format. + + Returns + ------- + None. + + """ + # stop recursion if maximum level reached + if reclevel > max_recursion_level: + raise AttributeError('Max Recursion Level Reached') + + if isinstance(data, dict): + # switch to correct angle depending on recursion level + if reclevel % 2 != 0: + alpha = primary_spine_angle + else: + alpha = secondary_spine_angle + + if isinstance(parentspine, matplotlib.lines.Line2D): + # calculate parent data + ([xpb, xpe], [ypb, ype]) = parentspine.get_data() + elif isinstance(parentspine, matplotlib.text.Annotation): + xpb, ypb = parentspine.xy + xpe, ype = parentspine._x, parentspine._y # parentspine._arrow_relpos + else: + raise AttributeError('Wrong Spine Graphical Element') + + plen = np.hypot(xpe - xpb, ype - ypb) + palpha = np.round(np.degrees(np.arctan2(ype - ypb, xpe - xpb))) + + # calculate spine spacing + # calculate couple pairs, at least 1 pair to start at middle branch + pairs = np.ceil(len(data) / 2) + 1 + spacing = plen / pairs + + spine_count = 0 + s = spacing + # draw spine + for problem, cause in data.items(): + # calculate arrow position in the graph + # fix primary spines spacing + if reclevel == 1: + if len(primary_spines_rel_xpos) == len(data): + x_end = primary_spines_rel_xpos[spine_count] + else: + x_end = xpe - (s / 1.5) * np.cos(np.radians(palpha)) + y_end = ype - s * np.sin(np.radians(palpha)) + + x_start = x_end - s * np.cos(np.radians(alpha)) + y_start = y_end - s * np.sin(np.radians(alpha)) + else: + x_end = xpe - s * np.cos(np.radians(palpha)) + y_end = ype - s * np.sin(np.radians(palpha)) + + x_start = x_end - s * np.cos(np.radians(alpha)) + y_start = y_end - s * np.sin(np.radians(alpha)) + + # draw arrow arc + if reclevel == 1: + props = dict(boxstyle='round', facecolor='lightsteelblue', alpha=1.0) + spine = ax.annotate(problem.upper(), xy=(x_end, y_end), + xytext=(x_start, y_start), + arrowprops=dict(arrowstyle="->", + facecolor=spine_color), + bbox=props, weight='bold') + else: + props = dict(boxstyle='round', facecolor='lavender', alpha=1.0) + spine = ax.annotate(problem, xy=(x_end, y_end), + xytext=(x_start, y_start), + arrowprops=dict(arrowstyle="->", + facecolor=spine_color), + bbox=props) + # Call recursion to draw subspines + drawspines(data=cause, ax=ax, parentspine=spine, + primary_spine_angle=primary_spine_angle, + secondary_spine_angle=secondary_spine_angle, + spine_color=spine_color, reclevel=reclevel + 1, + max_recursion_level=max_recursion_level) + # no primary_spines_rel_xpos is needed to be passed on recursion + # next spine settings - same level + alpha *= -1 + spine_count += 1 + if spine_count % 2 == 0: + s = s + spacing + return None + + +def ishikawaplot(data: dict, ax: plt.Axes, left_margin: float = 0.05, + right_margin: float = 0.05, + primary_spine_angle: float = 60.0, + secondary_spine_angle: float = 0.0, + primary_spines_rel_xpos: List[float] = [np.nan], + pd_width: int = 0.1, spine_color: str = "black") -> None: + """ + + Parameters + ---------- + data : dict + Plot data structure. + ax : matplotlib.pyplot.Axes + Axes in which to drow the plot. + left_margin : float, optional + Left spacing from frame border. The default is 0.05. + right_margin : float, optional + Right spacing from frame border. The default is 0.05. + primary_spine_angle : float, optional + First spine angle using during recursion. The default is 60.0. + secondary_spine_angle : float, optional + Secondary spine angle using during recursion. The default is 0.0. + primary_spines_rel_xpos : list, optional + Relative X-position of primary spines, a list of float is accepted. + The default is [np.nan]. + pd_width : int, optional + Problem description box relative width. The default is 0.1. + spine_color : str, optional + Spine color. The default is "black". + + Returns + ------- + None + + """ + # format axis + ax.set_xlim(0, 1.0) + ax.set_ylim(0, 1.0) + ax.axis('off') + + # draw main spine + main_spine = ax.axhline(y=0.5, xmin=left_margin, + xmax=1 - right_margin - pd_width, + color=spine_color) + + # draw fish head + props = dict(boxstyle='round', facecolor='wheat', alpha=1.0) + # add problem tag + ax.text((1 - right_margin - pd_width), 0.5, str.upper(list(data.keys())[0]), + fontsize=12, weight='bold', + verticalalignment='center', horizontalalignment='left', bbox=props) + + # draw fish tail + x = (0.0, 0.0, left_margin) + y = (0.5 - left_margin, 0.5 + left_margin, 0.5) + ax.fill(x, y, color=spine_color) + + # draw spines with recursion + drawspines(data=data[list(data.keys())[0]], ax=ax, parentspine=main_spine, + primary_spine_angle=primary_spine_angle, + secondary_spine_angle=secondary_spine_angle, + primary_spines_rel_xpos=primary_spines_rel_xpos, + spine_color=spine_color) + + return None + + +# USER DATA +data = {'problem': {'machine': {'cause1': ''}, + 'process': {'cause2': {'subcause1': '', 'subcause2': ''} + }, + 'man': {'cause3': {'subcause3': '', 'subcause4': ''} + }, + 'design': {'cause4': {'subcause5': '', 'subcause6': ''} + } + + } + } + +# Ishikawa plot generation +fig, ax = plt.subplots(figsize=(12, 6), layout='constrained') + +# try also without opt primary_spines rel_xpos +ishikawaplot(data, ax, primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) diff --git a/galleries/examples/specialty_plots/swotplot.py b/galleries/examples/specialty_plots/swotplot.py new file mode 100644 index 000000000000..4251c3938232 --- /dev/null +++ b/galleries/examples/specialty_plots/swotplot.py @@ -0,0 +1,175 @@ +""" +============ +SWOT Diagram +============ + +SWOT analysis (or SWOT matrix) is a strategic planning and strategic management +technique used to help a person or organization identify Strengths, Weaknesses, +Opportunities, and Threats related to business competition or project planning. +It is sometimes called situational assessment or situational analysis. +Additional acronyms using the same components include TOWS and WOTS-UP. +Source: https://en.wikipedia.org/wiki/SWOT_analysis + +""" +from typing import Tuple + +import matplotlib.pyplot as plt + + +def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, + q_x: Tuple[float, float, float, float], + q_y: Tuple[float, float, float, float], + q_title_pos: Tuple[float, float] = (0.0, 0.0), + q_title_props: dict = dict(boxstyle='round', + facecolor="white", + alpha=1.0), + q_point_props: dict = dict(boxstyle='round', + facecolor="white", + alpha=1.0) + ) -> None: + """ + Draw a quadrant of the SWOT plot + + Parameters + ---------- + q_data : dict + Data structure passed to populate the quadrant. + Format 'key':Tuple(float,float) + ax : plt.Axes + Matplotlib current axes + q_color : str + Quadrant color. + q_x : Tuple[float, float, float, float] + Quadrant X coordinates. + q_y : Tuple[float, float, float, float] + Quadrant Y coordinates. + q_title_pos : Tuple[float,float] + Plot title relative position + q_title_props : dict, optional + Title box style properties. The default is dict(boxstyle='round', + facecolor="white", alpha=1.0). + q_point_props : dict, optional + Point box style properties. The default is dict(boxstyle='round', + facecolor="white", alpha=1.0). + + Returns + ------- + None + + """ + # draw filled rectangle + ax.fill(q_x, q_y, color=q_color) + + # draw quadrant title + ax.text(x=q_title_pos[0], y=q_title_pos[1], + s=str.upper(list(q_data.keys())[0]), fontsize=12, weight='bold', + verticalalignment='center', horizontalalignment='center', + bbox=q_title_props) + + # fill quadrant with points + for k, v in q_data[list(q_data.keys())[0]].items(): + ax.text(x=v[0], y=v[1], s=k, fontsize=12, weight='bold', + verticalalignment='center', horizontalalignment='center', + bbox=q_point_props) + + return None + + +def swotplot(p_data: dict, ax: plt.Axes, + s_color: str = "forestgreen", w_color: str = "gold", + o_color: str = "skyblue", t_color: str = "firebrick", + left_margin: float = 0.05, right_margin: float = 0.05, + top_margin: float = 0.05, bottom_margin: float = 0.05, + ) -> None: + """ + Draw SWOT plot + + Parameters + ---------- + p_data : dict + Data structure passed to populate the plot. + ax : matplotlib.pyplot.Axes + axes in which to draw the plot + s_color : float, optional + Strength quadrant color. + w_color : float, optional + Weakness quadrant color. + o_color : float, optional + Opportunity quadrant color. + t_color : float, optional + Threat quadrant color. + left_margin : float, optional + Left spacing from frame border. The default is 0.05. + right_margin : float, optional + Right spacing from frame border. The default is 0.05. + top_margin : float, optional + Top spacing from frame border. The default is 0.05. + bottom_margin : float, optional + Bottom spacing from frame border. The default is 0.05. + + Returns + ------- + None + + """ + # format axis + ax.set_xlim(0, 1.0) + ax.set_ylim(0, 1.0) + ax.axis('off') + + # draw s quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[0], + p_data.items())), + ax=ax, + q_color=s_color, q_x=(left_margin, left_margin, 0.5, 0.5), + q_y=(0.5, 1 - top_margin, 1 - top_margin, 0.5), + q_title_pos=(0.25 + left_margin / 2, 1 - top_margin)) + + # draw w quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[1], + p_data.items())), + ax=ax, + q_color=w_color, q_x=(0.5, 0.5, 1 - right_margin, 1 - right_margin), + q_y=(0.5, 1 - top_margin, 1 - top_margin, 0.5), + q_title_pos=(0.25 + (1 - right_margin) / 2, 1 - top_margin)) + + # draw o quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[2], + p_data.items())), + ax=ax, + q_color=o_color, q_x=(left_margin, left_margin, 0.5, 0.5), + q_y=(bottom_margin, 0.5, 0.5, bottom_margin), + q_title_pos=(0.25 + left_margin / 2, bottom_margin)) + + # draw t quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[3], + p_data.items())), + ax=ax, + q_color=t_color, q_x=(0.5, 0.5, 1 - right_margin, 1 - right_margin), + q_y=(bottom_margin, 0.5, 0.5, bottom_margin), + q_title_pos=(0.25 + (1 - right_margin) / 2, bottom_margin)) + + return None + + +# USER DATA +# relative x_pos,y_pos make it easy to manage particular cases such as +# soft categorization +data = {'strength': {'SW automation': (0.3, 0.6), + 'teamwork': (0.4, 0.5) + }, + 'weakness': {'delayed payment': (0.5, 0.7), + 'hard to learn': (0.7, 0.9) + }, + 'opportunity': {'dedicated training': (0.3, 0.3), + 'just-in-time development': (0.2, 0.1) + }, + 'threat': {'project de-prioritization': (0.7, 0.3), + 'external consultancy': (0.5, 0.1) + }, + } + +# SWOT plot generation +fig, ax = plt.subplots(figsize=(12, 6), layout='constrained') + +swotplot(data, ax) diff --git a/galleries/examples/text_labels_and_annotations/angles_on_bracket_arrows.py b/galleries/examples/text_labels_and_annotations/angles_on_bracket_arrows.py index bb536cd5d2c3..a23c7adc3897 100644 --- a/galleries/examples/text_labels_and_annotations/angles_on_bracket_arrows.py +++ b/galleries/examples/text_labels_and_annotations/angles_on_bracket_arrows.py @@ -23,37 +23,39 @@ def get_point_of_rotated_vertical(origin, line_length, degrees): origin[1] + line_length * np.cos(rad)] -fig, ax = plt.subplots(figsize=(8, 7)) -ax.set(xlim=(0, 6), ylim=(-1, 4)) +fig, ax = plt.subplots() +ax.set(xlim=(0, 6), ylim=(-1, 5)) ax.set_title("Orientation of the bracket arrows relative to angleA and angleB") -for i, style in enumerate(["]-[", "|-|"]): - for j, angle in enumerate([-40, 60]): - y = 2*i + j - arrow_centers = ((1, y), (5, y)) - vlines = ((1, y + 0.5), (5, y + 0.5)) - anglesAB = (angle, -angle) - bracketstyle = f"{style}, angleA={anglesAB[0]}, angleB={anglesAB[1]}" - bracket = FancyArrowPatch(*arrow_centers, arrowstyle=bracketstyle, - mutation_scale=42) - ax.add_patch(bracket) - ax.text(3, y + 0.05, bracketstyle, ha="center", va="bottom") - ax.vlines([i[0] for i in vlines], [y, y], [i[1] for i in vlines], - linestyles="--", color="C0") - # Get the top coordinates for the drawn patches at A and B - patch_tops = [get_point_of_rotated_vertical(center, 0.5, angle) - for center, angle in zip(arrow_centers, anglesAB)] - # Define the connection directions for the annotation arrows - connection_dirs = (1, -1) if angle > 0 else (-1, 1) - # Add arrows and annotation text - arrowstyle = "Simple, tail_width=0.5, head_width=4, head_length=8" - for vline, dir, patch_top, angle in zip(vlines, connection_dirs, - patch_tops, anglesAB): - kw = dict(connectionstyle=f"arc3,rad={dir * 0.5}", - arrowstyle=arrowstyle, color="C0") - ax.add_patch(FancyArrowPatch(vline, patch_top, **kw)) - ax.text(vline[0] - dir * 0.15, y + 0.3, f'{angle}°', ha="center", - va="center") +style = ']-[' +for i, angle in enumerate([-40, 0, 60]): + y = 2*i + arrow_centers = ((1, y), (5, y)) + vlines = ((1, y + 0.5), (5, y + 0.5)) + anglesAB = (angle, -angle) + bracketstyle = f"{style}, angleA={anglesAB[0]}, angleB={anglesAB[1]}" + bracket = FancyArrowPatch(*arrow_centers, arrowstyle=bracketstyle, + mutation_scale=42) + ax.add_patch(bracket) + ax.text(3, y + 0.05, bracketstyle, ha="center", va="bottom", fontsize=14) + ax.vlines([line[0] for line in vlines], [y, y], [line[1] for line in vlines], + linestyles="--", color="C0") + # Get the top coordinates for the drawn patches at A and B + patch_tops = [get_point_of_rotated_vertical(center, 0.5, angle) + for center, angle in zip(arrow_centers, anglesAB)] + # Define the connection directions for the annotation arrows + connection_dirs = (1, -1) if angle > 0 else (-1, 1) + # Add arrows and annotation text + arrowstyle = "Simple, tail_width=0.5, head_width=4, head_length=8" + for vline, dir, patch_top, angle in zip(vlines, connection_dirs, + patch_tops, anglesAB): + kw = dict(connectionstyle=f"arc3,rad={dir * 0.5}", + arrowstyle=arrowstyle, color="C0") + ax.add_patch(FancyArrowPatch(vline, patch_top, **kw)) + ax.text(vline[0] - dir * 0.15, y + 0.7, f'{angle}°', ha="center", + va="center") + +plt.show() # %% # diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 3a5d0eaf6bbf..b4cf9e26ab18 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -992,6 +992,15 @@ def func(current_frame: int, total_frames: int) -> Any is a `.MovieWriter`, a `RuntimeError` will be raised. """ + all_anim = [self] + if extra_anim is not None: + all_anim.extend(anim for anim in extra_anim + if anim._fig is self._fig) + + # Disable "Animation was deleted without rendering" warning. + for anim in all_anim: + anim._draw_was_started = True + if writer is None: writer = mpl.rcParams['animation.writer'] elif (not isinstance(writer, str) and @@ -1030,11 +1039,6 @@ def func(current_frame: int, total_frames: int) -> Any if metadata is not None: writer_kwargs['metadata'] = metadata - all_anim = [self] - if extra_anim is not None: - all_anim.extend(anim for anim in extra_anim - if anim._fig is self._fig) - # If we have the name of a writer, instantiate an instance of the # registered class. if isinstance(writer, str): diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 8e348fea4675..7893d7434f0d 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1363,8 +1363,6 @@ def __clear(self): self.xaxis.set_clip_path(self.patch) self.yaxis.set_clip_path(self.patch) - self._shared_axes["x"].clean() - self._shared_axes["y"].clean() if self._sharex is not None: self.xaxis.set_visible(xaxis_visible) self.patch.set_visible(patch_visible) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 97cf5745c656..b0a70b5346ac 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -2240,13 +2240,18 @@ def _init(self): ) self.label_position = 'bottom' + if mpl.rcParams['xtick.labelcolor'] == 'inherit': + tick_color = mpl.rcParams['xtick.color'] + else: + tick_color = mpl.rcParams['xtick.labelcolor'] + self.offsetText.set( x=1, y=0, verticalalignment='top', horizontalalignment='right', transform=mtransforms.blended_transform_factory( self.axes.transAxes, mtransforms.IdentityTransform()), fontsize=mpl.rcParams['xtick.labelsize'], - color=mpl.rcParams['xtick.color'], + color=tick_color ) self.offset_text_position = 'bottom' @@ -2499,6 +2504,12 @@ def _init(self): mtransforms.IdentityTransform(), self.axes.transAxes), ) self.label_position = 'left' + + if mpl.rcParams['ytick.labelcolor'] == 'inherit': + tick_color = mpl.rcParams['ytick.color'] + else: + tick_color = mpl.rcParams['ytick.labelcolor'] + # x in axes coords, y in display coords(!). self.offsetText.set( x=0, y=0.5, @@ -2506,7 +2517,7 @@ def _init(self): transform=mtransforms.blended_transform_factory( self.axes.transAxes, mtransforms.IdentityTransform()), fontsize=mpl.rcParams['ytick.labelsize'], - color=mpl.rcParams['ytick.color'], + color=tick_color ) self.offset_text_position = 'left' diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 4f5cd61dbe0b..9ef707f4e39a 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -313,9 +313,11 @@ def draw_gouraud_triangles(self, gc, triangles_array, colors_array, Parameters ---------- - points : (N, 3, 2) array-like + gc : `.GraphicsContextBase` + The graphics context. + triangles_array : (N, 3, 2) array-like Array of *N* (x, y) points for the triangles. - colors : (N, 3, 4) array-like + colors_array : (N, 3, 4) array-like Array of *N* RGBA colors for each point of the triangles. transform : `matplotlib.transforms.Transform` An affine transform to apply to the points. @@ -611,6 +613,8 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): Parameters ---------- + gc : `.GraphicsContextBase` + The graphics context. x : float The x location of the text in display coords. y : float diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py index 5366e8e611f4..ac74ff97a4e8 100644 --- a/lib/matplotlib/backend_managers.py +++ b/lib/matplotlib/backend_managers.py @@ -238,10 +238,8 @@ def add_tool(self, name, tool, *args, **kwargs): tool : type Class of the tool to be added. A subclass will be used instead if one was registered for the current canvas class. - - Notes - ----- - args and kwargs get passed directly to the tools constructor. + *args, **kwargs + Passed to the *tool*'s constructor. See Also -------- diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 5da9d491ebaf..4ed5a1ee6850 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -677,8 +677,8 @@ def _rescale(self): # Text-only button is handled by the font setting instead. pass elif isinstance(widget, tk.Frame): - widget.configure(height='22p', pady='1p') - widget.pack_configure(padx='4p') + widget.configure(height='18p') + widget.pack_configure(padx='3p') elif isinstance(widget, tk.Label): pass # Text is handled by the font setting instead. else: diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index c9699b2e21f5..3c97e26f6316 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -786,48 +786,53 @@ class Grouper: """ def __init__(self, init=()): - self._mapping = {weakref.ref(x): [weakref.ref(x)] for x in init} + self._mapping = weakref.WeakKeyDictionary( + {x: weakref.WeakSet([x]) for x in init}) + + def __getstate__(self): + return { + **vars(self), + # Convert weak refs to strong ones. + "_mapping": {k: set(v) for k, v in self._mapping.items()}, + } + + def __setstate__(self, state): + vars(self).update(state) + # Convert strong refs to weak ones. + self._mapping = weakref.WeakKeyDictionary( + {k: weakref.WeakSet(v) for k, v in self._mapping.items()}) def __contains__(self, item): - return weakref.ref(item) in self._mapping + return item in self._mapping + @_api.deprecated("3.8", alternative="none, you no longer need to clean a Grouper") def clean(self): """Clean dead weak references from the dictionary.""" - mapping = self._mapping - to_drop = [key for key in mapping if key() is None] - for key in to_drop: - val = mapping.pop(key) - val.remove(key) def join(self, a, *args): """ Join given arguments into the same set. Accepts one or more arguments. """ mapping = self._mapping - set_a = mapping.setdefault(weakref.ref(a), [weakref.ref(a)]) + set_a = mapping.setdefault(a, weakref.WeakSet([a])) for arg in args: - set_b = mapping.get(weakref.ref(arg), [weakref.ref(arg)]) + set_b = mapping.get(arg, weakref.WeakSet([arg])) if set_b is not set_a: if len(set_b) > len(set_a): set_a, set_b = set_b, set_a - set_a.extend(set_b) + set_a.update(set_b) for elem in set_b: mapping[elem] = set_a - self.clean() - def joined(self, a, b): """Return whether *a* and *b* are members of the same set.""" - self.clean() - return (self._mapping.get(weakref.ref(a), object()) - is self._mapping.get(weakref.ref(b))) + return (self._mapping.get(a, object()) is self._mapping.get(b)) def remove(self, a): - self.clean() - set_a = self._mapping.pop(weakref.ref(a), None) + set_a = self._mapping.pop(a, None) if set_a: - set_a.remove(weakref.ref(a)) + set_a.remove(a) def __iter__(self): """ @@ -835,16 +840,14 @@ def __iter__(self): The iterator is invalid if interleaved with calls to join(). """ - self.clean() unique_groups = {id(group): group for group in self._mapping.values()} for group in unique_groups.values(): - yield [x() for x in group] + yield [x for x in group] def get_siblings(self, a): """Return all of the items joined with *a*, including itself.""" - self.clean() - siblings = self._mapping.get(weakref.ref(a), [weakref.ref(a)]) - return [x() for x in siblings] + siblings = self._mapping.get(a, [a]) + return [x for x in siblings] class GrouperView: diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 966eb0760b47..14c7c1e58b9a 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1003,7 +1003,7 @@ def _set_scale(self, scale, **kwargs): Parameters ---------- - value : {"linear", "log", "symlog", "logit", ...} or `.ScaleBase` + scale : {"linear", "log", "symlog", "logit", ...} or `.ScaleBase` The axis scale type to apply. **kwargs diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index a6c0d09adcd8..b9626d913269 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1311,7 +1311,7 @@ def __call__(self, value, clip=None): ---------- value Data to normalize. - clip : bool + clip : bool, optional If ``None``, defaults to ``self.clip`` (which defaults to ``False``). @@ -1450,7 +1450,7 @@ def autoscale_None(self, A): def __call__(self, value, clip=None): """ - Map value to the interval [0, 1]. The clip argument is unused. + Map value to the interval [0, 1]. The *clip* argument is unused. """ result, is_scalar = self.process_value(value) self.autoscale_None(result) # sets self.vmin, self.vmax if None @@ -1499,6 +1499,10 @@ def __init__(self, vcenter=0, halfrange=None, clip=False): *vcenter* + *halfrange* is ``1.0`` in the normalization. Defaults to the largest absolute difference to *vcenter* for the values in the dataset. + clip : bool, default: False + If ``True`` values falling outside the range ``[vmin, vmax]``, + are mapped to 0 or 1, whichever is closer, and masked values are + set to 1. If ``False`` masked values remain masked. Examples -------- @@ -2423,7 +2427,8 @@ def shade(self, data, cmap, norm=None, blend_mode='overlay', vmin=None, full illumination or shadow (and clipping any values that move beyond 0 or 1). Note that this is not visually or mathematically the same as vertical exaggeration. - Additional kwargs are passed on to the *blend_mode* function. + **kwargs + Additional kwargs are passed on to the *blend_mode* function. Returns ------- @@ -2484,7 +2489,8 @@ def shade_rgb(self, rgb, elevation, fraction=1., blend_mode='hsv', The x-spacing (columns) of the input *elevation* grid. dy : number, optional The y-spacing (rows) of the input *elevation* grid. - Additional kwargs are passed on to the *blend_mode* function. + **kwargs + Additional kwargs are passed on to the *blend_mode* function. Returns ------- diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 6516aa7c2ecb..90c5dd5feba0 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -1447,12 +1447,12 @@ def _contour_args(self, args, kwargs): else: raise _api.nargs_error(fn, takes="from 1 to 4", given=nargs) z = ma.masked_invalid(z, copy=False) - self.zmax = float(z.max()) - self.zmin = float(z.min()) + self.zmax = z.max().astype(float) + self.zmin = z.min().astype(float) if self.logscale and self.zmin <= 0: z = ma.masked_where(z <= 0, z) _api.warn_external('Log scale: values of z <= 0 have been masked') - self.zmin = float(z.min()) + self.zmin = z.min().astype(float) self._process_contour_level_args(args, z.dtype) return (x, y, z) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 10a407232827..04b1f5dfa727 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -949,7 +949,7 @@ def clear(self, keep_observers=False): Parameters ---------- - keep_observers: bool, default: False + keep_observers : bool, default: False Set *keep_observers* to True if, for example, a gui widget is tracking the Axes in the figure. """ @@ -989,7 +989,7 @@ def clf(self, keep_observers=False): Parameters ---------- - keep_observers: bool, default: False + keep_observers : bool, default: False Set *keep_observers* to True if, for example, a gui widget is tracking the Axes in the figure. """ @@ -1102,7 +1102,6 @@ def legend(self, *args, **kwargs): ---------------- %(_legend_kw_figure)s - See Also -------- .Axes.legend diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 25cd9ae78f41..11dc05b0e9d2 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -447,6 +447,10 @@ def tight_layout(self, figure, renderer=None, Parameters ---------- + figure : `.Figure` + The figure. + renderer : `.RendererBase` subclass, optional + The renderer to be used. pad : float Padding between the figure edge and the edges of subplots, as a fraction of the font-size. diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 7b066bdaf7b4..bc3e35bb7d2c 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1047,8 +1047,10 @@ def __init__(self, ax, *, interpolation='nearest', **kwargs): """ Parameters ---------- + ax : `~.axes.Axes` + The axes the image will belong to. interpolation : {'nearest', 'bilinear'}, default: 'nearest' - + The interpolation scheme used in the resampling. **kwargs All other keyword arguments are identical to those of `.AxesImage`. """ diff --git a/lib/matplotlib/layout_engine.py b/lib/matplotlib/layout_engine.py index 185be9857abb..a3eab93da329 100644 --- a/lib/matplotlib/layout_engine.py +++ b/lib/matplotlib/layout_engine.py @@ -167,7 +167,10 @@ def execute(self, fig): ---------- fig : `.Figure` to perform layout on. - See also: `.figure.Figure.tight_layout` and `.pyplot.tight_layout`. + See Also + -------- + .figure.Figure.tight_layout + .pyplot.tight_layout """ info = self._params renderer = fig._get_renderer() diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index ff6abdb95844..c25b519fdb03 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -158,8 +158,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas): same height, set to ``[0.5]``. markerscale : float, default: :rc:`legend.markerscale` - The relative size of legend markers compared with the originally - drawn ones. + The relative size of legend markers compared to the originally drawn ones. markerfirst : bool, default: True If *True*, legend marker is placed to the left of the legend label. @@ -254,52 +253,55 @@ def _update_bbox_to_anchor(self, loc_in_canvas): """ _loc_doc_base = """ -loc : str or pair of floats, {0} +loc : str or pair of floats, default: {default} The location of the legend. - The strings - ``'upper left', 'upper right', 'lower left', 'lower right'`` - place the legend at the corresponding corner of the axes/figure. + The strings ``'upper left'``, ``'upper right'``, ``'lower left'``, + ``'lower right'`` place the legend at the corresponding corner of the + {parent}. - The strings - ``'upper center', 'lower center', 'center left', 'center right'`` - place the legend at the center of the corresponding edge of the - axes/figure. - - The string ``'center'`` places the legend at the center of the axes/figure. - - The string ``'best'`` places the legend at the location, among the nine - locations defined so far, with the minimum overlap with other drawn - artists. This option can be quite slow for plots with large amounts of - data; your plotting speed may benefit from providing a specific location. + The strings ``'upper center'``, ``'lower center'``, ``'center left'``, + ``'center right'`` place the legend at the center of the corresponding edge + of the {parent}. + The string ``'center'`` places the legend at the center of the {parent}. +{best} The location can also be a 2-tuple giving the coordinates of the lower-left - corner of the legend in axes coordinates (in which case *bbox_to_anchor* + corner of the legend in {parent} coordinates (in which case *bbox_to_anchor* will be ignored). For back-compatibility, ``'center right'`` (but no other location) can also - be spelled ``'right'``, and each "string" locations can also be given as a + be spelled ``'right'``, and each "string" location can also be given as a numeric value: - =============== ============= - Location String Location Code - =============== ============= - 'best' 0 - 'upper right' 1 - 'upper left' 2 - 'lower left' 3 - 'lower right' 4 - 'right' 5 - 'center left' 6 - 'center right' 7 - 'lower center' 8 - 'upper center' 9 - 'center' 10 - =============== ============= - {1}""" - -_legend_kw_axes_st = (_loc_doc_base.format("default: :rc:`legend.loc`", '') + - _legend_kw_doc_base) + ================== ============= + Location String Location Code + ================== ============= + 'best' (Axes only) 0 + 'upper right' 1 + 'upper left' 2 + 'lower left' 3 + 'lower right' 4 + 'right' 5 + 'center left' 6 + 'center right' 7 + 'lower center' 8 + 'upper center' 9 + 'center' 10 + ================== ============= + {outside}""" + +_loc_doc_best = """ + The string ``'best'`` places the legend at the location, among the nine + locations defined so far, with the minimum overlap with other drawn + artists. This option can be quite slow for plots with large amounts of + data; your plotting speed may benefit from providing a specific location. +""" + +_legend_kw_axes_st = ( + _loc_doc_base.format(parent='axes', default=':rc:`legend.loc`', + best=_loc_doc_best, outside='') + + _legend_kw_doc_base) _docstring.interpd.update(_legend_kw_axes=_legend_kw_axes_st) _outside_doc = """ @@ -314,21 +316,23 @@ def _update_bbox_to_anchor(self, loc_in_canvas): :doc:`/tutorials/intermediate/legend_guide` for more details. """ -_legend_kw_figure_st = (_loc_doc_base.format("default: 'upper right'", - _outside_doc) + - _legend_kw_doc_base) +_legend_kw_figure_st = ( + _loc_doc_base.format(parent='figure', default="'upper right'", + best='', outside=_outside_doc) + + _legend_kw_doc_base) _docstring.interpd.update(_legend_kw_figure=_legend_kw_figure_st) _legend_kw_both_st = ( - _loc_doc_base.format("default: 'best' for axes, 'upper right' for figures", - _outside_doc) + + _loc_doc_base.format(parent='axes/figure', + default=":rc:`legend.loc` for Axes, 'upper right' for Figure", + best=_loc_doc_best, outside=_outside_doc) + _legend_kw_doc_base) _docstring.interpd.update(_legend_kw_doc=_legend_kw_both_st) class Legend(Artist): """ - Place a legend on the axes at location loc. + Place a legend on the figure/axes. """ # 'best' is only implemented for axes legends @@ -407,17 +411,6 @@ def __init__( List of `.Artist` objects added as legend entries. .. versionadded:: 3.7 - - Notes - ----- - Users can specify any arbitrary location for the legend using the - *bbox_to_anchor* keyword argument. *bbox_to_anchor* can be a - `.BboxBase` (or derived there from) or a tuple of 2 or 4 floats. - See `set_bbox_to_anchor` for more detail. - - The legend location can be specified by setting *loc* with a tuple of - 2 floats, which is interpreted as the lower-left corner of the legend - in the normalized axes coordinate. """ # local import only to avoid circularity from matplotlib.axes import Axes diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 1dee8a23d94c..a46b9cd4319d 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1505,7 +1505,6 @@ def __init__(self, ref_artist, use_blit=False): if not ref_artist.pickable(): ref_artist.set_picker(True) self.got_artist = False - self.canvas = self.ref_artist.figure.canvas self._use_blit = use_blit and self.canvas.supports_blit self.cids = [ self.canvas.callbacks._connect_picklable( @@ -1514,6 +1513,9 @@ def __init__(self, ref_artist, use_blit=False): 'button_release_event', self.on_release), ] + # A property, not an attribute, to maintain picklability. + canvas = property(lambda self: self.ref_artist.figure.canvas) + def on_motion(self, evt): if self._check_still_parented() and self.got_artist: dx = evt.x - self.mouse_x diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index fb6fec46622b..e60a14f592fb 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -3197,29 +3197,18 @@ def __init__(self, head_length=.4, head_width=.2, widthA=1., widthB=1., Parameters ---------- head_length : float, default: 0.4 - Length of the arrow head, relative to *mutation_scale*. + Length of the arrow head, relative to *mutation_size*. head_width : float, default: 0.2 - Width of the arrow head, relative to *mutation_scale*. - widthA : float, default: 1.0 - Width of the bracket at the beginning of the arrow - widthB : float, default: 1.0 - Width of the bracket at the end of the arrow - lengthA : float, default: 0.2 - Length of the bracket at the beginning of the arrow - lengthB : float, default: 0.2 - Length of the bracket at the end of the arrow - angleA : float, default 0 - Orientation of the bracket at the beginning, as a - counterclockwise angle. 0 degrees means perpendicular - to the line. - angleB : float, default 0 - Orientation of the bracket at the beginning, as a - counterclockwise angle. 0 degrees means perpendicular - to the line. - scaleA : float, default *mutation_size* - The mutation_size for the beginning bracket - scaleB : float, default *mutation_size* - The mutation_size for the end bracket + Width of the arrow head, relative to *mutation_size*. + widthA, widthB : float, default: 1.0 + Width of the bracket. + lengthA, lengthB : float, default: 0.2 + Length of the bracket. + angleA, angleB : float, default: 0 + Orientation of the bracket, as a counterclockwise angle. + 0 degrees means perpendicular to the line. + scaleA, scaleB : float, default: *mutation_size* + The scale of the brackets. """ self.head_length, self.head_width = head_length, head_width diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index 9f41ff5cc69c..49e6a374ea57 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -514,5 +514,5 @@ def test_movie_writer_invalid_path(anim): else: match_str = re.escape("[Errno 2] No such file or directory: '/foo") with pytest.raises(FileNotFoundError, match=match_str): - _ = anim.save("/foo/bar/aardvark/thiscannotreallyexist.mp4", - writer=animation.FFMpegFileWriter()) + anim.save("/foo/bar/aardvark/thiscannotreallyexist.mp4", + writer=animation.FFMpegFileWriter()) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 8483ed077bdf..c8e1f53465f2 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -7811,6 +7811,28 @@ def test_ytickcolor_is_not_yticklabelcolor(): assert tick.label1.get_color() == 'blue' +def test_xaxis_offsetText_color(): + plt.rcParams['xtick.labelcolor'] = 'blue' + ax = plt.axes() + assert ax.xaxis.offsetText.get_color() == 'blue' + + plt.rcParams['xtick.color'] = 'yellow' + plt.rcParams['xtick.labelcolor'] = 'inherit' + ax = plt.axes() + assert ax.xaxis.offsetText.get_color() == 'yellow' + + +def test_yaxis_offsetText_color(): + plt.rcParams['ytick.labelcolor'] = 'green' + ax = plt.axes() + assert ax.yaxis.offsetText.get_color() == 'green' + + plt.rcParams['ytick.color'] = 'red' + plt.rcParams['ytick.labelcolor'] = 'inherit' + ax = plt.axes() + assert ax.yaxis.offsetText.get_color() == 'red' + + @pytest.mark.parametrize('size', [size for size in mfont_manager.font_scalings if size is not None] + [8, 10, 12]) @mpl.style.context('default') diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index aa5c999b7079..da9c187a323a 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -1,7 +1,6 @@ import itertools import pickle -from weakref import ref from unittest.mock import patch, Mock from datetime import datetime, date, timedelta @@ -590,11 +589,11 @@ class Dummy: mapping = g._mapping for o in objs: - assert ref(o) in mapping + assert o in mapping - base_set = mapping[ref(objs[0])] + base_set = mapping[objs[0]] for o in objs[1:]: - assert mapping[ref(o)] is base_set + assert mapping[o] is base_set def test_flatiter(): diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index 58101a9adc24..d56a4c9a972a 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -715,3 +715,10 @@ def test_bool_autolevel(): assert plt.tricontour(x, y, z).levels.tolist() == [.5] assert plt.tricontourf(x, y, z.tolist()).levels.tolist() == [0, .5, 1] assert plt.tricontourf(x, y, z).levels.tolist() == [0, .5, 1] + + +def test_all_nan(): + x = np.array([[np.nan, np.nan], [np.nan, np.nan]]) + assert_array_almost_equal(plt.contour(x).levels, + [-1e-13, -7.5e-14, -5e-14, -2.4e-14, 0.0, + 2.4e-14, 5e-14, 7.5e-14, 1e-13]) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 7eecf5675a4b..b75d3c01b28e 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -185,7 +185,7 @@ def test_set_drawstyle(): @image_comparison( ['line_collection_dashes'], remove_text=True, style='mpl20', - tol=0.62 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) + tol=0.65 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) def test_set_line_coll_dash_image(): fig, ax = plt.subplots() np.random.seed(0) diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index ec6bdcc2fe14..baed5299e0ba 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -1,6 +1,7 @@ from io import BytesIO import ast import pickle +import pickletools import numpy as np import pytest @@ -58,6 +59,7 @@ def _generate_complete_test_figure(fig_ref): # Ensure lists also pickle correctly. plt.subplot(3, 3, 1) plt.plot(list(range(10))) + plt.ylabel("hello") plt.subplot(3, 3, 2) plt.contourf(data, hatches=['//', 'ooo']) @@ -68,6 +70,7 @@ def _generate_complete_test_figure(fig_ref): plt.subplot(3, 3, 4) plt.imshow(data) + plt.ylabel("hello\nworld!") plt.subplot(3, 3, 5) plt.pcolor(data) @@ -88,6 +91,9 @@ def _generate_complete_test_figure(fig_ref): plt.subplot(3, 3, 9) plt.errorbar(x, x * -0.5, xerr=0.2, yerr=0.4) + plt.legend(draggable=True) + + fig_ref.align_ylabels() # Test handling of _align_label_groups Groupers. @mpl.style.context("default") @@ -95,9 +101,13 @@ def _generate_complete_test_figure(fig_ref): def test_complete(fig_test, fig_ref): _generate_complete_test_figure(fig_ref) # plotting is done, now test its pickle-ability - pkl = BytesIO() - pickle.dump(fig_ref, pkl, pickle.HIGHEST_PROTOCOL) - loaded = pickle.loads(pkl.getbuffer()) + pkl = pickle.dumps(fig_ref, pickle.HIGHEST_PROTOCOL) + # FigureCanvasAgg is picklable and GUI canvases are generally not, but there should + # be no reference to the canvas in the pickle stream in either case. In order to + # keep the test independent of GUI toolkits, run it with Agg and check that there's + # no reference to FigureCanvasAgg in the pickle stream. + assert "FigureCanvasAgg" not in [arg for op, arg, pos in pickletools.genops(pkl)] + loaded = pickle.loads(pkl) loaded.canvas.draw() fig_test.set_size_inches(loaded.get_size_inches()) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 65aff05b035b..c80fefd37e14 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -701,6 +701,22 @@ def test_wrap(): 'times.') +def test_get_window_extent_wrapped(): + # Test that a long title that wraps to two lines has the same vertical + # extent as an explicit two line title. + + fig1 = plt.figure(figsize=(3, 3)) + fig1.suptitle("suptitle that is clearly too long in this case", wrap=True) + window_extent_test = fig1._suptitle.get_window_extent() + + fig2 = plt.figure(figsize=(3, 3)) + fig2.suptitle("suptitle that is clearly\ntoo long in this case") + window_extent_ref = fig2._suptitle.get_window_extent() + + assert window_extent_test.y0 == window_extent_ref.y0 + assert window_extent_test.y1 == window_extent_ref.y1 + + def test_long_word_wrap(): fig = plt.figure(figsize=(6, 4)) text = fig.text(9.5, 8, 'Alonglineoftexttowrap', wrap=True) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 92b87a96b3d2..5706df46e328 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1297,12 +1297,12 @@ def handle_positions(slider): else: return [h.get_xdata()[0] for h in slider._handles] - slider.set_val((0.2, 0.6)) - assert_allclose(slider.val, (0.2, 0.6)) - assert_allclose(handle_positions(slider), (0.2, 0.6)) + slider.set_val((0.4, 0.6)) + assert_allclose(slider.val, (0.4, 0.6)) + assert_allclose(handle_positions(slider), (0.4, 0.6)) box = slider.poly.get_extents().transformed(ax.transAxes.inverted()) - assert_allclose(box.get_points().flatten()[idx], [0.2, .25, 0.6, .75]) + assert_allclose(box.get_points().flatten()[idx], [0.4, .25, 0.6, .75]) slider.set_val((0.2, 0.1)) assert_allclose(slider.val, (0.1, 0.2)) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 0f874ba33db7..f1e300b838e2 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -367,7 +367,7 @@ def _get_layout(self, renderer): of a rotated text when necessary. """ thisx, thisy = 0.0, 0.0 - lines = self.get_text().split("\n") # Ensures lines is not empty. + lines = self._get_wrapped_text().split("\n") # Ensures lines is not empty. ws = [] hs = [] @@ -1626,6 +1626,9 @@ def draggable(self, state=None, use_blit=False): state : bool or None - True or False: set the draggability. - None: toggle the draggability. + use_blit : bool, default: False + Use blitting for faster image composition. For details see + :ref:`func-animation`. Returns ------- diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index cfb9877ad31f..a31e58da8dd7 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -613,7 +613,7 @@ def padded(self, w_pad, h_pad=None): ---------- w_pad : float Width pad - h_pad: float, optional + h_pad : float, optional Height pad. Defaults to *w_pad*. """ diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 35f3eec3e79e..c25644dbe6b8 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -702,7 +702,7 @@ def __init__( valmin, valmax, valfmt, dragging, valstep) # Set a value to allow _value_in_bounds() to work. - self.val = [valmin, valmax] + self.val = (valmin, valmax) if valinit is None: # Place at the 25th and 75th percentiles extent = valmax - valmin @@ -947,9 +947,9 @@ def set_val(self, val): """ val = np.sort(val) _api.check_shape((2,), val=val) - vmin, vmax = val - vmin = self._min_in_bounds(vmin) - vmax = self._max_in_bounds(vmax) + # Reset value to allow _value_in_bounds() to work. + self.val = (self.valmin, self.valmax) + vmin, vmax = self._value_in_bounds(val) self._update_selection_poly(vmin, vmax) if self.orientation == "vertical": self._handles[0].set_ydata([vmin]) @@ -2512,7 +2512,7 @@ def remove_state(self, state): Parameters ---------- - value : str + state : str Must be a supported state of the selector. See the `state_modifier_keys` parameters for details. diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index cb31aca6459e..67f438f107dd 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -640,7 +640,6 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, _tight = self._tight = bool(tight) if scalex and self.get_autoscalex_on(): - self._shared_axes["x"].clean() x0, x1 = self.xy_dataLim.intervalx xlocator = self.xaxis.get_major_locator() x0, x1 = xlocator.nonsingular(x0, x1) @@ -653,7 +652,6 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, self.set_xbound(x0, x1) if scaley and self.get_autoscaley_on(): - self._shared_axes["y"].clean() y0, y1 = self.xy_dataLim.intervaly ylocator = self.yaxis.get_major_locator() y0, y1 = ylocator.nonsingular(y0, y1) @@ -666,7 +664,6 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, self.set_ybound(y0, y1) if scalez and self.get_autoscalez_on(): - self._shared_axes["z"].clean() z0, z1 = self.zz_dataLim.intervalx zlocator = self.zaxis.get_major_locator() z0, z1 = zlocator.nonsingular(z0, z1) diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index 5b21c338782c..1c237ea7abb8 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -13,8 +13,8 @@ ipython ipywidgets numpydoc>=1.0 packaging>=20 -pydata-sphinx-theme>=0.12.0 -mpl-sphinx-theme>=3.7.0 +pydata-sphinx-theme~=0.12.0 +mpl-sphinx-theme~=3.7.0 pyyaml sphinxcontrib-svg2pdfconverter>=1.1.0 sphinx-gallery>=0.10 diff --git a/src/_macosx.m b/src/_macosx.m index 1fd987bb4825..30fdf8fbd75c 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -865,7 +865,6 @@ - (void)save_figure:(id)sender; typedef struct { PyObject_HEAD - NSPopUpButton* menu; NSTextView* messagebox; NavigationToolbar2Handler* handler; int height;